@dypai-ai/mcp 1.5.24 → 1.5.26
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 +5 -1
- package/src/generated/serverInstructions.js +8 -0
- package/src/index.js +227 -774
- package/src/lib/effective-workflows-runner.js +115 -0
- package/src/promptLoader.js +17 -0
- package/src/searchDocsFilter.js +47 -0
- package/src/toolProfiles.js +230 -0
- package/src/tools/generate-image.js +187 -0
- package/src/tools/manage-database.js +3 -3
- package/src/tools/project-context.js +66 -13
- package/src/tools/proxy.js +99 -46
- package/src/tools/sql-guard.js +17 -0
- package/src/tools/sync/diff.js +10 -14
- package/src/tools/sync/generate-types.js +101 -0
- package/src/tools/sync/index.js +4 -2
- package/src/tools/sync/planner.js +228 -11
- package/src/tools/sync/pull.js +96 -18
- package/src/tools/sync/push.js +143 -17
- package/src/tools/sync/test-endpoint.js +49 -14
- package/src/tools/sync/validate.js +65 -29
|
@@ -1,26 +1,29 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Auto-inject project_id into tool calls that accept it.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* the agent-facing API.
|
|
4
|
+
* Resolution order:
|
|
5
|
+
* 1. DYPAI_PROJECT_ID env (Studio / cursor-worker bound project)
|
|
6
|
+
* 2. dypai/dypai.config.yaml in the workspace
|
|
8
7
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* 2. Parse dypai.config.yaml once, cache the resolved project_id in memory.
|
|
12
|
-
* 3. For remote tool calls: if the tool's inputSchema declares project_id
|
|
13
|
-
* and the caller didn't pass one, inject the cached value.
|
|
8
|
+
* Studio profiles hide project_id from tool schemas — the bound id is injected
|
|
9
|
+
* server-side so the agent never passes it.
|
|
14
10
|
*/
|
|
15
11
|
|
|
16
12
|
import { readFileSync, existsSync } from "fs"
|
|
17
13
|
import { join, isAbsolute, dirname, delimiter } from "path"
|
|
18
14
|
import { homedir } from "os"
|
|
19
15
|
import YAML from "yaml"
|
|
16
|
+
import { isStudioProfile } from "../toolProfiles.js"
|
|
20
17
|
|
|
21
18
|
let _cached = null // { projectId, configPath } | null
|
|
22
19
|
let _resolved = false
|
|
23
20
|
|
|
21
|
+
export function getEnvBoundProjectId(env = process.env) {
|
|
22
|
+
const raw = env.DYPAI_PROJECT_ID
|
|
23
|
+
if (typeof raw === "string" && raw.trim()) return raw.trim()
|
|
24
|
+
return null
|
|
25
|
+
}
|
|
26
|
+
|
|
24
27
|
function findDypaiFolder() {
|
|
25
28
|
const envCandidates = [
|
|
26
29
|
process.env.CLAUDE_PROJECT_DIR,
|
|
@@ -48,7 +51,7 @@ function findDypaiFolder() {
|
|
|
48
51
|
return null
|
|
49
52
|
}
|
|
50
53
|
|
|
51
|
-
function
|
|
54
|
+
function resolveProjectIdFromConfig() {
|
|
52
55
|
if (_resolved) return _cached
|
|
53
56
|
_resolved = true
|
|
54
57
|
try {
|
|
@@ -71,6 +74,56 @@ export function invalidateProjectContext() {
|
|
|
71
74
|
_cached = null
|
|
72
75
|
}
|
|
73
76
|
|
|
77
|
+
/**
|
|
78
|
+
* Resolved project id for tool calls.
|
|
79
|
+
* @returns {{ projectId: string, source: "env" | "config", configPath?: string } | null}
|
|
80
|
+
*/
|
|
81
|
+
export function resolveBoundProjectId() {
|
|
82
|
+
const envId = getEnvBoundProjectId()
|
|
83
|
+
if (envId) return { projectId: envId, source: "env" }
|
|
84
|
+
|
|
85
|
+
const configCtx = resolveProjectIdFromConfig()
|
|
86
|
+
if (configCtx) {
|
|
87
|
+
return { projectId: configCtx.projectId, source: "config", configPath: configCtx.configPath }
|
|
88
|
+
}
|
|
89
|
+
return null
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function omitProjectIdFromInputSchema(inputSchema) {
|
|
93
|
+
if (!inputSchema?.properties?.project_id) return inputSchema
|
|
94
|
+
|
|
95
|
+
const { project_id: _removed, ...properties } = inputSchema.properties
|
|
96
|
+
const required = Array.isArray(inputSchema.required)
|
|
97
|
+
? inputSchema.required.filter((key) => key !== "project_id")
|
|
98
|
+
: inputSchema.required
|
|
99
|
+
|
|
100
|
+
const next = { ...inputSchema, properties }
|
|
101
|
+
if (required !== undefined) next.required = required
|
|
102
|
+
return next
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function presentToolInputSchema(tool, profile) {
|
|
106
|
+
if (!isStudioProfile(profile)) return tool.inputSchema
|
|
107
|
+
return omitProjectIdFromInputSchema(tool.inputSchema)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Studio runs bind one project — reject explicit overrides from the agent.
|
|
112
|
+
* @returns {string | null} error message
|
|
113
|
+
*/
|
|
114
|
+
export function assertBoundProjectIdMatches(profile, args) {
|
|
115
|
+
if (!isStudioProfile(profile)) return null
|
|
116
|
+
const bound = resolveBoundProjectId()
|
|
117
|
+
if (!bound?.projectId || !args?.project_id) return null
|
|
118
|
+
if (args.project_id !== bound.projectId) {
|
|
119
|
+
return (
|
|
120
|
+
`project_id is bound to ${bound.projectId} in Studio (DYPAI_PROJECT_ID). ` +
|
|
121
|
+
"Do not pass project_id — the MCP server injects it automatically."
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
return null
|
|
125
|
+
}
|
|
126
|
+
|
|
74
127
|
/**
|
|
75
128
|
* Inject project_id into args if the tool's schema declares it and the caller
|
|
76
129
|
* didn't provide one. No-op otherwise. Never overwrites an explicit value.
|
|
@@ -78,7 +131,7 @@ export function invalidateProjectContext() {
|
|
|
78
131
|
export function withProjectContext(toolDef, args) {
|
|
79
132
|
if (!toolDef?.inputSchema?.properties?.project_id) return args
|
|
80
133
|
if (args?.project_id) return args
|
|
81
|
-
const
|
|
82
|
-
if (!
|
|
83
|
-
return { ...args, project_id:
|
|
134
|
+
const bound = resolveBoundProjectId()
|
|
135
|
+
if (!bound) return args
|
|
136
|
+
return { ...args, project_id: bound.projectId }
|
|
84
137
|
}
|
package/src/tools/proxy.js
CHANGED
|
@@ -13,8 +13,40 @@ import http from "http"
|
|
|
13
13
|
|
|
14
14
|
const MCP_BASE = process.env.DYPAI_MCP_URL || "https://mcp.dypai.dev"
|
|
15
15
|
const MCP_ENDPOINT = `${MCP_BASE}/mcp`
|
|
16
|
+
// Slightly above MCP cloud POST timeout so the server 504s before we abort.
|
|
17
|
+
const MCP_PROXY_TIMEOUT_MS = Math.max(
|
|
18
|
+
30_000,
|
|
19
|
+
Number(process.env.DYPAI_MCP_PROXY_TIMEOUT_MS || 620_000),
|
|
20
|
+
)
|
|
16
21
|
|
|
17
22
|
let sessionId = null
|
|
23
|
+
let initPromise = null
|
|
24
|
+
|
|
25
|
+
function requestLabel(body) {
|
|
26
|
+
const method = body?.method || "mcp"
|
|
27
|
+
const tool = body?.params?.name
|
|
28
|
+
return tool ? `${method}:${tool}` : method
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function wrapTransportError(error, label) {
|
|
32
|
+
const message = error?.message || String(error)
|
|
33
|
+
const wrapped = new Error(`MCP ${label} transport error: ${message}`)
|
|
34
|
+
if (error?.code) wrapped.code = error.code
|
|
35
|
+
return wrapped
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function isRecoverableTransportError(error) {
|
|
39
|
+
const message = error?.message || ""
|
|
40
|
+
return /ECONNRESET|ECONNREFUSED|EPIPE|socket hang up|fetch failed|invalid session|session not found|MCP remote error 404/i.test(message)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function resetMcpSession(reason) {
|
|
44
|
+
if (sessionId || initPromise) {
|
|
45
|
+
process.stderr.write(`[dypai-mcp-proxy] resetting MCP session: ${reason}\n`)
|
|
46
|
+
}
|
|
47
|
+
sessionId = null
|
|
48
|
+
initPromise = null
|
|
49
|
+
}
|
|
18
50
|
|
|
19
51
|
function extractLastSseData(text) {
|
|
20
52
|
const events = text.split(/\r?\n\r?\n/).filter(Boolean)
|
|
@@ -82,6 +114,7 @@ function normalizeJsonRpcResponse(response) {
|
|
|
82
114
|
|
|
83
115
|
function mcpRequest(body) {
|
|
84
116
|
const token = process.env.DYPAI_TOKEN || ""
|
|
117
|
+
const label = requestLabel(body)
|
|
85
118
|
|
|
86
119
|
return new Promise((resolve, reject) => {
|
|
87
120
|
const parsed = new URL(MCP_ENDPOINT)
|
|
@@ -94,6 +127,10 @@ function mcpRequest(body) {
|
|
|
94
127
|
Authorization: `Bearer ${token}`,
|
|
95
128
|
Accept: "application/json, text/event-stream",
|
|
96
129
|
}
|
|
130
|
+
const docsChannel = process.env.DOCS_CHANNEL?.trim()
|
|
131
|
+
const docSet = process.env.DOC_SET?.trim()
|
|
132
|
+
if (docsChannel) headers["X-DYPAI-Docs-Channel"] = docsChannel
|
|
133
|
+
if (docSet) headers["X-DYPAI-Doc-Set"] = docSet
|
|
97
134
|
if (sessionId) {
|
|
98
135
|
headers["Mcp-Session-Id"] = sessionId
|
|
99
136
|
}
|
|
@@ -111,6 +148,12 @@ function mcpRequest(body) {
|
|
|
111
148
|
|
|
112
149
|
let buf = ""
|
|
113
150
|
res.on("data", (c) => (buf += c))
|
|
151
|
+
res.on("aborted", () => {
|
|
152
|
+
reject(new Error(`MCP ${label} response aborted after ${buf.length} bytes`))
|
|
153
|
+
})
|
|
154
|
+
res.on("error", (error) => {
|
|
155
|
+
reject(wrapTransportError(error, label))
|
|
156
|
+
})
|
|
114
157
|
res.on("end", () => {
|
|
115
158
|
if (res.statusCode >= 200 && res.statusCode < 300) {
|
|
116
159
|
// Handle SSE responses (text/event-stream)
|
|
@@ -121,21 +164,19 @@ function mcpRequest(body) {
|
|
|
121
164
|
try { resolve(JSON.parse(buf)) } catch { resolve({ result: buf }) }
|
|
122
165
|
}
|
|
123
166
|
} else {
|
|
124
|
-
reject(new Error(`MCP remote error ${res.statusCode}: ${buf.slice(0, 300)}`))
|
|
167
|
+
reject(new Error(`MCP remote error ${res.statusCode} during ${label}: ${buf.slice(0, 300)}`))
|
|
125
168
|
}
|
|
126
169
|
})
|
|
127
170
|
})
|
|
128
|
-
req.on("error", reject)
|
|
171
|
+
req.on("error", (error) => reject(wrapTransportError(error, label)))
|
|
172
|
+
req.setTimeout(MCP_PROXY_TIMEOUT_MS, () => {
|
|
173
|
+
req.destroy(new Error(`MCP proxy timeout after ${MCP_PROXY_TIMEOUT_MS}ms during ${label} (${MCP_ENDPOINT})`))
|
|
174
|
+
})
|
|
129
175
|
req.write(data)
|
|
130
176
|
req.end()
|
|
131
177
|
})
|
|
132
178
|
}
|
|
133
179
|
|
|
134
|
-
// Single in-flight init promise so concurrent first-calls (e.g. push with
|
|
135
|
-
// concurrency=5) don't all race to initialize and clobber each other's
|
|
136
|
-
// session id mid-handshake.
|
|
137
|
-
let initPromise = null
|
|
138
|
-
|
|
139
180
|
async function ensureInitialized() {
|
|
140
181
|
if (sessionId) return
|
|
141
182
|
if (initPromise) return initPromise
|
|
@@ -169,51 +210,63 @@ async function ensureInitialized() {
|
|
|
169
210
|
}
|
|
170
211
|
|
|
171
212
|
export async function proxyToolCall(toolName, args) {
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
// Extract result from JSON-RPC response
|
|
185
|
-
if (response.result) {
|
|
186
|
-
const { content, isError } = response.result
|
|
187
|
-
const text = Array.isArray(content) && content[0]?.text ? content[0].text : null
|
|
188
|
-
|
|
189
|
-
if (isError) {
|
|
190
|
-
throw new Error(text || "Remote tool call failed with isError=true")
|
|
191
|
-
}
|
|
213
|
+
async function callOnce() {
|
|
214
|
+
await ensureInitialized()
|
|
215
|
+
|
|
216
|
+
const response = normalizeJsonRpcResponse(await mcpRequest({
|
|
217
|
+
jsonrpc: "2.0",
|
|
218
|
+
id: `proxy-${Date.now()}`,
|
|
219
|
+
method: "tools/call",
|
|
220
|
+
params: {
|
|
221
|
+
name: toolName,
|
|
222
|
+
arguments: args || {},
|
|
223
|
+
},
|
|
224
|
+
}))
|
|
192
225
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
226
|
+
// Extract result from JSON-RPC response
|
|
227
|
+
if (response.result) {
|
|
228
|
+
const { content, isError } = response.result
|
|
229
|
+
const text = Array.isArray(content) && content[0]?.text ? content[0].text : null
|
|
230
|
+
|
|
231
|
+
if (isError) {
|
|
232
|
+
throw new Error(text || "Remote tool call failed with isError=true")
|
|
199
233
|
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
throw new Error(
|
|
234
|
+
|
|
235
|
+
if (text != null) {
|
|
236
|
+
let parsed
|
|
237
|
+
parsed = decodeTransportValue(text)
|
|
238
|
+
// Some remote errors come as a plain string without isError flag
|
|
239
|
+
if (typeof parsed === "string" && /^(Error:|Input validation error:|You don't have)/i.test(parsed)) {
|
|
240
|
+
throw new Error(parsed)
|
|
207
241
|
}
|
|
242
|
+
// Other remote errors come as a JSON object with success:false but no isError
|
|
243
|
+
// flag. The push pipeline was treating these as successes — promote them to
|
|
244
|
+
// 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
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return parsed
|
|
208
252
|
}
|
|
209
|
-
return
|
|
253
|
+
return response.result
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (response.error) {
|
|
257
|
+
throw new Error(response.error.message || "Remote tool call failed")
|
|
210
258
|
}
|
|
211
|
-
return response.result
|
|
212
|
-
}
|
|
213
259
|
|
|
214
|
-
|
|
215
|
-
throw new Error(response.error.message || "Remote tool call failed")
|
|
260
|
+
return response
|
|
216
261
|
}
|
|
217
262
|
|
|
218
|
-
|
|
263
|
+
try {
|
|
264
|
+
return await callOnce()
|
|
265
|
+
} catch (error) {
|
|
266
|
+
if (isRecoverableTransportError(error)) {
|
|
267
|
+
resetMcpSession(error.message)
|
|
268
|
+
return await callOnce()
|
|
269
|
+
}
|
|
270
|
+
throw error
|
|
271
|
+
}
|
|
219
272
|
}
|
package/src/tools/sql-guard.js
CHANGED
|
@@ -162,3 +162,20 @@ export function formatValidationError(v) {
|
|
|
162
162
|
if (v.ok) return null
|
|
163
163
|
return v.hint ? `${v.error}\nHint: ${v.hint}` : v.error
|
|
164
164
|
}
|
|
165
|
+
|
|
166
|
+
const MULTI_STATEMENT_RE =
|
|
167
|
+
/;\s*(?:SELECT|INSERT|UPDATE|DELETE|CREATE|DROP|ALTER|SET|GRANT|REVOKE|TRUNCATE|COPY|WITH|DO|COMMENT|LOAD|EXPLAIN)\b/i
|
|
168
|
+
|
|
169
|
+
export function shouldRouteSqlAsScript(sql) {
|
|
170
|
+
const cleaned = stripSqlNoise(sql)
|
|
171
|
+
return MULTI_STATEMENT_RE.test(cleaned)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function estimateStatementCount(sql) {
|
|
175
|
+
if (!shouldRouteSqlAsScript(sql)) return 1
|
|
176
|
+
const cleaned = stripSqlNoise(sql.trim())
|
|
177
|
+
let count = 1
|
|
178
|
+
const re = new RegExp(MULTI_STATEMENT_RE.source, "gi")
|
|
179
|
+
while (re.exec(cleaned)) count += 1
|
|
180
|
+
return count
|
|
181
|
+
}
|
package/src/tools/sync/diff.js
CHANGED
|
@@ -6,21 +6,13 @@
|
|
|
6
6
|
* - DRAFT : what's already been staged via prior pushes (system.config_drafts)
|
|
7
7
|
* - LIVE : what the engine actually serves at <project_id>.dypai.dev
|
|
8
8
|
*
|
|
9
|
-
* The diff body reports
|
|
10
|
-
* what `dypai_push` will queue. We additionally surface the DRAFT
|
|
11
|
-
* the agent can spot
|
|
12
|
-
* 1. Endpoints with a pending draft AND a local change → pushing again
|
|
13
|
-
* will REPLACE the existing draft with the new local version.
|
|
14
|
-
* 2. Endpoints with only a pending draft → run manage_drafts to publish/discard.
|
|
15
|
-
* 3. Endpoints with only a local change → push will create a new draft.
|
|
16
|
-
*
|
|
17
|
-
* A full 3-way semantic diff (local vs draft vs live, field-by-field) would
|
|
18
|
-
* require reserializing each draft payload and comparing — deferred. The
|
|
19
|
-
* overlap signal here covers the common workflow without the extra cost.
|
|
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.
|
|
20
12
|
*/
|
|
21
13
|
|
|
22
14
|
import { resolve as resolvePath } from "path"
|
|
23
|
-
import { fetchRemoteState,
|
|
15
|
+
import { fetchRemoteState, readLocalEffectiveState, readLocalStateSnapshot, readLocalConfig, computePlan, buildEffectiveRemoteState } from "./planner.js"
|
|
24
16
|
import { proxyToolCall } from "../proxy.js"
|
|
25
17
|
|
|
26
18
|
export const dypaiDiffTool = {
|
|
@@ -56,7 +48,7 @@ export const dypaiDiffTool = {
|
|
|
56
48
|
const targetProjectId = project_id || config?.project_id || null
|
|
57
49
|
|
|
58
50
|
const [local, remote, stateSnapshot, draftsResult] = await Promise.all([
|
|
59
|
-
|
|
51
|
+
readLocalEffectiveState(rootDir),
|
|
60
52
|
fetchRemoteState(targetProjectId),
|
|
61
53
|
readLocalStateSnapshot(rootDir),
|
|
62
54
|
// Pending drafts. Cheap on dev (always 0); on prod surfaces what's
|
|
@@ -85,9 +77,11 @@ export const dypaiDiffTool = {
|
|
|
85
77
|
}
|
|
86
78
|
}
|
|
87
79
|
|
|
88
|
-
const
|
|
80
|
+
const effectiveRemote = buildEffectiveRemoteState(remote, draftsResult)
|
|
81
|
+
const plan = computePlan(local, effectiveRemote, remote.mapsCtx, {
|
|
89
82
|
deleteOrphans: delete_orphans,
|
|
90
83
|
stateSnapshot,
|
|
84
|
+
liveRemote: remote,
|
|
91
85
|
})
|
|
92
86
|
|
|
93
87
|
const groupChanges = (plan.groups?.create?.length || 0) + (plan.groups?.delete?.length || 0)
|
|
@@ -144,6 +138,8 @@ export const dypaiDiffTool = {
|
|
|
144
138
|
? "Some YAMLs reference credentials missing remotely. Create them in the dashboard before pushing."
|
|
145
139
|
: overlap.length > 0
|
|
146
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 draft(s) — local matches what's already staged. Test with dypai_test_endpoint(mode:'draft'), then manage_drafts(operation:'publish', confirm:true) when ready.`
|
|
147
143
|
: draftCount > 0 && totalChanges > 0
|
|
148
144
|
? `${draftCount} draft(s) already pending; this diff would queue ${totalChanges} more. Consider manage_drafts(operation:'publish'|'discard', confirm:true) first to keep the staged set focused.`
|
|
149
145
|
: draftCount > 0
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regenerate dypai/types/endpoints.gen.ts from effective Flow/YAML contracts.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { resolve as resolvePath } from "path"
|
|
6
|
+
import {
|
|
7
|
+
resolveProjectRootFromDypaiDir,
|
|
8
|
+
runEffectiveWorkflowsCli,
|
|
9
|
+
} from "../../lib/effective-workflows-runner.js"
|
|
10
|
+
|
|
11
|
+
export async function runGenerateEndpointTypes(rootDir) {
|
|
12
|
+
const projectRoot = resolveProjectRootFromDypaiDir(resolvePath(rootDir))
|
|
13
|
+
const result = runEffectiveWorkflowsCli(projectRoot, ["generate-types"])
|
|
14
|
+
|
|
15
|
+
if (result.unavailable) {
|
|
16
|
+
return {
|
|
17
|
+
ok: false,
|
|
18
|
+
skipped: true,
|
|
19
|
+
reason: "runner_unavailable",
|
|
20
|
+
warning: result.warning?.message || "Effective workflow runner unavailable.",
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (!result.ok) {
|
|
25
|
+
return {
|
|
26
|
+
ok: false,
|
|
27
|
+
error: result.error || "generate-types failed",
|
|
28
|
+
stderr: result.stderr,
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const payload = result.data
|
|
33
|
+
if (!payload?.ok) {
|
|
34
|
+
return {
|
|
35
|
+
ok: false,
|
|
36
|
+
error: payload?.error || "generate-types returned failure",
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
ok: true,
|
|
42
|
+
endpointCount: payload.endpointCount ?? 0,
|
|
43
|
+
typesPath: payload.typesPath,
|
|
44
|
+
contractPath: payload.contractPath,
|
|
45
|
+
endpoints: payload.endpoints || [],
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export const dypaiGenerateTypesTool = {
|
|
50
|
+
name: "dypai_generate_types",
|
|
51
|
+
description:
|
|
52
|
+
"Regenerate frontend endpoint types from the local dypai/ backend (Flow IR + legacy YAML). " +
|
|
53
|
+
"Writes dypai/types/endpoints.gen.ts and dypai/types/endpoints.contract.json. " +
|
|
54
|
+
"Call after editing dypai/flows/*.flow.ts or endpoint YAML, before frontend work. " +
|
|
55
|
+
"Also runs automatically during dypai_push after validation.",
|
|
56
|
+
inputSchema: {
|
|
57
|
+
type: "object",
|
|
58
|
+
properties: {
|
|
59
|
+
root_dir: {
|
|
60
|
+
type: "string",
|
|
61
|
+
description: "Root of the dypai/ folder (default: ./dypai).",
|
|
62
|
+
default: "./dypai",
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
async execute({ root_dir = "./dypai" } = {}) {
|
|
68
|
+
const outcome = await runGenerateEndpointTypes(root_dir)
|
|
69
|
+
if (outcome.skipped) {
|
|
70
|
+
return {
|
|
71
|
+
success: false,
|
|
72
|
+
generated: false,
|
|
73
|
+
reason: outcome.reason,
|
|
74
|
+
hint: outcome.warning,
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (!outcome.ok) {
|
|
78
|
+
return {
|
|
79
|
+
success: false,
|
|
80
|
+
generated: false,
|
|
81
|
+
error: outcome.error,
|
|
82
|
+
stderr: outcome.stderr,
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
success: true,
|
|
87
|
+
generated: true,
|
|
88
|
+
endpoint_count: outcome.endpointCount,
|
|
89
|
+
types_path: "dypai/types/endpoints.gen.ts",
|
|
90
|
+
contract_path: "dypai/types/endpoints.contract.json",
|
|
91
|
+
endpoints: outcome.endpoints.slice(0, 50),
|
|
92
|
+
hint: outcome.endpointCount
|
|
93
|
+
? `Types refreshed for ${outcome.endpointCount} endpoint(s). Frontend can import from dypai/types/endpoints.gen.ts.`
|
|
94
|
+
: "No effective endpoints found — types file is empty.",
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export const __testing = {
|
|
100
|
+
runGenerateEndpointTypes,
|
|
101
|
+
}
|
package/src/tools/sync/index.js
CHANGED
|
@@ -5,13 +5,15 @@
|
|
|
5
5
|
* transforms.js — bidirectional field transforms (symmetric by construction)
|
|
6
6
|
* codec.js — endpoint-level serialize / deserialize (structural shape)
|
|
7
7
|
* pull.js — DB → filesystem (uses codec.serializeEndpoint)
|
|
8
|
-
* push.js — filesystem → DB (uses codec.deserializeEndpoint)
|
|
9
|
-
* diff.js — compares both sides without writing
|
|
8
|
+
* push.js — filesystem → DB (uses codec.deserializeEndpoint + effective Flow IR)
|
|
9
|
+
* diff.js — compares both sides without writing
|
|
10
|
+
* generate-types.js — regenerates dypai/types/endpoints.gen.ts from effective contracts
|
|
10
11
|
*/
|
|
11
12
|
|
|
12
13
|
export { dypaiPullTool } from "./pull.js"
|
|
13
14
|
export { dypaiDiffTool } from "./diff.js"
|
|
14
15
|
export { dypaiPushTool } from "./push.js"
|
|
16
|
+
export { dypaiGenerateTypesTool } from "./generate-types.js"
|
|
15
17
|
export { dypaiDescribeTool } from "./describe.js"
|
|
16
18
|
export { dypaiValidateTool } from "./validate.js"
|
|
17
19
|
export { dypaiTestTool } from "./test.js"
|