@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.
- package/package.json +1 -1
- package/src/generated/serverInstructions.js +3 -3
- package/src/index.js +4 -1
- package/src/lib/backendSnapshot.js +8 -0
- package/src/lib/effective-workflows-runner.js +6 -3
- package/src/toolProfiles.js +6 -5
- package/src/tools/automation-setup.js +227 -0
- package/src/tools/frontend.js +42 -8
- package/src/tools/project-deploy-production.js +54 -31
- package/src/tools/project-push.js +0 -5
- package/src/tools/storage.js +2 -0
- package/src/tools/sync/diff.js +28 -130
- package/src/tools/sync/planner.js +20 -1
- package/src/tools/sync/pull.js +38 -15
- package/src/tools/sync/push.js +46 -10
- package/src/tools/sync/source-diff.js +217 -0
package/src/tools/frontend.js
CHANGED
|
@@ -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
|
|
21
|
-
"This downloads the Studio source branch (src/, package.json, public/,
|
|
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
|
|
88
|
+
return await pullProjectSource({
|
|
55
89
|
project_id,
|
|
56
90
|
targetDirectory,
|
|
57
|
-
overwrite
|
|
58
|
-
source_ref
|
|
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
|
|
266
|
+
return await pullProjectSource({
|
|
233
267
|
project_id,
|
|
234
268
|
targetDirectory,
|
|
235
|
-
overwrite
|
|
236
|
-
source_ref
|
|
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
|
-
"
|
|
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
|
-
|
|
99
|
-
|
|
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
|
-
|
|
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
|
-
|
|
142
|
-
|
|
141
|
+
|
|
142
|
+
if (publishBackend) {
|
|
143
|
+
const draftsList = await proxyToolCall("manage_drafts", {
|
|
143
144
|
project_id: targetProjectId,
|
|
144
|
-
operation: "
|
|
145
|
-
confirm: true,
|
|
145
|
+
operation: "list",
|
|
146
146
|
})
|
|
147
|
-
|
|
147
|
+
pendingDrafts = draftCount(draftsList)
|
|
148
|
+
if (pendingDrafts == null) {
|
|
148
149
|
return {
|
|
149
150
|
success: false,
|
|
150
|
-
phase: "
|
|
151
|
-
|
|
152
|
-
|
|
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:
|
|
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
|
}
|
package/src/tools/storage.js
CHANGED
|
@@ -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),
|
package/src/tools/sync/diff.js
CHANGED
|
@@ -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 {
|
|
16
|
-
import {
|
|
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
|
-
"
|
|
22
|
-
"
|
|
23
|
-
"
|
|
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: "
|
|
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"
|
|
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
|
-
|
|
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: "
|
|
76
|
-
|
|
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
|
|
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:
|
|
153
|
-
update:
|
|
154
|
-
delete:
|
|
155
|
-
unchanged:
|
|
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
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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 ──────────────────────────────────────────────────
|
package/src/tools/sync/pull.js
CHANGED
|
@@ -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\`
|
|
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
|
-
|
|
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
|
|
612
|
-
`# Remote endpoint '${row.name}' is marked as
|
|
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
|
|
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 (
|
|
927
|
+
if (isEditableTsSource(workflowSource)) {
|
|
905
928
|
const groupName = row.group_id ? (mapsCtx.groupIdToName[row.group_id] || null) : null
|
|
906
|
-
const sourceFile =
|
|
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
|
|
949
|
-
`Restore/sync the
|
|
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/
|
|
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
|
|
1136
|
-
: ["Read dypai/schema.sql before writing queries.", "Edit Flow in dypai/flows/
|
|
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
|
},
|