@miphamai/cli 0.3.0 → 0.5.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.
Files changed (74) hide show
  1. package/bin/mipham.ts +188 -0
  2. package/dist/mipham +0 -0
  3. package/package.json +9 -9
  4. package/skills/mipham/om-artifact.mipham-skill.md +69 -0
  5. package/skills/mipham/om-model-optimize.mipham-skill.md +9 -6
  6. package/skills/mipham/om-security.mipham-skill.md +11 -8
  7. package/skills/standard/doc-generator.SKILL.md +6 -1
  8. package/skills/standard/github-ops.SKILL.md +12 -7
  9. package/skills/standard/memory.SKILL.md +1 -0
  10. package/skills/standard/self-review.SKILL.md +1 -0
  11. package/skills/standard/superpower.SKILL.md +7 -7
  12. package/skills/standard/web-search.SKILL.md +3 -0
  13. package/src/agent/agent-context.ts +56 -0
  14. package/src/agent/agent-registry.ts +134 -0
  15. package/src/agent/sub-agent.ts +73 -89
  16. package/src/agent/types.ts +39 -0
  17. package/src/agent-view/agent-view-manager.ts +217 -0
  18. package/src/agent-view/dashboard.tsx +218 -0
  19. package/src/agent-view/session-peek.tsx +72 -0
  20. package/src/agent-view/session-row.tsx +70 -0
  21. package/src/artifacts/manifest.ts +101 -0
  22. package/src/artifacts/server.ts +374 -0
  23. package/src/artifacts/versioning.ts +127 -0
  24. package/src/core/context-compact.ts +50 -0
  25. package/src/core/context-drain.ts +54 -0
  26. package/src/core/context-microcompact.ts +132 -0
  27. package/src/core/context-snip.ts +74 -0
  28. package/src/core/context-token.ts +108 -0
  29. package/src/core/context.ts +169 -0
  30. package/src/core/engine.ts +207 -58
  31. package/src/core/hooks-config.ts +52 -0
  32. package/src/core/hooks-executor.ts +113 -0
  33. package/src/core/hooks.ts +90 -30
  34. package/src/core/instructions.ts +12 -1
  35. package/src/core/memory/memory-loader.ts +23 -0
  36. package/src/core/memory/memory-manager.ts +192 -0
  37. package/src/core/memory/memory-writer.ts +72 -0
  38. package/src/core/permission-config.ts +34 -0
  39. package/src/core/permission-rules.ts +66 -0
  40. package/src/core/permission.ts +224 -34
  41. package/src/index.tsx +30 -2
  42. package/src/mcp/transport.ts +42 -5
  43. package/src/plugin/plugin-loader.ts +36 -0
  44. package/src/plugin/plugin-manager.ts +125 -0
  45. package/src/plugin/plugin-validator.ts +41 -0
  46. package/src/providers/fetch-utils.ts +1 -3
  47. package/src/providers/openai-compat.ts +2 -11
  48. package/src/shared/constants.ts +8 -1
  49. package/src/shared/types.ts +82 -2
  50. package/src/skills/fork-executor.ts +40 -0
  51. package/src/skills/loader.ts +48 -2
  52. package/src/tools/agent/agent.ts +20 -11
  53. package/src/tools/agent/skill.ts +32 -2
  54. package/src/tools/artifact/artifact.ts +144 -0
  55. package/src/tools/computer/app-launcher.ts +39 -0
  56. package/src/tools/computer/browser.ts +48 -0
  57. package/src/tools/computer/computer-use.ts +88 -0
  58. package/src/tools/computer/playwright.d.ts +7 -0
  59. package/src/tools/computer/screenshot.ts +57 -0
  60. package/src/tools/index.ts +6 -0
  61. package/src/ui/app.tsx +20 -7
  62. package/src/ui/chat.tsx +9 -5
  63. package/src/ui/commands.ts +185 -40
  64. package/src/ui/input.tsx +203 -27
  65. package/src/ui/picker.tsx +2 -5
  66. package/src/ui/vim-motions.ts +96 -0
  67. package/src/workflow/budget.ts +33 -0
  68. package/src/workflow/journal.ts +105 -0
  69. package/src/workflow/primitives/agent.ts +51 -0
  70. package/src/workflow/primitives/parallel.ts +8 -0
  71. package/src/workflow/primitives/phase.ts +19 -0
  72. package/src/workflow/primitives/pipeline.ts +24 -0
  73. package/src/workflow/runtime.ts +87 -0
  74. package/src/workflow/sandbox.ts +75 -0
