@kairyou/agent-tools 0.2.0 → 0.4.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.
Files changed (40) hide show
  1. package/README.md +112 -115
  2. package/README.zh-CN.md +108 -115
  3. package/dist/statusline/claude-statusline.mjs +355 -0
  4. package/dist/usage/cli.mjs +25 -0
  5. package/dist/usage/codex-hook.mjs +144 -0
  6. package/dist/usage/core.mjs +1054 -0
  7. package/dist/usage/opencode-plugin.mjs +80 -0
  8. package/dist/usage/opencode-tui.mjs +46 -0
  9. package/dist/vision/cli.mjs +13 -13
  10. package/dist/vision/mcp-server.mjs +12 -12
  11. package/{statusline/claude/statusline.mjs → integrations/statusline/claude-statusline.mjs} +33 -2
  12. package/integrations/usage/cli.mjs +27 -0
  13. package/{hooks/codex/usage-hook.mjs → integrations/usage/codex-hook.mjs} +1 -1
  14. package/{lib/usage.mjs → integrations/usage/core.mjs} +34 -1
  15. package/{plugins/opencode/usage-plugin.mjs → integrations/usage/opencode-plugin.mjs} +1 -1
  16. package/integrations/usage/skills/at-usage/SKILL.md +16 -0
  17. package/{plugins → integrations}/vision/mcp-server.mjs +4 -4
  18. package/package.json +8 -12
  19. package/scripts/build.mjs +65 -0
  20. package/scripts/install.mjs +135 -90
  21. package/scripts/release.mjs +75 -5
  22. package/hooks/claude/.gitkeep +0 -1
  23. package/hooks/codex/.gitkeep +0 -1
  24. package/hooks/common/.gitkeep +0 -1
  25. package/hooks/opencode/.gitkeep +0 -1
  26. package/scripts/build-vision.mjs +0 -35
  27. package/statusline/.gitkeep +0 -1
  28. package/statusline/codex/.gitkeep +0 -1
  29. /package/{plugins/opencode/usage-tui.mjs → integrations/usage/opencode-tui.mjs} +0 -0
  30. /package/{lib/vision → integrations/vision/lib}/cli.mjs +0 -0
  31. /package/{lib/vision → integrations/vision/lib}/config.mjs +0 -0
  32. /package/{lib/vision → integrations/vision/lib}/errors.mjs +0 -0
  33. /package/{lib/vision → integrations/vision/lib}/image-source.mjs +0 -0
  34. /package/{lib/vision → integrations/vision/lib}/inspect.mjs +0 -0
  35. /package/{lib/vision → integrations/vision/lib}/providers/anthropic-compatible.mjs +0 -0
  36. /package/{lib/vision → integrations/vision/lib}/providers/openai-compatible.mjs +0 -0
  37. /package/{lib/vision → integrations/vision/lib}/providers/shared.mjs +0 -0
  38. /package/{lib/vision → integrations/vision/lib}/rate-limit.mjs +0 -0
  39. /package/{lib/vision → integrations/vision/lib}/redact.mjs +0 -0
  40. /package/{plugins → integrations}/vision/skills/at-vision/SKILL.md +0 -0
