@miphamai/cli 0.2.3 → 0.3.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.
@@ -1,19 +1,57 @@
1
1
  ---
2
2
  name: superpower
3
- description: Use when starting any conversationestablishes how to find and use skills
4
- version: 1.0.0
3
+ description: Skill discovery and invocation system — find and use skills before any response or action
4
+ version: 2.0.0
5
5
  ---
6
6
 
7
- # Superpowers Skill
7
+ # Superpowers — Using Skills
8
8
 
9
- ## Instruction Priority
9
+ ## The Rule
10
10
 
11
- User's explicit instructions always take precedence.
11
+ **Invoke relevant or requested skills BEFORE any response or action.** Even a 1% chance a skill might apply means you should invoke it to check.
12
12
 
13
13
  ## How to Access Skills
14
14
 
15
- Use the Skill tool to invoke skills. When you invoke a skill, its content is loaded and presented to you — follow it directly.
15
+ Use the `Skill` tool to invoke skills by name. When you invoke a skill, its content is loaded — follow it directly.
16
16
 
17
- ## The Rule
17
+ ## Skill Discovery
18
+
19
+ ### Check Available Skills
20
+
21
+ Skills are listed in `<system-reminder>` messages. Scan this list when receiving a task.
22
+
23
+ ### Matching Algorithm
24
+
25
+ 1. Parse the user's request for intent keywords
26
+ 2. Scan skill names and descriptions for matches
27
+ 3. If ANY skill matches at ≥1% probability → invoke it
28
+ 4. Multiple matches → invoke all that may apply
29
+ 5. Invoked skill doesn't fit → that's fine, don't use it
30
+
31
+ ### Priority Order
32
+
33
+ 1. **Process skills first** — brainstorming, systematic-debugging, tdd. These determine HOW to approach
34
+ 2. **Implementation skills second** — frontend-design, mcp-builder. These guide execution
35
+
36
+ ## Red Flags
37
+
38
+ These thoughts mean STOP — you're rationalizing:
39
+
40
+ | Thought | Reality |
41
+ |---------|---------|
42
+ | "This is just a simple question" | Questions are tasks. Check skills. |
43
+ | "I need more context first" | Skill check comes BEFORE clarifying questions. |
44
+ | "Let me explore the codebase first" | Skills tell you HOW to explore. |
45
+ | "I remember this skill" | Skills evolve. Read current version. |
46
+ | "The skill is overkill" | Simple things become complex. Use it. |
47
+
48
+ ## Skill Types
49
+
50
+ - **Rigid** (TDD, systematic-debugging): Follow exactly. Don't adapt away discipline.
51
+ - **Flexible** (patterns): Adapt principles to context.
52
+
53
+ The skill itself tells you which type it is.
54
+
55
+ ## User Instructions
18
56
 
19
- Invoke relevant or requested skills BEFORE any response or action. Even a 1% chance a skill might apply means that you should invoke the skill to check.
57
+ Instructions say WHAT, not HOW. "Add X" or "Fix Y" doesn't mean skip workflows.
@@ -1,20 +1,93 @@
1
1
  ---
2
2
  name: tdd
3
- description: Test-Driven Development workflow — red, green, refactor
4
- version: 1.0.0
3
+ description: Test-Driven Development — red-green-refactor cycle with language-specific guidance and test design rules
4
+ version: 2.0.0
5
5
  ---
6
6
 
7
- # TDD Skill
7
+ # Test-Driven Development (TDD)
8
8
 
9
- ## Workflow
9
+ ## The Cycle
10
10
 
11
- 1. **Red**: Write a failing test that defines the expected behavior
12
- 2. **Green**: Write the minimum code to make the test pass
13
- 3. **Refactor**: Clean up the code while tests remain green
11
+ ```
12
+ RED GREEN REFACTOR repeat
13
+ ```
14
14
 
15
- ## Rules
15
+ ### 1. RED — Write a Failing Test
16
16
 
