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.
@@ -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: 'Overwrite entire file — use only for new files or full rewrites',
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
- writeFile(guardPath(path), content);
53
- return `written: ${path}`;
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 a file use for targeted edits to existing files',
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
- if (count > 1)
70
- throw new Error(`ambiguous: ${count} matches found in ${path} — add more surrounding context to make unique`);
71
- writeFile(safe, current.replace(old, newStr));
72
- return `patched: ${path}`;
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
- - To modify an existing file: use patch_file with the exact old text and new replacement do NOT rewrite the whole file
288
- - To create a new file: use edit_file with full content in the <content> block
289
- - read_file before patch_file so you know the exact text to match
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
- await runLoop(next, depth + 1, goal);
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.0",
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
  },