@premierstudio/ai-hooks 1.1.3 → 1.1.4
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/dist/adapters/all.js +2 -2056
- package/dist/adapters/all.js.map +1 -1
- package/dist/adapters/index.js +1 -120
- package/dist/adapters/index.js.map +1 -1
- package/dist/chunk-DSRP646D.js +100 -0
- package/dist/chunk-DSRP646D.js.map +1 -0
- package/dist/chunk-FHFRGMW6.js +122 -0
- package/dist/chunk-FHFRGMW6.js.map +1 -0
- package/dist/chunk-HCEOWJK3.js +261 -0
- package/dist/chunk-HCEOWJK3.js.map +1 -0
- package/dist/chunk-N7ASBTX5.js +63 -0
- package/dist/chunk-N7ASBTX5.js.map +1 -0
- package/dist/chunk-ZOWUSGNF.js +1945 -0
- package/dist/chunk-ZOWUSGNF.js.map +1 -0
- package/dist/cli/index.js +31 -2338
- package/dist/cli/index.js.map +1 -1
- package/dist/hooks/index.js +2 -153
- package/dist/hooks/index.js.map +1 -1
- package/dist/index.js +4 -528
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -1,2314 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
import
|
|
5
|
-
|
|
6
|
-
// src/config/loader.ts
|
|
7
|
-
var CONFIG_FILENAMES = [
|
|
8
|
-
"ai-hooks.config.ts",
|
|
9
|
-
"ai-hooks.config.js",
|
|
10
|
-
"ai-hooks.config.mjs",
|
|
11
|
-
"ai-hooks.config.mts"
|
|
12
|
-
];
|
|
13
|
-
function findConfigFile(cwd = process.cwd()) {
|
|
14
|
-
for (const name of CONFIG_FILENAMES) {
|
|
15
|
-
const fullPath = resolve(cwd, name);
|
|
16
|
-
if (existsSync(fullPath)) {
|
|
17
|
-
return fullPath;
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
return null;
|
|
21
|
-
}
|
|
22
|
-
async function loadConfig(configPath, cwd) {
|
|
23
|
-
const resolvedPath = configPath ?? findConfigFile(cwd);
|
|
24
|
-
if (!resolvedPath) {
|
|
25
|
-
throw new ConfigNotFoundError(process.cwd());
|
|
26
|
-
}
|
|
27
|
-
if (!existsSync(resolvedPath)) {
|
|
28
|
-
throw new ConfigNotFoundError(resolvedPath);
|
|
29
|
-
}
|
|
30
|
-
const fileUrl = pathToFileURL(resolve(resolvedPath)).href;
|
|
31
|
-
const mod = await import(fileUrl);
|
|
32
|
-
const config = mod.default ?? mod;
|
|
33
|
-
if (!config.hooks || !Array.isArray(config.hooks)) {
|
|
34
|
-
throw new ConfigValidationError(
|
|
35
|
-
"Config must have a `hooks` array. Did you forget to use `defineConfig()`?"
|
|
36
|
-
);
|
|
37
|
-
}
|
|
38
|
-
if (config.extends && config.extends.length > 0) {
|
|
39
|
-
const mergedHooks = [...config.extends.flatMap((preset) => preset.hooks), ...config.hooks];
|
|
40
|
-
return { ...config, hooks: mergedHooks, extends: void 0 };
|
|
41
|
-
}
|
|
42
|
-
return config;
|
|
43
|
-
}
|
|
44
|
-
var ConfigNotFoundError = class extends Error {
|
|
45
|
-
constructor(searchPath) {
|
|
46
|
-
super(
|
|
47
|
-
`No ai-hooks config found. Searched in: ${searchPath}
|
|
48
|
-
Create an ai-hooks.config.ts file or run: ai-hooks init`
|
|
49
|
-
);
|
|
50
|
-
this.name = "ConfigNotFoundError";
|
|
51
|
-
}
|
|
52
|
-
};
|
|
53
|
-
var ConfigValidationError = class extends Error {
|
|
54
|
-
constructor(message) {
|
|
55
|
-
super(message);
|
|
56
|
-
this.name = "ConfigValidationError";
|
|
57
|
-
}
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
// src/types/hooks.ts
|
|
61
|
-
function isBeforeEvent(event) {
|
|
62
|
-
return event.type === "session:start" || event.type === "prompt:submit" || event.type === "tool:before" || event.type === "file:write" || event.type === "file:edit" || event.type === "file:delete" || event.type === "shell:before" || event.type === "mcp:before";
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// src/runtime/chain.ts
|
|
66
|
-
async function executeChain(hooks, ctx, timeout) {
|
|
67
|
-
const sorted = [...hooks].toSorted((a, b) => (a.priority ?? 100) - (b.priority ?? 100));
|
|
68
|
-
let index = 0;
|
|
69
|
-
const next = async () => {
|
|
70
|
-
if (index >= sorted.length) return;
|
|
71
|
-
const hook2 = sorted[index];
|
|
72
|
-
if (!hook2) return;
|
|
73
|
-
index++;
|
|
74
|
-
if (hook2.enabled === false) {
|
|
75
|
-
await next();
|
|
76
|
-
return;
|
|
77
|
-
}
|
|
78
|
-
if (hook2.filter && !hook2.filter(ctx.event)) {
|
|
79
|
-
await next();
|
|
80
|
-
return;
|
|
81
|
-
}
|
|
82
|
-
const blocked = ctx.results.some((r) => r.blocked);
|
|
83
|
-
if (blocked && hook2.phase === "before") {
|
|
84
|
-
return;
|
|
85
|
-
}
|
|
86
|
-
await Promise.race([
|
|
87
|
-
Promise.resolve(hook2.handler(ctx, next)),
|
|
88
|
-
new Promise(
|
|
89
|
-
(_, reject) => setTimeout(() => reject(new HookTimeoutError(hook2.id, timeout)), timeout)
|
|
90
|
-
)
|
|
91
|
-
]);
|
|
92
|
-
};
|
|
93
|
-
try {
|
|
94
|
-
await next();
|
|
95
|
-
} catch (error) {
|
|
96
|
-
if (error instanceof HookTimeoutError) {
|
|
97
|
-
ctx.results.push({
|
|
98
|
-
blocked: false,
|
|
99
|
-
reason: `Hook "${error.hookId}" timed out after ${error.timeout}ms`
|
|
100
|
-
});
|
|
101
|
-
} else {
|
|
102
|
-
throw error;
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
return ctx.results;
|
|
106
|
-
}
|
|
107
|
-
var HookTimeoutError = class extends Error {
|
|
108
|
-
constructor(hookId, timeout) {
|
|
109
|
-
super(`Hook "${hookId}" timed out after ${timeout}ms`);
|
|
110
|
-
this.hookId = hookId;
|
|
111
|
-
this.timeout = timeout;
|
|
112
|
-
this.name = "HookTimeoutError";
|
|
113
|
-
}
|
|
114
|
-
};
|
|
115
|
-
|
|
116
|
-
// src/runtime/engine.ts
|
|
117
|
-
var DEFAULT_SETTINGS = {
|
|
118
|
-
cwd: process.cwd(),
|
|
119
|
-
logLevel: "warn",
|
|
120
|
-
hookTimeout: 5e3,
|
|
121
|
-
failMode: "open",
|
|
122
|
-
telemetry: false
|
|
123
|
-
};
|
|
124
|
-
var HookEngine = class {
|
|
125
|
-
hooks = /* @__PURE__ */ new Map();
|
|
126
|
-
settings;
|
|
127
|
-
constructor(config) {
|
|
128
|
-
this.settings = { ...DEFAULT_SETTINGS, ...config?.settings };
|
|
129
|
-
if (config) {
|
|
130
|
-
if (config.extends) {
|
|
131
|
-
for (const preset of config.extends) {
|
|
132
|
-
this.registerAll(preset.hooks);
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
this.registerAll(config.hooks);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
/**
|
|
139
|
-
* Register a single hook definition.
|
|
140
|
-
*/
|
|
141
|
-
register(hook2) {
|
|
142
|
-
for (const event of hook2.events) {
|
|
143
|
-
const existing = this.hooks.get(event) ?? [];
|
|
144
|
-
existing.push(hook2);
|
|
145
|
-
this.hooks.set(event, existing);
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
/**
|
|
149
|
-
* Register multiple hook definitions.
|
|
150
|
-
*/
|
|
151
|
-
registerAll(hooks) {
|
|
152
|
-
for (const hook2 of hooks) {
|
|
153
|
-
this.register(hook2);
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
/**
|
|
157
|
-
* Unregister a hook by ID.
|
|
158
|
-
*/
|
|
159
|
-
unregister(hookId) {
|
|
160
|
-
for (const [event, hooks] of this.hooks) {
|
|
161
|
-
const filtered = hooks.filter((h) => h.id !== hookId);
|
|
162
|
-
if (filtered.length === 0) {
|
|
163
|
-
this.hooks.delete(event);
|
|
164
|
-
} else {
|
|
165
|
-
this.hooks.set(event, filtered);
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
/**
|
|
170
|
-
* Emit an event and run the matching hook chain.
|
|
171
|
-
*
|
|
172
|
-
* For "before" events: returns results that may include blocks.
|
|
173
|
-
* For "after" events: returns observation results (no blocking).
|
|
174
|
-
*/
|
|
175
|
-
async emit(event, toolInfo) {
|
|
176
|
-
const eventType = event.type;
|
|
177
|
-
const phase = isBeforeEvent(event) ? "before" : "after";
|
|
178
|
-
const allHooks = this.hooks.get(eventType) ?? [];
|
|
179
|
-
const phaseHooks = allHooks.filter((h) => h.phase === phase);
|
|
180
|
-
if (phaseHooks.length === 0) {
|
|
181
|
-
return [];
|
|
182
|
-
}
|
|
183
|
-
const ctx = {
|
|
184
|
-
event,
|
|
185
|
-
tool: toolInfo,
|
|
186
|
-
cwd: this.settings.cwd,
|
|
187
|
-
state: /* @__PURE__ */ new Map(),
|
|
188
|
-
results: [],
|
|
189
|
-
startedAt: Date.now()
|
|
190
|
-
};
|
|
191
|
-
try {
|
|
192
|
-
return await executeChain(phaseHooks, ctx, this.settings.hookTimeout);
|
|
193
|
-
} catch (error) {
|
|
194
|
-
if (this.settings.failMode === "open") {
|
|
195
|
-
this.log("error", `Hook chain error (fail-open): ${error}`);
|
|
196
|
-
return [];
|
|
197
|
-
}
|
|
198
|
-
return [
|
|
199
|
-
{
|
|
200
|
-
blocked: true,
|
|
201
|
-
reason: `Hook chain error (fail-closed): ${error}`
|
|
202
|
-
}
|
|
203
|
-
];
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
/**
|
|
207
|
-
* Check if an event is blocked by running before hooks.
|
|
208
|
-
* Convenience wrapper around emit().
|
|
209
|
-
*/
|
|
210
|
-
async isBlocked(event, toolInfo) {
|
|
211
|
-
if (!isBeforeEvent(event)) {
|
|
212
|
-
return { blocked: false };
|
|
213
|
-
}
|
|
214
|
-
const results = await this.emit(event, toolInfo);
|
|
215
|
-
const blockResult = results.find((r) => r.blocked);
|
|
216
|
-
return blockResult ? { blocked: true, reason: blockResult.reason } : { blocked: false };
|
|
217
|
-
}
|
|
218
|
-
/**
|
|
219
|
-
* Get all registered hooks, optionally filtered by event type.
|
|
220
|
-
*/
|
|
221
|
-
getHooks(eventType) {
|
|
222
|
-
if (eventType) {
|
|
223
|
-
return this.hooks.get(eventType) ?? [];
|
|
224
|
-
}
|
|
225
|
-
const all = [];
|
|
226
|
-
const seen = /* @__PURE__ */ new Set();
|
|
227
|
-
for (const hooks of this.hooks.values()) {
|
|
228
|
-
for (const hook2 of hooks) {
|
|
229
|
-
if (!seen.has(hook2.id)) {
|
|
230
|
-
seen.add(hook2.id);
|
|
231
|
-
all.push(hook2);
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
return all;
|
|
236
|
-
}
|
|
237
|
-
/**
|
|
238
|
-
* Get current engine settings.
|
|
239
|
-
*/
|
|
240
|
-
getSettings() {
|
|
241
|
-
return { ...this.settings };
|
|
242
|
-
}
|
|
243
|
-
log(level, message) {
|
|
244
|
-
const levels = { silent: 0, error: 1, warn: 2, info: 3, debug: 4 };
|
|
245
|
-
const threshold = levels[this.settings.logLevel];
|
|
246
|
-
const messageLevel = levels[level];
|
|
247
|
-
if (messageLevel <= threshold) {
|
|
248
|
-
const prefix = `[ai-hooks:${level}]`;
|
|
249
|
-
if (level === "error") {
|
|
250
|
-
console.error(prefix, message);
|
|
251
|
-
} else if (level === "warn") {
|
|
252
|
-
console.warn(prefix, message);
|
|
253
|
-
} else {
|
|
254
|
-
console.log(prefix, message);
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
};
|
|
259
|
-
|
|
260
|
-
// src/adapters/registry.ts
|
|
261
|
-
var AdapterRegistry = class {
|
|
262
|
-
adapters = /* @__PURE__ */ new Map();
|
|
263
|
-
factories = /* @__PURE__ */ new Map();
|
|
264
|
-
/**
|
|
265
|
-
* Register an adapter instance.
|
|
266
|
-
*/
|
|
267
|
-
register(adapter10) {
|
|
268
|
-
this.adapters.set(adapter10.id, adapter10);
|
|
269
|
-
}
|
|
270
|
-
/**
|
|
271
|
-
* Register an adapter factory for lazy instantiation.
|
|
272
|
-
*/
|
|
273
|
-
registerFactory(id, factory) {
|
|
274
|
-
this.factories.set(id, factory);
|
|
275
|
-
}
|
|
276
|
-
/**
|
|
277
|
-
* Get a registered adapter by ID.
|
|
278
|
-
*/
|
|
279
|
-
get(id) {
|
|
280
|
-
const existing = this.adapters.get(id);
|
|
281
|
-
if (existing) return existing;
|
|
282
|
-
const factory = this.factories.get(id);
|
|
283
|
-
if (factory) {
|
|
284
|
-
const adapter10 = factory();
|
|
285
|
-
this.adapters.set(id, adapter10);
|
|
286
|
-
return adapter10;
|
|
287
|
-
}
|
|
288
|
-
return void 0;
|
|
289
|
-
}
|
|
290
|
-
/**
|
|
291
|
-
* Get all registered adapter IDs.
|
|
292
|
-
*/
|
|
293
|
-
list() {
|
|
294
|
-
return [.../* @__PURE__ */ new Set([...this.adapters.keys(), ...this.factories.keys()])];
|
|
295
|
-
}
|
|
296
|
-
/**
|
|
297
|
-
* Detect which tools are available in the current environment.
|
|
298
|
-
* Returns adapters that successfully detect their tool.
|
|
299
|
-
*/
|
|
300
|
-
async detectAll() {
|
|
301
|
-
const detected = [];
|
|
302
|
-
for (const id of this.list()) {
|
|
303
|
-
const adapter10 = this.get(id);
|
|
304
|
-
if (adapter10) {
|
|
305
|
-
try {
|
|
306
|
-
const found = await adapter10.detect();
|
|
307
|
-
if (found) {
|
|
308
|
-
detected.push(adapter10);
|
|
309
|
-
}
|
|
310
|
-
} catch {
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
return detected;
|
|
315
|
-
}
|
|
316
|
-
/**
|
|
317
|
-
* Clear the registry. Useful for testing.
|
|
318
|
-
*/
|
|
319
|
-
clear() {
|
|
320
|
-
this.adapters.clear();
|
|
321
|
-
this.factories.clear();
|
|
322
|
-
}
|
|
323
|
-
};
|
|
324
|
-
var registry = new AdapterRegistry();
|
|
325
|
-
var BaseAdapter = class {
|
|
326
|
-
/**
|
|
327
|
-
* Default install: write generated configs to disk.
|
|
328
|
-
*/
|
|
329
|
-
async install(configs) {
|
|
330
|
-
for (const config of configs) {
|
|
331
|
-
const fullPath = resolve(process.cwd(), config.path);
|
|
332
|
-
await mkdir(dirname(fullPath), { recursive: true });
|
|
333
|
-
await writeFile(fullPath, config.content, "utf-8");
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
/**
|
|
337
|
-
* Default uninstall: remove generated config files.
|
|
338
|
-
*/
|
|
339
|
-
async uninstall() {
|
|
340
|
-
}
|
|
341
|
-
// ── Utility Methods ───────────────────────────────────────
|
|
342
|
-
async fileExists(path) {
|
|
343
|
-
return existsSync(resolve(process.cwd(), path));
|
|
344
|
-
}
|
|
345
|
-
async readJsonFile(path) {
|
|
346
|
-
const fullPath = resolve(process.cwd(), path);
|
|
347
|
-
if (!existsSync(fullPath)) return null;
|
|
348
|
-
const content = await readFile(fullPath, "utf-8");
|
|
349
|
-
return JSON.parse(content);
|
|
350
|
-
}
|
|
351
|
-
async writeJsonFile(path, data) {
|
|
352
|
-
const fullPath = resolve(process.cwd(), path);
|
|
353
|
-
await mkdir(dirname(fullPath), { recursive: true });
|
|
354
|
-
await writeFile(fullPath, JSON.stringify(data, null, 2) + "\n", "utf-8");
|
|
355
|
-
}
|
|
356
|
-
async removeFile(path) {
|
|
357
|
-
const fullPath = resolve(process.cwd(), path);
|
|
358
|
-
if (existsSync(fullPath)) {
|
|
359
|
-
await rm(fullPath);
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
/**
|
|
363
|
-
* Check if a CLI command exists on PATH.
|
|
364
|
-
*/
|
|
365
|
-
async commandExists(command) {
|
|
366
|
-
const { exec } = await import('child_process');
|
|
367
|
-
return new Promise((resolve12) => {
|
|
368
|
-
exec(`which ${command}`, (error) => {
|
|
369
|
-
resolve12(!error);
|
|
370
|
-
});
|
|
371
|
-
});
|
|
372
|
-
}
|
|
373
|
-
};
|
|
374
|
-
var EVENT_MAP = {
|
|
375
|
-
"session:start": [],
|
|
376
|
-
"session:end": [],
|
|
377
|
-
"prompt:submit": [],
|
|
378
|
-
"prompt:response": [],
|
|
379
|
-
"tool:before": ["tool:pre-execute"],
|
|
380
|
-
"tool:after": ["tool:post-execute"],
|
|
381
|
-
"file:write": ["tool:pre-execute"],
|
|
382
|
-
"file:edit": ["tool:pre-execute"],
|
|
383
|
-
"file:delete": ["tool:pre-execute"],
|
|
384
|
-
"shell:before": ["tool:pre-execute"],
|
|
385
|
-
"shell:after": ["tool:post-execute"],
|
|
386
|
-
"mcp:before": ["tool:pre-execute"],
|
|
387
|
-
"mcp:after": ["tool:post-execute"]
|
|
388
|
-
};
|
|
389
|
-
var REVERSE_MAP = {
|
|
390
|
-
"tool:pre-execute": [
|
|
391
|
-
"tool:before",
|
|
392
|
-
"file:write",
|
|
393
|
-
"file:edit",
|
|
394
|
-
"file:delete",
|
|
395
|
-
"shell:before",
|
|
396
|
-
"mcp:before"
|
|
397
|
-
],
|
|
398
|
-
"tool:post-execute": ["tool:after", "shell:after", "mcp:after"]
|
|
399
|
-
};
|
|
400
|
-
var AmpAdapter = class extends BaseAdapter {
|
|
401
|
-
id = "amp";
|
|
402
|
-
name = "Amp";
|
|
403
|
-
version = "1.0";
|
|
404
|
-
capabilities = {
|
|
405
|
-
beforeHooks: false,
|
|
406
|
-
afterHooks: true,
|
|
407
|
-
mcp: true,
|
|
408
|
-
configFile: true,
|
|
409
|
-
supportedEvents: [
|
|
410
|
-
"tool:before",
|
|
411
|
-
"tool:after",
|
|
412
|
-
"file:write",
|
|
413
|
-
"file:edit",
|
|
414
|
-
"file:delete",
|
|
415
|
-
"shell:before",
|
|
416
|
-
"shell:after",
|
|
417
|
-
"mcp:before",
|
|
418
|
-
"mcp:after"
|
|
419
|
-
],
|
|
420
|
-
blockableEvents: []
|
|
421
|
-
};
|
|
422
|
-
async detect() {
|
|
423
|
-
const hasCommand = await this.commandExists("amp");
|
|
424
|
-
const hasDir = existsSync(resolve(process.cwd(), ".amp"));
|
|
425
|
-
return hasCommand || hasDir;
|
|
426
|
-
}
|
|
427
|
-
async generate(hooks) {
|
|
428
|
-
const configs = [];
|
|
429
|
-
const mcpConfig = {
|
|
430
|
-
mcpServers: {
|
|
431
|
-
"ai-hooks": {
|
|
432
|
-
command: "npx",
|
|
433
|
-
args: ["@premierstudio/mcp-server"],
|
|
434
|
-
env: {
|
|
435
|
-
AI_HOOKS_CONFIG: resolve(process.cwd(), "ai-hooks.config.ts")
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
};
|
|
440
|
-
configs.push({
|
|
441
|
-
path: ".amp/mcp.json",
|
|
442
|
-
content: JSON.stringify(mcpConfig, null, 2) + "\n",
|
|
443
|
-
format: "json"
|
|
444
|
-
});
|
|
445
|
-
const neededEvents = /* @__PURE__ */ new Set();
|
|
446
|
-
for (const hook2 of hooks) {
|
|
447
|
-
for (const event of hook2.events) {
|
|
448
|
-
const nativeEvents = this.mapEvent(event);
|
|
449
|
-
for (const ne of nativeEvents) {
|
|
450
|
-
neededEvents.add(ne);
|
|
451
|
-
}
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
const hooksList = hooks.map((h) => ({
|
|
455
|
-
id: h.id,
|
|
456
|
-
name: h.name,
|
|
457
|
-
events: h.events,
|
|
458
|
-
phase: h.phase,
|
|
459
|
-
nativeEvents: h.events.flatMap((e) => this.mapEvent(e))
|
|
460
|
-
}));
|
|
461
|
-
configs.push({
|
|
462
|
-
path: ".amp/ai-hooks-manifest.json",
|
|
463
|
-
content: JSON.stringify(
|
|
464
|
-
{
|
|
465
|
-
adapter: "amp",
|
|
466
|
-
version: "1.0",
|
|
467
|
-
hooks: hooksList,
|
|
468
|
-
nativeEvents: [...neededEvents],
|
|
469
|
-
mcpServer: "@premierstudio/mcp-server"
|
|
470
|
-
},
|
|
471
|
-
null,
|
|
472
|
-
2
|
|
473
|
-
) + "\n",
|
|
474
|
-
format: "json"
|
|
475
|
-
});
|
|
476
|
-
return configs;
|
|
477
|
-
}
|
|
478
|
-
mapEvent(event) {
|
|
479
|
-
return EVENT_MAP[event] ?? [];
|
|
480
|
-
}
|
|
481
|
-
mapNativeEvent(nativeEvent) {
|
|
482
|
-
return REVERSE_MAP[nativeEvent] ?? [];
|
|
483
|
-
}
|
|
484
|
-
async uninstall() {
|
|
485
|
-
await this.removeFile(".amp/mcp.json");
|
|
486
|
-
await this.removeFile(".amp/ai-hooks-manifest.json");
|
|
487
|
-
}
|
|
488
|
-
};
|
|
489
|
-
var adapter = new AmpAdapter();
|
|
490
|
-
registry.register(adapter);
|
|
491
|
-
var EVENT_MAP2 = {
|
|
492
|
-
"session:start": ["SessionStart"],
|
|
493
|
-
"session:end": [],
|
|
494
|
-
"prompt:submit": ["UserPromptSubmit"],
|
|
495
|
-
"prompt:response": ["PostToolUse"],
|
|
496
|
-
// approximate - Claude doesn't expose response directly
|
|
497
|
-
"tool:before": ["PreToolUse"],
|
|
498
|
-
"tool:after": ["PostToolUse"],
|
|
499
|
-
"file:read": ["PreToolUse"],
|
|
500
|
-
// Read tool
|
|
501
|
-
"file:write": ["PreToolUse"],
|
|
502
|
-
// Write tool
|
|
503
|
-
"file:edit": ["PreToolUse"],
|
|
504
|
-
// Edit tool
|
|
505
|
-
"file:delete": ["PreToolUse"],
|
|
506
|
-
"shell:before": ["PreToolUse"],
|
|
507
|
-
// Bash tool
|
|
508
|
-
"shell:after": ["PostToolUse"],
|
|
509
|
-
// Bash tool
|
|
510
|
-
"mcp:before": ["PreToolUse"],
|
|
511
|
-
"mcp:after": ["PostToolUse"],
|
|
512
|
-
notification: ["Notification"]
|
|
513
|
-
};
|
|
514
|
-
var REVERSE_MAP2 = {
|
|
515
|
-
SessionStart: ["session:start"],
|
|
516
|
-
UserPromptSubmit: ["prompt:submit"],
|
|
517
|
-
PreToolUse: [
|
|
518
|
-
"tool:before",
|
|
519
|
-
"file:write",
|
|
520
|
-
"file:edit",
|
|
521
|
-
"file:delete",
|
|
522
|
-
"shell:before",
|
|
523
|
-
"mcp:before"
|
|
524
|
-
],
|
|
525
|
-
PostToolUse: ["tool:after", "shell:after", "mcp:after"],
|
|
526
|
-
Notification: ["notification"]
|
|
527
|
-
};
|
|
528
|
-
var ClaudeCodeAdapter = class extends BaseAdapter {
|
|
529
|
-
id = "claude-code";
|
|
530
|
-
name = "Claude Code";
|
|
531
|
-
version = "1.0";
|
|
532
|
-
capabilities = {
|
|
533
|
-
beforeHooks: true,
|
|
534
|
-
afterHooks: true,
|
|
535
|
-
mcp: true,
|
|
536
|
-
configFile: true,
|
|
537
|
-
supportedEvents: [
|
|
538
|
-
"session:start",
|
|
539
|
-
"prompt:submit",
|
|
540
|
-
"tool:before",
|
|
541
|
-
"tool:after",
|
|
542
|
-
"file:read",
|
|
543
|
-
"file:write",
|
|
544
|
-
"file:edit",
|
|
545
|
-
"file:delete",
|
|
546
|
-
"shell:before",
|
|
547
|
-
"shell:after",
|
|
548
|
-
"mcp:before",
|
|
549
|
-
"mcp:after",
|
|
550
|
-
"notification"
|
|
551
|
-
],
|
|
552
|
-
blockableEvents: [
|
|
553
|
-
"prompt:submit",
|
|
554
|
-
"tool:before",
|
|
555
|
-
"file:write",
|
|
556
|
-
"file:edit",
|
|
557
|
-
"file:delete",
|
|
558
|
-
"shell:before",
|
|
559
|
-
"mcp:before"
|
|
560
|
-
]
|
|
561
|
-
};
|
|
562
|
-
async detect() {
|
|
563
|
-
const hasCommand = await this.commandExists("claude");
|
|
564
|
-
const hasDir = existsSync(resolve(process.cwd(), ".claude"));
|
|
565
|
-
return hasCommand || hasDir;
|
|
566
|
-
}
|
|
567
|
-
async generate(hooks) {
|
|
568
|
-
const configs = [];
|
|
569
|
-
configs.push(this.generateRunner());
|
|
570
|
-
configs.push(await this.generateSettings(hooks));
|
|
571
|
-
return configs;
|
|
572
|
-
}
|
|
573
|
-
mapEvent(event) {
|
|
574
|
-
return EVENT_MAP2[event] ?? [];
|
|
575
|
-
}
|
|
576
|
-
mapNativeEvent(nativeEvent) {
|
|
577
|
-
return REVERSE_MAP2[nativeEvent] ?? [];
|
|
578
|
-
}
|
|
579
|
-
async uninstall() {
|
|
580
|
-
await this.removeFile(".claude/hooks/ai-hooks-runner.js");
|
|
581
|
-
}
|
|
582
|
-
// -- Private Methods ---------------------------------------------
|
|
583
|
-
/**
|
|
584
|
-
* Generate the hook runner script that Claude Code hooks call.
|
|
585
|
-
* This script loads the ai-hooks config and runs the appropriate chain.
|
|
586
|
-
*/
|
|
587
|
-
generateRunner() {
|
|
588
|
-
const script = `#!/usr/bin/env node
|
|
589
|
-
/**
|
|
590
|
-
* ai-hooks runner for Claude Code.
|
|
591
|
-
* Generated by: ai-hooks generate
|
|
592
|
-
*
|
|
593
|
-
* This script is called by Claude Code hooks. It loads your
|
|
594
|
-
* ai-hooks.config.ts and runs the matching hook chain.
|
|
595
|
-
*
|
|
596
|
-
* DO NOT EDIT - regenerate with: ai-hooks generate
|
|
597
|
-
*/
|
|
598
|
-
|
|
599
|
-
import { loadConfig } from "@premierstudio/ai-hooks";
|
|
600
|
-
import { HookEngine } from "@premierstudio/ai-hooks";
|
|
601
|
-
|
|
602
|
-
const hookEvent = process.env.CLAUDE_HOOK_EVENT;
|
|
603
|
-
const toolName = process.env.CLAUDE_TOOL_NAME;
|
|
604
|
-
const inputJson = process.env.CLAUDE_TOOL_INPUT;
|
|
605
|
-
|
|
606
|
-
async function run() {
|
|
607
|
-
const config = await loadConfig();
|
|
608
|
-
const engine = new HookEngine(config);
|
|
609
|
-
|
|
610
|
-
// Build the event from Claude Code's environment variables
|
|
611
|
-
const event = buildEvent(hookEvent, toolName, inputJson);
|
|
612
|
-
if (!event) {
|
|
613
|
-
process.exit(0);
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
const toolInfo = { name: "claude-code", version: "1.0" };
|
|
617
|
-
const results = await engine.emit(event, toolInfo);
|
|
618
|
-
|
|
619
|
-
// Check for blocks
|
|
620
|
-
const blocked = results.find((r) => r.blocked);
|
|
621
|
-
if (blocked) {
|
|
622
|
-
// Claude Code reads stdout JSON for hook results
|
|
623
|
-
const output = JSON.stringify({
|
|
624
|
-
decision: "block",
|
|
625
|
-
reason: blocked.reason ?? "Blocked by ai-hooks",
|
|
626
|
-
});
|
|
627
|
-
process.stdout.write(output);
|
|
628
|
-
process.exit(0);
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
process.exit(0);
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
function buildEvent(hookEvent, toolName, inputJson) {
|
|
635
|
-
const timestamp = Date.now();
|
|
636
|
-
const metadata = {};
|
|
637
|
-
|
|
638
|
-
try {
|
|
639
|
-
const input = inputJson ? JSON.parse(inputJson) : {};
|
|
640
|
-
|
|
641
|
-
switch (hookEvent) {
|
|
642
|
-
case "PreToolUse":
|
|
643
|
-
return resolvePreToolUse(toolName, input, timestamp, metadata);
|
|
644
|
-
case "PostToolUse":
|
|
645
|
-
return resolvePostToolUse(toolName, input, timestamp, metadata);
|
|
646
|
-
case "SessionStart":
|
|
647
|
-
return {
|
|
648
|
-
type: "session:start",
|
|
649
|
-
tool: "claude-code",
|
|
650
|
-
version: "1.0",
|
|
651
|
-
workingDirectory: process.cwd(),
|
|
652
|
-
timestamp,
|
|
653
|
-
metadata,
|
|
654
|
-
};
|
|
655
|
-
case "UserPromptSubmit":
|
|
656
|
-
return {
|
|
657
|
-
type: "prompt:submit",
|
|
658
|
-
prompt: input.prompt ?? "",
|
|
659
|
-
timestamp,
|
|
660
|
-
metadata,
|
|
661
|
-
};
|
|
662
|
-
default:
|
|
663
|
-
return null;
|
|
664
|
-
}
|
|
665
|
-
} catch {
|
|
666
|
-
return null;
|
|
667
|
-
}
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
function resolvePreToolUse(toolName, input, timestamp, metadata) {
|
|
671
|
-
switch (toolName) {
|
|
672
|
-
case "Write":
|
|
673
|
-
return {
|
|
674
|
-
type: "file:write",
|
|
675
|
-
path: input.file_path ?? "",
|
|
676
|
-
content: input.content ?? "",
|
|
677
|
-
timestamp,
|
|
678
|
-
metadata,
|
|
679
|
-
};
|
|
680
|
-
case "Edit":
|
|
681
|
-
return {
|
|
682
|
-
type: "file:edit",
|
|
683
|
-
path: input.file_path ?? "",
|
|
684
|
-
oldContent: input.old_string ?? "",
|
|
685
|
-
newContent: input.new_string ?? "",
|
|
686
|
-
timestamp,
|
|
687
|
-
metadata,
|
|
688
|
-
};
|
|
689
|
-
case "Bash":
|
|
690
|
-
return {
|
|
691
|
-
type: "shell:before",
|
|
692
|
-
command: input.command ?? "",
|
|
693
|
-
cwd: process.cwd(),
|
|
694
|
-
timestamp,
|
|
695
|
-
metadata,
|
|
696
|
-
};
|
|
697
|
-
default:
|
|
698
|
-
return {
|
|
699
|
-
type: "tool:before",
|
|
700
|
-
toolName: toolName ?? "unknown",
|
|
701
|
-
input: input ?? {},
|
|
702
|
-
timestamp,
|
|
703
|
-
metadata,
|
|
704
|
-
};
|
|
705
|
-
}
|
|
706
|
-
}
|
|
707
|
-
|
|
708
|
-
function resolvePostToolUse(toolName, input, timestamp, metadata) {
|
|
709
|
-
switch (toolName) {
|
|
710
|
-
case "Bash":
|
|
711
|
-
return {
|
|
712
|
-
type: "shell:after",
|
|
713
|
-
command: input.command ?? "",
|
|
714
|
-
cwd: process.cwd(),
|
|
715
|
-
exitCode: input.exitCode ?? 0,
|
|
716
|
-
stdout: input.stdout ?? "",
|
|
717
|
-
stderr: input.stderr ?? "",
|
|
718
|
-
duration: 0,
|
|
719
|
-
timestamp,
|
|
720
|
-
metadata,
|
|
721
|
-
};
|
|
722
|
-
default:
|
|
723
|
-
return {
|
|
724
|
-
type: "tool:after",
|
|
725
|
-
toolName: toolName ?? "unknown",
|
|
726
|
-
input: input ?? {},
|
|
727
|
-
output: {},
|
|
728
|
-
duration: 0,
|
|
729
|
-
timestamp,
|
|
730
|
-
metadata,
|
|
731
|
-
};
|
|
732
|
-
}
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
run().catch((err) => {
|
|
736
|
-
console.error("[ai-hooks] Error:", err.message);
|
|
737
|
-
process.exit(1);
|
|
738
|
-
});
|
|
739
|
-
`;
|
|
740
|
-
return {
|
|
741
|
-
path: ".claude/hooks/ai-hooks-runner.js",
|
|
742
|
-
content: script,
|
|
743
|
-
format: "js",
|
|
744
|
-
gitignore: false
|
|
745
|
-
};
|
|
746
|
-
}
|
|
747
|
-
/**
|
|
748
|
-
* Generate the Claude Code settings.json hook entries.
|
|
749
|
-
*/
|
|
750
|
-
async generateSettings(hooks) {
|
|
751
|
-
const settingsPath = ".claude/settings.json";
|
|
752
|
-
let existing = {};
|
|
753
|
-
const fullPath = resolve(process.cwd(), settingsPath);
|
|
754
|
-
if (existsSync(fullPath)) {
|
|
755
|
-
const raw = await readFile(fullPath, "utf-8");
|
|
756
|
-
existing = JSON.parse(raw);
|
|
757
|
-
}
|
|
758
|
-
const neededEvents = /* @__PURE__ */ new Set();
|
|
759
|
-
for (const hook2 of hooks) {
|
|
760
|
-
for (const event of hook2.events) {
|
|
761
|
-
const nativeEvents = this.mapEvent(event);
|
|
762
|
-
for (const ne of nativeEvents) {
|
|
763
|
-
neededEvents.add(ne);
|
|
764
|
-
}
|
|
765
|
-
}
|
|
766
|
-
}
|
|
767
|
-
const hooksConfig = {};
|
|
768
|
-
for (const event of neededEvents) {
|
|
769
|
-
const hookEntry = {
|
|
770
|
-
type: "command",
|
|
771
|
-
command: `node .claude/hooks/ai-hooks-runner.js`,
|
|
772
|
-
timeout: 10,
|
|
773
|
-
description: `ai-hooks: ${event}`
|
|
774
|
-
};
|
|
775
|
-
if (!hooksConfig[event]) {
|
|
776
|
-
hooksConfig[event] = [];
|
|
777
|
-
}
|
|
778
|
-
hooksConfig[event].push({
|
|
779
|
-
hooks: [hookEntry]
|
|
780
|
-
});
|
|
781
|
-
}
|
|
782
|
-
const existingHooks = existing.hooks ?? {};
|
|
783
|
-
const mergedHooks = { ...existingHooks };
|
|
784
|
-
for (const [event, entries] of Object.entries(hooksConfig)) {
|
|
785
|
-
if (!mergedHooks[event]) {
|
|
786
|
-
mergedHooks[event] = [];
|
|
787
|
-
}
|
|
788
|
-
mergedHooks[event] = mergedHooks[event].filter((entry) => !entry.hooks?.some((h) => h.description?.startsWith("ai-hooks:")));
|
|
789
|
-
mergedHooks[event].push(...entries);
|
|
790
|
-
}
|
|
791
|
-
const mergedSettings = {
|
|
792
|
-
...existing,
|
|
793
|
-
hooks: mergedHooks
|
|
794
|
-
};
|
|
795
|
-
return {
|
|
796
|
-
path: settingsPath,
|
|
797
|
-
content: JSON.stringify(mergedSettings, null, 2) + "\n",
|
|
798
|
-
format: "json",
|
|
799
|
-
gitignore: false
|
|
800
|
-
};
|
|
801
|
-
}
|
|
802
|
-
};
|
|
803
|
-
var adapter2 = new ClaudeCodeAdapter();
|
|
804
|
-
registry.register(adapter2);
|
|
805
|
-
var EVENT_MAP3 = {
|
|
806
|
-
"session:start": ["TaskStart"],
|
|
807
|
-
"session:end": ["TaskCancel"],
|
|
808
|
-
"prompt:submit": ["UserPromptSubmit"],
|
|
809
|
-
"prompt:response": [],
|
|
810
|
-
"tool:before": ["PreToolUse"],
|
|
811
|
-
"tool:after": ["PostToolUse"],
|
|
812
|
-
"file:read": ["PreToolUse"],
|
|
813
|
-
"file:write": ["PreToolUse"],
|
|
814
|
-
"file:edit": ["PreToolUse"],
|
|
815
|
-
"file:delete": ["PreToolUse"],
|
|
816
|
-
"shell:before": ["PreToolUse"],
|
|
817
|
-
"shell:after": ["PostToolUse"],
|
|
818
|
-
"mcp:before": ["PreToolUse"],
|
|
819
|
-
"mcp:after": ["PostToolUse"],
|
|
820
|
-
notification: []
|
|
821
|
-
};
|
|
822
|
-
var REVERSE_MAP3 = {
|
|
823
|
-
TaskStart: ["session:start"],
|
|
824
|
-
TaskCancel: ["session:end"],
|
|
825
|
-
TaskResume: ["session:start"],
|
|
826
|
-
UserPromptSubmit: ["prompt:submit"],
|
|
827
|
-
PreToolUse: [
|
|
828
|
-
"tool:before",
|
|
829
|
-
"file:read",
|
|
830
|
-
"file:write",
|
|
831
|
-
"file:edit",
|
|
832
|
-
"file:delete",
|
|
833
|
-
"shell:before",
|
|
834
|
-
"mcp:before"
|
|
835
|
-
],
|
|
836
|
-
PostToolUse: ["tool:after", "shell:after", "mcp:after"],
|
|
837
|
-
PreCompact: []
|
|
838
|
-
};
|
|
839
|
-
var ClineAdapter = class extends BaseAdapter {
|
|
840
|
-
id = "cline";
|
|
841
|
-
name = "Cline";
|
|
842
|
-
version = "1.0";
|
|
843
|
-
capabilities = {
|
|
844
|
-
beforeHooks: true,
|
|
845
|
-
afterHooks: true,
|
|
846
|
-
mcp: true,
|
|
847
|
-
configFile: true,
|
|
848
|
-
supportedEvents: [
|
|
849
|
-
"session:start",
|
|
850
|
-
"session:end",
|
|
851
|
-
"prompt:submit",
|
|
852
|
-
"tool:before",
|
|
853
|
-
"tool:after",
|
|
854
|
-
"file:read",
|
|
855
|
-
"file:write",
|
|
856
|
-
"file:edit",
|
|
857
|
-
"file:delete",
|
|
858
|
-
"shell:before",
|
|
859
|
-
"shell:after",
|
|
860
|
-
"mcp:before",
|
|
861
|
-
"mcp:after"
|
|
862
|
-
],
|
|
863
|
-
blockableEvents: [
|
|
864
|
-
"tool:before",
|
|
865
|
-
"file:read",
|
|
866
|
-
"file:write",
|
|
867
|
-
"file:edit",
|
|
868
|
-
"file:delete",
|
|
869
|
-
"shell:before",
|
|
870
|
-
"mcp:before"
|
|
871
|
-
]
|
|
872
|
-
};
|
|
873
|
-
async detect() {
|
|
874
|
-
const hasCommand = await this.commandExists("cline");
|
|
875
|
-
const hasDir = existsSync(resolve(process.cwd(), ".clinerules"));
|
|
876
|
-
return hasCommand || hasDir;
|
|
877
|
-
}
|
|
878
|
-
async generate(hooks) {
|
|
879
|
-
const configs = [];
|
|
880
|
-
const neededEvents = /* @__PURE__ */ new Set();
|
|
881
|
-
for (const hook2 of hooks) {
|
|
882
|
-
for (const event of hook2.events) {
|
|
883
|
-
const nativeEvents = this.mapEvent(event);
|
|
884
|
-
for (const ne of nativeEvents) {
|
|
885
|
-
neededEvents.add(ne);
|
|
886
|
-
}
|
|
887
|
-
}
|
|
888
|
-
}
|
|
889
|
-
for (const event of neededEvents) {
|
|
890
|
-
configs.push({
|
|
891
|
-
path: `.clinerules/hooks/${event}`,
|
|
892
|
-
content: this.generateHookScript(event),
|
|
893
|
-
format: "js"
|
|
894
|
-
});
|
|
895
|
-
}
|
|
896
|
-
return configs;
|
|
897
|
-
}
|
|
898
|
-
mapEvent(event) {
|
|
899
|
-
return EVENT_MAP3[event] ?? [];
|
|
900
|
-
}
|
|
901
|
-
mapNativeEvent(nativeEvent) {
|
|
902
|
-
return REVERSE_MAP3[nativeEvent] ?? [];
|
|
903
|
-
}
|
|
904
|
-
async uninstall() {
|
|
905
|
-
const hookNames = [
|
|
906
|
-
"PreToolUse",
|
|
907
|
-
"PostToolUse",
|
|
908
|
-
"UserPromptSubmit",
|
|
909
|
-
"TaskStart",
|
|
910
|
-
"TaskResume",
|
|
911
|
-
"TaskCancel",
|
|
912
|
-
"PreCompact"
|
|
913
|
-
];
|
|
914
|
-
for (const name of hookNames) {
|
|
915
|
-
await this.removeFile(`.clinerules/hooks/${name}`);
|
|
916
|
-
}
|
|
917
|
-
}
|
|
918
|
-
generateHookScript(eventName) {
|
|
919
|
-
return `#!/usr/bin/env node
|
|
920
|
-
/**
|
|
921
|
-
* ai-hooks runner for Cline (${eventName}).
|
|
922
|
-
* Generated by: ai-hooks generate
|
|
923
|
-
*
|
|
924
|
-
* Cline passes hook event data as JSON via STDIN with:
|
|
925
|
-
* hookName, taskId, workspaceRoots, pendingToolInfo, etc.
|
|
926
|
-
*
|
|
927
|
-
* Output JSON with { cancel: true, errorMessage: "..." } to block.
|
|
928
|
-
* Output JSON with { cancel: false } to allow.
|
|
929
|
-
*
|
|
930
|
-
* DO NOT EDIT - regenerate with: ai-hooks generate
|
|
931
|
-
*/
|
|
932
|
-
import { loadConfig, HookEngine } from "@premierstudio/ai-hooks";
|
|
933
|
-
|
|
934
|
-
async function readStdin() {
|
|
935
|
-
const chunks = [];
|
|
936
|
-
for await (const chunk of process.stdin) {
|
|
937
|
-
chunks.push(chunk);
|
|
938
|
-
}
|
|
939
|
-
return Buffer.concat(chunks).toString("utf-8");
|
|
940
|
-
}
|
|
941
|
-
|
|
942
|
-
async function run() {
|
|
943
|
-
const raw = await readStdin();
|
|
944
|
-
const input = JSON.parse(raw || "{}");
|
|
945
|
-
const hookName = input.hookName ?? "${eventName}";
|
|
946
|
-
const pendingToolInfo = input.pendingToolInfo ?? {};
|
|
947
|
-
const toolName = pendingToolInfo.toolName ?? "";
|
|
948
|
-
|
|
949
|
-
const config = await loadConfig();
|
|
950
|
-
const engine = new HookEngine(config);
|
|
951
|
-
const toolInfo = { name: "cline", version: "1.0" };
|
|
952
|
-
const timestamp = Date.now();
|
|
953
|
-
const metadata = { taskId: input.taskId ?? "" };
|
|
954
|
-
|
|
955
|
-
let event;
|
|
956
|
-
switch (hookName) {
|
|
957
|
-
case "TaskStart":
|
|
958
|
-
case "TaskResume":
|
|
959
|
-
event = {
|
|
960
|
-
type: "session:start",
|
|
961
|
-
tool: "cline",
|
|
962
|
-
version: "1.0",
|
|
963
|
-
workingDirectory: (input.workspaceRoots ?? [])[0] ?? process.cwd(),
|
|
964
|
-
timestamp,
|
|
965
|
-
metadata,
|
|
966
|
-
};
|
|
967
|
-
break;
|
|
968
|
-
case "TaskCancel":
|
|
969
|
-
event = { type: "session:end", tool: "cline", duration: 0, timestamp, metadata };
|
|
970
|
-
break;
|
|
971
|
-
case "UserPromptSubmit":
|
|
972
|
-
event = { type: "prompt:submit", prompt: input.userMessage ?? "", timestamp, metadata };
|
|
973
|
-
break;
|
|
974
|
-
case "PreToolUse":
|
|
975
|
-
event = resolvePreToolEvent(toolName, pendingToolInfo, timestamp, metadata);
|
|
976
|
-
break;
|
|
977
|
-
case "PostToolUse":
|
|
978
|
-
event = resolvePostToolEvent(toolName, pendingToolInfo, timestamp, metadata);
|
|
979
|
-
break;
|
|
980
|
-
default:
|
|
981
|
-
process.stdout.write(JSON.stringify({ cancel: false }));
|
|
982
|
-
process.exit(0);
|
|
983
|
-
}
|
|
984
|
-
|
|
985
|
-
const results = await engine.emit(event, toolInfo);
|
|
986
|
-
const blocked = results.find((r) => r.blocked);
|
|
987
|
-
|
|
988
|
-
if (blocked) {
|
|
989
|
-
process.stdout.write(JSON.stringify({
|
|
990
|
-
cancel: true,
|
|
991
|
-
errorMessage: blocked.reason ?? "Blocked by ai-hooks",
|
|
992
|
-
}));
|
|
993
|
-
process.exit(0);
|
|
994
|
-
}
|
|
995
|
-
|
|
996
|
-
process.stdout.write(JSON.stringify({ cancel: false }));
|
|
997
|
-
}
|
|
998
|
-
|
|
999
|
-
function resolvePreToolEvent(toolName, info, timestamp, metadata) {
|
|
1000
|
-
switch (toolName) {
|
|
1001
|
-
case "write_to_file":
|
|
1002
|
-
return { type: "file:write", path: info.path ?? "", content: info.content ?? "", timestamp, metadata };
|
|
1003
|
-
case "replace_in_file":
|
|
1004
|
-
return { type: "file:edit", path: info.path ?? "", oldContent: info.diff ?? "", newContent: "", timestamp, metadata };
|
|
1005
|
-
case "read_file":
|
|
1006
|
-
return { type: "file:read", path: info.path ?? "", timestamp, metadata };
|
|
1007
|
-
case "execute_command":
|
|
1008
|
-
return { type: "shell:before", command: info.command ?? "", cwd: process.cwd(), timestamp, metadata };
|
|
1009
|
-
case "use_mcp_tool":
|
|
1010
|
-
return { type: "mcp:before", server: info.mcpServer ?? "", tool: info.mcpTool ?? "", input: {}, timestamp, metadata };
|
|
1011
|
-
default:
|
|
1012
|
-
return { type: "tool:before", toolName: toolName || "unknown", input: info, timestamp, metadata };
|
|
1013
|
-
}
|
|
1014
|
-
}
|
|
1015
|
-
|
|
1016
|
-
function resolvePostToolEvent(toolName, info, timestamp, metadata) {
|
|
1017
|
-
switch (toolName) {
|
|
1018
|
-
case "execute_command":
|
|
1019
|
-
return {
|
|
1020
|
-
type: "shell:after",
|
|
1021
|
-
command: info.command ?? "",
|
|
1022
|
-
cwd: process.cwd(),
|
|
1023
|
-
exitCode: 0,
|
|
1024
|
-
stdout: "",
|
|
1025
|
-
stderr: "",
|
|
1026
|
-
duration: 0,
|
|
1027
|
-
timestamp,
|
|
1028
|
-
metadata,
|
|
1029
|
-
};
|
|
1030
|
-
default:
|
|
1031
|
-
return { type: "tool:after", toolName: toolName || "unknown", input: info, output: {}, duration: 0, timestamp, metadata };
|
|
1032
|
-
}
|
|
1033
|
-
}
|
|
1034
|
-
|
|
1035
|
-
run().catch((err) => {
|
|
1036
|
-
console.error("[ai-hooks] Error:", err.message);
|
|
1037
|
-
process.stdout.write(JSON.stringify({ cancel: false }));
|
|
1038
|
-
process.exit(0);
|
|
1039
|
-
});
|
|
1040
|
-
`;
|
|
1041
|
-
}
|
|
1042
|
-
};
|
|
1043
|
-
var adapter3 = new ClineAdapter();
|
|
1044
|
-
registry.register(adapter3);
|
|
1045
|
-
var EVENT_MAP4 = {
|
|
1046
|
-
"session:start": ["session_start"],
|
|
1047
|
-
"session:end": ["session_end"],
|
|
1048
|
-
"prompt:submit": ["user_message"],
|
|
1049
|
-
"prompt:response": ["assistant_message"],
|
|
1050
|
-
"tool:before": ["before_tool_call"],
|
|
1051
|
-
"tool:after": ["after_tool_call"],
|
|
1052
|
-
"file:write": ["before_file_write"],
|
|
1053
|
-
"file:edit": ["before_file_edit"],
|
|
1054
|
-
"file:delete": ["before_file_delete"],
|
|
1055
|
-
"shell:before": ["before_shell"],
|
|
1056
|
-
"shell:after": ["after_shell"],
|
|
1057
|
-
"mcp:before": ["before_mcp_call"],
|
|
1058
|
-
"mcp:after": ["after_mcp_call"]
|
|
1059
|
-
};
|
|
1060
|
-
var REVERSE_MAP4 = {
|
|
1061
|
-
session_start: ["session:start"],
|
|
1062
|
-
session_end: ["session:end"],
|
|
1063
|
-
user_message: ["prompt:submit"],
|
|
1064
|
-
assistant_message: ["prompt:response"],
|
|
1065
|
-
before_tool_call: ["tool:before"],
|
|
1066
|
-
after_tool_call: ["tool:after"],
|
|
1067
|
-
before_file_write: ["file:write"],
|
|
1068
|
-
before_file_edit: ["file:edit"],
|
|
1069
|
-
before_file_delete: ["file:delete"],
|
|
1070
|
-
before_shell: ["shell:before"],
|
|
1071
|
-
after_shell: ["shell:after"],
|
|
1072
|
-
before_mcp_call: ["mcp:before"],
|
|
1073
|
-
after_mcp_call: ["mcp:after"]
|
|
1074
|
-
};
|
|
1075
|
-
var CodexAdapter = class extends BaseAdapter {
|
|
1076
|
-
id = "codex";
|
|
1077
|
-
name = "Codex CLI";
|
|
1078
|
-
version = "1.0";
|
|
1079
|
-
capabilities = {
|
|
1080
|
-
beforeHooks: true,
|
|
1081
|
-
afterHooks: true,
|
|
1082
|
-
mcp: true,
|
|
1083
|
-
configFile: true,
|
|
1084
|
-
supportedEvents: [
|
|
1085
|
-
"session:start",
|
|
1086
|
-
"session:end",
|
|
1087
|
-
"prompt:submit",
|
|
1088
|
-
"prompt:response",
|
|
1089
|
-
"tool:before",
|
|
1090
|
-
"tool:after",
|
|
1091
|
-
"file:write",
|
|
1092
|
-
"file:edit",
|
|
1093
|
-
"file:delete",
|
|
1094
|
-
"shell:before",
|
|
1095
|
-
"shell:after",
|
|
1096
|
-
"mcp:before",
|
|
1097
|
-
"mcp:after"
|
|
1098
|
-
],
|
|
1099
|
-
blockableEvents: [
|
|
1100
|
-
"tool:before",
|
|
1101
|
-
"file:write",
|
|
1102
|
-
"file:edit",
|
|
1103
|
-
"file:delete",
|
|
1104
|
-
"shell:before",
|
|
1105
|
-
"mcp:before"
|
|
1106
|
-
]
|
|
1107
|
-
};
|
|
1108
|
-
async detect() {
|
|
1109
|
-
const hasCommand = await this.commandExists("codex");
|
|
1110
|
-
const hasConfig = existsSync(resolve(process.cwd(), "codex.json")) || existsSync(resolve(process.cwd(), ".codex"));
|
|
1111
|
-
return hasCommand || hasConfig;
|
|
1112
|
-
}
|
|
1113
|
-
async generate(hooks) {
|
|
1114
|
-
const hookEntries = {};
|
|
1115
|
-
for (const hook2 of hooks) {
|
|
1116
|
-
for (const event of hook2.events) {
|
|
1117
|
-
const nativeEvents = this.mapEvent(event);
|
|
1118
|
-
for (const ne of nativeEvents) {
|
|
1119
|
-
hookEntries[ne] = {
|
|
1120
|
-
command: "node .codex/hooks/ai-hooks-runner.js",
|
|
1121
|
-
timeout: 10
|
|
1122
|
-
};
|
|
1123
|
-
}
|
|
1124
|
-
}
|
|
1125
|
-
}
|
|
1126
|
-
return [
|
|
1127
|
-
{
|
|
1128
|
-
path: ".codex/hooks/ai-hooks-runner.js",
|
|
1129
|
-
content: this.generateRunner(),
|
|
1130
|
-
format: "js"
|
|
1131
|
-
},
|
|
1132
|
-
{
|
|
1133
|
-
path: "codex.json",
|
|
1134
|
-
content: JSON.stringify({ hooks: hookEntries }, null, 2) + "\n",
|
|
1135
|
-
format: "json"
|
|
1136
|
-
}
|
|
1137
|
-
];
|
|
1138
|
-
}
|
|
1139
|
-
mapEvent(event) {
|
|
1140
|
-
return EVENT_MAP4[event] ?? [];
|
|
1141
|
-
}
|
|
1142
|
-
mapNativeEvent(nativeEvent) {
|
|
1143
|
-
return REVERSE_MAP4[nativeEvent] ?? [];
|
|
1144
|
-
}
|
|
1145
|
-
async uninstall() {
|
|
1146
|
-
await this.removeFile(".codex/hooks/ai-hooks-runner.js");
|
|
1147
|
-
}
|
|
1148
|
-
generateRunner() {
|
|
1149
|
-
return `#!/usr/bin/env node
|
|
1150
|
-
/**
|
|
1151
|
-
* ai-hooks runner for Codex CLI.
|
|
1152
|
-
* Generated by: ai-hooks generate
|
|
1153
|
-
*/
|
|
1154
|
-
import { loadConfig, HookEngine } from "@premierstudio/ai-hooks";
|
|
1155
|
-
|
|
1156
|
-
const hookEvent = process.env.CODEX_HOOK_EVENT;
|
|
1157
|
-
const toolInput = process.env.CODEX_TOOL_INPUT;
|
|
1158
|
-
|
|
1159
|
-
async function run() {
|
|
1160
|
-
const config = await loadConfig();
|
|
1161
|
-
const engine = new HookEngine(config);
|
|
1162
|
-
const toolInfo = { name: "codex", version: "1.0" };
|
|
1163
|
-
|
|
1164
|
-
const input = toolInput ? JSON.parse(toolInput) : {};
|
|
1165
|
-
const timestamp = Date.now();
|
|
1166
|
-
const metadata = {};
|
|
1167
|
-
|
|
1168
|
-
let event;
|
|
1169
|
-
switch (hookEvent) {
|
|
1170
|
-
case "before_shell":
|
|
1171
|
-
event = { type: "shell:before", command: input.command ?? "", cwd: process.cwd(), timestamp, metadata };
|
|
1172
|
-
break;
|
|
1173
|
-
case "before_file_write":
|
|
1174
|
-
event = { type: "file:write", path: input.path ?? "", content: input.content ?? "", timestamp, metadata };
|
|
1175
|
-
break;
|
|
1176
|
-
case "before_file_edit":
|
|
1177
|
-
event = { type: "file:edit", path: input.path ?? "", oldContent: input.old ?? "", newContent: input.new ?? "", timestamp, metadata };
|
|
1178
|
-
break;
|
|
1179
|
-
default:
|
|
1180
|
-
event = { type: "tool:before", toolName: hookEvent ?? "unknown", input, timestamp, metadata };
|
|
1181
|
-
}
|
|
1182
|
-
|
|
1183
|
-
const results = await engine.emit(event, toolInfo);
|
|
1184
|
-
const blocked = results.find((r) => r.blocked);
|
|
1185
|
-
|
|
1186
|
-
if (blocked) {
|
|
1187
|
-
console.log(JSON.stringify({ blocked: true, reason: blocked.reason }));
|
|
1188
|
-
process.exit(1);
|
|
1189
|
-
}
|
|
1190
|
-
}
|
|
1191
|
-
|
|
1192
|
-
run().catch(() => process.exit(1));
|
|
1193
|
-
`;
|
|
1194
|
-
}
|
|
1195
|
-
};
|
|
1196
|
-
var adapter4 = new CodexAdapter();
|
|
1197
|
-
registry.register(adapter4);
|
|
1198
|
-
var EVENT_MAP5 = {
|
|
1199
|
-
"session:start": [],
|
|
1200
|
-
"session:end": ["stop"],
|
|
1201
|
-
"prompt:submit": ["beforeSubmitPrompt"],
|
|
1202
|
-
"prompt:response": ["stop"],
|
|
1203
|
-
"tool:before": ["beforeMCPExecution"],
|
|
1204
|
-
"tool:after": [],
|
|
1205
|
-
"file:read": ["beforeReadFile"],
|
|
1206
|
-
"file:write": ["afterFileEdit"],
|
|
1207
|
-
"file:edit": ["afterFileEdit"],
|
|
1208
|
-
"file:delete": [],
|
|
1209
|
-
"shell:before": ["beforeShellExecution"],
|
|
1210
|
-
"shell:after": [],
|
|
1211
|
-
"mcp:before": ["beforeMCPExecution"],
|
|
1212
|
-
"mcp:after": []
|
|
1213
|
-
};
|
|
1214
|
-
var REVERSE_MAP5 = {
|
|
1215
|
-
beforeSubmitPrompt: ["prompt:submit"],
|
|
1216
|
-
beforeShellExecution: ["shell:before"],
|
|
1217
|
-
beforeMCPExecution: ["tool:before", "mcp:before"],
|
|
1218
|
-
beforeReadFile: ["file:read"],
|
|
1219
|
-
afterFileEdit: ["file:write", "file:edit"],
|
|
1220
|
-
stop: ["session:end", "prompt:response"]
|
|
1221
|
-
};
|
|
1222
|
-
var CursorAdapter = class extends BaseAdapter {
|
|
1223
|
-
id = "cursor";
|
|
1224
|
-
name = "Cursor";
|
|
1225
|
-
version = "1.0";
|
|
1226
|
-
capabilities = {
|
|
1227
|
-
beforeHooks: true,
|
|
1228
|
-
afterHooks: true,
|
|
1229
|
-
mcp: true,
|
|
1230
|
-
configFile: true,
|
|
1231
|
-
supportedEvents: [
|
|
1232
|
-
"session:end",
|
|
1233
|
-
"prompt:submit",
|
|
1234
|
-
"prompt:response",
|
|
1235
|
-
"tool:before",
|
|
1236
|
-
"file:read",
|
|
1237
|
-
"file:write",
|
|
1238
|
-
"file:edit",
|
|
1239
|
-
"shell:before",
|
|
1240
|
-
"mcp:before"
|
|
1241
|
-
],
|
|
1242
|
-
blockableEvents: ["shell:before", "mcp:before", "tool:before"]
|
|
1243
|
-
};
|
|
1244
|
-
async detect() {
|
|
1245
|
-
const hasCommand = await this.commandExists("cursor");
|
|
1246
|
-
const hasDir = existsSync(resolve(process.cwd(), ".cursor"));
|
|
1247
|
-
return hasCommand || hasDir;
|
|
1248
|
-
}
|
|
1249
|
-
async generate(hooks) {
|
|
1250
|
-
const configs = [];
|
|
1251
|
-
const neededEvents = /* @__PURE__ */ new Set();
|
|
1252
|
-
for (const hook2 of hooks) {
|
|
1253
|
-
for (const event of hook2.events) {
|
|
1254
|
-
const nativeEvents = this.mapEvent(event);
|
|
1255
|
-
for (const ne of nativeEvents) {
|
|
1256
|
-
neededEvents.add(ne);
|
|
1257
|
-
}
|
|
1258
|
-
}
|
|
1259
|
-
}
|
|
1260
|
-
configs.push({
|
|
1261
|
-
path: ".cursor/hooks/ai-hooks-runner.js",
|
|
1262
|
-
content: this.generateRunner(),
|
|
1263
|
-
format: "js"
|
|
1264
|
-
});
|
|
1265
|
-
const hooksConfig = {};
|
|
1266
|
-
for (const event of neededEvents) {
|
|
1267
|
-
if (!hooksConfig[event]) {
|
|
1268
|
-
hooksConfig[event] = [];
|
|
1269
|
-
}
|
|
1270
|
-
hooksConfig[event].push({
|
|
1271
|
-
command: `node hooks/ai-hooks-runner.js ${event}`
|
|
1272
|
-
});
|
|
1273
|
-
}
|
|
1274
|
-
configs.push({
|
|
1275
|
-
path: ".cursor/hooks.json",
|
|
1276
|
-
content: JSON.stringify({ version: 1, hooks: hooksConfig }, null, 2) + "\n",
|
|
1277
|
-
format: "json"
|
|
1278
|
-
});
|
|
1279
|
-
return configs;
|
|
1280
|
-
}
|
|
1281
|
-
mapEvent(event) {
|
|
1282
|
-
return EVENT_MAP5[event] ?? [];
|
|
1283
|
-
}
|
|
1284
|
-
mapNativeEvent(nativeEvent) {
|
|
1285
|
-
return REVERSE_MAP5[nativeEvent] ?? [];
|
|
1286
|
-
}
|
|
1287
|
-
async uninstall() {
|
|
1288
|
-
await this.removeFile(".cursor/hooks/ai-hooks-runner.js");
|
|
1289
|
-
await this.removeFile(".cursor/hooks.json");
|
|
1290
|
-
}
|
|
1291
|
-
generateRunner() {
|
|
1292
|
-
return `#!/usr/bin/env node
|
|
1293
|
-
/**
|
|
1294
|
-
* ai-hooks runner for Cursor.
|
|
1295
|
-
* Generated by: ai-hooks generate
|
|
1296
|
-
*
|
|
1297
|
-
* Cursor passes the hook event name as a CLI argument.
|
|
1298
|
-
* Blocking hooks (beforeShellExecution, beforeMCPExecution)
|
|
1299
|
-
* return JSON: { "permission": "deny", "agentMessage": "reason" }
|
|
1300
|
-
*
|
|
1301
|
-
* DO NOT EDIT - regenerate with: ai-hooks generate
|
|
1302
|
-
*/
|
|
1303
|
-
import { loadConfig, HookEngine } from "@premierstudio/ai-hooks";
|
|
1304
|
-
|
|
1305
|
-
async function readStdin() {
|
|
1306
|
-
const chunks = [];
|
|
1307
|
-
for await (const chunk of process.stdin) {
|
|
1308
|
-
chunks.push(chunk);
|
|
1309
|
-
}
|
|
1310
|
-
return Buffer.concat(chunks).toString("utf-8");
|
|
1311
|
-
}
|
|
1312
|
-
|
|
1313
|
-
async function run() {
|
|
1314
|
-
const hookEventName = process.argv[2] ?? "";
|
|
1315
|
-
let input = {};
|
|
1316
|
-
try {
|
|
1317
|
-
const raw = await readStdin();
|
|
1318
|
-
input = JSON.parse(raw || "{}");
|
|
1319
|
-
} catch {
|
|
1320
|
-
input = {};
|
|
1321
|
-
}
|
|
1322
|
-
|
|
1323
|
-
const config = await loadConfig();
|
|
1324
|
-
const engine = new HookEngine(config);
|
|
1325
|
-
const toolInfo = { name: "cursor", version: "1.0" };
|
|
1326
|
-
const timestamp = Date.now();
|
|
1327
|
-
const metadata = {};
|
|
1328
|
-
|
|
1329
|
-
let event;
|
|
1330
|
-
switch (hookEventName) {
|
|
1331
|
-
case "beforeSubmitPrompt":
|
|
1332
|
-
event = { type: "prompt:submit", prompt: input.prompt ?? "", timestamp, metadata };
|
|
1333
|
-
break;
|
|
1334
|
-
case "beforeShellExecution":
|
|
1335
|
-
event = { type: "shell:before", command: input.command ?? "", cwd: process.cwd(), timestamp, metadata };
|
|
1336
|
-
break;
|
|
1337
|
-
case "beforeMCPExecution":
|
|
1338
|
-
event = { type: "mcp:before", server: input.server ?? "", method: input.method ?? "", params: input.params ?? {}, timestamp, metadata };
|
|
1339
|
-
break;
|
|
1340
|
-
case "beforeReadFile":
|
|
1341
|
-
event = { type: "file:read", path: input.path ?? "", timestamp, metadata };
|
|
1342
|
-
break;
|
|
1343
|
-
case "afterFileEdit":
|
|
1344
|
-
event = { type: "file:edit", path: input.path ?? "", oldContent: "", newContent: "", timestamp, metadata };
|
|
1345
|
-
break;
|
|
1346
|
-
case "stop":
|
|
1347
|
-
event = { type: "session:end", tool: "cursor", duration: 0, timestamp, metadata };
|
|
1348
|
-
break;
|
|
1349
|
-
default:
|
|
1350
|
-
process.exit(0);
|
|
1351
|
-
}
|
|
1352
|
-
|
|
1353
|
-
const results = await engine.emit(event, toolInfo);
|
|
1354
|
-
const blocked = results.find((r) => r.blocked);
|
|
1355
|
-
|
|
1356
|
-
if (blocked) {
|
|
1357
|
-
const response = JSON.stringify({
|
|
1358
|
-
permission: "deny",
|
|
1359
|
-
agentMessage: blocked.reason ?? "Blocked by ai-hooks",
|
|
1360
|
-
userMessage: blocked.reason ?? "Blocked by ai-hooks",
|
|
1361
|
-
});
|
|
1362
|
-
process.stdout.write(response);
|
|
1363
|
-
process.exit(0);
|
|
1364
|
-
}
|
|
1365
|
-
|
|
1366
|
-
if (hookEventName === "beforeShellExecution" || hookEventName === "beforeMCPExecution") {
|
|
1367
|
-
process.stdout.write(JSON.stringify({ permission: "allow" }));
|
|
1368
|
-
}
|
|
1369
|
-
}
|
|
1370
|
-
|
|
1371
|
-
run().catch((err) => {
|
|
1372
|
-
console.error("[ai-hooks] Error:", err.message);
|
|
1373
|
-
process.exit(1);
|
|
1374
|
-
});
|
|
1375
|
-
`;
|
|
1376
|
-
}
|
|
1377
|
-
};
|
|
1378
|
-
var adapter5 = new CursorAdapter();
|
|
1379
|
-
registry.register(adapter5);
|
|
1380
|
-
var EVENT_MAP6 = {
|
|
1381
|
-
"session:start": ["SessionStart"],
|
|
1382
|
-
"session:end": ["SessionEnd"],
|
|
1383
|
-
"prompt:submit": ["UserPromptSubmit"],
|
|
1384
|
-
"prompt:response": ["Stop"],
|
|
1385
|
-
"tool:before": ["PreToolUse"],
|
|
1386
|
-
"tool:after": ["PostToolUse"],
|
|
1387
|
-
"file:read": ["PreToolUse"],
|
|
1388
|
-
"file:write": ["PreToolUse"],
|
|
1389
|
-
"file:edit": ["PreToolUse"],
|
|
1390
|
-
"file:delete": ["PreToolUse"],
|
|
1391
|
-
"shell:before": ["PreToolUse"],
|
|
1392
|
-
"shell:after": ["PostToolUse"],
|
|
1393
|
-
"mcp:before": ["PreToolUse"],
|
|
1394
|
-
"mcp:after": ["PostToolUse"],
|
|
1395
|
-
notification: ["Notification"]
|
|
1396
|
-
};
|
|
1397
|
-
var REVERSE_MAP6 = {
|
|
1398
|
-
SessionStart: ["session:start"],
|
|
1399
|
-
SessionEnd: ["session:end"],
|
|
1400
|
-
UserPromptSubmit: ["prompt:submit"],
|
|
1401
|
-
Stop: ["prompt:response"],
|
|
1402
|
-
PreToolUse: [
|
|
1403
|
-
"tool:before",
|
|
1404
|
-
"file:read",
|
|
1405
|
-
"file:write",
|
|
1406
|
-
"file:edit",
|
|
1407
|
-
"file:delete",
|
|
1408
|
-
"shell:before",
|
|
1409
|
-
"mcp:before"
|
|
1410
|
-
],
|
|
1411
|
-
PostToolUse: ["tool:after", "shell:after", "mcp:after"],
|
|
1412
|
-
Notification: ["notification"]
|
|
1413
|
-
};
|
|
1414
|
-
var DroidAdapter = class extends BaseAdapter {
|
|
1415
|
-
id = "droid";
|
|
1416
|
-
name = "Factory Droid";
|
|
1417
|
-
version = "1.0";
|
|
1418
|
-
capabilities = {
|
|
1419
|
-
beforeHooks: true,
|
|
1420
|
-
afterHooks: true,
|
|
1421
|
-
mcp: true,
|
|
1422
|
-
configFile: true,
|
|
1423
|
-
supportedEvents: [
|
|
1424
|
-
"session:start",
|
|
1425
|
-
"session:end",
|
|
1426
|
-
"prompt:submit",
|
|
1427
|
-
"prompt:response",
|
|
1428
|
-
"tool:before",
|
|
1429
|
-
"tool:after",
|
|
1430
|
-
"file:read",
|
|
1431
|
-
"file:write",
|
|
1432
|
-
"file:edit",
|
|
1433
|
-
"file:delete",
|
|
1434
|
-
"shell:before",
|
|
1435
|
-
"shell:after",
|
|
1436
|
-
"mcp:before",
|
|
1437
|
-
"mcp:after",
|
|
1438
|
-
"notification"
|
|
1439
|
-
],
|
|
1440
|
-
blockableEvents: [
|
|
1441
|
-
"tool:before",
|
|
1442
|
-
"file:read",
|
|
1443
|
-
"file:write",
|
|
1444
|
-
"file:edit",
|
|
1445
|
-
"file:delete",
|
|
1446
|
-
"shell:before",
|
|
1447
|
-
"mcp:before"
|
|
1448
|
-
]
|
|
1449
|
-
};
|
|
1450
|
-
async detect() {
|
|
1451
|
-
const hasCommand = await this.commandExists("droid");
|
|
1452
|
-
const hasDir = existsSync(resolve(process.cwd(), ".factory"));
|
|
1453
|
-
return hasCommand || hasDir;
|
|
1454
|
-
}
|
|
1455
|
-
async generate(hooks) {
|
|
1456
|
-
const configs = [];
|
|
1457
|
-
const neededEvents = /* @__PURE__ */ new Set();
|
|
1458
|
-
for (const hook2 of hooks) {
|
|
1459
|
-
for (const event of hook2.events) {
|
|
1460
|
-
const nativeEvents = this.mapEvent(event);
|
|
1461
|
-
for (const ne of nativeEvents) {
|
|
1462
|
-
neededEvents.add(ne);
|
|
1463
|
-
}
|
|
1464
|
-
}
|
|
1465
|
-
}
|
|
1466
|
-
configs.push({
|
|
1467
|
-
path: ".factory/hooks/ai-hooks-runner.js",
|
|
1468
|
-
content: this.generateRunner(),
|
|
1469
|
-
format: "js"
|
|
1470
|
-
});
|
|
1471
|
-
const hooksConfig = {};
|
|
1472
|
-
const runnerAbsPath = resolve(process.cwd(), ".factory/hooks/ai-hooks-runner.js");
|
|
1473
|
-
for (const event of neededEvents) {
|
|
1474
|
-
const hookEntry = {
|
|
1475
|
-
hooks: [
|
|
1476
|
-
{
|
|
1477
|
-
type: "command",
|
|
1478
|
-
command: `node ${runnerAbsPath}`,
|
|
1479
|
-
timeout: 30
|
|
1480
|
-
}
|
|
1481
|
-
]
|
|
1482
|
-
};
|
|
1483
|
-
if (event === "PreToolUse" || event === "PostToolUse") {
|
|
1484
|
-
hookEntry.matcher = "*";
|
|
1485
|
-
}
|
|
1486
|
-
if (!hooksConfig[event]) {
|
|
1487
|
-
hooksConfig[event] = [];
|
|
1488
|
-
}
|
|
1489
|
-
hooksConfig[event].push(hookEntry);
|
|
1490
|
-
}
|
|
1491
|
-
const settingsConfig = await this.mergeSettings(hooksConfig);
|
|
1492
|
-
configs.push({
|
|
1493
|
-
path: ".factory/settings.json",
|
|
1494
|
-
content: JSON.stringify(settingsConfig, null, 2) + "\n",
|
|
1495
|
-
format: "json"
|
|
1496
|
-
});
|
|
1497
|
-
return configs;
|
|
1498
|
-
}
|
|
1499
|
-
mapEvent(event) {
|
|
1500
|
-
return EVENT_MAP6[event] ?? [];
|
|
1501
|
-
}
|
|
1502
|
-
mapNativeEvent(nativeEvent) {
|
|
1503
|
-
return REVERSE_MAP6[nativeEvent] ?? [];
|
|
1504
|
-
}
|
|
1505
|
-
async uninstall() {
|
|
1506
|
-
await this.removeFile(".factory/hooks/ai-hooks-runner.js");
|
|
1507
|
-
}
|
|
1508
|
-
async mergeSettings(hooksConfig) {
|
|
1509
|
-
const settingsPath = resolve(process.cwd(), ".factory/settings.json");
|
|
1510
|
-
let existing = {};
|
|
1511
|
-
if (existsSync(settingsPath)) {
|
|
1512
|
-
const raw = await readFile(settingsPath, "utf-8");
|
|
1513
|
-
existing = JSON.parse(raw);
|
|
1514
|
-
}
|
|
1515
|
-
const existingHooks = existing.hooks ?? {};
|
|
1516
|
-
const mergedHooks = { ...existingHooks };
|
|
1517
|
-
for (const [event, entries] of Object.entries(hooksConfig)) {
|
|
1518
|
-
if (!mergedHooks[event]) {
|
|
1519
|
-
mergedHooks[event] = [];
|
|
1520
|
-
}
|
|
1521
|
-
mergedHooks[event] = mergedHooks[event].filter((entry) => !entry.hooks?.some((h) => h.command?.includes("ai-hooks-runner")));
|
|
1522
|
-
mergedHooks[event].push(...entries);
|
|
1523
|
-
}
|
|
1524
|
-
return {
|
|
1525
|
-
...existing,
|
|
1526
|
-
hooks: mergedHooks
|
|
1527
|
-
};
|
|
1528
|
-
}
|
|
1529
|
-
generateRunner() {
|
|
1530
|
-
return `#!/usr/bin/env node
|
|
1531
|
-
/**
|
|
1532
|
-
* ai-hooks runner for Factory Droid.
|
|
1533
|
-
* Generated by: ai-hooks generate
|
|
1534
|
-
*
|
|
1535
|
-
* Droid passes hook event data as JSON via STDIN with:
|
|
1536
|
-
* hook_event_name, session_id, cwd, tool_name, tool_input, tool_response
|
|
1537
|
-
*
|
|
1538
|
-
* Exit code 0 = success, exit code 2 = block (PreToolUse).
|
|
1539
|
-
* STDERR on exit code 2 is fed back to Droid as context.
|
|
1540
|
-
*
|
|
1541
|
-
* DO NOT EDIT - regenerate with: ai-hooks generate
|
|
1542
|
-
*/
|
|
1543
|
-
import { loadConfig, HookEngine } from "@premierstudio/ai-hooks";
|
|
1544
|
-
|
|
1545
|
-
async function readStdin() {
|
|
1546
|
-
const chunks = [];
|
|
1547
|
-
for await (const chunk of process.stdin) {
|
|
1548
|
-
chunks.push(chunk);
|
|
1549
|
-
}
|
|
1550
|
-
return Buffer.concat(chunks).toString("utf-8");
|
|
1551
|
-
}
|
|
1552
|
-
|
|
1553
|
-
async function run() {
|
|
1554
|
-
const raw = await readStdin();
|
|
1555
|
-
const input = JSON.parse(raw || "{}");
|
|
1556
|
-
const hookEventName = input.hook_event_name ?? "";
|
|
1557
|
-
const toolName = input.tool_name ?? "";
|
|
1558
|
-
const toolInput = input.tool_input ?? {};
|
|
1559
|
-
const toolResponse = input.tool_response ?? {};
|
|
1560
|
-
|
|
1561
|
-
const config = await loadConfig();
|
|
1562
|
-
const engine = new HookEngine(config);
|
|
1563
|
-
const toolInfo = { name: "droid", version: "1.0" };
|
|
1564
|
-
const timestamp = Date.now();
|
|
1565
|
-
const metadata = { sessionId: input.session_id ?? "" };
|
|
1566
|
-
|
|
1567
|
-
let event;
|
|
1568
|
-
switch (hookEventName) {
|
|
1569
|
-
case "SessionStart":
|
|
1570
|
-
event = {
|
|
1571
|
-
type: "session:start",
|
|
1572
|
-
tool: "droid",
|
|
1573
|
-
version: "1.0",
|
|
1574
|
-
workingDirectory: input.cwd ?? process.cwd(),
|
|
1575
|
-
timestamp,
|
|
1576
|
-
metadata,
|
|
1577
|
-
};
|
|
1578
|
-
break;
|
|
1579
|
-
case "SessionEnd":
|
|
1580
|
-
event = { type: "session:end", tool: "droid", duration: 0, timestamp, metadata };
|
|
1581
|
-
break;
|
|
1582
|
-
case "UserPromptSubmit":
|
|
1583
|
-
event = { type: "prompt:submit", prompt: toolInput.prompt ?? "", timestamp, metadata };
|
|
1584
|
-
break;
|
|
1585
|
-
case "Notification":
|
|
1586
|
-
event = { type: "notification", level: "info", message: toolInput.message ?? "", timestamp, metadata };
|
|
1587
|
-
break;
|
|
1588
|
-
case "Stop":
|
|
1589
|
-
event = { type: "session:end", tool: "droid", duration: 0, timestamp, metadata };
|
|
1590
|
-
break;
|
|
1591
|
-
case "PreToolUse":
|
|
1592
|
-
event = resolvePreToolEvent(toolName, toolInput, timestamp, metadata);
|
|
1593
|
-
break;
|
|
1594
|
-
case "PostToolUse":
|
|
1595
|
-
event = resolvePostToolEvent(toolName, toolInput, toolResponse, timestamp, metadata);
|
|
1596
|
-
break;
|
|
1597
|
-
default:
|
|
1598
|
-
process.exit(0);
|
|
1599
|
-
}
|
|
1600
|
-
|
|
1601
|
-
const results = await engine.emit(event, toolInfo);
|
|
1602
|
-
const blocked = results.find((r) => r.blocked);
|
|
1603
|
-
|
|
1604
|
-
if (blocked) {
|
|
1605
|
-
process.stderr.write(blocked.reason ?? "Blocked by ai-hooks");
|
|
1606
|
-
process.exit(2);
|
|
1607
|
-
}
|
|
1608
|
-
}
|
|
1609
|
-
|
|
1610
|
-
function resolvePreToolEvent(toolName, toolInput, timestamp, metadata) {
|
|
1611
|
-
switch (toolName) {
|
|
1612
|
-
case "Write":
|
|
1613
|
-
return { type: "file:write", path: toolInput.file_path ?? "", content: toolInput.content ?? "", timestamp, metadata };
|
|
1614
|
-
case "Edit":
|
|
1615
|
-
return { type: "file:edit", path: toolInput.file_path ?? "", oldContent: toolInput.old_string ?? "", newContent: toolInput.new_string ?? "", timestamp, metadata };
|
|
1616
|
-
case "Read":
|
|
1617
|
-
return { type: "file:read", path: toolInput.file_path ?? "", timestamp, metadata };
|
|
1618
|
-
case "Bash":
|
|
1619
|
-
return { type: "shell:before", command: toolInput.command ?? "", cwd: process.cwd(), timestamp, metadata };
|
|
1620
|
-
default:
|
|
1621
|
-
return { type: "tool:before", toolName: toolName || "unknown", input: toolInput, timestamp, metadata };
|
|
1622
|
-
}
|
|
1623
|
-
}
|
|
1624
|
-
|
|
1625
|
-
function resolvePostToolEvent(toolName, toolInput, toolResponse, timestamp, metadata) {
|
|
1626
|
-
switch (toolName) {
|
|
1627
|
-
case "Bash":
|
|
1628
|
-
return {
|
|
1629
|
-
type: "shell:after",
|
|
1630
|
-
command: toolInput.command ?? "",
|
|
1631
|
-
cwd: process.cwd(),
|
|
1632
|
-
exitCode: toolResponse.exitCode ?? 0,
|
|
1633
|
-
stdout: toolResponse.stdout ?? "",
|
|
1634
|
-
stderr: toolResponse.stderr ?? "",
|
|
1635
|
-
duration: 0,
|
|
1636
|
-
timestamp,
|
|
1637
|
-
metadata,
|
|
1638
|
-
};
|
|
1639
|
-
default:
|
|
1640
|
-
return { type: "tool:after", toolName: toolName || "unknown", input: toolInput, output: toolResponse, duration: 0, timestamp, metadata };
|
|
1641
|
-
}
|
|
1642
|
-
}
|
|
1643
|
-
|
|
1644
|
-
run().catch((err) => {
|
|
1645
|
-
console.error("[ai-hooks] Error:", err.message);
|
|
1646
|
-
process.exit(1);
|
|
1647
|
-
});
|
|
1648
|
-
`;
|
|
1649
|
-
}
|
|
1650
|
-
};
|
|
1651
|
-
var adapter6 = new DroidAdapter();
|
|
1652
|
-
registry.register(adapter6);
|
|
1653
|
-
var EVENT_MAP7 = {
|
|
1654
|
-
"session:start": ["SessionStart"],
|
|
1655
|
-
"session:end": ["SessionEnd"],
|
|
1656
|
-
"prompt:submit": ["BeforePrompt"],
|
|
1657
|
-
"prompt:response": ["AfterResponse"],
|
|
1658
|
-
"tool:before": ["BeforeTool"],
|
|
1659
|
-
"tool:after": ["AfterTool"],
|
|
1660
|
-
"file:write": ["BeforeTool"],
|
|
1661
|
-
// FileWrite tool
|
|
1662
|
-
"file:edit": ["BeforeTool"],
|
|
1663
|
-
// FileEdit tool
|
|
1664
|
-
"file:delete": ["BeforeTool"],
|
|
1665
|
-
// FileDelete tool
|
|
1666
|
-
"shell:before": ["BeforeShell"],
|
|
1667
|
-
"shell:after": ["AfterShell"],
|
|
1668
|
-
"mcp:before": ["BeforeTool"],
|
|
1669
|
-
"mcp:after": ["AfterTool"]
|
|
1670
|
-
};
|
|
1671
|
-
var REVERSE_MAP7 = {
|
|
1672
|
-
SessionStart: ["session:start"],
|
|
1673
|
-
SessionEnd: ["session:end"],
|
|
1674
|
-
BeforePrompt: ["prompt:submit"],
|
|
1675
|
-
AfterResponse: ["prompt:response"],
|
|
1676
|
-
BeforeTool: ["tool:before", "file:write", "file:edit", "file:delete", "mcp:before"],
|
|
1677
|
-
AfterTool: ["tool:after", "mcp:after"],
|
|
1678
|
-
BeforeShell: ["shell:before"],
|
|
1679
|
-
AfterShell: ["shell:after"]
|
|
1680
|
-
};
|
|
1681
|
-
var GeminiCliAdapter = class extends BaseAdapter {
|
|
1682
|
-
id = "gemini-cli";
|
|
1683
|
-
name = "Gemini CLI";
|
|
1684
|
-
version = "1.0";
|
|
1685
|
-
capabilities = {
|
|
1686
|
-
beforeHooks: true,
|
|
1687
|
-
afterHooks: true,
|
|
1688
|
-
mcp: true,
|
|
1689
|
-
configFile: true,
|
|
1690
|
-
supportedEvents: [
|
|
1691
|
-
"session:start",
|
|
1692
|
-
"session:end",
|
|
1693
|
-
"prompt:submit",
|
|
1694
|
-
"prompt:response",
|
|
1695
|
-
"tool:before",
|
|
1696
|
-
"tool:after",
|
|
1697
|
-
"file:write",
|
|
1698
|
-
"file:edit",
|
|
1699
|
-
"file:delete",
|
|
1700
|
-
"shell:before",
|
|
1701
|
-
"shell:after",
|
|
1702
|
-
"mcp:before",
|
|
1703
|
-
"mcp:after"
|
|
1704
|
-
],
|
|
1705
|
-
blockableEvents: [
|
|
1706
|
-
"prompt:submit",
|
|
1707
|
-
"tool:before",
|
|
1708
|
-
"file:write",
|
|
1709
|
-
"file:edit",
|
|
1710
|
-
"file:delete",
|
|
1711
|
-
"shell:before",
|
|
1712
|
-
"mcp:before"
|
|
1713
|
-
]
|
|
1714
|
-
};
|
|
1715
|
-
async detect() {
|
|
1716
|
-
const hasCommand = await this.commandExists("gemini");
|
|
1717
|
-
const hasConfig = existsSync(resolve(process.cwd(), ".gemini"));
|
|
1718
|
-
return hasCommand || hasConfig;
|
|
1719
|
-
}
|
|
1720
|
-
async generate(hooks) {
|
|
1721
|
-
const configs = [];
|
|
1722
|
-
const neededEvents = /* @__PURE__ */ new Set();
|
|
1723
|
-
for (const hook2 of hooks) {
|
|
1724
|
-
for (const event of hook2.events) {
|
|
1725
|
-
const nativeEvents = this.mapEvent(event);
|
|
1726
|
-
for (const ne of nativeEvents) {
|
|
1727
|
-
neededEvents.add(ne);
|
|
1728
|
-
}
|
|
1729
|
-
}
|
|
1730
|
-
}
|
|
1731
|
-
for (const event of neededEvents) {
|
|
1732
|
-
configs.push({
|
|
1733
|
-
path: `.gemini/hooks/${event}.js`,
|
|
1734
|
-
content: this.generateEventScript(event),
|
|
1735
|
-
format: "js"
|
|
1736
|
-
});
|
|
1737
|
-
}
|
|
1738
|
-
configs.push({
|
|
1739
|
-
path: ".gemini/settings.json",
|
|
1740
|
-
content: JSON.stringify(
|
|
1741
|
-
{
|
|
1742
|
-
hooks: {
|
|
1743
|
-
enabled: true,
|
|
1744
|
-
directory: ".gemini/hooks"
|
|
1745
|
-
}
|
|
1746
|
-
},
|
|
1747
|
-
null,
|
|
1748
|
-
2
|
|
1749
|
-
) + "\n",
|
|
1750
|
-
format: "json"
|
|
1751
|
-
});
|
|
1752
|
-
return configs;
|
|
1753
|
-
}
|
|
1754
|
-
mapEvent(event) {
|
|
1755
|
-
return EVENT_MAP7[event] ?? [];
|
|
1756
|
-
}
|
|
1757
|
-
mapNativeEvent(nativeEvent) {
|
|
1758
|
-
return REVERSE_MAP7[nativeEvent] ?? [];
|
|
1759
|
-
}
|
|
1760
|
-
async uninstall() {
|
|
1761
|
-
for (const event of Object.keys(REVERSE_MAP7)) {
|
|
1762
|
-
await this.removeFile(`.gemini/hooks/${event}.js`);
|
|
1763
|
-
}
|
|
1764
|
-
}
|
|
1765
|
-
generateEventScript(nativeEvent) {
|
|
1766
|
-
return `#!/usr/bin/env node
|
|
1767
|
-
/**
|
|
1768
|
-
* ai-hooks runner for Gemini CLI (${nativeEvent}).
|
|
1769
|
-
* Generated by: ai-hooks generate
|
|
1770
|
-
*/
|
|
1771
|
-
import { loadConfig, HookEngine } from "@premierstudio/ai-hooks";
|
|
1772
|
-
|
|
1773
|
-
const input = JSON.parse(process.env.GEMINI_HOOK_INPUT ?? "{}");
|
|
1774
|
-
|
|
1775
|
-
async function run() {
|
|
1776
|
-
const config = await loadConfig();
|
|
1777
|
-
const engine = new HookEngine(config);
|
|
1778
|
-
const toolInfo = { name: "gemini-cli", version: "1.0" };
|
|
1779
|
-
const timestamp = Date.now();
|
|
1780
|
-
const metadata = {};
|
|
1781
|
-
|
|
1782
|
-
let event;
|
|
1783
|
-
switch ("${nativeEvent}") {
|
|
1784
|
-
case "SessionStart":
|
|
1785
|
-
event = { type: "session:start", tool: "gemini-cli", version: "1.0", workingDirectory: process.cwd(), timestamp, metadata };
|
|
1786
|
-
break;
|
|
1787
|
-
case "BeforeShell":
|
|
1788
|
-
event = { type: "shell:before", command: input.command ?? "", cwd: process.cwd(), timestamp, metadata };
|
|
1789
|
-
break;
|
|
1790
|
-
case "BeforeTool":
|
|
1791
|
-
event = { type: "tool:before", toolName: input.toolName ?? "unknown", input, timestamp, metadata };
|
|
1792
|
-
break;
|
|
1793
|
-
case "BeforePrompt":
|
|
1794
|
-
event = { type: "prompt:submit", prompt: input.prompt ?? "", timestamp, metadata };
|
|
1795
|
-
break;
|
|
1796
|
-
default:
|
|
1797
|
-
event = { type: "tool:after", toolName: "${nativeEvent}", input, output: {}, duration: 0, timestamp, metadata };
|
|
1798
|
-
}
|
|
1799
|
-
|
|
1800
|
-
const results = await engine.emit(event, toolInfo);
|
|
1801
|
-
const blocked = results.find((r) => r.blocked);
|
|
1802
|
-
|
|
1803
|
-
if (blocked) {
|
|
1804
|
-
console.log(JSON.stringify({ blocked: true, reason: blocked.reason }));
|
|
1805
|
-
process.exit(1);
|
|
1806
|
-
}
|
|
1807
|
-
}
|
|
1808
|
-
|
|
1809
|
-
run().catch(() => process.exit(1));
|
|
1810
|
-
`;
|
|
1811
|
-
}
|
|
1812
|
-
};
|
|
1813
|
-
var adapter7 = new GeminiCliAdapter();
|
|
1814
|
-
registry.register(adapter7);
|
|
1815
|
-
var EVENT_MAP8 = {
|
|
1816
|
-
"session:start": ["agentSpawn"],
|
|
1817
|
-
"session:end": ["stop"],
|
|
1818
|
-
"prompt:submit": ["userPromptSubmit"],
|
|
1819
|
-
"prompt:response": ["stop"],
|
|
1820
|
-
"tool:before": ["preToolUse"],
|
|
1821
|
-
"tool:after": ["postToolUse"],
|
|
1822
|
-
"file:read": ["preToolUse"],
|
|
1823
|
-
"file:write": ["preToolUse"],
|
|
1824
|
-
"file:edit": ["preToolUse"],
|
|
1825
|
-
"file:delete": ["preToolUse"],
|
|
1826
|
-
"shell:before": ["preToolUse"],
|
|
1827
|
-
"shell:after": ["postToolUse"],
|
|
1828
|
-
"mcp:before": ["preToolUse"],
|
|
1829
|
-
"mcp:after": ["postToolUse"]
|
|
1830
|
-
};
|
|
1831
|
-
var REVERSE_MAP8 = {
|
|
1832
|
-
agentSpawn: ["session:start"],
|
|
1833
|
-
userPromptSubmit: ["prompt:submit"],
|
|
1834
|
-
preToolUse: [
|
|
1835
|
-
"tool:before",
|
|
1836
|
-
"file:read",
|
|
1837
|
-
"file:write",
|
|
1838
|
-
"file:edit",
|
|
1839
|
-
"file:delete",
|
|
1840
|
-
"shell:before",
|
|
1841
|
-
"mcp:before"
|
|
1842
|
-
],
|
|
1843
|
-
postToolUse: ["tool:after", "shell:after", "mcp:after"],
|
|
1844
|
-
stop: ["session:end", "prompt:response"]
|
|
1845
|
-
};
|
|
1846
|
-
var KiroAdapter = class extends BaseAdapter {
|
|
1847
|
-
id = "kiro";
|
|
1848
|
-
name = "Kiro CLI";
|
|
1849
|
-
version = "1.0";
|
|
1850
|
-
capabilities = {
|
|
1851
|
-
beforeHooks: true,
|
|
1852
|
-
afterHooks: true,
|
|
1853
|
-
mcp: true,
|
|
1854
|
-
configFile: true,
|
|
1855
|
-
supportedEvents: [
|
|
1856
|
-
"session:start",
|
|
1857
|
-
"session:end",
|
|
1858
|
-
"prompt:submit",
|
|
1859
|
-
"prompt:response",
|
|
1860
|
-
"tool:before",
|
|
1861
|
-
"tool:after",
|
|
1862
|
-
"file:read",
|
|
1863
|
-
"file:write",
|
|
1864
|
-
"file:edit",
|
|
1865
|
-
"file:delete",
|
|
1866
|
-
"shell:before",
|
|
1867
|
-
"shell:after",
|
|
1868
|
-
"mcp:before",
|
|
1869
|
-
"mcp:after"
|
|
1870
|
-
],
|
|
1871
|
-
blockableEvents: [
|
|
1872
|
-
"tool:before",
|
|
1873
|
-
"file:read",
|
|
1874
|
-
"file:write",
|
|
1875
|
-
"file:edit",
|
|
1876
|
-
"file:delete",
|
|
1877
|
-
"shell:before",
|
|
1878
|
-
"mcp:before"
|
|
1879
|
-
]
|
|
1880
|
-
};
|
|
1881
|
-
async detect() {
|
|
1882
|
-
const hasCommand = await this.commandExists("kiro");
|
|
1883
|
-
const hasDir = existsSync(resolve(process.cwd(), ".kiro"));
|
|
1884
|
-
return hasCommand || hasDir;
|
|
1885
|
-
}
|
|
1886
|
-
async generate(hooks) {
|
|
1887
|
-
const configs = [];
|
|
1888
|
-
const neededEvents = /* @__PURE__ */ new Set();
|
|
1889
|
-
for (const hook2 of hooks) {
|
|
1890
|
-
for (const event of hook2.events) {
|
|
1891
|
-
const nativeEvents = this.mapEvent(event);
|
|
1892
|
-
for (const ne of nativeEvents) {
|
|
1893
|
-
neededEvents.add(ne);
|
|
1894
|
-
}
|
|
1895
|
-
}
|
|
1896
|
-
}
|
|
1897
|
-
configs.push({
|
|
1898
|
-
path: ".kiro/hooks/ai-hooks-runner.js",
|
|
1899
|
-
content: this.generateRunner(),
|
|
1900
|
-
format: "js"
|
|
1901
|
-
});
|
|
1902
|
-
const hooksConfig = {};
|
|
1903
|
-
for (const event of neededEvents) {
|
|
1904
|
-
const entry = {
|
|
1905
|
-
command: "node .kiro/hooks/ai-hooks-runner.js"
|
|
1906
|
-
};
|
|
1907
|
-
if (event === "preToolUse" || event === "postToolUse") {
|
|
1908
|
-
entry.matcher = "*";
|
|
1909
|
-
}
|
|
1910
|
-
if (!hooksConfig[event]) {
|
|
1911
|
-
hooksConfig[event] = [];
|
|
1912
|
-
}
|
|
1913
|
-
hooksConfig[event].push(entry);
|
|
1914
|
-
}
|
|
1915
|
-
configs.push({
|
|
1916
|
-
path: ".kiro/hooks/ai-hooks.json",
|
|
1917
|
-
content: JSON.stringify({ hooks: hooksConfig }, null, 2) + "\n",
|
|
1918
|
-
format: "json"
|
|
1919
|
-
});
|
|
1920
|
-
return configs;
|
|
1921
|
-
}
|
|
1922
|
-
mapEvent(event) {
|
|
1923
|
-
return EVENT_MAP8[event] ?? [];
|
|
1924
|
-
}
|
|
1925
|
-
mapNativeEvent(nativeEvent) {
|
|
1926
|
-
return REVERSE_MAP8[nativeEvent] ?? [];
|
|
1927
|
-
}
|
|
1928
|
-
async uninstall() {
|
|
1929
|
-
await this.removeFile(".kiro/hooks/ai-hooks-runner.js");
|
|
1930
|
-
await this.removeFile(".kiro/hooks/ai-hooks.json");
|
|
1931
|
-
}
|
|
1932
|
-
generateRunner() {
|
|
1933
|
-
return `#!/usr/bin/env node
|
|
1934
|
-
/**
|
|
1935
|
-
* ai-hooks runner for Kiro CLI.
|
|
1936
|
-
* Generated by: ai-hooks generate
|
|
1937
|
-
*
|
|
1938
|
-
* Kiro passes hook event data as JSON via STDIN.
|
|
1939
|
-
* Exit code 0 = success, exit code 2 = block (preToolUse only).
|
|
1940
|
-
*
|
|
1941
|
-
* DO NOT EDIT - regenerate with: ai-hooks generate
|
|
1942
|
-
*/
|
|
1943
|
-
import { loadConfig, HookEngine } from "@premierstudio/ai-hooks";
|
|
1944
|
-
|
|
1945
|
-
async function readStdin() {
|
|
1946
|
-
const chunks = [];
|
|
1947
|
-
for await (const chunk of process.stdin) {
|
|
1948
|
-
chunks.push(chunk);
|
|
1949
|
-
}
|
|
1950
|
-
return Buffer.concat(chunks).toString("utf-8");
|
|
1951
|
-
}
|
|
1952
|
-
|
|
1953
|
-
async function run() {
|
|
1954
|
-
const raw = await readStdin();
|
|
1955
|
-
const input = JSON.parse(raw || "{}");
|
|
1956
|
-
const hookEventName = input.hook_event_name ?? "";
|
|
1957
|
-
const toolName = input.tool_name ?? "";
|
|
1958
|
-
const toolInput = input.tool_input ?? {};
|
|
1959
|
-
|
|
1960
|
-
const config = await loadConfig();
|
|
1961
|
-
const engine = new HookEngine(config);
|
|
1962
|
-
const toolInfo = { name: "kiro", version: "1.0" };
|
|
1963
|
-
const timestamp = Date.now();
|
|
1964
|
-
const metadata = {};
|
|
1965
|
-
|
|
1966
|
-
let event;
|
|
1967
|
-
switch (hookEventName) {
|
|
1968
|
-
case "agentSpawn":
|
|
1969
|
-
event = {
|
|
1970
|
-
type: "session:start",
|
|
1971
|
-
tool: "kiro",
|
|
1972
|
-
version: "1.0",
|
|
1973
|
-
workingDirectory: input.cwd ?? process.cwd(),
|
|
1974
|
-
timestamp,
|
|
1975
|
-
metadata,
|
|
1976
|
-
};
|
|
1977
|
-
break;
|
|
1978
|
-
case "userPromptSubmit":
|
|
1979
|
-
event = {
|
|
1980
|
-
type: "prompt:submit",
|
|
1981
|
-
prompt: toolInput.prompt ?? "",
|
|
1982
|
-
timestamp,
|
|
1983
|
-
metadata,
|
|
1984
|
-
};
|
|
1985
|
-
break;
|
|
1986
|
-
case "preToolUse":
|
|
1987
|
-
event = resolvePreToolEvent(toolName, toolInput, timestamp, metadata);
|
|
1988
|
-
break;
|
|
1989
|
-
case "postToolUse":
|
|
1990
|
-
event = resolvePostToolEvent(toolName, toolInput, input.tool_response ?? {}, timestamp, metadata);
|
|
1991
|
-
break;
|
|
1992
|
-
case "stop":
|
|
1993
|
-
event = {
|
|
1994
|
-
type: "session:end",
|
|
1995
|
-
tool: "kiro",
|
|
1996
|
-
duration: 0,
|
|
1997
|
-
timestamp,
|
|
1998
|
-
metadata,
|
|
1999
|
-
};
|
|
2000
|
-
break;
|
|
2001
|
-
default:
|
|
2002
|
-
process.exit(0);
|
|
2003
|
-
}
|
|
2004
|
-
|
|
2005
|
-
const results = await engine.emit(event, toolInfo);
|
|
2006
|
-
const blocked = results.find((r) => r.blocked);
|
|
2007
|
-
|
|
2008
|
-
if (blocked) {
|
|
2009
|
-
process.stderr.write(blocked.reason ?? "Blocked by ai-hooks");
|
|
2010
|
-
process.exit(2);
|
|
2011
|
-
}
|
|
2012
|
-
}
|
|
2013
|
-
|
|
2014
|
-
function resolvePreToolEvent(toolName, toolInput, timestamp, metadata) {
|
|
2015
|
-
switch (toolName) {
|
|
2016
|
-
case "fs_write":
|
|
2017
|
-
case "write":
|
|
2018
|
-
return { type: "file:write", path: toolInput.path ?? "", content: toolInput.content ?? "", timestamp, metadata };
|
|
2019
|
-
case "fs_edit":
|
|
2020
|
-
case "edit":
|
|
2021
|
-
return { type: "file:edit", path: toolInput.path ?? "", oldContent: toolInput.old_string ?? "", newContent: toolInput.new_string ?? "", timestamp, metadata };
|
|
2022
|
-
case "fs_read":
|
|
2023
|
-
case "read":
|
|
2024
|
-
return { type: "file:read", path: toolInput.path ?? "", timestamp, metadata };
|
|
2025
|
-
case "execute_bash":
|
|
2026
|
-
case "shell":
|
|
2027
|
-
return { type: "shell:before", command: toolInput.command ?? "", cwd: process.cwd(), timestamp, metadata };
|
|
2028
|
-
default:
|
|
2029
|
-
return { type: "tool:before", toolName: toolName || "unknown", input: toolInput, timestamp, metadata };
|
|
2030
|
-
}
|
|
2031
|
-
}
|
|
2032
|
-
|
|
2033
|
-
function resolvePostToolEvent(toolName, toolInput, toolResponse, timestamp, metadata) {
|
|
2034
|
-
switch (toolName) {
|
|
2035
|
-
case "execute_bash":
|
|
2036
|
-
case "shell":
|
|
2037
|
-
return {
|
|
2038
|
-
type: "shell:after",
|
|
2039
|
-
command: toolInput.command ?? "",
|
|
2040
|
-
cwd: process.cwd(),
|
|
2041
|
-
exitCode: toolResponse.exitCode ?? 0,
|
|
2042
|
-
stdout: toolResponse.stdout ?? "",
|
|
2043
|
-
stderr: toolResponse.stderr ?? "",
|
|
2044
|
-
duration: 0,
|
|
2045
|
-
timestamp,
|
|
2046
|
-
metadata,
|
|
2047
|
-
};
|
|
2048
|
-
default:
|
|
2049
|
-
return { type: "tool:after", toolName: toolName || "unknown", input: toolInput, output: toolResponse, duration: 0, timestamp, metadata };
|
|
2050
|
-
}
|
|
2051
|
-
}
|
|
2052
|
-
|
|
2053
|
-
run().catch((err) => {
|
|
2054
|
-
console.error("[ai-hooks] Error:", err.message);
|
|
2055
|
-
process.exit(1);
|
|
2056
|
-
});
|
|
2057
|
-
`;
|
|
2058
|
-
}
|
|
2059
|
-
};
|
|
2060
|
-
var adapter8 = new KiroAdapter();
|
|
2061
|
-
registry.register(adapter8);
|
|
2062
|
-
var EVENT_MAP9 = {
|
|
2063
|
-
"session:start": ["session.created"],
|
|
2064
|
-
"session:end": ["session.idle"],
|
|
2065
|
-
"prompt:submit": ["message.updated"],
|
|
2066
|
-
"prompt:response": ["message.part.updated"],
|
|
2067
|
-
"tool:before": ["tool.execute.before"],
|
|
2068
|
-
"tool:after": ["tool.execute.after"],
|
|
2069
|
-
"file:read": ["tool.execute.before"],
|
|
2070
|
-
"file:write": ["tool.execute.before", "file.edited"],
|
|
2071
|
-
"file:edit": ["tool.execute.before", "file.edited"],
|
|
2072
|
-
"file:delete": ["tool.execute.before"],
|
|
2073
|
-
"shell:before": ["tool.execute.before"],
|
|
2074
|
-
"shell:after": ["tool.execute.after"],
|
|
2075
|
-
"mcp:before": ["tool.execute.before"],
|
|
2076
|
-
"mcp:after": ["tool.execute.after"],
|
|
2077
|
-
notification: ["tui.toast.show"]
|
|
2078
|
-
};
|
|
2079
|
-
var REVERSE_MAP9 = {
|
|
2080
|
-
"session.created": ["session:start"],
|
|
2081
|
-
"session.idle": ["session:end"],
|
|
2082
|
-
"message.updated": ["prompt:submit"],
|
|
2083
|
-
"message.part.updated": ["prompt:response"],
|
|
2084
|
-
"tool.execute.before": [
|
|
2085
|
-
"tool:before",
|
|
2086
|
-
"file:read",
|
|
2087
|
-
"file:write",
|
|
2088
|
-
"file:edit",
|
|
2089
|
-
"file:delete",
|
|
2090
|
-
"shell:before",
|
|
2091
|
-
"mcp:before"
|
|
2092
|
-
],
|
|
2093
|
-
"tool.execute.after": ["tool:after", "shell:after", "mcp:after"],
|
|
2094
|
-
"file.edited": ["file:write", "file:edit"],
|
|
2095
|
-
"tui.toast.show": ["notification"]
|
|
2096
|
-
};
|
|
2097
|
-
var OpenCodeAdapter = class extends BaseAdapter {
|
|
2098
|
-
id = "opencode";
|
|
2099
|
-
name = "OpenCode";
|
|
2100
|
-
version = "1.0";
|
|
2101
|
-
capabilities = {
|
|
2102
|
-
beforeHooks: true,
|
|
2103
|
-
afterHooks: true,
|
|
2104
|
-
mcp: true,
|
|
2105
|
-
configFile: true,
|
|
2106
|
-
supportedEvents: [
|
|
2107
|
-
"session:start",
|
|
2108
|
-
"session:end",
|
|
2109
|
-
"prompt:submit",
|
|
2110
|
-
"prompt:response",
|
|
2111
|
-
"tool:before",
|
|
2112
|
-
"tool:after",
|
|
2113
|
-
"file:read",
|
|
2114
|
-
"file:write",
|
|
2115
|
-
"file:edit",
|
|
2116
|
-
"file:delete",
|
|
2117
|
-
"shell:before",
|
|
2118
|
-
"shell:after",
|
|
2119
|
-
"mcp:before",
|
|
2120
|
-
"mcp:after",
|
|
2121
|
-
"notification"
|
|
2122
|
-
],
|
|
2123
|
-
blockableEvents: [
|
|
2124
|
-
"tool:before",
|
|
2125
|
-
"file:read",
|
|
2126
|
-
"file:write",
|
|
2127
|
-
"file:edit",
|
|
2128
|
-
"file:delete",
|
|
2129
|
-
"shell:before",
|
|
2130
|
-
"mcp:before"
|
|
2131
|
-
]
|
|
2132
|
-
};
|
|
2133
|
-
async detect() {
|
|
2134
|
-
const hasCommand = await this.commandExists("opencode");
|
|
2135
|
-
const hasDir = existsSync(resolve(process.cwd(), ".opencode"));
|
|
2136
|
-
return hasCommand || hasDir;
|
|
2137
|
-
}
|
|
2138
|
-
async generate(hooks) {
|
|
2139
|
-
const configs = [];
|
|
2140
|
-
const neededEvents = /* @__PURE__ */ new Set();
|
|
2141
|
-
for (const hook2 of hooks) {
|
|
2142
|
-
for (const event of hook2.events) {
|
|
2143
|
-
const nativeEvents = this.mapEvent(event);
|
|
2144
|
-
for (const ne of nativeEvents) {
|
|
2145
|
-
neededEvents.add(ne);
|
|
2146
|
-
}
|
|
2147
|
-
}
|
|
2148
|
-
}
|
|
2149
|
-
configs.push({
|
|
2150
|
-
path: ".opencode/plugins/ai-hooks-plugin.js",
|
|
2151
|
-
content: this.generatePlugin(neededEvents),
|
|
2152
|
-
format: "js"
|
|
2153
|
-
});
|
|
2154
|
-
configs.push({
|
|
2155
|
-
path: ".opencode/plugins/package.json",
|
|
2156
|
-
content: JSON.stringify(
|
|
2157
|
-
{
|
|
2158
|
-
name: "ai-hooks-opencode-plugin",
|
|
2159
|
-
version: "1.0.0",
|
|
2160
|
-
type: "module",
|
|
2161
|
-
main: "ai-hooks-plugin.js",
|
|
2162
|
-
dependencies: {
|
|
2163
|
-
"@premierstudio/ai-hooks": "*"
|
|
2164
|
-
}
|
|
2165
|
-
},
|
|
2166
|
-
null,
|
|
2167
|
-
2
|
|
2168
|
-
) + "\n",
|
|
2169
|
-
format: "json"
|
|
2170
|
-
});
|
|
2171
|
-
return configs;
|
|
2172
|
-
}
|
|
2173
|
-
mapEvent(event) {
|
|
2174
|
-
return EVENT_MAP9[event] ?? [];
|
|
2175
|
-
}
|
|
2176
|
-
mapNativeEvent(nativeEvent) {
|
|
2177
|
-
return REVERSE_MAP9[nativeEvent] ?? [];
|
|
2178
|
-
}
|
|
2179
|
-
async uninstall() {
|
|
2180
|
-
await this.removeFile(".opencode/plugins/ai-hooks-plugin.js");
|
|
2181
|
-
await this.removeFile(".opencode/plugins/package.json");
|
|
2182
|
-
}
|
|
2183
|
-
generatePlugin(neededEvents) {
|
|
2184
|
-
const hookEntries = [...neededEvents].map((event) => {
|
|
2185
|
-
return ` "${event}": async (input, output) => {
|
|
2186
|
-
await handleHook("${event}", input, output);
|
|
2187
|
-
}`;
|
|
2188
|
-
}).join(",\n");
|
|
2189
|
-
return `/**
|
|
2190
|
-
* ai-hooks plugin for OpenCode.
|
|
2191
|
-
* Generated by: ai-hooks generate
|
|
2192
|
-
*
|
|
2193
|
-
* This plugin hooks into OpenCode's lifecycle events and delegates
|
|
2194
|
-
* to the ai-hooks engine for unified hook management.
|
|
2195
|
-
*
|
|
2196
|
-
* DO NOT EDIT - regenerate with: ai-hooks generate
|
|
2197
|
-
*/
|
|
2198
|
-
import { loadConfig, HookEngine } from "@premierstudio/ai-hooks";
|
|
2199
|
-
|
|
2200
|
-
let engine;
|
|
2201
|
-
|
|
2202
|
-
async function getEngine() {
|
|
2203
|
-
if (!engine) {
|
|
2204
|
-
const config = await loadConfig();
|
|
2205
|
-
engine = new HookEngine(config);
|
|
2206
|
-
}
|
|
2207
|
-
return engine;
|
|
2208
|
-
}
|
|
2209
|
-
|
|
2210
|
-
async function handleHook(hookName, input, output) {
|
|
2211
|
-
const eng = await getEngine();
|
|
2212
|
-
const toolInfo = { name: "opencode", version: "1.0" };
|
|
2213
|
-
const timestamp = Date.now();
|
|
2214
|
-
const metadata = {};
|
|
2215
|
-
|
|
2216
|
-
let event;
|
|
2217
|
-
switch (hookName) {
|
|
2218
|
-
case "session.created":
|
|
2219
|
-
event = { type: "session:start", tool: "opencode", version: "1.0", workingDirectory: process.cwd(), timestamp, metadata };
|
|
2220
|
-
break;
|
|
2221
|
-
case "session.idle":
|
|
2222
|
-
event = { type: "session:end", tool: "opencode", duration: 0, timestamp, metadata };
|
|
2223
|
-
break;
|
|
2224
|
-
case "tool.execute.before":
|
|
2225
|
-
event = resolveToolBefore(input, timestamp, metadata);
|
|
2226
|
-
break;
|
|
2227
|
-
case "tool.execute.after":
|
|
2228
|
-
event = resolveToolAfter(input, timestamp, metadata);
|
|
2229
|
-
break;
|
|
2230
|
-
case "file.edited":
|
|
2231
|
-
event = { type: "file:edit", path: input.path ?? "", oldContent: "", newContent: "", timestamp, metadata };
|
|
2232
|
-
break;
|
|
2233
|
-
case "message.updated":
|
|
2234
|
-
event = { type: "prompt:submit", prompt: input.content ?? "", timestamp, metadata };
|
|
2235
|
-
break;
|
|
2236
|
-
case "message.part.updated":
|
|
2237
|
-
event = { type: "prompt:response", response: input.content ?? "", model: "unknown", tokens: { input: 0, output: 0 }, timestamp, metadata };
|
|
2238
|
-
break;
|
|
2239
|
-
case "tui.toast.show":
|
|
2240
|
-
event = { type: "notification", level: "info", message: input.message ?? "", timestamp, metadata };
|
|
2241
|
-
break;
|
|
2242
|
-
default:
|
|
2243
|
-
return output;
|
|
2244
|
-
}
|
|
2245
|
-
|
|
2246
|
-
const results = await eng.emit(event, toolInfo);
|
|
2247
|
-
const blocked = results.find((r) => r.blocked);
|
|
2248
|
-
|
|
2249
|
-
if (blocked && hookName === "tool.execute.before") {
|
|
2250
|
-
return { ...output, blocked: true, reason: blocked.reason ?? "Blocked by ai-hooks" };
|
|
2251
|
-
}
|
|
2252
|
-
|
|
2253
|
-
return output;
|
|
2254
|
-
}
|
|
2255
|
-
|
|
2256
|
-
function resolveToolBefore(input, timestamp, metadata) {
|
|
2257
|
-
const toolName = input.tool ?? input.name ?? "unknown";
|
|
2258
|
-
const toolInput = input.input ?? input.args ?? {};
|
|
2259
|
-
|
|
2260
|
-
switch (toolName) {
|
|
2261
|
-
case "file_write":
|
|
2262
|
-
case "write":
|
|
2263
|
-
return { type: "file:write", path: toolInput.path ?? "", content: toolInput.content ?? "", timestamp, metadata };
|
|
2264
|
-
case "file_edit":
|
|
2265
|
-
case "edit":
|
|
2266
|
-
return { type: "file:edit", path: toolInput.path ?? "", oldContent: toolInput.old ?? "", newContent: toolInput.new ?? "", timestamp, metadata };
|
|
2267
|
-
case "file_read":
|
|
2268
|
-
case "read":
|
|
2269
|
-
return { type: "file:read", path: toolInput.path ?? "", timestamp, metadata };
|
|
2270
|
-
case "bash":
|
|
2271
|
-
case "shell":
|
|
2272
|
-
return { type: "shell:before", command: toolInput.command ?? "", cwd: process.cwd(), timestamp, metadata };
|
|
2273
|
-
default:
|
|
2274
|
-
return { type: "tool:before", toolName, input: toolInput, timestamp, metadata };
|
|
2275
|
-
}
|
|
2276
|
-
}
|
|
2277
|
-
|
|
2278
|
-
function resolveToolAfter(input, timestamp, metadata) {
|
|
2279
|
-
const toolName = input.tool ?? input.name ?? "unknown";
|
|
2280
|
-
const toolInput = input.input ?? input.args ?? {};
|
|
2281
|
-
const toolOutput = input.output ?? input.result ?? {};
|
|
2282
|
-
|
|
2283
|
-
switch (toolName) {
|
|
2284
|
-
case "bash":
|
|
2285
|
-
case "shell":
|
|
2286
|
-
return {
|
|
2287
|
-
type: "shell:after",
|
|
2288
|
-
command: toolInput.command ?? "",
|
|
2289
|
-
cwd: process.cwd(),
|
|
2290
|
-
exitCode: toolOutput.exitCode ?? 0,
|
|
2291
|
-
stdout: toolOutput.stdout ?? "",
|
|
2292
|
-
stderr: toolOutput.stderr ?? "",
|
|
2293
|
-
duration: 0,
|
|
2294
|
-
timestamp,
|
|
2295
|
-
metadata,
|
|
2296
|
-
};
|
|
2297
|
-
default:
|
|
2298
|
-
return { type: "tool:after", toolName, input: toolInput, output: toolOutput, duration: 0, timestamp, metadata };
|
|
2299
|
-
}
|
|
2300
|
-
}
|
|
2301
|
-
|
|
2302
|
-
export const AiHooksPlugin = async ({ project, directory }) => {
|
|
2303
|
-
return {
|
|
2304
|
-
${hookEntries}
|
|
2305
|
-
};
|
|
2306
|
-
};
|
|
2307
|
-
`;
|
|
2308
|
-
}
|
|
2309
|
-
};
|
|
2310
|
-
var adapter9 = new OpenCodeAdapter();
|
|
2311
|
-
registry.register(adapter9);
|
|
1
|
+
import { findConfigFile, loadConfig, HookEngine } from '../chunk-HCEOWJK3.js';
|
|
2
|
+
import '../chunk-ZOWUSGNF.js';
|
|
3
|
+
import { registry } from '../chunk-FHFRGMW6.js';
|
|
4
|
+
import '../chunk-N7ASBTX5.js';
|
|
2312
5
|
|
|
2313
6
|
// src/cli/index.ts
|
|
2314
7
|
var HELP = `
|
|
@@ -2382,7 +75,7 @@ async function cmdInit(flags) {
|
|
|
2382
75
|
console.log(`Config already exists: ${existing}`);
|
|
2383
76
|
return;
|
|
2384
77
|
}
|
|
2385
|
-
const { writeFile
|
|
78
|
+
const { writeFile } = await import('fs/promises');
|
|
2386
79
|
const template = `import { defineConfig, hook, builtinHooks } from "@premierstudio/ai-hooks";
|
|
2387
80
|
|
|
2388
81
|
export default defineConfig({
|
|
@@ -2412,7 +105,7 @@ export default defineConfig({
|
|
|
2412
105
|
console.log("[dry-run] Would create ai-hooks.config.ts");
|
|
2413
106
|
return;
|
|
2414
107
|
}
|
|
2415
|
-
await
|
|
108
|
+
await writeFile("ai-hooks.config.ts", template, "utf-8");
|
|
2416
109
|
console.log("Created ai-hooks.config.ts");
|
|
2417
110
|
console.log("");
|
|
2418
111
|
console.log("Next steps:");
|
|
@@ -2425,18 +118,18 @@ async function cmdDetect(flags) {
|
|
|
2425
118
|
const detected = await registry.detectAll();
|
|
2426
119
|
const all = registry.list();
|
|
2427
120
|
for (const id of all) {
|
|
2428
|
-
const
|
|
2429
|
-
if (!
|
|
121
|
+
const adapter = registry.get(id);
|
|
122
|
+
if (!adapter) continue;
|
|
2430
123
|
const isDetected = detected.some((d) => d.id === id);
|
|
2431
124
|
const icon = isDetected ? "\u2713" : "\u2717";
|
|
2432
125
|
const color = isDetected ? "\x1B[32m" : "\x1B[90m";
|
|
2433
126
|
const reset = "\x1B[0m";
|
|
2434
127
|
const caps = [];
|
|
2435
|
-
if (
|
|
2436
|
-
if (
|
|
2437
|
-
let line = ` ${color}${icon}${reset} ${
|
|
128
|
+
if (adapter.capabilities.beforeHooks) caps.push("hooks");
|
|
129
|
+
if (adapter.capabilities.mcp) caps.push("mcp");
|
|
130
|
+
let line = ` ${color}${icon}${reset} ${adapter.name.padEnd(20)} ${caps.join(", ")}`;
|
|
2438
131
|
if (flags.verbose) {
|
|
2439
|
-
line += ` (${
|
|
132
|
+
line += ` (${adapter.capabilities.supportedEvents.length} events)`;
|
|
2440
133
|
}
|
|
2441
134
|
console.log(line);
|
|
2442
135
|
}
|
|
@@ -2455,8 +148,8 @@ async function cmdGenerate(flags) {
|
|
|
2455
148
|
}
|
|
2456
149
|
console.log(`Generating configs for ${adapters.length} tool(s)...
|
|
2457
150
|
`);
|
|
2458
|
-
for (const
|
|
2459
|
-
const configs = await
|
|
151
|
+
for (const adapter of adapters) {
|
|
152
|
+
const configs = await adapter.generate(config.hooks);
|
|
2460
153
|
for (const cfg of configs) {
|
|
2461
154
|
if (flags.dryRun) {
|
|
2462
155
|
console.log(` [dry-run] Would write: ${cfg.path}`);
|
|
@@ -2479,24 +172,24 @@ async function cmdInstall(flags) {
|
|
|
2479
172
|
}
|
|
2480
173
|
console.log(`Installing hooks into ${adapters.length} tool(s)...
|
|
2481
174
|
`);
|
|
2482
|
-
for (const
|
|
2483
|
-
const configs = await
|
|
175
|
+
for (const adapter of adapters) {
|
|
176
|
+
const configs = await adapter.generate(config.hooks);
|
|
2484
177
|
if (flags.dryRun) {
|
|
2485
178
|
for (const cfg of configs) {
|
|
2486
179
|
console.log(` [dry-run] Would install: ${cfg.path}`);
|
|
2487
180
|
}
|
|
2488
181
|
} else {
|
|
2489
|
-
await
|
|
2490
|
-
console.log(` \u2713 ${
|
|
182
|
+
await adapter.install(configs);
|
|
183
|
+
console.log(` \u2713 ${adapter.name}`);
|
|
2491
184
|
}
|
|
2492
185
|
}
|
|
2493
186
|
console.log("\nHooks installed!");
|
|
2494
187
|
}
|
|
2495
188
|
async function cmdUninstall(flags) {
|
|
2496
189
|
const adapters = await resolveAdapters(flags);
|
|
2497
|
-
for (const
|
|
2498
|
-
await
|
|
2499
|
-
console.log(` \u2713 Removed from ${
|
|
190
|
+
for (const adapter of adapters) {
|
|
191
|
+
await adapter.uninstall();
|
|
192
|
+
console.log(` \u2713 Removed from ${adapter.name}`);
|
|
2500
193
|
}
|
|
2501
194
|
console.log("\nHooks uninstalled.");
|
|
2502
195
|
}
|
|
@@ -2534,8 +227,8 @@ async function cmdStatus(flags) {
|
|
|
2534
227
|
console.log(` Hooks: ${hooks.length} registered`);
|
|
2535
228
|
}
|
|
2536
229
|
console.log("");
|
|
2537
|
-
for (const
|
|
2538
|
-
console.log(` \u2713 ${
|
|
230
|
+
for (const adapter of detected) {
|
|
231
|
+
console.log(` \u2713 ${adapter.name} (${adapter.id})`);
|
|
2539
232
|
}
|
|
2540
233
|
}
|
|
2541
234
|
function parseFlags(args) {
|
|
@@ -2558,9 +251,9 @@ async function resolveAdapters(flags) {
|
|
|
2558
251
|
const ids = flags.tools.split(",").map((t) => t.trim());
|
|
2559
252
|
const adapters = [];
|
|
2560
253
|
for (const id of ids) {
|
|
2561
|
-
const
|
|
2562
|
-
if (
|
|
2563
|
-
adapters.push(
|
|
254
|
+
const adapter = registry.get(id);
|
|
255
|
+
if (adapter) {
|
|
256
|
+
adapters.push(adapter);
|
|
2564
257
|
} else {
|
|
2565
258
|
console.warn(` Warning: Unknown adapter "${id}"`);
|
|
2566
259
|
}
|
|
@@ -2570,12 +263,12 @@ async function resolveAdapters(flags) {
|
|
|
2570
263
|
return registry.detectAll();
|
|
2571
264
|
}
|
|
2572
265
|
async function writeConfigs(configs) {
|
|
2573
|
-
const { writeFile
|
|
2574
|
-
const { dirname
|
|
266
|
+
const { writeFile, mkdir } = await import('fs/promises');
|
|
267
|
+
const { dirname, resolve } = await import('path');
|
|
2575
268
|
for (const cfg of configs) {
|
|
2576
|
-
const fullPath =
|
|
2577
|
-
await
|
|
2578
|
-
await
|
|
269
|
+
const fullPath = resolve(process.cwd(), cfg.path);
|
|
270
|
+
await mkdir(dirname(fullPath), { recursive: true });
|
|
271
|
+
await writeFile(fullPath, cfg.content, "utf-8");
|
|
2579
272
|
}
|
|
2580
273
|
}
|
|
2581
274
|
|