@dypai-ai/mcp 1.4.3 → 1.4.6

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.6",
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
@@ -54,6 +54,7 @@ import { dypaiPullTool, dypaiDiffTool, dypaiPushTool, dypaiValidateTool, dypaiTe
54
54
  import { proxyToolCall } from "./tools/proxy.js"
55
55
  import { enrichSuccess, enrichError } from "./tools/enrich.js"
56
56
  import { maybeRefreshSchemaAfterExecuteSql } from "./tools/sql-side-effects.js"
57
+ import { maybeOffloadSearchLogs } from "./tools/search-logs-offload.js"
57
58
  import { withProjectContext, invalidateProjectContext } from "./tools/project-context.js"
58
59
  // summarizeDypaiTraceResponse (from ./tools/trace-summarize.js) is kept on
59
60
  // disk for when dypai_trace is re-enabled, but not imported here.
@@ -110,7 +111,7 @@ const REMOTE_TOOLS = [
110
111
  // Note: `get_app_tables` is intentionally NOT exposed — dypai/schema.sql already
111
112
  // caches table info locally (auto-refreshed on DDL). For ad-hoc introspection,
112
113
  // 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"] } },
114
+ { 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
115
 
115
116
  // ── API Endpoints ─────────────────────────────────────────────────────────
116
117
  // Full CRUD + exploration goes through the git-first flow:
@@ -230,6 +231,56 @@ Operations:
230
231
  },
231
232
  },
232
233
 
234
+ // ── Drafts (production-only staging area) ────────────────────────────────
235
+ // manage_drafts wraps the cloud SDK that talks to /api/engine/{id}/endpoints/
236
+ // Single tool, three ops (list / publish / discard) — same shape as
237
+ // manage_users / manage_roles / manage_storage so the agent's mental
238
+ // model stays uniform. Every project starts in draft-publish mode by
239
+ // default: dypai_push stages mutations as drafts and the user (or
240
+ // agent on their behalf) publishes when ready.
241
+ {
242
+ name: "manage_drafts",
243
+ description: `BACKEND ONLY — inspect, publish, or discard pending backend changes (drafts).
244
+
245
+ Mental model: every change made by \`dypai_push\` (endpoints, webhooks,
246
+ crons, realtime policies) is staged as a DRAFT first. Drafts do NOT
247
+ affect live traffic; they only show up in the live config once the user
248
+ publishes them. This is the universal default — works the same on every
249
+ project, no environment flags to worry about. The frontend is a separate
250
+ stack — use \`manage_frontend(deploy)\` to publish frontend changes.
251
+
252
+ Operations:
253
+ - list: Return pending drafts grouped by resource type. Read-only,
254
+ no confirmation. Run this BEFORE publish/discard so the user
255
+ sees exactly what will ship or be thrown away.
256
+ - publish: Atomically apply ALL pending drafts to live (creates, updates,
257
+ deletions) and invalidate affected caches. Requires confirm:true.
258
+ - discard: Drop pending drafts without applying. By default discards
259
+ every draft; pass resource_names:[...] to scope the discard
260
+ to specific endpoints. Requires confirm:true.
261
+
262
+ Typical flow:
263
+ 1. dypai_push → changes saved as drafts
264
+ 2. manage_drafts(operation:'list') → show user what's pending
265
+ 3. (optional) test the draft from local dev / preview
266
+ 4. manage_drafts(operation:'publish', confirm:true) → make it live
267
+ OR manage_drafts(operation:'discard', confirm:true) → throw it away`,
268
+ inputSchema: {
269
+ type: "object",
270
+ properties: {
271
+ project_id: { type: "string", description: "Project UUID. Required for user tokens; auto-detected for project tokens." },
272
+ operation: { type: "string", enum: ["list", "publish", "discard"], description: "Operation to perform." },
273
+ confirm: { type: "boolean", description: "Required true for publish and discard. Without it the tool returns a confirmation_required hint instead of mutating.", default: false },
274
+ 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." },
275
+ },
276
+ required: ["operation"],
277
+ allOf: [
278
+ { if: { properties: { operation: { const: "publish" } }, required: ["operation"] }, then: { required: ["confirm"] } },
279
+ { if: { properties: { operation: { const: "discard" } }, required: ["operation"] }, then: { required: ["confirm"] } },
280
+ ],
281
+ },
282
+ },
283
+
233
284
  // ── Storage ───────────────────────────────────────────────────────────────
234
285
  // manage_storage covers BOTH bucket-level and object-level operations.
235
286
  // The remote also accepts the legacy name `list_buckets` (alias) so older
@@ -397,6 +448,26 @@ endpoint YAML and \`dypai_push\`. This tool does NOT modify the definition.`,
397
448
  },
