@miphamai/cli 0.2.3

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 (67) hide show
  1. package/README.md +76 -0
  2. package/assets/icon.icns +0 -0
  3. package/assets/icon.jpg +0 -0
  4. package/bin/mipham +51 -0
  5. package/bin/mipham.ts +8 -0
  6. package/package.json +59 -0
  7. package/skills/mipham/om-model-optimize.mipham-skill.md +20 -0
  8. package/skills/mipham/om-security.mipham-skill.md +21 -0
  9. package/skills/standard/code-review.SKILL.md +116 -0
  10. package/skills/standard/compassionate-communication.SKILL.md +80 -0
  11. package/skills/standard/doc-generator.SKILL.md +21 -0
  12. package/skills/standard/github-ops.SKILL.md +22 -0
  13. package/skills/standard/memory.SKILL.md +22 -0
  14. package/skills/standard/mipham-code-setup.SKILL.md +113 -0
  15. package/skills/standard/security-review.SKILL.md +122 -0
  16. package/skills/standard/self-review.SKILL.md +20 -0
  17. package/skills/standard/superpower.SKILL.md +19 -0
  18. package/skills/standard/tdd.SKILL.md +20 -0
  19. package/skills/standard/web-search.SKILL.md +23 -0
  20. package/src/agent/sub-agent.ts +122 -0
  21. package/src/config/defaults.ts +10 -0
  22. package/src/config/loader.ts +30 -0
  23. package/src/core/context.ts +135 -0
  24. package/src/core/engine.ts +193 -0
  25. package/src/core/hooks.ts +116 -0
  26. package/src/core/instructions.ts +126 -0
  27. package/src/core/permission.ts +54 -0
  28. package/src/core/session-store.ts +136 -0
  29. package/src/index.tsx +93 -0
  30. package/src/mcp/client.ts +170 -0
  31. package/src/mcp/protocol.ts +104 -0
  32. package/src/mcp/transport.ts +169 -0
  33. package/src/mcp/types.ts +112 -0
  34. package/src/providers/anthropic.ts +286 -0
  35. package/src/providers/bootstrap.ts +28 -0
  36. package/src/providers/openai-compat.ts +158 -0
  37. package/src/providers/registry.ts +70 -0
  38. package/src/security/path.ts +90 -0
  39. package/src/security/url.ts +96 -0
  40. package/src/shared/constants.ts +368 -0
  41. package/src/shared/index.ts +2 -0
  42. package/src/shared/types.ts +161 -0
  43. package/src/skills/loader.ts +109 -0
  44. package/src/skills/mipham/runtime.ts +63 -0
  45. package/src/skills/standard/runtime.ts +59 -0
  46. package/src/tools/agent/agent.ts +51 -0
  47. package/src/tools/agent/memory.ts +51 -0
  48. package/src/tools/agent/plan.ts +87 -0
  49. package/src/tools/agent/skill.ts +58 -0
  50. package/src/tools/exec/bash.ts +111 -0
  51. package/src/tools/exec/git.ts +63 -0
  52. package/src/tools/exec/task.ts +69 -0
  53. package/src/tools/file/edit.ts +57 -0
  54. package/src/tools/file/glob.ts +29 -0
  55. package/src/tools/file/grep.ts +52 -0
  56. package/src/tools/file/read.ts +38 -0
  57. package/src/tools/file/write.ts +27 -0
  58. package/src/tools/index.ts +126 -0
  59. package/src/tools/network/web-fetch.ts +61 -0
  60. package/src/tools/network/web-search.ts +32 -0
  61. package/src/tools/system/config.ts +68 -0
  62. package/src/tools/system/mcp.ts +75 -0
  63. package/src/ui/app.tsx +317 -0
  64. package/src/ui/chat.tsx +76 -0
  65. package/src/ui/commands.ts +2232 -0
  66. package/src/ui/input.tsx +83 -0
  67. package/src/ui/picker.tsx +209 -0
