@dypai-ai/mcp 1.6.16 → 1.6.18

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.
@@ -14,11 +14,45 @@ import { deployFromSource } from "./deploy.js"
14
14
  import { syncFromRemote } from "./sync.js"
15
15
  import { proxyToolCall } from "./proxy.js"
16
16
 
17
+ function isSuccessful(result) {
18
+ return result && result.success !== false && !result.error
19
+ }
20
+
21
+ export async function pullProjectSource({
22
+ project_id,
23
+ targetDirectory,
24
+ overwrite,
25
+ source_ref,
26
+ } = {}, deps = {}) {
27
+ const syncSource = deps.syncFromRemote ?? syncFromRemote
28
+
29
+ const source = await syncSource({
30
+ project_id,
31
+ targetDirectory,
32
+ overwrite: !!overwrite,
33
+ source_ref: source_ref || undefined,
34
+ })
35
+
36
+ if (!isSuccessful(source)) {
37
+ return {
38
+ ...source,
39
+ source_sync: "failed",
40
+ }
41
+ }
42
+
43
+ return {
44
+ ...source,
45
+ source_files_written: source.files_written,
46
+ source_sync: "success",
47
+ }
48
+ }
49
+
17
50
  export const dypaiPullTool = {
18
51
  name: "dypai_pull",
19
52
  description:
20
- "NORMAL FIRST STEP — pull/sync the editable DYPAI project source from Git/Studio into a local workspace. " +
21
- "This downloads the Studio source branch (src/, package.json, public/, and committed dypai/ such as flows/types/lib). " +
53
+ "NORMAL FIRST STEP — pull/sync the editable DYPAI project source into a local workspace. " +
54
+ "This downloads the Studio source branch (src/, package.json, public/, committed dypai/ source). " +
55
+ "It does not recreate endpoints from platform metadata: Git/Studio source is authoritative. " +
22
56
  "Use this when starting work on an existing project or after create_project. " +
23
57
  "This is a safe local download: it preserves local-only files like .env, node_modules, and .vscode.",
24
58
  inputSchema: {
@@ -51,11 +85,11 @@ export const dypaiPullTool = {
51
85
  if (!targetDirectory) {
52
86
  return { success: false, error: "targetDirectory is required (absolute path where the source will be written)." }
53
87
  }
54
- return await syncFromRemote({
88
+ return await pullProjectSource({
55
89
  project_id,
56
90
  targetDirectory,
57
- overwrite: !!overwrite,
58
- source_ref: source_ref || undefined,
91
+ overwrite,
92
+ source_ref,
59
93
  })
60
94
  },
61
95
  }
@@ -229,11 +263,11 @@ export const manageFrontendTool = {
229
263
  if (!targetDirectory) {
230
264
  return { success: false, error: "operation 'sync' requires 'targetDirectory' (absolute path where the source will be written)." }
231
265
  }
232
- return await syncFromRemote({
266
+ return await pullProjectSource({
233
267
  project_id,
234
268
  targetDirectory,
235
- overwrite: !!overwrite,
236
- source_ref: source_ref || undefined,
269
+ overwrite,
270
+ source_ref,
237
271
  })
238
272
 
239
273
  case "status":
@@ -50,7 +50,8 @@ export const dypaiDeployProductionTool = {
50
50
  name: "dypai_deploy_production",
51
51
  description:
52
52
  "PUBLISH PRODUCTION — make the saved DYPAI project live. Requires confirm:true. " +
53
- "Publishes/merges pending backend drafts first, then deploys the frontend to production. " +
53
+ "By default publishes/merges pending backend drafts first, then deploys the frontend to production. " +
54
+ "Use target:'backend' for backend-only Flow/Automation releases; use target:'frontend' only when backend drafts must stay pending. " +
54
55
  "Use only after the user explicitly approves going live. For saving work without production, use dypai_push.",
55
56
  inputSchema: {
56
57
  type: "object",
@@ -77,6 +78,12 @@ export const dypaiDeployProductionTool = {
77
78
  description: "Frontend only. Re-send all files even if the local manifest says nothing changed.",
78
79
  default: false,
79
80
  },
81
+ target: {
82
+ type: "string",
83
+ enum: ["all", "backend", "frontend"],
84
+ description: "What to publish. all = backend drafts + frontend production. backend = publish only pending backend drafts. frontend = deploy only frontend source.",
85
+ default: "all",
86
+ },
80
87
  confirm: {
81
88
  type: "boolean",
82
89
  description: "Required true. This publishes production/live.",
@@ -86,22 +93,29 @@ export const dypaiDeployProductionTool = {
86
93
  required: ["confirm"],
87
94
  },
88
95
 
89
- async execute({ project_id, workspace_root, sourceDirectory, root_dir = "./dypai", force = false, confirm = false } = {}) {
96
+ async execute({ project_id, workspace_root, sourceDirectory, root_dir = "./dypai", force = false, target = "all", confirm = false } = {}) {
90
97
  const { workspaceRoot, rootDir, source } = resolveWorkspace({ workspace_root, sourceDirectory, root_dir })
91
98
  const targetProjectId = project_id || readProjectIdFromConfig(rootDir)
99
+ const publishTarget = ["all", "backend", "frontend"].includes(target) ? target : "all"
100
+ const publishBackend = publishTarget === "all" || publishTarget === "backend"
101
+ const deployFrontend = publishTarget === "all" || publishTarget === "frontend"
92
102
 
93
103
  if (confirm !== true) {
94
104
  return {
95
105
  confirmation_required: true,
96
106
  live_changed: false,
97
107
  summary:
98
- "This will publish pending backend drafts and deploy the frontend to PRODUCTION. " +
99
- "Get explicit user approval, then call again with confirm:true.",
108
+ publishTarget === "backend"
109
+ ? "This will publish pending backend drafts to PRODUCTION without deploying frontend. Get explicit user approval, then call again with confirm:true."
110
+ : publishTarget === "frontend"
111
+ ? "This will deploy the frontend to PRODUCTION without publishing pending backend drafts. Get explicit user approval, then call again with confirm:true."
112
+ : "This will publish pending backend drafts and deploy the frontend to PRODUCTION. Get explicit user approval, then call again with confirm:true.",
100
113
  next_call: {
101
114
  tool: "dypai_deploy_production",
102
115
  ...(targetProjectId ? { project_id: targetProjectId } : {}),
103
116
  workspace_root: workspaceRoot,
104
117
  ...(force ? { force: true } : {}),
118
+ ...(publishTarget !== "all" ? { target: publishTarget } : {}),
105
119
  confirm: true,
106
120
  },
107
121
  }
@@ -118,49 +132,57 @@ export const dypaiDeployProductionTool = {
118
132
  }
119
133
  }
120
134
 
121
- const draftsList = await proxyToolCall("manage_drafts", {
122
- project_id: targetProjectId,
123
- operation: "list",
124
- })
125
- const pendingDrafts = draftCount(draftsList)
126
- if (pendingDrafts == null) {
127
- return {
128
- success: false,
129
- phase: "list_backend_drafts",
130
- backend: draftsList,
131
- frontend: { skipped: true, reason: "could_not_list_backend_drafts" },
132
- live_changed: false,
133
- }
134
- }
135
-
135
+ let pendingDrafts = 0
136
136
  let backend = {
137
137
  skipped: true,
138
- reason: "no_pending_drafts",
138
+ reason: publishBackend ? "no_pending_drafts" : "target_frontend",
139
139
  pending_drafts: 0,
140
140
  }
141
- if (pendingDrafts > 0) {
142
- backend = await proxyToolCall("manage_drafts", {
141
+
142
+ if (publishBackend) {
143
+ const draftsList = await proxyToolCall("manage_drafts", {
143
144
  project_id: targetProjectId,
144
- operation: "publish",
145
- confirm: true,
145
+ operation: "list",
146
146
  })
147
- if (!isSuccessful(backend)) {
147
+ pendingDrafts = draftCount(draftsList)
148
+ if (pendingDrafts == null) {
148
149
  return {
149
150
  success: false,
150
- phase: "publish_backend_drafts",
151
- backend,
152
- frontend: { skipped: true, reason: "backend_publish_failed" },
151
+ phase: "list_backend_drafts",
152
+ target: publishTarget,
153
+ backend: draftsList,
154
+ frontend: { skipped: true, reason: "could_not_list_backend_drafts" },
153
155
  live_changed: false,
154
156
  }
155
157
  }
158
+
159
+ if (pendingDrafts > 0) {
160
+ backend = await proxyToolCall("manage_drafts", {
161
+ project_id: targetProjectId,
162
+ operation: "publish",
163
+ confirm: true,
164
+ })
165
+ if (!isSuccessful(backend)) {
166
+ return {
167
+ success: false,
168
+ phase: "publish_backend_drafts",
169
+ target: publishTarget,
170
+ backend,
171
+ frontend: { skipped: true, reason: "backend_publish_failed" },
172
+ live_changed: false,
173
+ }
174
+ }
175
+ }
156
176
  }
157
177
 
158
178
  let frontend = {
159
179
  skipped: true,
160
- reason: "no_local_package_json",
161
- note: "Temporary limitation: frontend production deploy currently needs local sourceDirectory/workspace_root. API deploy-from-Studio-HEAD is the next backend change.",
180
+ reason: deployFrontend ? "no_local_package_json" : "target_backend",
181
+ note: deployFrontend
182
+ ? "Temporary limitation: frontend production deploy currently needs local sourceDirectory/workspace_root. API deploy-from-Studio-HEAD is the next backend change."
183
+ : undefined,
162
184
  }
163
- if (existsSync(join(workspaceRoot, "package.json"))) {
185
+ if (deployFrontend && existsSync(join(workspaceRoot, "package.json"))) {
164
186
  frontend = await deployFromSource({
165
187
  sourceDirectory: workspaceRoot,
166
188
  project_id: targetProjectId,
@@ -174,6 +196,7 @@ export const dypaiDeployProductionTool = {
174
196
  success: frontendOk,
175
197
  project_id: targetProjectId,
176
198
  workspace_root: workspaceRoot,
199
+ target: publishTarget,
177
200
  backend,
178
201
  frontend,
179
202
  backend_published: pendingDrafts > 0 && isSuccessful(backend),
@@ -37,11 +37,6 @@ function isSuccessful(result) {
37
37
  function sanitizeLegacyShipText(value) {
38
38
  if (typeof value === "string") {
39
39
  return value
40
- .replace(/manage_drafts\(operation:'publish', confirm:true\)/g, "dypai_deploy_production(confirm:true)")
41
- .replace(/manage_drafts\(operation:"publish", confirm:true\)/g, "dypai_deploy_production(confirm:true)")
42
- .replace(/manage_drafts\(operation:'list'\)/g, "dypai_diff or dypai_test_endpoint(mode:'draft')")
43
- .replace(/manage_drafts\(operation:"list"\)/g, "dypai_diff or dypai_test_endpoint(mode:'draft')")
44
- .replace(/\bmanage_drafts\b/g, "dypai_deploy_production")
45
40
  .replace(/\bmanage_frontend\b/g, "dypai_push")
46
41
  .replace(/Frontend code is not affected[^.]*\./g, "Frontend source is saved by the project-level dypai_push wrapper.")
47
42
  }
@@ -255,6 +255,8 @@ export async function uploadFile({
255
255
  success: true,
256
256
  bucket,
257
257
  name: verified?.name || filename,
258
+ storage_path: verified?.name || filename,
259
+ file_path,
258
260
  object_id: verified?.id || null,
259
261
  size_bytes: stat.size,
260
262
  size_human: formatBytes(stat.size),
@@ -1,26 +1,13 @@
1
- /**
2
- * dypai_diff — preview what dypai_push would change. Read-only, safe.
3
- *
4
- * The picture has THREE layers, not two:
5
- * - LOCAL : what's on disk in dypai/
6
- * - DRAFT : what's already been staged via prior pushes (system.config_drafts)
7
- * - LIVE : what the engine actually serves at <project_id>.dypai.dev
8
- *
9
- * The diff body reports LOCAL → EFFECTIVE_REMOTE (live + pending drafts) —
10
- * what `dypai_push` will actually queue. We additionally surface the DRAFT
11
- * layer metadata so the agent can spot overlap and publish/discard decisions.
12
- */
13
-
14
1
  import { resolve as resolvePath } from "path"
15
- import { fetchRemoteState, readLocalEffectiveState, readLocalStateSnapshot, readLocalConfig, computePlan, buildEffectiveRemoteState } from "./planner.js"
16
- import { proxyToolCall } from "../proxy.js"
2
+ import { readLocalConfig } from "./planner.js"
3
+ import { diffBackendSource } from "./source-diff.js"
17
4
 
18
5
  export const dypaiDiffTool = {
19
6
  name: "dypai_diff",
20
7
  description:
21
- "Compare the local ./dypai/ folder against the remote project state without applying anything. " +
22
- "Returns a plan: endpoints to create, update (with changed fields), delete, and unchanged. " +
23
- "Always call this before dypai_push to preview changes safely.",
8
+ "Fast source diff for the local ./dypai/ folder. " +
9
+ "It does not compile, call remote services, or stage changes. " +
10
+ "Use dypai_push to compile, validate, save backend drafts, and regenerate types.",
24
11
  inputSchema: {
25
12
  type: "object",
26
13
  properties: {
@@ -35,138 +22,49 @@ export const dypaiDiffTool = {
35
22
  },
36
23
  delete_orphans: {
37
24
  type: "boolean",
38
- description: "If true, endpoints present in remote but missing locally are queued for deletion. Default: false (safe).",
25
+ description: "Ignored by dypai_diff. Deletions are only applied by dypai_push when explicitly requested.",
39
26
  default: false,
40
27
  },
41
28
  },
42
29
  },
43
30
 
44
- async execute({ project_id, root_dir = "./dypai", delete_orphans = false } = {}) {
31
+ async execute({ project_id, root_dir = "./dypai" } = {}) {
45
32
  const rootDir = resolvePath(process.cwd(), root_dir)
46
33
 
47
34
  const config = await readLocalConfig(rootDir)
48
35
  const targetProjectId = project_id || config?.project_id || null
36
+ const diff = await diffBackendSource(rootDir)
49
37
 
50
- const [local, remote, stateSnapshot, draftsResult] = await Promise.all([
51
- readLocalEffectiveState(rootDir, targetProjectId),
52
- fetchRemoteState(targetProjectId),
53
- readLocalStateSnapshot(rootDir),
54
- // Pending drafts. Cheap on dev (always 0); on prod surfaces what's
55
- // already staged so the agent can reason about overlap with the local
56
- // change set. Old engines without the drafts API silently fall back.
57
- proxyToolCall("manage_drafts", targetProjectId
58
- ? { project_id: targetProjectId, operation: "list" }
59
- : { operation: "list" }
60
- ).catch(() => ({ total: 0, drafts: [], _unavailable: true })),
61
- ])
62
-
63
- if (local.errors.length) {
64
- return {
65
- success: false,
66
- phase: "read_local",
67
- errors: local.errors,
68
- hint: "Fix the YAML/file errors before running diff again.",
69
- }
70
- }
71
-
72
- if (!remote || !remote.mapsCtx) {
38
+ if (!diff.ok) {
73
39
  return {
74
40
  success: false,
75
- phase: "fetch_remote",
76
- error: "Remote state could not be fetched (mapsCtx missing). Check your DYPAI_TOKEN and project_id.",
41
+ phase: "source_diff",
42
+ reason: diff.reason,
43
+ error: diff.error,
44
+ hint: "Fix the local dypai/.dypai/backend-baseline.json or run dypai_pull/sync again.",
77
45
  }
78
46
  }
79
47
 
80
- const effectiveRemote = buildEffectiveRemoteState(remote, draftsResult)
81
- const plan = computePlan(local, effectiveRemote, remote.mapsCtx, {
82
- deleteOrphans: delete_orphans,
83
- stateSnapshot,
84
- liveRemote: remote,
85
- })
86
-
87
- const groupChanges = (plan.groups?.create?.length || 0) + (plan.groups?.delete?.length || 0)
88
- const totalChanges = plan.create.length + plan.update.length + plan.delete.length + groupChanges
89
- const hasConflicts = (plan.warnings || []).some(w => w.type === "remote_changed_since_pull")
90
-
91
- // ─── Drafts overlay ───────────────────────────────────────────────────
92
- // Build a `pending_drafts` view that tells the agent what's already
93
- // staged AND highlights the overlap with this diff's local-change set.
94
- // The overlap is the agent-relevant signal: same endpoint touched on
95
- // BOTH sides means pushing now will overwrite the existing draft.
96
- const draftItems = Array.isArray(draftsResult?.drafts) ? draftsResult.drafts : []
97
- const draftCount = draftsResult?.total || 0
98
-
99
- const localChangeNames = new Set([
100
- ...plan.create.map(p => p.name),
101
- ...plan.update.map(p => p.name),
102
- ...plan.delete.map(p => p.name),
103
- ])
104
-
105
- const overlap = draftItems
106
- .filter(d => d.resource_type === "endpoint" && localChangeNames.has(d.resource_name))
107
- .map(d => ({
108
- endpoint: d.resource_name,
109
- existing_draft_op: d.op,
110
- local_change_op: plan.create.find(p => p.name === d.resource_name) ? "create"
111
- : plan.update.find(p => p.name === d.resource_name) ? "update"
112
- : "delete",
113
- }))
114
-
115
- const pendingDrafts = draftCount > 0 ? {
116
- total: draftCount,
117
- counts_by_type: draftsResult?.counts_by_type || {},
118
- items: draftItems
119
- .map(d => ({
120
- resource_type: d.resource_type,
121
- resource_name: d.resource_name,
122
- op: d.op,
123
- }))
124
- .sort((a, b) => (a.resource_name || "").localeCompare(b.resource_name || "")),
125
- // Endpoints that are BOTH staged AND modified locally — pushing again
126
- // replaces the existing draft. Empty array = no overlap (clean signal).
127
- overlap_with_local: overlap,
128
- } : null
129
-
130
- // ─── Hint priority ────────────────────────────────────────────────────
131
- // 1. Conflicts and missing creds always win (block push).
132
- // 2. Overlap is next: explicitly tell the agent that re-push overwrites.
133
- // 3. Pending drafts (no overlap): suggest publishing or discarding before
134
- // stacking more on top.
135
- const hint = hasConflicts
136
- ? "Remote changed since your last pull — dypai_pull first, or dypai_push will be blocked."
137
- : (plan.warnings || []).some(w => w.type === "missing_credential")
138
- ? "Some YAMLs reference credentials missing remotely. Create them in the dashboard before pushing."
139
- : overlap.length > 0
140
- ? `${overlap.length} endpoint(s) have BOTH a pending draft AND a local change — dypai_push will REPLACE the existing draft with the new version. Review pending_drafts.overlap_with_local before pushing.`
141
- : totalChanges === 0 && draftCount > 0
142
- ? `${draftCount} pending backend change(s) — local matches what's already saved. Test with dypai_test_endpoint(mode:'draft'), then use dypai_deploy_production(confirm:true) when the user approves going live.`
143
- : draftCount > 0 && totalChanges > 0
144
- ? `${draftCount} backend change(s) already pending; this diff would save ${totalChanges} more. Keep testing in preview, or use dypai_deploy_production(confirm:true) after explicit approval.`
145
- : draftCount > 0
146
- ? `${draftCount} pending backend change(s) — no local changes to push. Test with dypai_test_endpoint(mode:'draft'), then use dypai_deploy_production(confirm:true) after explicit approval.`
147
- : undefined
48
+ const totalChanges = diff.summary.create + diff.summary.update + diff.summary.delete
148
49
 
149
50
  return {
150
51
  success: true,
52
+ mode: "source",
53
+ project_id: targetProjectId,
54
+ baseline: diff.baseline,
151
55
  summary: {
152
- create: plan.create.length,
153
- update: plan.update.length,
154
- delete: plan.delete.length,
155
- unchanged: plan.unchanged.length,
156
- orphans_ignored: plan.orphansIgnored?.length || 0,
157
- groups_create: plan.groups?.create?.length || 0,
158
- groups_delete: plan.groups?.delete?.length || 0,
159
- groups_unchanged: plan.groups?.unchanged?.length || 0,
160
- warnings: plan.warnings?.length || 0,
161
- pending_drafts: draftCount,
162
- drafts_overlap_with_local: overlap.length,
56
+ create: diff.summary.create,
57
+ update: diff.summary.update,
58
+ delete: diff.summary.delete,
59
+ unchanged: diff.summary.unchanged,
163
60
  },
164
- // Only include plan details when there are actual changes or warnings
165
- plan: (totalChanges > 0 || plan.warnings?.length) ? plan : undefined,
166
- // Always present so the agent knows the key exists. `null` = nothing
167
- // staged (cleaner signal than omitted-or-zero ambiguity).
168
- pending_drafts: pendingDrafts,
169
- hint,
61
+ source_changes: totalChanges > 0 ? diff.files : undefined,
62
+ requires_push: totalChanges > 0,
63
+ requires_compile: totalChanges > 0,
64
+ pending_drafts: null,
65
+ hint: totalChanges > 0
66
+ ? "Local backend source changed. Run dypai_push to compile, validate, save preview drafts, and regenerate types."
67
+ : "No local backend source changes. Nothing to push.",
170
68
  }
171
69
  },
172
70
  }
@@ -40,6 +40,15 @@ function parseMaybeJson(v) {
40
40
  try { return JSON.parse(v) } catch { return v }
41
41
  }
42
42
 
43
+ function isBackendSourcePath(path) {
44
+ if (typeof path !== "string") return false
45
+ return (
46
+ (path.startsWith("dypai/flows/") && /\.flow\.ts$/i.test(path)) ||
47
+ (path.startsWith("dypai/automations/") && /\.automation\.ts$/i.test(path)) ||
48
+ (path.startsWith("dypai/endpoints/") && /\.(ya?ml)$/i.test(path))
49
+ )
50
+ }
51
+
43
52
  function hydrateRow(row) {
44
53
  return {
45
54
  ...row,
@@ -453,6 +462,7 @@ export async function readLocalEffectiveState(rootDir, projectId = null, deps =
453
462
 
454
463
  const payloads = Array.isArray(bulkResult.data?.payloads) ? bulkResult.data.payloads : []
455
464
  const diagnostics = Array.isArray(bulkResult.data?.diagnostics) ? bulkResult.data.diagnostics : []
465
+ const rejected = Array.isArray(bulkResult.data?.rejected) ? bulkResult.data.rejected : []
456
466
  const byName = {}
457
467
  const shadowed = []
458
468
  const flowNames = new Set()
@@ -484,12 +494,21 @@ export async function readLocalEffectiveState(rootDir, projectId = null, deps =
484
494
  })
485
495
  }
486
496
 
497
+ for (const item of rejected) {
498
+ if (!isBackendSourcePath(item?.path)) continue
499
+ yamlState.errors.push({
500
+ file: item.path,
501
+ error: `Backend compiler rejected source file: ${item.reason || "rejected"}`,
502
+ rule: "backend_compiler_rejected_source",
503
+ })
504
+ }
505
+
487
506
  for (const [name, entry] of Object.entries(yamlState.byName)) {
488
507
  if (flowNames.has(name)) continue
489
508
  byName[name] = { ...entry, source: "legacy-yaml" }
490
509
  }
491
510
 
492
- return { byName, errors: yamlState.errors, shadowed, runnerWarning: null }
511
+ return { byName, errors: yamlState.errors, shadowed, runnerWarning: null, automations: bulkResult.data?.automations ?? null }
493
512
  }
494
513
 
495
514
  // ─── Normalization + diff ──────────────────────────────────────────────────
@@ -84,7 +84,7 @@ function suspiciousPathWarning(resolvedPath, source) {
84
84
 
85
85
  // Subfolders that are always created so the layout is predictable. An agent
86
86
  // never has to check "does this folder exist?" before writing a new SQL/prompt/JS file.
87
- const CANONICAL_SUBDIRS = ["endpoints", "flows", "sql", "prompts", "code", "migrations"]
87
+ const CANONICAL_SUBDIRS = ["endpoints", "flows", "automations", "sql", "prompts", "code", "migrations"]
88
88
 
89
89
  const README_CONTENT = `# dypai/
90
90
 
@@ -97,6 +97,8 @@ Declarative snapshot of your DYPAI project's backend.
97
97
  Subfolders represent endpoint groups, e.g. \`endpoints/Admin/foo.yaml\` → group "Admin".
98
98
  - \`flows/\` — TypeScript flow definitions (\`*.flow.ts\`). **Authoritative for new work.**
99
99
  When a flow exists for an endpoint name, it wins over legacy YAML at validate/push/test time.
100
+ - \`automations/\` — TypeScript automation definitions (\`*.automation.ts\`) for scheduled/webhook business processes.
101
+ Automations compile to normal backend workflows, but also declare setup requirements, notifications, and history metadata for the organization Automations page.
100
102
  - \`capability-catalog.json\` — grouped capability catalog (synced by \`dypai_pull\`).
101
103
  - \`capability-brief.md\` — compact capability index for agents.
102
104
  - \`node-catalog.json\` — every node_type with input/output schemas.
@@ -108,7 +110,7 @@ Declarative snapshot of your DYPAI project's backend.
108
110
  ## Workflow
109
111
 
110
112
  1. \`dypai_pull\` to snapshot the remote state into this folder
111
- 2. Edit \`flows/*.flow.ts\` (preferred) or legacy YAML, SQL, prompts, code
113
+ 2. Edit \`flows/*.flow.ts\` for callable endpoints, \`automations/*.automation.ts\` for scheduled/webhook processes, or legacy YAML, SQL, prompts, code
112
114
  3. \`dypai_validate\` / \`dypai_test_endpoint\` before push
113
115
  4. \`dypai_diff\` to preview changes
114
116
  5. \`dypai_push\` to apply to the remote
@@ -523,17 +525,36 @@ function isFlowTsSource(workflowSource) {
523
525
  return workflowSource?.kind === "flow-ts"
524
526
  }
525
527
 
528
+ function isAutomationTsSource(workflowSource) {
529
+ return workflowSource?.kind === "automation-ts"
530
+ }
531
+
532
+ function isEditableTsSource(workflowSource) {
533
+ return isFlowTsSource(workflowSource) || isAutomationTsSource(workflowSource)
534
+ }
535
+
526
536
  function isFlowSourcePath(path) {
527
537
  return typeof path === "string" && path.startsWith("dypai/flows/") && /\.flow\.ts$/i.test(path)
528
538
  }
529
539
 
530
- export function normalizeFlowSourceFile(workflowSource, endpointName) {
540
+ function isAutomationSourcePath(path) {
541
+ return typeof path === "string" && path.startsWith("dypai/automations/") && /\.automation\.ts$/i.test(path)
542
+ }
543
+
544
+ export function normalizeEditableSourceFile(workflowSource, endpointName) {
531
545
  const raw = typeof workflowSource?.file === "string" ? workflowSource.file.trim().replace(/\\/g, "/") : ""
532
546
  if (isFlowSourcePath(raw)) return raw
547
+ if (isAutomationSourcePath(raw)) return raw
533
548
  if (raw.startsWith("flows/") && /\.flow\.ts$/i.test(raw)) return `dypai/${raw}`
549
+ if (raw.startsWith("automations/") && /\.automation\.ts$/i.test(raw)) return `dypai/${raw}`
550
+ if (isAutomationTsSource(workflowSource)) return `dypai/automations/${endpointName}.automation.ts`
534
551
  return `dypai/flows/${endpointName}.flow.ts`
535
552
  }
536
553
 
554
+ export function normalizeFlowSourceFile(workflowSource, endpointName) {
555
+ return normalizeEditableSourceFile(workflowSource, endpointName)
556
+ }
557
+
537
558
  function flowSourceRelPath(sourceFile) {
538
559
  return sourceFile.replace(/^dypai\//, "")
539
560
  }
@@ -553,7 +574,7 @@ export function buildPersistedFlowSourceMap(sourcePayload) {
553
574
  const byPath = new Map()
554
575
  for (const entry of files) {
555
576
  const path = typeof entry?.path === "string" ? entry.path.trim().replace(/\\/g, "/") : ""
556
- if (!isFlowSourcePath(path)) continue
577
+ if (!isFlowSourcePath(path) && !isAutomationSourcePath(path)) continue
557
578
  const decoded = decodeSourceFileContent(entry)
558
579
  if (decoded == null) continue
559
580
  byPath.set(path, decoded)
@@ -607,13 +628,15 @@ async function removeEndpointYamlFiles(outDir, name, groupName = null) {
607
628
  function renderMissingFlowStub(row, sourceFile, sourceFetch) {
608
629
  const refLine = sourceFetch?.ref ? `# Checked Git source ref: ${sourceFetch.ref}\n` : ""
609
630
  const errorLine = sourceFetch?.error ? `# Source fetch error: ${sourceFetch.error}\n` : ""
631
+ const sourceKind = sourceFile.endsWith(".automation.ts") ? "AUTOMATION" : "FLOW"
632
+ const extension = sourceFile.endsWith(".automation.ts") ? ".automation.ts" : ".flow.ts"
610
633
  return (
611
- `# MISSING AUTHORITATIVE FLOW SOURCE\n` +
612
- `# Remote endpoint '${row.name}' is marked as Flow-authored, but dypai_pull could not find:\n` +
634
+ `# MISSING AUTHORITATIVE ${sourceKind} SOURCE\n` +
635
+ `# Remote endpoint '${row.name}' is marked as ${sourceKind.toLowerCase()}-authored, but dypai_pull could not find:\n` +
613
636
  `# ${sourceFile}\n` +
614
637
  refLine +
615
638
  errorLine +
616
- `# Do not edit this YAML stub. Restore or sync the .flow.ts source, then rerun dypai_pull.\n\n` +
639
+ `# Do not edit this YAML stub. Restore or sync the ${extension} source, then rerun dypai_pull.\n\n` +
617
640
  `# name: ${row.name}\n` +
618
641
  `# method: ${row.method || "POST"}\n`
619
642
  )
@@ -901,9 +924,9 @@ export const dypaiPullTool = {
901
924
  row.workflow_source = parseMaybeJson(row.workflow_source)
902
925
  try {
903
926
  const workflowSource = row.workflow_source
904
- if (isFlowTsSource(workflowSource)) {
927
+ if (isEditableTsSource(workflowSource)) {
905
928
  const groupName = row.group_id ? (mapsCtx.groupIdToName[row.group_id] || null) : null
906
- const sourceFile = normalizeFlowSourceFile(workflowSource, row.name)
929
+ const sourceFile = normalizeEditableSourceFile(workflowSource, row.name)
907
930
  const relFlowPath = flowSourceRelPath(sourceFile)
908
931
  const flowContent = persistedFlowSources.byPath.get(sourceFile)
909
932
  // Drop stale YAML/stubs from a previous pull. Any YAML next to a real
@@ -945,8 +968,8 @@ export const dypaiPullTool = {
945
968
  errors.push({
946
969
  endpoint: row.name,
947
970
  error:
948
- `Endpoint is Flow-authored but ${sourceFile} was not found in persisted project source. ` +
949
- `Restore/sync the .flow.ts source, then rerun dypai_pull.`,
971
+ `Endpoint is source-authored but ${sourceFile} was not found in persisted project source. ` +
972
+ `Restore/sync the editable TypeScript source, then rerun dypai_pull.`,
950
973
  })
951
974
  continue
952
975
  }
@@ -1129,11 +1152,11 @@ export const dypaiPullTool = {
1129
1152
  next_steps: pendingDrafts
1130
1153
  ? [
1131
1154
  `${draftsTotal} pending draft(s). Review with manage_drafts(operation:'list'), verify with dypai_test_endpoint(mode:'draft'), then publish or discard.`,
1132
- "After resolving drafts, edit Flow in dypai/flows/ (preferred) or legacy YAML in dypai/endpoints/, then dypai_diff → dypai_push to stage more.",
1155
+ "After resolving drafts, edit Flow in dypai/flows/, Automations in dypai/automations/, or legacy YAML in dypai/endpoints/, then dypai_diff → dypai_push to stage more.",
1133
1156
  ]
1134
1157
  : (endpoints || []).length === 0
1135
- ? ["Empty project. Create tables via execute_sql, then write dypai/flows/<name>.flow.ts and dypai_push."]
1136
- : ["Read dypai/schema.sql before writing queries.", "Edit Flow in dypai/flows/ (preferred) or legacy YAML in dypai/endpoints/, then dypai_diff → dypai_push."],
1158
+ ? ["Empty project. Create tables via execute_sql, then write dypai/flows/<name>.flow.ts for callable endpoints or dypai/automations/<name>.automation.ts for scheduled/webhook processes, then dypai_push."]
1159
+ : ["Read dypai/schema.sql before writing queries.", "Edit Flow in dypai/flows/, Automations in dypai/automations/, or legacy YAML in dypai/endpoints/, then dypai_diff → dypai_push."],
1137
1160
  }
1138
1161
 
1139
1162
  return {
@@ -1155,7 +1178,7 @@ export const dypaiPullTool = {
1155
1178
  : draftsTotal > 0
1156
1179
  ? `${draftsTotal} pending draft(s) on this project — see overview.pending_drafts and decide publish vs discard before pushing more.`
1157
1180
  : endpoints.length === 0
1158
- ? "Empty project. Create tables with execute_sql, then write flows/<name>.flow.ts and dypai_push."
1181
+ ? "Empty project. Create tables with execute_sql, then write flows/<name>.flow.ts or automations/<name>.automation.ts and dypai_push."
1159
1182
  : undefined,
1160
1183
  }
1161
1184
  },