Skip to content

feat: local-first DX with zero-ceremony setup and model selector overhaul#2528

Open
vibegui wants to merge 15 commits intomainfrom
feat/local-first-dx
Open

feat: local-first DX with zero-ceremony setup and model selector overhaul#2528
vibegui wants to merge 15 commits intomainfrom
feat/local-first-dx

Conversation

@vibegui
Copy link
Contributor

@vibegui vibegui commented Feb 28, 2026

Summary

  • Local-first setup: bunx @decocms/mesh creates a ~/deco data directory, auto-provisions a local admin user (using OS username), seeds the database, and starts the server — zero config required
  • Auto-auth connections: Installing a provider (e.g. OpenRouter) triggers automatic OAuth authentication and redirects back to home instead of the connection detail page
  • Model selector overhaul: Complete redesign of the model picker as a centered Dialog modal with tier-based categorization, shortlist management, and rich comparison tools

Model Selector Details

  • Tier categories: Models classified into Smarter / Faster / Cheaper using selective pattern matching on model IDs (flagship models only, variants go to "Other")
  • Shortlist management: "Manage models" view with per-tier checkboxes, persisted per-connection in localStorage. Default shortlist covers Anthropic, OpenAI, Google, xAI, Cohere, Mistral, Qwen, MiniMax
  • Rich details panel: Cost color coding (green→red) with percentile tags (Bottom 10%, Budget, Premium, Top 5%), token size tags (Small/Medium/Large/XL), model ID display, vendor/name split header
  • UX improvements: Centered Dialog instead of Popover (no viewport overflow), always-visible model name on desktop, default to selected model details on open, alphabetical sorting within tiers

Local Mode Details

  • CLI prompts for data directory on first run, persists secrets in ~/deco/secrets.json
  • Auto-builds frontend if dist/client/ is missing
  • Dev script (scripts/dev.ts) mirrors CLI environment with Vite HMR
  • Local admin uses DB-backed role with auto-redirect to first org

Test plan

  • bunx @decocms/mesh from scratch (no ~/deco) → prompts for directory, starts server, auto-login works
  • Click "Install OpenRouter" → auto-authenticates, returns to home
  • Open model selector → Smarter/Faster/Cheaper tiers, default model pre-selected in details panel
  • Click "Manage models" → grouped checkboxes, hover shows details with price/percentile tags
  • Toggle models → persists across page refresh
  • bun run fmt && bun run check && bun run lint all pass

🤖 Generated with Claude Code

@github-actions
Copy link
Contributor

🧪 Benchmark

Should we run the Virtual MCP strategy benchmark for this PR?

React with 👍 to run the benchmark.

Reaction Action
👍 Run quick benchmark (10 & 128 tools)

Benchmark will run on the next push after you react.

@github-actions
Copy link
Contributor

Release Options

Should a new version be published when this PR is merged?

React with an emoji to vote on the release type:

Reaction Type Next Version
👍 Prerelease 2.123.4-alpha.1
🎉 Patch 2.123.4
❤️ Minor 2.124.0
🚀 Major 3.0.0

Current version: 2.123.3

Deployment

  • Deploy to production (triggers ArgoCD sync after Docker image is published)

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>
@vibegui vibegui force-pushed the feat/local-first-dx branch from 225c9f7 to f2d1ea3 Compare February 28, 2026 10:36
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +329 to +345
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;
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Feb 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Suggested change
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;
Fix with Cubic

Comment on lines +521 to 526
if (existing) return null;

await this.db
.insertInto("workflow_execution_step_result")
.values({
execution_id: data.execution_id,
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Feb 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Fix with Cubic

Comment on lines +273 to +280
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();
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Feb 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Suggested change
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;
Fix with Cubic

Comment on lines +362 to 373
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();

Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Feb 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Suggested change
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;
Fix with Cubic

import { auth } from "./index";

const LOCAL_ADMIN_EMAIL = "admin@localhost";
const LOCAL_ADMIN_PASSWORD = "admin";
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Feb 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Fix with Cubic

Comment on lines +113 to +126
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;
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Feb 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Suggested change
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;
});
Fix with Cubic

.returningAll()
.execute();

return await this.db
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Feb 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Fix with Cubic

{"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}
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Feb 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Fix with Cubic

vibegui and others added 11 commits February 28, 2026 08:09
- 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>
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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(
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Feb 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Suggested change
const answer = await prompt(
const answer = process.stdin.isTTY
? await prompt(
` Where should Mesh store its data? ${dim}(${displayDefault})${reset} `,
)
: "";
Fix with Cubic

// 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
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Feb 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Suggested change
if (nextChar === "-") continue; // skip sub-variants like -preview
if (
nextChar === "-" &&
/[a-z]/i.test(modelId[prefix.length + 1] ?? "")
)
continue; // skip sub-variants like -preview
Fix with Cubic

- 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>
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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}`);
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Feb 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Fix with Cubic

meshHome = defaultHome;
} else {
meshHome = answer.startsWith("~")
? join(homedir(), answer.slice(1))
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Feb 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Fix with Cubic

@vibegui vibegui changed the title feat: local-first developer experience with zero-ceremony setup feat: local-first DX with zero-ceremony setup and model selector overhaul Feb 28, 2026
@vibegui
Copy link
Contributor Author

vibegui commented Feb 28, 2026

@cubic-dev-ai can you make a new complete revision? and aggregate all in one prompt for me

@cubic-dev-ai
Copy link
Contributor

cubic-dev-ai bot commented Feb 28, 2026

@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>
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Feb 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Fix with Cubic

.execute();

return !!result;
return true;
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Feb 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Fix with Cubic

await query.execute();

// Re-fetch and verify the update was applied
const row = await this.db
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Feb 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Fix with Cubic

if (!row) return null;

// If onlyIfStatus was specified, verify the status actually changed
if (options?.onlyIfStatus && data.status && row.status !== data.status) {
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Feb 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 with Cubic

- 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>
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +154 to +156
meshHome = values.home.startsWith("~")
? join(homedir(), values.home.slice(1))
: values.home;
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Feb 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Suggested change
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;
Fix with Cubic

Copy link
Contributor

@tlgimenes tlgimenes left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

~/deco is intrusive. Why not ~/.deco?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants