@agiflowai/hooks-adapter 0.0.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.
- package/LICENSE +661 -0
- package/README.md +123 -0
- package/dist/index.cjs +513 -0
- package/dist/index.d.cts +319 -0
- package/dist/index.d.mts +319 -0
- package/dist/index.mjs +480 -0
- package/package.json +50 -0
package/README.md
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# @agiflowai/hooks-adapter
|
|
2
|
+
|
|
3
|
+
Hook adapters for normalizing AI agent hook formats (Claude Code, Gemini, etc.). This library provides a unified interface for handling hooks from different AI coding agents.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm add @agiflowai/hooks-adapter
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
### Basic Hook Execution
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
import { AdapterProxyService, PRE_TOOL_USE, POST_TOOL_USE } from '@agiflowai/hooks-adapter';
|
|
17
|
+
import { CLAUDE_CODE } from '@agiflowai/coding-agent-bridge';
|
|
18
|
+
|
|
19
|
+
// Define your hook callback
|
|
20
|
+
const myPreToolUseHook = async (context) => {
|
|
21
|
+
console.log('Tool:', context.toolName);
|
|
22
|
+
console.log('File:', context.filePath);
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
decision: 'allow',
|
|
26
|
+
message: 'Proceeding with operation',
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// Execute the hook
|
|
31
|
+
await AdapterProxyService.execute(CLAUDE_CODE, PRE_TOOL_USE, myPreToolUseHook);
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### Hook Types
|
|
35
|
+
|
|
36
|
+
The library exports two hook type constants:
|
|
37
|
+
|
|
38
|
+
- `PRE_TOOL_USE` - Hook that runs before a tool is executed
|
|
39
|
+
- `POST_TOOL_USE` - Hook that runs after a tool is executed
|
|
40
|
+
|
|
41
|
+
### Hook Context
|
|
42
|
+
|
|
43
|
+
The normalized `HookContext` passed to callbacks includes:
|
|
44
|
+
|
|
45
|
+
```typescript
|
|
46
|
+
interface HookContext {
|
|
47
|
+
toolName: string; // Name of the tool (e.g., "Read", "Write", "Edit")
|
|
48
|
+
toolInput: Record<string, any>; // Input parameters
|
|
49
|
+
filePath?: string; // File path for file operations
|
|
50
|
+
operation?: 'read' | 'write' | 'edit'; // Operation type
|
|
51
|
+
cwd: string; // Current working directory
|
|
52
|
+
sessionId: string; // Unique session identifier
|
|
53
|
+
llmTool?: string; // Optional LLM tool identifier
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Hook Response
|
|
58
|
+
|
|
59
|
+
Callbacks should return a `HookResponse`:
|
|
60
|
+
|
|
61
|
+
```typescript
|
|
62
|
+
interface HookResponse {
|
|
63
|
+
decision: 'allow' | 'deny' | 'ask' | 'skip'; // Permission decision
|
|
64
|
+
message: string; // Message for the LLM
|
|
65
|
+
userMessage?: string; // Optional message for the user only
|
|
66
|
+
updatedInput?: Record<string, any>; // Optional updated input parameters
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Decision Types
|
|
71
|
+
|
|
72
|
+
- `allow` - Allow the operation to proceed
|
|
73
|
+
- `deny` - Block the operation
|
|
74
|
+
- `ask` - Ask the user for confirmation
|
|
75
|
+
- `skip` - Skip the hook silently (no output)
|
|
76
|
+
|
|
77
|
+
## Adapters
|
|
78
|
+
|
|
79
|
+
### ClaudeCodeAdapter
|
|
80
|
+
|
|
81
|
+
Adapter for Claude Code PreToolUse hooks. Parses Claude Code's JSON stdin format and formats responses accordingly.
|
|
82
|
+
|
|
83
|
+
### ClaudeCodePostToolUseAdapter
|
|
84
|
+
|
|
85
|
+
Adapter for Claude Code PostToolUse hooks. Handles post-execution hook format with tool response data.
|
|
86
|
+
|
|
87
|
+
### BaseAdapter
|
|
88
|
+
|
|
89
|
+
Abstract base class for creating custom adapters. Implements the Template Method pattern for hook execution flow.
|
|
90
|
+
|
|
91
|
+
## Services
|
|
92
|
+
|
|
93
|
+
### AdapterProxyService
|
|
94
|
+
|
|
95
|
+
Routes hook execution to the appropriate adapter based on agent and hook type.
|
|
96
|
+
|
|
97
|
+
```typescript
|
|
98
|
+
AdapterProxyService.execute(agentName, hookType, callback);
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### ExecutionLogService
|
|
102
|
+
|
|
103
|
+
Tracks hook executions to prevent duplicate actions within a session.
|
|
104
|
+
|
|
105
|
+
```typescript
|
|
106
|
+
// Check if already executed
|
|
107
|
+
const executed = await ExecutionLogService.hasExecuted(sessionId, filePath, decision);
|
|
108
|
+
|
|
109
|
+
// Log an execution
|
|
110
|
+
await ExecutionLogService.logExecution({
|
|
111
|
+
sessionId,
|
|
112
|
+
filePath,
|
|
113
|
+
operation: 'read',
|
|
114
|
+
decision: 'allow',
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// Check if file has changed since last execution
|
|
118
|
+
const changed = await ExecutionLogService.hasFileChanged(sessionId, filePath, decision);
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## License
|
|
122
|
+
|
|
123
|
+
AGPL-3.0
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,513 @@
|
|
|
1
|
+
//#region rolldown:runtime
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __copyProps = (to, from, except, desc) => {
|
|
9
|
+
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
10
|
+
key = keys[i];
|
|
11
|
+
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
12
|
+
get: ((k) => from[k]).bind(null, key),
|
|
13
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
|
|
19
|
+
value: mod,
|
|
20
|
+
enumerable: true
|
|
21
|
+
}) : target, mod));
|
|
22
|
+
|
|
23
|
+
//#endregion
|
|
24
|
+
let node_fs_promises = require("node:fs/promises");
|
|
25
|
+
node_fs_promises = __toESM(node_fs_promises);
|
|
26
|
+
let node_path = require("node:path");
|
|
27
|
+
node_path = __toESM(node_path);
|
|
28
|
+
let node_os = require("node:os");
|
|
29
|
+
node_os = __toESM(node_os);
|
|
30
|
+
let node_crypto = require("node:crypto");
|
|
31
|
+
node_crypto = __toESM(node_crypto);
|
|
32
|
+
let __agiflowai_coding_agent_bridge = require("@agiflowai/coding-agent-bridge");
|
|
33
|
+
|
|
34
|
+
//#region src/constants/index.ts
|
|
35
|
+
/**
|
|
36
|
+
* @agiflowai/hooks-adapter - Constants
|
|
37
|
+
*
|
|
38
|
+
* DESIGN PATTERNS:
|
|
39
|
+
* - Strongly-typed constant exports for compile-time safety
|
|
40
|
+
* - Immutable by default (as const assertions)
|
|
41
|
+
*
|
|
42
|
+
* CODING STANDARDS:
|
|
43
|
+
* - Primitive constants: UPPER_SNAKE_CASE
|
|
44
|
+
* - Always include JSDoc with purpose and usage
|
|
45
|
+
*
|
|
46
|
+
* AVOID:
|
|
47
|
+
* - Mutable exports (let, var)
|
|
48
|
+
* - Magic strings without explanation
|
|
49
|
+
*/
|
|
50
|
+
/**
|
|
51
|
+
* Hook type identifiers for different hook events
|
|
52
|
+
*/
|
|
53
|
+
const PRE_TOOL_USE = "PreToolUse";
|
|
54
|
+
const POST_TOOL_USE = "PostToolUse";
|
|
55
|
+
|
|
56
|
+
//#endregion
|
|
57
|
+
//#region src/adapters/BaseAdapter.ts
|
|
58
|
+
/**
|
|
59
|
+
* Abstract base adapter for normalizing AI agent hook formats
|
|
60
|
+
*/
|
|
61
|
+
var BaseAdapter = class {
|
|
62
|
+
/**
|
|
63
|
+
* Execute hook callback with normalized context
|
|
64
|
+
* Template method that orchestrates the hook execution flow
|
|
65
|
+
*
|
|
66
|
+
* @param callback - Hook callback function to execute
|
|
67
|
+
*/
|
|
68
|
+
async execute(callback) {
|
|
69
|
+
try {
|
|
70
|
+
const stdin = await this.readStdin();
|
|
71
|
+
const response = await callback(this.parseInput(stdin));
|
|
72
|
+
if (response.decision === "skip") {
|
|
73
|
+
process.exit(0);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const output = this.formatOutput(response);
|
|
77
|
+
console.log(output);
|
|
78
|
+
process.exit(0);
|
|
79
|
+
} catch (error) {
|
|
80
|
+
this.handleError(error);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Read stdin from AI agent
|
|
85
|
+
* @returns Promise resolving to stdin content
|
|
86
|
+
*/
|
|
87
|
+
readStdin() {
|
|
88
|
+
return new Promise((resolve, reject) => {
|
|
89
|
+
const chunks = [];
|
|
90
|
+
process.stdin.on("data", (chunk) => {
|
|
91
|
+
chunks.push(chunk);
|
|
92
|
+
});
|
|
93
|
+
process.stdin.on("end", () => {
|
|
94
|
+
resolve(Buffer.concat(chunks).toString("utf8"));
|
|
95
|
+
});
|
|
96
|
+
process.stdin.on("error", (error) => {
|
|
97
|
+
reject(error);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Handle errors with fail-open behavior
|
|
103
|
+
* Allows operation to proceed with warning message
|
|
104
|
+
*
|
|
105
|
+
* @param error - Error that occurred during hook execution
|
|
106
|
+
*/
|
|
107
|
+
handleError(error) {
|
|
108
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
109
|
+
const output = this.formatOutput({
|
|
110
|
+
decision: "allow",
|
|
111
|
+
message: `⚠️ Hook error: ${errorMessage}`
|
|
112
|
+
});
|
|
113
|
+
console.log(output);
|
|
114
|
+
process.exit(0);
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
//#endregion
|
|
119
|
+
//#region src/adapters/ClaudeCodeAdapter.ts
|
|
120
|
+
/**
|
|
121
|
+
* ClaudeCodeAdapter - Adapter for Claude Code hook format
|
|
122
|
+
*
|
|
123
|
+
* DESIGN PATTERNS:
|
|
124
|
+
* - Adapter pattern: Converts Claude Code format to normalized format
|
|
125
|
+
* - Parser pattern: Extracts file paths and operations from tool inputs
|
|
126
|
+
*
|
|
127
|
+
* CODING STANDARDS:
|
|
128
|
+
* - Parse Claude Code JSON stdin format exactly as specified
|
|
129
|
+
* - Format output to match Claude Code hook response schema
|
|
130
|
+
* - Handle missing/optional fields gracefully
|
|
131
|
+
*
|
|
132
|
+
* AVOID:
|
|
133
|
+
* - Assuming all fields are present
|
|
134
|
+
* - Hardcoding tool names (use constants if needed)
|
|
135
|
+
* - Mutating input objects
|
|
136
|
+
*/
|
|
137
|
+
/**
|
|
138
|
+
* Adapter for Claude Code hook format
|
|
139
|
+
*/
|
|
140
|
+
var ClaudeCodeAdapter = class extends BaseAdapter {
|
|
141
|
+
/**
|
|
142
|
+
* Parse Claude Code stdin into normalized HookContext
|
|
143
|
+
*
|
|
144
|
+
* @param stdin - Raw JSON string from Claude Code
|
|
145
|
+
* @returns Normalized hook context
|
|
146
|
+
*/
|
|
147
|
+
parseInput(stdin) {
|
|
148
|
+
const input = JSON.parse(stdin);
|
|
149
|
+
return {
|
|
150
|
+
toolName: input.tool_name,
|
|
151
|
+
toolInput: input.tool_input,
|
|
152
|
+
filePath: this.extractFilePath(input.tool_name, input.tool_input),
|
|
153
|
+
operation: this.extractOperation(input.tool_name),
|
|
154
|
+
cwd: input.cwd,
|
|
155
|
+
sessionId: input.session_id,
|
|
156
|
+
llmTool: input.llm_tool
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Format normalized HookResponse into Claude Code output
|
|
161
|
+
*
|
|
162
|
+
* @param response - Normalized hook response
|
|
163
|
+
* @returns JSON string for Claude Code
|
|
164
|
+
*/
|
|
165
|
+
formatOutput(response) {
|
|
166
|
+
if (response.decision === "skip") return JSON.stringify({}, null, 2);
|
|
167
|
+
const output = { hookSpecificOutput: {
|
|
168
|
+
hookEventName: "PreToolUse",
|
|
169
|
+
permissionDecision: response.decision,
|
|
170
|
+
permissionDecisionReason: response.message
|
|
171
|
+
} };
|
|
172
|
+
if (response.updatedInput) output.hookSpecificOutput.updatedInput = response.updatedInput;
|
|
173
|
+
return JSON.stringify(output, null, 2);
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Extract file path from tool input
|
|
177
|
+
*
|
|
178
|
+
* @param toolName - Name of the tool
|
|
179
|
+
* @param toolInput - Tool input parameters
|
|
180
|
+
* @returns File path if this is a file operation
|
|
181
|
+
*/
|
|
182
|
+
extractFilePath(toolName, toolInput) {
|
|
183
|
+
if ([
|
|
184
|
+
"Read",
|
|
185
|
+
"Write",
|
|
186
|
+
"Edit"
|
|
187
|
+
].includes(toolName)) return toolInput.file_path;
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Extract operation type from tool name
|
|
191
|
+
*
|
|
192
|
+
* @param toolName - Name of the tool
|
|
193
|
+
* @returns Operation type if this is a file operation
|
|
194
|
+
*/
|
|
195
|
+
extractOperation(toolName) {
|
|
196
|
+
return {
|
|
197
|
+
Read: "read",
|
|
198
|
+
Write: "write",
|
|
199
|
+
Edit: "edit"
|
|
200
|
+
}[toolName];
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
//#endregion
|
|
205
|
+
//#region src/adapters/ClaudeCodePostToolUseAdapter.ts
|
|
206
|
+
/**
|
|
207
|
+
* ClaudeCodePostToolUseAdapter - Adapter for Claude Code PostToolUse hook format
|
|
208
|
+
*
|
|
209
|
+
* DESIGN PATTERNS:
|
|
210
|
+
* - Adapter pattern: Converts Claude Code PostToolUse format to normalized format
|
|
211
|
+
* - Parser pattern: Extracts file paths and operations from tool response
|
|
212
|
+
*
|
|
213
|
+
* CODING STANDARDS:
|
|
214
|
+
* - Parse Claude Code JSON stdin format exactly as specified
|
|
215
|
+
* - Format output to match Claude Code PostToolUse hook response schema
|
|
216
|
+
* - Handle missing/optional fields gracefully
|
|
217
|
+
*
|
|
218
|
+
* AVOID:
|
|
219
|
+
* - Assuming all fields are present
|
|
220
|
+
* - Hardcoding tool names (use constants if needed)
|
|
221
|
+
* - Mutating input objects
|
|
222
|
+
*/
|
|
223
|
+
/**
|
|
224
|
+
* Adapter for Claude Code PostToolUse hook format
|
|
225
|
+
*/
|
|
226
|
+
var ClaudeCodePostToolUseAdapter = class extends BaseAdapter {
|
|
227
|
+
/**
|
|
228
|
+
* Parse Claude Code PostToolUse stdin into normalized HookContext
|
|
229
|
+
*
|
|
230
|
+
* @param stdin - Raw JSON string from Claude Code
|
|
231
|
+
* @returns Normalized hook context
|
|
232
|
+
*/
|
|
233
|
+
parseInput(stdin) {
|
|
234
|
+
const input = JSON.parse(stdin);
|
|
235
|
+
return {
|
|
236
|
+
toolName: input.tool_name,
|
|
237
|
+
toolInput: input.tool_input,
|
|
238
|
+
filePath: this.extractFilePath(input.tool_name, input.tool_input, input.tool_response),
|
|
239
|
+
operation: this.extractOperation(input.tool_name),
|
|
240
|
+
cwd: input.cwd,
|
|
241
|
+
sessionId: input.session_id,
|
|
242
|
+
llmTool: input.llm_tool
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Format normalized HookResponse into Claude Code PostToolUse output
|
|
247
|
+
*
|
|
248
|
+
* @param response - Normalized hook response
|
|
249
|
+
* @returns JSON string for Claude Code
|
|
250
|
+
*/
|
|
251
|
+
formatOutput(response) {
|
|
252
|
+
const output = { hookSpecificOutput: { hookEventName: "PostToolUse" } };
|
|
253
|
+
if (response.decision === "deny") {
|
|
254
|
+
output.decision = "block";
|
|
255
|
+
output.reason = response.message;
|
|
256
|
+
}
|
|
257
|
+
if (response.decision === "allow" && response.message) output.hookSpecificOutput.additionalContext = response.message;
|
|
258
|
+
return JSON.stringify(output, null, 2);
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Extract file path from tool input or response
|
|
262
|
+
*
|
|
263
|
+
* @param toolName - Name of the tool
|
|
264
|
+
* @param toolInput - Tool input parameters
|
|
265
|
+
* @param toolResponse - Tool response data
|
|
266
|
+
* @returns File path if this is a file operation
|
|
267
|
+
*/
|
|
268
|
+
extractFilePath(toolName, toolInput, toolResponse) {
|
|
269
|
+
if ([
|
|
270
|
+
"Read",
|
|
271
|
+
"Write",
|
|
272
|
+
"Edit"
|
|
273
|
+
].includes(toolName)) return toolInput.file_path || toolResponse.filePath;
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Extract operation type from tool name
|
|
277
|
+
*
|
|
278
|
+
* @param toolName - Name of the tool
|
|
279
|
+
* @returns Operation type if this is a file operation
|
|
280
|
+
*/
|
|
281
|
+
extractOperation(toolName) {
|
|
282
|
+
return {
|
|
283
|
+
Read: "read",
|
|
284
|
+
Write: "write",
|
|
285
|
+
Edit: "edit"
|
|
286
|
+
}[toolName];
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
//#endregion
|
|
291
|
+
//#region src/services/ExecutionLogService.ts
|
|
292
|
+
/**
|
|
293
|
+
* ExecutionLogService - Tracks hook executions to prevent duplicate actions
|
|
294
|
+
*
|
|
295
|
+
* DESIGN PATTERNS:
|
|
296
|
+
* - Repository pattern: Abstracts data access to execution log
|
|
297
|
+
* - Query pattern: Provides efficient lookups for hook execution history
|
|
298
|
+
* - Singleton cache: In-memory cache for performance
|
|
299
|
+
*
|
|
300
|
+
* CODING STANDARDS:
|
|
301
|
+
* - Use static methods for stateless operations
|
|
302
|
+
* - Handle file system errors gracefully
|
|
303
|
+
* - Optimize for performance with efficient data structures
|
|
304
|
+
*
|
|
305
|
+
* AVOID:
|
|
306
|
+
* - Loading entire log file into memory
|
|
307
|
+
* - Blocking I/O operations
|
|
308
|
+
* - Complex parsing logic (keep it simple)
|
|
309
|
+
*/
|
|
310
|
+
/**
|
|
311
|
+
* Service for tracking hook executions using an append-only log
|
|
312
|
+
* Prevents duplicate hook actions (e.g., showing design patterns twice for same file)
|
|
313
|
+
*/
|
|
314
|
+
var ExecutionLogService = class ExecutionLogService {
|
|
315
|
+
/** Log file path - stored in system temp directory */
|
|
316
|
+
static LOG_FILE = node_path.join(node_os.tmpdir(), "hook-adapter-executions.jsonl");
|
|
317
|
+
/** In-memory cache of recent executions (last 1000 entries) */
|
|
318
|
+
static cache = null;
|
|
319
|
+
/** Max cache size to prevent memory bloat */
|
|
320
|
+
static MAX_CACHE_SIZE = 1e3;
|
|
321
|
+
/**
|
|
322
|
+
* Check if a specific action was already taken for this file in this session
|
|
323
|
+
*
|
|
324
|
+
* @param sessionId - Session identifier
|
|
325
|
+
* @param filePath - File path to check
|
|
326
|
+
* @param decision - Decision to check for (e.g., 'deny' means we already showed patterns)
|
|
327
|
+
* @returns true if the action was already taken
|
|
328
|
+
*/
|
|
329
|
+
static async hasExecuted(sessionId, filePath, decision) {
|
|
330
|
+
const entries = await ExecutionLogService.loadLog();
|
|
331
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
332
|
+
const entry = entries[i];
|
|
333
|
+
if (entry.sessionId === sessionId && entry.filePath === filePath && entry.decision === decision) return true;
|
|
334
|
+
}
|
|
335
|
+
return false;
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Log a hook execution
|
|
339
|
+
*
|
|
340
|
+
* @param params - Log execution parameters
|
|
341
|
+
*/
|
|
342
|
+
static async logExecution(params) {
|
|
343
|
+
const entry = {
|
|
344
|
+
timestamp: Date.now(),
|
|
345
|
+
sessionId: params.sessionId,
|
|
346
|
+
filePath: params.filePath,
|
|
347
|
+
operation: params.operation,
|
|
348
|
+
decision: params.decision,
|
|
349
|
+
filePattern: params.filePattern,
|
|
350
|
+
fileMtime: params.fileMtime,
|
|
351
|
+
fileChecksum: params.fileChecksum
|
|
352
|
+
};
|
|
353
|
+
try {
|
|
354
|
+
await node_fs_promises.appendFile(ExecutionLogService.LOG_FILE, `${JSON.stringify(entry)}\n`, "utf-8");
|
|
355
|
+
if (ExecutionLogService.cache) {
|
|
356
|
+
ExecutionLogService.cache.push(entry);
|
|
357
|
+
if (ExecutionLogService.cache.length > ExecutionLogService.MAX_CACHE_SIZE) ExecutionLogService.cache = ExecutionLogService.cache.slice(-ExecutionLogService.MAX_CACHE_SIZE);
|
|
358
|
+
}
|
|
359
|
+
} catch (error) {
|
|
360
|
+
console.error("Failed to log hook execution:", error);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* Load execution log from file
|
|
365
|
+
* Uses in-memory cache for performance
|
|
366
|
+
*/
|
|
367
|
+
static async loadLog() {
|
|
368
|
+
if (ExecutionLogService.cache !== null) return ExecutionLogService.cache;
|
|
369
|
+
try {
|
|
370
|
+
const lines = (await node_fs_promises.readFile(ExecutionLogService.LOG_FILE, "utf-8")).trim().split("\n").filter(Boolean);
|
|
371
|
+
const entries = [];
|
|
372
|
+
for (const line of lines) try {
|
|
373
|
+
entries.push(JSON.parse(line));
|
|
374
|
+
} catch {}
|
|
375
|
+
ExecutionLogService.cache = entries.slice(-ExecutionLogService.MAX_CACHE_SIZE);
|
|
376
|
+
return ExecutionLogService.cache;
|
|
377
|
+
} catch (error) {
|
|
378
|
+
if (error.code === "ENOENT") {
|
|
379
|
+
ExecutionLogService.cache = [];
|
|
380
|
+
return ExecutionLogService.cache;
|
|
381
|
+
}
|
|
382
|
+
console.error("Failed to load execution log:", error);
|
|
383
|
+
ExecutionLogService.cache = [];
|
|
384
|
+
return ExecutionLogService.cache;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* Clear the execution log (for testing)
|
|
389
|
+
*/
|
|
390
|
+
static async clearLog() {
|
|
391
|
+
try {
|
|
392
|
+
await node_fs_promises.unlink(ExecutionLogService.LOG_FILE);
|
|
393
|
+
ExecutionLogService.cache = [];
|
|
394
|
+
} catch (error) {
|
|
395
|
+
if (error.code !== "ENOENT") throw error;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* Get log statistics (for debugging)
|
|
400
|
+
*/
|
|
401
|
+
static async getStats() {
|
|
402
|
+
const entries = await ExecutionLogService.loadLog();
|
|
403
|
+
const sessions = new Set(entries.map((e) => e.sessionId));
|
|
404
|
+
const files = new Set(entries.map((e) => e.filePath));
|
|
405
|
+
return {
|
|
406
|
+
totalEntries: entries.length,
|
|
407
|
+
uniqueSessions: sessions.size,
|
|
408
|
+
uniqueFiles: files.size
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
/**
|
|
412
|
+
* Get file metadata (mtime and checksum) for a file
|
|
413
|
+
*
|
|
414
|
+
* @param filePath - Path to the file
|
|
415
|
+
* @returns File metadata or null if file doesn't exist
|
|
416
|
+
*/
|
|
417
|
+
static async getFileMetadata(filePath) {
|
|
418
|
+
try {
|
|
419
|
+
const content = await node_fs_promises.readFile(filePath, "utf-8");
|
|
420
|
+
const checksum = node_crypto.createHash("md5").update(content).digest("hex");
|
|
421
|
+
return {
|
|
422
|
+
mtime: (await node_fs_promises.stat(filePath)).mtimeMs,
|
|
423
|
+
checksum
|
|
424
|
+
};
|
|
425
|
+
} catch {
|
|
426
|
+
return null;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* Check if a file has changed since the last execution for this session
|
|
431
|
+
* Returns true if the file should be reviewed (new file or content changed)
|
|
432
|
+
*
|
|
433
|
+
* @param sessionId - Session identifier
|
|
434
|
+
* @param filePath - File path to check
|
|
435
|
+
* @param decision - Decision type to check for
|
|
436
|
+
* @returns true if file has changed or no previous execution found
|
|
437
|
+
*/
|
|
438
|
+
static async hasFileChanged(sessionId, filePath, decision) {
|
|
439
|
+
const entries = await ExecutionLogService.loadLog();
|
|
440
|
+
let lastExecution = null;
|
|
441
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
442
|
+
const entry = entries[i];
|
|
443
|
+
if (entry.sessionId === sessionId && entry.filePath === filePath && entry.decision === decision) {
|
|
444
|
+
lastExecution = entry;
|
|
445
|
+
break;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
if (!lastExecution || !lastExecution.fileChecksum) return true;
|
|
449
|
+
const currentMetadata = await ExecutionLogService.getFileMetadata(filePath);
|
|
450
|
+
if (!currentMetadata) return true;
|
|
451
|
+
return currentMetadata.checksum !== lastExecution.fileChecksum;
|
|
452
|
+
}
|
|
453
|
+
};
|
|
454
|
+
|
|
455
|
+
//#endregion
|
|
456
|
+
//#region src/services/AdapterProxyService.ts
|
|
457
|
+
/**
|
|
458
|
+
* AdapterProxyService - Routes hook execution to appropriate adapter
|
|
459
|
+
*
|
|
460
|
+
* DESIGN PATTERNS:
|
|
461
|
+
* - Proxy pattern: Routes requests to appropriate handlers
|
|
462
|
+
* - Factory pattern: Creates adapter instances based on agent name
|
|
463
|
+
*
|
|
464
|
+
* CODING STANDARDS:
|
|
465
|
+
* - Use static methods for stateless operations
|
|
466
|
+
* - Provide clear error messages for invalid inputs
|
|
467
|
+
* - Follow TitleCase naming convention for service classes
|
|
468
|
+
*
|
|
469
|
+
* AVOID:
|
|
470
|
+
* - Creating adapter instances unnecessarily
|
|
471
|
+
* - Silently falling back to defaults
|
|
472
|
+
* - Complex conditional logic (use lookup tables)
|
|
473
|
+
*/
|
|
474
|
+
/**
|
|
475
|
+
* Proxy service for routing hook execution
|
|
476
|
+
* Eliminates duplication across commands by centralizing hook routing logic
|
|
477
|
+
*/
|
|
478
|
+
var AdapterProxyService = class AdapterProxyService {
|
|
479
|
+
/**
|
|
480
|
+
* Execute hook with the appropriate adapter for the agent
|
|
481
|
+
*
|
|
482
|
+
* @param agentName - Agent identifier (e.g., "claude-code")
|
|
483
|
+
* @param hookType - Type of hook ("PreToolUse" or "PostToolUse")
|
|
484
|
+
* @param callback - Hook callback function to execute
|
|
485
|
+
*/
|
|
486
|
+
static async execute(agentName, hookType, callback) {
|
|
487
|
+
await AdapterProxyService.getAdapter(agentName, hookType).execute(callback);
|
|
488
|
+
}
|
|
489
|
+
/**
|
|
490
|
+
* Get adapter instance for agent and hook type
|
|
491
|
+
*
|
|
492
|
+
* @param agentName - Name of the AI agent (e.g., "claude-code")
|
|
493
|
+
* @param hookType - Type of hook ("PreToolUse" or "PostToolUse")
|
|
494
|
+
* @returns Adapter instance
|
|
495
|
+
*/
|
|
496
|
+
static getAdapter(agentName, hookType) {
|
|
497
|
+
switch (agentName) {
|
|
498
|
+
case __agiflowai_coding_agent_bridge.CLAUDE_CODE:
|
|
499
|
+
if (hookType === POST_TOOL_USE) return new ClaudeCodePostToolUseAdapter();
|
|
500
|
+
return new ClaudeCodeAdapter();
|
|
501
|
+
default: throw new Error(`Unknown agent: ${agentName}. Supported: ${__agiflowai_coding_agent_bridge.CLAUDE_CODE}`);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
};
|
|
505
|
+
|
|
506
|
+
//#endregion
|
|
507
|
+
exports.AdapterProxyService = AdapterProxyService;
|
|
508
|
+
exports.BaseAdapter = BaseAdapter;
|
|
509
|
+
exports.ClaudeCodeAdapter = ClaudeCodeAdapter;
|
|
510
|
+
exports.ClaudeCodePostToolUseAdapter = ClaudeCodePostToolUseAdapter;
|
|
511
|
+
exports.ExecutionLogService = ExecutionLogService;
|
|
512
|
+
exports.POST_TOOL_USE = POST_TOOL_USE;
|
|
513
|
+
exports.PRE_TOOL_USE = PRE_TOOL_USE;
|