@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.
- package/README.md +76 -0
- package/assets/icon.icns +0 -0
- package/assets/icon.jpg +0 -0
- package/bin/mipham +51 -0
- package/bin/mipham.ts +8 -0
- package/package.json +59 -0
- package/skills/mipham/om-model-optimize.mipham-skill.md +20 -0
- package/skills/mipham/om-security.mipham-skill.md +21 -0
- package/skills/standard/code-review.SKILL.md +116 -0
- package/skills/standard/compassionate-communication.SKILL.md +80 -0
- package/skills/standard/doc-generator.SKILL.md +21 -0
- package/skills/standard/github-ops.SKILL.md +22 -0
- package/skills/standard/memory.SKILL.md +22 -0
- package/skills/standard/mipham-code-setup.SKILL.md +113 -0
- package/skills/standard/security-review.SKILL.md +122 -0
- package/skills/standard/self-review.SKILL.md +20 -0
- package/skills/standard/superpower.SKILL.md +19 -0
- package/skills/standard/tdd.SKILL.md +20 -0
- package/skills/standard/web-search.SKILL.md +23 -0
- package/src/agent/sub-agent.ts +122 -0
- package/src/config/defaults.ts +10 -0
- package/src/config/loader.ts +30 -0
- package/src/core/context.ts +135 -0
- package/src/core/engine.ts +193 -0
- package/src/core/hooks.ts +116 -0
- package/src/core/instructions.ts +126 -0
- package/src/core/permission.ts +54 -0
- package/src/core/session-store.ts +136 -0
- package/src/index.tsx +93 -0
- package/src/mcp/client.ts +170 -0
- package/src/mcp/protocol.ts +104 -0
- package/src/mcp/transport.ts +169 -0
- package/src/mcp/types.ts +112 -0
- package/src/providers/anthropic.ts +286 -0
- package/src/providers/bootstrap.ts +28 -0
- package/src/providers/openai-compat.ts +158 -0
- package/src/providers/registry.ts +70 -0
- package/src/security/path.ts +90 -0
- package/src/security/url.ts +96 -0
- package/src/shared/constants.ts +368 -0
- package/src/shared/index.ts +2 -0
- package/src/shared/types.ts +161 -0
- package/src/skills/loader.ts +109 -0
- package/src/skills/mipham/runtime.ts +63 -0
- package/src/skills/standard/runtime.ts +59 -0
- package/src/tools/agent/agent.ts +51 -0
- package/src/tools/agent/memory.ts +51 -0
- package/src/tools/agent/plan.ts +87 -0
- package/src/tools/agent/skill.ts +58 -0
- package/src/tools/exec/bash.ts +111 -0
- package/src/tools/exec/git.ts +63 -0
- package/src/tools/exec/task.ts +69 -0
- package/src/tools/file/edit.ts +57 -0
- package/src/tools/file/glob.ts +29 -0
- package/src/tools/file/grep.ts +52 -0
- package/src/tools/file/read.ts +38 -0
- package/src/tools/file/write.ts +27 -0
- package/src/tools/index.ts +126 -0
- package/src/tools/network/web-fetch.ts +61 -0
- package/src/tools/network/web-search.ts +32 -0
- package/src/tools/system/config.ts +68 -0
- package/src/tools/system/mcp.ts +75 -0
- package/src/ui/app.tsx +317 -0
- package/src/ui/chat.tsx +76 -0
- package/src/ui/commands.ts +2232 -0
- package/src/ui/input.tsx +83 -0
- 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
|
+
}
|