@dypai-ai/mcp 1.5.16 → 1.5.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/tools/deploy.js +24 -7
- package/src/tools/scaffold.js +6 -5
- package/src/tools/sync/pull.js +37 -7
- package/src/tools/sync/validate.js +49 -0
package/package.json
CHANGED
package/src/tools/deploy.js
CHANGED
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
*/
|
|
27
27
|
|
|
28
28
|
import { createHash } from "crypto"
|
|
29
|
-
import { readFileSync, readdirSync, statSync, existsSync, writeFileSync, mkdirSync } from "fs"
|
|
29
|
+
import { readFileSync, readdirSync, statSync, lstatSync, existsSync, writeFileSync, mkdirSync } from "fs"
|
|
30
30
|
import { join, basename, dirname, resolve } from "path"
|
|
31
31
|
import { api } from "../api.js"
|
|
32
32
|
|
|
@@ -37,7 +37,8 @@ const MAX_BUNDLED_FILE = 25 * 1024 * 1024
|
|
|
37
37
|
|
|
38
38
|
// ─── Directories to skip ────────────────────────────────────────────────────
|
|
39
39
|
|
|
40
|
-
|
|
40
|
+
// Skipped at any depth in the tree — these never carry user code.
|
|
41
|
+
const IGNORE_DIRS_ANYWHERE = new Set([
|
|
41
42
|
"node_modules", ".git",
|
|
42
43
|
// Build outputs
|
|
43
44
|
"dist", "build", "out", ".output", ".vercel", ".netlify",
|
|
@@ -46,8 +47,16 @@ const IGNORE_DIRS = new Set([
|
|
|
46
47
|
".cache", ".turbo", ".vite", ".parcel-cache", ".wrangler",
|
|
47
48
|
// Test / misc
|
|
48
49
|
"coverage", "storybook-static", "__pycache__", ".idea", ".vscode",
|
|
49
|
-
|
|
50
|
-
|
|
50
|
+
])
|
|
51
|
+
|
|
52
|
+
// Skipped ONLY at the project root. `dypai/` at root holds backend metadata
|
|
53
|
+
// (endpoint YAMLs, schema.sql, prompts) materialized by dypai_pull and
|
|
54
|
+
// shipped via dypai_push — it must NOT enter the frontend deploy. But a
|
|
55
|
+
// nested folder named `dypai/` (e.g. `src/integrations/dypai/`) is legitimate
|
|
56
|
+
// user code — the SDK client setup lives there. Matching by name alone (the
|
|
57
|
+
// previous behavior) silently dropped that folder and produced
|
|
58
|
+
// "Could not resolve '../integrations/dypai/client'" build errors.
|
|
59
|
+
const IGNORE_DIRS_AT_ROOT = new Set([
|
|
51
60
|
"dypai",
|
|
52
61
|
])
|
|
53
62
|
|
|
@@ -220,7 +229,7 @@ function classifySkip(path, ext, size) {
|
|
|
220
229
|
return null
|
|
221
230
|
}
|
|
222
231
|
|
|
223
|
-
function collectSource(dir) {
|
|
232
|
+
export function collectSource(dir) {
|
|
224
233
|
const allFiles = []
|
|
225
234
|
const skipped = []
|
|
226
235
|
const textByPath = new Map()
|
|
@@ -231,10 +240,18 @@ function collectSource(dir) {
|
|
|
231
240
|
let entries
|
|
232
241
|
try { entries = readdirSync(d) } catch { return }
|
|
233
242
|
for (const entry of entries) {
|
|
234
|
-
if (
|
|
243
|
+
if (IGNORE_DIRS_ANYWHERE.has(entry)) continue
|
|
244
|
+
if (rel === "" && IGNORE_DIRS_AT_ROOT.has(entry)) continue
|
|
235
245
|
const full = join(d, entry)
|
|
236
246
|
try {
|
|
237
|
-
|
|
247
|
+
// lstat (not stat): a symlink should be classified as a symlink, NOT
|
|
248
|
+
// as the target it points to. Otherwise a folder like `src/sneaky →
|
|
249
|
+
// /Users/me/something` would be walked and code outside the project
|
|
250
|
+
// root would be quietly committed to the user's repo on deploy.
|
|
251
|
+
// Symlinks are skipped outright — DYPAI projects don't need them
|
|
252
|
+
// and there is no safe automatic policy for following them.
|
|
253
|
+
const stat = lstatSync(full)
|
|
254
|
+
if (stat.isSymbolicLink()) continue
|
|
238
255
|
if (stat.isDirectory()) {
|
|
239
256
|
if (entry.startsWith(".")) continue
|
|
240
257
|
walk(full, rel ? `${rel}/${entry}` : entry)
|
package/src/tools/scaffold.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { writeFileSync, mkdirSync, existsSync } from "fs"
|
|
9
|
-
import { join } from "path"
|
|
9
|
+
import { join, dirname } from "path"
|
|
10
10
|
import { api } from "../api.js"
|
|
11
11
|
|
|
12
12
|
export const scaffoldTool = {
|
|
@@ -70,7 +70,7 @@ Use only visible Studio catalog template slugs; do not invent legacy slugs.`,
|
|
|
70
70
|
name: directory.split("/").pop() || "my-app",
|
|
71
71
|
private: true, version: "0.0.1", type: "module",
|
|
72
72
|
scripts: { dev: "vite", build: "vite build", preview: "vite preview" },
|
|
73
|
-
dependencies: { "@dypai-ai/client-sdk": "
|
|
73
|
+
dependencies: { "@dypai-ai/client-sdk": "1.11.0", react: "^19.0.0", "react-dom": "^19.0.0" },
|
|
74
74
|
devDependencies: { "@vitejs/plugin-react": "^4.3.0", vite: "^6.0.0", typescript: "^5.6.0", "@types/react": "^19.0.0", "@types/react-dom": "^19.0.0" },
|
|
75
75
|
}, null, 2) },
|
|
76
76
|
{ path: "vite.config.ts", content: `import { defineConfig } from 'vite'\nimport react from '@vitejs/plugin-react'\n\nexport default defineConfig({ plugins: [react()] })\n` },
|
|
@@ -116,12 +116,13 @@ Use only visible Studio catalog template slugs; do not invent legacy slugs.`,
|
|
|
116
116
|
// SDK client helper (lib/dypai.ts)
|
|
117
117
|
files.push({ path: "src/lib/dypai.ts", content: `import { createClient } from "@dypai-ai/client-sdk";\n\nexport const dypai = createClient(import.meta.env.VITE_DYPAI_URL);\n` })
|
|
118
118
|
|
|
119
|
-
// Write files to disk
|
|
119
|
+
// Write files to disk. Use `dirname()` rather than slicing on "/" — on
|
|
120
|
+
// Windows the path separator is "\" so the slice approach produced a
|
|
121
|
+
// garbage parent dir and the writeFileSync below failed with ENOENT.
|
|
120
122
|
let created = 0
|
|
121
123
|
for (const file of files) {
|
|
122
124
|
const fullPath = join(directory, file.path)
|
|
123
|
-
|
|
124
|
-
mkdirSync(dir, { recursive: true })
|
|
125
|
+
mkdirSync(dirname(fullPath), { recursive: true })
|
|
125
126
|
writeFileSync(fullPath, file.content)
|
|
126
127
|
created++
|
|
127
128
|
}
|
package/src/tools/sync/pull.js
CHANGED
|
@@ -503,6 +503,28 @@ function renderYaml(doc) {
|
|
|
503
503
|
})
|
|
504
504
|
}
|
|
505
505
|
|
|
506
|
+
/**
|
|
507
|
+
* Build the state.json payload, restricted to endpoints that were actually
|
|
508
|
+
* written to disk during this pull. The push planner uses this snapshot to
|
|
509
|
+
* detect remote drift since pull (compares remote.updated_at against
|
|
510
|
+
* snapshot.updated_at per endpoint). Including endpoints that failed to
|
|
511
|
+
* serialize would create a phantom "we have a fresh copy of this" claim and
|
|
512
|
+
* silently mask real conflicts on those rows.
|
|
513
|
+
*
|
|
514
|
+
* Exported for unit testing.
|
|
515
|
+
*/
|
|
516
|
+
export function buildStateSnapshot({ endpoints, successfullyPulledIds, projectId, now = new Date() }) {
|
|
517
|
+
return {
|
|
518
|
+
pulled_at: now.toISOString(),
|
|
519
|
+
project_id: projectId,
|
|
520
|
+
endpoints: Object.fromEntries(
|
|
521
|
+
endpoints
|
|
522
|
+
.filter(e => successfullyPulledIds.has(e.id))
|
|
523
|
+
.map(e => [e.name, { id: e.id, updated_at: e.updated_at }])
|
|
524
|
+
),
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
506
528
|
// schema.sql dump lives in ./schema-dump.js (shared with execute_sql auto-refresh)
|
|
507
529
|
|
|
508
530
|
export const dypaiPullTool = {
|
|
@@ -668,6 +690,15 @@ export const dypaiPullTool = {
|
|
|
668
690
|
filesWritten.push("realtime.yaml")
|
|
669
691
|
}
|
|
670
692
|
|
|
693
|
+
// Track which endpoints survived serialization → only those go into
|
|
694
|
+
// state.json. Including failed ones would lie to the push planner about
|
|
695
|
+
// what's locally in sync with remote (the conflict detector compares
|
|
696
|
+
// remote.updated_at against the snapshot per endpoint, and a phantom
|
|
697
|
+
// snapshot row for an endpoint that never made it to disk hides a real
|
|
698
|
+
// conflict). Tracked by row id so endpoint renames at the same name
|
|
699
|
+
// can't accidentally inherit a stale snapshot.
|
|
700
|
+
const successfullyPulled = new Set()
|
|
701
|
+
|
|
671
702
|
for (const rawRow of endpoints) {
|
|
672
703
|
const row = hydrateRow(rawRow)
|
|
673
704
|
try {
|
|
@@ -702,6 +733,7 @@ export const dypaiPullTool = {
|
|
|
702
733
|
|
|
703
734
|
await writeFileEnsured(join(outDir, relPath), content)
|
|
704
735
|
filesWritten.push(relPath)
|
|
736
|
+
successfullyPulled.add(row.id)
|
|
705
737
|
} catch (e) {
|
|
706
738
|
errors.push({ endpoint: row.name, error: e.message })
|
|
707
739
|
}
|
|
@@ -756,13 +788,11 @@ export const dypaiPullTool = {
|
|
|
756
788
|
await writeFile(configPath, configYaml, "utf8")
|
|
757
789
|
|
|
758
790
|
// .dypai/state.json: GITIGNORED — per-endpoint updated_at for conflict detection.
|
|
759
|
-
const state = {
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
),
|
|
765
|
-
}
|
|
791
|
+
const state = buildStateSnapshot({
|
|
792
|
+
endpoints,
|
|
793
|
+
successfullyPulledIds: successfullyPulled,
|
|
794
|
+
projectId: resolvedProjectId,
|
|
795
|
+
})
|
|
766
796
|
await writeFileEnsured(join(outDir, ".dypai", "state.json"), JSON.stringify(state, null, 2) + "\n")
|
|
767
797
|
|
|
768
798
|
// Codegen removed from v1. If we reintroduce it, this is where it wires in.
|
|
@@ -316,6 +316,53 @@ async function readSchemaColumns(rootDir) {
|
|
|
316
316
|
}
|
|
317
317
|
|
|
318
318
|
/** Extract referenced table names from a SQL string: `FROM public.X`, `JOIN public.X`, `INTO public.X`, `UPDATE public.X`. */
|
|
319
|
+
function skipSqlBalancedParens(sql, start) {
|
|
320
|
+
let depth = 0
|
|
321
|
+
for (let i = start; i < sql.length; i++) {
|
|
322
|
+
const ch = sql[i]
|
|
323
|
+
if (ch === "\"") {
|
|
324
|
+
i++
|
|
325
|
+
while (i < sql.length && sql[i] !== "\"") i++
|
|
326
|
+
continue
|
|
327
|
+
}
|
|
328
|
+
if (ch === "(") depth++
|
|
329
|
+
else if (ch === ")") {
|
|
330
|
+
depth--
|
|
331
|
+
if (depth === 0) return i + 1
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
return sql.length
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function extractSqlCteNames(cleanSql) {
|
|
338
|
+
const names = new Set()
|
|
339
|
+
const withMatch = /^\s*WITH\s+(?:RECURSIVE\s+)?/i.exec(cleanSql)
|
|
340
|
+
if (!withMatch) return names
|
|
341
|
+
let i = withMatch[0].length
|
|
342
|
+
while (i < cleanSql.length) {
|
|
343
|
+
while (/\s|,/.test(cleanSql[i] || "")) i++
|
|
344
|
+
const nameMatch = /^"?([A-Za-z_][A-Za-z0-9_]*)"?/.exec(cleanSql.slice(i))
|
|
345
|
+
if (!nameMatch) break
|
|
346
|
+
const cteName = nameMatch[1]
|
|
347
|
+
if (SQL_KEYWORDS_AFTER_FROM.has(cteName.toUpperCase())) break
|
|
348
|
+
i += nameMatch[0].length
|
|
349
|
+
while (/\s/.test(cleanSql[i] || "")) i++
|
|
350
|
+
if (cleanSql[i] === "(") {
|
|
351
|
+
i = skipSqlBalancedParens(cleanSql, i)
|
|
352
|
+
while (/\s/.test(cleanSql[i] || "")) i++
|
|
353
|
+
}
|
|
354
|
+
if (!/^AS\b/i.test(cleanSql.slice(i))) break
|
|
355
|
+
i += 2
|
|
356
|
+
while (/\s/.test(cleanSql[i] || "")) i++
|
|
357
|
+
if (cleanSql[i] !== "(") break
|
|
358
|
+
names.add(cteName)
|
|
359
|
+
i = skipSqlBalancedParens(cleanSql, i)
|
|
360
|
+
while (/\s/.test(cleanSql[i] || "")) i++
|
|
361
|
+
if (cleanSql[i] !== ",") break
|
|
362
|
+
}
|
|
363
|
+
return names
|
|
364
|
+
}
|
|
365
|
+
|
|
319
366
|
function extractSqlTables(sql) {
|
|
320
367
|
const tables = new Set()
|
|
321
368
|
if (typeof sql !== "string" || sql.length === 0) return tables
|
|
@@ -325,6 +372,7 @@ function extractSqlTables(sql) {
|
|
|
325
372
|
.replace(/--[^\n]*/g, " ")
|
|
326
373
|
.replace(/\/\*[\s\S]*?\*\//g, " ")
|
|
327
374
|
.replace(/'(?:[^']|'')*'/g, "''")
|
|
375
|
+
const cteNames = extractSqlCteNames(clean)
|
|
328
376
|
// Single regex that captures BOTH a possible schema and the table name,
|
|
329
377
|
// optionally preceded by `ONLY` (Postgres inheritance modifier). Splitting
|
|
330
378
|
// schema/table into separate groups makes filtering by schema trivial.
|
|
@@ -339,6 +387,7 @@ function extractSqlTables(sql) {
|
|
|
339
387
|
// when the regex was greedy enough (defensive — the optional `ONLY` above
|
|
340
388
|
// already handles the common case).
|
|
341
389
|
if (SQL_KEYWORDS_AFTER_FROM.has(tableName.toUpperCase())) continue
|
|
390
|
+
if (cteNames.has(tableName)) continue
|
|
342
391
|
// Only validate tables in the user-managed `public` schema. System
|
|
343
392
|
// schemas (auth, system, ext, pg_catalog, information_schema) are
|
|
344
393
|
// managed by the engine and not present in dypai/schema.sql.
|