@mthanh/vespercli 2.0.0 → 2.1.0
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/README.md +36 -11
- package/assets/claude/.claude-plugin/marketplace.json +8 -8
- package/assets/claude/.claude-plugin/plugin.json +1 -1
- package/assets/claude/packs/backend/.claude-plugin/plugin.json +1 -1
- package/assets/claude/packs/docs/.claude-plugin/plugin.json +1 -1
- package/assets/claude/packs/frontend/.claude-plugin/plugin.json +1 -1
- package/assets/claude/packs/integrations/.claude-plugin/plugin.json +1 -1
- package/assets/claude/packs/playwright/.claude-plugin/plugin.json +1 -1
- package/assets/claude/packs/polyglot/.claude-plugin/plugin.json +1 -1
- package/assets/claude/packs/styles/.claude-plugin/plugin.json +1 -1
- package/assets/claude/vesper-orchestrator.md +1 -1
- package/assets/hooks/pre-commit +1 -1
- package/assets/opencode/vesper-plugin.js +120 -0
- package/dist/cli.min.js +119 -97
- package/dist/opencode-render.js +254 -0
- package/package.json +2 -2
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
// src/cli/utils/opencode-render.js
|
|
2
|
+
var CORE_PLUGIN_NAME = "vesper";
|
|
3
|
+
var OPENCODE_AGENT_DIR_RELATIVE = ".opencode/agents";
|
|
4
|
+
var MODEL_PROVIDER_PREFIX = "anthropic/";
|
|
5
|
+
var CLAUDE_TO_OPENCODE_TOOLS = Object.freeze({
|
|
6
|
+
Read: "read",
|
|
7
|
+
Write: "edit",
|
|
8
|
+
Edit: "edit",
|
|
9
|
+
MultiEdit: "edit",
|
|
10
|
+
NotebookEdit: "edit",
|
|
11
|
+
Bash: "bash",
|
|
12
|
+
Grep: "grep",
|
|
13
|
+
Glob: "glob",
|
|
14
|
+
LS: "list",
|
|
15
|
+
WebFetch: "webfetch",
|
|
16
|
+
WebSearch: "websearch",
|
|
17
|
+
Task: "task",
|
|
18
|
+
Agent: "task",
|
|
19
|
+
TodoWrite: "todowrite",
|
|
20
|
+
TodoRead: "todowrite",
|
|
21
|
+
AskUserQuestion: "question",
|
|
22
|
+
Skill: "skill"
|
|
23
|
+
});
|
|
24
|
+
var OPENCODE_GATED_PERMISSIONS = Object.freeze([
|
|
25
|
+
"read",
|
|
26
|
+
"edit",
|
|
27
|
+
"glob",
|
|
28
|
+
"grep",
|
|
29
|
+
"bash",
|
|
30
|
+
"task",
|
|
31
|
+
"todowrite",
|
|
32
|
+
"webfetch",
|
|
33
|
+
"websearch",
|
|
34
|
+
"question"
|
|
35
|
+
]);
|
|
36
|
+
function sanitizeMcpName(value) {
|
|
37
|
+
return String(value).replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
38
|
+
}
|
|
39
|
+
function agentId(name, plugin = CORE_PLUGIN_NAME) {
|
|
40
|
+
return `${plugin || CORE_PLUGIN_NAME}:${name}`;
|
|
41
|
+
}
|
|
42
|
+
function mapModel(model) {
|
|
43
|
+
const m = String(model ?? "").trim();
|
|
44
|
+
if (!m || m === "inherit")
|
|
45
|
+
return void 0;
|
|
46
|
+
if (m.includes("/"))
|
|
47
|
+
return m;
|
|
48
|
+
if (m.startsWith("claude-"))
|
|
49
|
+
return `${MODEL_PROVIDER_PREFIX}${m}`;
|
|
50
|
+
return void 0;
|
|
51
|
+
}
|
|
52
|
+
function normalizeToolList(tools) {
|
|
53
|
+
if (tools === null || tools === void 0)
|
|
54
|
+
return null;
|
|
55
|
+
const list = Array.isArray(tools) ? tools : String(tools).split(",");
|
|
56
|
+
const names = list.map((s) => String(s).trim()).filter(Boolean);
|
|
57
|
+
return names.length ? names : null;
|
|
58
|
+
}
|
|
59
|
+
function mapTools(tools) {
|
|
60
|
+
const names = normalizeToolList(tools);
|
|
61
|
+
if (names === null)
|
|
62
|
+
return { permission: {}, unmapped: [], restricted: false };
|
|
63
|
+
const allowed = /* @__PURE__ */ new Set();
|
|
64
|
+
const mcpGlobs = /* @__PURE__ */ new Set();
|
|
65
|
+
const unmapped = [];
|
|
66
|
+
for (const tool of names) {
|
|
67
|
+
const mcp = /^mcp__(.+?)__(.+)$/.exec(tool);
|
|
68
|
+
if (mcp) {
|
|
69
|
+
mcpGlobs.add(`${sanitizeMcpName(mcp[1])}_*`);
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
const key = CLAUDE_TO_OPENCODE_TOOLS[tool];
|
|
73
|
+
if (key)
|
|
74
|
+
allowed.add(key);
|
|
75
|
+
else
|
|
76
|
+
unmapped.push(tool);
|
|
77
|
+
}
|
|
78
|
+
const permission = {};
|
|
79
|
+
for (const key of OPENCODE_GATED_PERMISSIONS)
|
|
80
|
+
permission[key] = allowed.has(key) ? "allow" : "deny";
|
|
81
|
+
for (const key of allowed)
|
|
82
|
+
if (!(key in permission))
|
|
83
|
+
permission[key] = "allow";
|
|
84
|
+
for (const glob of [...mcpGlobs].sort())
|
|
85
|
+
permission[glob] = "allow";
|
|
86
|
+
return { permission, unmapped, restricted: true };
|
|
87
|
+
}
|
|
88
|
+
function splitFrontmatter(text) {
|
|
89
|
+
const src = String(text ?? "").replace(/\r\n/g, "\n");
|
|
90
|
+
const m = /^---\n([\s\S]*?)\n---(?:\n|$)/.exec(src);
|
|
91
|
+
if (!m)
|
|
92
|
+
return { raw: "", body: src };
|
|
93
|
+
return { raw: m[1], body: src.slice(m[0].length) };
|
|
94
|
+
}
|
|
95
|
+
function scalar(value) {
|
|
96
|
+
const s = String(value).trim();
|
|
97
|
+
if (s.startsWith('"') && s.endsWith('"') && s.length >= 2) {
|
|
98
|
+
try {
|
|
99
|
+
return JSON.parse(s);
|
|
100
|
+
} catch {
|
|
101
|
+
return s.slice(1, -1);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (s.startsWith("'") && s.endsWith("'") && s.length >= 2)
|
|
105
|
+
return s.slice(1, -1).replace(/''/g, "'");
|
|
106
|
+
return s;
|
|
107
|
+
}
|
|
108
|
+
function parseFrontmatter(raw) {
|
|
109
|
+
const out = {};
|
|
110
|
+
const lines = String(raw ?? "").replace(/\r\n/g, "\n").split("\n");
|
|
111
|
+
for (let i = 0; i < lines.length; i++) {
|
|
112
|
+
const line = lines[i];
|
|
113
|
+
if (!line.trim() || /^\s*#/.test(line))
|
|
114
|
+
continue;
|
|
115
|
+
const m = /^([A-Za-z0-9_-]+):\s*(.*)$/.exec(line);
|
|
116
|
+
if (!m)
|
|
117
|
+
continue;
|
|
118
|
+
const key = m[1];
|
|
119
|
+
const value = m[2].trim();
|
|
120
|
+
if (/^[>|]-?$/.test(value)) {
|
|
121
|
+
const block = [];
|
|
122
|
+
while (i + 1 < lines.length && (/^\s+\S/.test(lines[i + 1]) || lines[i + 1].trim() === ""))
|
|
123
|
+
block.push(lines[++i].trim());
|
|
124
|
+
while (block.length && block[block.length - 1] === "")
|
|
125
|
+
block.pop();
|
|
126
|
+
out[key] = value.startsWith(">") ? block.join(" ").replace(/\s+/g, " ") : block.join("\n");
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
if (value === "") {
|
|
130
|
+
const items = [];
|
|
131
|
+
while (i + 1 < lines.length && /^\s*-\s/.test(lines[i + 1]))
|
|
132
|
+
items.push(scalar(lines[++i].replace(/^\s*-\s*/, "")));
|
|
133
|
+
out[key] = items.length ? items : "";
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
if (value.startsWith("[") && value.endsWith("]")) {
|
|
137
|
+
out[key] = value.slice(1, -1).split(",").map((s) => scalar(s)).filter((s) => s !== "");
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
out[key] = scalar(value);
|
|
141
|
+
}
|
|
142
|
+
return out;
|
|
143
|
+
}
|
|
144
|
+
function parseAgentMarkdown(text) {
|
|
145
|
+
const { raw, body } = splitFrontmatter(text);
|
|
146
|
+
return { frontmatter: parseFrontmatter(raw), body };
|
|
147
|
+
}
|
|
148
|
+
function yamlKey(key) {
|
|
149
|
+
return /^[A-Za-z_][A-Za-z0-9_-]*$/.test(key) ? key : JSON.stringify(key);
|
|
150
|
+
}
|
|
151
|
+
function renderFrontmatter({ description, mode, model, permission }) {
|
|
152
|
+
const lines = ["---", `description: ${JSON.stringify(description)}`, `mode: ${mode}`];
|
|
153
|
+
if (model)
|
|
154
|
+
lines.push(`model: ${model}`);
|
|
155
|
+
if (permission && Object.keys(permission).length) {
|
|
156
|
+
lines.push("permission:");
|
|
157
|
+
for (const [key, action] of Object.entries(permission))
|
|
158
|
+
lines.push(` ${yamlKey(key)}: ${action}`);
|
|
159
|
+
}
|
|
160
|
+
lines.push("---");
|
|
161
|
+
return lines.join("\n");
|
|
162
|
+
}
|
|
163
|
+
function convertClaudeAgent({ name, frontmatter = {}, body = "", plugin = CORE_PLUGIN_NAME } = {}) {
|
|
164
|
+
if (!name || typeof name !== "string")
|
|
165
|
+
throw new Error("convertClaudeAgent: `name` is required");
|
|
166
|
+
const fm = frontmatter && typeof frontmatter === "object" ? frontmatter : {};
|
|
167
|
+
const description = String(fm.description ?? "").replace(/\s+/g, " ").trim() || `Vesper specialist: ${name}`;
|
|
168
|
+
const model = mapModel(fm.model);
|
|
169
|
+
const { permission, unmapped, restricted } = mapTools(fm.tools);
|
|
170
|
+
const text = String(body ?? "");
|
|
171
|
+
const config = { description, mode: "subagent" };
|
|
172
|
+
if (model)
|
|
173
|
+
config.model = model;
|
|
174
|
+
if (restricted)
|
|
175
|
+
config.permission = permission;
|
|
176
|
+
config.prompt = text;
|
|
177
|
+
const markdown = `${renderFrontmatter({ description, mode: "subagent", model, permission: restricted ? permission : void 0 })}
|
|
178
|
+
${text}`;
|
|
179
|
+
return { id: agentId(name, plugin), name, plugin: plugin || CORE_PLUGIN_NAME, markdown, config, unmapped };
|
|
180
|
+
}
|
|
181
|
+
var ORCHESTRATOR_RULES = Object.freeze([
|
|
182
|
+
[".claude/rules/vesper-orchestrator.md", "the orchestrator playbook (delegation law, purpose router, pipelines, degraded mode, host-mismatch warning)"],
|
|
183
|
+
[".claude/rules/vesper-orchestration.md", "the team roster + task-management config (the single authority for which `vesper:<name>` specialist fills which role)"],
|
|
184
|
+
[".claude/rules/vesper-context.md", "the slim project whitepaper (architecture, domain model, workflows, freshness)"]
|
|
185
|
+
]);
|
|
186
|
+
var DEFAULT_ORCHESTRATOR_DESCRIPTION = "Vesper super-orchestrator \u2014 the primary agent of a Vesper-managed project on OpenCode. Routes every request to the specialist team per the always-on orchestrator playbook and team roster in `.claude/rules/`.";
|
|
187
|
+
function renderOrchestratorAgent({ source, description } = {}) {
|
|
188
|
+
let desc = description;
|
|
189
|
+
if (!desc && source)
|
|
190
|
+
desc = parseAgentMarkdown(source).frontmatter.description;
|
|
191
|
+
desc = String(desc || DEFAULT_ORCHESTRATOR_DESCRIPTION).replace(/\s+/g, " ").trim();
|
|
192
|
+
const body = [
|
|
193
|
+
"# Vesper (Super Orchestrator) \u2014 OpenCode primary agent",
|
|
194
|
+
"",
|
|
195
|
+
"You are Vesper, the super-orchestrator for this project. Your complete operating playbook \u2014",
|
|
196
|
+
"the mandatory delegation law, the purpose router, every pipeline, the degraded-mode carve-out",
|
|
197
|
+
"and the host-mismatch warning \u2014 is the **always-on rules** `opencode.json` loads through",
|
|
198
|
+
"`instructions` into every session:",
|
|
199
|
+
"",
|
|
200
|
+
...ORCHESTRATOR_RULES.map(([path, what]) => `- \`${path}\` \u2014 ${what}.`),
|
|
201
|
+
"",
|
|
202
|
+
"This agent file is intentionally thin: it exists so `opencode.json` `default_agent:",
|
|
203
|
+
'"vesper-orchestrator"` resolves a real primary agent. All behavior lives in the rules above \u2014',
|
|
204
|
+
"follow them exactly. Do not restate or override them here.",
|
|
205
|
+
"",
|
|
206
|
+
"Specialists are delegated to by their namespaced names (`vesper:<name>` for the core catalog,",
|
|
207
|
+
"`vesper-<pack>:<name>` for an enabled pack), registered by the Vesper plugin",
|
|
208
|
+
"(`.opencode/plugins/vesper.js`) from the installed `@mthanh/vespercli` package \u2014 or, when",
|
|
209
|
+
'`vesper.agentsMode` is `"files"`, from the generated `.opencode/agents/<id>.md` files. The',
|
|
210
|
+
"committed maintenance skills resolve from `.claude/skills/` and are invoked unqualified",
|
|
211
|
+
"(`/reinit`, `/compact-knowledge`, `/revalidate-knowledge`, `/revalidate-team`).",
|
|
212
|
+
""
|
|
213
|
+
].join("\n");
|
|
214
|
+
return `${renderFrontmatter({ description: desc, mode: "primary" })}
|
|
215
|
+
|
|
216
|
+
${body}`;
|
|
217
|
+
}
|
|
218
|
+
var opencode_render_default = {
|
|
219
|
+
CORE_PLUGIN_NAME,
|
|
220
|
+
OPENCODE_AGENT_DIR_RELATIVE,
|
|
221
|
+
CLAUDE_TO_OPENCODE_TOOLS,
|
|
222
|
+
OPENCODE_GATED_PERMISSIONS,
|
|
223
|
+
ORCHESTRATOR_RULES,
|
|
224
|
+
agentId,
|
|
225
|
+
mapModel,
|
|
226
|
+
mapTools,
|
|
227
|
+
normalizeToolList,
|
|
228
|
+
sanitizeMcpName,
|
|
229
|
+
splitFrontmatter,
|
|
230
|
+
parseFrontmatter,
|
|
231
|
+
parseAgentMarkdown,
|
|
232
|
+
convertClaudeAgent,
|
|
233
|
+
renderOrchestratorAgent
|
|
234
|
+
};
|
|
235
|
+
export {
|
|
236
|
+
CLAUDE_TO_OPENCODE_TOOLS,
|
|
237
|
+
CORE_PLUGIN_NAME,
|
|
238
|
+
DEFAULT_ORCHESTRATOR_DESCRIPTION,
|
|
239
|
+
MODEL_PROVIDER_PREFIX,
|
|
240
|
+
OPENCODE_AGENT_DIR_RELATIVE,
|
|
241
|
+
OPENCODE_GATED_PERMISSIONS,
|
|
242
|
+
ORCHESTRATOR_RULES,
|
|
243
|
+
agentId,
|
|
244
|
+
convertClaudeAgent,
|
|
245
|
+
opencode_render_default as default,
|
|
246
|
+
mapModel,
|
|
247
|
+
mapTools,
|
|
248
|
+
normalizeToolList,
|
|
249
|
+
parseAgentMarkdown,
|
|
250
|
+
parseFrontmatter,
|
|
251
|
+
renderOrchestratorAgent,
|
|
252
|
+
sanitizeMcpName,
|
|
253
|
+
splitFrontmatter
|
|
254
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mthanh/vespercli",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "Vesper Agent Suite — CLI to manage AI agents, prompts, instructions, and skills for GitHub Copilot (VS Code) and Claude Code",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/cli/index.js",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"test": "vitest run",
|
|
30
30
|
"test:unit": "vitest run",
|
|
31
31
|
"test:watch": "vitest",
|
|
32
|
-
"build:cli": "esbuild src/cli/index.js --bundle --platform=node --format=esm --minify --outfile=dist/cli.min.js --external:@inquirer/prompts --external:commander --external:picocolors --external:strip-json-comments",
|
|
32
|
+
"build:cli": "esbuild src/cli/index.js --bundle --platform=node --format=esm --minify --outfile=dist/cli.min.js --external:@inquirer/prompts --external:commander --external:picocolors --external:strip-json-comments && esbuild src/cli/utils/opencode-render.js --bundle --platform=node --format=esm --outfile=dist/opencode-render.js",
|
|
33
33
|
"build:mcp": "cd src/mcp/vesper-indexing && npm install && npm run build",
|
|
34
34
|
"build": "npm run build:cli && npm run build:mcp",
|
|
35
35
|
"eval": "node scripts/eval-orchestrator.js",
|