miii-agent 0.1.7 → 0.1.9

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.
Files changed (3) hide show
  1. package/README.md +68 -6
  2. package/dist/cli.js +1790 -1335
  3. package/package.json +3 -1
package/dist/cli.js CHANGED
@@ -1,34 +1,26 @@
1
1
  #!/usr/bin/env node
2
-
3
- // src/cli.tsx
4
- import { render } from "ink";
5
- import { createElement } from "react";
6
-
7
- // src/ui/App.tsx
8
- import { useState as useState4, useEffect as useEffect3 } from "react";
9
- import { Box as Box10, Text as Text10, useApp } from "ink";
10
- import { homedir as homedir3 } from "os";
11
- import { sep } from "path";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __esm = (fn, res) => function __init() {
5
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
6
+ };
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
12
11
 
13
12
  // src/ollama/client.ts
14
13
  import { Ollama } from "ollama";
15
14
  import { execFileSync } from "child_process";
16
- var ollama = new Ollama({
17
- host: process.env.OLLAMA_HOST ?? "http://localhost:11434"
18
- });
19
- var OLLAMA_NOT_INSTALLED = "Ollama is not installed. Install it with: npm i -g ollama\nOr download from https://ollama.com/download";
20
- var OLLAMA_NOT_RUNNING = "Ollama is not running. Start it with: ollama serve";
21
15
  function ollamaInstalled() {
22
16
  try {
23
- const cmd = process.platform === "win32" ? "where" : "which";
24
- execFileSync(cmd, ["ollama"], { stdio: "ignore" });
17
+ const cmd2 = process.platform === "win32" ? "where" : "which";
18
+ execFileSync(cmd2, ["ollama"], { stdio: "ignore" });
25
19
  return true;
26
20
  } catch {
27
21
  return false;
28
22
  }
29
23
  }
30
- var HARMONY_RE = /<\|?\/?(?:channel|message|start|end|return|constrain|assistant|user|system|developer|tool|tool_call|tool_response|final|analysis|commentary)\|?>/gi;
31
- var CHANNEL_LABEL_RE = /^(?:analysis|commentary|final)\s*(?=\w)/i;
32
24
  function stripHarmony(s) {
33
25
  if (s == null) return s;
34
26
  let out = s.replace(HARMONY_RE, "");
@@ -138,1405 +130,1862 @@ async function* chat(model, messages, tools, opts) {
138
130
  if (opts?.signal) opts.signal.removeEventListener("abort", onAbort);
139
131
  }
140
132
  }
133
+ var ollama, OLLAMA_NOT_INSTALLED, OLLAMA_NOT_RUNNING, HARMONY_RE, CHANNEL_LABEL_RE;
134
+ var init_client = __esm({
135
+ "src/ollama/client.ts"() {
136
+ "use strict";
137
+ ollama = new Ollama({
138
+ host: process.env.OLLAMA_HOST ?? "http://localhost:11434"
139
+ });
140
+ OLLAMA_NOT_INSTALLED = "Ollama is not installed. Install it with: npm i -g ollama\nOr download from https://ollama.com/download";
141
+ OLLAMA_NOT_RUNNING = "Ollama is not running. Start it with: ollama serve";
142
+ HARMONY_RE = /<\|?\/?(?:channel|message|start|end|return|constrain|assistant|user|system|developer|tool|tool_call|tool_response|final|analysis|commentary)\|?>/gi;
143
+ CHANNEL_LABEL_RE = /^(?:analysis|commentary|final)\s*(?=\w)/i;
144
+ }
145
+ });
141
146
 
142
- // src/config.ts
143
- import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
144
- import { join } from "path";
145
- import { homedir } from "os";
146
- var CONFIG_DIR = join(homedir(), ".miii");
147
- var CONFIG_PATH = join(CONFIG_DIR, "config.json");
148
- function loadConfig() {
149
- if (!existsSync(CONFIG_PATH)) return {};
150
- try {
151
- return JSON.parse(readFileSync(CONFIG_PATH, "utf-8"));
152
- } catch {
153
- return {};
147
+ // src/tools/paths.ts
148
+ import { resolve, relative as relative2, isAbsolute, sep } from "path";
149
+ function confinePath(p) {
150
+ if (typeof p !== "string" || p.length === 0) {
151
+ throw new Error("Path is required.");
154
152
  }
153
+ const root = process.cwd();
154
+ const abs = resolve(root, p);
155
+ const rel = relative2(root, abs);
156
+ if (rel === ".." || rel.startsWith(".." + sep) || isAbsolute(rel)) {
157
+ throw new Error(`Path "${p}" is outside the working directory (${root}). Access denied.`);
158
+ }
159
+ return abs;
155
160
  }
156
- function saveConfig(config) {
157
- mkdirSync(CONFIG_DIR, { recursive: true });
158
- writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), "utf-8");
159
- }
160
- function setModel(model) {
161
- saveConfig({ ...loadConfig(), model });
162
- }
163
- function setEffort(effort) {
164
- saveConfig({ ...loadConfig(), effort });
165
- }
161
+ var init_paths = __esm({
162
+ "src/tools/paths.ts"() {
163
+ "use strict";
164
+ }
165
+ });
166
166
 
167
- // src/ui/WelcomeBlock.tsx
168
- import { Box, Text } from "ink";
169
- import { jsx, jsxs } from "react/jsx-runtime";
170
- function WelcomeBlock({ model, activeCtx, effort, cwd }) {
171
- const ctxLabel = activeCtx != null ? `${Math.round(activeCtx / 1024)}k ctx` : "\u2014 ctx";
172
- return /* @__PURE__ */ jsxs(
173
- Box,
174
- {
175
- flexDirection: "column",
176
- borderStyle: "round",
177
- borderColor: "gray",
178
- paddingX: 2,
179
- marginBottom: 1,
180
- children: [
181
- /* @__PURE__ */ jsxs(Box, { gap: 2, children: [
182
- /* @__PURE__ */ jsx(Text, { bold: true, color: "blue", children: "MIII CLI" }),
183
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\xB7" }),
184
- /* @__PURE__ */ jsx(Text, { children: model ?? "/models" }),
185
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\xB7" }),
186
- /* @__PURE__ */ jsx(Text, { children: ctxLabel }),
187
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\xB7" }),
188
- /* @__PURE__ */ jsxs(Text, { children: [
189
- effort,
190
- " effort"
191
- ] })
192
- ] }),
193
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: cwd })
194
- ]
195
- }
196
- );
197
- }
167
+ // src/tools/edit_file.ts
168
+ import { readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
169
+ var edit_file;
170
+ var init_edit_file = __esm({
171
+ "src/tools/edit_file.ts"() {
172
+ "use strict";
173
+ init_paths();
174
+ edit_file = {
175
+ name: "edit_file",
176
+ description: "Replace an exact string in a file. old_str must be unique.",
177
+ input_schema: {
178
+ type: "object",
179
+ properties: {
180
+ path: { type: "string", description: "File path" },
181
+ old_str: { type: "string", description: "Exact text to replace" },
182
+ new_str: { type: "string", description: "Replacement text" }
183
+ },
184
+ required: ["path", "old_str", "new_str"]
185
+ },
186
+ handler: ({ path, old_str, new_str }) => {
187
+ try {
188
+ const abs = confinePath(path);
189
+ const src = readFileSync3(abs, "utf-8");
190
+ const first = src.indexOf(old_str);
191
+ if (first === -1) {
192
+ return { content: `old_str not found in ${path}`, is_error: true };
193
+ }
194
+ if (src.indexOf(old_str, first + 1) !== -1) {
195
+ return { content: `old_str not unique in ${path}`, is_error: true };
196
+ }
197
+ writeFileSync3(abs, src.slice(0, first) + new_str + src.slice(first + old_str.length), "utf-8");
198
+ return { content: `Edited ${path}` };
199
+ } catch (err) {
200
+ return { content: err instanceof Error ? err.message : String(err), is_error: true };
201
+ }
202
+ }
203
+ };
204
+ }
205
+ });
198
206
 
199
- // src/ui/ModelList.tsx
200
- import { Box as Box2, Text as Text2 } from "ink";
201
- import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
202
- function ModelList({ models, cursor, activeModel, showActive }) {
203
- if (models.length === 0) {
204
- return /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
205
- "no models found. run: ollama pull ",
206
- "<model>"
207
- ] });
207
+ // src/tools/read_file.ts
208
+ import { readFileSync as readFileSync4 } from "fs";
209
+ var read_file;
210
+ var init_read_file = __esm({
211
+ "src/tools/read_file.ts"() {
212
+ "use strict";
213
+ init_paths();
214
+ read_file = {
215
+ name: "read_file",
216
+ description: "Read entire file contents as UTF-8 text.",
217
+ input_schema: {
218
+ type: "object",
219
+ properties: {
220
+ path: { type: "string", description: "File path" }
221
+ },
222
+ required: ["path"]
223
+ },
224
+ handler: ({ path }) => {
225
+ try {
226
+ const MAX = 2e5;
227
+ const raw = readFileSync4(confinePath(path), "utf-8");
228
+ const truncated = raw.length > MAX;
229
+ const body = truncated ? raw.slice(0, MAX) + `
230
+ [truncated: ${raw.length - MAX} more chars]` : raw;
231
+ return { content: body };
232
+ } catch (err) {
233
+ return { content: err instanceof Error ? err.message : String(err), is_error: true };
234
+ }
235
+ }
236
+ };
208
237
  }
209
- return /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", borderStyle: "round", borderColor: "gray", paddingX: 1, children: models.map((m, i) => /* @__PURE__ */ jsxs2(Text2, { color: i === cursor ? "blue" : void 0, dimColor: i !== cursor, children: [
210
- i === cursor ? "\u276F " : " ",
211
- m,
212
- showActive && m === activeModel ? /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: " (active)" }) : null
213
- ] }, m)) });
238
+ });
239
+
240
+ // src/tools/write_file.ts
241
+ import { writeFileSync as writeFileSync4, mkdirSync as mkdirSync3 } from "fs";
242
+ import { dirname } from "path";
243
+ var write_file;
244
+ var init_write_file = __esm({
245
+ "src/tools/write_file.ts"() {
246
+ "use strict";
247
+ init_paths();
248
+ write_file = {
249
+ name: "write_file",
250
+ description: "Create or overwrite a file with the given content. Parent dirs auto-created.",
251
+ input_schema: {
252
+ type: "object",
253
+ properties: {
254
+ path: { type: "string", description: "File path" },
255
+ content: { type: "string", description: "Full file content" }
256
+ },
257
+ required: ["path", "content"]
258
+ },
259
+ handler: ({ path, content }) => {
260
+ try {
261
+ const abs = confinePath(path);
262
+ mkdirSync3(dirname(abs), { recursive: true });
263
+ writeFileSync4(abs, content, "utf-8");
264
+ return { content: `Wrote ${path} (${content.length} bytes)` };
265
+ } catch (err) {
266
+ return { content: err instanceof Error ? err.message : String(err), is_error: true };
267
+ }
268
+ }
269
+ };
270
+ }
271
+ });
272
+
273
+ // src/tools/run_bash.ts
274
+ import { execa } from "execa";
275
+ var run_bash;
276
+ var init_run_bash = __esm({
277
+ "src/tools/run_bash.ts"() {
278
+ "use strict";
279
+ run_bash = {
280
+ name: "run_bash",
281
+ description: "Execute a shell command (bash on Unix, cmd on Windows). Returns stdout+stderr. Non-interactive only.",
282
+ input_schema: {
283
+ type: "object",
284
+ properties: {
285
+ command: { type: "string", description: "Shell command to run" },
286
+ timeout_ms: { type: "number", description: "Timeout in ms (default 30000)" }
287
+ },
288
+ required: ["command"]
289
+ },
290
+ handler: async ({ command, timeout_ms }) => {
291
+ try {
292
+ const isWin = process.platform === "win32";
293
+ const shell = isWin ? "cmd" : "bash";
294
+ const shellArgs = isWin ? ["/c", command] : ["-c", command];
295
+ const { stdout, stderr, exitCode } = await execa(shell, shellArgs, {
296
+ timeout: timeout_ms ?? 3e4,
297
+ reject: false,
298
+ all: false
299
+ });
300
+ const out = [stdout, stderr].filter(Boolean).join("\n");
301
+ const is_error = exitCode !== 0;
302
+ const body = out || (is_error ? `(no output)` : "");
303
+ const content = is_error ? `${body}
304
+ [exit ${exitCode}]` : body;
305
+ return {
306
+ content: content.slice(0, 32e3),
307
+ is_error
308
+ };
309
+ } catch (err) {
310
+ return { content: err instanceof Error ? err.message : String(err), is_error: true };
311
+ }
312
+ }
313
+ };
314
+ }
315
+ });
316
+
317
+ // src/tools/grep.ts
318
+ import { execa as execa2 } from "execa";
319
+ var grep;
320
+ var init_grep = __esm({
321
+ "src/tools/grep.ts"() {
322
+ "use strict";
323
+ grep = {
324
+ name: "grep",
325
+ description: "Search file contents for a regex pattern. Uses ripgrep if available, falls back to grep -R.",
326
+ input_schema: {
327
+ type: "object",
328
+ properties: {
329
+ pattern: { type: "string", description: "Regex pattern" },
330
+ path: { type: "string", description: "Root path to search (default cwd)" },
331
+ glob: { type: "string", description: 'File glob filter, e.g. "*.ts"' },
332
+ case_insensitive: { type: "string", description: 'Set "true" for case-insensitive' },
333
+ max_results: { type: "number", description: "Max matching lines (default 200)" }
334
+ },
335
+ required: ["pattern"]
336
+ },
337
+ handler: async ({ pattern, path, glob: glob2, case_insensitive, max_results }) => {
338
+ const root = path ?? process.cwd();
339
+ const limit = max_results ?? 200;
340
+ const ci = case_insensitive === true || String(case_insensitive) === "true";
341
+ const tryRg = async () => {
342
+ const args = ["--line-number", "--no-heading", "--color=never", "-m", String(limit)];
343
+ if (ci) args.push("-i");
344
+ if (glob2) args.push("--glob", glob2);
345
+ args.push("--", pattern, root);
346
+ return execa2("rg", args, { reject: false, timeout: 2e4 });
347
+ };
348
+ const tryGrep = async () => {
349
+ const args = ["-R", "-n", "--color=never"];
350
+ if (ci) args.push("-i");
351
+ if (glob2) args.push("--include", glob2);
352
+ args.push("--", pattern, root);
353
+ return execa2("grep", args, { reject: false, timeout: 2e4 });
354
+ };
355
+ try {
356
+ let res;
357
+ try {
358
+ res = await tryRg();
359
+ if (res.exitCode === 127 || (res.stderr ?? "").includes("command not found")) {
360
+ res = await tryGrep();
361
+ }
362
+ } catch {
363
+ res = await tryGrep();
364
+ }
365
+ const lines = (res.stdout ?? "").split("\n").slice(0, limit);
366
+ const out = lines.join("\n");
367
+ const code = res.exitCode ?? 0;
368
+ if (!out && code === 1) return { content: "No matches." };
369
+ return { content: out || res.stderr || "No matches.", is_error: code > 1 };
370
+ } catch (err) {
371
+ return { content: err instanceof Error ? err.message : String(err), is_error: true };
372
+ }
373
+ }
374
+ };
375
+ }
376
+ });
377
+
378
+ // src/tools/glob.ts
379
+ import { execa as execa3 } from "execa";
380
+ function globToFindName(glob2) {
381
+ return glob2;
214
382
  }
383
+ var glob;
384
+ var init_glob = __esm({
385
+ "src/tools/glob.ts"() {
386
+ "use strict";
387
+ glob = {
388
+ name: "glob",
389
+ description: 'List files matching a glob pattern (e.g. "**/*.ts"). Uses ripgrep --files if available.',
390
+ input_schema: {
391
+ type: "object",
392
+ properties: {
393
+ pattern: { type: "string", description: 'Glob pattern, e.g. "**/*.ts"' },
394
+ path: { type: "string", description: "Root path (default cwd)" },
395
+ max_results: { type: "number", description: "Max paths returned (default 500)" }
396
+ },
397
+ required: ["pattern"]
398
+ },
399
+ handler: async ({ pattern, path, max_results }) => {
400
+ const root = path ?? process.cwd();
401
+ const limit = max_results ?? 500;
402
+ const tryRg = () => execa3("rg", ["--files", "--hidden", "--glob", pattern, root], {
403
+ reject: false,
404
+ timeout: 2e4
405
+ });
406
+ const tryFind = () => {
407
+ const name = globToFindName(pattern.replace(/^\*\*\//, ""));
408
+ return execa3("find", [root, "-type", "f", "-name", name], {
409
+ reject: false,
410
+ timeout: 2e4
411
+ });
412
+ };
413
+ try {
414
+ let res;
415
+ try {
416
+ res = await tryRg();
417
+ if (res.exitCode === 127 || (res.stderr ?? "").includes("command not found")) {
418
+ res = await tryFind();
419
+ }
420
+ } catch {
421
+ res = await tryFind();
422
+ }
423
+ const lines = (res.stdout ?? "").split("\n").filter(Boolean).slice(0, limit);
424
+ if (lines.length === 0) return { content: "No files matched." };
425
+ return { content: lines.join("\n") };
426
+ } catch (err) {
427
+ return { content: err instanceof Error ? err.message : String(err), is_error: true };
428
+ }
429
+ }
430
+ };
431
+ }
432
+ });
215
433
 
216
- // src/ui/InputBar.tsx
217
- import { useEffect, useState } from "react";
218
- import { Box as Box3, Text as Text3 } from "ink";
219
- import { Fragment, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
220
- var SPIN = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
221
- function InputBar({ input, disabled, processingLabel }) {
222
- const [frame, setFrame] = useState(0);
223
- useEffect(() => {
224
- if (!disabled) return;
225
- const t = setInterval(() => setFrame((f) => (f + 1) % SPIN.length), 150);
226
- return () => clearInterval(t);
227
- }, [disabled]);
228
- return /* @__PURE__ */ jsx3(
229
- Box3,
230
- {
231
- borderStyle: "single",
232
- borderTop: true,
233
- borderBottom: true,
234
- borderLeft: false,
235
- borderRight: false,
236
- borderColor: disabled ? "yellow" : "white dim",
237
- paddingX: 1,
238
- children: disabled ? /* @__PURE__ */ jsxs3(Fragment, { children: [
239
- /* @__PURE__ */ jsx3(Text3, { color: "yellow", children: SPIN[frame] + " " }),
240
- /* @__PURE__ */ jsx3(Text3, { dimColor: true, italic: true, children: processingLabel ?? "processing\u2026" }),
241
- /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: " (esc to cancel)" })
242
- ] }) : /* @__PURE__ */ jsxs3(Fragment, { children: [
243
- /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "> " }),
244
- /* @__PURE__ */ jsx3(Text3, { children: input }),
245
- /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "\u258C" })
246
- ] })
434
+ // src/tools/registry.ts
435
+ function getTool(name) {
436
+ return TOOLS.find((t) => t.name === name);
437
+ }
438
+ function toOllamaTools(tools = TOOLS) {
439
+ return tools.map((t) => ({
440
+ type: "function",
441
+ function: {
442
+ name: t.name,
443
+ description: t.description,
444
+ parameters: {
445
+ type: "object",
446
+ properties: t.input_schema.properties,
447
+ required: t.input_schema.required
448
+ }
247
449
  }
248
- );
450
+ }));
249
451
  }
