@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,59 @@
|
|
|
1
|
+
import type { SkillDefinition } from '../../shared/index.ts'
|
|
2
|
+
|
|
3
|
+
export interface StandardRuntimeContext {
|
|
4
|
+
skill: SkillDefinition
|
|
5
|
+
cwd: string
|
|
6
|
+
sessionId: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Standard skill runtime — executes SKILL.md definitions.
|
|
11
|
+
* Standard skills follow the open-source SKILL.md specification.
|
|
12
|
+
*/
|
|
13
|
+
export class StandardRuntime {
|
|
14
|
+
private context: StandardRuntimeContext
|
|
15
|
+
|
|
16
|
+
constructor(context: StandardRuntimeContext) {
|
|
17
|
+
this.context = context
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
getSkill(): SkillDefinition {
|
|
21
|
+
return this.context.skill
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
getPrompts(): Record<string, string> {
|
|
25
|
+
return this.context.skill.prompts || {}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
getPrompt(name: string): string | undefined {
|
|
29
|
+
return this.context.skill.prompts?.[name]
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
getTools(): SkillDefinition['tools'] {
|
|
33
|
+
return this.context.skill.tools || []
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
getHooks(): SkillDefinition['hooks'] {
|
|
37
|
+
return this.context.skill.hooks || []
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Execute a named prompt from the skill.
|
|
42
|
+
* Returns the prompt text with any variable substitution applied.
|
|
43
|
+
*/
|
|
44
|
+
async executePrompt(name: string, variables?: Record<string, string>): Promise<string> {
|
|
45
|
+
const prompt = this.getPrompt(name)
|
|
46
|
+
if (!prompt) {
|
|
47
|
+
throw new Error(`Prompt "${name}" not found in skill "${this.context.skill.name}"`)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
let result = prompt
|
|
51
|
+
if (variables) {
|
|
52
|
+
for (const [key, value] of Object.entries(variables)) {
|
|
53
|
+
result = result.replaceAll(`\${${key}}`, value)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return result
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { ToolDefinition } from '../../shared/index.ts'
|
|
2
|
+
import { SubAgent, type SubAgentType } from '../../agent/sub-agent'
|
|
3
|
+
|
|
4
|
+
const VALID_TYPES: SubAgentType[] = ['general', 'explore', 'plan', 'code-review']
|
|
5
|
+
|
|
6
|
+
export const agentTool: ToolDefinition = {
|
|
7
|
+
name: 'Agent',
|
|
8
|
+
description:
|
|
9
|
+
'Launch a sub-agent to handle complex, multi-step tasks independently. Available types: general, explore, plan, code-review.',
|
|
10
|
+
category: 'agent',
|
|
11
|
+
permission: 'ask',
|
|
12
|
+
parameters: {
|
|
13
|
+
type: 'object',
|
|
14
|
+
properties: {
|
|
15
|
+
description: { type: 'string', description: 'Short description of the task' },
|
|
16
|
+
prompt: { type: 'string', description: 'The task for the agent to perform' },
|
|
17
|
+
subagent_type: {
|
|
18
|
+
type: 'string',
|
|
19
|
+
description: 'Type: general (default), explore (code search), plan (design), code-review',
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
required: ['description', 'prompt'],
|
|
23
|
+
},
|
|
24
|
+
async execute(params, ctx) {
|
|
25
|
+
const description = params.description as string
|
|
26
|
+
const prompt = params.prompt as string
|
|
27
|
+
const agentType = (params.subagent_type as SubAgentType) || 'general'
|
|
28
|
+
|
|
29
|
+
if (!VALID_TYPES.includes(agentType)) {
|
|
30
|
+
return {
|
|
31
|
+
success: false,
|
|
32
|
+
content: '',
|
|
33
|
+
error: `Invalid subagent_type "${agentType}". Valid types: ${VALID_TYPES.join(', ')}`,
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const registry = ctx.registry
|
|
38
|
+
if (!registry) {
|
|
39
|
+
// No registry in context — return structured simulation
|
|
40
|
+
const sub = new SubAgent(
|
|
41
|
+
null as unknown as import('../../providers/registry').ProviderRegistry,
|
|
42
|
+
)
|
|
43
|
+
const result = await sub.execute(prompt, description, { type: agentType })
|
|
44
|
+
return { success: true, content: result }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const sub = new SubAgent(registry)
|
|
48
|
+
const result = await sub.execute(prompt, description, { type: agentType })
|
|
49
|
+
return { success: true, content: result }
|
|
50
|
+
},
|
|
51
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync } from 'node:fs'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
import type { ToolDefinition } from '../../shared/index.ts'
|
|
4
|
+
|
|
5
|
+
const MEMORY_DIR = join(process.env.HOME || '~', '.mipham', 'memory')
|
|
6
|
+
|
|
7
|
+
export const memoryTool: ToolDefinition = {
|
|
8
|
+
name: 'Memory',
|
|
9
|
+
description: 'Read and write persistent memory files in ~/.mipham/memory/.',
|
|
10
|
+
category: 'agent',
|
|
11
|
+
permission: 'auto',
|
|
12
|
+
parameters: {
|
|
13
|
+
type: 'object',
|
|
14
|
+
properties: {
|
|
15
|
+
action: {
|
|
16
|
+
type: 'string',
|
|
17
|
+
enum: ['read', 'write', 'list'],
|
|
18
|
+
description: 'Action to perform',
|
|
19
|
+
},
|
|
20
|
+
name: { type: 'string', description: 'Memory file name (slug)' },
|
|
21
|
+
content: { type: 'string', description: 'Content to write (for write action)' },
|
|
22
|
+
},
|
|
23
|
+
required: ['action'],
|
|
24
|
+
},
|
|
25
|
+
async execute(params, _ctx) {
|
|
26
|
+
const action = params.action as string
|
|
27
|
+
mkdirSync(MEMORY_DIR, { recursive: true })
|
|
28
|
+
|
|
29
|
+
if (action === 'list') {
|
|
30
|
+
const files = readdirSync(MEMORY_DIR).filter((f) => f.endsWith('.md'))
|
|
31
|
+
return { success: true, content: files.join('\n') || '(no memories)' }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const name = params.name as string
|
|
35
|
+
if (!name) return { success: false, content: '', error: 'name is required' }
|
|
36
|
+
const filePath = join(MEMORY_DIR, `${name}.md`)
|
|
37
|
+
|
|
38
|
+
if (action === 'read') {
|
|
39
|
+
if (!existsSync(filePath))
|
|
40
|
+
return { success: false, content: '', error: `Memory "${name}" not found` }
|
|
41
|
+
return { success: true, content: readFileSync(filePath, 'utf-8') }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (action === 'write') {
|
|
45
|
+
writeFileSync(filePath, params.content as string, 'utf-8')
|
|
46
|
+
return { success: true, content: `Memory "${name}" written` }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return { success: false, content: '', error: `Unknown action: ${action}` }
|
|
50
|
+
},
|
|
51
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { mkdirSync, writeFileSync } from 'node:fs'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
import type { ToolDefinition } from '../../shared/index.ts'
|
|
4
|
+
|
|
5
|
+
export const planTool: ToolDefinition = {
|
|
6
|
+
name: 'Plan',
|
|
7
|
+
description:
|
|
8
|
+
'Enter plan mode — read-only analysis and design. Creates a structured plan file in .mipham/plans/.',
|
|
9
|
+
category: 'agent',
|
|
10
|
+
permission: 'auto',
|
|
11
|
+
parameters: {
|
|
12
|
+
type: 'object',
|
|
13
|
+
properties: {
|
|
14
|
+
title: {
|
|
15
|
+
type: 'string',
|
|
16
|
+
description: 'Plan title (e.g., "Add user authentication")',
|
|
17
|
+
},
|
|
18
|
+
description: {
|
|
19
|
+
type: 'string',
|
|
20
|
+
description: 'Brief description of what the plan should cover',
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
async execute(params, ctx) {
|
|
25
|
+
const title = (params.title as string) || 'Implementation Plan'
|
|
26
|
+
const description = (params.description as string) || ''
|
|
27
|
+
|
|
28
|
+
const planDir = join(ctx.cwd, '.mipham', 'plans')
|
|
29
|
+
mkdirSync(planDir, { recursive: true })
|
|
30
|
+
|
|
31
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19)
|
|
32
|
+
const filename = `plan-${timestamp}.md`
|
|
33
|
+
const filepath = join(planDir, filename)
|
|
34
|
+
|
|
35
|
+
const content = [
|
|
36
|
+
`# ${title}`,
|
|
37
|
+
'',
|
|
38
|
+
`> Generated: ${new Date().toISOString()}`,
|
|
39
|
+
`> Status: Draft`,
|
|
40
|
+
description ? `> ${description}` : '',
|
|
41
|
+
'',
|
|
42
|
+
'## Overview',
|
|
43
|
+
'',
|
|
44
|
+
'[Describe what this plan aims to achieve]',
|
|
45
|
+
'',
|
|
46
|
+
'## Files to Modify',
|
|
47
|
+
'',
|
|
48
|
+
'| File | Change | Reason |',
|
|
49
|
+
'|------|--------|--------|',
|
|
50
|
+
'| | | |',
|
|
51
|
+
'',
|
|
52
|
+
'## Implementation Steps',
|
|
53
|
+
'',
|
|
54
|
+
'1. ',
|
|
55
|
+
'2. ',
|
|
56
|
+
'3. ',
|
|
57
|
+
'',
|
|
58
|
+
'## Verification',
|
|
59
|
+
'',
|
|
60
|
+
'- [ ] Tests pass',
|
|
61
|
+
'- [ ] Typecheck passes',
|
|
62
|
+
'- [ ] Manual verification',
|
|
63
|
+
'',
|
|
64
|
+
'## Notes',
|
|
65
|
+
'',
|
|
66
|
+
'[Any additional context, risks, or dependencies]',
|
|
67
|
+
].join('\n')
|
|
68
|
+
|
|
69
|
+
writeFileSync(filepath, content, 'utf-8')
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
success: true,
|
|
73
|
+
content: [
|
|
74
|
+
`── Plan Mode Activated ──`,
|
|
75
|
+
``,
|
|
76
|
+
`Plan file: ${filepath}`,
|
|
77
|
+
`Directory: ${planDir}`,
|
|
78
|
+
``,
|
|
79
|
+
`The plan template has been created. Edit ${filename} to fill in the details.`,
|
|
80
|
+
`Use /no-plan to exit plan mode.`,
|
|
81
|
+
``,
|
|
82
|
+
`Plan mode is read-only — the AI will analyze and design without`,
|
|
83
|
+
`executing code changes until you explicitly approve the plan.`,
|
|
84
|
+
].join('\n'),
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { ToolDefinition } from '../../shared/index.ts'
|
|
2
|
+
|
|
3
|
+
export const skillTool: ToolDefinition = {
|
|
4
|
+
name: 'Skill',
|
|
5
|
+
description:
|
|
6
|
+
'Execute a skill (.SKILL.md or .mipham-skill.md) by name. Skills extend AI capabilities with specialized instructions.',
|
|
7
|
+
category: 'agent',
|
|
8
|
+
permission: 'auto',
|
|
9
|
+
parameters: {
|
|
10
|
+
type: 'object',
|
|
11
|
+
properties: {
|
|
12
|
+
skill: { type: 'string', description: 'Name of the skill to invoke' },
|
|
13
|
+
args: { type: 'string', description: 'Optional arguments for the skill' },
|
|
14
|
+
},
|
|
15
|
+
required: ['skill'],
|
|
16
|
+
},
|
|
17
|
+
async execute(params, ctx) {
|
|
18
|
+
const skillName = params.skill as string
|
|
19
|
+
const args = (params.args as string) || ''
|
|
20
|
+
|
|
21
|
+
// Try to load the skill via SkillsLoader if available
|
|
22
|
+
const loader = ctx.skillsLoader
|
|
23
|
+
if (loader) {
|
|
24
|
+
const skill = loader.get(skillName)
|
|
25
|
+
if (!skill) {
|
|
26
|
+
const available = loader
|
|
27
|
+
.list()
|
|
28
|
+
.map((s) => ` • ${s.name} (${s.type})`)
|
|
29
|
+
.join('\n')
|
|
30
|
+
return {
|
|
31
|
+
success: false,
|
|
32
|
+
content: '',
|
|
33
|
+
error: `Skill "${skillName}" not found.\n\nAvailable skills (${loader.list().length}):\n${available}\n\nUse /skills to browse, or create a .SKILL.md file in .mipham/skills/.`,
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Build skill invocation content
|
|
38
|
+
const lines: string[] = [
|
|
39
|
+
`── Skill Invoked: ${skill.name} ──`,
|
|
40
|
+
`Type: ${skill.type} | Version: ${skill.version}`,
|
|
41
|
+
skill.description ? `Description: ${skill.description}` : '',
|
|
42
|
+
args ? `Arguments: ${args}` : '',
|
|
43
|
+
'',
|
|
44
|
+
'The AI should now follow the instructions from this skill.',
|
|
45
|
+
'Skill content has been loaded into context.',
|
|
46
|
+
].filter(Boolean)
|
|
47
|
+
|
|
48
|
+
return { success: true, content: lines.join('\n') }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Fallback: no SkillsLoader in context
|
|
52
|
+
return {
|
|
53
|
+
success: false,
|
|
54
|
+
content: '',
|
|
55
|
+
error: `SkillsLoader not available. The skill "${skillName}" cannot be loaded.\n\nSkills are loaded from:\n • skills/standard/*.SKILL.md (built-in)\n • skills/mipham/*.mipham-skill.md (Mipham exclusive)\n • .mipham/skills/ (project custom)`,
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import type { ToolDefinition } from '../../shared/index.ts'
|
|
2
|
+
|
|
3
|
+
// ── Dangerous command patterns ──
|
|
4
|
+
const BLOCKED_PATTERNS = [
|
|
5
|
+
// Recursive root deletion without preserve-root safeguard
|
|
6
|
+
/\brm\s+-rf\s+\/(\s|$)/,
|
|
7
|
+
/\brm\s+-rf\s+\/\*\s*$/,
|
|
8
|
+
// Filesystem manipulation
|
|
9
|
+
/\bmkfs\./,
|
|
10
|
+
/\bdd\s+if=/,
|
|
11
|
+
// Fork bomb
|
|
12
|
+
/:\s*\(\s*\)\s*\{\s*:\s*\|/,
|
|
13
|
+
// Recursive root chmod
|
|
14
|
+
/\bchmod\s+.*777\s+\//,
|
|
15
|
+
// Direct block device write
|
|
16
|
+
/>\s*\/dev\/sd[a-z]/,
|
|
17
|
+
// SSH private key theft
|
|
18
|
+
/\bcat\s+.*\/\.ssh\/id_/,
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
const BLOCKED_COMMANDS = [
|
|
22
|
+
'mkfs',
|
|
23
|
+
'mkfs.ext2',
|
|
24
|
+
'mkfs.ext3',
|
|
25
|
+
'mkfs.ext4',
|
|
26
|
+
'mkfs.xfs',
|
|
27
|
+
'mkfs.btrfs',
|
|
28
|
+
'mkfs.fat',
|
|
29
|
+
'mkfs.vfat',
|
|
30
|
+
'mkswap',
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
function isBlocked(command: string): string | null {
|
|
34
|
+
// Check exact blocked commands
|
|
35
|
+
const firstWord = command.trim().split(/\s+/)[0]
|
|
36
|
+
if (firstWord && BLOCKED_COMMANDS.includes(firstWord)) {
|
|
37
|
+
return `Command "${firstWord}" is blocked (destructive filesystem operation).`
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Check dangerous patterns
|
|
41
|
+
for (const pattern of BLOCKED_PATTERNS) {
|
|
42
|
+
if (pattern.test(command)) {
|
|
43
|
+
return `Command rejected by security policy. Pattern matched: ${pattern.source.slice(0, 40)}...`
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return null // safe
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export const bashTool: ToolDefinition = {
|
|
51
|
+
name: 'Bash',
|
|
52
|
+
description:
|
|
53
|
+
'Execute a bash command. Returns stdout and stderr. Timeout: 120s. Use with caution.',
|
|
54
|
+
category: 'exec',
|
|
55
|
+
permission: 'ask',
|
|
56
|
+
parameters: {
|
|
57
|
+
type: 'object',
|
|
58
|
+
properties: {
|
|
59
|
+
command: { type: 'string', description: 'The bash command to execute' },
|
|
60
|
+
description: {
|
|
61
|
+
type: 'string',
|
|
62
|
+
description: 'What this command does (for audit log)',
|
|
63
|
+
},
|
|
64
|
+
timeout: {
|
|
65
|
+
type: 'integer',
|
|
66
|
+
description: 'Timeout in milliseconds (max 600000)',
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
required: ['command'],
|
|
70
|
+
},
|
|
71
|
+
async execute(params, ctx) {
|
|
72
|
+
const command = params.command as string
|
|
73
|
+
const timeout = Math.min((params.timeout as number) || 120_000, 600_000)
|
|
74
|
+
|
|
75
|
+
// Security: check command against deny list
|
|
76
|
+
const blockedReason = isBlocked(command)
|
|
77
|
+
if (blockedReason) {
|
|
78
|
+
return { success: false, content: '', error: blockedReason }
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
const proc = Bun.spawn(['bash', '-c', command], {
|
|
83
|
+
cwd: ctx.cwd,
|
|
84
|
+
stdout: 'pipe',
|
|
85
|
+
stderr: 'pipe',
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
const timer = setTimeout(() => proc.kill(), timeout)
|
|
89
|
+
const output = await new Response(proc.stdout).text()
|
|
90
|
+
const exitCode = await proc.exited
|
|
91
|
+
clearTimeout(timer)
|
|
92
|
+
|
|
93
|
+
if (exitCode !== 0) {
|
|
94
|
+
const stderr = await new Response(proc.stderr).text()
|
|
95
|
+
return {
|
|
96
|
+
success: false,
|
|
97
|
+
content: output.slice(0, 5_000),
|
|
98
|
+
error: `Exit code ${exitCode}: ${stderr.slice(0, 1_000)}`,
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return { success: true, content: output.slice(0, 100_000) || '(no output)' }
|
|
103
|
+
} catch (err) {
|
|
104
|
+
return {
|
|
105
|
+
success: false,
|
|
106
|
+
content: '',
|
|
107
|
+
error: `Command failed: ${String(err)}`,
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { ToolDefinition } from '../../shared/index.ts'
|
|
2
|
+
|
|
3
|
+
const DANGEROUS_COMMANDS = ['push --force', 'reset --hard', 'clean -fd', 'branch -D']
|
|
4
|
+
|
|
5
|
+
export const gitTool: ToolDefinition = {
|
|
6
|
+
name: 'Git',
|
|
7
|
+
description: 'Execute git commands. Dangerous operations (force push, hard reset) are blocked.',
|
|
8
|
+
category: 'exec',
|
|
9
|
+
permission: 'auto',
|
|
10
|
+
parameters: {
|
|
11
|
+
type: 'object',
|
|
12
|
+
properties: {
|
|
13
|
+
command: {
|
|
14
|
+
type: 'string',
|
|
15
|
+
description: 'Git subcommand + args (e.g., "status", "log --oneline")',
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
required: ['command'],
|
|
19
|
+
},
|
|
20
|
+
async execute(params, ctx) {
|
|
21
|
+
const command = params.command as string
|
|
22
|
+
|
|
23
|
+
for (const dangerous of DANGEROUS_COMMANDS) {
|
|
24
|
+
if (command.includes(dangerous)) {
|
|
25
|
+
return {
|
|
26
|
+
success: false,
|
|
27
|
+
content: '',
|
|
28
|
+
error: `Dangerous git command blocked: "${dangerous}". Run manually if intended.`,
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
const proc = Bun.spawn(['git', ...command.split(/\s+/)], {
|
|
35
|
+
cwd: ctx.cwd,
|
|
36
|
+
stdout: 'pipe',
|
|
37
|
+
stderr: 'pipe',
|
|
38
|
+
})
|
|
39
|
+
const output = await new Response(proc.stdout).text()
|
|
40
|
+
const exitCode = await proc.exited
|
|
41
|
+
|
|
42
|
+
if (exitCode !== 0) {
|
|
43
|
+
const stderr = await new Response(proc.stderr).text()
|
|
44
|
+
return {
|
|
45
|
+
success: false,
|
|
46
|
+
content: '',
|
|
47
|
+
error: `Git error (exit ${exitCode}): ${stderr.slice(0, 1000)}`,
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
success: true,
|
|
53
|
+
content: output.slice(0, 50_000) || '(no output)',
|
|
54
|
+
}
|
|
55
|
+
} catch (err) {
|
|
56
|
+
return {
|
|
57
|
+
success: false,
|
|
58
|
+
content: '',
|
|
59
|
+
error: `Git execution failed: ${String(err)}`,
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { ToolDefinition } from '../../shared/index.ts'
|
|
2
|
+
|
|
3
|
+
interface Task {
|
|
4
|
+
id: string
|
|
5
|
+
subject: string
|
|
6
|
+
description: string
|
|
7
|
+
status: 'pending' | 'in_progress' | 'completed'
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const tasks = new Map<string, Task>()
|
|
11
|
+
let taskCounter = 0
|
|
12
|
+
|
|
13
|
+
export const taskTool: ToolDefinition = {
|
|
14
|
+
name: 'Task',
|
|
15
|
+
description: 'Create and manage structured task lists for tracking progress.',
|
|
16
|
+
category: 'exec',
|
|
17
|
+
permission: 'auto',
|
|
18
|
+
parameters: {
|
|
19
|
+
type: 'object',
|
|
20
|
+
properties: {
|
|
21
|
+
action: {
|
|
22
|
+
type: 'string',
|
|
23
|
+
enum: ['create', 'list', 'update'],
|
|
24
|
+
description: 'Action to perform',
|
|
25
|
+
},
|
|
26
|
+
subject: { type: 'string', description: 'Task subject (for create)' },
|
|
27
|
+
description: { type: 'string', description: 'Task description (for create)' },
|
|
28
|
+
taskId: { type: 'string', description: 'Task ID (for update)' },
|
|
29
|
+
status: {
|
|
30
|
+
type: 'string',
|
|
31
|
+
enum: ['pending', 'in_progress', 'completed'],
|
|
32
|
+
description: 'New status (for update)',
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
required: ['action'],
|
|
36
|
+
},
|
|
37
|
+
async execute(params, _ctx) {
|
|
38
|
+
const action = params.action as string
|
|
39
|
+
|
|
40
|
+
if (action === 'create') {
|
|
41
|
+
const id = String(++taskCounter)
|
|
42
|
+
const subject = (params.subject as string) || 'Untitled'
|
|
43
|
+
tasks.set(id, {
|
|
44
|
+
id,
|
|
45
|
+
subject,
|
|
46
|
+
description: (params.description as string) || '',
|
|
47
|
+
status: 'pending',
|
|
48
|
+
})
|
|
49
|
+
return { success: true, content: `Task #${id} created: ${subject}` }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (action === 'list') {
|
|
53
|
+
const list = Array.from(tasks.values())
|
|
54
|
+
.map((t) => `[${t.status}] #${t.id}: ${t.subject}`)
|
|
55
|
+
.join('\n')
|
|
56
|
+
return { success: true, content: list || '(no tasks)' }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (action === 'update') {
|
|
60
|
+
const taskId = params.taskId as string
|
|
61
|
+
const task = tasks.get(taskId)
|
|
62
|
+
if (!task) return { success: false, content: '', error: `Task #${taskId} not found` }
|
|
63
|
+
if (params.status) task.status = params.status as Task['status']
|
|
64
|
+
return { success: true, content: `Task #${taskId} updated: ${task.status}` }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return { success: false, content: '', error: `Unknown action: ${action}` }
|
|
68
|
+
},
|
|
69
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync } from 'node:fs'
|
|
2
|
+
import type { ToolDefinition } from '../../shared/index.ts'
|
|
3
|
+
import { resolveSafe } from '../../security/path'
|
|
4
|
+
|
|
5
|
+
export const editTool: ToolDefinition = {
|
|
6
|
+
name: 'Edit',
|
|
7
|
+
description:
|
|
8
|
+
'Perform exact string replacement in a file. old_string must match exactly and be unique.',
|
|
9
|
+
category: 'file',
|
|
10
|
+
permission: 'ask',
|
|
11
|
+
parameters: {
|
|
12
|
+
type: 'object',
|
|
13
|
+
properties: {
|
|
14
|
+
file_path: { type: 'string' },
|
|
15
|
+
old_string: { type: 'string' },
|
|
16
|
+
new_string: { type: 'string' },
|
|
17
|
+
replace_all: { type: 'boolean', default: false },
|
|
18
|
+
},
|
|
19
|
+
required: ['file_path', 'old_string', 'new_string'],
|
|
20
|
+
},
|
|
21
|
+
async execute(params, ctx) {
|
|
22
|
+
const filePath = resolveSafe(ctx.cwd, params.file_path as string)
|
|
23
|
+
const oldStr = params.old_string as string
|
|
24
|
+
const newStr = params.new_string as string
|
|
25
|
+
const replaceAll = params.replace_all as boolean
|
|
26
|
+
|
|
27
|
+
const content = readFileSync(filePath, 'utf-8')
|
|
28
|
+
|
|
29
|
+
if (replaceAll) {
|
|
30
|
+
if (!content.includes(oldStr)) {
|
|
31
|
+
return { success: false, content: '', error: 'old_string not found in file' }
|
|
32
|
+
}
|
|
33
|
+
const updated = content.replaceAll(oldStr, newStr)
|
|
34
|
+
writeFileSync(filePath, updated, 'utf-8')
|
|
35
|
+
const count = content.split(oldStr).length - 1
|
|
36
|
+
return { success: true, content: `Replaced ${count} occurrences in ${filePath}` }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const firstIndex = content.indexOf(oldStr)
|
|
40
|
+
if (firstIndex === -1) {
|
|
41
|
+
return { success: false, content: '', error: 'old_string not found in file' }
|
|
42
|
+
}
|
|
43
|
+
const secondIndex = content.indexOf(oldStr, firstIndex + 1)
|
|
44
|
+
if (secondIndex !== -1) {
|
|
45
|
+
return {
|
|
46
|
+
success: false,
|
|
47
|
+
content: '',
|
|
48
|
+
error: 'old_string is not unique in file. Use replace_all or make it more specific.',
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const updated =
|
|
53
|
+
content.slice(0, firstIndex) + newStr + content.slice(firstIndex + oldStr.length)
|
|
54
|
+
writeFileSync(filePath, updated, 'utf-8')
|
|
55
|
+
return { success: true, content: `Replaced 1 occurrence in ${filePath}` }
|
|
56
|
+
},
|
|
57
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Glob } from 'bun'
|
|
2
|
+
import type { ToolDefinition } from '../../shared/index.ts'
|
|
3
|
+
import { resolveSafe } from '../../security/path'
|
|
4
|
+
|
|
5
|
+
export const globTool: ToolDefinition = {
|
|
6
|
+
name: 'Glob',
|
|
7
|
+
description: 'Find files matching a glob pattern.',
|
|
8
|
+
category: 'file',
|
|
9
|
+
permission: 'auto',
|
|
10
|
+
parameters: {
|
|
11
|
+
type: 'object',
|
|
12
|
+
properties: {
|
|
13
|
+
pattern: { type: 'string', description: 'Glob pattern (e.g., "src/**/*.ts")' },
|
|
14
|
+
path: { type: 'string', description: 'Base directory' },
|
|
15
|
+
},
|
|
16
|
+
required: ['pattern'],
|
|
17
|
+
},
|
|
18
|
+
async execute(params, ctx) {
|
|
19
|
+
const pattern = params.pattern as string
|
|
20
|
+
const basePath = resolveSafe(ctx.cwd, (params.path as string) || '.')
|
|
21
|
+
const glob = new Glob(pattern)
|
|
22
|
+
const results: string[] = []
|
|
23
|
+
for await (const file of glob.scan({ cwd: basePath, absolute: true })) {
|
|
24
|
+
results.push(file)
|
|
25
|
+
if (results.length >= 500) break
|
|
26
|
+
}
|
|
27
|
+
return { success: true, content: results.join('\n') || '(no matches)' }
|
|
28
|
+
},
|
|
29
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { $ } from 'bun'
|
|
2
|
+
import type { ToolDefinition } from '../../shared/index.ts'
|
|
3
|
+
import { resolveSafe } from '../../security/path'
|
|
4
|
+
|
|
5
|
+
export const grepTool: ToolDefinition = {
|
|
6
|
+
name: 'Grep',
|
|
7
|
+
description: 'Search file contents using ripgrep. Fast regex search across files.',
|
|
8
|
+
category: 'file',
|
|
9
|
+
permission: 'auto',
|
|
10
|
+
parameters: {
|
|
11
|
+
type: 'object',
|
|
12
|
+
properties: {
|
|
13
|
+
pattern: { type: 'string', description: 'Regex pattern to search for' },
|
|
14
|
+
path: { type: 'string', description: 'Directory or file to search in' },
|
|
15
|
+
include: { type: 'string', description: 'File pattern to include (e.g., "*.ts")' },
|
|
16
|
+
},
|
|
17
|
+
required: ['pattern'],
|
|
18
|
+
},
|
|
19
|
+
async execute(params, ctx) {
|
|
20
|
+
const pattern = params.pattern as string
|
|
21
|
+
const searchPath = resolveSafe(ctx.cwd, (params.path as string) || '.')
|
|
22
|
+
const include = params.include as string | undefined
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
const args = ['-n', '--heading', '--color=never', '-M', '500', pattern]
|
|
26
|
+
if (include) args.push('--glob', include)
|
|
27
|
+
args.push(searchPath)
|
|
28
|
+
|
|
29
|
+
const result = await $`rg ${args}`.cwd(ctx.cwd).quiet().text()
|
|
30
|
+
return { success: true, content: result || '(no matches)' }
|
|
31
|
+
} catch (err: unknown) {
|
|
32
|
+
const exitErr = err as { exitCode?: number }
|
|
33
|
+
if (exitErr.exitCode === 1) {
|
|
34
|
+
return { success: true, content: '(no matches)' }
|
|
35
|
+
}
|
|
36
|
+
// Fallback to grep
|
|
37
|
+
try {
|
|
38
|
+
const content = await $`grep -rn ${pattern} ${searchPath}`.cwd(ctx.cwd).quiet().text()
|
|
39
|
+
return {
|
|
40
|
+
success: true,
|
|
41
|
+
content: content.slice(0, 50000) || '(no matches)',
|
|
42
|
+
}
|
|
43
|
+
} catch {
|
|
44
|
+
return {
|
|
45
|
+
success: false,
|
|
46
|
+
content: '',
|
|
47
|
+
error: 'grep failed. Install ripgrep: brew install ripgrep',
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
}
|