@@ -0,0 +1,122 @@
1
+ ---
2
+ name: security-review
3
+ description: Security audit skill — vulnerability scanning, OWASP Top 10, secrets detection, supply chain analysis, and compliance checking
4
+ version: 1.0.0
5
+ ---
6
+
7
+ # Security Review
8
+
9
+ Comprehensive security audit for codebases. Covers vulnerability detection, compliance, and hardening recommendations.
10
+
11
+ ## Audit Checklist
12
+
13
+ ### 1. Secrets & Credentials
14
+
15
+ - [ ] No hardcoded API keys, tokens, or passwords in source files
16
+ - [ ] `.env` and `*.pem` files in `.gitignore`
17
+ - [ ] API keys use environment variables or secret managers
18
+ - [ ] No credentials in git history (check `git log -p`)
19
+ - [ ] CI/CD secrets stored securely (not in workflow files)
20
+
21
+ ### 2. OWASP Top 10
22
+
23
+ - [ ] **Injection**: SQL, NoSQL, OS command, LDAP injection points
24
+ - [ ] **Broken Authentication**: Weak password policies, missing MFA
25
+ - [ ] **Sensitive Data Exposure**: Unencrypted PII, missing TLS
26
+ - [ ] **XXE**: XML external entity processing
27
+ - [ ] **Broken Access Control**: Missing authorization checks
28
+ - [ ] **Security Misconfiguration**: Default credentials, verbose errors
29
+ - [ ] **XSS**: Reflected, stored, DOM-based cross-site scripting
30
+ - [ ] **Insecure Deserialization**: Untrusted data deserialization
31
+ - [ ] **Using Vulnerable Components**: Outdated dependencies with CVEs
32
+ - [ ] **Insufficient Logging**: Missing audit trails for auth events
33
+
34
+ ### 3. Supply Chain
35
+
36
+ - [ ] All dependencies have known licenses (no copyleft/GPL)
37
+ - [ ] No dependencies with critical CVEs
38
+ - [ ] Lock files committed (pnpm-lock.yaml, package-lock.json)
39
+ - [ ] Dependency update policy in place
40
+ - [ ] SBOM (Software Bill of Materials) available
41
+
42
+ ### 4. Network & API Security
43
+
44
+ - [ ] TLS 1.3 enforced for all external communications
45
+ - [ ] API endpoints have rate limiting
46
+ - [ ] CORS configured with explicit origins (not `*`)
47
+ - [ ] SSRF protections in place (URL validation, IP filtering)
48
+ - [ ] WebSocket connections use WSS
49
+ - [ ] GraphQL endpoints have query depth limits
50
+
51
+ ### 5. File System & Path Security
52
+
53
+ - [ ] Path traversal protections (no `../../../etc/passwd`)
54
+ - [ ] File upload validation (type, size, content inspection)
55
+ - [ ] Symlink attacks prevented
56
+ - [ ] Sensitive directories blocked (`/etc`, `/proc`, `/sys`)
57
+ - [ ] Temporary files cleaned up after use
58
+
59
+ ### 6. Code-Level Security
60
+
61
+ - [ ] No `eval()` or `Function()` with user input
62
+ - [ ] No `child_process.exec()` with unsanitized input
63
+ - [ ] Regex patterns safe from ReDoS
64
+ - [ ] Prototype pollution prevented
65
+ - [ ] No `dangerouslySetInnerHTML` without sanitization (React)
66
+ - [ ] SQL queries use parameterized statements
67
+
68
+ ### 7. Authentication & Sessions
69
+
70
+ - [ ] Passwords hashed with bcrypt/argon2 (not MD5/SHA1)
71
+ - [ ] Session tokens use `httpOnly`, `secure`, `SameSite=Strict`
72
+ - [ ] JWT tokens have reasonable expiration
73
+ - [ ] Account lockout after failed attempts
74
+ - [ ] Password reset tokens expire and are single-use
75
+
76
+ ### 8. Data Protection
77
+
78
+ - [ ] PII data encrypted at rest (AES-256-GCM)
79
+ - [ ] Data encrypted in transit (TLS 1.3)
80
+ - [ ] Logs do not contain sensitive data
81
+ - [ ] Database backups encrypted
82
+ - [ ] Data retention policies defined
83
+
84
+ ### 9. Infrastructure
85
+
86
+ - [ ] Infrastructure as Code (Terraform/Pulumi) used
87
+ - [ ] Cloud resources not publicly exposed unless intended
88
+ - [ ] Security groups / firewalls restrict inbound traffic
89
+ - [ ] Container images scanned for vulnerabilities
90
+ - [ ] Kubernetes pods run as non-root
91
+
92
+ ### 10. Logging & Monitoring
93
+
94
+ - [ ] Authentication events logged
95
+ - [ ] Failed access attempts logged and alerted
96
+ - [ ] Structured logging format (JSON)
97
+ - [ ] No PII in log messages
98
+ - [ ] Alert thresholds configured for critical events
99
+
100
+ ## Report Format
101
+
102
+ ```
103
+ Security Review Report
104
+ ======================
105
+ Date: YYYY-MM-DD
106
+ Severity: Critical | High | Medium | Low
107
+
108
+ Finding #N: [Title]
109
+ Severity: Critical/High/Medium/Low
110
+ Location: file:line
111
+ Description: [What was found]
112
+ Risk: [What could happen]
113
+ Fix: [How to resolve]
114
+ ```
115
+
116
+ ## Compliance Standards
117
+
118
+ - OWASP ASVS Level 2
119
+ - PCI DSS (if handling payment data)
120
+ - GDPR (if handling EU personal data)
121
+ - SOC 2 Type II
122
+ - ISO 27001
@@ -0,0 +1,20 @@
1
+ ---
2
+ name: self-review
3
+ description: Review the current diff for correctness bugs and reuse/simplification efficiency cleanups
4
+ version: 1.0.0
5
+ ---
6
+
7
+ # Self Review Skill
8
+
9
+ Review changed code for reuse, simplification, efficiency, and altitude cleanups. Quality only — it does not hunt for bugs.
10
+
11
+ ## Review Focus
12
+
13
+ - Code reuse opportunities
14
+ - Simplification: reduce complexity
15
+ - Efficiency: performance improvements
16
+ - Altitude: architectural alignment
17
+
18
+ ## Output
19
+
20
+ Apply fixes where appropriate. Explain reasoning for each change.
@@ -0,0 +1,19 @@
1
+ ---
2
+ name: superpower
3
+ description: Use when starting any conversation — establishes how to find and use skills
4
+ version: 1.0.0
5
+ ---
6
+
7
+ # Superpowers Skill
8
+
9
+ ## Instruction Priority
10
+
11
+ User's explicit instructions always take precedence.
12
+
13
+ ## How to Access Skills
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.
16
+
17
+ ## The Rule
18
+
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.
@@ -0,0 +1,20 @@
1
+ ---
2
+ name: tdd
3
+ description: Test-Driven Development workflow — red, green, refactor
4
+ version: 1.0.0
5
+ ---
6
+
7
+ # TDD Skill
8
+
9
+ ## Workflow
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
14
+
15
+ ## Rules
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
@@ -0,0 +1,23 @@
1
+ ---
2
+ name: web-search
3
+ description: Search the web for current information, documentation, and news
4
+ version: 1.0.0
5
+ ---
6
+
7
+ # Web Search Skill
8
+
9
+ Use WebSearch tool to find current information.
10
+
11
+ ## When to Use
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
17
+
18
+ ## Best Practices
19
+
20
+ - Be specific with search queries
21
+ - Use allowed_domains to focus results
22
+ - Verify information from multiple sources
23
+ - Cite sources in responses
@@ -0,0 +1,122 @@
1
+ import { ContextManager } from '../core/context'
2
+ import type { ProviderRegistry } from '../providers/registry'
3
+ import type { Message } from '../shared/types'
4
+
5
+ export type SubAgentType = 'general' | 'explore' | 'plan' | 'code-review'
6
+
7
+ interface SubAgentOptions {
8
+ type?: SubAgentType
9
+ systemPrompt?: string
10
+ maxContextMessages?: number
11
+ }
12
+
13
+ const TYPE_SYSTEM_PROMPTS: Record<SubAgentType, string> = {
14
+ general: 'You are a focused sub-agent. Complete the assigned task thoroughly and return results.',
15
+ explore:
16
+ 'You are an exploration sub-agent. Search, read, and analyze code. Return structured findings with file paths and line numbers.',
17
+ plan: 'You are a planning sub-agent. Design implementation approaches. Return a step-by-step plan with files to modify.',
18
+ 'code-review':
19
+ 'You are a code review sub-agent. Find bugs, security issues, and code quality problems. Return findings by severity.',
20
+ }
21
+
22
+ /**
23
+ * Sub-agent engine — creates an isolated conversation context and processes
24
+ * a single prompt independently. Returns the consolidated result.
25
+ *
26
+ * Phase 7: Pipeline-ready. Without a real AI provider, returns a structured
27
+ * analysis of the prompt. Full AI integration in M3.
28
+ */
29
+ export class SubAgent {
30
+ constructor(private registry: ProviderRegistry) {}
31
+
32
+ async execute(
33
+ prompt: string,
34
+ description: string,
35
+ options: SubAgentOptions = {},
36
+ ): Promise<string> {
37
+ const type = options.type || 'general'
38
+ const systemPrompt = options.systemPrompt || TYPE_SYSTEM_PROMPTS[type]
39
+
40
+ // Create isolated context for this sub-agent
41
+ const context = new ContextManager({
42
+ maxTokens: 100_000,
43
+ compactionThreshold: 0.9,
44
+ })
45
+ context.setSystemPrompt(systemPrompt)
46
+ context.addMessage({ role: 'user', content: prompt })
47
+
48
+ // Phase 7 pipeline: attempt real AI, fall back to structured simulation
49
+ try {
50
+ const provider = this.registry.getActive()
51
+ if (provider) {
52
+ const messages = context.getMessages()
53
+ const chunks: string[] = []
54
+
55
+ for await (const chunk of provider.chat({
56
+ model: this.registry.getActiveModel(),
57
+ messages,
58
+ systemPrompt,
59
+ maxTokens: 4096,
60
+ })) {
61
+ if (chunk.type === 'text' && chunk.content) {
62
+ chunks.push(chunk.content)
63
+ }
64
+ if (chunk.type === 'error') {
65
+ // Fall through to simulation on API error
66
+ throw new Error(chunk.error)
67
+ }
68
+ }
69
+
70
+ if (chunks.length > 0) {
71
+ return chunks.join('')
72
+ }
73
+ }
74
+ } catch {
75
+ // API unavailable — use simulation mode
76
+ }
77
+
78
+ // Simulation mode: return structured task analysis
79
+ return this.simulate(prompt, description, type)
80
+ }
81
+
82
+ private simulate(prompt: string, description: string, type: SubAgentType): string {
83
+ const lines: string[] = [
84
+ `── Sub-Agent Result (${type}) ──`,
85
+ '',
86
+ `Task: ${description}`,
87
+ `Prompt: ${prompt.slice(0, 200)}${prompt.length > 200 ? '...' : ''}`,
88
+ '',
89
+ ]
90
+
91
+ switch (type) {
92
+ case 'explore':
93
+ lines.push(
94
+ 'Analysis: This exploration task would search the codebase for relevant',
95
+ 'files, patterns, and structures. The sub-agent pipeline is ready — connect',
96
+ 'a provider API key for real AI-powered exploration.',
97
+ )
98
+ break
99
+ case 'plan':
100
+ lines.push(
101
+ 'Plan: A step-by-step implementation plan would be generated based on',
102
+ 'the requirements. Use /plan mode for interactive planning.',
103
+ )
104
+ break
105
+ case 'code-review':
106
+ lines.push(
107
+ 'Review: The code review sub-agent would analyze the specified files for',
108
+ 'bugs, security issues, and code quality concerns. Use /review for',
109
+ 'interactive code review.',
110
+ )
111
+ break
112
+ default:
113
+ lines.push(
114
+ 'The sub-agent pipeline is operational. Connect an API key to enable',
115
+ 'AI-powered sub-agent execution. The infrastructure for context isolation,',
116
+ 'prompt routing, and result aggregation is in place.',
117
+ )
118
+ }
119
+
120
+ return lines.join('\n')
121
+ }
122
+ }
@@ -0,0 +1,10 @@
1
+ import type { MiphamConfig } from '../shared/index.ts'
2
+ import { DEFAULT_PROVIDERS } from '../shared/index.ts'
3
+
4
+ export const DEFAULT_CONFIG: MiphamConfig = {
5
+ version: '0.1.0',
6
+ defaultProvider: 'anthropic',
7
+ defaultModel: 'claude-sonnet-4-6',
8
+ permission: 'auto',
9
+ providers: DEFAULT_PROVIDERS,
10
+ }
@@ -0,0 +1,30 @@
1
+ import { readFileSync, existsSync } from 'node:fs'
2
+ import { join } from 'node:path'
3
+ import { parse as parseYaml } from 'yaml'
4
+ import type { MiphamConfig } from '../shared/index.ts'
5
+ import { DEFAULT_CONFIG } from './defaults'
6
+
7
+ export function loadConfig(cwd: string = process.cwd()): MiphamConfig {
8
+ const configPath = join(cwd, '.mipham', 'config.yml')
9
+ const userConfigPath = join(process.env.HOME || '~', '.mipham', 'config.yml')
10
+
11
+ let config = { ...DEFAULT_CONFIG }
12
+
13
+ if (existsSync(configPath)) {
14
+ const raw = readFileSync(configPath, 'utf-8')
15
+ const projectConfig = parseYaml(raw) as Partial<MiphamConfig>
16
+ config = mergeConfig(config, projectConfig)
17
+ }
18
+
19
+ if (existsSync(userConfigPath)) {
20
+ const raw = readFileSync(userConfigPath, 'utf-8')
21
+ const userConfig = parseYaml(raw) as Partial<MiphamConfig>
22
+ config = mergeConfig(config, userConfig)
23
+ }
24
+
25
+ return config
26
+ }
27
+
28
+ function mergeConfig(base: MiphamConfig, override: Partial<MiphamConfig>): MiphamConfig {
29
+ return { ...base, ...override, providers: override.providers ?? base.providers }
30
+ }
@@ -0,0 +1,135 @@
1
+ import type { Message } from '../shared/index.ts'
2
+
3
+ interface ContextConfig {
4
+ maxTokens: number
5
+ compactionThreshold: number // e.g. 0.9 → compact at 90% usage
6
+ }
7
+
8
+ interface Checkpoint {
9
+ id: number
10
+ messages: Message[]
11
+ estimatedTokens: number
12
+ timestamp: Date
13
+ label: string
14
+ }
15
+
16
+ export class ContextManager {
17
+ private messages: Message[] = []
18
+ private systemPrompt = ''
19
+ private estimatedTokens = 0
20
+ private checkpoints: Checkpoint[] = []
21
+ private checkpointCounter = 0
22
+
23
+ constructor(private config: ContextConfig) {}
24
+
25
+ setSystemPrompt(prompt: string): void {
26
+ this.systemPrompt = prompt
27
+ this.estimatedTokens = this.estimateTokens(prompt)
28
+ }
29
+
30
+ getSystemPrompt(): string {
31
+ return this.systemPrompt
32
+ }
33
+
34
+ addMessage(msg: Message): void {
35
+ this.messages.push(msg)
36
+ this.estimatedTokens += this.estimateTokens(
37
+ typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content),
38
+ )
39
+ }
40
+
41
+ getMessages(): Message[] {
42
+ return [...this.messages]
43
+ }
44
+
45
+ needsCompaction(): boolean {
46
+ return this.estimatedTokens > this.config.maxTokens * this.config.compactionThreshold
47
+ }
48
+
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
53
+ this.messages = this.messages.slice(-keep)
54
+
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
+ }
62
+ }
63
+ }
64
+
65
+ getEstimatedTokens(): number {
66
+ return this.estimatedTokens
67
+ }
68
+
69
+ clear(): void {
70
+ this.messages = []
71
+ this.checkpoints = []
72
+ this.checkpointCounter = 0
73
+ this.estimatedTokens = this.estimateTokens(this.systemPrompt)
74
+ }
75
+
76
+ getMessageCount(): number {
77
+ return this.messages.length
78
+ }
79
+
80
+ // ── Checkpoint / Rewind ──
81
+
82
+ saveCheckpoint(label = 'auto'): number {
83
+ this.checkpointCounter++
84
+ const checkpoint: Checkpoint = {
85
+ id: this.checkpointCounter,
86
+ messages: structuredClone(this.messages),
87
+ estimatedTokens: this.estimatedTokens,
88
+ timestamp: new Date(),
89
+ label,
90
+ }
91
+ this.checkpoints.push(checkpoint)
92
+ // Keep only last 10 checkpoints
93
+ if (this.checkpoints.length > 10) {
94
+ this.checkpoints = this.checkpoints.slice(-10)
95
+ }
96
+ return checkpoint.id
97
+ }
98
+
99
+ restoreCheckpoint(checkpointId?: number): {
100
+ restored: boolean
101
+ messageCount: number
102
+ label: string
103
+ } {
104
+ // If no id given, restore the most recent checkpoint
105
+ const target = checkpointId
106
+ ? this.checkpoints.find((cp) => cp.id === checkpointId)
107
+ : this.checkpoints.at(-1)
108
+
109
+ if (!target) {
110
+ return { restored: false, messageCount: this.messages.length, label: '' }
111
+ }
112
+
113
+ this.messages = structuredClone(target.messages)
114
+ this.estimatedTokens = target.estimatedTokens
115
+ return { restored: true, messageCount: this.messages.length, label: target.label }
116
+ }
117
+
118
+ getCheckpoints(): Array<{ id: number; messageCount: number; timestamp: Date; label: string }> {
119
+ return this.checkpoints.map((cp) => ({
120
+ id: cp.id,
121
+ messageCount: cp.messages.length,
122
+ timestamp: cp.timestamp,
123
+ label: cp.label,
124
+ }))
125
+ }
126
+
127
+ getLastCheckpointId(): number | undefined {
128
+ return this.checkpoints.at(-1)?.id
129
+ }
130
+
131
+ private estimateTokens(text: string): number {
132
+ // Simple estimation: ~4 chars per token
133
+ return Math.ceil(text.length / 4)
134
+ }
135
+ }
@@ -0,0 +1,193 @@
1
+ import type { Message, StreamChunk, ToolDefinition, ToolResult } from '../shared/index.ts'
2
+ import { ProviderRegistry } from '../providers/registry'
3
+ import { ContextManager } from './context'
4
+ import { PermissionSystem } from './permission'
5
+
6
+ export class QueryEngine {
7
+ constructor(
8
+ private registry: ProviderRegistry,
9
+ private context: ContextManager,
10
+ private tools: Map<string, ToolDefinition>,
11
+ private permission: PermissionSystem = new PermissionSystem('bypass'),
12
+ ) {}
13
+
14
+ getPermission(): PermissionSystem {
15
+ return this.permission
16
+ }
17
+
18
+ async *process(userInput: string): AsyncGenerator<StreamChunk> {
19
+ // Add user message to context
20
+ this.context.addMessage({ role: 'user', content: userInput })
21
+
22
+ // Check compaction before processing
23
+ if (this.context.needsCompaction()) {
24
+ await this.context.compact('conversation summary')
25
+ }
26
+
27
+ const systemPrompt = this.context.getSystemPrompt()
28
+ const messages = this.context.getMessages()
29
+ const toolDefs = this.getToolDefinitions()
30
+
31
+ let assistantContent = ''
32
+ const toolUses: Array<{ id: string; name: string; input: Record<string, unknown> }> = []
33
+
34
+ // Stream model response
35
+ for await (const chunk of this.registry.chat({
36
+ model: this.registry.getActiveModel(),
37
+ messages,
38
+ systemPrompt,
39
+ tools: toolDefs.length > 0 ? toolDefs : undefined,
40
+ })) {
41
+ yield chunk
42
+
43
+ if (chunk.type === 'error') {
44
+ this.context.addMessage({ role: 'assistant', content: `Error: ${chunk.error}` })
45
+ return
46
+ }
47
+
48
+ if (chunk.type === 'text' && chunk.content) {
49
+ assistantContent += chunk.content
50
+ }
51
+
52
+ if (chunk.type === 'tool_use' && chunk.toolUse) {
53
+ toolUses.push({
54
+ id: chunk.toolUse.id,
55
+ name: chunk.toolUse.name,
56
+ input: chunk.toolUse.input,
57
+ })
58
+ }
59
+
60
+ if (chunk.type === 'stop') {
61
+ // Add assistant response to context
62
+ if (assistantContent) {
63
+ this.context.addMessage({ role: 'assistant', content: assistantContent })
64
+ }
65
+ }
66
+ }
67
+
68
+ // Execute any tools that were requested
69
+ for (const toolUse of toolUses) {
70
+ const result = await this.executeTool(toolUse.name, toolUse.input)
71
+ yield {
72
+ type: 'tool_result',
73
+ tool_use_id: toolUse.id,
74
+ content: result.content,
75
+ }
76
+
77
+ // Add tool use + result to context
78
+ this.context.addMessage({
79
+ role: 'assistant',
80
+ content: [{ type: 'tool_use', id: toolUse.id, name: toolUse.name, input: toolUse.input }],
81
+ })
82
+ this.context.addMessage({
83
+ role: 'user',
84
+ content: [{ type: 'tool_result', tool_use_id: toolUse.id, content: result.content }],
85
+ })
86
+ }
87
+
88
+ // If tools were executed, recursively continue the conversation
89
+ if (toolUses.length > 0) {
90
+ yield* this.continueWithTools()
91
+ }
92
+ }
93
+
94
+ private async *continueWithTools(): AsyncGenerator<StreamChunk> {
95
+ const systemPrompt = this.context.getSystemPrompt()
96
+ const messages = this.context.getMessages()
97
+ const toolDefs = this.getToolDefinitions()
98
+
99
+ let assistantContent = ''
100
+ const toolUses: Array<{ id: string; name: string; input: Record<string, unknown> }> = []
101
+
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
109
+
110
+ if (chunk.type === 'error') return
111
+ if (chunk.type === 'text' && chunk.content) assistantContent += chunk.content
112
+
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
+ }
121
+
122
+ if (assistantContent) {
123
+ this.context.addMessage({ role: 'assistant', content: assistantContent })
124
+ }
125
+
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,
133
+ }
134
+
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
+ })
143
+ }
144
+ }
145
+
146
+ private async executeTool(name: string, params: Record<string, unknown>): Promise<ToolResult> {
147
+ const tool = this.tools.get(name)
148
+ if (!tool) {
149
+ return { success: false, content: '', error: `Unknown tool: ${name}` }
150
+ }
151
+
152
+ // Security: check permission before executing
153
+ if (this.permission.needsApproval(tool, params)) {
154
+ return {
155
+ success: false,
156
+ content: '',
157
+ error: `Tool "${name}" requires user approval (permission: ask). The tool was not executed.`,
158
+ }
159
+ }
160
+
161
+ try {
162
+ return await tool.execute(params, {
163
+ cwd: process.cwd(),
164
+ sessionId: 'session-1',
165
+ provider: this.registry.getActive().config.id,
166
+ model: this.registry.getActiveModel(),
167
+ })
168
+ } catch (err) {
169
+ return { success: false, content: '', error: String(err) }
170
+ }
171
+ }
172
+
173
+ private getToolDefinitions(): Record<string, unknown>[] {
174
+ return Array.from(this.tools.values()).map((t) => ({
175
+ name: t.name,
176
+ description: t.description,
177
+ parameters: t.parameters,
178
+ input_schema: t.parameters, // Anthropic-style naming
179
+ }))
180
+ }
181
+
182
+ getContext(): ContextManager {
183
+ return this.context
184
+ }
185
+
186
+ getTools(): Map<string, ToolDefinition> {
187
+ return this.tools
188
+ }
189
+
190
+ switchProvider(providerId: string, modelId?: string): void {
191
+ this.registry.switchProvider(providerId, modelId)
192
+ }
193
+ }