@dypai-ai/mcp 1.6.16 → 1.6.17

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.
@@ -0,0 +1,217 @@
1
+ import { createHash } from "node:crypto"
2
+ import { mkdir, readFile, readdir, stat, writeFile } from "fs/promises"
3
+ import { dirname, join } from "path"
4
+
5
+ const BACKEND_SOURCE_DIRS = ["flows", "automations", "endpoints", "migrations", "types", "lib"]
6
+ const BACKEND_SOURCE_EXACT = [
7
+ "schema.sql",
8
+ "realtime.yaml",
9
+ "credentials.yaml",
10
+ "node-catalog.json",
11
+ "capability-catalog.json",
12
+ "capability-brief.md",
13
+ "dypai.config.yaml",
14
+ ]
15
+ const BACKEND_SOURCE_EXTS = new Set([
16
+ ".ts", ".tsx", ".js", ".jsx", ".mjs", ".mts", ".cjs", ".cts",
17
+ ".json", ".yaml", ".yml", ".sql", ".md", ".txt",
18
+ ])
19
+
20
+ function extOfPath(path) {
21
+ const filename = path.split("/").pop() || path
22
+ return filename.includes(".") ? `.${filename.split(".").pop().toLowerCase()}` : ""
23
+ }
24
+
25
+ function isTextBackendPath(path) {
26
+ return BACKEND_SOURCE_EXTS.has(extOfPath(path))
27
+ }
28
+
29
+ function normalizePath(path) {
30
+ return String(path || "").replace(/\\/g, "/").replace(/^\/+/, "")
31
+ }
32
+
33
+ export function isTrackedBackendSourcePath(path) {
34
+ const normalized = normalizePath(path)
35
+ if (!normalized.startsWith("dypai/")) return false
36
+ if (normalized.startsWith("dypai/.dypai/")) return false
37
+ const rel = normalized.slice("dypai/".length)
38
+ if (BACKEND_SOURCE_EXACT.includes(rel)) return true
39
+ return BACKEND_SOURCE_DIRS.some((dir) => rel.startsWith(`${dir}/`)) && isTextBackendPath(rel)
40
+ }
41
+
42
+ function hashContent(content) {
43
+ return createHash("sha256").update(content).digest("hex")
44
+ }
45
+
46
+ async function addFile(files, absPath, relPath) {
47
+ if (!isTextBackendPath(relPath)) return
48
+ const content = await readFile(absPath, "utf8")
49
+ files.push({
50
+ path: `dypai/${relPath}`,
51
+ content,
52
+ sha256: hashContent(content),
53
+ })
54
+ }
55
+
56
+ async function walkDir(files, absDir, relBase) {
57
+ let entries = []
58
+ try {
59
+ entries = await readdir(absDir, { withFileTypes: true })
60
+ } catch {
61
+ return
62
+ }
63
+ for (const entry of entries) {
64
+ if (entry.name.startsWith(".")) continue
65
+ const relPath = `${relBase}/${entry.name}`.replace(/^\/+/, "")
66
+ const absPath = join(absDir, entry.name)
67
+ if (entry.isDirectory()) {
68
+ await walkDir(files, absPath, relPath)
69
+ } else if (entry.isFile()) {
70
+ await addFile(files, absPath, relPath)
71
+ }
72
+ }
73
+ }
74
+
75
+ export async function collectBackendSourceFiles(rootDir) {
76
+ const files = []
77
+
78
+ for (const relPath of BACKEND_SOURCE_EXACT) {
79
+ const absPath = join(rootDir, relPath)
80
+ try {
81
+ const s = await stat(absPath)
82
+ if (s.isFile()) await addFile(files, absPath, relPath)
83
+ } catch {
84
+ // optional
85
+ }
86
+ }
87
+
88
+ for (const dir of BACKEND_SOURCE_DIRS) {
89
+ await walkDir(files, join(rootDir, dir), dir)
90
+ }
91
+
92
+ files.sort((a, b) => a.path.localeCompare(b.path))
93
+ return files
94
+ }
95
+
96
+ function normalizeBaseline(raw) {
97
+ const parsed = JSON.parse(raw)
98
+ const files = parsed?.files
99
+ const result = new Map()
100
+ if (Array.isArray(files)) {
101
+ for (const file of files) {
102
+ if (typeof file?.path === "string" && typeof file?.sha256 === "string" && isTrackedBackendSourcePath(file.path)) {
103
+ result.set(file.path, file.sha256)
104
+ }
105
+ }
106
+ } else if (files && typeof files === "object") {
107
+ for (const [path, value] of Object.entries(files)) {
108
+ if (!isTrackedBackendSourcePath(path)) continue
109
+ if (typeof value === "string") result.set(path, value)
110
+ else if (value && typeof value === "object" && typeof value.sha256 === "string") result.set(path, value.sha256)
111
+ }
112
+ }
113
+ return result
114
+ }
115
+
116
+ export async function readBackendSourceBaseline(rootDir) {
117
+ try {
118
+ const raw = await readFile(join(rootDir, ".dypai", "backend-baseline.json"), "utf8")
119
+ return {
120
+ ok: true,
121
+ baseline: normalizeBaseline(raw),
122
+ }
123
+ } catch (error) {
124
+ if (error?.code === "ENOENT") {
125
+ return {
126
+ ok: false,
127
+ missing: true,
128
+ baseline: new Map(),
129
+ }
130
+ }
131
+ return {
132
+ ok: false,
133
+ error: error?.message || String(error),
134
+ baseline: new Map(),
135
+ }
136
+ }
137
+ }
138
+
139
+ export async function diffBackendSource(rootDir) {
140
+ const currentFiles = await collectBackendSourceFiles(rootDir)
141
+ const current = new Map(currentFiles.map((file) => [file.path, file.sha256]))
142
+ const baselineResult = await readBackendSourceBaseline(rootDir)
143
+
144
+ if (!baselineResult.ok && !baselineResult.missing) {
145
+ return {
146
+ ok: false,
147
+ reason: "baseline_parse_error",
148
+ error: baselineResult.error,
149
+ }
150
+ }
151
+
152
+ if (baselineResult.missing) {
153
+ return {
154
+ ok: true,
155
+ baseline: false,
156
+ files: {
157
+ created: currentFiles.map((file) => file.path),
158
+ updated: [],
159
+ deleted: [],
160
+ },
161
+ summary: {
162
+ create: currentFiles.length,
163
+ update: 0,
164
+ delete: 0,
165
+ unchanged: 0,
166
+ },
167
+ }
168
+ }
169
+
170
+ const baseline = baselineResult.baseline
171
+ const created = []
172
+ const updated = []
173
+ const deleted = []
174
+ const unchanged = []
175
+
176
+ for (const [path, hash] of current) {
177
+ if (!baseline.has(path)) created.push(path)
178
+ else if (baseline.get(path) !== hash) updated.push(path)
179
+ else unchanged.push(path)
180
+ }
181
+ for (const path of baseline.keys()) {
182
+ if (!current.has(path)) deleted.push(path)
183
+ }
184
+
185
+ return {
186
+ ok: true,
187
+ baseline: true,
188
+ files: {
189
+ created,
190
+ updated,
191
+ deleted,
192
+ },
193
+ summary: {
194
+ create: created.length,
195
+ update: updated.length,
196
+ delete: deleted.length,
197
+ unchanged: unchanged.length,
198
+ },
199
+ }
200
+ }
201
+
202
+ export async function writeBackendSourceBaseline(rootDir, projectId = null) {
203
+ const files = await collectBackendSourceFiles(rootDir)
204
+ const payload = {
205
+ synced_at: new Date().toISOString(),
206
+ ...(projectId ? { project_id: projectId } : {}),
207
+ files: files.map((file) => ({
208
+ path: file.path,
209
+ sha256: file.sha256,
210
+ })),
211
+ }
212
+ const target = join(rootDir, ".dypai", "backend-baseline.json")
213
+ await mkdir(dirname(target), { recursive: true })
214
+ await writeFile(target, JSON.stringify(payload, null, 2) + "\n", "utf8")
215
+ return payload
216
+ }
217
+