miii-cli 1.1.0 → 1.1.2
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/dist/tools/index.js +40 -14
- package/dist/tui/hooks/useRunLoop.js +40 -1
- package/package.json +2 -1
package/dist/tools/index.js
CHANGED
|
@@ -34,7 +34,7 @@ export const tools = [
|
|
|
34
34
|
},
|
|
35
35
|
{
|
|
36
36
|
name: 'create_file',
|
|
37
|
-
description: 'Create a new file — fails if file already exists',
|
|
37
|
+
description: 'Create a new file with content — fails if file already exists. Prefer edit_file for new files.',
|
|
38
38
|
params: '{"path": "string", "content": "string"}',
|
|
39
39
|
execute: async ({ path, content }) => {
|
|
40
40
|
const safe = guardPath(path);
|
|
@@ -46,16 +46,24 @@ export const tools = [
|
|
|
46
46
|
},
|
|
47
47
|
{
|
|
48
48
|
name: 'edit_file',
|
|
49
|
-
description: '
|
|
49
|
+
description: 'Write a new file — only for files that do not exist yet. Use patch_file to modify existing files.',
|
|
50
50
|
params: '{"path": "string", "content": "string"}',
|
|
51
51
|
execute: async ({ path, content }) => {
|
|
52
|
-
|
|
53
|
-
|
|
52
|
+
const safe = guardPath(path);
|
|
53
|
+
if (existsSync(safe)) {
|
|
54
|
+
throw new Error(`edit_file cannot overwrite existing file: ${path}\n` +
|
|
55
|
+
`Use patch_file with <old> and <new> blocks to make targeted edits.\n` +
|
|
56
|
+
`Call read_file first to get the exact current text.`);
|
|
57
|
+
}
|
|
58
|
+
const text = content;
|
|
59
|
+
writeFile(safe, text);
|
|
60
|
+
const lines = text.split('\n').length;
|
|
61
|
+
return `created: ${path} (${lines} line${lines === 1 ? '' : 's'})`;
|
|
54
62
|
},
|
|
55
63
|
},
|
|
56
64
|
{
|
|
57
65
|
name: 'patch_file',
|
|
58
|
-
description: 'Replace an exact string in
|
|
66
|
+
description: 'Replace an exact unique string in an existing file. Always call read_file first to get the exact text.',
|
|
59
67
|
params: '{"path": "string", "old": "string", "new": "string"}',
|
|
60
68
|
execute: async ({ path, old: oldStr, new: newStr }) => {
|
|
61
69
|
const safe = guardPath(path);
|
|
@@ -64,12 +72,29 @@ export const tools = [
|
|
|
64
72
|
throw new Error(`file not found or empty: ${path}`);
|
|
65
73
|
const old = oldStr;
|
|
66
74
|
const count = current.split(old).length - 1;
|
|
67
|
-
if (count === 0)
|
|
68
|
-
throw new Error(`old text not found in ${path}`
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
75
|
+
if (count === 0) {
|
|
76
|
+
throw new Error(`old text not found in ${path} — file may have changed since last read.\n` +
|
|
77
|
+
`Call read_file again to get current content, then retry with exact matching text.`);
|
|
78
|
+
}
|
|
79
|
+
if (count > 1) {
|
|
80
|
+
throw new Error(`ambiguous: ${count} matches found in ${path} — extend <old> block with more surrounding lines to make it unique`);
|
|
81
|
+
}
|
|
82
|
+
const updated = current.replace(old, newStr);
|
|
83
|
+
writeFile(safe, updated);
|
|
84
|
+
// Compute affected line range for the snippet
|
|
85
|
+
const startLine = current.slice(0, current.indexOf(old)).split('\n').length;
|
|
86
|
+
const oldLines = old.split('\n').length;
|
|
87
|
+
const newLines = newStr.split('\n').length;
|
|
88
|
+
const updatedArr = updated.split('\n');
|
|
89
|
+
const snippetStart = Math.max(0, startLine - 3);
|
|
90
|
+
const snippetEnd = Math.min(updatedArr.length, startLine + newLines + 2);
|
|
91
|
+
const snippet = updatedArr
|
|
92
|
+
.slice(snippetStart, snippetEnd)
|
|
93
|
+
.map((l, i) => `${String(snippetStart + i + 1).padStart(4)} │ ${l}`)
|
|
94
|
+
.join('\n');
|
|
95
|
+
const delta = newLines - oldLines;
|
|
96
|
+
const deltaStr = delta === 0 ? '' : delta > 0 ? ` (+${delta} line${delta === 1 ? '' : 's'})` : ` (${delta} line${Math.abs(delta) === 1 ? '' : 's'})`;
|
|
97
|
+
return `patched: ${path}${deltaStr}\n\nLines ${snippetStart + 1}–${snippetEnd}:\n${snippet}`;
|
|
73
98
|
},
|
|
74
99
|
},
|
|
75
100
|
{
|
|
@@ -284,9 +309,10 @@ ${toolDocs}
|
|
|
284
309
|
${deepThinkDoc}
|
|
285
310
|
|
|
286
311
|
Rules:
|
|
287
|
-
-
|
|
288
|
-
- To
|
|
289
|
-
-
|
|
312
|
+
- edit_file only works on NEW files — it throws an error if the file exists. Never call it on existing files
|
|
313
|
+
- To modify any existing file: call read_file first, then patch_file with the exact text from that read as the <old> block
|
|
314
|
+
- Never guess or reuse old text from earlier in the conversation — always re-read immediately before patching
|
|
315
|
+
- If patch_file reports "old text not found", call read_file again and retry with the exact current text
|
|
290
316
|
- Never delete without confirming
|
|
291
317
|
- Use git_status and git_diff before any refactor to understand what has already changed
|
|
292
318
|
- Use git_log to understand recent history before suggesting changes
|
|
@@ -155,12 +155,35 @@ export function useRunLoop(config, currentModelRef, pushHistory, extraTools = []
|
|
|
155
155
|
}
|
|
156
156
|
if (tool) {
|
|
157
157
|
try {
|
|
158
|
+
// Guard: for patch_file, verify old text still matches before executing.
|
|
159
|
+
// If stale, inject fresh file content and skip — model will retry.
|
|
160
|
+
if (tc.name === 'patch_file') {
|
|
161
|
+
const filePath = tc.args.path;
|
|
162
|
+
const oldText = tc.args.old;
|
|
163
|
+
if (filePath && oldText && existsSync(filePath)) {
|
|
164
|
+
const current = readFileSync(filePath, 'utf-8');
|
|
165
|
+
if (!current.includes(oldText)) {
|
|
166
|
+
printer.errorMsg(`patch stale: old text not found in ${filePath} — injecting fresh content`);
|
|
167
|
+
next.push({ role: 'user', content: `Tool read_file result:\n${current}` });
|
|
168
|
+
next.push({ role: 'user', content: `patch_file failed: old text not found in ${filePath}. The file content above is the current state. Retry patch_file with the correct exact text.` });
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
158
173
|
printer.toolCallStart(tc.name, tc.args);
|
|
159
174
|
const result = await tool.execute(tc.args);
|
|
160
175
|
printer.toolResultSummary(tc.name, tc.args, result);
|
|
161
176
|
if (SHOW_RESULT_TOOLS.has(tc.name))
|
|
162
177
|
printer.toolMsg(tc.name, result);
|
|
163
178
|
next.push({ role: 'user', content: `Tool ${tc.name} result:\n${result}` });
|
|
179
|
+
// After any file edit, inject fresh file state so next tool sees actual content
|
|
180
|
+
if (FILE_EDIT_TOOLS.has(tc.name)) {
|
|
181
|
+
const filePath = tc.args.path;
|
|
182
|
+
if (filePath && existsSync(filePath)) {
|
|
183
|
+
const fresh = readFileSync(filePath, 'utf-8');
|
|
184
|
+
next.push({ role: 'user', content: `[current state of ${filePath} after edit]\n${fresh}` });
|
|
185
|
+
}
|
|
186
|
+
}
|
|
164
187
|
}
|
|
165
188
|
catch (e) {
|
|
166
189
|
const err = `Tool ${tc.name} error: ${e}`;
|
|
@@ -202,7 +225,23 @@ export function useRunLoop(config, currentModelRef, pushHistory, extraTools = []
|
|
|
202
225
|
}
|
|
203
226
|
}
|
|
204
227
|
}
|
|
205
|
-
|
|
228
|
+
// For file-edit turns: slim context (system + goal + fresh file states + recent results)
|
|
229
|
+
// For non-edit turns: full next (model needs full conversational context)
|
|
230
|
+
if (didEditFiles) {
|
|
231
|
+
const systemMsg = msgs.find(m => m.role === 'system');
|
|
232
|
+
const goalMsg = msgs.find(m => m.role === 'user' && !m.content.startsWith('[') && !m.content.startsWith('Tool '));
|
|
233
|
+
const batchStart = msgs.length + 1; // index in next where this batch's messages start
|
|
234
|
+
const batchMsgs = next.slice(batchStart);
|
|
235
|
+
const slimCtx = [
|
|
236
|
+
...(systemMsg ? [systemMsg] : []),
|
|
237
|
+
...(goalMsg ? [goalMsg] : []),
|
|
238
|
+
...batchMsgs,
|
|
239
|
+
];
|
|
240
|
+
await runLoop(slimCtx, depth + 1, goal);
|
|
241
|
+
}
|
|
242
|
+
else {
|
|
243
|
+
await runLoop(next, depth + 1, goal);
|
|
244
|
+
}
|
|
206
245
|
},
|
|
207
246
|
onError(err) {
|
|
208
247
|
if (err.name !== 'AbortError')
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "miii-cli",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "The high-performance local AI coding agent for your terminal. Automate complex workflows with local LLMs.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
"url": "https://github.com/maruakshay/miii-cli.git"
|
|
13
13
|
},
|
|
14
14
|
"homepage": "https://www.miii.in",
|
|
15
|
+
"readme": "README.md",
|
|
15
16
|
"bugs": {
|
|
16
17
|
"url": "https://github.com/maruakshay/miii-cli/issues"
|
|
17
18
|
},
|