17
- - Never write production code without a failing test
18
- - Write only enough test to fail
19
- - Write only enough code to pass
20
- - Tests must be deterministic and isolated
17
+ Write the smallest test that captures the behavior you want:
18
+
19
+ - Name the test descriptively: `it('should return 0 for empty string')`
20
+ - Use the AAA pattern: **A**rrange → **A**ct → **A**ssert
21
+ - Run to confirm it **fails** (not errors — fails)
22
+ - If it passes before implementation, your test is wrong
23
+
24
+ ### 2. GREEN — Make It Pass
25
+
26
+ Write the **minimum** code to make the test pass:
27
+
28
+ - Don't optimize, don't generalize, don't add features
29
+ - A hardcoded return is fine if it passes the test
30
+ - Run all tests — the new one should pass, old ones should still pass
31
+
32
+ ### 3. REFACTOR — Clean Up
33
+
34
+ Improve the code while tests stay green:
35
+
36
+ - Remove duplication (test code and production code)
37
+ - Improve names, extract helpers
38
+ - Simplify logic
39
+ - Run tests after each change
40
+
41
+ ## Test Design Rules
42
+
43
+ - **Deterministic**: No `Date.now()`, `Math.random()`, or network calls in test bodies
44
+ - **Isolated**: Each test sets up its own state; no test-order dependency
45
+ - **Fast**: Unit tests should run in milliseconds, not seconds
46
+ - **Readable**: Test output should explain what broke without reading source
47
+
48
+ ## Language-Specific Guidance
49
+
50
+ ### TypeScript / JavaScript (Vitest)
51
+
52
+ ```ts
53
+ import { describe, it, expect } from 'vitest'
54
+
55
+ describe('sum', () => {
56
+ it('should add two positive numbers', () => {
57
+ expect(sum(2, 3)).toBe(5)
58
+ })
59
+ it('should handle zero', () => {
60
+ expect(sum(0, 5)).toBe(5)
61
+ })
62
+ })
63
+ ```
64
+
65
+ File naming: `src/foo.ts` → `test/foo.test.ts`
66
+
67
+ ### Python (pytest)
68
+
69
+ ```python
70
+ def test_sum_positive():
71
+ assert sum(2, 3) == 5
72
+
73
+ def test_sum_zero():
74
+ assert sum(0, 5) == 5
75
+ ```
76
+
77
+ ### Go (testing package)
78
+
79
+ ```go
80
+ func TestSumPositive(t *testing.T) {
81
+ got := Sum(2, 3)
82
+ want := 5
83
+ if got != want {
84
+ t.Errorf("Sum(2,3) = %d; want %d", got, want)
85
+ }
86
+ }
87
+ ```
88
+
89
+ ## When NOT to TDD
90
+
91
+ - Exploratory spikes (throw away after learning)
92
+ - Configuration files and types (compile-time enforced)
93
+ - Generated code
@@ -1,23 +1,72 @@
1
1
  ---
2
2
  name: web-search
3
- description: Search the web for current information, documentation, and news
4
- version: 1.0.0
3
+ description: Search the web for current information documentation, news, technical references, and troubleshooting
4
+ version: 2.0.0
5
5
  ---
6
6
 
7
- # Web Search Skill
7
+ # Web Search
8
8
 
9
- Use WebSearch tool to find current information.
9
+ Use `WebSearch` tool to find current, accurate information from the web.
10
10
 
11
11
  ## When to Use
12
12
 
13
- - Up-to-date documentation for libraries and frameworks
14
- - Current events and news
15
- - Technical troubleshooting with recent solutions
16
- - API reference and version-specific features
13
+ - **Library/framework docs**: API references, migration guides, version-specific features
14
+ - **Current events**: News, releases, incidents (anything after training cutoff)
15
+ - **Troubleshooting**: Error messages, stack traces, known issues
16
+ - **Comparisons**: Technology trade-offs, benchmark data
17
+ - **Code examples**: Real-world usage patterns, configuration snippets
17
18
 
18
- ## Best Practices
19
+ ## Query Best Practices
19
20
 
