Add @decocms/local-dev MCP daemon for local filesystem access#2479
Add @decocms/local-dev MCP daemon for local filesystem access#2479
Conversation
🧪 BenchmarkShould we run the Virtual MCP strategy benchmark for this PR? React with 👍 to run the benchmark.
Benchmark will run on the next push after you react. |
There was a problem hiding this comment.
7 issues found across 14 files
Prompt for AI agents (all issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="packages/local-dev/src/server.ts">
<violation number="1" location="packages/local-dev/src/server.ts:63">
P1: Missing `Access-Control-Expose-Headers` for `mcp-session-id`. Browser-based MCP clients (e.g., web UIs connecting to this server) won't be able to read the session ID from the response, causing session establishment to fail. This is a well-known pitfall documented in MCP framework guides.</violation>
</file>
<file name="packages/local-dev/src/cli.ts">
<violation number="1" location="packages/local-dev/src/cli.ts:23">
P2: Validate the parsed port before returning it. Right now a non-numeric `PORT`/`--port` value results in `NaN`, which later causes the server to crash when listening.</violation>
</file>
<file name="packages/local-dev/src/tools.test.ts">
<violation number="1" location="packages/local-dev/src/tools.test.ts:114">
P2: This test depends on the previous test creating `subdir`, making it order-dependent and flaky if tests are run in isolation or reordered. Create the directory within this test (or a beforeEach) to keep it self-contained.</violation>
</file>
<file name="packages/local-dev/src/watch.ts">
<violation number="1" location="packages/local-dev/src/watch.ts:19">
P2: Handle FSWatcher "error" events to avoid an unhandled error crashing the daemon when the watch fails (e.g., permissions or missing path).</violation>
</file>
<file name="packages/local-dev/src/tools.ts">
<violation number="1" location="packages/local-dev/src/tools.ts:1515">
P1: Regex escaping of `.` in step 5 corrupts the `.*` and `.` patterns produced by the earlier glob-to-regex conversions (steps 3–4). As a result, `**` matches zero-or-more literal dots instead of any characters, and `?` only matches a literal dot. Fix by escaping regex-special characters from the *input* first, then converting glob syntax.</violation>
</file>
<file name="packages/local-dev/src/storage.ts">
<violation number="1" location="packages/local-dev/src/storage.ts:197">
P1: Path traversal defense-in-depth check doesn't enforce a directory boundary. `startsWith(this.rootDir)` will also match sibling directories that share the same prefix (e.g., rootDir `/tmp/foo` would match `/tmp/foobar/evil`). The check should require an exact match or a `/` separator after the root.</violation>
</file>
<file name="packages/local-dev/src/bash.ts">
<violation number="1" location="packages/local-dev/src/bash.ts:119">
P2: Shutdown always waits the full 5-second timeout even when all children exit immediately. After sending SIGTERM to children, the code checks `activeChildren.size === 0` only once (before children have had time to exit). There's no mechanism to detect when all children have finished and exit early. Consider listening for each child's exit and calling `process.exit(0)` when `activeChildren.size` reaches 0.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
| const resolved = resolve(this.rootDir, sanitized); | ||
|
|
||
| // Defense-in-depth: verify resolved path is within rootDir | ||
| if (!resolved.startsWith(this.rootDir)) { |
There was a problem hiding this comment.
P1: Path traversal defense-in-depth check doesn't enforce a directory boundary. startsWith(this.rootDir) will also match sibling directories that share the same prefix (e.g., rootDir /tmp/foo would match /tmp/foobar/evil). The check should require an exact match or a / separator after the root.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/local-dev/src/storage.ts, line 197:
<comment>Path traversal defense-in-depth check doesn't enforce a directory boundary. `startsWith(this.rootDir)` will also match sibling directories that share the same prefix (e.g., rootDir `/tmp/foo` would match `/tmp/foobar/evil`). The check should require an exact match or a `/` separator after the root.</comment>
<file context>
@@ -0,0 +1,487 @@
+ const resolved = resolve(this.rootDir, sanitized);
+
+ // Defense-in-depth: verify resolved path is within rootDir
+ if (!resolved.startsWith(this.rootDir)) {
+ throw new Error("Path traversal attempt detected");
+ }
</file context>
| }); | ||
|
|
||
| test("lists directory contents shows [DIR] for directories", async () => { | ||
| // The subdir created in previous test should appear as [DIR] |
There was a problem hiding this comment.
P2: This test depends on the previous test creating subdir, making it order-dependent and flaky if tests are run in isolation or reordered. Create the directory within this test (or a beforeEach) to keep it self-contained.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/local-dev/src/tools.test.ts, line 114:
<comment>This test depends on the previous test creating `subdir`, making it order-dependent and flaky if tests are run in isolation or reordered. Create the directory within this test (or a beforeEach) to keep it self-contained.</comment>
<file context>
@@ -0,0 +1,263 @@
+ });
+
+ test("lists directory contents shows [DIR] for directories", async () => {
+ // The subdir created in previous test should appear as [DIR]
+ const result = await client.callTool({
+ name: "list_directory",
</file context>
Release OptionsShould a new version be published when this PR is merged? React with an emoji to vote on the release type:
Current version: Deployment
|
There was a problem hiding this comment.
10 issues found across 24 files (changes from recent commits).
Prompt for AI agents (all issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="packages/cli/src/commands/mesh/link.ts">
<violation number="1" location="packages/cli/src/commands/mesh/link.ts:236">
P2: When reusing an existing link state, the project URL is built with the freshly resolved `meshUrl` instead of `existing.meshUrl`. This means if the Mesh URL changes between runs, the banner and browser open will point to the wrong instance. Use `existing.meshUrl` when reusing existing state, or validate that the resolved URL matches the stored one.</violation>
</file>
<file name="apps/mesh/src/tools/projects/delete.ts">
<violation number="1" location="apps/mesh/src/tools/projects/delete.ts:69">
P2: Deleting a project now deletes any localhost connection without checking whether other projects in the same organization still reference it. Since connections are organization-scoped and project plugin configs can share them, this can remove a shared connection and break other projects’ bindings. Consider deleting the connection only if no other project_plugin_configs rows reference it (or add an org-scoped check before removal).</violation>
</file>
<file name="packages/local-dev/src/server.ts">
<violation number="1" location="packages/local-dev/src/server.ts:197">
P2: When the server falls back to a new port, tool registration still uses the original `port`, so presigned URLs and any port-dependent tool behavior will point at the wrong port. Use the actual bound port after listening (or update tool registration to read `actualPort`) to avoid returning invalid URLs.</violation>
</file>
<file name="packages/cli/src/commands.ts">
<violation number="1" location="packages/cli/src/commands.ts:287">
P2: The new optional `[folder]` argument captures the first positional token, so legacy tunnel usage like `deco link npm run dev` is now treated as Mesh link mode and never runs the command. Gate Mesh mode so it only triggers when the user passes just a folder (no extra args), preserving the existing tunnel behavior.</violation>
</file>
<file name="apps/mesh/src/api/routes/local-dev-discover.ts">
<violation number="1" location="apps/mesh/src/api/routes/local-dev-discover.ts:98">
P1: Validate `port` as an integer in the allowed range before interpolating it into `connectionUrl`. Without runtime validation, a crafted string (e.g., `80@evil.com`) turns the URL into a request to an arbitrary host.</violation>
</file>
<file name="packages/cli/src/lib/mesh-auth.ts">
<violation number="1" location="packages/cli/src/lib/mesh-auth.ts:63">
P0: Shell injection vulnerability: `JSON.stringify()` is not a shell escaping function. It does not escape `$()`, backticks, or other shell metacharacters inside double-quoted strings. A crafted `meshUrl` (e.g., containing `$(malicious)`) or `apiKey` will be evaluated by the shell. Use a proper escaping approach — e.g., pass values via environment variables or `stdin` instead of string interpolation into shell commands.</violation>
<violation number="2" location="packages/cli/src/lib/mesh-auth.ts:109">
P0: Shell injection vulnerability (Linux path): Same `JSON.stringify`-as-shell-escaping issue. Both `meshUrl` and `apiKey` are interpolated unsafely. Use environment variables or `stdin` to pass secrets into `secret-tool`.</violation>
<violation number="3" location="packages/cli/src/lib/mesh-auth.ts:241">
P1: Bug: `createMeshApiKey(meshUrl, "")` is called with an empty cookie before the actual Bearer-token fetch. Since `createMeshApiKey` will throw on the unauthenticated request (non-200 response), the rest of this branch is unreachable. This line should be removed — the token-based API key creation is handled by the fetch below it.</violation>
</file>
<file name="packages/cli/src/lib/local-dev-manager.ts">
<violation number="1" location="packages/cli/src/lib/local-dev-manager.ts:58">
P2: Avoid piping stdout/stderr without consuming them; the child can block once its output buffers fill. Use inherited or ignored stdio instead.</violation>
</file>
<file name=".planning/ROADMAP.md">
<violation number="1" location=".planning/ROADMAP.md:116">
P3: The progress table rows for phases 16–18 are misaligned with the `Phase | Milestone | Plans Complete | Status | Completed` header (milestone values are missing, and the dates shifted into the wrong columns). Update these rows to include the milestone and align the columns so the roadmap data remains accurate.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
There was a problem hiding this comment.
1 issue found across 12 files (changes from recent commits).
Prompt for AI agents (all issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="packages/mesh-plugin-preview/components/preview-frame.tsx">
<violation number="1" location="packages/mesh-plugin-preview/components/preview-frame.tsx:174">
P2: Add `noopener` (and ideally `noreferrer`) to the `window.open` call to prevent the opened dev server tab from gaining access to `window.opener`.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
| const handleRefresh = () => setIframeKey((k) => k + 1); | ||
|
|
||
| const handleOpenInTab = () => { | ||
| window.open(iframeUrl, "_blank"); |
There was a problem hiding this comment.
P2: Add noopener (and ideally noreferrer) to the window.open call to prevent the opened dev server tab from gaining access to window.opener.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/mesh-plugin-preview/components/preview-frame.tsx, line 174:
<comment>Add `noopener` (and ideally `noreferrer`) to the `window.open` call to prevent the opened dev server tab from gaining access to `window.opener`.</comment>
<file context>
@@ -0,0 +1,278 @@
+ const handleRefresh = () => setIframeKey((k) => k + 1);
+
+ const handleOpenInTab = () => {
+ window.open(iframeUrl, "_blank");
+ };
+
</file context>
There was a problem hiding this comment.
1 issue found across 19 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="apps/mesh/src/api/routes/cli-auth.ts">
<violation number="1" location="apps/mesh/src/api/routes/cli-auth.ts:74">
P2: When `getFullOrganization` returns null (e.g., org was deleted, permission issue), the code silently creates an API key with `slug: undefined` and `name: undefined` in the metadata instead of returning an error. The first branch correctly checks for null org and returns 400 — this branch should do the same.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
There was a problem hiding this comment.
4 issues found across 12 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="apps/mesh/src/web/components/git-panel.tsx">
<violation number="1" location="apps/mesh/src/web/components/git-panel.tsx:536">
P1: Side effect during render: `generateMessage.mutate()` is called directly in the render body. This should be moved into a `useEffect` to avoid triggering async work during the render phase, which violates React's rules and can cause issues with concurrent rendering.</violation>
</file>
<file name="apps/mesh/src/web/lib/git-api.ts">
<violation number="1" location="apps/mesh/src/web/lib/git-api.ts:145">
P2: Branch names starting with `-` are treated as git options here because the command doesn’t terminate option parsing. Use `--` before the branch name to prevent option injection/checkout failures.</violation>
<violation number="2" location="apps/mesh/src/web/lib/git-api.ts:156">
P2: `git checkout -b` still treats `-foo` as an option. Add `--` before the branch name to avoid option injection and handle branch names that start with `-`.</violation>
</file>
<file name="apps/mesh/src/web/components/topbar/save-changes-button.tsx">
<violation number="1" location="apps/mesh/src/web/components/topbar/save-changes-button.tsx:22">
P3: Handle singular vs. plural so the button doesn’t render "1 changes" when there is only one change.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
| className="h-7 gap-1.5 relative" | ||
| > | ||
| <Save01 size={14} /> | ||
| <span>{changeCount > 0 ? `${changeCount} changes` : "Save"}</span> |
There was a problem hiding this comment.
P3: Handle singular vs. plural so the button doesn’t render "1 changes" when there is only one change.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/mesh/src/web/components/topbar/save-changes-button.tsx, line 22:
<comment>Handle singular vs. plural so the button doesn’t render "1 changes" when there is only one change.</comment>
<file context>
@@ -0,0 +1,28 @@
+ className="h-7 gap-1.5 relative"
+ >
+ <Save01 size={14} />
+ <span>{changeCount > 0 ? `${changeCount} changes` : "Save"}</span>
+ {changeCount > 0 && !isOpen && (
+ <span className="absolute -top-1 -right-1 h-2.5 w-2.5 rounded-full bg-yellow-500 border-2 border-background" />
</file context>
d50e0fa to
11703b1
Compare
|
@cubic-dev-ai i rebased from main, can you perform a clean review? |
@vibegui I have started the AI code review. It will take a few minutes to complete. |
There was a problem hiding this comment.
21 issues found across 65 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="apps/mesh/src/web/components/local-dev-banner.tsx">
<violation number="1" location="apps/mesh/src/web/components/local-dev-banner.tsx:18">
P3: folderName only handles POSIX `/` separators, so Windows-style paths will render the full path instead of the folder name. Consider splitting on both `/` and `\\` to show the correct folder name across platforms.</violation>
</file>
<file name="packages/local-dev/src/server.ts">
<violation number="1" location="packages/local-dev/src/server.ts:66">
P1: Missing `Access-Control-Expose-Headers` for `mcp-session-id`. The browser cannot read the session ID from the response in cross-origin requests, which will break MCP session management. `Allow-Headers` lets the client *send* the header; `Expose-Headers` lets the client *read* it from the response.</violation>
<violation number="2" location="packages/local-dev/src/server.ts:96">
P2: No `Content-Type` header set when serving files. Browsers will treat all files as `application/octet-stream`, causing images to download instead of rendering, CSS not to apply, etc. Consider using the file extension to set the appropriate MIME type before piping.</violation>
</file>
<file name="packages/local-dev/src/bash.ts">
<violation number="1" location="packages/local-dev/src/bash.ts:123">
P2: Shutdown always waits the full 5-second force-exit timeout even when all child processes exit immediately. After sending SIGTERM to children, the `activeChildren.size === 0` check runs synchronously (before children have exited). When children do exit, their `'exit'` handlers remove them from `activeChildren` but nothing triggers `process.exit()`. Since the HTTP server keeps the event loop alive, users always wait 5s on Ctrl+C.
Consider tracking shutdown state and checking in the child exit handler whether all children are done.</violation>
</file>
<file name="packages/cli/src/lib/mesh-auth.test.ts">
<violation number="1" location="packages/cli/src/lib/mesh-auth.test.ts:174">
P2: Close the callback server before rejecting the promise in the no-auth branch to avoid leaking an open listener and hanging tests.</violation>
</file>
<file name="packages/local-dev/src/storage.ts">
<violation number="1" location="packages/local-dev/src/storage.ts:464">
P2: No backpressure between `nodeStream` and `countingStream` — if the source is faster than disk writes, chunks accumulate in memory without bound. Replace the manual Readable + event wiring with a `Transform` stream that counts bytes, so `pipeline(nodeStream, countingTransform, writeStream)` propagates backpressure end-to-end.</violation>
</file>
<file name="apps/mesh/src/tools/projects/delete.ts">
<violation number="1" location="apps/mesh/src/tools/projects/delete.ts:69">
P2: Deleting localhost connections during project removal can break other projects because connections are organization-scoped and may be shared. Guard deletion by ensuring no other projects/agents still reference the connection before deleting it.</violation>
</file>
<file name="apps/mesh/src/web/components/git-panel.tsx">
<violation number="1" location="apps/mesh/src/web/components/git-panel.tsx:424">
P2: Branch error UI always shows "Commit or stash your changes first" regardless of the actual git error. The mutation captures the real error message from stderr/stdout, but it's never displayed — unlike the commit form which correctly shows `commit.error.message`. This hides the real cause when the error is something other than a dirty working tree (e.g., merge conflicts, invalid ref).</violation>
<violation number="2" location="apps/mesh/src/web/components/git-panel.tsx:895">
P2: Missing `staleTime` on the status query in `useGitChangeCount`. The same query key is used in `ChangedFilesList` with `staleTime: 10_000`, but this observer defaults to `0`, causing unnecessary refetches (e.g., on every window focus) since TanStack Query evaluates staleness per-observer.</violation>
</file>
<file name="packages/cli/src/commands.ts">
<violation number="1" location="packages/cli/src/commands.ts:278">
P2: The new optional `folder` positional argument hijacks the first token, so `deco link npm run dev` is now treated as a mesh folder link instead of tunnel mode. This breaks the documented build-command usage for `deco link`.</violation>
</file>
<file name="apps/mesh/src/api/routes/local-dev-discover.ts">
<violation number="1" location="apps/mesh/src/api/routes/local-dev-discover.ts:94">
P1: Validate the requested port before using it to build the localhost connection URL; otherwise authenticated callers can force the server to connect to arbitrary localhost ports (SSRF against local services).</violation>
</file>
<file name="packages/cli/src/lib/mesh-auth.ts">
<violation number="1" location="packages/cli/src/lib/mesh-auth.ts:62">
P1: **Shell command injection**: `JSON.stringify()` is not a shell-safe escaping mechanism. Inside bash double quotes, `$(...)` and backtick expressions are still expanded. Use `execFileSync` (which bypasses the shell) instead of `execSync` to safely pass `meshUrl` as an argument.
The same pattern recurs in `saveMeshToken` (lines ~101 and ~108) and should be fixed there too.</violation>
<violation number="2" location="packages/cli/src/lib/mesh-auth.ts:235">
P2: Broken code path: the comment says "Fall through to cookie-based flow" but the `if/else if` chain prevents fallthrough. Instead, `createMeshApiKey(meshUrl, "")` is called with an empty cookie, which will fail at the server with a 401. If this path isn't supported yet, it should either fall through to the `cookie` branch or return an explicit error.</violation>
</file>
<file name="packages/local-dev/src/tools.ts">
<violation number="1" location="packages/local-dev/src/tools.ts:1523">
P1: The `.` → `\\.` escaping is applied after `**` → `.*` conversion, which corrupts the `.*` regex. Glob patterns containing `**` (e.g., `**/*.ts`, `**/node_modules`) will not match correctly. Move the dot-escaping step to happen first, before any glob-to-regex conversions.</violation>
<violation number="2" location="packages/local-dev/src/tools.ts:1525">
P2: The fallback unanchored regex test (`new RegExp(regex).test(str)`) causes overly permissive substring matching. For example, a pattern `*.json` would match `file.json.bak` via the unanchored test. Consider whether the unanchored test is truly needed, or anchor both ends.</violation>
</file>
<file name="packages/mesh-sdk/src/hooks/use-chat-bridge.tsx">
<violation number="1" location="packages/mesh-sdk/src/hooks/use-chat-bridge.tsx:16">
P1: Use the context provider component. Rendering the context object directly will throw and the value won’t be available to consumers.</violation>
</file>
<file name=".planning/ROADMAP.md">
<violation number="1" location=".planning/ROADMAP.md:116">
P3: The progress table rows for phases 16–18 are missing the Milestone column value, so the columns are misaligned (e.g., “4/4” is now under Milestone). Fill in the Milestone column (e.g., `v1.3`) so the table renders correctly.</violation>
</file>
<file name="packages/local-dev/src/cli.ts">
<violation number="1" location="packages/local-dev/src/cli.ts:13">
P2: Validate the parsed port and fail fast on invalid values so the daemon doesn't start with NaN/invalid ports.</violation>
</file>
<file name="packages/local-dev/src/watch.ts">
<violation number="1" location="packages/local-dev/src/watch.ts:34">
P2: Add an `error` listener to the `FSWatcher`. Without it, watcher failures emit an unhandled `error` event and can crash the local-dev daemon.</violation>
</file>
<file name="packages/cli/src/commands/mesh/link.ts">
<violation number="1" location="packages/cli/src/commands/mesh/link.ts:74">
P3: The CLI always prints port 4201, but local-dev can auto-increment to a different port when 4201 is in use. This makes the displayed URL and status messages incorrect; use the actual `localDevServer.port` for output.</violation>
</file>
<file name="apps/mesh/src/web/lib/git-api.ts">
<violation number="1" location="apps/mesh/src/web/lib/git-api.ts:79">
P2: Handle rename statuses that include a work-tree modifier (e.g., `RM`, `RD`) so renamed files aren’t mislabeled as unknown.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
| openChat, | ||
| }: PropsWithChildren<ChatBridge>) { | ||
| return ( | ||
| <ChatBridgeContext value={{ sendMessage, openChat }}> |
There was a problem hiding this comment.
P1: Use the context provider component. Rendering the context object directly will throw and the value won’t be available to consumers.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/mesh-sdk/src/hooks/use-chat-bridge.tsx, line 16:
<comment>Use the context provider component. Rendering the context object directly will throw and the value won’t be available to consumers.</comment>
<file context>
@@ -0,0 +1,24 @@
+ openChat,
+}: PropsWithChildren<ChatBridge>) {
+ return (
+ <ChatBridgeContext value={{ sendMessage, openChat }}>
+ {children}
+ </ChatBridgeContext>
</file context>
8c867d9 to
89d602c
Compare
Introduces a complete local development workflow with Mesh: - `packages/local-dev` — MCP daemon with filesystem tools, bash, object storage, SSE file watching - `packages/cli` — `deco link` command to start daemon and connect to Mesh - `packages/mesh-plugin-preview` — Dev server preview plugin with auto-detection and iframe rendering - `apps/mesh` — Git panel sidebar for branch management, commits, AI-generated messages, and pre-commit error handling - Auto-discovery of local-dev daemons with one-click project creation - File watcher pause/resume during git operations to prevent server overload Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
89d602c to
fc876ff
Compare
Cancel in-flight status refetches before optimistically clearing data to prevent the topbar watcher from overwriting with stale state. Delay invalidation until filesystem settles. Close the panel on success. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add a dedicated /viewer route for viewing file contents. Clicking a file in the file browser navigates to the viewer, which fetches content via presigned URL and renders images inline, markdown with a Raw/Preview toggle, and other text in monospace. Clicking a changed file in the git panel also navigates to the viewer. - Fix plugin router useNavigate/Link to read pluginId from route context (not just params) so cross-route navigation works on static plugin routes - Fix git status --porcelain parser to handle inconsistent whitespace from MCP bash output (was truncating first char of filenames) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
1 issue found across 7 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="packages/mesh-plugin-object-storage/components/file-viewer.tsx">
<violation number="1" location="packages/mesh-plugin-object-storage/components/file-viewer.tsx:55">
P2: viewMode is only initialized on first render, so switching from a non-markdown file to a markdown file won’t reset the default to preview. Consider syncing viewMode when `key`/`isMarkdown` changes so markdown files default to preview as intended.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
| const isMarkdown = MARKDOWN_EXTENSIONS.has(ext); | ||
| const fileName = getFileName(key); | ||
|
|
||
| const [viewMode, setViewMode] = useState<"raw" | "preview">( |
There was a problem hiding this comment.
P2: viewMode is only initialized on first render, so switching from a non-markdown file to a markdown file won’t reset the default to preview. Consider syncing viewMode when key/isMarkdown changes so markdown files default to preview as intended.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/mesh-plugin-object-storage/components/file-viewer.tsx, line 55:
<comment>viewMode is only initialized on first render, so switching from a non-markdown file to a markdown file won’t reset the default to preview. Consider syncing viewMode when `key`/`isMarkdown` changes so markdown files default to preview as intended.</comment>
<file context>
@@ -0,0 +1,204 @@
+ const isMarkdown = MARKDOWN_EXTENSIONS.has(ext);
+ const fileName = getFileName(key);
+
+ const [viewMode, setViewMode] = useState<"raw" | "preview">(
+ isMarkdown ? "preview" : "raw",
+ );
</file context>
…ile viewer JSON files now get a Raw/Preview toggle like markdown. Preview mode pretty-prints with 2-space indentation and syntax coloring (keys, strings, numbers, booleans/null in distinct colors). Invalid JSON falls back to plain text. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The SaveChangesToolbarButton used useState with a one-time localStorage read, so it never updated when the panel was closed via the X button in the shell layout. Now uses the same TanStack Query key as useLocalStorage so both components share reactive state. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…er add-project - Fix plugin showing "Not Configured" after adding project by using full query invalidation instead of selective invalidation - Detect package manager from lock files (bun, pnpm, yarn, npm) - Parse port from script --port/-p flags and framework config files - Auto-save preview config when both command and port are confidently detected - Match discovered instances by root path instead of port only - Return root path from local-dev probe for accurate instance matching Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
2 issues found across 4 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="apps/mesh/src/api/routes/local-dev-discover.ts">
<violation number="1" location="apps/mesh/src/api/routes/local-dev-discover.ts:144">
P2: Normalize localDevRoot (e.g., trim trailing slashes) before storing and when comparing, otherwise the exact string match can incorrectly treat the same path as a new instance.</violation>
</file>
<file name="apps/mesh/src/web/components/local-dev-banner.tsx">
<violation number="1" location="apps/mesh/src/web/components/local-dev-banner.tsx:64">
P2: Invalidating all queries here can cause unnecessary refetches across the app. Prefer scoping invalidation to the project lists/configs that actually changed, as before.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
| organization_id: organizationId, | ||
| created_by: userId, | ||
| tools: tools?.length ? tools : null, | ||
| metadata: { localDevRoot: root }, |
There was a problem hiding this comment.
P2: Normalize localDevRoot (e.g., trim trailing slashes) before storing and when comparing, otherwise the exact string match can incorrectly treat the same path as a new instance.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/mesh/src/api/routes/local-dev-discover.ts, line 144:
<comment>Normalize localDevRoot (e.g., trim trailing slashes) before storing and when comparing, otherwise the exact string match can incorrectly treat the same path as a new instance.</comment>
<file context>
@@ -132,6 +141,7 @@ app.post("/add-project", async (c) => {
organization_id: organizationId,
created_by: userId,
tools: tools?.length ? tools : null,
+ metadata: { localDevRoot: root },
});
</file context>
| ); | ||
|
|
||
| queryClient.cancelQueries({ queryKey: KEYS.localDevDiscovery() }); | ||
| await queryClient.invalidateQueries(); |
There was a problem hiding this comment.
P2: Invalidating all queries here can cause unnecessary refetches across the app. Prefer scoping invalidation to the project lists/configs that actually changed, as before.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/mesh/src/web/components/local-dev-banner.tsx, line 64:
<comment>Invalidating all queries here can cause unnecessary refetches across the app. Prefer scoping invalidation to the project lists/configs that actually changed, as before.</comment>
<file context>
@@ -61,14 +61,7 @@ function DiscoveryCard({ instance }: { instance: DiscoveredInstance }) {
- queryKey: KEYS.projectPluginConfigs(result.project.id),
- }),
- ]);
+ await queryClient.invalidateQueries();
navigate({
to: "/$org/$project",
</file context>
useProjectBash() was grabbing the first org-wide connection with a bash tool, causing the git panel to show branches from a different project. Now looks up the project's plugin configs (object-storage, preview) to find the correct connection for the current project. Also sorts branch list by most recent commit date. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The git panel renders at shell-layout level where project.id is not available in context (only slug). Now fetches project data first via useProject(orgId, slug) — which hits the React Query cache from ProjectLayout — then uses the ID to look up plugin configs. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Click the localhost:PORT indicator to edit the URL. Supports changing the path (e.g. /about, /dashboard) and navigating with Enter. Escape or blur cancels editing. The path is reflected in the iframe and open-in-tab action. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When local-dev daemons restart, they may land on different ports (4201-4210). Previously the proxy used the stale connection_url, which could route to the wrong project's daemon. Now every MCP proxy request for a local-dev connection verifies the stored port serves the correct project root (via /_ready endpoint). If the port is stale, probes all ports to find the right daemon and updates the DB. If the daemon isn't running at all, returns 503 instead of silently proxying to the wrong project. Uses a 30s TTL cache for confirmed-correct ports and a 2s TTL for offline daemons so they're picked up quickly when started. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
2 issues found across 2 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="apps/mesh/src/api/routes/proxy.ts">
<violation number="1" location="apps/mesh/src/api/routes/proxy.ts:141">
P1: The new local-dev reconciliation block treats any `connection_url === null` as a local-dev outage. STDIO connections legitimately have null URLs, so this will now throw/503 for STDIO connections and break proxying. Gate the error handling to local-dev connections (e.g., metadata.localDevRoot) and only update the URL when a new non-null URL is provided.</violation>
</file>
<file name="apps/mesh/src/api/routes/local-dev-discover.ts">
<violation number="1" location="apps/mesh/src/api/routes/local-dev-discover.ts:192">
P1: Unhandled error in `reconcileLocalDevConnection` will crash the entire `/discover` endpoint. The reconciliation is a non-critical side-effect here (its return value is discarded), so a failure should not prevent discovery from working. Wrap in try/catch so the loop continues for other connections.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
| if (reconciled.connection_url === null) { | ||
| throw new Error( | ||
| "Local dev server is not running. Start it with `deco link` and try again.", | ||
| ); | ||
| } | ||
| if (reconciled.connection_url !== connection.connection_url) { | ||
| connection.connection_url = reconciled.connection_url; | ||
| } |
There was a problem hiding this comment.
P1: The new local-dev reconciliation block treats any connection_url === null as a local-dev outage. STDIO connections legitimately have null URLs, so this will now throw/503 for STDIO connections and break proxying. Gate the error handling to local-dev connections (e.g., metadata.localDevRoot) and only update the URL when a new non-null URL is provided.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/mesh/src/api/routes/proxy.ts, line 141:
<comment>The new local-dev reconciliation block treats any `connection_url === null` as a local-dev outage. STDIO connections legitimately have null URLs, so this will now throw/503 for STDIO connections and break proxying. Gate the error handling to local-dev connections (e.g., metadata.localDevRoot) and only update the URL when a new non-null URL is provided.</comment>
<file context>
@@ -135,6 +136,17 @@ async function createMCPProxyDoNotUseDirectly(
+ // Reconcile local-dev port drift before connecting
+ const reconciled = await reconcileLocalDevConnection(connection, ctx.storage);
+ if (reconciled.connection_url === null) {
+ throw new Error(
+ "Local dev server is not running. Start it with `deco link` and try again.",
</file context>
| if (reconciled.connection_url === null) { | |
| throw new Error( | |
| "Local dev server is not running. Start it with `deco link` and try again.", | |
| ); | |
| } | |
| if (reconciled.connection_url !== connection.connection_url) { | |
| connection.connection_url = reconciled.connection_url; | |
| } | |
| const meta = connection.metadata as { localDevRoot?: string } | null; | |
| if (meta?.localDevRoot && reconciled.connection_url === null) { | |
| throw new Error( | |
| "Local dev server is not running. Start it with `deco link` and try again.", | |
| ); | |
| } | |
| if ( | |
| reconciled.connection_url && | |
| reconciled.connection_url !== connection.connection_url | |
| ) { | |
| connection.connection_url = reconciled.connection_url; | |
| } |
| linkedRoots.add(meta.localDevRoot); | ||
|
|
||
| // Reconcile port drift for this connection | ||
| await reconcileLocalDevConnection(conn, meshContext.storage); |
There was a problem hiding this comment.
P1: Unhandled error in reconcileLocalDevConnection will crash the entire /discover endpoint. The reconciliation is a non-critical side-effect here (its return value is discarded), so a failure should not prevent discovery from working. Wrap in try/catch so the loop continues for other connections.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/mesh/src/api/routes/local-dev-discover.ts, line 192:
<comment>Unhandled error in `reconcileLocalDevConnection` will crash the entire `/discover` endpoint. The reconciliation is a non-critical side-effect here (its return value is discarded), so a failure should not prevent discovery from working. Wrap in try/catch so the loop continues for other connections.</comment>
<file context>
@@ -69,6 +187,10 @@ app.get("/discover", async (c) => {
linkedRoots.add(meta.localDevRoot);
+
+ // Reconcile port drift for this connection
+ await reconcileLocalDevConnection(conn, meshContext.storage);
+
continue;
</file context>
Objective
Enable a complete local development workflow with Mesh. A developer runs
deco linkin their project folder, and Mesh automatically discovers the local daemon, creates the project with all plugins (file browser, object storage, preview, Virtual MCP), and provides a full Git UI for branching, committing, and managing changes — all without leaving the browser.What's now possible:
deco link .in any project folderpackage.jsonand renders it in an iframeEnd-to-End Usage Guide
Step 1: Link your local project
deco link .This starts the
local-devMCP daemon on port 4201 (auto-increments if busy). The daemon exposes:/mcp— MCP endpoint with 18 filesystem tools + bash + object storage/watch— SSE stream of file changes for real-time UI updates/_ready— health checkStep 2: Add as project in Mesh
Open Mesh in your browser. A banner appears showing discovered local-dev instances. Click "Add as project" — this automatically:
Step 3: Preview your app
Navigate to the Preview tab in the sidebar. The preview plugin:
package.jsonStep 4: Make changes and commit
Click "Save" in the topbar (shows a badge with change count). The Git panel provides:
Step 5: Stop
Press
Ctrl+Cin the terminal to stop the daemon.What Changed
New Packages
packages/local-dev— Standalone MCP daemon: filesystem tools, object storage, bash, SSE file watcher, HTTP serverpackages/mesh-plugin-preview— Dev server preview plugin with auto-detection, iframe rendering, and AI setup helperNew CLI Command
deco link [folder]— Resolves Mesh URL, starts local-dev daemon inline, keeps running until Ctrl+C. No browser auto-open, no auth flow.New API Routes
GET /api/local-dev/discover— Probes ports 4201-4210 for local-dev daemons, filters already-linked onesPOST /api/local-dev/add-project— One-click project creation: connection + project + plugins + Virtual MCPNew UI Components
git-panel.tsx— Full git sidebar: branch selector (Popover+Command combobox), changed files, commit form with AI generation, commit history, error handling with "Help me fix"local-dev-banner.tsx— Discovery banner with "Add as project" buttonsave-changes-button.tsx— Topbar button with change count badgeuse-connection-watch.ts— SSE file watcher hook with pause/resume for git operationsuse-project-bash.ts— Hook to find the bash-capable connection for the current projectgit-api.ts— Git operations via MCP bash: branch, status, diff, log, commit, checkout, branch listInfrastructure Fixes
useMCPClientOptionalgcTime 0 → 5min (prevents reconnection storm on re-renders)How to Test
Automated Tests
Manual Testing
Local dev discovery:
deco link .in a project folderPreview plugin:
Git panel — branch operations:
Git panel — commit flow:
Stability:
🤖 Generated with Claude Code