@dypai-ai/mcp 1.4.3 → 1.4.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dypai-ai/mcp",
3
- "version": "1.4.3",
3
+ "version": "1.4.5",
4
4
  "description": "DYPAI MCP Server — AI agent toolkit for building and deploying full-stack apps",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
package/src/api.js CHANGED
@@ -63,9 +63,21 @@ export function request(method, path, body) {
63
63
  res.on("end", () => {
64
64
  if (res.statusCode >= 200 && res.statusCode < 300) {
65
65
  try { resolve(JSON.parse(buf)) } catch { resolve(buf) }
66
- } else {
67
- reject(new Error(`HTTP ${res.statusCode}: ${buf.slice(0, 500)}`))
66
+ return
68
67
  }
68
+ // Build a richer error for quota-specific 429s so MCP tools can surface
69
+ // them to the agent without parsing raw HTTP strings.
70
+ let parsedBody = null
71
+ try { parsedBody = JSON.parse(buf) } catch {}
72
+ const err = new Error(`HTTP ${res.statusCode}: ${buf.slice(0, 500)}`)
73
+ err.statusCode = res.statusCode
74
+ err.body = parsedBody
75
+ const detail = parsedBody && parsedBody.detail
76
+ if (detail && typeof detail === "object" && detail.error) {
77
+ err.code = detail.error
78
+ err.detail = detail
79
+ }
80
+ reject(err)
69
81
  })
70
82
  })
71
83
  req.on("error", reject)
@@ -21,6 +21,11 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
21
21
  const PKG_PATH = join(__dirname, "..", "package.json");
22
22
  const PKG_NAME = "@dypai-ai/mcp";
23
23
  const REGISTRY_URL = `https://registry.npmjs.org/${PKG_NAME}/latest`;
24
+ // Dist-tags endpoint is tiny (<200B) — used to check if there's a CRITICAL
25
+ // release the user must upgrade to immediately, bypassing the 6h throttle.
26
+ // To mark a version as critical after publish:
27
+ // npm dist-tag add @dypai-ai/mcp@1.4.5 critical
28
+ const DIST_TAGS_URL = `https://registry.npmjs.org/-/package/${PKG_NAME}/dist-tags`;
24
29
  const CHECK_TIMEOUT_MS = 2000;
25
30
  const THROTTLE_HOURS = 6;
26
31
  const STATE_FILE = join(tmpdir(), "dypai-mcp-update-state.json");
@@ -68,6 +73,32 @@ async function fetchLatestManifest() {
68
73
  }
69
74
  }
70
75
 
