@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,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
+ }