452
+ var TOOLS;
453
+ var init_registry = __esm({
454
+ "src/tools/registry.ts"() {
455
+ "use strict";
456
+ init_edit_file();
457
+ init_read_file();
458
+ init_write_file();
459
+ init_run_bash();
460
+ init_grep();
461
+ init_glob();
462
+ TOOLS = [
463
+ edit_file,
464
+ read_file,
465
+ write_file,
466
+ run_bash,
467
+ grep,
468
+ glob
469
+ ];
470
+ }
471
+ });
250
472
 
251
- // src/ui/ModelsView.tsx
252
- import { Box as Box4, Text as Text4 } from "ink";
253
- import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
254
- function ModelsView({ models, cursor, model, ollamaHost, effort }) {
255
- return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", marginLeft: 2, children: [
256
- /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", marginBottom: 1, children: [
257
- /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "config" }),
258
- /* @__PURE__ */ jsxs4(Box4, { marginTop: 1, flexDirection: "column", children: [
259
- /* @__PURE__ */ jsxs4(Text4, { children: [
260
- /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "model " }),
261
- /* @__PURE__ */ jsx4(Text4, { children: model ?? "\u2014" })
262
- ] }),
263
- /* @__PURE__ */ jsxs4(Text4, { children: [
264
- /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "host " }),
265
- /* @__PURE__ */ jsx4(Text4, { children: ollamaHost ?? "http://localhost:11434" })
266
- ] }),
267
- /* @__PURE__ */ jsxs4(Text4, { children: [
268
- /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "effort " }),
269
- /* @__PURE__ */ jsx4(Text4, { children: effort }),
270
- /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: " (\u2190 \u2192)" })
271
- ] })
272
- ] })
273
- ] }),
274
- /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "switch model" }),
275
- /* @__PURE__ */ jsx4(Box4, { marginTop: 1, children: /* @__PURE__ */ jsx4(ModelList, { models, cursor, activeModel: model, showActive: true }) }),
276
- /* @__PURE__ */ jsx4(Box4, { marginTop: 1, children: /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "\u2191\u2193 navigate enter switch \u2190\u2192 effort esc close" }) })
277
- ] });
473
+ // src/tools/validate.ts
474
+ import { z } from "zod";
475
+ function propSchema(spec) {
476
+ if (spec.enum && spec.enum.length) return z.enum(spec.enum);
477
+ switch (spec.type) {
478
+ case "string":
479
+ return z.string();
480
+ case "number":
481
+ return z.number();
482
+ case "integer":
483
+ return z.number().int();
484
+ case "boolean":
485
+ return z.boolean();
486
+ case "array":
487
+ return z.array(z.unknown());
488
+ case "object":
489
+ return z.record(z.unknown());
490
+ default:
491
+ return z.unknown();
492
+ }
278
493
  }
279
-
280
- // src/ui/SessionsView.tsx
281
- import { Box as Box5, Text as Text5 } from "ink";
282
- import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
283
- function relativeTime(iso) {
284
- const diff = Date.now() - new Date(iso).getTime();
285
- const min = Math.floor(diff / 6e4);
286
- if (min < 1) return "just now";
287
- if (min < 60) return `${min}m ago`;
288
- const hr = Math.floor(min / 60);
289
- if (hr < 24) return `${hr}h ago`;
290
- const d = Math.floor(hr / 24);
291
- return `${d}d ago`;
494
+ function toZod(schema) {
495
+ const required = new Set(schema.required ?? []);
496
+ const shape = {};
497
+ for (const [key, spec] of Object.entries(schema.properties)) {
498
+ shape[key] = required.has(key) ? propSchema(spec) : z.unknown().optional();
499
+ }
500
+ return z.object(shape).passthrough();
292
501
  }
293
- function SessionsView({ sessions, cursor }) {
294
- return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", marginLeft: 2, children: [
295
- /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "resume session" }),
296
- /* @__PURE__ */ jsx5(Box5, { marginTop: 1, flexDirection: "column", borderStyle: "round", borderColor: "gray", paddingX: 1, children: sessions.length === 0 ? /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "no saved sessions yet" }) : sessions.map((s, i) => {
297
- const active = i === cursor;
298
- const label = s.title;
299
- return /* @__PURE__ */ jsxs5(Box5, { gap: 1, children: [
300
- /* @__PURE__ */ jsxs5(Text5, { color: active ? "blue" : void 0, dimColor: !active, children: [
301
- active ? "\u276F " : " ",
302
- label
303
- ] }),
304
- /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: `\xB7 ${s.messageCount} msgs \xB7 ${relativeTime(s.updatedAt)}` })
305
- ] }, s.id);
306
- }) }),
307
- /* @__PURE__ */ jsx5(Box5, { marginTop: 1, children: /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "\u2191\u2193 navigate enter resume d delete esc cancel" }) })
308
- ] });
502
+ function validateInput(schema, input) {
503
+ const result = toZod(schema).safeParse(input ?? {});
504
+ if (result.success) return null;
505
+ const issues = result.error.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ");
506
+ return `Invalid arguments: ${issues}`;
309
507
  }
508
+ var init_validate = __esm({
509
+ "src/tools/validate.ts"() {
510
+ "use strict";
511
+ }
512
+ });
310
513
 
311
- // src/ui/CommandPalette.tsx
312
- import { Box as Box6, Text as Text6 } from "ink";
313
- import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
314
- var COMMANDS = [
315
- { name: "/models", description: "switch model or adjust effort" },
316
- { name: "/new", description: "save current session and start fresh" },
317
- { name: "/sessions", description: "list sessions and resume one" },
318
- { name: "/clear", description: "clear chat and reset context" },
319
- { name: "/exit", description: "quit miii" }
320
- ];
321
- function CommandPalette({ filter, cursor }) {
322
- const filtered = COMMANDS.filter((c) => c.name.startsWith(filter));
323
- if (filtered.length === 0) return null;
324
- const nameWidth = Math.max(...filtered.map((c) => c.name.length));
325
- return /* @__PURE__ */ jsxs6(
326
- Box6,
327
- {
328
- flexDirection: "column",
329
- borderStyle: "round",
330
- borderColor: "gray",
331
- marginX: 1,
332
- marginBottom: 0,
333
- paddingX: 1,
334
- children: [
335
- filtered.map((cmd, i) => {
336
- const active = i === cursor;
337
- return /* @__PURE__ */ jsxs6(Box6, { gap: 2, children: [
338
- /* @__PURE__ */ jsxs6(Text6, { bold: active, color: active ? "blue" : void 0, dimColor: !active, children: [
339
- active ? "\u276F " : " ",
340
- cmd.name.padEnd(nameWidth)
341
- ] }),
342
- /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: cmd.description })
343
- ] }, cmd.name);
344
- }),
345
- /* @__PURE__ */ jsx6(Box6, { marginTop: 0, children: /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "\u2191\u2193 navigate tab/enter autocomplete esc dismiss" }) })
346
- ]
347
- }
348
- );
349
- }
350
- function filteredCommands(filter) {
351
- return COMMANDS.filter((c) => c.name.startsWith(filter));
352
- }
514
+ // src/prompt/system.ts
515
+ function buildSystemPrompt(tools, cwd) {
516
+ const toolLines = tools.map((t) => `- ${t.name}: ${t.description}`).join("\n");
517
+ return `You are miii, a senior software engineer running in a terminal.
353
518
 
354
- // src/session/store.ts
355
- import { writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, existsSync as existsSync2, readdirSync, readFileSync as readFileSync2, rmSync } from "fs";
356
- import { join as join2 } from "path";
357
- import { homedir as homedir2 } from "os";
358
- import { randomUUID } from "crypto";
359
- function encodeProjectDir(cwd) {
360
- return cwd.replace(/[/\\]/g, "-").replace(/^-+/, "");
361
- }
362
- var SESSION_DIR = join2(homedir2(), ".miii", "projects", encodeProjectDir(process.cwd()), "session");
363
- function newSessionId() {
364
- return randomUUID();
365
- }
366
- function sessionPath(id) {
367
- return join2(SESSION_DIR, `${id}.jsonl`);
368
- }
369
- function messageText(m) {
370
- if (typeof m.content === "string") return m.content;
371
- return m.content.map((b) => {
372
- if (b.type === "text") return b.text;
373
- if (b.type === "tool_use") return `[tool ${b.name}]`;
374
- if (b.type === "tool_result") return "[result]";
375
- return "";
376
- }).join(" ");
377
- }
378
- function firstUserText(messages) {
379
- const first = messages.find((m) => m.role === "user");
380
- if (!first) return "untitled";
381
- return messageText(first).trim().slice(0, 80) || "untitled";
519
+ Working directory: ${cwd}
520
+
521
+ # Goal Understanding (read this first, every turn)
522
+ Before acting on any request, extract and hold three things:
523
+ GOAL: what the user ultimately wants (outcome, not steps)
524
+ CRITERION: how you will know the goal is met
525
+ GAPS: anything unclear that would force you to guess
526
+
527
+ If GAPS is non-empty, ask the minimum questions needed to fill them \u2014 one message, numbered list \u2014 before touching any file or running any command. Do not guess. Do not act on assumptions.
528
+
529
+ Re-read GOAL before every tool call. If a tool call does not move toward GOAL, skip it.
530
+
531
+ # Attention: re-attend to goal at each step
532
+ After each tool result, answer silently: "Does this result move me toward GOAL?"
533
+ YES \u2192 continue
534
+ NO \u2192 stop, re-derive plan from GOAL, explain the correction in one line
535
+
536
+ This prevents drift. Each step attends to the original goal, not just the previous step.
537
+
538
+ # Output format
539
+ - Always reply in plain text. Never use Markdown syntax: no \`#\` headings, no \`**bold**\`, no \`-\` bullet lists, no fenced \`\`\` code blocks, no inline backticks.
540
+ - Quote code, paths, and identifiers inline as plain text. Do not wrap them.
541
+ - Keep prose terse.
542
+
543
+ # Engineering mindset
544
+ - Treat every request as one of: bug, feature, or fix. Name which one before you start.
545
+ - Apply first principles: decompose unclear tasks into smallest concrete sub-problems, solve each explicitly, compose the result.
546
+ - Never guess. If a fact (file path, function signature, current behavior) is unknown, read or search for it first.
547
+
548
+ # Clarifying questions \u2014 when to ask
549
+ Ask BEFORE acting when:
550
+ - The goal has more than one valid interpretation
551
+ - Success criterion is ambiguous (e.g. "make it better" \u2014 better how?)
552
+ - Required context is missing (which file? which behavior? which user?)
553
+ - Two reasonable approaches have different tradeoffs the user should choose
554
+
555
+ Do NOT ask when:
556
+ - The answer is findable by reading the codebase
557
+ - There is only one sensible interpretation
558
+ - The user has already answered this implicitly
559
+
560
+ Ask in a numbered list. One round of questions per turn. Then wait.
561
+
562
+ # Tool calls
563
+ - When you need a tool, emit the tool call directly. No preamble, no narration, no "I will use X".
564
+ - Never describe a tool call instead of emitting it. If you cannot emit the call, answer in plain text.
565
+ - After a tool result, move directly to the next tool call or the final answer. Do not restate what the previous tool did.
566
+
567
+ # Tools
568
+ You have access to the following tools. Call them via the function-calling interface.
569
+ ${toolLines}
570
+
571
+ # Loop semantics
572
+ - When you need to act on the filesystem or run a command, emit a tool call.
573
+ - After each tool result, decide: more tool calls, or a final plain-text answer.
574
+ - Stop emitting tool calls when GOAL is met. Reply with a concise plain-text final message confirming CRITERION is satisfied.
575
+
576
+ # Rules
577
+ - Always read a file before updating it. Never edit, overwrite, or create-over a file you have not read first this turn.
578
+ - Prefer editing existing files over creating new ones.
579
+ - For edit_file, ensure old_str is unique within the target file.
580
+ - Never invent file paths. Read, glob, or grep before editing.
581
+ - No filler, no pleasantries, no apologies.
582
+
583
+ # Testing and verification
584
+ - Always test the code after a change. Run the project's tests (e.g. npm test, pytest, go test) or the relevant script via run_bash before declaring a task done.
585
+ - If no test exists for the change, run the affected entry point via run_bash to verify it behaves correctly.
586
+ - Treat a green test run or a successful command as the completion signal. If it fails, fix and re-run.
587
+
588
+ # Permissions
589
+ - File tools are confined to the working directory; paths outside it are denied.
590
+ - Each tool call may prompt the user for approval. If they choose "don't ask again", the exact command or path is persisted to ~/.miii/permissions.json and the same call is auto-allowed thereafter.
591
+ `;
382
592
  }
