miii-cli 0.3.4 → 0.3.5

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.
@@ -0,0 +1,131 @@
1
+ import { useCallback, useRef } from 'react';
2
+ import { generateId } from '../../types.js';
3
+ import { chat } from '../../llm/stream.js';
4
+ import { MicroQueue } from '../../tasks/queue.js';
5
+ import { fileEditContext } from '../../tasks/compactor.js';
6
+ import { StreamParser } from '../../parser/stream-parser.js';
7
+ import * as printer from '../printer.js';
8
+ export function useRefactor(deps) {
9
+ const depsRef = useRef(deps);
10
+ depsRef.current = deps;
11
+ const runRefactor = useCallback(async (goal) => {
12
+ const { config, currentModelRef, systemPromptRef, abortRef, macroQueueRef, executorRef, setStatus, setTaskLabel, setCurrentTool, pushHistory, } = depsRef.current;
13
+ printer.systemMsg(`refactor: ${goal}`);
14
+ setTaskLabel(`planning: ${goal}`);
15
+ setStatus('thinking');
16
+ const planCtx = [
17
+ { role: 'system', content: systemPromptRef.current },
18
+ {
19
+ role: 'user',
20
+ content: `Refactor goal: ${goal}\n\nList every file that needs to change. For each file output:\nFILE: <path>\nCHANGE: <one sentence describing the edit>\n\nUse list_files and read_file to discover relevant files first. Only list files that genuinely need changes.`,
21
+ },
22
+ ];
23
+ abortRef.current = new AbortController();
24
+ let planText = '';
25
+ await chat({
26
+ provider: config.provider,
27
+ model: currentModelRef.current,
28
+ baseUrl: config.baseUrl,
29
+ messages: planCtx,
30
+ signal: abortRef.current.signal,
31
+ async onDone(text) { planText = text; },
32
+ onError(err) { printer.errorMsg(err.message); },
33
+ });
34
+ if (!planText) {
35
+ setStatus('idle');
36
+ setTaskLabel(undefined);
37
+ return;
38
+ }
39
+ printer.assistantMsg(planText);
40
+ const filePlan = [];
41
+ const lines = planText.split('\n');
42
+ let lastPath = '';
43
+ for (const line of lines) {
44
+ const fm = line.match(/^FILE:\s*(.+)/);
45
+ const cm = line.match(/^CHANGE:\s*(.+)/);
46
+ if (fm)
47
+ lastPath = fm[1].trim();
48
+ if (cm && lastPath) {
49
+ filePlan.push({ path: lastPath, change: cm[1].trim() });
50
+ lastPath = '';
51
+ }
52
+ }
53
+ if (!filePlan.length) {
54
+ printer.systemMsg('no files identified in plan — done');
55
+ setStatus('idle');
56
+ setTaskLabel(undefined);
57
+ return;
58
+ }
59
+ printer.systemMsg(`plan: ${filePlan.length} file(s) to change`);
60
+ const micro = new MicroQueue();
61
+ for (const fp of filePlan) {
62
+ const t = { id: `read:${fp.path}`, priority: 1, tool: 'read_file', args: { path: fp.path }, deps: [], status: 'pending' };
63
+ micro.push(t);
64
+ }
65
+ const macro = {
66
+ id: generateId(),
67
+ goal,
68
+ priority: 0,
69
+ microtasks: micro.toArray(),
70
+ status: 'running',
71
+ };
72
+ macroQueueRef.current.enqueue(macro);
73
+ setTaskLabel(`reading ${filePlan.length} file(s)…`);
74
+ const readResults = await executorRef.current.drain(micro, ({ task, error }) => {
75
+ if (error)
76
+ printer.errorMsg(`read failed: ${task.args.path} — ${error}`);
77
+ else
78
+ printer.systemMsg(`read: ${task.args.path}`);
79
+ });
80
+ setTaskLabel(`applying changes…`);
81
+ const writeMicro = new MicroQueue();
82
+ for (const fp of filePlan) {
83
+ const readId = `read:${fp.path}`;
84
+ const fileContent = readResults.get(readId) ?? '';
85
+ if (!fileContent) {
86
+ printer.systemMsg(`skip (unreadable): ${fp.path}`);
87
+ continue;
88
+ }
89
+ setCurrentTool(`edit ${fp.path}`);
90
+ setTaskLabel(`editing: ${fp.path}`);
91
+ const editCtx = fileEditContext(systemPromptRef.current, goal, fp.path, fileContent, fp.change);
92
+ let editText = '';
93
+ await chat({
94
+ provider: config.provider,
95
+ model: currentModelRef.current,
96
+ baseUrl: config.baseUrl,
97
+ messages: editCtx,
98
+ signal: abortRef.current?.signal,
99
+ async onDone(text) { editText = text; },
100
+ onError(err) { printer.errorMsg(`edit LLM error: ${err.message}`); },
101
+ });
102
+ if (!editText)
103
+ continue;
104
+ printer.assistantMsg(editText);
105
+ const parser = new StreamParser();
106
+ for (const item of [...parser.feed(editText), ...parser.flush()]) {
107
+ if (item.type === 'tool_call') {
108
+ writeMicro.push({ id: generateId(), priority: 2, tool: item.toolName, args: item.toolArgs, deps: [], status: 'pending' });
109
+ }
110
+ }
111
+ }
112
+ if (writeMicro.size > 0) {
113
+ setTaskLabel(`writing ${writeMicro.size} change(s)…`);
114
+ await executorRef.current.drain(writeMicro, ({ task, result, error }) => {
115
+ if (error)
116
+ printer.errorMsg(`${task.tool} failed: ${error}`);
117
+ else
118
+ printer.toolMsg(task.tool, result ?? '');
119
+ });
120
+ }
121
+ macro.status = 'done';
122
+ macroQueueRef.current.dequeue();
123
+ setCurrentTool(undefined);
124
+ setTaskLabel(undefined);
125
+ setStatus('idle');
126
+ printer.systemMsg(`refactor done — ${filePlan.length} file(s) processed`);
127
+ pushHistory({ role: 'user', content: `[refactor] ${goal}` });
128
+ pushHistory({ role: 'assistant', content: planText });
129
+ }, []);
130
+ return { runRefactor };
131
+ }
@@ -0,0 +1,382 @@
1
+ import { useCallback, useRef } from 'react';
2
+ import { readFile } from '../../files/ops.js';
3
+ import { getSystemPrompt } from '../../tools/index.js';
4
+ import { loadSession, saveSession, listSessions, deleteSession, deleteAllSessions } from '../../sessions.js';
5
+ import { runDeepThink } from '../deepThink.js';
6
+ import { buildGitContext, looksCodeRelated } from '../git-context.js';
7
+ import { getTavilyKey, saveTavilyKey } from '../../tavily/client.js';
8
+ import { buildIndex } from '../../index/indexer.js';
9
+ import { indexStats, clearIndex } from '../../index/store.js';
10
+ import { embed } from '../../index/embedder.js';
11
+ import { loadIndex } from '../../index/store.js';
12
+ import { topK } from '../../index/search.js';
13
+ import * as printer from '../printer.js';
14
+ function buildAtContext(text) {
15
+ const refs = [...text.matchAll(/@([\w./\-]+)/g)];
16
+ if (!refs.length)
17
+ return '';
18
+ const parts = [];
19
+ for (const m of refs) {
20
+ try {
21
+ const content = readFile(m[1]);
22
+ if (content)
23
+ parts.push(`<file path="${m[1]}">\n${content}\n</file>`);
24
+ }
25
+ catch { }
26
+ }
27
+ return parts.length ? parts.join('\n\n') + '\n\n' : '';
28
+ }
29
+ export function useSubmit(deps) {
30
+ const depsRef = useRef(deps);
31
+ depsRef.current = deps;
32
+ const handleSubmit = useCallback(async (text) => {
33
+ const { config, skills, cwd, version, currentModelRef, setCurrentModel, historyRef, sessionNameRef, saveTimerRef, systemPromptRef, abortRef, planningMode, setPlanningMode, runLoop, buildContext, pushHistory, setSessionName, renameFromMessage, openPicker, setStatus, setTaskLabel, setCurrentTool, runRefactor, handleGit, lastGitStatusRef, } = depsRef.current;
34
+ const cmd = text.trim();
35
+ if (cmd === '?') {
36
+ printer.systemMsg('shortcuts:\n' +
37
+ ' enter send message\n' +
38
+ ' ctrl+j insert newline\n' +
39
+ ' ↑ / ↓ history navigate\n' +
40
+ ' ctrl+a / ctrl+e line start / end\n' +
41
+ ' ctrl+w delete word back\n' +
42
+ ' ctrl+k kill to line end\n' +
43
+ ' ctrl+u clear line\n' +
44
+ ' ctrl+← / → jump word\n' +
45
+ ' @filename inject file into context\n' +
46
+ ' /cmd open command palette\n' +
47
+ ' esc abort / clear input\n' +
48
+ ' ctrl+c abort / exit');
49
+ return;
50
+ }
51
+ if (cmd === '/version') {
52
+ printer.systemMsg(`miii v${version ?? 'unknown'}`);
53
+ return;
54
+ }
55
+ if (cmd === '/tavily-key' || cmd.startsWith('/tavily-key ')) {
56
+ const key = cmd.slice(11).trim();
57
+ if (!key) {
58
+ const existing = getTavilyKey();
59
+ printer.systemMsg(existing ? 'Tavily key set (use /tavily-key <key> to update)' : 'No Tavily key set. Usage: /tavily-key tvly-...');
60
+ return;
61
+ }
62
+ if (!key.startsWith('tvly-')) {
63
+ printer.systemMsg('Key should start with tvly-. Get yours at https://tavily.com');
64
+ return;
65
+ }
66
+ saveTavilyKey(key);
67
+ printer.systemMsg('Tavily API key saved to ~/.config/miii/tavily.key (mode 600)');
68
+ return;
69
+ }
70
+ if (cmd === '/skills' || cmd.startsWith('/skills ')) {
71
+ const sub = cmd.slice(7).trim();
72
+ if (!sub || sub === 'list') {
73
+ const pkgs = skills.listNpmSkills();
74
+ printer.systemMsg(pkgs.length ? `installed npm skills:\n${pkgs.map(p => ` ${p}`).join('\n')}` : 'no npm skills installed — try /skills install <name>');
75
+ return;
76
+ }
77
+ if (sub.startsWith('install ')) {
78
+ const pkg = sub.slice(8).trim();
79
+ if (!pkg) {
80
+ printer.systemMsg('usage: /skills install <name> (e.g. /skills install git-summary)');
81
+ return;
82
+ }
83
+ printer.systemMsg(`installing miii-skill-${pkg}…`);
84
+ try {
85
+ printer.systemMsg(await skills.installSkill(pkg));
86
+ }
87
+ catch (e) {
88
+ printer.errorMsg(String(e));
89
+ }
90
+ return;
91
+ }
92
+ if (sub.startsWith('uninstall ')) {
93
+ const pkg = sub.slice(10).trim();
94
+ if (!pkg) {
95
+ printer.systemMsg('usage: /skills uninstall <name>');
96
+ return;
97
+ }
98
+ try {
99
+ printer.systemMsg(await skills.uninstallSkill(pkg));
100
+ }
101
+ catch (e) {
102
+ printer.errorMsg(String(e));
103
+ }
104
+ return;
105
+ }
106
+ printer.systemMsg('usage: /skills install <name> | /skills uninstall <name> | /skills list');
107
+ return;
108
+ }
109
+ if (cmd === '/model' || cmd.startsWith('/model ')) {
110
+ const name = cmd.slice(6).trim();
111
+ if (!name) {
112
+ printer.systemMsg(`current model: ${currentModelRef.current}`);
113
+ return;
114
+ }
115
+ setCurrentModel(name);
116
+ currentModelRef.current = name;
117
+ printer.systemMsg(`model → ${name}`);
118
+ return;
119
+ }
120
+ if (cmd === '/models') {
121
+ await openPicker();
122
+ return;
123
+ }
124
+ if (cmd === '/new') {
125
+ if (saveTimerRef.current) {
126
+ clearTimeout(saveTimerRef.current);
127
+ saveTimerRef.current = null;
128
+ }
129
+ saveSession(sessionNameRef.current, historyRef.current);
130
+ const newName = new Date().toISOString().slice(0, 19).replace(/[:T]/g, '-');
131
+ historyRef.current = [];
132
+ setSessionName(newName);
133
+ setPlanningMode(false);
134
+ systemPromptRef.current = getSystemPrompt(`\n- CWD: ${cwd}`);
135
+ printer.systemMsg(`new session → ${newName}`);
136
+ return;
137
+ }
138
+ if (cmd === '/clear') {
139
+ historyRef.current = [];
140
+ saveSession(sessionNameRef.current, []);
141
+ setPlanningMode(false);
142
+ systemPromptRef.current = getSystemPrompt(`\n- CWD: ${cwd}`);
143
+ printer.systemMsg('chat cleared');
144
+ return;
145
+ }
146
+ if (cmd === '/exit') {
147
+ process.exit(0);
148
+ }
149
+ if (cmd === '/git' || cmd.startsWith('/git ')) {
150
+ await handleGit(cmd.slice(4).trim());
151
+ return;
152
+ }
153
+ if (cmd.startsWith('/refactor ') || cmd === '/refactor') {
154
+ const goal = cmd.slice(9).trim();
155
+ if (!goal) {
156
+ printer.systemMsg('usage: /refactor <goal>');
157
+ return;
158
+ }
159
+ await runRefactor(goal);
160
+ return;
161
+ }
162
+ if (cmd.startsWith('/think ') || cmd === '/think') {
163
+ const query = cmd.slice(6).trim();
164
+ if (!query) {
165
+ printer.systemMsg('usage: /think <query>');
166
+ return;
167
+ }
168
+ printer.userMsg(`/think ${query}`);
169
+ setStatus('thinking');
170
+ setTaskLabel(`gathering: ${query}`);
171
+ abortRef.current = new AbortController();
172
+ try {
173
+ const result = await runDeepThink(query, config, currentModelRef.current, abortRef.current.signal, (toolName) => setCurrentTool(`gather:${toolName}`));
174
+ setCurrentTool(undefined);
175
+ printer.systemMsg(`gathered: ${result.toolCalls} tool call(s), ${result.webCalls} web call(s)`);
176
+ if (result.findings) {
177
+ pushHistory({ role: 'user', content: `/think ${query}` });
178
+ pushHistory({ role: 'assistant', content: result.findings });
179
+ pushHistory({ role: 'user', content: `Based on your research above, give a complete answer to: ${query}` });
180
+ await runLoop(buildContext(), 0, query);
181
+ }
182
+ else {
183
+ printer.systemMsg('nothing gathered — try rephrasing');
184
+ setStatus('idle');
185
+ }
186
+ }
187
+ catch (e) {
188
+ printer.errorMsg(`deep think failed: ${e}`);
189
+ setStatus('idle');
190
+ }
191
+ finally {
192
+ setCurrentTool(undefined);
193
+ setTaskLabel(undefined);
194
+ }
195
+ return;
196
+ }
197
+ if (cmd === '/plan' || cmd.startsWith('/plan ')) {
198
+ const topic = cmd.slice(5).trim();
199
+ setPlanningMode(true);
200
+ systemPromptRef.current = getSystemPrompt(`\n- CWD: ${cwd}\n- MODE: Planning assistant. Help the user plan step by step. Ask clarifying questions. Suggest concrete next steps. Use plain text only — no markdown, no headers, no bold, no bullets with asterisks, no backtick blocks. Use numbered lists and plain indentation for structure.`);
201
+ const msg = topic ? `I want to plan: ${topic}` : 'I want to start planning. Help me think through my goals step by step.';
202
+ printer.userMsg(msg);
203
+ pushHistory({ role: 'user', content: msg });
204
+ await runLoop(buildContext());
205
+ return;
206
+ }
207
+ if (cmd === '/plan:done') {
208
+ setPlanningMode(false);
209
+ systemPromptRef.current = getSystemPrompt(`\n- CWD: ${cwd}`);
210
+ printer.systemMsg('planning mode off');
211
+ return;
212
+ }
213
+ if (cmd.startsWith('/plan:')) {
214
+ const subCmd = cmd.slice(6);
215
+ const subPrompts = {
216
+ next: 'What are the next concrete steps I should take?',
217
+ breakdown: 'Can you break this down into specific subtasks?',
218
+ review: 'Please review and critique our plan so far. What are we missing?',
219
+ };
220
+ const msg = subPrompts[subCmd];
221
+ if (msg) {
222
+ printer.userMsg(msg);
223
+ pushHistory({ role: 'user', content: msg });
224
+ await runLoop(buildContext());
225
+ return;
226
+ }
227
+ }
228
+ if (cmd === '/sessions') {
229
+ const sessions = listSessions();
230
+ if (!sessions.length) {
231
+ printer.systemMsg('no saved sessions');
232
+ return;
233
+ }
234
+ printer.systemMsg(sessions.map(s => `${s.name === sessionNameRef.current ? '▶ ' : ' '}${s.name} (${s.messageCount} msgs)`).join('\n'));
235
+ return;
236
+ }
237
+ if (cmd.startsWith('/session')) {
238
+ const arg = cmd.slice(8).trim();
239
+ if (!arg) {
240
+ printer.systemMsg(`current: ${sessionNameRef.current}`);
241
+ return;
242
+ }
243
+ if (arg.startsWith('delete ')) {
244
+ const target = arg.slice(7).trim();
245
+ if (!target) {
246
+ printer.systemMsg('usage: /session delete <name|all>');
247
+ return;
248
+ }
249
+ if (target === 'all') {
250
+ const count = deleteAllSessions(sessionNameRef.current);
251
+ printer.systemMsg(`deleted ${count} session(s) — kept active: ${sessionNameRef.current}`);
252
+ return;
253
+ }
254
+ if (target === sessionNameRef.current) {
255
+ printer.systemMsg('cannot delete active session — switch first');
256
+ return;
257
+ }
258
+ try {
259
+ deleteSession(target);
260
+ printer.systemMsg(`deleted: ${target}`);
261
+ }
262
+ catch (e) {
263
+ printer.errorMsg(`delete failed: ${String(e)}`);
264
+ }
265
+ return;
266
+ }
267
+ if (saveTimerRef.current) {
268
+ clearTimeout(saveTimerRef.current);
269
+ saveTimerRef.current = null;
270
+ }
271
+ saveSession(sessionNameRef.current, historyRef.current);
272
+ historyRef.current = loadSession(arg);
273
+ setSessionName(arg);
274
+ printer.systemMsg(`session → ${arg} (${historyRef.current.length} messages)`);
275
+ return;
276
+ }
277
+ if (cmd === '/index' || cmd.startsWith('/index ')) {
278
+ const sub = cmd.slice(6).trim();
279
+ if (!sub || sub === 'status') {
280
+ const stats = indexStats(cwd);
281
+ if (!stats) {
282
+ printer.systemMsg('no index — run /index build');
283
+ return;
284
+ }
285
+ const age = Math.round((Date.now() - stats.mtime) / 60000);
286
+ printer.systemMsg(`index: ${stats.count} chunks, ${stats.sizeKb} KB, built ${age < 2 ? 'just now' : `${age}m ago`}`);
287
+ return;
288
+ }
289
+ if (sub === 'build') {
290
+ const embedModel = config.embedModel ?? 'nomic-embed-text';
291
+ printer.systemMsg(`building index with ${embedModel}…`);
292
+ setStatus('thinking');
293
+ setTaskLabel('indexing codebase…');
294
+ try {
295
+ const result = await buildIndex(config, cwd, ({ file, done, total }) => {
296
+ setTaskLabel(`indexing ${done + 1}/${total}: ${file}`);
297
+ });
298
+ printer.systemMsg(`index built: ${result.indexed} chunks across ${result.files} files${result.skipped ? `, ${result.skipped} embed errors` : ''}`);
299
+ }
300
+ catch (e) {
301
+ printer.errorMsg(`index build failed: ${e}`);
302
+ }
303
+ finally {
304
+ setStatus('idle');
305
+ setTaskLabel(undefined);
306
+ }
307
+ return;
308
+ }
309
+ if (sub === 'clear') {
310
+ clearIndex(cwd);
311
+ printer.systemMsg('index cleared');
312
+ return;
313
+ }
314
+ if (sub.startsWith('search ')) {
315
+ const query = sub.slice(7).trim();
316
+ if (!query) {
317
+ printer.systemMsg('usage: /index search <query>');
318
+ return;
319
+ }
320
+ const chunks = loadIndex(cwd);
321
+ if (!chunks.length) {
322
+ printer.systemMsg('no index — run /index build');
323
+ return;
324
+ }
325
+ const embedModel = config.embedModel ?? 'nomic-embed-text';
326
+ try {
327
+ const queryVec = await embed(config.baseUrl, embedModel, query);
328
+ const results = topK(chunks, queryVec, 5);
329
+ printer.systemMsg(results.map((r, i) => `[${i + 1}] ${r.file} lines ${r.start + 1}–${r.end + 1} (score ${r.score.toFixed(3)})`).join('\n'));
330
+ }
331
+ catch (e) {
332
+ printer.errorMsg(`search failed: ${e}`);
333
+ }
334
+ return;
335
+ }
336
+ printer.systemMsg('usage: /index build | /index status | /index search <query> | /index clear');
337
+ return;
338
+ }
339
+ if (text.startsWith('/')) {
340
+ const [slashCmd, ...rest] = text.slice(1).split(' ');
341
+ const skill = skills.get(slashCmd);
342
+ if (skill) {
343
+ if (skill.name === 'list') {
344
+ printer.systemMsg(skills.list().map(s => `/${s.ns === 'default' ? '' : s.ns + ':'}${s.name} — ${s.description}`).join('\n'));
345
+ return;
346
+ }
347
+ if (skill.execute) {
348
+ const ctx = {
349
+ messages: historyRef.current.map(m => ({ role: m.role, content: m.content })),
350
+ appendMessage: (_role, content) => printer.systemMsg(content),
351
+ setSystemPrompt: (p) => { systemPromptRef.current = p; },
352
+ getSystemPrompt: () => systemPromptRef.current,
353
+ };
354
+ const result = await skill.execute(rest.join(' '), ctx);
355
+ if (result)
356
+ printer.systemMsg(result);
357
+ return;
358
+ }
359
+ if (skill.prompt) {
360
+ printer.userMsg(skill.prompt);
361
+ pushHistory({ role: 'user', content: skill.prompt });
362
+ await runLoop(buildContext());
363
+ return;
364
+ }
365
+ }
366
+ printer.systemMsg(`unknown command: /${slashCmd} — try /list`);
367
+ return;
368
+ }
369
+ renameFromMessage(text);
370
+ const contextPrefix = buildAtContext(text);
371
+ const shouldInjectGit = config.gitContext !== false && looksCodeRelated(text);
372
+ const { prefix: gitPrefix, label: gitLabel } = shouldInjectGit
373
+ ? await buildGitContext(cwd, lastGitStatusRef)
374
+ : { prefix: '', label: '' };
375
+ if (gitLabel)
376
+ printer.systemMsg(gitLabel);
377
+ printer.userMsg(text);
378
+ pushHistory({ role: 'user', content: gitPrefix + contextPrefix + text });
379
+ await runLoop(buildContext());
380
+ }, []);
381
+ return { handleSubmit };
382
+ }
@@ -132,10 +132,12 @@ export function welcome(provider, model, cwd, version, updateAvailable, linked)
132
132
  const pr = [...rightLines, ...Array(Math.max(0, maxLen - rightLines.length)).fill('')];
