plugnz 0.0.15 → 0.0.16
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 +2 -0
- package/dist/hook-runtime.mjs +288 -0
- package/dist/plugnz.mjs +834 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -18,6 +18,8 @@ re-`add` from the source recorded in `state.json`, plus the pins that install
|
|
|
18
18
|
carried. Both refuse rather than guess, and neither will touch a plugin this
|
|
19
19
|
tool did not install. Details: `docs/pin-and-update.md`.
|
|
20
20
|
|
|
21
|
+
**Portable context hooks:** `plugnz hooks build <plugin-dir> --target <host> --out <dir>` compiles session-start, stop, and pre/post-tool handlers for Claude Code, Codex, OMP, Pi v1, OpenCode, and ZCode. Handlers receive normalized JSON and return context; native adapters expose their delivery and continuation limits through `plugnz hooks targets`. See [the contract, usage, and native map](docs/hooks.md).
|
|
22
|
+
|
|
21
23
|
Why another tool: `npx plugins` is closed-source and its Cursor target does not install into Cursor; nothing in the ecosystem checks that an installed plugin still *works*. See `docs/research/`.
|
|
22
24
|
|
|
23
25
|
The published CLI runs under Node.js 22 or newer; `npx` needs npm, not Bun. `bunx` also works. Bun is used for development and building. `add` accepts a local plugin or marketplace directory as well as a Git URL. Native plugin targets currently include Claude Code, Codex, Kimi, Cursor, OMP, Hermes, Grok, DCode, and ZCode CLI. Run `plugnz targets --all` to see supported and unsupported capabilities for each host; standalone skill distribution is outside this CLI's scope.
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
// src/hooks.ts
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
4
|
+
import { isAbsolute, join, resolve } from "node:path";
|
|
5
|
+
var HOOK_EVENTS = ["session.start", "turn.stop", "tool.before", "tool.after"];
|
|
6
|
+
var HOOK_CONFIG_FILE = "plugnz.hooks.json";
|
|
7
|
+
var DEFAULT_HOOK_TIMEOUT_MS = 1e4;
|
|
8
|
+
var MAX_HOOK_TIMEOUT_MS = 60000;
|
|
9
|
+
var MAX_HOOK_OUTPUT_BYTES = 1048576;
|
|
10
|
+
function record(value, label) {
|
|
11
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
12
|
+
throw new Error(label + " must be an object");
|
|
13
|
+
return value;
|
|
14
|
+
}
|
|
15
|
+
function keys(value, allowed, label) {
|
|
16
|
+
const unknown = Object.keys(value).find((key) => !allowed.includes(key));
|
|
17
|
+
if (unknown !== undefined)
|
|
18
|
+
throw new Error(label + ': unsupported field "' + unknown + '"');
|
|
19
|
+
}
|
|
20
|
+
function text(value, label, nonblank = true) {
|
|
21
|
+
if (typeof value !== "string" || value.includes("\x00") || nonblank && !value.trim())
|
|
22
|
+
throw new Error(label + " must be a nonblank string without NUL");
|
|
23
|
+
return value;
|
|
24
|
+
}
|
|
25
|
+
function optionalText(value, label) {
|
|
26
|
+
return value === undefined ? undefined : text(value, label, false);
|
|
27
|
+
}
|
|
28
|
+
function isHookEvent(value) {
|
|
29
|
+
return typeof value === "string" && HOOK_EVENTS.includes(value);
|
|
30
|
+
}
|
|
31
|
+
function validateHookConfig(value) {
|
|
32
|
+
const root = record(value, "Hook configuration");
|
|
33
|
+
keys(root, ["version", "hooks"], "Hook configuration");
|
|
34
|
+
if (root["version"] !== 1)
|
|
35
|
+
throw new Error("Hook configuration version must be 1");
|
|
36
|
+
const sourceHooks = record(root["hooks"], "hooks");
|
|
37
|
+
keys(sourceHooks, HOOK_EVENTS, "hooks");
|
|
38
|
+
const hooks = {};
|
|
39
|
+
for (const event of HOOK_EVENTS) {
|
|
40
|
+
const handlers = sourceHooks[event];
|
|
41
|
+
if (handlers === undefined)
|
|
42
|
+
continue;
|
|
43
|
+
if (!Array.isArray(handlers))
|
|
44
|
+
throw new Error(event + " handlers must be an array");
|
|
45
|
+
hooks[event] = handlers.map((value, index) => {
|
|
46
|
+
const label = event + "[" + index + "]";
|
|
47
|
+
const handler = record(value, label);
|
|
48
|
+
keys(handler, ["command", "tools", "timeoutMs"], label);
|
|
49
|
+
const command = handler["command"];
|
|
50
|
+
if (!Array.isArray(command) || command.length === 0)
|
|
51
|
+
throw new Error(label + ".command must be a nonempty argv array");
|
|
52
|
+
const argv = command.map((argument, i) => text(argument, label + ".command[" + i + "]", i === 0));
|
|
53
|
+
const filter = handler["tools"];
|
|
54
|
+
let tools;
|
|
55
|
+
if (filter !== undefined) {
|
|
56
|
+
if (event !== "tool.before" && event !== "tool.after")
|
|
57
|
+
throw new Error(label + ".tools is only valid for tool events");
|
|
58
|
+
if (!Array.isArray(filter))
|
|
59
|
+
throw new Error(label + ".tools must be an array of exact tool names");
|
|
60
|
+
tools = filter.map((name, i) => text(name, label + ".tools[" + i + "]"));
|
|
61
|
+
}
|
|
62
|
+
const timeout = handler["timeoutMs"];
|
|
63
|
+
if (timeout !== undefined && (typeof timeout !== "number" || !Number.isInteger(timeout) || timeout < 1 || timeout > MAX_HOOK_TIMEOUT_MS)) {
|
|
64
|
+
throw new Error(label + ".timeoutMs must be an integer from 1 to " + MAX_HOOK_TIMEOUT_MS);
|
|
65
|
+
}
|
|
66
|
+
return { command: argv, ...tools === undefined ? {} : { tools }, ...timeout === undefined ? {} : { timeoutMs: timeout } };
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
return { version: 1, hooks };
|
|
70
|
+
}
|
|
71
|
+
function readHookConfig(pluginRoot) {
|
|
72
|
+
const path = join(pluginRoot, HOOK_CONFIG_FILE);
|
|
73
|
+
if (!existsSync(path))
|
|
74
|
+
return;
|
|
75
|
+
let value;
|
|
76
|
+
try {
|
|
77
|
+
value = JSON.parse(readFileSync(path, "utf8"));
|
|
78
|
+
} catch (error) {
|
|
79
|
+
throw new Error("Cannot read " + HOOK_CONFIG_FILE + ": " + error.message);
|
|
80
|
+
}
|
|
81
|
+
return validateHookConfig(value);
|
|
82
|
+
}
|
|
83
|
+
function validateHookInput(value) {
|
|
84
|
+
const input = record(value, "Hook input");
|
|
85
|
+
keys(input, ["version", "event", "harness", "sessionId", "cwd", "reason", "tool", "stop"], "Hook input");
|
|
86
|
+
if (input["version"] !== 1)
|
|
87
|
+
throw new Error("Hook input version must be 1");
|
|
88
|
+
const event = input["event"];
|
|
89
|
+
if (!isHookEvent(event))
|
|
90
|
+
throw new Error("Hook input event is unsupported");
|
|
91
|
+
const harness = text(input["harness"], "Hook input harness");
|
|
92
|
+
const cwd = text(input["cwd"], "Hook input cwd");
|
|
93
|
+
if (!isAbsolute(cwd))
|
|
94
|
+
throw new Error("Hook input cwd must be absolute");
|
|
95
|
+
const sessionId = optionalText(input["sessionId"], "Hook input sessionId");
|
|
96
|
+
const reason = optionalText(input["reason"], "Hook input reason");
|
|
97
|
+
let tool;
|
|
98
|
+
if (input["tool"] !== undefined) {
|
|
99
|
+
const source = record(input["tool"], "Hook input tool");
|
|
100
|
+
keys(source, ["name", "id", "input", "output", "isError"], "Hook input tool");
|
|
101
|
+
const name = text(source["name"], "Hook input tool.name");
|
|
102
|
+
const id = optionalText(source["id"], "Hook input tool.id");
|
|
103
|
+
if (source["isError"] !== undefined && typeof source["isError"] !== "boolean")
|
|
104
|
+
throw new Error("Hook input tool.isError must be boolean");
|
|
105
|
+
tool = {
|
|
106
|
+
name,
|
|
107
|
+
...id === undefined ? {} : { id },
|
|
108
|
+
...source["input"] === undefined ? {} : { input: source["input"] },
|
|
109
|
+
...source["output"] === undefined ? {} : { output: source["output"] },
|
|
110
|
+
...source["isError"] === undefined ? {} : { isError: source["isError"] }
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
if ((event === "tool.before" || event === "tool.after") && tool === undefined)
|
|
114
|
+
throw new Error(event + " requires tool.name");
|
|
115
|
+
let stop;
|
|
116
|
+
if (input["stop"] !== undefined) {
|
|
117
|
+
const source = record(input["stop"], "Hook input stop");
|
|
118
|
+
keys(source, ["active"], "Hook input stop");
|
|
119
|
+
if (typeof source["active"] !== "boolean")
|
|
120
|
+
throw new Error("Hook input stop.active must be boolean");
|
|
121
|
+
stop = { active: source["active"] };
|
|
122
|
+
}
|
|
123
|
+
if (event === "turn.stop" && stop === undefined)
|
|
124
|
+
throw new Error("turn.stop requires stop.active");
|
|
125
|
+
return {
|
|
126
|
+
version: 1,
|
|
127
|
+
event,
|
|
128
|
+
harness,
|
|
129
|
+
cwd,
|
|
130
|
+
...sessionId === undefined ? {} : { sessionId },
|
|
131
|
+
...reason === undefined ? {} : { reason },
|
|
132
|
+
...tool === undefined ? {} : { tool },
|
|
133
|
+
...stop === undefined ? {} : { stop }
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
function validateHookResult(value, input) {
|
|
137
|
+
const result = record(value, "Hook result");
|
|
138
|
+
keys(result, ["context", "continue"], "Hook result");
|
|
139
|
+
if (result["context"] !== undefined && typeof result["context"] !== "string")
|
|
140
|
+
throw new Error("Hook result context must be a string");
|
|
141
|
+
if (result["continue"] !== undefined && typeof result["continue"] !== "boolean")
|
|
142
|
+
throw new Error("Hook result continue must be boolean");
|
|
143
|
+
const context = result["context"];
|
|
144
|
+
const shouldContinue = result["continue"];
|
|
145
|
+
const hasContext = context !== undefined && context.trim().length > 0;
|
|
146
|
+
if (shouldContinue === true) {
|
|
147
|
+
if (input.event !== "turn.stop")
|
|
148
|
+
throw new Error("continue:true is only valid for turn.stop");
|
|
149
|
+
if (!hasContext)
|
|
150
|
+
throw new Error("turn.stop continuation requires nonblank context");
|
|
151
|
+
if (input.stop?.active)
|
|
152
|
+
throw new Error("turn.stop cannot continue while stop.active is true");
|
|
153
|
+
}
|
|
154
|
+
if (input.event === "turn.stop" && hasContext && shouldContinue !== true) {
|
|
155
|
+
throw new Error("turn.stop context requires continue:true; return {} to let the turn settle");
|
|
156
|
+
}
|
|
157
|
+
return { ...hasContext ? { context } : {}, ...shouldContinue === undefined ? {} : { continue: shouldContinue } };
|
|
158
|
+
}
|
|
159
|
+
async function runHooks(configValue, inputValue, pluginRoot) {
|
|
160
|
+
const config = validateHookConfig(configValue);
|
|
161
|
+
const input = validateHookInput(inputValue);
|
|
162
|
+
const root = resolve(text(pluginRoot, "Plugin root"));
|
|
163
|
+
const contexts = [];
|
|
164
|
+
let requestedContinuation;
|
|
165
|
+
const handlers = config.hooks[input.event] ?? [];
|
|
166
|
+
for (let index = 0;index < handlers.length; index++) {
|
|
167
|
+
const handler = handlers[index];
|
|
168
|
+
if (handler.tools !== undefined && !handler.tools.includes(input.tool?.name ?? ""))
|
|
169
|
+
continue;
|
|
170
|
+
const label = input.event + "[" + index + "]";
|
|
171
|
+
try {
|
|
172
|
+
const stdout = await executeHandler(handler, input, root);
|
|
173
|
+
let raw = {};
|
|
174
|
+
if (stdout.trim()) {
|
|
175
|
+
try {
|
|
176
|
+
raw = JSON.parse(stdout);
|
|
177
|
+
} catch {
|
|
178
|
+
throw new Error("stdout must contain one JSON result object");
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
const result = validateHookResult(raw, input);
|
|
182
|
+
if (result.context !== undefined)
|
|
183
|
+
contexts.push(result.context);
|
|
184
|
+
if (result.continue !== undefined)
|
|
185
|
+
requestedContinuation = requestedContinuation === true || result.continue;
|
|
186
|
+
} catch (error) {
|
|
187
|
+
throw new Error(label + ": " + (error instanceof Error ? error.message : String(error)));
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return {
|
|
191
|
+
...contexts.length === 0 ? {} : { context: contexts.join(`
|
|
192
|
+
|
|
193
|
+
`) },
|
|
194
|
+
...requestedContinuation === undefined ? {} : { continue: requestedContinuation }
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
function decode(chunks) {
|
|
198
|
+
const result = new Uint8Array(chunks.reduce((length, chunk) => length + chunk.length, 0));
|
|
199
|
+
let offset = 0;
|
|
200
|
+
for (const chunk of chunks) {
|
|
201
|
+
result.set(chunk, offset);
|
|
202
|
+
offset += chunk.length;
|
|
203
|
+
}
|
|
204
|
+
return new TextDecoder().decode(result);
|
|
205
|
+
}
|
|
206
|
+
function executeHandler(handler, input, pluginRoot) {
|
|
207
|
+
const token = "$" + "{PLUGIN_ROOT}";
|
|
208
|
+
const argv = handler.command.map((argument) => argument.replaceAll(token, pluginRoot));
|
|
209
|
+
const serialized = JSON.stringify(input) + `
|
|
210
|
+
`;
|
|
211
|
+
const timeoutMs = handler.timeoutMs ?? DEFAULT_HOOK_TIMEOUT_MS;
|
|
212
|
+
return new Promise((resolveResult, reject) => {
|
|
213
|
+
const child = spawn(argv[0], argv.slice(1), {
|
|
214
|
+
cwd: input.cwd,
|
|
215
|
+
env: process.env,
|
|
216
|
+
shell: false,
|
|
217
|
+
detached: process.platform !== "win32",
|
|
218
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
219
|
+
});
|
|
220
|
+
const stdout = [];
|
|
221
|
+
const stderr = [];
|
|
222
|
+
let bytes = 0;
|
|
223
|
+
let settled = false;
|
|
224
|
+
let timer;
|
|
225
|
+
const kill = () => {
|
|
226
|
+
if (process.platform !== "win32" && child.pid !== undefined) {
|
|
227
|
+
try {
|
|
228
|
+
process.kill(-child.pid, "SIGKILL");
|
|
229
|
+
return;
|
|
230
|
+
} catch {}
|
|
231
|
+
}
|
|
232
|
+
child.kill("SIGKILL");
|
|
233
|
+
};
|
|
234
|
+
const finish = (error) => {
|
|
235
|
+
if (settled)
|
|
236
|
+
return;
|
|
237
|
+
settled = true;
|
|
238
|
+
clearTimeout(timer);
|
|
239
|
+
if (error !== undefined) {
|
|
240
|
+
child.stdin.destroy();
|
|
241
|
+
child.stdout.destroy();
|
|
242
|
+
child.stderr.destroy();
|
|
243
|
+
reject(error);
|
|
244
|
+
} else
|
|
245
|
+
resolveResult(decode(stdout));
|
|
246
|
+
};
|
|
247
|
+
const collect = (chunks) => (chunk) => {
|
|
248
|
+
if (settled)
|
|
249
|
+
return;
|
|
250
|
+
bytes += chunk.length;
|
|
251
|
+
if (bytes > MAX_HOOK_OUTPUT_BYTES) {
|
|
252
|
+
kill();
|
|
253
|
+
finish(new Error("handler output exceeded " + MAX_HOOK_OUTPUT_BYTES + " bytes"));
|
|
254
|
+
} else
|
|
255
|
+
chunks.push(chunk);
|
|
256
|
+
};
|
|
257
|
+
child.stdout.on("data", collect(stdout));
|
|
258
|
+
child.stderr.on("data", collect(stderr));
|
|
259
|
+
child.stdin.on("error", () => {});
|
|
260
|
+
child.once("error", (error) => finish(error));
|
|
261
|
+
child.once("close", (code, signal) => {
|
|
262
|
+
if (settled)
|
|
263
|
+
return;
|
|
264
|
+
if (code !== 0) {
|
|
265
|
+
const diagnostic = decode(stderr).trim().slice(0, 4096);
|
|
266
|
+
finish(new Error("handler exited " + (signal ?? String(code)) + (diagnostic ? ": " + diagnostic : "")));
|
|
267
|
+
} else
|
|
268
|
+
finish();
|
|
269
|
+
});
|
|
270
|
+
timer = setTimeout(() => {
|
|
271
|
+
kill();
|
|
272
|
+
finish(new Error("handler timed out after " + timeoutMs + "ms"));
|
|
273
|
+
}, timeoutMs);
|
|
274
|
+
child.stdin.end(serialized);
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
export {
|
|
278
|
+
DEFAULT_HOOK_TIMEOUT_MS,
|
|
279
|
+
HOOK_CONFIG_FILE,
|
|
280
|
+
HOOK_EVENTS,
|
|
281
|
+
MAX_HOOK_OUTPUT_BYTES,
|
|
282
|
+
MAX_HOOK_TIMEOUT_MS,
|
|
283
|
+
readHookConfig,
|
|
284
|
+
runHooks,
|
|
285
|
+
validateHookConfig,
|
|
286
|
+
validateHookInput,
|
|
287
|
+
validateHookResult
|
|
288
|
+
};
|
package/dist/plugnz.mjs
CHANGED
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
|
|
4
|
+
var __esm = (fn, res, err) => () => {
|
|
5
|
+
if (fn)
|
|
6
|
+
try {
|
|
7
|
+
res = fn(fn = 0);
|
|
8
|
+
} catch (e) {
|
|
9
|
+
err = [e];
|
|
10
|
+
}
|
|
11
|
+
if (err)
|
|
12
|
+
throw err[0];
|
|
13
|
+
return res;
|
|
14
|
+
};
|
|
4
15
|
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
5
16
|
|
|
6
17
|
// node_modules/yaml/dist/nodes/identity.js
|
|
@@ -6945,8 +6956,816 @@ var require_public_api = __commonJS(function(exports) {
|
|
|
6945
6956
|
exports.stringify = stringify;
|
|
6946
6957
|
});
|
|
6947
6958
|
|
|
6959
|
+
// src/hooks.ts
|
|
6960
|
+
import { existsSync as existsSync29, readFileSync as readFileSync31 } from "node:fs";
|
|
6961
|
+
import { isAbsolute as isAbsolute13, join as join39, resolve as resolve26 } from "node:path";
|
|
6962
|
+
function record3(value, label) {
|
|
6963
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
6964
|
+
throw new Error(label + " must be an object");
|
|
6965
|
+
return value;
|
|
6966
|
+
}
|
|
6967
|
+
function keys2(value, allowed, label) {
|
|
6968
|
+
const unknown = Object.keys(value).find((key) => !allowed.includes(key));
|
|
6969
|
+
if (unknown !== undefined)
|
|
6970
|
+
throw new Error(label + ': unsupported field "' + unknown + '"');
|
|
6971
|
+
}
|
|
6972
|
+
function text3(value, label, nonblank = true) {
|
|
6973
|
+
if (typeof value !== "string" || value.includes("\x00") || nonblank && !value.trim())
|
|
6974
|
+
throw new Error(label + " must be a nonblank string without NUL");
|
|
6975
|
+
return value;
|
|
6976
|
+
}
|
|
6977
|
+
function validateHookConfig(value) {
|
|
6978
|
+
const root = record3(value, "Hook configuration");
|
|
6979
|
+
keys2(root, ["version", "hooks"], "Hook configuration");
|
|
6980
|
+
if (root["version"] !== 1)
|
|
6981
|
+
throw new Error("Hook configuration version must be 1");
|
|
6982
|
+
const sourceHooks = record3(root["hooks"], "hooks");
|
|
6983
|
+
keys2(sourceHooks, HOOK_EVENTS, "hooks");
|
|
6984
|
+
const hooks = {};
|
|
6985
|
+
for (const event of HOOK_EVENTS) {
|
|
6986
|
+
const handlers = sourceHooks[event];
|
|
6987
|
+
if (handlers === undefined)
|
|
6988
|
+
continue;
|
|
6989
|
+
if (!Array.isArray(handlers))
|
|
6990
|
+
throw new Error(event + " handlers must be an array");
|
|
6991
|
+
hooks[event] = handlers.map((value, index) => {
|
|
6992
|
+
const label = event + "[" + index + "]";
|
|
6993
|
+
const handler = record3(value, label);
|
|
6994
|
+
keys2(handler, ["command", "tools", "timeoutMs"], label);
|
|
6995
|
+
const command = handler["command"];
|
|
6996
|
+
if (!Array.isArray(command) || command.length === 0)
|
|
6997
|
+
throw new Error(label + ".command must be a nonempty argv array");
|
|
6998
|
+
const argv = command.map((argument, i) => text3(argument, label + ".command[" + i + "]", i === 0));
|
|
6999
|
+
const filter = handler["tools"];
|
|
7000
|
+
let tools;
|
|
7001
|
+
if (filter !== undefined) {
|
|
7002
|
+
if (event !== "tool.before" && event !== "tool.after")
|
|
7003
|
+
throw new Error(label + ".tools is only valid for tool events");
|
|
7004
|
+
if (!Array.isArray(filter))
|
|
7005
|
+
throw new Error(label + ".tools must be an array of exact tool names");
|
|
7006
|
+
tools = filter.map((name, i) => text3(name, label + ".tools[" + i + "]"));
|
|
7007
|
+
}
|
|
7008
|
+
const timeout = handler["timeoutMs"];
|
|
7009
|
+
if (timeout !== undefined && (typeof timeout !== "number" || !Number.isInteger(timeout) || timeout < 1 || timeout > MAX_HOOK_TIMEOUT_MS)) {
|
|
7010
|
+
throw new Error(label + ".timeoutMs must be an integer from 1 to " + MAX_HOOK_TIMEOUT_MS);
|
|
7011
|
+
}
|
|
7012
|
+
return { command: argv, ...tools === undefined ? {} : { tools }, ...timeout === undefined ? {} : { timeoutMs: timeout } };
|
|
7013
|
+
});
|
|
7014
|
+
}
|
|
7015
|
+
return { version: 1, hooks };
|
|
7016
|
+
}
|
|
7017
|
+
function readHookConfig(pluginRoot) {
|
|
7018
|
+
const path = join39(pluginRoot, HOOK_CONFIG_FILE);
|
|
7019
|
+
if (!existsSync29(path))
|
|
7020
|
+
return;
|
|
7021
|
+
let value;
|
|
7022
|
+
try {
|
|
7023
|
+
value = JSON.parse(readFileSync31(path, "utf8"));
|
|
7024
|
+
} catch (error) {
|
|
7025
|
+
throw new Error("Cannot read " + HOOK_CONFIG_FILE + ": " + error.message);
|
|
7026
|
+
}
|
|
7027
|
+
return validateHookConfig(value);
|
|
7028
|
+
}
|
|
7029
|
+
var HOOK_EVENTS, HOOK_CONFIG_FILE = "plugnz.hooks.json", DEFAULT_HOOK_TIMEOUT_MS = 1e4, MAX_HOOK_TIMEOUT_MS = 60000;
|
|
7030
|
+
var init_hooks = __esm(() => {
|
|
7031
|
+
HOOK_EVENTS = ["session.start", "turn.stop", "tool.before", "tool.after"];
|
|
7032
|
+
});
|
|
7033
|
+
|
|
7034
|
+
// src/hosts/command-hooks.ts
|
|
7035
|
+
function commandHookAdapter(options) {
|
|
7036
|
+
return {
|
|
7037
|
+
id: options.id,
|
|
7038
|
+
baselineVersion: options.baselineVersion,
|
|
7039
|
+
capabilities: {
|
|
7040
|
+
"session.start": { nativeEvent: options.nativeEvents["session.start"].join(" / "), context: true, continue: false, delivery: options.contextDelivery },
|
|
7041
|
+
"tool.before": { nativeEvent: options.nativeEvents["tool.before"].join(" / "), context: true, continue: false, delivery: options.contextDelivery },
|
|
7042
|
+
"tool.after": { nativeEvent: options.nativeEvents["tool.after"].join(" / "), context: true, continue: false, delivery: options.contextDelivery },
|
|
7043
|
+
"turn.stop": { nativeEvent: options.nativeEvents["turn.stop"].join(" / "), context: true, continue: true, delivery: "with explicit continuation only" }
|
|
7044
|
+
},
|
|
7045
|
+
emit(config) {
|
|
7046
|
+
const wrapperPath = `hooks/plugnz-${options.id}.mjs`;
|
|
7047
|
+
const native = {};
|
|
7048
|
+
const eventMap = {};
|
|
7049
|
+
for (const [event, handlers] of Object.entries(config.hooks)) {
|
|
7050
|
+
if (!handlers?.length)
|
|
7051
|
+
continue;
|
|
7052
|
+
for (const name of options.nativeEvents[event]) {
|
|
7053
|
+
eventMap[name] = event;
|
|
7054
|
+
const seconds = Math.ceil((handlers.reduce((sum, handler) => sum + (handler.timeoutMs ?? DEFAULT_HOOK_TIMEOUT_MS), 0) + 1000) / 1000);
|
|
7055
|
+
native[name] = [{ hooks: [{ type: "command", command: `node "\${CLAUDE_PLUGIN_ROOT}/${wrapperPath}"`, async: false, [options.timeoutKey ?? "timeout"]: seconds }] }];
|
|
7056
|
+
}
|
|
7057
|
+
}
|
|
7058
|
+
const wrapper = `import { fileURLToPath } from 'node:url';
|
|
7059
|
+
import { dirname, resolve } from 'node:path';
|
|
7060
|
+
import { runHooks } from '../hook-runtime.mjs';
|
|
7061
|
+
const config = ${JSON.stringify(config)};
|
|
7062
|
+
const events = ${JSON.stringify(eventMap)};
|
|
7063
|
+
const pluginRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
7064
|
+
try {
|
|
7065
|
+
let raw = '';
|
|
7066
|
+
for await (const chunk of process.stdin) {
|
|
7067
|
+
raw += chunk;
|
|
7068
|
+
if (Buffer.byteLength(raw) > 1048576) throw new Error('Native hook input exceeds 1 MiB');
|
|
7069
|
+
}
|
|
7070
|
+
const native = JSON.parse(raw);
|
|
7071
|
+
const nativeEvent = native.hook_event_name ?? native.hookEventName;
|
|
7072
|
+
const event = events[nativeEvent];
|
|
7073
|
+
if (!event) throw new Error('Unsupported native hook event: ' + nativeEvent);
|
|
7074
|
+
const isTool = event === 'tool.before' || event === 'tool.after';
|
|
7075
|
+
const input = {
|
|
7076
|
+
version: 1, event, harness: ${JSON.stringify(options.id)},
|
|
7077
|
+
cwd: native.cwd ?? process.cwd(),
|
|
7078
|
+
...(native.session_id ?? native.sessionId ? { sessionId: native.session_id ?? native.sessionId } : {}),
|
|
7079
|
+
...(native.source ?? native.reason ? { reason: native.source ?? native.reason } : {}),
|
|
7080
|
+
...(isTool ? { tool: {
|
|
7081
|
+
name: native.tool_name ?? native.toolName,
|
|
7082
|
+
...(native.tool_use_id ?? native.tool_call_id ?? native.toolCallId ? { id: native.tool_use_id ?? native.tool_call_id ?? native.toolCallId } : {}),
|
|
7083
|
+
input: native.tool_input ?? native.toolInput,
|
|
7084
|
+
...(event === 'tool.after' ? {
|
|
7085
|
+
output: native.tool_response ?? native.toolResponse ?? native.error,
|
|
7086
|
+
isError: nativeEvent === 'PostToolUseFailure' || native.is_error === true || native.isError === true,
|
|
7087
|
+
} : {}),
|
|
7088
|
+
} } : {}),
|
|
7089
|
+
...(event === 'turn.stop' ? { stop: { active: native.stop_hook_active === true || native.stopHookActive === true } } : {}),
|
|
7090
|
+
};
|
|
7091
|
+
const result = await runHooks(config, input, pluginRoot);
|
|
7092
|
+
let output = {};
|
|
7093
|
+
if (event === 'turn.stop') {
|
|
7094
|
+
if (result.continue === true) output = ${options.stopOutputSource};
|
|
7095
|
+
} else if (result.context !== undefined) {
|
|
7096
|
+
output = { hookSpecificOutput: { hookEventName: nativeEvent, additionalContext: result.context } };
|
|
7097
|
+
}
|
|
7098
|
+
process.stdout.write(JSON.stringify(output) + '\\n');
|
|
7099
|
+
} catch (error) {
|
|
7100
|
+
process.stderr.write('plugnz hook: ' + (error instanceof Error ? error.message : String(error)) + '\\n');
|
|
7101
|
+
process.exitCode = 1;
|
|
7102
|
+
}
|
|
7103
|
+
`;
|
|
7104
|
+
return { "hooks/hooks.json": JSON.stringify({ hooks: native }, null, 2) + `
|
|
7105
|
+
`, [wrapperPath]: wrapper };
|
|
7106
|
+
}
|
|
7107
|
+
};
|
|
7108
|
+
}
|
|
7109
|
+
var init_command_hooks = __esm(() => {
|
|
7110
|
+
init_hooks();
|
|
7111
|
+
});
|
|
7112
|
+
|
|
7113
|
+
// src/hosts/command-hook-manifest.ts
|
|
7114
|
+
import { existsSync as existsSync30, mkdirSync as mkdirSync15, readFileSync as readFileSync32, rmSync as rmSync16, writeFileSync as writeFileSync15 } from "node:fs";
|
|
7115
|
+
import { dirname as dirname19, join as join40 } from "node:path";
|
|
7116
|
+
function manifest(path) {
|
|
7117
|
+
let parsed;
|
|
7118
|
+
try {
|
|
7119
|
+
parsed = JSON.parse(readFileSync32(path, "utf8"));
|
|
7120
|
+
} catch (error) {
|
|
7121
|
+
throw new Error("Cannot read hook plugin manifest " + path + ": " + error.message);
|
|
7122
|
+
}
|
|
7123
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed))
|
|
7124
|
+
throw new Error("Hook plugin manifest must be an object: " + path);
|
|
7125
|
+
const value = parsed;
|
|
7126
|
+
if (typeof value["name"] !== "string" || !/^[a-z0-9][a-z0-9._-]{0,127}$/.test(value["name"]))
|
|
7127
|
+
throw new Error("Hook plugin manifest has an invalid native plugin name: " + path);
|
|
7128
|
+
for (const field of ["version", "description"]) {
|
|
7129
|
+
if (value[field] !== undefined && (typeof value[field] !== "string" || value[field].includes("\x00")))
|
|
7130
|
+
throw new Error("Hook plugin manifest " + field + " must be a string: " + path);
|
|
7131
|
+
}
|
|
7132
|
+
return value;
|
|
7133
|
+
}
|
|
7134
|
+
function prepareCommandHookManifest(stagingRoot, relativeNativePath, removeRoot) {
|
|
7135
|
+
const rootPath = join40(stagingRoot, "plugin.json");
|
|
7136
|
+
const nativePath = join40(stagingRoot, relativeNativePath);
|
|
7137
|
+
const root = existsSync30(rootPath) ? manifest(rootPath) : undefined;
|
|
7138
|
+
if (root) {
|
|
7139
|
+
const unsupported = Object.keys(root).find((key) => !IDENTITY_FIELDS.includes(key));
|
|
7140
|
+
if (unsupported)
|
|
7141
|
+
throw new Error('Hook build cannot project root plugin.json field "' + unsupported + '"; use an explicit native plugin manifest');
|
|
7142
|
+
}
|
|
7143
|
+
const native = existsSync30(nativePath) ? manifest(nativePath) : undefined;
|
|
7144
|
+
if (!native && !root)
|
|
7145
|
+
throw new Error("Hook build requires a plugin identity in plugin.json or " + relativeNativePath);
|
|
7146
|
+
if (native) {
|
|
7147
|
+
for (const key of IDENTITY_FIELDS) {
|
|
7148
|
+
if (root?.[key] !== undefined && native[key] !== root[key])
|
|
7149
|
+
throw new Error("Conflicting hook plugin manifest identity: " + key);
|
|
7150
|
+
}
|
|
7151
|
+
if (native["hooks"] !== undefined && native["hooks"] !== "./hooks/hooks.json" && native["hooks"] !== "hooks/hooks.json") {
|
|
7152
|
+
throw new Error("Hook build cannot merge a custom native hooks route");
|
|
7153
|
+
}
|
|
7154
|
+
} else {
|
|
7155
|
+
mkdirSync15(dirname19(nativePath), { recursive: true });
|
|
7156
|
+
writeFileSync15(nativePath, JSON.stringify(root, null, 2) + `
|
|
7157
|
+
`);
|
|
7158
|
+
}
|
|
7159
|
+
if (removeRoot && root)
|
|
7160
|
+
rmSync16(rootPath);
|
|
7161
|
+
}
|
|
7162
|
+
var IDENTITY_FIELDS;
|
|
7163
|
+
var init_command_hook_manifest = __esm(() => {
|
|
7164
|
+
IDENTITY_FIELDS = ["name", "version", "description"];
|
|
7165
|
+
});
|
|
7166
|
+
|
|
7167
|
+
// src/hosts/claude-code-hooks.ts
|
|
7168
|
+
function prepareHookPlugin(stagingRoot) {
|
|
7169
|
+
prepareCommandHookManifest(stagingRoot, ".claude-plugin/plugin.json", false);
|
|
7170
|
+
}
|
|
7171
|
+
var adapter, hookAdapter;
|
|
7172
|
+
var init_claude_code_hooks = __esm(() => {
|
|
7173
|
+
init_command_hooks();
|
|
7174
|
+
init_command_hook_manifest();
|
|
7175
|
+
adapter = commandHookAdapter({
|
|
7176
|
+
id: "claude-code",
|
|
7177
|
+
baselineVersion: "2.1.285",
|
|
7178
|
+
nativeEvents: {
|
|
7179
|
+
"session.start": ["SessionStart"],
|
|
7180
|
+
"tool.before": ["PreToolUse"],
|
|
7181
|
+
"tool.after": ["PostToolUse", "PostToolUseFailure"],
|
|
7182
|
+
"turn.stop": ["Stop"]
|
|
7183
|
+
},
|
|
7184
|
+
contextDelivery: "native additionalContext; post success and executed failure",
|
|
7185
|
+
stopOutputSource: "{ decision: 'block', reason: result.context }"
|
|
7186
|
+
});
|
|
7187
|
+
hookAdapter = { ...adapter, prepare: prepareHookPlugin };
|
|
7188
|
+
});
|
|
7189
|
+
|
|
7190
|
+
// src/hosts/codex-hooks.ts
|
|
7191
|
+
function prepareHookPlugin2(stagingRoot) {
|
|
7192
|
+
prepareCommandHookManifest(stagingRoot, ".codex-plugin/plugin.json", true);
|
|
7193
|
+
}
|
|
7194
|
+
var adapter2, hookAdapter2;
|
|
7195
|
+
var init_codex_hooks = __esm(() => {
|
|
7196
|
+
init_command_hooks();
|
|
7197
|
+
init_command_hook_manifest();
|
|
7198
|
+
adapter2 = commandHookAdapter({
|
|
7199
|
+
id: "codex",
|
|
7200
|
+
baselineVersion: "0.161.0",
|
|
7201
|
+
nativeEvents: {
|
|
7202
|
+
"session.start": ["SessionStart"],
|
|
7203
|
+
"tool.before": ["PreToolUse"],
|
|
7204
|
+
"tool.after": ["PostToolUse"],
|
|
7205
|
+
"turn.stop": ["Stop"]
|
|
7206
|
+
},
|
|
7207
|
+
contextDelivery: "developer context; post-success only on qualified native routes",
|
|
7208
|
+
stopOutputSource: "{ decision: 'block', reason: result.context }"
|
|
7209
|
+
});
|
|
7210
|
+
hookAdapter2 = { ...adapter2, prepare: prepareHookPlugin2 };
|
|
7211
|
+
});
|
|
7212
|
+
|
|
7213
|
+
// src/hosts/omp-hooks.ts
|
|
7214
|
+
var hookAdapter3;
|
|
7215
|
+
var init_omp_hooks = __esm(() => {
|
|
7216
|
+
hookAdapter3 = {
|
|
7217
|
+
id: "omp",
|
|
7218
|
+
baselineVersion: "18.8.7",
|
|
7219
|
+
capabilities: {
|
|
7220
|
+
"session.start": { nativeEvent: "session_start", context: true, continue: false, delivery: "user message before the next prompt" },
|
|
7221
|
+
"tool.before": { nativeEvent: "tool_call", context: true, continue: false, delivery: "native additional context after the tool batch" },
|
|
7222
|
+
"tool.after": { nativeEvent: "tool_result", context: true, continue: false, delivery: "native additional context after the tool batch" },
|
|
7223
|
+
"turn.stop": { nativeEvent: "session_stop", context: true, continue: true, delivery: "user message in one native session_stop continuation" }
|
|
7224
|
+
},
|
|
7225
|
+
emit(config) {
|
|
7226
|
+
return {
|
|
7227
|
+
"extension.mjs": `import { fileURLToPath } from 'node:url';
|
|
7228
|
+
import { runHooks } from './hook-runtime.mjs';
|
|
7229
|
+
|
|
7230
|
+
const config = ${JSON.stringify(config)};
|
|
7231
|
+
const pluginRoot = fileURLToPath(new URL('.', import.meta.url));
|
|
7232
|
+
|
|
7233
|
+
export default function (pi) {
|
|
7234
|
+
const input = (event, ctx, extra = {}) => ({
|
|
7235
|
+
version: 1, event, harness: 'omp', cwd: ctx.cwd,
|
|
7236
|
+
sessionId: ctx.sessionManager.getSessionId(), ...extra,
|
|
7237
|
+
});
|
|
7238
|
+
|
|
7239
|
+
if (config.hooks['session.start']?.length) {
|
|
7240
|
+
pi.on('session_start', async (event, ctx) => {
|
|
7241
|
+
const result = await runHooks(config, input('session.start', ctx, {
|
|
7242
|
+
reason: event.reason,
|
|
7243
|
+
}), pluginRoot);
|
|
7244
|
+
if (result.context) {
|
|
7245
|
+
pi.sendMessage({
|
|
7246
|
+
customType: 'plugnz:session.start', content: result.context, display: false,
|
|
7247
|
+
}, { deliverAs: 'nextTurn', triggerTurn: false });
|
|
7248
|
+
}
|
|
7249
|
+
});
|
|
7250
|
+
}
|
|
7251
|
+
|
|
7252
|
+
if (config.hooks['tool.before']?.length) {
|
|
7253
|
+
pi.on('tool_call', async (event, ctx) => {
|
|
7254
|
+
const result = await runHooks(config, input('tool.before', ctx, {
|
|
7255
|
+
tool: { name: event.toolName, id: event.toolCallId, input: event.input },
|
|
7256
|
+
}), pluginRoot);
|
|
7257
|
+
if (result.context) return { additionalContext: result.context };
|
|
7258
|
+
});
|
|
7259
|
+
}
|
|
7260
|
+
|
|
7261
|
+
if (config.hooks['tool.after']?.length) {
|
|
7262
|
+
pi.on('tool_result', async (event, ctx) => {
|
|
7263
|
+
const result = await runHooks(config, input('tool.after', ctx, {
|
|
7264
|
+
tool: {
|
|
7265
|
+
name: event.toolName, id: event.toolCallId, input: event.input,
|
|
7266
|
+
output: { content: event.content, details: event.details },
|
|
7267
|
+
isError: event.isError,
|
|
7268
|
+
},
|
|
7269
|
+
}), pluginRoot);
|
|
7270
|
+
if (result.context) return { additionalContext: result.context };
|
|
7271
|
+
});
|
|
7272
|
+
}
|
|
7273
|
+
|
|
7274
|
+
if (config.hooks['turn.stop']?.length) {
|
|
7275
|
+
pi.on('session_stop', async (event, ctx) => {
|
|
7276
|
+
const result = await runHooks(config, input('turn.stop', ctx, {
|
|
7277
|
+
stop: { active: event.stop_hook_active === true },
|
|
7278
|
+
}), pluginRoot);
|
|
7279
|
+
if (result.continue && result.context) {
|
|
7280
|
+
return { continue: true, additionalContext: result.context };
|
|
7281
|
+
}
|
|
7282
|
+
});
|
|
7283
|
+
}
|
|
7284
|
+
}
|
|
7285
|
+
`
|
|
7286
|
+
};
|
|
7287
|
+
}
|
|
7288
|
+
};
|
|
7289
|
+
});
|
|
7290
|
+
|
|
7291
|
+
// src/hosts/pi-hooks.ts
|
|
7292
|
+
var hookAdapter4;
|
|
7293
|
+
var init_pi_hooks = __esm(() => {
|
|
7294
|
+
hookAdapter4 = {
|
|
7295
|
+
id: "pi",
|
|
7296
|
+
baselineVersion: "1.1.0",
|
|
7297
|
+
capabilities: {
|
|
7298
|
+
"session.start": { nativeEvent: "session_start", context: true, continue: false, delivery: "user message before the next prompt" },
|
|
7299
|
+
"tool.before": { nativeEvent: "tool_call", context: true, continue: false, delivery: "user message after the tool batch" },
|
|
7300
|
+
"tool.after": { nativeEvent: "tool_result", context: true, continue: false, delivery: "text appended to the native tool result" },
|
|
7301
|
+
"turn.stop": { nativeEvent: "agent_before_settle", context: true, continue: true, delivery: "user message in one native agent_before_settle continuation" }
|
|
7302
|
+
},
|
|
7303
|
+
emit(config) {
|
|
7304
|
+
return {
|
|
7305
|
+
"extension.mjs": `import { fileURLToPath } from 'node:url';
|
|
7306
|
+
import { runHooks } from './hook-runtime.mjs';
|
|
7307
|
+
|
|
7308
|
+
const config = ${JSON.stringify(config)};
|
|
7309
|
+
const pluginRoot = fileURLToPath(new URL('.', import.meta.url));
|
|
7310
|
+
|
|
7311
|
+
export default function (pi) {
|
|
7312
|
+
let stopActive = false;
|
|
7313
|
+
const input = (event, ctx, extra = {}) => ({
|
|
7314
|
+
version: 1, event, harness: 'pi', cwd: ctx.cwd,
|
|
7315
|
+
sessionId: ctx.sessionManager.getSessionId(), ...extra,
|
|
7316
|
+
});
|
|
7317
|
+
|
|
7318
|
+
if (config.hooks['session.start']?.length) {
|
|
7319
|
+
pi.on('session_start', async (event, ctx) => {
|
|
7320
|
+
const result = await runHooks(config, input('session.start', ctx, {
|
|
7321
|
+
reason: event.reason,
|
|
7322
|
+
}), pluginRoot);
|
|
7323
|
+
if (result.context) {
|
|
7324
|
+
pi.sendMessage({
|
|
7325
|
+
customType: 'plugnz:session.start', content: result.context, display: false,
|
|
7326
|
+
}, { deliverAs: 'nextTurn', triggerTurn: false });
|
|
7327
|
+
}
|
|
7328
|
+
});
|
|
7329
|
+
}
|
|
7330
|
+
|
|
7331
|
+
if (config.hooks['tool.before']?.length) {
|
|
7332
|
+
pi.on('tool_call', async (event, ctx) => {
|
|
7333
|
+
const result = await runHooks(config, input('tool.before', ctx, {
|
|
7334
|
+
tool: { name: event.toolName, id: event.toolCallId, input: event.input },
|
|
7335
|
+
}), pluginRoot);
|
|
7336
|
+
if (result.context) {
|
|
7337
|
+
// Context-only messages flush after all results, preserving tool-call/result pairing.
|
|
7338
|
+
pi.sendMessage({
|
|
7339
|
+
customType: 'plugnz:tool.before', content: result.context, display: false,
|
|
7340
|
+
}, { triggerTurn: false });
|
|
7341
|
+
}
|
|
7342
|
+
});
|
|
7343
|
+
}
|
|
7344
|
+
|
|
7345
|
+
if (config.hooks['tool.after']?.length) {
|
|
7346
|
+
pi.on('tool_result', async (event, ctx) => {
|
|
7347
|
+
const result = await runHooks(config, input('tool.after', ctx, {
|
|
7348
|
+
tool: {
|
|
7349
|
+
name: event.toolName, id: event.toolCallId, input: event.input,
|
|
7350
|
+
output: {
|
|
7351
|
+
content: event.content, details: event.details,
|
|
7352
|
+
structuredContent: event.structuredContent, usage: event.usage,
|
|
7353
|
+
},
|
|
7354
|
+
isError: event.isError,
|
|
7355
|
+
},
|
|
7356
|
+
}), pluginRoot);
|
|
7357
|
+
if (result.context) {
|
|
7358
|
+
// Pi clears structuredContent when content is replaced without returning it.
|
|
7359
|
+
return {
|
|
7360
|
+
content: [...event.content, { type: 'text', text: result.context }],
|
|
7361
|
+
structuredContent: event.structuredContent,
|
|
7362
|
+
};
|
|
7363
|
+
}
|
|
7364
|
+
});
|
|
7365
|
+
}
|
|
7366
|
+
|
|
7367
|
+
if (config.hooks['turn.stop']?.length) {
|
|
7368
|
+
// Native continuations do not rerun before_agent_start. A new user prompt does.
|
|
7369
|
+
pi.on('before_agent_start', () => { stopActive = false; });
|
|
7370
|
+
pi.on('agent_before_settle', async (event, ctx) => {
|
|
7371
|
+
const result = await runHooks(config, input('turn.stop', ctx, {
|
|
7372
|
+
stop: { active: stopActive },
|
|
7373
|
+
}), pluginRoot);
|
|
7374
|
+
if (result.continue && result.context) {
|
|
7375
|
+
stopActive = true;
|
|
7376
|
+
return {
|
|
7377
|
+
entries: [...event.entries, {
|
|
7378
|
+
type: 'custom_message', customType: 'plugnz:turn.stop',
|
|
7379
|
+
content: result.context, display: false,
|
|
7380
|
+
}],
|
|
7381
|
+
continue: true,
|
|
7382
|
+
};
|
|
7383
|
+
}
|
|
7384
|
+
});
|
|
7385
|
+
}
|
|
7386
|
+
}
|
|
7387
|
+
`
|
|
7388
|
+
};
|
|
7389
|
+
}
|
|
7390
|
+
};
|
|
7391
|
+
});
|
|
7392
|
+
|
|
7393
|
+
// src/hosts/opencode-hooks.ts
|
|
7394
|
+
var hookAdapter5;
|
|
7395
|
+
var init_opencode_hooks = __esm(() => {
|
|
7396
|
+
hookAdapter5 = {
|
|
7397
|
+
id: "opencode",
|
|
7398
|
+
baselineVersion: "1.19.0",
|
|
7399
|
+
capabilities: {
|
|
7400
|
+
"session.start": { nativeEvent: "experimental.chat.system.transform", context: true, continue: false, delivery: "Awaited before the first model request; cached per session in the plugin instance and included in its system context." },
|
|
7401
|
+
"turn.stop": { nativeEvent: "session.idle", context: false, continue: false, delivery: "session.idle notification: checks only. Feedback and continuation are unsupported." },
|
|
7402
|
+
"tool.before": { nativeEvent: "tool.execute.before", context: true, continue: false, delivery: "Handler runs before execution; context reaches the next model request with the tool result, or through system context when execution fails." },
|
|
7403
|
+
"tool.after": { nativeEvent: "tool.execute.after", context: true, continue: false, delivery: "Append context to successful native or MCP tool results; preserve existing content and metadata." }
|
|
7404
|
+
},
|
|
7405
|
+
emit(config) {
|
|
7406
|
+
return {
|
|
7407
|
+
"plugin.mjs": `import { dirname } from 'node:path';
|
|
7408
|
+
import { fileURLToPath } from 'node:url';
|
|
7409
|
+
import { runHooks } from './hook-runtime.mjs';
|
|
7410
|
+
|
|
7411
|
+
const config = ${JSON.stringify(config)};
|
|
7412
|
+
const pluginRoot = dirname(fileURLToPath(import.meta.url));
|
|
7413
|
+
|
|
7414
|
+
export default async function plugnzHooks({ directory }) {
|
|
7415
|
+
const starts = new Map();
|
|
7416
|
+
const before = new Map();
|
|
7417
|
+
const active = new Set();
|
|
7418
|
+
|
|
7419
|
+
const input = (event, sessionId, extra = {}) => ({
|
|
7420
|
+
version: 1, event, harness: 'opencode', sessionId, cwd: directory, ...extra,
|
|
7421
|
+
});
|
|
7422
|
+
const report = (error) => {
|
|
7423
|
+
console.error('[plugnz hooks: opencode] ' + (error instanceof Error ? error.message : String(error)));
|
|
7424
|
+
};
|
|
7425
|
+
const startup = (sessionId) => {
|
|
7426
|
+
if (!starts.has(sessionId)) {
|
|
7427
|
+
const pending = runHooks(config, input('session.start', sessionId, { reason: 'first-model-request' }), pluginRoot);
|
|
7428
|
+
starts.set(sessionId, pending);
|
|
7429
|
+
pending.catch(() => { if (starts.get(sessionId) === pending) starts.delete(sessionId); });
|
|
7430
|
+
}
|
|
7431
|
+
return starts.get(sessionId);
|
|
7432
|
+
};
|
|
7433
|
+
const pendingFor = (sessionId) => {
|
|
7434
|
+
if (!before.has(sessionId)) before.set(sessionId, new Map());
|
|
7435
|
+
return before.get(sessionId);
|
|
7436
|
+
};
|
|
7437
|
+
|
|
7438
|
+
return {
|
|
7439
|
+
'chat.message': async ({ sessionID }) => {
|
|
7440
|
+
active.add(sessionID);
|
|
7441
|
+
},
|
|
7442
|
+
'experimental.chat.system.transform': async ({ sessionID }, output) => {
|
|
7443
|
+
// Auxiliary model requests without a session do not start a conversation.
|
|
7444
|
+
if (!sessionID) return;
|
|
7445
|
+
const result = await startup(sessionID);
|
|
7446
|
+
if (result.context) output.system.push(result.context);
|
|
7447
|
+
// Failed tools may have no after callback. Deliver their pre-tool context
|
|
7448
|
+
// at the next request instead of attaching it to another tool call.
|
|
7449
|
+
const pending = before.get(sessionID);
|
|
7450
|
+
if (pending?.size) {
|
|
7451
|
+
for (const context of pending.values()) output.system.push(context);
|
|
7452
|
+
pending.clear();
|
|
7453
|
+
}
|
|
7454
|
+
},
|
|
7455
|
+
'tool.execute.before': async ({ tool, sessionID, callID }, output) => {
|
|
7456
|
+
const result = await runHooks(config, input('tool.before', sessionID, {
|
|
7457
|
+
tool: { name: tool, id: callID, input: output.args },
|
|
7458
|
+
}), pluginRoot);
|
|
7459
|
+
if (result.context) pendingFor(sessionID).set(callID, result.context);
|
|
7460
|
+
},
|
|
7461
|
+
'tool.execute.after': async ({ tool, sessionID, callID, args }, output) => {
|
|
7462
|
+
const result = await runHooks(config, input('tool.after', sessionID, {
|
|
7463
|
+
tool: {
|
|
7464
|
+
name: tool, id: callID, input: args,
|
|
7465
|
+
output: typeof output.output === 'string' ? output.output : output.content,
|
|
7466
|
+
...(typeof output.isError === 'boolean' ? { isError: output.isError } : {}),
|
|
7467
|
+
},
|
|
7468
|
+
}), pluginRoot);
|
|
7469
|
+
const pending = before.get(sessionID);
|
|
7470
|
+
const context = [pending?.get(callID), result.context].filter(Boolean).join('\\n\\n');
|
|
7471
|
+
if (!context) return;
|
|
7472
|
+
// Built-in tools and the MCP path pass different native result shapes.
|
|
7473
|
+
if (typeof output.output === 'string') output.output += '\\n\\n' + context;
|
|
7474
|
+
else if (Array.isArray(output.content)) output.content = [...output.content, { type: 'text', text: context }];
|
|
7475
|
+
else throw new Error('plugnz hooks: unsupported OpenCode tool result shape; context was not delivered');
|
|
7476
|
+
pending?.delete(callID);
|
|
7477
|
+
},
|
|
7478
|
+
event: async ({ event }) => {
|
|
7479
|
+
const sessionId = event.properties?.sessionID ?? event.properties?.info?.id;
|
|
7480
|
+
if (!sessionId) return;
|
|
7481
|
+
if (event.type === 'session.deleted') {
|
|
7482
|
+
starts.delete(sessionId);
|
|
7483
|
+
before.delete(sessionId);
|
|
7484
|
+
active.delete(sessionId);
|
|
7485
|
+
return;
|
|
7486
|
+
}
|
|
7487
|
+
if (event.type === 'session.status' && event.properties.status?.type === 'busy') active.add(sessionId);
|
|
7488
|
+
if (event.type !== 'session.idle' || !active.delete(sessionId)) return;
|
|
7489
|
+
try {
|
|
7490
|
+
const result = await runHooks(config, input('turn.stop', sessionId, { stop: { active: false } }), pluginRoot);
|
|
7491
|
+
if (result.context || result.continue) {
|
|
7492
|
+
throw new Error('OpenCode session.idle cannot deliver stop feedback or continue a turn; return {} from this check');
|
|
7493
|
+
}
|
|
7494
|
+
} catch (error) {
|
|
7495
|
+
// The native event dispatcher does not await this callback.
|
|
7496
|
+
// Catch explicitly to report failures without an unhandled rejection.
|
|
7497
|
+
report(error);
|
|
7498
|
+
}
|
|
7499
|
+
},
|
|
7500
|
+
};
|
|
7501
|
+
}
|
|
7502
|
+
`
|
|
7503
|
+
};
|
|
7504
|
+
}
|
|
7505
|
+
};
|
|
7506
|
+
});
|
|
7507
|
+
|
|
7508
|
+
// src/hosts/zcode-cli-hooks.ts
|
|
7509
|
+
function prepareHookPlugin3(stagingRoot) {
|
|
7510
|
+
prepareCommandHookManifest(stagingRoot, ".zcode-plugin/plugin.json", false);
|
|
7511
|
+
}
|
|
7512
|
+
var adapter3, hookAdapter6;
|
|
7513
|
+
var init_zcode_cli_hooks = __esm(() => {
|
|
7514
|
+
init_command_hooks();
|
|
7515
|
+
init_command_hook_manifest();
|
|
7516
|
+
adapter3 = commandHookAdapter({
|
|
7517
|
+
id: "zcode-cli",
|
|
7518
|
+
baselineVersion: "0.16.9",
|
|
7519
|
+
nativeEvents: {
|
|
7520
|
+
"session.start": ["SessionStart"],
|
|
7521
|
+
"tool.before": ["PreToolUse"],
|
|
7522
|
+
"tool.after": ["PostToolUse", "PostToolUseFailure"],
|
|
7523
|
+
"turn.stop": ["Stop"]
|
|
7524
|
+
},
|
|
7525
|
+
contextDelivery: "startup user context; pre/post context appended to native tool result",
|
|
7526
|
+
stopOutputSource: "{ continue: true, hookSpecificOutput: { hookEventName: 'Stop', additionalContext: result.context } }"
|
|
7527
|
+
});
|
|
7528
|
+
hookAdapter6 = { ...adapter3, prepare: prepareHookPlugin3 };
|
|
7529
|
+
});
|
|
7530
|
+
|
|
7531
|
+
// src/hook-build.ts
|
|
7532
|
+
import { spawnSync as spawnSync6 } from "node:child_process";
|
|
7533
|
+
import { cpSync as cpSync14, existsSync as existsSync31, lstatSync as lstatSync24, mkdirSync as mkdirSync16, mkdtempSync as mkdtempSync15, readFileSync as readFileSync33, readdirSync as readdirSync24, realpathSync as realpathSync7, renameSync as renameSync14, rmSync as rmSync17, writeFileSync as writeFileSync16 } from "node:fs";
|
|
7534
|
+
import { dirname as dirname20, isAbsolute as isAbsolute14, join as join41, relative as relative10, resolve as resolve27 } from "node:path";
|
|
7535
|
+
import { fileURLToPath } from "node:url";
|
|
7536
|
+
function hookRuntimeSource() {
|
|
7537
|
+
const moduleDir = dirname20(fileURLToPath(import.meta.url));
|
|
7538
|
+
const source = join41(moduleDir, "hooks.ts");
|
|
7539
|
+
if (existsSync31(source) && process.versions["bun"]) {
|
|
7540
|
+
if (developmentRuntime !== undefined)
|
|
7541
|
+
return developmentRuntime;
|
|
7542
|
+
const built = spawnSync6(process.execPath, ["build", source, "--target=node", "--format=esm"], {
|
|
7543
|
+
encoding: "buffer",
|
|
7544
|
+
timeout: 60000,
|
|
7545
|
+
killSignal: "SIGKILL",
|
|
7546
|
+
maxBuffer: 4 * 1024 * 1024
|
|
7547
|
+
});
|
|
7548
|
+
const output = new TextDecoder().decode(built.stdout);
|
|
7549
|
+
if (built.status !== 0 || !output.trim()) {
|
|
7550
|
+
throw new Error("Cannot bundle hook runtime: " + new TextDecoder().decode(built.stderr).trim());
|
|
7551
|
+
}
|
|
7552
|
+
developmentRuntime = output;
|
|
7553
|
+
return output;
|
|
7554
|
+
}
|
|
7555
|
+
for (const path of [join41(moduleDir, RUNTIME_FILE), join41(moduleDir, "..", "dist", RUNTIME_FILE)]) {
|
|
7556
|
+
if (existsSync31(path))
|
|
7557
|
+
return readFileSync33(path, "utf8");
|
|
7558
|
+
}
|
|
7559
|
+
throw new Error("Hook runtime is missing from the distribution; rebuild with bun run build");
|
|
7560
|
+
}
|
|
7561
|
+
function entryExists(path) {
|
|
7562
|
+
try {
|
|
7563
|
+
lstatSync24(path);
|
|
7564
|
+
return true;
|
|
7565
|
+
} catch (error) {
|
|
7566
|
+
if (error.code === "ENOENT")
|
|
7567
|
+
return false;
|
|
7568
|
+
throw error;
|
|
7569
|
+
}
|
|
7570
|
+
}
|
|
7571
|
+
function artifactPath(root, path) {
|
|
7572
|
+
if (!path || isAbsolute14(path) || path.includes("\\") || path.includes("\x00") || path.split("/").some((part) => part === "" || part === "." || part === "..")) {
|
|
7573
|
+
throw new Error("Unsafe generated hook artifact path: " + path);
|
|
7574
|
+
}
|
|
7575
|
+
const file = join41(root, path);
|
|
7576
|
+
let current = root;
|
|
7577
|
+
for (const part of path.split("/").slice(0, -1)) {
|
|
7578
|
+
current = join41(current, part);
|
|
7579
|
+
if (entryExists(current) && (lstatSync24(current).isSymbolicLink() || !lstatSync24(current).isDirectory())) {
|
|
7580
|
+
throw new Error("Generated hook artifact parent is not an owned directory: " + current);
|
|
7581
|
+
}
|
|
7582
|
+
}
|
|
7583
|
+
return file;
|
|
7584
|
+
}
|
|
7585
|
+
function emitHooksToDirectory(adapter, configValue, stagingRoot) {
|
|
7586
|
+
const config = validateHookConfig(configValue);
|
|
7587
|
+
const root = resolve27(stagingRoot);
|
|
7588
|
+
if (!existsSync31(root) || lstatSync24(root).isSymbolicLink() || !lstatSync24(root).isDirectory())
|
|
7589
|
+
throw new Error("Hook staging root must be a directory");
|
|
7590
|
+
adapter.prepare?.(root);
|
|
7591
|
+
const artifacts = adapter.emit(config);
|
|
7592
|
+
if (Object.prototype.hasOwnProperty.call(artifacts, RUNTIME_FILE))
|
|
7593
|
+
throw new Error("Adapter cannot replace the shared hook runtime");
|
|
7594
|
+
artifacts[RUNTIME_FILE] = hookRuntimeSource();
|
|
7595
|
+
const paths = Object.keys(artifacts).sort();
|
|
7596
|
+
for (const path of paths) {
|
|
7597
|
+
if (typeof artifacts[path] !== "string")
|
|
7598
|
+
throw new Error("Generated hook artifact must be text: " + path);
|
|
7599
|
+
const file = artifactPath(root, path);
|
|
7600
|
+
if (entryExists(file))
|
|
7601
|
+
throw new Error("Refusing to overwrite authored hook artifact: " + file);
|
|
7602
|
+
if (paths.some((other) => other !== path && other.startsWith(path + "/")))
|
|
7603
|
+
throw new Error("Generated hook file conflicts with a directory: " + path);
|
|
7604
|
+
}
|
|
7605
|
+
for (const path of paths) {
|
|
7606
|
+
const file = artifactPath(root, path);
|
|
7607
|
+
mkdirSync16(dirname20(file), { recursive: true });
|
|
7608
|
+
writeFileSync16(file, artifacts[path]);
|
|
7609
|
+
}
|
|
7610
|
+
return paths;
|
|
7611
|
+
}
|
|
7612
|
+
function copyAuthoredTree(source, destination, top = true) {
|
|
7613
|
+
mkdirSync16(destination, { recursive: true });
|
|
7614
|
+
for (const name of readdirSync24(source).sort()) {
|
|
7615
|
+
if (name === ".git" || top && name === HOOK_CONFIG_FILE)
|
|
7616
|
+
continue;
|
|
7617
|
+
const from = join41(source, name);
|
|
7618
|
+
const to = join41(destination, name);
|
|
7619
|
+
const stat = lstatSync24(from);
|
|
7620
|
+
if (stat.isSymbolicLink())
|
|
7621
|
+
throw new Error("Hook source symlinks are unsupported: " + from);
|
|
7622
|
+
if (stat.isDirectory())
|
|
7623
|
+
copyAuthoredTree(from, to, false);
|
|
7624
|
+
else if (stat.isFile())
|
|
7625
|
+
cpSync14(from, to);
|
|
7626
|
+
else
|
|
7627
|
+
throw new Error("Hook source contains a non-file resource: " + from);
|
|
7628
|
+
}
|
|
7629
|
+
}
|
|
7630
|
+
function prospectivePath(path) {
|
|
7631
|
+
const suffix = [];
|
|
7632
|
+
let parent = resolve27(path);
|
|
7633
|
+
while (!existsSync31(parent)) {
|
|
7634
|
+
const next = dirname20(parent);
|
|
7635
|
+
if (next === parent)
|
|
7636
|
+
throw new Error("Cannot resolve output ancestor: " + path);
|
|
7637
|
+
suffix.unshift(relative10(next, parent));
|
|
7638
|
+
parent = next;
|
|
7639
|
+
}
|
|
7640
|
+
return join41(realpathSync7(parent), ...suffix);
|
|
7641
|
+
}
|
|
7642
|
+
function buildHooks(pluginDir, targets, outputDir) {
|
|
7643
|
+
const requestedSource = resolve27(pluginDir);
|
|
7644
|
+
if (!existsSync31(requestedSource) || lstatSync24(requestedSource).isSymbolicLink() || !lstatSync24(requestedSource).isDirectory()) {
|
|
7645
|
+
throw new Error("Hook source must be a directory: " + requestedSource);
|
|
7646
|
+
}
|
|
7647
|
+
const source = realpathSync7(requestedSource);
|
|
7648
|
+
const config = readHookConfig(source);
|
|
7649
|
+
if (config === undefined)
|
|
7650
|
+
throw new Error("Hook source requires " + HOOK_CONFIG_FILE);
|
|
7651
|
+
if (targets.length === 0)
|
|
7652
|
+
throw new Error("hooks build requires at least one --target");
|
|
7653
|
+
if (new Set(targets).size !== targets.length)
|
|
7654
|
+
throw new Error("Duplicate hook target");
|
|
7655
|
+
const adapters = targets.map((id) => {
|
|
7656
|
+
const adapter = hookAdapters.find((item) => item.id === id);
|
|
7657
|
+
if (adapter === undefined)
|
|
7658
|
+
throw new Error("Unknown hook target: " + id);
|
|
7659
|
+
if (!/^[a-z0-9][a-z0-9-]*$/u.test(adapter.id))
|
|
7660
|
+
throw new Error("Unsafe hook target id");
|
|
7661
|
+
return adapter;
|
|
7662
|
+
});
|
|
7663
|
+
const output = resolve27(outputDir);
|
|
7664
|
+
const physicalOutput = prospectivePath(output);
|
|
7665
|
+
if (physicalOutput === source || physicalOutput.startsWith(source + "/"))
|
|
7666
|
+
throw new Error("Hook output must be outside the plugin source");
|
|
7667
|
+
if (entryExists(output) && (lstatSync24(output).isSymbolicLink() || !lstatSync24(output).isDirectory() || readdirSync24(output).length !== 0)) {
|
|
7668
|
+
throw new Error("Hook output must be absent or an empty directory: " + output);
|
|
7669
|
+
}
|
|
7670
|
+
mkdirSync16(dirname20(output), { recursive: true });
|
|
7671
|
+
const stage = mkdtempSync15(join41(dirname20(output), ".plugnz-hooks-build-"));
|
|
7672
|
+
try {
|
|
7673
|
+
const built = [];
|
|
7674
|
+
for (const adapter of adapters) {
|
|
7675
|
+
const target = join41(stage, adapter.id);
|
|
7676
|
+
copyAuthoredTree(source, target);
|
|
7677
|
+
const files = emitHooksToDirectory(adapter, config, target);
|
|
7678
|
+
built.push({ id: adapter.id, path: join41(output, adapter.id), files });
|
|
7679
|
+
}
|
|
7680
|
+
renameSync14(stage, output);
|
|
7681
|
+
return { version: 1, outputDir: output, targets: built };
|
|
7682
|
+
} finally {
|
|
7683
|
+
rmSync17(stage, { recursive: true, force: true });
|
|
7684
|
+
}
|
|
7685
|
+
}
|
|
7686
|
+
var hookAdapters, RUNTIME_FILE = "hook-runtime.mjs", developmentRuntime;
|
|
7687
|
+
var init_hook_build = __esm(() => {
|
|
7688
|
+
init_hooks();
|
|
7689
|
+
init_claude_code_hooks();
|
|
7690
|
+
init_codex_hooks();
|
|
7691
|
+
init_omp_hooks();
|
|
7692
|
+
init_pi_hooks();
|
|
7693
|
+
init_opencode_hooks();
|
|
7694
|
+
init_zcode_cli_hooks();
|
|
7695
|
+
hookAdapters = [hookAdapter, hookAdapter2, hookAdapter3, hookAdapter4, hookAdapter5, hookAdapter6];
|
|
7696
|
+
});
|
|
7697
|
+
|
|
7698
|
+
// src/hook-cli.ts
|
|
7699
|
+
function runHookCli(args) {
|
|
7700
|
+
try {
|
|
7701
|
+
const verb = args[0];
|
|
7702
|
+
if (verb === "--help" || verb === "-h" || verb === "help") {
|
|
7703
|
+
console.log(USAGE);
|
|
7704
|
+
return 0;
|
|
7705
|
+
}
|
|
7706
|
+
if (verb === "targets") {
|
|
7707
|
+
if (args.length !== 1)
|
|
7708
|
+
throw new Error("targets takes no arguments");
|
|
7709
|
+
console.log(JSON.stringify({
|
|
7710
|
+
version: 1,
|
|
7711
|
+
targets: hookAdapters.map(({ id, baselineVersion, capabilities }) => ({ id, baselineVersion, capabilities }))
|
|
7712
|
+
}, null, 2));
|
|
7713
|
+
return 0;
|
|
7714
|
+
}
|
|
7715
|
+
if (verb !== "build")
|
|
7716
|
+
throw new Error(USAGE);
|
|
7717
|
+
let source;
|
|
7718
|
+
let output;
|
|
7719
|
+
const targets = [];
|
|
7720
|
+
for (let index = 1;index < args.length; index++) {
|
|
7721
|
+
const argument = args[index];
|
|
7722
|
+
if (argument === "--target" || argument === "--out") {
|
|
7723
|
+
const value = args[++index];
|
|
7724
|
+
if (value === undefined || !value.trim() || value.startsWith("-"))
|
|
7725
|
+
throw new Error(argument + " requires a value");
|
|
7726
|
+
if (argument === "--target")
|
|
7727
|
+
targets.push(value);
|
|
7728
|
+
else {
|
|
7729
|
+
if (output !== undefined)
|
|
7730
|
+
throw new Error("--out accepts one directory");
|
|
7731
|
+
output = value;
|
|
7732
|
+
}
|
|
7733
|
+
} else if (argument.startsWith("-"))
|
|
7734
|
+
throw new Error("Unknown hooks build option: " + argument);
|
|
7735
|
+
else {
|
|
7736
|
+
if (source !== undefined)
|
|
7737
|
+
throw new Error("hooks build accepts one plugin directory");
|
|
7738
|
+
source = argument;
|
|
7739
|
+
}
|
|
7740
|
+
}
|
|
7741
|
+
if (source === undefined || !source.trim())
|
|
7742
|
+
throw new Error("hooks build requires a plugin directory");
|
|
7743
|
+
if (output === undefined)
|
|
7744
|
+
throw new Error("hooks build requires --out");
|
|
7745
|
+
console.log(JSON.stringify(buildHooks(source, targets, output), null, 2));
|
|
7746
|
+
return 0;
|
|
7747
|
+
} catch (error) {
|
|
7748
|
+
console.error("plugnz hooks: " + (error instanceof Error ? error.message : String(error)));
|
|
7749
|
+
return 2;
|
|
7750
|
+
}
|
|
7751
|
+
}
|
|
7752
|
+
var USAGE;
|
|
7753
|
+
var init_hook_cli = __esm(() => {
|
|
7754
|
+
init_hook_build();
|
|
7755
|
+
USAGE = [
|
|
7756
|
+
"usage: plugnz hooks build <plugin-dir> --target <id>... --out <output-dir>",
|
|
7757
|
+
" plugnz hooks targets",
|
|
7758
|
+
"",
|
|
7759
|
+
"Builds one native artifact directory at <output-dir>/<target>.",
|
|
7760
|
+
"The source must contain plugnz.hooks.json; source files are not changed.",
|
|
7761
|
+
"The output must be absent or empty and outside the source directory.",
|
|
7762
|
+
"Targets and build results are printed as JSON."
|
|
7763
|
+
].join(`
|
|
7764
|
+
`);
|
|
7765
|
+
});
|
|
7766
|
+
|
|
6948
7767
|
// src/cli.ts
|
|
6949
|
-
import { readFileSync as
|
|
7768
|
+
import { readFileSync as readFileSync34 } from "node:fs";
|
|
6950
7769
|
|
|
6951
7770
|
// src/hosts/claude-code.ts
|
|
6952
7771
|
import { accessSync as accessSync2, constants as constants2, existsSync as existsSync2, statSync as statSync2 } from "node:fs";
|
|
@@ -14126,6 +14945,9 @@ class SemanticInventoryError extends Error {
|
|
|
14126
14945
|
}
|
|
14127
14946
|
function inventoryPackageSemantics(plugin) {
|
|
14128
14947
|
const files = walkFiles(plugin.dir);
|
|
14948
|
+
if (files.includes("plugnz.hooks.json")) {
|
|
14949
|
+
invalid5("Portable hooks must be compiled before installation: use plugnz hooks build <source> --target <host> --out <directory>, then load the generated native package. add/sync do not execute an uncompiled plugnz.hooks.json.");
|
|
14950
|
+
}
|
|
14129
14951
|
const skills = files.filter((path) => path.startsWith("skills/") && path.endsWith("/SKILL.md"));
|
|
14130
14952
|
const componentInventory = inventoryComponentRoots(plugin.dir);
|
|
14131
14953
|
const commands = componentInventory.definitions.filter(({ kind }) => kind === "command").map(({ path }) => path).sort();
|
|
@@ -32462,7 +33284,7 @@ function legacyCommandAction(command) {
|
|
|
32462
33284
|
// package.json
|
|
32463
33285
|
var package_default = {
|
|
32464
33286
|
name: "plugnz",
|
|
32465
|
-
version: "0.0.
|
|
33287
|
+
version: "0.0.16",
|
|
32466
33288
|
description: "Install, diagnose and update agent plugins (Agent Plugins spec) and MCP configs across Claude Code, Codex, Cursor, Kimi and more",
|
|
32467
33289
|
license: "MIT",
|
|
32468
33290
|
type: "module",
|
|
@@ -32496,7 +33318,7 @@ var package_default = {
|
|
|
32496
33318
|
};
|
|
32497
33319
|
|
|
32498
33320
|
// src/cli.ts
|
|
32499
|
-
var
|
|
33321
|
+
var USAGE2 = `plugnz — install, diagnose and update agent plugins and MCP configs
|
|
32500
33322
|
|
|
32501
33323
|
usage: plugnz <verb> [options]
|
|
32502
33324
|
|
|
@@ -32515,6 +33337,8 @@ verbs:
|
|
|
32515
33337
|
retire-source <source-or-scope> end one recorded scope [--target <host>] [--instance <id>] [--dry-run]
|
|
32516
33338
|
scopes [source-or-scope] read recorded scopes [--target <host>] [--instance <id>] [--json]
|
|
32517
33339
|
targets [--all] list detected agent hosts; --all includes frozen consumer profiles
|
|
33340
|
+
hooks build <dir> --target <id>... --out <dir> compile portable hooks into native artifacts
|
|
33341
|
+
hooks targets print portable hook capabilities as JSON
|
|
32518
33342
|
|
|
32519
33343
|
mutation output:
|
|
32520
33344
|
--json schema-v1 lifecycle report
|
|
@@ -33695,7 +34519,7 @@ async function runSync(argv, json) {
|
|
|
33695
34519
|
async function runSyncManifest(manifestPath, dryRun, json) {
|
|
33696
34520
|
let parsed;
|
|
33697
34521
|
try {
|
|
33698
|
-
parsed = JSON.parse(
|
|
34522
|
+
parsed = JSON.parse(readFileSync34(manifestPath, "utf8"));
|
|
33699
34523
|
} catch (error) {
|
|
33700
34524
|
return emitValidatedReport(usageReport("sync", dryRun, unknownErrorDiagnostic(error)), json);
|
|
33701
34525
|
}
|
|
@@ -33848,9 +34672,13 @@ async function main(argv) {
|
|
|
33848
34672
|
return 0;
|
|
33849
34673
|
}
|
|
33850
34674
|
if (verb === undefined || verb === "help" || verb === "--help" || verb === "-h") {
|
|
33851
|
-
console.log(
|
|
34675
|
+
console.log(USAGE2);
|
|
33852
34676
|
return verb === undefined ? 2 : 0;
|
|
33853
34677
|
}
|
|
34678
|
+
if (verb === "hooks") {
|
|
34679
|
+
await Promise.resolve().then(() => init_hook_cli());
|
|
34680
|
+
return runHookCli(args.slice(1));
|
|
34681
|
+
}
|
|
33854
34682
|
if (verb === "doctor") {
|
|
33855
34683
|
const flags = parseFlags(args.slice(1));
|
|
33856
34684
|
if (rejectDisallowed(flags, new Set(["target"])) || flags.positionals.length > 0)
|
|
@@ -34525,7 +35353,7 @@ async function main(argv) {
|
|
|
34525
35353
|
return runScopes(args.slice(1), wantsJson);
|
|
34526
35354
|
fail4(`plugnz: unknown verb '${verb}'
|
|
34527
35355
|
|
|
34528
|
-
${
|
|
35356
|
+
${USAGE2}`, 2);
|
|
34529
35357
|
}
|
|
34530
35358
|
if (process.argv[1] !== undefined && process.argv[1].endsWith("cli.ts")) {
|
|
34531
35359
|
main(process.argv.slice(2)).then((code) => {
|
package/package.json
CHANGED