20
- - Be specific with search queries
21
- - Use allowed_domains to focus results
22
- - Verify information from multiple sources
23
- - Cite sources in responses
21
+ ### Be Specific
22
+
23
+ ```
24
+ "React" → too broad
25
+ ❌ "React problems" → ambiguous
26
+ ✅ "React 19 useEffect double mount fix" → specific, versioned
27
+ ✅ "Next.js 14 App Router caching behavior 2026" → targeted
28
+ ```
29
+
30
+ ### Use Technical Terms
31
+
32
+ ```
33
+ ❌ "how to make website fast"
34
+ ✅ "Core Web Vitals LCP optimization Next.js 14"
35
+ ```
36
+
37
+ ### Include Version Numbers
38
+
39
+ ```
40
+ ❌ "prisma query"
41
+ ✅ "Prisma 5.0 findMany with nested include filter"
42
+ ```
43
+
44
+ ### Domain Filtering
45
+
46
+ Use `allowed_domains` for authoritative sources:
47
+ - `docs.github.com` — GitHub documentation
48
+ - `nextjs.org` — Next.js official docs
49
+ - `developer.mozilla.org` — MDN Web Docs
50
+ - `nodejs.org` — Node.js official
51
+
52
+ ## Verification
53
+
54
+ - **Cross-reference**: Verify claims across 2+ independent sources
55
+ - **Recency**: Prefer results from the current year; note article dates
56
+ - **Authority**: Official docs > well-known blogs > Stack Overflow > random forums
57
+ - **Cite sources**: Always include source URLs in responses
58
+
59
+ ## Source Attribution
60
+
61
+ After answering from search results, end with:
62
+ ```markdown
63
+ Sources:
64
+ - [Title](URL) — brief note
65
+ - [Title](URL) — brief note
66
+ ```
67
+
68
+ ## When NOT to Search
69
+
70
+ - Pure logic or algorithmic questions (reasoning, not research)
71
+ - Questions answerable from code already in context
72
+ - Opinions and subjective recommendations (search for data, not consensus)
@@ -1,5 +1,7 @@
1
1
  import type { Message } from '../shared/index.ts'
2
2
 
