miii-cli 0.2.3 → 0.2.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.
@@ -1,53 +1,29 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useState, useCallback, useRef, useEffect } from 'react';
2
+ import { useState, useCallback, useRef } from 'react';
3
3
  import { Box, Text, useStdout } from 'ink';
4
4
  import { InputArea } from './components/InputArea.js';
5
5
  import { ModelPicker } from './components/ModelPicker.js';
6
6
  import { Divider } from './components/StatusBar.js';
7
- import { chat } from '../llm/stream.js';
8
- import { listModels, pullModel } from '../llm/ollama.js';
9
- import { StreamParser } from '../parser/stream-parser.js';
10
- import { tools, getSystemPrompt } from '../tools/index.js';
7
+ import { tools } from '../tools/index.js';
11
8
  import { readFile } from '../files/ops.js';
12
- import { resolve } from 'path';
9
+ import { generateId } from '../types.js';
13
10
  import * as printer from './printer.js';
14
11
  import { loadSession, saveSession, listSessions } from '../sessions.js';
15
- import { shouldCompact, compactContext, fileEditContext } from '../tasks/compactor.js';
16
12
  import { MacroQueue, MicroQueue } from '../tasks/queue.js';
17
13
  import { TaskExecutor } from '../tasks/executor.js';
18
- import { generateId } from '../types.js';
14
+ import { fileEditContext } from '../tasks/compactor.js';
15
+ import { StreamParser } from '../parser/stream-parser.js';
16
+ import { chat } from '../llm/stream.js';
19
17
  import { exec } from 'child_process';
20
18
  import { promisify } from 'util';
19
+ import { getTavilyKey, saveTavilyKey } from '../tavily/client.js';
20
+ import { getSystemPrompt } from '../tools/index.js';
21
+ import { THINKING_PHRASES, SPARKLE } from './thinking.js';
22
+ import { buildGitContext, looksCodeRelated } from './git-context.js';
23
+ import { useSession } from './hooks/useSession.js';
24
+ import { useModelPicker } from './hooks/useModelPicker.js';
25
+ import { useRunLoop } from './hooks/useRunLoop.js';
21
26
  const gitRun = promisify(exec);
22
- const MAX_TOOL_DEPTH = 6;
23
- const THINKING_PHRASES = [
24
- 'oh wow, a question. let me pretend to care…',
25
- 'consulting the void…',
26
- 'making something up, just a sec…',
27
- 'definitely not hallucinating right now…',
28
- 'running 47 mental tabs…',
29
- 'staring into the abyss (it blinked)…',
30
- 'calculating your fate, no pressure…',
31
- 'doing the thinking you pay me for…',
32
- 'processing your questionable life choices…',
33
- 'summoning coherent thoughts, rarely works…',
34
- 'asking my imaginary friend for help…',
35
- 'pretending this is a hard problem…',
36
- 'yes, yes, very interesting. anyway…',
37
- 'googling it (not really, I can\'t)…',
38
- 'simulating intelligence… please wait…',
39
- 'having a brief existential crisis…',
40
- 'cross-referencing vibes…',
41
- 'totally not making this up…',
42
- 'the answer is 42. now finding the question…',
43
- 'my other tab is loading…',
44
- 'channelling the spirit of stack overflow…',
45
- 'trying not to confidently be wrong…',
46
- 'applying artificial to the intelligence…',
47
- 'phoning a friend who also doesn\'t know…',
48
- 'checking if this is even my problem to solve…',
49
- ];
50
- const SPARKLE = ['✦', '✧', '✶', '✷', '✸', '✹'];
51
27
  function buildAtContext(text) {
52
28
  const refs = [...text.matchAll(/@([\w./\-]+)/g)];
53
29
  if (!refs.length)
@@ -63,238 +39,21 @@ function buildAtContext(text) {
63
39
  }
64
40
  return parts.length ? parts.join('\n\n') + '\n\n' : '';
65
41
  }