383
- function readMeta(id) {
593
+ var init_system = __esm({
594
+ "src/prompt/system.ts"() {
595
+ "use strict";
596
+ }
597
+ });
598
+
599
+ // src/permissions/policy.ts
600
+ import { readFileSync as readFileSync5, writeFileSync as writeFileSync5, mkdirSync as mkdirSync4, existsSync as existsSync3, renameSync } from "fs";
601
+ import { join as join4 } from "path";
602
+ import { homedir as homedir3 } from "os";
603
+ function loadRules() {
604
+ if (!existsSync3(RULES_PATH)) return [];
384
605
  try {
385
- const raw = readFileSync2(sessionPath(id), "utf-8");
386
- const firstLine = raw.slice(0, raw.indexOf("\n") === -1 ? raw.length : raw.indexOf("\n"));
387
- const parsed = JSON.parse(firstLine);
388
- if (parsed.type !== "meta") return null;
389
- const { type: _t, ...meta } = parsed;
390
- return meta;
606
+ const data = JSON.parse(readFileSync5(RULES_PATH, "utf-8"));
607
+ return Array.isArray(data.rules) ? data.rules : [];
391
608
  } catch {
392
- return null;
609
+ return [];
393
610
  }
394
611
  }
395
- function persistSession(id, messages, title) {
396
- if (!messages.length) return;
397
- mkdirSync2(SESSION_DIR, { recursive: true });
398
- const existing = readMeta(id);
399
- const now = (/* @__PURE__ */ new Date()).toISOString();
400
- const meta = {
401
- id,
402
- createdAt: existing?.createdAt ?? now,
403
- updatedAt: now,
404
- title: title ?? existing?.title ?? firstUserText(messages),
405
- messageCount: messages.length
406
- };
407
- const lines = [JSON.stringify({ type: "meta", ...meta })];
408
- for (const message of messages) {
409
- lines.push(JSON.stringify({ type: "message", message }));
410
- }
411
- writeFileSync2(sessionPath(id), lines.join("\n") + "\n", "utf-8");
612
+ function saveRules(rules) {
613
+ mkdirSync4(RULES_DIR, { recursive: true });
614
+ const tmp = RULES_PATH + ".tmp";
615
+ writeFileSync5(tmp, JSON.stringify({ rules }, null, 2), "utf-8");
616
+ renameSync(tmp, RULES_PATH);
617
+ }
618
+ function addRule(tool, pattern) {
619
+ const rules = loadRules();
620
+ if (rules.some((r) => r.tool === tool && r.pattern === pattern)) return;
621
+ rules.push({ tool, pattern });
622
+ saveRules(rules);
623
+ }
624
+ function subjectFor(toolName, input) {
625
+ const obj = input ?? {};
626
+ if (toolName === "run_bash") return typeof obj.command === "string" ? obj.command : "";
627
+ if (typeof obj.path === "string") return obj.path;
628
+ return "";
412
629
  }
413
- function listSessions() {
414
- if (!existsSync2(SESSION_DIR)) return [];
415
- const metas = [];
416
- for (const file of readdirSync(SESSION_DIR)) {
417
- if (!file.endsWith(".jsonl")) continue;
418
- const meta = readMeta(file.replace(/\.jsonl$/, ""));
419
- if (meta) metas.push(meta);
420
- }
421
- return metas.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
630
+ function globToRegExp(glob2) {
631
+ const escaped = glob2.replace(/[.+^${}()|[\]\\]/g, "\\$&");
632
+ const pattern = escaped.replace(/\*/g, ".*").replace(/\?/g, ".");
633
+ return new RegExp(`^${pattern}$`);
422
634
  }
423
- function deleteSession(id) {
635
+ function matches(rule, toolName, subject) {
636
+ if (rule.tool !== toolName) return false;
424
637
  try {
425
- rmSync(sessionPath(id), { force: true });
638
+ return globToRegExp(rule.pattern).test(subject);
426
639
  } catch {
640
+ return false;
427
641
  }
428
642
  }
429
- function loadSession(id) {
430
- try {
431
- const raw = readFileSync2(sessionPath(id), "utf-8");
432
- const messages = [];
433
- for (const line of raw.split("\n")) {
434
- if (!line.trim()) continue;
435
- const parsed = JSON.parse(line);
436
- if (parsed.type === "message") messages.push(parsed.message);
437
- }
438
- return messages;
439
- } catch {
440
- return [];
643
+ async function check(toolName, input, ctx) {
644
+ const subject = subjectFor(toolName, input);
645
+ const rules = loadRules();
646
+ if (rules.some((r) => matches(r, toolName, subject))) return "allow";
647
+ const answer = await ctx.ask(toolName, input);
648
+ if (answer === "no") return "deny";
649
+ if (answer === "always") addRule(toolName, subject);
650
+ return "allow";
651
+ }
652
+ var RULES_DIR, RULES_PATH;
653
+ var init_policy = __esm({
654
+ "src/permissions/policy.ts"() {
655
+ "use strict";
656
+ RULES_DIR = join4(homedir3(), ".miii");
657
+ RULES_PATH = join4(RULES_DIR, "permissions.json");
441
658
  }
659
+ });
660
+
661
+ // src/agent/adapter.ts
662
+ function mintToolUseId() {
663
+ const rand = Math.random().toString(36).slice(2, 14);
664
+ return `toolu_${rand}`;
442
665
  }
443
- function toDisplayMessages(history) {
444
- const out = [];
445
- for (const m of history) {
446
- if (m.role === "system") continue;
447
- const blocks = Array.isArray(m.content) ? m.content : [{ type: "text", text: m.content }];
448
- if (m.role === "user") {
449
- const text = blocks.filter((b) => b.type === "text").map((b) => b.text).join("");
450
- const results = blocks.filter((b) => b.type === "tool_result");
451
- if (results.length && out.length) {
452
- const last = out[out.length - 1];
453
- last.tool_results = [
454
- ...last.tool_results ?? [],
455
- ...results.map((r) => ({
456
- tool_use_id: r.tool_use_id,
457
- content: r.content,
458
- is_error: r.is_error
459
- }))
460
- ];
666
+ function toOllamaMessages(history, system) {
667
+ const out = [{ role: "system", content: system }];
668
+ for (const msg of history) {
669
+ if (typeof msg.content === "string") {
670
+ out.push({ role: msg.role === "system" ? "system" : msg.role, content: msg.content });
671
+ continue;
672
+ }
673
+ if (msg.role === "assistant") {
674
+ const text = msg.content.filter((b) => b.type === "text").map((b) => b.text).join("");
675
+ const tool_uses = msg.content.filter((b) => b.type === "tool_use");
676
+ const ollamaMsg = { role: "assistant", content: text };
677
+ if (tool_uses.length > 0) {
678
+ ollamaMsg.tool_calls = tool_uses.map((u) => ({
679
+ function: { name: u.name, arguments: u.input }
680
+ }));
461
681
  }
462
- if (text.trim()) out.push({ role: "user", content: text });
463
- } else {
464
- const text = blocks.filter((b) => b.type === "text").map((b) => b.text).join("");
465
- const uses = blocks.filter((b) => b.type === "tool_use").map((b) => ({ id: b.id, name: b.name, input: b.input }));
466
- out.push({
467
- role: "assistant",
468
- content: text,
469
- tool_uses: uses.length ? uses : void 0
470
- });
682
+ out.push(ollamaMsg);
683
+ continue;
471
684
  }
472
- }
473
- return out;
474
- }
475
- async function summarizeMessage(model, text) {
476
- const fallback = text.trim().slice(0, 80) || "untitled";
477
- const prompt = `Summarize this user request as a short title, 3-6 words, no punctuation. Reply with the title only.
478
-
479
- Request:
480
- ${text.slice(0, 2e3)}`;
481
- try {
482
- let out = "";
483
- for await (const chunk of chat(
484
- model,
485
- [{ role: "user", content: prompt }],
486
- void 0,
487
- { temperature: 0.2, num_predict: 32 }
488
- )) {
489
- if (chunk.content) out += chunk.content;
685
+ if (msg.role === "user") {
686
+ const tool_results = msg.content.filter((b) => b.type === "tool_result");
687
+ const texts = msg.content.filter((b) => b.type === "text");
688
+ for (const tr of tool_results) {
689
+ out.push({ role: "tool", content: tr.content });
690
+ }
691
+ if (texts.length > 0) {
692
+ out.push({ role: "user", content: texts.map((t) => t.text).join("") });
693
+ }
490
694
  }
491
- return out.trim().split("\n").filter(Boolean)[0]?.trim() || fallback;
492
- } catch {
493
- return fallback;
494
695
  }
696
+ return out;
495
697
  }
496
-
497
- // src/ui/FilePicker.tsx
498
- import { Box as Box7, Text as Text7 } from "ink";
499
- import { readdirSync as readdirSync2 } from "fs";
500
- import { join as join3, relative } from "path";
501
- import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
502
- var IGNORE = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", ".next", "coverage", ".miii"]);
503
- var MAX_RESULTS = 10;
504
- var MAX_SCAN = 2e3;
505
- var cache = null;
506
- function listFiles(cwd) {
507
- if (cache && cache.cwd === cwd) return cache.files;
508
- const out = [];
509
- const stack = [cwd];
510
- while (stack.length && out.length < MAX_SCAN) {
511
- const dir = stack.pop();
512
- let entries;
513
- try {
514
- entries = readdirSync2(dir, { withFileTypes: true });
515
- } catch {
516
- continue;
698
+ function parseTextToolCalls(text, knownToolNames) {
699
+ if (!text) return { calls: [], cleanedText: text };
700
+ const calls = [];
701
+ let cleaned = text;
702
+ const tagRe = /<\|?tool_call\|?>\s*([\s\S]*?)\s*<\|?\/?tool_call\|?>/g;
703
+ cleaned = cleaned.replace(tagRe, (_m, body) => {
704
+ const c = tryParse(body, knownToolNames);
705
+ if (c) calls.push(c);
706
+ return "";
707
+ });
708
+ const fenceRe = /```(?:json|tool_call)?\s*([\s\S]*?)```/g;
709
+ cleaned = cleaned.replace(fenceRe, (_m, body) => {
710
+ const c = tryParse(body, knownToolNames);
711
+ if (c) {
712
+ calls.push(c);
713
+ return "";
517
714
  }
518
- for (const e of entries) {
519
- if (IGNORE.has(e.name) || e.name.startsWith(".")) continue;
520
- const full = join3(dir, e.name);
521
- if (e.isDirectory()) stack.push(full);
522
- else if (e.isFile()) out.push(relative(cwd, full));
523
- if (out.length >= MAX_SCAN) break;
715
+ return _m;
716
+ });
717
+ if (calls.length === 0) {
718
+ const candidate = extractFirstJsonObject(cleaned);
719
+ if (candidate) {
720
+ const c = tryParse(candidate.json, knownToolNames);
721
+ if (c) {
722
+ calls.push(c);
723
+ cleaned = (cleaned.slice(0, candidate.start) + cleaned.slice(candidate.end)).trim();
724
+ }
524
725
  }
525
726
  }
526
- cache = { cwd, files: out };
527
- return out;
528
- }
529
- function parseMention(input) {
530
- const m = input.match(/(?:^|\s)@([^\s]*)$/);
531
- if (!m) return null;
532
- return { query: m[1], start: input.length - m[1].length - 1 };
727
+ return { calls, cleanedText: cleaned.trim() };
533
728
  }
534
- function searchFiles(cwd, query) {
535
- const files = listFiles(cwd);
536
- const q = query.toLowerCase();
537
- if (!q) return files.slice(0, MAX_RESULTS);
538
- const scored = [];
539
- for (const f of files) {
540
- const lf = f.toLowerCase();
541
- const idx = lf.indexOf(q);
542
- if (idx === -1) continue;
543
- const base = lf.split("/").pop() ?? lf;
544
- const baseIdx = base.indexOf(q);
545
- const score = baseIdx === 0 ? 0 : baseIdx > -1 ? 1 : 2 + idx;
546
- scored.push([score, f]);
547
- if (scored.length > 500) break;
729
+ function tryParse(raw, knownToolNames) {
730
+ const s = raw.trim();
731
+ if (!s.startsWith("{")) return null;
732
+ try {
733
+ const obj = JSON.parse(s);
734
+ const name = typeof obj.name === "string" ? obj.name : void 0;
735
+ const args = obj.arguments ?? obj.parameters ?? obj.input ?? {};
736
+ if (!name || !knownToolNames.includes(name)) return null;
737
+ return { function: { name, arguments: args } };
738
+ } catch {
739
+ return null;
548
740
  }
549
- scored.sort((a, b) => a[0] - b[0] || a[1].length - b[1].length);
550
- return scored.slice(0, MAX_RESULTS).map(([, f]) => f);
551
741
  }
552
- function FilePicker({ matches, cursor }) {
553
- if (matches.length === 0) return null;
554
- return /* @__PURE__ */ jsxs7(
555
- Box7,
556
- {
557
- flexDirection: "column",
558
- borderStyle: "round",
559
- borderColor: "gray",
560
- marginX: 1,
561
- marginBottom: 0,
562
- paddingX: 1,
563
- children: [
564
- matches.map((f, i) => {
565
- const active = i === cursor;
566
- return /* @__PURE__ */ jsx7(Box7, { children: /* @__PURE__ */ jsxs7(Text7, { bold: active, color: active ? "blue" : void 0, dimColor: !active, children: [
567
- active ? "\u276F " : " ",
568
- f
569
- ] }) }, f);
570
- }),
571
- /* @__PURE__ */ jsx7(Box7, { marginTop: 0, children: /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "\u2191\u2193 navigate tab insert esc dismiss" }) })
572
- ]
742
+ function extractFirstJsonObject(s) {
743
+ const start = s.indexOf("{");
744
+ if (start === -1) return null;
745
+ let depth = 0;
746
+ let inStr = false;
747
+ let esc = false;
748
+ for (let i = start; i < s.length; i++) {
749
+ const ch = s[i];
750
+ if (inStr) {
751
+ if (esc) esc = false;
752
+ else if (ch === "\\") esc = true;
753
+ else if (ch === '"') inStr = false;
754
+ continue;
573
755
  }
574
- );
575
- }
576
-
577
- // src/ui/ChatView.tsx
578
- import { Box as Box9, Text as Text9 } from "ink";
579
-
580
- // src/ui/ThinkingBlock.tsx
581
- import { useState as useState2, useEffect as useEffect2 } from "react";
582
- import { Box as Box8, Text as Text8 } from "ink";
583
- import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
584
- var FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
585
- var globalThinkingVisible = false;
586
- var listeners = /* @__PURE__ */ new Set();
587
- function toggleThinkingVisible() {
588
- globalThinkingVisible = !globalThinkingVisible;
589
- listeners.forEach((fn) => fn());
590
- }
591
- function useThinkingVisible() {
592
- const [visible, setVisible] = useState2(globalThinkingVisible);
593
- useEffect2(() => {
594
- const handler = () => setVisible(globalThinkingVisible);
595
- listeners.add(handler);
596
- return () => {
597
- listeners.delete(handler);
598
- };
599
- }, []);
600
- return visible;
756
+ if (ch === '"') {
757
+ inStr = true;
758
+ continue;
759
+ }
760
+ if (ch === "{") depth++;
761
+ else if (ch === "}") {
762
+ depth--;
763
+ if (depth === 0) return { json: s.slice(start, i + 1), start, end: i + 1 };
764
+ }
765
+ }
766
+ return null;
601
767
  }
602
- function ThinkingBlock({ content }) {
603
- const [frame, setFrame] = useState2(0);
604
- const visible = useThinkingVisible();
605
- useEffect2(() => {
606
- const t = setInterval(() => setFrame((f) => (f + 1) % FRAMES.length), 80);
607
- return () => clearInterval(t);
608
- }, []);
609
- return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", marginLeft: 2, marginBottom: 1, children: [
610
- /* @__PURE__ */ jsxs8(Box8, { children: [
611
- /* @__PURE__ */ jsxs8(Text8, { color: "blue", children: [
612
- FRAMES[frame],
613
- " "
614
- ] }),
615
- /* @__PURE__ */ jsx8(Text8, { dimColor: true, italic: true, children: "thinking\u2026" }),
616
- /* @__PURE__ */ jsxs8(Text8, { dimColor: true, children: [
617
- " \xB7 ctrl+t to ",
618
- visible ? "hide" : "show",
619
- " thoughts"
620
- ] })
621
- ] }),
622
- visible && content ? /* @__PURE__ */ jsx8(Box8, { marginLeft: 2, children: /* @__PURE__ */ jsx8(Text8, { dimColor: true, italic: true, children: content }) }) : null
623
- ] });
768
+ function blocksFromOllama(text, tool_calls, knownToolNames = []) {
769
+ const blocks = [];
770
+ let finalText = text;
771
+ let finalCalls = tool_calls ?? [];
772
+ if (finalCalls.length === 0 && knownToolNames.length > 0) {
773
+ const parsed = parseTextToolCalls(text, knownToolNames);
774
+ if (parsed.calls.length > 0) {
775
+ finalCalls = parsed.calls;
776
+ finalText = parsed.cleanedText;
777
+ }
778
+ }
779
+ if (finalText) blocks.push({ type: "text", text: finalText });
780
+ for (const tc of finalCalls) {
781
+ blocks.push({
782
+ type: "tool_use",
783
+ id: mintToolUseId(),
784
+ name: tc.function.name,
785
+ input: tc.function.arguments ?? {}
786
+ });
787
+ }
788
+ return blocks;
624
789
  }
790
+ var init_adapter = __esm({
791
+ "src/agent/adapter.ts"() {
792
+ "use strict";
793
+ }
794
+ });
625
795
 
626
- // src/ui/constants.ts
627
- var EMPTY_STATE_HINTS = [
628
- "\u2022 explain @file \u2014 reference a file with @",
629
- "\u2022 /models \u2014 switch model or effort",
630
- "\u2022 /new \u2014 start a new chat",
631
- "\u2022 /sessions \u2014 view saved chats",
632
- "\u2022 ctrl+t \u2014 toggle thinking"
633
- ];
634
- var EMPTY_STATE_TITLE = "Ask anything, or try:";
635
-
636
- // src/ui/ChatView.tsx
637
- import { jsx as jsx9, jsxs as jsxs9 } from "react/jsx-runtime";
638
- function formatTokens(n) {
639
- if (n >= 1e3) return (n / 1e3).toFixed(n >= 1e4 ? 0 : 1) + "k";
640
- return String(n);
641
- }
642
- function formatDuration(ms) {
643
- const totalSec = ms / 1e3;
644
- if (totalSec < 60) return `${totalSec.toFixed(1)}s`;
645
- const m = Math.floor(totalSec / 60);
646
- const s = Math.round(totalSec - m * 60);
647
- return `${m}m ${s}s`;
648
- }
649
- function countLines(s) {
650
- if (!s) return 0;
651
- return s.split("\n").length;
652
- }
653
- function FileEditBlock({
654
- label,
655
- path,
656
- added,
657
- removed,
658
- previewLines
659
- }) {
660
- const MAX = 16;
661
- const shown = previewLines.slice(0, MAX);
662
- const extra = previewLines.length - shown.length;
663
- return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", marginLeft: 2, children: [
664
- /* @__PURE__ */ jsxs9(Box9, { children: [
665
- /* @__PURE__ */ jsx9(Text9, { color: "yellow", children: "\u25CF " }),
666
- /* @__PURE__ */ jsxs9(Text9, { color: "yellow", children: [
667
- label,
668
- " "
669
- ] }),
670
- /* @__PURE__ */ jsx9(Text9, { children: "(" }),
671
- /* @__PURE__ */ jsx9(Text9, { bold: true, children: path }),
672
- /* @__PURE__ */ jsx9(Text9, { children: ")" })
673
- ] }),
674
- /* @__PURE__ */ jsx9(Box9, { marginLeft: 2, children: /* @__PURE__ */ jsxs9(Text9, { dimColor: true, children: [
675
- "\u23BF ",
676
- removed > 0 ? `Added ${added} lines, removed ${removed} lines` : `Added ${added} lines`
677
- ] }) }),
678
- shown.map((ln, i) => /* @__PURE__ */ jsx9(Box9, { marginLeft: 4, children: /* @__PURE__ */ jsxs9(Text9, { color: ln.sign === "+" ? "green" : ln.sign === "-" ? "red" : void 0, dimColor: ln.sign === " ", children: [
679
- ln.sign,
680
- " ",
681
- ln.text
682
- ] }) }, i)),
683
- extra > 0 && /* @__PURE__ */ jsx9(Box9, { marginLeft: 4, children: /* @__PURE__ */ jsxs9(Text9, { dimColor: true, children: [
684
- "\u2026 ",
685
- extra,
686
- " more lines"
687
- ] }) })
688
- ] });
689
- }
690
- var TOOL_LABEL = {
691
- write_file: "Write",
692
- edit_file: "Update",
693
- read_file: "Read",
694
- run_bash: "Bash",
695
- glob: "Glob",
696
- grep: "Grep"
697
- };
698
- function truncate(s, max) {
699
- if (s.length <= max) return s;
700
- return s.slice(0, max - 1) + "\u2026";
701
- }
702
- function toolHeader(use) {
703
- const label = TOOL_LABEL[use.name] ?? use.name;
704
- const input = use.input ?? {};
705
- let arg = "";
706
- switch (use.name) {
707
- case "write_file":
708
- case "edit_file":
709
- case "read_file":
710
- arg = String(input.path ?? input.file_path ?? "");
711
- break;
712
- case "run_bash": {
713
- const cmd = String(input.command ?? "").replace(/\s+/g, " ");
714
- arg = truncate(cmd, 120);
715
- break;
796
+ // src/agent/loop.ts
797
+ async function* runAgent(opts) {
798
+ const { model, cwd, permissions, hooks, signal, num_ctx } = opts;
799
+ const startTime = Date.now();
800
+ const system = buildSystemPrompt(TOOLS, cwd);
801
+ const ollamaTools = toOllamaTools(TOOLS);
802
+ const history = [
803
+ ...opts.history,
804
+ { role: "user", content: opts.userText }
805
+ ];
806
+ let promptTokens = 0;
807
+ let evalTokens = 0;
808
+ let lastAssistantSig = "";
809
+ let repeatCount = 0;
810
+ for (let turn = 0; turn < MAX_TURNS; turn++) {
811
+ let text = "";
812
+ let tool_calls;
813
+ let lastTail = "";
814
+ let tailRepeats = 0;
815
+ let streamLooped = false;
816
+ const ac = new AbortController();
817
+ const composedSignal = signal ? AbortSignal.any ? AbortSignal.any([signal, ac.signal]) : ac.signal : ac.signal;
818
+ if (signal) signal.addEventListener("abort", () => ac.abort(), { once: true });
819
+ try {
820
+ for await (const chunk of chat(model, toOllamaMessages(history, system), ollamaTools, { signal: composedSignal, num_ctx, num_predict: NUM_PREDICT })) {
821
+ if (signal?.aborted) break;
822
+ if (chunk.content) {
823
+ text += chunk.content;
824
+ yield { type: "text-delta", text: chunk.content };
825
+ if (text.length >= REPEAT_TAIL) {
826
+ const tail = text.slice(-REPEAT_TAIL);
827
+ if (tail === lastTail) {
828
+ tailRepeats++;
829
+ if (tailRepeats >= REPEAT_KILL) {
830
+ streamLooped = true;
831
+ ac.abort();
832
+ break;
833
+ }
834
+ } else {
835
+ tailRepeats = 0;
836
+ lastTail = tail;
837
+ }
838
+ }
839
+ }
840
+ if (chunk.thinking) {
841
+ yield { type: "thinking-delta", text: chunk.thinking };
842
+ }
843
+ if (chunk.tool_calls && chunk.tool_calls.length > 0) {
844
+ tool_calls = chunk.tool_calls;
845
+ }
846
+ if (chunk.done) {
847
+ promptTokens += chunk.prompt_eval_count ?? 0;
848
+ evalTokens += chunk.eval_count ?? 0;
849
+ }
850
+ }
851
+ } catch (err) {
852
+ if (streamLooped) {
853
+ yield { type: "error", message: "Model stuck in repetition. Aborted stream. Try a different model or shorten context." };
854
+ return history;
855
+ }
856
+ yield { type: "error", message: err instanceof Error ? err.message : String(err) };
857
+ return history;
716
858
  }
717
- case "glob":
718
- case "grep":
719
- arg = truncate(String(input.pattern ?? ""), 120);
720
- break;
721
- default: {
722
- arg = truncate(JSON.stringify(input), 80);
859
+ if (streamLooped) {
860
+ yield { type: "error", message: "Model stuck in repetition. Aborted stream. Try a different model or shorten context." };
861
+ return history;
723
862
  }
724
- }
725
- return { label, arg };
726
- }
727
- function summarizeResult(res, toolName) {
728
- const content = res.content ?? "";
729
- const lines = content.split("\n");
730
- if (!res.is_error) {
731
- if (toolName === "read_file") {
732
- const total = lines.length;
733
- return `Read ${total} line${total === 1 ? "" : "s"}`;
863
+ if (signal?.aborted) {
864
+ yield {
865
+ type: "aborted",
866
+ prompt_tokens: promptTokens,
867
+ eval_tokens: evalTokens,
868
+ duration_ms: Date.now() - startTime
869
+ };
870
+ return history;
734
871
  }
735
- if (toolName === "grep") {
736
- if (content === "No matches.") return "No matches";
737
- const n = lines.filter(Boolean).length;
738
- return `${n} match${n === 1 ? "" : "es"}`;
872
+ const blocks = blocksFromOllama(text, tool_calls, TOOLS.map((t) => t.name));
873
+ const tool_uses = blocks.filter((b) => b.type === "tool_use");
874
+ history.push({ role: "assistant", content: blocks });
875
+ if (tool_uses.length === 0) {
876
+ yield { type: "turn-end", stop_reason: "end_turn" };
877
+ break;
739
878
  }
740
- if (toolName === "glob") {
741
- if (content === "No files matched.") return "No files";
742
- const n = lines.filter(Boolean).length;
743
- return `${n} file${n === 1 ? "" : "s"}`;
879
+ const sig = JSON.stringify(
880
+ blocks.map(
881
+ (b) => b.type === "tool_use" ? { t: "u", n: b.name, i: b.input } : b.type === "text" ? { t: "t", x: b.text.trim() } : b
882
+ )
883
+ );
884
+ if (sig === lastAssistantSig) {
885
+ repeatCount++;
886
+ if (repeatCount >= 2) {
887
+ yield { type: "error", message: "Agent loop detected: assistant produced identical output 3 turns in a row" };
888
+ return history;
889
+ }
890
+ } else {
891
+ repeatCount = 0;
892
+ lastAssistantSig = sig;
893
+ }
894
+ for (const u of tool_uses) yield { type: "tool-use", block: u };
895
+ const results = [];
896
+ for (const use of tool_uses) {
897
+ const tool = getTool(use.name);
898
+ if (!tool) {
899
+ const r2 = {
900
+ type: "tool_result",
901
+ tool_use_id: use.id,
902
+ content: `Unknown tool: ${use.name}`,
903
+ is_error: true
904
+ };
905
+ results.push(r2);
906
+ yield { type: "tool-result", block: r2 };
907
+ continue;
908
+ }
909
+ const invalid = validateInput(tool.input_schema, use.input);
910
+ if (invalid) {
911
+ const r2 = {
912
+ type: "tool_result",
913
+ tool_use_id: use.id,
914
+ content: `${invalid} for ${use.name}.`,
915
+ is_error: true
916
+ };
917
+ results.push(r2);
918
+ yield { type: "tool-result", block: r2 };
919
+ continue;
920
+ }
921
+ const decision = await check(use.name, use.input, permissions);
922
+ if (decision === "deny") {
923
+ const r2 = {
924
+ type: "tool_result",
925
+ tool_use_id: use.id,
926
+ content: `Permission denied for ${use.name}.`,
927
+ is_error: true
928
+ };
929
+ results.push(r2);
930
+ yield { type: "permission-denied", toolName: use.name, tool_use_id: use.id };
931
+ yield { type: "tool-result", block: r2 };
932
+ continue;
933
+ }
934
+ try {
935
+ await hooks?.firePre(use);
936
+ } catch {
937
+ }
938
+ let r;
939
+ try {
940
+ const out = await tool.handler(use.input);
941
+ r = {
942
+ type: "tool_result",
943
+ tool_use_id: use.id,
944
+ content: out.content,
945
+ is_error: out.is_error
946
+ };
947
+ } catch (err) {
948
+ r = {
949
+ type: "tool_result",
950
+ tool_use_id: use.id,
951
+ content: err instanceof Error ? err.message : String(err),
952
+ is_error: true
953
+ };
954
+ }
955
+ try {
956
+ await hooks?.firePost(use, r);
957
+ } catch {
958
+ }
959
+ results.push(r);
960
+ yield { type: "tool-result", block: r };
744
961
  }
962
+ history.push({ role: "user", content: results });
963
+ yield { type: "turn-end", stop_reason: "tool_use" };
745
964
  }
746
- const firstNonEmpty = lines.find((l) => l.trim().length > 0) ?? "";
747
- const extra = lines.length - 1;
748
- const head = firstNonEmpty.length > 100 ? firstNonEmpty.slice(0, 97) + "..." : firstNonEmpty;
749
- return extra > 0 ? `${head} (+${extra} lines)` : head;
965
+ yield { type: "done", prompt_tokens: promptTokens, eval_tokens: evalTokens };
966
+ return history;
750
967
  }
751
- function ToolResultBlock({ result, toolName }) {
752
- const content = result.content ?? "";
753
- const lines = content.split("\n");
754
- const showMulti = (toolName === "run_bash" || toolName === "grep" || toolName === "glob" || result.is_error) && lines.length > 1;
755
- if (!showMulti) {
756
- return /* @__PURE__ */ jsx9(Box9, { marginLeft: 2, children: /* @__PURE__ */ jsxs9(Text9, { color: result.is_error ? "red" : void 0, dimColor: !result.is_error, children: [
757
- "\u23BF ",
758
- summarizeResult(result, toolName)
759
- ] }) });
760
- }
761
- const MAX_LINES = 10;
762
- const MAX_LINE_WIDTH = 200;
763
- const shown = lines.slice(0, MAX_LINES).map((l) => truncate(l, MAX_LINE_WIDTH));
764
- const extra = lines.length - shown.length;
765
- return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", marginLeft: 2, children: [
766
- /* @__PURE__ */ jsxs9(Text9, { color: result.is_error ? "red" : void 0, dimColor: !result.is_error, children: [
767
- "\u23BF ",
768
- summarizeResult(result, toolName)
769
- ] }),
770
- shown.map((ln, i) => /* @__PURE__ */ jsx9(Box9, { marginLeft: 4, children: /* @__PURE__ */ jsx9(Text9, { color: result.is_error ? "red" : void 0, dimColor: true, children: ln || " " }) }, i)),
771
- extra > 0 && /* @__PURE__ */ jsx9(Box9, { marginLeft: 4, children: /* @__PURE__ */ jsxs9(Text9, { dimColor: true, children: [
772
- "\u2026 ",
773
- extra,
774
- " more lines"
775
- ] }) })
776
- ] });
777
- }
778
- function ToolUseLine({ use, result }) {
779
- if (use.name === "write_file" && !result?.is_error) {
780
- const input = use.input;
781
- const content = input.content ?? "";
782
- const added = countLines(content);
783
- const preview = content.split("\n").map((t) => ({ sign: "+", text: t }));
784
- return /* @__PURE__ */ jsx9(FileEditBlock, { label: "Write", path: input.path ?? "", added, removed: 0, previewLines: preview });
785
- }
786
- if (use.name === "edit_file" && !result?.is_error) {
787
- const input = use.input;
788
- const oldS = input.old_str ?? "";
789
- const newS = input.new_str ?? "";
790
- const added = countLines(newS);
791
- const removed = countLines(oldS);
792
- const preview = [
793
- ...oldS.split("\n").map((t) => ({ sign: "-", text: t })),
794
- ...newS.split("\n").map((t) => ({ sign: "+", text: t }))
795
- ];
796
- return /* @__PURE__ */ jsx9(FileEditBlock, { label: "Update", path: input.path ?? "", added, removed, previewLines: preview });
797
- }
798
- const { label, arg } = toolHeader(use);
799
- return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", marginLeft: 2, children: [
800
- /* @__PURE__ */ jsxs9(Box9, { children: [
801
- /* @__PURE__ */ jsx9(Text9, { color: "yellow", children: "\u25CF " }),
802
- /* @__PURE__ */ jsxs9(Text9, { color: "yellow", children: [
803
- label,
804
- " "
805
- ] }),
806
- /* @__PURE__ */ jsx9(Text9, { children: "(" }),
807
- /* @__PURE__ */ jsx9(Text9, { bold: true, children: arg }),
808
- /* @__PURE__ */ jsx9(Text9, { children: ")" })
809
- ] }),
810
- result && /* @__PURE__ */ jsx9(ToolResultBlock, { result, toolName: use.name })
811
- ] });
812
- }
813
- function AssistantMessage({ msg }) {
814
- return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", marginBottom: 1, children: [
815
- msg.content && /* @__PURE__ */ jsxs9(Box9, { flexDirection: "row", children: [
816
- /* @__PURE__ */ jsx9(Text9, { color: "white", children: "\u25CF " }),
817
- /* @__PURE__ */ jsx9(Box9, { flexGrow: 1, children: /* @__PURE__ */ jsx9(Text9, { children: msg.content }) })
818
- ] }),
819
- msg.tool_uses?.map((u) => {
820
- const r = msg.tool_results?.find((x) => x.tool_use_id === u.id);
821
- return /* @__PURE__ */ jsx9(ToolUseLine, { use: u, result: r }, u.id);
822
- }),
823
- msg.tokens && /* @__PURE__ */ jsx9(Box9, { marginLeft: 2, children: /* @__PURE__ */ jsxs9(Text9, { dimColor: true, children: [
824
- `\u21B3 Completed \xB7 ${formatTokens(msg.tokens.prompt_eval + msg.tokens.eval)} tokens`,
825
- msg.duration != null ? ` \xB7 ${formatDuration(msg.duration)}` : ""
826
- ] }) })
827
- ] });
828
- }
829
- function summarizeInput(input) {
830
- if (!input || typeof input !== "object") return "";
831
- const obj = input;
832
- const priority = ["path", "file_path", "command", "pattern", "query"];
833
- for (const k of priority) {
834
- const v = obj[k];
835
- if (typeof v === "string" && v.length > 0) return `${k}: ${v}`;
968
+ var MAX_TURNS, NUM_PREDICT, REPEAT_TAIL, REPEAT_KILL;
969
+ var init_loop = __esm({
970
+ "src/agent/loop.ts"() {
971
+ "use strict";
972
+ init_client();
973
+ init_registry();
974
+ init_validate();
975
+ init_system();
976
+ init_policy();
977
+ init_adapter();
978
+ MAX_TURNS = 25;
979
+ NUM_PREDICT = 4096;
980
+ REPEAT_TAIL = 120;
981
+ REPEAT_KILL = 4;
836
982
  }
837
- const first = Object.entries(obj).find(([, v]) => typeof v === "string");
838
- if (first) {
839
- const [k, v] = first;
840
- const trimmed = v.length > 80 ? v.slice(0, 80) + "\u2026" : v;
841
- return `${k}: ${trimmed}`;
842
- }
843
- return "";
844
- }
845
- function PermissionPrompt({ req, cursor }) {
846
- const label = TOOL_LABEL[req.toolName] ?? req.toolName;
847
- const options = [
848
- { label: "Yes", key: "yes" },
849
- { label: "No", key: "no" }
850
- ];
851
- const summary = summarizeInput(req.input);
852
- return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", marginBottom: 1, borderStyle: "round", borderColor: "blue", paddingX: 1, children: [
853
- /* @__PURE__ */ jsx9(Text9, { color: "blue", bold: true, children: "Tool use" }),
854
- /* @__PURE__ */ jsx9(Box9, { marginTop: 1, children: /* @__PURE__ */ jsxs9(Text9, { children: [
855
- "Allow ",
856
- /* @__PURE__ */ jsx9(Text9, { bold: true, children: label }),
857
- "?"
858
- ] }) }),
859
- summary && /* @__PURE__ */ jsx9(Box9, { marginLeft: 2, children: /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: summary }) }),
860
- /* @__PURE__ */ jsx9(Box9, { flexDirection: "column", marginTop: 1, children: options.map((opt, i) => /* @__PURE__ */ jsxs9(Text9, { color: i === cursor ? "blue" : void 0, children: [
861
- i === cursor ? "\u276F " : " ",
862
- i + 1,
863
- ". ",
864
- opt.label
865
- ] }, opt.key)) })
866
- ] });
867
- }
868
- function ChatView({
869
- messages,
870
- streaming,
871
- streamingContent,
872
- thinking,
873
- thinkingContent,
874
- error,
875
- pendingPermission,
876
- permissionCursor = 0,
877
- activeToolUses,
878
- activeToolResults
879
- }) {
880
- const empty = messages.length === 0 && !streaming && !thinking && !pendingPermission && !error;
881
- return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", marginLeft: 1, marginBottom: 1, children: [
882
- empty && /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", marginBottom: 1, children: [
883
- /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: EMPTY_STATE_TITLE }),
884
- EMPTY_STATE_HINTS.map((h, i) => /* @__PURE__ */ jsxs9(Text9, { dimColor: true, children: [
885
- " ",
886
- h
887
- ] }, i))
888
- ] }),
889
- messages.map(
890
- (msg, i) => msg.role === "user" ? /* @__PURE__ */ jsxs9(Box9, { flexDirection: "row", marginBottom: 1, children: [
891
- /* @__PURE__ */ jsx9(Text9, { color: "blue", children: "\u25CF " }),
892
- /* @__PURE__ */ jsx9(Box9, { flexGrow: 1, children: /* @__PURE__ */ jsx9(Text9, { children: msg.content }) })
893
- ] }, i) : /* @__PURE__ */ jsx9(AssistantMessage, { msg }, i)
894
- ),
895
- thinking && /* @__PURE__ */ jsx9(ThinkingBlock, { content: thinkingContent }),
896
- streaming && streamingContent && /* @__PURE__ */ jsxs9(Box9, { flexDirection: "row", marginBottom: 1, children: [
897
- /* @__PURE__ */ jsx9(Text9, { color: "white", children: "\u25CF " }),
898
- /* @__PURE__ */ jsx9(Box9, { flexGrow: 1, children: /* @__PURE__ */ jsx9(Text9, { children: streamingContent }) })
899
- ] }),
900
- activeToolUses?.map((u) => {
901
- const r = activeToolResults?.find((x) => x.tool_use_id === u.id);
902
- return /* @__PURE__ */ jsx9(ToolUseLine, { use: u, result: r }, u.id);
903
- }),
904
- pendingPermission && /* @__PURE__ */ jsx9(PermissionPrompt, { req: pendingPermission, cursor: permissionCursor }),
905
- error && /* @__PURE__ */ jsxs9(Box9, { flexDirection: "row", marginBottom: 1, children: [
906
- /* @__PURE__ */ jsx9(Text9, { color: "red", children: "\u25CF " }),
907
- /* @__PURE__ */ jsx9(Text9, { color: "red", children: error })
908
- ] })
909
- ] });
910
- }
911
-
912
- // src/ui/hooks/useAgentRunner.ts
913
- import { useState as useState3, useRef } from "react";
983
+ });
914
984
 