76
+ /**
77
+ * Fetch the `critical` dist-tag (if published). Used to bypass the 6h throttle
78
+ * when a release is important enough that users must upgrade on next spawn.
79
+ *
80
+ * Returns the critical version string (e.g. "1.4.5") or null if no critical
81
+ * tag is set, the registry is unreachable, or the response is malformed.
82
+ *
83
+ * Cost: one tiny JSON fetch (~200 bytes) per spawn. Adds ~50-150ms to startup
84
+ * but runs in parallel with the rest of the MCP init, so wall-clock impact is
85
+ * usually zero.
86
+ */
87
+ async function fetchCriticalVersion() {
88
+ const ctrl = new AbortController();
89
+ const timer = setTimeout(() => ctrl.abort(), CHECK_TIMEOUT_MS);
90
+ try {
91
+ const res = await fetch(DIST_TAGS_URL, { signal: ctrl.signal });
92
+ if (!res.ok) return null;
93
+ const tags = await res.json();
94
+ return typeof tags?.critical === "string" ? tags.critical : null;
95
+ } catch {
96
+ return null;
97
+ } finally {
98
+ clearTimeout(timer);
99
+ }
100
+ }
101
+
71
102
  /**
72
103
  * After npm publish there's a 30s–2min window where the registry knows the
73
104
  * version but the tarball is not yet replicated across the CDN. If we clear
@@ -145,7 +176,19 @@ export async function checkForUpdates({ force = false } = {}) {
145
176
  const current = getCurrentVersion();
146
177
  if (!current) return { skipped: "no current version" };
147
178
 
148
- // Throttle
179
+ // ── Critical release check (bypasses the 6h throttle) ────────────────────
180
+ // If an ops person has run `npm dist-tag add @dypai-ai/mcp@X.Y.Z critical`,
181
+ // every spawn picks that up and forces an upgrade regardless of when the
182
+ // last normal check ran. Used for security / data-loss bug fixes where
183
+ // 6-24h propagation is too slow.
184
+ const criticalVersion = await fetchCriticalVersion();
185
+ const hasCritical = criticalVersion && compareVersions(criticalVersion, current) > 0;
186
+ if (hasCritical) {
187
+ log(`CRITICAL update required: ${current} → ${criticalVersion} (bypassing throttle)`);
188
+ force = true;
189
+ }
190
+
191
+ // Throttle (skipped when force or critical)
149
192
  if (!force) {
150
193
  const state = readState();
151
194
  if (state.lastCheckAt) {
package/src/index.js CHANGED
@@ -110,7 +110,7 @@ const REMOTE_TOOLS = [
110
110
  // Note: `get_app_tables` is intentionally NOT exposed — dypai/schema.sql already
111
111
  // caches table info locally (auto-refreshed on DDL). For ad-hoc introspection,
112
112
  // use execute_sql against information_schema.
113
- { name: "execute_sql", description: "Executes any SQL query on the project database (PostgreSQL). Supports SELECT, INSERT, UPDATE, DELETE, CREATE TABLE, ALTER TABLE, DROP TABLE. Platform schemas (system, auth, storage) are read-only for security. DDL on public.* auto-refreshes dypai/schema.sql.", inputSchema: { type: "object", properties: { project_id: { type: "string" }, sql: { type: "string", description: "SQL query to execute" } }, required: ["sql"] } },
113
+ { name: "execute_sql", description: "BACKEND ONLY — executes any SQL query on the project database (PostgreSQL). Supports SELECT, INSERT, UPDATE, DELETE, CREATE TABLE, ALTER TABLE, DROP TABLE. Platform schemas (system, auth, storage) are read-only for security. DDL on public.* auto-refreshes dypai/schema.sql. Note: DDL applies IMMEDIATELY to the live database (no draft stage) — for destructive DDL like DROP TABLE / TRUNCATE, summarize the impact and get the user's explicit OK before calling.", inputSchema: { type: "object", properties: { project_id: { type: "string" }, sql: { type: "string", description: "SQL query to execute" } }, required: ["sql"] } },
114
114
 
115
115
  // ── API Endpoints ─────────────────────────────────────────────────────────
116
116
  // Full CRUD + exploration goes through the git-first flow:
@@ -230,6 +230,56 @@ Operations:
230
230
  },
231
231
  },
232
232
 
233
+ // ── Drafts (production-only staging area) ────────────────────────────────
234
+ // manage_drafts wraps the cloud SDK that talks to /api/engine/{id}/endpoints/
235
+ // Single tool, three ops (list / publish / discard) — same shape as
236
+ // manage_users / manage_roles / manage_storage so the agent's mental
237
+ // model stays uniform. Every project starts in draft-publish mode by
238
+ // default: dypai_push stages mutations as drafts and the user (or
239
+ // agent on their behalf) publishes when ready.
240
+ {
241
+ name: "manage_drafts",
242
+ description: `BACKEND ONLY — inspect, publish, or discard pending backend changes (drafts).
243
+
244
+ Mental model: every change made by \`dypai_push\` (endpoints, webhooks,
245
+ crons, realtime policies) is staged as a DRAFT first. Drafts do NOT
246
+ affect live traffic; they only show up in the live config once the user
247
+ publishes them. This is the universal default — works the same on every
248
+ project, no environment flags to worry about. The frontend is a separate
249
+ stack — use \`manage_frontend(deploy)\` to publish frontend changes.
250
+
251
+ Operations:
252
+ - list: Return pending drafts grouped by resource type. Read-only,
253
+ no confirmation. Run this BEFORE publish/discard so the user
254
+ sees exactly what will ship or be thrown away.
255
+ - publish: Atomically apply ALL pending drafts to live (creates, updates,
256
+ deletions) and invalidate affected caches. Requires confirm:true.
257
+ - discard: Drop pending drafts without applying. By default discards
258
+ every draft; pass resource_names:[...] to scope the discard
259
+ to specific endpoints. Requires confirm:true.
260
+
261
+ Typical flow:
262
+ 1. dypai_push → changes saved as drafts
263
+ 2. manage_drafts(operation:'list') → show user what's pending
264
+ 3. (optional) test the draft from local dev / preview
265
+ 4. manage_drafts(operation:'publish', confirm:true) → make it live
266
+ OR manage_drafts(operation:'discard', confirm:true) → throw it away`,
267
+ inputSchema: {
268
+ type: "object",
269
+ properties: {
270
+ project_id: { type: "string", description: "Project UUID. Required for user tokens; auto-detected for project tokens." },
271
+ operation: { type: "string", enum: ["list", "publish", "discard"], description: "Operation to perform." },
272
+ confirm: { type: "boolean", description: "Required true for publish and discard. Without it the tool returns a confirmation_required hint instead of mutating.", default: false },
273
+ resource_names: { type: "array", items: { type: "string" }, description: "Optional. For discard: scope to drafts whose resource_name matches one of these. Ignored by list/publish." },
274
+ },
275
+ required: ["operation"],
276
+ allOf: [
277
+ { if: { properties: { operation: { const: "publish" } }, required: ["operation"] }, then: { required: ["confirm"] } },
278
+ { if: { properties: { operation: { const: "discard" } }, required: ["operation"] }, then: { required: ["confirm"] } },
279
+ ],
280
+ },
281
+ },
282
+
233
283
  // ── Storage ───────────────────────────────────────────────────────────────
234
284
  // manage_storage covers BOTH bucket-level and object-level operations.
235
285
  // The remote also accepts the legacy name `list_buckets` (alias) so older
@@ -405,7 +455,109 @@ endpoint YAML and \`dypai_push\`. This tool does NOT modify the definition.`,
405
455
 
406
456
  // ── Server Instructions ──────────────────────────────────────────────────────
407
457
 
408
- const SERVER_INSTRUCTIONS = `You are building full-stack applications on the DYPAI platform. You handle backend (endpoints, database, auth, realtime) AND frontend (SDK integration, UI code).
458
+ const SERVER_INSTRUCTIONS = `You are building full-stack applications on the DYPAI platform. You handle BACKEND (workflow endpoints, database, auth, realtime) and FRONTEND (SDK integration, React/Vite/Next code).
459
+
460
+ # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
461
+ # QUICK START — read this section even if you skip everything else
462
+ # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
463
+
464
+ ## What you ship — two completely separate stacks
465
+
466
+ | Stack | What it is | Where it lives | How you change it | How you publish |
467
+ |---|---|---|---|---|
468
+ | **BACKEND** | Workflow endpoints (HTTP, cron, webhook, telegram), DB schema, realtime policies | \`dypai/\` folder | Edit YAMLs / SQL files / realtime.yaml on disk | \`dypai_push\` (saves draft) → \`manage_drafts(publish, confirm:true)\` |
469
+ | **FRONTEND** | React/Vite/Next bundle (UI, SDK calls) | \`src/\`, \`public/\`, \`package.json\` | Edit React code | \`manage_frontend(deploy, confirm:true)\` |
470
+
471
+ Two things to internalize:
472
+ 1. The two stacks are SHIPPED INDEPENDENTLY. Editing backend never touches frontend, and vice versa.
473
+ 2. Backend has a draft stage; frontend does NOT. Both publish operations require \`confirm:true\`.
474
+
475
+ ## Backend lifecycle — "save" = \`dypai_push\` (this is the rule that trips up new agents)
476
+
477
+ Editing files inside \`dypai/\` only changes YOUR DISK. The platform doesn't see them, the draft overlay doesn't serve them, the local frontend can't call them. There are exactly **three** states and only \`dypai_push\` and \`manage_drafts(publish)\` move you between them:
478
+
479
+ \`\`\`
480
+ ┌────────────┐ edit YAML / SQL / md ┌────────────┐ dypai_push ┌────────────┐ manage_drafts(publish, confirm:true) ┌────────────┐
481
+ │ LIVE │ ──────────────────────► │ LOCAL DISK │ ────────────► │ DRAFT │ ──────────────────────────────────────► │ LIVE │
482
+ │ (engine) │ (no platform impact) │ (your edit)│ (stages it) │ (overlay) │ (atomic, all drafts at once) │ (engine) │
483
+ └────────────┘ └────────────┘ └────────────┘ └────────────┘
484
+
485
+ draft overlay │ served at https://dev-<project_id>.dypai.dev
486
+ │ (what the user's local frontend points to)
487
+ \`\`\`
488
+
489
+ Practical consequences — internalize these:
490
+ - **After EVERY meaningful change set, call \`dypai_push\`.** Don't batch a session's worth of edits hoping to push at the end — if you forget, the user tests the local UI and sees the OLD behavior, gets confused, and you waste a debug round-trip blaming the code. The push is cheap, idempotent, and creates ONE draft per resource (subsequent pushes overwrite the draft, not stack new ones).
491
+ - **\`dypai_push\` is the "save" button. It is NOT a publish.** Live traffic is untouched. You can push 20 times in a row without affecting a single user. Tell the user that explicitly when they ask "did it ship?" — push = staged draft, publish = live.
492
+ - **The draft overlay (\`dev-<project_id>.dypai.dev\`) only sees what you've pushed.** A change still on disk is invisible to the local frontend. If the user says "I tested it locally and nothing changed", your first check is "did I run \`dypai_push\` after the last edit?".
493
+ - **\`dypai_validate\` before \`dypai_push\`** — push runs validate as a pre-flight, but running it explicitly first gives you the lint output without committing. Cheap insurance.
494
+ - **Order during a multi-step feature**: edit → \`dypai_validate\` → \`dypai_push\` → \`dypai_test_endpoint(mode:'draft')\` (or tell the user to test their local UI). Repeat per change. ONLY at the end, when the user signs off, do \`manage_drafts(operation:'list')\` → \`manage_drafts(operation:'publish', confirm:true)\`.
495
+ - **DDL is the exception**: \`execute_sql\` with CREATE / ALTER / DROP TABLE applies to live IMMEDIATELY (no draft stage for schema). Drafts only exist for endpoints / webhooks / crons / realtime policies. Summarize destructive DDL to the user before running it.
496
+
497
+ ## User intent → tool to call (decision table)
498
+
499
+ Use this BEFORE picking a tool. If unsure which row matches, ask the user.
500
+
501
+ | If the user asks to... | First call | Then |
502
+ |---|---|---|
503
+ | "Create a new project" | \`search_project_templates\` (find a starter) | \`create_project(template_slug: ...)\` |
504
+ | "Show me what we have" / "I want to work on existing project X" | \`list_projects\` → \`dypai_pull\` (backend) + \`manage_frontend(sync)\` (frontend) | Read \`dypai/\` files + \`src/\` |
505
+ | "Add/change a backend endpoint, table, cron, webhook, agent, integration" | Edit files in \`dypai/\` | \`dypai_validate\` → \`dypai_push\` |
506
+ | "Publish my backend changes" / "make it live" | \`manage_drafts(operation:'list')\` to show what's pending | \`manage_drafts(operation:'publish', confirm:true)\` |
507
+ | "Test an endpoint before publishing" | \`dypai_test_endpoint(mode:'local')\` (your edits) or \`(mode:'draft')\` (after push) | — |
508
+ | "Test the new endpoint from my local frontend, end-to-end, before publishing" | Tell user: their local frontend already points to \`https://dev-<project_id>.dypai.dev\` (set by \`manage_frontend(sync)\`), which serves drafts on top of live. So after \`dypai_push\` the local UI hits the draft overlay automatically — nothing else to do. | — |
509
+ | "Throw away my backend changes" | \`manage_drafts(operation:'discard', confirm:true)\` | — |
510
+ | "Change the UI / change colors / add a page" | Edit files in \`src/\` | \`manage_frontend(deploy, confirm:true)\` |
511
+ | "Publish the new UI" / "ship the frontend" | \`manage_frontend(deploy, confirm:true)\` | (deploy is the publish — there is no separate step) |
512
+ | "Roll back" | Backend: \`get_endpoint_versions\` then write old code back. Frontend: re-deploy older source. | — |
513
+ | "Upload a file / a CSV / seed data" | \`bulk_upsert\` (data) or \`manage_storage(upload_file)\` (binary) | — |
514
+
515
+ ## Confirm rules — the ONLY operations that need \`confirm:true\`
516
+
517
+ There are exactly THREE write operations that mutate live state and require explicit user approval (return a \`confirmation_required\` hint without it):
518
+
519
+ 1. **\`manage_drafts(operation:'publish', confirm:true)\`** — promotes backend drafts to live.
520
+ 2. **\`manage_drafts(operation:'discard', confirm:true)\`** — throws away pending backend drafts.
521
+ 3. **\`manage_frontend(operation:'deploy', confirm:true)\`** — replaces the live frontend bundle.
522
+
523
+ Everything else (\`dypai_push\`, \`execute_sql\`, \`bulk_upsert\`, all read tools) does NOT require confirm. \`dypai_push\` is safe by design: it stages drafts, so you can iterate freely without affecting the live site.
524
+
525
+ When you receive a \`confirmation_required\` response: SUMMARIZE the change to the user in plain language (what will go live, any warnings about pending backend drafts), wait for an explicit "yes" / "go ahead", then re-call with \`confirm:true\`.
526
+
527
+ ## End-to-end example — adding a feature that touches BOTH backend and frontend
528
+
529
+ User: "Add a /api/list-tasks endpoint that returns the current user's tasks, and show them on the dashboard."
530
+
531
+ \`\`\`
532
+ 1. dypai_pull(project_id) # materialize backend if not already on disk
533
+ 2. manage_frontend(operation:'sync', ...) # materialize frontend if not already on disk
534
+ 3. # Backend: create the endpoint
535
+ Write dypai/endpoints/list-tasks.yaml # trigger.http_api auth_mode:jwt + dypai_database query
536
+ 4. dypai_validate # catch typos before push
537
+ 5. dypai_push # stages as draft, NOT live yet
538
+ 6. dypai_test_endpoint(name:'list-tasks', mode:'draft', as_user:'<uuid>') # verify the staged version
539
+ 7. manage_drafts(operation:'list') # show pending changes to user
540
+ 8. # ASK USER: "Ready to publish list-tasks to live?"
541
+ 9. manage_drafts(operation:'publish', confirm:true) # backend now live ✅
542
+ 10. # Frontend: call the new endpoint from React
543
+ Edit src/pages/Dashboard.tsx # useEndpoint('list-tasks')
544
+ 11. # ASK USER: "Ready to deploy the dashboard UI?"
545
+ 12. manage_frontend(operation:'deploy', sourceDirectory, confirm:true) # frontend now live ✅
546
+ \`\`\`
547
+
548
+ **Order matters**: publish backend BEFORE deploying frontend. Otherwise the new UI calls an endpoint that doesn't exist on live yet → 404s for users. The \`manage_frontend(deploy)\` confirmation hint will warn you if backend drafts are still pending.
549
+
550
+ ## What you do NOT have to think about
551
+
552
+ - "Development vs production environment" — the user never sees this. Backend changes always go through draft-and-publish. Frontend changes always go through deploy. That's the whole model.
553
+ - "Create auth endpoints" — auth is built into the SDK. \`dypai.auth.signInWithPassword()\` works out of the box. NEVER write a login/signup workflow.
554
+ - "RLS / row-level security" — there is none. You filter by \`\${current_user_id}\` in your SQL WHERE clauses. Forgetting this is the #1 multi-tenancy bug.
555
+ - "Rate limiting / CORS / JWT verification" — handled by the engine.
556
+ - "Promoting projects to production" — every new project already has the draft-publish flow enabled. \`manage_project(promote_to_production)\` is legacy and you should never need it.
557
+
558
+ # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
559
+ # DEEP REFERENCE — the rest of this document
560
+ # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
409
561
 
410
562
  ## Getting Started
411
563
  1. list_projects() → pick project_id.
@@ -431,14 +583,15 @@ Mental translations:
431
583
  ## Build Backend (git-first workflow)
432
584
  Endpoints live in ./dypai/ — there is NO create_endpoint / update_endpoint / add_node tool.
433
585
 
434
- 1. Tables: \`execute_sql\` for DDL. \`schema.sql\` auto-refreshes.
435
- 2. Endpoints:
586
+ 1. Tables: \`execute_sql\` for DDL. \`schema.sql\` auto-refreshes. **DDL is the only backend mutation that bypasses drafts — it hits live immediately.**
587
+ 2. Endpoints / realtime / webhooks / crons:
436
588
  - Edit/Write YAMLs in \`dypai/endpoints/<group>/<name>.yaml\`.
437
589
  - Long SQL / prompts / code go in \`dypai/sql/\`, \`dypai/prompts/\`, \`dypai/code/\` (referenced via \`query_file\`, \`system_prompt_file\`, \`code_file\`).
438
- - \`dypai_validate\` → catches placeholder / schema / credential / node-param errors.
590
+ - \`dypai_validate\` → catches placeholder / schema / credential / node-param errors. Run before push (push also runs it as pre-flight).
439
591
  - \`dypai_diff\` → preview changes (read-only).
440
- - \`dypai_push\` → apply.
441
- 3. \`dypai_test_endpoint\` to run the LOCAL YAML against the engine. Iterate edit test → push.
592
+ - \`dypai_push\` → **stages your edits as drafts on the platform**. This is the "save" step. NOT a publish. Run after every meaningful change set, not just at the end of a session — until you push, neither the engine nor the local frontend (which talks to the draft overlay) can see your edits.
593
+ - \`manage_drafts(operation:'publish', confirm:true)\` ONLY when the user signs off. Promotes ALL pending drafts atomically to live.
594
+ 3. \`dypai_test_endpoint\` to execute an endpoint against the engine. Three sources via \`mode\`: \`local\` (default — your YAML on disk, BEFORE push, fastest while iterating), \`draft\` (the version staged by \`dypai_push\` but not yet published — what live will look like after \`manage_drafts(publish)\`), \`live\` (currently deployed). Canonical loop: edit → \`dypai_validate\` → \`dypai_test_endpoint(mode:'local')\` → \`dypai_push\` → \`dypai_test_endpoint(mode:'draft')\` (or user tests local UI on the draft overlay) → \`manage_drafts(publish, confirm:true)\`.
442
595
 
443
596
  ## Picking nodes — catalog-first
444
597
 
@@ -494,7 +647,14 @@ Any endpoint can be flagged \`tool: true\` to be callable by \`agent\` nodes. Ho
494
647
 
495
648
  ## Testing & Debugging
496
649
 
497
- - **\`dypai_test_endpoint\`** single endpoint, LOCAL YAML (unpushed edits). Pass \`as_user\` UUID for jwt endpoints. Trace modes: \`smart\` (default), \`full\`, \`minimal\`. Iterate edit → test → push.
650
+ Four ways to validate a backend change, in increasing fidelity:
651
+
652
+ 1. **\`dypai_test_endpoint(mode:'local')\`** — runs YOUR YAML on disk against the engine BEFORE \`dypai_push\`. Fastest feedback loop while iterating on a single endpoint. Pass \`as_user\` UUID for jwt endpoints.
653
+ 2. **\`dypai_test_endpoint(mode:'draft')\`** — runs the version staged by \`dypai_push\` (i.e. exactly what \`manage_drafts(publish)\` will promote). Use as the final isolated check before publishing.
654
+ 3. **End-to-end from the local frontend** (Layer 2.5 draft overlay) — after \`dypai_push\`, the user's local frontend already calls \`https://dev-<project_id>.dypai.dev\` (set by \`manage_frontend(sync)\`), which serves drafts on top of live. So real UI flows hit the draft transparently. No setup, no env-flip, no headers. Read-only impact on prod data — drafts share the SAME database as live.
655
+ 4. **\`dypai_test_endpoint(mode:'live')\`** — repro a bug that's already in production.
656
+
657
+ Other tools:
498
658
  - **\`dypai_test\`** — YAML regression suites at \`dypai/tests/<name>.test.yaml\` with assertions (equals, matches, contains, type, exists, gte, lte) + setup_sql / teardown_sql.
499
659
  - **\`dypai_validate\`** — static linting (placeholders, tables, columns, node params, credentials). Run before EVERY push.
500
660
  - **Prod debugging**: \`get_recent_workflow_activity(only_errors=true)\` surfaces recent failures.
@@ -577,7 +737,8 @@ query_file: /absolute/path/sql/get-orders.sql
577
737
  - \`/api/v0/<endpoint_name>\` — HTTP endpoints
578
738
  - \`/api/v0/webhooks/<endpoint_name>\` — webhook endpoints (different path prefix)
579
739
  - \`/public/<path>\` — media served from the storage bucket (auto-populated on deploy; see "Frontend deploy")
580
- - \`https://<project_id>.dypai.app\` — the engine base URL (what the SDK points to)
740
+ - \`https://<project_id>.dypai.dev\` — engine base URL serving LIVE traffic (what the deployed frontend's SDK points to)
741
+ - \`https://dev-<project_id>.dypai.dev\` — engine base URL serving the **draft overlay** (Layer 2.5): drafts staged via \`dypai_push\` are served here, falling back to live for anything not drafted. This is what the SDK in \`.env.local\` points to during local frontend development, so a local UI can validate backend drafts end-to-end BEFORE \`manage_drafts(publish)\`.
581
742
 
582
743
  ## Endpoint YAML skeleton (top-level fields)
583
744
 
@@ -785,6 +946,8 @@ Pre-configured at \`src/lib/dypai.ts\`. Every method returns \`{ data, error }\`
785
946
  - **Credentials not created** → workflow fails with "credential not found". Check \`get_app_credentials\` before referencing one in a node. Create in dashboard (not via MCP yet).
786
947
  - **Binary files in \`dypai/code/\`** → only text code files here. Binary assets go to the frontend \`public/\` or to a bucket.
787
948
  - **\`dypai_push\` without \`dypai_validate\`** → pushing a broken workflow. Always validate first.
949
+ - **Editing a YAML and forgetting \`dypai_push\`** → the user reloads their local frontend (which points at the draft overlay \`dev-<project_id>.dypai.dev\`) and sees the OLD behavior because your edit only exists on YOUR DISK. Symptom: "I tested it locally and nothing changed." First check: did you push? Push after every meaningful change set, not at the end.
950
+ - **Treating \`dypai_push\` as a deploy** → It's a "save as draft", not a publish. Live traffic is unaffected until \`manage_drafts(publish, confirm:true)\`. Don't ask the user "ready to ship?" before push — push freely, only ask before publish.
788
951
  - **Frontend dev server + remote media** → media files are auto-uploaded to the storage bucket on deploy but \`vite dev\` doesn't proxy to it. Run \`manage_frontend(sync)\` first to pull media to disk.
789
952
 
790
953
  ## Frontend
@@ -797,19 +960,23 @@ SDK is pre-configured at \`src/lib/dypai.ts\`. Import \`dypai\` from there. Ever
797
960
  - **Realtime hooks**: \`useRealtime\`, \`useChannel\`, \`useChannelMessages\` (see Realtime section)
798
961
  - **Rule**: NEVER \`fetch()\` directly — always through the SDK
799
962
 
800
- **\`.env\` required** — \`.env\` is gitignored so \`manage_frontend(sync)\` does NOT fetch it. If \`env_file_missing: true\` in the sync response, create it:
963
+ **\`.env.local\` is auto-managed by \`manage_frontend(sync)\`** when missing, sync writes it for you pointing at the **draft overlay** (\`https://dev-<project_id>.dypai.dev\`) so your local frontend transparently consumes backend drafts. The variable name follows your framework: \`VITE_DYPAI_URL\` for Vite, \`NEXT_PUBLIC_DYPAI_URL\` for Next.js. **Do not overwrite a user-authored \`.env.local\`** — sync respects an existing file. Only create it manually if \`env_file_missing: true\` in the sync response AND you have a reason to deviate.
964
+
801
965
  \`\`\`bash
802
- # Vite
803
- VITE_DYPAI_URL=https://<project_id>.dypai.app
804
- VITE_PROJECT_ID=<project_id>
805
- # Next.js (NEXT_PUBLIC_ prefix is required)
806
- NEXT_PUBLIC_DYPAI_URL=https://<project_id>.dypai.app
966
+ # What sync writes (Vite)
967
+ VITE_DYPAI_URL=https://dev-<project_id>.dypai.dev
968
+
969
+ # What sync writes (Next.js)
970
+ NEXT_PUBLIC_DYPAI_URL=https://dev-<project_id>.dypai.dev
807
971
  \`\`\`
808
- Without \`.env\`, all SDK calls fail. It's always the missing env var, not the code.
972
+
973
+ For **production** the deployed frontend bundle automatically receives the live URL (\`https://<project_id>.dypai.dev\`, no \`dev-\` prefix) injected as a build-time env var by \`manage_frontend(deploy)\` — the user never has to flip URLs by hand. Without an \`.env.local\` the local SDK has no engine to call → every API call fails. It's always the env var, not the code.
809
974
 
810
975
  ## Frontend deploy
811
976
 
812
- \`manage_frontend(operation: deploy, sourceDirectory, project_id)\` → returns immediately with build_status=queued. Poll \`operation: build_status\` every ~5s until "success" or "failure". On failure: \`operation: list_deployments\` \`operation: logs\` with deployment_id. Supports Vite, React, Next.js, Astro, SvelteKit, Nuxt, Remix, Angular, CRA (auto-detected).
977
+ \`manage_frontend(operation: deploy, sourceDirectory, project_id, confirm:true)\` → ships the frontend bundle. **Replaces the live site IMMEDIATELY** no draft stage, no automatic rollback. \`confirm:true\` is REQUIRED; without it the tool returns a \`confirmation_required\` hint (and warns if backend drafts are still pending you should publish those FIRST so the frontend doesn't call non-existent endpoints).
978
+
979
+ Returns immediately with build_status=queued. Poll \`operation: build_status\` every ~5s until "success" or "failure". On failure: \`operation: list_deployments\` → \`operation: logs\` with deployment_id. Supports Vite, React, Next.js, Astro, SvelteKit, Nuxt, Remix, Angular, CRA (auto-detected).
813
980
 
814
981
  The deploy is delta by default: only files changed since last deploy are sent. Large media (>25MB) surfaces in \`assets_requiring_action\` with ready-to-exec \`manage_storage(upload_file)\` calls.
815
982
 
@@ -822,6 +989,7 @@ The deploy is delta by default: only files changed since last deploy are sent. L
822
989
  - \`manage_users\` / \`manage_roles\` — app-level RBAC (users via better-auth).
823
990
  - \`manage_schedules\` / \`manage_webhooks\` — pause/resume/history. To change the DEFINITION, edit the endpoint YAML and push.
824
991
  - \`manage_storage\` — buckets + objects. \`upload_file\` reads local path, signs URL, PUTs direct to the storage bucket, registers. Max 100MB/file. → \`search_docs("file storage")\`.
992
+ - \`manage_drafts\` — universal draft-and-publish workflow. \`dypai_push\` always saves changes as drafts; use \`list\` to show the user what's pending, \`publish\` (confirm:true) to apply them atomically to live, \`discard\` to throw them away. Test pending drafts with \`dypai_test_endpoint(mode:'draft')\` before publishing.
825
993
 
826
994
  ## Deep docs — search_docs topic map
827
995
 
@@ -489,13 +489,19 @@ export async function deployFromSource({ sourceDirectory, project_id, force = fa
489
489
  const assetsAction = buildAssetsRequiringAction(skipped, mediaManifest, project_id)
490
490
  const hasUnresolvedAssets = assetsAction && assetsAction.count > 0
491
491
 
492
+ const quota = result.build_quota || null
493
+ const quotaWarning = buildQuotaWarning(quota)
494
+
492
495
  const baseMessage =
493
496
  `Deploy accepted — ${allFiles.length} files (${formatBytes(total)}). ` +
494
497
  `Build running (${label}, ~20-60s). Poll manage_frontend({operation:"build_status"}) every ~5s.`
495
498
 
496
499
  let message = baseMessage
500
+ if (quotaWarning) {
501
+ message = `${quotaWarning}\n\n${message}`
502
+ }
497
503
  if (hasUnresolvedAssets) {
498
- message = `${assetsAction.message}\n\n${baseMessage}`
504
+ message = `${assetsAction.message}\n\n${message}`
499
505
  }
500
506
 
501
507
  return {
@@ -513,6 +519,8 @@ export async function deployFromSource({ sourceDirectory, project_id, force = fa
513
519
  ...(hasUnresolvedAssets ? { assets_requiring_action: assetsAction } : {}),
514
520
  ...(assetsAction?.all_synced ? { assets_synced: assetsAction.already_in_bucket } : {}),
515
521
 
522
+ ...(quota ? { build_quota: quota } : {}),
523
+
516
524
  next_step: hasUnresolvedAssets
517
525
  ? {
518
526
  action: "resolve_pending_assets_then_poll_build",
@@ -536,6 +544,46 @@ export async function deployFromSource({ sourceDirectory, project_id, force = fa
536
544
  message,
537
545
  }
538
546
  } catch (e) {
547
+ if (e.statusCode === 429 && e.detail && e.detail.error === "build_quota_exceeded") {
548
+ return {
549
+ success: false,
550
+ error: e.detail.message || "Monthly build minutes limit reached.",
551
+ error_code: "build_quota_exceeded",
552
+ build_quota: {
553
+ minutes_used: e.detail.minutes_used,
554
+ minutes_limit: e.detail.minutes_limit,
555
+ resets_at: e.detail.resets_at,
556
+ },
557
+ upgrade_url: e.detail.upgrade_url,
558
+ advice: "Do not retry this deploy. Inform the user that the monthly build minute quota is exhausted and suggest upgrading the project plan.",
559
+ }
560
+ }
561
+ if (e.statusCode === 429 && e.detail && e.detail.error === "concurrent_builds_limit") {
562
+ return {
563
+ success: false,
564
+ error: e.detail.message || "Concurrent build limit reached.",
565
+ error_code: "concurrent_builds_limit",
566
+ concurrent_active: e.detail.concurrent_active,
567
+ concurrent_limit: e.detail.concurrent_limit,
568
+ advice: "Wait for the active build to finish (poll manage_frontend({operation:'build_status'})) before re-deploying.",
569
+ }
570
+ }
539
571
  return { error: `Deploy failed: ${e.message}` }
540
572
  }
541
573
  }
574
+
575
+ /**
576
+ * Human-readable warning when the project is near its monthly build quota.
577
+ * Returns null when plan is unlimited or usage is below 80%.
578
+ */
579
+ function buildQuotaWarning(quota) {
580
+ if (!quota) return null
581
+ const { minutes_used, minutes_limit, minutes_remaining } = quota
582
+ if (minutes_limit == null) return null
583
+ const pct = minutes_limit > 0 ? (minutes_used / minutes_limit) : 0
584
+ if (pct < 0.8) return null
585
+ if (pct >= 1) {
586
+ return `⚠️ Monthly build minutes exhausted (${minutes_used}/${minutes_limit} min). This deploy used your last minutes — upgrade your plan to deploy again.`
587
+ }
588
+ return `⚠️ Build quota at ${Math.round(pct * 100)}% (${minutes_used}/${minutes_limit} min, ${minutes_remaining} remaining). Consider upgrading soon.`
589
+ }
@@ -14,11 +14,13 @@
14
14
  import { api } from "../api.js"
