miii-cli 0.2.1 → 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 (103) hide show
  1. package/README.md +190 -83
  2. package/dist/config.js +0 -1
  3. package/dist/files/ops.js +22 -4
  4. package/dist/index.js +0 -1
  5. package/dist/init.js +0 -1
  6. package/dist/llm/ollama.js +0 -1
  7. package/dist/llm/stream.js +4 -3
  8. package/dist/parser/stream-parser.js +1 -13
  9. package/dist/sessions.js +0 -1
  10. package/dist/skills/loader.js +0 -1
  11. package/dist/tasks/compactor.js +68 -0
  12. package/dist/tasks/executor.js +88 -0
  13. package/dist/tasks/queue.js +72 -0
  14. package/dist/tools/index.js +108 -5
  15. package/dist/tui/App.js +0 -1
  16. package/dist/tui/InputBar.js +379 -32
  17. package/dist/tui/components/AtPicker.js +0 -1
  18. package/dist/tui/components/CommandPalette.js +4 -3
  19. package/dist/tui/components/InputArea.js +25 -13
  20. package/dist/tui/components/MessageList.js +12 -1
  21. package/dist/tui/components/ModelPicker.js +0 -1
  22. package/dist/tui/components/StatusBar.js +0 -1
  23. package/dist/tui/printer.js +0 -1
  24. package/dist/types.js +0 -1
  25. package/dist/workers/context.worker.js +0 -1
  26. package/dist/workers/spawn.js +0 -1
  27. package/package.json +6 -3
  28. package/.claude/settings.local.json +0 -28
  29. package/CONTRIBUTING.md +0 -55
  30. package/Makefile +0 -13
  31. package/dist/config.d.ts +0 -2
  32. package/dist/config.js.map +0 -1
  33. package/dist/files/ops.d.ts +0 -14
  34. package/dist/files/ops.js.map +0 -1
  35. package/dist/index.d.ts +0 -2
  36. package/dist/index.js.map +0 -1
  37. package/dist/init.d.ts +0 -1
  38. package/dist/init.js.map +0 -1
  39. package/dist/llm/ollama.d.ts +0 -10
  40. package/dist/llm/ollama.js.map +0 -1
  41. package/dist/llm/stream.d.ts +0 -12
  42. package/dist/llm/stream.js.map +0 -1
  43. package/dist/parser/stream-parser.d.ts +0 -21
  44. package/dist/parser/stream-parser.js.map +0 -1
  45. package/dist/sessions.d.ts +0 -9
  46. package/dist/sessions.js.map +0 -1
  47. package/dist/skills/loader.d.ts +0 -23
  48. package/dist/skills/loader.js.map +0 -1
  49. package/dist/tools/index.d.ts +0 -8
  50. package/dist/tools/index.js.map +0 -1
  51. package/dist/tui/App.d.ts +0 -9
  52. package/dist/tui/App.js.map +0 -1
  53. package/dist/tui/InputBar.d.ts +0 -10
  54. package/dist/tui/InputBar.js.map +0 -1
  55. package/dist/tui/components/AtPicker.d.ts +0 -8
  56. package/dist/tui/components/AtPicker.js.map +0 -1
  57. package/dist/tui/components/CommandPalette.d.ts +0 -8
  58. package/dist/tui/components/CommandPalette.js.map +0 -1
  59. package/dist/tui/components/InputArea.d.ts +0 -12
  60. package/dist/tui/components/InputArea.js.map +0 -1
  61. package/dist/tui/components/MessageList.d.ts +0 -11
  62. package/dist/tui/components/MessageList.js.map +0 -1
  63. package/dist/tui/components/ModelPicker.d.ts +0 -18
  64. package/dist/tui/components/ModelPicker.js.map +0 -1
  65. package/dist/tui/components/StatusBar.d.ts +0 -12
  66. package/dist/tui/components/StatusBar.js.map +0 -1
  67. package/dist/tui/printer.d.ts +0 -7
  68. package/dist/tui/printer.js.map +0 -1
  69. package/dist/types.d.ts +0 -20
  70. package/dist/types.js.map +0 -1
  71. package/dist/workers/context.worker.js.map +0 -1
  72. package/dist/workers/diff.worker.d.ts +0 -1
  73. package/dist/workers/diff.worker.js +0 -12
  74. package/dist/workers/diff.worker.js.map +0 -1
  75. package/dist/workers/spawn.d.ts +0 -1
  76. package/dist/workers/spawn.js.map +0 -1
  77. package/install.sh +0 -6
  78. package/mii-cli.gif +0 -0
  79. package/src/config.ts +0 -32
  80. package/src/files/ops.ts +0 -89
  81. package/src/index.ts +0 -11
  82. package/src/init.ts +0 -41
  83. package/src/llm/ollama.ts +0 -110
  84. package/src/llm/stream.ts +0 -55
  85. package/src/parser/stream-parser.ts +0 -196
  86. package/src/sessions.ts +0 -54
  87. package/src/skills/loader.ts +0 -144
  88. package/src/tools/index.ts +0 -151
  89. package/src/tui/App.tsx +0 -355
  90. package/src/tui/InputBar.tsx +0 -381
  91. package/src/tui/components/AtPicker.tsx +0 -49
  92. package/src/tui/components/CommandPalette.tsx +0 -50
  93. package/src/tui/components/InputArea.tsx +0 -297
  94. package/src/tui/components/MessageList.tsx +0 -219
  95. package/src/tui/components/ModelPicker.tsx +0 -134
  96. package/src/tui/components/StatusBar.tsx +0 -36
  97. package/src/tui/printer.ts +0 -130
  98. package/src/types.ts +0 -26
  99. package/src/workers/context.worker.ts +0 -66
  100. package/src/workers/diff.worker.ts +0 -20
  101. package/src/workers/spawn.ts +0 -19
  102. package/tsconfig.json +0 -18
  103. /package/dist/{workers/context.worker.d.ts → tasks/types.js} +0 -0
