@dypai-ai/mcp 1.5.26 → 1.5.28
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/auto-update.js +14 -59
- package/src/index.js +3 -3
- package/src/lib/backendSnapshot.js +214 -0
- package/src/lib/cloudBackendCompiler.js +54 -0
- package/src/lib/effective-workflows-runner.js +98 -3
- package/src/tools/sync/diff.js +1 -1
- package/src/tools/sync/generate-types.js +33 -10
- package/src/tools/sync/planner.js +4 -6
- package/src/tools/sync/push.js +2 -2
- package/src/tools/sync/test-endpoint.js +4 -6
- package/src/tools/sync/validate.js +3 -5
package/package.json
CHANGED
package/src/auto-update.js
CHANGED
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Self-update for @dypai-ai/mcp.
|
|
3
3
|
*
|
|
4
|
-
* On startup, checks the npm registry for a newer version. If found, performs
|
|
4
|
+
* On every startup, checks the npm registry for a newer version. If found, performs
|
|
5
5
|
* the appropriate update (clear npx cache or `npm install -g`) and exits with
|
|
6
6
|
* code 0 so the host (Cursor / Claude / Trae / VSCode) re-spawns the process
|
|
7
7
|
* with the freshly installed version.
|
|
8
8
|
*
|
|
9
|
-
* Throttled to one check every 6h to avoid hammering the registry.
|
|
10
|
-
*
|
|
11
9
|
* Disable with: DYPAI_NO_AUTOUPDATE=1
|
|
12
10
|
*/
|
|
13
11
|
|
|
@@ -21,13 +19,7 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
|
21
19
|
const PKG_PATH = join(__dirname, "..", "package.json");
|
|
22
20
|
const PKG_NAME = "@dypai-ai/mcp";
|
|
23
21
|
const REGISTRY_URL = `https://registry.npmjs.org/${PKG_NAME}/latest`;
|
|
24
|
-
// Dist-tags endpoint is tiny (<200B) — used to check if there's a CRITICAL
|
|
25
|
-
// release the user must upgrade to immediately, bypassing the 6h throttle.
|
|
26
|
-
// To mark a version as critical after publish:
|
|
27
|
-
// npm dist-tag add @dypai-ai/mcp@1.4.5 critical
|
|
28
|
-
const DIST_TAGS_URL = `https://registry.npmjs.org/-/package/${PKG_NAME}/dist-tags`;
|
|
29
22
|
const CHECK_TIMEOUT_MS = 2000;
|
|
30
|
-
const THROTTLE_HOURS = 6;
|
|
31
23
|
const STATE_FILE = join(tmpdir(), "dypai-mcp-update-state.json");
|
|
32
24
|
|
|
33
25
|
function log(msg) {
|
|
@@ -73,32 +65,6 @@ async function fetchLatestManifest() {
|
|
|
73
65
|
}
|
|
74
66
|
}
|
|
75
67
|
|
|
76
|
-
/**
|
|
77
|
-
* Fetch the `critical` dist-tag (if published). Used to bypass the 6h throttle
|
|
78
|
-
* when a release is important enough that users must upgrade on next spawn.
|
|
79
|
-
*
|
|
80
|
-
* Returns the critical version string (e.g. "1.4.5") or null if no critical
|
|
81
|
-
* tag is set, the registry is unreachable, or the response is malformed.
|
|
82
|
-
*
|
|
83
|
-
* Cost: one tiny JSON fetch (~200 bytes) per spawn. Adds ~50-150ms to startup
|
|
84
|
-
* but runs in parallel with the rest of the MCP init, so wall-clock impact is
|
|
85
|
-
* usually zero.
|
|
86
|
-
*/
|
|
87
|
-
async function fetchCriticalVersion() {
|
|
88
|
-
const ctrl = new AbortController();
|
|
89
|
-
const timer = setTimeout(() => ctrl.abort(), CHECK_TIMEOUT_MS);
|
|
90
|
-
try {
|
|
91
|
-
const res = await fetch(DIST_TAGS_URL, { signal: ctrl.signal });
|
|
92
|
-
if (!res.ok) return null;
|
|
93
|
-
const tags = await res.json();
|
|
94
|
-
return typeof tags?.critical === "string" ? tags.critical : null;
|
|
95
|
-
} catch {
|
|
96
|
-
return null;
|
|
97
|
-
} finally {
|
|
98
|
-
clearTimeout(timer);
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
68
|
/**
|
|
103
69
|
* After npm publish there's a 30s–2min window where the registry knows the
|
|
104
70
|
* version but the tarball is not yet replicated across the CDN. If we clear
|
|
@@ -170,37 +136,21 @@ function installGlobalLatest() {
|
|
|
170
136
|
* Main entry point — call once at startup.
|
|
171
137
|
* Returns quickly. Exits the process if an update was applied.
|
|
172
138
|
*/
|
|
173
|
-
export async function checkForUpdates(
|
|
139
|
+
export async function checkForUpdates() {
|
|
174
140
|
if (process.env.DYPAI_NO_AUTOUPDATE === "1") return { skipped: "disabled" };
|
|
175
141
|
|
|
176
142
|
const current = getCurrentVersion();
|
|
177
143
|
if (!current) return { skipped: "no current version" };
|
|
178
144
|
|
|
179
|
-
// ── Critical release check (bypasses the 6h throttle) ────────────────────
|
|
180
|
-
// If an ops person has run `npm dist-tag add @dypai-ai/mcp@X.Y.Z critical`,
|
|
181
|
-
// every spawn picks that up and forces an upgrade regardless of when the
|
|
182
|
-
// last normal check ran. Used for security / data-loss bug fixes where
|
|
183
|
-
// 6-24h propagation is too slow.
|
|
184
|
-
const criticalVersion = await fetchCriticalVersion();
|
|
185
|
-
const hasCritical = criticalVersion && compareVersions(criticalVersion, current) > 0;
|
|
186
|
-
if (hasCritical) {
|
|
187
|
-
log(`CRITICAL update required: ${current} → ${criticalVersion} (bypassing throttle)`);
|
|
188
|
-
force = true;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
// Throttle (skipped when force or critical)
|
|
192
|
-
if (!force) {
|
|
193
|
-
const state = readState();
|
|
194
|
-
if (state.lastCheckAt) {
|
|
195
|
-
const hoursAgo = (Date.now() - state.lastCheckAt) / 3_600_000;
|
|
196
|
-
if (hoursAgo < THROTTLE_HOURS) return { skipped: "throttled" };
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
|
|
200
145
|
const manifest = await fetchLatestManifest();
|
|
201
146
|
const latest = manifest?.version || null;
|
|
202
147
|
const tarballUrl = manifest?.dist?.tarball || null;
|
|
203
|
-
writeState({
|
|
148
|
+
writeState({
|
|
149
|
+
...readState(),
|
|
150
|
+
lastCheckAt: Date.now(),
|
|
151
|
+
lastSeenLatest: latest,
|
|
152
|
+
current,
|
|
153
|
+
});
|
|
204
154
|
|
|
205
155
|
if (!latest) return { skipped: "registry unreachable" };
|
|
206
156
|
if (compareVersions(latest, current) <= 0) return { uptodate: true, current };
|
|
@@ -213,7 +163,7 @@ export async function checkForUpdates({ force = false } = {}) {
|
|
|
213
163
|
// If we clear the cache + exit during that window, next spawn fails.
|
|
214
164
|
const ready = await isTarballReady(tarballUrl);
|
|
215
165
|
if (!ready) {
|
|
216
|
-
log(`new version ${latest} not yet downloadable from CDN; will retry
|
|
166
|
+
log(`new version ${latest} not yet downloadable from CDN; will retry on next spawn`);
|
|
217
167
|
return { skipped: "tarball not ready" };
|
|
218
168
|
}
|
|
219
169
|
|
|
@@ -223,6 +173,11 @@ export async function checkForUpdates({ force = false } = {}) {
|
|
|
223
173
|
if (method === "npx") {
|
|
224
174
|
log(`tarball verified — clearing npx cache so the next spawn pulls ${PKG_NAME}@${latest}`);
|
|
225
175
|
updated = clearNpxCacheForPackage();
|
|
176
|
+
// Even if no cached dir matched, exit so npx re-resolves @latest on respawn.
|
|
177
|
+
if (!updated) {
|
|
178
|
+
log("no npx cache entry found; exiting anyway so the host re-resolves @latest");
|
|
179
|
+
updated = true;
|
|
180
|
+
}
|
|
226
181
|
} else if (method === "global") {
|
|
227
182
|
log(`tarball verified — running: npm install -g ${PKG_NAME}@latest`);
|
|
228
183
|
updated = installGlobalLatest();
|
package/src/index.js
CHANGED
|
@@ -71,9 +71,9 @@ import { filterSearchDocsForStudio } from "./searchDocsFilter.js"
|
|
|
71
71
|
// disk for when dypai_trace is re-enabled, but not imported here.
|
|
72
72
|
|
|
73
73
|
// ── Self-update ─────────────────────────────────────────────────────────────
|
|
74
|
-
//
|
|
75
|
-
//
|
|
76
|
-
//
|
|
74
|
+
// Checks the npm registry on every spawn. If a newer version is available,
|
|
75
|
+
// the update is performed and the process exits cleanly so the IDE re-spawns
|
|
76
|
+
// with the latest. Disable with DYPAI_NO_AUTOUPDATE=1.
|
|
77
77
|
// Network failures are silently ignored — never blocks startup more than ~2s.
|
|
78
78
|
await checkForUpdates().catch(() => {})
|
|
79
79
|
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Strict allowlist for backend snapshots sent to the cloud compiler.
|
|
3
|
+
* Mirrors dypai-backend-compiler/src/snapshot.ts
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { readFile, readdir, stat } from "node:fs/promises"
|
|
7
|
+
import { join, relative } from "node:path"
|
|
8
|
+
|
|
9
|
+
export const DEFAULT_MAX_SNAPSHOT_BYTES = 5 * 1024 * 1024
|
|
10
|
+
export const DEFAULT_MAX_FILE_BYTES = 512 * 1024
|
|
11
|
+
export const SCHEMA_SQL_MAX_BYTES = 2 * 1024 * 1024
|
|
12
|
+
|
|
13
|
+
const DENY_PREFIXES = [
|
|
14
|
+
"node_modules/",
|
|
15
|
+
"src/",
|
|
16
|
+
"public/",
|
|
17
|
+
".git/",
|
|
18
|
+
".env",
|
|
19
|
+
"dypai/types/",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
const DENY_SUFFIXES = [
|
|
23
|
+
".env",
|
|
24
|
+
".pem",
|
|
25
|
+
".key",
|
|
26
|
+
".p12",
|
|
27
|
+
".png",
|
|
28
|
+
".jpg",
|
|
29
|
+
".jpeg",
|
|
30
|
+
".gif",
|
|
31
|
+
".webp",
|
|
32
|
+
".zip",
|
|
33
|
+
".tar",
|
|
34
|
+
".gz",
|
|
35
|
+
".wasm",
|
|
36
|
+
".pdf",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
function isFlowPath(path) {
|
|
40
|
+
return path.startsWith("dypai/flows/") && /\.flow\.ts$/i.test(path)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function isEndpointYaml(path) {
|
|
44
|
+
return path.startsWith("dypai/endpoints/") && /\.(ya?ml)$/i.test(path)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function isAllowedCatalog(path) {
|
|
48
|
+
return [
|
|
49
|
+
"dypai/capability-catalog.json",
|
|
50
|
+
"dypai/capability-brief.md",
|
|
51
|
+
"dypai/node-catalog.json",
|
|
52
|
+
].includes(path)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function normalizeSnapshotPath(rawPath) {
|
|
56
|
+
const trimmed = String(rawPath || "").trim().replace(/\\/g, "/").replace(/^\.\//, "")
|
|
57
|
+
if (!trimmed || trimmed.startsWith("/")) return null
|
|
58
|
+
if (trimmed.includes("\0")) return null
|
|
59
|
+
|
|
60
|
+
const segments = trimmed.split("/")
|
|
61
|
+
if (segments.some((segment) => !segment || segment === "." || segment === "..")) return null
|
|
62
|
+
|
|
63
|
+
const normalized = segments.join("/")
|
|
64
|
+
const lower = normalized.toLowerCase()
|
|
65
|
+
|
|
66
|
+
for (const prefix of DENY_PREFIXES) {
|
|
67
|
+
if (lower.startsWith(prefix) || lower === prefix.replace(/\/$/, "")) return null
|
|
68
|
+
}
|
|
69
|
+
for (const suffix of DENY_SUFFIXES) {
|
|
70
|
+
if (lower.endsWith(suffix)) return null
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (isFlowPath(normalized)) return normalized
|
|
74
|
+
if (isEndpointYaml(normalized)) return normalized
|
|
75
|
+
if (normalized === "dypai/realtime.yaml") return normalized
|
|
76
|
+
if (normalized === "dypai/schema.sql") return normalized
|
|
77
|
+
if (isAllowedCatalog(normalized)) return normalized
|
|
78
|
+
if (normalized === "dypai.config.yaml") return normalized
|
|
79
|
+
|
|
80
|
+
return null
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function filterSnapshotFiles(files, options = {}) {
|
|
84
|
+
const maxSnapshotBytes = options.maxSnapshotBytes ?? DEFAULT_MAX_SNAPSHOT_BYTES
|
|
85
|
+
const maxFileBytes = options.maxFileBytes ?? DEFAULT_MAX_FILE_BYTES
|
|
86
|
+
const schemaSqlMaxBytes = options.schemaSqlMaxBytes ?? SCHEMA_SQL_MAX_BYTES
|
|
87
|
+
|
|
88
|
+
const accepted = new Map()
|
|
89
|
+
const rejected = []
|
|
90
|
+
let totalBytes = 0
|
|
91
|
+
|
|
92
|
+
for (const file of files) {
|
|
93
|
+
const rawPath = typeof file?.path === "string" ? file.path : ""
|
|
94
|
+
const content = typeof file?.content === "string" ? file.content : null
|
|
95
|
+
const normalized = normalizeSnapshotPath(rawPath)
|
|
96
|
+
|
|
97
|
+
if (!normalized) {
|
|
98
|
+
rejected.push({ path: rawPath || "(missing path)", reason: "path_not_allowlisted" })
|
|
99
|
+
continue
|
|
100
|
+
}
|
|
101
|
+
if (content === null) {
|
|
102
|
+
rejected.push({ path: normalized, reason: "content_must_be_string" })
|
|
103
|
+
continue
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const bytes = Buffer.byteLength(content, "utf8")
|
|
107
|
+
const limit = normalized === "dypai/schema.sql" ? schemaSqlMaxBytes : maxFileBytes
|
|
108
|
+
if (bytes > limit) {
|
|
109
|
+
rejected.push({ path: normalized, reason: `file_too_large:${bytes}>${limit}` })
|
|
110
|
+
continue
|
|
111
|
+
}
|
|
112
|
+
if (totalBytes + bytes > maxSnapshotBytes) {
|
|
113
|
+
rejected.push({ path: normalized, reason: "snapshot_size_limit_exceeded" })
|
|
114
|
+
continue
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
accepted.set(normalized, { path: normalized, content })
|
|
118
|
+
totalBytes += bytes
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (accepted.size === 0) {
|
|
122
|
+
return {
|
|
123
|
+
ok: false,
|
|
124
|
+
files: [],
|
|
125
|
+
rejected,
|
|
126
|
+
totalBytes,
|
|
127
|
+
error: "snapshot has no allowlisted backend files",
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
ok: true,
|
|
133
|
+
files: [...accepted.values()].sort((a, b) => a.path.localeCompare(b.path)),
|
|
134
|
+
rejected,
|
|
135
|
+
totalBytes,
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function walkAllowlistedFiles(rootDir, projectRoot) {
|
|
140
|
+
const out = []
|
|
141
|
+
const candidates = [
|
|
142
|
+
join(rootDir, "flows"),
|
|
143
|
+
join(rootDir, "endpoints"),
|
|
144
|
+
join(rootDir, "realtime.yaml"),
|
|
145
|
+
join(rootDir, "schema.sql"),
|
|
146
|
+
join(rootDir, "capability-catalog.json"),
|
|
147
|
+
join(rootDir, "capability-brief.md"),
|
|
148
|
+
join(rootDir, "node-catalog.json"),
|
|
149
|
+
join(projectRoot, "dypai.config.yaml"),
|
|
150
|
+
]
|
|
151
|
+
|
|
152
|
+
async function walkDir(dir, relBase) {
|
|
153
|
+
let entries = []
|
|
154
|
+
try {
|
|
155
|
+
entries = await readdir(dir, { withFileTypes: true })
|
|
156
|
+
} catch {
|
|
157
|
+
return
|
|
158
|
+
}
|
|
159
|
+
for (const entry of entries) {
|
|
160
|
+
const rel = `${relBase}/${entry.name}`.replace(/^\//, "")
|
|
161
|
+
const full = join(dir, entry.name)
|
|
162
|
+
if (entry.isDirectory()) {
|
|
163
|
+
await walkDir(full, rel)
|
|
164
|
+
} else if (entry.isFile()) {
|
|
165
|
+
const normalized = normalizeSnapshotPath(rel)
|
|
166
|
+
if (normalized) out.push({ full, path: normalized })
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
await walkDir(join(rootDir, "flows"), "dypai/flows")
|
|
172
|
+
await walkDir(join(rootDir, "endpoints"), "dypai/endpoints")
|
|
173
|
+
|
|
174
|
+
for (const candidate of candidates.slice(2)) {
|
|
175
|
+
try {
|
|
176
|
+
const s = await stat(candidate)
|
|
177
|
+
if (!s.isFile()) continue
|
|
178
|
+
const rel = candidate === join(projectRoot, "dypai.config.yaml")
|
|
179
|
+
? "dypai.config.yaml"
|
|
180
|
+
: `dypai/${relative(rootDir, candidate).replace(/\\/g, "/")}`
|
|
181
|
+
const normalized = normalizeSnapshotPath(rel)
|
|
182
|
+
if (normalized) out.push({ full: candidate, path: normalized })
|
|
183
|
+
} catch {
|
|
184
|
+
// optional file
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const deduped = new Map()
|
|
189
|
+
for (const item of out) deduped.set(item.path, item)
|
|
190
|
+
return [...deduped.values()].sort((a, b) => a.path.localeCompare(b.path))
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export async function collectBackendSnapshotFromDypaiDir(dypaiRootDir, projectRoot = null) {
|
|
194
|
+
const resolvedRoot = dypaiRootDir
|
|
195
|
+
const resolvedProjectRoot = projectRoot || (resolvedRoot.endsWith("/dypai") || resolvedRoot.endsWith("\\dypai")
|
|
196
|
+
? join(resolvedRoot, "..")
|
|
197
|
+
: resolvedRoot)
|
|
198
|
+
|
|
199
|
+
const fileRefs = await walkAllowlistedFiles(resolvedRoot, resolvedProjectRoot)
|
|
200
|
+
const files = []
|
|
201
|
+
for (const ref of fileRefs) {
|
|
202
|
+
files.push({
|
|
203
|
+
path: ref.path,
|
|
204
|
+
content: await readFile(ref.full, "utf8"),
|
|
205
|
+
})
|
|
206
|
+
}
|
|
207
|
+
return filterSnapshotFiles(files)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export const __testing = {
|
|
211
|
+
isFlowPath,
|
|
212
|
+
isEndpointYaml,
|
|
213
|
+
walkAllowlistedFiles,
|
|
214
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cloud backend compiler client — calls Core API snapshot compile routes.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { api } from "../api.js"
|
|
6
|
+
import { collectBackendSnapshotFromDypaiDir } from "./backendSnapshot.js"
|
|
7
|
+
|
|
8
|
+
function compilerBasePath(projectId) {
|
|
9
|
+
return `/api/engine/${encodeURIComponent(projectId)}/backend/snapshot`
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async function postSnapshotRoute(projectId, route, body) {
|
|
13
|
+
return api.post(`${compilerBasePath(projectId)}/${route}`, body)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function buildSnapshotPayload(dypaiRootDir, projectRoot = null) {
|
|
17
|
+
return collectBackendSnapshotFromDypaiDir(dypaiRootDir, projectRoot)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function cloudValidateSnapshot(projectId, files) {
|
|
21
|
+
return postSnapshotRoute(projectId, "validate", { files })
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function cloudListEffectiveEntries(projectId, files) {
|
|
25
|
+
return postSnapshotRoute(projectId, "list", { files })
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function cloudResolveEffectiveEndpoint(projectId, files, endpoint) {
|
|
29
|
+
return postSnapshotRoute(projectId, "resolve", { files, endpoint })
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function cloudBuildPushPayload(projectId, files, endpoint) {
|
|
33
|
+
return postSnapshotRoute(projectId, "push-payload", { files, endpoint })
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function cloudBuildEffectivePushPayloads(projectId, files) {
|
|
37
|
+
return postSnapshotRoute(projectId, "effective-push-payloads", { files })
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function cloudGenerateTypes(projectId, files) {
|
|
41
|
+
return postSnapshotRoute(projectId, "generate-types", { files })
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function compileFromDypaiDir(projectId, dypaiRootDir, projectRoot = null) {
|
|
45
|
+
const snapshot = await buildSnapshotPayload(dypaiRootDir, projectRoot)
|
|
46
|
+
if (!snapshot.ok) {
|
|
47
|
+
return { ok: false, snapshot, error: snapshot.error || "empty snapshot" }
|
|
48
|
+
}
|
|
49
|
+
return { ok: true, snapshot }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function shouldUseLocalFlowCompiler() {
|
|
53
|
+
return Boolean(process.env.DYPAI_MONOREPO_ROOT || process.env.DYPAI_USE_LOCAL_FLOW_COMPILER === "1")
|
|
54
|
+
}
|
|
@@ -1,12 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* Used by validate, test-endpoint, planner/push for .flow.ts effective workflows.
|
|
2
|
+
* Effective workflow runner — cloud compiler by default, local monorepo CLI for dev override.
|
|
4
3
|
*/
|
|
5
4
|
|
|
6
5
|
import { spawnSync } from "node:child_process"
|
|
7
6
|
import { existsSync } from "node:fs"
|
|
8
7
|
import { dirname, join, resolve as resolvePath, basename } from "node:path"
|
|
9
8
|
import { fileURLToPath } from "node:url"
|
|
9
|
+
import {
|
|
10
|
+
buildSnapshotPayload,
|
|
11
|
+
cloudBuildEffectivePushPayloads,
|
|
12
|
+
cloudBuildPushPayload,
|
|
13
|
+
cloudGenerateTypes,
|
|
14
|
+
cloudListEffectiveEntries,
|
|
15
|
+
cloudResolveEffectiveEndpoint,
|
|
16
|
+
cloudValidateSnapshot,
|
|
17
|
+
shouldUseLocalFlowCompiler,
|
|
18
|
+
} from "./cloudBackendCompiler.js"
|
|
10
19
|
|
|
11
20
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
12
21
|
|
|
@@ -52,7 +61,7 @@ export function runEffectiveWorkflowsCli(projectRoot, commandArgs = []) {
|
|
|
52
61
|
severity: "warn",
|
|
53
62
|
rule: "effective_workflow_runner_unavailable",
|
|
54
63
|
message: "Effective workflow runner unavailable (monorepo root not found). YAML workflows still work.",
|
|
55
|
-
fix_hint: "
|
|
64
|
+
fix_hint: "Flow validation now runs in DYPAI cloud by default. Ensure DYPAI_TOKEN is set.",
|
|
56
65
|
},
|
|
57
66
|
}
|
|
58
67
|
}
|
|
@@ -113,3 +122,89 @@ export function runEffectiveWorkflowsCli(projectRoot, commandArgs = []) {
|
|
|
113
122
|
|
|
114
123
|
return { ok: true, data: parsed }
|
|
115
124
|
}
|
|
125
|
+
|
|
126
|
+
function endpointArg(commandArgs) {
|
|
127
|
+
const idx = commandArgs.indexOf("--endpoint")
|
|
128
|
+
if (idx < 0) return null
|
|
129
|
+
return commandArgs[idx + 1] || null
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function commandFromArgs(commandArgs) {
|
|
133
|
+
return commandArgs[0] || "validate"
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async function runEffectiveWorkflowsCloud(dypaiRootDir, projectRoot, projectId, commandArgs = []) {
|
|
137
|
+
if (!projectId) {
|
|
138
|
+
return {
|
|
139
|
+
ok: false,
|
|
140
|
+
unavailable: true,
|
|
141
|
+
warning: {
|
|
142
|
+
severity: "warn",
|
|
143
|
+
rule: "effective_workflow_runner_unavailable",
|
|
144
|
+
message: "Flow compilation requires project_id (set DYPAI_PROJECT_ID or pass project_id).",
|
|
145
|
+
fix_hint: "Pass project_id to dypai_validate / dypai_push, or set DYPAI_PROJECT_ID in MCP config.",
|
|
146
|
+
},
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const snapshot = await buildSnapshotPayload(dypaiRootDir, projectRoot)
|
|
151
|
+
if (!snapshot.ok) {
|
|
152
|
+
const hasFlowFiles = snapshot.rejected?.some((item) => String(item.path || "").includes(".flow.ts"))
|
|
153
|
+
if (!hasFlowFiles) {
|
|
154
|
+
return { ok: true, data: { diagnostics: [], entries: [] } }
|
|
155
|
+
}
|
|
156
|
+
return {
|
|
157
|
+
ok: false,
|
|
158
|
+
unavailable: false,
|
|
159
|
+
error: snapshot.error || "Failed to build backend snapshot",
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const command = commandFromArgs(commandArgs)
|
|
164
|
+
const endpoint = endpointArg(commandArgs)
|
|
165
|
+
|
|
166
|
+
try {
|
|
167
|
+
if (command === "validate") {
|
|
168
|
+
const data = await cloudValidateSnapshot(projectId, snapshot.files)
|
|
169
|
+
return { ok: true, data }
|
|
170
|
+
}
|
|
171
|
+
if (command === "list") {
|
|
172
|
+
const data = await cloudListEffectiveEntries(projectId, snapshot.files)
|
|
173
|
+
return { ok: true, data }
|
|
174
|
+
}
|
|
175
|
+
if (command === "resolve") {
|
|
176
|
+
const data = await cloudResolveEffectiveEndpoint(projectId, snapshot.files, endpoint)
|
|
177
|
+
return { ok: true, data }
|
|
178
|
+
}
|
|
179
|
+
if (command === "push-payload") {
|
|
180
|
+
const data = await cloudBuildPushPayload(projectId, snapshot.files, endpoint)
|
|
181
|
+
return { ok: true, data }
|
|
182
|
+
}
|
|
183
|
+
if (command === "push-payloads") {
|
|
184
|
+
const data = await cloudBuildEffectivePushPayloads(projectId, snapshot.files)
|
|
185
|
+
return { ok: true, data }
|
|
186
|
+
}
|
|
187
|
+
if (command === "generate-types") {
|
|
188
|
+
const data = await cloudGenerateTypes(projectId, snapshot.files)
|
|
189
|
+
return { ok: true, data }
|
|
190
|
+
}
|
|
191
|
+
return { ok: false, error: `Unsupported cloud compiler command: ${command}` }
|
|
192
|
+
} catch (error) {
|
|
193
|
+
return {
|
|
194
|
+
ok: false,
|
|
195
|
+
unavailable: false,
|
|
196
|
+
error: error?.detail?.detail || error?.message || String(error),
|
|
197
|
+
statusCode: error?.statusCode,
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export async function runEffectiveWorkflows(dypaiRootDir, projectId, commandArgs = []) {
|
|
203
|
+
const projectRoot = resolveProjectRootFromDypaiDir(dypaiRootDir)
|
|
204
|
+
if (shouldUseLocalFlowCompiler()) {
|
|
205
|
+
return runEffectiveWorkflowsCli(projectRoot, commandArgs)
|
|
206
|
+
}
|
|
207
|
+
return runEffectiveWorkflowsCloud(dypaiRootDir, projectRoot, projectId, commandArgs)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export { shouldUseLocalFlowCompiler }
|
package/src/tools/sync/diff.js
CHANGED
|
@@ -48,7 +48,7 @@ export const dypaiDiffTool = {
|
|
|
48
48
|
const targetProjectId = project_id || config?.project_id || null
|
|
49
49
|
|
|
50
50
|
const [local, remote, stateSnapshot, draftsResult] = await Promise.all([
|
|
51
|
-
readLocalEffectiveState(rootDir),
|
|
51
|
+
readLocalEffectiveState(rootDir, targetProjectId),
|
|
52
52
|
fetchRemoteState(targetProjectId),
|
|
53
53
|
readLocalStateSnapshot(rootDir),
|
|
54
54
|
// Pending drafts. Cheap on dev (always 0); on prod surfaces what's
|
|
@@ -2,15 +2,13 @@
|
|
|
2
2
|
* Regenerate dypai/types/endpoints.gen.ts from effective Flow/YAML contracts.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
runEffectiveWorkflowsCli,
|
|
9
|
-
} from "../../lib/effective-workflows-runner.js"
|
|
5
|
+
import { mkdir, writeFile } from "fs/promises"
|
|
6
|
+
import { dirname, join, resolve as resolvePath } from "path"
|
|
7
|
+
import { runEffectiveWorkflows } from "../../lib/effective-workflows-runner.js"
|
|
10
8
|
|
|
11
|
-
export async function runGenerateEndpointTypes(rootDir) {
|
|
12
|
-
const
|
|
13
|
-
const result =
|
|
9
|
+
export async function runGenerateEndpointTypes(rootDir, projectId = null) {
|
|
10
|
+
const resolvedRoot = resolvePath(rootDir)
|
|
11
|
+
const result = await runEffectiveWorkflows(resolvedRoot, projectId, ["generate-types"])
|
|
14
12
|
|
|
15
13
|
if (result.unavailable) {
|
|
16
14
|
return {
|
|
@@ -37,6 +35,22 @@ export async function runGenerateEndpointTypes(rootDir) {
|
|
|
37
35
|
}
|
|
38
36
|
}
|
|
39
37
|
|
|
38
|
+
if (payload.typesContent && payload.contractJsonContent) {
|
|
39
|
+
const outDir = join(resolvedRoot, "types")
|
|
40
|
+
await mkdir(outDir, { recursive: true })
|
|
41
|
+
const typesPath = join(outDir, "endpoints.gen.ts")
|
|
42
|
+
const contractPath = join(outDir, "endpoints.contract.json")
|
|
43
|
+
await writeFile(typesPath, payload.typesContent, "utf8")
|
|
44
|
+
await writeFile(contractPath, payload.contractJsonContent, "utf8")
|
|
45
|
+
return {
|
|
46
|
+
ok: true,
|
|
47
|
+
endpointCount: payload.endpointCount ?? 0,
|
|
48
|
+
typesPath,
|
|
49
|
+
contractPath,
|
|
50
|
+
endpoints: payload.endpoints || [],
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
40
54
|
return {
|
|
41
55
|
ok: true,
|
|
42
56
|
endpointCount: payload.endpointCount ?? 0,
|
|
@@ -56,6 +70,10 @@ export const dypaiGenerateTypesTool = {
|
|
|
56
70
|
inputSchema: {
|
|
57
71
|
type: "object",
|
|
58
72
|
properties: {
|
|
73
|
+
project_id: {
|
|
74
|
+
type: "string",
|
|
75
|
+
description: "Project UUID. Auto-resolved from DYPAI_PROJECT_ID or dypai.config.yaml if omitted.",
|
|
76
|
+
},
|
|
59
77
|
root_dir: {
|
|
60
78
|
type: "string",
|
|
61
79
|
description: "Root of the dypai/ folder (default: ./dypai).",
|
|
@@ -64,8 +82,13 @@ export const dypaiGenerateTypesTool = {
|
|
|
64
82
|
},
|
|
65
83
|
},
|
|
66
84
|
|
|
67
|
-
async execute({ root_dir = "./dypai" } = {}) {
|
|
68
|
-
const
|
|
85
|
+
async execute({ project_id, root_dir = "./dypai" } = {}) {
|
|
86
|
+
const { readLocalConfig } = await import("./planner.js")
|
|
87
|
+
const { getEnvBoundProjectId } = await import("../project-context.js")
|
|
88
|
+
const rootDir = resolvePath(process.cwd(), root_dir)
|
|
89
|
+
const config = await readLocalConfig(rootDir)
|
|
90
|
+
const projectId = project_id || getEnvBoundProjectId() || config?.project_id || null
|
|
91
|
+
const outcome = await runGenerateEndpointTypes(rootDir, projectId)
|
|
69
92
|
if (outcome.skipped) {
|
|
70
93
|
return {
|
|
71
94
|
success: false,
|
|
@@ -14,8 +14,7 @@ import YAML from "yaml"
|
|
|
14
14
|
import { proxyToolCall } from "../proxy.js"
|
|
15
15
|
import { deserializeEndpoint, serializeEndpoint } from "./codec.js"
|
|
16
16
|
import {
|
|
17
|
-
|
|
18
|
-
runEffectiveWorkflowsCli,
|
|
17
|
+
runEffectiveWorkflows,
|
|
19
18
|
} from "../../lib/effective-workflows-runner.js"
|
|
20
19
|
|
|
21
20
|
const ENDPOINT_NAME_RE = /^[a-z][a-z0-9]*(?:[-_][a-z0-9]+)*$/
|
|
@@ -434,10 +433,9 @@ export async function readLocalState(rootDir) {
|
|
|
434
433
|
* Merge YAML endpoints with effective .flow.ts entries. Flow wins over YAML
|
|
435
434
|
* with the same name (shadowed YAML is excluded from push/diff plans).
|
|
436
435
|
*/
|
|
437
|
-
export async function readLocalEffectiveState(rootDir) {
|
|
436
|
+
export async function readLocalEffectiveState(rootDir, projectId = null) {
|
|
438
437
|
const yamlState = await readLocalState(rootDir)
|
|
439
|
-
const
|
|
440
|
-
const listResult = runEffectiveWorkflowsCli(projectRoot, ["list"])
|
|
438
|
+
const listResult = await runEffectiveWorkflows(rootDir, projectId, ["list"])
|
|
441
439
|
|
|
442
440
|
if (!listResult.ok) {
|
|
443
441
|
return {
|
|
@@ -454,7 +452,7 @@ export async function readLocalEffectiveState(rootDir) {
|
|
|
454
452
|
|
|
455
453
|
for (const entry of entries) {
|
|
456
454
|
if (entry.source === "flow") {
|
|
457
|
-
const payloadResult =
|
|
455
|
+
const payloadResult = await runEffectiveWorkflows(rootDir, projectId, [
|
|
458
456
|
"push-payload",
|
|
459
457
|
"--endpoint",
|
|
460
458
|
entry.name,
|
package/src/tools/sync/push.js
CHANGED
|
@@ -369,7 +369,7 @@ export const dypaiPushTool = {
|
|
|
369
369
|
|
|
370
370
|
let typesGenerated = null
|
|
371
371
|
try {
|
|
372
|
-
typesGenerated = await runGenerateEndpointTypes(rootDir)
|
|
372
|
+
typesGenerated = await runGenerateEndpointTypes(rootDir, targetProjectId)
|
|
373
373
|
} catch (e) {
|
|
374
374
|
typesGenerated = { ok: false, error: e.message }
|
|
375
375
|
}
|
|
@@ -379,7 +379,7 @@ export const dypaiPushTool = {
|
|
|
379
379
|
let stateSnapshot
|
|
380
380
|
let draftsResult
|
|
381
381
|
try {
|
|
382
|
-
local = await readLocalEffectiveState(rootDir)
|
|
382
|
+
local = await readLocalEffectiveState(rootDir, targetProjectId)
|
|
383
383
|
} catch (e) {
|
|
384
384
|
return {
|
|
385
385
|
success: false,
|
|
@@ -23,8 +23,7 @@ import { proxyToolCall } from "../proxy.js"
|
|
|
23
23
|
import { deserializeEndpoint } from "./codec.js"
|
|
24
24
|
import { readLocalConfig, fetchRemoteState } from "./planner.js"
|
|
25
25
|
import {
|
|
26
|
-
|
|
27
|
-
runEffectiveWorkflowsCli,
|
|
26
|
+
runEffectiveWorkflows,
|
|
28
27
|
} from "../../lib/effective-workflows-runner.js"
|
|
29
28
|
import { runValidation } from "./validate.js"
|
|
30
29
|
import { getEnvBoundProjectId } from "../project-context.js"
|
|
@@ -94,9 +93,8 @@ async function findEndpointByName(rootDir, name) {
|
|
|
94
93
|
// or:
|
|
95
94
|
// { error, hint?, ...debug } (caller short-circuits with success:false)
|
|
96
95
|
|
|
97
|
-
async function resolveLocal(rootDir, endpoint, mapsCtx) {
|
|
98
|
-
const
|
|
99
|
-
const effective = runEffectiveWorkflowsCli(projectRoot, ["resolve", "--endpoint", endpoint])
|
|
96
|
+
async function resolveLocal(rootDir, endpoint, mapsCtx, projectId = null) {
|
|
97
|
+
const effective = await runEffectiveWorkflows(rootDir, projectId, ["resolve", "--endpoint", endpoint])
|
|
100
98
|
if (effective.ok && effective.data?.status === "resolved") {
|
|
101
99
|
const entry = effective.data.entry || {}
|
|
102
100
|
return {
|
|
@@ -340,7 +338,7 @@ export const dypaiTestEndpointTool = {
|
|
|
340
338
|
hint: "Check your DYPAI_TOKEN and that the project_id is correct.",
|
|
341
339
|
}
|
|
342
340
|
}
|
|
343
|
-
resolved = await resolveLocal(rootDir, endpoint, mapsCtx)
|
|
341
|
+
resolved = await resolveLocal(rootDir, endpoint, mapsCtx, targetProjectId)
|
|
344
342
|
} else if (mode === "draft") {
|
|
345
343
|
resolved = await resolveDraft(targetProjectId, endpoint)
|
|
346
344
|
} else {
|
|
@@ -19,8 +19,7 @@ import { readFile, writeFile, stat } from "fs/promises"
|
|
|
19
19
|
import { join, resolve as resolvePath } from "path"
|
|
20
20
|
import { fetchRemoteState, readLocalState, readLocalConfig, readLocalRealtime } from "./planner.js"
|
|
21
21
|
import {
|
|
22
|
-
|
|
23
|
-
runEffectiveWorkflowsCli,
|
|
22
|
+
runEffectiveWorkflows,
|
|
24
23
|
} from "../../lib/effective-workflows-runner.js"
|
|
25
24
|
import { proxyToolCall } from "../proxy.js"
|
|
26
25
|
import { getEnvBoundProjectId } from "../project-context.js"
|
|
@@ -2206,8 +2205,7 @@ export async function runValidation(rootDir, projectId) {
|
|
|
2206
2205
|
})
|
|
2207
2206
|
}
|
|
2208
2207
|
|
|
2209
|
-
const
|
|
2210
|
-
const flowValidation = runEffectiveWorkflowsCli(projectRoot, ["validate"])
|
|
2208
|
+
const flowValidation = await runEffectiveWorkflows(rootDir, targetProjectId, ["validate"])
|
|
2211
2209
|
let flowFilesChecked = 0
|
|
2212
2210
|
let effectiveEndpoints = 0
|
|
2213
2211
|
if (flowValidation.ok) {
|
|
@@ -2215,7 +2213,7 @@ export async function runValidation(rootDir, projectId) {
|
|
|
2215
2213
|
? flowValidation.data.diagnostics
|
|
2216
2214
|
: []
|
|
2217
2215
|
diagnostics.push(...flowDiagnostics)
|
|
2218
|
-
const listResult =
|
|
2216
|
+
const listResult = await runEffectiveWorkflows(rootDir, targetProjectId, ["list"])
|
|
2219
2217
|
if (listResult.ok && Array.isArray(listResult.data?.entries)) {
|
|
2220
2218
|
effectiveEndpoints = listResult.data.entries.length
|
|
2221
2219
|
flowFilesChecked = listResult.data.entries.filter(e => e.source === "flow").length
|