15
15
  import { deployFromSource } from "./deploy.js"
16
16
  import { syncFromRemote } from "./sync.js"
17
+ import { proxyToolCall } from "./proxy.js"
17
18
 
18
19
  export const manageFrontendTool = {
19
20
  name: "manage_frontend",
20
21
  description:
21
- "Manage the project's frontend: download the source code to disk AND the deploy lifecycle.\n\n" +
22
+ "FRONTEND ONLY — manages the project's static frontend (HTML/CSS/JS bundle). For BACKEND endpoints/workflows use dypai_push + manage_drafts; never use this tool for backend work.\n\n" +
23
+ "Two phases: download source to local disk (`sync`), then ship changes back (`deploy`).\n\n" +
22
24
  "Use `sync` FIRST whenever you start working on a project whose frontend code isn't already on this machine — " +
23
25
  "without it you have no React/Vite source to read or edit. Call `deploy` to ship your changes.\n\n" +
24
26
  "Operations:\n" +
@@ -28,19 +30,25 @@ export const manageFrontendTool = {
28
30
  "Writes only; does NOT delete local files that were removed upstream — you may have stale files after sync (call them out to the user). " +
29
31
  "By default refuses to overwrite a directory that already has a package.json — pass overwrite:true to allow it (local-only files like .env, node_modules, .vscode are always preserved). " +
30
32
  "AFTER SYNC: .env is gitignored so it's NOT included in the download. If the target directory has no .env, the response sets `env_file_missing: true` and adds a `next_steps` line with the exact VITE_DYPAI_URL / NEXT_PUBLIC_DYPAI_URL value to write. Follow it — without .env the SDK can't reach the engine.\n" +
31
- " - deploy: Upload source files from a local directory and queue a build. Returns immediately with build_status=\"queued\" poll with `build_status` until \"success\" or \"failure\".\n" +
33
+ " - deploy: Upload source files from a local directory and queue a build. **DESTRUCTIVE: replaces the LIVE site immediately, no draft stage, no rollback button.** " +
34
+ "Requires `confirm: true` — without it the tool returns a confirmation_required hint instead of deploying. " +
35
+ "If backend drafts are pending, the hint includes a warning to publish backend FIRST (otherwise the new frontend may call endpoints that don't exist yet). " +
36
+ "Returns immediately with build_status=\"queued\" — poll with `build_status` until \"success\" or \"failure\". " +
37
+ "The response includes `build_quota` with remaining monthly build minutes. If minutes_remaining is low, tell the user. If 0, DO NOT retry — suggest upgrading the plan.\n" +
32
38
  " - status: Current live deploy info (URL, last deploy time, size).\n" +
33
39
  " - build_status: Progress of the current/latest build (queued/building/success/failure + stage + %).\n" +
40
+ " - usage: Full frontend usage snapshot including build_quota (minutes used/limit/remaining, deploy counts, resets_at). Call BEFORE deploy if unsure how much quota is left.\n" +
34
41
  " - list_deployments: Recent deploy history (status, commit, duration, URL).\n" +
35
42
  " - logs: Build logs for a specific deployment (needs deployment_id from list_deployments).\n\n" +
36
- "Related: `dypai_pull` brings BACKEND state (YAML endpoints, SQL, prompts). The two are independent — run both when starting fresh on a full-stack project.",
43
+ "Related: `dypai_pull` brings BACKEND state (YAML endpoints, SQL, prompts). The two are independent — run both when starting fresh on a full-stack project.\n\n" +
44
+ "Order rule when both backend AND frontend changed: 1) dypai_push (saves backend as draft) → 2) manage_drafts(publish, confirm:true) → 3) manage_frontend(deploy, confirm:true). Inverting steps 2 and 3 may serve a frontend that calls non-existent endpoints.",
37
45
 
38
46
  inputSchema: {
39
47
  type: "object",
40
48
  properties: {
41
49
  operation: {
42
50
  type: "string",
43
- enum: ["deploy", "sync", "status", "build_status", "list_deployments", "logs"],
51
+ enum: ["deploy", "sync", "status", "build_status", "usage", "list_deployments", "logs"],
44
52
  description: "Which action to run.",
45
53
  },
46
54
  project_id: {
@@ -65,6 +73,11 @@ export const manageFrontendTool = {
65
73
  description: "deploy only. Bypass the delta manifest and re-send ALL files (full deploy). Use when the previous deploy's remote build FAILED — the manifest says 'synced' but the remote never built, so a normal delta incorrectly reports no_changes. Default: false.",
66
74
  default: false,
67
75
  },
76
+ confirm: {
77
+ type: "boolean",
78
+ description: "Required `true` for `deploy`. Without it the tool returns a confirmation_required hint (with a ready-to-call next_call) instead of replacing the live site. The agent MUST get explicit user approval before passing confirm:true.",
79
+ default: false,
80
+ },
68
81
  deployment_id: {
69
82
  type: "string",
70
83
  description: "logs only. Deployment UUID obtained from list_deployments.",
@@ -77,7 +90,7 @@ export const manageFrontendTool = {
77
90
  required: ["operation"],
78
91
  },
79
92
 
80
- async execute({ operation, project_id, sourceDirectory, targetDirectory, overwrite, force, deployment_id, limit } = {}) {
93
+ async execute({ operation, project_id, sourceDirectory, targetDirectory, overwrite, force, confirm, deployment_id, limit } = {}) {
81
94
  if (!operation) {
82
95
  return { success: false, error: "operation is required (deploy | sync | status | build_status | list_deployments | logs)." }
83
96
  }
@@ -91,6 +104,43 @@ export const manageFrontendTool = {
91
104
  if (!sourceDirectory) {
92
105
  return { success: false, error: "operation 'deploy' requires 'sourceDirectory' (absolute path to your frontend project root)." }
93
106
  }
107
+ // Defense-in-depth gate: deploy replaces the live site immediately
108
+ // with no rollback. Without explicit confirm we return a structured
109
+ // hint (with ready-to-execute next_call). We also surface any
110
+ // pending backend drafts as warnings — the agent should ALWAYS
111
+ // publish backend drafts before deploying frontend, otherwise the
112
+ // new frontend may call endpoints that don't exist yet on live.
113
+ if (confirm !== true) {
114
+ const warnings = []
115
+ try {
116
+ const draftsResult = await proxyToolCall("manage_drafts", { project_id, operation: "list" })
117
+ const draftsTotal = draftsResult?.total || 0
118
+ if (draftsTotal > 0) {
119
+ warnings.push(
120
+ `${draftsTotal} backend draft(s) pending. Publish them BEFORE deploying the frontend with manage_drafts(operation:'publish', confirm:true) — otherwise the new frontend may call endpoints that don't exist on live yet.`,
121
+ )
122
+ }
123
+ } catch {
124
+ // Soft-fail — drafts check is advisory, not gating.
125
+ }
126
+ return {
127
+ confirmation_required: true,
128
+ summary: `About to replace the LIVE frontend at this project's public URL with the contents of '${sourceDirectory}'. This is IMMEDIATE and there is NO automatic rollback.`,
129
+ warnings: warnings.length > 0 ? warnings : undefined,
130
+ next_call: {
131
+ tool: "manage_frontend",
132
+ operation: "deploy",
133
+ project_id,
134
+ sourceDirectory,
135
+ ...(force ? { force: true } : {}),
136
+ confirm: true,
137
+ },
138
+ hint:
139
+ "Summarize the change to the user (what visual/functional changes are about to go live, " +
140
+ "and any pending backend drafts) and wait for explicit user approval. Then re-call this " +
141
+ "tool with confirm:true.",
142
+ }
143
+ }
94
144
  return await deployFromSource({ sourceDirectory, project_id, force: !!force })
95
145
 
96
146
  case "sync":
@@ -105,6 +155,9 @@ export const manageFrontendTool = {
105
155
  case "build_status":
106
156
  return await api.get(`/api/engine/${project_id}/frontend/build-status`)
107
157
 
158
+ case "usage":
159
+ return await api.get(`/api/engine/${project_id}/frontend/usage`)
160
+
108
161
  case "list_deployments":
109
162
  return await api.get(`/api/engine/${project_id}/frontend/deployments?limit=${limit || 10}`)
110
163
 
@@ -115,7 +168,7 @@ export const manageFrontendTool = {
115
168
  return await api.get(`/api/engine/${project_id}/frontend/deployments/${deployment_id}/logs`)
116
169
 
117
170
  default:
118
- return { success: false, error: `Unknown operation '${operation}'. Use deploy | sync | status | build_status | list_deployments | logs.` }
171
+ return { success: false, error: `Unknown operation '${operation}'. Use deploy | sync | status | build_status | usage | list_deployments | logs.` }
119
172
  }
120
173
  } catch (e) {
121
174
  return { success: false, error: e.message, operation }
@@ -47,9 +47,13 @@ Or use "blank" for an empty starter project.`,
47
47
  return { error: `Directory already has a package.json. Pick an empty directory or a new name.` }
48
48
  }
49
49
 
50
- // Production engine URL: https://<project-id>.dypai.app (NOT .dypai.dev that's the dev tier).
50
+ // Engine base URL — `<project_id>.dypai.dev` serves LIVE traffic; the
51
+ // local-development URL `dev-<project_id>.dypai.dev` (Layer 2.5 draft
52
+ // overlay) is written into `.env.local` separately by `sync` once the
53
+ // user has the frontend on disk. Scaffold writes the production URL
54
+ // because at scaffold-time we expect the user to deploy soon.
51
55
  // Override with DYPAI_ENGINE_BASE for self-hosted / staging setups.
52
- const engineBase = process.env.DYPAI_ENGINE_BASE || "dypai.app"
56
+ const engineBase = process.env.DYPAI_ENGINE_BASE || "dypai.dev"
53
57
  const engineUrl = `https://${project_id}.${engineBase}`
54
58
 
55
59
  // Try to download template from API