@premierstudio/ai-hooks 1.0.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/index.d.ts +2 -0
- package/dist/adapters/index.js +122 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/hooks/index.d.ts +27 -0
- package/dist/hooks/index.js +155 -0
- package/dist/hooks/index.js.map +1 -0
- package/dist/hooks-CMnWrmJU.d.ts +212 -0
- package/dist/index-bRA_mZA5.d.ts +148 -0
- package/dist/index.d.ts +178 -0
- package/dist/index.js +530 -0
- package/dist/index.js.map +1 -0
- package/package.json +58 -0
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { existsSync } from 'fs';
|
|
2
|
+
import { mkdir, writeFile, readFile, rm } from 'fs/promises';
|
|
3
|
+
import { resolve, dirname } from 'path';
|
|
4
|
+
|
|
5
|
+
// src/adapters/registry.ts
|
|
6
|
+
var AdapterRegistry = class {
|
|
7
|
+
adapters = /* @__PURE__ */ new Map();
|
|
8
|
+
factories = /* @__PURE__ */ new Map();
|
|
9
|
+
/**
|
|
10
|
+
* Register an adapter instance.
|
|
11
|
+
*/
|
|
12
|
+
register(adapter) {
|
|
13
|
+
this.adapters.set(adapter.id, adapter);
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Register an adapter factory for lazy instantiation.
|
|
17
|
+
*/
|
|
18
|
+
registerFactory(id, factory) {
|
|
19
|
+
this.factories.set(id, factory);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Get a registered adapter by ID.
|
|
23
|
+
*/
|
|
24
|
+
get(id) {
|
|
25
|
+
const existing = this.adapters.get(id);
|
|
26
|
+
if (existing) return existing;
|
|
27
|
+
const factory = this.factories.get(id);
|
|
28
|
+
if (factory) {
|
|
29
|
+
const adapter = factory();
|
|
30
|
+
this.adapters.set(id, adapter);
|
|
31
|
+
return adapter;
|
|
32
|
+
}
|
|
33
|
+
return void 0;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Get all registered adapter IDs.
|
|
37
|
+
*/
|
|
38
|
+
list() {
|
|
39
|
+
return [.../* @__PURE__ */ new Set([...this.adapters.keys(), ...this.factories.keys()])];
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Detect which tools are available in the current environment.
|
|
43
|
+
* Returns adapters that successfully detect their tool.
|
|
44
|
+
*/
|
|
45
|
+
async detectAll() {
|
|
46
|
+
const detected = [];
|
|
47
|
+
for (const id of this.list()) {
|
|
48
|
+
const adapter = this.get(id);
|
|
49
|
+
if (adapter) {
|
|
50
|
+
try {
|
|
51
|
+
const found = await adapter.detect();
|
|
52
|
+
if (found) {
|
|
53
|
+
detected.push(adapter);
|
|
54
|
+
}
|
|
55
|
+
} catch {
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return detected;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Clear the registry. Useful for testing.
|
|
63
|
+
*/
|
|
64
|
+
clear() {
|
|
65
|
+
this.adapters.clear();
|
|
66
|
+
this.factories.clear();
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
var registry = new AdapterRegistry();
|
|
70
|
+
var BaseAdapter = class {
|
|
71
|
+
/**
|
|
72
|
+
* Default install: write generated configs to disk.
|
|
73
|
+
*/
|
|
74
|
+
async install(configs) {
|
|
75
|
+
for (const config of configs) {
|
|
76
|
+
const fullPath = resolve(process.cwd(), config.path);
|
|
77
|
+
await mkdir(dirname(fullPath), { recursive: true });
|
|
78
|
+
await writeFile(fullPath, config.content, "utf-8");
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Default uninstall: remove generated config files.
|
|
83
|
+
*/
|
|
84
|
+
async uninstall() {
|
|
85
|
+
}
|
|
86
|
+
// ── Utility Methods ───────────────────────────────────────
|
|
87
|
+
async fileExists(path) {
|
|
88
|
+
return existsSync(resolve(process.cwd(), path));
|
|
89
|
+
}
|
|
90
|
+
async readJsonFile(path) {
|
|
91
|
+
const fullPath = resolve(process.cwd(), path);
|
|
92
|
+
if (!existsSync(fullPath)) return null;
|
|
93
|
+
const content = await readFile(fullPath, "utf-8");
|
|
94
|
+
return JSON.parse(content);
|
|
95
|
+
}
|
|
96
|
+
async writeJsonFile(path, data) {
|
|
97
|
+
const fullPath = resolve(process.cwd(), path);
|
|
98
|
+
await mkdir(dirname(fullPath), { recursive: true });
|
|
99
|
+
await writeFile(fullPath, JSON.stringify(data, null, 2) + "\n", "utf-8");
|
|
100
|
+
}
|
|
101
|
+
async removeFile(path) {
|
|
102
|
+
const fullPath = resolve(process.cwd(), path);
|
|
103
|
+
if (existsSync(fullPath)) {
|
|
104
|
+
await rm(fullPath);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Check if a CLI command exists on PATH.
|
|
109
|
+
*/
|
|
110
|
+
async commandExists(command) {
|
|
111
|
+
const { exec } = await import('child_process');
|
|
112
|
+
return new Promise((resolve2) => {
|
|
113
|
+
exec(`which ${command}`, (error) => {
|
|
114
|
+
resolve2(!error);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
export { BaseAdapter, registry };
|
|
121
|
+
//# sourceMappingURL=index.js.map
|
|
122
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/adapters/registry.ts","../../src/adapters/base.ts"],"names":["resolve"],"mappings":"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","file":"index.js","sourcesContent":["import type { Adapter, AdapterFactory } from \"../types/index.js\";\n\n/**\n * Global adapter registry.\n * Adapters register themselves when imported, or can be manually added.\n */\nclass AdapterRegistry {\n private adapters: Map<string, Adapter> = new Map();\n private factories: Map<string, AdapterFactory> = new Map();\n\n /**\n * Register an adapter instance.\n */\n register(adapter: Adapter): void {\n this.adapters.set(adapter.id, adapter);\n }\n\n /**\n * Register an adapter factory for lazy instantiation.\n */\n registerFactory(id: string, factory: AdapterFactory): void {\n this.factories.set(id, factory);\n }\n\n /**\n * Get a registered adapter by ID.\n */\n get(id: string): Adapter | undefined {\n const existing = this.adapters.get(id);\n if (existing) return existing;\n\n // Try factory\n const factory = this.factories.get(id);\n if (factory) {\n const adapter = factory();\n this.adapters.set(id, adapter);\n return adapter;\n }\n\n return undefined;\n }\n\n /**\n * Get all registered adapter IDs.\n */\n list(): string[] {\n return [...new Set([...this.adapters.keys(), ...this.factories.keys()])];\n }\n\n /**\n * Detect which tools are available in the current environment.\n * Returns adapters that successfully detect their tool.\n */\n async detectAll(): Promise<Adapter[]> {\n const detected: Adapter[] = [];\n\n for (const id of this.list()) {\n const adapter = this.get(id);\n if (adapter) {\n try {\n const found = await adapter.detect();\n if (found) {\n detected.push(adapter);\n }\n } catch {\n // Detection failed, skip this adapter\n }\n }\n }\n\n return detected;\n }\n\n /**\n * Clear the registry. Useful for testing.\n */\n clear(): void {\n this.adapters.clear();\n this.factories.clear();\n }\n}\n\nexport const registry = new AdapterRegistry();\n","import { existsSync } from \"node:fs\";\nimport { readFile, writeFile, mkdir, rm } from \"node:fs/promises\";\nimport { dirname, resolve } from \"node:path\";\nimport type {\n Adapter,\n AdapterCapabilities,\n GeneratedConfig,\n HookDefinition,\n HookEventType,\n} from \"../types/index.js\";\n\n/**\n * Base adapter class with shared utilities.\n * Tool-specific adapters extend this and implement the abstract methods.\n */\nexport abstract class BaseAdapter implements Adapter {\n abstract readonly id: string;\n abstract readonly name: string;\n abstract readonly version: string;\n abstract readonly capabilities: AdapterCapabilities;\n\n abstract detect(): Promise<boolean>;\n abstract generate(hooks: HookDefinition[]): Promise<GeneratedConfig[]>;\n abstract mapEvent(event: HookEventType): string[];\n abstract mapNativeEvent(nativeEvent: string): HookEventType[];\n\n /**\n * Default install: write generated configs to disk.\n */\n async install(configs: GeneratedConfig[]): Promise<void> {\n for (const config of configs) {\n const fullPath = resolve(process.cwd(), config.path);\n await mkdir(dirname(fullPath), { recursive: true });\n await writeFile(fullPath, config.content, \"utf-8\");\n }\n }\n\n /**\n * Default uninstall: remove generated config files.\n */\n async uninstall(): Promise<void> {\n // Subclasses should override with specific file paths\n }\n\n // ── Utility Methods ───────────────────────────────────────\n\n protected async fileExists(path: string): Promise<boolean> {\n return existsSync(resolve(process.cwd(), path));\n }\n\n protected async readJsonFile<T>(path: string): Promise<T | null> {\n const fullPath = resolve(process.cwd(), path);\n if (!existsSync(fullPath)) return null;\n const content = await readFile(fullPath, \"utf-8\");\n return JSON.parse(content) as T;\n }\n\n protected async writeJsonFile(path: string, data: unknown): Promise<void> {\n const fullPath = resolve(process.cwd(), path);\n await mkdir(dirname(fullPath), { recursive: true });\n await writeFile(fullPath, JSON.stringify(data, null, 2) + \"\\n\", \"utf-8\");\n }\n\n protected async removeFile(path: string): Promise<void> {\n const fullPath = resolve(process.cwd(), path);\n if (existsSync(fullPath)) {\n await rm(fullPath);\n }\n }\n\n /**\n * Check if a CLI command exists on PATH.\n */\n protected async commandExists(command: string): Promise<boolean> {\n const { exec } = await import(\"node:child_process\");\n return new Promise((resolve) => {\n exec(`which ${command}`, (error) => {\n resolve(!error);\n });\n });\n }\n}\n"]}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { H as HookDefinition } from '../hooks-CMnWrmJU.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Built-in hook: Block dangerous shell commands.
|
|
5
|
+
* Prevents rm -rf /, drop database, and other destructive patterns.
|
|
6
|
+
*/
|
|
7
|
+
declare const blockDangerousCommands: HookDefinition;
|
|
8
|
+
/**
|
|
9
|
+
* Built-in hook: Scan for secrets in file writes.
|
|
10
|
+
* Detects API keys, tokens, and credentials being written to files.
|
|
11
|
+
*/
|
|
12
|
+
declare const scanSecrets: HookDefinition;
|
|
13
|
+
/**
|
|
14
|
+
* Built-in hook: Protect gitignored files from being read.
|
|
15
|
+
*/
|
|
16
|
+
declare const protectGitignored: HookDefinition;
|
|
17
|
+
/**
|
|
18
|
+
* Built-in hook: Log all shell commands (after phase).
|
|
19
|
+
*/
|
|
20
|
+
declare const auditShellCommands: HookDefinition;
|
|
21
|
+
/**
|
|
22
|
+
* All built-in hooks as an array.
|
|
23
|
+
* Use with `extends` in defineConfig to include defaults.
|
|
24
|
+
*/
|
|
25
|
+
declare const builtinHooks: HookDefinition[];
|
|
26
|
+
|
|
27
|
+
export { auditShellCommands, blockDangerousCommands, builtinHooks, protectGitignored, scanSecrets };
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
// src/config/define.ts
|
|
2
|
+
function hook(phase, events, handler) {
|
|
3
|
+
return new HookBuilderChain(phase, events, handler);
|
|
4
|
+
}
|
|
5
|
+
var HookBuilderChain = class {
|
|
6
|
+
constructor(phase, events, handler) {
|
|
7
|
+
this.phase = phase;
|
|
8
|
+
this.events = events;
|
|
9
|
+
this.handler = handler;
|
|
10
|
+
this._id = `hook-${events.join("-")}-${Date.now()}`;
|
|
11
|
+
this._name = `Hook for ${events.join(", ")}`;
|
|
12
|
+
}
|
|
13
|
+
_id;
|
|
14
|
+
_name;
|
|
15
|
+
_description;
|
|
16
|
+
_priority;
|
|
17
|
+
_filter;
|
|
18
|
+
_enabled;
|
|
19
|
+
id(id) {
|
|
20
|
+
this._id = id;
|
|
21
|
+
return this;
|
|
22
|
+
}
|
|
23
|
+
name(name) {
|
|
24
|
+
this._name = name;
|
|
25
|
+
return this;
|
|
26
|
+
}
|
|
27
|
+
description(desc) {
|
|
28
|
+
this._description = desc;
|
|
29
|
+
return this;
|
|
30
|
+
}
|
|
31
|
+
priority(p) {
|
|
32
|
+
this._priority = p;
|
|
33
|
+
return this;
|
|
34
|
+
}
|
|
35
|
+
filter(fn) {
|
|
36
|
+
this._filter = fn;
|
|
37
|
+
return this;
|
|
38
|
+
}
|
|
39
|
+
enabled(e) {
|
|
40
|
+
this._enabled = e;
|
|
41
|
+
return this;
|
|
42
|
+
}
|
|
43
|
+
build() {
|
|
44
|
+
return {
|
|
45
|
+
id: this._id,
|
|
46
|
+
name: this._name,
|
|
47
|
+
description: this._description,
|
|
48
|
+
events: this.events,
|
|
49
|
+
handler: this.handler,
|
|
50
|
+
phase: this.phase,
|
|
51
|
+
priority: this._priority,
|
|
52
|
+
filter: this._filter,
|
|
53
|
+
enabled: this._enabled
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// src/hooks/builtin.ts
|
|
59
|
+
var blockDangerousCommands = hook("before", ["shell:before"], async (ctx, next) => {
|
|
60
|
+
const command = ctx.event.command;
|
|
61
|
+
const dangerous = DANGEROUS_PATTERNS.find((p) => p.pattern.test(command));
|
|
62
|
+
if (dangerous) {
|
|
63
|
+
ctx.results.push({
|
|
64
|
+
blocked: true,
|
|
65
|
+
reason: `Blocked dangerous command: ${dangerous.description}`
|
|
66
|
+
});
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
await next();
|
|
70
|
+
}).id("ai-hooks:block-dangerous-commands").name("Block Dangerous Commands").description("Prevents destructive shell commands like rm -rf /, drop database, etc.").priority(1).build();
|
|
71
|
+
var DANGEROUS_PATTERNS = [
|
|
72
|
+
{ pattern: /rm\s+(-[a-zA-Z]*f[a-zA-Z]*\s+)?\/\s*$/, description: "rm -rf /" },
|
|
73
|
+
{ pattern: /rm\s+-[a-zA-Z]*f[a-zA-Z]*\s+~\/?\s*$/, description: "rm -rf ~" },
|
|
74
|
+
{ pattern: /mkfs\./, description: "filesystem format" },
|
|
75
|
+
{ pattern: /dd\s+.*of=\/dev\/[sh]d/, description: "disk overwrite" },
|
|
76
|
+
{ pattern: /:\(\)\s*\{\s*:\|:&\s*\}\s*;:/, description: "fork bomb" },
|
|
77
|
+
{ pattern: />\s*\/dev\/[sh]d/, description: "device overwrite" },
|
|
78
|
+
{ pattern: /chmod\s+(-R\s+)?777\s+\//, description: "chmod 777 /" },
|
|
79
|
+
{ pattern: /DROP\s+DATABASE/i, description: "DROP DATABASE" },
|
|
80
|
+
{ pattern: /DROP\s+TABLE/i, description: "DROP TABLE" },
|
|
81
|
+
{ pattern: /TRUNCATE\s+TABLE/i, description: "TRUNCATE TABLE" }
|
|
82
|
+
];
|
|
83
|
+
var scanSecrets = hook("before", ["file:write", "file:edit"], async (ctx, next) => {
|
|
84
|
+
const content = ctx.event.type === "file:write" ? ctx.event.content : ctx.event.newContent;
|
|
85
|
+
const found = SECRET_PATTERNS.find((p) => p.pattern.test(content));
|
|
86
|
+
if (found) {
|
|
87
|
+
ctx.results.push({
|
|
88
|
+
blocked: true,
|
|
89
|
+
reason: `Potential secret detected: ${found.description}. Use environment variables instead.`
|
|
90
|
+
});
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
await next();
|
|
94
|
+
}).id("ai-hooks:scan-secrets").name("Scan for Secrets").description("Prevents hardcoded API keys, tokens, and credentials in file writes.").priority(2).build();
|
|
95
|
+
var SECRET_PATTERNS = [
|
|
96
|
+
{ pattern: /(?:api[_-]?key|apikey)\s*[:=]\s*['"][a-zA-Z0-9]{20,}['"]/i, description: "API key" },
|
|
97
|
+
{
|
|
98
|
+
pattern: /(?:secret|token|password|passwd|pwd)\s*[:=]\s*['"][^'"]{8,}['"]/i,
|
|
99
|
+
description: "Secret/token/password"
|
|
100
|
+
},
|
|
101
|
+
{ pattern: /-----BEGIN (?:RSA |EC |DSA )?PRIVATE KEY-----/, description: "Private key" },
|
|
102
|
+
{ pattern: /ghp_[a-zA-Z0-9]{36}/, description: "GitHub personal access token" },
|
|
103
|
+
{ pattern: /sk-[a-zA-Z0-9]{20,}/, description: "OpenAI/Stripe secret key" },
|
|
104
|
+
{ pattern: /AKIA[0-9A-Z]{16}/, description: "AWS access key ID" },
|
|
105
|
+
{ pattern: /xox[bpors]-[a-zA-Z0-9-]{10,}/, description: "Slack token" }
|
|
106
|
+
];
|
|
107
|
+
var protectGitignored = hook("before", ["file:write"], async (ctx, next) => {
|
|
108
|
+
const path = ctx.event.path;
|
|
109
|
+
const sensitive = SENSITIVE_FILES.some((f) => path.endsWith(f));
|
|
110
|
+
if (sensitive) {
|
|
111
|
+
ctx.results.push({
|
|
112
|
+
blocked: true,
|
|
113
|
+
reason: `Cannot write to sensitive file: ${path}. This file should be managed manually.`
|
|
114
|
+
});
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
await next();
|
|
118
|
+
}).id("ai-hooks:protect-sensitive-files").name("Protect Sensitive Files").description("Prevents AI tools from overwriting .env, credentials, and other sensitive files.").priority(3).build();
|
|
119
|
+
var SENSITIVE_FILES = [
|
|
120
|
+
".env",
|
|
121
|
+
".env.local",
|
|
122
|
+
".env.production",
|
|
123
|
+
"credentials.json",
|
|
124
|
+
"service-account.json",
|
|
125
|
+
"id_rsa",
|
|
126
|
+
"id_ed25519",
|
|
127
|
+
".npmrc",
|
|
128
|
+
".pypirc"
|
|
129
|
+
];
|
|
130
|
+
var auditShellCommands = hook("after", ["shell:after"], async (ctx, next) => {
|
|
131
|
+
const { command, exitCode, duration } = ctx.event;
|
|
132
|
+
ctx.results.push({
|
|
133
|
+
data: {
|
|
134
|
+
audit: {
|
|
135
|
+
type: "shell",
|
|
136
|
+
command,
|
|
137
|
+
exitCode,
|
|
138
|
+
duration,
|
|
139
|
+
timestamp: ctx.event.timestamp,
|
|
140
|
+
tool: ctx.tool.name
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
await next();
|
|
145
|
+
}).id("ai-hooks:audit-shell").name("Audit Shell Commands").description("Records all shell command executions for audit trail.").priority(999).build();
|
|
146
|
+
var builtinHooks = [
|
|
147
|
+
blockDangerousCommands,
|
|
148
|
+
scanSecrets,
|
|
149
|
+
protectGitignored,
|
|
150
|
+
auditShellCommands
|
|
151
|
+
];
|
|
152
|
+
|
|
153
|
+
export { auditShellCommands, blockDangerousCommands, builtinHooks, protectGitignored, scanSecrets };
|
|
154
|
+
//# sourceMappingURL=index.js.map
|
|
155
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/config/define.ts","../../src/hooks/builtin.ts"],"names":[],"mappings":";AA6DO,SAAS,IAAA,CACd,KAAA,EACA,MAAA,EACA,OAAA,EACqB;AACrB,EAAA,OAAO,IAAI,gBAAA,CAAiB,KAAA,EAAO,MAAA,EAAQ,OAAO,CAAA;AACpD;AAEA,IAAM,mBAAN,MAAgD;AAAA,EAQ9C,WAAA,CACU,KAAA,EACA,MAAA,EACA,OAAA,EACR;AAHQ,IAAA,IAAA,CAAA,KAAA,GAAA,KAAA;AACA,IAAA,IAAA,CAAA,MAAA,GAAA,MAAA;AACA,IAAA,IAAA,CAAA,OAAA,GAAA,OAAA;AAER,IAAA,IAAA,CAAK,GAAA,GAAM,QAAQ,MAAA,CAAO,IAAA,CAAK,GAAG,CAAC,CAAA,CAAA,EAAI,IAAA,CAAK,GAAA,EAAK,CAAA,CAAA;AACjD,IAAA,IAAA,CAAK,KAAA,GAAQ,CAAA,SAAA,EAAY,MAAA,CAAO,IAAA,CAAK,IAAI,CAAC,CAAA,CAAA;AAAA,EAC5C;AAAA,EAdQ,GAAA;AAAA,EACA,KAAA;AAAA,EACA,YAAA;AAAA,EACA,SAAA;AAAA,EACA,OAAA;AAAA,EACA,QAAA;AAAA,EAWR,GAAG,EAAA,EAAkB;AACnB,IAAA,IAAA,CAAK,GAAA,GAAM,EAAA;AACX,IAAA,OAAO,IAAA;AAAA,EACT;AAAA,EAEA,KAAK,IAAA,EAAoB;AACvB,IAAA,IAAA,CAAK,KAAA,GAAQ,IAAA;AACb,IAAA,OAAO,IAAA;AAAA,EACT;AAAA,EAEA,YAAY,IAAA,EAAoB;AAC9B,IAAA,IAAA,CAAK,YAAA,GAAe,IAAA;AACpB,IAAA,OAAO,IAAA;AAAA,EACT;AAAA,EAEA,SAAS,CAAA,EAAiB;AACxB,IAAA,IAAA,CAAK,SAAA,GAAY,CAAA;AACjB,IAAA,OAAO,IAAA;AAAA,EACT;AAAA,EAEA,OAAO,EAAA,EAA0C;AAC/C,IAAA,IAAA,CAAK,OAAA,GAAU,EAAA;AACf,IAAA,OAAO,IAAA;AAAA,EACT;AAAA,EAEA,QAAQ,CAAA,EAAkB;AACxB,IAAA,IAAA,CAAK,QAAA,GAAW,CAAA;AAChB,IAAA,OAAO,IAAA;AAAA,EACT;AAAA,EAEA,KAAA,GAAwB;AAKtB,IAAA,OAAO;AAAA,MACL,IAAI,IAAA,CAAK,GAAA;AAAA,MACT,MAAM,IAAA,CAAK,KAAA;AAAA,MACX,aAAa,IAAA,CAAK,YAAA;AAAA,MAClB,QAAQ,IAAA,CAAK,MAAA;AAAA,MACb,SAAS,IAAA,CAAK,OAAA;AAAA,MACd,OAAO,IAAA,CAAK,KAAA;AAAA,MACZ,UAAU,IAAA,CAAK,SAAA;AAAA,MACf,QAAQ,IAAA,CAAK,OAAA;AAAA,MACb,SAAS,IAAA,CAAK;AAAA,KAChB;AAAA,EACF;AACF,CAAA;;;AC9HO,IAAM,sBAAA,GAAyB,KAAK,QAAA,EAAU,CAAC,cAAc,CAAA,EAAG,OAAO,KAAK,IAAA,KAAS;AAC1F,EAAA,MAAM,OAAA,GAAU,IAAI,KAAA,CAAM,OAAA;AAC1B,EAAA,MAAM,SAAA,GAAY,mBAAmB,IAAA,CAAK,CAAC,MAAM,CAAA,CAAE,OAAA,CAAQ,IAAA,CAAK,OAAO,CAAC,CAAA;AAExE,EAAA,IAAI,SAAA,EAAW;AACb,IAAA,GAAA,CAAI,QAAQ,IAAA,CAAK;AAAA,MACf,OAAA,EAAS,IAAA;AAAA,MACT,MAAA,EAAQ,CAAA,2BAAA,EAA8B,SAAA,CAAU,WAAW,CAAA;AAAA,KAC5D,CAAA;AACD,IAAA;AAAA,EACF;AAEA,EAAA,MAAM,IAAA,EAAK;AACb,CAAC,CAAA,CACE,EAAA,CAAG,mCAAmC,CAAA,CACtC,IAAA,CAAK,0BAA0B,CAAA,CAC/B,WAAA,CAAY,wEAAwE,CAAA,CACpF,QAAA,CAAS,CAAC,EACV,KAAA;AAEH,IAAM,kBAAA,GAAqB;AAAA,EACzB,EAAE,OAAA,EAAS,uCAAA,EAAyC,WAAA,EAAa,UAAA,EAAW;AAAA,EAC5E,EAAE,OAAA,EAAS,sCAAA,EAAwC,WAAA,EAAa,UAAA,EAAW;AAAA,EAC3E,EAAE,OAAA,EAAS,QAAA,EAAU,WAAA,EAAa,mBAAA,EAAoB;AAAA,EACtD,EAAE,OAAA,EAAS,wBAAA,EAA0B,WAAA,EAAa,gBAAA,EAAiB;AAAA,EACnE,EAAE,OAAA,EAAS,8BAAA,EAAgC,WAAA,EAAa,WAAA,EAAY;AAAA,EACpE,EAAE,OAAA,EAAS,kBAAA,EAAoB,WAAA,EAAa,kBAAA,EAAmB;AAAA,EAC/D,EAAE,OAAA,EAAS,0BAAA,EAA4B,WAAA,EAAa,aAAA,EAAc;AAAA,EAClE,EAAE,OAAA,EAAS,kBAAA,EAAoB,WAAA,EAAa,eAAA,EAAgB;AAAA,EAC5D,EAAE,OAAA,EAAS,eAAA,EAAiB,WAAA,EAAa,YAAA,EAAa;AAAA,EACtD,EAAE,OAAA,EAAS,mBAAA,EAAqB,WAAA,EAAa,gBAAA;AAC/C,CAAA;AAMO,IAAM,WAAA,GAAc,KAAK,QAAA,EAAU,CAAC,cAAc,WAAW,CAAA,EAAG,OAAO,GAAA,EAAK,IAAA,KAAS;AAC1F,EAAA,MAAM,OAAA,GAAU,IAAI,KAAA,CAAM,IAAA,KAAS,eAAe,GAAA,CAAI,KAAA,CAAM,OAAA,GAAU,GAAA,CAAI,KAAA,CAAM,UAAA;AAEhF,EAAA,MAAM,KAAA,GAAQ,gBAAgB,IAAA,CAAK,CAAC,MAAM,CAAA,CAAE,OAAA,CAAQ,IAAA,CAAK,OAAO,CAAC,CAAA;AAEjE,EAAA,IAAI,KAAA,EAAO;AACT,IAAA,GAAA,CAAI,QAAQ,IAAA,CAAK;AAAA,MACf,OAAA,EAAS,IAAA;AAAA,MACT,MAAA,EAAQ,CAAA,2BAAA,EAA8B,KAAA,CAAM,WAAW,CAAA,oCAAA;AAAA,KACxD,CAAA;AACD,IAAA;AAAA,EACF;AAEA,EAAA,MAAM,IAAA,EAAK;AACb,CAAC,CAAA,CACE,EAAA,CAAG,uBAAuB,CAAA,CAC1B,IAAA,CAAK,kBAAkB,CAAA,CACvB,WAAA,CAAY,sEAAsE,CAAA,CAClF,QAAA,CAAS,CAAC,EACV,KAAA;AAEH,IAAM,eAAA,GAAkB;AAAA,EACtB,EAAE,OAAA,EAAS,2DAAA,EAA6D,WAAA,EAAa,SAAA,EAAU;AAAA,EAC/F;AAAA,IACE,OAAA,EAAS,kEAAA;AAAA,IACT,WAAA,EAAa;AAAA,GACf;AAAA,EACA,EAAE,OAAA,EAAS,+CAAA,EAAiD,WAAA,EAAa,aAAA,EAAc;AAAA,EACvF,EAAE,OAAA,EAAS,qBAAA,EAAuB,WAAA,EAAa,8BAAA,EAA+B;AAAA,EAC9E,EAAE,OAAA,EAAS,qBAAA,EAAuB,WAAA,EAAa,0BAAA,EAA2B;AAAA,EAC1E,EAAE,OAAA,EAAS,kBAAA,EAAoB,WAAA,EAAa,mBAAA,EAAoB;AAAA,EAChE,EAAE,OAAA,EAAS,8BAAA,EAAgC,WAAA,EAAa,aAAA;AAC1D,CAAA;AAKO,IAAM,iBAAA,GAAoB,KAAK,QAAA,EAAU,CAAC,YAAY,CAAA,EAAG,OAAO,KAAK,IAAA,KAAS;AACnF,EAAA,MAAM,IAAA,GAAO,IAAI,KAAA,CAAM,IAAA;AAGvB,EAAA,MAAM,SAAA,GAAY,gBAAgB,IAAA,CAAK,CAAC,MAAM,IAAA,CAAK,QAAA,CAAS,CAAC,CAAC,CAAA;AAC9D,EAAA,IAAI,SAAA,EAAW;AACb,IAAA,GAAA,CAAI,QAAQ,IAAA,CAAK;AAAA,MACf,OAAA,EAAS,IAAA;AAAA,MACT,MAAA,EAAQ,mCAAmC,IAAI,CAAA,uCAAA;AAAA,KAChD,CAAA;AACD,IAAA;AAAA,EACF;AAEA,EAAA,MAAM,IAAA,EAAK;AACb,CAAC,CAAA,CACE,EAAA,CAAG,kCAAkC,CAAA,CACrC,IAAA,CAAK,yBAAyB,CAAA,CAC9B,WAAA,CAAY,kFAAkF,CAAA,CAC9F,QAAA,CAAS,CAAC,EACV,KAAA;AAEH,IAAM,eAAA,GAAkB;AAAA,EACtB,MAAA;AAAA,EACA,YAAA;AAAA,EACA,iBAAA;AAAA,EACA,kBAAA;AAAA,EACA,sBAAA;AAAA,EACA,QAAA;AAAA,EACA,YAAA;AAAA,EACA,QAAA;AAAA,EACA;AACF,CAAA;AAKO,IAAM,kBAAA,GAAqB,KAAK,OAAA,EAAS,CAAC,aAAa,CAAA,EAAG,OAAO,KAAK,IAAA,KAAS;AACpF,EAAA,MAAM,EAAE,OAAA,EAAS,QAAA,EAAU,QAAA,KAAa,GAAA,CAAI,KAAA;AAC5C,EAAA,GAAA,CAAI,QAAQ,IAAA,CAAK;AAAA,IACf,IAAA,EAAM;AAAA,MACJ,KAAA,EAAO;AAAA,QACL,IAAA,EAAM,OAAA;AAAA,QACN,OAAA;AAAA,QACA,QAAA;AAAA,QACA,QAAA;AAAA,QACA,SAAA,EAAW,IAAI,KAAA,CAAM,SAAA;AAAA,QACrB,IAAA,EAAM,IAAI,IAAA,CAAK;AAAA;AACjB;AACF,GACD,CAAA;AACD,EAAA,MAAM,IAAA,EAAK;AACb,CAAC,CAAA,CACE,EAAA,CAAG,sBAAsB,CAAA,CACzB,IAAA,CAAK,sBAAsB,CAAA,CAC3B,WAAA,CAAY,uDAAuD,CAAA,CACnE,QAAA,CAAS,GAAG,EACZ,KAAA;AAMI,IAAM,YAAA,GAAiC;AAAA,EAC5C,sBAAA;AAAA,EACA,WAAA;AAAA,EACA,iBAAA;AAAA,EACA;AACF","file":"index.js","sourcesContent":["import type {\n AiHooksConfig,\n HookDefinition,\n HookEventType,\n HookContext,\n EventOf,\n} from \"../types/index.js\";\n\n/**\n * Define an ai-hooks configuration.\n * Use this as the default export of your `ai-hooks.config.ts`.\n *\n * @example\n * ```ts\n * import { defineConfig, hook } from \"@premierstudio/ai-hooks\";\n *\n * export default defineConfig({\n * hooks: [\n * hook(\"before\", [\"shell:before\"], async (ctx, next) => {\n * if (ctx.event.command.includes(\"rm -rf /\")) {\n * ctx.results.push({ blocked: true, reason: \"Dangerous command\" });\n * return;\n * }\n * await next();\n * }).id(\"block-dangerous\").name(\"Block Dangerous Commands\").build(),\n * ],\n * });\n * ```\n */\nexport function defineConfig(config: AiHooksConfig): AiHooksConfig {\n return config;\n}\n\n/**\n * Fluent builder for creating hook definitions.\n * The generic parameter provides type-safe access to event properties\n * inside the handler, while the output is a non-generic HookDefinition\n * for collection compatibility.\n *\n * @example\n * ```ts\n * hook(\"before\", [\"file:write\", \"file:edit\"], async (ctx, next) => {\n * // validate file changes\n * await next();\n * })\n * .id(\"validate-writes\")\n * .name(\"Validate File Writes\")\n * .priority(10)\n * .build()\n * ```\n */\nexport function hook<T extends HookEventType>(\n phase: \"before\",\n events: T[],\n handler: (ctx: HookContext<T>, next: () => Promise<void>) => Promise<void> | void,\n): HookBuilderChain<T>;\nexport function hook<T extends HookEventType>(\n phase: \"after\",\n events: T[],\n handler: (ctx: HookContext<T>, next: () => Promise<void>) => Promise<void> | void,\n): HookBuilderChain<T>;\nexport function hook<T extends HookEventType>(\n phase: \"before\" | \"after\",\n events: T[],\n handler: (ctx: HookContext<T>, next: () => Promise<void>) => Promise<void> | void,\n): HookBuilderChain<T> {\n return new HookBuilderChain(phase, events, handler);\n}\n\nclass HookBuilderChain<T extends HookEventType> {\n private _id: string;\n private _name: string;\n private _description?: string;\n private _priority?: number;\n private _filter?: (event: EventOf<T>) => boolean;\n private _enabled?: boolean;\n\n constructor(\n private phase: \"before\" | \"after\",\n private events: T[],\n private handler: (ctx: HookContext<T>, next: () => Promise<void>) => Promise<void> | void,\n ) {\n this._id = `hook-${events.join(\"-\")}-${Date.now()}`;\n this._name = `Hook for ${events.join(\", \")}`;\n }\n\n id(id: string): this {\n this._id = id;\n return this;\n }\n\n name(name: string): this {\n this._name = name;\n return this;\n }\n\n description(desc: string): this {\n this._description = desc;\n return this;\n }\n\n priority(p: number): this {\n this._priority = p;\n return this;\n }\n\n filter(fn: (event: EventOf<T>) => boolean): this {\n this._filter = fn;\n return this;\n }\n\n enabled(e: boolean): this {\n this._enabled = e;\n return this;\n }\n\n build(): HookDefinition {\n // The handler/filter are widened from their narrow generic types to\n // the base HookContext/HookEvent types. This is runtime-safe because\n // the handler only accesses properties of the specific event type\n // it was designed for, which are always present on the actual event.\n return {\n id: this._id,\n name: this._name,\n description: this._description,\n events: this.events,\n handler: this.handler as unknown as HookDefinition[\"handler\"],\n phase: this.phase,\n priority: this._priority,\n filter: this._filter as unknown as HookDefinition[\"filter\"],\n enabled: this._enabled,\n };\n }\n}\n","import { hook } from \"../config/define.js\";\nimport type { HookDefinition } from \"../types/index.js\";\n\n/**\n * Built-in hook: Block dangerous shell commands.\n * Prevents rm -rf /, drop database, and other destructive patterns.\n */\nexport const blockDangerousCommands = hook(\"before\", [\"shell:before\"], async (ctx, next) => {\n const command = ctx.event.command;\n const dangerous = DANGEROUS_PATTERNS.find((p) => p.pattern.test(command));\n\n if (dangerous) {\n ctx.results.push({\n blocked: true,\n reason: `Blocked dangerous command: ${dangerous.description}`,\n });\n return;\n }\n\n await next();\n})\n .id(\"ai-hooks:block-dangerous-commands\")\n .name(\"Block Dangerous Commands\")\n .description(\"Prevents destructive shell commands like rm -rf /, drop database, etc.\")\n .priority(1)\n .build();\n\nconst DANGEROUS_PATTERNS = [\n { pattern: /rm\\s+(-[a-zA-Z]*f[a-zA-Z]*\\s+)?\\/\\s*$/, description: \"rm -rf /\" },\n { pattern: /rm\\s+-[a-zA-Z]*f[a-zA-Z]*\\s+~\\/?\\s*$/, description: \"rm -rf ~\" },\n { pattern: /mkfs\\./, description: \"filesystem format\" },\n { pattern: /dd\\s+.*of=\\/dev\\/[sh]d/, description: \"disk overwrite\" },\n { pattern: /:\\(\\)\\s*\\{\\s*:\\|:&\\s*\\}\\s*;:/, description: \"fork bomb\" },\n { pattern: />\\s*\\/dev\\/[sh]d/, description: \"device overwrite\" },\n { pattern: /chmod\\s+(-R\\s+)?777\\s+\\//, description: \"chmod 777 /\" },\n { pattern: /DROP\\s+DATABASE/i, description: \"DROP DATABASE\" },\n { pattern: /DROP\\s+TABLE/i, description: \"DROP TABLE\" },\n { pattern: /TRUNCATE\\s+TABLE/i, description: \"TRUNCATE TABLE\" },\n];\n\n/**\n * Built-in hook: Scan for secrets in file writes.\n * Detects API keys, tokens, and credentials being written to files.\n */\nexport const scanSecrets = hook(\"before\", [\"file:write\", \"file:edit\"], async (ctx, next) => {\n const content = ctx.event.type === \"file:write\" ? ctx.event.content : ctx.event.newContent;\n\n const found = SECRET_PATTERNS.find((p) => p.pattern.test(content));\n\n if (found) {\n ctx.results.push({\n blocked: true,\n reason: `Potential secret detected: ${found.description}. Use environment variables instead.`,\n });\n return;\n }\n\n await next();\n})\n .id(\"ai-hooks:scan-secrets\")\n .name(\"Scan for Secrets\")\n .description(\"Prevents hardcoded API keys, tokens, and credentials in file writes.\")\n .priority(2)\n .build();\n\nconst SECRET_PATTERNS = [\n { pattern: /(?:api[_-]?key|apikey)\\s*[:=]\\s*['\"][a-zA-Z0-9]{20,}['\"]/i, description: \"API key\" },\n {\n pattern: /(?:secret|token|password|passwd|pwd)\\s*[:=]\\s*['\"][^'\"]{8,}['\"]/i,\n description: \"Secret/token/password\",\n },\n { pattern: /-----BEGIN (?:RSA |EC |DSA )?PRIVATE KEY-----/, description: \"Private key\" },\n { pattern: /ghp_[a-zA-Z0-9]{36}/, description: \"GitHub personal access token\" },\n { pattern: /sk-[a-zA-Z0-9]{20,}/, description: \"OpenAI/Stripe secret key\" },\n { pattern: /AKIA[0-9A-Z]{16}/, description: \"AWS access key ID\" },\n { pattern: /xox[bpors]-[a-zA-Z0-9-]{10,}/, description: \"Slack token\" },\n];\n\n/**\n * Built-in hook: Protect gitignored files from being read.\n */\nexport const protectGitignored = hook(\"before\", [\"file:write\"], async (ctx, next) => {\n const path = ctx.event.path;\n\n // Block writes to common sensitive files\n const sensitive = SENSITIVE_FILES.some((f) => path.endsWith(f));\n if (sensitive) {\n ctx.results.push({\n blocked: true,\n reason: `Cannot write to sensitive file: ${path}. This file should be managed manually.`,\n });\n return;\n }\n\n await next();\n})\n .id(\"ai-hooks:protect-sensitive-files\")\n .name(\"Protect Sensitive Files\")\n .description(\"Prevents AI tools from overwriting .env, credentials, and other sensitive files.\")\n .priority(3)\n .build();\n\nconst SENSITIVE_FILES = [\n \".env\",\n \".env.local\",\n \".env.production\",\n \"credentials.json\",\n \"service-account.json\",\n \"id_rsa\",\n \"id_ed25519\",\n \".npmrc\",\n \".pypirc\",\n];\n\n/**\n * Built-in hook: Log all shell commands (after phase).\n */\nexport const auditShellCommands = hook(\"after\", [\"shell:after\"], async (ctx, next) => {\n const { command, exitCode, duration } = ctx.event;\n ctx.results.push({\n data: {\n audit: {\n type: \"shell\",\n command,\n exitCode,\n duration,\n timestamp: ctx.event.timestamp,\n tool: ctx.tool.name,\n },\n },\n });\n await next();\n})\n .id(\"ai-hooks:audit-shell\")\n .name(\"Audit Shell Commands\")\n .description(\"Records all shell command executions for audit trail.\")\n .priority(999)\n .build();\n\n/**\n * All built-in hooks as an array.\n * Use with `extends` in defineConfig to include defaults.\n */\nexport const builtinHooks: HookDefinition[] = [\n blockDangerousCommands,\n scanSecrets,\n protectGitignored,\n auditShellCommands,\n];\n"]}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Universal event types that map across all AI coding tools.
|
|
3
|
+
*
|
|
4
|
+
* Each tool adapter translates these universal events to/from
|
|
5
|
+
* the tool's native event system.
|
|
6
|
+
*/
|
|
7
|
+
type SessionStartEvent = {
|
|
8
|
+
type: "session:start";
|
|
9
|
+
tool: string;
|
|
10
|
+
version: string;
|
|
11
|
+
workingDirectory: string;
|
|
12
|
+
timestamp: number;
|
|
13
|
+
metadata: Record<string, unknown>;
|
|
14
|
+
};
|
|
15
|
+
type SessionEndEvent = {
|
|
16
|
+
type: "session:end";
|
|
17
|
+
tool: string;
|
|
18
|
+
duration: number;
|
|
19
|
+
timestamp: number;
|
|
20
|
+
metadata: Record<string, unknown>;
|
|
21
|
+
};
|
|
22
|
+
type PromptSubmitEvent = {
|
|
23
|
+
type: "prompt:submit";
|
|
24
|
+
prompt: string;
|
|
25
|
+
timestamp: number;
|
|
26
|
+
metadata: Record<string, unknown>;
|
|
27
|
+
};
|
|
28
|
+
type PromptResponseEvent = {
|
|
29
|
+
type: "prompt:response";
|
|
30
|
+
response: string;
|
|
31
|
+
model: string;
|
|
32
|
+
tokens: {
|
|
33
|
+
input: number;
|
|
34
|
+
output: number;
|
|
35
|
+
};
|
|
36
|
+
timestamp: number;
|
|
37
|
+
metadata: Record<string, unknown>;
|
|
38
|
+
};
|
|
39
|
+
type ToolCallEvent = {
|
|
40
|
+
type: "tool:before";
|
|
41
|
+
toolName: string;
|
|
42
|
+
input: Record<string, unknown>;
|
|
43
|
+
timestamp: number;
|
|
44
|
+
metadata: Record<string, unknown>;
|
|
45
|
+
};
|
|
46
|
+
type ToolResultEvent = {
|
|
47
|
+
type: "tool:after";
|
|
48
|
+
toolName: string;
|
|
49
|
+
input: Record<string, unknown>;
|
|
50
|
+
output: unknown;
|
|
51
|
+
duration: number;
|
|
52
|
+
timestamp: number;
|
|
53
|
+
metadata: Record<string, unknown>;
|
|
54
|
+
};
|
|
55
|
+
type FileReadEvent = {
|
|
56
|
+
type: "file:read";
|
|
57
|
+
path: string;
|
|
58
|
+
timestamp: number;
|
|
59
|
+
metadata: Record<string, unknown>;
|
|
60
|
+
};
|
|
61
|
+
type FileWriteEvent = {
|
|
62
|
+
type: "file:write";
|
|
63
|
+
path: string;
|
|
64
|
+
content: string;
|
|
65
|
+
timestamp: number;
|
|
66
|
+
metadata: Record<string, unknown>;
|
|
67
|
+
};
|
|
68
|
+
type FileEditEvent = {
|
|
69
|
+
type: "file:edit";
|
|
70
|
+
path: string;
|
|
71
|
+
oldContent: string;
|
|
72
|
+
newContent: string;
|
|
73
|
+
timestamp: number;
|
|
74
|
+
metadata: Record<string, unknown>;
|
|
75
|
+
};
|
|
76
|
+
type FileDeleteEvent = {
|
|
77
|
+
type: "file:delete";
|
|
78
|
+
path: string;
|
|
79
|
+
timestamp: number;
|
|
80
|
+
metadata: Record<string, unknown>;
|
|
81
|
+
};
|
|
82
|
+
type ShellBeforeEvent = {
|
|
83
|
+
type: "shell:before";
|
|
84
|
+
command: string;
|
|
85
|
+
cwd: string;
|
|
86
|
+
timestamp: number;
|
|
87
|
+
metadata: Record<string, unknown>;
|
|
88
|
+
};
|
|
89
|
+
type ShellAfterEvent = {
|
|
90
|
+
type: "shell:after";
|
|
91
|
+
command: string;
|
|
92
|
+
cwd: string;
|
|
93
|
+
exitCode: number;
|
|
94
|
+
stdout: string;
|
|
95
|
+
stderr: string;
|
|
96
|
+
duration: number;
|
|
97
|
+
timestamp: number;
|
|
98
|
+
metadata: Record<string, unknown>;
|
|
99
|
+
};
|
|
100
|
+
type McpCallEvent = {
|
|
101
|
+
type: "mcp:before";
|
|
102
|
+
server: string;
|
|
103
|
+
method: string;
|
|
104
|
+
params: Record<string, unknown>;
|
|
105
|
+
timestamp: number;
|
|
106
|
+
metadata: Record<string, unknown>;
|
|
107
|
+
};
|
|
108
|
+
type McpResultEvent = {
|
|
109
|
+
type: "mcp:after";
|
|
110
|
+
server: string;
|
|
111
|
+
method: string;
|
|
112
|
+
params: Record<string, unknown>;
|
|
113
|
+
result: unknown;
|
|
114
|
+
duration: number;
|
|
115
|
+
timestamp: number;
|
|
116
|
+
metadata: Record<string, unknown>;
|
|
117
|
+
};
|
|
118
|
+
type NotificationEvent = {
|
|
119
|
+
type: "notification";
|
|
120
|
+
level: "info" | "warn" | "error";
|
|
121
|
+
message: string;
|
|
122
|
+
timestamp: number;
|
|
123
|
+
metadata: Record<string, unknown>;
|
|
124
|
+
};
|
|
125
|
+
type BeforeEvent = SessionStartEvent | PromptSubmitEvent | ToolCallEvent | FileWriteEvent | FileEditEvent | FileDeleteEvent | ShellBeforeEvent | McpCallEvent;
|
|
126
|
+
type AfterEvent = SessionEndEvent | PromptResponseEvent | ToolResultEvent | FileReadEvent | ShellAfterEvent | McpResultEvent | NotificationEvent;
|
|
127
|
+
type HookEvent = BeforeEvent | AfterEvent;
|
|
128
|
+
type HookEventType = HookEvent["type"];
|
|
129
|
+
/**
|
|
130
|
+
* Extract the event shape for a given event type string.
|
|
131
|
+
*/
|
|
132
|
+
type EventOf<T extends HookEventType> = Extract<HookEvent, {
|
|
133
|
+
type: T;
|
|
134
|
+
}>;
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Result of a "before" hook. Controls whether the event proceeds.
|
|
138
|
+
*/
|
|
139
|
+
type HookResult = {
|
|
140
|
+
/** If true, the event is blocked and won't proceed to the tool. */
|
|
141
|
+
blocked?: boolean;
|
|
142
|
+
/** Reason for blocking (shown to the user/tool). */
|
|
143
|
+
reason?: string;
|
|
144
|
+
/** Modified event data to pass forward (mutation). */
|
|
145
|
+
mutated?: Partial<HookEvent>;
|
|
146
|
+
/** Arbitrary data to attach to the hook context. */
|
|
147
|
+
data?: Record<string, unknown>;
|
|
148
|
+
};
|
|
149
|
+
/**
|
|
150
|
+
* The context object passed to every hook function.
|
|
151
|
+
* Inspired by Express.js req/res pattern but adapted for AI tools.
|
|
152
|
+
*/
|
|
153
|
+
type HookContext<T extends HookEventType = HookEventType> = {
|
|
154
|
+
/** The event that triggered this hook. */
|
|
155
|
+
event: EventOf<T>;
|
|
156
|
+
/** The AI tool that emitted this event. */
|
|
157
|
+
tool: {
|
|
158
|
+
name: string;
|
|
159
|
+
version: string;
|
|
160
|
+
};
|
|
161
|
+
/** Working directory where the tool is running. */
|
|
162
|
+
cwd: string;
|
|
163
|
+
/** Shared state bag for passing data between hooks in a chain. */
|
|
164
|
+
state: Map<string, unknown>;
|
|
165
|
+
/** Accumulated results from previous hooks in the chain. */
|
|
166
|
+
results: HookResult[];
|
|
167
|
+
/** Timestamp of when the hook chain started. */
|
|
168
|
+
startedAt: number;
|
|
169
|
+
};
|
|
170
|
+
/**
|
|
171
|
+
* A "before" hook function. Runs before the event is processed.
|
|
172
|
+
* Can block, mutate, or pass through.
|
|
173
|
+
*/
|
|
174
|
+
type BeforeHookFn<T extends HookEventType = HookEventType> = (ctx: HookContext<T>, next: () => Promise<void>) => Promise<void> | void;
|
|
175
|
+
/**
|
|
176
|
+
* An "after" hook function. Runs after the event is processed.
|
|
177
|
+
* Cannot block (event already happened), but can observe and react.
|
|
178
|
+
*/
|
|
179
|
+
type AfterHookFn<T extends HookEventType = HookEventType> = (ctx: HookContext<T>, next: () => Promise<void>) => Promise<void> | void;
|
|
180
|
+
/**
|
|
181
|
+
* A hook definition with metadata.
|
|
182
|
+
*
|
|
183
|
+
* The generic parameter provides type safety when creating hooks
|
|
184
|
+
* for specific events. For collections/storage, use the non-generic
|
|
185
|
+
* default which accepts any event type.
|
|
186
|
+
*/
|
|
187
|
+
type HookDefinition = {
|
|
188
|
+
/** Unique identifier for this hook. */
|
|
189
|
+
id: string;
|
|
190
|
+
/** Human-readable name. */
|
|
191
|
+
name: string;
|
|
192
|
+
/** Description of what this hook does. */
|
|
193
|
+
description?: string;
|
|
194
|
+
/** Which event types this hook listens to. */
|
|
195
|
+
events: HookEventType[];
|
|
196
|
+
/** The hook function. Takes the widest context; narrowing happens at creation time via hook(). */
|
|
197
|
+
handler: (ctx: HookContext, next: () => Promise<void>) => Promise<void> | void;
|
|
198
|
+
/** Priority (lower = runs first). Default: 100. */
|
|
199
|
+
priority?: number;
|
|
200
|
+
/** Whether this hook runs "before" or "after" the event. */
|
|
201
|
+
phase: "before" | "after";
|
|
202
|
+
/** Optional filter to narrow when this hook runs. */
|
|
203
|
+
filter?: (event: HookEvent) => boolean;
|
|
204
|
+
/** Whether this hook is enabled. Default: true. */
|
|
205
|
+
enabled?: boolean;
|
|
206
|
+
};
|
|
207
|
+
/**
|
|
208
|
+
* Type guard: is this a "before" event type?
|
|
209
|
+
*/
|
|
210
|
+
declare function isBeforeEvent(event: HookEvent): event is BeforeEvent;
|
|
211
|
+
|
|
212
|
+
export { type AfterEvent as A, type BeforeEvent as B, type EventOf as E, type FileDeleteEvent as F, type HookDefinition as H, type McpCallEvent as M, type NotificationEvent as N, type PromptResponseEvent as P, type SessionEndEvent as S, type ToolCallEvent as T, type HookEventType as a, type HookContext as b, type HookEvent as c, type HookResult as d, type AfterHookFn as e, type BeforeHookFn as f, type FileEditEvent as g, type FileReadEvent as h, type FileWriteEvent as i, type McpResultEvent as j, type PromptSubmitEvent as k, type SessionStartEvent as l, type ShellAfterEvent as m, type ShellBeforeEvent as n, type ToolResultEvent as o, isBeforeEvent as p };
|