@bitfun-vsl/plugin 0.0.0-stage → 0.0.1

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.
Files changed (48) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +62 -3
  3. package/assets/bitfun/command/vsl/cr-health.md +5 -0
  4. package/assets/bitfun/command/vsl/review.md +5 -0
  5. package/assets/bitfun/command/vsl/trajectory.md +8 -0
  6. package/assets/build.json +4 -0
  7. package/assets/claude-code/.claude-plugin/plugin.json +5 -0
  8. package/assets/claude-code/.mcp.json +10 -0
  9. package/assets/claude-code/agents/vsl-trajectory.md +6 -0
  10. package/assets/claude-code/commands/cr-health.md +7 -0
  11. package/assets/claude-code/commands/review.md +7 -0
  12. package/assets/claude-code/commands/trajectory.md +10 -0
  13. package/assets/claude-code/scripts/mcp-server.js +741 -0
  14. package/assets/claude-code/skills/vsl-trajectory-analysis/SKILL.md +7 -0
  15. package/assets/deveco/skills/vsl-trajectory-analysis/SKILL.md +7 -0
  16. package/assets/install-manifest.json +58 -0
  17. package/assets/skills/vsl-trajectory-analysis/SKILL.md +7 -0
  18. package/dist/cli/index.d.ts +17 -0
  19. package/dist/cli/index.js +4576 -0
  20. package/dist/hosts/bitfun-kernel/commands.d.ts +26 -0
  21. package/dist/hosts/bitfun-kernel/plugin.d.ts +2 -0
  22. package/dist/hosts/bitfun-kernel/plugin.js +661 -0
  23. package/dist/hosts/claude-code/index.d.ts +1 -0
  24. package/dist/hosts/claude-code/index.js +680 -0
  25. package/dist/hosts/claude-code/mcp-server.d.ts +45 -0
  26. package/dist/hosts/deveco/plugin.d.ts +2 -0
  27. package/dist/hosts/deveco/plugin.js +659 -0
  28. package/dist/hosts/shared/opencode.d.ts +12 -0
  29. package/dist/hosts/shared/schema.d.ts +8 -0
  30. package/dist/index.d.ts +2 -0
  31. package/dist/index.js +663 -0
  32. package/dist/install/config.d.ts +50 -0
  33. package/dist/install/index.d.ts +182 -0
  34. package/dist/install/resources.d.ts +7 -0
  35. package/dist/install/resources.js +590 -0
  36. package/dist/plugin/contracts.d.ts +63 -0
  37. package/dist/plugin/index.d.ts +19 -0
  38. package/dist/plugin/index.js +539 -0
  39. package/dist/plugin/modules/code-review/index.d.ts +4 -0
  40. package/dist/plugin/modules/code-review/runtime.d.ts +42 -0
  41. package/dist/plugin/modules/trajectory-analysis.d.ts +2 -0
  42. package/dist/plugin/registry.d.ts +16 -0
  43. package/dist/plugin/schema.d.ts +2 -0
  44. package/dist/runtime.d.ts +22 -0
  45. package/docs/code-review-integration.md +182 -0
  46. package/docs/init-installation.md +97 -0
  47. package/docs/plugin-design.md +391 -0
  48. package/package.json +74 -6