915
- // src/tools/edit_file.ts
916
- import { readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
917
- var edit_file = {
918
- name: "edit_file",
919
- description: "Replace an exact string in a file. old_str must be unique.",
920
- input_schema: {
921
- type: "object",
922
- properties: {
923
- path: { type: "string", description: "File path" },
924
- old_str: { type: "string", description: "Exact text to replace" },
925
- new_str: { type: "string", description: "Replacement text" }
926
- },
927
- required: ["path", "old_str", "new_str"]
928
- },
929
- handler: ({ path, old_str, new_str }) => {
930
- const src = readFileSync3(path, "utf-8");
931
- const first = src.indexOf(old_str);
932
- if (first === -1) {
933
- return { content: `old_str not found in ${path}`, is_error: true };
934
- }
935
- if (src.indexOf(old_str, first + 1) !== -1) {
936
- return { content: `old_str not unique in ${path}`, is_error: true };
937
- }
938
- writeFileSync3(path, src.slice(0, first) + new_str + src.slice(first + old_str.length), "utf-8");
939
- return { content: `Edited ${path}` };
985
+ // eval/runner.ts
986
+ import { mkdtempSync, writeFileSync as writeFileSync6, mkdirSync as mkdirSync5, rmSync as rmSync2 } from "fs";
987
+ import { dirname as dirname2, join as join5 } from "path";
988
+ import { tmpdir } from "os";
989
+ async function runScenario(model, s) {
990
+ const dir = mkdtempSync(join5(tmpdir(), "miii-eval-"));
991
+ const prevCwd = process.cwd();
992
+ for (const [rel, content] of Object.entries(s.files ?? {})) {
993
+ const abs = join5(dir, rel);
994
+ mkdirSync5(dirname2(abs), { recursive: true });
995
+ writeFileSync6(abs, content, "utf-8");
940
996
  }
941
- };
942
-
943
- // src/tools/read_file.ts
944
- import { readFileSync as readFileSync4 } from "fs";
945
- var read_file = {
946
- name: "read_file",
947
- description: "Read entire file contents as UTF-8 text.",
948
- input_schema: {
949
- type: "object",
950
- properties: {
951
- path: { type: "string", description: "File path" }
952
- },
953
- required: ["path"]
954
- },
955
- handler: ({ path }) => {
956
- try {
957
- const MAX = 2e5;
958
- const raw = readFileSync4(path, "utf-8");
959
- const truncated = raw.length > MAX;
960
- const body = truncated ? raw.slice(0, MAX) + `
961
- [truncated: ${raw.length - MAX} more chars]` : raw;
962
- return { content: body };
963
- } catch (err) {
964
- return { content: err instanceof Error ? err.message : String(err), is_error: true };
997
+ const r = {
998
+ name: s.name,
999
+ pass: false,
1000
+ toolCalls: 0,
1001
+ promptTokens: 0,
1002
+ evalTokens: 0,
1003
+ durationMs: 0
1004
+ };
1005
+ const start = Date.now();
1006
+ let finalText = "";
1007
+ try {
1008
+ process.chdir(dir);
1009
+ const gen = runAgent({
1010
+ model,
1011
+ cwd: dir,
1012
+ history: [],
1013
+ userText: s.prompt,
1014
+ permissions: autoYes
1015
+ });
1016
+ for await (const ev of gen) {
1017
+ if (ev.type === "tool-use") r.toolCalls++;
1018
+ else if (ev.type === "text-delta") finalText += ev.text;
1019
+ else if (ev.type === "turn-end" && ev.stop_reason === "tool_use") finalText = "";
1020
+ else if (ev.type === "done") {
1021
+ r.promptTokens = ev.prompt_tokens;
1022
+ r.evalTokens = ev.eval_tokens;
1023
+ } else if (ev.type === "error") r.error = ev.message;
965
1024
  }
1025
+ } catch (err) {
1026
+ r.error = err instanceof Error ? err.message : String(err);
1027
+ } finally {
1028
+ process.chdir(prevCwd);
966
1029
  }
967
- };
968
-
969
- // src/tools/write_file.ts
970
- import { writeFileSync as writeFileSync4, mkdirSync as mkdirSync3 } from "fs";
971
- import { dirname } from "path";
972
- var write_file = {
973
- name: "write_file",
974
- description: "Create or overwrite a file with the given content. Parent dirs auto-created.",
975
- input_schema: {
976
- type: "object",
977
- properties: {
978
- path: { type: "string", description: "File path" },
979
- content: { type: "string", description: "Full file content" }
980
- },
981
- required: ["path", "content"]
982
- },
983
- handler: ({ path, content }) => {
984
- try {
985
- mkdirSync3(dirname(path), { recursive: true });
986
- writeFileSync4(path, content, "utf-8");
987
- return { content: `Wrote ${path} (${content.length} bytes)` };
988
- } catch (err) {
989
- return { content: err instanceof Error ? err.message : String(err), is_error: true };
990
- }
1030
+ r.durationMs = Date.now() - start;
1031
+ if (r.error) {
1032
+ r.reason = `loop error: ${r.error}`;
1033
+ rmSync2(dir, { recursive: true, force: true });
1034
+ return r;
991
1035
  }
992
- };
993
-
994
- // src/tools/run_bash.ts
995
- import { execa } from "execa";
996
- var run_bash = {
997
- name: "run_bash",
998
- description: "Execute a shell command (bash on Unix, cmd on Windows). Returns stdout+stderr. Non-interactive only.",
999
- input_schema: {
1000
- type: "object",
1001
- properties: {
1002
- command: { type: "string", description: "Shell command to run" },
1003
- timeout_ms: { type: "number", description: "Timeout in ms (default 30000)" }
1004
- },
1005
- required: ["command"]
1006
- },
1007
- handler: async ({ command, timeout_ms }) => {
1008
- try {
1009
- const isWin = process.platform === "win32";
1010
- const shell = isWin ? "cmd" : "bash";
1011
- const shellArgs = isWin ? ["/c", command] : ["-c", command];
1012
- const { stdout, stderr, exitCode } = await execa(shell, shellArgs, {
1013
- timeout: timeout_ms ?? 3e4,
1014
- reject: false,
1015
- all: false
1016
- });
1017
- const out = [stdout, stderr].filter(Boolean).join("\n");
1018
- const is_error = exitCode !== 0;
1019
- const body = out || (is_error ? `(no output)` : "");
1020
- const content = is_error ? `${body}
1021
- [exit ${exitCode}]` : body;
1022
- return {
1023
- content: content.slice(0, 32e3),
1024
- is_error
1025
- };
1026
- } catch (err) {
1027
- return { content: err instanceof Error ? err.message : String(err), is_error: true };
1028
- }
1036
+ try {
1037
+ const verdict2 = await s.check(dir, finalText.trim());
1038
+ if (verdict2 === true) r.pass = true;
1039
+ else r.reason = typeof verdict2 === "string" ? verdict2 : "check returned false";
1040
+ } catch (err) {
1041
+ r.reason = `check threw: ${err instanceof Error ? err.message : String(err)}`;
1029
1042
  }
1030
- };
1043
+ rmSync2(dir, { recursive: true, force: true });
1044
+ return r;
1045
+ }
1046
+ var autoYes;
1047
+ var init_runner = __esm({
1048
+ "eval/runner.ts"() {
1049
+ "use strict";
1050
+ init_loop();
1051
+ autoYes = { ask: async () => "yes" };
1052
+ }
1053
+ });
1031
1054
 
1032
- // src/tools/grep.ts
1033
- import { execa as execa2 } from "execa";
1034
- var grep = {
1035
- name: "grep",
1036
- description: "Search file contents for a regex pattern. Uses ripgrep if available, falls back to grep -R.",
1037
- input_schema: {
1038
- type: "object",
1039
- properties: {
1040
- pattern: { type: "string", description: "Regex pattern" },
1041
- path: { type: "string", description: "Root path to search (default cwd)" },
1042
- glob: { type: "string", description: 'File glob filter, e.g. "*.ts"' },
1043
- case_insensitive: { type: "string", description: 'Set "true" for case-insensitive' },
1044
- max_results: { type: "number", description: "Max matching lines (default 200)" }
1045
- },
1046
- required: ["pattern"]
1047
- },
1048
- handler: async ({ pattern, path, glob: glob2, case_insensitive, max_results }) => {
1049
- const root = path ?? process.cwd();
1050
- const limit = max_results ?? 200;
1051
- const ci = case_insensitive === true || String(case_insensitive) === "true";
1052
- const tryRg = async () => {
1053
- const args = ["--line-number", "--no-heading", "--color=never", "-m", String(limit)];
1054
- if (ci) args.push("-i");
1055
- if (glob2) args.push("--glob", glob2);
1056
- args.push("--", pattern, root);
1057
- return execa2("rg", args, { reject: false, timeout: 2e4 });
1058
- };
1059
- const tryGrep = async () => {
1060
- const args = ["-R", "-n", "--color=never"];
1061
- if (ci) args.push("-i");
1062
- if (glob2) args.push("--include", glob2);
1063
- args.push("--", pattern, root);
1064
- return execa2("grep", args, { reject: false, timeout: 2e4 });
1065
- };
1066
- try {
1067
- let res;
1068
- try {
1069
- res = await tryRg();
1070
- if (res.exitCode === 127 || (res.stderr ?? "").includes("command not found")) {
1071
- res = await tryGrep();
1055
+ // eval/scenarios.ts
1056
+ import { readFileSync as readFileSync6, existsSync as existsSync4 } from "fs";
1057
+ import { join as join6 } from "path";
1058
+ var read, scenarios;
1059
+ var init_scenarios = __esm({
1060
+ "eval/scenarios.ts"() {
1061
+ "use strict";
1062
+ read = (dir, f) => existsSync4(join6(dir, f)) ? readFileSync6(join6(dir, f), "utf-8") : null;
1063
+ scenarios = [
1064
+ {
1065
+ name: "edit-exact-string",
1066
+ prompt: "In config.js, change the port from 3000 to 8080. Change nothing else.",
1067
+ files: { "config.js": 'export const port = 3000\nexport const host = "localhost"\n' },
1068
+ check: (dir) => {
1069
+ const out = read(dir, "config.js");
1070
+ if (out == null) return "config.js missing";
1071
+ if (!out.includes("8080")) return "port not changed to 8080";
1072
+ if (out.includes("3000")) return "old port 3000 still present";
1073
+ if (!out.includes('host = "localhost"')) return "unrelated line damaged";
1074
+ return true;
1075
+ }
1076
+ },
1077
+ {
1078
+ name: "read-then-answer",
1079
+ prompt: "What is the value of the MAX_RETRIES constant in limits.js? Reply with just the number.",
1080
+ files: { "limits.js": "export const MAX_RETRIES = 7\n" },
1081
+ check: (dir, finalText) => {
1082
+ if (read(dir, "limits.js")?.includes("MAX_RETRIES = 7") !== true)
1083
+ return "agent mutated a read-only task";
1084
+ if (!/\b7\b/.test(finalText)) return `answer missing "7": ${JSON.stringify(finalText)}`;
1085
+ return true;
1086
+ }
1087
+ },
1088
+ {
1089
+ name: "create-new-file",
1090
+ prompt: "Create a file named greeting.txt containing exactly the text: hello world",
1091
+ check: (dir) => {
1092
+ const out = read(dir, "greeting.txt");
1093
+ if (out == null) return "greeting.txt not created";
1094
+ if (out.trim() !== "hello world") return `wrong content: ${JSON.stringify(out)}`;
1095
+ return true;
1096
+ }
1097
+ },
1098
+ {
1099
+ name: "grep-locate",
1100
+ prompt: "Which file defines a function called computeTax? Reply with just the filename.",
1101
+ files: {
1102
+ "a.js": "export function formatDate() {}\n",
1103
+ "b.js": "export function computeTax(x) { return x * 0.1 }\n",
1104
+ "c.js": "export function parseArgs() {}\n"
1105
+ },
1106
+ check: (dir, finalText) => {
1107
+ if (read(dir, "b.js")?.includes("computeTax") !== true) return "b.js damaged";
1108
+ if (!/\bb\.js\b/.test(finalText)) return `answer missing "b.js": ${JSON.stringify(finalText)}`;
1109
+ return true;
1072
1110
  }
1073
- } catch {
1074
- res = await tryGrep();
1075
1111
  }
1076
- const lines = (res.stdout ?? "").split("\n").slice(0, limit);
1077
- const out = lines.join("\n");
1078
- const code = res.exitCode ?? 0;
1079
- if (!out && code === 1) return { content: "No matches." };
1080
- return { content: out || res.stderr || "No matches.", is_error: code > 1 };
1081
- } catch (err) {
1082
- return { content: err instanceof Error ? err.message : String(err), is_error: true };
1083
- }
1112
+ ];
1084
1113
  }
1085
- };
1114
+ });
1086
1115
 
1087
- // src/tools/glob.ts
1088
- import { execa as execa3 } from "execa";
1089
- function globToFindName(glob2) {
1090
- return glob2;
1091
- }
1092
- var glob = {
1093
- name: "glob",
1094
- description: 'List files matching a glob pattern (e.g. "**/*.ts"). Uses ripgrep --files if available.',
1095
- input_schema: {
1096
- type: "object",
1097
- properties: {
1098
- pattern: { type: "string", description: 'Glob pattern, e.g. "**/*.ts"' },
1099
- path: { type: "string", description: "Root path (default cwd)" },
1100
- max_results: { type: "number", description: "Max paths returned (default 500)" }
1101
- },
1102
- required: ["pattern"]
1103
- },
1104
- handler: async ({ pattern, path, max_results }) => {
1105
- const root = path ?? process.cwd();
1106
- const limit = max_results ?? 500;
1107
- const tryRg = () => execa3("rg", ["--files", "--hidden", "--glob", pattern, root], {
1108
- reject: false,
1109
- timeout: 2e4
1110
- });
1111
- const tryFind = () => {
1112
- const name = globToFindName(pattern.replace(/^\*\*\//, ""));
1113
- return execa3("find", [root, "-type", "f", "-name", name], {
1114
- reject: false,
1115
- timeout: 2e4
1116
- });
1117
- };
1118
- try {
1119
- let res;
1120
- try {
1121
- res = await tryRg();
1122
- if (res.exitCode === 127 || (res.stderr ?? "").includes("command not found")) {
1123
- res = await tryFind();
1124
- }
1125
- } catch {
1126
- res = await tryFind();
1127
- }
1128
- const lines = (res.stdout ?? "").split("\n").filter(Boolean).slice(0, limit);
1129
- if (lines.length === 0) return { content: "No files matched." };
1130
- return { content: lines.join("\n") };
1131
- } catch (err) {
1132
- return { content: err instanceof Error ? err.message : String(err), is_error: true };
1116
+ // eval/run.ts
1117
+ var run_exports = {};
1118
+ __export(run_exports, {
1119
+ runEval: () => runEval
1120
+ });
1121
+ function pad(s, n) {
1122
+ return s.length >= n ? s.slice(0, n) : s + " ".repeat(n - s.length);
1123
+ }
1124
+ async function resolveModels(modelsArg) {
1125
+ if (modelsArg !== "all") return modelsArg.split(",").map((m) => m.trim()).filter(Boolean);
1126
+ return (await listModels()).filter((m) => !m.includes("cloud"));
1127
+ }
1128
+ function verdict(passed, total) {
1129
+ const ratio = total === 0 ? 0 : passed / total;
1130
+ if (ratio === 1) return "ready";
1131
+ if (ratio >= 0.5) return "marginal \u2014 some tasks fail";
1132
+ return "not recommended \u2014 weak tool-calling";
1133
+ }
1134
+ async function runModel(model, picked) {
1135
+ console.log(`
1136
+ === ${model} ===`);
1137
+ const results = [];
1138
+ for (const s of picked) {
1139
+ const r = await runScenario(model, s);
1140
+ results.push(r);
1141
+ const mark = r.pass ? "PASS" : "FAIL";
1142
+ const detail = r.pass ? "" : ` ${r.reason ?? r.error ?? ""}`;
1143
+ console.log(
1144
+ `${mark} ${pad(r.name, 22)} ${pad(`${r.toolCalls} calls`, 9)} ${pad(`${r.evalTokens} tok`, 11)} ${pad(`${r.durationMs}ms`, 8)}${detail}`
1145
+ );
1146
+ }
1147
+ const passed = results.filter((r) => r.pass).length;
1148
+ console.log(` \u2192 ${model}: ${passed}/${picked.length} \u2014 ${verdict(passed, picked.length)}`);
1149
+ return results;
1150
+ }
1151
+ function printMatrix(models, picked, grid) {
1152
+ const w = Math.max(...picked.map((s) => s.name.length), 3) + 1;
1153
+ const modelW = Math.max(...models.map((m) => m.length), 5) + 1;
1154
+ console.log("\nMatrix\n");
1155
+ let header = pad("", modelW);
1156
+ for (const s of picked) header += pad(s.name.slice(0, w - 1), w);
1157
+ console.log(header + " score");
1158
+ for (const m of models) {
1159
+ let row = pad(m, modelW);
1160
+ const rs = grid.get(m) ?? [];
1161
+ let passed = 0;
1162
+ for (const s of picked) {
1163
+ const r = rs.find((x) => x.name === s.name);
1164
+ const cell = !r ? "?" : r.pass ? "+" : ".";
1165
+ if (r?.pass) passed++;
1166
+ row += pad(cell, w);
1133
1167
  }
1168
+ row += ` ${passed}/${picked.length}`;
1169
+ console.log(row);
1134
1170
  }
1135
- };
1171
+ console.log("\n + pass . fail ? not run");
1172
+ }
1173
+ async function runEval(args) {
1174
+ const strip = (s) => (s ?? "").replace(/^-+/, "");
1175
+ const modelsArg = strip(args[0]) || process.env.MIII_EVAL_MODEL || "all";
1176
+ const filter = strip(args[1]);
1177
+ const picked = filter ? scenarios.filter((s) => s.name.includes(filter)) : scenarios;
1178
+ if (picked.length === 0) {
1179
+ console.error(`No scenarios match "${filter}"`);
1180
+ return 1;
1181
+ }
1182
+ const models = await resolveModels(modelsArg);
1183
+ if (models.length === 0) {
1184
+ console.error("No models to run.");
1185
+ return 1;
1186
+ }
1187
+ console.log(`models: ${models.length} scenarios: ${picked.length}`);
1188
+ const grid = /* @__PURE__ */ new Map();
1189
+ for (const model of models) grid.set(model, await runModel(model, picked));
1190
+ if (models.length > 1) printMatrix(models, picked, grid);
1191
+ const allPass = [...grid.values()].every((rs) => rs.every((r) => r.pass));
1192
+ return allPass ? 0 : 1;
1193
+ }
1194
+ var init_run = __esm({
1195
+ "eval/run.ts"() {
1196
+ "use strict";
1197
+ init_runner();
1198
+ init_scenarios();
1199
+ init_client();
1200
+ }
1201
+ });
1136
1202
 
1137
- // src/tools/registry.ts
1138
- var TOOLS = [
1139
- edit_file,
1140
- read_file,
1141
- write_file,
1142
- run_bash,
1143
- grep,
1144
- glob
1145
- ];
1146
- function getTool(name) {
1147
- return TOOLS.find((t) => t.name === name);
1148
- }
1149
- function toOllamaTools(tools = TOOLS) {
1150
- return tools.map((t) => ({
1151
- type: "function",
1152
- function: {
1153
- name: t.name,
1154
- description: t.description,
1155
- parameters: {
1156
- type: "object",
1157
- properties: t.input_schema.properties,
1158
- required: t.input_schema.required
1159
- }
1160
- }
1161
- }));
1162
- }
1163
-
1164
- // src/prompt/system.ts
1165
- function buildSystemPrompt(tools, cwd) {
1166
- const toolLines = tools.map((t) => `- ${t.name}: ${t.description}`).join("\n");
1167
- return `You are miii, a senior software engineer running in a terminal.
1168
-
1169
- Working directory: ${cwd}
1170
-
1171
- # Goal Understanding (read this first, every turn)
1172
- Before acting on any request, extract and hold three things:
1173
- GOAL: what the user ultimately wants (outcome, not steps)
1174
- CRITERION: how you will know the goal is met
1175
- GAPS: anything unclear that would force you to guess
1176
-
1177
- If GAPS is non-empty, ask the minimum questions needed to fill them \u2014 one message, numbered list \u2014 before touching any file or running any command. Do not guess. Do not act on assumptions.
1178
-
1179
- Re-read GOAL before every tool call. If a tool call does not move toward GOAL, skip it.
1180
-
1181
- # Attention: re-attend to goal at each step
1182
- After each tool result, answer silently: "Does this result move me toward GOAL?"
1183
- YES \u2192 continue
1184
- NO \u2192 stop, re-derive plan from GOAL, explain the correction in one line
1185
-
1186
- This prevents drift. Each step attends to the original goal, not just the previous step.
1187
-
1188
- # Output format
1189
- - Always reply in plain text. Never use Markdown syntax: no \`#\` headings, no \`**bold**\`, no \`-\` bullet lists, no fenced \`\`\` code blocks, no inline backticks.
1190
- - Quote code, paths, and identifiers inline as plain text. Do not wrap them.
1191
- - Keep prose terse.
1192
-
1193
- # Engineering mindset
1194
- - Treat every request as one of: bug, feature, or fix. Name which one before you start.
1195
- - Apply first principles: decompose unclear tasks into smallest concrete sub-problems, solve each explicitly, compose the result.
1196
- - Never guess. If a fact (file path, function signature, current behavior) is unknown, read or search for it first.
1197
-
1198
- # Clarifying questions \u2014 when to ask
1199
- Ask BEFORE acting when:
1200
- - The goal has more than one valid interpretation
1201
- - Success criterion is ambiguous (e.g. "make it better" \u2014 better how?)
1202
- - Required context is missing (which file? which behavior? which user?)
1203
- - Two reasonable approaches have different tradeoffs the user should choose
1204
-
1205
- Do NOT ask when:
1206
- - The answer is findable by reading the codebase
1207
- - There is only one sensible interpretation
1208
- - The user has already answered this implicitly
1209
-
1210
- Ask in a numbered list. One round of questions per turn. Then wait.
1211
-
1212
- # Tool calls
1213
- - When you need a tool, emit the tool call directly. No preamble, no narration, no "I will use X".
1214
- - Never describe a tool call instead of emitting it. If you cannot emit the call, answer in plain text.
1215
- - After a tool result, move directly to the next tool call or the final answer. Do not restate what the previous tool did.
1216
-
1217
- # Tools
1218
- You have access to the following tools. Call them via the function-calling interface.
1219
- ${toolLines}
1220
-
1221
- # Loop semantics
1222
- - When you need to act on the filesystem or run a command, emit a tool call.
1223
- - After each tool result, decide: more tool calls, or a final plain-text answer.
1224
- - Stop emitting tool calls when GOAL is met. Reply with a concise plain-text final message confirming CRITERION is satisfied.
1225
-
1226
- # Rules
1227
- - Prefer editing existing files over creating new ones.
1228
- - For edit_file, ensure old_str is unique within the target file.
1229
- - Never invent file paths. Read, glob, or grep before editing.
1230
- - No filler, no pleasantries, no apologies.
1203
+ // src/cli.tsx
1204
+ import { render } from "ink";
1205
+ import { createElement } from "react";
1231
1206
 
1232
- # Testing and verification
1233
- - Always test the code after a change. Run the project's tests (e.g. npm test, pytest, go test) or the relevant script via run_bash before declaring a task done.
1234
- - If no test exists for the change, run the affected entry point via run_bash to verify it behaves correctly.
1235
- - Treat a green test run or a successful command as the completion signal. If it fails, fix and re-run.
1207
+ // src/ui/App.tsx
1208
+ init_client();
1209
+ import { useState as useState4, useEffect as useEffect3 } from "react";
1210
+ import { Box as Box10, Text as Text10, useApp } from "ink";
1211
+ import { homedir as homedir4 } from "os";
1212
+ import { sep as sep2 } from "path";
1236
1213
 
1237
- # Permissions
1238
- - When a new bash command pattern, file path, or glob pattern is needed, ask the user once; on approval it persists as a Tool(pattern) rule (e.g. Bash(npm test *), WriteFile(src/*)).
1239
- `;
1214
+ // src/config.ts
1215
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
1216
+ import { join } from "path";
1217
+ import { homedir } from "os";
1218
+ var CONFIG_DIR = join(homedir(), ".miii");
1219
+ var CONFIG_PATH = join(CONFIG_DIR, "config.json");
1220
+ function loadConfig() {
1221
+ if (!existsSync(CONFIG_PATH)) return {};
1222
+ try {
1223
+ return JSON.parse(readFileSync(CONFIG_PATH, "utf-8"));
1224
+ } catch {
1225
+ return {};
1226
+ }
1240
1227
  }
1241
-
1242
- // src/permissions/policy.ts
1243
- var DEFAULT_ALLOW = /* @__PURE__ */ new Set(["read_file"]);
1244
- async function check(toolName, input, ctx) {
1245
- if (DEFAULT_ALLOW.has(toolName)) return "allow";
1246
- const answer = await ctx.ask(toolName, input);
1247
- return answer === "no" ? "deny" : "allow";
1228
+ function saveConfig(config) {
1229
+ mkdirSync(CONFIG_DIR, { recursive: true });
1230
+ writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), "utf-8");
1248
1231
  }
1249
-
1250
- // src/agent/adapter.ts
1251
- function mintToolUseId() {
1252
- const rand = Math.random().toString(36).slice(2, 14);
1253
- return `toolu_${rand}`;
1232
+ function setModel(model) {
1233
+ saveConfig({ ...loadConfig(), model });
1254
1234
  }
1255
- function toOllamaMessages(history, system) {
1256
- const out = [{ role: "system", content: system }];
1257
- for (const msg of history) {
1258
- if (typeof msg.content === "string") {
1259
- out.push({ role: msg.role === "system" ? "system" : msg.role, content: msg.content });
1260
- continue;
1261
- }
1262
- if (msg.role === "assistant") {
1263
- const text = msg.content.filter((b) => b.type === "text").map((b) => b.text).join("");
1264
- const tool_uses = msg.content.filter((b) => b.type === "tool_use");
1265
- const ollamaMsg = { role: "assistant", content: text };
1266
- if (tool_uses.length > 0) {
1267
- ollamaMsg.tool_calls = tool_uses.map((u) => ({
1268
- function: { name: u.name, arguments: u.input }
1269
- }));
1270
- }
1271
- out.push(ollamaMsg);
1272
- continue;
1273
- }
1274
- if (msg.role === "user") {
1275
- const tool_results = msg.content.filter((b) => b.type === "tool_result");
1276
- const texts = msg.content.filter((b) => b.type === "text");
1277
- for (const tr of tool_results) {
1278
- out.push({ role: "tool", content: tr.content });
1279
- }
1280
- if (texts.length > 0) {
1281
- out.push({ role: "user", content: texts.map((t) => t.text).join("") });
1282
- }
1283
- }
1284
- }
1285
- return out;
1235
+ function setEffort(effort) {
1236
+ saveConfig({ ...loadConfig(), effort });
1286
1237
  }
1287
- function parseTextToolCalls(text, knownToolNames) {
1288
- if (!text) return { calls: [], cleanedText: text };
1289
- const calls = [];
1290
- let cleaned = text;
1291
- const tagRe = /<\|?tool_call\|?>\s*([\s\S]*?)\s*<\|?\/?tool_call\|?>/g;
1292
- cleaned = cleaned.replace(tagRe, (_m, body) => {
1293
- const c = tryParse(body, knownToolNames);
1294
- if (c) calls.push(c);
1295
- return "";
1296
- });
1297
- const fenceRe = /```(?:json|tool_call)?\s*([\s\S]*?)```/g;
1298
- cleaned = cleaned.replace(fenceRe, (_m, body) => {
1299
- const c = tryParse(body, knownToolNames);
1300
- if (c) {
1301
- calls.push(c);
1302
- return "";
1303
- }
1304
- return _m;
1305
- });
1306
- if (calls.length === 0) {
1307
- const candidate = extractFirstJsonObject(cleaned);
1308
- if (candidate) {
1309
- const c = tryParse(candidate.json, knownToolNames);
1310
- if (c) {
1311
- calls.push(c);
1312
- cleaned = (cleaned.slice(0, candidate.start) + cleaned.slice(candidate.end)).trim();
1313
- }
1238
+
1239
+ // src/ui/WelcomeBlock.tsx
1240
+ import { Box, Text } from "ink";
1241
+ import { jsx, jsxs } from "react/jsx-runtime";
1242
+ function WelcomeBlock({ model, activeCtx, effort, cwd }) {
1243
+ const ctxLabel = activeCtx != null ? `${Math.round(activeCtx / 1024)}k ctx` : "\u2014 ctx";
1244
+ return /* @__PURE__ */ jsxs(
1245
+ Box,
1246
+ {
1247
+ flexDirection: "column",
1248
+ borderStyle: "round",
1249
+ borderColor: "gray",
1250
+ paddingX: 2,
1251
+ marginBottom: 1,
1252
+ children: [
1253
+ /* @__PURE__ */ jsxs(Box, { gap: 2, children: [
1254
+ /* @__PURE__ */ jsx(Text, { bold: true, color: "blue", children: "MIII CLI" }),
1255
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\xB7" }),
1256
+ /* @__PURE__ */ jsx(Text, { children: model ?? "/models" }),
1257
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\xB7" }),
1258
+ /* @__PURE__ */ jsx(Text, { children: ctxLabel }),
1259
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\xB7" }),
1260
+ /* @__PURE__ */ jsxs(Text, { children: [
1261
+ effort,
1262
+ " effort"
1263
+ ] })
1264
+ ] }),
1265
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: cwd })
1266
+ ]
1314
1267
  }
1315
- }
1316
- return { calls, cleanedText: cleaned.trim() };
1317
- }
1318
- function tryParse(raw, knownToolNames) {
1319
- const s = raw.trim();
1320
- if (!s.startsWith("{")) return null;
1321
- try {
1322
- const obj = JSON.parse(s);
1323
- const name = typeof obj.name === "string" ? obj.name : void 0;
1324
- const args = obj.arguments ?? obj.parameters ?? obj.input ?? {};
1325
- if (!name || !knownToolNames.includes(name)) return null;
1326
- return { function: { name, arguments: args } };
1327
- } catch {
1328
- return null;
1329
- }
1268
+ );
1330
1269
  }
1331
- function extractFirstJsonObject(s) {
1332
- const start = s.indexOf("{");
1333
- if (start === -1) return null;
1334
- let depth = 0;
1335
- let inStr = false;
1336
- let esc = false;
1337
- for (let i = start; i < s.length; i++) {
1338
- const ch = s[i];
1339
- if (inStr) {
1340
- if (esc) esc = false;
1341
- else if (ch === "\\") esc = true;
1342
- else if (ch === '"') inStr = false;
1343
- continue;
1344
- }
1345
- if (ch === '"') {
1346
- inStr = true;
1347
- continue;
1348
- }
1349
- if (ch === "{") depth++;
1350
- else if (ch === "}") {
1351
- depth--;
1352
- if (depth === 0) return { json: s.slice(start, i + 1), start, end: i + 1 };
1353
- }
1270
+
1271
+ // src/ui/ModelList.tsx
1272
+ import { Box as Box2, Text as Text2 } from "ink";
1273
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
1274
+ function ModelList({ models, cursor, activeModel, showActive }) {
1275
+ if (models.length === 0) {
1276
+ return /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
1277
+ "no models found. run: ollama pull ",
1278
+ "<model>"
1279
+ ] });
1354
1280
  }
1355
- return null;
1281
+ return /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", borderStyle: "round", borderColor: "gray", paddingX: 1, children: models.map((m, i) => /* @__PURE__ */ jsxs2(Text2, { color: i === cursor ? "blue" : void 0, dimColor: i !== cursor, children: [
1282
+ i === cursor ? "\u276F " : " ",
1283
+ m,
1284
+ showActive && m === activeModel ? /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: " (active)" }) : null
1285
+ ] }, m)) });
1356
1286
  }
1357
- function blocksFromOllama(text, tool_calls, knownToolNames = []) {
1358
- const blocks = [];
1359
- let finalText = text;
1360
- let finalCalls = tool_calls ?? [];
1361
- if (finalCalls.length === 0 && knownToolNames.length > 0) {
1362
- const parsed = parseTextToolCalls(text, knownToolNames);
1363
- if (parsed.calls.length > 0) {
1364
- finalCalls = parsed.calls;
1365
- finalText = parsed.cleanedText;
1287
+
1288
+ // src/ui/InputBar.tsx
1289
+ import { useEffect, useState } from "react";
1290
+ import { Box as Box3, Text as Text3 } from "ink";
1291
+ import { Fragment, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
1292
+ var SPIN = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
1293
+ function InputBar({ input, disabled, processingLabel }) {
1294
+ const [frame, setFrame] = useState(0);
1295
+ useEffect(() => {
1296
+ if (!disabled) return;
1297
+ const t = setInterval(() => setFrame((f) => (f + 1) % SPIN.length), 150);
1298
+ return () => clearInterval(t);
1299
+ }, [disabled]);
1300
+ return /* @__PURE__ */ jsx3(
1301
+ Box3,
1302
+ {
1303
+ borderStyle: "single",
1304
+ borderTop: true,
1305
+ borderBottom: true,
1306
+ borderLeft: false,
1307
+ borderRight: false,
1308
+ borderColor: disabled ? "yellow" : "white dim",
1309
+ paddingX: 1,
1310
+ children: disabled ? /* @__PURE__ */ jsxs3(Fragment, { children: [
1311
+ /* @__PURE__ */ jsx3(Text3, { color: "yellow", children: SPIN[frame] + " " }),
1312
+ /* @__PURE__ */ jsx3(Text3, { dimColor: true, italic: true, children: processingLabel ?? "processing\u2026" }),
1313
+ /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: " (esc to cancel)" })
1314
+ ] }) : /* @__PURE__ */ jsxs3(Fragment, { children: [
1315
+ /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "> " }),
1316
+ /* @__PURE__ */ jsx3(Text3, { children: input }),
1317
+ /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "\u258C" })
1318
+ ] })
1319
+ }
1320
+ );
1321
+ }
1322
+
1323
+ // src/ui/ModelsView.tsx
1324
+ import { Box as Box4, Text as Text4 } from "ink";
1325
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
1326
+ function ModelsView({ models, cursor, model, ollamaHost, effort }) {
1327
+ return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", marginLeft: 2, children: [
1328
+ /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", marginBottom: 1, children: [
1329
+ /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "config" }),
1330
+ /* @__PURE__ */ jsxs4(Box4, { marginTop: 1, flexDirection: "column", children: [
1331
+ /* @__PURE__ */ jsxs4(Text4, { children: [
1332
+ /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "model " }),
1333
+ /* @__PURE__ */ jsx4(Text4, { children: model ?? "\u2014" })
1334
+ ] }),
1335
+ /* @__PURE__ */ jsxs4(Text4, { children: [
1336
+ /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "host " }),
1337
+ /* @__PURE__ */ jsx4(Text4, { children: ollamaHost ?? "http://localhost:11434" })
1338
+ ] }),
1339
+ /* @__PURE__ */ jsxs4(Text4, { children: [
1340
+ /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "effort " }),
1341
+ /* @__PURE__ */ jsx4(Text4, { children: effort }),
1342
+ /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: " (\u2190 \u2192)" })
1343
+ ] })
1344
+ ] })
1345
+ ] }),
1346
+ /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "switch model" }),
1347
+ /* @__PURE__ */ jsx4(Box4, { marginTop: 1, children: /* @__PURE__ */ jsx4(ModelList, { models, cursor, activeModel: model, showActive: true }) }),
1348
+ /* @__PURE__ */ jsx4(Box4, { marginTop: 1, children: /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "\u2191\u2193 navigate enter switch \u2190\u2192 effort esc close" }) })
1349
+ ] });
1350
+ }
1351
+
1352
+ // src/ui/SessionsView.tsx
1353
+ import { Box as Box5, Text as Text5 } from "ink";
1354
+ import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
1355
+ function relativeTime(iso) {
1356
+ const diff = Date.now() - new Date(iso).getTime();
1357
+ const min = Math.floor(diff / 6e4);
1358
+ if (min < 1) return "just now";
1359
+ if (min < 60) return `${min}m ago`;
1360
+ const hr = Math.floor(min / 60);
1361
+ if (hr < 24) return `${hr}h ago`;
1362
+ const d = Math.floor(hr / 24);
1363
+ return `${d}d ago`;
1364
+ }
1365
+ function SessionsView({ sessions, cursor }) {
1366
+ return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", marginLeft: 2, children: [
1367
+ /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "resume session" }),
1368
+ /* @__PURE__ */ jsx5(Box5, { marginTop: 1, flexDirection: "column", borderStyle: "round", borderColor: "gray", paddingX: 1, children: sessions.length === 0 ? /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "no saved sessions yet" }) : sessions.map((s, i) => {
1369
+ const active = i === cursor;
1370
+ const label = s.title;
1371
+ return /* @__PURE__ */ jsxs5(Box5, { gap: 1, children: [
1372
+ /* @__PURE__ */ jsxs5(Text5, { color: active ? "blue" : void 0, dimColor: !active, children: [
1373
+ active ? "\u276F " : " ",
1374
+ label
1375
+ ] }),
1376
+ /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: `\xB7 ${s.messageCount} msgs \xB7 ${relativeTime(s.updatedAt)}` })
1377
+ ] }, s.id);
1378
+ }) }),
1379
+ /* @__PURE__ */ jsx5(Box5, { marginTop: 1, children: /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "\u2191\u2193 navigate enter resume d delete esc cancel" }) })
1380
+ ] });
1381
+ }
1382
+
1383
+ // src/ui/CommandPalette.tsx
1384
+ import { Box as Box6, Text as Text6 } from "ink";
1385
+ import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
1386
+ var COMMANDS = [
1387
+ { name: "/models", description: "switch model or adjust effort" },
1388
+ { name: "/new", description: "save current session and start fresh" },
1389
+ { name: "/sessions", description: "list sessions and resume one" },
1390
+ { name: "/clear", description: "clear chat and reset context" },
1391
+ { name: "/exit", description: "quit miii" }
1392
+ ];
1393
+ function CommandPalette({ filter, cursor }) {
1394
+ const filtered = COMMANDS.filter((c) => c.name.startsWith(filter));
1395
+ if (filtered.length === 0) return null;
1396
+ const nameWidth = Math.max(...filtered.map((c) => c.name.length));
1397
+ return /* @__PURE__ */ jsxs6(
1398
+ Box6,
1399
+ {
1400
+ flexDirection: "column",
1401
+ borderStyle: "round",
1402
+ borderColor: "gray",
1403
+ marginX: 1,
1404
+ marginBottom: 0,
1405
+ paddingX: 1,
1406
+ children: [
1407
+ filtered.map((cmd2, i) => {
1408
+ const active = i === cursor;
1409
+ return /* @__PURE__ */ jsxs6(Box6, { gap: 2, children: [
1410
+ /* @__PURE__ */ jsxs6(Text6, { bold: active, color: active ? "blue" : void 0, dimColor: !active, children: [
1411
+ active ? "\u276F " : " ",
1412
+ cmd2.name.padEnd(nameWidth)
1413
+ ] }),
1414
+ /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: cmd2.description })
1415
+ ] }, cmd2.name);
1416
+ }),
1417
+ /* @__PURE__ */ jsx6(Box6, { marginTop: 0, children: /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "\u2191\u2193 navigate tab/enter autocomplete esc dismiss" }) })
1418
+ ]
1419
+ }
1420
+ );
1421
+ }
1422
+ function filteredCommands(filter) {
1423
+ return COMMANDS.filter((c) => c.name.startsWith(filter));
1424
+ }
1425
+
1426
+ // src/session/store.ts
1427
+ init_client();
1428
+ import { writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, existsSync as existsSync2, readdirSync, readFileSync as readFileSync2, rmSync } from "fs";
1429
+ import { join as join2 } from "path";
1430
+ import { homedir as homedir2 } from "os";
1431
+ import { randomUUID } from "crypto";
1432
+ function encodeProjectDir(cwd) {
1433
+ return cwd.replace(/[/\\]/g, "-").replace(/^-+/, "");
1434
+ }
1435
+ var SESSION_DIR = join2(homedir2(), ".miii", "projects", encodeProjectDir(process.cwd()), "session");
1436
+ function newSessionId() {
1437
+ return randomUUID();
1438
+ }
1439
+ function sessionPath(id) {
1440
+ return join2(SESSION_DIR, `${id}.jsonl`);
1441
+ }
1442
+ function messageText(m) {
1443
+ if (typeof m.content === "string") return m.content;
1444
+ return m.content.map((b) => {
1445
+ if (b.type === "text") return b.text;
1446
+ if (b.type === "tool_use") return `[tool ${b.name}]`;
1447
+ if (b.type === "tool_result") return "[result]";
1448
+ return "";
1449
+ }).join(" ");
1450
+ }
1451
+ function firstUserText(messages) {
1452
+ const first = messages.find((m) => m.role === "user");
1453
+ if (!first) return "untitled";
1454
+ return messageText(first).trim().slice(0, 80) || "untitled";
1455
+ }
1456
+ function readMeta(id) {
1457
+ try {
1458
+ const raw = readFileSync2(sessionPath(id), "utf-8");
1459
+ const firstLine = raw.slice(0, raw.indexOf("\n") === -1 ? raw.length : raw.indexOf("\n"));
1460
+ const parsed = JSON.parse(firstLine);
1461
+ if (parsed.type !== "meta") return null;
1462
+ const { type: _t, ...meta } = parsed;
1463
+ return meta;
1464
+ } catch {
1465
+ return null;
1466
+ }
1467
+ }
1468
+ function persistSession(id, messages, title) {
1469
+ if (!messages.length) return;
1470
+ mkdirSync2(SESSION_DIR, { recursive: true });
1471
+ const existing = readMeta(id);
1472
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1473
+ const meta = {
1474
+ id,
1475
+ createdAt: existing?.createdAt ?? now,
1476
+ updatedAt: now,
1477
+ title: title ?? existing?.title ?? firstUserText(messages),
1478
+ messageCount: messages.length
1479
+ };
1480
+ const lines = [JSON.stringify({ type: "meta", ...meta })];
1481
+ for (const message of messages) {
1482
+ lines.push(JSON.stringify({ type: "message", message }));
1483
+ }
1484
+ writeFileSync2(sessionPath(id), lines.join("\n") + "\n", "utf-8");
1485
+ }
1486
+ function listSessions() {
1487
+ if (!existsSync2(SESSION_DIR)) return [];
1488
+ const metas = [];
1489
+ for (const file of readdirSync(SESSION_DIR)) {
1490
+ if (!file.endsWith(".jsonl")) continue;
1491
+ const meta = readMeta(file.replace(/\.jsonl$/, ""));
1492
+ if (meta) metas.push(meta);
1493
+ }
1494
+ return metas.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
1495
+ }
1496
+ function deleteSession(id) {
1497
+ try {
1498
+ rmSync(sessionPath(id), { force: true });
1499
+ } catch {
1500
+ }
1501
+ }
1502
+ function loadSession(id) {
1503
+ try {
1504
+ const raw = readFileSync2(sessionPath(id), "utf-8");
1505
+ const messages = [];
1506
+ for (const line of raw.split("\n")) {
1507
+ if (!line.trim()) continue;
1508
+ const parsed = JSON.parse(line);
1509
+ if (parsed.type === "message") messages.push(parsed.message);
1366
1510
  }
1511
+ return messages;
1512
+ } catch {
1513
+ return [];
1367
1514
  }
1368
- if (finalText) blocks.push({ type: "text", text: finalText });
1369
- for (const tc of finalCalls) {
1370
- blocks.push({
1371
- type: "tool_use",
1372
- id: mintToolUseId(),
1373
- name: tc.function.name,
1374
- input: tc.function.arguments ?? {}
1375
- });
1515
+ }
1516
+ function toDisplayMessages(history) {
1517
+ const out = [];
1518
+ for (const m of history) {
1519
+ if (m.role === "system") continue;
1520
+ const blocks = Array.isArray(m.content) ? m.content : [{ type: "text", text: m.content }];
1521
+ if (m.role === "user") {
1522
+ const text = blocks.filter((b) => b.type === "text").map((b) => b.text).join("");
1523
+ const results = blocks.filter((b) => b.type === "tool_result");
1524
+ if (results.length && out.length) {
1525
+ const last = out[out.length - 1];
1526
+ last.tool_results = [
1527
+ ...last.tool_results ?? [],
1528
+ ...results.map((r) => ({
1529
+ tool_use_id: r.tool_use_id,
1530
+ content: r.content,
1531
+ is_error: r.is_error
1532
+ }))
1533
+ ];
1534
+ }
1535
+ if (text.trim()) out.push({ role: "user", content: text });
1536
+ } else {
1537
+ const text = blocks.filter((b) => b.type === "text").map((b) => b.text).join("");
1538
+ const uses = blocks.filter((b) => b.type === "tool_use").map((b) => ({ id: b.id, name: b.name, input: b.input }));
1539
+ out.push({
1540
+ role: "assistant",
1541
+ content: text,
1542
+ tool_uses: uses.length ? uses : void 0
1543
+ });
1544
+ }
1376
1545
  }
1377
- return blocks;
1546
+ return out;
1378
1547
  }
1548
+ async function summarizeMessage(model, text) {
1549
+ const fallback = text.trim().slice(0, 80) || "untitled";
1550
+ const prompt = `Summarize this user request as a short title, 3-6 words, no punctuation. Reply with the title only.
1379
1551
 
1380
- // src/agent/loop.ts
1381
- var MAX_TURNS = 25;
1382
- var NUM_PREDICT = 4096;
1383
- var REPEAT_TAIL = 120;
1384
- var REPEAT_KILL = 4;
1385
- async function* runAgent(opts) {
1386
- const { model, cwd, permissions, hooks, signal, num_ctx } = opts;
1387
- const startTime = Date.now();
1388
- const system = buildSystemPrompt(TOOLS, cwd);
1389
- const ollamaTools = toOllamaTools(TOOLS);
1390
- const history = [
1391
- ...opts.history,
1392
- { role: "user", content: opts.userText }
1393
- ];
1394
- let promptTokens = 0;
1395
- let evalTokens = 0;
1396
- let lastAssistantSig = "";
1397
- let repeatCount = 0;
1398
- for (let turn = 0; turn < MAX_TURNS; turn++) {
1399
- let text = "";
1400
- let tool_calls;
1401
- let lastTail = "";
1402
- let tailRepeats = 0;
1403
- let streamLooped = false;
1404
- const ac = new AbortController();
1405
- const composedSignal = signal ? AbortSignal.any ? AbortSignal.any([signal, ac.signal]) : ac.signal : ac.signal;
1406
- if (signal) signal.addEventListener("abort", () => ac.abort(), { once: true });
1552
+ Request:
1553
+ ${text.slice(0, 2e3)}`;
1554
+ try {
1555
+ let out = "";
1556
+ for await (const chunk of chat(
1557
+ model,
1558
+ [{ role: "user", content: prompt }],
1559
+ void 0,
1560
+ { temperature: 0.2, num_predict: 32 }
1561
+ )) {
1562
+ if (chunk.content) out += chunk.content;
1563
+ }
1564
+ return out.trim().split("\n").filter(Boolean)[0]?.trim() || fallback;
1565
+ } catch {
1566
+ return fallback;
1567
+ }
1568
+ }
1569
+
1570
+ // src/ui/FilePicker.tsx
1571
+ import { Box as Box7, Text as Text7 } from "ink";
1572
+ import { readdirSync as readdirSync2 } from "fs";
1573
+ import { join as join3, relative } from "path";
1574
+ import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
1575
+ var IGNORE = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", ".next", "coverage", ".miii"]);
1576
+ var MAX_RESULTS = 10;
1577
+ var MAX_SCAN = 2e3;
1578
+ var cache = null;
1579
+ function listFiles(cwd) {
1580
+ if (cache && cache.cwd === cwd) return cache.files;
1581
+ const out = [];
1582
+ const stack = [cwd];
1583
+ while (stack.length && out.length < MAX_SCAN) {
1584
+ const dir = stack.pop();
1585
+ let entries;
1407
1586
  try {
1408
- for await (const chunk of chat(model, toOllamaMessages(history, system), ollamaTools, { signal: composedSignal, num_ctx, num_predict: NUM_PREDICT })) {
1409
- if (signal?.aborted) break;
1410
- if (chunk.content) {
1411
- text += chunk.content;
1412
- yield { type: "text-delta", text: chunk.content };
1413
- if (text.length >= REPEAT_TAIL) {
1414
- const tail = text.slice(-REPEAT_TAIL);
1415
- if (tail === lastTail) {
1416
- tailRepeats++;
1417
- if (tailRepeats >= REPEAT_KILL) {
1418
- streamLooped = true;
1419
- ac.abort();
1420
- break;
1421
- }
1422
- } else {
1423
- tailRepeats = 0;
1424
- lastTail = tail;
1425
- }
1426
- }
1427
- }
1428
- if (chunk.thinking) {
1429
- yield { type: "thinking-delta", text: chunk.thinking };
1430
- }
1431
- if (chunk.tool_calls && chunk.tool_calls.length > 0) {
1432
- tool_calls = chunk.tool_calls;
1433
- }
1434
- if (chunk.done) {
1435
- promptTokens += chunk.prompt_eval_count ?? 0;
1436
- evalTokens += chunk.eval_count ?? 0;
1437
- }
1438
- }
1439
- } catch (err) {
1440
- if (streamLooped) {
1441
- yield { type: "error", message: "Model stuck in repetition. Aborted stream. Try a different model or shorten context." };
1442
- return history;
1443
- }
1444
- yield { type: "error", message: err instanceof Error ? err.message : String(err) };
1445
- return history;
1587
+ entries = readdirSync2(dir, { withFileTypes: true });
1588
+ } catch {
1589
+ continue;
1446
1590
  }
1447
- if (streamLooped) {
1448
- yield { type: "error", message: "Model stuck in repetition. Aborted stream. Try a different model or shorten context." };
1449
- return history;
1591
+ for (const e of entries) {
1592
+ if (IGNORE.has(e.name) || e.name.startsWith(".")) continue;
1593
+ const full = join3(dir, e.name);
1594
+ if (e.isDirectory()) stack.push(full);
1595
+ else if (e.isFile()) out.push(relative(cwd, full));
1596
+ if (out.length >= MAX_SCAN) break;
1450
1597
  }
1451
- if (signal?.aborted) {
1452
- yield {
1453
- type: "aborted",
1454
- prompt_tokens: promptTokens,
1455
- eval_tokens: evalTokens,
1456
- duration_ms: Date.now() - startTime
1457
- };
1458
- return history;
1598
+ }
1599
+ cache = { cwd, files: out };
1600
+ return out;
1601
+ }
1602
+ function parseMention(input) {
1603
+ const m = input.match(/(?:^|\s)@([^\s]*)$/);
1604
+ if (!m) return null;
1605
+ return { query: m[1], start: input.length - m[1].length - 1 };
1606
+ }
1607
+ function searchFiles(cwd, query) {
1608
+ const files = listFiles(cwd);
1609
+ const q = query.toLowerCase();
1610
+ if (!q) return files.slice(0, MAX_RESULTS);
1611
+ const scored = [];
1612
+ for (const f of files) {
1613
+ const lf = f.toLowerCase();
1614
+ const idx = lf.indexOf(q);
1615
+ if (idx === -1) continue;
1616
+ const base = lf.split("/").pop() ?? lf;
1617
+ const baseIdx = base.indexOf(q);
1618
+ const score = baseIdx === 0 ? 0 : baseIdx > -1 ? 1 : 2 + idx;
1619
+ scored.push([score, f]);
1620
+ if (scored.length > 500) break;
1621
+ }
1622
+ scored.sort((a, b) => a[0] - b[0] || a[1].length - b[1].length);
1623
+ return scored.slice(0, MAX_RESULTS).map(([, f]) => f);
1624
+ }
1625
+ function FilePicker({ matches: matches2, cursor }) {
1626
+ if (matches2.length === 0) return null;
1627
+ return /* @__PURE__ */ jsxs7(
1628
+ Box7,
1629
+ {
1630
+ flexDirection: "column",
1631
+ borderStyle: "round",
1632
+ borderColor: "gray",
1633
+ marginX: 1,
1634
+ marginBottom: 0,
1635
+ paddingX: 1,
1636
+ children: [
1637
+ matches2.map((f, i) => {
1638
+ const active = i === cursor;
1639
+ return /* @__PURE__ */ jsx7(Box7, { children: /* @__PURE__ */ jsxs7(Text7, { bold: active, color: active ? "blue" : void 0, dimColor: !active, children: [
1640
+ active ? "\u276F " : " ",
1641
+ f
1642
+ ] }) }, f);
1643
+ }),
1644
+ /* @__PURE__ */ jsx7(Box7, { marginTop: 0, children: /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "\u2191\u2193 navigate tab insert esc dismiss" }) })
1645
+ ]
1459
1646
  }
1460
- const blocks = blocksFromOllama(text, tool_calls, TOOLS.map((t) => t.name));
1461
- const tool_uses = blocks.filter((b) => b.type === "tool_use");
1462
- history.push({ role: "assistant", content: blocks });
1463
- if (tool_uses.length === 0) {
1464
- yield { type: "turn-end", stop_reason: "end_turn" };
1647
+ );
1648
+ }
1649
+
1650
+ // src/ui/ChatView.tsx
1651
+ import { Box as Box9, Text as Text9 } from "ink";
1652
+
1653
+ // src/ui/ThinkingBlock.tsx
1654
+ import { useState as useState2, useEffect as useEffect2 } from "react";
1655
+ import { Box as Box8, Text as Text8 } from "ink";
1656
+ import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
1657
+ var FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
1658
+ var globalThinkingVisible = false;
1659
+ var listeners = /* @__PURE__ */ new Set();
1660
+ function toggleThinkingVisible() {
1661
+ globalThinkingVisible = !globalThinkingVisible;
1662
+ listeners.forEach((fn) => fn());
1663
+ }
1664
+ function useThinkingVisible() {
1665
+ const [visible, setVisible] = useState2(globalThinkingVisible);
1666
+ useEffect2(() => {
1667
+ const handler = () => setVisible(globalThinkingVisible);
1668
+ listeners.add(handler);
1669
+ return () => {
1670
+ listeners.delete(handler);
1671
+ };
1672
+ }, []);
1673
+ return visible;
1674
+ }
1675
+ function ThinkingBlock({ content }) {
1676
+ const [frame, setFrame] = useState2(0);
1677
+ const visible = useThinkingVisible();
1678
+ useEffect2(() => {
1679
+ const t = setInterval(() => setFrame((f) => (f + 1) % FRAMES.length), 80);
1680
+ return () => clearInterval(t);
1681
+ }, []);
1682
+ return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", marginLeft: 2, marginBottom: 1, children: [
1683
+ /* @__PURE__ */ jsxs8(Box8, { children: [
1684
+ /* @__PURE__ */ jsxs8(Text8, { color: "blue", children: [
1685
+ FRAMES[frame],
1686
+ " "
1687
+ ] }),
1688
+ /* @__PURE__ */ jsx8(Text8, { dimColor: true, italic: true, children: "thinking\u2026" }),
1689
+ /* @__PURE__ */ jsxs8(Text8, { dimColor: true, children: [
1690
+ " \xB7 ctrl+t to ",
1691
+ visible ? "hide" : "show",
1692
+ " thoughts"
1693
+ ] })
1694
+ ] }),
1695
+ visible && content ? /* @__PURE__ */ jsx8(Box8, { marginLeft: 2, children: /* @__PURE__ */ jsx8(Text8, { dimColor: true, italic: true, children: content }) }) : null
1696
+ ] });
1697
+ }
1698
+
1699
+ // src/ui/constants.ts
1700
+ var EMPTY_STATE_HINTS = [
1701
+ "\u2022 explain @file \u2014 reference a file with @",
1702
+ "\u2022 /models \u2014 switch model or effort",
1703
+ "\u2022 /new \u2014 start a new chat",
1704
+ "\u2022 /sessions \u2014 view saved chats",
1705
+ "\u2022 ctrl+t \u2014 toggle thinking"
1706
+ ];
1707
+ var EMPTY_STATE_TITLE = "Ask anything, or try:";
1708
+
1709
+ // src/ui/ChatView.tsx
1710
+ import { jsx as jsx9, jsxs as jsxs9 } from "react/jsx-runtime";
1711
+ function formatTokens(n) {
1712
+ if (n >= 1e3) return (n / 1e3).toFixed(n >= 1e4 ? 0 : 1) + "k";
1713
+ return String(n);
1714
+ }
1715
+ function formatDuration(ms) {
1716
+ const totalSec = ms / 1e3;
1717
+ if (totalSec < 60) return `${totalSec.toFixed(1)}s`;
1718
+ const m = Math.floor(totalSec / 60);
1719
+ const s = Math.round(totalSec - m * 60);
1720
+ return `${m}m ${s}s`;
1721
+ }
1722
+ function countLines(s) {
1723
+ if (!s) return 0;
1724
+ return s.split("\n").length;
1725
+ }
1726
+ function FileEditBlock({
1727
+ label,
1728
+ path,
1729
+ added,
1730
+ removed,
1731
+ previewLines
1732
+ }) {
1733
+ const MAX = 16;
1734
+ const shown = previewLines.slice(0, MAX);
1735
+ const extra = previewLines.length - shown.length;
1736
+ return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", marginLeft: 2, children: [
1737
+ /* @__PURE__ */ jsxs9(Box9, { children: [
1738
+ /* @__PURE__ */ jsx9(Text9, { color: "yellow", children: "\u25CF " }),
1739
+ /* @__PURE__ */ jsxs9(Text9, { color: "yellow", children: [
1740
+ label,
1741
+ " "
1742
+ ] }),
1743
+ /* @__PURE__ */ jsx9(Text9, { children: "(" }),
1744
+ /* @__PURE__ */ jsx9(Text9, { bold: true, children: path }),
1745
+ /* @__PURE__ */ jsx9(Text9, { children: ")" })
1746
+ ] }),
1747
+ /* @__PURE__ */ jsx9(Box9, { marginLeft: 2, children: /* @__PURE__ */ jsxs9(Text9, { dimColor: true, children: [
1748
+ "\u23BF ",
1749
+ removed > 0 ? `Added ${added} lines, removed ${removed} lines` : `Added ${added} lines`
1750
+ ] }) }),
1751
+ shown.map((ln, i) => /* @__PURE__ */ jsx9(Box9, { marginLeft: 4, children: /* @__PURE__ */ jsxs9(Text9, { color: ln.sign === "+" ? "green" : ln.sign === "-" ? "red" : void 0, dimColor: ln.sign === " ", children: [
1752
+ ln.sign,
1753
+ " ",
1754
+ ln.text
1755
+ ] }) }, i)),
1756
+ extra > 0 && /* @__PURE__ */ jsx9(Box9, { marginLeft: 4, children: /* @__PURE__ */ jsxs9(Text9, { dimColor: true, children: [
1757
+ "\u2026 ",
1758
+ extra,
1759
+ " more lines"
1760
+ ] }) })
1761
+ ] });
1762
+ }
1763
+ var TOOL_LABEL = {
1764
+ write_file: "Write",
1765
+ edit_file: "Update",
1766
+ read_file: "Read",
1767
+ run_bash: "Bash",
1768
+ glob: "Glob",
1769
+ grep: "Grep"
1770
+ };
1771
+ function truncate(s, max) {
1772
+ if (s.length <= max) return s;
1773
+ return s.slice(0, max - 1) + "\u2026";
1774
+ }
1775
+ function toolHeader(use) {
1776
+ const label = TOOL_LABEL[use.name] ?? use.name;
1777
+ const input = use.input ?? {};
1778
+ let arg = "";
1779
+ switch (use.name) {
1780
+ case "write_file":
1781
+ case "edit_file":
1782
+ case "read_file":
1783
+ arg = String(input.path ?? input.file_path ?? "");
1784
+ break;
1785
+ case "run_bash": {
1786
+ const cmd2 = String(input.command ?? "").replace(/\s+/g, " ");
1787
+ arg = truncate(cmd2, 120);
1788
+ break;
1789
+ }
1790
+ case "glob":
1791
+ case "grep":
1792
+ arg = truncate(String(input.pattern ?? ""), 120);
1465
1793
  break;
1794
+ default: {
1795
+ arg = truncate(JSON.stringify(input), 80);
1466
1796
  }
1467
- const sig = JSON.stringify(
1468
- blocks.map(
1469
- (b) => b.type === "tool_use" ? { t: "u", n: b.name, i: b.input } : b.type === "text" ? { t: "t", x: b.text.trim() } : b
1470
- )
1471
- );
1472
- if (sig === lastAssistantSig) {
1473
- repeatCount++;
1474
- if (repeatCount >= 2) {
1475
- yield { type: "error", message: "Agent loop detected: assistant produced identical output 3 turns in a row" };
1476
- return history;
1477
- }
1478
- } else {
1479
- repeatCount = 0;
1480
- lastAssistantSig = sig;
1797
+ }
1798
+ return { label, arg };
1799
+ }
1800
+ function summarizeResult(res, toolName) {
1801
+ const content = res.content ?? "";
1802
+ const lines = content.split("\n");
1803
+ if (!res.is_error) {
1804
+ if (toolName === "read_file") {
1805
+ const total = lines.length;
1806
+ return `Read ${total} line${total === 1 ? "" : "s"}`;
1481
1807
  }
1482
- for (const u of tool_uses) yield { type: "tool-use", block: u };
1483
- const results = [];
1484
- for (const use of tool_uses) {
1485
- const tool = getTool(use.name);
1486
- if (!tool) {
1487
- const r2 = {
1488
- type: "tool_result",
1489
- tool_use_id: use.id,
1490
- content: `Unknown tool: ${use.name}`,
1491
- is_error: true
1492
- };
1493
- results.push(r2);
1494
- yield { type: "tool-result", block: r2 };
1495
- continue;
1496
- }
1497
- const decision = await check(use.name, use.input, permissions);
1498
- if (decision === "deny") {
1499
- const r2 = {
1500
- type: "tool_result",
1501
- tool_use_id: use.id,
1502
- content: `Permission denied for ${use.name}.`,
1503
- is_error: true
1504
- };
1505
- results.push(r2);
1506
- yield { type: "permission-denied", toolName: use.name, tool_use_id: use.id };
1507
- yield { type: "tool-result", block: r2 };
1508
- continue;
1509
- }
1510
- await hooks?.firePre(use);
1511
- let r;
1512
- try {
1513
- const out = await tool.handler(use.input);
1514
- r = {
1515
- type: "tool_result",
1516
- tool_use_id: use.id,
1517
- content: out.content,
1518
- is_error: out.is_error
1519
- };
1520
- } catch (err) {
1521
- r = {
1522
- type: "tool_result",
1523
- tool_use_id: use.id,
1524
- content: err instanceof Error ? err.message : String(err),
1525
- is_error: true
1526
- };
1527
- }
1528
- await hooks?.firePost(use, r);
1529
- results.push(r);
1530
- yield { type: "tool-result", block: r };
1808
+ if (toolName === "grep") {
1809
+ if (content === "No matches.") return "No matches";
1810
+ const n = lines.filter(Boolean).length;
1811
+ return `${n} match${n === 1 ? "" : "es"}`;
1812
+ }
1813
+ if (toolName === "glob") {
1814
+ if (content === "No files matched.") return "No files";
1815
+ const n = lines.filter(Boolean).length;
1816
+ return `${n} file${n === 1 ? "" : "s"}`;
1531
1817
  }
1532
- history.push({ role: "user", content: results });
1533
- yield { type: "turn-end", stop_reason: "tool_use" };
1534
1818
  }
1535
- yield { type: "done", prompt_tokens: promptTokens, eval_tokens: evalTokens };
1536
- return history;
1819
+ const firstNonEmpty = lines.find((l) => l.trim().length > 0) ?? "";
1820
+ const extra = lines.length - 1;
1821
+ const head = firstNonEmpty.length > 100 ? firstNonEmpty.slice(0, 97) + "..." : firstNonEmpty;
1822
+ return extra > 0 ? `${head} (+${extra} lines)` : head;
1823
+ }
1824
+ function ToolResultBlock({ result, toolName }) {
1825
+ const content = result.content ?? "";
1826
+ const lines = content.split("\n");
1827
+ const showMulti = (toolName === "run_bash" || toolName === "grep" || toolName === "glob" || result.is_error) && lines.length > 1;
1828
+ if (!showMulti) {
1829
+ return /* @__PURE__ */ jsx9(Box9, { marginLeft: 2, children: /* @__PURE__ */ jsxs9(Text9, { color: result.is_error ? "red" : void 0, dimColor: !result.is_error, children: [
1830
+ "\u23BF ",
1831
+ summarizeResult(result, toolName)
1832
+ ] }) });
1833
+ }
1834
+ const MAX_LINES = 10;
1835
+ const MAX_LINE_WIDTH = 200;
1836
+ const shown = lines.slice(0, MAX_LINES).map((l) => truncate(l, MAX_LINE_WIDTH));
1837
+ const extra = lines.length - shown.length;
1838
+ return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", marginLeft: 2, children: [
1839
+ /* @__PURE__ */ jsxs9(Text9, { color: result.is_error ? "red" : void 0, dimColor: !result.is_error, children: [
1840
+ "\u23BF ",
1841
+ summarizeResult(result, toolName)
1842
+ ] }),
1843
+ shown.map((ln, i) => /* @__PURE__ */ jsx9(Box9, { marginLeft: 4, children: /* @__PURE__ */ jsx9(Text9, { color: result.is_error ? "red" : void 0, dimColor: true, children: ln || " " }) }, i)),
1844
+ extra > 0 && /* @__PURE__ */ jsx9(Box9, { marginLeft: 4, children: /* @__PURE__ */ jsxs9(Text9, { dimColor: true, children: [
1845
+ "\u2026 ",
1846
+ extra,
1847
+ " more lines"
1848
+ ] }) })
1849
+ ] });
1850
+ }
1851
+ function ToolUseLine({ use, result }) {
1852
+ if (use.name === "write_file" && !result?.is_error) {
1853
+ const input = use.input;
1854
+ const content = input.content ?? "";
1855
+ const added = countLines(content);
1856
+ const preview = content.split("\n").map((t) => ({ sign: "+", text: t }));
1857
+ return /* @__PURE__ */ jsx9(FileEditBlock, { label: "Write", path: input.path ?? "", added, removed: 0, previewLines: preview });
1858
+ }
1859
+ if (use.name === "edit_file" && !result?.is_error) {
1860
+ const input = use.input;
1861
+ const oldS = input.old_str ?? "";
1862
+ const newS = input.new_str ?? "";
1863
+ const added = countLines(newS);
1864
+ const removed = countLines(oldS);
1865
+ const preview = [
1866
+ ...oldS.split("\n").map((t) => ({ sign: "-", text: t })),
1867
+ ...newS.split("\n").map((t) => ({ sign: "+", text: t }))
1868
+ ];
1869
+ return /* @__PURE__ */ jsx9(FileEditBlock, { label: "Update", path: input.path ?? "", added, removed, previewLines: preview });
1870
+ }
1871
+ const { label, arg } = toolHeader(use);
1872
+ return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", marginLeft: 2, children: [
1873
+ /* @__PURE__ */ jsxs9(Box9, { children: [
1874
+ /* @__PURE__ */ jsx9(Text9, { color: "yellow", children: "\u25CF " }),
1875
+ /* @__PURE__ */ jsxs9(Text9, { color: "yellow", children: [
1876
+ label,
1877
+ " "
1878
+ ] }),
1879
+ /* @__PURE__ */ jsx9(Text9, { children: "(" }),
1880
+ /* @__PURE__ */ jsx9(Text9, { bold: true, children: arg }),
1881
+ /* @__PURE__ */ jsx9(Text9, { children: ")" })
1882
+ ] }),
1883
+ result && /* @__PURE__ */ jsx9(ToolResultBlock, { result, toolName: use.name })
1884
+ ] });
1885
+ }
1886
+ function AssistantMessage({ msg }) {
1887
+ return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", marginBottom: 1, children: [
1888
+ msg.content && /* @__PURE__ */ jsxs9(Box9, { flexDirection: "row", children: [
1889
+ /* @__PURE__ */ jsx9(Text9, { color: "white", children: "\u25CF " }),
1890
+ /* @__PURE__ */ jsx9(Box9, { flexGrow: 1, children: /* @__PURE__ */ jsx9(Text9, { children: msg.content }) })
1891
+ ] }),
1892
+ msg.tool_uses?.map((u) => {
1893
+ const r = msg.tool_results?.find((x) => x.tool_use_id === u.id);
1894
+ return /* @__PURE__ */ jsx9(ToolUseLine, { use: u, result: r }, u.id);
1895
+ }),
1896
+ msg.tokens && /* @__PURE__ */ jsx9(Box9, { marginLeft: 2, children: /* @__PURE__ */ jsxs9(Text9, { dimColor: true, children: [
1897
+ `\u21B3 Completed \xB7 ${formatTokens(msg.tokens.prompt_eval + msg.tokens.eval)} tokens`,
1898
+ msg.duration != null ? ` \xB7 ${formatDuration(msg.duration)}` : ""
1899
+ ] }) })
1900
+ ] });
1901
+ }
1902
+ function summarizeInput(input) {
1903
+ if (!input || typeof input !== "object") return "";
1904
+ const obj = input;
1905
+ const priority = ["path", "file_path", "command", "pattern", "query"];
1906
+ for (const k of priority) {
1907
+ const v = obj[k];
1908
+ if (typeof v === "string" && v.length > 0) return `${k}: ${v}`;
1909
+ }
1910
+ const first = Object.entries(obj).find(([, v]) => typeof v === "string");
1911
+ if (first) {
1912
+ const [k, v] = first;
1913
+ const trimmed = v.length > 80 ? v.slice(0, 80) + "\u2026" : v;
1914
+ return `${k}: ${trimmed}`;
1915
+ }
1916
+ return "";
1917
+ }
1918
+ function PermissionPrompt({ req, cursor }) {
1919
+ const label = TOOL_LABEL[req.toolName] ?? req.toolName;
1920
+ const options = [
1921
+ { label: "Yes", key: "yes" },
1922
+ { label: "Yes, don't ask again for this", key: "always" },
1923
+ { label: "No", key: "no" }
1924
+ ];
1925
+ const summary = summarizeInput(req.input);
1926
+ return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", marginBottom: 1, borderStyle: "round", borderColor: "blue", paddingX: 1, children: [
1927
+ /* @__PURE__ */ jsx9(Text9, { color: "blue", bold: true, children: "Tool use" }),
1928
+ /* @__PURE__ */ jsx9(Box9, { marginTop: 1, children: /* @__PURE__ */ jsxs9(Text9, { children: [
1929
+ "Allow ",
1930
+ /* @__PURE__ */ jsx9(Text9, { bold: true, children: label }),
1931
+ "?"
1932
+ ] }) }),
1933
+ summary && /* @__PURE__ */ jsx9(Box9, { marginLeft: 2, children: /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: summary }) }),
1934
+ /* @__PURE__ */ jsx9(Box9, { flexDirection: "column", marginTop: 1, children: options.map((opt, i) => /* @__PURE__ */ jsxs9(Text9, { color: i === cursor ? "blue" : void 0, children: [
1935
+ i === cursor ? "\u276F " : " ",
1936
+ i + 1,
1937
+ ". ",
1938
+ opt.label
1939
+ ] }, opt.key)) })
1940
+ ] });
1941
+ }
1942
+ function ChatView({
1943
+ messages,
1944
+ streaming,
1945
+ streamingContent,
1946
+ thinking,
1947
+ thinkingContent,
1948
+ error,
1949
+ pendingPermission,
1950
+ permissionCursor = 0,
1951
+ activeToolUses,
1952
+ activeToolResults
1953
+ }) {
1954
+ const empty = messages.length === 0 && !streaming && !thinking && !pendingPermission && !error;
1955
+ return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", marginLeft: 1, marginBottom: 1, children: [
1956
+ empty && /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", marginBottom: 1, children: [
1957
+ /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: EMPTY_STATE_TITLE }),
1958
+ EMPTY_STATE_HINTS.map((h, i) => /* @__PURE__ */ jsxs9(Text9, { dimColor: true, children: [
1959
+ " ",
1960
+ h
1961
+ ] }, i))
1962
+ ] }),
1963
+ messages.map(
1964
+ (msg, i) => msg.role === "user" ? /* @__PURE__ */ jsxs9(Box9, { flexDirection: "row", marginBottom: 1, children: [
1965
+ /* @__PURE__ */ jsx9(Text9, { color: "blue", children: "\u25CF " }),
1966
+ /* @__PURE__ */ jsx9(Box9, { flexGrow: 1, children: /* @__PURE__ */ jsx9(Text9, { children: msg.content }) })
1967
+ ] }, i) : /* @__PURE__ */ jsx9(AssistantMessage, { msg }, i)
1968
+ ),
1969
+ thinking && /* @__PURE__ */ jsx9(ThinkingBlock, { content: thinkingContent }),
1970
+ streaming && streamingContent && /* @__PURE__ */ jsxs9(Box9, { flexDirection: "row", marginBottom: 1, children: [
1971
+ /* @__PURE__ */ jsx9(Text9, { color: "white", children: "\u25CF " }),
1972
+ /* @__PURE__ */ jsx9(Box9, { flexGrow: 1, children: /* @__PURE__ */ jsx9(Text9, { children: streamingContent }) })
1973
+ ] }),
1974
+ activeToolUses?.map((u) => {
1975
+ const r = activeToolResults?.find((x) => x.tool_use_id === u.id);
1976
+ return /* @__PURE__ */ jsx9(ToolUseLine, { use: u, result: r }, u.id);
1977
+ }),
1978
+ pendingPermission && /* @__PURE__ */ jsx9(PermissionPrompt, { req: pendingPermission, cursor: permissionCursor }),
1979
+ error && /* @__PURE__ */ jsxs9(Box9, { flexDirection: "row", marginBottom: 1, children: [
1980
+ /* @__PURE__ */ jsx9(Text9, { color: "red", children: "\u25CF " }),
1981
+ /* @__PURE__ */ jsx9(Text9, { color: "red", children: error })
1982
+ ] })
1983
+ ] });
1537
1984
  }