398
449
  },
399
450
 
451
+ // ── Observability ─────────────────────────────────────────────────────────
452
+ {
453
+ name: "search_logs",
454
+ description: "Search recent errors and warnings for the current project. ALWAYS call this FIRST when the user reports any error, bug, or 'this isn't working' — don't guess from the code; check what actually broke. Returns a unified, time-ordered list mixing failed workflow executions and warn/error log lines from the engine. Defaults to the last 24h. Data retention: 7 days.\n\nWorkflow:\n 1) Call with no args (or just `since:'1h'`) → see recent failures.\n 2) Pick the relevant entry → call again with `endpoint` + tighter `query` to narrow down.\n 3) For the full step-by-step debug trace of a specific failure, set `include_trace:true` (response is much larger; you'll likely get a `file_path` to read the full JSON from disk).\n\nUse `environment:'live'` when investigating a production user complaint (excludes draft test runs). Use `environment:'draft'` when the user says 'I just tested X locally and it failed' (their local UI hits the draft overlay).",
455
+ inputSchema: {
456
+ type: "object",
457
+ properties: {
458
+ project_id: { type: "string", description: "Project UUID. Auto-detected for project tokens." },
459
+ query: { type: "string", description: "Optional substring to match (case-insensitive) in error messages and log lines. e.g. 'timeout', 'OpenAI', 'permission denied'." },
460
+ endpoint: { type: "string", description: "Optional endpoint name filter (e.g. 'create-order')." },
461
+ since: { type: "string", default: "24h", description: "Time window: relative ('15m', '1h', '24h', '7d') or ISO 8601 timestamp. Default 24h. Hard cap: 7d (retention)." },
462
+ level: { type: "string", enum: ["error", "warn", "all"], default: "all", description: "Filter by severity. 'error' includes failed/timeout executions + error logs. 'warn' is warning logs. 'all' (default) returns both." },
463
+ environment: { type: "string", enum: ["live", "draft", "all"], default: "all", description: "live = production traffic only (excludes draft overlay test runs). draft = only requests through dev-<project_id>.dypai.dev. all = both. Use 'live' for real user bug reports." },
464
+ limit: { type: "integer", default: 50, minimum: 1, maximum: 200, description: "Max items to return. Default 50, max 200." },
465
+ include_trace: { type: "boolean", default: false, description: "Attach the full step-by-step debug trace per failed execution. Verbose — combine with `query`/`endpoint` filters and a low `limit`. If the response gets large, the local proxy writes it to disk and returns a file_path you can Read." }
466
+ },
467
+ required: []
468
+ }
469
+ },
470
+
400
471
  // ── Knowledge ─────────────────────────────────────────────────────────────
401
472
  { name: "search_docs", description: "Search DYPAI documentation. Use this when unsure about SDK usage, auth patterns, workflow nodes, or platform features. Returns relevant documentation chunks.", inputSchema: { type: "object", properties: { query: { type: "string", description: "What you want to learn about" } }, required: ["query"] } },
402
473
  { name: "search_workflow_templates", description: "Search workflow templates by description. Returns ready-to-use workflow code for common patterns: CRUD operations, payment gateways, email sending, AI chatbots, data pipelines, etc.", inputSchema: { type: "object", properties: { query: { type: "string", description: "What the workflow should do (e.g. 'send email', 'stripe payment')" }, category: { type: "string", description: "Optional: AI, Database, Payments, Communication, Logic, Storage" } }, required: ["query"] } },
@@ -405,7 +476,153 @@ endpoint YAML and \`dypai_push\`. This tool does NOT modify the definition.`,
405
476
 