66
- const CODE_PATTERN = /\.(ts|js|tsx|jsx|py|go|rs|java|rb|sh|css|html|json|yaml|yml)\b|function|class|import|export|const|let|var|def |async|await|error|bug|fix|refactor|implement|`[^`]+`/i;
67
- function looksCodeRelated(text) {
68
- return text.length >= 10 && CODE_PATTERN.test(text);
69
- }
70
- async function buildGitContext(cwd, lastStatusRef) {
71
- try {
72
- const { stdout } = await gitRun('git status --short', { cwd, timeout: 5000 });
73
- const status = stdout.trim();
74
- if (!status || status === lastStatusRef.current)
75
- return { prefix: '', label: '' };
76
- lastStatusRef.current = status;
77
- const MAX_TOTAL = 40_000;
78
- const MAX_FILE = 15_000;
79
- let total = 0;
80
- const parts = [];
81
- const skipped = [];
82
- for (const line of status.split('\n')) {
83
- const code = line.slice(0, 2);
84
- if (code.includes('D'))
85
- continue;
86
- const raw = line.slice(3).trim().replace(/^"|"$/g, '');
87
- const rel = raw.includes(' -> ') ? raw.split(' -> ')[1] : raw;
88
- if (!rel)
89
- continue;
90
- try {
91
- const content = readFile(resolve(cwd, rel));
92
- if (!content || content.length > MAX_FILE) {
93
- skipped.push(rel);
94
- continue;
95
- }
96
- total += content.length;
97
- if (total > MAX_TOTAL) {
98
- skipped.push(rel);
99
- continue;
100
- }
101
- parts.push(`<file path="${rel}">\n${content}\n</file>`);
102
- }
103
- catch {
104
- skipped.push(rel);
105
- }
106
- }
107
- if (!parts.length && !skipped.length)
108
- return { prefix: '', label: '' };
109
- let prefix = '[Auto-context: git-changed files]\n' + parts.join('\n') + '\n';
110
- if (skipped.length)
111
- prefix += `Files changed but too large to auto-load: ${skipped.join(', ')}\n`;
112
- prefix += '\n';
113
- const label = `auto-loaded ${parts.length} changed file(s)${skipped.length ? `, skipped ${skipped.length} (too large)` : ''}`;
114
- return { prefix, label };
115
- }
116
- catch {
117
- return { prefix: '', label: '' };
118
- }
119
- }
120
- export function InputBar({ config, skills, cwd, session }) {
42
+ export function InputBar({ config, skills, cwd, session, version }) {
121
43
  const { stdout } = useStdout();
122
44
  const cols = stdout.columns ?? 80;
123
- const [status, setStatus] = useState('idle');
124
- const [tick, setTick] = useState(0);
125
- const [currentModel, setCurrentModel] = useState(config.model);
126
- const [sessionName, setSessionName] = useState(session);
127
- const [currentTool, setCurrentTool] = useState();
128
- const [taskLabel, setTaskLabel] = useState();
129
45
  const [planningMode, setPlanningMode] = useState(false);
130
- // picker opens on mount — force model selection every launch
131
- const [pickerOpen, setPickerOpen] = useState(true);
132
- const [pickerModels, setPickerModels] = useState([]);
133
- const [pickerLoading, setPickerLoading] = useState(false);
134
- const [pickerError, setPickerError] = useState();
135
- const [pullState, setPullState] = useState();
136
- const abortRef = useRef(null);
137
- const pullAbortRef = useRef(null);
138
- const thinkingStartRef = useRef(0);
139
46
  const macroQueueRef = useRef(new MacroQueue());
140
47
  const executorRef = useRef(new TaskExecutor(tools));
141
- const systemPromptRef = useRef(getSystemPrompt(`\n- CWD: ${cwd}`));
142
- const currentModelRef = useRef(currentModel);
143
- const sessionNameRef = useRef(sessionName);
144
- const historyRef = useRef([]);
145
- const saveTimerRef = useRef(null);
146
48
  const lastGitStatusRef = useRef('');
147
- function scheduleSave() {
148
- if (saveTimerRef.current)
149
- clearTimeout(saveTimerRef.current);
150
- saveTimerRef.current = setTimeout(() => {
151
- saveSession(sessionNameRef.current, historyRef.current);
152
- saveTimerRef.current = null;
153
- }, 2000);
154
- }
155
- function pushHistory(msg) {
156
- historyRef.current.push(msg);
157
- if (historyRef.current.length > 100)
158
- historyRef.current.splice(0, historyRef.current.length - 100);
159
- scheduleSave();
160
- }
161
- useEffect(() => { currentModelRef.current = currentModel; }, [currentModel]);
162
- useEffect(() => { sessionNameRef.current = sessionName; }, [sessionName]);
163
- // mount: load session history + fetch models for initial picker
164
- useEffect(() => {
165
- const history = loadSession(session);
166
- historyRef.current = history;
167
- if (history.length) {
168
- printer.systemMsg(`resumed "${session}" — ${history.length} messages`);
169
- }
170
- setPickerLoading(true);
171
- listModels(config.baseUrl)
172
- .then(m => { setPickerModels(m); setPickerLoading(false); })
173
- .catch(e => { setPickerError(String(e)); setPickerLoading(false); });
174
- }, []);
175
- useEffect(() => {
176
- if (status === 'idle')
177
- return;
178
- const t = setInterval(() => setTick(n => n + 1), 80);
179
- return () => clearInterval(t);
180
- }, [status]);
181
- function buildContext(extra) {
182
- const ctx = [{ role: 'system', content: systemPromptRef.current }];
183
- ctx.push(...historyRef.current);
184
- if (extra)
185
- ctx.push(extra);
186
- return ctx;
187
- }
188
- const runLoop = useCallback(async (contextMsgs, depth = 0, goal) => {
189
- if (depth >= MAX_TOOL_DEPTH) {
190
- setStatus('idle');
191
- return;
192
- }
193
- setStatus('thinking');
194
- if (depth === 0)
195
- thinkingStartRef.current = Date.now();
196
- // Auto-compact context when local model starts losing the thread
197
- const msgs = shouldCompact(contextMsgs) ? compactContext(contextMsgs, goal) : contextMsgs;
198
- abortRef.current = new AbortController();
199
- await chat({
200
- provider: config.provider,
201
- model: currentModelRef.current,
202
- baseUrl: config.baseUrl,
203
- messages: msgs,
204
- signal: abortRef.current.signal,
205
- async onDone(fullText) {
206
- const pendingTools = [];
207
- const parser = new StreamParser();
208
- for (const item of [...parser.feed(fullText), ...parser.flush()]) {
209
- if (item.type === 'tool_call')
210
- pendingTools.push({ name: item.toolName, args: item.toolArgs });
211
- }
212
- printer.assistantMsg(fullText);
213
- pushHistory({ role: 'assistant', content: fullText });
214
- if (!pendingTools.length) {
215
- setStatus('idle');
216
- return;
217
- }
218
- setStatus('tool');
219
- const next = [...msgs, { role: 'assistant', content: fullText }];
220
- try {
221
- for (const tc of pendingTools) {
222
- const tool = tools.find(t => t.name === tc.name);
223
- setCurrentTool(tc.name);
224
- if (tool) {
225
- try {
226
- const result = await tool.execute(tc.args);
227
- printer.toolMsg(tc.name, result);
228
- next.push({ role: 'user', content: `Tool ${tc.name} result:\n${result}` });
229
- }
230
- catch (e) {
231
- const err = `Tool ${tc.name} error: ${e}`;
232
- printer.errorMsg(err);
233
- next.push({ role: 'user', content: err });
234
- }
235
- }
236
- else {
237
- printer.errorMsg(`unknown tool: ${tc.name}`);
238
- next.push({ role: 'user', content: `unknown tool: ${tc.name}` });
239
- }
240
- }
241
- }
242
- finally {
243
- setCurrentTool(undefined);
244
- }
245
- await runLoop(next, depth + 1, goal);
246
- },
247
- onError(err) {
248
- if (err.name !== 'AbortError')
249
- printer.errorMsg(err.message);
250
- setStatus('idle');
251
- },
252
- });
253
- }, [config]);
254
- // ─── model picker ──────────────────────────────────────────────────────────
255
- const openPicker = useCallback(async () => {
256
- setPickerOpen(true);
257
- setPickerLoading(true);
258
- setPickerError(undefined);
259
- try {
260
- setPickerModels(await listModels(config.baseUrl));
261
- }
262
- catch (e) {
263
- setPickerError(String(e));
264
- }
265
- finally {
266
- setPickerLoading(false);
267
- }
268
- }, [config.baseUrl]);
269
- const handleModelSelect = useCallback((name) => {
270
- setCurrentModel(name);
271
- currentModelRef.current = name;
272
- setPickerOpen(false);
273
- printer.systemMsg(`model → ${name}`);
274
- }, []);
275
- const handleModelPull = useCallback(async (name) => {
276
- setPullState({ name, status: 'starting...', pct: undefined });
277
- pullAbortRef.current = new AbortController();
278
- try {
279
- await pullModel(config.baseUrl, name, (s, p) => setPullState({ name, status: s, pct: p }), pullAbortRef.current.signal);
280
- setPickerModels(await listModels(config.baseUrl));
281
- setPullState(undefined);
282
- setCurrentModel(name);
283
- currentModelRef.current = name;
284
- setPickerOpen(false);
285
- printer.systemMsg(`pulled ${name} → active`);
286
- }
287
- catch (e) {
288
- setPullState(undefined);
289
- setPickerError(`pull failed: ${e}`);
290
- }
291
- }, [config.baseUrl]);
49
+ const { setSessionName, sessionNameRef, historyRef, saveTimerRef, systemPromptRef, pushHistory, buildContext, } = useSession(session, cwd, config);
50
+ const { currentModel, setCurrentModel, currentModelRef, pickerOpen, setPickerOpen, pickerModels, pickerLoading, pickerError, pullState, openPicker, handleModelSelect, handleModelPull, } = useModelPicker(config);
51
+ const { status, setStatus, tick, currentTool, setCurrentTool, taskLabel, setTaskLabel, thinkingStartRef, abortRef, runLoop, handleAbort, } = useRunLoop(config, currentModelRef, pushHistory);
292
52
  // ─── refactor ─────────────────────────────────────────────────────────────
293
53
  const runRefactor = useCallback(async (goal) => {
294
54
  printer.systemMsg(`refactor: ${goal}`);
295
55
  setTaskLabel(`planning: ${goal}`);
296
56
  setStatus('thinking');
297
- // Phase 1 — planning: ask model to list files and describe changes
298
57
  const planCtx = [
299
58
  { role: 'system', content: systemPromptRef.current },
300
59
  {
@@ -319,7 +78,6 @@ export function InputBar({ config, skills, cwd, session }) {
319
78
  return;
320
79
  }
321
80
  printer.assistantMsg(planText);
322
- // Parse FILE:/CHANGE: pairs from plan
323
81
  const filePlan = [];
324
82
  const lines = planText.split('\n');
325
83
  let lastPath = '';
@@ -340,9 +98,7 @@ export function InputBar({ config, skills, cwd, session }) {
340
98
  return;
341
99
  }
342
100
  printer.systemMsg(`plan: ${filePlan.length} file(s) to change`);
343
- // Phase 2 — execute via macro/micro queue
344
101
  const micro = new MicroQueue();
345
- // P1: read all files in parallel
346
102
  for (const fp of filePlan) {
347
103
  const t = { id: `read:${fp.path}`, priority: 1, tool: 'read_file', args: { path: fp.path }, deps: [], status: 'pending' };
348
104
  micro.push(t);
@@ -356,13 +112,12 @@ export function InputBar({ config, skills, cwd, session }) {
356
112
  };
357
113
  macroQueueRef.current.enqueue(macro);
358
114
  setTaskLabel(`reading ${filePlan.length} file(s)…`);
359
- const readResults = await executorRef.current.drain(micro, ({ task, result, error }) => {
115
+ const readResults = await executorRef.current.drain(micro, ({ task, error }) => {
360
116
  if (error)
361
117
  printer.errorMsg(`read failed: ${task.args.path} — ${error}`);
362
118
  else
363
119
  printer.systemMsg(`read: ${task.args.path}`);
364
120
  });
365
- // Phase 3 — per-file LLM call with isolated context → patch
366
121
  setTaskLabel(`applying changes…`);
367
122
  const writeMicro = new MicroQueue();
368
123
  for (const fp of filePlan) {
@@ -374,7 +129,6 @@ export function InputBar({ config, skills, cwd, session }) {
374
129
  }
375
130
  setCurrentTool(`edit ${fp.path}`);
376
131
  setTaskLabel(`editing: ${fp.path}`);
377
- // Isolated context per file keeps model focused
378
132
  const editCtx = fileEditContext(systemPromptRef.current, goal, fp.path, fileContent, fp.change);
379
133
  let editText = '';
380
134
  await chat({
@@ -389,7 +143,6 @@ export function InputBar({ config, skills, cwd, session }) {
389
143
  if (!editText)
390
144
  continue;
391
145
  printer.assistantMsg(editText);
392
- // Queue write tasks from LLM's tool calls (P2)
393
146
  const parser = new StreamParser();
394
147
  for (const item of [...parser.feed(editText), ...parser.flush()]) {
395
148
  if (item.type === 'tool_call') {
@@ -397,7 +150,6 @@ export function InputBar({ config, skills, cwd, session }) {
397
150
  }
398
151
  }
399
152
  }
400
- // Execute all writes
401
153
  if (writeMicro.size > 0) {
402
154
  setTaskLabel(`writing ${writeMicro.size} change(s)…`);
403
155
  await executorRef.current.drain(writeMicro, ({ task, result, error }) => {
@@ -427,28 +179,21 @@ export function InputBar({ config, skills, cwd, session }) {
427
179
  return e.message ?? String(e);
428
180
  }
429
181
  };
430
- // /git or /git status
431
182
  if (!sub || sub === 'status') {
432
- const out = await git('status');
433
- printer.systemMsg(out);
183
+ printer.systemMsg(await git('status'));
434
184
  return;
435
185
  }
436
- // /git log [n]
437
186
  if (sub === 'log' || sub.startsWith('log ')) {
438
187
  const n = parseInt(sub.split(' ')[1] ?? '10', 10) || 10;
439
- const out = await git(`log --oneline --decorate -${Math.min(n, 50)}`);
440
- printer.systemMsg(out);
188
+ printer.systemMsg(await git(`log --oneline --decorate -${Math.min(n, 50)}`));
441
189
  return;
442
190
  }
443
- // /git diff [--staged] [file]
444
191
  if (sub === 'diff' || sub.startsWith('diff ')) {
445
192
  const args = sub.slice(4).trim();
446
193
  const out = await git(`diff ${args}`.trim());
447
- const display = out.length > 6000 ? out.slice(0, 6000) + '\n…[truncated]' : out;
448
- printer.systemMsg(display || '(no diff)');
194
+ printer.systemMsg(out.length > 6000 ? out.slice(0, 6000) + '\n…[truncated]' : out || '(no diff)');
449
195
  return;
450
196
  }
451
- // /git review — inject diff into context, ask model to review
452
197
  if (sub === 'review') {
453
198
  const diff = await git('diff HEAD');
454
199
  const staged = await git('diff --staged');
@@ -464,40 +209,91 @@ export function InputBar({ config, skills, cwd, session }) {
464
209
  await runLoop(buildContext());
465
210
  return;
466
211
  }
467
- // /git branch
468
212
  if (sub === 'branch' || sub.startsWith('branch ')) {
469
- const args = sub.slice(6).trim();
470
- const out = await git(`branch ${args}`.trim());
471
- printer.systemMsg(out || '(done)');
213
+ printer.systemMsg(await git(`branch ${sub.slice(6).trim()}`.trim()) || '(done)');
472
214
  return;
473
215
  }
474
- // /git commit <msg>
475
216
  if (sub.startsWith('commit ')) {
476
217
  const msg = sub.slice(7).trim();
477
218
  if (!msg) {
478
219
  printer.systemMsg('usage: /git commit <message>');
479
220
  return;
480
221
  }
481
- const status = await git('status --short');
482
- if (!status || status === '(clean — no changes)') {
222
+ const gitStatus = await git('status --short');
223
+ if (!gitStatus || gitStatus === '(clean — no changes)') {
483
224
  printer.systemMsg('nothing to commit — working tree clean');
484
225
  return;
485
226
  }
486
- printer.systemMsg(`staging and committing:\n${status}`);
227
+ printer.systemMsg(`staging and committing:\n${gitStatus}`);
487
228
  const stageOut = await git('add -A');
488
229
  if (stageOut)
489
230
  printer.systemMsg(stageOut);
490
- const commitOut = await git(`commit -m ${JSON.stringify(msg)}`);
491
- printer.systemMsg(commitOut);
231
+ printer.systemMsg(await git(`commit -m ${JSON.stringify(msg)}`));
492
232
  return;
493
233
  }
494
- // fallthrough run arbitrary git subcommand
495
- const out = await git(sub);
496
- printer.systemMsg(out || '(done)');
234
+ printer.systemMsg(await git(sub) || '(done)');
497
235
  }, []);
498
236
  // ─── submit ────────────────────────────────────────────────────────────────
499
237
  const handleSubmit = useCallback(async (text) => {
500
238
  const cmd = text.trim();
239
+ if (cmd === '/version') {
240
+ printer.systemMsg(`miii v${version ?? 'unknown'}`);
241
+ return;
242
+ }
243
+ if (cmd === '/tavily-key' || cmd.startsWith('/tavily-key ')) {
244
+ const key = cmd.slice(11).trim();
245
+ if (!key) {
246
+ const existing = getTavilyKey();
247
+ printer.systemMsg(existing ? 'Tavily key set (use /tavily-key <key> to update)' : 'No Tavily key set. Usage: /tavily-key tvly-...');
248
+ return;
249
+ }
250
+ if (!key.startsWith('tvly-')) {
251
+ printer.systemMsg('Key should start with tvly-. Get yours at https://tavily.com');
252
+ return;
253
+ }
254
+ saveTavilyKey(key);
255
+ printer.systemMsg('Tavily API key saved to ~/.config/miii/tavily.key (mode 600)');
256
+ return;
257
+ }
258
+ if (cmd === '/skills' || cmd.startsWith('/skills ')) {
259
+ const sub = cmd.slice(7).trim();
260
+ if (!sub || sub === 'list') {
261
+ const pkgs = skills.listNpmSkills();
262
+ printer.systemMsg(pkgs.length ? `installed npm skills:\n${pkgs.map(p => ` ${p}`).join('\n')}` : 'no npm skills installed — try /skills install <name>');
263
+ return;
264
+ }
265
+ if (sub.startsWith('install ')) {
266
+ const pkg = sub.slice(8).trim();
267
+ if (!pkg) {
268
+ printer.systemMsg('usage: /skills install <name> (e.g. /skills install git-summary)');
269
+ return;
270
+ }
271
+ printer.systemMsg(`installing miii-skill-${pkg}…`);
272
+ try {
273
+ printer.systemMsg(await skills.installSkill(pkg));
274
+ }
275
+ catch (e) {
276
+ printer.errorMsg(String(e));
277
+ }
278
+ return;
279
+ }
280
+ if (sub.startsWith('uninstall ')) {
281
+ const pkg = sub.slice(10).trim();
282
+ if (!pkg) {
283
+ printer.systemMsg('usage: /skills uninstall <name>');
284
+ return;
285
+ }
286
+ try {
287
+ printer.systemMsg(await skills.uninstallSkill(pkg));
288
+ }
289
+ catch (e) {
290
+ printer.errorMsg(String(e));
291
+ }
292
+ return;
293
+ }
294
+ printer.systemMsg('usage: /skills install <name> | /skills uninstall <name> | /skills list');
295
+ return;
296
+ }
501
297
  if (cmd === '/model' || cmd.startsWith('/model ')) {
502
298
  const name = cmd.slice(6).trim();
503
299
  if (!name) {
@@ -505,6 +301,7 @@ export function InputBar({ config, skills, cwd, session }) {
505
301
  return;
506
302
  }
507
303
  setCurrentModel(name);
304
+ currentModelRef.current = name;
508
305
  printer.systemMsg(`model → ${name}`);
509
306
  return;
510
307
  }
@@ -538,8 +335,7 @@ export function InputBar({ config, skills, cwd, session }) {
538
335
  process.exit(0);
539
336
  }
540
337
  if (cmd === '/git' || cmd.startsWith('/git ')) {
541
- const sub = cmd.slice(4).trim();
542
- await handleGit(sub);
338
+ await handleGit(cmd.slice(4).trim());
543
339
  return;
544
340
  }
545
341
  if (cmd.startsWith('/refactor ') || cmd === '/refactor') {
@@ -555,9 +351,7 @@ export function InputBar({ config, skills, cwd, session }) {
555
351
  const topic = cmd.slice(5).trim();
556
352
  setPlanningMode(true);
557
353
  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.`);