1538
1985
 
1539
1986
  // src/ui/hooks/useAgentRunner.ts
1987
+ init_loop();
1988
+ import { useState as useState3, useRef } from "react";
1540
1989
  var FLUSH_MS = 100;
1541
1990
  function useAgentRunner(model, activeCtx) {
1542
1991
  const [messages, setMessages] = useState3([]);
@@ -1556,8 +2005,8 @@ function useAgentRunner(model, activeCtx) {
1556
2005
  const abortRef = useRef(null);
1557
2006
  const pendingPermissionRef = useRef(null);
1558
2007
  function askPermission(toolName, input) {
1559
- return new Promise((resolve) => {
1560
- const req = { toolName, input, resolve };
2008
+ return new Promise((resolve2) => {
2009
+ const req = { toolName, input, resolve: resolve2 };
1561
2010
  pendingPermissionRef.current = req;
1562
2011
  setPermissionCursor(0);
1563
2012
  setPendingPermission(req);
@@ -1566,7 +2015,7 @@ function useAgentRunner(model, activeCtx) {
1566
2015
  function resolvePermission(cursor) {
1567
2016
  const req = pendingPermissionRef.current;
1568
2017
  if (!req) return;
1569
- const answers = ["yes", "no"];
2018
+ const answers = ["yes", "always", "no"];
1570
2019
  pendingPermissionRef.current = null;
1571
2020
  setPendingPermission(null);
1572
2021
  req.resolve(answers[cursor]);
@@ -1902,7 +2351,7 @@ function useKeyboard(opts) {
1902
2351
  return;
1903
2352
  }
1904
2353
  if (key.downArrow) {
1905
- setPermissionCursor((i) => Math.min(1, i + 1));
2354
+ setPermissionCursor((i) => Math.min(2, i + 1));
1906
2355
  return;
1907
2356
  }
1908
2357
  if (key.return) {
@@ -1914,7 +2363,7 @@ function useKeyboard(opts) {
1914
2363
  if (state === "ready") {
1915
2364
  if (busyRef.current) return;
1916
2365
  const paletteOpen = input.startsWith("/");
1917
- const matches = paletteOpen ? filteredCommands(input) : [];
2366
+ const matches2 = paletteOpen ? filteredCommands(input) : [];
1918
2367
  const mention = !paletteOpen ? parseMention(input) : null;
1919
2368
  const fileMatches = mention ? searchFiles(process.cwd(), mention.query) : [];
1920
2369
  const fileOpen = mention !== null && fileMatches.length > 0;
@@ -1923,11 +2372,11 @@ function useKeyboard(opts) {
1923
2372
  return;
1924
2373
  }
1925
2374
  if (paletteOpen && key.downArrow) {
1926
- setPaletteCursor((i) => Math.min(matches.length - 1, i + 1));
2375
+ setPaletteCursor((i) => Math.min(matches2.length - 1, i + 1));
1927
2376
  return;
1928
2377
  }
1929
- if (paletteOpen && (key.tab || key.return) && matches[paletteCursor] && input !== matches[paletteCursor].name) {
1930
- setInput(() => matches[paletteCursor].name);
2378
+ if (paletteOpen && (key.tab || key.return) && matches2[paletteCursor] && input !== matches2[paletteCursor].name) {
2379
+ setInput(() => matches2[paletteCursor].name);
1931
2380
  setPaletteCursor(() => 0);
1932
2381
  return;
1933
2382
  }
@@ -2046,7 +2495,7 @@ async function checkForUpdate() {
2046
2495
  import { Fragment as Fragment2, jsx as jsx10, jsxs as jsxs10 } from "react/jsx-runtime";
2047
2496
  function App() {
2048
2497
  const { exit } = useApp();
2049
- const cwd = process.cwd().replace(homedir3(), "~").split(sep).join("/");
2498
+ const cwd = process.cwd().replace(homedir4(), "~").split(sep2).join("/");
2050
2499
  const [cfg, setCfg] = useState4(loadConfig());
2051
2500
  const [models, setModels] = useState4([]);
2052
2501
  const [contexts, setContexts] = useState4({});
@@ -2184,4 +2633,10 @@ function App() {
2184
2633
  }
2185
2634
 
2186
2635
  // src/cli.tsx
2187
- render(createElement(App));
2636
+ var [, , cmd, ...rest] = process.argv;
2637
+ if (cmd === "doctor" || cmd === "eval") {
2638
+ const { runEval: runEval2 } = await Promise.resolve().then(() => (init_run(), run_exports));
2639
+ process.exit(await runEval2(rest));
2640
+ } else {
2641
+ render(createElement(App));
2642
+ }