133
133
  const contentRows = pl.map((l, i) => row(l, pr[i]));
134
134
  const upgradeCmd = linked ? 'cd <miii-dir> && npm run build' : 'npm install -g miii-cli';
135
- const separator = gray('│') + bold(yellow(' ⬆ update available: v' + updateAvailable + ' — run: ' + upgradeCmd)).padEnd(innerW - 1) + gray('│');
136
- const updateRow = updateAvailable
137
- ? [gray('├' + '─'.repeat(innerW) + '┤'), separator, gray('├' + '─'.repeat(innerW) + '┤')]
138
- : [];
135
+ const updateRow = updateAvailable ? (() => {
136
+ const updateText = bold(yellow(` ⬆ update available: v${updateAvailable} — run: ${upgradeCmd}`));
137
+ const pad = Math.max(0, innerW - vis(updateText).length);
138
+ const separator = gray('│') + updateText + ' '.repeat(pad) + gray('│');
139
+ return [gray('├' + '─'.repeat(innerW) + '┤'), separator, gray('├' + '─'.repeat(innerW) + '┤')];
140
+ })() : [];
139
141
  const lines = [
140
142
  top,
141
143
  ...contentRows,
@@ -162,10 +164,37 @@ export function assistantMsg(text) {
162
164
  }
163
165
  const EDIT_TOOLS = new Set(['edit_file', 'patch_file', 'create_file', 'write_file']);
164
166
  const DELETE_TOOLS = new Set(['delete_file', 'remove_file']);
167
+ function toolLabel(name, args) {
168
+ const a = args;
169
+ const short = (s, n = 55) => s.length > n ? s.slice(0, n) + '…' : s;
170
+ switch (name) {
171
+ case 'read_file': return `Reading ${a.path ?? ''}`;
172
+ case 'list_files': return `Listing ${a.path || '.'}`;
173
+ case 'create_file': return `Creating ${a.path ?? ''}`;
174
+ case 'edit_file': return `Writing ${a.path ?? ''}`;
175
+ case 'patch_file': return `Editing ${a.path ?? ''}`;
176
+ case 'delete_file': return `Deleting ${a.path ?? ''}`;
177
+ case 'move_file': return `Moving ${a.from} → ${a.to}`;
178
+ case 'create_folder': return `Creating folder ${a.path ?? ''}`;
179
+ case 'run_command': return `Running ${short(a.command ?? '')}`;
180
+ case 'git_status': return 'Checking git status';
181
+ case 'git_diff': return 'Reading diff';
182
+ case 'git_log': return 'Reading commits';
183
+ case 'git_commit': return `Committing: ${short(a.message ?? '')}`;
184
+ case 'run_tests': return a.path ? `Running tests › ${a.path}` : 'Running tests';
185
+ case 'web_search': return `Searching: ${short(a.query ?? '')}`;
186
+ case 'web_extract': return `Extracting page`;
187
+ case 'deep_think': return `Researching: ${short(a.query ?? '')}`;
188
+ case 'search_codebase': return `Searching codebase: ${short(a.query ?? '')}`;
189
+ default: {
190
+ const s = toolArgSummary(args);
191
+ return s ? `${name} ${s}` : name;
192
+ }
193
+ }
194
+ }
165
195
  export function toolCallStart(name, args) {
166
- const summary = toolArgSummary(args);
167
196
  const dot = DELETE_TOOLS.has(name) ? red('●') : EDIT_TOOLS.has(name) ? green('●') : blue('●');
168
- write(` ${dot} ${cyan(name)}${summary ? gray('(' + summary + ')') : ''}\n`);
197
+ write(` ${dot} ${toolLabel(name, args)}\n`);
169
198
  }
170
199
  export function toolMsg(name, result) {
171
200
  const preview = result.length > 250 ? result.slice(0, 250) + '…' : result;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "miii-cli",
3
- "version": "0.3.4",
3
+ "version": "0.3.5",
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",