feat: local-first DX with zero-ceremony setup and model selector overhaul#2528
feat: local-first DX with zero-ceremony setup and model selector overhaul#2528
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. |
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
|
When running `npx @decocms/mesh`, developers now get a fully working mesh without sign-up, login, or org creation. Key changes: - Local mode: auto-creates admin@localhost user and "Local" org on first run, with auto-login middleware that skips the login screen - MESH_HOME (~\/deco\/): all data (database, secrets, assets) stored in a stable home directory instead of relative ./data/ - Local filesystem MCP: pre-configured object storage connection backed by ~/deco/assets/, available in local mode regardless of NODE_ENV - SQLite feature parity: replaced all 11 RETURNING clause usages with INSERT+SELECT pattern (threads.ts, workflow-collection.ts, workflow-execution.ts) so everything works on SQLite - CLI improvements: --home flag, --no-local-mode flag, informative startup banner showing mode/paths/URL Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
225c9f7 to
f2d1ea3
Compare
There was a problem hiding this comment.
9 issues found across 15 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="packages/mesh-plugin-workflows/server/storage/workflow-execution.ts">
<violation number="1" location="packages/mesh-plugin-workflows/server/storage/workflow-execution.ts:273">
P0: Race condition: Ignoring UPDATE affected rows allows multiple workers to concurrently claim the same execution.</violation>
<violation number="2" location="packages/mesh-plugin-workflows/server/storage/workflow-execution.ts:329">
P0: Race condition: Relying on a post-update SELECT to verify `onlyIfStatus` allows concurrent updates to incorrectly return success.</violation>
<violation number="3" location="packages/mesh-plugin-workflows/server/storage/workflow-execution.ts:362">
P1: Incorrect return value: cancelExecution unconditionally returns true even if the UPDATE fails due to concurrent status changes.</violation>
<violation number="4" location="packages/mesh-plugin-workflows/server/storage/workflow-execution.ts:521">
P0: Race condition: Ignoring INSERT affected rows breaks the idempotent step claim guarantee, allowing multiple workers to claim the same step.</violation>
</file>
<file name="apps/mesh/src/auth/local-mode.ts">
<violation number="1" location="apps/mesh/src/auth/local-mode.ts:14">
P2: Hardcoded "admin" as the local admin password creates a predictable credential when local mode is enabled by default. If the server is exposed beyond localhost, this is trivially guessable. Generate a random password (stored in the local secrets file) or require an explicit env override for local mode.</violation>
</file>
<file name="apps/mesh/src/api/routes/auth.ts">
<violation number="1" location="apps/mesh/src/api/routes/auth.ts:133">
P2: Hardcoded credentials duplicate the `LOCAL_ADMIN_EMAIL` and `LOCAL_ADMIN_PASSWORD` constants already defined in `local-mode.ts`. Export those constants and import them here to keep a single source of truth, avoiding silent breakage if the seed values change.</violation>
</file>
<file name=".planning/activity.jsonl">
<violation number="1" location=".planning/activity.jsonl:5">
P3: Remove the planning activity log from the repo; it contains developer-local absolute paths and exploratory prompts that should not be committed.</violation>
</file>
<file name="packages/mesh-plugin-workflows/server/storage/workflow-collection.ts">
<violation number="1" location="packages/mesh-plugin-workflows/server/storage/workflow-collection.ts:101">
P2: The update now performs an UPDATE followed by a separate SELECT without a transaction, so the returned row can reflect concurrent changes rather than the update just applied. Wrap the update+select in a transaction to keep the result consistent with the write.</violation>
<violation number="2" location="packages/mesh-plugin-workflows/server/storage/workflow-collection.ts:113">
P2: The delete is now split into SELECT then DELETE without a transaction, so you can return a row even if another request deletes it first. Wrap the read+delete in a transaction to keep behavior equivalent to the previous atomic `returningAll()`.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
| await query.execute(); | ||
|
|
||
| // Re-fetch and verify the update was applied | ||
| const row = await this.db | ||
| .selectFrom("workflow_execution") | ||
| .selectAll() | ||
| .where("id", "=", id) | ||
| .executeTakeFirst(); | ||
|
|
||
| if (!row) return null; | ||
|
|
||
| // If onlyIfStatus was specified, verify the status actually changed | ||
| if (options?.onlyIfStatus && data.status && row.status !== data.status) { | ||
| return null; | ||
| } | ||
|
|
||
| return row; |
There was a problem hiding this comment.
P0: Race condition: Relying on a post-update SELECT to verify onlyIfStatus allows concurrent updates to incorrectly return success.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/mesh-plugin-workflows/server/storage/workflow-execution.ts, line 329:
<comment>Race condition: Relying on a post-update SELECT to verify `onlyIfStatus` allows concurrent updates to incorrectly return success.</comment>
<file context>
@@ -312,25 +326,52 @@ export class WorkflowExecutionStorage {
- const row = await query.returningAll().executeTakeFirst();
- return row ?? null;
+ await query.execute();
+
+ // Re-fetch and verify the update was applied
</file context>
| await query.execute(); | |
| // Re-fetch and verify the update was applied | |
| const row = await this.db | |
| .selectFrom("workflow_execution") | |
| .selectAll() | |
| .where("id", "=", id) | |
| .executeTakeFirst(); | |
| if (!row) return null; | |
| // If onlyIfStatus was specified, verify the status actually changed | |
| if (options?.onlyIfStatus && data.status && row.status !== data.status) { | |
| return null; | |
| } | |
| return row; | |
| const updateResult = await query.execute(); | |
| if (Number(updateResult.numUpdatedRows) === 0) return null; | |
| const row = await this.db | |
| .selectFrom("workflow_execution") | |
| .selectAll() | |
| .where("id", "=", id) | |
| .executeTakeFirst(); | |
| return row ?? null; |
| if (existing) return null; | ||
|
|
||
| await this.db | ||
| .insertInto("workflow_execution_step_result") | ||
| .values({ | ||
| execution_id: data.execution_id, |
There was a problem hiding this comment.
P0: Race condition: Ignoring INSERT affected rows breaks the idempotent step claim guarantee, allowing multiple workers to claim the same step.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/mesh-plugin-workflows/server/storage/workflow-execution.ts, line 521:
<comment>Race condition: Ignoring INSERT affected rows breaks the idempotent step claim guarantee, allowing multiple workers to claim the same step.</comment>
<file context>
@@ -460,7 +510,17 @@ export class WorkflowExecutionStorage {
+ .where("step_id", "=", data.step_id)
+ .executeTakeFirst();
+
+ if (existing) return null;
+
+ await this.db
</file context>
| if (current?.status !== "enqueued") return null; | ||
|
|
||
| await this.db | ||
| .updateTable("workflow_execution") | ||
| .set({ status: "running", updated_at: now }) | ||
| .where("id", "=", executionId) | ||
| .where("status", "=", "enqueued") | ||
| .returningAll() | ||
| .execute(); |
There was a problem hiding this comment.
P0: Race condition: Ignoring UPDATE affected rows allows multiple workers to concurrently claim the same execution.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/mesh-plugin-workflows/server/storage/workflow-execution.ts, line 273:
<comment>Race condition: Ignoring UPDATE affected rows allows multiple workers to concurrently claim the same execution.</comment>
<file context>
@@ -263,12 +263,26 @@ export class WorkflowExecutionStorage {
+ .where("id", "=", executionId)
+ .executeTakeFirst();
+
+ if (current?.status !== "enqueued") return null;
+
+ await this.db
</file context>
| if (current?.status !== "enqueued") return null; | |
| await this.db | |
| .updateTable("workflow_execution") | |
| .set({ status: "running", updated_at: now }) | |
| .where("id", "=", executionId) | |
| .where("status", "=", "enqueued") | |
| .returningAll() | |
| .execute(); | |
| if (current?.status !== "enqueued") return null; | |
| const updateResult = await this.db | |
| .updateTable("workflow_execution") | |
| .set({ status: "running", updated_at: now }) | |
| .where("id", "=", executionId) | |
| .where("status", "=", "enqueued") | |
| .execute(); | |
| if (Number(updateResult.numUpdatedRows) === 0) return null; |
| if (!current || !["enqueued", "running"].includes(current.status)) { | ||
| return false; | ||
| } | ||
|
|
||
| await this.db | ||
| .updateTable("workflow_execution") | ||
| .set({ status: "cancelled", updated_at: now }) | ||
| .where("id", "=", executionId) | ||
| .where("organization_id", "=", organizationId) | ||
| .where("status", "in", ["enqueued", "running"]) | ||
| .returningAll() | ||
| .executeTakeFirst(); | ||
| .execute(); | ||
|
|
There was a problem hiding this comment.
P1: Incorrect return value: cancelExecution unconditionally returns true even if the UPDATE fails due to concurrent status changes.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/mesh-plugin-workflows/server/storage/workflow-execution.ts, line 362:
<comment>Incorrect return value: cancelExecution unconditionally returns true even if the UPDATE fails due to concurrent status changes.</comment>
<file context>
@@ -312,25 +326,52 @@ export class WorkflowExecutionStorage {
+ .where("organization_id", "=", organizationId)
+ .executeTakeFirst();
+
+ if (!current || !["enqueued", "running"].includes(current.status)) {
+ return false;
+ }
</file context>
| if (!current || !["enqueued", "running"].includes(current.status)) { | |
| return false; | |
| } | |
| await this.db | |
| .updateTable("workflow_execution") | |
| .set({ status: "cancelled", updated_at: now }) | |
| .where("id", "=", executionId) | |
| .where("organization_id", "=", organizationId) | |
| .where("status", "in", ["enqueued", "running"]) | |
| .returningAll() | |
| .executeTakeFirst(); | |
| .execute(); | |
| if (!current || !["enqueued", "running"].includes(current.status)) { | |
| return false; | |
| } | |
| const result = await this.db | |
| .updateTable("workflow_execution") | |
| .set({ status: "cancelled", updated_at: now }) | |
| .where("id", "=", executionId) | |
| .where("organization_id", "=", organizationId) | |
| .where("status", "in", ["enqueued", "running"]) | |
| .execute(); | |
| return Number(result.numUpdatedRows) > 0; |
apps/mesh/src/auth/local-mode.ts
Outdated
| import { auth } from "./index"; | ||
|
|
||
| const LOCAL_ADMIN_EMAIL = "admin@localhost"; | ||
| const LOCAL_ADMIN_PASSWORD = "admin"; |
There was a problem hiding this comment.
P2: Hardcoded "admin" as the local admin password creates a predictable credential when local mode is enabled by default. If the server is exposed beyond localhost, this is trivially guessable. Generate a random password (stored in the local secrets file) or require an explicit env override for local mode.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/mesh/src/auth/local-mode.ts, line 14:
<comment>Hardcoded "admin" as the local admin password creates a predictable credential when local mode is enabled by default. If the server is exposed beyond localhost, this is trivially guessable. Generate a random password (stored in the local secrets file) or require an explicit env override for local mode.</comment>
<file context>
@@ -0,0 +1,95 @@
+import { auth } from "./index";
+
+const LOCAL_ADMIN_EMAIL = "admin@localhost";
+const LOCAL_ADMIN_PASSWORD = "admin";
+const LOCAL_ADMIN_NAME = "Local Admin";
+const LOCAL_ORG_NAME = "Local";
</file context>
| const row = await this.db | ||
| .selectFrom("workflow_collection") | ||
| .selectAll() | ||
| .where("id", "=", id) | ||
| .where("organization_id", "=", organizationId) | ||
| .returningAll() | ||
| .executeTakeFirstOrThrow(); | ||
|
|
||
| await this.db | ||
| .deleteFrom("workflow_collection") | ||
| .where("id", "=", id) | ||
| .where("organization_id", "=", organizationId) | ||
| .execute(); | ||
|
|
||
| return row; |
There was a problem hiding this comment.
P2: The delete is now split into SELECT then DELETE without a transaction, so you can return a row even if another request deletes it first. Wrap the read+delete in a transaction to keep behavior equivalent to the previous atomic returningAll().
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/mesh-plugin-workflows/server/storage/workflow-collection.ts, line 113:
<comment>The delete is now split into SELECT then DELETE without a transaction, so you can return a row even if another request deletes it first. Wrap the read+delete in a transaction to keep behavior equivalent to the previous atomic `returningAll()`.</comment>
<file context>
@@ -86,27 +88,41 @@ export class WorkflowCollectionStorage {
): Promise<WorkflowCollectionRow> {
- return await this.db
- .deleteFrom("workflow_collection")
+ const row = await this.db
+ .selectFrom("workflow_collection")
+ .selectAll()
</file context>
| const row = await this.db | |
| .selectFrom("workflow_collection") | |
| .selectAll() | |
| .where("id", "=", id) | |
| .where("organization_id", "=", organizationId) | |
| .returningAll() | |
| .executeTakeFirstOrThrow(); | |
| await this.db | |
| .deleteFrom("workflow_collection") | |
| .where("id", "=", id) | |
| .where("organization_id", "=", organizationId) | |
| .execute(); | |
| return row; | |
| return await this.db.transaction().execute(async (trx) => { | |
| const row = await trx | |
| .selectFrom("workflow_collection") | |
| .selectAll() | |
| .where("id", "=", id) | |
| .where("organization_id", "=", organizationId) | |
| .executeTakeFirstOrThrow(); | |
| await trx | |
| .deleteFrom("workflow_collection") | |
| .where("id", "=", id) | |
| .where("organization_id", "=", organizationId) | |
| .execute(); | |
| return row; | |
| }); |
| .returningAll() | ||
| .execute(); | ||
|
|
||
| return await this.db |
There was a problem hiding this comment.
P2: The update now performs an UPDATE followed by a separate SELECT without a transaction, so the returned row can reflect concurrent changes rather than the update just applied. Wrap the update+select in a transaction to keep the result consistent with the write.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/mesh-plugin-workflows/server/storage/workflow-collection.ts, line 101:
<comment>The update now performs an UPDATE followed by a separate SELECT without a transaction, so the returned row can reflect concurrent changes rather than the update just applied. Wrap the update+select in a transaction to keep the result consistent with the write.</comment>
<file context>
@@ -86,27 +88,41 @@ export class WorkflowCollectionStorage {
- .returningAll()
+ .execute();
+
+ return await this.db
+ .selectFrom("workflow_collection")
+ .selectAll()
</file context>
.planning/activity.jsonl
Outdated
| {"ts":1772232509008,"phase":"done","tool":"Task","desc":"Explore resources tab UI","agent":"Explore","prompt":"I need to understand how the Resources tab works in the connection detail page and how the Tools tab implements its search/filter pattern so I can make Resources match Tools.\n\nSpecifically explore:\n1.","bg":true} | ||
| {"ts":1772233470948,"phase":"start","tool":"Task","desc":"Find Badge component and tools tab","agent":"Explore","prompt":"Find the Badge component in @deco/ui and the tools-tab.tsx file to understand existing patterns. I need:\n1. The Badge component import path and its variant options (especially \"outline\" and \"secondary","bg":false} | ||
| {"ts":1772233479535,"phase":"done","tool":"Task","desc":"Find Badge component and tools tab","agent":"Explore","prompt":"Find the Badge component in @deco/ui and the tools-tab.tsx file to understand existing patterns. I need:\n1. The Badge component import path and its variant options (especially \"outline\" and \"secondary","bg":true} | ||
| {"ts":1772233724701,"phase":"start","tool":"Task","desc":"Find form result UI widget","agent":"Explore","prompt":"In /Users/guilherme/Projects/mesh, find the UI widget that renders \"Form Result\" - it likely contains HTML with \"Registration\", \"NAME\", \"EMAIL\" fields. Search for:\n1. Files related to \"form-result\" or","bg":false} |
There was a problem hiding this comment.
P3: Remove the planning activity log from the repo; it contains developer-local absolute paths and exploratory prompts that should not be committed.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At .planning/activity.jsonl, line 5:
<comment>Remove the planning activity log from the repo; it contains developer-local absolute paths and exploratory prompts that should not be committed.</comment>
<file context>
@@ -0,0 +1,22 @@
+{"ts":1772232509008,"phase":"done","tool":"Task","desc":"Explore resources tab UI","agent":"Explore","prompt":"I need to understand how the Resources tab works in the connection detail page and how the Tools tab implements its search/filter pattern so I can make Resources match Tools.\n\nSpecifically explore:\n1.","bg":true}
+{"ts":1772233470948,"phase":"start","tool":"Task","desc":"Find Badge component and tools tab","agent":"Explore","prompt":"Find the Badge component in @deco/ui and the tools-tab.tsx file to understand existing patterns. I need:\n1. The Badge component import path and its variant options (especially \"outline\" and \"secondary","bg":false}
+{"ts":1772233479535,"phase":"done","tool":"Task","desc":"Find Badge component and tools tab","agent":"Explore","prompt":"Find the Badge component in @deco/ui and the tools-tab.tsx file to understand existing patterns. I need:\n1. The Badge component import path and its variant options (especially \"outline\" and \"secondary","bg":true}
+{"ts":1772233724701,"phase":"start","tool":"Task","desc":"Find form result UI widget","agent":"Explore","prompt":"In /Users/guilherme/Projects/mesh, find the UI widget that renders \"Form Result\" - it likely contains HTML with \"Registration\", \"NAME\", \"EMAIL\" fields. Search for:\n1. Files related to \"form-result\" or","bg":false}
+{"ts":1772233740692,"phase":"done","tool":"Task","desc":"Find form result UI widget","agent":"Explore","prompt":"In /Users/guilherme/Projects/mesh, find the UI widget that renders \"Form Result\" - it likely contains HTML with \"Registration\", \"NAME\", \"EMAIL\" fields. Search for:\n1. Files related to \"form-result\" or","bg":true}
+{"ts":1772240067904,"phase":"start","tool":"Task","desc":"Find chat tool result rendering","agent":"Explore","prompt":"In /Users/guilherme/Projects/mesh, I need to understand how tool call results are rendered in the chat UI. The issue is that MCP UI tools (like \"Ui Greeting\", \"Ui Stats Grid\", etc.) should render thei","bg":false}
</file context>
- On first run (~/deco/ doesn't exist), prompt user to confirm or choose a different data directory instead of silently creating it - When running from source (src/cli.ts), set NODE_ENV=development so the asset server proxies to Vite dev server instead of trying to serve from nonexistent dist/client/ Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- On first run, prompt user to confirm ~/deco/ or choose a custom data directory instead of silently creating it - Auto-build the frontend (vite build) if dist/client/ doesn't exist, so the CLI just works without needing a separate build step Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When running from src/index.ts, resolveClientDir("../client") points
to a nonexistent apps/mesh/client/ instead of apps/mesh/dist/client/.
Fall back to dist/client/ relative to the source file.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The CLI owns the data directory, so it should always explicitly set DATABASE_URL to the SQLite path inside MESH_HOME. This prevents accidentally connecting to a PostgreSQL database from the user's shell environment (e.g. DATABASE_URL=postgresql://...). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Better Auth rejects "admin@localhost" as invalid. Use "admin@localhost.mesh" which passes email validation. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Better Auth requires minimum password length. Use "admin@mesh". Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Set admin role directly in DB instead of via auth.api.setRole (which requires auth headers that aren't available during seed) - Let Better Auth's signup hook create the org automatically - Home route fetches user's org list and redirects to first org instead of hardcoding /local/org-admin slug Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Move the local mode seed (admin user + org creation) from cli.ts (before server) to index.ts (after Bun.serve). The org seed fetches tools from http://localhost:3000/mcp/self, which requires the server to be listening first. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Detect OS username (e.g. "guilherme") via os.userInfo()
- Create admin as "Guilherme <guilherme@localhost.mesh>"
- Rename auto-created org to "{username}-local" (e.g. "guilherme-local")
- Auto-login endpoint finds admin user dynamically
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The org seed creates API keys for connections that need auth, but fetchToolsFromMCP was using the original connection_token (null) instead of the newly created API key. This caused 401 errors when fetching tools from self MCP and local-files during seed. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ment, and rich details
- Replace flat model list with Smarter/Faster/Cheaper tier classification
using pattern-based model ID matching (selective prefixes, not broad catches)
- Add shortlist management via "Manage models" (localStorage per connection)
with checkbox UI grouped by tier, hover-to-preview in manage mode
- Switch from Popover to centered Dialog modal (no viewport overflow)
- Add price color coding (green→red) with percentile tags (Bottom 10%, Budget,
Premium, Top 5%) computed from all available models
- Add token size tags (Small/Medium/Large/XL) for context window and output limit
- Show model ID as monospace subtext, vendor as uppercase tag above model name
- Details panel defaults to showing selected model (no empty "Hover to preview")
- Always show model name + chevron on desktop (no hover-to-reveal)
- Strip vendor prefix from trigger display ("Claude Opus 4.6" not "Anthropic: ...")
- Default model auto-selection: Opus 4.6 → Sonnet 4.6 → first available
- Default shortlist includes flagships from Anthropic, OpenAI, Google, xAI,
Cohere, Mistral, Qwen, MiniMax across all tiers
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
2 issues found across 8 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/cli.ts">
<violation number="1" location="apps/mesh/src/cli.ts:164">
P2: Guard the first-run prompt for non-interactive environments. When stdin isn’t a TTY, skip prompting and fall back to the default path to avoid hanging in CI or scripted usage.</violation>
</file>
<file name="apps/mesh/src/web/components/chat/select-model.tsx">
<violation number="1" location="apps/mesh/src/web/components/chat/select-model.tsx:125">
P2: Exact-only prefix logic skips all "-" suffixes, which contradicts the stated intent to allow non-alpha/date suffixes. This causes date-stamped variants (e.g., ...-202405) to be misclassified into the "other" tier. Consider allowing numeric/date suffixes and only excluding alpha sub-variants like "-preview".</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
| console.log(""); | ||
| console.log(`${bold}${cyan}MCP Mesh${reset}`); | ||
| console.log(""); | ||
| const answer = await prompt( |
There was a problem hiding this comment.
P2: Guard the first-run prompt for non-interactive environments. When stdin isn’t a TTY, skip prompting and fall back to the default path to avoid hanging in CI or scripted usage.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/mesh/src/cli.ts, line 164:
<comment>Guard the first-run prompt for non-interactive environments. When stdin isn’t a TTY, skip prompting and fall back to the default path to avoid hanging in CI or scripted usage.</comment>
<file context>
@@ -119,10 +122,65 @@ if (values.version) {
+ console.log("");
+ console.log(`${bold}${cyan}MCP Mesh${reset}`);
+ console.log("");
+ const answer = await prompt(
+ ` Where should Mesh store its data? ${dim}(${displayDefault})${reset} `,
+ );
</file context>
| const answer = await prompt( | |
| const answer = process.stdin.isTTY | |
| ? await prompt( | |
| ` Where should Mesh store its data? ${dim}(${displayDefault})${reset} `, | |
| ) | |
| : ""; |
| // For exact-only prefixes, the rest must be empty or start with a non-alpha char (date suffixes ok) | ||
| if (EXACT_ONLY_PREFIXES.has(prefix) && modelId.length > prefix.length) { | ||
| const nextChar = modelId[prefix.length]; | ||
| if (nextChar === "-") continue; // skip sub-variants like -preview |
There was a problem hiding this comment.
P2: Exact-only prefix logic skips all "-" suffixes, which contradicts the stated intent to allow non-alpha/date suffixes. This causes date-stamped variants (e.g., ...-202405) to be misclassified into the "other" tier. Consider allowing numeric/date suffixes and only excluding alpha sub-variants like "-preview".
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/chat/select-model.tsx, line 125:
<comment>Exact-only prefix logic skips all "-" suffixes, which contradicts the stated intent to allow non-alpha/date suffixes. This causes date-stamped variants (e.g., ...-202405) to be misclassified into the "other" tier. Consider allowing numeric/date suffixes and only excluding alpha sub-variants like "-preview".</comment>
<file context>
@@ -40,66 +43,270 @@ import {
+ // For exact-only prefixes, the rest must be empty or start with a non-alpha char (date suffixes ok)
+ if (EXACT_ONLY_PREFIXES.has(prefix) && modelId.length > prefix.length) {
+ const nextChar = modelId[prefix.length];
+ if (nextChar === "-") continue; // skip sub-variants like -preview
+ }
+ return tier;
</file context>
| if (nextChar === "-") continue; // skip sub-variants like -preview | |
| if ( | |
| nextChar === "-" && | |
| /[a-z]/i.test(modelId[prefix.length + 1] ?? "") | |
| ) | |
| continue; // skip sub-variants like -preview |
- Add authenticate-connection helper for automatic OAuth/auth on install - Simplify connection detail page by extracting auth logic - Update no-llm-binding empty state for streamlined install flow - Add dev.ts script mirroring CLI local-mode setup with Vite HMR Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
2 issues found across 6 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/lib/authenticate-connection.ts">
<violation number="1" location="apps/mesh/src/web/lib/authenticate-connection.ts:23">
P2: When `token` is falsy but `error` is also falsy (e.g., `authenticateMcp` returns `{ token: undefined, error: undefined }`), the toast will display "Authentication failed: undefined". Use a fallback message.</violation>
</file>
<file name="apps/mesh/scripts/dev.ts">
<violation number="1" location="apps/mesh/scripts/dev.ts:60">
P2: `~/` expansion is incorrect: `answer.slice(1)` starts with `/`, so `join(homedir(), ...)` resolves to the filesystem root (e.g. `/deco`) instead of the user’s home directory. Strip the leading `~/` before joining to avoid writing data in the wrong location.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
| const { token, tokenInfo, error } = await authenticateMcp({ connectionId }); | ||
|
|
||
| if (error || !token) { | ||
| toast.error(`Authentication failed: ${error}`); |
There was a problem hiding this comment.
P2: When token is falsy but error is also falsy (e.g., authenticateMcp returns { token: undefined, error: undefined }), the toast will display "Authentication failed: undefined". Use a fallback message.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/mesh/src/web/lib/authenticate-connection.ts, line 23:
<comment>When `token` is falsy but `error` is also falsy (e.g., `authenticateMcp` returns `{ token: undefined, error: undefined }`), the toast will display "Authentication failed: undefined". Use a fallback message.</comment>
<file context>
@@ -0,0 +1,82 @@
+ const { token, tokenInfo, error } = await authenticateMcp({ connectionId });
+
+ if (error || !token) {
+ toast.error(`Authentication failed: ${error}`);
+ return false;
+ }
</file context>
| meshHome = defaultHome; | ||
| } else { | ||
| meshHome = answer.startsWith("~") | ||
| ? join(homedir(), answer.slice(1)) |
There was a problem hiding this comment.
P2: ~/ expansion is incorrect: answer.slice(1) starts with /, so join(homedir(), ...) resolves to the filesystem root (e.g. /deco) instead of the user’s home directory. Strip the leading ~/ before joining to avoid writing data in the wrong location.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/mesh/scripts/dev.ts, line 60:
<comment>`~/` expansion is incorrect: `answer.slice(1)` starts with `/`, so `join(homedir(), ...)` resolves to the filesystem root (e.g. `/deco`) instead of the user’s home directory. Strip the leading `~/` before joining to avoid writing data in the wrong location.</comment>
<file context>
@@ -0,0 +1,168 @@
+ meshHome = defaultHome;
+ } else {
+ meshHome = answer.startsWith("~")
+ ? join(homedir(), answer.slice(1))
+ : answer;
+ }
</file context>
|
@cubic-dev-ai can you make a new complete revision? and aggregate all in one prompt for me |
@vibegui I have started the AI code review. It will take a few minutes to complete. |
Remove Mistral, Cohere, Qwen, and MiniMax from default shortlists as they don't work correctly with Mesh yet. They remain in tier classification so users can add them manually via "Manage models". Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
9 issues found across 23 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/scripts/dev.ts">
<violation number="1" location="apps/mesh/scripts/dev.ts:60">
P2: Tilde expansion incorrectly drops the home directory for inputs like "~/deco" because the slice keeps a leading slash. Strip the leading slash before joining so `~/...` resolves under the user’s home directory.</violation>
</file>
<file name="apps/mesh/src/web/components/chat/select-model.tsx">
<violation number="1" location="apps/mesh/src/web/components/chat/select-model.tsx:746">
P2: Avoid scheduling state updates during render. `queueMicrotask` here runs in the render phase and can fire after an abandoned render, causing state updates on unmounted trees. Move the `onSelectedModelResolved` call into a `useEffect` tied to `selectedModel` and `allModels` instead.</violation>
</file>
<file name="apps/mesh/src/api/routes/auth.ts">
<violation number="1" location="apps/mesh/src/api/routes/auth.ts:143">
P1: Hardcoded password `"admin@mesh"` duplicates the `LOCAL_ADMIN_PASSWORD` constant in `local-mode.ts`. Import and reuse the constant to avoid silent breakage if the password is ever changed in one place but not the other.</violation>
</file>
<file name="apps/mesh/src/cli.ts">
<violation number="1" location="apps/mesh/src/cli.ts:154">
P1: The `--home` flag path does not expand `~`, unlike the interactive prompt path. If the shell doesn't expand the tilde (quoted argument, CI, Windows), all derived paths (database, secrets) will be wrong.</violation>
</file>
<file name="apps/mesh/src/web/routes/login.tsx">
<violation number="1" location="apps/mesh/src/web/routes/login.tsx:85">
P2: Validate `redirectTo` before assigning to `window.location.href` to avoid open redirects from the untrusted `next` query param in local-mode auto-login.</violation>
</file>
<file name="packages/mesh-plugin-workflows/server/storage/workflow-execution.ts">
<violation number="1" location="packages/mesh-plugin-workflows/server/storage/workflow-execution.ts:282">
P1: claimExecution can return a running execution even when this worker didn’t win the claim race because the update result isn’t checked before re-selecting the row.</violation>
<violation number="2" location="packages/mesh-plugin-workflows/server/storage/workflow-execution.ts:332">
P2: createStepResult can return a step result even when another worker claimed it first because the insert result isn’t checked before selecting the row.</violation>
<violation number="3" location="packages/mesh-plugin-workflows/server/storage/workflow-execution.ts:341">
P2: updateExecution can return a row even when the onlyIfStatus precondition failed (e.g., status mismatch while updating output), because the update result isn’t checked unless data.status is set.</violation>
<violation number="4" location="packages/mesh-plugin-workflows/server/storage/workflow-execution.ts:374">
P2: cancelExecution always returns true even if the update failed due to a race, so callers can think an execution was cancelled when it wasn’t.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
| .returningAll() | ||
| .execute(); | ||
|
|
||
| const updated = await this.db |
There was a problem hiding this comment.
P1: claimExecution can return a running execution even when this worker didn’t win the claim race because the update result isn’t checked before re-selecting the row.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/mesh-plugin-workflows/server/storage/workflow-execution.ts, line 282:
<comment>claimExecution can return a running execution even when this worker didn’t win the claim race because the update result isn’t checked before re-selecting the row.</comment>
<file context>
@@ -263,12 +263,26 @@ export class WorkflowExecutionStorage {
- .returningAll()
+ .execute();
+
+ const updated = await this.db
+ .selectFrom("workflow_execution")
+ .selectAll()
</file context>
| .execute(); | ||
|
|
||
| return !!result; | ||
| return true; |
There was a problem hiding this comment.
P2: cancelExecution always returns true even if the update failed due to a race, so callers can think an execution was cancelled when it wasn’t.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/mesh-plugin-workflows/server/storage/workflow-execution.ts, line 374:
<comment>cancelExecution always returns true even if the update failed due to a race, so callers can think an execution was cancelled when it wasn’t.</comment>
<file context>
@@ -312,25 +326,52 @@ export class WorkflowExecutionStorage {
+ .execute();
- return !!result;
+ return true;
}
</file context>
| await query.execute(); | ||
|
|
||
| // Re-fetch and verify the update was applied | ||
| const row = await this.db |
There was a problem hiding this comment.
P2: createStepResult can return a step result even when another worker claimed it first because the insert result isn’t checked before selecting the row.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/mesh-plugin-workflows/server/storage/workflow-execution.ts, line 332:
<comment>createStepResult can return a step result even when another worker claimed it first because the insert result isn’t checked before selecting the row.</comment>
<file context>
@@ -312,25 +326,52 @@ export class WorkflowExecutionStorage {
+ await query.execute();
+
+ // Re-fetch and verify the update was applied
+ const row = await this.db
+ .selectFrom("workflow_execution")
+ .selectAll()
</file context>
| if (!row) return null; | ||
|
|
||
| // If onlyIfStatus was specified, verify the status actually changed | ||
| if (options?.onlyIfStatus && data.status && row.status !== data.status) { |
There was a problem hiding this comment.
P2: updateExecution can return a row even when the onlyIfStatus precondition failed (e.g., status mismatch while updating output), because the update result isn’t checked unless data.status is set.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/mesh-plugin-workflows/server/storage/workflow-execution.ts, line 341:
<comment>updateExecution can return a row even when the onlyIfStatus precondition failed (e.g., status mismatch while updating output), because the update result isn’t checked unless data.status is set.</comment>
<file context>
@@ -312,25 +326,52 @@ export class WorkflowExecutionStorage {
+ if (!row) return null;
+
+ // If onlyIfStatus was specified, verify the status actually changed
+ if (options?.onlyIfStatus && data.status && row.status !== data.status) {
+ return null;
+ }
</file context>
- fix(model-selector): use useEffect instead of queueMicrotask for resolving selected model (avoids state updates during render) - fix(auth): import LOCAL_ADMIN_PASSWORD constant instead of hardcoding - fix(cli): add tilde expansion for --home flag argument - fix(login): validate redirectTo to prevent open redirects in local-mode - fix(dev): use repo-local .mesh-dev/ in CI/non-TTY to avoid touching ~/deco; skip interactive prompt when no TTY available 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="apps/mesh/src/cli.ts">
<violation number="1" location="apps/mesh/src/cli.ts:154">
P2: `~/` paths resolve to the filesystem root because `slice(1)` leaves the leading slash. Use a `~/`-specific branch (or handle `~` alone) so the join keeps the home directory.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
| meshHome = values.home.startsWith("~") | ||
| ? join(homedir(), values.home.slice(1)) | ||
| : values.home; |
There was a problem hiding this comment.
P2: ~/ paths resolve to the filesystem root because slice(1) leaves the leading slash. Use a ~/-specific branch (or handle ~ alone) so the join keeps the home directory.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/mesh/src/cli.ts, line 154:
<comment>`~/` paths resolve to the filesystem root because `slice(1)` leaves the leading slash. Use a `~/`-specific branch (or handle `~` alone) so the join keeps the home directory.</comment>
<file context>
@@ -150,8 +150,10 @@ function prompt(question: string): Promise<string> {
- // Explicitly passed via --home flag
- meshHome = values.home;
+ // Explicitly passed via --home flag — expand ~ to home directory
+ meshHome = values.home.startsWith("~")
+ ? join(homedir(), values.home.slice(1))
+ : values.home;
</file context>
| meshHome = values.home.startsWith("~") | |
| ? join(homedir(), values.home.slice(1)) | |
| : values.home; | |
| meshHome = values.home === "~" | |
| ? homedir() | |
| : values.home.startsWith("~/") | |
| ? join(homedir(), values.home.slice(2)) | |
| : values.home; |
tlgimenes
left a comment
There was a problem hiding this comment.
~/deco is intrusive. Why not ~/.deco?
Summary
bunx @decocms/meshcreates a~/decodata directory, auto-provisions a local admin user (using OS username), seeds the database, and starts the server — zero config requiredModel Selector Details
Local Mode Details
~/deco/secrets.jsondist/client/is missingscripts/dev.ts) mirrors CLI environment with Vite HMRTest plan
bunx @decocms/meshfrom scratch (no~/deco) → prompts for directory, starts server, auto-login worksbun run fmt && bun run check && bun run lintall pass🤖 Generated with Claude Code