@@ -1,196 +0,0 @@
1
- import { appendFileSync } from 'fs'
2
-
3
- export interface ParsedText { type: 'text'; content: string }
4
- export interface ParsedTool { type: 'tool_call'; content: string; toolName: string; toolArgs: Record<string, unknown> }
5
- export type ParsedItem = ParsedText | ParsedTool
6
-
7
- const OPEN = '<tool_call>'
8
- const CLOSE = '</tool_call>'
9
- const CTAG_OPEN = '<content>'
10
- const CTAG_CLOSE = '</content>'
11
- const OLD_OPEN = '<old>'
12
- const OLD_CLOSE = '</old>'
13
- const NEW_OPEN = '<new>'
14
- const NEW_CLOSE = '</new>'
15
- const DEBUG_LOG = '/tmp/miii-debug.log'
16
-
17
- function dbg(msg: string) {
18
- try { appendFileSync(DEBUG_LOG, `[${new Date().toISOString()}] ${msg}\n`) } catch {}
19
- }
20
-
21
- // Fix literal newlines/tabs inside JSON string values — common LLM output mistake
22
- function sanitizeJson(s: string): string {
23
- let result = ''
24
- let inString = false
25
- let escaped = false
26
- for (const ch of s) {
27
- if (escaped) {
28
- result += ch
29
- escaped = false
30
- } else if (ch === '\\' && inString) {
31
- result += ch
32
- escaped = true
33
- } else if (ch === '"') {
34
- result += ch
35
- inString = !inString
36
- } else if (inString && ch === '\n') {
37
- result += '\\n'
38
- } else if (inString && ch === '\r') {
39
- result += '\\r'
40
- } else if (inString && ch === '\t') {
41
- result += '\\t'
42
- } else {
43
- result += ch
44
- }
45
- }
46
- return result
47
- }
48
-
49
- function parseToolJson(s: string): { name: string; args?: Record<string, unknown> } {
50
- try { return JSON.parse(s) }
51
- catch { return JSON.parse(sanitizeJson(s)) }
52
- }
53
-
54
- // Find end of a JSON object starting at `from`, correctly tracking strings
55
- function findJsonEnd(text: string, from: number): number {
56
- let depth = 0, inStr = false, escaped = false
57
- for (let i = from; i < text.length; i++) {
58
- const ch = text[i]
59
- if (escaped) { escaped = false; continue }
60
- if (ch === '\\' && inStr) { escaped = true; continue }
61
- if (ch === '"') { inStr = !inStr; continue }
62
- if (inStr) continue
63
- if (ch === '{') depth++
64
- else if (ch === '}') { depth--; if (depth === 0) return i }
65
- }
66
- return -1
67
- }
68
-
69
- // For file-writing tools: content field may have unescaped chars — extract with lastIndexOf heuristic
70
- function extractFileToolArgs(text: string, toolName: string): Record<string, unknown> | null {
71
- if (!text.includes(`"${toolName}"`)) return null
72
- const args: Record<string, string> = {}
73
-
74
- const pathM = text.match(/"path"\s*:\s*"([^"]*)"/)
75
- if (pathM) args.path = pathM[1]
76
-
77
- // content is always the last string field — find its opening quote, take to last " before final }}
78
- const ctIdx = text.indexOf('"content"')
79
- if (ctIdx !== -1) {
80
- const colon = text.indexOf(':', ctIdx)
81
- const openQ = text.indexOf('"', colon + 1)
82
- const lastBrace = text.lastIndexOf('}')
83
- const closeQ = text.lastIndexOf('"', lastBrace - 1)
84
- if (openQ !== -1 && closeQ > openQ) {
85
- const raw = text.slice(openQ + 1, closeQ)
86
- args.content = raw
87
- .replace(/\\n/g, '\n').replace(/\\r/g, '\r').replace(/\\t/g, '\t')
88
- .replace(/\\"/g, '"').replace(/\\\\/g, '\\')
89
- }
90
- }
91
-
92
- // For patch_file: extract old/new fields
93
- const oldM = text.match(/"old"\s*:\s*"([\s\S]*?)"(?:\s*,|\s*\})/)
94
- if (oldM) args.old = oldM[1].replace(/\\n/g, '\n').replace(/\\"/g, '"')
95
- const newM = text.match(/"new"\s*:\s*"([\s\S]*?)"(?:\s*,|\s*\})/)
96
- if (newM) args.new = newM[1].replace(/\\n/g, '\n').replace(/\\"/g, '"')
97
-
98
- return Object.keys(args).length > 0 ? args : null
99
- }
100
-
101
- // Extract a bare tool-call JSON from arbitrary text (LLM skipped <tool_call> wrapper)
102
- export function extractBareToolCall(text: string): { name: string; args: Record<string, unknown> } | null {
103
- // First try standard JSON parsing
104
- let pos = 0
105
- while (true) {
106
- const start = text.indexOf('{"name"', pos)
107
- if (start === -1) break
108
- const end = findJsonEnd(text, start)
109
- if (end === -1) break
110
- try {
111
- const obj = parseToolJson(text.slice(start, end + 1))
112
- if (typeof obj.name === 'string') return { name: obj.name, args: (obj.args ?? {}) as Record<string, unknown> }
113
- } catch {}
114
- pos = start + 1
115
- }
116
-
117
- // Fallback: content-aware extraction for file-writing tools (immune to unescaped chars)
118
- for (const name of ['edit_file', 'create_file', 'patch_file']) {
119
- const args = extractFileToolArgs(text, name)
120
- if (args) return { name, args }
121
- }
122
-
123
- return null
124
- }
125
-
126
- export class StreamParser {
127
- private buf = ''
128
- private inTool = false
129
-
130
- feed(token: string): ParsedItem[] {
131
- this.buf += token
132
- const out: ParsedItem[] = []
133
-
134
- while (true) {
135
- if (this.inTool) {
136
- const end = this.buf.indexOf(CLOSE)
137
- if (end === -1) break
138
- const raw = this.buf.slice(0, end).trim()
139
- this.buf = this.buf.slice(end + CLOSE.length)
140
- this.inTool = false
141
- try {
142
- dbg(`raw block (${raw.length} chars): ${raw.slice(0, 300)}`)
143
- // Extract named content blocks so file content never needs JSON escaping
144
- const extraArgs: Record<string, string> = {}
145
- let jsonPart = raw
146
-
147
- function extractBlock(open: string, close: string, key: string): void {
148
- const s = raw.indexOf(open), e = raw.indexOf(close)
149
- if (s === -1 || e === -1 || e <= s) return
150
- let val = raw.slice(s + open.length, e)
151
- if (val.startsWith('\n')) val = val.slice(1)
152
- if (val.endsWith('\n')) val = val.slice(0, -1)
153
- extraArgs[key] = val
154
- // shrink jsonPart to before the first block
155
- const blockStart = raw.indexOf(open)
156
- if (blockStart < jsonPart.length) jsonPart = raw.slice(0, blockStart).trim()
157
- }
158
-
159
- extractBlock(CTAG_OPEN, CTAG_CLOSE, 'content')
160
- extractBlock(OLD_OPEN, OLD_CLOSE, 'old')
161
- extractBlock(NEW_OPEN, NEW_CLOSE, 'new')
162
-
163
- const obj = parseToolJson(jsonPart)
164
- obj.args = { ...(obj.args ?? {}), ...extraArgs }
165
- dbg(`parsed ok: name=${obj.name} args_keys=${Object.keys(obj.args).join(',')}`)
166
- out.push({ type: 'tool_call', content: raw, toolName: obj.name, toolArgs: obj.args })
167
- } catch (e) {
168
- dbg(`parse FAILED: ${e} | raw: ${raw.slice(0, 300)}`)
169
- out.push({ type: 'text', content: `${OPEN}${raw}${CLOSE}` })
170
- }
171
- } else {
172
- const start = this.buf.indexOf(OPEN)
173
- if (start === -1) {
174
- const safe = this.buf.length > OPEN.length ? this.buf.slice(0, -OPEN.length) : ''
175
- if (safe) { out.push({ type: 'text', content: safe }); this.buf = this.buf.slice(safe.length) }
176
- break
177
- }
178
- if (start > 0) {
179
- out.push({ type: 'text', content: this.buf.slice(0, start) })
180
- this.buf = this.buf.slice(start)
181
- }
182
- this.buf = this.buf.slice(OPEN.length)
183
- this.inTool = true
184
- }
185
- }
186
- return out
187
- }
188
-
189
- flush(): ParsedItem[] {
190
- const out: ParsedItem[] = []
191
- if (this.buf.trim()) out.push({ type: 'text', content: this.buf })
192
- this.buf = ''
193
- this.inTool = false
194
- return out
195
- }
196
- }
package/src/sessions.ts DELETED
@@ -1,54 +0,0 @@
1
- import { readFileSync, writeFileSync, mkdirSync, readdirSync, statSync, existsSync, unlinkSync } from 'fs'
2
- import { join } from 'path'
3
- import { homedir } from 'os'
4
- import type { ChatMessage } from './types.js'
5
-
6
- const SESSIONS_DIR = join(homedir(), '.config', 'miii', 'sessions')
7
-
8
- function ensureDir() {
9
- mkdirSync(SESSIONS_DIR, { recursive: true })
10
- }
11
-
12
- function sanitizeName(name: string): string {
13
- if (!/^[\w-]+$/.test(name)) throw new Error(`invalid session name: ${name}`)
14
- return name
15
- }
16
-
17
- export function listSessions(): Array<{ name: string; messageCount: number; updatedAt: number }> {
18
- ensureDir()
19
- return readdirSync(SESSIONS_DIR)
20
- .filter(f => f.endsWith('.json'))
21
- .map(f => {
22
- const name = f.replace('.json', '')
23
- const p = join(SESSIONS_DIR, f)
24
- let messageCount = 0
25
- let updatedAt = 0
26
- try {
27
- updatedAt = statSync(p).mtimeMs
28
- const msgs = JSON.parse(readFileSync(p, 'utf-8'))
29
- messageCount = Array.isArray(msgs) ? msgs.length : 0
30
- } catch {}
31
- return { name, messageCount, updatedAt }
32
- })
33
- .sort((a, b) => b.updatedAt - a.updatedAt)
34
- }
35
-
36
- export function loadSession(name: string): ChatMessage[] {
37
- ensureDir()
38
- const p = join(SESSIONS_DIR, `${sanitizeName(name)}.json`)
39
- if (!existsSync(p)) return []
40
- try {
41
- const parsed = JSON.parse(readFileSync(p, 'utf-8'))
42
- return Array.isArray(parsed) ? parsed : []
43
- } catch { return [] }
44
- }
45
-
46
- export function saveSession(name: string, messages: ChatMessage[]) {
47
- ensureDir()
48
- writeFileSync(join(SESSIONS_DIR, `${sanitizeName(name)}.json`), JSON.stringify(messages))
49
- }
50
-
51
- export function deleteSession(name: string) {
52
- const p = join(SESSIONS_DIR, `${sanitizeName(name)}.json`)
53
- if (existsSync(p)) unlinkSync(p)
54
- }
@@ -1,144 +0,0 @@
1
- import { readFileSync, existsSync, readdirSync } from 'fs'
2
- import { join, basename } from 'path'
3
- import { homedir } from 'os'
4
- import { createDir, moveFile, writeFile, guardPath } from '../files/ops.js'
5
-
6
- export interface SkillContext {
7
- messages: Array<{ role: string; content: string }>
8
- appendMessage: (role: string, content: string) => void
9
- setSystemPrompt: (p: string) => void
10
- getSystemPrompt: () => string
11
- }
12
-
13
- export interface Skill {
14
- name: string
15
- ns: string
16
- description: string
17
- prompt?: string
18
- execute?: (args: string, ctx: SkillContext) => string | Promise<string>
19
- }
20
-
21
- const builtin: Skill[] = [
22
- {
23
- name: 'caveman',
24
- ns: 'caveman',
25
- description: 'Ultra-compressed terse mode',
26
- execute: (_, ctx) => {
27
- const cur = ctx.getSystemPrompt()
28
- ctx.setSystemPrompt(cur + '\n\nRespond ultra-compressed. Drop articles, filler, pleasantries, hedging. Fragments OK. Technical terms exact.')
29
- return 'Caveman mode active.'
30
- },
31
- },
32
- {
33
- name: 'normal',
34
- ns: 'caveman',
35
- description: 'Revert to normal tone',
36
- execute: (_, ctx) => {
37
- const cur = ctx.getSystemPrompt()
38
- ctx.setSystemPrompt(cur.replace(/\n\nRespond ultra-compressed.*$/s, ''))
39
- return 'Normal mode.'
40
- },
41
- },
42
- {
43
- name: 'review',
44
- ns: 'default',
45
- description: 'Review codebase for bugs/security/quality',
46
- prompt: 'Review the code in this project. Look for bugs, security issues, performance problems, and quality issues. Be specific and actionable. List findings grouped by severity.',
47
- },
48
- {
49
- name: 'help',
50
- ns: 'default',
51
- description: 'Show available commands',
52
- execute: (_, ctx) => {
53
- return 'Built-in: /review /mkdir /mv /touch /models /sessions /session /clear /list /help\nType /list for all loaded skills.'
54
- },
55
- },
56
- {
57
- name: 'list',
58
- ns: 'default',
59
- description: 'List all skills',
60
- execute: () => '', // handled dynamically in loader.list()
61
- },
62
- {
63
- name: 'models',
64
- ns: 'default',
65
- description: 'Choose or pull Ollama models',
66
- // execute handled specially in App.tsx before skill lookup
67
- },
68
- {
69
- name: 'mkdir',
70
- ns: 'default',
71
- description: 'Create a folder — usage: /mkdir <path>',
72
- execute: (args) => {
73
- const p = args.trim()
74
- if (!p) return 'Usage: /mkdir <path>'
75
- createDir(guardPath(p))
76
- return `created: ${p}`
77
- },
78
- },
79
- {
80
- name: 'mv',
81
- ns: 'default',
82
- description: 'Move or rename file/folder — usage: /mv <from> <to>',
83
- execute: (args) => {
84
- const parts = args.trim().split(/\s+/)
85
- if (parts.length < 2) return 'Usage: /mv <from> <to>'
86
- const [from, to] = parts
87
- moveFile(guardPath(from), guardPath(to))
88
- return `moved: ${from} → ${to}`
89
- },
90
- },
91
- {
92
- name: 'touch',
93
- ns: 'default',
94
- description: 'Create empty file — usage: /touch <path>',
95
- execute: (args) => {
96
- const p = args.trim()
97
- if (!p) return 'Usage: /touch <path>'
98
- writeFile(guardPath(p), '')
99
- return `created: ${p}`
100
- },
101
- },
102
- ]
103
-
104
- export class SkillLoader {
105
- private map = new Map<string, Skill>()
106
-
107
- constructor() {
108
- for (const s of builtin) {
109
- this.map.set(`${s.ns}:${s.name}`, s)
110
- this.map.set(s.name, s)
111
- }
112
- }
113
-
114
- async loadAll(): Promise<void> {
115
- const dirs = [
116
- join(homedir(), '.config', 'miii', 'skills'),
117
- join(process.cwd(), '.miii', 'skills'),
118
- ]
119
- for (const dir of dirs) {
120
- if (!existsSync(dir)) continue
121
- for (const entry of readdirSync(dir)) {
122
- if (!entry.endsWith('.md')) continue
123
- const name = basename(entry, '.md')
124
- const content = readFileSync(join(dir, entry), 'utf-8')
125
- const skill: Skill = {
126
- name,
127
- ns: 'custom',
128
- description: content.split('\n')[0].replace(/^#+\s*/, '').trim(),
129
- prompt: content,
130
- }
131
- this.map.set(name, skill)
132
- this.map.set(`custom:${name}`, skill)
133
- }
134
- }
135
- }
136
-
137
- get(ref: string): Skill | undefined {
138
- return this.map.get(ref)
139
- }
140
-
141
- list(): Skill[] {
142
- return [...new Set(this.map.values())]
143
- }
144
- }
@@ -1,151 +0,0 @@
1
- import { readFile, writeFile, deleteFile, listFiles, createDir, moveFile, guardPath } from '../files/ops.js'
2
- import { existsSync } from 'fs'
3
- import { exec } from 'child_process'
4
- import { promisify } from 'util'
5
-
6
- const run = promisify(exec)
7
- const EXEC_TIMEOUT_MS = 30_000
8
-
9
- export interface Tool {
10
- name: string
11
- description: string
12
- params: string
13
- execute: (args: Record<string, unknown>) => Promise<string>
14
- }
15
-
16
- export const tools: Tool[] = [
17
- {
18
- name: 'read_file',
19
- description: 'Read file contents',
20
- params: '{"path": "string"}',
21
- execute: async ({ path }) => {
22
- try { return readFile(guardPath(path as string)) }
23
- catch (e) { throw new Error(`read_file: ${e}`) }
24
- },
25
- },
26
- {
27
- name: 'list_files',
28
- description: 'List directory contents',
29
- params: '{"path": "string", "recursive": "boolean (optional)"}',
30
- execute: async ({ path, recursive = false }) => {
31
- const entries = listFiles(guardPath(path as string), recursive as boolean)
32
- if (!entries.length) return '(empty)'
33
- return entries.map(e => `${e.type === 'dir' ? 'd' : 'f'} ${e.rel}`).join('\n')
34
- },
35
- },
36
- {
37
- name: 'create_file',
38
- description: 'Create a new file — fails if file already exists',
39
- params: '{"path": "string", "content": "string"}',
40
- execute: async ({ path, content }) => {
41
- const safe = guardPath(path as string)
42
- if (existsSync(safe)) throw new Error(`file already exists: ${path}`)
43
- writeFile(safe, content as string)
44
- return `created: ${path}`
45
- },
46
- },
47
- {
48
- name: 'edit_file',
49
- description: 'Overwrite entire file — use only for new files or full rewrites',
50
- params: '{"path": "string", "content": "string"}',
51
- execute: async ({ path, content }) => {
52
- writeFile(guardPath(path as string), content as string)
53
- return `written: ${path}`
54
- },
55
- },
56
- {
57
- name: 'patch_file',
58
- description: 'Replace an exact string in a file — use for targeted edits to existing files',
59
- params: '{"path": "string", "old": "string", "new": "string"}',
60
- execute: async ({ path, old: oldStr, new: newStr }) => {
61
- const safe = guardPath(path as string)
62
- const current = readFile(safe)
63
- if (!current) throw new Error(`file not found or empty: ${path}`)
64
- if (!current.includes(oldStr as string)) throw new Error(`old text not found in ${path}`)
65
- const updated = current.replace(oldStr as string, newStr as string)
66
- writeFile(safe, updated)
67
- return `patched: ${path}`
68
- },
69
- },
70
- {
71
- name: 'delete_file',
72
- description: 'Delete a file',
73
- params: '{"path": "string"}',
74
- execute: async ({ path }) => {
75
- deleteFile(guardPath(path as string))
76
- return `deleted: ${path}`
77
- },
78
- },
79
- {
80
- name: 'run_command',
81
- description: 'Run a shell command in cwd',
82
- params: '{"command": "string"}',
83
- execute: async ({ command }) => {
84
- const { stdout, stderr } = await run(command as string, { cwd: process.cwd(), timeout: EXEC_TIMEOUT_MS })
85
- return [stdout, stderr ? `stderr: ${stderr}` : ''].filter(Boolean).join('\n').trim()
86
- },
87
- },
88
- {
89
- name: 'create_folder',
90
- description: 'Create a directory (and any missing parents)',
91
- params: '{"path": "string"}',
92
- execute: async ({ path }) => {
93
- createDir(guardPath(path as string))
94
- return `created: ${path}`
95
- },
96
- },
97
- {
98
- name: 'move_file',
99
- description: 'Move or rename a file or directory',
100
- params: '{"from": "string", "to": "string"}',
101
- execute: async ({ from, to }) => {
102
- moveFile(guardPath(from as string), guardPath(to as string))
103
- return `moved: ${from} → ${to}`
104
- },
105
- },
106
- ]
107
-
108
- export function getSystemPrompt(extra = ''): string {
109
- const toolDocs = tools.map(t => `- ${t.name}(${t.params}): ${t.description}`).join('\n')
110
- return `You are Miii — a fast, local AI coding assistant.
111
-
112
- Use tools by emitting:
113
- <tool_call>
114
- {"name": "tool_name", "args": {...}}
115
- </tool_call>
116
-
117
- Put file content in named blocks (never inside JSON — avoids escaping errors):
118
-
119
- For edit_file / create_file use <content> block:
120
- <tool_call>
121
- {"name": "edit_file", "args": {"path": "src/foo.ts"}}
122
- <content>
123
- full file content here
124
- </content>
125
- </tool_call>
126
-
127
- For patch_file use <old> and <new> blocks:
128
- <tool_call>
129
- {"name": "patch_file", "args": {"path": "src/foo.ts"}}
130
- <old>
131
- exact text to replace
132
- </old>
133
- <new>
134
- replacement text
135
- </new>
136
- </tool_call>
137
-
138
- Tools:
139
- ${toolDocs}
140
-
141
- Rules:
142
- - To modify an existing file: use patch_file with the exact old text and new replacement — do NOT rewrite the whole file
143
- - To create a new file: use edit_file with full content in the <content> block
144
- - read_file before patch_file so you know the exact text to match
145
- - Never delete without confirming
146
- - Be concise
147
- - Output plain text only — never use markdown formatting in your responses
148
- - No headers (no #, ##), no bold (**text**), no italic (*text*), no bullet points with *, no horizontal rules (---)
149
- - No fenced code blocks with backticks in prose
150
- - Use plain indentation and labels for structure. This is a terminal, not a chat UI${extra}`
151
- }