@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,741 @@
1
+ // @bun
2
+ // package.json
3
+ var package_default = {
4
+ $schema: "https://json.schemastore.org/package.json",
5
+ name: "@bitfun-vsl/plugin",
6
+ version: "0.0.1",
7
+ main: "./dist/index.js",
8
+ types: "./dist/index.d.ts",
9
+ type: "module",
10
+ license: "MIT",
11
+ description: "Shared modules and native host adapters for DevEco Code, Claude Code and BitFun",
12
+ repository: {
13
+ type: "git",
14
+ url: "git+https://gitcode.com/OpenBitFun/bitfun_horizon_verify.git"
15
+ },
16
+ homepage: "https://gitcode.com/OpenBitFun/bitfun_horizon_verify#readme",
17
+ scripts: {
18
+ prepack: "bun run build",
19
+ typecheck: "tsgo --noEmit",
20
+ test: "bun test",
21
+ "install:bitfun": "bun src/hosts/bitfun-kernel/commands.ts",
22
+ build: "bun run script/build.ts"
23
+ },
24
+ exports: {
25
+ ".": {
26
+ types: "./dist/index.d.ts",
27
+ import: "./dist/index.js"
28
+ },
29
+ "./plugin": {
30
+ types: "./dist/plugin/index.d.ts",
31
+ import: "./dist/plugin/index.js"
32
+ },
33
+ "./hosts/deveco": {
34
+ types: "./dist/hosts/deveco/plugin.d.ts",
35
+ import: "./dist/hosts/deveco/plugin.js"
36
+ },
37
+ "./server": {
38
+ types: "./dist/hosts/deveco/plugin.d.ts",
39
+ import: "./dist/hosts/deveco/plugin.js"
40
+ },
41
+ "./hosts/bitfun-kernel": {
42
+ types: "./dist/hosts/bitfun-kernel/plugin.d.ts",
43
+ import: "./dist/hosts/bitfun-kernel/plugin.js"
44
+ },
45
+ "./hosts/claude-code": {
46
+ types: "./dist/hosts/claude-code/index.d.ts",
47
+ import: "./dist/hosts/claude-code/index.js"
48
+ }
49
+ },
50
+ files: [
51
+ "dist",
52
+ "assets",
53
+ "README.md",
54
+ "docs/plugin-design.md",
55
+ "docs/init-installation.md",
56
+ "docs/code-review-integration.md"
57
+ ],
58
+ dependencies: {
59
+ "@inquirer/prompts": "^8.7.5",
60
+ "@opencode-ai/plugin": "1.17.9",
61
+ "jsonc-parser": "3.3.1"
62
+ },
63
+ devDependencies: {
64
+ "@tsconfig/bun": "1.0.9",
65
+ "@types/bun": "1.3.13",
66
+ "@types/node": "24.12.2",
67
+ typescript: "5.8.2",
68
+ "@typescript/native-preview": "7.0.0-dev.20251207.1"
69
+ },
70
+ bin: {
71
+ vsl: "dist/cli/index.js"
72
+ },
73
+ publishConfig: {
74
+ access: "public"
75
+ }
76
+ };
77
+
78
+ // src/plugin/modules/trajectory-analysis.ts
79
+ var inputSchema = {
80
+ type: "object",
81
+ properties: {
82
+ source: { type: "string", description: "\u8F68\u8FF9\u6587\u4EF6\u6216\u76EE\u5F55\uFF1Bmock \u6A21\u5F0F\u53EA\u7528\u4E8E\u6807\u8BC6\u8F93\u5165" }
83
+ },
84
+ required: ["source"],
85
+ additionalProperties: false
86
+ };
87
+ var sourceOf = (input) => {
88
+ if (!input || typeof input !== "object" || typeof input.source !== "string")
89
+ throw new Error("source must be a string");
90
+ return input.source;
91
+ };
92
+ var mockRead = async (input, context) => {
93
+ const source = sourceOf(input);
94
+ context.report("\u8BFB\u53D6\u8F68\u8FF9\uFF08mock\uFF09");
95
+ return {
96
+ title: "Trajectory loaded",
97
+ output: JSON.stringify({ source, events: 12, durationMs: 4800, mock: true }),
98
+ metadata: { source, mock: true }
99
+ };
100
+ };
101
+ var mockAnalyze = async (input, context) => {
102
+ const source = sourceOf(input);
103
+ context.report("\u5206\u6790\u8F68\u8FF9\uFF08mock\uFF09");
104
+ return {
105
+ title: "Trajectory analyzed",
106
+ output: JSON.stringify({ source, eventCount: 12, phases: 3, anomalies: 1, score: 0.92, mock: true }),
107
+ metadata: { source, eventCount: 12, phases: 3, anomalies: 1, score: 0.92, mock: true }
108
+ };
109
+ };
110
+ var mockReport = async (input, context) => {
111
+ const source = sourceOf(input);
112
+ context.report("\u751F\u6210\u8F68\u8FF9\u62A5\u544A\uFF08mock\uFF09");
113
+ return {
114
+ title: "Trajectory report ready",
115
+ output: `Mock trajectory report for ${source}: score=0.92, anomalies=1`,
116
+ metadata: { source, mock: true }
117
+ };
118
+ };
119
+ function createTrajectoryAnalysisModule() {
120
+ return {
121
+ id: "trajectory-analysis",
122
+ version: "0.1.0",
123
+ description: "\u63D0\u4F9B\u8F68\u8FF9\u8BFB\u53D6\u3001\u5206\u6790\u548C\u62A5\u544A\u80FD\u529B\u7684 mock \u6A21\u5757",
124
+ commands: [
125
+ {
126
+ name: "vsl:trajectory",
127
+ description: "\u5206\u6790\u6307\u5B9A\u8F68\u8FF9\u5E76\u751F\u6210\u5206\u6790\u7ED3\u679C",
128
+ agent: "vsl-trajectory",
129
+ template: [
130
+ "\u5206\u6790\u7528\u6237\u6307\u5B9A\u7684\u8F68\u8FF9\u3002",
131
+ "\u5148\u52A0\u8F7D vsl-trajectory-analysis Skill\u3002",
132
+ "\u5FC5\u987B\u8C03\u7528 vsl_trajectory_analyze\uFF0C\u4E0D\u8981\u4EC5\u6839\u636E\u5BF9\u8BDD\u5185\u5BB9\u63A8\u6D4B\u7ED3\u679C\u3002",
133
+ "\u7528\u6237\u53C2\u6570\uFF1A$ARGUMENTS"
134
+ ].join(`
135
+ `),
136
+ toolNames: ["vsl_trajectory_analyze"]
137
+ }
138
+ ],
139
+ agents: [
140
+ {
141
+ name: "vsl-trajectory",
142
+ description: "\u8D1F\u8D23\u7EC4\u7EC7\u8F68\u8FF9\u5206\u6790\u5DE5\u5177\u8C03\u7528",
143
+ mode: "subagent",
144
+ 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"
145
+ }
146
+ ],
147
+ skills: [
148
+ {
149
+ name: "vsl-trajectory-analysis",
150
+ description: "\u8F68\u8FF9\u683C\u5F0F\u3001\u5206\u6790\u6B65\u9AA4\u548C\u7ED3\u679C\u89E3\u91CA\u89C4\u5219",
151
+ content: [
152
+ "\u4F7F\u7528\u8F68\u8FF9\u5DE5\u5177\u5B8C\u6210\u5206\u6790\uFF0C\u4E0D\u8981\u6839\u636E\u5BF9\u8BDD\u731C\u6D4B\u6570\u636E\u3002",
153
+ "\u5148\u8C03\u7528 vsl_trajectory_read\uFF0C\u518D\u8C03\u7528 vsl_trajectory_analyze\uFF0C\u6700\u540E\u6309\u9700\u8C03\u7528 vsl_trajectory_report\u3002"
154
+ ].join(`
155
+ `)
156
+ }
157
+ ],
158
+ tools: [
159
+ { name: "vsl_trajectory_read", description: "\u8BFB\u53D6\u5E76\u6821\u9A8C\u8F68\u8FF9\uFF08mock\uFF09", input: inputSchema, execute: mockRead },
160
+ { name: "vsl_trajectory_analyze", description: "\u6267\u884C\u8F68\u8FF9\u5206\u6790\uFF08mock\uFF09", input: inputSchema, execute: mockAnalyze },
161
+ { name: "vsl_trajectory_report", description: "\u751F\u6210\u8F68\u8FF9\u62A5\u544A\uFF08mock\uFF09", input: inputSchema, execute: mockReport }
162
+ ]
163
+ };
164
+ }
165
+
166
+ // src/plugin/schema.ts
167
+ function validateInput(schema, input, path = "input") {
168
+ if (schema.type === "object") {
169
+ if (!input || typeof input !== "object" || Array.isArray(input))
170
+ throw new Error(`${path} must be an object`);
171
+ const values = input;
172
+ schema.required?.forEach((key) => {
173
+ if (values[key] === undefined)
174
+ throw new Error(`${path}.${key} is required`);
175
+ });
176
+ Object.entries(values).forEach(([key, value]) => {
177
+ const property = schema.properties?.[key];
178
+ if (!property && schema.additionalProperties === false)
179
+ throw new Error(`${path}.${key} is not supported`);
180
+ if (property)
181
+ validateInput(property, value, `${path}.${key}`);
182
+ });
183
+ return;
184
+ }
185
+ if (schema.type === "array") {
186
+ if (!Array.isArray(input))
187
+ throw new Error(`${path} must be an array`);
188
+ if (schema.items)
189
+ input.forEach((value, index) => validateInput(schema.items, value, `${path}[${index}]`));
190
+ return;
191
+ }
192
+ if (schema.type === "integer" || schema.type === "number") {
193
+ if (typeof input !== "number" || !Number.isFinite(input) || schema.type === "integer" && !Number.isInteger(input))
194
+ throw new Error(`${path} must be a finite ${schema.type}`);
195
+ if (schema.minimum !== undefined && input < schema.minimum)
196
+ throw new Error(`${path} must be >= ${schema.minimum}`);
197
+ if (schema.maximum !== undefined && input > schema.maximum)
198
+ throw new Error(`${path} must be <= ${schema.maximum}`);
199
+ return;
200
+ }
201
+ if (typeof input !== schema.type)
202
+ throw new Error(`${path} must be a ${schema.type}`);
203
+ if (schema.enum && !schema.enum.includes(String(input)))
204
+ throw new Error(`${path} must be one of ${schema.enum.join(", ")}`);
205
+ }
206
+
207
+ // src/plugin/modules/code-review/runtime.ts
208
+ import { spawn } from "child_process";
209
+ import { createWriteStream } from "fs";
210
+ import { access, mkdir, readFile, realpath, stat } from "fs/promises";
211
+ import { homedir } from "os";
212
+ import { delimiter, isAbsolute, join, resolve } from "path";
213
+ import { pipeline } from "stream/promises";
214
+ async function review(input, context, options) {
215
+ if (Boolean(input.from) !== Boolean(input.to))
216
+ throw new Error("from and to must be provided together");
217
+ if (input.commit && (input.from || input.to))
218
+ throw new Error("commit cannot be combined with from/to");
219
+ if (input.preview && input.resume)
220
+ throw new Error("preview cannot be combined with resume");
221
+ if (input.resume && !input.commit && !input.from)
222
+ throw new Error("resume requires commit or from/to; workspace resume is not supported");
223
+ const directory = await realpath(resolve(context.directory, input.repo ?? "."));
224
+ const args = ["review", "--repo", directory, "--audience", "agent", "--format", "json"];
225
+ const flags = {
226
+ commit: "commit",
227
+ from: "from",
228
+ to: "to",
229
+ resume: "resume",
230
+ background: "background",
231
+ backgroundFile: "background-file",
232
+ rule: "rule",
233
+ provider: "provider",
234
+ model: "model",
235
+ effort: "effort",
236
+ concurrency: "concurrency",
237
+ taskTimeoutMinutes: "timeout",
238
+ maxTokensBudget: "max-tokens-budget"
239
+ };
240
+ Object.entries(flags).forEach(([key, flag]) => {
241
+ const value = input[key];
242
+ if (value !== undefined)
243
+ args.push(`--${flag}`, String(value));
244
+ });
245
+ if (input.preview)
246
+ args.push("--preview");
247
+ if (input.exclude?.length)
248
+ args.push("--exclude", input.exclude.join(","));
249
+ context.report(input.preview ? "Previewing CR review scope" : "Running CR review");
250
+ const result = await runCr(await resolveCr(options.binary), args, {
251
+ directory,
252
+ signal: context.signal,
253
+ timeoutMs: (input.overallTimeoutMinutes ?? 60) * 60000,
254
+ cacheDirectory: options.cacheDirectory
255
+ });
256
+ return reviewResult(result, directory, input.preview);
257
+ }
258
+ async function reviewResult(result, directory, preview = false) {
259
+ const text = await readFile(result.resultFile, "utf8");
260
+ const parsed = await Promise.resolve().then(() => JSON.parse(text)).catch(() => {
261
+ return;
262
+ });
263
+ const payload = record(parsed);
264
+ const manifest = record(payload?.manifest);
265
+ const coverage = record(manifest?.coverage);
266
+ const terminal = manifest?.terminal_state;
267
+ const knownState = typeof terminal === "string" && ["complete", "partial", "failed", "skipped"].includes(terminal);
268
+ const validCoverage = ["selected", "completed", "reused", "failed", "waived"].every((key) => Array.isArray(coverage?.[key]));
269
+ const validReview = knownState && manifest?.schema_version === "cr.run-manifest/v1" && validCoverage && Array.isArray(payload?.comments);
270
+ const noFiles = !manifest && payload?.status === "skipped" && Array.isArray(payload.comments) && payload.comments.length === 0 && typeof payload.message === "string";
271
+ const validPreview = preview && typeof payload?.total_files === "number" && typeof payload.reviewable_count === "number" && Array.isArray(payload.files);
272
+ const diagnostic = (await readFile(result.stderrFile, "utf8")).slice(-4000);
273
+ 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);
274
+ const comments = Array.isArray(payload?.comments) ? payload.comments : [];
275
+ const metadata = {
276
+ repository: directory,
277
+ executable: result.executable,
278
+ exitCode: result.exitCode,
279
+ state: validReview ? terminal : !error && validPreview ? "preview" : !error && noFiles ? "skipped" : "error",
280
+ terminalState: knownState ? terminal : undefined,
281
+ message: typeof payload?.message === "string" ? payload.message.slice(0, 1000) : undefined,
282
+ sessionId: payload?.session_id,
283
+ resultFile: result.resultFile,
284
+ stderrFile: result.stderrFile
285
+ };
286
+ const output = JSON.stringify({
287
+ ...metadata,
288
+ error,
289
+ ...error ? { diagnostic } : {},
290
+ manifest,
291
+ summary: payload?.summary,
292
+ warnings: payload?.warnings,
293
+ resume: payload?.resume,
294
+ ...preview ? { preview: payload } : {
295
+ findingsCount: comments.length,
296
+ comments: text.length > 48000 ? [] : comments.slice(0, 30),
297
+ findingsTruncated: comments.length > 30 || text.length > 48000
298
+ }
299
+ });
300
+ return {
301
+ title: error ? "CR execution failed" : preview ? "CR preview ready" : `CR review: ${metadata.state}`,
302
+ isError: Boolean(error || terminal === "failed"),
303
+ metadata,
304
+ output: output.length <= 64000 ? output : JSON.stringify({
305
+ ...metadata,
306
+ error,
307
+ diagnostic: error ? diagnostic : undefined,
308
+ findingsCount: comments.length,
309
+ coverageCounts: Object.fromEntries(Object.entries(coverage ?? {}).filter(([, value]) => Array.isArray(value)).map(([key, value]) => [key, value.length])),
310
+ detailsTruncated: true,
311
+ findingsTruncated: !preview,
312
+ instruction: "Read resultFile in bounded batches for the complete review/preview, findings, warnings and coverage."
313
+ })
314
+ };
315
+ }
316
+ async function health(input, context, options) {
317
+ const executable = await resolveCr(options.binary);
318
+ const version = await runCr(executable, ["version"], { directory: context.directory, signal: context.signal, timeoutMs: 30000, cacheDirectory: options.cacheDirectory });
319
+ const help = await runCr(executable, ["review", "--help"], { directory: context.directory, signal: context.signal, timeoutMs: 30000, cacheDirectory: options.cacheDirectory });
320
+ const helpText = await readFile(help.resultFile, "utf8");
321
+ const compatible = ["--format", "--audience", "--preview", "--resume", "--max-tokens-budget"].every((flag) => helpText.includes(flag));
322
+ const connection = input.checkConnection ? await runCr(executable, ["llm", "test"], {
323
+ directory: context.directory,
324
+ signal: context.signal,
325
+ timeoutMs: 120000,
326
+ cacheDirectory: options.cacheDirectory
327
+ }) : undefined;
328
+ const isError = !compatible || [version, help, ...connection ? [connection] : []].some((item) => item.error || item.exitCode !== 0);
329
+ return {
330
+ title: isError ? "CR health check failed" : "CR is available",
331
+ isError,
332
+ output: JSON.stringify({
333
+ executable,
334
+ version: (await readFile(version.resultFile, "utf8")).trim(),
335
+ cliFlagsCompatible: compatible,
336
+ manifestCompatibility: "Validated against cr.run-manifest/v1 when an actual review runs",
337
+ modelConnection: connection ? { exitCode: connection.exitCode, error: connection.error, output: (await readFile(connection.resultFile, "utf8")).trim(), stderrFile: connection.stderrFile } : "not checked",
338
+ diagnostics: [version, help]
339
+ })
340
+ };
341
+ }
342
+ async function resolveCr(binary = process.env.VSL_CR_BIN) {
343
+ if (binary) {
344
+ if (!isAbsolute(binary))
345
+ throw new Error("VSL_CR_BIN must be an absolute path to the native CR executable");
346
+ if (/\.(cmd|bat|ps1|js)$/i.test(binary))
347
+ throw new Error("VSL_CR_BIN must point to the native executable, not a shell or npm shim");
348
+ await access(binary);
349
+ return realpath(binary);
350
+ }
351
+ const names = process.platform === "win32" ? ["cr.exe"] : ["cr"];
352
+ const candidates = (process.env.PATH ?? "").split(delimiter).filter(Boolean).flatMap((directory) => names.map((name) => join(directory, name)));
353
+ const found = await Promise.all(candidates.map(async (path) => await access(path).then(() => path, () => {
354
+ return;
355
+ })));
356
+ const executable = found.find((path) => path !== undefined);
357
+ if (!executable)
358
+ throw new Error("CR native executable not found. Install/build the review CLI and set VSL_CR_BIN to its absolute executable path.");
359
+ return realpath(executable);
360
+ }
361
+ async function runCr(executable, args, options) {
362
+ options.signal.throwIfAborted();
363
+ 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());
364
+ await mkdir(directory, { recursive: true, mode: 448 });
365
+ const resultFile = join(directory, "result.json");
366
+ const stderrFile = join(directory, "stderr.log");
367
+ const child = spawn(executable, args, {
368
+ cwd: options.directory,
369
+ env: { ...process.env, CR_NO_UPDATE: "1" },
370
+ shell: false,
371
+ detached: process.platform !== "win32",
372
+ windowsHide: true,
373
+ stdio: ["ignore", "pipe", "pipe"]
374
+ });
375
+ let error;
376
+ let closed = false;
377
+ let forceTimer;
378
+ const kill = () => {
379
+ if (closed || !child.pid)
380
+ return;
381
+ if (process.platform === "win32") {
382
+ spawn("taskkill", ["/pid", String(child.pid), "/T", "/F"], { windowsHide: true, stdio: "ignore" }).on("error", () => child.kill("SIGKILL"));
383
+ return;
384
+ }
385
+ try {
386
+ process.kill(-child.pid, "SIGTERM");
387
+ } catch {
388
+ child.kill("SIGTERM");
389
+ }
390
+ forceTimer = setTimeout(() => {
391
+ if (!closed && child.pid) {
392
+ try {
393
+ process.kill(-child.pid, "SIGKILL");
394
+ } catch {
395
+ child.kill("SIGKILL");
396
+ }
397
+ }
398
+ }, 3000);
399
+ };
400
+ const stop = (message) => {
401
+ if (error)
402
+ return;
403
+ error = message;
404
+ kill();
405
+ };
406
+ const abort = () => stop("CR execution cancelled");
407
+ const timer = setTimeout(() => stop("CR overall execution deadline exceeded"), options.timeoutMs);
408
+ options.signal.addEventListener("abort", abort, { once: true });
409
+ if (options.signal.aborted)
410
+ abort();
411
+ child.on("error", (cause) => {
412
+ error ??= `Cannot start CR: ${cause.message}`;
413
+ });
414
+ const streams = Promise.all([
415
+ pipeline(child.stdout, createWriteStream(resultFile, { mode: 384 })),
416
+ pipeline(child.stderr, createWriteStream(stderrFile, { mode: 384 }))
417
+ ]).catch((cause) => stop(`Cannot save CR output: ${cause instanceof Error ? cause.message : String(cause)}`));
418
+ let outputBytes = 0;
419
+ [child.stdout, child.stderr].forEach((stream) => stream.on("data", (chunk) => {
420
+ outputBytes += chunk.length;
421
+ if (outputBytes > 32 * 1024 * 1024)
422
+ stop("CR output exceeds 32 MiB; partial evidence retained on disk");
423
+ }));
424
+ const exitCode = await new Promise((done) => child.once("close", (code) => {
425
+ closed = true;
426
+ clearTimeout(timer);
427
+ clearTimeout(forceTimer);
428
+ options.signal.removeEventListener("abort", abort);
429
+ done(code);
430
+ }));
431
+ await streams;
432
+ const size = (await stat(resultFile)).size;
433
+ if (size > 32 * 1024 * 1024)
434
+ throw new Error(`CR JSON exceeds the 32 MiB parsing limit; full output: ${resultFile}; stderr: ${stderrFile}`);
435
+ return { executable, exitCode, resultFile, stderrFile, error };
436
+ }
437
+ function record(value) {
438
+ return value && typeof value === "object" && !Array.isArray(value) ? value : undefined;
439
+ }
440
+
441
+ // src/plugin/modules/code-review/index.ts
442
+ var reviewSchema = {
443
+ type: "object",
444
+ additionalProperties: false,
445
+ properties: {
446
+ repo: { type: "string", description: "Repository path; defaults to the host project. In Claude pass the current project explicitly if MCP cwd differs." },
447
+ commit: { type: "string", description: "Single commit; mutually exclusive with from/to" },
448
+ from: { type: "string", description: "Base ref; requires to" },
449
+ to: { type: "string", description: "Target ref; requires from" },
450
+ preview: { type: "boolean", description: "Preview scope without calling the CR model" },
451
+ resume: { type: "string", description: "CR session ID; requires original commit or from/to; incompatible with preview" },
452
+ background: { type: "string" },
453
+ backgroundFile: { type: "string" },
454
+ rule: { type: "string" },
455
+ exclude: { type: "array", items: { type: "string" } },
456
+ provider: { type: "string" },
457
+ model: { type: "string" },
458
+ effort: { type: "string", enum: ["low", "medium", "high"] },
459
+ concurrency: { type: "integer", minimum: 1 },
460
+ taskTimeoutMinutes: { type: "integer", minimum: 1 },
461
+ maxTokensBudget: { type: "integer", minimum: 0 },
462
+ overallTimeoutMinutes: { type: "number", minimum: 0.001, maximum: 1440, description: "Whole process deadline, default 60 minutes; independent of CR per-task timeout" }
463
+ }
464
+ };
465
+ var healthSchema = {
466
+ type: "object",
467
+ additionalProperties: false,
468
+ properties: { checkConnection: { type: "boolean", description: "Explicitly test CR's configured model connection; default false" } }
469
+ };
470
+ function createCodeReviewModule(options = {}) {
471
+ return {
472
+ id: "code-review",
473
+ version: "0.1.0",
474
+ description: "Run the complete review engine through its native CLI",
475
+ commands: [
476
+ {
477
+ name: "vsl:review",
478
+ description: "Run a complete CR review of workspace, commit, or branch changes",
479
+ toolNames: ["vsl_cr_review"],
480
+ 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"
481
+ },
482
+ {
483
+ name: "vsl:cr-health",
484
+ description: "Check CR executable, CLI compatibility, and optionally its model connection",
485
+ toolNames: ["vsl_cr_health"],
486
+ 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"
487
+ }
488
+ ],
489
+ tools: [
490
+ {
491
+ name: "vsl_cr_review",
492
+ 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.",
493
+ input: reviewSchema,
494
+ async execute(input, context) {
495
+ validateInput(reviewSchema, input);
496
+ return review(input, context, options);
497
+ }
498
+ },
499
+ {
500
+ name: "vsl_cr_health",
501
+ description: "Locate the native CR executable and check version/required CLI flags. Model connectivity is only tested when checkConnection=true.",
502
+ input: healthSchema,
503
+ async execute(input, context) {
504
+ validateInput(healthSchema, input);
505
+ return health(input, context, options);
506
+ }
507
+ }
508
+ ]
509
+ };
510
+ }
511
+
512
+ // src/plugin/registry.ts
513
+ var assertUnique = (kind, names) => {
514
+ const seen = new Set;
515
+ const duplicate = names.find((name) => {
516
+ if (seen.has(name))
517
+ return true;
518
+ seen.add(name);
519
+ return false;
520
+ });
521
+ if (duplicate)
522
+ throw new Error(`${kind} conflict: ${duplicate}`);
523
+ };
524
+ function createModuleRegistry(input = {}) {
525
+ const modules = (input.modules ?? []).filter((module) => input.enabled?.[module.id] !== false);
526
+ if (new Set(modules.map((module) => module.id)).size !== modules.length)
527
+ throw new Error("module conflict");
528
+ const resources = {
529
+ modules,
530
+ commands: modules.flatMap((module) => module.commands ?? []),
531
+ agents: modules.flatMap((module) => module.agents ?? []),
532
+ skills: modules.flatMap((module) => module.skills ?? []),
533
+ tools: modules.flatMap((module) => module.tools ?? [])
534
+ };
535
+ assertUnique("command", resources.commands.map((item) => item.name));
536
+ assertUnique("agent", resources.agents.map((item) => item.name));
537
+ assertUnique("skill", resources.skills.map((item) => item.name));
538
+ assertUnique("tool", resources.tools.map((item) => item.name));
539
+ const agents = new Set(resources.agents.map((item) => item.name));
540
+ const tools = new Set(resources.tools.map((item) => item.name));
541
+ resources.commands.forEach((command) => {
542
+ if (command.agent && !agents.has(command.agent))
543
+ throw new Error(`command ${command.name} references missing agent ${command.agent}`);
544
+ command.toolNames?.forEach((tool) => {
545
+ if (!tools.has(tool))
546
+ throw new Error(`command ${command.name} references missing tool ${tool}`);
547
+ });
548
+ });
549
+ return {
550
+ enabled: () => resources,
551
+ command: (name) => resources.commands.find((item) => item.name === name),
552
+ tool: (name) => resources.tools.find((item) => item.name === name)
553
+ };
554
+ }
555
+
556
+ // src/runtime.ts
557
+ import { readFileSync, existsSync } from "fs";
558
+ import { dirname, join as join2, resolve as resolve2 } from "path";
559
+ import { fileURLToPath } from "url";
560
+ // assets/build.json
561
+ var build_default = {
562
+ version: "0.0.1",
563
+ buildId: "bbceff2246e5fb0e4b93bb05081457dbc113400b937c6148764573618d8534b5"
564
+ };
565
+
566
+ // src/runtime.ts
567
+ function packageRoot(url) {
568
+ const visit = (directory) => {
569
+ if (existsSync(join2(directory, ".vsl-runtime.json")))
570
+ return directory;
571
+ const manifest = join2(directory, "package.json");
572
+ if (existsSync(manifest) && JSON.parse(readFileSync(manifest, "utf8")).name === "@bitfun-vsl/plugin")
573
+ return directory;
574
+ if (dirname(directory) === directory)
575
+ throw new Error(`Cannot locate VSL package from ${url}`);
576
+ return visit(dirname(directory));
577
+ };
578
+ return visit(dirname(fileURLToPath(url)));
579
+ }
580
+ function runtimeConfig(root) {
581
+ const path = existsSync(join2(root, ".vsl-runtime.json")) ? join2(root, ".vsl-runtime.json") : join2(root, "assets/runtime.json");
582
+ if (!existsSync(path))
583
+ return;
584
+ const value = JSON.parse(readFileSync(path, "utf8"));
585
+ if (!value || typeof value !== "object" || !("schemaVersion" in value) || value.schemaVersion !== 1 || !("host" in value) || !["deveco", "claude-code", "bitfun"].includes(String(value.host)) || !("modules" in value) || !Array.isArray(value.modules) || !value.modules.every((item) => typeof item === "string") || !("installationId" in value) || typeof value.installationId !== "string" || "crBinary" in value && typeof value.crBinary !== "string")
586
+ throw new Error(`Invalid VSL runtime config: ${path}`);
587
+ return value;
588
+ }
589
+ function runtimeInfo(root, modules) {
590
+ return {
591
+ ...build_default,
592
+ packageRoot: resolve2(root),
593
+ installationId: runtimeConfig(root)?.installationId,
594
+ modules: modules.map((item) => ({ id: item.id, version: item.version }))
595
+ };
596
+ }
597
+
598
+ // src/plugin/index.ts
599
+ function createRegistry(input = {}) {
600
+ return createModuleRegistry({ modules: input.modules ?? [createTrajectoryAnalysisModule(), createCodeReviewModule({ binary: input.crBinary })], enabled: input.enabled });
601
+ }
602
+ function createHostRegistry(root) {
603
+ const config = runtimeConfig(root);
604
+ const modules = createRegistry({ crBinary: process.env.VSL_CR_BIN ?? config?.crBinary }).enabled().modules;
605
+ const unknown = config?.modules.find((name) => !modules.some((module) => module.id === name));
606
+ if (unknown)
607
+ throw new Error(`Unknown VSL module: ${unknown}`);
608
+ const selected = modules.filter((module) => !config || config.modules.includes(module.id));
609
+ return createModuleRegistry({ modules: [...selected, {
610
+ id: "runtime",
611
+ version: "1.0.0",
612
+ description: "Installed host build identity",
613
+ tools: [{
614
+ name: "vsl_runtime_info",
615
+ description: "Report the VSL build actually loaded by this host; does not run a model or modify files.",
616
+ input: { type: "object", properties: {}, additionalProperties: false },
617
+ async execute() {
618
+ return { title: "VSL runtime", output: JSON.stringify(runtimeInfo(root, selected)) };
619
+ }
620
+ }]
621
+ }] });
622
+ }
623
+
624
+ // src/hosts/claude-code/mcp-server.ts
625
+ function createMcpProtocol(options = {}) {
626
+ const tools = (options.registry ?? createHostRegistry(packageRoot(import.meta.url))).enabled().tools;
627
+ const byName = new Map(tools.map((item) => [item.name, item]));
628
+ const running = new Map;
629
+ return {
630
+ abortAll() {
631
+ running.forEach((controller) => controller.abort());
632
+ },
633
+ async handle(message) {
634
+ const respond = (result) => ({ jsonrpc: "2.0", id: message.id ?? null, result });
635
+ if (message.method === "notifications/cancelled") {
636
+ if (message.params?.requestId !== undefined)
637
+ running.get(message.params.requestId)?.abort();
638
+ return;
639
+ }
640
+ if (message.method?.startsWith("notifications/"))
641
+ return;
642
+ if (message.id === undefined || message.id === null)
643
+ return;
644
+ if (message.method === "initialize")
645
+ return respond({ protocolVersion: "2024-11-05", capabilities: { tools: {} }, serverInfo: { name: "vsl", version: package_default.version } });
646
+ if (message.method === "ping")
647
+ return respond({});
648
+ if (message.method === "tools/list")
649
+ return respond({ tools: tools.map((item) => ({ name: item.name, description: item.description, inputSchema: item.input })) });
650
+ if (message.method === "tools/call") {
651
+ const item = byName.get(message.params?.name ?? "");
652
+ if (!item)
653
+ return { jsonrpc: "2.0", id: message.id, error: { code: -32602, message: `unknown tool: ${message.params?.name}` } };
654
+ if (running.has(message.id))
655
+ return { jsonrpc: "2.0", id: message.id, error: { code: -32600, message: "duplicate active request ID" } };
656
+ const controller = new AbortController;
657
+ running.set(message.id, controller);
658
+ try {
659
+ const args = message.params?.arguments ?? {};
660
+ validateInput(item.input, args);
661
+ const directory = options.directory ?? process.cwd();
662
+ const output = await item.execute(args, { directory, worktree: directory, signal: controller.signal, report: () => {
663
+ return;
664
+ } });
665
+ return respond({ content: [{ type: "text", text: output.output }], isError: output.isError ?? false });
666
+ } catch (error) {
667
+ return respond({ content: [{ type: "text", text: error instanceof Error ? error.message : String(error) }], isError: true });
668
+ } finally {
669
+ running.delete(message.id);
670
+ }
671
+ }
672
+ return { jsonrpc: "2.0", id: message.id, error: { code: -32601, message: `unknown method: ${message.method}` } };
673
+ }
674
+ };
675
+ }
676
+ async function runMcpServer(input, write, protocol = createMcpProtocol()) {
677
+ const reader = input.getReader();
678
+ const decoder = new TextDecoder;
679
+ const pending = new Set;
680
+ let buffer = "";
681
+ const dispatch = (line) => {
682
+ if (!line.trim())
683
+ return;
684
+ if (line.length > 4 * 1024 * 1024)
685
+ throw new Error("MCP request exceeds 4 MiB");
686
+ const task = Promise.resolve().then(async () => {
687
+ const message = JSON.parse(line);
688
+ if (!message || typeof message !== "object" || Array.isArray(message) || !("method" in message) || typeof message.method !== "string" || !("jsonrpc" in message) || message.jsonrpc !== "2.0" || "id" in message && message.id !== null && typeof message.id !== "string" && typeof message.id !== "number" || "params" in message && (!message.params || typeof message.params !== "object" || Array.isArray(message.params))) {
689
+ write(`${JSON.stringify({ jsonrpc: "2.0", id: null, error: { code: -32600, message: "Invalid Request" } })}
690
+ `);
691
+ return;
692
+ }
693
+ const response = await protocol.handle(message);
694
+ if (response)
695
+ write(`${JSON.stringify(response)}
696
+ `);
697
+ }).catch((error) => {
698
+ write(`${JSON.stringify({ jsonrpc: "2.0", id: null, error: {
699
+ code: error instanceof SyntaxError ? -32700 : -32603,
700
+ message: error instanceof SyntaxError ? "Parse error" : "Internal error"
701
+ } })}
702
+ `);
703
+ });
704
+ pending.add(task);
705
+ task.then(() => pending.delete(task));
706
+ };
707
+ try {
708
+ for (;; ) {
709
+ const result = await reader.read();
710
+ if (result.done)
711
+ break;
712
+ buffer += decoder.decode(result.value, { stream: true });
713
+ for (;; ) {
714
+ const newline = buffer.indexOf(`
715
+ `);
716
+ if (newline < 0)
717
+ break;
718
+ dispatch(buffer.slice(0, newline));
719
+ buffer = buffer.slice(newline + 1);
720
+ }
721
+ if (buffer.length > 4 * 1024 * 1024)
722
+ throw new Error("MCP request exceeds 4 MiB");
723
+ }
724
+ buffer += decoder.decode();
725
+ if (buffer.trim())
726
+ dispatch(buffer);
727
+ await Promise.resolve();
728
+ } finally {
729
+ protocol.abortAll();
730
+ await Promise.allSettled(pending);
731
+ reader.releaseLock();
732
+ }
733
+ }
734
+ if (import.meta.main)
735
+ await runMcpServer(Bun.stdin.stream(), (line) => {
736
+ process.stdout.write(line);
737
+ });
738
+ export {
739
+ runMcpServer,
740
+ createMcpProtocol
741
+ };