miii-cli 1.2.4 → 1.3.1
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 +159 -127
- package/dist/config.js +1 -1
- package/dist/init.js +47 -2
- package/dist/llm/stream.js +181 -18
- package/dist/mcp/client.js +8 -1
- package/dist/memory/extractor.js +34 -3
- package/dist/skills/loader.js +6 -2
- package/dist/tasks/compactor.js +4 -1
- package/dist/tools/index.js +73 -20
- package/dist/tui/InputBar.js +2 -2
- package/dist/tui/components/ConfigPicker.js +12 -2
- package/dist/tui/components/InputArea.js +15 -4
- package/dist/tui/deepThink.js +0 -1
- package/dist/tui/hooks/useRefactor.js +4 -3
- package/dist/tui/hooks/useRunLoop.js +158 -78
- package/dist/tui/hooks/useSession.js +2 -2
- package/dist/tui/hooks/useSubmit.js +12 -0
- package/dist/tui/printer.js +7 -6
- package/package.json +1 -1
package/dist/llm/stream.js
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
// Transient errors worth retrying: rate limits + server-side faults
|
|
2
1
|
const RETRYABLE_STATUS = new Set([429, 500, 502, 503, 529]);
|
|
3
2
|
const MAX_RETRIES = 4;
|
|
4
3
|
const MAX_DELAY_MS = 30_000;
|
|
5
4
|
function retryDelay(attempt) {
|
|
6
|
-
// Exponential backoff: 1s → 2s → 4s → 8s, capped at 30s, ±20% jitter
|
|
7
5
|
const base = 1_000 * Math.pow(2, attempt);
|
|
8
6
|
const capped = Math.min(base, MAX_DELAY_MS);
|
|
9
7
|
return Math.round(capped * (0.8 + Math.random() * 0.4));
|
|
@@ -43,6 +41,37 @@ async function fetchWithRetry(url, init, signal, onRetry) {
|
|
|
43
41
|
}
|
|
44
42
|
throw new Error('fetchWithRetry: exhausted retries without returning');
|
|
45
43
|
}
|
|
44
|
+
// Convert Tool params string to JSON Schema for native tool_calls APIs
|
|
45
|
+
function paramsToSchema(paramsStr) {
|
|
46
|
+
try {
|
|
47
|
+
const obj = JSON.parse(paramsStr);
|
|
48
|
+
const properties = {};
|
|
49
|
+
const required = [];
|
|
50
|
+
for (const [key, typeStr] of Object.entries(obj)) {
|
|
51
|
+
const isOptional = typeStr.toLowerCase().includes('optional');
|
|
52
|
+
const isArray = typeStr.toLowerCase().includes('[]') || typeStr.toLowerCase().startsWith('array');
|
|
53
|
+
const base = typeStr.split(' ')[0].toLowerCase().replace('[]', '');
|
|
54
|
+
if (isArray) {
|
|
55
|
+
properties[key] = { type: 'array', items: { type: 'string' } };
|
|
56
|
+
}
|
|
57
|
+
else if (base === 'boolean') {
|
|
58
|
+
properties[key] = { type: 'boolean' };
|
|
59
|
+
}
|
|
60
|
+
else if (base === 'number') {
|
|
61
|
+
properties[key] = { type: 'number' };
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
properties[key] = { type: 'string' };
|
|
65
|
+
}
|
|
66
|
+
if (!isOptional)
|
|
67
|
+
required.push(key);
|
|
68
|
+
}
|
|
69
|
+
return { type: 'object', properties, required };
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return { type: 'object', properties: {}, required: [] };
|
|
73
|
+
}
|
|
74
|
+
}
|
|
46
75
|
export async function warmup(provider, baseUrl, model) {
|
|
47
76
|
if (provider !== 'ollama')
|
|
48
77
|
return;
|
|
@@ -121,12 +150,21 @@ async function chatOllama(cfg) {
|
|
|
121
150
|
}
|
|
122
151
|
}
|
|
123
152
|
async function chatOpenAI(cfg) {
|
|
124
|
-
const { model, messages, baseUrl, apiKey, signal, onDone, onError, onUsage, onChunk, onRetry } = cfg;
|
|
153
|
+
const { model, messages, baseUrl, apiKey, signal, onDone, onError, onUsage, onChunk, onRetry, tools, toolChoice } = cfg;
|
|
154
|
+
const body = { model, messages, stream: !!onChunk };
|
|
155
|
+
if (tools?.length) {
|
|
156
|
+
body.tools = tools.map(t => ({
|
|
157
|
+
type: 'function',
|
|
158
|
+
function: { name: t.name, description: t.description, parameters: paramsToSchema(t.params) },
|
|
159
|
+
}));
|
|
160
|
+
if (toolChoice === 'none')
|
|
161
|
+
body.tool_choice = 'none';
|
|
162
|
+
}
|
|
125
163
|
try {
|
|
126
164
|
const res = await fetchWithRetry(`${baseUrl}/v1/chat/completions`, {
|
|
127
165
|
method: 'POST',
|
|
128
166
|
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey ?? 'local'}` },
|
|
129
|
-
body: JSON.stringify(
|
|
167
|
+
body: JSON.stringify(body),
|
|
130
168
|
}, signal, onRetry);
|
|
131
169
|
if (!res.ok) {
|
|
132
170
|
onError(new Error(`LLM ${res.status}: ${await res.text()}`));
|
|
@@ -135,13 +173,26 @@ async function chatOpenAI(cfg) {
|
|
|
135
173
|
if (!onChunk) {
|
|
136
174
|
const obj = await res.json();
|
|
137
175
|
onUsage?.(obj?.usage?.prompt_tokens ?? 0, obj?.usage?.completion_tokens ?? 0);
|
|
138
|
-
|
|
176
|
+
const message = obj?.choices?.[0]?.message;
|
|
177
|
+
let text = message?.content ?? '';
|
|
178
|
+
if (message?.tool_calls?.length) {
|
|
179
|
+
for (const tc of message.tool_calls) {
|
|
180
|
+
let args = {};
|
|
181
|
+
try {
|
|
182
|
+
args = JSON.parse(tc.function?.arguments ?? '{}');
|
|
183
|
+
}
|
|
184
|
+
catch { }
|
|
185
|
+
text += `\n<tool_call>\n{"name": ${JSON.stringify(tc.function?.name)}, "args": ${JSON.stringify(args)}}\n</tool_call>`;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
await onDone(text);
|
|
139
189
|
return;
|
|
140
190
|
}
|
|
141
191
|
const reader = res.body.getReader();
|
|
142
192
|
const decoder = new TextDecoder();
|
|
143
193
|
let full = '';
|
|
144
194
|
let buf = '';
|
|
195
|
+
const tcAccum = {};
|
|
145
196
|
while (true) {
|
|
146
197
|
const { done, value } = await reader.read();
|
|
147
198
|
if (done)
|
|
@@ -157,15 +208,41 @@ async function chatOpenAI(cfg) {
|
|
|
157
208
|
continue;
|
|
158
209
|
try {
|
|
159
210
|
const obj = JSON.parse(data);
|
|
160
|
-
const
|
|
211
|
+
const delta = obj?.choices?.[0]?.delta;
|
|
212
|
+
if (!delta)
|
|
213
|
+
continue;
|
|
214
|
+
const chunk = delta.content ?? '';
|
|
161
215
|
if (chunk) {
|
|
162
216
|
full += chunk;
|
|
163
217
|
onChunk(chunk);
|
|
164
218
|
}
|
|
219
|
+
if (delta.tool_calls) {
|
|
220
|
+
for (const tc of delta.tool_calls) {
|
|
221
|
+
const idx = tc.index ?? 0;
|
|
222
|
+
if (!tcAccum[idx])
|
|
223
|
+
tcAccum[idx] = { id: '', name: '', args: '' };
|
|
224
|
+
if (tc.id)
|
|
225
|
+
tcAccum[idx].id = tc.id;
|
|
226
|
+
if (tc.function?.name)
|
|
227
|
+
tcAccum[idx].name += tc.function.name;
|
|
228
|
+
if (tc.function?.arguments)
|
|
229
|
+
tcAccum[idx].args += tc.function.arguments;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
165
232
|
}
|
|
166
233
|
catch { }
|
|
167
234
|
}
|
|
168
235
|
}
|
|
236
|
+
// Serialize accumulated tool_calls to XML for run loop compatibility
|
|
237
|
+
for (const idx of Object.keys(tcAccum).map(Number).sort((a, b) => a - b)) {
|
|
238
|
+
const tc = tcAccum[idx];
|
|
239
|
+
let args = {};
|
|
240
|
+
try {
|
|
241
|
+
args = JSON.parse(tc.args);
|
|
242
|
+
}
|
|
243
|
+
catch { }
|
|
244
|
+
full += `\n<tool_call>\n{"name": ${JSON.stringify(tc.name)}, "args": ${JSON.stringify(args)}}\n</tool_call>`;
|
|
245
|
+
}
|
|
169
246
|
await onDone(full);
|
|
170
247
|
}
|
|
171
248
|
catch (err) {
|
|
@@ -174,20 +251,30 @@ async function chatOpenAI(cfg) {
|
|
|
174
251
|
}
|
|
175
252
|
}
|
|
176
253
|
async function chatAnthropic(cfg) {
|
|
177
|
-
const { model, messages, baseUrl, apiKey, signal, onDone, onError, onUsage, onRetry } = cfg;
|
|
254
|
+
const { model, messages, baseUrl, apiKey, signal, onDone, onError, onUsage, onChunk, onRetry, tools, toolChoice } = cfg;
|
|
178
255
|
const url = baseUrl && baseUrl !== 'http://localhost:11434'
|
|
179
256
|
? `${baseUrl}/v1/messages`
|
|
180
257
|
: 'https://api.anthropic.com/v1/messages';
|
|
181
258
|
const systemParts = messages.filter(m => m.role === 'system').map(m => m.content);
|
|
182
259
|
const filtered = messages.filter(m => m.role !== 'system');
|
|
260
|
+
const body = {
|
|
261
|
+
model,
|
|
262
|
+
max_tokens: 8192,
|
|
263
|
+
stream: !!onChunk,
|
|
264
|
+
messages: filtered,
|
|
265
|
+
};
|
|
266
|
+
if (systemParts.length)
|
|
267
|
+
body.system = systemParts.join('\n\n');
|
|
268
|
+
if (tools?.length) {
|
|
269
|
+
body.tools = tools.map(t => ({
|
|
270
|
+
name: t.name,
|
|
271
|
+
description: t.description,
|
|
272
|
+
input_schema: paramsToSchema(t.params),
|
|
273
|
+
}));
|
|
274
|
+
if (toolChoice === 'none')
|
|
275
|
+
body.tool_choice = { type: 'none' };
|
|
276
|
+
}
|
|
183
277
|
try {
|
|
184
|
-
const body = {
|
|
185
|
-
model,
|
|
186
|
-
max_tokens: 8192,
|
|
187
|
-
messages: filtered,
|
|
188
|
-
};
|
|
189
|
-
if (systemParts.length)
|
|
190
|
-
body.system = systemParts.join('\n\n');
|
|
191
278
|
const res = await fetchWithRetry(url, {
|
|
192
279
|
method: 'POST',
|
|
193
280
|
headers: {
|
|
@@ -201,10 +288,86 @@ async function chatAnthropic(cfg) {
|
|
|
201
288
|
onError(new Error(`Anthropic ${res.status}: ${await res.text()}`));
|
|
202
289
|
return;
|
|
203
290
|
}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
291
|
+
if (!onChunk) {
|
|
292
|
+
const obj = await res.json();
|
|
293
|
+
let fullText = '';
|
|
294
|
+
for (const block of obj?.content ?? []) {
|
|
295
|
+
if (block.type === 'text')
|
|
296
|
+
fullText += block.text ?? '';
|
|
297
|
+
else if (block.type === 'tool_use') {
|
|
298
|
+
const args = block.input ?? {};
|
|
299
|
+
fullText += `\n<tool_call>\n{"name": ${JSON.stringify(block.name ?? '')}, "args": ${JSON.stringify(args)}}\n</tool_call>`;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
onUsage?.(obj?.usage?.input_tokens ?? 0, obj?.usage?.output_tokens ?? 0);
|
|
303
|
+
await onDone(fullText);
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
const reader = res.body.getReader();
|
|
307
|
+
const decoder = new TextDecoder();
|
|
308
|
+
let buf = '';
|
|
309
|
+
let fullText = '';
|
|
310
|
+
let promptTokens = 0;
|
|
311
|
+
let completionTokens = 0;
|
|
312
|
+
// Track native tool_use content blocks
|
|
313
|
+
const toolBlocks = [];
|
|
314
|
+
let activeToolIdx = -1;
|
|
315
|
+
while (true) {
|
|
316
|
+
const { done, value } = await reader.read();
|
|
317
|
+
if (done)
|
|
318
|
+
break;
|
|
319
|
+
buf += decoder.decode(value, { stream: true });
|
|
320
|
+
const lines = buf.split('\n');
|
|
321
|
+
buf = lines.pop() ?? '';
|
|
322
|
+
for (const line of lines) {
|
|
323
|
+
if (!line.startsWith('data: '))
|
|
324
|
+
continue;
|
|
325
|
+
const data = line.slice(6).trim();
|
|
326
|
+
if (!data || data === '[DONE]')
|
|
327
|
+
continue;
|
|
328
|
+
try {
|
|
329
|
+
const evt = JSON.parse(data);
|
|
330
|
+
if (evt.type === 'message_start') {
|
|
331
|
+
promptTokens = (evt.message?.usage?.input_tokens) ?? 0;
|
|
332
|
+
}
|
|
333
|
+
else if (evt.type === 'content_block_start') {
|
|
334
|
+
const block = evt.content_block;
|
|
335
|
+
if (block.type === 'tool_use') {
|
|
336
|
+
activeToolIdx = toolBlocks.length;
|
|
337
|
+
toolBlocks.push({ id: block.id ?? '', name: block.name ?? '', inputJson: '' });
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
else if (evt.type === 'content_block_delta') {
|
|
341
|
+
const delta = evt.delta;
|
|
342
|
+
if (delta.type === 'text_delta' && delta.text) {
|
|
343
|
+
fullText += delta.text;
|
|
344
|
+
onChunk?.(delta.text);
|
|
345
|
+
}
|
|
346
|
+
else if (delta.type === 'input_json_delta' && activeToolIdx >= 0) {
|
|
347
|
+
toolBlocks[activeToolIdx].inputJson += delta.partial_json ?? '';
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
else if (evt.type === 'content_block_stop') {
|
|
351
|
+
activeToolIdx = -1;
|
|
352
|
+
}
|
|
353
|
+
else if (evt.type === 'message_delta') {
|
|
354
|
+
completionTokens = (evt.usage?.output_tokens) ?? 0;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
catch { }
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
// Serialize native tool_use blocks to XML for run loop compatibility
|
|
361
|
+
for (const block of toolBlocks) {
|
|
362
|
+
let args = {};
|
|
363
|
+
try {
|
|
364
|
+
args = JSON.parse(block.inputJson);
|
|
365
|
+
}
|
|
366
|
+
catch { }
|
|
367
|
+
fullText += `\n<tool_call>\n{"name": ${JSON.stringify(block.name)}, "args": ${JSON.stringify(args)}}\n</tool_call>`;
|
|
368
|
+
}
|
|
369
|
+
onUsage?.(promptTokens, completionTokens);
|
|
370
|
+
await onDone(fullText);
|
|
208
371
|
}
|
|
209
372
|
catch (err) {
|
|
210
373
|
if (err?.name !== 'AbortError')
|
package/dist/mcp/client.js
CHANGED
|
@@ -22,7 +22,9 @@ export class MCPClient {
|
|
|
22
22
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
23
23
|
env: { ...process.env, ...cfg.env },
|
|
24
24
|
});
|
|
25
|
-
this.proc.stderr?.on('data', () => {
|
|
25
|
+
this.proc.stderr?.on('data', (d) => {
|
|
26
|
+
d.toString().split('\n').filter(Boolean).forEach(line => process.stderr.write(`[MCP:${this.name}] ${line}\n`));
|
|
27
|
+
});
|
|
26
28
|
const rl = createInterface({ input: this.proc.stdout });
|
|
27
29
|
rl.on('line', (line) => {
|
|
28
30
|
if (!line.trim())
|
|
@@ -73,6 +75,11 @@ export class MCPClient {
|
|
|
73
75
|
resolve: (v) => { clearTimeout(timer); resolve(v); },
|
|
74
76
|
reject: (e) => { clearTimeout(timer); reject(e); },
|
|
75
77
|
});
|
|
78
|
+
if (!this.proc?.stdin?.writable) {
|
|
79
|
+
this.pending.delete(id);
|
|
80
|
+
reject(new Error('MCP process stdin not writable'));
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
76
83
|
this.proc.stdin.write(JSON.stringify({ jsonrpc: '2.0', id, method, params }) + '\n');
|
|
77
84
|
timer = setTimeout(() => {
|
|
78
85
|
if (this.pending.has(id)) {
|
package/dist/memory/extractor.js
CHANGED
|
@@ -26,12 +26,43 @@ export function extractFacts(messages, config, model) {
|
|
|
26
26
|
],
|
|
27
27
|
onDone(text) {
|
|
28
28
|
try {
|
|
29
|
-
const
|
|
30
|
-
if (
|
|
29
|
+
const start = text.indexOf('[');
|
|
30
|
+
if (start === -1) {
|
|
31
31
|
resolve([]);
|
|
32
32
|
return;
|
|
33
33
|
}
|
|
34
|
-
|
|
34
|
+
let depth = 0, inStr = false, esc = false, end = -1;
|
|
35
|
+
for (let i = start; i < text.length; i++) {
|
|
36
|
+
const ch = text[i];
|
|
37
|
+
if (esc) {
|
|
38
|
+
esc = false;
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (ch === '\\' && inStr) {
|
|
42
|
+
esc = true;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (ch === '"') {
|
|
46
|
+
inStr = !inStr;
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (inStr)
|
|
50
|
+
continue;
|
|
51
|
+
if (ch === '[')
|
|
52
|
+
depth++;
|
|
53
|
+
else if (ch === ']') {
|
|
54
|
+
depth--;
|
|
55
|
+
if (depth === 0) {
|
|
56
|
+
end = i;
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (end === -1) {
|
|
62
|
+
resolve([]);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
const arr = JSON.parse(text.slice(start, end + 1));
|
|
35
66
|
resolve(Array.isArray(arr) ? arr.filter((f) => typeof f === 'string') : []);
|
|
36
67
|
}
|
|
37
68
|
catch {
|
package/dist/skills/loader.js
CHANGED
|
@@ -167,8 +167,10 @@ export class SkillLoader {
|
|
|
167
167
|
const pkg = nameOrPkg.includes('/') || nameOrPkg.startsWith('miii-skill-')
|
|
168
168
|
? nameOrPkg
|
|
169
169
|
: `miii-skill-${nameOrPkg}`;
|
|
170
|
+
if (!/^[a-zA-Z0-9@/._-]+$/.test(pkg))
|
|
171
|
+
throw new Error(`invalid package name: ${pkg}`);
|
|
170
172
|
createDir(NPM_SKILLS_DIR);
|
|
171
|
-
const { stdout, stderr } = await run(`npm install --prefix ${JSON.stringify(NPM_SKILLS_DIR)} ${pkg}`);
|
|
173
|
+
const { stdout, stderr } = await run(`npm install --prefix ${JSON.stringify(NPM_SKILLS_DIR)} ${JSON.stringify(pkg)}`);
|
|
172
174
|
const out = (stdout + stderr).trim();
|
|
173
175
|
// Reload newly installed skill
|
|
174
176
|
await this.loadAll();
|
|
@@ -178,7 +180,9 @@ export class SkillLoader {
|
|
|
178
180
|
const pkg = nameOrPkg.includes('/') || nameOrPkg.startsWith('miii-skill-')
|
|
179
181
|
? nameOrPkg
|
|
180
182
|
: `miii-skill-${nameOrPkg}`;
|
|
181
|
-
|
|
183
|
+
if (!/^[a-zA-Z0-9@/._-]+$/.test(pkg))
|
|
184
|
+
throw new Error(`invalid package name: ${pkg}`);
|
|
185
|
+
const { stdout, stderr } = await run(`npm uninstall --prefix ${JSON.stringify(NPM_SKILLS_DIR)} ${JSON.stringify(pkg)}`);
|
|
182
186
|
const out = (stdout + stderr).trim();
|
|
183
187
|
// Remove from map
|
|
184
188
|
const shortName = pkg.replace(/^miii-skill-/, '');
|
package/dist/tasks/compactor.js
CHANGED
|
@@ -30,7 +30,7 @@ What still needs to be done, if anything.
|
|
|
30
30
|
Any constraints, errors encountered, important facts the agent must remember to continue correctly.
|
|
31
31
|
|
|
32
32
|
Be factual. No padding. Include file paths, error messages, and command outputs verbatim when relevant.`;
|
|
33
|
-
export async function compactContext(messages, cfg, goal) {
|
|
33
|
+
export async function compactContext(messages, cfg, goal, signal) {
|
|
34
34
|
if (contextSize(messages) <= COMPACT_CHAR_THRESHOLD)
|
|
35
35
|
return messages;
|
|
36
36
|
const system = messages[0]?.role === 'system' ? messages[0] : null;
|
|
@@ -50,6 +50,7 @@ export async function compactContext(messages, cfg, goal) {
|
|
|
50
50
|
let compactErr = '';
|
|
51
51
|
await chat({
|
|
52
52
|
...cfg,
|
|
53
|
+
signal,
|
|
53
54
|
messages: [
|
|
54
55
|
{ role: 'system', content: COMPACT_SYSTEM },
|
|
55
56
|
{ role: 'user', content: userPrompt },
|
|
@@ -59,6 +60,8 @@ export async function compactContext(messages, cfg, goal) {
|
|
|
59
60
|
});
|
|
60
61
|
if (compactErr)
|
|
61
62
|
console.error(`[compactor] LLM error: ${compactErr}`);
|
|
63
|
+
if (signal?.aborted)
|
|
64
|
+
return messages;
|
|
62
65
|
// Fallback to dumb compaction if LLM fails
|
|
63
66
|
if (!summary)
|
|
64
67
|
return dumbCompact(messages, goal);
|
package/dist/tools/index.js
CHANGED
|
@@ -20,7 +20,10 @@ export const tools = [
|
|
|
20
20
|
params: '{"path": "string"}',
|
|
21
21
|
execute: async ({ path }) => {
|
|
22
22
|
try {
|
|
23
|
-
|
|
23
|
+
const safe = guardPath(requireArg(path, 'path', 'read_file'));
|
|
24
|
+
if (!existsSync(safe))
|
|
25
|
+
throw new Error(`file not found: ${path}`);
|
|
26
|
+
return readFile(safe);
|
|
24
27
|
}
|
|
25
28
|
catch (e) {
|
|
26
29
|
throw new Error(`read_file: ${e}`);
|
|
@@ -73,15 +76,18 @@ export const tools = [
|
|
|
73
76
|
params: '{"path": "string", "old": "string", "new": "string"}',
|
|
74
77
|
execute: async ({ path, old: oldStr, new: newStr }) => {
|
|
75
78
|
const safe = guardPath(requireArg(path, 'path', 'update_file'));
|
|
76
|
-
|
|
77
|
-
if (current === null)
|
|
79
|
+
if (!existsSync(safe))
|
|
78
80
|
throw new Error(`file not found: ${path}`);
|
|
81
|
+
const current = readFile(safe);
|
|
79
82
|
if (current === '')
|
|
80
83
|
throw new Error(`file empty: ${path}`);
|
|
81
84
|
const old = requireArg(oldStr, 'old', 'update_file');
|
|
82
85
|
if (newStr === undefined || newStr === null)
|
|
83
86
|
throw new Error('update_file: "new" argument is required');
|
|
84
|
-
const
|
|
87
|
+
const norm = (s) => s.replace(/\r\n/g, '\n');
|
|
88
|
+
const currentNorm = norm(current);
|
|
89
|
+
const oldNorm = norm(old);
|
|
90
|
+
const count = currentNorm.split(oldNorm).length - 1;
|
|
85
91
|
if (count === 0) {
|
|
86
92
|
throw new Error(`old text not found in ${path} — file may have changed since last read.\n` +
|
|
87
93
|
`Call read_file again to get current content, then retry with exact matching text.`);
|
|
@@ -89,11 +95,11 @@ export const tools = [
|
|
|
89
95
|
if (count > 1) {
|
|
90
96
|
throw new Error(`ambiguous: ${count} matches found in ${path} — extend <old> block with more surrounding lines to make it unique`);
|
|
91
97
|
}
|
|
92
|
-
const updated =
|
|
98
|
+
const updated = currentNorm.replace(oldNorm, norm(String(newStr)));
|
|
93
99
|
writeFile(safe, updated);
|
|
94
100
|
// Compute affected line range for the snippet
|
|
95
|
-
const startLine =
|
|
96
|
-
const oldLines =
|
|
101
|
+
const startLine = currentNorm.slice(0, currentNorm.indexOf(oldNorm)).split('\n').length;
|
|
102
|
+
const oldLines = oldNorm.split('\n').length;
|
|
97
103
|
const newLines = newStr.split('\n').length;
|
|
98
104
|
const updatedArr = updated.split('\n');
|
|
99
105
|
const snippetStart = Math.max(0, startLine - 3);
|
|
@@ -287,14 +293,16 @@ export function getSystemPrompt(extra = '', extraTools = []) {
|
|
|
287
293
|
const toolDocs = allTools.map(t => `- ${t.name}(${t.params}): ${t.description}`).join('\n');
|
|
288
294
|
const deepThinkDoc = `- deep_think({"query": "string", "needs_web": "boolean (optional)"}): Research tool — gathers information from files, git, and optionally the web before answering. Returns a compiled research summary. Guardrails: read-only tools only, max 6 tool calls, max 4 web calls inside. Use when a question requires reading multiple files or searching the web first.
|
|
289
295
|
- search_codebase({"query": "string", "k": "number (optional)"}): Semantic vector search over the indexed codebase. Returns top-k relevant code snippets by meaning. Requires the user to have run /index build. Use this when you need to find code by concept rather than exact string — e.g. "authentication logic", "error handling patterns", "database queries".`;
|
|
290
|
-
return `You are Miii — AI coding assistant.
|
|
296
|
+
return `You are Miii — a precise, disciplined AI coding assistant. You implement exactly what is asked. Nothing more.
|
|
297
|
+
|
|
298
|
+
## Tool format
|
|
291
299
|
|
|
292
|
-
Tools via:
|
|
293
300
|
<tool_call>
|
|
294
301
|
{"name": "tool_name", "args": {...}}
|
|
295
302
|
</tool_call>
|
|
296
303
|
|
|
297
|
-
File content in named blocks
|
|
304
|
+
File content goes in named blocks outside the JSON — never inside it:
|
|
305
|
+
|
|
298
306
|
<tool_call>
|
|
299
307
|
{"name": "edit_file", "args": {"path": "src/foo.ts"}}
|
|
300
308
|
<content>
|
|
@@ -305,23 +313,68 @@ full file content here
|
|
|
305
313
|
<tool_call>
|
|
306
314
|
{"name": "update_file", "args": {"path": "src/foo.ts"}}
|
|
307
315
|
<old>
|
|
308
|
-
exact text to replace
|
|
316
|
+
exact text to replace (copy verbatim from read_file output)
|
|
309
317
|
</old>
|
|
310
318
|
<new>
|
|
311
319
|
replacement text
|
|
312
320
|
</new>
|
|
313
321
|
</tool_call>
|
|
314
322
|
|
|
315
|
-
Tools
|
|
323
|
+
## Tools
|
|
316
324
|
${toolDocs}
|
|
317
325
|
${deepThinkDoc}
|
|
318
326
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
+
## Execution protocol
|
|
328
|
+
|
|
329
|
+
For every task, follow this sequence:
|
|
330
|
+
1. Read relevant files first — never assume file contents. When reading multiple independent files, emit all read_file calls in a single batch — do not wait for one before requesting the next.
|
|
331
|
+
2. Make the minimal targeted change that satisfies the request
|
|
332
|
+
3. Run run_tests after any edit. If tests fail, fix and retry up to 3 times before reporting
|
|
333
|
+
4. For refactors or commits: git_status → git_diff first, always
|
|
334
|
+
|
|
335
|
+
Parallel tool calls: when multiple tool calls have no dependency between them, issue them together in one batch. Sequential only when a later call depends on an earlier result.
|
|
336
|
+
|
|
337
|
+
For exploratory questions ("how should we approach X?", "what could we do about Y?"):
|
|
338
|
+
- Respond in 2-3 sentences: recommendation + main tradeoff
|
|
339
|
+
- Do not implement until the user agrees
|
|
340
|
+
|
|
341
|
+
For UI or frontend changes: verify the change works in a browser before reporting done. If browser testing is not possible, say so explicitly rather than claiming success.
|
|
342
|
+
|
|
343
|
+
## Code discipline
|
|
344
|
+
|
|
345
|
+
- Implement exactly what is asked. A bug fix is not a refactor opportunity. A one-shot task does not need a helper abstraction.
|
|
346
|
+
- Three similar lines of code is better than a premature abstraction.
|
|
347
|
+
- Write no comments by default. Add one only when the WHY is non-obvious: a hidden constraint, a subtle invariant, a specific bug workaround. Never explain what the code does — names do that.
|
|
348
|
+
- Add no error handling for scenarios that cannot occur. Trust framework and internal code guarantees. Validate only at system boundaries: user input, external APIs, file I/O.
|
|
349
|
+
- Add no backwards-compatibility shims, feature flags, or dead code for hypothetical future requirements.
|
|
350
|
+
|
|
351
|
+
## File editing rules
|
|
352
|
+
|
|
353
|
+
- edit_file: new files only — throws if file exists. For existing files: read_file → update_file.
|
|
354
|
+
- update_file: copy the <old> text verbatim from read_file output. Never guess or paraphrase it.
|
|
355
|
+
- If "old text not found": read_file again immediately and retry with exact current text.
|
|
356
|
+
- Prefer update_file (surgical patch) over edit_file (full rewrite) for existing files.
|
|
357
|
+
- Read a file immediately before patching it — not from earlier in the conversation.
|
|
358
|
+
|
|
359
|
+
## Safety and reversibility
|
|
360
|
+
|
|
361
|
+
- Before any destructive action (delete_file, overwriting content, git_commit with -A), verify the blast radius.
|
|
362
|
+
- Never introduce security vulnerabilities: no command injection, no path traversal, no hardcoded secrets, no XSS, no SQL injection. If you wrote insecure code, fix it immediately.
|
|
363
|
+
- run_command executes in a shell — validate any user-supplied values before interpolating into commands.
|
|
364
|
+
|
|
365
|
+
## Git discipline
|
|
366
|
+
|
|
367
|
+
- git_status before every commit. Never commit if working tree is unexpected.
|
|
368
|
+
- Stage specific files. Use -A only when all changes are intentional and reviewed.
|
|
369
|
+
- Never amend a commit unless explicitly asked.
|
|
370
|
+
- Never force-push unless explicitly asked and confirmed.
|
|
371
|
+
- Never skip hooks (--no-verify) unless explicitly asked. If a hook fails, diagnose and fix the root cause.
|
|
372
|
+
- Never use interactive git flags (-i) — they require terminal input that is not available.
|
|
373
|
+
|
|
374
|
+
## Communication
|
|
375
|
+
|
|
376
|
+
- Plain text only. No markdown (no #, *, \`, ---). No code blocks in responses — write code with tools.
|
|
377
|
+
- No filler: no "sure", "certainly", "happy to", "great question". State results and next steps directly.
|
|
378
|
+
- web_search requires "query" key exactly. Never say you can't search — always call web_search.
|
|
379
|
+
- deep_think: read-only research only. Cannot edit files.${extra}`;
|
|
327
380
|
}
|
package/dist/tui/InputBar.js
CHANGED
|
@@ -101,7 +101,8 @@ export function InputBar({ config: initialConfig, skills, cwd, session, version,
|
|
|
101
101
|
const abortRef = useRef(null);
|
|
102
102
|
const [designTeachState, setDesignTeachState] = useState(null);
|
|
103
103
|
const [designReadyPrompt, setDesignReadyPrompt] = useState(null);
|
|
104
|
-
const {
|
|
104
|
+
const { currentModel, setCurrentModel, currentModelRef, pickerOpen, setPickerOpen, pickerModels, pickerLoading, pickerError, pullState, handleModelSelect, handleModelPull, } = useModelPicker(config);
|
|
105
|
+
const { projectDir, setSessionName, sessionNameRef, historyRef, saveTimerRef, systemPromptRef, pushHistory, setHistory, buildContext, renameFromMessage, updateMemory, } = useSession(session, cwd, config, mcpTools, currentModelRef);
|
|
105
106
|
const startDesignTeach = useCallback(() => {
|
|
106
107
|
setDesignTeachState({ answers: [], idx: 0 });
|
|
107
108
|
}, []);
|
|
@@ -119,7 +120,6 @@ export function InputBar({ config: initialConfig, skills, cwd, session, version,
|
|
|
119
120
|
return { answers, idx: nextIdx };
|
|
120
121
|
});
|
|
121
122
|
}, []);
|
|
122
|
-
const { currentModel, setCurrentModel, currentModelRef, pickerOpen, setPickerOpen, pickerModels, pickerLoading, pickerError, pullState, handleModelSelect, handleModelPull, } = useModelPicker(config);
|
|
123
123
|
const deepThinkTool = useMemo(() => ({
|
|
124
124
|
name: 'deep_think',
|
|
125
125
|
description: 'Research tool: gather info from files and web before answering.',
|
|
@@ -12,6 +12,7 @@ const MENU_ITEMS = [
|
|
|
12
12
|
{ key: 'key', label: 'API Key' },
|
|
13
13
|
{ key: 'url', label: 'Base URL' },
|
|
14
14
|
{ key: 'tavily', label: 'Tavily Key' },
|
|
15
|
+
{ key: 'streaming', label: 'Streaming' },
|
|
15
16
|
];
|
|
16
17
|
function truncate(s, n) {
|
|
17
18
|
return s.length > n ? s.slice(0, n) + '…' : s;
|
|
@@ -76,7 +77,12 @@ export function ConfigPicker({ config, currentModel, tavilyKey, onUpdate, onTavi
|
|
|
76
77
|
return;
|
|
77
78
|
}
|
|
78
79
|
if (key.return) {
|
|
79
|
-
|
|
80
|
+
const item = MENU_ITEMS[menuIdx];
|
|
81
|
+
if (item.key === 'streaming') {
|
|
82
|
+
onUpdate({ streaming: !config.streaming });
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
openScreen(item.key);
|
|
80
86
|
return;
|
|
81
87
|
}
|
|
82
88
|
return;
|
|
@@ -161,7 +167,11 @@ export function ConfigPicker({ config, currentModel, tavilyKey, onUpdate, onTavi
|
|
|
161
167
|
val = truncate(config.baseUrl, 36);
|
|
162
168
|
if (item.key === 'tavily')
|
|
163
169
|
val = tavilyDisplay;
|
|
164
|
-
|
|
170
|
+
const isStreaming = item.key === 'streaming';
|
|
171
|
+
const streamingOn = config.streaming === true;
|
|
172
|
+
return (_jsxs(Box, { children: [_jsxs(Text, { color: active ? 'cyan' : 'white', bold: active, children: [active ? '▶ ' : ' ', item.label.padEnd(12)] }), isStreaming
|
|
173
|
+
? _jsx(Text, { color: streamingOn ? 'green' : 'gray', children: streamingOn ? 'on' : 'off' })
|
|
174
|
+
: _jsx(Text, { color: active ? 'white' : 'gray', children: val })] }, item.key));
|
|
165
175
|
}), screen === 'provider' && PROVIDERS.map((p, i) => {
|
|
166
176
|
const active = i === provIdx;
|
|
167
177
|
const current = p.key === config.provider;
|
|
@@ -21,6 +21,7 @@ const BUILTIN_COMMANDS = [
|
|
|
21
21
|
{ ns: 'builtin', name: 'list', description: 'list all loaded skills and their descriptions' },
|
|
22
22
|
// ── AI modes ─────────────────────────────────────────────────────────────
|
|
23
23
|
{ ns: 'builtin', name: 'plan', description: 'enter planning mode — AI helps think through a goal step-by-step' },
|
|
24
|
+
{ ns: 'builtin', name: 'plan exec', description: 'two-phase: AI outputs plan first (no tools), say "go" to execute — /plan exec <task>' },
|
|
24
25
|
{ ns: 'builtin', name: 'refactor', description: 'multi-file AI refactor — plans, reads, then edits — /refactor <goal>' },
|
|
25
26
|
{ ns: 'builtin', name: 'think', description: 'deep research before answering — reads files + optional web — /think <query>' },
|
|
26
27
|
{ ns: 'builtin', name: 'watch', description: 'watch for file changes, run tests, auto-fix failures — /watch stop to cancel' },
|
|
@@ -176,8 +177,10 @@ export function InputArea({ status, skills, cwd, planningMode, permissionRequest
|
|
|
176
177
|
: skill.ns === 'git'
|
|
177
178
|
? `/git ${skill.name}`
|
|
178
179
|
: `/${skill.ns}:${skill.name}`;
|
|
179
|
-
|
|
180
|
-
|
|
180
|
+
setLines([name]);
|
|
181
|
+
setCursor({ row: 0, col: name.length });
|
|
182
|
+
setOverlay('none');
|
|
183
|
+
setOverlayIdx(0);
|
|
181
184
|
}
|
|
182
185
|
function selectFile(file) {
|
|
183
186
|
const r = cursor.row;
|
|
@@ -428,10 +431,14 @@ export function InputArea({ status, skills, cwd, planningMode, permissionRequest
|
|
|
428
431
|
appendChar(input);
|
|
429
432
|
if (prospective.startsWith('/')) {
|
|
430
433
|
if (prospective.slice(1).includes(' ')) {
|
|
431
|
-
if (input === '@'
|
|
434
|
+
if (input === '@') {
|
|
435
|
+
filesLoadedRef.current = false;
|
|
432
436
|
setOverlay('at');
|
|
433
437
|
setOverlayIdx(0);
|
|
434
438
|
}
|
|
439
|
+
else if (overlay === 'at') {
|
|
440
|
+
setOverlay('at');
|
|
441
|
+
}
|
|
435
442
|
else {
|
|
436
443
|
setOverlay('none');
|
|
437
444
|
}
|
|
@@ -441,10 +448,14 @@ export function InputArea({ status, skills, cwd, planningMode, permissionRequest
|
|
|
441
448
|
setOverlayIdx(0);
|
|
442
449
|
}
|
|
443
450
|
}
|
|
444
|
-
else if (input === '@'
|
|
451
|
+
else if (input === '@') {
|
|
452
|
+
filesLoadedRef.current = false;
|
|
445
453
|
setOverlay('at');
|
|
446
454
|
setOverlayIdx(0);
|
|
447
455
|
}
|
|
456
|
+
else if (overlay === 'at' && atQuery !== '') {
|
|
457
|
+
setOverlay('at');
|
|
458
|
+
}
|
|
448
459
|
else if (overlay === 'command') {
|
|
449
460
|
setOverlay('none');
|
|
450
461
|
}
|