@dypai-ai/mcp 1.5.18 → 1.5.22
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 +2 -2
- package/src/index.js +42 -20
- package/src/lib/workflow-placeholder-contract.d.ts +50 -0
- package/src/lib/workflow-placeholder-contract.js +364 -0
- package/src/tools/project-artifacts.js +627 -0
- package/src/tools/sync/validate.js +151 -8
- package/src/tools/capability-kits.js +0 -830
|
@@ -1,830 +0,0 @@
|
|
|
1
|
-
import crypto from "crypto"
|
|
2
|
-
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync, copyFileSync } from "fs"
|
|
3
|
-
import { tmpdir } from "os"
|
|
4
|
-
import { basename, delimiter, dirname, isAbsolute, join, relative, resolve, sep } from "path"
|
|
5
|
-
import { fileURLToPath } from "url"
|
|
6
|
-
import { proxyToolCall } from "./proxy.js"
|
|
7
|
-
|
|
8
|
-
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
9
|
-
const DEFAULT_LIMIT = 5
|
|
10
|
-
const DEFAULT_R2_BUCKET = "organizations-storage"
|
|
11
|
-
const DEFAULT_R2_PREFIX = "capability-kits"
|
|
12
|
-
|
|
13
|
-
function readJson(path) {
|
|
14
|
-
return JSON.parse(readFileSync(path, "utf8"))
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function isObject(value) {
|
|
18
|
-
return Boolean(value) && typeof value === "object" && !Array.isArray(value)
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function normalizeList(value) {
|
|
22
|
-
return Array.isArray(value) ? value.filter((item) => typeof item === "string" && item.trim()) : []
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function walkUp(start, predicate) {
|
|
26
|
-
let cursor = resolve(start)
|
|
27
|
-
for (let i = 0; i < 10; i++) {
|
|
28
|
-
if (predicate(cursor)) return cursor
|
|
29
|
-
const parent = dirname(cursor)
|
|
30
|
-
if (parent === cursor) break
|
|
31
|
-
cursor = parent
|
|
32
|
-
}
|
|
33
|
-
return null
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function resolveKitsRoot(inputRoot) {
|
|
37
|
-
const candidates = [
|
|
38
|
-
inputRoot,
|
|
39
|
-
process.env.DYPAI_CAPABILITY_KITS_ROOT,
|
|
40
|
-
].filter(Boolean)
|
|
41
|
-
|
|
42
|
-
for (const candidate of candidates) {
|
|
43
|
-
const root = resolve(candidate)
|
|
44
|
-
if (existsSync(join(root, "kits"))) return root
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const fromCwd = walkUp(process.cwd(), (dir) => existsSync(join(dir, "dypai-capability-kits", "kits")))
|
|
48
|
-
if (fromCwd) return join(fromCwd, "dypai-capability-kits")
|
|
49
|
-
|
|
50
|
-
const fromThisFile = walkUp(__dirname, (dir) => existsSync(join(dir, "dypai-capability-kits", "kits")))
|
|
51
|
-
if (fromThisFile) return join(fromThisFile, "dypai-capability-kits")
|
|
52
|
-
|
|
53
|
-
throw new Error(
|
|
54
|
-
"Capability kit repo not found locally. Set DYPAI_CAPABILITY_KITS_ROOT for local authoring or configure R2 credentials for remote kit install.",
|
|
55
|
-
)
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function tryResolveKitsRoot(inputRoot) {
|
|
59
|
-
try {
|
|
60
|
-
return resolveKitsRoot(inputRoot)
|
|
61
|
-
} catch {
|
|
62
|
-
return null
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function hasLocalKit(root, slug, version) {
|
|
67
|
-
return Boolean(root) && existsSync(join(root, "kits", slug, version, "kit.json"))
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function sha256Buffer(buffer) {
|
|
71
|
-
return crypto.createHash("sha256").update(buffer).digest("hex")
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function safeLogicalPath(path) {
|
|
75
|
-
if (typeof path !== "string" || !path.trim()) throw new Error("Remote kit file path is invalid")
|
|
76
|
-
const normalized = path.replace(/\\/g, "/").replace(/^\/+/, "")
|
|
77
|
-
if (!normalized || normalized.includes("..") || isAbsolute(normalized)) {
|
|
78
|
-
throw new Error(`Unsafe remote kit file path: ${path}`)
|
|
79
|
-
}
|
|
80
|
-
return normalized
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function parseR2SourceUri(sourceUri, slug) {
|
|
84
|
-
if (typeof sourceUri !== "string" || !sourceUri.startsWith("r2://")) return null
|
|
85
|
-
const withoutScheme = sourceUri.slice("r2://".length)
|
|
86
|
-
const slash = withoutScheme.indexOf("/")
|
|
87
|
-
if (slash <= 0) throw new Error(`Invalid capability kit source_uri: ${sourceUri}`)
|
|
88
|
-
const bucket = withoutScheme.slice(0, slash)
|
|
89
|
-
const rootKey = withoutScheme.slice(slash + 1).replace(/^\/+|\/+$/g, "")
|
|
90
|
-
if (!rootKey || !rootKey.split("/").includes(slug)) {
|
|
91
|
-
throw new Error(`Capability kit source_uri does not look like a kit root for ${slug}: ${sourceUri}`)
|
|
92
|
-
}
|
|
93
|
-
return { bucket, rootKey }
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function resolveRemoteKitLocation(slug, sourceUri) {
|
|
97
|
-
const parsed = parseR2SourceUri(sourceUri, slug)
|
|
98
|
-
if (parsed) return parsed
|
|
99
|
-
const bucket = process.env.CAPABILITY_KITS_R2_BUCKET
|
|
100
|
-
|| process.env.DYPAI_CAPABILITY_KITS_R2_BUCKET
|
|
101
|
-
|| process.env.CLOUDFLARE_R2_CAPABILITY_KITS_BUCKET
|
|
102
|
-
|| DEFAULT_R2_BUCKET
|
|
103
|
-
const prefix = (process.env.CAPABILITY_KITS_R2_PREFIX || DEFAULT_R2_PREFIX).replace(/^\/+|\/+$/g, "")
|
|
104
|
-
return {
|
|
105
|
-
bucket,
|
|
106
|
-
rootKey: `${prefix}/${slug}`.replace(/^\/+|\/+$/g, "").replace(/\/+/g, "/"),
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function resolveR2Config(slug, sourceUri) {
|
|
111
|
-
const endpoint = (process.env.CLOUDFLARE_R2_ENDPOINT_URL || "").replace(/\/$/, "")
|
|
112
|
-
const accessKeyId = process.env.CLOUDFLARE_R2_ACCESS_KEY_ID || ""
|
|
113
|
-
const secretAccessKey = process.env.CLOUDFLARE_R2_SECRET_ACCESS_KEY || ""
|
|
114
|
-
if (!endpoint || !accessKeyId || !secretAccessKey) {
|
|
115
|
-
throw new Error("Capability kit R2 storage is not configured. Set CLOUDFLARE_R2_ENDPOINT_URL, CLOUDFLARE_R2_ACCESS_KEY_ID, and CLOUDFLARE_R2_SECRET_ACCESS_KEY.")
|
|
116
|
-
}
|
|
117
|
-
return { endpoint, accessKeyId, secretAccessKey, ...resolveRemoteKitLocation(slug, sourceUri) }
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
function hmac(key, value, encoding) {
|
|
121
|
-
return crypto.createHmac("sha256", key).update(value).digest(encoding)
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
function sha256Hex(value) {
|
|
125
|
-
return crypto.createHash("sha256").update(value).digest("hex")
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
function encodeRfc3986(value) {
|
|
129
|
-
return encodeURIComponent(value).replace(/[!'()*]/g, (char) => `%${char.charCodeAt(0).toString(16).toUpperCase()}`)
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
function r2Url(config, key) {
|
|
133
|
-
const encodedKey = key.split("/").map(encodeRfc3986).join("/")
|
|
134
|
-
return new URL(`${config.endpoint}/${encodeRfc3986(config.bucket)}/${encodedKey}`)
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
function signingKey(secretAccessKey, dateStamp) {
|
|
138
|
-
const dateKey = hmac(`AWS4${secretAccessKey}`, dateStamp)
|
|
139
|
-
const dateRegionKey = hmac(dateKey, "auto")
|
|
140
|
-
const dateRegionServiceKey = hmac(dateRegionKey, "s3")
|
|
141
|
-
return hmac(dateRegionServiceKey, "aws4_request")
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
function signedR2Headers(config, method, url, payloadHash) {
|
|
145
|
-
const now = new Date()
|
|
146
|
-
const amzDate = now.toISOString().replace(/[:-]|\.\d{3}/g, "")
|
|
147
|
-
const dateStamp = amzDate.slice(0, 8)
|
|
148
|
-
const canonicalHeaders = [
|
|
149
|
-
`host:${url.host}`,
|
|
150
|
-
`x-amz-content-sha256:${payloadHash}`,
|
|
151
|
-
`x-amz-date:${amzDate}`,
|
|
152
|
-
"",
|
|
153
|
-
].join("\n")
|
|
154
|
-
const signedHeaders = "host;x-amz-content-sha256;x-amz-date"
|
|
155
|
-
const canonicalRequest = [
|
|
156
|
-
method,
|
|
157
|
-
url.pathname,
|
|
158
|
-
url.searchParams.toString(),
|
|
159
|
-
canonicalHeaders,
|
|
160
|
-
signedHeaders,
|
|
161
|
-
payloadHash,
|
|
162
|
-
].join("\n")
|
|
163
|
-
const credentialScope = `${dateStamp}/auto/s3/aws4_request`
|
|
164
|
-
const stringToSign = [
|
|
165
|
-
"AWS4-HMAC-SHA256",
|
|
166
|
-
amzDate,
|
|
167
|
-
credentialScope,
|
|
168
|
-
sha256Hex(canonicalRequest),
|
|
169
|
-
].join("\n")
|
|
170
|
-
const signature = hmac(signingKey(config.secretAccessKey, dateStamp), stringToSign, "hex")
|
|
171
|
-
|
|
172
|
-
return {
|
|
173
|
-
Authorization: `AWS4-HMAC-SHA256 Credential=${config.accessKeyId}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}`,
|
|
174
|
-
"x-amz-content-sha256": payloadHash,
|
|
175
|
-
"x-amz-date": amzDate,
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
async function r2Get(config, key) {
|
|
180
|
-
const url = r2Url(config, key)
|
|
181
|
-
const response = await fetch(url, {
|
|
182
|
-
method: "GET",
|
|
183
|
-
headers: signedR2Headers(config, "GET", url, sha256Hex(Buffer.alloc(0))),
|
|
184
|
-
})
|
|
185
|
-
if (!response.ok) {
|
|
186
|
-
throw new Error(`R2 GET failed for ${key}: ${response.status} ${await response.text()}`)
|
|
187
|
-
}
|
|
188
|
-
return Buffer.from(await response.arrayBuffer())
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
function parseRemoteFilesManifest(value, slug, requestedVersion) {
|
|
192
|
-
const parsed = JSON.parse(value)
|
|
193
|
-
if (!isObject(parsed)) throw new Error("Remote kit manifest must be a JSON object")
|
|
194
|
-
if (parsed.slug && parsed.slug !== slug) throw new Error(`Remote kit manifest slug mismatch: expected ${slug}, got ${parsed.slug}`)
|
|
195
|
-
if (parsed.version && parsed.version !== requestedVersion) {
|
|
196
|
-
throw new Error(`Remote kit manifest version mismatch: expected ${requestedVersion}, got ${parsed.version}`)
|
|
197
|
-
}
|
|
198
|
-
if (!Array.isArray(parsed.files) || !parsed.files.length) {
|
|
199
|
-
throw new Error(`Remote kit manifest for ${slug} has no files`)
|
|
200
|
-
}
|
|
201
|
-
return parsed
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
function cachedRemoteKitComplete(kitDirPath, manifest) {
|
|
205
|
-
return (manifest.files || []).every((file) => {
|
|
206
|
-
try {
|
|
207
|
-
const rel = safeLogicalPath(file.path)
|
|
208
|
-
const path = join(kitDirPath, rel)
|
|
209
|
-
if (!existsSync(path)) return false
|
|
210
|
-
return file.sha256 ? sha256Buffer(readFileSync(path)) === file.sha256 : true
|
|
211
|
-
} catch {
|
|
212
|
-
return false
|
|
213
|
-
}
|
|
214
|
-
})
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
function remoteKitKey(config, logicalPath) {
|
|
218
|
-
return `${config.rootKey}/${logicalPath}`.replace(/\/+/g, "/").replace(/^\/+/, "")
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
async function downloadRemoteCapabilityKit(slug, version, sourceUri) {
|
|
222
|
-
const config = resolveR2Config(slug, sourceUri)
|
|
223
|
-
const manifestKey = remoteKitKey(config, "manifest.files.json")
|
|
224
|
-
const manifestText = (await r2Get(config, manifestKey)).toString("utf8")
|
|
225
|
-
const manifest = parseRemoteFilesManifest(manifestText, slug, version)
|
|
226
|
-
const contentHash = typeof manifest.contentHash === "string" && manifest.contentHash.trim()
|
|
227
|
-
? manifest.contentHash.trim()
|
|
228
|
-
: sha256Buffer(Buffer.from(manifestText))
|
|
229
|
-
const cacheRoot = join(tmpdir(), "dypai-capability-kits", slug, contentHash)
|
|
230
|
-
const kitDirPath = join(cacheRoot, "kits", slug, version)
|
|
231
|
-
const localManifestPath = join(kitDirPath, "manifest.files.json")
|
|
232
|
-
|
|
233
|
-
if (existsSync(localManifestPath) && cachedRemoteKitComplete(kitDirPath, manifest)) {
|
|
234
|
-
return cacheRoot
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
mkdirSync(kitDirPath, { recursive: true })
|
|
238
|
-
for (const file of manifest.files || []) {
|
|
239
|
-
const rel = safeLogicalPath(file.path)
|
|
240
|
-
const buffer = await r2Get(config, remoteKitKey(config, rel))
|
|
241
|
-
if (file.sha256 && sha256Buffer(buffer) !== file.sha256) {
|
|
242
|
-
throw new Error(`Capability kit file hash mismatch for ${slug}/${rel}`)
|
|
243
|
-
}
|
|
244
|
-
const target = join(kitDirPath, rel)
|
|
245
|
-
mkdirSync(dirname(target), { recursive: true })
|
|
246
|
-
writeFileSync(target, buffer)
|
|
247
|
-
}
|
|
248
|
-
writeFileSync(localManifestPath, manifestText)
|
|
249
|
-
return cacheRoot
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
async function resolveKitsRootForKit({ kits_root, slug, version, source_uri }) {
|
|
253
|
-
const sourceMode = (process.env.DYPAI_CAPABILITY_KITS_SOURCE || "").toLowerCase()
|
|
254
|
-
const localRoot = sourceMode === "remote" ? null : tryResolveKitsRoot(kits_root)
|
|
255
|
-
if (hasLocalKit(localRoot, slug, version)) return { root: localRoot, source: "local_repo" }
|
|
256
|
-
const remoteRoot = await downloadRemoteCapabilityKit(slug, version, source_uri)
|
|
257
|
-
return { root: remoteRoot, source: "r2" }
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
function shouldSearchRemote(kitsRoot) {
|
|
261
|
-
const source = (process.env.DYPAI_CAPABILITY_KITS_SOURCE || "").toLowerCase()
|
|
262
|
-
if (source === "local") return false
|
|
263
|
-
if (source === "remote") return true
|
|
264
|
-
if (kitsRoot || process.env.DYPAI_CAPABILITY_KITS_ROOT) return false
|
|
265
|
-
return Boolean(process.env.DYPAI_TOKEN)
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
function resolveWorkspaceRoot(inputRoot) {
|
|
269
|
-
if (inputRoot) {
|
|
270
|
-
const root = resolve(inputRoot)
|
|
271
|
-
mkdirSync(root, { recursive: true })
|
|
272
|
-
return root
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
const envCandidates = [
|
|
276
|
-
process.env.CLAUDE_PROJECT_DIR,
|
|
277
|
-
process.env.DYPAI_WORKSPACE_ROOT,
|
|
278
|
-
process.env.PROJECT_ROOT,
|
|
279
|
-
process.env.WORKSPACE_FOLDER_PATHS?.split(delimiter)[0],
|
|
280
|
-
].filter(Boolean)
|
|
281
|
-
|
|
282
|
-
for (const candidate of envCandidates) {
|
|
283
|
-
const root = resolve(candidate)
|
|
284
|
-
if (existsSync(root)) return root
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
const fromCwd = walkUp(process.cwd(), (dir) =>
|
|
288
|
-
existsSync(join(dir, ".git")) ||
|
|
289
|
-
existsSync(join(dir, "package.json")) ||
|
|
290
|
-
existsSync(join(dir, "dypai")) ||
|
|
291
|
-
existsSync(join(dir, "src")),
|
|
292
|
-
)
|
|
293
|
-
if (fromCwd) return fromCwd
|
|
294
|
-
|
|
295
|
-
throw new Error("Could not determine workspace root. Pass workspace_root as an absolute project path.")
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
function safeTarget(workspaceRoot, targetPath) {
|
|
299
|
-
if (!targetPath || typeof targetPath !== "string") throw new Error("Invalid target path")
|
|
300
|
-
if (isAbsolute(targetPath)) throw new Error(`Target must be workspace-relative, got absolute path: ${targetPath}`)
|
|
301
|
-
const normalized = targetPath.replace(/\\/g, "/").replace(/^\/+/, "")
|
|
302
|
-
if (normalized.includes("..")) throw new Error(`Target path cannot contain '..': ${targetPath}`)
|
|
303
|
-
const full = resolve(workspaceRoot, normalized)
|
|
304
|
-
const rel = relative(workspaceRoot, full)
|
|
305
|
-
if (rel.startsWith("..") || rel === "" || isAbsolute(rel)) {
|
|
306
|
-
throw new Error(`Target escapes workspace root: ${targetPath}`)
|
|
307
|
-
}
|
|
308
|
-
return full
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
function kitDir(kitsRoot, slug, version) {
|
|
312
|
-
if (!slug || !version) throw new Error("slug and version are required")
|
|
313
|
-
return join(kitsRoot, "kits", slug, version)
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
function loadKit(kitsRoot, slug, version) {
|
|
317
|
-
const dir = kitDir(kitsRoot, slug, version)
|
|
318
|
-
const manifestPath = join(dir, "kit.json")
|
|
319
|
-
if (!existsSync(manifestPath)) throw new Error(`Kit not found: ${slug}@${version}`)
|
|
320
|
-
const manifest = readJson(manifestPath)
|
|
321
|
-
return { dir, manifest }
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
function listKits(kitsRoot) {
|
|
325
|
-
const kitsDir = join(kitsRoot, "kits")
|
|
326
|
-
const kits = []
|
|
327
|
-
for (const slug of readdirSync(kitsDir)) {
|
|
328
|
-
const slugDir = join(kitsDir, slug)
|
|
329
|
-
if (!existsSync(slugDir) || slug === "index.json") continue
|
|
330
|
-
for (const version of readdirSync(slugDir)) {
|
|
331
|
-
const manifestPath = join(slugDir, version, "kit.json")
|
|
332
|
-
if (!existsSync(manifestPath)) continue
|
|
333
|
-
kits.push({ dir: join(slugDir, version), manifest: readJson(manifestPath) })
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
return kits
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
function searchText(kit) {
|
|
340
|
-
const parts = [
|
|
341
|
-
kit.slug,
|
|
342
|
-
kit.name,
|
|
343
|
-
kit.kitType,
|
|
344
|
-
kit.category,
|
|
345
|
-
kit.description,
|
|
346
|
-
kit.useWhen,
|
|
347
|
-
kit.avoidWhen,
|
|
348
|
-
kit.userFacingSummary,
|
|
349
|
-
kit.agentInstructions,
|
|
350
|
-
...normalizeList(kit.appTypes),
|
|
351
|
-
...normalizeList(kit.screenTypes),
|
|
352
|
-
...normalizeList(kit.featureTags),
|
|
353
|
-
...normalizeList(kit.visualStyles),
|
|
354
|
-
...normalizeList(kit.provides),
|
|
355
|
-
...normalizeList(kit.requires),
|
|
356
|
-
...normalizeList(kit.dependencies),
|
|
357
|
-
]
|
|
358
|
-
return parts.filter(Boolean).join(" ").toLowerCase()
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
function matchesFilter(kit, filters = {}) {
|
|
362
|
-
if (!isObject(filters)) return true
|
|
363
|
-
if (filters.category && kit.category !== filters.category) return false
|
|
364
|
-
if (filters.maturity) {
|
|
365
|
-
const allowed = Array.isArray(filters.maturity) ? filters.maturity : [filters.maturity]
|
|
366
|
-
if (!allowed.includes(kit.maturity)) return false
|
|
367
|
-
}
|
|
368
|
-
const listChecks = [
|
|
369
|
-
["app_type", "appTypes"],
|
|
370
|
-
["screen_type", "screenTypes"],
|
|
371
|
-
["feature_tag", "featureTags"],
|
|
372
|
-
]
|
|
373
|
-
for (const [filterKey, kitKey] of listChecks) {
|
|
374
|
-
if (filters[filterKey] && !normalizeList(kit[kitKey]).includes(filters[filterKey])) return false
|
|
375
|
-
}
|
|
376
|
-
if (Array.isArray(filters.requires)) {
|
|
377
|
-
const requires = normalizeList(kit.requires)
|
|
378
|
-
for (const required of filters.requires) {
|
|
379
|
-
if (!requires.includes(required)) return false
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
return true
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
function scoreKit(query, kit) {
|
|
386
|
-
const text = searchText(kit)
|
|
387
|
-
const terms = String(query || "")
|
|
388
|
-
.toLowerCase()
|
|
389
|
-
.split(/[^a-z0-9áéíóúñ]+/i)
|
|
390
|
-
.map((term) => term.trim())
|
|
391
|
-
.filter((term) => term.length >= 3)
|
|
392
|
-
if (!terms.length) return 0
|
|
393
|
-
let score = 0
|
|
394
|
-
for (const term of terms) {
|
|
395
|
-
if (kit.slug?.includes(term)) score += 5
|
|
396
|
-
if (kit.category?.toLowerCase() === term) score += 4
|
|
397
|
-
if (text.includes(term)) score += 1
|
|
398
|
-
}
|
|
399
|
-
return score
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
function assetIndex(dir) {
|
|
403
|
-
const out = []
|
|
404
|
-
function visit(abs, rel = "") {
|
|
405
|
-
for (const name of readdirSync(abs, { withFileTypes: true })) {
|
|
406
|
-
const childAbs = join(abs, name.name)
|
|
407
|
-
const childRel = rel ? `${rel}/${name.name}` : name.name
|
|
408
|
-
if (name.isDirectory()) visit(childAbs, childRel)
|
|
409
|
-
else out.push({ logicalPath: childRel, sizeBytes: readFileSync(childAbs).length })
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
visit(dir)
|
|
413
|
-
return out.sort((a, b) => a.logicalPath.localeCompare(b.logicalPath))
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
function stripFrontendPrefix(source) {
|
|
417
|
-
return source.replace(/^frontend\/src\//, "")
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
function defaultTargetForSource(manifest, source, kind) {
|
|
421
|
-
if (kind === "frontend") return `src/dypai-kits/${manifest.slug}/${stripFrontendPrefix(source)}`
|
|
422
|
-
if (kind === "backend") return `dypai/endpoints/${manifest.slug}/${source.split("/").pop()}`
|
|
423
|
-
if (kind === "database") return `dypai/kit-installations/${manifest.slug}/${manifest.version}/${source.split("/").pop()}`
|
|
424
|
-
return `.dypai/kits/${manifest.slug}/${source.split("/").pop()}`
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
function pluralize(word) {
|
|
428
|
-
if (!word) return word
|
|
429
|
-
if (word.endsWith("s")) return word
|
|
430
|
-
if (word.endsWith("y")) return `${word.slice(0, -1)}ies`
|
|
431
|
-
return `${word}s`
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
function namingContext(manifest, naming = {}) {
|
|
435
|
-
const adaptation = isObject(manifest.adaptation) ? manifest.adaptation : {}
|
|
436
|
-
const sourceEntity = adaptation.defaultEntity || normalizeList(adaptation.entityAliases)[0] || ""
|
|
437
|
-
const sourcePlural = adaptation.defaultEndpointPrefix || pluralize(sourceEntity)
|
|
438
|
-
const sourceTable = adaptation.defaultTableName || sourcePlural
|
|
439
|
-
const targetEntity = naming.entity || sourceEntity
|
|
440
|
-
const targetPlural = naming.endpointPrefix || pluralize(targetEntity)
|
|
441
|
-
const targetTable = naming.tableName || targetPlural
|
|
442
|
-
return { sourceEntity, sourcePlural, sourceTable, targetEntity, targetPlural, targetTable }
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
function replaceWholeWords(text, replacements) {
|
|
446
|
-
let out = text
|
|
447
|
-
for (const [from, to] of replacements) {
|
|
448
|
-
if (!from || !to || from === to) continue
|
|
449
|
-
out = out.replace(new RegExp(`\\b${from.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`, "g"), to)
|
|
450
|
-
}
|
|
451
|
-
return out
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
function adaptText(content, manifest, naming, kind) {
|
|
455
|
-
const n = namingContext(manifest, naming)
|
|
456
|
-
if (!n.sourceEntity && !n.sourcePlural && !n.sourceTable) return content
|
|
457
|
-
if (kind === "frontend") return content
|
|
458
|
-
|
|
459
|
-
if (kind === "backend") {
|
|
460
|
-
const withEndpointName = content.replace(/^name:\s*([a-z0-9-]+)\s*$/m, (_line, endpointName) => {
|
|
461
|
-
return `name: ${adaptTarget(endpointName, manifest, naming)}`
|
|
462
|
-
})
|
|
463
|
-
return replaceWholeWords(withEndpointName, [
|
|
464
|
-
[n.sourceTable, n.targetTable],
|
|
465
|
-
[n.sourceEntity, n.targetEntity],
|
|
466
|
-
])
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
return replaceWholeWords(content, [
|
|
470
|
-
[n.sourceTable, n.targetTable],
|
|
471
|
-
[n.sourcePlural, n.targetTable],
|
|
472
|
-
[n.sourceEntity, n.targetEntity],
|
|
473
|
-
])
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
function adaptTarget(target, manifest, naming) {
|
|
477
|
-
const n = namingContext(manifest, naming)
|
|
478
|
-
return replaceWholeWords(target, [
|
|
479
|
-
[n.sourcePlural, n.targetPlural],
|
|
480
|
-
[n.sourceEntity, n.targetEntity],
|
|
481
|
-
[n.sourceTable, n.targetTable],
|
|
482
|
-
])
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
function installRecordPath(workspaceRoot) {
|
|
486
|
-
return join(workspaceRoot, ".dypai", "kits", "installed.json")
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
function readInstallRecord(workspaceRoot) {
|
|
490
|
-
const path = installRecordPath(workspaceRoot)
|
|
491
|
-
if (!existsSync(path)) return { schemaVersion: 1, kits: [] }
|
|
492
|
-
try {
|
|
493
|
-
const parsed = readJson(path)
|
|
494
|
-
if (isObject(parsed) && Array.isArray(parsed.kits)) return parsed
|
|
495
|
-
} catch {}
|
|
496
|
-
return { schemaVersion: 1, kits: [] }
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
function writeInstallRecord(workspaceRoot, record) {
|
|
500
|
-
const path = installRecordPath(workspaceRoot)
|
|
501
|
-
mkdirSync(dirname(path), { recursive: true })
|
|
502
|
-
writeFileSync(path, `${JSON.stringify(record, null, 2)}\n`)
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
function ownedTargets(record, slug, version) {
|
|
506
|
-
const targets = new Set()
|
|
507
|
-
for (const item of record.kits || []) {
|
|
508
|
-
if (item.slug === slug && item.version === version) {
|
|
509
|
-
for (const file of item.files || []) {
|
|
510
|
-
if (file.target) targets.add(file.target)
|
|
511
|
-
}
|
|
512
|
-
}
|
|
513
|
-
}
|
|
514
|
-
return targets
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
function writeTemplateFile({ sourceAbs, targetRel, workspaceRoot, manifest, naming, kind, overwrite, record, copied, skipped }) {
|
|
518
|
-
const targetAbs = safeTarget(workspaceRoot, targetRel)
|
|
519
|
-
const targetExists = existsSync(targetAbs)
|
|
520
|
-
if (targetExists) {
|
|
521
|
-
if (overwrite === "fail") throw new Error(`Target already exists: ${targetRel}`)
|
|
522
|
-
if (overwrite !== "replace" || !ownedTargets(record, manifest.slug, manifest.version).has(targetRel)) {
|
|
523
|
-
skipped.push(targetRel)
|
|
524
|
-
return false
|
|
525
|
-
}
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
mkdirSync(dirname(targetAbs), { recursive: true })
|
|
529
|
-
const isText = /\.(tsx?|jsx?|ya?ml|json|md|sql|css|scss|html)$/i.test(sourceAbs)
|
|
530
|
-
if (isText) {
|
|
531
|
-
const content = adaptText(readFileSync(sourceAbs, "utf8"), manifest, naming, kind)
|
|
532
|
-
writeFileSync(targetAbs, content)
|
|
533
|
-
} else {
|
|
534
|
-
copyFileSync(sourceAbs, targetAbs)
|
|
535
|
-
}
|
|
536
|
-
copied.push(targetRel)
|
|
537
|
-
return true
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
export const searchCapabilityKitsTool = {
|
|
541
|
-
name: "search_capability_kits",
|
|
542
|
-
description: "Search local DYPAI capability kits before building complex UI like calendars, maps, Kanban, upload flows, CRUD tables, dashboards, or editors.",
|
|
543
|
-
inputSchema: {
|
|
544
|
-
type: "object",
|
|
545
|
-
properties: {
|
|
546
|
-
query: { type: "string", description: "Feature need and app domain, e.g. booking calendar for hotel reservations." },
|
|
547
|
-
limit: { type: "integer", default: DEFAULT_LIMIT, minimum: 1, maximum: 10 },
|
|
548
|
-
filters: { type: "object", description: "Optional filters: category, maturity, app_type, screen_type, feature_tag, requires." },
|
|
549
|
-
kits_root: { type: "string", description: "Optional local path to dypai-capability-kits. When omitted, the tool may use the remote MCP registry so local behavior matches production." },
|
|
550
|
-
},
|
|
551
|
-
required: ["query"],
|
|
552
|
-
},
|
|
553
|
-
async execute({ query, limit = DEFAULT_LIMIT, filters = {}, kits_root }) {
|
|
554
|
-
if (shouldSearchRemote(kits_root)) {
|
|
555
|
-
try {
|
|
556
|
-
const remote = await proxyToolCall("search_capability_kits", { query, limit, filters })
|
|
557
|
-
if (isObject(remote)) {
|
|
558
|
-
return { ...remote, source: remote.source || "mcp_cloud" }
|
|
559
|
-
}
|
|
560
|
-
return remote
|
|
561
|
-
} catch (error) {
|
|
562
|
-
if (!tryResolveKitsRoot(kits_root)) throw error
|
|
563
|
-
process.stderr.write(`Capability kit remote search failed, falling back to local repo: ${error.message}\n`)
|
|
564
|
-
}
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
const root = resolveKitsRoot(kits_root)
|
|
568
|
-
const kits = listKits(root)
|
|
569
|
-
.filter(({ manifest }) => matchesFilter(manifest, filters))
|
|
570
|
-
.map(({ manifest }) => ({ manifest, score: scoreKit(query, manifest) }))
|
|
571
|
-
.filter((item) => item.score > 0 || !query)
|
|
572
|
-
.sort((a, b) => b.score - a.score)
|
|
573
|
-
.slice(0, Math.max(1, Math.min(Number(limit) || DEFAULT_LIMIT, 10)))
|
|
574
|
-
|
|
575
|
-
return {
|
|
576
|
-
ok: true,
|
|
577
|
-
source: "local_repo",
|
|
578
|
-
kitsRoot: root,
|
|
579
|
-
count: kits.length,
|
|
580
|
-
kits: kits.map(({ manifest, score }) => ({
|
|
581
|
-
slug: manifest.slug,
|
|
582
|
-
version: manifest.version,
|
|
583
|
-
name: manifest.name,
|
|
584
|
-
category: manifest.category,
|
|
585
|
-
maturity: manifest.maturity,
|
|
586
|
-
description: manifest.description,
|
|
587
|
-
useWhen: manifest.useWhen,
|
|
588
|
-
provides: manifest.provides,
|
|
589
|
-
requires: manifest.requires,
|
|
590
|
-
dependencies: manifest.dependencies,
|
|
591
|
-
frontendManifest: manifest.frontendManifest,
|
|
592
|
-
backendManifest: manifest.backendManifest,
|
|
593
|
-
databaseManifest: manifest.databaseManifest,
|
|
594
|
-
score,
|
|
595
|
-
})),
|
|
596
|
-
}
|
|
597
|
-
},
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
async function inspectCapabilityKit({ slug, version = "1.0.0", kits_root, source_uri }) {
|
|
601
|
-
const { root, source } = await resolveKitsRootForKit({ kits_root, slug, version, source_uri })
|
|
602
|
-
const { dir, manifest } = loadKit(root, slug, version)
|
|
603
|
-
return {
|
|
604
|
-
ok: true,
|
|
605
|
-
operation: "inspect",
|
|
606
|
-
source,
|
|
607
|
-
kitsRoot: root,
|
|
608
|
-
kit: manifest,
|
|
609
|
-
assets: assetIndex(dir),
|
|
610
|
-
agentNotes: existsSync(join(dir, "agent.md")) ? readFileSync(join(dir, "agent.md"), "utf8") : undefined,
|
|
611
|
-
checklist: existsSync(join(dir, "verification", "checklist.md"))
|
|
612
|
-
? readFileSync(join(dir, "verification", "checklist.md"), "utf8")
|
|
613
|
-
: undefined,
|
|
614
|
-
}
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
async function applyCapabilityKit({
|
|
618
|
-
slug,
|
|
619
|
-
version = "1.0.0",
|
|
620
|
-
targetFeature = "",
|
|
621
|
-
install = {},
|
|
622
|
-
naming = {},
|
|
623
|
-
target = {},
|
|
624
|
-
overwrite = "skip",
|
|
625
|
-
workspace_root,
|
|
626
|
-
kits_root,
|
|
627
|
-
source_uri,
|
|
628
|
-
}) {
|
|
629
|
-
const { root, source } = await resolveKitsRootForKit({ kits_root, slug, version, source_uri })
|
|
630
|
-
const workspaceRoot = resolveWorkspaceRoot(workspace_root)
|
|
631
|
-
const { dir, manifest } = loadKit(root, slug, version)
|
|
632
|
-
const record = readInstallRecord(workspaceRoot)
|
|
633
|
-
const previousEntries = (record.kits || []).filter((item) => item.slug === manifest.slug && item.version === manifest.version)
|
|
634
|
-
const copied = []
|
|
635
|
-
const copiedByKind = {
|
|
636
|
-
frontend: [],
|
|
637
|
-
backend: [],
|
|
638
|
-
database: [],
|
|
639
|
-
}
|
|
640
|
-
const databaseTargetsBySource = new Map()
|
|
641
|
-
const skipped = []
|
|
642
|
-
|
|
643
|
-
const installFrontend = install.frontend !== false
|
|
644
|
-
const installBackend = install.backend !== false
|
|
645
|
-
const installDatabase = install.database !== false
|
|
646
|
-
|
|
647
|
-
if (installFrontend) {
|
|
648
|
-
for (const asset of manifest.frontendManifest || []) {
|
|
649
|
-
const sourceAbs = join(dir, asset.source)
|
|
650
|
-
if (!existsSync(sourceAbs)) throw new Error(`Missing kit asset: ${asset.source}`)
|
|
651
|
-
let targetRel = asset.suggestedTarget || defaultTargetForSource(manifest, asset.source, "frontend")
|
|
652
|
-
if (target.frontendDir) {
|
|
653
|
-
targetRel = join(target.frontendDir, stripFrontendPrefix(asset.source)).split(sep).join("/")
|
|
654
|
-
}
|
|
655
|
-
if (writeTemplateFile({ sourceAbs, targetRel, workspaceRoot, manifest, naming, kind: "frontend", overwrite, record, copied, skipped })) {
|
|
656
|
-
copiedByKind.frontend.push(targetRel)
|
|
657
|
-
}
|
|
658
|
-
}
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
if (installBackend) {
|
|
662
|
-
for (const asset of manifest.backendManifest || []) {
|
|
663
|
-
const sourceAbs = join(dir, asset.source)
|
|
664
|
-
if (!existsSync(sourceAbs)) throw new Error(`Missing kit asset: ${asset.source}`)
|
|
665
|
-
let targetRel = asset.suggestedTarget || defaultTargetForSource(manifest, asset.source, "backend")
|
|
666
|
-
targetRel = join(dirname(targetRel), adaptTarget(basename(targetRel), manifest, naming)).split(sep).join("/")
|
|
667
|
-
if (target.endpointDir) {
|
|
668
|
-
targetRel = join(target.endpointDir, adaptTarget(asset.source.split("/").pop(), manifest, naming)).split(sep).join("/")
|
|
669
|
-
}
|
|
670
|
-
if (writeTemplateFile({ sourceAbs, targetRel, workspaceRoot, manifest, naming, kind: "backend", overwrite, record, copied, skipped })) {
|
|
671
|
-
copiedByKind.backend.push(targetRel)
|
|
672
|
-
}
|
|
673
|
-
}
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
if (installDatabase && isObject(manifest.databaseManifest)) {
|
|
677
|
-
for (const key of ["schema", "seed"]) {
|
|
678
|
-
const source = manifest.databaseManifest[key]
|
|
679
|
-
if (!source) continue
|
|
680
|
-
const sourceAbs = join(dir, source)
|
|
681
|
-
if (!existsSync(sourceAbs)) throw new Error(`Missing kit asset: ${source}`)
|
|
682
|
-
let targetRel = defaultTargetForSource(manifest, source, "database")
|
|
683
|
-
if (target.databaseDir) {
|
|
684
|
-
targetRel = join(target.databaseDir, source.split("/").pop()).split(sep).join("/")
|
|
685
|
-
}
|
|
686
|
-
if (writeTemplateFile({ sourceAbs, targetRel, workspaceRoot, manifest, naming, kind: "database", overwrite, record, copied, skipped })) {
|
|
687
|
-
copiedByKind.database.push(targetRel)
|
|
688
|
-
databaseTargetsBySource.set(source, targetRel)
|
|
689
|
-
}
|
|
690
|
-
}
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
const imported = (manifest.frontendManifest || []).map((asset) => ({
|
|
694
|
-
name: asset.exportName,
|
|
695
|
-
from: target.frontendDir
|
|
696
|
-
? `@/${join(target.frontendDir, stripFrontendPrefix(asset.source)).replace(/^src\//, "").replace(/\\/g, "/").replace(/\.(tsx?|jsx?)$/, "")}`
|
|
697
|
-
: asset.importPath,
|
|
698
|
-
})).filter((item) => item.name && item.from)
|
|
699
|
-
|
|
700
|
-
const backendEndpoints = (manifest.backendManifest || []).map((asset) =>
|
|
701
|
-
adaptTarget(asset.endpointName || asset.source.split("/").pop().replace(/\.ya?ml$/, ""), manifest, naming),
|
|
702
|
-
)
|
|
703
|
-
const databaseTables = normalizeList(manifest.databaseManifest?.tables).map((table) =>
|
|
704
|
-
adaptTarget(table, manifest, naming),
|
|
705
|
-
)
|
|
706
|
-
const schemaSource = installDatabase && isObject(manifest.databaseManifest) && typeof manifest.databaseManifest.schema === "string"
|
|
707
|
-
? manifest.databaseManifest.schema
|
|
708
|
-
: ""
|
|
709
|
-
const seedSource = installDatabase && isObject(manifest.databaseManifest) && typeof manifest.databaseManifest.seed === "string"
|
|
710
|
-
? manifest.databaseManifest.seed
|
|
711
|
-
: ""
|
|
712
|
-
const schemaFile = schemaSource ? databaseTargetsBySource.get(schemaSource) : undefined
|
|
713
|
-
const seedFile = seedSource ? databaseTargetsBySource.get(seedSource) : undefined
|
|
714
|
-
|
|
715
|
-
const previousFiles = previousEntries.flatMap((item) => Array.isArray(item.files) ? item.files : [])
|
|
716
|
-
const fileMap = new Map()
|
|
717
|
-
for (const file of previousFiles) {
|
|
718
|
-
if (file?.target) fileMap.set(file.target, file)
|
|
719
|
-
}
|
|
720
|
-
for (const targetPath of copied) {
|
|
721
|
-
fileMap.set(targetPath, { target: targetPath })
|
|
722
|
-
}
|
|
723
|
-
|
|
724
|
-
const entry = {
|
|
725
|
-
slug: manifest.slug,
|
|
726
|
-
version: manifest.version,
|
|
727
|
-
installedAt: new Date().toISOString(),
|
|
728
|
-
targetFeature,
|
|
729
|
-
files: [...fileMap.values()],
|
|
730
|
-
skippedFiles: skipped,
|
|
731
|
-
naming,
|
|
732
|
-
}
|
|
733
|
-
record.kits = (record.kits || []).filter((item) => !(item.slug === manifest.slug && item.version === manifest.version))
|
|
734
|
-
record.kits.push(entry)
|
|
735
|
-
writeInstallRecord(workspaceRoot, record)
|
|
736
|
-
|
|
737
|
-
const dependencies = manifest.dependencies || []
|
|
738
|
-
|
|
739
|
-
return {
|
|
740
|
-
ok: true,
|
|
741
|
-
operation: "apply",
|
|
742
|
-
source,
|
|
743
|
-
slug: manifest.slug,
|
|
744
|
-
version: manifest.version,
|
|
745
|
-
workspaceRoot,
|
|
746
|
-
installed: {
|
|
747
|
-
files: copied,
|
|
748
|
-
frontendFiles: copiedByKind.frontend,
|
|
749
|
-
backendFiles: copiedByKind.backend,
|
|
750
|
-
databaseFiles: copiedByKind.database,
|
|
751
|
-
skipped,
|
|
752
|
-
recordFile: ".dypai/kits/installed.json",
|
|
753
|
-
},
|
|
754
|
-
imports: imported,
|
|
755
|
-
backend: {
|
|
756
|
-
endpoints: backendEndpoints,
|
|
757
|
-
requiresPublish: Boolean(manifest.install?.requiresBackendPublish || backendEndpoints.length),
|
|
758
|
-
},
|
|
759
|
-
database: {
|
|
760
|
-
tables: databaseTables,
|
|
761
|
-
sqlFiles: copiedByKind.database,
|
|
762
|
-
schemaFile,
|
|
763
|
-
seedFile,
|
|
764
|
-
requiresExecuteSql: Boolean(manifest.install?.requiresExecuteSql || databaseTables.length),
|
|
765
|
-
applyWith: "execute_sql",
|
|
766
|
-
schemaRefresh: "automatic_after_successful_ddl",
|
|
767
|
-
},
|
|
768
|
-
dependencies,
|
|
769
|
-
nextSteps: [
|
|
770
|
-
...(dependencies.length
|
|
771
|
-
? [`Add any missing package dependencies to the target workspace package.json, then install them locally before building: ${dependencies.join(", ")}.`]
|
|
772
|
-
: []),
|
|
773
|
-
"Wire installed frontend components into the selected app page or route.",
|
|
774
|
-
...(databaseTables.length ? [
|
|
775
|
-
`Before backend tests, ensure tables exist (${databaseTables.join(", ")}): read ${schemaFile || "the installed schema SQL"} and execute its safe CREATE/ALTER statements with execute_sql. Do not edit dypai/schema.sql; it refreshes automatically.`,
|
|
776
|
-
] : []),
|
|
777
|
-
...(backendEndpoints.length ? ["Run backend validation and endpoint tests for installed/renamed endpoints."] : []),
|
|
778
|
-
"Run frontend verification after wiring imports and data callbacks.",
|
|
779
|
-
],
|
|
780
|
-
}
|
|
781
|
-
}
|
|
782
|
-
|
|
783
|
-
export const manageCapabilityKitTool = {
|
|
784
|
-
name: "manage_capability_kit",
|
|
785
|
-
description: "Inspect or install a local DYPAI capability kit. Use operation='inspect' after search when you need full manifest details, then operation='apply' to copy editable source into the workspace. Apply never executes SQL, publishes backend, deploys frontend, or installs npm packages.",
|
|
786
|
-
inputSchema: {
|
|
787
|
-
type: "object",
|
|
788
|
-
properties: {
|
|
789
|
-
operation: { type: "string", enum: ["inspect", "apply"], description: "inspect returns manifest/assets. apply copies kit files into the workspace." },
|
|
790
|
-
slug: { type: "string" },
|
|
791
|
-
version: { type: "string", default: "1.0.0" },
|
|
792
|
-
source_uri: { type: "string", description: "Optional r2://... kit root from search results. Used when the kit is not available locally." },
|
|
793
|
-
targetFeature: { type: "string", description: "Domain/feature being built, e.g. hotel reservations." },
|
|
794
|
-
install: {
|
|
795
|
-
type: "object",
|
|
796
|
-
properties: {
|
|
797
|
-
frontend: { type: "boolean", default: true },
|
|
798
|
-
backend: { type: "boolean", default: true },
|
|
799
|
-
database: { type: "boolean", default: true },
|
|
800
|
-
examples: { type: "boolean", default: false },
|
|
801
|
-
},
|
|
802
|
-
},
|
|
803
|
-
naming: {
|
|
804
|
-
type: "object",
|
|
805
|
-
properties: {
|
|
806
|
-
entity: { type: "string", description: "Singular domain entity, e.g. reservation." },
|
|
807
|
-
endpointPrefix: { type: "string", description: "Plural endpoint/table-friendly domain name, e.g. reservations." },
|
|
808
|
-
tableName: { type: "string", description: "Database table name, e.g. reservations." },
|
|
809
|
-
},
|
|
810
|
-
},
|
|
811
|
-
target: {
|
|
812
|
-
type: "object",
|
|
813
|
-
properties: {
|
|
814
|
-
frontendDir: { type: "string" },
|
|
815
|
-
endpointDir: { type: "string" },
|
|
816
|
-
databaseDir: { type: "string" },
|
|
817
|
-
},
|
|
818
|
-
},
|
|
819
|
-
overwrite: { type: "string", enum: ["skip", "fail", "replace"], default: "skip" },
|
|
820
|
-
workspace_root: { type: "string", description: "Optional absolute path to the target app workspace." },
|
|
821
|
-
kits_root: { type: "string", description: "Optional local path to dypai-capability-kits. Pass this to force local authoring source; omit it to allow R2 fallback." },
|
|
822
|
-
},
|
|
823
|
-
required: ["operation", "slug"],
|
|
824
|
-
},
|
|
825
|
-
async execute(args) {
|
|
826
|
-
if (args?.operation === "inspect") return inspectCapabilityKit(args)
|
|
827
|
-
if (args?.operation === "apply") return applyCapabilityKit(args)
|
|
828
|
-
throw new Error("manage_capability_kit requires operation 'inspect' or 'apply'.")
|
|
829
|
-
},
|
|
830
|
-
}
|