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