558
- const msg = topic
559
- ? `I want to plan: ${topic}`
560
- : 'I want to start planning. Help me think through my goals step by step.';
354
+ const msg = topic ? `I want to plan: ${topic}` : 'I want to start planning. Help me think through my goals step by step.';
561
355
  printer.userMsg(msg);
562
356
  pushHistory({ role: 'user', content: msg });
563
357
  await runLoop(buildContext());
@@ -650,13 +444,9 @@ export function InputBar({ config, skills, cwd, session }) {
650
444
  pushHistory({ role: 'user', content: gitPrefix + contextPrefix + text });
651
445
  await runLoop(buildContext());
652
446
  }, [skills, runLoop, openPicker]);
653
- const handleAbort = useCallback(() => {
654
- abortRef.current?.abort();
655
- setStatus('idle');
656
- }, []);
657
447
  const skillList = skills.list();
658
448
  // ─── render ────────────────────────────────────────────────────────────────
659
- return (_jsxs(Box, { flexDirection: "column", children: [pickerOpen ? (_jsxs(_Fragment, { children: [_jsx(ModelPicker, { models: pickerModels, current: currentModel, loading: pickerLoading, error: pickerError, pull: pullState, onSelect: handleModelSelect, onPull: handleModelPull, onClose: () => { setPickerOpen(false); setPullState(undefined); } }), _jsx(Divider, { cols: cols })] })) : (status === 'thinking' || status === 'tool') ? (_jsxs(_Fragment, { children: [_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsx(Text, { bold: true, color: "green", children: "miii" }), _jsxs(Box, { paddingLeft: 2, flexDirection: "column", children: [_jsx(Box, { children: status === 'thinking'
449
+ return (_jsxs(Box, { flexDirection: "column", children: [pickerOpen ? (_jsxs(_Fragment, { children: [_jsx(ModelPicker, { models: pickerModels, current: currentModel, loading: pickerLoading, error: pickerError, pull: pullState, onSelect: handleModelSelect, onPull: handleModelPull, onClose: () => { setPickerOpen(false); } }), _jsx(Divider, { cols: cols })] })) : (status === 'thinking' || status === 'tool') ? (_jsxs(_Fragment, { children: [_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsx(Text, { bold: true, color: "green", children: "miii" }), _jsxs(Box, { paddingLeft: 2, flexDirection: "column", children: [_jsx(Box, { children: status === 'thinking'
660
450
  ? _jsxs(_Fragment, { children: [_jsxs(Text, { color: "yellow", children: [SPARKLE[tick % SPARKLE.length], " "] }), _jsx(Text, { color: "gray", dimColor: true, italic: true, children: THINKING_PHRASES[Math.floor(tick / 62) % THINKING_PHRASES.length] })] })
661
451
  : _jsxs(Text, { color: "yellow", dimColor: true, children: ["\u2699 running ", currentTool ?? 'tool', "\u2026"] }) }), _jsxs(Box, { gap: 2, children: [_jsxs(Text, { color: "gray", dimColor: true, children: [Math.floor((Date.now() - thinkingStartRef.current) / 1000), "s"] }), taskLabel && _jsx(Text, { color: "cyan", dimColor: true, children: taskLabel })] })] })] }), _jsx(Divider, { cols: cols })] })) : null, _jsx(InputArea, { status: status, skills: skillList, cwd: cwd, planningMode: planningMode, onSubmit: handleSubmit, onAbort: handleAbort })] }));
662
452
  }
@@ -12,6 +12,9 @@ const BUILTIN_COMMANDS = [
12
12
  { ns: 'builtin', name: 'session', description: 'switch session /session <name>' },
13
13
  { ns: 'builtin', name: 'exit', description: 'exit miii' },
14
14
  { ns: 'builtin', name: 'model', description: 'switch model mid-session /model <name>' },
15
+ { ns: 'builtin', name: 'version', description: 'show current miii version' },
16
+ { ns: 'builtin', name: 'tavily-key', description: 'set Tavily API key for web search /tavily-key tvly-...' },
17
+ { ns: 'builtin', name: 'skills', description: 'install/uninstall/list npm skills /skills install <name>' },
15
18
  { ns: 'builtin', name: 'list', description: 'list all loaded skills' },
16
19
  { ns: 'builtin', name: 'plan', description: 'start planning mode /plan [topic]' },
17
20
  { ns: 'builtin', name: 'refactor', description: 'multi-file AI refactor /refactor <goal>' },
@@ -0,0 +1,59 @@
1
+ import { readFile } from '../files/ops.js';
2
+ import { resolve } from 'path';
3
+ import { exec } from 'child_process';
4
+ import { promisify } from 'util';
5
+ const gitRun = promisify(exec);
6
+ const CODE_PATTERN = /\.(ts|js|tsx|jsx|py|go|rs|java|rb|sh|css|html|json|yaml|yml)\b|function|class|import|export|const|let|var|def |async|await|error|bug|fix|refactor|implement|`[^`]+`/i;
7
+ export function looksCodeRelated(text) {
8
+ return text.length >= 10 && CODE_PATTERN.test(text);
9
+ }
10
+ export async function buildGitContext(cwd, lastStatusRef) {
11
+ try {
12
+ const { stdout } = await gitRun('git status --short', { cwd, timeout: 5000 });
13
+ const status = stdout.trim();
14
+ if (!status || status === lastStatusRef.current)
15
+ return { prefix: '', label: '' };
16
+ lastStatusRef.current = status;
17
+ const MAX_TOTAL = 40_000;
18
+ const MAX_FILE = 15_000;
19
+ let total = 0;
20
+ const parts = [];
21
+ const skipped = [];
22
+ for (const line of status.split('\n')) {
23
+ const code = line.slice(0, 2);
24
+ if (code.includes('D'))
25
+ continue;
26
+ const raw = line.slice(3).trim().replace(/^"|"$/g, '');
27
+ const rel = raw.includes(' -> ') ? raw.split(' -> ')[1] : raw;
28
+ if (!rel)
29
+ continue;
30
+ try {
31
+ const content = readFile(resolve(cwd, rel));
32
+ if (!content || content.length > MAX_FILE) {
33
+ skipped.push(rel);
34
+ continue;
35
+ }
36
+ total += content.length;
37
+ if (total > MAX_TOTAL) {
38
+ skipped.push(rel);
39
+ continue;
40
+ }
41
+ parts.push(`<file path="${rel}">\n${content}\n</file>`);
42
+ }
43
+ catch {
44
+ skipped.push(rel);
45
+ }
46
+ }
47
+ if (!parts.length && !skipped.length)
48
+ return { prefix: '', label: '' };
49
+ let prefix = '[Auto-context: git-changed files]\n' + parts.join('\n') + '\n';
50
+ if (skipped.length)
51
+ prefix += `Files changed but too large to auto-load: ${skipped.join(', ')}\n`;
52
+ prefix += '\n';
53
+ const label = `auto-loaded ${parts.length} changed file(s)${skipped.length ? `, skipped ${skipped.length} (too large)` : ''}`;
54
+ return { prefix, label };
55
+ }
56
+ catch {
57
+ return { prefix: '', label: '' };
58
+ }
59
+ }