jinzd-ai-cli 0.4.217 → 0.4.219
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 +5 -2
- package/dist/{batch-YCOVKTXD.js → batch-CHAPYRVM.js} +2 -2
- package/dist/chunk-3I4RZYYO.js +638 -0
- package/dist/{chunk-KOPUCJXM.js → chunk-4ZN6L6K5.js} +3 -3
- package/dist/{chunk-VTH7BLXK.js → chunk-64NUW3WL.js} +1 -1
- package/dist/{chunk-QAYOI57M.js → chunk-EGMORH5W.js} +1 -1
- package/dist/{chunk-MGBMNCHG.js → chunk-GEN4O5QH.js} +94 -270
- package/dist/chunk-GGKQHPB3.js +604 -0
- package/dist/{chunk-WKOQ5CYC.js → chunk-KIEZBTVD.js} +1 -1
- package/dist/{chunk-OUAZOE5U.js → chunk-QUFYBR6Q.js} +47 -206
- package/dist/{chunk-XJGEQIYS.js → chunk-U7KVU25H.js} +1 -1
- package/dist/{chunk-WZ3VKLF3.js → chunk-VADHBW7T.js} +1 -1
- package/dist/{ci-52RZIYWB.js → ci-QPP66T53.js} +4 -4
- package/dist/{ci-format-73UXKE65.js → ci-format-5S3EEYRK.js} +2 -2
- package/dist/{constants-DIXAD35W.js → constants-FJOLBABC.js} +1 -1
- package/dist/doctor-cli-KB2LUDF3.js +17 -0
- package/dist/electron-server.js +921 -274
- package/dist/{hub-GIGBITZN.js → hub-DIM7SKKY.js} +1 -1
- package/dist/index.js +192 -200
- package/dist/{pr-KPQ5RPKB.js → pr-YQGH72N6.js} +4 -4
- package/dist/{run-tests-ZDSA3QES.js → run-tests-GEZSSNJM.js} +2 -2
- package/dist/{run-tests-K7QR5QN4.js → run-tests-O76JIBCW.js} +1 -1
- package/dist/{server-QT3SC2KI.js → server-5E2AIXVX.js} +108 -80
- package/dist/{server-22YF3U34.js → server-Z6O3G2LY.js} +9 -9
- package/dist/{task-orchestrator-BHQQCVTY.js → task-orchestrator-5HBW4O64.js} +9 -9
- package/dist/{usage-WZZFSFLM.js → usage-BMK6M5U3.js} +2 -2
- package/package.json +1 -1
- package/dist/chunk-524WZOKS.js +0 -166
- package/dist/chunk-SNJAOXFT.js +0 -129
- package/dist/doctor-cli-7GOQPULZ.js +0 -226
|
@@ -0,0 +1,604 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
CONFIG_DIR_NAME,
|
|
4
|
+
PLUGINS_DIR_NAME
|
|
5
|
+
} from "./chunk-U7KVU25H.js";
|
|
6
|
+
import {
|
|
7
|
+
atomicWriteFileSync
|
|
8
|
+
} from "./chunk-IW3Q7AE5.js";
|
|
9
|
+
|
|
10
|
+
// src/diagnostics/tool-stats.ts
|
|
11
|
+
import { existsSync, readFileSync, mkdirSync } from "fs";
|
|
12
|
+
import { join, dirname } from "path";
|
|
13
|
+
import { homedir } from "os";
|
|
14
|
+
var STATS_FILE_NAME = "tool-stats.json";
|
|
15
|
+
var STATS_VERSION = 1;
|
|
16
|
+
var FLUSH_EVERY_N = 50;
|
|
17
|
+
var state = null;
|
|
18
|
+
var dirty = false;
|
|
19
|
+
var pendingWrites = 0;
|
|
20
|
+
var configDirOverride;
|
|
21
|
+
function statsFilePath() {
|
|
22
|
+
const base = configDirOverride ?? join(homedir(), CONFIG_DIR_NAME);
|
|
23
|
+
return join(base, STATS_FILE_NAME);
|
|
24
|
+
}
|
|
25
|
+
function load() {
|
|
26
|
+
const path = statsFilePath();
|
|
27
|
+
if (!existsSync(path)) {
|
|
28
|
+
return { version: STATS_VERSION, startedAt: (/* @__PURE__ */ new Date()).toISOString(), entries: {} };
|
|
29
|
+
}
|
|
30
|
+
try {
|
|
31
|
+
const raw = JSON.parse(readFileSync(path, "utf-8"));
|
|
32
|
+
if (raw.version !== STATS_VERSION || !raw.entries) {
|
|
33
|
+
return { version: STATS_VERSION, startedAt: (/* @__PURE__ */ new Date()).toISOString(), entries: {} };
|
|
34
|
+
}
|
|
35
|
+
return raw;
|
|
36
|
+
} catch {
|
|
37
|
+
return { version: STATS_VERSION, startedAt: (/* @__PURE__ */ new Date()).toISOString(), entries: {} };
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function ensureLoaded() {
|
|
41
|
+
if (state === null) state = load();
|
|
42
|
+
return state;
|
|
43
|
+
}
|
|
44
|
+
function recordToolCall(name, durationMs, ok, errorMessage) {
|
|
45
|
+
const s = ensureLoaded();
|
|
46
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
47
|
+
let entry = s.entries[name];
|
|
48
|
+
if (!entry) {
|
|
49
|
+
entry = {
|
|
50
|
+
name,
|
|
51
|
+
calls: 0,
|
|
52
|
+
failures: 0,
|
|
53
|
+
totalDurationMs: 0,
|
|
54
|
+
firstSeenAt: now,
|
|
55
|
+
lastUsedAt: now
|
|
56
|
+
};
|
|
57
|
+
s.entries[name] = entry;
|
|
58
|
+
}
|
|
59
|
+
entry.calls += 1;
|
|
60
|
+
entry.totalDurationMs += Math.max(0, Math.round(durationMs));
|
|
61
|
+
entry.lastUsedAt = now;
|
|
62
|
+
if (!ok) {
|
|
63
|
+
entry.failures += 1;
|
|
64
|
+
entry.lastFailureAt = now;
|
|
65
|
+
if (errorMessage) entry.lastFailureMessage = errorMessage.slice(0, 300);
|
|
66
|
+
}
|
|
67
|
+
dirty = true;
|
|
68
|
+
pendingWrites += 1;
|
|
69
|
+
if (pendingWrites >= FLUSH_EVERY_N) {
|
|
70
|
+
flush();
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
async function runTool(tool, args, name) {
|
|
74
|
+
const start = Date.now();
|
|
75
|
+
try {
|
|
76
|
+
const result = await tool.execute(args);
|
|
77
|
+
recordToolCall(name, Date.now() - start, true);
|
|
78
|
+
return result;
|
|
79
|
+
} catch (err) {
|
|
80
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
81
|
+
recordToolCall(name, Date.now() - start, false, msg);
|
|
82
|
+
throw err;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function flush() {
|
|
86
|
+
if (!dirty || state === null) return;
|
|
87
|
+
const path = statsFilePath();
|
|
88
|
+
try {
|
|
89
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
90
|
+
atomicWriteFileSync(path, JSON.stringify(state, null, 2));
|
|
91
|
+
dirty = false;
|
|
92
|
+
pendingWrites = 0;
|
|
93
|
+
} catch {
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
function getStatsSnapshot() {
|
|
97
|
+
const s = ensureLoaded();
|
|
98
|
+
return Object.values(s.entries).map((e) => ({ ...e }));
|
|
99
|
+
}
|
|
100
|
+
function getTopFailingTools(limit = 5) {
|
|
101
|
+
return getStatsSnapshot().filter((e) => e.failures > 0).sort((a, b) => {
|
|
102
|
+
const rateA = a.failures / Math.max(1, a.calls);
|
|
103
|
+
const rateB = b.failures / Math.max(1, b.calls);
|
|
104
|
+
if (rateB !== rateA) return rateB - rateA;
|
|
105
|
+
return b.failures - a.failures;
|
|
106
|
+
}).slice(0, limit);
|
|
107
|
+
}
|
|
108
|
+
function getTopUsedTools(limit = 5) {
|
|
109
|
+
return getStatsSnapshot().sort((a, b) => b.calls - a.calls).slice(0, limit);
|
|
110
|
+
}
|
|
111
|
+
function resetStats() {
|
|
112
|
+
state = { version: STATS_VERSION, startedAt: (/* @__PURE__ */ new Date()).toISOString(), entries: {} };
|
|
113
|
+
dirty = true;
|
|
114
|
+
flush();
|
|
115
|
+
}
|
|
116
|
+
var exitHookInstalled = false;
|
|
117
|
+
function installFlushOnExit() {
|
|
118
|
+
if (exitHookInstalled) return;
|
|
119
|
+
exitHookInstalled = true;
|
|
120
|
+
process.on("exit", () => flush());
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// src/plugins/plugin-manager.ts
|
|
124
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readdirSync, readFileSync as readFileSync2, cpSync, rmSync, statSync } from "fs";
|
|
125
|
+
import { basename, dirname as dirname2, join as join2, resolve } from "path";
|
|
126
|
+
import { createHash } from "crypto";
|
|
127
|
+
import { z } from "zod";
|
|
128
|
+
var MANIFEST_RELATIVE = ".aicli-plugin/plugin.json";
|
|
129
|
+
var STATE_FILE = "plugin-state.json";
|
|
130
|
+
var HookCommandSchema = z.union([
|
|
131
|
+
z.string(),
|
|
132
|
+
z.object({
|
|
133
|
+
command: z.string(),
|
|
134
|
+
source: z.enum(["user", "project", "managed"]).optional(),
|
|
135
|
+
description: z.string().optional(),
|
|
136
|
+
required: z.boolean().optional(),
|
|
137
|
+
timeoutMs: z.number().int().min(100).max(3e4).optional(),
|
|
138
|
+
disabled: z.boolean().optional()
|
|
139
|
+
}),
|
|
140
|
+
z.array(z.union([
|
|
141
|
+
z.string(),
|
|
142
|
+
z.object({
|
|
143
|
+
command: z.string(),
|
|
144
|
+
source: z.enum(["user", "project", "managed"]).optional(),
|
|
145
|
+
description: z.string().optional(),
|
|
146
|
+
required: z.boolean().optional(),
|
|
147
|
+
timeoutMs: z.number().int().min(100).max(3e4).optional(),
|
|
148
|
+
disabled: z.boolean().optional()
|
|
149
|
+
})
|
|
150
|
+
]))
|
|
151
|
+
]);
|
|
152
|
+
var HookEventNameSchema = z.enum([
|
|
153
|
+
"SessionStart",
|
|
154
|
+
"UserPromptSubmit",
|
|
155
|
+
"PreToolUse",
|
|
156
|
+
"PermissionRequest",
|
|
157
|
+
"PostToolUse",
|
|
158
|
+
"PreCompact",
|
|
159
|
+
"PostCompact",
|
|
160
|
+
"Stop",
|
|
161
|
+
"SubagentStart",
|
|
162
|
+
"SubagentStop"
|
|
163
|
+
]);
|
|
164
|
+
var McpServerSchema = z.object({
|
|
165
|
+
command: z.string(),
|
|
166
|
+
args: z.array(z.string()).default([]),
|
|
167
|
+
env: z.record(z.string()).optional(),
|
|
168
|
+
timeout: z.number().default(3e4)
|
|
169
|
+
});
|
|
170
|
+
var PluginManifestSchema = z.object({
|
|
171
|
+
name: z.string().regex(/^[a-zA-Z0-9._-]{1,80}$/),
|
|
172
|
+
version: z.string().min(1),
|
|
173
|
+
description: z.string().optional(),
|
|
174
|
+
skills: z.array(z.string()).default([]),
|
|
175
|
+
hooks: z.object({
|
|
176
|
+
enabled: z.boolean().optional(),
|
|
177
|
+
events: z.record(HookEventNameSchema, HookCommandSchema).default({})
|
|
178
|
+
}).default({ events: {} }),
|
|
179
|
+
commands: z.array(z.string()).default([]),
|
|
180
|
+
mcpServers: z.record(McpServerSchema).default({}),
|
|
181
|
+
agents: z.array(z.string()).default([]),
|
|
182
|
+
permissionHints: z.array(z.string()).default([])
|
|
183
|
+
}).strict();
|
|
184
|
+
function pluginRoot(configDir) {
|
|
185
|
+
return join2(configDir, PLUGINS_DIR_NAME);
|
|
186
|
+
}
|
|
187
|
+
function pluginManifestPath(pluginDir) {
|
|
188
|
+
return join2(pluginDir, MANIFEST_RELATIVE);
|
|
189
|
+
}
|
|
190
|
+
function statePath(configDir) {
|
|
191
|
+
return join2(pluginRoot(configDir), STATE_FILE);
|
|
192
|
+
}
|
|
193
|
+
function loadState(configDir) {
|
|
194
|
+
const file = statePath(configDir);
|
|
195
|
+
if (!existsSync2(file)) return { version: 1, plugins: [] };
|
|
196
|
+
try {
|
|
197
|
+
const parsed = JSON.parse(readFileSync2(file, "utf-8"));
|
|
198
|
+
return { version: 1, plugins: Array.isArray(parsed.plugins) ? parsed.plugins : [] };
|
|
199
|
+
} catch {
|
|
200
|
+
return { version: 1, plugins: [] };
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
function saveState(configDir, state2) {
|
|
204
|
+
mkdirSync2(pluginRoot(configDir), { recursive: true });
|
|
205
|
+
atomicWriteFileSync(statePath(configDir), JSON.stringify(state2, null, 2));
|
|
206
|
+
}
|
|
207
|
+
function hashPluginManifest(manifestPath) {
|
|
208
|
+
return createHash("sha256").update(readFileSync2(manifestPath)).digest("hex");
|
|
209
|
+
}
|
|
210
|
+
function readPluginManifest(manifestPath) {
|
|
211
|
+
return PluginManifestSchema.parse(JSON.parse(readFileSync2(manifestPath, "utf-8")));
|
|
212
|
+
}
|
|
213
|
+
function findPluginSourceDir(inputPath) {
|
|
214
|
+
const abs = resolve(inputPath);
|
|
215
|
+
const direct = statSync(abs).isDirectory() ? abs : dirname2(abs);
|
|
216
|
+
if (existsSync2(pluginManifestPath(direct))) return direct;
|
|
217
|
+
const nested = join2(direct, ".aicli-plugin");
|
|
218
|
+
if (existsSync2(join2(nested, "plugin.json"))) return direct;
|
|
219
|
+
throw new Error(`plugin manifest not found: ${MANIFEST_RELATIVE}`);
|
|
220
|
+
}
|
|
221
|
+
function installPlugin(configDir, inputPath) {
|
|
222
|
+
const sourceDir = findPluginSourceDir(inputPath);
|
|
223
|
+
const manifest = readPluginManifest(pluginManifestPath(sourceDir));
|
|
224
|
+
const targetDir = join2(pluginRoot(configDir), manifest.name);
|
|
225
|
+
mkdirSync2(pluginRoot(configDir), { recursive: true });
|
|
226
|
+
if (existsSync2(targetDir)) rmSync(targetDir, { recursive: true, force: true });
|
|
227
|
+
cpSync(sourceDir, targetDir, { recursive: true });
|
|
228
|
+
const manifestPath = pluginManifestPath(targetDir);
|
|
229
|
+
const hash = hashPluginManifest(manifestPath);
|
|
230
|
+
const state2 = loadState(configDir);
|
|
231
|
+
const previous = state2.plugins.find((p) => p.name === manifest.name);
|
|
232
|
+
const next = {
|
|
233
|
+
name: manifest.name,
|
|
234
|
+
enabled: previous?.enabled ?? false,
|
|
235
|
+
trusted: false,
|
|
236
|
+
hash,
|
|
237
|
+
installedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
238
|
+
};
|
|
239
|
+
state2.plugins = [...state2.plugins.filter((p) => p.name !== manifest.name), next];
|
|
240
|
+
saveState(configDir, state2);
|
|
241
|
+
return { name: manifest.name, dir: targetDir, manifestPath, manifest, hash, enabled: next.enabled, trusted: false, valid: true };
|
|
242
|
+
}
|
|
243
|
+
function setPluginEnabled(configDir, name, enabled) {
|
|
244
|
+
const plugin = getInstalledPlugin(configDir, name);
|
|
245
|
+
if (!plugin.valid) throw new Error(plugin.error ?? `invalid plugin: ${name}`);
|
|
246
|
+
const state2 = loadState(configDir);
|
|
247
|
+
const current = state2.plugins.find((p) => p.name === plugin.name) ?? {
|
|
248
|
+
name: plugin.name,
|
|
249
|
+
enabled: false,
|
|
250
|
+
trusted: false,
|
|
251
|
+
hash: plugin.hash,
|
|
252
|
+
installedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
253
|
+
};
|
|
254
|
+
const next = { ...current, enabled, hash: plugin.hash, enabledAt: enabled ? (/* @__PURE__ */ new Date()).toISOString() : current.enabledAt };
|
|
255
|
+
state2.plugins = [...state2.plugins.filter((p) => p.name !== plugin.name), next];
|
|
256
|
+
saveState(configDir, state2);
|
|
257
|
+
return next;
|
|
258
|
+
}
|
|
259
|
+
function trustPlugin(configDir, name) {
|
|
260
|
+
const plugin = getInstalledPlugin(configDir, name);
|
|
261
|
+
if (!plugin.valid) throw new Error(plugin.error ?? `invalid plugin: ${name}`);
|
|
262
|
+
const state2 = loadState(configDir);
|
|
263
|
+
const current = state2.plugins.find((p) => p.name === plugin.name) ?? {
|
|
264
|
+
name: plugin.name,
|
|
265
|
+
enabled: false,
|
|
266
|
+
trusted: false,
|
|
267
|
+
hash: plugin.hash,
|
|
268
|
+
installedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
269
|
+
};
|
|
270
|
+
const next = { ...current, trusted: true, hash: plugin.hash, trustedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
271
|
+
state2.plugins = [...state2.plugins.filter((p) => p.name !== plugin.name), next];
|
|
272
|
+
saveState(configDir, state2);
|
|
273
|
+
return next;
|
|
274
|
+
}
|
|
275
|
+
function getInstalledPlugin(configDir, name) {
|
|
276
|
+
const plugins = listInstalledPlugins(configDir);
|
|
277
|
+
const matches = plugins.filter((p) => p.name === name || p.name.startsWith(name));
|
|
278
|
+
if (matches.length === 0) throw new Error(`plugin not found: ${name}`);
|
|
279
|
+
if (matches.length > 1) throw new Error(`plugin name is ambiguous: ${name}`);
|
|
280
|
+
return matches[0];
|
|
281
|
+
}
|
|
282
|
+
function listInstalledPlugins(configDir) {
|
|
283
|
+
const root = pluginRoot(configDir);
|
|
284
|
+
if (!existsSync2(root)) return [];
|
|
285
|
+
const state2 = loadState(configDir);
|
|
286
|
+
const out = [];
|
|
287
|
+
for (const entry of readdirSync(root)) {
|
|
288
|
+
const dir = join2(root, entry);
|
|
289
|
+
try {
|
|
290
|
+
if (!statSync(dir).isDirectory()) continue;
|
|
291
|
+
} catch {
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
const manifestPath = pluginManifestPath(dir);
|
|
295
|
+
if (!existsSync2(manifestPath)) continue;
|
|
296
|
+
try {
|
|
297
|
+
const manifest = readPluginManifest(manifestPath);
|
|
298
|
+
const hash = hashPluginManifest(manifestPath);
|
|
299
|
+
const st = state2.plugins.find((p) => p.name === manifest.name);
|
|
300
|
+
const trusted = st?.trusted === true && st.hash === hash;
|
|
301
|
+
out.push({
|
|
302
|
+
name: manifest.name,
|
|
303
|
+
dir,
|
|
304
|
+
manifestPath,
|
|
305
|
+
manifest,
|
|
306
|
+
hash,
|
|
307
|
+
enabled: st?.enabled === true,
|
|
308
|
+
trusted,
|
|
309
|
+
valid: true
|
|
310
|
+
});
|
|
311
|
+
} catch (err) {
|
|
312
|
+
out.push({
|
|
313
|
+
name: entry,
|
|
314
|
+
dir,
|
|
315
|
+
manifestPath,
|
|
316
|
+
manifest: { name: entry, version: "invalid", skills: [], commands: [], agents: [], hooks: { events: {} }, mcpServers: {}, permissionHints: [] },
|
|
317
|
+
hash: "",
|
|
318
|
+
enabled: false,
|
|
319
|
+
trusted: false,
|
|
320
|
+
valid: false,
|
|
321
|
+
error: err instanceof Error ? err.message : String(err)
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
return out.sort((a, b) => a.name.localeCompare(b.name));
|
|
326
|
+
}
|
|
327
|
+
function activePlugins(configDir) {
|
|
328
|
+
return listInstalledPlugins(configDir).filter((p) => p.valid && p.enabled && p.trusted);
|
|
329
|
+
}
|
|
330
|
+
function uniqueDirs(plugin, rels) {
|
|
331
|
+
const dirs = /* @__PURE__ */ new Set();
|
|
332
|
+
for (const rel of rels) {
|
|
333
|
+
const abs = resolve(plugin.dir, rel);
|
|
334
|
+
if (!abs.startsWith(resolve(plugin.dir))) continue;
|
|
335
|
+
if (existsSync2(abs)) dirs.add(statSync(abs).isDirectory() ? abs : dirname2(abs));
|
|
336
|
+
}
|
|
337
|
+
return [...dirs];
|
|
338
|
+
}
|
|
339
|
+
function prefixCommand(plugin, command) {
|
|
340
|
+
return command.replaceAll("{pluginDir}", plugin.dir).replaceAll("$PLUGIN_DIR", plugin.dir);
|
|
341
|
+
}
|
|
342
|
+
function pluginHooks(plugin) {
|
|
343
|
+
const events = plugin.manifest.hooks?.events ?? {};
|
|
344
|
+
const out = {};
|
|
345
|
+
for (const [event, raw] of Object.entries(events)) {
|
|
346
|
+
const normalize = (item) => {
|
|
347
|
+
if (typeof item === "string") return { command: prefixCommand(plugin, item), source: "project", description: `plugin:${plugin.name}` };
|
|
348
|
+
if (item && typeof item === "object") {
|
|
349
|
+
const obj = item;
|
|
350
|
+
return { ...obj, command: prefixCommand(plugin, String(obj.command ?? "")), source: "project", description: obj.description ?? `plugin:${plugin.name}` };
|
|
351
|
+
}
|
|
352
|
+
return item;
|
|
353
|
+
};
|
|
354
|
+
out[event] = Array.isArray(raw) ? raw.map(normalize) : normalize(raw);
|
|
355
|
+
}
|
|
356
|
+
return Object.keys(out).length > 0 ? { enabled: plugin.manifest.hooks?.enabled ?? true, events: out } : void 0;
|
|
357
|
+
}
|
|
358
|
+
function mergePluginHooks(base, configDir) {
|
|
359
|
+
const plugins = activePlugins(configDir);
|
|
360
|
+
if (plugins.length === 0) return base;
|
|
361
|
+
const merged = { ...base ?? {}, enabled: base?.enabled ?? true, events: { ...base?.events ?? {} } };
|
|
362
|
+
for (const plugin of plugins) {
|
|
363
|
+
const hooks = pluginHooks(plugin);
|
|
364
|
+
if (!hooks?.events) continue;
|
|
365
|
+
for (const [event, entry] of Object.entries(hooks.events)) {
|
|
366
|
+
const key = event;
|
|
367
|
+
const existing = merged.events?.[key];
|
|
368
|
+
const existingItems = existing === void 0 ? [] : Array.isArray(existing) ? existing : [existing];
|
|
369
|
+
const newItems = Array.isArray(entry) ? entry : [entry];
|
|
370
|
+
merged.events[key] = [...existingItems, ...newItems];
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
return merged;
|
|
374
|
+
}
|
|
375
|
+
function getActivePluginAssets(configDir) {
|
|
376
|
+
const plugins = activePlugins(configDir);
|
|
377
|
+
const mcpServers = {};
|
|
378
|
+
const skillDirs = [];
|
|
379
|
+
const commandDirs = [];
|
|
380
|
+
const agentDirs = [];
|
|
381
|
+
for (const plugin of plugins) {
|
|
382
|
+
skillDirs.push(...uniqueDirs(plugin, plugin.manifest.skills));
|
|
383
|
+
commandDirs.push(...uniqueDirs(plugin, plugin.manifest.commands));
|
|
384
|
+
agentDirs.push(...uniqueDirs(plugin, plugin.manifest.agents));
|
|
385
|
+
for (const [serverName, server] of Object.entries(plugin.manifest.mcpServers)) {
|
|
386
|
+
mcpServers[`plugin_${plugin.name}_${serverName}`] = server;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
return { plugins, skillDirs, commandDirs, agentDirs, hooks: mergePluginHooks(void 0, configDir), mcpServers };
|
|
390
|
+
}
|
|
391
|
+
function describePlugin(plugin) {
|
|
392
|
+
const m = plugin.manifest;
|
|
393
|
+
return [
|
|
394
|
+
`${plugin.name}@${m.version}`,
|
|
395
|
+
` status: ${plugin.enabled ? "enabled" : "disabled"} \xB7 ${plugin.trusted ? "trusted" : "untrusted"}${plugin.valid ? "" : " \xB7 invalid"}`,
|
|
396
|
+
` dir: ${plugin.dir}`,
|
|
397
|
+
m.description ? ` description: ${m.description}` : void 0,
|
|
398
|
+
` assets: ${m.skills.length} skill(s), ${m.commands.length} command(s), ${m.agents.length} agent(s), ${Object.keys(m.mcpServers).length} MCP server(s), ${Object.keys(m.hooks?.events ?? {}).length} hook event(s)`,
|
|
399
|
+
m.permissionHints.length ? ` permission hints: ${m.permissionHints.join("; ")}` : void 0,
|
|
400
|
+
plugin.error ? ` error: ${plugin.error}` : void 0
|
|
401
|
+
].filter((line) => Boolean(line));
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// src/tools/hooks.ts
|
|
405
|
+
import { execSync } from "child_process";
|
|
406
|
+
import { createHash as createHash2 } from "crypto";
|
|
407
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync3 } from "fs";
|
|
408
|
+
import { join as join3 } from "path";
|
|
409
|
+
var TRUST_FILE = "hooks-trust.json";
|
|
410
|
+
var DEFAULT_TIMEOUT_MS = 5e3;
|
|
411
|
+
function asArray(entry) {
|
|
412
|
+
if (!entry) return [];
|
|
413
|
+
const raw = Array.isArray(entry) ? entry : [entry];
|
|
414
|
+
return raw.map((item) => typeof item === "string" ? { command: item } : item);
|
|
415
|
+
}
|
|
416
|
+
function hookTrustPath(configDir) {
|
|
417
|
+
return join3(configDir, TRUST_FILE);
|
|
418
|
+
}
|
|
419
|
+
function loadHookTrustStore(configDir) {
|
|
420
|
+
const file = hookTrustPath(configDir);
|
|
421
|
+
if (!existsSync3(file)) return { records: [] };
|
|
422
|
+
try {
|
|
423
|
+
const parsed = JSON.parse(readFileSync3(file, "utf-8"));
|
|
424
|
+
return { records: Array.isArray(parsed.records) ? parsed.records : [] };
|
|
425
|
+
} catch {
|
|
426
|
+
return { records: [] };
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
function saveHookTrustStore(configDir, store) {
|
|
430
|
+
mkdirSync3(configDir, { recursive: true });
|
|
431
|
+
atomicWriteFileSync(hookTrustPath(configDir), JSON.stringify(store, null, 2));
|
|
432
|
+
}
|
|
433
|
+
function hashHook(event, command) {
|
|
434
|
+
return createHash2("sha256").update(`${event}
|
|
435
|
+
${command}`, "utf-8").digest("hex");
|
|
436
|
+
}
|
|
437
|
+
function isTrusted(source, event, hash, store) {
|
|
438
|
+
if (source === "user" || source === "managed") return true;
|
|
439
|
+
return store.records.some((r) => r.source === source && r.event === event && r.hash === hash);
|
|
440
|
+
}
|
|
441
|
+
function listHooks(config, configDir) {
|
|
442
|
+
if (!config || config.enabled === false) return [];
|
|
443
|
+
const store = loadHookTrustStore(configDir);
|
|
444
|
+
const out = [];
|
|
445
|
+
const push = (event, hook, index) => {
|
|
446
|
+
if (!hook.command) return;
|
|
447
|
+
const source = hook.source ?? "user";
|
|
448
|
+
const hash = hashHook(event, hook.command);
|
|
449
|
+
out.push({
|
|
450
|
+
id: `${event}:${index}`,
|
|
451
|
+
event,
|
|
452
|
+
command: hook.command,
|
|
453
|
+
source,
|
|
454
|
+
hash,
|
|
455
|
+
trusted: isTrusted(source, event, hash, store),
|
|
456
|
+
disabled: hook.disabled === true,
|
|
457
|
+
description: hook.description
|
|
458
|
+
});
|
|
459
|
+
};
|
|
460
|
+
if (config.preToolExecution) push("PreToolUse", { command: config.preToolExecution }, 0);
|
|
461
|
+
if (config.postToolExecution) push("PostToolUse", { command: config.postToolExecution }, 0);
|
|
462
|
+
for (const [event, entry] of Object.entries(config.events ?? {})) {
|
|
463
|
+
asArray(entry).forEach((hook, i) => push(event, hook, i));
|
|
464
|
+
}
|
|
465
|
+
return out;
|
|
466
|
+
}
|
|
467
|
+
function trustHook(configDir, hook) {
|
|
468
|
+
const store = loadHookTrustStore(configDir);
|
|
469
|
+
const next = store.records.filter((r) => !(r.source === hook.source && r.event === hook.event && r.id === hook.id));
|
|
470
|
+
next.push({ id: hook.id, event: hook.event, source: hook.source, hash: hook.hash, trustedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
471
|
+
saveHookTrustStore(configDir, { records: next });
|
|
472
|
+
}
|
|
473
|
+
function untrustHook(configDir, hook) {
|
|
474
|
+
const store = loadHookTrustStore(configDir);
|
|
475
|
+
saveHookTrustStore(configDir, {
|
|
476
|
+
records: store.records.filter((r) => !(r.source === hook.source && r.event === hook.event && r.hash === hook.hash))
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
function getPendingHookTrust(config, configDir) {
|
|
480
|
+
return listHooks(config, configDir).filter((h) => h.source === "project" && !h.disabled && !h.trusted);
|
|
481
|
+
}
|
|
482
|
+
function normalizeDecision(value, hook) {
|
|
483
|
+
if (!value || typeof value !== "object") return null;
|
|
484
|
+
const obj = value;
|
|
485
|
+
const action = obj.action;
|
|
486
|
+
if (action !== "allow" && action !== "deny" && action !== "ask") return null;
|
|
487
|
+
return {
|
|
488
|
+
action,
|
|
489
|
+
reason: typeof obj.reason === "string" ? obj.reason : void 0,
|
|
490
|
+
prompt: typeof obj.prompt === "string" ? obj.prompt : void 0,
|
|
491
|
+
contextAppend: typeof obj.contextAppend === "string" ? obj.contextAppend : void 0,
|
|
492
|
+
warning: typeof obj.warning === "string" ? obj.warning : void 0,
|
|
493
|
+
warnings: Array.isArray(obj.warnings) ? obj.warnings.filter((x) => typeof x === "string") : void 0,
|
|
494
|
+
hook
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
function runStructuredHook(hook, descriptor, payload) {
|
|
498
|
+
if (hook.disabled === true) return null;
|
|
499
|
+
const timeout = Math.max(100, Math.min(hook.timeoutMs ?? DEFAULT_TIMEOUT_MS, 3e4));
|
|
500
|
+
try {
|
|
501
|
+
const stdout = execSync(hook.command, {
|
|
502
|
+
timeout,
|
|
503
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
504
|
+
encoding: "utf-8",
|
|
505
|
+
env: {
|
|
506
|
+
...process.env,
|
|
507
|
+
AICLI_HOOK_EVENT: payload.event,
|
|
508
|
+
AICLI_HOOK_EVENT_JSON: JSON.stringify(payload)
|
|
509
|
+
}
|
|
510
|
+
}).trim();
|
|
511
|
+
if (!stdout) return null;
|
|
512
|
+
return normalizeDecision(JSON.parse(stdout), descriptor);
|
|
513
|
+
} catch (err) {
|
|
514
|
+
process.stderr.write(`\u26A0 Hook failed: ${hook.command.slice(0, 100)}
|
|
515
|
+
`);
|
|
516
|
+
if (hook.required) {
|
|
517
|
+
return { action: "deny", reason: err instanceof Error ? err.message : String(err), hook: descriptor };
|
|
518
|
+
}
|
|
519
|
+
return null;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
function runLifecycleHooks(config, event, payload, opts) {
|
|
523
|
+
if (!config || config.enabled === false) return [];
|
|
524
|
+
const options = typeof opts === "string" ? { configDir: opts } : opts;
|
|
525
|
+
const store = loadHookTrustStore(options.configDir);
|
|
526
|
+
const hooks = asArray(config.events?.[event]);
|
|
527
|
+
const decisions = [];
|
|
528
|
+
for (let i = 0; i < hooks.length; i++) {
|
|
529
|
+
const hook = hooks[i];
|
|
530
|
+
if (!hook.command || hook.disabled) continue;
|
|
531
|
+
const source = hook.source ?? "user";
|
|
532
|
+
const hash = hashHook(event, hook.command);
|
|
533
|
+
const descriptor = {
|
|
534
|
+
id: `${event}:${i}`,
|
|
535
|
+
event,
|
|
536
|
+
command: hook.command,
|
|
537
|
+
source,
|
|
538
|
+
hash,
|
|
539
|
+
trusted: isTrusted(source, event, hash, store),
|
|
540
|
+
disabled: false,
|
|
541
|
+
description: hook.description
|
|
542
|
+
};
|
|
543
|
+
if (!descriptor.trusted) {
|
|
544
|
+
options.onSummary?.(`hook skipped: ${event} project hook requires trust (${hash.slice(0, 12)})`);
|
|
545
|
+
continue;
|
|
546
|
+
}
|
|
547
|
+
const decision = runStructuredHook(hook, descriptor, { event, ...payload });
|
|
548
|
+
if (decision) {
|
|
549
|
+
decisions.push(decision);
|
|
550
|
+
options.onSummary?.(`hook ${event}: ${decision.action}${decision.reason ? ` (${decision.reason})` : ""}`);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
return decisions;
|
|
554
|
+
}
|
|
555
|
+
function rewriteTemplate(template, isWindows) {
|
|
556
|
+
const ref = (name) => isWindows ? `%${name}%` : `$${name}`;
|
|
557
|
+
return template.replace(/\{tool\}/g, ref("AICLI_HOOK_TOOL")).replace(/\{dangerLevel\}/g, ref("AICLI_HOOK_DANGER_LEVEL")).replace(/\{args\}/g, ref("AICLI_HOOK_ARGS")).replace(/\{status\}/g, ref("AICLI_HOOK_STATUS"));
|
|
558
|
+
}
|
|
559
|
+
function runHook(template, vars) {
|
|
560
|
+
if (!template) return;
|
|
561
|
+
const isWindows = process.platform === "win32";
|
|
562
|
+
const cmd = rewriteTemplate(template, isWindows);
|
|
563
|
+
try {
|
|
564
|
+
execSync(cmd, {
|
|
565
|
+
timeout: DEFAULT_TIMEOUT_MS,
|
|
566
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
567
|
+
encoding: "utf-8",
|
|
568
|
+
env: {
|
|
569
|
+
...process.env,
|
|
570
|
+
AICLI_HOOK_TOOL: vars.tool,
|
|
571
|
+
AICLI_HOOK_DANGER_LEVEL: vars.dangerLevel ?? "",
|
|
572
|
+
AICLI_HOOK_ARGS: vars.args ?? "",
|
|
573
|
+
AICLI_HOOK_STATUS: vars.status ?? ""
|
|
574
|
+
}
|
|
575
|
+
});
|
|
576
|
+
} catch {
|
|
577
|
+
process.stderr.write(`\u26A0 Hook failed: ${cmd.slice(0, 100)}
|
|
578
|
+
`);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
export {
|
|
583
|
+
runTool,
|
|
584
|
+
getStatsSnapshot,
|
|
585
|
+
getTopFailingTools,
|
|
586
|
+
getTopUsedTools,
|
|
587
|
+
resetStats,
|
|
588
|
+
installFlushOnExit,
|
|
589
|
+
listHooks,
|
|
590
|
+
trustHook,
|
|
591
|
+
untrustHook,
|
|
592
|
+
getPendingHookTrust,
|
|
593
|
+
runLifecycleHooks,
|
|
594
|
+
runHook,
|
|
595
|
+
pluginRoot,
|
|
596
|
+
installPlugin,
|
|
597
|
+
setPluginEnabled,
|
|
598
|
+
trustPlugin,
|
|
599
|
+
getInstalledPlugin,
|
|
600
|
+
listInstalledPlugins,
|
|
601
|
+
mergePluginHooks,
|
|
602
|
+
getActivePluginAssets,
|
|
603
|
+
describePlugin
|
|
604
|
+
};
|