@dypai-ai/mcp 1.5.27 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dypai-ai/mcp",
3
- "version": "1.5.27",
3
+ "version": "1.5.28",
4
4
  "description": "DYPAI MCP Server — AI agent toolkit for building and deploying full-stack apps",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
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
- // Throttled (6h) check against the npm registry. If a newer version is
75
- // available, the update is performed and the process exits cleanly so the
76
- // IDE re-spawns it with the latest. Disable with DYPAI_NO_AUTOUPDATE=1.
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
- * Bridge to dypai-workspace/scripts/effective-workflows-cli.ts via bun.
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: "Set DYPAI_MONOREPO_ROOT or run MCP local from the DYPAI monorepo checkout with bun installed.",
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 }
@@ -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 { resolve as resolvePath } from "path"
6
- import {
7
- resolveProjectRootFromDypaiDir,
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 projectRoot = resolveProjectRootFromDypaiDir(resolvePath(rootDir))
13
- const result = runEffectiveWorkflowsCli(projectRoot, ["generate-types"])
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 outcome = await runGenerateEndpointTypes(root_dir)
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
- resolveProjectRootFromDypaiDir,
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 projectRoot = resolveProjectRootFromDypaiDir(rootDir)
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 = runEffectiveWorkflowsCli(projectRoot, [
455
+ const payloadResult = await runEffectiveWorkflows(rootDir, projectId, [
458
456
  "push-payload",
459
457
  "--endpoint",
460
458
  entry.name,
@@ -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
- resolveProjectRootFromDypaiDir,
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 projectRoot = resolveProjectRootFromDypaiDir(rootDir)
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
- resolveProjectRootFromDypaiDir,
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 projectRoot = resolveProjectRootFromDypaiDir(rootDir)
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 = runEffectiveWorkflowsCli(projectRoot, ["list"])
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