406
477
  // ── Server Instructions ──────────────────────────────────────────────────────
407
478
 
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).
479
+ 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).
480
+
481
+ # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
482
+ # QUICK START — read this section even if you skip everything else
483
+ # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
484
+
485
+ ## What you ship — two completely separate stacks
486
+
487
+ | Stack | What it is | Where it lives | How you change it | How you publish |
488
+ |---|---|---|---|---|
489
+ | **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)\` |
490
+ | **FRONTEND** | React/Vite/Next bundle (UI, SDK calls) | \`src/\`, \`public/\`, \`package.json\` | Edit React code | \`manage_frontend(deploy, confirm:true)\` |
491
+
492
+ Two things to internalize:
493
+ 1. The two stacks are SHIPPED INDEPENDENTLY. Editing backend never touches frontend, and vice versa.
494
+ 2. Backend has a draft stage; frontend does NOT. Both publish operations require \`confirm:true\`.
495
+
496
+ ## Backend lifecycle — "save" = \`dypai_push\` (this is the rule that trips up new agents)
497
+
498
+ 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:
499
+
500
+ \`\`\`
501
+ ┌────────────┐ edit YAML / SQL / md ┌────────────┐ dypai_push ┌────────────┐ manage_drafts(publish, confirm:true) ┌────────────┐
502
+ │ LIVE │ ──────────────────────► │ LOCAL DISK │ ────────────► │ DRAFT │ ──────────────────────────────────────► │ LIVE │
503
+ │ (engine) │ (no platform impact) │ (your edit)│ (stages it) │ (overlay) │ (atomic, all drafts at once) │ (engine) │
504
+ └────────────┘ └────────────┘ └────────────┘ └────────────┘
505
+
506
+ draft overlay │ served at https://dev-<project_id>.dypai.dev
507
+ │ (what the user's local frontend points to)
508
+ \`\`\`
509
+
510
+ Practical consequences — internalize these:
511
+ - **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).
512
+ - **\`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.
513
+ - **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?".
514
+ - **\`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.
515
+ - **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)\`.
516
+ - **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.
517
+
518
+ ## User intent → tool to call (decision table)
519
+
520
+ Use this BEFORE picking a tool. If unsure which row matches, ask the user.
521
+
522
+ | If the user asks to... | First call | Then |
523
+ |---|---|---|
524
+ | "Create a new project" | \`search_project_templates\` (find a starter) | \`create_project(template_slug: ...)\` |
525
+ | "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/\` |
526
+ | "Add/change a backend endpoint, table, cron, webhook, agent, integration" | Edit files in \`dypai/\` | \`dypai_validate\` → \`dypai_push\` |
527
+ | "Publish my backend changes" / "make it live" | \`manage_drafts(operation:'list')\` to show what's pending | \`manage_drafts(operation:'publish', confirm:true)\` |
528
+ | "Test an endpoint before publishing" | \`dypai_test_endpoint(mode:'local')\` (your edits) or \`(mode:'draft')\` (after push) | — |
529
+ | "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. | — |
530
+ | "Throw away my backend changes" | \`manage_drafts(operation:'discard', confirm:true)\` | — |
531
+ | "Change the UI / change colors / add a page" | Edit files in \`src/\` | \`manage_frontend(deploy, confirm:true)\` |
532
+ | "Publish the new UI" / "ship the frontend" | \`manage_frontend(deploy, confirm:true)\` | (deploy is the publish — there is no separate step) |
533
+ | "Roll back" | Backend: \`get_endpoint_versions\` then write old code back. Frontend: re-deploy older source. | — |
534
+ | "Upload a file / a CSV / seed data" | \`bulk_upsert\` (data) or \`manage_storage(upload_file)\` (binary) | — |
535
+ | "X is broken" / "I'm getting an error" / "this doesn't work" / "users are reporting Y" | \`search_logs\` FIRST (don't guess from the code) | If a specific failure is found → \`search_logs(include_trace:true, query:'...')\` for the full step-by-step trace |
536
+
537
+ ## Confirm rules — the ONLY operations that need \`confirm:true\`
538
+
539
+ There are exactly THREE write operations that mutate live state and require explicit user approval (return a \`confirmation_required\` hint without it):
540
+
541
+ 1. **\`manage_drafts(operation:'publish', confirm:true)\`** — promotes backend drafts to live.
542
+ 2. **\`manage_drafts(operation:'discard', confirm:true)\`** — throws away pending backend drafts.
543
+ 3. **\`manage_frontend(operation:'deploy', confirm:true)\`** — replaces the live frontend bundle.
544
+
545
+ 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.
546
+
547
+ 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\`.
548
+
549
+ ## End-to-end example — adding a feature that touches BOTH backend and frontend
550
+
551
+ User: "Add a /api/list-tasks endpoint that returns the current user's tasks, and show them on the dashboard."
552
+
553
+ \`\`\`
554
+ 1. dypai_pull(project_id) # materialize backend if not already on disk
555
+ 2. manage_frontend(operation:'sync', ...) # materialize frontend if not already on disk
556
+ 3. # Backend: create the endpoint
557
+ Write dypai/endpoints/list-tasks.yaml # trigger.http_api auth_mode:jwt + dypai_database query
558
+ 4. dypai_validate # catch typos before push
559
+ 5. dypai_push # stages as draft, NOT live yet
560
+ 6. dypai_test_endpoint(name:'list-tasks', mode:'draft', as_user:'<uuid>') # verify the staged version
561
+ 7. manage_drafts(operation:'list') # show pending changes to user
562
+ 8. # ASK USER: "Ready to publish list-tasks to live?"
563
+ 9. manage_drafts(operation:'publish', confirm:true) # backend now live ✅
564
+ 10. # Frontend: call the new endpoint from React
565
+ Edit src/pages/Dashboard.tsx # useEndpoint('list-tasks')
566
+ 11. # ASK USER: "Ready to deploy the dashboard UI?"
567
+ 12. manage_frontend(operation:'deploy', sourceDirectory, confirm:true) # frontend now live ✅
568
+ \`\`\`
569
+
570
+ **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.
571
+
572
+ ## Debugging user-reported errors — \`search_logs\` is your starting point
573
+
574
+ **Rule**: whenever the user says any of these — "X is broken", "this isn't working", "I'm getting an error", "users are reporting Y", "the page is white", "nothing happens when I click" — **call \`search_logs\` BEFORE reading any code**. The engine's logs are the ground truth; the code is your hypothesis. Trying to debug from the source first is how you waste 20 minutes solving the wrong problem.
575
+
576
+ ### The standard flow
577
+
578
+ \`\`\`
579
+ 1. search_logs({ since: "1h", level: "error" })
580
+ → Quick scan of recent failures. If empty, widen to "24h".
581
+
582
+ 2. # Did the user say "I just tested this in my local UI"?
583
+ # → add environment: "draft" (their UI hits the draft overlay)
584
+ # Did they say "production users are reporting..."?
585
+ # → add environment: "live" (excludes their own draft test runs)
586
+
587
+ 3. # Found the relevant entry? Narrow down:
588
+ search_logs({ endpoint: "create-order", query: "stripe", since: "1h" })
589
+
590
+ 4. # For the full step-by-step trace of one specific failure:
591
+ search_logs({
592
+ endpoint: "create-order",
593
+ query: "<a unique substring from the error message>",
594
+ include_trace: true,
595
+ limit: 5
596
+ })
597
+ → If the response is large the local proxy writes it to a temp file
598
+ and returns a \`file_path\`. Read that file with the Read tool ONLY
599
+ when you need fields beyond the inline summary.
600
+
601
+ 5. # Now you know exactly which node failed and why → fix the code.
602
+ \`\`\`
603
+
604
+ ### What \`search_logs\` returns
605
+
606
+ Each item has \`type\` (\`execution_failed\` | \`log\`), \`level\` (\`error\` | \`warn\`), \`time\`, \`endpoint\`, \`message\`, and \`environment\` (\`live\` | \`draft\` | null for legacy rows). Failed executions also include \`status\` (\`error\` | \`timeout\`) and \`duration_ms\`. With \`include_trace:true\` they also include \`trace\` — a per-node log of inputs, outputs, errors, and stacks.
607
+
608
+ ### Common pitfalls
609
+
610
+ - **Don't skip this and read code first.** The bug is almost never where you'd guess. Logs tell you exactly which node blew up and the exact error string.
611
+ - **Don't dump every error you see at the user.** Filter, summarize, then propose ONE fix.
612
+ - **\`environment\` matters.** A draft test failure is the user testing pending changes — fixing the draft is fine. A live failure is real users hitting production — fix urgently and follow up with backend publish.
613
+ - **Retention is 7 days.** If the user reports a bug from "last week", the data is likely gone. Tell them.
614
+
615
+ ## What you do NOT have to think about
616
+
617
+ - "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.
618
+ - "Create auth endpoints" — auth is built into the SDK. \`dypai.auth.signInWithPassword()\` works out of the box. NEVER write a login/signup workflow.
619
+ - "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.
620
+ - "Rate limiting / CORS / JWT verification" — handled by the engine.
621
+ - "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.
622
+
623
+ # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
624
+ # DEEP REFERENCE — the rest of this document
625
+ # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
409
626
 
410
627
  ## Getting Started
411
628
  1. list_projects() → pick project_id.
@@ -431,14 +648,15 @@ Mental translations:
431
648
  ## Build Backend (git-first workflow)
432
649
  Endpoints live in ./dypai/ — there is NO create_endpoint / update_endpoint / add_node tool.
433
650
 
434
- 1. Tables: \`execute_sql\` for DDL. \`schema.sql\` auto-refreshes.
435
- 2. Endpoints:
651
+ 1. Tables: \`execute_sql\` for DDL. \`schema.sql\` auto-refreshes. **DDL is the only backend mutation that bypasses drafts — it hits live immediately.**
652
+ 2. Endpoints / realtime / webhooks / crons:
436
653
  - Edit/Write YAMLs in \`dypai/endpoints/<group>/<name>.yaml\`.
437
654
  - 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.
655
+ - \`dypai_validate\` → catches placeholder / schema / credential / node-param errors. Run before push (push also runs it as pre-flight).
439
656
  - \`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.
657
+ - \`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.
658
+ - \`manage_drafts(operation:'publish', confirm:true)\` ONLY when the user signs off. Promotes ALL pending drafts atomically to live.
659
+ 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
660
 
443
661
  ## Picking nodes — catalog-first
444
662
 
@@ -494,10 +712,17 @@ Any endpoint can be flagged \`tool: true\` to be callable by \`agent\` nodes. Ho
494
712
 
495
713
  ## Testing & Debugging
496
714
 
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.
715
+ Four ways to validate a backend change, in increasing fidelity:
716
+
717
+ 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.
718
+ 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.
719
+ 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.
720
+ 4. **\`dypai_test_endpoint(mode:'live')\`** — repro a bug that's already in production.
721
+
722
+ Other tools:
498
723
  - **\`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
724
  - **\`dypai_validate\`** — static linting (placeholders, tables, columns, node params, credentials). Run before EVERY push.
500
- - **Prod debugging**: \`get_recent_workflow_activity(only_errors=true)\` surfaces recent failures.
725
+ - **Prod debugging**: \`search_logs\` is the entry point — see the "Debugging user-reported errors" section above. Returns failed executions + warn/error logs from the last 7 days; pass \`environment:'live'\` to exclude draft-overlay test runs and \`include_trace:true\` for the per-node failure trace.
501
726
 
502
727
  → Deep patterns: \`search_docs("testing endpoints")\` (test setup + assertions), \`search_docs("troubleshooting")\` (common failures + fixes).
503
728
 
@@ -577,7 +802,8 @@ query_file: /absolute/path/sql/get-orders.sql
577
802
  - \`/api/v0/<endpoint_name>\` — HTTP endpoints
578
803
  - \`/api/v0/webhooks/<endpoint_name>\` — webhook endpoints (different path prefix)
579
804
  - \`/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)
805
+ - \`https://<project_id>.dypai.dev\` — engine base URL serving LIVE traffic (what the deployed frontend's SDK points to)
806
+ - \`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
807
 
582
808
  ## Endpoint YAML skeleton (top-level fields)
583
809
 
@@ -757,7 +983,7 @@ Pre-configured at \`src/lib/dypai.ts\`. Every method returns \`{ data, error }\`
757
983
  - **JWT verification** — jwt auth_mode validates the session token automatically. \`\${current_user_id}\` is trusted.
758
984
  - **Rate limiting** — per-plan. Returns 429 automatically.
759
985
  - **CORS** — allowed origins per project (configured in dashboard).
760
- - **Request logging** — every execution in \`system.workflow_runs\` with duration, status, tokens (for agents). View via \`get_recent_workflow_activity\`.
986
+ - **Request logging** — every execution is recorded with duration, status, environment (live/draft), and (on failure) a per-node debug trace. Warn/error \`userLog\` lines are persisted alongside. Query both via \`search_logs\` (last 7 days).
761
987
  - **Input validation** — if the endpoint has \`input:\` schema, requests with invalid payloads are rejected with 400 + details BEFORE the workflow runs.
762
988
  - **SQL injection** — placeholders bind as Postgres params. Safe by construction.
763
989
  - **Secrets management** — credentials and env vars never appear in YAML or logs.
@@ -785,6 +1011,8 @@ Pre-configured at \`src/lib/dypai.ts\`. Every method returns \`{ data, error }\`
785
1011
  - **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
1012
  - **Binary files in \`dypai/code/\`** → only text code files here. Binary assets go to the frontend \`public/\` or to a bucket.
787
1013
  - **\`dypai_push\` without \`dypai_validate\`** → pushing a broken workflow. Always validate first.
1014
+ - **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.
1015
+ - **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
1016
  - **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
1017
 
790
1018
  ## Frontend
@@ -797,19 +1025,23 @@ SDK is pre-configured at \`src/lib/dypai.ts\`. Import \`dypai\` from there. Ever
797
1025
  - **Realtime hooks**: \`useRealtime\`, \`useChannel\`, \`useChannelMessages\` (see Realtime section)
798
1026
  - **Rule**: NEVER \`fetch()\` directly — always through the SDK
799
1027
 
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:
1028
+ **\`.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.
1029
+
801
1030
  \`\`\`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
1031
+ # What sync writes (Vite)
1032
+ VITE_DYPAI_URL=https://dev-<project_id>.dypai.dev
1033
+
1034
+ # What sync writes (Next.js)
1035
+ NEXT_PUBLIC_DYPAI_URL=https://dev-<project_id>.dypai.dev
807
1036
  \`\`\`
808
- Without \`.env\`, all SDK calls fail. It's always the missing env var, not the code.
1037
+
1038
+ 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
1039
 
810
1040
  ## Frontend deploy
811
1041
 
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).
1042
+ \`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).
1043
+
1044
+ 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
1045
 
814
1046
  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
1047
 
@@ -822,6 +1054,7 @@ The deploy is delta by default: only files changed since last deploy are sent. L
822
1054
  - \`manage_users\` / \`manage_roles\` — app-level RBAC (users via better-auth).
823
1055
  - \`manage_schedules\` / \`manage_webhooks\` — pause/resume/history. To change the DEFINITION, edit the endpoint YAML and push.
824
1056
  - \`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")\`.
1057
+ - \`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
1058
 
826
1059
  ## Deep docs — search_docs topic map
827
1060
 
@@ -941,6 +1174,14 @@ async function handleRequest(msg) {
941
1174
  }
942
1175
  }
943
1176
 
1177
+ // search_logs can return huge payloads when include_trace=true.
1178
+ // Offload to a temp file when the serialized response > 60 KB so
1179
+ // the agent's context stays clean — it gets a summary + file path
1180
+ // and only Reads the file when it actually needs the detail.
1181
+ if (name === "search_logs") {
1182
+ result = maybeOffloadSearchLogs(result)
1183
+ }
1184
+
944
1185
  // Note: test_workflow is no longer agent-facing (wrapped by
945
1186
  // dypai_test_endpoint). dypai_trace is temporarily hidden until
946
1187
  // the engine captures debug traces for real production executions.
@@ -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
+ }