@dypai-ai/mcp 1.7.2 → 1.7.4

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.
@@ -8,6 +8,7 @@ export const DEFAULT_MCP_PROFILE = "local";
8
8
 
9
9
  const STUDIO_WORKER_TOOLS = [
10
10
  "bulk_upsert",
11
+ "dypai_generate_types",
11
12
  "dypai_test_endpoint",
12
13
  "dypai_validate",
13
14
  "execute_sql",
@@ -247,15 +248,17 @@ export function assertStudioInstructionsSanitized(instructions, { allowDebugTool
247
248
  throw new Error("Studio instructions must describe orchestrator sync/build/validate");
248
249
  }
249
250
 
250
- const withoutNegatedTypegen = instructions.replace(
251
- /endpoint typescript types are handled by studio[^\n]*/gi,
252
- "",
253
- );
254
- if (/\bdypai_generate_types\b/.test(withoutNegatedTypegen)) {
255
- throw new Error("Studio instructions must not mention dypai_generate_types");
251
+ if (!/\bdypai_generate_types\b/.test(instructions)) {
252
+ throw new Error("Studio instructions must mention dypai_generate_types");
253
+ }
254
+ if (!/\bendpoints\.gen\.ts\b/.test(instructions)) {
255
+ throw new Error("Studio instructions must mention endpoints.gen.ts");
256
+ }
257
+ if (!/dypai_validate[\s\S]{0,240}dypai_generate_types|dypai_generate_types[\s\S]{0,240}dypai_validate/i.test(instructions)) {
258
+ throw new Error("Studio instructions must describe validate then generate types");
256
259
  }
257
- if (/\bendpoints\.gen\.ts\b/.test(withoutNegatedTypegen)) {
258
- throw new Error("Studio instructions must not mention endpoints.gen.ts");
260
+ if (!/read[^\n]*endpoints\.gen\.ts|read `dypai\/types\//i.test(instructions)) {
261
+ throw new Error("Studio instructions must tell the agent to read generated endpoint types");
259
262
  }
260
263
 
261
264
  }
@@ -4,7 +4,6 @@ import YAML from "yaml"
4
4
  import { deployFromSource } from "./deploy.js"
5
5
  import { proxyToolCall } from "./proxy.js"
6
6
  import { resolveOutDir } from "./sync/path-resolver.js"
7
- import { refreshLocalStateSnapshot } from "./sync/planner.js"
8
7
 
9
8
  function resolveWorkspace({ workspace_root, sourceDirectory, root_dir } = {}) {
10
9
  if (workspace_root) {
@@ -173,13 +172,13 @@ export const dypaiDeployProductionTool = {
173
172
  live_changed: false,
174
173
  }
175
174
  }
176
- try {
177
- await refreshLocalStateSnapshot({ projectId: targetProjectId, rootDir })
178
- } catch (refreshError) {
179
- backend = {
180
- ...backend,
181
- state_baseline_refresh_warning: refreshError.message,
182
- }
175
+ backend = {
176
+ ...backend,
177
+ state_baseline_refresh: {
178
+ skipped: true,
179
+ reason: "publish_does_not_prove_local_source_ownership",
180
+ hint: "Run dypai_pull before editing a just-published endpoint again. Automatic project-wide refresh is disabled to preserve multi-user conflict detection.",
181
+ },
183
182
  }
184
183
  }
185
184
  }
@@ -65,8 +65,9 @@ function readProjectIdFromConfig(rootDir) {
65
65
  export const dypaiPushProjectTool = {
66
66
  name: "dypai_push",
67
67
  description:
68
- "SAVE CHANGES — push all local DYPAI project changes to Studio/DYPAI without publishing production. " +
68
+ "SAVE CHANGES — push local DYPAI project changes to Studio/DYPAI without publishing production. " +
69
69
  "Backend files under dypai/ are staged as drafts. Frontend files under src/, public/, package.json, and versionable dypai/ source are saved to the Studio branch. " +
70
+ "Use only_endpoints for an isolated backend push; endpoint-scoped pushes intentionally skip frontend saves and unrelated backend resources. " +
70
71
  "Production is never changed by this tool. Use dypai_deploy_production(confirm:true) after explicit user approval to publish live.",
71
72
  inputSchema: {
72
73
  type: "object",
@@ -113,6 +114,13 @@ export const dypaiPushProjectTool = {
113
114
  description: "If false, only pushes backend drafts. Default: true.",
114
115
  default: true,
115
116
  },
117
+ only_endpoints: {
118
+ type: "array",
119
+ minItems: 1,
120
+ uniqueItems: true,
121
+ items: { type: "string" },
122
+ description: "Explicit backend endpoint allowlist. When set, unrelated backend resources and the frontend save are skipped.",
123
+ },
116
124
  },
117
125
  required: [],
118
126
  },
@@ -127,6 +135,7 @@ export const dypaiPushProjectTool = {
127
135
  force = false,
128
136
  skip_validation = false,
129
137
  save_frontend = true,
138
+ only_endpoints = null,
130
139
  } = {}) {
131
140
  const { workspaceRoot, rootDir, source } = resolveWorkspace({ workspace_root, sourceDirectory, root_dir })
132
141
  const targetProjectId = project_id || readProjectIdFromConfig(rootDir)
@@ -165,6 +174,7 @@ export const dypaiPushProjectTool = {
165
174
  dry_run,
166
175
  force,
167
176
  skip_validation,
177
+ only_endpoints,
168
178
  }))
169
179
  }
170
180
 
@@ -187,6 +197,8 @@ export const dypaiPushProjectTool = {
187
197
  skipped: true,
188
198
  reason: dry_run
189
199
  ? "dry_run"
200
+ : only_endpoints
201
+ ? "endpoint_scoped_push"
190
202
  : !save_frontend
191
203
  ? "save_frontend_false"
192
204
  : hasFrontend
@@ -194,7 +206,7 @@ export const dypaiPushProjectTool = {
194
206
  : "no_package_json",
195
207
  }
196
208
 
197
- if (!dry_run && save_frontend && hasFrontend) {
209
+ if (!dry_run && !only_endpoints && save_frontend && hasFrontend) {
198
210
  frontend = await deployFromSource({
199
211
  sourceDirectory: workspaceRoot,
200
212
  project_id: targetProjectId,
@@ -214,7 +226,9 @@ export const dypaiPushProjectTool = {
214
226
  frontend_saved: hasFrontend && frontendOk && frontend.skipped !== true,
215
227
  live_changed: false,
216
228
  next_step: frontendOk
217
- ? "Changes are saved to Studio/DYPAI. Test the preview. To publish live, use dypai_deploy_production(confirm:true) after explicit user approval."
229
+ ? only_endpoints
230
+ ? `Selected backend endpoint drafts are saved. Test them, then publish only the same allowlist with manage_drafts(operation:'publish', resource_names:${JSON.stringify(only_endpoints)}, confirm:true) after explicit approval.`
231
+ : "Changes are saved to Studio/DYPAI. Test the preview. To publish live, use dypai_deploy_production(confirm:true) after explicit user approval."
218
232
  : "Frontend save failed. Fix the frontend/source issue and run dypai_push again.",
219
233
  }
220
234
  },
@@ -209,7 +209,15 @@ async function ensureInitialized() {
209
209
  return initPromise
210
210
  }
211
211
 
212
- export async function proxyToolCall(toolName, args) {
212
+ function shouldPromoteRemoteFailure(parsed, allowFailureResult = false) {
213
+ return !allowFailureResult
214
+ && parsed
215
+ && typeof parsed === "object"
216
+ && !Array.isArray(parsed)
217
+ && (parsed.success === false || parsed.ok === false)
218
+ }
219
+
220
+ export async function proxyToolCall(toolName, args, { allowFailureResult = false } = {}) {
213
221
  async function callOnce() {
214
222
  await ensureInitialized()
215
223
 
@@ -242,11 +250,9 @@ export async function proxyToolCall(toolName, args) {
242
250
  // Other remote errors come as a JSON object with success:false but no isError
243
251
  // flag. The push pipeline was treating these as successes — promote them to
244
252
  // throws here so the caller's try/catch sees them.
245
- if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
246
- if (parsed.success === false || parsed.ok === false) {
247
- const msg = parsed.error || parsed.message || parsed.detail || JSON.stringify(parsed).slice(0, 300)
248
- throw new Error(typeof msg === "string" ? msg : JSON.stringify(msg))
249
- }
253
+ if (shouldPromoteRemoteFailure(parsed, allowFailureResult)) {
254
+ const msg = parsed.error || parsed.message || parsed.detail || JSON.stringify(parsed).slice(0, 300)
255
+ throw new Error(typeof msg === "string" ? msg : JSON.stringify(msg))
250
256
  }
251
257
  return parsed
252
258
  }
@@ -270,3 +276,7 @@ export async function proxyToolCall(toolName, args) {
270
276
  throw error
271
277
  }
272
278
  }
279
+
280
+ export const __testing = {
281
+ shouldPromoteRemoteFailure,
282
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Normalize endpoint names only for migration diagnostics.
3
+ *
4
+ * Underscores and hyphens remain distinct endpoint identities at runtime.
5
+ * This normalized value must never be used for reads or mutations; it only
6
+ * surfaces legacy pairs such as `create_task` + `create-task`.
7
+ */
8
+ export function migrationCanonicalEndpointName(name) {
9
+ return typeof name === "string" ? name.replace(/_/g, "-") : ""
10
+ }
11
+
12
+ export function findEndpointNameCollisions(endpointsOrNames) {
13
+ const buckets = new Map()
14
+ for (const item of endpointsOrNames || []) {
15
+ const name = typeof item === "string" ? item : item?.name
16
+ if (!name) continue
17
+ const canonicalName = migrationCanonicalEndpointName(name)
18
+ if (!buckets.has(canonicalName)) buckets.set(canonicalName, new Set())
19
+ buckets.get(canonicalName).add(name)
20
+ }
21
+
22
+ return [...buckets.entries()]
23
+ .map(([canonical_name, names]) => ({
24
+ canonical_name,
25
+ endpoints: [...names].sort(),
26
+ }))
27
+ .filter((item) => item.endpoints.length > 1)
28
+ .sort((a, b) => a.canonical_name.localeCompare(b.canonical_name))
29
+ }
@@ -2,13 +2,94 @@
2
2
  * Regenerate dypai/types/endpoints.gen.ts from effective Flow/YAML contracts.
3
3
  */
4
4
 
5
+ import { createHash } from "node:crypto"
6
+ import { existsSync, lstatSync, readFileSync, realpathSync } from "node:fs"
5
7
  import { mkdir, writeFile } from "fs/promises"
6
- import { dirname, join, resolve as resolvePath } from "path"
8
+ import { dirname, isAbsolute, join, resolve as resolvePath, sep } from "path"
7
9
  import { runEffectiveWorkflows } from "../../lib/effective-workflows-runner.js"
10
+ import { resolveAndGuard } from "./path-resolver.js"
11
+ import { isStudioProfile, resolveMcpProfile } from "../../toolProfiles.js"
8
12
 
9
- export async function runGenerateEndpointTypes(rootDir, projectId = null) {
13
+ function assertUnderRoot(root, target) {
14
+ const resolvedRoot = resolvePath(root)
15
+ const resolvedTarget = resolvePath(target)
16
+ if (resolvedTarget !== resolvedRoot && !resolvedTarget.startsWith(resolvedRoot + sep)) {
17
+ throw new Error("Path escapes workspace root")
18
+ }
19
+ }
20
+
21
+ function assertNoSymlinkPath(root, target) {
22
+ const resolvedRoot = resolvePath(root)
23
+ const resolvedTarget = resolvePath(target)
24
+ assertUnderRoot(resolvedRoot, resolvedTarget)
25
+ if (!existsSync(resolvedRoot)) {
26
+ throw new Error("Workspace root does not exist")
27
+ }
28
+
29
+ const realRoot = realpathSync(resolvedRoot)
30
+ let cursor = resolvedTarget
31
+ while (true) {
32
+ if (existsSync(cursor)) {
33
+ if (lstatSync(cursor).isSymbolicLink()) {
34
+ throw new Error(`Symlink paths are not allowed for generated artifacts: ${cursor}`)
35
+ }
36
+ assertUnderRoot(realRoot, realpathSync(cursor))
37
+ }
38
+ if (cursor === resolvedRoot) break
39
+ const parent = dirname(cursor)
40
+ if (
41
+ parent === cursor
42
+ || (parent !== resolvedRoot && !parent.startsWith(resolvedRoot + sep))
43
+ ) {
44
+ throw new Error("Path escapes workspace root")
45
+ }
46
+ cursor = parent
47
+ }
48
+ }
49
+
50
+ function hashSnapshotFiles(files) {
51
+ const parts = (files || [])
52
+ .map((file) => `${file.path}\0${file.content}`)
53
+ .sort((a, b) => a.localeCompare(b))
54
+ return createHash("sha256").update(parts.join("\n")).digest("hex").slice(0, 16)
55
+ }
56
+
57
+ async function writeGeneratedArtifacts(resolvedRoot, payload) {
58
+ const outDir = join(resolvedRoot, "types")
59
+ assertNoSymlinkPath(resolvedRoot, outDir)
60
+ await mkdir(outDir, { recursive: true })
61
+ const typesPath = join(outDir, "endpoints.gen.ts")
62
+ const contractPath = join(outDir, "endpoints.contract.json")
63
+ assertNoSymlinkPath(resolvedRoot, typesPath)
64
+ assertNoSymlinkPath(resolvedRoot, contractPath)
65
+
66
+ const writes = []
67
+ if (payload.typesContent) {
68
+ writes.push({ path: typesPath, content: payload.typesContent })
69
+ }
70
+ if (payload.contractJsonContent) {
71
+ writes.push({ path: contractPath, content: payload.contractJsonContent })
72
+ }
73
+
74
+ const writtenPaths = []
75
+ const unchangedPaths = []
76
+
77
+ for (const item of writes) {
78
+ const current = existsSync(item.path) ? readFileSync(item.path, "utf8") : null
79
+ if (current === item.content) {
80
+ unchangedPaths.push(item.path)
81
+ continue
82
+ }
83
+ await writeFile(item.path, item.content, "utf8")
84
+ writtenPaths.push(item.path)
85
+ }
86
+
87
+ return { typesPath, contractPath, writtenPaths, unchangedPaths }
88
+ }
89
+
90
+ export async function runGenerateEndpointTypes(rootDir, projectId = null, options = {}) {
10
91
  const resolvedRoot = resolvePath(rootDir)
11
- const result = await runEffectiveWorkflows(resolvedRoot, projectId, ["generate-types"])
92
+ const result = await runEffectiveWorkflows(resolvedRoot, projectId, ["generate-types"], options.deps)
12
93
 
13
94
  if (result.unavailable) {
14
95
  return {
@@ -35,36 +116,150 @@ export async function runGenerateEndpointTypes(rootDir, projectId = null) {
35
116
  }
36
117
  }
37
118
 
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")
119
+ const errorCount = payload.errorCount ?? 0
120
+ const warningCount = payload.warningCount ?? 0
121
+ const diagnosticCount = payload.diagnosticCount ?? 0
122
+ const diagnostics = Array.isArray(payload.diagnostics) ? payload.diagnostics.slice(0, 10) : []
123
+ const endpoints = payload.endpoints || []
124
+ const endpointCount = payload.endpointCount ?? 0
125
+ const snapshotHash = Array.isArray(payload.files)
126
+ ? hashSnapshotFiles(payload.files)
127
+ : options.snapshotHash ?? null
128
+
129
+ if (
130
+ typeof payload.typesContent !== "string"
131
+ || !payload.typesContent.trim()
132
+ || typeof payload.contractJsonContent !== "string"
133
+ || !payload.contractJsonContent.trim()
134
+ ) {
45
135
  return {
46
- ok: true,
47
- endpointCount: payload.endpointCount ?? 0,
48
- typesPath,
49
- contractPath,
50
- endpoints: payload.endpoints || [],
51
- diagnosticCount: payload.diagnosticCount ?? 0,
52
- errorCount: payload.errorCount ?? 0,
53
- warningCount: payload.warningCount ?? 0,
54
- diagnostics: Array.isArray(payload.diagnostics) ? payload.diagnostics.slice(0, 10) : [],
136
+ ok: false,
137
+ error: "generate-types returned an incomplete artifact payload",
55
138
  }
56
139
  }
57
140
 
141
+ try {
142
+ const contract = JSON.parse(payload.contractJsonContent)
143
+ if (!contract || typeof contract !== "object" || Array.isArray(contract)) {
144
+ throw new Error("contract JSON must be an object")
145
+ }
146
+ } catch (error) {
147
+ return {
148
+ ok: false,
149
+ error: `generate-types returned invalid contract JSON: ${error?.message || String(error)}`,
150
+ }
151
+ }
152
+
153
+ const { typesPath, contractPath, writtenPaths, unchangedPaths } = await writeGeneratedArtifacts(
154
+ resolvedRoot,
155
+ payload,
156
+ )
58
157
  return {
59
158
  ok: true,
60
- endpointCount: payload.endpointCount ?? 0,
61
- typesPath: payload.typesPath,
62
- contractPath: payload.contractPath,
63
- endpoints: payload.endpoints || [],
64
- diagnosticCount: payload.diagnosticCount ?? 0,
65
- errorCount: payload.errorCount ?? 0,
66
- warningCount: payload.warningCount ?? 0,
67
- diagnostics: Array.isArray(payload.diagnostics) ? payload.diagnostics.slice(0, 10) : [],
159
+ endpointCount,
160
+ typesPath,
161
+ contractPath,
162
+ endpoints,
163
+ diagnosticCount,
164
+ errorCount,
165
+ warningCount,
166
+ diagnostics,
167
+ contractReady: errorCount === 0,
168
+ writtenPaths,
169
+ unchangedPaths,
170
+ snapshotHash,
171
+ cached: result.cached === true,
172
+ }
173
+ }
174
+
175
+ function resolveGenerateTypesRoot(root_dir = "./dypai") {
176
+ const profile = resolveMcpProfile(process.env)
177
+ if (isStudioProfile(profile)) {
178
+ const workspaceRoot = process.env.DYPAI_WORKSPACE_ROOT
179
+ if (!workspaceRoot) {
180
+ return {
181
+ ok: false,
182
+ error: {
183
+ success: false,
184
+ generated: false,
185
+ error: "Studio worker requires DYPAI_WORKSPACE_ROOT for endpoint type generation.",
186
+ },
187
+ }
188
+ }
189
+
190
+ const resolvedWorkspace = resolvePath(workspaceRoot)
191
+ const resolvedDypai = resolvePath(resolvedWorkspace, "dypai")
192
+ const suppliedPath = isAbsolute(root_dir)
193
+ ? resolvePath(root_dir)
194
+ : resolvePath(resolvedWorkspace, root_dir)
195
+ if (suppliedPath !== resolvedDypai) {
196
+ return {
197
+ ok: false,
198
+ error: {
199
+ success: false,
200
+ generated: false,
201
+ error: "Studio endpoint type generation is restricted to the canonical dypai/ directory.",
202
+ resolved_to: suppliedPath,
203
+ required_root: resolvedDypai,
204
+ },
205
+ }
206
+ }
207
+
208
+ try {
209
+ assertNoSymlinkPath(resolvedWorkspace, resolvedDypai)
210
+ } catch (error) {
211
+ return {
212
+ ok: false,
213
+ error: {
214
+ success: false,
215
+ generated: false,
216
+ error: "Studio dypai/ must be a real directory inside the workspace (symlinks are not allowed).",
217
+ detail: error?.message || String(error),
218
+ resolved_to: resolvedDypai,
219
+ workspace_root: resolvedWorkspace,
220
+ },
221
+ }
222
+ }
223
+
224
+ return { ok: true, path: resolvedDypai, source: "env:DYPAI_WORKSPACE_ROOT" }
225
+ }
226
+
227
+ const guarded = resolveAndGuard(root_dir, { tool: "dypai_generate_types", arg_name: "root_dir" })
228
+ if (!guarded.ok) {
229
+ return guarded
230
+ }
231
+
232
+ return { ok: true, path: guarded.path, source: guarded.source }
233
+ }
234
+
235
+ function buildSuccessResponse(outcome) {
236
+ const generated = outcome.writtenPaths.length > 0
237
+ const refreshed = generated || outcome.unchangedPaths.length > 0
238
+ return {
239
+ success: true,
240
+ generated: refreshed,
241
+ files_written: outcome.writtenPaths.map((path) => path.replace(/^.*\/dypai\//, "dypai/")),
242
+ files_unchanged: outcome.unchangedPaths.map((path) => path.replace(/^.*\/dypai\//, "dypai/")),
243
+ contract_ready: outcome.contractReady,
244
+ endpoint_count: outcome.endpointCount,
245
+ types_path: "dypai/types/endpoints.gen.ts",
246
+ contract_path: "dypai/types/endpoints.contract.json",
247
+ endpoints: outcome.endpoints.slice(0, 50),
248
+ snapshot_hash: outcome.snapshotHash ?? undefined,
249
+ cached: outcome.cached === true,
250
+ diagnostics_summary: outcome.diagnosticCount
251
+ ? {
252
+ total: outcome.diagnosticCount,
253
+ errors: outcome.errorCount,
254
+ warnings: outcome.warningCount,
255
+ sample: outcome.diagnostics,
256
+ }
257
+ : undefined,
258
+ hint: outcome.contractReady
259
+ ? outcome.endpointCount
260
+ ? `Types refreshed for ${outcome.endpointCount} endpoint(s). Read dypai/types/endpoints.gen.ts before wiring frontend.`
261
+ : "No effective endpoints found — types file is empty."
262
+ : "Types were written but backend validation reported errors. Fix backend issues, run dypai_validate, then call dypai_generate_types again before frontend work.",
68
263
  }
69
264
  }
70
265
 
@@ -73,7 +268,8 @@ export const dypaiGenerateTypesTool = {
73
268
  description:
74
269
  "Regenerate frontend endpoint types from the local dypai/ backend (Flow IR + legacy YAML). " +
75
270
  "Writes dypai/types/endpoints.gen.ts and dypai/types/endpoints.contract.json. " +
76
- "Call after editing dypai/flows/*.flow.ts or endpoint YAML, before frontend work. " +
271
+ "Call after editing dypai/flows/*.flow.ts or endpoint YAML and after dypai_validate, before frontend work. " +
272
+ "In Studio worker mode, call this after backend contract edits and before wiring src/. " +
77
273
  "Also runs automatically during dypai_push after validation.",
78
274
  inputSchema: {
79
275
  type: "object",
@@ -91,9 +287,14 @@ export const dypaiGenerateTypesTool = {
91
287
  },
92
288
 
93
289
  async execute({ project_id, root_dir = "./dypai" } = {}) {
290
+ const resolved = resolveGenerateTypesRoot(root_dir)
291
+ if (!resolved.ok) {
292
+ return resolved.error
293
+ }
294
+
94
295
  const { readLocalConfig } = await import("./planner.js")
95
296
  const { getEnvBoundProjectId } = await import("../project-context.js")
96
- const rootDir = resolvePath(process.cwd(), root_dir)
297
+ const rootDir = resolved.path
97
298
  const config = await readLocalConfig(rootDir)
98
299
  const projectId = project_id || getEnvBoundProjectId() || config?.project_id || null
99
300
  const outcome = await runGenerateEndpointTypes(rootDir, projectId)
@@ -101,6 +302,7 @@ export const dypaiGenerateTypesTool = {
101
302
  return {
102
303
  success: false,
103
304
  generated: false,
305
+ contract_ready: false,
104
306
  reason: outcome.reason,
105
307
  hint: outcome.warning,
106
308
  }
@@ -109,32 +311,18 @@ export const dypaiGenerateTypesTool = {
109
311
  return {
110
312
  success: false,
111
313
  generated: false,
314
+ contract_ready: false,
112
315
  error: outcome.error,
113
316
  stderr: outcome.stderr,
114
317
  }
115
318
  }
116
- return {
117
- success: true,
118
- generated: true,
119
- endpoint_count: outcome.endpointCount,
120
- types_path: "dypai/types/endpoints.gen.ts",
121
- contract_path: "dypai/types/endpoints.contract.json",
122
- endpoints: outcome.endpoints.slice(0, 50),
123
- diagnostics_summary: outcome.diagnosticCount
124
- ? {
125
- total: outcome.diagnosticCount,
126
- errors: outcome.errorCount,
127
- warnings: outcome.warningCount,
128
- sample: outcome.diagnostics,
129
- }
130
- : undefined,
131
- hint: outcome.endpointCount
132
- ? `Types refreshed for ${outcome.endpointCount} endpoint(s). Frontend can import from dypai/types/endpoints.gen.ts.${outcome.errorCount ? " Backend errors were found; run dypai_validate before trusting frontend call sites." : ""}`
133
- : "No effective endpoints found — types file is empty.",
134
- }
319
+ return buildSuccessResponse(outcome)
135
320
  },
136
321
  }
137
322
 
138
323
  export const __testing = {
139
324
  runGenerateEndpointTypes,
325
+ resolveGenerateTypesRoot,
326
+ buildSuccessResponse,
327
+ writeGeneratedArtifacts,
140
328
  }