@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 +1 -1
- package/src/api.js +14 -2
- package/src/auto-update.js +44 -1
- package/src/index.js +260 -19
- package/src/tools/deploy.js +49 -1
- package/src/tools/frontend.js +59 -6
- package/src/tools/scaffold.js +6 -2
- package/src/tools/search-logs-offload.js +151 -0
- package/src/tools/sync/diff.js +88 -7
- package/src/tools/sync/pull.js +75 -8
- package/src/tools/sync/push.js +129 -96
- package/src/tools/sync/test-endpoint.js +217 -73
- package/src/tools/sync/validate.js +415 -48
- package/src/tools/sync.js +85 -13
- package/src/tools/status.js +0 -94
package/package.json
CHANGED
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
|
-
|
|
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)
|
package/src/auto-update.js
CHANGED
|
@@ -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
|
-
//
|
|
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: "
|
|
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
|
|
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\` →
|
|
441
|
-
|
|
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
|
-
|
|
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**: \`
|
|
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.
|
|
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
|
|
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\`
|
|
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
|
|
804
|
-
|
|
805
|
-
# Next.js
|
|
806
|
-
NEXT_PUBLIC_DYPAI_URL=https
|
|
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
|
-
|
|
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)\` →
|
|
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.
|
package/src/tools/deploy.js
CHANGED
|
@@ -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${
|
|
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
|
+
}
|