Refactor src/ into layered tools/operations/infra architecture#110
Merged
Refactor src/ into layered tools/operations/infra architecture#110
Conversation
Splits the logger and logAndThrow out of the grab-bag utils.ts into a pure, zero-internal-import module at src/logging.ts. This is the first cycle-breaker in the layered refactor: utils <-> notes and utils <-> bear-urls cycles existed partly because logger lived alongside code that imported from those modules. A pure logging.ts can be reached from any layer (tools, operations, infra) without creating cycles. No behavior change. All importers (main, notes, tags, bear-urls, database, and utils itself) now pull logger/logAndThrow from ./logging.js instead of ./utils.js.
…ses.ts Moves createToolResponse and createErrorResponse verbatim out of utils.ts into src/tools/responses.ts — the first file in the new tools/ layer. Signatures and behavior unchanged (same Pick<CallToolResult, ...> return types, same annotations audience). Updates main.ts and utils.ts (which still holds handleNoteTextUpdate temporarily) to import from the new location.
…ear-encoding.ts Moves the 4 Bear-format encoding helpers (decodeTagName, cleanBase64, convertCoreDataTimestamp, convertDateToCoreDataTimestamp) out of utils.ts into a dedicated operations/bear-encoding.ts. These share a single concern — converting between Bear's on-disk/on-URL formats and JS types — and are consumed by operations (notes, tags) plus the bear-add-file tool via cleanBase64. Co-locates the convertCoreDataTimestamp test into the new module's bear-encoding.test.ts. Updates the notes.ts DECODED_TAG_TITLE sync comment to reference the helper's new home. All 37 unit tests still pass (1 moved from utils.test.ts to bear-encoding.test.ts; utils.test.ts now has 18 of its previous 19 describe assertions remaining).
First file relocated into the infra layer. No logic change — just: - the file now sits at src/infra/database.ts - its own imports go up one level (../config.js, ../logging.js) - the two operations that read from it (notes.ts, tags.ts) now import from ./infra/database.js while they still sit at src/ root (their own relocation into operations/ happens in steps 6 and 7).
bear-urls.ts joins database.ts in the infra/ layer. Both are the only modules that speak to the outside world (Bear's SQLite DB and the x-callback-url URL scheme via 'open'). Their own imports go up one level (../config.js, ../logging.js); the co-located test imports remain sibling-relative (./bear-urls.js). Callers (main.ts, utils.ts) now import from ./infra/bear-urls.js. Note: the utils <-> bear-urls cycle is still present at this step because utils.ts still holds handleNoteTextUpdate, which calls buildBearUrl and executeBearXCallbackApi. That cycle dissolves in step 9 when handleNoteTextUpdate moves into tools/note-tools.ts and utils.ts is deleted.
notes.ts joins bear-encoding.ts in the operations/ layer. As part of the move, three helpers that lived in the grab-bag utils.ts get folded in where they belong: - parseDateString — already used only by searchNotes' date filter - stripLeadingHeader and noteHasHeader — pure markdown content helpers used by handleNoteTextUpdate Their 3 describe blocks move verbatim from utils.test.ts into a new co-located operations/notes.test.ts, preserving all 17 assertions. utils.test.ts is now empty of useful content and is deleted. utils.ts shrinks to ~85 lines — it now contains only handleNoteTextUpdate (which will move into tools/note-tools.ts in step 9, at which point utils.ts disappears entirely). Its import of getNoteContent and the two markdown helpers now resolves to ./operations/notes.js. Tests: 37 total, same as before. Test files: utils.test.ts is gone (19 tests → 18 moved to operations/notes.test.ts + 1 moved to operations/bear-encoding.test.ts in step 3).
tags.ts joins notes.ts and bear-encoding.ts in the operations/ layer. Its imports now resolve to the correct parent paths (../types, ../logging, ../infra/database) and its sibling bear-encoding.js. main.ts imports from ./operations/tags.js.
…ions/ Simplest move in the refactor: note-conventions.ts has no cross-file imports and its test is co-located. Only main.ts's import path needs updating to ./operations/note-conventions.js.
… delete utils.ts The architectural crown of the refactor. Creates src/tools/note-tools.ts exporting registerNoteTools(server), which registers all 9 note-domain tools verbatim from main.ts: - bear-open-note, bear-create-note, bear-search-notes - bear-add-text, bear-replace-text, bear-add-file - bear-find-untagged-notes, bear-add-tag, bear-archive-note All schemas, descriptions, annotations, handler bodies, and registration order preserved. handleNoteTextUpdate moves into this file as a module-private helper (used only by bear-add-text and bear-replace-text). main.ts shrinks from ~970 lines to ~220 — it now creates the McpServer, calls registerNoteTools(server), then still holds the 3 tag tools inline (move in step 10). Unused imports in main.ts removed. src/utils.ts is now empty of content and deleted. Both legacy cycles (utils <-> notes, utils <-> bear-urls) are gone. Verified: npx madge --circular --extensions ts src/ → ✔ No circular dependency found! 37 tests still pass.
Creates src/tools/tag-tools.ts exporting registerTagTools(server), which registers all 3 tag-global tools verbatim from main.ts: - bear-list-tags (with its formatTagTree helper) - bear-rename-tag - bear-delete-tag All schemas, descriptions, annotations, and handler bodies preserved. main.ts is now a thin entry point: create the McpServer, call registerNoteTools(server) + registerTagTools(server), attach process error handlers, connect the stdio transport. Dropped from 969 lines (pre-refactor) to 55 lines. No server.registerTool(...) calls remain in main.ts. No circular dependencies (verified with madge). 37 unit tests pass.
…ectories The old pack step 'cp -r dist/*.js ...' globbed only top-level .js files, which silently broke the .mcpb bundle once src/ was layered — dist/tools/*, dist/operations/*, and dist/infra/* would never reach tmp/bundle/, so main.js's layered imports failed at runtime. Replaces the single glob with two copies: cp -R dist/. tmp/bundle/ # preserves full subdir tree cp -r assets/* manifest.json package.json tmp/bundle/ # same as before The trailing '/.' on dist matters: without it we'd get tmp/bundle/dist/ and manifest.json's entry_point='main.js' would fail to resolve. Assets continue to be copied via the existing glob pattern so dotfiles like .DS_Store stay out of the bundle. Verified: manual simulation of the pack copy produces the expected tree under tmp/bundle/ with main.js at root + tools/, operations/, infra/ subdirectories and logging.js/config.js/types.js at root.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Contributor
There was a problem hiding this comment.
Pull request overview
Refactors the codebase from a flat src/ layout into a layered architecture (tools/ → operations/ → infra/ plus root modules) to remove import cycles and make tool registration and dependencies clearer, while keeping tool behavior consistent.
Changes:
- Split the former
utils.tsinto focused modules (logging.ts,tools/responses.ts,operations/bear-encoding.ts) and moved note mutation handling into the tools layer. - Extracted tool registrations into
src/tools/*and simplifiedsrc/main.tsto server wiring + tool registration. - Updated packaging in
Taskfile.ymlto bundle the fulldist/tree (including new subdirectories).
Reviewed changes
Copilot reviewed 14 out of 17 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| src/utils.ts | Removed monolithic utilities module; functionality redistributed across layers. |
| src/logging.ts | New centralized logger + logAndThrow, used across operations/infra/tools. |
| src/main.ts | Reduced to server creation + registering note/tag tool modules + transport connect. |
| src/tools/responses.ts | New shared MCP response helpers (createToolResponse, createErrorResponse). |
| src/tools/note-tools.ts | New module registering note-domain MCP tools and shared note-text update handler. |
| src/tools/tag-tools.ts | New module registering tag-domain MCP tools and tag tree formatting. |
| src/operations/notes.ts | Updated imports + now houses date parsing and header utilities used by tools. |
| src/operations/notes.test.ts | Updated imports to match refactor; moved timestamp test elsewhere. |
| src/operations/tags.ts | Updated imports; uses new logging/infra and bear-encoding helpers. |
| src/operations/bear-encoding.ts | New module for tag decoding, base64 cleanup, and Core Data timestamp conversions. |
| src/operations/bear-encoding.test.ts | New unit test coverage for Core Data timestamp conversion. |
| src/operations/note-conventions.ts | New module to embed tags as inline Bear tag syntax on note creation. |
| src/operations/note-conventions.test.ts | New unit tests for note-conventions behavior. |
| src/infra/bear-urls.ts | Updated imports to root logging/config after move. |
| src/infra/bear-urls.test.ts | New unit tests for URL encoding behavior. |
| src/infra/database.ts | Updated imports to root logging/config after move. |
| Taskfile.yml | Fix bundling to copy full dist/ directory structure into the .mcpb bundle. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



Summary
src/into a layered architecture:tools/ → operations/ → infra/with shared root modules (config.ts,types.ts,logging.ts,main.ts)utils ↔ notes,utils ↔ bear-urls) by splittingloggerinto a purelogging.tsand pushinghandleNoteTextUpdateinto the tools layermain.tsfrom 969 → 55 lines — it now reads as "create server → register tools → connect transport"src/is understandable top-downTaskfile.yml: the oldcp -r dist/*.jsglob would have silently omitted the newdist/tools/,dist/operations/,dist/infra/subdirectories from the.mcpb, breaking Claude Desktop installs on the first post-refactor release. Now usescp -R dist/. tmp/bundle/which preserves the tree.Target structure
Layer rule: tools → operations → infra → root; root imports nothing inside
src/.Why
The flat
src/had grown a grab-bagutils.tswith two real import cycles becauseloggerlived alongside helpers that calledgetNoteContentandexecuteBearXCallbackApi. As more tools and operations accreted,main.tsbecame a 969-line registration monolith that mixed server wiring, tool definitions, and response formatting. The layered structure gives each concern a single home, eliminates the cycles, and makes the codebase navigable for future contributors (human and AI) without needing to trace through a grab-bag module to understand dependencies.