blun-king-cli 9.1.561 → 9.1.562
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.
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { readFile, stat } from "node:fs/promises";
|
|
4
|
+
import { isAbsolute, relative, resolve } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
|
|
7
|
+
function assert(condition, message) {
|
|
8
|
+
if (!condition) throw new Error(message);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async function json(root, path) {
|
|
12
|
+
return JSON.parse(await readFile(resolve(root, path), "utf8"));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function expand(value, variable, root) {
|
|
16
|
+
assert(typeof value === "string" && value.length > 0, "host command values must be non-empty strings");
|
|
17
|
+
return value.split(`\${${variable}}`).join(root);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function validateEntrypoint(root, value) {
|
|
21
|
+
const target = resolve(value);
|
|
22
|
+
const within = relative(root, target);
|
|
23
|
+
assert(within && !within.startsWith("..") && !isAbsolute(within), "MCP entrypoint must remain inside the plugin");
|
|
24
|
+
const metadata = await stat(target);
|
|
25
|
+
assert(metadata.isFile(), "MCP entrypoint must be a regular file");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function validateHooks(root, hooks, { required, commandRoot }) {
|
|
29
|
+
assert(hooks && typeof hooks === "object" && !Array.isArray(hooks), "hook bundle is missing");
|
|
30
|
+
assert(Object.keys(hooks).every((key) => ["description", "hooks"].includes(key)), "hook bundle contains unsupported top-level metadata");
|
|
31
|
+
assert(hooks.description && typeof hooks.description === "string", "hook bundle description is missing");
|
|
32
|
+
for (const event of required) {
|
|
33
|
+
const registrations = hooks.hooks?.[event];
|
|
34
|
+
assert(Array.isArray(registrations) && registrations.length === 1, `${event} must have exactly one registration`);
|
|
35
|
+
assert(Array.isArray(registrations[0].hooks) && registrations[0].hooks.length === 1, `${event} must have exactly one hook command`);
|
|
36
|
+
const command = registrations[0].hooks[0];
|
|
37
|
+
assert(command.type === "command", `${event} must use a command hook`);
|
|
38
|
+
assert(command.command === `node "\${${commandRoot}}/src/hook.js"`, `${event} must use the bundled lifecycle adapter`);
|
|
39
|
+
assert(Number.isInteger(command.timeout) && command.timeout > 0 && command.timeout <= 15, `${event} timeout is unsafe`);
|
|
40
|
+
}
|
|
41
|
+
const extras = Object.keys(hooks.hooks || {}).filter((event) => !required.includes(event));
|
|
42
|
+
assert(extras.length === 0, `unknown hook events: ${extras.join(", ")}`);
|
|
43
|
+
return { events: required, commands: required.length, entrypoint: relative(root, resolve(root, "src/hook.js")) };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function validateBlunHooks(root, hooks) {
|
|
47
|
+
const required = [
|
|
48
|
+
"SessionStart", "UserPromptSubmit", "PreToolUse", "PostToolUse",
|
|
49
|
+
"PreCompact", "PostCompact", "Stop", "SubagentStop"
|
|
50
|
+
];
|
|
51
|
+
assert(Array.isArray(hooks), "BLUN hook bundle is missing");
|
|
52
|
+
assert(hooks.length === required.length, "BLUN must register exactly one command per lifecycle event");
|
|
53
|
+
assert(JSON.stringify(hooks.map(({ event }) => event)) === JSON.stringify(required),
|
|
54
|
+
"BLUN lifecycle events must remain complete and ordered");
|
|
55
|
+
for (const event of required) {
|
|
56
|
+
const registrations = hooks.filter((hook) => hook.event === event);
|
|
57
|
+
assert(registrations.length === 1, `${event} must have exactly one BLUN registration`);
|
|
58
|
+
const command = registrations[0];
|
|
59
|
+
assert(command.command === 'node "./src/hook.js"', `${event} must use the bundled BLUN lifecycle adapter`);
|
|
60
|
+
assert(Number.isInteger(command.timeout) && command.timeout > 0 && command.timeout <= 15, `${event} BLUN timeout is unsafe`);
|
|
61
|
+
}
|
|
62
|
+
return { events: required, commands: required.length, entrypoint: relative(root, resolve(root, "src/hook.js")) };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function initializeServer({ label, root, variable, server, version }) {
|
|
66
|
+
assert(server && typeof server === "object" && !Array.isArray(server), `${label} MCP registration is missing`);
|
|
67
|
+
assert(server.command === "node", `${label} MCP registration must use the Node.js runtime`);
|
|
68
|
+
assert(Array.isArray(server.args) && server.args.length === 1, `${label} MCP registration must name exactly one entrypoint`);
|
|
69
|
+
const command = process.execPath;
|
|
70
|
+
const args = server.args.map((value) => {
|
|
71
|
+
const expanded = expand(value, variable, root);
|
|
72
|
+
return isAbsolute(expanded) ? expanded : resolve(root, expanded);
|
|
73
|
+
});
|
|
74
|
+
assert(!args.some((value) => value.includes("${")), `${label} MCP registration contains an unresolved variable`);
|
|
75
|
+
await validateEntrypoint(root, args[0]);
|
|
76
|
+
|
|
77
|
+
return await new Promise((resolveResult, reject) => {
|
|
78
|
+
const child = spawn(command, args, {
|
|
79
|
+
cwd: root,
|
|
80
|
+
env: { ...process.env, CLAUDE_PLUGIN_ROOT: root, PLUGIN_ROOT: root },
|
|
81
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
82
|
+
});
|
|
83
|
+
let stdout = "";
|
|
84
|
+
let stderr = "";
|
|
85
|
+
let settled = false;
|
|
86
|
+
const finish = (error, value) => {
|
|
87
|
+
if (settled) return;
|
|
88
|
+
settled = true;
|
|
89
|
+
clearTimeout(timer);
|
|
90
|
+
const settle = () => {
|
|
91
|
+
if (error) reject(error);
|
|
92
|
+
else resolveResult(value);
|
|
93
|
+
};
|
|
94
|
+
if (child.exitCode !== null || child.signalCode !== null) return settle();
|
|
95
|
+
child.once("close", settle);
|
|
96
|
+
child.kill();
|
|
97
|
+
};
|
|
98
|
+
const timer = setTimeout(() => {
|
|
99
|
+
finish(new Error(`${label} MCP initialize timed out${stderr ? `: ${stderr.trim()}` : ""}`));
|
|
100
|
+
}, 3000);
|
|
101
|
+
child.stderr.setEncoding("utf8");
|
|
102
|
+
child.stderr.on("data", (chunk) => { stderr += chunk; });
|
|
103
|
+
child.stdout.setEncoding("utf8");
|
|
104
|
+
child.stdout.on("data", (chunk) => {
|
|
105
|
+
stdout += chunk;
|
|
106
|
+
const newline = stdout.indexOf("\n");
|
|
107
|
+
if (newline < 0) return;
|
|
108
|
+
try {
|
|
109
|
+
const message = JSON.parse(stdout.slice(0, newline));
|
|
110
|
+
assert(message.id === 1, `${label} MCP initialize returned the wrong request id`);
|
|
111
|
+
assert(message.result?.serverInfo?.name === "agent-spine", `${label} MCP initialize returned the wrong server identity`);
|
|
112
|
+
assert(message.result?.serverInfo?.version === version, `${label} MCP initialize returned a stale server version`);
|
|
113
|
+
finish(null, { label, server: message.result.serverInfo.name, entrypoint: relative(root, args[0]) });
|
|
114
|
+
} catch (error) {
|
|
115
|
+
finish(error);
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
child.once("error", (error) => finish(error));
|
|
119
|
+
child.once("close", (code) => {
|
|
120
|
+
if (!settled) finish(new Error(`${label} MCP server exited with ${code}${stderr ? `: ${stderr.trim()}` : ""}`));
|
|
121
|
+
});
|
|
122
|
+
// A real MCP host keeps stdio open after initialization. Closing stdin
|
|
123
|
+
// here let macOS and Windows terminate the server before its queued reply.
|
|
124
|
+
child.stdin.write(`${JSON.stringify({
|
|
125
|
+
jsonrpc: "2.0", id: 1, method: "initialize", params: { protocolVersion: "2025-06-18" }
|
|
126
|
+
})}\n`);
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export async function checkHosts(root = process.cwd()) {
|
|
131
|
+
root = resolve(root);
|
|
132
|
+
const [pkg, blunManifest, claudeManifest, claudeMcp, codexManifest, claudeHooks, codexHooks, hookVersion] = await Promise.all([
|
|
133
|
+
json(root, "package.json"),
|
|
134
|
+
json(root, "blun.plugin.json"),
|
|
135
|
+
json(root, ".claude-plugin/plugin.json"),
|
|
136
|
+
json(root, ".mcp.json"),
|
|
137
|
+
json(root, ".codex-plugin/plugin.json"),
|
|
138
|
+
json(root, "hooks/hooks.json"),
|
|
139
|
+
json(root, "hooks/codex.json"),
|
|
140
|
+
json(root, "hooks/version.json")
|
|
141
|
+
]);
|
|
142
|
+
assert(blunManifest.version === pkg.version && claudeManifest.version === pkg.version && codexManifest.version === pkg.version,
|
|
143
|
+
"host manifests must use the package cache version");
|
|
144
|
+
assert(hookVersion.schema === "agentspine.hook-bundle/v1" && hookVersion.version === pkg.version,
|
|
145
|
+
"hook bundle version must match the package cache version");
|
|
146
|
+
assert(hookVersion.contract === "agentspine.preflight/v2", "hook bundle preflight contract is missing");
|
|
147
|
+
assert(pkg.bin?.["agentspine-worker"] === "./src/worker.js", "package must register exactly one gateway worker entrypoint");
|
|
148
|
+
await validateEntrypoint(root, resolve(root, pkg.bin["agentspine-worker"]));
|
|
149
|
+
assert(claudeManifest.mcpServers === "./.mcp.json", "Claude manifest must explicitly reference ./.mcp.json");
|
|
150
|
+
assert(claudeManifest.hooks === undefined, "default hooks/hooks.json must not also be registered through a supplemental manifest path");
|
|
151
|
+
assert(codexManifest.hooks === undefined, "Codex manifest must omit unsupported hook registration fields");
|
|
152
|
+
assert(claudeMcp.mcpServers && Object.keys(claudeMcp.mcpServers).length === 1, "Claude MCP file must contain one mcpServers registration");
|
|
153
|
+
assert(codexManifest.mcpServers && Object.keys(codexManifest.mcpServers).length === 1, "Codex manifest must contain one MCP registration");
|
|
154
|
+
const commonEvents = ["SessionStart", "UserPromptSubmit", "PreToolUse", "PostToolUse", "PreCompact", "PostCompact", "Stop", "SubagentStop"];
|
|
155
|
+
const claudeHookInventory = validateHooks(root, claudeHooks, {
|
|
156
|
+
required: [...commonEvents, "InstructionsLoaded"], commandRoot: "CLAUDE_PLUGIN_ROOT"
|
|
157
|
+
});
|
|
158
|
+
const codexHookInventory = validateHooks(root, codexHooks, { required: commonEvents, commandRoot: "PLUGIN_ROOT" });
|
|
159
|
+
const blunHookInventory = validateBlunHooks(root, blunManifest.hooks);
|
|
160
|
+
const registrations = await Promise.all([
|
|
161
|
+
initializeServer({ label: "blun", root, variable: "BLUN_PLUGIN_ROOT", server: blunManifest.mcpServers["agent-spine"], version: pkg.version }),
|
|
162
|
+
initializeServer({ label: "claude", root, variable: "CLAUDE_PLUGIN_ROOT", server: claudeMcp.mcpServers["agent-spine"], version: pkg.version }),
|
|
163
|
+
initializeServer({ label: "codex", root, variable: "PLUGIN_ROOT", server: codexManifest.mcpServers["agent-spine"], version: pkg.version })
|
|
164
|
+
]);
|
|
165
|
+
return {
|
|
166
|
+
ok: true, root, version: pkg.version, registrations,
|
|
167
|
+
hooks: { blun: blunHookInventory, claude: claudeHookInventory, codex: codexHookInventory },
|
|
168
|
+
hookDiscovery: {
|
|
169
|
+
blun: "plugin-manifest", claude: "default-hooks-directory", codex: "bundled-host-adapter",
|
|
170
|
+
trust: "host-user-required", liveTrustVerified: false
|
|
171
|
+
},
|
|
172
|
+
worker: { entrypoint: pkg.bin["agentspine-worker"], setsPerInstall: 1 },
|
|
173
|
+
exactlyOnce: { mcpServersPerHost: 1, hookSetsPerHost: 1, workerSetsPerInstall: 1 },
|
|
174
|
+
authority: "registration-check-only"
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async function main() {
|
|
179
|
+
const args = process.argv.slice(2);
|
|
180
|
+
let root = process.cwd();
|
|
181
|
+
let pretty = false;
|
|
182
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
183
|
+
if (args[index] === "--root") root = args[++index];
|
|
184
|
+
else if (args[index] === "--json") pretty = true;
|
|
185
|
+
else throw new Error(`unknown host-check argument: ${args[index]}`);
|
|
186
|
+
}
|
|
187
|
+
process.stdout.write(`${JSON.stringify(await checkHosts(root), null, pretty ? 2 : 0)}\n`);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (process.argv[1] && resolve(process.argv[1]) === resolve(fileURLToPath(import.meta.url))) {
|
|
191
|
+
main().catch((error) => {
|
|
192
|
+
process.stderr.write(`AgentSpine host check failed: ${error.message}\n`);
|
|
193
|
+
process.exitCode = 1;
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "blun-king-cli",
|
|
3
|
-
"version": "9.1.
|
|
3
|
+
"version": "9.1.562",
|
|
4
4
|
"description": "BLUN CLI - your own AI agent with a Telegram channel. Get it done. With BLUN.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"bin": {
|
|
@@ -45,6 +45,7 @@
|
|
|
45
45
|
"agent-spine-plugin/hooks/",
|
|
46
46
|
"agent-spine-plugin/LICENSE",
|
|
47
47
|
"agent-spine-plugin/package.json",
|
|
48
|
+
"agent-spine-plugin/scripts/check-hosts.js",
|
|
48
49
|
"agent-spine-plugin/skill/",
|
|
49
50
|
"agent-spine-plugin/skills/",
|
|
50
51
|
"agent-spine-plugin/src/",
|