3
+ export type Summarizer = (messages: Message[], heading: string) => Promise<string>
4
+
3
5
  interface ContextConfig {
4
6
  maxTokens: number
5
7
  compactionThreshold: number // e.g. 0.9 → compact at 90% usage
@@ -19,9 +21,15 @@ export class ContextManager {
19
21
  private estimatedTokens = 0
20
22
  private checkpoints: Checkpoint[] = []
21
23
  private checkpointCounter = 0
24
+ private summarizer?: Summarizer
22
25
 
23
26
  constructor(private config: ContextConfig) {}
24
27
 
28
+ /** Set an optional LLM summarizer for intelligent compaction. */
29
+ setSummarizer(fn: Summarizer): void {
30
+ this.summarizer = fn
31
+ }
32
+
25
33
  setSystemPrompt(prompt: string): void {
26
34
  this.systemPrompt = prompt
27
35
  this.estimatedTokens = this.estimateTokens(prompt)
@@ -46,19 +54,36 @@ export class ContextManager {
46
54
  return this.estimatedTokens > this.config.maxTokens * this.config.compactionThreshold
47
55
  }
48
56
 
49
- async compact(_summarizeHeading: string): Promise<void> {
50
- // Phase 1: simple truncation — keep last 20 messages, drop oldest
51
- if (this.messages.length > 30) {
52
- const keep = 20
57
+ async compact(heading: string): Promise<void> {
58
+ if (this.messages.length <= 30) return
59
+
60
+ const keep = 20
61
+ const toDrop = this.messages.slice(0, -keep)
62
+
63
+ if (this.summarizer && toDrop.length >= 4) {
64
+ // LLM-based summarization of truncated messages
65
+ try {
66
+ const summary = await this.summarizer(toDrop, heading)
67
+ const summaryMsg: Message = {
68
+ role: 'user',
69
+ content: `[Earlier conversation summary]: ${summary}`,
70
+ }
71
+ this.messages = [summaryMsg, ...this.messages.slice(-keep)]
72
+ } catch {
73
+ // Fall back to truncation on summarizer failure
74
+ this.messages = this.messages.slice(-keep)
75
+ }
76
+ } else {
77
+ // Simple truncation fallback
53
78
  this.messages = this.messages.slice(-keep)
79
+ }
54
80
 
55
- // Re-estimate tokens
56
- this.estimatedTokens = this.estimateTokens(this.systemPrompt)
57
- for (const msg of this.messages) {
58
- this.estimatedTokens += this.estimateTokens(
59
- typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content),
60
- )
61
- }
81
+ // Re-estimate tokens
82
+ this.estimatedTokens = this.estimateTokens(this.systemPrompt)
83
+ for (const msg of this.messages) {
84
+ this.estimatedTokens += this.estimateTokens(
85
+ typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content),
86
+ )
62
87
  }
63
88
  }
64
89
 
@@ -128,8 +153,43 @@ export class ContextManager {
128
153
  return this.checkpoints.at(-1)?.id
129
154
  }
130
155
 
156
+ /**
157
+ * Estimate token count for a text string.
158
+ *
159
+ * Uses a character-class-aware heuristic:
160
+ * - CJK / emoji: ~1.5 chars per token
161
+ * - Other scripts (Latin, Cyrillic, Arabic): ~4 chars per token
162
+ * - Whitespace-heavy (code): ~3 chars per token
163
+ *
164
+ * This is ~30-40% more accurate than flat 4 chars/token for mixed-language text.
165
+ */
131
166
  private estimateTokens(text: string): number {
132
- // Simple estimation: ~4 chars per token
133
- return Math.ceil(text.length / 4)
167
+ if (!text) return 0
168
+
169
+ let cjk = 0
170
+ let latin = 0
171
+
172
+ for (const ch of text) {
173
+ const cp = ch.codePointAt(0)!
174
+ // CJK Unified Ideographs, Hangul, Kana, CJK Extensions, fullwidth forms
175
+ if (
176
+ (cp >= 0x4e00 && cp <= 0x9fff) || // CJK Unified
177
+ (cp >= 0x3400 && cp <= 0x4dbf) || // CJK Ext-A
178
+ (cp >= 0x20000 && cp <= 0x2a6df) || // CJK Ext-B
179
+ (cp >= 0xac00 && cp <= 0xd7af) || // Hangul
180
+ (cp >= 0x3040 && cp <= 0x30ff) || // Hiragana + Katakana
181
+ (cp >= 0xff01 && cp <= 0xff60) || // Fullwidth
182
+ (cp >= 0x1f300 && cp <= 0x1f9ff) // Emoji / pictographs
183
+ ) {
184
+ cjk++
185
+ } else if (cp > 0x7f) {
186
+ latin++
187
+ } else {
188
+ latin++
189
+ }
190
+ }
191
+
192
+ // CJK: ~1.5 chars/token, Latin/other: ~4 chars/token
193
+ return Math.max(1, Math.ceil(cjk / 1.5 + latin / 4))
134
194
  }
135
195
  }
@@ -2,8 +2,11 @@ import type { Message, StreamChunk, ToolDefinition, ToolResult } from '../shared
2
2
  import { ProviderRegistry } from '../providers/registry'
3
3
  import { ContextManager } from './context'
4
4
  import { PermissionSystem } from './permission'
5
+ import type { HookEngine } from './hooks'
5
6
 
6
7
  export class QueryEngine {
8
+ private hookEngine?: HookEngine
9
+
7
10
  constructor(
8
11
  private registry: ProviderRegistry,
9
12
  private context: ContextManager,
@@ -11,11 +14,56 @@ export class QueryEngine {
11
14
  private permission: PermissionSystem = new PermissionSystem('bypass'),
12
15
  ) {}
13
16
 
17
+ /** Register a hook engine for pre/post tool-use lifecycle events. */
18
+ setHookEngine(hooks: HookEngine): void {
19
+ this.hookEngine = hooks
20
+ }
21
+
22
+ /** Wire LLM-based conversation summarization into the context manager. */
23
+ setupContextSummarizer(): void {
24
+ this.context.setSummarizer(async (messages, heading) => {
25
+ const text = messages
26
+ .map((m) => {
27
+ const role = m.role
28
+ const content = typeof m.content === 'string' ? m.content : JSON.stringify(m.content)
29
+ return `[${role}]: ${content.slice(0, 500)}`
30
+ })
31
+ .join('\n')
32
+
33
+ // 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}`
36
+
37
+ // Collect full summary text from streaming response
38
+ let summary = ''
39
+ try {
40
+ for await (const chunk of this.registry.chat({
41
+ model: this.registry.getActiveModel(),
42
+ messages: [
43
+ { role: 'system', content: summaryPrompt },
44
+ { role: 'user', content: text },
45
+ ],
46
+ maxTokens: 300,
47
+ })) {
48
+ if (chunk.type === 'text' && chunk.content) {
49
+ summary += chunk.content
50
+ }
51
+ if (chunk.type === 'stop') break
52
+ if (chunk.type === 'error') break
53
+ }
54
+ } catch {
55
+ // Return a minimal summary on failure
56
+ }
57
+
58
+ return summary.slice(0, 2000) || 'Prior conversation context omitted.'
59
+ })
60
+ }
61
+
14
62
  getPermission(): PermissionSystem {
15
63
  return this.permission
16
64
  }
17
65
 
18
- async *process(userInput: string): AsyncGenerator<StreamChunk> {
66
+ async *process(userInput: string, signal?: AbortSignal): AsyncGenerator<StreamChunk> {
19
67
  // Add user message to context
20
68
  this.context.addMessage({ role: 'user', content: userInput })
21
69
 
@@ -32,11 +80,13 @@ export class QueryEngine {
32
80
  const toolUses: Array<{ id: string; name: string; input: Record<string, unknown> }> = []
33
81
 
34
82
  // Stream model response
83
+ try {
35
84
  for await (const chunk of this.registry.chat({
36
85
  model: this.registry.getActiveModel(),
37
86
  messages,
38
87
  systemPrompt,
39
88
  tools: toolDefs.length > 0 ? toolDefs : undefined,
89
+ signal,
40
90
  })) {
41
91
  yield chunk
42
92
 
@@ -64,6 +114,18 @@ export class QueryEngine {
64
114
  }
65
115
  }
66
116
  }
117
+ } catch (err) {
118
+ if (isAbortError(err)) {
119
+ // User interrupted — keep partial content, stop gracefully
120
+ if (assistantContent) {
121
+ this.context.addMessage({ role: 'assistant', content: assistantContent })
122
+ }
123
+ yield { type: 'stop' }
124
+ return
125
+ }
126
+ yield { type: 'error', error: String(err) }
127
+ return
128
+ }
67
129
 
68
130
  // Execute any tools that were requested
69
131
  for (const toolUse of toolUses) {
@@ -71,7 +133,7 @@ export class QueryEngine {
71
133
  yield {
72
134
  type: 'tool_result',
73
135
  tool_use_id: toolUse.id,
74
- content: result.content,
136
+ content: result.success ? result.content : (result.error || result.content),
75
137
  }
76
138
 
77
139
  // Add tool use + result to context
@@ -87,60 +149,80 @@ export class QueryEngine {
87
149
 
88
150
  // If tools were executed, recursively continue the conversation
89
151
  if (toolUses.length > 0) {
90
- yield* this.continueWithTools()
152
+ yield* this.continueWithTools(signal)
91
153
  }
92
154
  }
93
155
 
94
- private async *continueWithTools(): AsyncGenerator<StreamChunk> {
95
- const systemPrompt = this.context.getSystemPrompt()
96
- const messages = this.context.getMessages()
156
+ private async *continueWithTools(signal?: AbortSignal): AsyncGenerator<StreamChunk> {
157
+ const MAX_TURNS = 10
97
158
  const toolDefs = this.getToolDefinitions()
98
159
 
99
- let assistantContent = ''
100
- const toolUses: Array<{ id: string; name: string; input: Record<string, unknown> }> = []
160
+ for (let turn = 0; turn < MAX_TURNS; turn++) {
161
+ const systemPrompt = this.context.getSystemPrompt()
162
+ const messages = this.context.getMessages()
101
163
 
102
- for await (const chunk of this.registry.chat({
103
- model: this.registry.getActiveModel(),
104
- messages,
105
- systemPrompt,
106
- tools: toolDefs.length > 0 ? toolDefs : undefined,
107
- })) {
108
- yield chunk
164
+ let assistantContent = ''
165
+ const toolUses: Array<{ id: string; name: string; input: Record<string, unknown> }> = []
109
166
 
110
- if (chunk.type === 'error') return
111
- if (chunk.type === 'text' && chunk.content) assistantContent += chunk.content
167
+ 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
112
176
 
113
- if (chunk.type === 'tool_use' && chunk.toolUse) {
114
- toolUses.push({
115
- id: chunk.toolUse.id,
116
- name: chunk.toolUse.name,
117
- input: chunk.toolUse.input,
118
- })
119
- }
120
- }
177
+ if (chunk.type === 'error') return
178
+ if (chunk.type === 'text' && chunk.content) assistantContent += chunk.content
121
179
 
122
- if (assistantContent) {
123
- this.context.addMessage({ role: 'assistant', content: assistantContent })
124
- }
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
+ })
186
+ }
187
+ }
188
+ } catch (err) {
189
+ if (isAbortError(err)) {
190
+ if (assistantContent) {
191
+ this.context.addMessage({ role: 'assistant', content: assistantContent })
192
+ }
193
+ yield { type: 'stop' }
194
+ return
195
+ }
196
+ return
197
+ }
125
198
 
126
- // Execute tools (max 1 level of recursion to prevent infinite loops)
127
- for (const toolUse of toolUses) {
128
- const result = await this.executeTool(toolUse.name, toolUse.input)
129
- yield {
130
- type: 'tool_result',
131
- tool_use_id: toolUse.id,
132
- content: result.content,
199
+ if (assistantContent) {
200
+ this.context.addMessage({ role: 'assistant', content: assistantContent })
133
201
  }
134
202
 
135
- this.context.addMessage({
136
- role: 'assistant',
137
- content: [{ type: 'tool_use', id: toolUse.id, name: toolUse.name, input: toolUse.input }],
138
- })
139
- this.context.addMessage({
140
- role: 'user',
141
- content: [{ type: 'tool_result', tool_use_id: toolUse.id, content: result.content }],
142
- })
203
+ // No more tool calls — conversation complete
204
+ if (toolUses.length === 0) return
205
+
206
+ // Execute tools and feed results back to the model for the next turn
207
+ for (const toolUse of toolUses) {
208
+ const result = await this.executeTool(toolUse.name, toolUse.input)
209
+ yield {
210
+ type: 'tool_result',
211
+ tool_use_id: toolUse.id,
212
+ content: result.content,
213
+ }
214
+
215
+ this.context.addMessage({
216
+ role: 'assistant',
217
+ content: [{ type: 'tool_use', id: toolUse.id, name: toolUse.name, input: toolUse.input }],
218
+ })
219
+ this.context.addMessage({
220
+ role: 'user',
221
+ content: [{ type: 'tool_result', tool_use_id: toolUse.id, content: result.content }],
222
+ })
223
+ }
143
224
  }
225
+ // Max turns reached — safety limit, stop gracefully
144
226
  }
145
227
 
146
228
  private async executeTool(name: string, params: Record<string, unknown>): Promise<ToolResult> {
@@ -158,13 +240,40 @@ export class QueryEngine {
158
240
  }
159
241
  }
160
242
 
243
+ // Run PreToolUse hooks
244
+ let effectiveParams = params
245
+ if (this.hookEngine) {
246
+ const preResult = await this.hookEngine.executePreToolUse(
247
+ name,
248
+ params,
249
+ 'session-1',
250
+ )
251
+ if (!preResult.allowed) {
252
+ return {
253
+ success: false,
254
+ content: '',
255
+ error: preResult.reason || `Tool "${name}" blocked by hook`,
256
+ }
257
+ }
258
+ if (preResult.modifiedInput) {
259
+ effectiveParams = { ...params, ...preResult.modifiedInput }
260
+ }
261
+ }
262
+
161
263
  try {
162
- return await tool.execute(params, {
264
+ const result = await tool.execute(effectiveParams, {
163
265
  cwd: process.cwd(),
164
266
  sessionId: 'session-1',
165
267
  provider: this.registry.getActive().config.id,
166
268
  model: this.registry.getActiveModel(),
167
269
  })
270
+
271
+ // Run PostToolUse hooks
272
+ if (this.hookEngine) {
273
+ await this.hookEngine.executePostToolUse(name, effectiveParams, result, 'session-1')
274
+ }
275
+
276
+ return result
168
277
  } catch (err) {
169
278
  return { success: false, content: '', error: String(err) }
170
279
  }
@@ -191,3 +300,9 @@ export class QueryEngine {
191
300
  this.registry.switchProvider(providerId, modelId)
192
301
  }
193
302
  }
303
+
304
+ function isAbortError(err: unknown): boolean {
305
+ if (err instanceof Error && err.name === 'AbortError') return true
306
+ if (typeof DOMException !== 'undefined' && err instanceof DOMException && err.name === 'AbortError') return true
307
+ return false
308
+ }