@premierstudio/ai-hooks 1.0.7 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2588 @@
1
+ #!/usr/bin/env node
2
+ import { pathToFileURL } from 'url';
3
+ import { existsSync } from 'fs';
4
+ import { resolve, dirname } from 'path';
5
+ import { readFile, mkdir, writeFile, rm } from 'fs/promises';
6
+
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);
2312
+
2313
+ // src/cli/index.ts
2314
+ var HELP = `
2315
+ ai-hooks - Universal hooks framework for AI coding tools
2316
+
2317
+ USAGE:
2318
+ ai-hooks <command> [options]
2319
+
2320
+ COMMANDS:
2321
+ init Create an ai-hooks.config.ts in the current directory
2322
+ detect Detect which AI tools are installed
2323
+ generate Generate native configs for detected/specified tools
2324
+ install Generate and install hooks into detected tools
2325
+ uninstall Remove ai-hooks from all detected tools
2326
+ list List all registered hooks from your config
2327
+ status Show current hook status across tools
2328
+ help Show this help message
2329
+
2330
+ OPTIONS:
2331
+ --tools Comma-separated list of tools (e.g., --tools=claude-code,codex)
2332
+ --config Path to config file (default: ai-hooks.config.ts)
2333
+ --verbose Show detailed output
2334
+ --dry-run Show what would be generated without writing files
2335
+
2336
+ EXAMPLES:
2337
+ ai-hooks init # Create config file
2338
+ ai-hooks detect # See which AI tools are installed
2339
+ ai-hooks generate # Generate configs for all detected tools
2340
+ ai-hooks install --tools=claude-code # Install hooks for Claude Code only
2341
+ `;
2342
+ async function run(args) {
2343
+ const command = args[0];
2344
+ const flags = parseFlags(args.slice(1));
2345
+ switch (command) {
2346
+ case "init":
2347
+ await cmdInit(flags);
2348
+ break;
2349
+ case "detect":
2350
+ await cmdDetect(flags);
2351
+ break;
2352
+ case "generate":
2353
+ await cmdGenerate(flags);
2354
+ break;
2355
+ case "install":
2356
+ await cmdInstall(flags);
2357
+ break;
2358
+ case "uninstall":
2359
+ await cmdUninstall(flags);
2360
+ break;
2361
+ case "list":
2362
+ await cmdList(flags);
2363
+ break;
2364
+ case "status":
2365
+ await cmdStatus(flags);
2366
+ break;
2367
+ case "help":
2368
+ case "--help":
2369
+ case "-h":
2370
+ case void 0:
2371
+ console.log(HELP);
2372
+ break;
2373
+ default:
2374
+ console.error(`Unknown command: ${command}`);
2375
+ console.log(HELP);
2376
+ process.exit(1);
2377
+ }
2378
+ }
2379
+ async function cmdInit(flags) {
2380
+ const existing = findConfigFile();
2381
+ if (existing) {
2382
+ console.log(`Config already exists: ${existing}`);
2383
+ return;
2384
+ }
2385
+ const { writeFile: writeFile2 } = await import('fs/promises');
2386
+ const template = `import { defineConfig, hook, builtinHooks } from "@premierstudio/ai-hooks";
2387
+
2388
+ export default defineConfig({
2389
+ // Start with built-in security hooks
2390
+ extends: [{ hooks: builtinHooks }],
2391
+
2392
+ hooks: [
2393
+ // Add your custom hooks here:
2394
+ //
2395
+ // hook("before", ["shell:before"], async (ctx, next) => {
2396
+ // console.log("Running:", ctx.event.command);
2397
+ // await next();
2398
+ // })
2399
+ // .id("my-hook")
2400
+ // .name("Log Shell Commands")
2401
+ // .build(),
2402
+ ],
2403
+
2404
+ settings: {
2405
+ logLevel: "warn",
2406
+ hookTimeout: 5000,
2407
+ failMode: "open",
2408
+ },
2409
+ });
2410
+ `;
2411
+ if (flags.dryRun) {
2412
+ console.log("[dry-run] Would create ai-hooks.config.ts");
2413
+ return;
2414
+ }
2415
+ await writeFile2("ai-hooks.config.ts", template, "utf-8");
2416
+ console.log("Created ai-hooks.config.ts");
2417
+ console.log("");
2418
+ console.log("Next steps:");
2419
+ console.log(" 1. Edit ai-hooks.config.ts to add your hooks");
2420
+ console.log(" 2. Run: ai-hooks detect (see which AI tools are installed)");
2421
+ console.log(" 3. Run: ai-hooks install (install hooks into your tools)");
2422
+ }
2423
+ async function cmdDetect(flags) {
2424
+ console.log("Detecting AI coding tools...\n");
2425
+ const detected = await registry.detectAll();
2426
+ const all = registry.list();
2427
+ for (const id of all) {
2428
+ const adapter10 = registry.get(id);
2429
+ if (!adapter10) continue;
2430
+ const isDetected = detected.some((d) => d.id === id);
2431
+ const icon = isDetected ? "\u2713" : "\u2717";
2432
+ const color = isDetected ? "\x1B[32m" : "\x1B[90m";
2433
+ const reset = "\x1B[0m";
2434
+ const caps = [];
2435
+ if (adapter10.capabilities.beforeHooks) caps.push("hooks");
2436
+ if (adapter10.capabilities.mcp) caps.push("mcp");
2437
+ let line = ` ${color}${icon}${reset} ${adapter10.name.padEnd(20)} ${caps.join(", ")}`;
2438
+ if (flags.verbose) {
2439
+ line += ` (${adapter10.capabilities.supportedEvents.length} events)`;
2440
+ }
2441
+ console.log(line);
2442
+ }
2443
+ console.log(`
2444
+ Detected ${detected.length}/${all.length} tools`);
2445
+ if (detected.length > 0 && !findConfigFile()) {
2446
+ console.log('\nRun "ai-hooks init" to create a config file');
2447
+ }
2448
+ }
2449
+ async function cmdGenerate(flags) {
2450
+ const config = await loadConfig(flags.config);
2451
+ const adapters = await resolveAdapters(flags);
2452
+ if (adapters.length === 0) {
2453
+ console.log("No AI tools detected. Use --tools to specify manually.");
2454
+ return;
2455
+ }
2456
+ console.log(`Generating configs for ${adapters.length} tool(s)...
2457
+ `);
2458
+ for (const adapter10 of adapters) {
2459
+ const configs = await adapter10.generate(config.hooks);
2460
+ for (const cfg of configs) {
2461
+ if (flags.dryRun) {
2462
+ console.log(` [dry-run] Would write: ${cfg.path}`);
2463
+ } else {
2464
+ console.log(` Generated: ${cfg.path}`);
2465
+ }
2466
+ }
2467
+ if (!flags.dryRun) {
2468
+ await writeConfigs(configs);
2469
+ }
2470
+ }
2471
+ console.log("\nDone!");
2472
+ }
2473
+ async function cmdInstall(flags) {
2474
+ const config = await loadConfig(flags.config);
2475
+ const adapters = await resolveAdapters(flags);
2476
+ if (adapters.length === 0) {
2477
+ console.log("No AI tools detected. Use --tools to specify manually.");
2478
+ return;
2479
+ }
2480
+ console.log(`Installing hooks into ${adapters.length} tool(s)...
2481
+ `);
2482
+ for (const adapter10 of adapters) {
2483
+ const configs = await adapter10.generate(config.hooks);
2484
+ if (flags.dryRun) {
2485
+ for (const cfg of configs) {
2486
+ console.log(` [dry-run] Would install: ${cfg.path}`);
2487
+ }
2488
+ } else {
2489
+ await adapter10.install(configs);
2490
+ console.log(` \u2713 ${adapter10.name}`);
2491
+ }
2492
+ }
2493
+ console.log("\nHooks installed!");
2494
+ }
2495
+ async function cmdUninstall(flags) {
2496
+ const adapters = await resolveAdapters(flags);
2497
+ for (const adapter10 of adapters) {
2498
+ await adapter10.uninstall();
2499
+ console.log(` \u2713 Removed from ${adapter10.name}`);
2500
+ }
2501
+ console.log("\nHooks uninstalled.");
2502
+ }
2503
+ async function cmdList(flags) {
2504
+ const config = await loadConfig(flags.config);
2505
+ const engine = new HookEngine(config);
2506
+ const hooks = engine.getHooks();
2507
+ if (hooks.length === 0) {
2508
+ console.log("No hooks registered. Edit ai-hooks.config.ts to add hooks.");
2509
+ return;
2510
+ }
2511
+ console.log(`${hooks.length} hook(s) registered:
2512
+ `);
2513
+ for (const h of hooks) {
2514
+ const status = h.enabled === false ? "\x1B[90m(disabled)\x1B[0m" : "";
2515
+ const priority = h.priority ?? 100;
2516
+ console.log(` [${h.phase}] ${h.name} ${status}`);
2517
+ console.log(` id: ${h.id} priority: ${priority} events: ${h.events.join(", ")}`);
2518
+ if (h.description && flags.verbose) {
2519
+ console.log(` ${h.description}`);
2520
+ }
2521
+ console.log("");
2522
+ }
2523
+ }
2524
+ async function cmdStatus(flags) {
2525
+ const hasConfig = findConfigFile();
2526
+ const detected = await registry.detectAll();
2527
+ console.log("ai-hooks status\n");
2528
+ console.log(` Config: ${hasConfig ?? "not found"}`);
2529
+ console.log(` Tools: ${detected.length} detected`);
2530
+ if (hasConfig) {
2531
+ const config = await loadConfig(flags.config);
2532
+ const engine = new HookEngine(config);
2533
+ const hooks = engine.getHooks();
2534
+ console.log(` Hooks: ${hooks.length} registered`);
2535
+ }
2536
+ console.log("");
2537
+ for (const adapter10 of detected) {
2538
+ console.log(` \u2713 ${adapter10.name} (${adapter10.id})`);
2539
+ }
2540
+ }
2541
+ function parseFlags(args) {
2542
+ const flags = {};
2543
+ for (const arg of args) {
2544
+ if (arg.startsWith("--tools=")) {
2545
+ flags.tools = arg.slice(8);
2546
+ } else if (arg.startsWith("--config=")) {
2547
+ flags.config = arg.slice(9);
2548
+ } else if (arg === "--verbose") {
2549
+ flags.verbose = true;
2550
+ } else if (arg === "--dry-run") {
2551
+ flags.dryRun = true;
2552
+ }
2553
+ }
2554
+ return flags;
2555
+ }
2556
+ async function resolveAdapters(flags) {
2557
+ if (flags.tools) {
2558
+ const ids = flags.tools.split(",").map((t) => t.trim());
2559
+ const adapters = [];
2560
+ for (const id of ids) {
2561
+ const adapter10 = registry.get(id);
2562
+ if (adapter10) {
2563
+ adapters.push(adapter10);
2564
+ } else {
2565
+ console.warn(` Warning: Unknown adapter "${id}"`);
2566
+ }
2567
+ }
2568
+ return adapters;
2569
+ }
2570
+ return registry.detectAll();
2571
+ }
2572
+ async function writeConfigs(configs) {
2573
+ const { writeFile: writeFile2, mkdir: mkdir2 } = await import('fs/promises');
2574
+ const { dirname: dirname2, resolve: resolve12 } = await import('path');
2575
+ for (const cfg of configs) {
2576
+ const fullPath = resolve12(process.cwd(), cfg.path);
2577
+ await mkdir2(dirname2(fullPath), { recursive: true });
2578
+ await writeFile2(fullPath, cfg.content, "utf-8");
2579
+ }
2580
+ }
2581
+
2582
+ // src/cli/bin.ts
2583
+ run(process.argv.slice(2)).catch((err) => {
2584
+ console.error(err.message);
2585
+ process.exit(1);
2586
+ });
2587
+ //# sourceMappingURL=bin.js.map
2588
+ //# sourceMappingURL=bin.js.map