@@ -0,0 +1,355 @@
1
+ #!/usr/bin/env node
2
+
3
+ // integrations/statusline/claude-statusline.mjs
4
+ import { execFileSync, spawn } from "node:child_process";
5
+ import fs from "node:fs";
6
+ import { basename, dirname, join } from "node:path";
7
+ import { fileURLToPath } from "node:url";
8
+ var SCRIPT_DIR = dirname(fileURLToPath(import.meta.url));
9
+ var AGENT_TOOLS_HOME = process.env.AGENT_TOOLS_HOME || join(SCRIPT_DIR, "..", "..");
10
+ var DEFAULT_CONFIG_FILE = join(AGENT_TOOLS_HOME, "config.jsonc");
11
+ var SNAPSHOT_FILE = join(AGENT_TOOLS_HOME, "cache", "usage-snapshot.json");
12
+ var REFRESH_STATE_FILE = join(AGENT_TOOLS_HOME, "cache", "usage-refresh-state.json");
13
+ var USAGE_RUNTIME = join(AGENT_TOOLS_HOME, "dist", "usage", "core.mjs");
14
+ var DEFAULT_SNAPSHOT_TTL_MS = 6e4;
15
+ var DEFAULT_REFRESH_COOLDOWN_MS = 3e4;
16
+ var DEFAULT_FAILURE_BACKOFF_MS = 12e4;
17
+ var DEFAULT_CONFIG = {
18
+ fields: ["branch", "model", "fiveHour", "week"],
19
+ separator: " | ",
20
+ symbols: {
21
+ branch: "\u2387",
22
+ reset: "\u27F3",
23
+ empty: "\u2013",
24
+ fiveHour: "5h",
25
+ week: "w",
26
+ context: "ctx"
27
+ }
28
+ };
29
+ var FIELD_ALIASES = {
30
+ cwd: "directory",
31
+ dir: "directory",
32
+ five: "fiveHour",
33
+ five_hour: "fiveHour",
34
+ "5h": "fiveHour",
35
+ sevenDay: "week",
36
+ seven_day: "week",
37
+ "7d": "week",
38
+ weekly: "week",
39
+ ctx: "context"
40
+ };
41
+ async function readStdin() {
42
+ const chunks = [];
43
+ for await (const chunk of process.stdin) chunks.push(chunk);
44
+ return Buffer.concat(chunks).toString("utf8");
45
+ }
46
+ function readJsonFile(file) {
47
+ try {
48
+ if (!fs.existsSync(file)) return {};
49
+ const raw = stripTrailingCommas(stripJsonComments(fs.readFileSync(file, "utf8").replace(/^\uFEFF/, "")));
50
+ return raw.trim() ? JSON.parse(raw) : {};
51
+ } catch {
52
+ return {};
53
+ }
54
+ }
55
+ function stripJsonComments(input) {
56
+ let out = "";
57
+ let inString = false;
58
+ let escaped = false;
59
+ for (let i = 0; i < input.length; i++) {
60
+ const ch = input[i];
61
+ const next = input[i + 1];
62
+ if (inString) {
63
+ out += ch;
64
+ escaped = ch === "\\" ? !escaped : false;
65
+ if (ch === '"' && !escaped) inString = false;
66
+ continue;
67
+ }
68
+ if (ch === '"') {
69
+ inString = true;
70
+ out += ch;
71
+ continue;
72
+ }
73
+ if (ch === "/" && next === "/") {
74
+ while (i < input.length && input[i] !== "\n") i++;
75
+ out += "\n";
76
+ continue;
77
+ }
78
+ if (ch === "/" && next === "*") {
79
+ i += 2;
80
+ while (i < input.length && !(input[i] === "*" && input[i + 1] === "/")) i++;
81
+ i++;
82
+ continue;
83
+ }
84
+ out += ch;
85
+ }
86
+ return out;
87
+ }
88
+ function stripTrailingCommas(input) {
89
+ let out = "";
90
+ let inString = false;
91
+ let escaped = false;
92
+ for (let i = 0; i < input.length; i++) {
93
+ const ch = input[i];
94
+ if (inString) {
95
+ out += ch;
96
+ escaped = ch === "\\" ? !escaped : false;
97
+ if (ch === '"' && !escaped) inString = false;
98
+ continue;
99
+ }
100
+ if (ch === '"') {
101
+ inString = true;
102
+ out += ch;
103
+ continue;
104
+ }
105
+ if (ch === ",") {
106
+ let j = i + 1;
107
+ while (j < input.length && /\s/.test(input[j])) j++;
108
+ if (input[j] === "}" || input[j] === "]") continue;
109
+ }
110
+ out += ch;
111
+ }
112
+ return out;
113
+ }
114
+ function parseArgs(argv) {
115
+ const opts = {};
116
+ for (let i = 0; i < argv.length; i++) {
117
+ const arg = argv[i];
118
+ if (arg === "--fields" && argv[i + 1]) {
119
+ opts.fields = argv[++i];
120
+ } else if (arg.startsWith("--fields=")) {
121
+ opts.fields = arg.slice("--fields=".length);
122
+ } else if (arg === "--separator" && argv[i + 1]) {
123
+ opts.separator = argv[++i];
124
+ } else if (arg.startsWith("--separator=")) {
125
+ opts.separator = arg.slice("--separator=".length);
126
+ }
127
+ }
128
+ return opts;
129
+ }
130
+ function splitFields(value) {
131
+ if (Array.isArray(value)) return value;
132
+ if (typeof value !== "string") return null;
133
+ return value.split(/[,\s]+/).map((part) => part.trim()).filter(Boolean);
134
+ }
135
+ function normalizeField(field) {
136
+ return FIELD_ALIASES[field] || field;
137
+ }
138
+ function mergeConfig(cli) {
139
+ const rootConfig = readJsonFile(DEFAULT_CONFIG_FILE);
140
+ const fileConfig = rootConfig.statusline || {};
141
+ const envFields = process.env.AGENT_TOOLS_STATUSLINE_FIELDS;
142
+ const envSeparator = process.env.AGENT_TOOLS_STATUSLINE_SEPARATOR;
143
+ const config = {
144
+ ...DEFAULT_CONFIG,
145
+ ...fileConfig,
146
+ symbols: { ...DEFAULT_CONFIG.symbols, ...fileConfig.symbols || {} }
147
+ };
148
+ const fields = splitFields(cli.fields) || splitFields(envFields) || splitFields(fileConfig.fields) || DEFAULT_CONFIG.fields;
149
+ config.fields = fields.map(normalizeField);
150
+ if (typeof envSeparator === "string") config.separator = envSeparator;
151
+ if (typeof cli.separator === "string") config.separator = cli.separator;
152
+ return config;
153
+ }
154
+ function gitBranch(cwd) {
155
+ try {
156
+ const out = execFileSync("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
157
+ cwd,
158
+ stdio: ["ignore", "pipe", "ignore"],
159
+ encoding: "utf8"
160
+ }).trim();
161
+ return out && out !== "HEAD" ? out : "";
162
+ } catch {
163
+ return "";
164
+ }
165
+ }
166
+ function secondsUntil(unixSeconds) {
167
+ if (!unixSeconds) return null;
168
+ const seconds = Number(unixSeconds) - Math.floor(Date.now() / 1e3);
169
+ return Number.isFinite(seconds) ? Math.max(0, seconds) : null;
170
+ }
171
+ function compactDuration(totalSeconds) {
172
+ if (totalSeconds == null) return "";
173
+ if (totalSeconds <= 0) return "0m";
174
+ let seconds = totalSeconds;
175
+ const days = Math.floor(seconds / 86400);
176
+ seconds %= 86400;
177
+ const hours = Math.floor(seconds / 3600);
178
+ seconds %= 3600;
179
+ const minutes = Math.floor(seconds / 60);
180
+ if (days) return `${days}d${hours}h`;
181
+ if (hours) return `${hours}h${minutes}m`;
182
+ return `${minutes}m`;
183
+ }
184
+ function usageWindow(window, config) {
185
+ if (!window || typeof window.used_percentage !== "number") {
186
+ return "";
187
+ }
188
+ const pct = `${Math.round(window.used_percentage)}%`;
189
+ const left = compactDuration(secondsUntil(window.resets_at));
190
+ return left ? `${pct} ${config.symbols.reset}${left}` : pct;
191
+ }
192
+ function showMissingUsageWindow() {
193
+ const baseUrl = activeRelayBaseUrl();
194
+ return !baseUrl || isOfficialBaseUrl(baseUrl);
195
+ }
196
+ function shortModelName(name) {
197
+ if (!name) return "";
198
+ return String(name).replace(/^Claude\s+/i, "").replace(/\s*\[1m\]\s*$/i, "").trim();
199
+ }
200
+ function renderField(field, data, config) {
201
+ const dir = data?.workspace?.current_dir || data?.cwd || process.cwd() || "";
202
+ const projectDir = data?.workspace?.project_dir || dir;
203
+ switch (field) {
204
+ case "branch": {
205
+ const branch = gitBranch(projectDir);
206
+ return branch ? `${config.symbols.branch} ${branch}` : "";
207
+ }
208
+ case "model":
209
+ return shortModelName(data?.model?.display_name || data?.model?.id || "");
210
+ case "fiveHour": {
211
+ const value = usageWindow(data?.rate_limits?.five_hour, config);
212
+ return value || showMissingUsageWindow() ? `${config.symbols.fiveHour} ${value || config.symbols.empty}` : "";
213
+ }
214
+ case "week": {
215
+ const value = usageWindow(data?.rate_limits?.seven_day, config);
216
+ return value || showMissingUsageWindow() ? `${config.symbols.week} ${value || config.symbols.empty}` : "";
217
+ }
218
+ case "context": {
219
+ const pct = data?.context_window?.used_percentage;
220
+ return typeof pct === "number" ? `${config.symbols.context} ${Math.round(pct)}%` : "";
221
+ }
222
+ case "directory":
223
+ return dir ? basename(dir) : "";
224
+ default:
225
+ return "";
226
+ }
227
+ }
228
+ function numberFromEnv(name, fallback) {
229
+ const value = Number(process.env[name]);
230
+ return Number.isFinite(value) && value >= 0 ? value : fallback;
231
+ }
232
+ function cleanBaseUrl(baseUrl) {
233
+ return String(baseUrl || "").replace(/\/+$/, "");
234
+ }
235
+ function isOfficialBaseUrl(baseUrl) {
236
+ if (!baseUrl) return true;
237
+ const clean = cleanBaseUrl(baseUrl);
238
+ return [
239
+ "https://api.anthropic.com",
240
+ "https://api.anthropic.com/v1",
241
+ "https://api.openai.com",
242
+ "https://api.openai.com/v1"
243
+ ].includes(clean);
244
+ }
245
+ function usageRouteCacheKey(baseUrl) {
246
+ try {
247
+ const url = new URL(cleanBaseUrl(baseUrl));
248
+ url.hash = "";
249
+ url.search = "";
250
+ url.pathname = url.pathname.replace(/\/+$/, "").replace(/\/api\/v1$/i, "").replace(/\/v1$/i, "");
251
+ return url.toString().replace(/\/$/, "");
252
+ } catch {
253
+ return cleanBaseUrl(baseUrl).endsWith("/v1") ? cleanBaseUrl(baseUrl).slice(0, -3) : cleanBaseUrl(baseUrl);
254
+ }
255
+ }
256
+ function readJsonFileRaw(file) {
257
+ try {
258
+ if (!fs.existsSync(file)) return {};
259
+ const raw = fs.readFileSync(file, "utf8").replace(/^\uFEFF/, "");
260
+ return raw.trim() ? JSON.parse(raw) : {};
261
+ } catch {
262
+ return {};
263
+ }
264
+ }
265
+ function activeRelayBaseUrl() {
266
+ return process.env.PROVIDER_USAGE_BASE_URL || process.env.ANTHROPIC_BASE_URL || "";
267
+ }
268
+ function hasClaudeUsageToken() {
269
+ return Boolean(
270
+ process.env.PROVIDER_USAGE_API_KEY || process.env.ANTHROPIC_AUTH_TOKEN || process.env.ANTHROPIC_API_KEY
271
+ );
272
+ }
273
+ function snapshotForBaseUrl(baseUrl) {
274
+ const snapshot = readJsonFileRaw(SNAPSHOT_FILE);
275
+ const key = usageRouteCacheKey(baseUrl);
276
+ const item = snapshot?.items?.[key];
277
+ return item?.text ? item : null;
278
+ }
279
+ function refreshStateForBaseUrl(baseUrl) {
280
+ const state = readJsonFileRaw(REFRESH_STATE_FILE);
281
+ return state?.items?.[usageRouteCacheKey(baseUrl)] || {};
282
+ }
283
+ function ageMs(isoDate) {
284
+ const time = Date.parse(isoDate || "");
285
+ return Number.isFinite(time) ? Date.now() - time : Number.POSITIVE_INFINITY;
286
+ }
287
+ function writeRefreshAttempt(baseUrl) {
288
+ try {
289
+ const state = readJsonFileRaw(REFRESH_STATE_FILE);
290
+ const key = usageRouteCacheKey(baseUrl);
291
+ state.version = 1;
292
+ state.items = state.items && typeof state.items === "object" ? state.items : {};
293
+ state.items[key] = {
294
+ ...state.items[key] || {},
295
+ baseUrl,
296
+ lastAttemptAt: (/* @__PURE__ */ new Date()).toISOString()
297
+ };
298
+ fs.mkdirSync(dirname(REFRESH_STATE_FILE), { recursive: true });
299
+ fs.writeFileSync(REFRESH_STATE_FILE, `${JSON.stringify(state, null, 2)}
300
+ `);
301
+ } catch {
302
+ }
303
+ }
304
+ function shouldRefreshUsage(baseUrl, snapshot) {
305
+ if (process.env.AGENT_TOOLS_USAGE_REFRESH === "0") return false;
306
+ if (!baseUrl || isOfficialBaseUrl(baseUrl)) return false;
307
+ if (!hasClaudeUsageToken()) return false;
308
+ if (!fs.existsSync(USAGE_RUNTIME)) return false;
309
+ const ttlMs = numberFromEnv("AGENT_TOOLS_USAGE_SNAPSHOT_TTL_MS", DEFAULT_SNAPSHOT_TTL_MS);
310
+ const cooldownMs = numberFromEnv("AGENT_TOOLS_USAGE_REFRESH_COOLDOWN_MS", DEFAULT_REFRESH_COOLDOWN_MS);
311
+ const failureBackoffMs = numberFromEnv("AGENT_TOOLS_USAGE_FAILURE_BACKOFF_MS", DEFAULT_FAILURE_BACKOFF_MS);
312
+ const state = refreshStateForBaseUrl(baseUrl);
313
+ if (ageMs(state.lastAttemptAt) < cooldownMs) return false;
314
+ if (state.lastError && ageMs(state.lastFailureAt) < failureBackoffMs) return false;
315
+ return !snapshot || ageMs(snapshot.updatedAt) >= ttlMs;
316
+ }
317
+ function refreshUsageInBackground(baseUrl) {
318
+ if (!shouldRefreshUsage(baseUrl, snapshotForBaseUrl(baseUrl))) return;
319
+ writeRefreshAttempt(baseUrl);
320
+ try {
321
+ const child = spawn(process.execPath, [USAGE_RUNTIME, "refresh", "--agent", "claude"], {
322
+ detached: true,
323
+ stdio: "ignore",
324
+ env: process.env,
325
+ windowsHide: true
326
+ });
327
+ child.unref();
328
+ } catch {
329
+ }
330
+ }
331
+ function providerUsageStatus() {
332
+ const baseUrl = activeRelayBaseUrl();
333
+ if (!baseUrl || isOfficialBaseUrl(baseUrl)) return "";
334
+ const snapshot = snapshotForBaseUrl(baseUrl);
335
+ if (shouldRefreshUsage(baseUrl, snapshot)) refreshUsageInBackground(baseUrl);
336
+ return snapshot?.text || "";
337
+ }
338
+ function render(data, config) {
339
+ const fields = config.fields.map((field) => renderField(field, data, config)).filter(Boolean);
340
+ const providerUsage = providerUsageStatus();
341
+ if (providerUsage) fields.push(providerUsage);
342
+ return fields.join(config.separator);
343
+ }
344
+ async function main() {
345
+ const config = mergeConfig(parseArgs(process.argv.slice(2)));
346
+ let data = {};
347
+ try {
348
+ const raw = await readStdin();
349
+ data = raw.trim() ? JSON.parse(raw) : {};
350
+ } catch {
351
+ data = {};
352
+ }
353
+ process.stdout.write(render(data, config));
354
+ }
355
+ main();
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env node
2
+
3
+ // integrations/usage/cli.mjs
4
+ import { queryAgentProviderUsage } from "./core.mjs";
5
+ function parseAgent(argv) {
6
+ for (let index = 0; index < argv.length; index += 1) {
7
+ const arg = argv[index];
8
+ if (arg === "--agent" && argv[index + 1]) return argv[index + 1];
9
+ if (arg.startsWith("--agent=")) return arg.slice("--agent=".length);
10
+ }
11
+ return null;
12
+ }
13
+ var agent = parseAgent(process.argv.slice(2));
14
+ if (agent !== "claude" && agent !== "codex") {
15
+ process.stderr.write(agent ? `Unsupported agent: ${agent}
16
+ ` : "Missing --agent <claude|codex>\n");
17
+ process.exitCode = 2;
18
+ } else {
19
+ try {
20
+ const result = await queryAgentProviderUsage(agent);
21
+ if (result?.text) process.stdout.write(`${result.text}
22
+ `);
23
+ } catch {
24
+ }
25
+ }
@@ -0,0 +1,144 @@
1
+ #!/usr/bin/env node
2
+
3
+ // integrations/usage/codex-hook.mjs
4
+ import fs from "node:fs";
5
+ import os from "node:os";
6
+ import path from "node:path";
7
+ import { spawn } from "node:child_process";
8
+ import { fileURLToPath } from "node:url";
9
+ var SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
10
+ var AGENT_TOOLS_HOME = process.env.AGENT_TOOLS_HOME || path.resolve(SCRIPT_DIR, "..", "..");
11
+ var USAGE_SCRIPT = path.join(SCRIPT_DIR, "core.mjs");
12
+ var LOG_PATH = path.join(AGENT_TOOLS_HOME, "logs", "usage-hook.log");
13
+ var TIMEOUT_MS = Number(process.env.AGENT_TOOLS_USAGE_HOOK_TIMEOUT_MS || 4500);
14
+ var MAX_LOG_BYTES = Number(process.env.AGENT_TOOLS_USAGE_HOOK_LOG_BYTES || 256 * 1024);
15
+ var KEEP_LOG_BYTES = 128 * 1024;
16
+ function hookOut(message) {
17
+ const payload = { continue: true };
18
+ if (message) payload.systemMessage = message;
19
+ process.stdout.write(`${JSON.stringify(payload)}
20
+ `);
21
+ }
22
+ function preview(text) {
23
+ return String(text || "").replace(/\s+/g, " ").trim().slice(0, 500);
24
+ }
25
+ function logFailure(event) {
26
+ try {
27
+ fs.mkdirSync(path.dirname(LOG_PATH), { recursive: true });
28
+ rotateLogIfNeeded();
29
+ fs.appendFileSync(
30
+ LOG_PATH,
31
+ `${JSON.stringify({ at: (/* @__PURE__ */ new Date()).toISOString(), ...event })}
32
+ `
33
+ );
34
+ } catch {
35
+ }
36
+ }
37
+ function rotateLogIfNeeded() {
38
+ const maxBytes = Number.isFinite(MAX_LOG_BYTES) && MAX_LOG_BYTES > 0 ? MAX_LOG_BYTES : 256 * 1024;
39
+ if (!fs.existsSync(LOG_PATH)) return;
40
+ const stat = fs.statSync(LOG_PATH);
41
+ if (stat.size <= maxBytes) return;
42
+ const keepBytes = Math.min(KEEP_LOG_BYTES, Math.floor(maxBytes / 2));
43
+ const fd = fs.openSync(LOG_PATH, "r");
44
+ try {
45
+ const buffer = Buffer.alloc(keepBytes);
46
+ fs.readSync(fd, buffer, 0, keepBytes, Math.max(0, stat.size - keepBytes));
47
+ fs.writeFileSync(
48
+ LOG_PATH,
49
+ `${JSON.stringify({ at: (/* @__PURE__ */ new Date()).toISOString(), reason: "log rotated", previousBytes: stat.size })}
50
+ ` + buffer.toString("utf8").replace(/^[^\n]*\n?/, "")
51
+ );
52
+ } finally {
53
+ fs.closeSync(fd);
54
+ }
55
+ }
56
+ function failureMessage() {
57
+ return `API usage hook failed; see ${LOG_PATH.replace(/\\/g, "/")}`;
58
+ }
59
+ function parseHookJson(stdout) {
60
+ const text = stdout.trim();
61
+ if (!text) return { continue: true };
62
+ return JSON.parse(text);
63
+ }
64
+ async function runUsageScript() {
65
+ if (!fs.existsSync(USAGE_SCRIPT)) {
66
+ logFailure({ reason: "missing usage script", usageScript: USAGE_SCRIPT });
67
+ hookOut(failureMessage());
68
+ return;
69
+ }
70
+ const result = await new Promise((resolve) => {
71
+ const child = spawn(process.execPath, [USAGE_SCRIPT, "hook", "--agent", "codex"], {
72
+ cwd: process.cwd(),
73
+ env: process.env,
74
+ stdio: ["ignore", "pipe", "pipe"],
75
+ windowsHide: true
76
+ });
77
+ let stdout = "";
78
+ let stderr = "";
79
+ let settled = false;
80
+ const timeout = setTimeout(() => {
81
+ if (settled) return;
82
+ settled = true;
83
+ child.kill();
84
+ resolve({ status: null, signal: "timeout", stdout, stderr });
85
+ }, Number.isFinite(TIMEOUT_MS) && TIMEOUT_MS > 0 ? TIMEOUT_MS : 4500);
86
+ child.stdout.on("data", (chunk) => {
87
+ stdout += chunk;
88
+ });
89
+ child.stderr.on("data", (chunk) => {
90
+ stderr += chunk;
91
+ });
92
+ child.on("error", (error) => {
93
+ if (settled) return;
94
+ settled = true;
95
+ clearTimeout(timeout);
96
+ resolve({ status: null, error, stdout, stderr });
97
+ });
98
+ child.on("exit", (status, signal) => {
99
+ if (settled) return;
100
+ settled = true;
101
+ clearTimeout(timeout);
102
+ resolve({ status, signal, stdout, stderr });
103
+ });
104
+ });
105
+ if (result.status !== 0) {
106
+ logFailure({
107
+ reason: "usage script exited non-zero",
108
+ status: result.status,
109
+ signal: result.signal || "",
110
+ error: result.error?.message || "",
111
+ stdout: preview(result.stdout),
112
+ stderr: preview(result.stderr),
113
+ usageScript: USAGE_SCRIPT,
114
+ node: process.version,
115
+ platform: `${process.platform} ${os.release()}`
116
+ });
117
+ hookOut(failureMessage());
118
+ return;
119
+ }
120
+ try {
121
+ const payload = parseHookJson(result.stdout);
122
+ process.stdout.write(`${JSON.stringify(payload)}
123
+ `);
124
+ } catch (error) {
125
+ logFailure({
126
+ reason: "usage script returned invalid hook JSON",
127
+ error: error.message,
128
+ stdout: preview(result.stdout),
129
+ stderr: preview(result.stderr),
130
+ usageScript: USAGE_SCRIPT
131
+ });
132
+ hookOut(failureMessage());
133
+ }
134
+ }
135
+ try {
136
+ await runUsageScript();
137
+ } catch (error) {
138
+ logFailure({
139
+ reason: "wrapper exception",
140
+ error: error?.stack || error?.message || String(error),
141
+ usageScript: USAGE_SCRIPT
142
+ });
143
+ hookOut(failureMessage());
144
+ }