@@ -1,11 +1,22 @@
1
- import type { Message, StreamChunk, ToolDefinition, ToolResult } from '../shared/index.ts'
1
+ import type { StreamChunk, ToolDefinition, ToolResult } from '../shared/index.ts'
2
2
  import { ProviderRegistry } from '../providers/registry'
3
3
  import { ContextManager } from './context'
4
4
  import { PermissionSystem } from './permission'
5
5
  import type { HookEngine } from './hooks'
6
+ import type { ArtifactServer } from '../artifacts/server'
7
+ import type { AgentRegistry } from '../agent/agent-registry'
8
+ import { analyzeForMemory } from './memory/memory-writer'
9
+ import { getMemoryManager } from './memory/memory-loader'
10
+ import type { AgentViewManager } from '../agent-view/agent-view-manager'
6
11
 
7
12
  export class QueryEngine {
8
13
  private hookEngine?: HookEngine
14
+ private artifactServer?: ArtifactServer
15
+ private agentRegistry?: AgentRegistry
16
+ private agentViewManager?: AgentViewManager
17
+ private goal?: string
18
+ private maxGoalLoops = 20
19
+ private lastAssistantContent?: string
9
20
 
10
21
  constructor(
11
22
  private registry: ProviderRegistry,
@@ -19,6 +30,41 @@ export class QueryEngine {
19
30
  this.hookEngine = hooks
20
31
  }
21
32
 
33
+ /** Register the artifact server for tool context. */
34
+ setArtifactServer(server: ArtifactServer): void {
35
+ this.artifactServer = server
36
+ }
37
+
38
+ /** Register the agent registry for custom agent definitions. */
39
+ setAgentRegistry(reg: AgentRegistry): void {
40
+ this.agentRegistry = reg
41
+ }
42
+
43
+ /** Get the registered agent registry (may be undefined if not wired). */
44
+ getAgentRegistry(): AgentRegistry | undefined {
45
+ return this.agentRegistry
46
+ }
47
+
48
+ /** Register the AgentViewManager for background agent session management. */
49
+ setAgentViewManager(mgr: AgentViewManager): void {
50
+ this.agentViewManager = mgr
51
+ }
52
+
53
+ /** Get the AgentViewManager (may be undefined if not wired). */
54
+ getAgentViewManager(): AgentViewManager | undefined {
55
+ return this.agentViewManager
56
+ }
57
+
58
+ /** Set the session goal for goal-driven execution. */
59
+ setGoal(goal: string): void {
60
+ this.goal = goal
61
+ }
62
+
63
+ /** Get the last assistant text content. */
64
+ getLastAssistantContent(): string | undefined {
65
+ return this.lastAssistantContent
66
+ }
67
+
22
68
  /** Wire LLM-based conversation summarization into the context manager. */
23
69
  setupContextSummarizer(): void {
24
70
  this.context.setSummarizer(async (messages, heading) => {
@@ -31,8 +77,7 @@ export class QueryEngine {
31
77
  .join('\n')
32
78
 
33
79
  // Build a minimal system prompt for summarization
34
- const summaryPrompt =
35
- `You are a conversation summarizer. Create a concise summary (1-3 paragraphs) of this conversation excerpt. Focus on: key topics discussed, decisions made, code changes mentioned, and open questions. Heading: ${heading}`
80
+ const summaryPrompt = `You are a conversation summarizer. Create a concise summary (1-3 paragraphs) of this conversation excerpt. Focus on: key topics discussed, decisions made, code changes mentioned, and open questions. Heading: ${heading}`
36
81
 
37
82
  // Collect full summary text from streaming response
38
83
  let summary = ''
@@ -64,12 +109,30 @@ export class QueryEngine {
64
109
  }
65
110
 
66
111
  async *process(userInput: string, signal?: AbortSignal): AsyncGenerator<StreamChunk> {
112
+ // Fire UserPromptSubmit hooks before processing
113
+ if (this.hookEngine) {
114
+ const submitResult = await this.hookEngine.executeUserPromptSubmit(userInput, 'session-1')
115
+ if (submitResult.additionalContext) {
116
+ this.context.addMessage({
117
+ role: 'user',
118
+ content: `[Hook context]: ${submitResult.additionalContext}`,
119
+ })
120
+ }
121
+ if (!submitResult.allowed) {
122
+ yield {
123
+ type: 'error',
124
+ error: submitResult.reason || 'User input blocked by hook.',
125
+ }
126
+ return
127
+ }
128
+ }
129
+
67
130
  // Add user message to context
68
131
  this.context.addMessage({ role: 'user', content: userInput })
69
132
 
70
133
  // Check compaction before processing
71
134
  if (this.context.needsCompaction()) {
72
- await this.context.compact('conversation summary')
135
+ await this.compactWithHooks('conversation summary')
73
136
  }
74
137
 
75
138
  const systemPrompt = this.context.getSystemPrompt()
@@ -81,39 +144,39 @@ export class QueryEngine {
81
144
 
82
145
  // Stream model response
83
146
  try {
84
- for await (const chunk of this.registry.chat({
85
- model: this.registry.getActiveModel(),
86
- messages,
87
- systemPrompt,
88
- tools: toolDefs.length > 0 ? toolDefs : undefined,
89
- signal,
90
- })) {
91
- yield chunk
92
-
93
- if (chunk.type === 'error') {
94
- this.context.addMessage({ role: 'assistant', content: `Error: ${chunk.error}` })
95
- return
96
- }
147
+ for await (const chunk of this.registry.chat({
148
+ model: this.registry.getActiveModel(),
149
+ messages,
150
+ systemPrompt,
151
+ tools: toolDefs.length > 0 ? toolDefs : undefined,
152
+ signal,
153
+ })) {
154
+ yield chunk
97
155
 
98
- if (chunk.type === 'text' && chunk.content) {
99
- assistantContent += chunk.content
100
- }
156
+ if (chunk.type === 'error') {
157
+ this.context.addMessage({ role: 'assistant', content: `Error: ${chunk.error}` })
158
+ return
159
+ }
101
160
 
102
- if (chunk.type === 'tool_use' && chunk.toolUse) {
103
- toolUses.push({
104
- id: chunk.toolUse.id,
105
- name: chunk.toolUse.name,
106
- input: chunk.toolUse.input,
107
- })
108
- }
161
+ if (chunk.type === 'text' && chunk.content) {
162
+ assistantContent += chunk.content
163
+ }
109
164
 
110
- if (chunk.type === 'stop') {
111
- // Add assistant response to context
112
- if (assistantContent) {
113
- this.context.addMessage({ role: 'assistant', content: assistantContent })
165
+ if (chunk.type === 'tool_use' && chunk.toolUse) {
166
+ toolUses.push({
167
+ id: chunk.toolUse.id,
168
+ name: chunk.toolUse.name,
169
+ input: chunk.toolUse.input,
170
+ })
171
+ }
172
+
173
+ if (chunk.type === 'stop') {
174
+ // Add assistant response to context
175
+ if (assistantContent) {
176
+ this.context.addMessage({ role: 'assistant', content: assistantContent })
177
+ }
114
178
  }
115
179
  }
116
- }
117
180
  } catch (err) {
118
181
  if (isAbortError(err)) {
119
182
  // User interrupted — keep partial content, stop gracefully
@@ -127,13 +190,27 @@ export class QueryEngine {
127
190
  return
128
191
  }
129
192
 
193
+ // Track last assistant content for goal checking
194
+ if (assistantContent) {
195
+ this.lastAssistantContent = assistantContent
196
+ }
197
+
198
+ // Analyze user message for memory-worthy content after AI response
199
+ if (assistantContent && userInput) {
200
+ try {
201
+ analyzeForMemory(userInput, getMemoryManager())
202
+ } catch {
203
+ // memory analysis is non-critical
204
+ }
205
+ }
206
+
130
207
  // Execute any tools that were requested
131
208
  for (const toolUse of toolUses) {
132
209
  const result = await this.executeTool(toolUse.name, toolUse.input)
133
210
  yield {
134
211
  type: 'tool_result',
135
212
  tool_use_id: toolUse.id,
136
- content: result.success ? result.content : (result.error || result.content),
213
+ content: result.success ? result.content : result.error || result.content,
137
214
  }
138
215
 
139
216
  // Add tool use + result to context
@@ -150,6 +227,26 @@ export class QueryEngine {
150
227
  // If tools were executed, recursively continue the conversation
151
228
  if (toolUses.length > 0) {
152
229
  yield* this.continueWithTools(signal)
230
+ return
231
+ }
232
+
233
+ // Fire Stop hooks when AI finishes with no tool calls
234
+ yield* this.checkStopHook(signal)
235
+ }
236
+
237
+ async *processWithGoal(input: string, signal?: AbortSignal): AsyncGenerator<StreamChunk> {
238
+ let loop = 0
239
+ while (loop < this.maxGoalLoops) {
240
+ yield* this.process(input, signal)
241
+ loop++
242
+
243
+ if (!this.goal) break
244
+
245
+ // Ask AI to check the goal condition
246
+ const checkMsg = `Has this goal been achieved? "${this.goal}" Answer YES or NO with reason.`
247
+ yield* this.process(checkMsg, signal)
248
+ // If AI responds "YES", break
249
+ if (this.lastAssistantContent?.includes('YES')) break
153
250
  }
154
251
  }
155
252
 
@@ -165,26 +262,26 @@ export class QueryEngine {
165
262
  const toolUses: Array<{ id: string; name: string; input: Record<string, unknown> }> = []
166
263
 
167
264
  try {
168
- for await (const chunk of this.registry.chat({
169
- model: this.registry.getActiveModel(),
170
- messages,
171
- systemPrompt,
172
- tools: toolDefs.length > 0 ? toolDefs : undefined,
173
- signal,
174
- })) {
175
- yield chunk
265
+ for await (const chunk of this.registry.chat({
266
+ model: this.registry.getActiveModel(),
267
+ messages,
268
+ systemPrompt,
269
+ tools: toolDefs.length > 0 ? toolDefs : undefined,
270
+ signal,
271
+ })) {
272
+ yield chunk
176
273
 
177
- if (chunk.type === 'error') return
178
- if (chunk.type === 'text' && chunk.content) assistantContent += chunk.content
274
+ if (chunk.type === 'error') return
275
+ if (chunk.type === 'text' && chunk.content) assistantContent += chunk.content
179
276
 
180
- if (chunk.type === 'tool_use' && chunk.toolUse) {
181
- toolUses.push({
182
- id: chunk.toolUse.id,
183
- name: chunk.toolUse.name,
184
- input: chunk.toolUse.input,
185
- })
277
+ if (chunk.type === 'tool_use' && chunk.toolUse) {
278
+ toolUses.push({
279
+ id: chunk.toolUse.id,
280
+ name: chunk.toolUse.name,
281
+ input: chunk.toolUse.input,
282
+ })
283
+ }
186
284
  }
187
- }
188
285
  } catch (err) {
189
286
  if (isAbortError(err)) {
190
287
  if (assistantContent) {
@@ -200,8 +297,11 @@ export class QueryEngine {
200
297
  this.context.addMessage({ role: 'assistant', content: assistantContent })
201
298
  }
202
299
 
203
- // No more tool calls — conversation complete
204
- if (toolUses.length === 0) return
300
+ // No more tool calls — fire Stop hook and potentially continue
301
+ if (toolUses.length === 0) {
302
+ yield* this.checkStopHook(signal)
303
+ return
304
+ }
205
305
 
206
306
  // Execute tools and feed results back to the model for the next turn
207
307
  for (const toolUse of toolUses) {
@@ -243,11 +343,7 @@ export class QueryEngine {
243
343
  // Run PreToolUse hooks
244
344
  let effectiveParams = params
245
345
  if (this.hookEngine) {
246
- const preResult = await this.hookEngine.executePreToolUse(
247
- name,
248
- params,
249
- 'session-1',
250
- )
346
+ const preResult = await this.hookEngine.executePreToolUse(name, params, 'session-1')
251
347
  if (!preResult.allowed) {
252
348
  return {
253
349
  success: false,
@@ -266,6 +362,10 @@ export class QueryEngine {
266
362
  sessionId: 'session-1',
267
363
  provider: this.registry.getActive().config.id,
268
364
  model: this.registry.getActiveModel(),
365
+ registry: this.registry,
366
+ toolRegistry: this.tools,
367
+ artifactServer: this.artifactServer,
368
+ agentRegistry: this.agentRegistry,
269
369
  })
270
370
 
271
371
  // Run PostToolUse hooks
@@ -292,6 +392,10 @@ export class QueryEngine {
292
392
  return this.context
293
393
  }
294
394
 
395
+ getRegistry(): ProviderRegistry {
396
+ return this.registry
397
+ }
398
+
295
399
  getTools(): Map<string, ToolDefinition> {
296
400
  return this.tools
297
401
  }
@@ -299,10 +403,55 @@ export class QueryEngine {
299
403
  switchProvider(providerId: string, modelId?: string): void {
300
404
  this.registry.switchProvider(providerId, modelId)
301
405
  }
406
+
407
+ /** Wrap context compaction with PreCompact/PostCompact hooks. */
408
+ private async compactWithHooks(heading: string): Promise<void> {
409
+ if (this.hookEngine) {
410
+ const preResult = await this.hookEngine.executePreCompact('session-1')
411
+ if (preResult.additionalContext) {
412
+ this.context.addMessage({
413
+ role: 'user',
414
+ content: `[Pre-compact context]: ${preResult.additionalContext}`,
415
+ })
416
+ }
417
+ }
418
+
419
+ await this.context.compact(heading)
420
+
421
+ if (this.hookEngine) {
422
+ const postResult = await this.hookEngine.executePostCompact('session-1')
423
+ if (postResult.additionalContext) {
424
+ this.context.addMessage({
425
+ role: 'user',
426
+ content: `[Post-compact context]: ${postResult.additionalContext}`,
427
+ })
428
+ }
429
+ }
430
+ }
431
+
432
+ /** Fire Stop hook. If blocked, feed the reason back to the AI and continue. */
433
+ private async *checkStopHook(signal?: AbortSignal): AsyncGenerator<StreamChunk> {
434
+ if (!this.hookEngine) return
435
+
436
+ const stopResult = await this.hookEngine.executeStop('session-1')
437
+ if (stopResult.decision === 'block') {
438
+ // Feed the block reason back to the AI and continue
439
+ this.context.addMessage({
440
+ role: 'user',
441
+ content: `[The Stop hook blocked completion]: ${stopResult.reason || 'Continue working.'}`,
442
+ })
443
+ yield* this.continueWithTools(signal)
444
+ }
445
+ }
302
446
  }
303
447
 
304
448
  function isAbortError(err: unknown): boolean {
305
449
  if (err instanceof Error && err.name === 'AbortError') return true
306
- if (typeof DOMException !== 'undefined' && err instanceof DOMException && err.name === 'AbortError') return true
450
+ if (
451
+ typeof DOMException !== 'undefined' &&
452
+ err instanceof DOMException &&
453
+ err.name === 'AbortError'
454
+ )
455
+ return true
307
456
  return false
308
457
  }
@@ -0,0 +1,52 @@
1
+ import type { HookConfig, HookEvent, HookDefinition, HookContext } from '../shared/index.ts'
2
+ import { executeHook } from './hooks-executor'
3
+
4
+ interface HookConfigEntry {
5
+ matcher: string
6
+ hooks: HookConfig[]
7
+ }
8
+
9
+ interface SettingsHooks {
10
+ PreToolUse?: HookConfigEntry[]
11
+ PostToolUse?: HookConfigEntry[]
12
+ Stop?: HookConfigEntry[]
13
+ UserPromptSubmit?: HookConfigEntry[]
14
+ PreCompact?: HookConfigEntry[]
15
+ PostCompact?: HookConfigEntry[]
16
+ SessionStart?: HookConfigEntry[]
17
+ SessionEnd?: HookConfigEntry[]
18
+ Notification?: HookConfigEntry[]
19
+ ConfigChange?: HookConfigEntry[]
20
+ }
21
+
22
+ /**
23
+ * Load hook configurations from a settings object and convert them to
24
+ * executable HookDefinitions suitable for the HookEngine.
25
+ */
26
+ export function loadHookConfigs(configs: SettingsHooks): HookDefinition[] {
27
+ const definitions: HookDefinition[] = []
28
+
29
+ for (const [eventName, entries] of Object.entries(configs)) {
30
+ if (!entries || !Array.isArray(entries)) continue
31
+
32
+ for (const entry of entries) {
33
+ const matcherRegex = entry.matcher ? new RegExp(entry.matcher) : null
34
+
35
+ for (const hookCfg of entry.hooks) {
36
+ definitions.push({
37
+ event: eventName as HookEvent,
38
+ toolName: entry.matcher || undefined,
39
+ handler: async (ctx: HookContext) => {
40
+ // Check matcher if tool-specific
41
+ if (matcherRegex && ctx.toolName && !matcherRegex.test(ctx.toolName)) {
42
+ return { allowed: true }
43
+ }
44
+ return executeHook(hookCfg, ctx)
45
+ },
46
+ })
47
+ }
48
+ }
49
+ }
50
+
51
+ return definitions
52
+ }
@@ -0,0 +1,113 @@
1
+ import { execSync } from 'node:child_process'
2
+ import type { HookConfig, HookContext, HookResult } from '../shared/index.ts'
3
+
4
+ /**
5
+ * Execute a hook based on its type, returning a HookResult.
6
+ *
7
+ * Supported types:
8
+ * - command: Execute a shell command. Exit code 0 = allow, 2 = block with stderr as reason.
9
+ * - http: POST to a URL, response body becomes additionalContext.
10
+ * - mcp_tool: Call an MCP tool (delegates to MCP client -- stub for now).
11
+ * - code: No-op (handled inline by the handler function directly).
12
+ */
13
+ export async function executeHook(cfg: HookConfig, ctx: HookContext): Promise<HookResult> {
14
+ switch (cfg.type) {
15
+ case 'command':
16
+ return executeCommand(cfg, ctx)
17
+ case 'http':
18
+ return executeHttp(cfg, ctx)
19
+ case 'mcp_tool':
20
+ return executeMcpTool(cfg, ctx)
21
+ default:
22
+ return { allowed: true }
23
+ }
24
+ }
25
+
26
+ function substituteVars(template: string, ctx: HookContext): string {
27
+ return template
28
+ .replace(/\$TOOL_NAME/g, ctx.toolName || '')
29
+ .replace(/\$INPUT/g, ctx.toolInput ? JSON.stringify(ctx.toolInput) : '')
30
+ .replace(/\$SESSION_ID/g, ctx.sessionId)
31
+ }
32
+
33
+ function executeCommand(cfg: HookConfig, ctx: HookContext): HookResult {
34
+ if (!cfg.command) return { allowed: true }
35
+
36
+ try {
37
+ const args = cfg.args ? cfg.args.map((a) => substituteVars(a, ctx)) : []
38
+
39
+ const cmd = [cfg.command, ...args].join(' ')
40
+
41
+ execSync(cmd, {
42
+ timeout: 30_000,
43
+ encoding: 'utf-8',
44
+ stdio: ['ignore', 'pipe', 'pipe'],
45
+ })
46
+
47
+ // Exit code 0 = success, allow
48
+ return { allowed: true }
49
+ } catch (err) {
50
+ const stderr = (err as { stderr?: string }).stderr || String(err)
51
+
52
+ // Exit code 2 = block with reason
53
+ if ((err as { status?: number }).status === 2) {
54
+ return {
55
+ allowed: false,
56
+ reason: stderr.trim() || 'Blocked by hook',
57
+ additionalContext: cfg.continueOnBlock ? stderr.trim() : undefined,
58
+ }
59
+ }
60
+
61
+ // Other non-zero exit: don't block, log the error as context
62
+ return {
63
+ allowed: true,
64
+ additionalContext: `Hook warning (${cfg.command}): ${stderr.trim()}`,
65
+ }
66
+ }
67
+ }
68
+
69
+ async function executeHttp(cfg: HookConfig, ctx: HookContext): Promise<HookResult> {
70
+ if (!cfg.url) return { allowed: true }
71
+
72
+ try {
73
+ const response = await fetch(cfg.url, {
74
+ method: cfg.method || 'POST',
75
+ headers: {
76
+ 'Content-Type': 'application/json',
77
+ ...(cfg.headers || {}),
78
+ },
79
+ body: JSON.stringify({
80
+ event: ctx.event,
81
+ toolName: ctx.toolName,
82
+ sessionId: ctx.sessionId,
83
+ }),
84
+ signal: AbortSignal.timeout(10_000),
85
+ })
86
+
87
+ const body = await response.text()
88
+
89
+ if (!response.ok) {
90
+ return {
91
+ allowed: false,
92
+ reason: `HTTP hook returned ${response.status}: ${body.slice(0, 200)}`,
93
+ }
94
+ }
95
+
96
+ return {
97
+ allowed: true,
98
+ additionalContext: body.slice(0, 2000) || undefined,
99
+ }
100
+ } catch (err) {
101
+ // HTTP hook failures should not block
102
+ return {
103
+ allowed: true,
104
+ additionalContext: `HTTP hook error (${cfg.url}): ${String(err)}`,
105
+ }
106
+ }
107
+ }
108
+
109
+ async function executeMcpTool(_cfg: HookConfig, _ctx: HookContext): Promise<HookResult> {
110
+ // Stub: MCP tool hook execution requires MCP client integration.
111
+ // For now, return allow to not block execution.
112
+ return { allowed: true }
113
+ }