@@ -0,0 +1,590 @@
1
+ // @bun
2
+ // src/install/resources.ts
3
+ import { join as join2, resolve as resolve2, relative, isAbsolute as isAbsolute2, dirname } from "path";
4
+ import { mkdir as mkdir2, readdir } from "fs/promises";
5
+
6
+ // src/plugin/modules/trajectory-analysis.ts
7
+ var inputSchema = {
8
+ type: "object",
9
+ properties: {
10
+ source: { type: "string", description: "\u8F68\u8FF9\u6587\u4EF6\u6216\u76EE\u5F55\uFF1Bmock \u6A21\u5F0F\u53EA\u7528\u4E8E\u6807\u8BC6\u8F93\u5165" }
11
+ },
12
+ required: ["source"],
13
+ additionalProperties: false
14
+ };
15
+ var sourceOf = (input) => {
16
+ if (!input || typeof input !== "object" || typeof input.source !== "string")
17
+ throw new Error("source must be a string");
18
+ return input.source;
19
+ };
20
+ var mockRead = async (input, context) => {
21
+ const source = sourceOf(input);
22
+ context.report("\u8BFB\u53D6\u8F68\u8FF9\uFF08mock\uFF09");
23
+ return {
24
+ title: "Trajectory loaded",
25
+ output: JSON.stringify({ source, events: 12, durationMs: 4800, mock: true }),
26
+ metadata: { source, mock: true }
27
+ };
28
+ };
29
+ var mockAnalyze = async (input, context) => {
30
+ const source = sourceOf(input);
31
+ context.report("\u5206\u6790\u8F68\u8FF9\uFF08mock\uFF09");
32
+ return {
33
+ title: "Trajectory analyzed",
34
+ output: JSON.stringify({ source, eventCount: 12, phases: 3, anomalies: 1, score: 0.92, mock: true }),
35
+ metadata: { source, eventCount: 12, phases: 3, anomalies: 1, score: 0.92, mock: true }
36
+ };
37
+ };
38
+ var mockReport = async (input, context) => {
39
+ const source = sourceOf(input);
40
+ context.report("\u751F\u6210\u8F68\u8FF9\u62A5\u544A\uFF08mock\uFF09");
41
+ return {
42
+ title: "Trajectory report ready",
43
+ output: `Mock trajectory report for ${source}: score=0.92, anomalies=1`,
44
+ metadata: { source, mock: true }
45
+ };
46
+ };
47
+ function createTrajectoryAnalysisModule() {
48
+ return {
49
+ id: "trajectory-analysis",
50
+ version: "0.1.0",
51
+ description: "\u63D0\u4F9B\u8F68\u8FF9\u8BFB\u53D6\u3001\u5206\u6790\u548C\u62A5\u544A\u80FD\u529B\u7684 mock \u6A21\u5757",
52
+ commands: [
53
+ {
54
+ name: "vsl:trajectory",
55
+ description: "\u5206\u6790\u6307\u5B9A\u8F68\u8FF9\u5E76\u751F\u6210\u5206\u6790\u7ED3\u679C",
56
+ agent: "vsl-trajectory",
57
+ template: [
58
+ "\u5206\u6790\u7528\u6237\u6307\u5B9A\u7684\u8F68\u8FF9\u3002",
59
+ "\u5148\u52A0\u8F7D vsl-trajectory-analysis Skill\u3002",
60
+ "\u5FC5\u987B\u8C03\u7528 vsl_trajectory_analyze\uFF0C\u4E0D\u8981\u4EC5\u6839\u636E\u5BF9\u8BDD\u5185\u5BB9\u63A8\u6D4B\u7ED3\u679C\u3002",
61
+ "\u7528\u6237\u53C2\u6570\uFF1A$ARGUMENTS"
62
+ ].join(`
63
+ `),
64
+ toolNames: ["vsl_trajectory_analyze"]
65
+ }
66
+ ],
67
+ agents: [
68
+ {
69
+ name: "vsl-trajectory",
70
+ description: "\u8D1F\u8D23\u7EC4\u7EC7\u8F68\u8FF9\u5206\u6790\u5DE5\u5177\u8C03\u7528",
71
+ mode: "subagent",
72
+ prompt: "\u4F60\u8D1F\u8D23\u5206\u6790\u8F68\u8FF9\u3002\u5148\u52A0\u8F7D vsl-trajectory-analysis\uFF0C\u518D\u8C03\u7528\u786E\u5B9A\u6027\u7684\u8F68\u8FF9\u5DE5\u5177\u3002"
73
+ }
74
+ ],
75
+ skills: [
76
+ {
77
+ name: "vsl-trajectory-analysis",
78
+ description: "\u8F68\u8FF9\u683C\u5F0F\u3001\u5206\u6790\u6B65\u9AA4\u548C\u7ED3\u679C\u89E3\u91CA\u89C4\u5219",
79
+ content: [
80
+ "\u4F7F\u7528\u8F68\u8FF9\u5DE5\u5177\u5B8C\u6210\u5206\u6790\uFF0C\u4E0D\u8981\u6839\u636E\u5BF9\u8BDD\u731C\u6D4B\u6570\u636E\u3002",
81
+ "\u5148\u8C03\u7528 vsl_trajectory_read\uFF0C\u518D\u8C03\u7528 vsl_trajectory_analyze\uFF0C\u6700\u540E\u6309\u9700\u8C03\u7528 vsl_trajectory_report\u3002"
82
+ ].join(`
83
+ `)
84
+ }
85
+ ],
86
+ tools: [
87
+ { name: "vsl_trajectory_read", description: "\u8BFB\u53D6\u5E76\u6821\u9A8C\u8F68\u8FF9\uFF08mock\uFF09", input: inputSchema, execute: mockRead },
88
+ { name: "vsl_trajectory_analyze", description: "\u6267\u884C\u8F68\u8FF9\u5206\u6790\uFF08mock\uFF09", input: inputSchema, execute: mockAnalyze },
89
+ { name: "vsl_trajectory_report", description: "\u751F\u6210\u8F68\u8FF9\u62A5\u544A\uFF08mock\uFF09", input: inputSchema, execute: mockReport }
90
+ ]
91
+ };
92
+ }
93
+
94
+ // src/plugin/schema.ts
95
+ function validateInput(schema, input, path = "input") {
96
+ if (schema.type === "object") {
97
+ if (!input || typeof input !== "object" || Array.isArray(input))
98
+ throw new Error(`${path} must be an object`);
99
+ const values = input;
100
+ schema.required?.forEach((key) => {
101
+ if (values[key] === undefined)
102
+ throw new Error(`${path}.${key} is required`);
103
+ });
104
+ Object.entries(values).forEach(([key, value]) => {
105
+ const property = schema.properties?.[key];
106
+ if (!property && schema.additionalProperties === false)
107
+ throw new Error(`${path}.${key} is not supported`);
108
+ if (property)
109
+ validateInput(property, value, `${path}.${key}`);
110
+ });
111
+ return;
112
+ }
113
+ if (schema.type === "array") {
114
+ if (!Array.isArray(input))
115
+ throw new Error(`${path} must be an array`);
116
+ if (schema.items)
117
+ input.forEach((value, index) => validateInput(schema.items, value, `${path}[${index}]`));
118
+ return;
119
+ }
120
+ if (schema.type === "integer" || schema.type === "number") {
121
+ if (typeof input !== "number" || !Number.isFinite(input) || schema.type === "integer" && !Number.isInteger(input))
122
+ throw new Error(`${path} must be a finite ${schema.type}`);
123
+ if (schema.minimum !== undefined && input < schema.minimum)
124
+ throw new Error(`${path} must be >= ${schema.minimum}`);
125
+ if (schema.maximum !== undefined && input > schema.maximum)
126
+ throw new Error(`${path} must be <= ${schema.maximum}`);
127
+ return;
128
+ }
129
+ if (typeof input !== schema.type)
130
+ throw new Error(`${path} must be a ${schema.type}`);
131
+ if (schema.enum && !schema.enum.includes(String(input)))
132
+ throw new Error(`${path} must be one of ${schema.enum.join(", ")}`);
133
+ }
134
+
135
+ // src/plugin/modules/code-review/runtime.ts
136
+ import { spawn } from "child_process";
137
+ import { createWriteStream } from "fs";
138
+ import { access, mkdir, readFile, realpath, stat } from "fs/promises";
139
+ import { homedir } from "os";
140
+ import { delimiter, isAbsolute, join, resolve } from "path";
141
+ import { pipeline } from "stream/promises";
142
+ async function review(input, context, options) {
143
+ if (Boolean(input.from) !== Boolean(input.to))
144
+ throw new Error("from and to must be provided together");
145
+ if (input.commit && (input.from || input.to))
146
+ throw new Error("commit cannot be combined with from/to");
147
+ if (input.preview && input.resume)
148
+ throw new Error("preview cannot be combined with resume");
149
+ if (input.resume && !input.commit && !input.from)
150
+ throw new Error("resume requires commit or from/to; workspace resume is not supported");
151
+ const directory = await realpath(resolve(context.directory, input.repo ?? "."));
152
+ const args = ["review", "--repo", directory, "--audience", "agent", "--format", "json"];
153
+ const flags = {
154
+ commit: "commit",
155
+ from: "from",
156
+ to: "to",
157
+ resume: "resume",
158
+ background: "background",
159
+ backgroundFile: "background-file",
160
+ rule: "rule",
161
+ provider: "provider",
162
+ model: "model",
163
+ effort: "effort",
164
+ concurrency: "concurrency",
165
+ taskTimeoutMinutes: "timeout",
166
+ maxTokensBudget: "max-tokens-budget"
167
+ };
168
+ Object.entries(flags).forEach(([key, flag]) => {
169
+ const value = input[key];
170
+ if (value !== undefined)
171
+ args.push(`--${flag}`, String(value));
172
+ });
173
+ if (input.preview)
174
+ args.push("--preview");
175
+ if (input.exclude?.length)
176
+ args.push("--exclude", input.exclude.join(","));
177
+ context.report(input.preview ? "Previewing CR review scope" : "Running CR review");
178
+ const result = await runCr(await resolveCr(options.binary), args, {
179
+ directory,
180
+ signal: context.signal,
181
+ timeoutMs: (input.overallTimeoutMinutes ?? 60) * 60000,
182
+ cacheDirectory: options.cacheDirectory
183
+ });
184
+ return reviewResult(result, directory, input.preview);
185
+ }
186
+ async function reviewResult(result, directory, preview = false) {
187
+ const text = await readFile(result.resultFile, "utf8");
188
+ const parsed = await Promise.resolve().then(() => JSON.parse(text)).catch(() => {
189
+ return;
190
+ });
191
+ const payload = record(parsed);
192
+ const manifest = record(payload?.manifest);
193
+ const coverage = record(manifest?.coverage);
194
+ const terminal = manifest?.terminal_state;
195
+ const knownState = typeof terminal === "string" && ["complete", "partial", "failed", "skipped"].includes(terminal);
196
+ const validCoverage = ["selected", "completed", "reused", "failed", "waived"].every((key) => Array.isArray(coverage?.[key]));
197
+ const validReview = knownState && manifest?.schema_version === "cr.run-manifest/v1" && validCoverage && Array.isArray(payload?.comments);
198
+ const noFiles = !manifest && payload?.status === "skipped" && Array.isArray(payload.comments) && payload.comments.length === 0 && typeof payload.message === "string";
199
+ const validPreview = preview && typeof payload?.total_files === "number" && typeof payload.reviewable_count === "number" && Array.isArray(payload.files);
200
+ const diagnostic = (await readFile(result.stderrFile, "utf8")).slice(-4000);
201
+ const error = result.error || (result.exitCode !== 0 ? `CR exited with code ${result.exitCode}` : undefined) || (!(preview ? validPreview : validReview || noFiles) ? "CR returned empty, invalid, or incompatible JSON; review completion is unknown" : undefined);
202
+ const comments = Array.isArray(payload?.comments) ? payload.comments : [];
203
+ const metadata = {
204
+ repository: directory,
205
+ executable: result.executable,
206
+ exitCode: result.exitCode,
207
+ state: validReview ? terminal : !error && validPreview ? "preview" : !error && noFiles ? "skipped" : "error",
208
+ terminalState: knownState ? terminal : undefined,
209
+ message: typeof payload?.message === "string" ? payload.message.slice(0, 1000) : undefined,
210
+ sessionId: payload?.session_id,
211
+ resultFile: result.resultFile,
212
+ stderrFile: result.stderrFile
213
+ };
214
+ const output = JSON.stringify({
215
+ ...metadata,
216
+ error,
217
+ ...error ? { diagnostic } : {},
218
+ manifest,
219
+ summary: payload?.summary,
220
+ warnings: payload?.warnings,
221
+ resume: payload?.resume,
222
+ ...preview ? { preview: payload } : {
223
+ findingsCount: comments.length,
224
+ comments: text.length > 48000 ? [] : comments.slice(0, 30),
225
+ findingsTruncated: comments.length > 30 || text.length > 48000
226
+ }
227
+ });
228
+ return {
229
+ title: error ? "CR execution failed" : preview ? "CR preview ready" : `CR review: ${metadata.state}`,
230
+ isError: Boolean(error || terminal === "failed"),
231
+ metadata,
232
+ output: output.length <= 64000 ? output : JSON.stringify({
233
+ ...metadata,
234
+ error,
235
+ diagnostic: error ? diagnostic : undefined,
236
+ findingsCount: comments.length,
237
+ coverageCounts: Object.fromEntries(Object.entries(coverage ?? {}).filter(([, value]) => Array.isArray(value)).map(([key, value]) => [key, value.length])),
238
+ detailsTruncated: true,
239
+ findingsTruncated: !preview,
240
+ instruction: "Read resultFile in bounded batches for the complete review/preview, findings, warnings and coverage."
241
+ })
242
+ };
243
+ }
244
+ async function health(input, context, options) {
245
+ const executable = await resolveCr(options.binary);
246
+ const version = await runCr(executable, ["version"], { directory: context.directory, signal: context.signal, timeoutMs: 30000, cacheDirectory: options.cacheDirectory });
247
+ const help = await runCr(executable, ["review", "--help"], { directory: context.directory, signal: context.signal, timeoutMs: 30000, cacheDirectory: options.cacheDirectory });
248
+ const helpText = await readFile(help.resultFile, "utf8");
249
+ const compatible = ["--format", "--audience", "--preview", "--resume", "--max-tokens-budget"].every((flag) => helpText.includes(flag));
250
+ const connection = input.checkConnection ? await runCr(executable, ["llm", "test"], {
251
+ directory: context.directory,
252
+ signal: context.signal,
253
+ timeoutMs: 120000,
254
+ cacheDirectory: options.cacheDirectory
255
+ }) : undefined;
256
+ const isError = !compatible || [version, help, ...connection ? [connection] : []].some((item) => item.error || item.exitCode !== 0);
257
+ return {
258
+ title: isError ? "CR health check failed" : "CR is available",
259
+ isError,
260
+ output: JSON.stringify({
261
+ executable,
262
+ version: (await readFile(version.resultFile, "utf8")).trim(),
263
+ cliFlagsCompatible: compatible,
264
+ manifestCompatibility: "Validated against cr.run-manifest/v1 when an actual review runs",
265
+ modelConnection: connection ? { exitCode: connection.exitCode, error: connection.error, output: (await readFile(connection.resultFile, "utf8")).trim(), stderrFile: connection.stderrFile } : "not checked",
266
+ diagnostics: [version, help]
267
+ })
268
+ };
269
+ }
270
+ async function resolveCr(binary = process.env.VSL_CR_BIN) {
271
+ if (binary) {
272
+ if (!isAbsolute(binary))
273
+ throw new Error("VSL_CR_BIN must be an absolute path to the native CR executable");
274
+ if (/\.(cmd|bat|ps1|js)$/i.test(binary))
275
+ throw new Error("VSL_CR_BIN must point to the native executable, not a shell or npm shim");
276
+ await access(binary);
277
+ return realpath(binary);
278
+ }
279
+ const names = process.platform === "win32" ? ["cr.exe"] : ["cr"];
280
+ const candidates = (process.env.PATH ?? "").split(delimiter).filter(Boolean).flatMap((directory) => names.map((name) => join(directory, name)));
281
+ const found = await Promise.all(candidates.map(async (path) => await access(path).then(() => path, () => {
282
+ return;
283
+ })));
284
+ const executable = found.find((path) => path !== undefined);
285
+ if (!executable)
286
+ throw new Error("CR native executable not found. Install/build the review CLI and set VSL_CR_BIN to its absolute executable path.");
287
+ return realpath(executable);
288
+ }
289
+ async function runCr(executable, args, options) {
290
+ options.signal.throwIfAborted();
291
+ const directory = join(options.cacheDirectory ?? process.env.VSL_CR_CACHE_DIR ?? join(process.env.LOCALAPPDATA ?? process.env.XDG_CACHE_HOME ?? join(homedir(), ".cache"), "vsl", "cr"), crypto.randomUUID());
292
+ await mkdir(directory, { recursive: true, mode: 448 });
293
+ const resultFile = join(directory, "result.json");
294
+ const stderrFile = join(directory, "stderr.log");
295
+ const child = spawn(executable, args, {
296
+ cwd: options.directory,
297
+ env: { ...process.env, CR_NO_UPDATE: "1" },
298
+ shell: false,
299
+ detached: process.platform !== "win32",
300
+ windowsHide: true,
301
+ stdio: ["ignore", "pipe", "pipe"]
302
+ });
303
+ let error;
304
+ let closed = false;
305
+ let forceTimer;
306
+ const kill = () => {
307
+ if (closed || !child.pid)
308
+ return;
309
+ if (process.platform === "win32") {
310
+ spawn("taskkill", ["/pid", String(child.pid), "/T", "/F"], { windowsHide: true, stdio: "ignore" }).on("error", () => child.kill("SIGKILL"));
311
+ return;
312
+ }
313
+ try {
314
+ process.kill(-child.pid, "SIGTERM");
315
+ } catch {
316
+ child.kill("SIGTERM");
317
+ }
318
+ forceTimer = setTimeout(() => {
319
+ if (!closed && child.pid) {
320
+ try {
321
+ process.kill(-child.pid, "SIGKILL");
322
+ } catch {
323
+ child.kill("SIGKILL");
324
+ }
325
+ }
326
+ }, 3000);
327
+ };
328
+ const stop = (message) => {
329
+ if (error)
330
+ return;
331
+ error = message;
332
+ kill();
333
+ };
334
+ const abort = () => stop("CR execution cancelled");
335
+ const timer = setTimeout(() => stop("CR overall execution deadline exceeded"), options.timeoutMs);
336
+ options.signal.addEventListener("abort", abort, { once: true });
337
+ if (options.signal.aborted)
338
+ abort();
339
+ child.on("error", (cause) => {
340
+ error ??= `Cannot start CR: ${cause.message}`;
341
+ });
342
+ const streams = Promise.all([
343
+ pipeline(child.stdout, createWriteStream(resultFile, { mode: 384 })),
344
+ pipeline(child.stderr, createWriteStream(stderrFile, { mode: 384 }))
345
+ ]).catch((cause) => stop(`Cannot save CR output: ${cause instanceof Error ? cause.message : String(cause)}`));
346
+ let outputBytes = 0;
347
+ [child.stdout, child.stderr].forEach((stream) => stream.on("data", (chunk) => {
348
+ outputBytes += chunk.length;
349
+ if (outputBytes > 32 * 1024 * 1024)
350
+ stop("CR output exceeds 32 MiB; partial evidence retained on disk");
351
+ }));
352
+ const exitCode = await new Promise((done) => child.once("close", (code) => {
353
+ closed = true;
354
+ clearTimeout(timer);
355
+ clearTimeout(forceTimer);
356
+ options.signal.removeEventListener("abort", abort);
357
+ done(code);
358
+ }));
359
+ await streams;
360
+ const size = (await stat(resultFile)).size;
361
+ if (size > 32 * 1024 * 1024)
362
+ throw new Error(`CR JSON exceeds the 32 MiB parsing limit; full output: ${resultFile}; stderr: ${stderrFile}`);
363
+ return { executable, exitCode, resultFile, stderrFile, error };
364
+ }
365
+ function record(value) {
366
+ return value && typeof value === "object" && !Array.isArray(value) ? value : undefined;
367
+ }
368
+
369
+ // src/plugin/modules/code-review/index.ts
370
+ var reviewSchema = {
371
+ type: "object",
372
+ additionalProperties: false,
373
+ properties: {
374
+ repo: { type: "string", description: "Repository path; defaults to the host project. In Claude pass the current project explicitly if MCP cwd differs." },
375
+ commit: { type: "string", description: "Single commit; mutually exclusive with from/to" },
376
+ from: { type: "string", description: "Base ref; requires to" },
377
+ to: { type: "string", description: "Target ref; requires from" },
378
+ preview: { type: "boolean", description: "Preview scope without calling the CR model" },
379
+ resume: { type: "string", description: "CR session ID; requires original commit or from/to; incompatible with preview" },
380
+ background: { type: "string" },
381
+ backgroundFile: { type: "string" },
382
+ rule: { type: "string" },
383
+ exclude: { type: "array", items: { type: "string" } },
384
+ provider: { type: "string" },
385
+ model: { type: "string" },
386
+ effort: { type: "string", enum: ["low", "medium", "high"] },
387
+ concurrency: { type: "integer", minimum: 1 },
388
+ taskTimeoutMinutes: { type: "integer", minimum: 1 },
389
+ maxTokensBudget: { type: "integer", minimum: 0 },
390
+ overallTimeoutMinutes: { type: "number", minimum: 0.001, maximum: 1440, description: "Whole process deadline, default 60 minutes; independent of CR per-task timeout" }
391
+ }
392
+ };
393
+ var healthSchema = {
394
+ type: "object",
395
+ additionalProperties: false,
396
+ properties: { checkConnection: { type: "boolean", description: "Explicitly test CR's configured model connection; default false" } }
397
+ };
398
+ function createCodeReviewModule(options = {}) {
399
+ return {
400
+ id: "code-review",
401
+ version: "0.1.0",
402
+ description: "Run the complete review engine through its native CLI",
403
+ commands: [
404
+ {
405
+ name: "vsl:review",
406
+ description: "Run a complete CR review of workspace, commit, or branch changes",
407
+ toolNames: ["vsl_cr_review"],
408
+ template: "Use vsl_cr_review to run the complete CR engine. Convert the user's scope to structured arguments; pass the current repository. Default to workspace review. CR uses its own model configuration. Report manifest terminal state and coverage; partial/skipped/failed are not complete. Preserve CR findings and severity; do not substitute your own review. If findingsTruncated or detailsTruncated is true, read resultFile in bounded batches before reporting results. Preview is scope only. Resume requires the original commit or from/to. Do not modify code or publish comments unless requested. User request: $ARGUMENTS"
409
+ },
410
+ {
411
+ name: "vsl:cr-health",
412
+ description: "Check CR executable, CLI compatibility, and optionally its model connection",
413
+ toolNames: ["vsl_cr_health"],
414
+ template: "Use vsl_cr_health. Only set checkConnection=true when the user asks to test the model connection. Report executable path, version, compatibility and diagnostics. User request: $ARGUMENTS"
415
+ }
416
+ ],
417
+ tools: [
418
+ {
419
+ name: "vsl_cr_review",
420
+ description: "Run the complete CR review engine with its own configured model. Read manifest coverage and terminal state; zero findings or exit 0 alone do not prove completion. Full raw output is retained in resultFile. Read that file if findingsTruncated. No code is changed by VSL.",
421
+ input: reviewSchema,
422
+ async execute(input, context) {
423
+ validateInput(reviewSchema, input);
424
+ return review(input, context, options);
425
+ }
426
+ },
427
+ {
428
+ name: "vsl_cr_health",
429
+ description: "Locate the native CR executable and check version/required CLI flags. Model connectivity is only tested when checkConnection=true.",
430
+ input: healthSchema,
431
+ async execute(input, context) {
432
+ validateInput(healthSchema, input);
433
+ return health(input, context, options);
434
+ }
435
+ }
436
+ ]
437
+ };
438
+ }
439
+
440
+ // src/plugin/registry.ts
441
+ var assertUnique = (kind, names) => {
442
+ const seen = new Set;
443
+ const duplicate = names.find((name) => {
444
+ if (seen.has(name))
445
+ return true;
446
+ seen.add(name);
447
+ return false;
448
+ });
449
+ if (duplicate)
450
+ throw new Error(`${kind} conflict: ${duplicate}`);
451
+ };
452
+ function createModuleRegistry(input = {}) {
453
+ const modules = (input.modules ?? []).filter((module) => input.enabled?.[module.id] !== false);
454
+ if (new Set(modules.map((module) => module.id)).size !== modules.length)
455
+ throw new Error("module conflict");
456
+ const resources = {
457
+ modules,
458
+ commands: modules.flatMap((module) => module.commands ?? []),
459
+ agents: modules.flatMap((module) => module.agents ?? []),
460
+ skills: modules.flatMap((module) => module.skills ?? []),
461
+ tools: modules.flatMap((module) => module.tools ?? [])
462
+ };
463
+ assertUnique("command", resources.commands.map((item) => item.name));
464
+ assertUnique("agent", resources.agents.map((item) => item.name));
465
+ assertUnique("skill", resources.skills.map((item) => item.name));
466
+ assertUnique("tool", resources.tools.map((item) => item.name));
467
+ const agents = new Set(resources.agents.map((item) => item.name));
468
+ const tools = new Set(resources.tools.map((item) => item.name));
469
+ resources.commands.forEach((command) => {
470
+ if (command.agent && !agents.has(command.agent))
471
+ throw new Error(`command ${command.name} references missing agent ${command.agent}`);
472
+ command.toolNames?.forEach((tool) => {
473
+ if (!tools.has(tool))
474
+ throw new Error(`command ${command.name} references missing tool ${tool}`);
475
+ });
476
+ });
477
+ return {
478
+ enabled: () => resources,
479
+ command: (name) => resources.commands.find((item) => item.name === name),
480
+ tool: (name) => resources.tools.find((item) => item.name === name)
481
+ };
482
+ }
483
+ // src/plugin/index.ts
484
+ function createRegistry(input = {}) {
485
+ return createModuleRegistry({ modules: input.modules ?? [createTrajectoryAnalysisModule(), createCodeReviewModule({ binary: input.crBinary })], enabled: input.enabled });
486
+ }
487
+
488
+ // src/hosts/bitfun-kernel/commands.ts
489
+ var bitfunCommandPath = (name) => `${name.replaceAll(":", "/")}.md`;
490
+ var bitfunCommandFile = (command, includeAgent = false) => {
491
+ const frontMatter = [`description: ${JSON.stringify(command.description)}`];
492
+ if (includeAgent && command.agent)
493
+ frontMatter.push(`agent: ${JSON.stringify(command.agent)}`);
494
+ if (command.model)
495
+ frontMatter.push(`model: ${JSON.stringify(command.model)}`);
496
+ if (command.subtask !== undefined)
497
+ frontMatter.push(`subtask: ${command.subtask}`);
498
+ return `---
499
+ ${frontMatter.join(`
500
+ `)}
501
+ ---
502
+
503
+ ${command.template.trim()}
504
+ `;
505
+ };
506
+ if (false) {}
507
+
508
+ // src/install/resources.ts
509
+ var digest = (content) => new Bun.CryptoHasher("sha256").update(content).digest("hex");
510
+ function inside(root, path) {
511
+ const target = resolve2(root, path);
512
+ const part = relative(resolve2(root), target);
513
+ if (part === ".." || part.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`) || isAbsolute2(part))
514
+ throw new Error(`Path escapes VSL root: ${path}`);
515
+ return target;
516
+ }
517
+ async function filesUnder(root) {
518
+ return (await Promise.all((await readdir(root, { withFileTypes: true })).map(async (entry) => {
519
+ if (entry.isSymbolicLink())
520
+ throw new Error(`Symlink not allowed in managed resources: ${join2(root, entry.name)}`);
521
+ if (entry.isDirectory())
522
+ return (await filesUnder(join2(root, entry.name))).map((file) => `${entry.name}/${file}`);
523
+ return entry.isFile() ? [entry.name] : [];
524
+ }))).flat().sort();
525
+ }
526
+ function selectResources(names) {
527
+ const all = createRegistry().enabled();
528
+ const unknown = names?.find((name) => !all.modules.some((module) => module.id === name));
529
+ if (unknown)
530
+ throw new Error(`Unknown VSL module: ${unknown}`);
531
+ if (names && new Set(names).size !== names.length)
532
+ throw new Error("Duplicate VSL module selection");
533
+ return createRegistry({ modules: all.modules.filter((module) => !names || names.includes(module.id)) }).enabled();
534
+ }
535
+ function renderResources(resources, version) {
536
+ const invalid = [...resources.commands.map((item) => item.name.replace(/^vsl:/, "")), ...resources.agents.map((item) => item.name), ...resources.skills.map((item) => item.name)].find((name) => !/^[a-z0-9][a-z0-9-]*$/.test(name));
537
+ if (invalid)
538
+ throw new Error(`Resource name cannot be mapped to host files: ${invalid}`);
539
+ const unsupported = resources.agents.find((agent) => agent.permission && Object.keys(agent.permission).length || agent.hidden || agent.mode !== "subagent");
540
+ if (unsupported)
541
+ throw new Error(`Claude resource generation requires an explicit adapter for agent permissions, visibility or mode: ${unsupported.name}`);
542
+ const frontmatter = (values) => `---
543
+ ${Object.entries(values).map(([key, value]) => `${key}: ${JSON.stringify(value)}`).join(`
544
+ `)}
545
+ ---
546
+
547
+ `;
548
+ return {
549
+ "claude-code/.claude-plugin/plugin.json": `${JSON.stringify({ name: "vsl", version, description: "VSL shared modules and tools" }, null, 2)}
550
+ `,
551
+ "claude-code/.mcp.json": `${JSON.stringify({ mcpServers: { vsl: { command: "bun", args: ["${CLAUDE_PLUGIN_ROOT}/scripts/mcp-server.js"] } } }, null, 2)}
552
+ `,
553
+ ...Object.fromEntries(resources.commands.map((command) => [
554
+ `claude-code/commands/${command.name.replace(/^vsl:/, "")}.md`,
555
+ `${frontmatter({ description: command.description, ...command.model ? { model: command.model } : {} })}Use the VSL MCP tools with their registered host prefix. Pass the absolute current project as repo when applicable.
556
+
557
+ ${command.template}
558
+ `
559
+ ])),
560
+ ...Object.fromEntries(resources.agents.map((agent) => [
561
+ `claude-code/agents/${agent.name}.md`,
562
+ `${frontmatter({ name: agent.name, description: agent.description ?? agent.name, ...agent.model ? { model: agent.model } : {} })}${agent.prompt}
563
+ `
564
+ ])),
565
+ ...Object.fromEntries(resources.commands.map((command) => [`bitfun/command/${bitfunCommandPath(command.name)}`, bitfunCommandFile(command)])),
566
+ ...Object.fromEntries(["deveco", "claude-code", "shared"].flatMap((host) => resources.skills.flatMap((skill) => [
567
+ [`${host === "shared" ? "" : host + "/"}skills/${skill.name}/SKILL.md`, `${frontmatter({ name: skill.name, description: skill.description })}${skill.content}
568
+ `],
569
+ ...Object.entries(skill.files ?? {}).map(([path, content]) => {
570
+ inside(resolve2("/vsl-resource", skill.name), path);
571
+ return [`${host === "shared" ? "" : host + "/"}skills/${skill.name}/${path}`, content];
572
+ })
573
+ ])))
574
+ };
575
+ }
576
+ async function writeResources(root, resources) {
577
+ await Promise.all(Object.entries(resources).map(async ([path, content]) => {
578
+ const target = inside(root, path);
579
+ await mkdir2(dirname(target), { recursive: true });
580
+ await Bun.write(target, content);
581
+ }));
582
+ }
583
+ export {
584
+ writeResources,
585
+ selectResources,
586
+ renderResources,
587
+ inside,
588
+ filesUnder,
589
+ digest
590
+ };
@@ -0,0 +1,63 @@
1
+ export type JsonSchema = {
2
+ type: "object" | "string" | "boolean" | "number" | "integer" | "array";
3
+ description?: string;
4
+ properties?: Record<string, JsonSchema>;
5
+ items?: JsonSchema;
6
+ enum?: string[];
7
+ minimum?: number;
8
+ maximum?: number;
9
+ required?: string[];
10
+ additionalProperties?: boolean;
11
+ };
12
+ export type ModuleContext = {
13
+ directory: string;
14
+ worktree: string;
15
+ signal: AbortSignal;
16
+ sessionId?: string;
17
+ report(title: string): void;
18
+ };
19
+ export type ToolOutput = {
20
+ title: string;
21
+ output: string;
22
+ metadata?: Record<string, unknown>;
23
+ isError?: boolean;
24
+ };
25
+ export type ModuleTool = {
26
+ name: string;
27
+ description: string;
28
+ input: JsonSchema;
29
+ execute(input: unknown, context: ModuleContext): Promise<ToolOutput>;
30
+ };
31
+ export type ModuleCommand = {
32
+ name: string;
33
+ description: string;
34
+ template: string;
35
+ agent?: string;
36
+ model?: string;
37
+ subtask?: boolean;
38
+ toolNames?: string[];
39
+ };
40
+ export type ModuleAgent = {
41
+ name: string;
42
+ description?: string;
43
+ mode: "primary" | "subagent" | "all";
44
+ prompt: string;
45
+ model?: string;
46
+ permission?: Record<string, "allow" | "ask" | "deny">;
47
+ hidden?: boolean;
48
+ };
49
+ export type ModuleSkill = {
50
+ name: string;
51
+ description: string;
52
+ content: string;
53
+ files?: Record<string, string | Uint8Array>;
54
+ };
55
+ export type PluginModule = {
56
+ id: string;
57
+ version: string;
58
+ description: string;
59
+ commands?: ModuleCommand[];
60
+ agents?: ModuleAgent[];
61
+ skills?: ModuleSkill[];
62
+ tools?: ModuleTool[];
63
+ };
@@ -0,0 +1,19 @@
1
+ import type { PluginModule } from "./contracts.js";
2
+ export * from "./contracts.js";
3
+ export { createModuleRegistry } from "./registry.js";
4
+ export { createTrajectoryAnalysisModule } from "./modules/trajectory-analysis.js";
5
+ export { createCodeReviewModule } from "./modules/code-review/index.js";
6
+ export declare function createRegistry(input?: {
7
+ modules?: PluginModule[];
8
+ enabled?: Record<string, boolean>;
9
+ crBinary?: string;
10
+ }): {
11
+ enabled: () => import("./registry.js").PluginResources;
12
+ command: (name: string) => import("./contracts.js").ModuleCommand | undefined;
13
+ tool: (name: string) => import("./contracts.js").ModuleTool | undefined;
14
+ };
15
+ export declare function createHostRegistry(root: string): {
16
+ enabled: () => import("./registry.js").PluginResources;
17
+ command: (name: string) => import("./contracts.js").ModuleCommand | undefined;
18
+ tool: (name: string) => import("./contracts.js").ModuleTool | undefined;
19
+ };