@dypai-ai/mcp 1.5.7 → 1.5.9
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/index.js +126 -34
- package/src/tools/frontend.js +3 -3
- package/src/tools/scaffold.js +3 -2
- package/src/tools/search-logs-offload.js +1 -0
- package/src/tools/sync/push.js +1 -1
- package/src/tools/sync/test-endpoint.js +3 -0
- package/src/tools/sync/validate.js +185 -46
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -115,8 +115,38 @@ const REMOTE_TOOLS = [
|
|
|
115
115
|
// ── Project ───────────────────────────────────────────────────────────────
|
|
116
116
|
{ name: "list_projects", description: "Lists all projects you have access to across your organizations. Returns project id, name, description, organization, subscription plan, and status. Use this as the first step to discover which projects are available, then pass project_id to other tools.", inputSchema: { type: "object", properties: { organization_id: { type: "string", description: "Optional. Filter projects by organization UUID." } }, required: [] } },
|
|
117
117
|
{ name: "get_project", description: "Gets detailed information about a specific project. Returns project name, description, organization, plan, status, engine URL, frontend slug, and timestamps.", inputSchema: { type: "object", properties: { project_id: { type: "string" } }, required: ["project_id"] } },
|
|
118
|
-
{
|
|
119
|
-
|
|
118
|
+
{
|
|
119
|
+
name: "manage_project_access_profile",
|
|
120
|
+
description: `Read or update the project's product access profile.
|
|
121
|
+
|
|
122
|
+
Use this when you know what kind of app is being built:
|
|
123
|
+
- private/admin tool: app_visibility='private', auth_scope='admin_only', has_admin_area=true, root_requires_auth=true
|
|
124
|
+
- public landing with admin panel: app_visibility='mixed', auth_scope='admin_only', has_public_area=true, has_admin_area=true
|
|
125
|
+
- customer/user portal: app_visibility='private' or 'mixed', auth_scope='end_users' or 'admin_and_end_users', has_end_user_accounts=true
|
|
126
|
+
- public-only site: app_visibility='public', auth_scope='none', role_model='none', root_requires_auth=false
|
|
127
|
+
|
|
128
|
+
This stores classification metadata only. It does not create users, roles, login UI, tables, endpoints, or publish anything.`,
|
|
129
|
+
inputSchema: {
|
|
130
|
+
type: "object",
|
|
131
|
+
properties: {
|
|
132
|
+
project_id: { type: "string", description: "Project UUID. Required for user tokens; auto-detected for project tokens." },
|
|
133
|
+
operation: { type: "string", enum: ["get", "update"], description: "Read or update the project access classification." },
|
|
134
|
+
app_visibility: { type: "string", enum: ["public", "private", "mixed"], description: "Public surface of the app." },
|
|
135
|
+
auth_scope: { type: "string", enum: ["none", "admin_only", "internal_users", "end_users", "admin_and_end_users"], description: "Who needs authentication." },
|
|
136
|
+
role_model: { type: "string", enum: ["none", "single_role", "multi_role"], description: "Whether the app has no roles, one role, or multiple roles." },
|
|
137
|
+
has_admin_area: { type: "boolean", description: "Whether the app should include an admin/private management area." },
|
|
138
|
+
has_public_area: { type: "boolean", description: "Whether the app should include pages usable without login." },
|
|
139
|
+
has_end_user_accounts: { type: "boolean", description: "Whether non-admin end users have their own accounts." },
|
|
140
|
+
root_requires_auth: { type: "boolean", description: "Whether the root route should require login." },
|
|
141
|
+
metadata_patch: { type: "object", description: "Optional non-sensitive notes to merge into access_metadata. Sensitive-looking keys are dropped." },
|
|
142
|
+
reason: { type: "string", description: "Short reason for the update." },
|
|
143
|
+
source: { type: "string", description: "Optional caller label. Defaults to mcp." },
|
|
144
|
+
},
|
|
145
|
+
required: ["operation"],
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
{ name: "list_ai_models", description: "List only the DYPAI Managed AI models that are active for a project. Returns the project-gated OpenRouter model catalog priced in AI Credits per 1M tokens, RPM limit, max output tokens, active/available counts, billing metadata, and the exact node parameters to use. Call this before creating or editing an AI Agent node with DYPAI Managed models. Agents must not invent or use inactive model ids. Use provider='openrouter' and do NOT set credential_id; DYPAI uses the platform OpenRouter key and deducts usage from the organization's AI Credits.", inputSchema: { type: "object", properties: { project_id: { type: "string", description: "Project UUID whose plan and Model Gateway settings determine the active Managed AI catalog." } }, required: ["project_id"] } },
|
|
149
|
+
{ name: "create_project", description: "Create a new DYPAI project (free plan). Creates a full project with database, engine, GitHub repo, and frontend hosting. BLOCKS by default until provisioning finishes (~60s typical, 120s max) — when it returns, the project_id is ready to use with execute_sql, endpoint tools, etc. Pass wait_until_ready:false for batch flows.\n\nName collision: if another project in the same org already uses the name (case-insensitive), returns {error:'name_taken', existing_project_id, suggestions:[...]}. Pick a different name or use the existing project.\n\nProject limits are enforced by the DYPAI API at organization/workspace scope according to the workspace plan. If it returns {error:'project_limit_reached'}, do not retry create_project; show list_projects for that organization and ask the user to reuse, archive/pause, upgrade the workspace to Pro, or add capacity.\n\nIMPORTANT: before calling, check for a matching template with `search_project_templates`. Passing a `template_slug` drops in a ready-made schema + endpoints + UI that cover 70% of common app types. Use built-in bases when appropriate: `private-admin` for private internal tools, `user-accounts` for apps with signup/login users, `landing-admin` for public landing plus admin, and `blank` only when no base fits.", inputSchema: { type: "object", properties: { name: { type: "string", description: "Project name (e.g. 'My Veterinary App')" }, organization_id: { type: "string", description: "Optional. Uses default org if omitted." }, description: { type: "string" }, template_slug: { type: "string", description: "RECOMMENDED. Project template slug to start from (e.g. 'clinic', 'gym', 'private-admin', 'user-accounts', 'landing-admin', 'blank'). Always call search_project_templates first to find the best match." }, wait_until_ready: { type: "boolean", description: "If true (default), blocks until provisioning completes and the project is ready for all operations. If false, returns immediately with status='provisioning' — caller must poll get_project before using.", default: true } }, required: ["name"] } },
|
|
120
150
|
{ name: "get_app_credentials", description: "Lists available credentials in the current application. Returns API keys, anon key, service role key, and engine URL needed for SDK configuration.", inputSchema: { type: "object", properties: { project_id: { type: "string" } }, required: [] } },
|
|
121
151
|
|
|
122
152
|
// ── Database ──────────────────────────────────────────────────────────────
|
|
@@ -437,15 +467,16 @@ endpoint YAML and \`dypai_push\`. This tool does NOT modify the definition.`,
|
|
|
437
467
|
// ── Observability ─────────────────────────────────────────────────────────
|
|
438
468
|
{
|
|
439
469
|
name: "search_logs",
|
|
440
|
-
description: "Search recent
|
|
470
|
+
description: "Search recent backend activity for the current project. ALWAYS call this FIRST when the user reports any error, bug, 'this isn't working', or a click/action that appears to do nothing — don't guess from the code; check what actually happened. Returns a unified, time-ordered list mixing workflow executions and warn/error log lines from the engine. Defaults to recent problems only (`status:'problem'`) over the last 24h. Raw data is retained 7 days and cleaned automatically by metrics-db retention policies.\n\nWorkflow:\n 1) Call with no args (or just `since:'1h'`) → see recent failures/warnings.\n 2) If the user says a button/action did nothing and there is no visible error → call `search_logs({ since:'30m', status:'all', endpoint:'...' })` or `status:'success'` to see successful backend calls too.\n 3) Pick the relevant entry → call again with `endpoint` + tighter `query` to narrow down.\n 4) 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 overlay test runs). Use `environment:'draft'` when the user says 'I just tested X locally and it failed' (their local UI hits the draft overlay).",
|
|
441
471
|
inputSchema: {
|
|
442
472
|
type: "object",
|
|
443
473
|
properties: {
|
|
444
474
|
project_id: { type: "string", description: "Project UUID. Auto-detected for project tokens." },
|
|
445
|
-
query: { type: "string", description: "Optional substring to match (case-insensitive) in
|
|
475
|
+
query: { type: "string", description: "Optional substring to match (case-insensitive) in messages, endpoint names, statuses, request IDs, and log lines. e.g. 'timeout', 'OpenAI', 'permission denied'." },
|
|
446
476
|
endpoint: { type: "string", description: "Optional endpoint name filter (e.g. 'create-order')." },
|
|
447
477
|
since: { type: "string", default: "24h", description: "Time window: relative ('15m', '1h', '24h', '7d') or ISO 8601 timestamp. Default 24h. Hard cap: 7d (retention)." },
|
|
448
|
-
level: { type: "string", enum: ["error", "warn", "all"], default: "all", description: "Filter by severity. 'error' includes failed/timeout executions + error logs. 'warn'
|
|
478
|
+
level: { type: "string", enum: ["error", "warn", "all"], default: "all", description: "Filter by severity. 'error' includes failed/timeout executions + error logs. 'warn' returns warning logs only. 'all' returns every severity allowed by `status`." },
|
|
479
|
+
status: { type: "string", enum: ["problem", "success", "error", "timeout", "all"], default: "problem", description: "Workflow execution status filter. 'problem' (default) keeps the old lightweight behavior: failed/timed-out executions plus warn/error logs. 'success' shows successful executions only. 'all' shows successful and failed executions plus warn/error logs. Use 'success' or 'all' when debugging a user action that produced no visible backend error." },
|
|
449
480
|
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." },
|
|
450
481
|
limit: { type: "integer", default: 50, minimum: 1, maximum: 200, description: "Max items to return. Default 50, max 200." },
|
|
451
482
|
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." }
|
|
@@ -456,8 +487,9 @@ endpoint YAML and \`dypai_push\`. This tool does NOT modify the definition.`,
|
|
|
456
487
|
|
|
457
488
|
// ── Knowledge ─────────────────────────────────────────────────────────────
|
|
458
489
|
{ 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"] } },
|
|
490
|
+
{ name: "search_design_patterns", description: "Search compact DYPAI UI/design recipes. Use before designing substantial screens.", inputSchema: { type: "object", properties: { query: { type: "string", description: "Design need, with starter/domain/screen/style context when known." }, starter_slug: { type: "string", description: "Optional: private-admin, user-accounts, landing-admin, or blank." }, app_type: { type: "string", description: "Optional domain/app type." }, screen_type: { type: "string", description: "Optional screen/workflow." }, visual_style: { type: "string", description: "Optional style." }, category: { type: "string", description: "Optional category." }, limit: { type: "integer", default: 3, minimum: 1, maximum: 4 } }, required: ["query"] } },
|
|
459
491
|
{ 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"] } },
|
|
460
|
-
{ name: "search_project_templates", description: "Search project starter templates by description. Returns template metadata and slugs for
|
|
492
|
+
{ name: "search_project_templates", description: "Search project starter templates by description. Returns template metadata and slugs for marketplace templates plus built-in bases: private-admin, user-accounts, landing-admin, and blank.", inputSchema: { type: "object", properties: { query: { type: "string", description: "What kind of project starter you need (e.g. 'gym app', 'private admin dashboard', 'user accounts portal', 'landing plus admin')" }, category: { type: "string", description: "Optional category filter" } }, required: ["query"] } },
|
|
461
493
|
]
|
|
462
494
|
|
|
463
495
|
// ── Server Instructions ──────────────────────────────────────────────────────
|
|
@@ -486,16 +518,26 @@ First reflex, always:
|
|
|
486
518
|
|
|
487
519
|
1. **Acknowledge briefly** what they want to build (one short line, their language).
|
|
488
520
|
2. **\`search_project_templates(query: "<keywords from their request>")\`** — keywords in their language. Templates cover common app types (gym, clinic, waitlist, saas dashboard, etc.).
|
|
489
|
-
3. **Decide: template
|
|
521
|
+
3. **Decide: marketplace template, built-in base, or blank.** Marketplace templates are only right when the match is OBVIOUS and STRONG:
|
|
490
522
|
- ✅ User says *"app para mi gimnasio"* + there's \`gym-manager\` (exact domain + feature overlap) → template.
|
|
491
|
-
- ❌ User says *"algo para gestionar reservas"* + there's \`gym-manager\` (soft match, many interpretations) → **blank**. Don't assume they want the gym's specific schema (classes, memberships, check-ins) — they didn't ask for it.
|
|
492
|
-
-
|
|
493
|
-
|
|
494
|
-
|
|
523
|
+
- ❌ User says *"algo para gestionar reservas"* + there's \`gym-manager\` (soft match, many interpretations) → use a built-in base or **blank**. Don't assume they want the gym's specific schema (classes, memberships, check-ins) — they didn't ask for it.
|
|
524
|
+
- Built-in bases are safe defaults:
|
|
525
|
+
- private/internal/admin/dashboard/backoffice/business management → \`private-admin\`
|
|
526
|
+
- end-user signup/login/customer/member portal/marketplace/SaaS accounts → \`user-accounts\`
|
|
527
|
+
- public landing/marketing site plus private admin → \`landing-admin\`
|
|
528
|
+
- no clear access pattern or explicitly custom/from scratch → \`blank\`
|
|
529
|
+
- ❌ User is a dev with a concrete spec (*"crea un proyecto con estas 3 tablas y estos endpoints"*) → usually **blank**, unless they explicitly want one of the built-in bases.
|
|
530
|
+
- ❌ No marketplace or built-in base fits → **blank**.
|
|
531
|
+
4. **Call it** → \`create_project(name: "<their name>", template_slug: "<matched_slug>" | "private-admin" | "user-accounts" | "landing-admin" | "blank")\`.
|
|
495
532
|
If you went with a template, acknowledge in ONE line what's included so the user can push back: *"Lo arranco con la plantilla X, que trae socios, clases y pagos. ¿Te vale o prefieres algo más simple?"*
|
|
496
533
|
If you went blank, just say: *"Arranco un proyecto en blanco y lo construimos a medida."*
|
|
497
534
|
5. **After \`create_project\`** → ask for an absolute workspace path, then \`dypai_pull\` + \`manage_frontend(sync)\` (see next section).
|
|
498
535
|
|
|
536
|
+
Before designing substantial UI (app shell, dashboard, login, tables/lists,
|
|
537
|
+
forms, calendars, or domain-specific screens), use \`search_design_patterns\`
|
|
538
|
+
with the app/starter/screen/style context. It returns curated recipes; adapt
|
|
539
|
+
them to the project instead of inventing generic starter UI.
|
|
540
|
+
|
|
499
541
|
**The template system exists to save time when the fit is obvious, not to force-match every request.** When in doubt → blank is always correct. Iterating up from blank is cheaper than deleting 80% of a mismatched template.
|
|
500
542
|
|
|
501
543
|
## The one legit follow-up question
|
|
@@ -520,6 +562,34 @@ DYPAI builds the **backend** (DB, auth, API, storage, realtime) for any client.
|
|
|
520
562
|
|
|
521
563
|
For everything else, **the stack is decided. Start building.**
|
|
522
564
|
|
|
565
|
+
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
566
|
+
# PROJECT ACCESS PROFILE — classify the app once you know the product shape
|
|
567
|
+
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
568
|
+
|
|
569
|
+
DYPAI projects store a lightweight product access profile: whether the app is
|
|
570
|
+
public, private, mixed, admin-only, internal-user based, end-user-account based,
|
|
571
|
+
single-role, or multi-role. This helps later agents and platform UI reason
|
|
572
|
+
about login, first-run credentials, preview instructions, and route protection.
|
|
573
|
+
|
|
574
|
+
When you know the product shape, call
|
|
575
|
+
\`manage_project_access_profile(operation:"update", ...)\`. Do this as metadata
|
|
576
|
+
only; it does NOT replace implementing the actual auth UI, roles, protected
|
|
577
|
+
routes, tables, or endpoints.
|
|
578
|
+
|
|
579
|
+
Defaults:
|
|
580
|
+
- Private/admin app: \`app_visibility:"private"\`, \`auth_scope:"admin_only"\`,
|
|
581
|
+
\`role_model:"single_role"\`, \`has_admin_area:true\`, \`has_public_area:false\`,
|
|
582
|
+
\`has_end_user_accounts:false\`, \`root_requires_auth:true\`.
|
|
583
|
+
- Public site with admin panel: \`app_visibility:"mixed"\`,
|
|
584
|
+
\`auth_scope:"admin_only"\`, \`has_public_area:true\`, \`has_admin_area:true\`,
|
|
585
|
+
\`root_requires_auth:false\`.
|
|
586
|
+
- User portal / marketplace / SaaS: use \`auth_scope:"admin_and_end_users"\`
|
|
587
|
+
when both admins and customers log in, \`role_model:"multi_role"\` if there
|
|
588
|
+
are materially different permissions, and \`has_end_user_accounts:true\`.
|
|
589
|
+
- Public-only landing/docs/blog: \`app_visibility:"public"\`,
|
|
590
|
+
\`auth_scope:"none"\`, \`role_model:"none"\`, \`has_admin_area:false\`,
|
|
591
|
+
\`root_requires_auth:false\`.
|
|
592
|
+
|
|
523
593
|
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
524
594
|
# BEFORE YOU DO ANYTHING — materialize the project locally
|
|
525
595
|
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
@@ -689,7 +759,7 @@ Use phrases like:
|
|
|
689
759
|
Default is **no tool names in user-facing text**.
|
|
690
760
|
|
|
691
761
|
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
692
|
-
# SEARCH BEFORE YOU GUESS — \`search_docs\`
|
|
762
|
+
# SEARCH BEFORE YOU GUESS — \`search_docs\` and \`search_design_patterns\`
|
|
693
763
|
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
694
764
|
|
|
695
765
|
This prompt is the MAP of the DYPAI platform. The detailed docs live in
|
|
@@ -732,6 +802,17 @@ synonym.
|
|
|
732
802
|
- \`"agent ai"\` — agent node: providers, tools, memory, streaming, tool endpoints
|
|
733
803
|
- \`"javascript code node"\` — custom code escape hatch when native nodes don't fit
|
|
734
804
|
|
|
805
|
+
### Managed AI Models
|
|
806
|
+
- Before creating or editing any AI Agent node that uses DYPAI Managed AI,
|
|
807
|
+
always call \`list_ai_models\` first.
|
|
808
|
+
- Use only model IDs from \`list_ai_models.models[].id\`; never invent OpenRouter
|
|
809
|
+
model IDs.
|
|
810
|
+
- For DYPAI Managed AI, set \`provider: "openrouter"\` and do not set
|
|
811
|
+
\`credential_id\`; DYPAI uses the platform key server-side.
|
|
812
|
+
- Treat model pricing as AI Credits, using
|
|
813
|
+
\`input_ai_credits_per_million\` / \`output_ai_credits_per_million\` when you
|
|
814
|
+
need to explain cost.
|
|
815
|
+
|
|
735
816
|
### Auth
|
|
736
817
|
- \`"auth flows"\` — signup / login / reset / magic link / role upgrade canonical flows
|
|
737
818
|
- \`"auth defaults"\` — what auth_mode to pick when the user doesn't specify
|
|
@@ -800,12 +881,13 @@ Editing files inside \`dypai/\` only changes YOUR DISK. The platform doesn't see
|
|
|
800
881
|
\`\`\`
|
|
801
882
|
|
|
802
883
|
Practical consequences — internalize these:
|
|
803
|
-
- **
|
|
804
|
-
-
|
|
805
|
-
-
|
|
884
|
+
- **Never publish backend changes just to test them.** Backend changes are testable before production: save them to preview, verify with \`dypai_test_endpoint(mode:'draft')\` when possible, then tell the user exactly what to try in preview.
|
|
885
|
+
- **After EVERY meaningful backend 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 preview and sees the OLD behavior. The push is cheap, idempotent, and creates ONE preview version per resource (subsequent pushes overwrite the pending preview version, not stack new ones).
|
|
886
|
+
- **\`dypai_push\` is the internal save-to-preview step. It is NOT a production publish.** Live traffic is untouched. You can run it repeatedly without affecting real users. In user-facing prose, say "listo para probar" or "en previsualización", not "pushed" or "draft".
|
|
887
|
+
- **The preview host (\`dev-<project_id>.dypai.dev\`) only sees what you've saved to preview.** A change still only on disk is invisible to the user's preview. If the user says "I tested it and nothing changed", first check whether the backend change was saved to preview after the last edit.
|
|
806
888
|
- **\`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.
|
|
807
|
-
- **Order during a multi-step feature**: edit → \`dypai_validate\` → \`dypai_push\` → \`dypai_test_endpoint(mode:'draft')\` (or tell the user to test
|
|
808
|
-
- **DDL is the exception**: \`execute_sql\` with CREATE / ALTER / DROP TABLE applies to live IMMEDIATELY (no
|
|
889
|
+
- **Order during a multi-step backend feature**: edit → \`dypai_validate\` → \`dypai_push\` → \`dypai_test_endpoint(mode:'draft')\` (or tell the user to test preview). Repeat per change. ONLY when the user explicitly approves production do \`manage_drafts(operation:'list')\` → \`manage_drafts(operation:'publish', confirm:true)\`.
|
|
890
|
+
- **DDL is the exception**: \`execute_sql\` with CREATE / ALTER / DROP TABLE applies to live IMMEDIATELY (no preview layer for schema). Preview only exists for endpoints / webhooks / crons / realtime policies. Summarize destructive DDL to the user before running it.
|
|
809
891
|
|
|
810
892
|
## User intent → tool to call (decision table)
|
|
811
893
|
|
|
@@ -815,13 +897,14 @@ Use this BEFORE picking a tool. If unsure which row matches, ask the user.
|
|
|
815
897
|
|---|---|---|
|
|
816
898
|
| "Create a new project" | \`search_project_templates\` (find a starter) | \`create_project(template_slug: ...)\` |
|
|
817
899
|
| "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/\` |
|
|
900
|
+
| "This is a private admin app / public site / user portal / multi-role app" | \`manage_project_access_profile(operation:'update')\` | Then implement the actual auth/UI/data behavior normally |
|
|
818
901
|
| "Add/change a backend endpoint, table, cron, webhook, agent, integration" | Edit files in \`dypai/\` | \`dypai_validate\` → \`dypai_push\` |
|
|
819
902
|
| "Publish my backend changes" / "make it live" | \`manage_drafts(operation:'list')\` to show what's pending | \`manage_drafts(operation:'publish', confirm:true)\` |
|
|
820
903
|
| "Test an endpoint before publishing" | \`dypai_test_endpoint(mode:'local')\` (your edits) or \`(mode:'draft')\` (after push) | — |
|
|
821
904
|
| "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. | — |
|
|
822
905
|
| "Throw away my backend changes" | \`manage_drafts(operation:'discard', confirm:true)\` | — |
|
|
823
|
-
| "Change the UI / change colors / add a page" | Edit files in \`src/\` | \`manage_frontend(deploy, confirm:true)\` |
|
|
824
|
-
| "Publish the new UI" / "ship the frontend" | \`manage_frontend(deploy, confirm:true)\`
|
|
906
|
+
| "Change the UI / change colors / add a page" | Edit files in \`src/\` | Test locally/with browser when possible. \`manage_frontend(deploy, confirm:true)\` only when the user approves publishing the UI. |
|
|
907
|
+
| "Publish the new UI" / "ship the frontend" | If backend changes are needed by the new UI, publish those backend changes first with explicit approval | Then \`manage_frontend(deploy, confirm:true)\` — deploy is live, not a preview. |
|
|
825
908
|
| "Roll back" | Backend: \`get_endpoint_versions\` then write old code back. Frontend: re-deploy older source. | — |
|
|
826
909
|
| "Upload a file / a CSV / seed data" | \`bulk_upsert\` (data) or \`manage_storage(upload_file)\` (binary) | — |
|
|
827
910
|
| "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 |
|
|
@@ -847,19 +930,21 @@ User: "Add a /api/list-tasks endpoint that returns the current user's tasks, and
|
|
|
847
930
|
2. manage_frontend(operation:'sync', ...) # materialize frontend if not already on disk
|
|
848
931
|
3. # Backend: create the endpoint
|
|
849
932
|
Write dypai/endpoints/list-tasks.yaml # trigger.http_api auth_mode:jwt + dypai_database query
|
|
850
|
-
4. dypai_validate # catch typos before
|
|
851
|
-
5. dypai_push #
|
|
852
|
-
6. dypai_test_endpoint(
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
933
|
+
4. dypai_validate # catch typos before saving to preview
|
|
934
|
+
5. dypai_push # saves to preview, NOT production
|
|
935
|
+
6. dypai_test_endpoint(endpoint:'list-tasks', mode:'draft', as_user:'<user_id>')
|
|
936
|
+
# verifies the preview version; do NOT publish just to test
|
|
937
|
+
7. # Frontend: call the new endpoint from React
|
|
938
|
+
Edit src/pages/Dashboard.tsx # useEndpoint('list-tasks')
|
|
939
|
+
8. # Test locally/browser if available. Then tell the user in plain language:
|
|
940
|
+
# "Ya está listo para probar. Abre la previsualización y revisa la lista de tareas. Todavía no está publicado para tus usuarios."
|
|
941
|
+
9. # ONLY after the user confirms it is good:
|
|
942
|
+
manage_drafts(operation:'list') # internal: inspect what will publish
|
|
943
|
+
10. manage_drafts(operation:'publish', confirm:true) # backend live after explicit approval
|
|
944
|
+
11. manage_frontend(operation:'deploy', sourceDirectory, confirm:true) # frontend live after explicit approval
|
|
860
945
|
\`\`\`
|
|
861
946
|
|
|
862
|
-
**
|
|
947
|
+
**Testing rule**: never publish backend changes just to test them. Backend can be verified from the preview version. **Production order rule**: when you are truly publishing a full-stack change, publish backend BEFORE deploying the frontend; otherwise the live UI may call backend functionality that is not live yet.
|
|
863
948
|
|
|
864
949
|
## Debugging user-reported errors — \`search_logs\` is your starting point
|
|
865
950
|
|
|
@@ -876,10 +961,16 @@ User: "Add a /api/list-tasks endpoint that returns the current user's tasks, and
|
|
|
876
961
|
# Did they say "production users are reporting..."?
|
|
877
962
|
# → add environment: "live" (excludes their own draft test runs)
|
|
878
963
|
|
|
879
|
-
3. #
|
|
964
|
+
3. # Did they say "I clicked the button and it disappeared / nothing happened"
|
|
965
|
+
# and the error scan is empty?
|
|
966
|
+
search_logs({ since: "30m", status: "all", endpoint: "<likely-endpoint>" })
|
|
967
|
+
→ This shows successful executions too, so you can tell whether the backend
|
|
968
|
+
was called, returned 200, was slow, or was never reached.
|
|
969
|
+
|
|
970
|
+
4. # Found the relevant entry? Narrow down:
|
|
880
971
|
search_logs({ endpoint: "create-order", query: "stripe", since: "1h" })
|
|
881
972
|
|
|
882
|
-
|
|
973
|
+
5. # For the full step-by-step trace of one specific failure:
|
|
883
974
|
search_logs({
|
|
884
975
|
endpoint: "create-order",
|
|
885
976
|
query: "<a unique substring from the error message>",
|
|
@@ -890,12 +981,12 @@ User: "Add a /api/list-tasks endpoint that returns the current user's tasks, and
|
|
|
890
981
|
and returns a \`file_path\`. Read that file with the Read tool ONLY
|
|
891
982
|
when you need fields beyond the inline summary.
|
|
892
983
|
|
|
893
|
-
|
|
984
|
+
6. # Now you know exactly which node failed, succeeded, or was never called → fix the code.
|
|
894
985
|
\`\`\`
|
|
895
986
|
|
|
896
987
|
### What \`search_logs\` returns
|
|
897
988
|
|
|
898
|
-
Each item has \`type\` (\`execution_failed\` | \`log\`), \`level\` (\`error\` | \`warn\`), \`time\`, \`endpoint\`, \`message\`, and \`environment\` (\`live\` | \`draft\` | null for legacy rows).
|
|
989
|
+
Each item has \`type\` (\`execution\` | \`execution_failed\` | \`log\`), \`level\` (\`info\` | \`error\` | \`warn\`), \`time\`, \`endpoint\`, \`message\`, and \`environment\` (\`live\` | \`draft\` | null for legacy rows). Workflow executions include \`status\` (\`success\` | \`error\` | \`timeout\`) and \`duration_ms\`. With \`include_trace:true\`, failed executions can include \`trace\` — a per-node log of inputs, outputs, errors, and stacks.
|
|
899
990
|
|
|
900
991
|
### Common pitfalls
|
|
901
992
|
|
|
@@ -944,6 +1035,7 @@ Mental translations: "edge function" → workflow with one code node; "cron" →
|
|
|
944
1035
|
4. **\`public\` auth_mode with \`\${current_user_id}\`** — no JWT → placeholder empty → SQL fails or returns wrong data. Use \`jwt\` if you need the user.
|
|
945
1036
|
5. **Missing \`return: true\`** — endpoint returns \`null\`. Every path that should produce an HTTP response needs one node with \`return: true\`.
|
|
946
1037
|
6. **\`tool_ids\` in YAML instead of \`tools\`** — write \`tools: [name1, name2]\`. \`tool_ids\` bypasses the codec and fails silently in prod.
|
|
1038
|
+
7. **Putting workflow placeholders inside \`javascript_code.code\`** — code is raw JavaScript, so JS template literals like \`\${where.join(" AND ")}\` are safe and not rendered by DYPAI. Pass workflow values via \`input_data\`, \`ctx.nodes\`, \`ctx.user\`, or \`ctx.env\`; do not write \`\${input.email}\` inside code or set \`code\` from another node output.
|
|
947
1039
|
|
|
948
1040
|
→ Longer list of common pitfalls + fixes: \`search_docs("troubleshooting")\`.
|
|
949
1041
|
|
package/src/tools/frontend.js
CHANGED
|
@@ -32,7 +32,7 @@ export const manageFrontendTool = {
|
|
|
32
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" +
|
|
33
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
34
|
"Requires `confirm: true` — without it the tool returns a confirmation_required hint instead of deploying. " +
|
|
35
|
-
"If backend drafts are pending, the hint
|
|
35
|
+
"If backend drafts are pending, the hint warns that publishing the frontend is a live production action; only publish backend FIRST when the user has explicitly approved going live. Do NOT publish backend just to test it — use preview/draft testing instead. " +
|
|
36
36
|
"Returns immediately with build_status=\"queued\" — poll with `build_status` until \"success\" or \"failure\". " +
|
|
37
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" +
|
|
38
38
|
" - status: Current live deploy info (URL, last deploy time, size).\n" +
|
|
@@ -41,7 +41,7 @@ export const manageFrontendTool = {
|
|
|
41
41
|
" - list_deployments: Recent deploy history (status, commit, duration, URL).\n" +
|
|
42
42
|
" - logs: Build logs for a specific deployment (needs deployment_id from list_deployments).\n\n" +
|
|
43
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
|
-
"
|
|
44
|
+
"Testing rule: backend changes can be tested in preview after dypai_push; do NOT publish backend just to test. Production order rule when both backend AND frontend changed and the user approved going live: 1) publish backend drafts with manage_drafts(publish, confirm:true) → 2) deploy frontend with manage_frontend(deploy, confirm:true). Inverting production order may serve a live frontend that calls backend functionality not live yet.",
|
|
45
45
|
|
|
46
46
|
inputSchema: {
|
|
47
47
|
type: "object",
|
|
@@ -117,7 +117,7 @@ export const manageFrontendTool = {
|
|
|
117
117
|
const draftsTotal = draftsResult?.total || 0
|
|
118
118
|
if (draftsTotal > 0) {
|
|
119
119
|
warnings.push(
|
|
120
|
-
`${draftsTotal} backend
|
|
120
|
+
`${draftsTotal} backend preview change(s) pending. This frontend deploy is LIVE. Only publish the backend first if the user explicitly approved production; for testing, keep backend in preview and test without deploying live frontend.`,
|
|
121
121
|
)
|
|
122
122
|
}
|
|
123
123
|
} catch {
|
package/src/tools/scaffold.js
CHANGED
|
@@ -20,7 +20,8 @@ Scaffolds a project directory with:
|
|
|
20
20
|
- .env with engine URL
|
|
21
21
|
|
|
22
22
|
Use search_project_templates first to find available templates, then pass the template slug here.
|
|
23
|
-
|
|
23
|
+
Use "private-admin" for private internal tools, "user-accounts" for apps with signup/login users,
|
|
24
|
+
"landing-admin" for public landing plus admin, or "blank" only when no base fits.`,
|
|
24
25
|
|
|
25
26
|
inputSchema: {
|
|
26
27
|
type: "object",
|
|
@@ -35,7 +36,7 @@ Or use "blank" for an empty starter project.`,
|
|
|
35
36
|
},
|
|
36
37
|
template: {
|
|
37
38
|
type: "string",
|
|
38
|
-
description: 'Template slug (e.g. "clinic", "gym", "blank"). Use search_project_templates to find available templates.',
|
|
39
|
+
description: 'Template slug (e.g. "clinic", "gym", "private-admin", "user-accounts", "landing-admin", "blank"). Use search_project_templates to find available templates.',
|
|
39
40
|
default: "blank",
|
|
40
41
|
},
|
|
41
42
|
},
|
package/src/tools/sync/push.js
CHANGED
|
@@ -524,7 +524,7 @@ export const dypaiPushTool = {
|
|
|
524
524
|
next_step: errors.length
|
|
525
525
|
? "Fix the offending YAMLs and push again."
|
|
526
526
|
: draftCount > 0
|
|
527
|
-
? `${draftCount} change(s) saved
|
|
527
|
+
? `${draftCount} change(s) saved to preview — they're not live yet. Verify with dypai_test_endpoint(mode:'draft', endpoint:'<name>') or ask the user to test the preview. Publish with manage_drafts(operation:'publish', confirm:true) ONLY after explicit approval.`
|
|
528
528
|
: changedNames.length
|
|
529
529
|
? `Test changed endpoint(s) with dypai_test_endpoint: ${changedNames.slice(0, 3).join(", ")}${changedNames.length > 3 ? "…" : ""}`
|
|
530
530
|
: undefined,
|
|
@@ -361,6 +361,9 @@ export const dypaiTestEndpointTool = {
|
|
|
361
361
|
data: input,
|
|
362
362
|
trace_mode, // used by the local MCP enrichment layer
|
|
363
363
|
}
|
|
364
|
+
if (mode === "local") execArgs.draft_mode = true
|
|
365
|
+
if (mode === "draft") execArgs.draft_mode = true
|
|
366
|
+
if (mode === "live") execArgs.draft_mode = false
|
|
364
367
|
if (as_user) execArgs.impersonated_user_id = as_user
|
|
365
368
|
|
|
366
369
|
// Build a compact source-meta block for the response so the agent can see
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* - `credential: foo` pointing to a credential that doesn't exist remotely
|
|
9
9
|
* - Agent `tools: [foo]` pointing to endpoints that aren't tools
|
|
10
10
|
* - SQL queries referencing tables that don't exist in schema.sql
|
|
11
|
+
* - dypai_storage/static storage bucket references that don't exist remotely
|
|
11
12
|
*
|
|
12
13
|
* Returns a list of diagnostics with severity + fix_hint. Non-zero errors
|
|
13
14
|
* mean push will refuse (unless skip_validation is passed).
|
|
@@ -179,6 +180,44 @@ function* walkStrings(node, path = "") {
|
|
|
179
180
|
}
|
|
180
181
|
}
|
|
181
182
|
|
|
183
|
+
const BUCKET_LOC_RE = /(?:^|\.)(?:bucket|bucket_name|bucketName|storage_bucket|storageBucket)$/
|
|
184
|
+
const STATIC_BUCKET_RE = /^[a-z0-9][a-z0-9-]{1,61}[a-z0-9]$/
|
|
185
|
+
|
|
186
|
+
function isStaticBucketName(value) {
|
|
187
|
+
const trimmed = String(value || "").trim()
|
|
188
|
+
if (!trimmed || trimmed.includes("${") || trimmed.includes("{{")) return false
|
|
189
|
+
return STATIC_BUCKET_RE.test(trimmed)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function collectStorageBucketRefs(entry, endpoint, file) {
|
|
193
|
+
const refs = new Map()
|
|
194
|
+
const add = (bucket, loc) => {
|
|
195
|
+
const name = String(bucket || "").trim()
|
|
196
|
+
if (!isStaticBucketName(name)) return
|
|
197
|
+
refs.set(`${name}|${loc}`, { bucket: name, endpoint, file, loc })
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
for (const { path, value } of walkStrings(entry.doc)) {
|
|
201
|
+
if (BUCKET_LOC_RE.test(path)) add(value, path)
|
|
202
|
+
|
|
203
|
+
// JS helper pattern from workflow code files/inline code:
|
|
204
|
+
// storage.upload("documents", ...), storage.read("documents", ...), etc.
|
|
205
|
+
if (/(^|\.|])code$/.test(path)) {
|
|
206
|
+
const re = /\bstorage\.(?:upload|read|list|getUrl|delete)\(\s*["']([a-z0-9][a-z0-9-]{1,61}[a-z0-9])["']/g
|
|
207
|
+
let m
|
|
208
|
+
while ((m = re.exec(value)) !== null) add(m[1], path)
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
for (const [filePath, content] of Object.entries(entry.fileMap || {})) {
|
|
213
|
+
const re = /\bstorage\.(?:upload|read|list|getUrl|delete)\(\s*["']([a-z0-9][a-z0-9-]{1,61}[a-z0-9])["']/g
|
|
214
|
+
let m
|
|
215
|
+
while ((m = re.exec(content)) !== null) add(m[1], filePath)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return [...refs.values()]
|
|
219
|
+
}
|
|
220
|
+
|
|
182
221
|
/** Extract all ${...} placeholder expressions from a string. */
|
|
183
222
|
function extractPlaceholders(s) {
|
|
184
223
|
const out = []
|
|
@@ -188,6 +227,13 @@ function extractPlaceholders(s) {
|
|
|
188
227
|
return out
|
|
189
228
|
}
|
|
190
229
|
|
|
230
|
+
function isRawCodePlaceholderSource(source, loc) {
|
|
231
|
+
if (source === "file") {
|
|
232
|
+
return loc.startsWith("code/") || /\.(js|ts|py)$/.test(loc)
|
|
233
|
+
}
|
|
234
|
+
return /(^|\.|])code$/.test(loc)
|
|
235
|
+
}
|
|
236
|
+
|
|
191
237
|
/**
|
|
192
238
|
* Extract the first identifier (a-z, A-Z, _, 0-9 — JS-ident shape) from the
|
|
193
239
|
* head of an expression, ignoring any trailing template filter or coalesce
|
|
@@ -289,6 +335,31 @@ const LEGACY_OPS_THAT_USE_TABLE_FIELD = new Set([
|
|
|
289
335
|
"select", "insert", "update", "delete", "upsert", "aggregate",
|
|
290
336
|
])
|
|
291
337
|
|
|
338
|
+
function nodeParams(node) {
|
|
339
|
+
return node?.parameters && typeof node.parameters === "object" && !Array.isArray(node.parameters)
|
|
340
|
+
? node.parameters
|
|
341
|
+
: null
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function nodeField(node, params, key) {
|
|
345
|
+
return node?.[key] ?? params?.[key]
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function lookupFileMapContent(fileMap, ref) {
|
|
349
|
+
if (!fileMap || typeof ref !== "string" || !ref.trim()) return ""
|
|
350
|
+
const key = ref.trim()
|
|
351
|
+
return fileMap[key] || fileMap[key.replace(/^dypai\//, "")] || fileMap[`dypai/${key}`] || ""
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function collectMissingTableCandidates(ctx, referencedTables, endpoint, file) {
|
|
355
|
+
if (!ctx.schemaTables) return
|
|
356
|
+
for (const table of referencedTables) {
|
|
357
|
+
if (!ctx.schemaTables.has(table)) {
|
|
358
|
+
ctx.suspectedMissingTables.push({ table, endpoint, file })
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
292
363
|
// ─── Rules ──────────────────────────────────────────────────────────────────
|
|
293
364
|
|
|
294
365
|
function ruleUsesJwt(trigger) {
|
|
@@ -307,17 +378,24 @@ function validateEndpoint(entry, ctx) {
|
|
|
307
378
|
|
|
308
379
|
const jwt = ruleUsesJwt(doc.trigger)
|
|
309
380
|
|
|
310
|
-
// Collect all strings
|
|
381
|
+
// Collect all template-rendered strings, including query_file and prompts.
|
|
382
|
+
// Raw code is intentionally skipped: javascript_code.code/code_file may
|
|
383
|
+
// contain normal JS template literals such as `${where.join(" AND ")}`.
|
|
311
384
|
const sources = []
|
|
312
385
|
for (const { path, value } of walkStrings(doc)) {
|
|
386
|
+
if (isRawCodePlaceholderSource("yaml", path)) continue
|
|
313
387
|
sources.push({ source: "yaml", loc: path, value })
|
|
314
388
|
}
|
|
315
389
|
for (const [filePath, content] of Object.entries(fileMap || {})) {
|
|
390
|
+
if (isRawCodePlaceholderSource("file", filePath)) continue
|
|
316
391
|
sources.push({ source: "file", loc: filePath, value: content })
|
|
317
392
|
}
|
|
318
393
|
|
|
319
394
|
// Collect SQL tables referenced (before checking each individually)
|
|
320
395
|
const referencedTables = new Set()
|
|
396
|
+
for (const ref of collectStorageBucketRefs(entry, name, file)) {
|
|
397
|
+
ctx.suspectedStorageBuckets.push(ref)
|
|
398
|
+
}
|
|
321
399
|
|
|
322
400
|
// Aggregate missing input.X refs across the whole endpoint so we emit ONE
|
|
323
401
|
// diagnostic per endpoint instead of N (an endpoint with 11 stray refs
|
|
@@ -457,23 +535,6 @@ function validateEndpoint(entry, ctx) {
|
|
|
457
535
|
}
|
|
458
536
|
}
|
|
459
537
|
|
|
460
|
-
// --- SQL table existence (if schema.sql available) ---
|
|
461
|
-
// Defer the actual error emission to runValidation: it batch-checks all
|
|
462
|
-
// missing tables against the remote in ONE query before deciding what's
|
|
463
|
-
// a real error vs a stale-local-schema. We just collect the misses here
|
|
464
|
-
// along with enough context for runValidation to emit per-endpoint errors.
|
|
465
|
-
if (ctx.schemaTables) {
|
|
466
|
-
for (const table of referencedTables) {
|
|
467
|
-
if (!ctx.schemaTables.has(table)) {
|
|
468
|
-
ctx.suspectedMissingTables.push({
|
|
469
|
-
table,
|
|
470
|
-
endpoint: name,
|
|
471
|
-
file,
|
|
472
|
-
})
|
|
473
|
-
}
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
|
|
477
538
|
// --- Per-node catalog-based validation (unknown type, missing/unknown params) ---
|
|
478
539
|
for (const node of doc.workflow?.nodes || []) {
|
|
479
540
|
if (!node || typeof node !== "object") continue
|
|
@@ -504,30 +565,35 @@ function validateEndpoint(entry, ctx) {
|
|
|
504
565
|
|
|
505
566
|
// dypai_database — coherence checks for the new canonical operations.
|
|
506
567
|
if (nodeType === "dypai_database") {
|
|
507
|
-
const
|
|
568
|
+
const params = nodeParams(node)
|
|
569
|
+
const op = nodeField(node, params, "operation")
|
|
570
|
+
const table = nodeField(node, params, "table") ?? nodeField(node, params, "table_name")
|
|
571
|
+
const query = nodeField(node, params, "query")
|
|
572
|
+
const queryFile = nodeField(node, params, "query_file")
|
|
573
|
+
const insertValue = nodeField(node, params, "insert")
|
|
574
|
+
const updateValue = nodeField(node, params, "update")
|
|
575
|
+
const deleteValue = nodeField(node, params, "delete")
|
|
576
|
+
const whereValue = nodeField(node, params, "where")
|
|
508
577
|
|
|
509
578
|
// Extract referenced tables from real SQL fields ONLY. Doing this here
|
|
510
579
|
// (instead of in the generic walkStrings pass) eliminates the class of
|
|
511
580
|
// false positive where a prompt/comment/label happens to contain words
|
|
512
581
|
// like "INSERT" or "SELECT". Also covers `mutation` (table: <name>)
|
|
513
582
|
// since that's a guaranteed table reference.
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
583
|
+
const sqlTexts = []
|
|
584
|
+
if (typeof query === "string" && query.trim()) sqlTexts.push(query)
|
|
585
|
+
const queryFileSql = lookupFileMapContent(fileMap, queryFile)
|
|
586
|
+
if (queryFileSql) sqlTexts.push(queryFileSql)
|
|
587
|
+
for (const sqlText of sqlTexts) {
|
|
588
|
+
for (const t of extractSqlTables(sqlText)) referencedTables.add(t)
|
|
519
589
|
}
|
|
520
|
-
if (op === "mutation" && typeof
|
|
521
|
-
referencedTables.add(
|
|
590
|
+
if (op === "mutation" && typeof table === "string") {
|
|
591
|
+
referencedTables.add(table)
|
|
522
592
|
}
|
|
523
593
|
// Legacy ops like `select` / `insert` / `update` / `delete` use `table:`
|
|
524
594
|
// as the target table directly.
|
|
525
|
-
if (op && LEGACY_OPS_THAT_USE_TABLE_FIELD.has(op) && typeof
|
|
526
|
-
referencedTables.add(
|
|
527
|
-
}
|
|
528
|
-
// Resolved query_file content also counts as SQL (loaded by the codec).
|
|
529
|
-
if (typeof node.query === "string" && node.query.length > 0) {
|
|
530
|
-
for (const t of extractSqlTables(node.query)) referencedTables.add(t)
|
|
595
|
+
if (op && LEGACY_OPS_THAT_USE_TABLE_FIELD.has(op) && typeof table === "string") {
|
|
596
|
+
referencedTables.add(table)
|
|
531
597
|
}
|
|
532
598
|
const LEGACY_OPS = new Set(["select", "insert", "update", "delete", "upsert", "aggregate", "copy_to", "custom_query"])
|
|
533
599
|
if (op && LEGACY_OPS.has(op)) {
|
|
@@ -549,7 +615,7 @@ function validateEndpoint(entry, ctx) {
|
|
|
549
615
|
// Fields like `query`, `query_file`, `params` belong to `operation: query`
|
|
550
616
|
// and are silently ignored here — surface as ERROR so the agent reroutes.
|
|
551
617
|
if (op === "mutation") {
|
|
552
|
-
if (!
|
|
618
|
+
if (!table) {
|
|
553
619
|
diagnostics.push({
|
|
554
620
|
severity: "error",
|
|
555
621
|
rule: "mutation_missing_table",
|
|
@@ -558,9 +624,9 @@ function validateEndpoint(entry, ctx) {
|
|
|
558
624
|
fix_hint: `Add 'table: <name>'. mutation also needs exactly one of: insert / update / delete.`,
|
|
559
625
|
})
|
|
560
626
|
}
|
|
561
|
-
const wantsInsert =
|
|
562
|
-
const wantsUpdate =
|
|
563
|
-
const wantsDelete =
|
|
627
|
+
const wantsInsert = insertValue !== undefined && insertValue !== null
|
|
628
|
+
const wantsUpdate = updateValue !== undefined && updateValue !== null
|
|
629
|
+
const wantsDelete = deleteValue === true
|
|
564
630
|
const opCount = [wantsInsert, wantsUpdate, wantsDelete].filter(Boolean).length
|
|
565
631
|
if (opCount === 0) {
|
|
566
632
|
diagnostics.push({
|
|
@@ -582,7 +648,7 @@ function validateEndpoint(entry, ctx) {
|
|
|
582
648
|
if (wantsUpdate || wantsDelete) {
|
|
583
649
|
// `where: {}` is just as dangerous as omitting `where:` entirely — both
|
|
584
650
|
// produce an unconstrained UPDATE/DELETE in the engine.
|
|
585
|
-
const whereVal =
|
|
651
|
+
const whereVal = whereValue
|
|
586
652
|
const whereIsEmpty =
|
|
587
653
|
whereVal === undefined ||
|
|
588
654
|
whereVal === null ||
|
|
@@ -603,7 +669,7 @@ function validateEndpoint(entry, ctx) {
|
|
|
603
669
|
// Foreign fields that belong to `operation: query`
|
|
604
670
|
const QUERY_ONLY = ["query", "query_file", "params"]
|
|
605
671
|
for (const k of QUERY_ONLY) {
|
|
606
|
-
if (node[k] !== undefined) {
|
|
672
|
+
if (node[k] !== undefined || params?.[k] !== undefined) {
|
|
607
673
|
diagnostics.push({
|
|
608
674
|
severity: "error",
|
|
609
675
|
rule: "mutation_with_query_field",
|
|
@@ -620,7 +686,7 @@ function validateEndpoint(entry, ctx) {
|
|
|
620
686
|
// Fields like `table`, `insert`, `update`, `delete`, `where`, `on_conflict`,
|
|
621
687
|
// `returning` belong to `operation: mutation` and are ignored here.
|
|
622
688
|
if (op === "query") {
|
|
623
|
-
if (!
|
|
689
|
+
if (!query && !queryFile) {
|
|
624
690
|
diagnostics.push({
|
|
625
691
|
severity: "error",
|
|
626
692
|
rule: "query_missing_sql",
|
|
@@ -630,7 +696,10 @@ function validateEndpoint(entry, ctx) {
|
|
|
630
696
|
})
|
|
631
697
|
}
|
|
632
698
|
const MUTATION_ONLY = ["table", "insert", "update", "delete", "where", "on_conflict", "returning"]
|
|
633
|
-
const foreign = MUTATION_ONLY.filter(k =>
|
|
699
|
+
const foreign = MUTATION_ONLY.filter(k => {
|
|
700
|
+
const value = node[k] ?? params?.[k]
|
|
701
|
+
return value !== undefined && value !== false
|
|
702
|
+
})
|
|
634
703
|
if (foreign.length > 0) {
|
|
635
704
|
diagnostics.push({
|
|
636
705
|
severity: "error",
|
|
@@ -855,6 +924,11 @@ function validateEndpoint(entry, ctx) {
|
|
|
855
924
|
}
|
|
856
925
|
}
|
|
857
926
|
|
|
927
|
+
// --- SQL table existence (if schema.sql available) ---
|
|
928
|
+
// Defer actual error emission to runValidation: it batch-checks all missing
|
|
929
|
+
// tables against the remote before deciding whether local schema.sql is stale.
|
|
930
|
+
collectMissingTableCandidates(ctx, referencedTables, name, file)
|
|
931
|
+
|
|
858
932
|
// --- Credential references ---
|
|
859
933
|
for (const node of doc.workflow?.nodes || []) {
|
|
860
934
|
if (!node || typeof node !== "object") continue
|
|
@@ -982,14 +1056,15 @@ function validateEndpoint(entry, ctx) {
|
|
|
982
1056
|
for (const node of allNodes) {
|
|
983
1057
|
const nodeType = node?.type ?? node?.node_type
|
|
984
1058
|
if (nodeType !== "dypai_database") continue
|
|
985
|
-
const
|
|
1059
|
+
const params = nodeParams(node)
|
|
1060
|
+
const op = nodeField(node, params, "operation")
|
|
986
1061
|
let writeKind = null
|
|
987
1062
|
if (op === "mutation") {
|
|
988
|
-
if (node
|
|
989
|
-
else if (node
|
|
990
|
-
else if (node
|
|
1063
|
+
if (nodeField(node, params, "insert") !== undefined && nodeField(node, params, "insert") !== null) writeKind = "INSERT"
|
|
1064
|
+
else if (nodeField(node, params, "update") !== undefined && nodeField(node, params, "update") !== null) writeKind = "UPDATE"
|
|
1065
|
+
else if (nodeField(node, params, "delete") === true) writeKind = "DELETE"
|
|
991
1066
|
} else if (op === "query") {
|
|
992
|
-
const sql = String(node
|
|
1067
|
+
const sql = String(nodeField(node, params, "query") || "")
|
|
993
1068
|
if (/\b(INSERT|UPDATE|DELETE|TRUNCATE|DROP|ALTER|CREATE)\b/i.test(sql)) {
|
|
994
1069
|
writeKind = "write SQL"
|
|
995
1070
|
}
|
|
@@ -1069,6 +1144,68 @@ async function verifyTablesInRemote(missingTables, projectId) {
|
|
|
1069
1144
|
}
|
|
1070
1145
|
}
|
|
1071
1146
|
|
|
1147
|
+
function bucketNamesFromStorageList(payload) {
|
|
1148
|
+
const names = new Set()
|
|
1149
|
+
const add = (value) => {
|
|
1150
|
+
if (typeof value === "string" && value.trim()) names.add(value.trim())
|
|
1151
|
+
}
|
|
1152
|
+
const candidates = []
|
|
1153
|
+
if (Array.isArray(payload)) {
|
|
1154
|
+
candidates.push(payload)
|
|
1155
|
+
} else if (payload && typeof payload === "object") {
|
|
1156
|
+
for (const key of ["buckets", "data", "items", "results"]) {
|
|
1157
|
+
if (Array.isArray(payload[key])) candidates.push(payload[key])
|
|
1158
|
+
}
|
|
1159
|
+
if (!candidates.length) candidates.push([payload])
|
|
1160
|
+
}
|
|
1161
|
+
for (const list of candidates) {
|
|
1162
|
+
for (const item of list) {
|
|
1163
|
+
if (typeof item === "string") {
|
|
1164
|
+
add(item)
|
|
1165
|
+
} else if (item && typeof item === "object") {
|
|
1166
|
+
add(item.name)
|
|
1167
|
+
add(item.bucket)
|
|
1168
|
+
add(item.bucket_name)
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
return names
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
async function verifyStorageBucketsInRemote(bucketRefs, projectId) {
|
|
1176
|
+
if (!bucketRefs.length) return []
|
|
1177
|
+
const uniqueRefs = new Map(bucketRefs.map(ref => [`${ref.bucket}|${ref.endpoint}|${ref.file}|${ref.loc}`, ref]))
|
|
1178
|
+
const refs = [...uniqueRefs.values()]
|
|
1179
|
+
const bucketNames = [...new Set(refs.map(ref => ref.bucket))].sort()
|
|
1180
|
+
try {
|
|
1181
|
+
const args = { operation: "list" }
|
|
1182
|
+
if (projectId) args.project_id = projectId
|
|
1183
|
+
const result = await proxyToolCall("manage_storage", args)
|
|
1184
|
+
const existing = bucketNamesFromStorageList(result)
|
|
1185
|
+
return refs
|
|
1186
|
+
.filter(ref => !existing.has(ref.bucket))
|
|
1187
|
+
.map(ref => ({
|
|
1188
|
+
severity: "error",
|
|
1189
|
+
rule: "storage_bucket_missing",
|
|
1190
|
+
endpoint: ref.endpoint,
|
|
1191
|
+
file: ref.file,
|
|
1192
|
+
loc: ref.loc,
|
|
1193
|
+
message: `Storage bucket '${ref.bucket}' is referenced by endpoint '${ref.endpoint}', but it does not exist in this project.`,
|
|
1194
|
+
fix_hint:
|
|
1195
|
+
`Create it before testing/pushing this endpoint: ` +
|
|
1196
|
+
`manage_storage({ operation: "create", name: "${ref.bucket}", public: false }). ` +
|
|
1197
|
+
`Use public: true only for public assets; user documents/attachments should stay private.`,
|
|
1198
|
+
}))
|
|
1199
|
+
} catch (e) {
|
|
1200
|
+
return [{
|
|
1201
|
+
severity: "warn",
|
|
1202
|
+
rule: "storage_bucket_check_failed",
|
|
1203
|
+
message: `Could not verify storage buckets: ${bucketNames.join(", ")}.`,
|
|
1204
|
+
fix_hint: `Retry manage_storage({ operation: "list" }) or create the expected bucket before testing upload/storage endpoints. Detail: ${e.message}`,
|
|
1205
|
+
}]
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1072
1209
|
/**
|
|
1073
1210
|
* Refresh dypai/schema.sql from the remote. Best-effort — if it fails the
|
|
1074
1211
|
* caller continues with the existing local schema (just logs the warning).
|
|
@@ -1116,6 +1253,7 @@ export async function runValidation(rootDir, projectId) {
|
|
|
1116
1253
|
// Collected during per-endpoint pass; resolved against the remote AFTER
|
|
1117
1254
|
// all endpoints are checked (one batch query instead of N).
|
|
1118
1255
|
suspectedMissingTables: [],
|
|
1256
|
+
suspectedStorageBuckets: [],
|
|
1119
1257
|
}
|
|
1120
1258
|
|
|
1121
1259
|
const diagnostics = []
|
|
@@ -1185,6 +1323,7 @@ export async function runValidation(rootDir, projectId) {
|
|
|
1185
1323
|
|
|
1186
1324
|
// Realtime YAML rules
|
|
1187
1325
|
diagnostics.push(...await validateRealtime(rootDir, ctx))
|
|
1326
|
+
diagnostics.push(...await verifyStorageBucketsInRemote(ctx.suspectedStorageBuckets, targetProjectId))
|
|
1188
1327
|
|
|
1189
1328
|
// Surface any file-read errors too
|
|
1190
1329
|
for (const err of local.errors || []) {
|
|
@@ -1219,7 +1358,7 @@ export const dypaiValidateTool = {
|
|
|
1219
1358
|
name: "dypai_validate",
|
|
1220
1359
|
description:
|
|
1221
1360
|
"Lint the local dypai/ folder BEFORE pushing. Catches ${input.x} / ${nodes.x.y} / ${current_user_*} refs that don't resolve, " +
|
|
1222
|
-
"SQL tables not in schema.sql, credentials that don't exist remotely, and agent tool refs to non-tool endpoints. " +
|
|
1361
|
+
"SQL tables not in schema.sql, missing storage buckets, credentials that don't exist remotely, and agent tool refs to non-tool endpoints. " +
|
|
1223
1362
|
"Run this after editing YAMLs to catch typos pre-flight. Push already calls it by default — pass skip_validation:true to override.",
|
|
1224
1363
|
inputSchema: {
|
|
1225
1364
|
type: "object",
|