nimi-code 0.0.0-stage → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/main.js ADDED
@@ -0,0 +1,2081 @@
1
+ #!/usr/bin/env node
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __esm = (fn, res, err) => function __init() {
5
+ if (err) throw err[0];
6
+ try {
7
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
8
+ } catch (e) {
9
+ throw err = [e], e;
10
+ }
11
+ };
12
+ var __export = (target, all) => {
13
+ for (var name in all)
14
+ __defProp(target, name, { get: all[name], enumerable: true });
15
+ };
16
+
17
+ // src/providers/models.ts
18
+ function getModelInfo(id) {
19
+ for (const m of MODELS) {
20
+ if (m.id === id) return m;
21
+ }
22
+ return void 0;
23
+ }
24
+ var MODELS;
25
+ var init_models = __esm({
26
+ "src/providers/models.ts"() {
27
+ "use strict";
28
+ MODELS = [
29
+ { id: "claude-sonnet-5", provider: "anthropic", contextWindow: 2e5 },
30
+ { id: "qwen/qwen3.8-27b", provider: "groq", contextWindow: 128e3 },
31
+ { id: "muse-spark-1.3-contributor", provider: "meta", contextWindow: 128e3 },
32
+ { id: "deepseek-flash", provider: "deepseek", contextWindow: 128e3 },
33
+ { id: "gpt-4o-mini", provider: "openai", contextWindow: 128e3 }
34
+ ];
35
+ }
36
+ });
37
+
38
+ // src/agent/compaction.ts
39
+ function estimateTokens(messages) {
40
+ let chars = 0;
41
+ for (const m of messages) {
42
+ if (m.role === "user") chars += m.content.length;
43
+ else if (m.role === "assistant") {
44
+ for (const b of m.content) {
45
+ if (b.type === "text") chars += b.text.length;
46
+ else chars += b.name.length + JSON.stringify(b.arguments).length;
47
+ }
48
+ } else chars += m.toolName.length + m.content.length;
49
+ }
50
+ return Math.ceil(chars / 4);
51
+ }
52
+ function getContextLimit(model2, override) {
53
+ if (override && override > 0) return override;
54
+ return getModelInfo(model2)?.contextWindow;
55
+ }
56
+ function shouldCompact(messages, model2, override) {
57
+ const limit = getContextLimit(model2, override);
58
+ if (!limit) return false;
59
+ return estimateTokens(messages) > limit * COMPACT_FRACTION;
60
+ }
61
+ function findSafeSplit(messages, keepRecent) {
62
+ let index = Math.max(0, messages.length - keepRecent);
63
+ while (index > 0 && messages[index].role === "toolResult") {
64
+ index--;
65
+ }
66
+ return index;
67
+ }
68
+ function renderForSummary(m) {
69
+ if (m.role === "user") return "user: " + m.content;
70
+ if (m.role === "toolResult") return "tool " + m.toolName + ": " + m.content.slice(0, 200);
71
+ const parts = [];
72
+ for (const b of m.content) {
73
+ if (b.type === "text") parts.push(b.text);
74
+ else parts.push("[called " + b.name + " " + JSON.stringify(b.arguments).slice(0, 200) + "]");
75
+ }
76
+ return "assistant: " + parts.join(" ");
77
+ }
78
+ async function compactMessages(messages, provider2, model2) {
79
+ const unchanged = () => ({ messages, summary: "", compacted: false });
80
+ const split = findSafeSplit(messages, KEEP_RECENT);
81
+ if (split <= 0) return unchanged();
82
+ const history = messages.slice(0, split).map(renderForSummary).join("\n");
83
+ const prompt = "Summarize this conversation concisely: what was accomplished, which files were read or changed, and what the next turns need.\n\nHISTORY:\n" + history;
84
+ let summary = "";
85
+ for await (const e of provider2.stream({
86
+ messages: [{ role: "user", content: prompt }],
87
+ model: model2,
88
+ system: "You summarize conversations concisely.",
89
+ tools: []
90
+ })) {
91
+ if (e.type === "text_delta") summary += e.delta;
92
+ }
93
+ summary = summary.trim();
94
+ if (!summary) return unchanged();
95
+ return {
96
+ messages: [{ role: "user", content: "[Previous conversation summary]: " + summary }, ...messages.slice(split)],
97
+ summary,
98
+ compacted: true
99
+ };
100
+ }
101
+ var KEEP_RECENT, COMPACT_FRACTION;
102
+ var init_compaction = __esm({
103
+ "src/agent/compaction.ts"() {
104
+ "use strict";
105
+ init_models();
106
+ KEEP_RECENT = 6;
107
+ COMPACT_FRACTION = 0.75;
108
+ }
109
+ });
110
+
111
+ // src/agent.ts
112
+ async function* runAgent(options) {
113
+ const maxTurns = options.maxTurns ?? DEFAULT_MAX_TURNS;
114
+ yield { type: "agent_start" };
115
+ for (let round = 0; round < maxTurns; round++) {
116
+ options.signal?.throwIfAborted();
117
+ if (shouldCompact(options.messages, options.model, options.contextLimit)) {
118
+ const before = options.messages.length;
119
+ const result = await compactMessages(options.messages, options.provider, options.model);
120
+ if (result.compacted) {
121
+ options.messages.length = 0;
122
+ options.messages.push(...result.messages);
123
+ yield { type: "compacted", before, after: options.messages.length };
124
+ }
125
+ }
126
+ let reply = void 0;
127
+ for await (const event of options.provider.stream({
128
+ messages: options.messages,
129
+ model: options.model,
130
+ tools: options.tools,
131
+ system: options.system
132
+ })) {
133
+ options.signal?.throwIfAborted();
134
+ if (event.type === "text_delta") {
135
+ yield { type: "text_delta", delta: event.delta };
136
+ } else {
137
+ reply = event.message;
138
+ }
139
+ }
140
+ if (reply === void 0) {
141
+ throw new Error("Provider stream ended without a final message");
142
+ }
143
+ options.messages.push(reply);
144
+ const toolCalls = [];
145
+ for (const block of reply.content) {
146
+ if (block.type === "toolCall") {
147
+ toolCalls.push(block);
148
+ }
149
+ }
150
+ if (toolCalls.length === 0) {
151
+ yield { type: "turn_end" };
152
+ yield { type: "agent_end", message: reply };
153
+ return;
154
+ }
155
+ yield { type: "turn_end" };
156
+ for (const call of toolCalls) {
157
+ options.signal?.throwIfAborted();
158
+ yield { type: "tool_start", id: call.id, name: call.name, input: call.arguments };
159
+ let toolToRun = void 0;
160
+ for (const candidate of options.tools) {
161
+ if (candidate.name === call.name) {
162
+ toolToRun = candidate;
163
+ }
164
+ }
165
+ if (toolToRun === void 0) {
166
+ const content = "Unknown tool: " + call.name;
167
+ options.messages.push({
168
+ role: "toolResult",
169
+ toolCallId: call.id,
170
+ toolName: call.name,
171
+ content,
172
+ isError: true
173
+ });
174
+ yield { type: "tool_end", id: call.id, name: call.name, result: content, isError: true };
175
+ continue;
176
+ }
177
+ if (toolToRun.readOnly !== true && options.canUseTool !== void 0) {
178
+ const decision = await options.canUseTool(call.name, call.arguments);
179
+ if (decision.behavior === "deny") {
180
+ const content = decision.message;
181
+ options.messages.push({
182
+ role: "toolResult",
183
+ toolCallId: call.id,
184
+ toolName: call.name,
185
+ content,
186
+ isError: true
187
+ });
188
+ yield { type: "denied", id: call.id, name: call.name, reason: content };
189
+ yield { type: "tool_end", id: call.id, name: call.name, result: content, isError: true };
190
+ continue;
191
+ }
192
+ }
193
+ try {
194
+ const output = await toolToRun.execute(call.arguments);
195
+ options.messages.push({
196
+ role: "toolResult",
197
+ toolCallId: call.id,
198
+ toolName: call.name,
199
+ content: output,
200
+ isError: false
201
+ });
202
+ yield { type: "tool_end", id: call.id, name: call.name, result: output, isError: false };
203
+ } catch (error) {
204
+ let message;
205
+ if (error instanceof Error) {
206
+ message = error.message;
207
+ } else {
208
+ message = String(error);
209
+ }
210
+ options.messages.push({
211
+ role: "toolResult",
212
+ toolCallId: call.id,
213
+ toolName: call.name,
214
+ content: message,
215
+ isError: true
216
+ });
217
+ yield { type: "tool_end", id: call.id, name: call.name, result: message, isError: true };
218
+ }
219
+ }
220
+ }
221
+ throw new Error("Agent exceeded max turns (" + maxTurns + ")");
222
+ }
223
+ var DEFAULT_MAX_TURNS;
224
+ var init_agent = __esm({
225
+ "src/agent.ts"() {
226
+ "use strict";
227
+ init_compaction();
228
+ DEFAULT_MAX_TURNS = 20;
229
+ }
230
+ });
231
+
232
+ // src/tools/output.ts
233
+ function truncateOutput(text) {
234
+ if (text.length <= MAX_OUTPUT_CHARS) return text;
235
+ return text.slice(0, MAX_OUTPUT_CHARS) + "\n[truncated at 20000 chars]";
236
+ }
237
+ var MAX_OUTPUT_CHARS;
238
+ var init_output = __esm({
239
+ "src/tools/output.ts"() {
240
+ "use strict";
241
+ MAX_OUTPUT_CHARS = 2e4;
242
+ }
243
+ });
244
+
245
+ // src/tools/bash.ts
246
+ import { exec } from "child_process";
247
+ import { promisify } from "util";
248
+ var run, BASH_TIMEOUT_MS, bashTool;
249
+ var init_bash = __esm({
250
+ "src/tools/bash.ts"() {
251
+ "use strict";
252
+ init_output();
253
+ run = promisify(exec);
254
+ BASH_TIMEOUT_MS = 3e4;
255
+ bashTool = {
256
+ name: "bash",
257
+ description: "Run a shell command in the current folder and return its output",
258
+ parameters: {
259
+ type: "object",
260
+ properties: {
261
+ command: {
262
+ type: "string",
263
+ description: "The shell command to run"
264
+ }
265
+ },
266
+ required: ["command"]
267
+ },
268
+ async execute(args2) {
269
+ const { stdout, stderr } = await run(String(args2.command), {
270
+ cwd: process.cwd(),
271
+ timeout: BASH_TIMEOUT_MS
272
+ });
273
+ return truncateOutput(stdout + stderr);
274
+ }
275
+ };
276
+ }
277
+ });
278
+
279
+ // src/tools/edit.ts
280
+ import { readFile, writeFile } from "fs/promises";
281
+ import { resolve } from "path";
282
+ var editTool;
283
+ var init_edit = __esm({
284
+ "src/tools/edit.ts"() {
285
+ "use strict";
286
+ editTool = {
287
+ name: "edit",
288
+ description: "Edit a text file by replacing old text with new text",
289
+ parameters: {
290
+ type: "object",
291
+ properties: {
292
+ path: {
293
+ type: "string",
294
+ description: "Path to the file, relative to the current folder"
295
+ },
296
+ oldText: {
297
+ type: "string",
298
+ description: "The exact text to find in the file"
299
+ },
300
+ newText: {
301
+ type: "string",
302
+ description: "The text to replace it with"
303
+ }
304
+ },
305
+ required: ["path", "oldText", "newText"]
306
+ },
307
+ async execute(args2) {
308
+ const filePath = resolve(process.cwd(), String(args2.path));
309
+ const oldText = String(args2.oldText);
310
+ const newText = String(args2.newText);
311
+ const content = await readFile(filePath, "utf-8");
312
+ if (!content.includes(oldText)) {
313
+ throw new Error("oldText not found in " + String(args2.path));
314
+ }
315
+ const updated = content.replace(oldText, newText);
316
+ await writeFile(filePath, updated, "utf-8");
317
+ return "Edited " + String(args2.path);
318
+ }
319
+ };
320
+ }
321
+ });
322
+
323
+ // src/tools/find.ts
324
+ import { readdir } from "fs/promises";
325
+ import { join, relative, resolve as resolve2 } from "path";
326
+ function globToRegExp(glob) {
327
+ let out = "";
328
+ for (const ch of glob) {
329
+ if (ch === "*") out += ".*";
330
+ else if (ch === "?") out += ".";
331
+ else if ("+()^$.{}|[]\\".includes(ch)) out += "\\" + ch;
332
+ else out += ch;
333
+ }
334
+ return new RegExp("^" + out + "$");
335
+ }
336
+ var SKIP, findTool;
337
+ var init_find = __esm({
338
+ "src/tools/find.ts"() {
339
+ "use strict";
340
+ SKIP = ["node_modules", ".git", "dist", "build", ".next"];
341
+ findTool = {
342
+ name: "find",
343
+ description: "Find files by glob pattern, e.g. *.ts",
344
+ readOnly: true,
345
+ parameters: {
346
+ type: "object",
347
+ properties: {
348
+ pattern: { type: "string", description: "Glob, e.g. *.ts" },
349
+ path: { type: "string", description: "Dir to search (default: .)" }
350
+ },
351
+ required: ["pattern"]
352
+ },
353
+ async execute(args2) {
354
+ const root = resolve2(process.cwd(), String(args2.path ?? "."));
355
+ const match = globToRegExp(String(args2.pattern));
356
+ const found = [];
357
+ const stack = [root];
358
+ while (stack.length > 0 && found.length < 100) {
359
+ const dir = stack.pop();
360
+ for (const e of await readdir(dir, { withFileTypes: true })) {
361
+ if (e.isDirectory()) {
362
+ if (!SKIP.includes(e.name)) stack.push(join(dir, e.name));
363
+ } else if (match.test(e.name)) found.push(relative(process.cwd(), join(dir, e.name)));
364
+ }
365
+ }
366
+ return found.length > 0 ? found.join("\n") : "(no matches)";
367
+ }
368
+ };
369
+ }
370
+ });
371
+
372
+ // src/tools/git.ts
373
+ import { spawn } from "child_process";
374
+ function runGit(args2) {
375
+ return new Promise((resolve7, reject) => {
376
+ const child = spawn("git", args2, { cwd: process.cwd() });
377
+ let out = "";
378
+ let err = "";
379
+ const timer = setTimeout(() => {
380
+ child.kill();
381
+ reject(new Error("git timed out after 30s"));
382
+ }, 3e4);
383
+ child.stdout.on("data", (d) => {
384
+ out += String(d);
385
+ });
386
+ child.stderr.on("data", (d) => {
387
+ err += String(d);
388
+ });
389
+ child.on("error", (e) => {
390
+ clearTimeout(timer);
391
+ reject(e);
392
+ });
393
+ child.on("close", (code) => {
394
+ clearTimeout(timer);
395
+ const full = out + (err ? "\n" + err : "");
396
+ if (code !== 0) reject(new Error(full.trim() || "git exited " + code));
397
+ else resolve7(truncateOutput(full.trim() || "(no output)"));
398
+ });
399
+ });
400
+ }
401
+ var gitStatusTool, gitLogTool, gitDiffTool, gitAddTool, gitCommitTool, gitBranchTool, gitCheckoutTool, gitResetTool;
402
+ var init_git = __esm({
403
+ "src/tools/git.ts"() {
404
+ "use strict";
405
+ init_output();
406
+ gitStatusTool = {
407
+ name: "git_status",
408
+ description: "Show git working tree status",
409
+ readOnly: true,
410
+ parameters: {
411
+ type: "object",
412
+ properties: { porcelain: { type: "boolean", description: "Machine-readable short format" } },
413
+ required: []
414
+ },
415
+ async execute(args2) {
416
+ const a = ["status"];
417
+ if (args2.porcelain) a.push("--porcelain");
418
+ return runGit(a);
419
+ }
420
+ };
421
+ gitLogTool = {
422
+ name: "git_log",
423
+ description: "Show recent commit history",
424
+ readOnly: true,
425
+ parameters: {
426
+ type: "object",
427
+ properties: {
428
+ limit: { type: "number", description: "How many commits (default 10)" },
429
+ oneline: { type: "boolean", description: "One line per commit (default true)" }
430
+ },
431
+ required: []
432
+ },
433
+ async execute(args2) {
434
+ const limit = Number(args2.limit ?? 10);
435
+ const a = ["log", "-n", String(limit)];
436
+ if (args2.oneline !== false) a.push("--oneline");
437
+ return runGit(a);
438
+ }
439
+ };
440
+ gitDiffTool = {
441
+ name: "git_diff",
442
+ description: "Show unstaged/staged diff, optionally stat only or for a file",
443
+ readOnly: true,
444
+ parameters: {
445
+ type: "object",
446
+ properties: {
447
+ cached: { type: "boolean", description: "Show staged (--cached) instead of unstaged" },
448
+ stat: { type: "boolean", description: "Show --stat summary only" },
449
+ file: { type: "string", description: "Limit diff to one file" }
450
+ },
451
+ required: []
452
+ },
453
+ async execute(args2) {
454
+ const a = ["diff"];
455
+ if (args2.cached) a.push("--cached");
456
+ if (args2.stat) a.push("--stat");
457
+ if (args2.file) a.push("--", String(args2.file));
458
+ return runGit(a);
459
+ }
460
+ };
461
+ gitAddTool = {
462
+ name: "git_add",
463
+ description: "Stage files for commit",
464
+ parameters: {
465
+ type: "object",
466
+ properties: {
467
+ paths: { type: "array", description: 'Files to stage, e.g. ["src/a.ts"]' },
468
+ all: { type: "boolean", description: "Stage all changes including deletions" }
469
+ },
470
+ required: []
471
+ },
472
+ async execute(args2) {
473
+ const paths = args2.paths ?? [];
474
+ const a = ["add"];
475
+ if (args2.all) a.push("--all");
476
+ else if (paths.length > 0) a.push(...paths.map(String));
477
+ else throw new Error("No files specified. Pass paths or set all=true.");
478
+ return runGit(a);
479
+ }
480
+ };
481
+ gitCommitTool = {
482
+ name: "git_commit",
483
+ description: "Create a commit with staged changes",
484
+ parameters: {
485
+ type: "object",
486
+ properties: {
487
+ message: { type: "string", description: "Commit message (required)" },
488
+ amend: { type: "boolean", description: "Amend the previous commit" }
489
+ },
490
+ required: ["message"]
491
+ },
492
+ async execute(args2) {
493
+ const message = String(args2.message ?? "").trim();
494
+ if (!message) throw new Error("Commit message is required and cannot be empty.");
495
+ const a = ["commit", "-m", message];
496
+ if (args2.amend) a.push("--amend");
497
+ return runGit(a);
498
+ }
499
+ };
500
+ gitBranchTool = {
501
+ name: "git_branch",
502
+ description: "List, create, or delete branches",
503
+ parameters: {
504
+ type: "object",
505
+ properties: {
506
+ action: { type: "string", description: "list, create, or delete (default list)" },
507
+ name: { type: "string", description: "Branch name for create/delete" },
508
+ all: { type: "boolean", description: "List local and remote branches" }
509
+ },
510
+ required: []
511
+ },
512
+ async execute(args2) {
513
+ const action = String(args2.action ?? "list");
514
+ const a = ["branch"];
515
+ if (action === "list") {
516
+ if (args2.all) a.push("-a");
517
+ } else if (action === "create" || action === "delete") {
518
+ if (!args2.name) throw new Error("Branch name required for " + action + ".");
519
+ if (action === "delete") a.push("-d");
520
+ a.push(String(args2.name));
521
+ } else throw new Error("Unknown action: " + action + " (use list/create/delete).");
522
+ return runGit(a);
523
+ }
524
+ };
525
+ gitCheckoutTool = {
526
+ name: "git_checkout",
527
+ description: "Switch branches or restore a file from HEAD",
528
+ parameters: {
529
+ type: "object",
530
+ properties: {
531
+ branch: { type: "string", description: "Branch name to checkout" },
532
+ createNew: { type: "boolean", description: "Create and checkout a new branch (-b)" },
533
+ file: { type: "string", description: "File path to restore instead of switching" }
534
+ },
535
+ required: []
536
+ },
537
+ async execute(args2) {
538
+ const a = ["checkout"];
539
+ if (args2.file) a.push("--", String(args2.file));
540
+ else if (args2.branch) {
541
+ if (args2.createNew) a.push("-b");
542
+ a.push(String(args2.branch));
543
+ } else throw new Error("Specify either branch or file.");
544
+ return runGit(a);
545
+ }
546
+ };
547
+ gitResetTool = {
548
+ name: "git_reset",
549
+ description: "Unstage changes or reset to a commit",
550
+ parameters: {
551
+ type: "object",
552
+ properties: {
553
+ target: { type: "string", description: "Commit to reset to (default HEAD)" },
554
+ mode: { type: "string", description: "soft, mixed, or hard (default mixed)" },
555
+ file: { type: "string", description: "Specific file to unstage" }
556
+ },
557
+ required: []
558
+ },
559
+ async execute(args2) {
560
+ const mode = String(args2.mode ?? "mixed");
561
+ if (mode !== "soft" && mode !== "mixed" && mode !== "hard") throw new Error("Unknown mode: " + mode + ".");
562
+ const a = ["reset"];
563
+ if (mode !== "mixed") a.push("--" + mode);
564
+ a.push(String(args2.target ?? "HEAD"));
565
+ if (args2.file) a.push(String(args2.file));
566
+ return runGit(a);
567
+ }
568
+ };
569
+ }
570
+ });
571
+
572
+ // src/tools/grep.ts
573
+ import { readFile as readFile2, readdir as readdir2, stat } from "fs/promises";
574
+ import { join as join2, relative as relative2, resolve as resolve3 } from "path";
575
+ function globToRegExp2(glob) {
576
+ let out = "";
577
+ for (const ch of glob) {
578
+ if (ch === "*") out += ".*";
579
+ else if (ch === "?") out += ".";
580
+ else if ("+()^$.{}|[]\\".includes(ch)) out += "\\" + ch;
581
+ else out += ch;
582
+ }
583
+ return new RegExp("^" + out + "$");
584
+ }
585
+ var SKIP2, grepTool;
586
+ var init_grep = __esm({
587
+ "src/tools/grep.ts"() {
588
+ "use strict";
589
+ SKIP2 = ["node_modules", ".git", "dist", "build", ".next"];
590
+ grepTool = {
591
+ name: "grep",
592
+ description: "Search file contents with a regex, returns file:line matches",
593
+ readOnly: true,
594
+ parameters: {
595
+ type: "object",
596
+ properties: {
597
+ pattern: { type: "string", description: "Regex, e.g. TODO|FIXME" },
598
+ path: { type: "string", description: "File or dir to search (default: .)" },
599
+ include: { type: "string", description: "Only files matching glob, e.g. *.ts" }
600
+ },
601
+ required: ["pattern"]
602
+ },
603
+ async execute(args2) {
604
+ let regex;
605
+ try {
606
+ regex = new RegExp(String(args2.pattern));
607
+ } catch {
608
+ throw new Error("Invalid regex: " + String(args2.pattern));
609
+ }
610
+ const include = args2.include ? globToRegExp2(String(args2.include)) : null;
611
+ const start = resolve3(process.cwd(), String(args2.path ?? "."));
612
+ const stack = [start];
613
+ const targets = [];
614
+ while (stack.length > 0) {
615
+ const cur = stack.pop();
616
+ const s = await stat(cur);
617
+ if (s.isDirectory()) {
618
+ for (const e of await readdir2(cur, { withFileTypes: true })) {
619
+ if (e.isDirectory()) {
620
+ if (!SKIP2.includes(e.name)) stack.push(join2(cur, e.name));
621
+ } else if (!include || include.test(e.name)) targets.push(join2(cur, e.name));
622
+ }
623
+ } else targets.push(cur);
624
+ }
625
+ const hits = [];
626
+ for (const file of targets) {
627
+ if (hits.length >= 100) break;
628
+ if ((await stat(file)).size > 5e5) continue;
629
+ let content;
630
+ try {
631
+ content = await readFile2(file, "utf-8");
632
+ } catch {
633
+ continue;
634
+ }
635
+ const lines = content.split("\n");
636
+ for (let i = 0; i < lines.length; i++) {
637
+ if (regex.test(lines[i])) {
638
+ hits.push(relative2(process.cwd(), file) + ":" + (i + 1) + ": " + lines[i].trim().slice(0, 120));
639
+ if (hits.length >= 100) break;
640
+ }
641
+ }
642
+ }
643
+ return hits.length > 0 ? hits.join("\n") + (hits.length >= 100 ? "\n[truncated]" : "") : "(no matches)";
644
+ }
645
+ };
646
+ }
647
+ });
648
+
649
+ // src/tools/ls.ts
650
+ import { readdir as readdir3, stat as stat2 } from "fs/promises";
651
+ import { resolve as resolve4 } from "path";
652
+ var lsTool;
653
+ var init_ls = __esm({
654
+ "src/tools/ls.ts"() {
655
+ "use strict";
656
+ lsTool = {
657
+ name: "ls",
658
+ description: "List files in a directory with types and sizes",
659
+ readOnly: true,
660
+ parameters: {
661
+ type: "object",
662
+ properties: { path: { type: "string", description: "Dir to list (default: .)" } },
663
+ required: []
664
+ },
665
+ async execute(args2) {
666
+ const dir = resolve4(process.cwd(), String(args2.path ?? "."));
667
+ const entries = await readdir3(dir, { withFileTypes: true });
668
+ const lines = [];
669
+ for (const e of entries) {
670
+ if (e.isDirectory()) lines.push("dir - " + e.name + "/");
671
+ else lines.push("file " + String((await stat2(resolve4(dir, e.name))).size) + " " + e.name);
672
+ }
673
+ return lines.length > 0 ? lines.join("\n") : "(empty directory)";
674
+ }
675
+ };
676
+ }
677
+ });
678
+
679
+ // src/tools/read.ts
680
+ import { readFile as readFile3 } from "fs/promises";
681
+ import { resolve as resolve5 } from "path";
682
+ var readTool;
683
+ var init_read = __esm({
684
+ "src/tools/read.ts"() {
685
+ "use strict";
686
+ init_output();
687
+ readTool = {
688
+ name: "read",
689
+ description: "Read a text file and return its contents",
690
+ readOnly: true,
691
+ parameters: {
692
+ type: "object",
693
+ properties: {
694
+ path: {
695
+ type: "string",
696
+ description: "Path to the file, relative to the current folder"
697
+ }
698
+ },
699
+ required: ["path"]
700
+ },
701
+ async execute(args2) {
702
+ const text = await readFile3(
703
+ resolve5(process.cwd(), String(args2.path)),
704
+ "utf-8"
705
+ );
706
+ return truncateOutput(text);
707
+ }
708
+ };
709
+ }
710
+ });
711
+
712
+ // src/tools/write.ts
713
+ import { mkdir, writeFile as writeFile2 } from "fs/promises";
714
+ import { dirname, resolve as resolve6 } from "path";
715
+ var writeTool;
716
+ var init_write = __esm({
717
+ "src/tools/write.ts"() {
718
+ "use strict";
719
+ writeTool = {
720
+ name: "write",
721
+ description: "Write text to a file, creating it or overwriting it",
722
+ parameters: {
723
+ type: "object",
724
+ properties: {
725
+ path: {
726
+ type: "string",
727
+ description: "Path to the file, relative to the current folder"
728
+ },
729
+ content: {
730
+ type: "string",
731
+ description: "The full text to write to the file"
732
+ }
733
+ },
734
+ required: ["path", "content"]
735
+ },
736
+ async execute(args2) {
737
+ const filePath = resolve6(process.cwd(), String(args2.path));
738
+ const content = String(args2.content);
739
+ await mkdir(dirname(filePath), { recursive: true });
740
+ await writeFile2(filePath, content, "utf-8");
741
+ return "Wrote " + content.length + " chars to " + String(args2.path);
742
+ }
743
+ };
744
+ }
745
+ });
746
+
747
+ // src/types.ts
748
+ var init_types = __esm({
749
+ "src/types.ts"() {
750
+ "use strict";
751
+ }
752
+ });
753
+
754
+ // src/tools/index.ts
755
+ var ALL_TOOLS;
756
+ var init_tools = __esm({
757
+ "src/tools/index.ts"() {
758
+ "use strict";
759
+ init_bash();
760
+ init_edit();
761
+ init_find();
762
+ init_git();
763
+ init_grep();
764
+ init_ls();
765
+ init_read();
766
+ init_write();
767
+ init_types();
768
+ ALL_TOOLS = [readTool, writeTool, editTool, bashTool, lsTool, findTool, grepTool, gitStatusTool, gitLogTool, gitDiffTool, gitAddTool, gitCommitTool, gitBranchTool, gitCheckoutTool, gitResetTool];
769
+ }
770
+ });
771
+
772
+ // src/permissions.ts
773
+ function allowAll() {
774
+ return async () => ({ behavior: "allow" });
775
+ }
776
+ function createPromptPermission(rl) {
777
+ const always = /* @__PURE__ */ new Set();
778
+ return async (name, input) => {
779
+ if (always.has(name)) return { behavior: "allow" };
780
+ const what = input.command ?? input.path ?? input.file ?? JSON.stringify(input);
781
+ console.log(`
782
+ Permission needed: ${name} ${String(what).slice(0, 120)}`);
783
+ console.log("[y]es / [a]lways / [n]o (default)");
784
+ const answer = await new Promise((resolve7) => rl.question("> ", resolve7));
785
+ const choice = answer.trim().toLowerCase();
786
+ if (choice === "a" || choice === "always") {
787
+ always.add(name);
788
+ return { behavior: "allow" };
789
+ }
790
+ if (choice === "y" || choice === "yes") return { behavior: "allow" };
791
+ return { behavior: "deny", message: `User denied ${name}. Do not retry; ask what to do instead.` };
792
+ };
793
+ }
794
+ var init_permissions = __esm({
795
+ "src/permissions.ts"() {
796
+ "use strict";
797
+ }
798
+ });
799
+
800
+ // src/providers/anthropic.ts
801
+ import Anthropic from "@anthropic-ai/sdk";
802
+ function toAnthropic(message) {
803
+ return message.map((m) => {
804
+ if (m.role === "user") return { role: "user", content: m.content };
805
+ if (m.role === "assistant") {
806
+ return {
807
+ role: "assistant",
808
+ content: m.content.map(
809
+ (b) => b.type === "text" ? { type: "text", text: b.text } : { type: "tool_use", id: b.id, name: b.name, input: b.arguments }
810
+ )
811
+ };
812
+ }
813
+ ;
814
+ return {
815
+ role: "user",
816
+ content: [{ type: "tool_result", tool_use_id: m.toolCallId, content: m.content, is_error: m.isError }]
817
+ };
818
+ });
819
+ }
820
+ function createAnthropic() {
821
+ const client = new Anthropic();
822
+ return {
823
+ name: "anthropic",
824
+ defaultModel: "claude-sonnet-5",
825
+ async *stream({ messages, model: model2, system: system2, tools = [] }) {
826
+ const stream = client.messages.stream({
827
+ model: model2,
828
+ max_tokens: 4096,
829
+ system: system2,
830
+ messages: toAnthropic(messages),
831
+ tools: tools.map((t) => ({
832
+ name: t.name,
833
+ description: t.description,
834
+ input_schema: t.parameters
835
+ }))
836
+ });
837
+ let text = "";
838
+ const content = [];
839
+ const jsonParts = [];
840
+ for await (const event of stream) {
841
+ const idx = event.index ?? content.length - 1;
842
+ if (event.type === "content_block_start") {
843
+ const b = event.content_block;
844
+ if (b.type === "text") {
845
+ content.push({ type: "text", text: "" });
846
+ jsonParts.push("");
847
+ } else if (b.type === "tool_use") {
848
+ content.push({ type: "toolCall", id: b.id, name: b.name, arguments: {} });
849
+ jsonParts.push("");
850
+ }
851
+ } else if (event.type === "content_block_delta") {
852
+ const block = content[idx] ?? content.at(-1);
853
+ if (event.delta.type === "text_delta" && block?.type === "text") {
854
+ block.text += event.delta.text;
855
+ yield { type: "text_delta", delta: event.delta.text };
856
+ } else if (event.delta.type === "input_json_delta") {
857
+ jsonParts[idx] += event.delta.partial_json;
858
+ }
859
+ } else if (event.type === "content_block_stop") {
860
+ const at = event.index ?? content.length - 1;
861
+ const block = content[at];
862
+ if (block?.type === "toolCall") {
863
+ try {
864
+ block.arguments = jsonParts[at] ? JSON.parse(jsonParts[at]) : {};
865
+ } catch {
866
+ block.arguments = {};
867
+ }
868
+ }
869
+ }
870
+ }
871
+ const final = await stream.finalMessage();
872
+ const stopReason = final.stop_reason === "tool_use" ? "toolUse" : final.stop_reason === "max_tokens" ? "length" : "stop";
873
+ yield {
874
+ type: "done",
875
+ message: {
876
+ role: "assistant",
877
+ content,
878
+ usage: { input: final.usage.input_tokens, output: final.usage.output_tokens },
879
+ stopReason
880
+ }
881
+ };
882
+ }
883
+ };
884
+ }
885
+ var init_anthropic = __esm({
886
+ "src/providers/anthropic.ts"() {
887
+ "use strict";
888
+ }
889
+ });
890
+
891
+ // src/providers/openai-compat.ts
892
+ import OpenAI from "openai";
893
+ function toOpenAI(messages) {
894
+ return messages.map((message) => {
895
+ if (message.role === "user") {
896
+ return { role: "user", content: message.content };
897
+ }
898
+ if (message.role === "assistant") {
899
+ const text = message.content.filter((block) => block.type === "text").map((block) => block.text).join("");
900
+ const toolCalls = message.content.filter(
901
+ (block) => block.type === "toolCall"
902
+ );
903
+ if (toolCalls.length > 0) {
904
+ return {
905
+ role: "assistant",
906
+ content: text || null,
907
+ tool_calls: toolCalls.map((tool) => ({
908
+ id: tool.id,
909
+ type: "function",
910
+ function: {
911
+ name: tool.name,
912
+ arguments: JSON.stringify(tool.arguments)
913
+ }
914
+ }))
915
+ };
916
+ }
917
+ return { role: "assistant", content: text };
918
+ }
919
+ return {
920
+ role: "tool",
921
+ tool_call_id: message.toolCallId,
922
+ content: message.content
923
+ };
924
+ });
925
+ }
926
+ function createOpenAICompat(name, baseURL, apiKey, defaultModel) {
927
+ const client = new OpenAI({ baseURL, apiKey });
928
+ return {
929
+ name,
930
+ defaultModel,
931
+ async *stream({ messages, model: model2, system: system2, tools = [] }) {
932
+ const response = await client.chat.completions.create({
933
+ model: model2,
934
+ stream: true,
935
+ stream_options: { include_usage: true },
936
+ messages: [
937
+ ...system2 ? [{ role: "system", content: system2 }] : [],
938
+ ...toOpenAI(messages)
939
+ ],
940
+ tools: tools.map((tool) => ({
941
+ type: "function",
942
+ function: {
943
+ name: tool.name,
944
+ description: tool.description,
945
+ parameters: tool.parameters
946
+ }
947
+ })),
948
+ tool_choice: tools.length ? "auto" : void 0
949
+ });
950
+ let fullText = "";
951
+ const toolCalls = [];
952
+ let usage = { input: 0, output: 0 };
953
+ let stopReason = "stop";
954
+ for await (const chunk of response) {
955
+ const choice = chunk.choices[0];
956
+ const delta = choice?.delta;
957
+ if (delta?.content) {
958
+ fullText += delta.content;
959
+ yield { type: "text_delta", delta: delta.content };
960
+ }
961
+ for (const tool of delta?.tool_calls ?? []) {
962
+ const index = tool.index;
963
+ if (!toolCalls[index]) {
964
+ toolCalls[index] = { id: "", name: "", arguments: "" };
965
+ }
966
+ if (tool.id) toolCalls[index].id = tool.id;
967
+ if (tool.function?.name) {
968
+ toolCalls[index].name += tool.function.name;
969
+ }
970
+ if (tool.function?.arguments) {
971
+ toolCalls[index].arguments += tool.function.arguments;
972
+ }
973
+ }
974
+ if (choice?.finish_reason === "tool_calls") {
975
+ stopReason = "toolUse";
976
+ } else if (choice?.finish_reason === "length") {
977
+ stopReason = "length";
978
+ }
979
+ if (chunk.usage) {
980
+ usage = {
981
+ input: chunk.usage.prompt_tokens,
982
+ output: chunk.usage.completion_tokens
983
+ };
984
+ }
985
+ }
986
+ const content = [];
987
+ if (fullText) {
988
+ content.push({ type: "text", text: fullText });
989
+ }
990
+ for (const tool of toolCalls) {
991
+ if (!tool) continue;
992
+ let parsed = {};
993
+ if (tool.arguments) {
994
+ try {
995
+ parsed = JSON.parse(tool.arguments);
996
+ } catch {
997
+ parsed = {};
998
+ }
999
+ }
1000
+ content.push({
1001
+ type: "toolCall",
1002
+ id: tool.id,
1003
+ name: tool.name,
1004
+ arguments: parsed
1005
+ });
1006
+ }
1007
+ yield {
1008
+ type: "done",
1009
+ message: { role: "assistant", content, usage, stopReason }
1010
+ };
1011
+ }
1012
+ };
1013
+ }
1014
+ var init_openai_compat = __esm({
1015
+ "src/providers/openai-compat.ts"() {
1016
+ "use strict";
1017
+ }
1018
+ });
1019
+
1020
+ // src/providers/index.ts
1021
+ function requireKey(envVar, provider2) {
1022
+ const key = process.env[envVar];
1023
+ if (!key) throw new Error(`Missing ${envVar} for ${provider2}. Add it to .env or environment.`);
1024
+ return key;
1025
+ }
1026
+ function getProvider(name) {
1027
+ const create = providers[name];
1028
+ if (!create) {
1029
+ throw new Error(`Unknown provider: ${name}. Available: ${Object.keys(providers).join(", ")}`);
1030
+ }
1031
+ return create();
1032
+ }
1033
+ var providers;
1034
+ var init_providers = __esm({
1035
+ "src/providers/index.ts"() {
1036
+ "use strict";
1037
+ init_anthropic();
1038
+ init_openai_compat();
1039
+ providers = {
1040
+ anthropic: createAnthropic,
1041
+ "anthropic-openai": () => createOpenAICompat("anthropic-openai", "https://api.anthropic.com/v1/", requireKey("ANTHROPIC_API_KEY", "anthropic-openai"), "claude-sonnet-5"),
1042
+ "groq": () => createOpenAICompat("groq", "https://api.groq.com/openai/v1", requireKey("GROQ_API_KEY", "groq"), "qwen/qwen3.8-27b"),
1043
+ "meta": () => createOpenAICompat("meta", "https://api.meta.ai/v1", requireKey("META_API_KEY", "meta"), "muse-spark-1.3-contributor"),
1044
+ "deepseek": () => createOpenAICompat("deepseek", "https://api.deepseek.com", requireKey("DEEPSEEK_API_KEY", "deepseek"), "deepseek-flash"),
1045
+ "openai": () => createOpenAICompat("openai", "https://api.openai.com/v1", requireKey("OPENAI_API_KEY", "openai"), "gpt-4o-mini")
1046
+ };
1047
+ }
1048
+ });
1049
+
1050
+ // src/session/store.ts
1051
+ import { existsSync as existsSync3 } from "fs";
1052
+ import { appendFile, mkdir as mkdir2, readdir as readdir4, readFile as readFile6 } from "fs/promises";
1053
+ import { join as join5 } from "path";
1054
+ function getSessionDir() {
1055
+ return join5(process.cwd(), ".nimi", "sessions");
1056
+ }
1057
+ function getSessionPath(id) {
1058
+ return join5(getSessionDir(), id + ".jsonl");
1059
+ }
1060
+ function newSessionId() {
1061
+ return Date.now().toString(36) + Math.random().toString(36).slice(2, 8);
1062
+ }
1063
+ async function appendEntry(id, entry) {
1064
+ await mkdir2(getSessionDir(), { recursive: true });
1065
+ await appendFile(getSessionPath(id), JSON.stringify(entry) + "\n", "utf-8");
1066
+ }
1067
+ async function appendMessages(id, messages) {
1068
+ for (const message of messages) {
1069
+ await appendEntry(id, { type: "message", message });
1070
+ }
1071
+ }
1072
+ async function loadSession(id) {
1073
+ if (!existsSync3(getSessionPath(id))) return null;
1074
+ const raw = await readFile6(getSessionPath(id), "utf-8");
1075
+ let info = null;
1076
+ const messages = [];
1077
+ for (const line of raw.split("\n")) {
1078
+ if (!line.trim()) continue;
1079
+ let entry;
1080
+ try {
1081
+ entry = JSON.parse(line);
1082
+ } catch {
1083
+ continue;
1084
+ }
1085
+ if (entry.type === "session_info") info = entry;
1086
+ else if (entry.type === "message") messages.push(entry.message);
1087
+ }
1088
+ if (!info) return null;
1089
+ return { info, messages };
1090
+ }
1091
+ async function listSessions() {
1092
+ await mkdir2(getSessionDir(), { recursive: true });
1093
+ const files = await readdir4(getSessionDir());
1094
+ const ids = [];
1095
+ for (const f of files) {
1096
+ if (f.endsWith(".jsonl")) ids.push(f.slice(0, -6));
1097
+ }
1098
+ ids.sort();
1099
+ return ids;
1100
+ }
1101
+ var init_store = __esm({
1102
+ "src/session/store.ts"() {
1103
+ "use strict";
1104
+ }
1105
+ });
1106
+
1107
+ // src/trace/recorder.ts
1108
+ import { appendFileSync, mkdirSync, writeFileSync } from "fs";
1109
+ import { dirname as dirname2 } from "path";
1110
+ function isSnapshotEvent(type) {
1111
+ return type === "turn_end" || type === "agent_end" || type === "compacted";
1112
+ }
1113
+ function createTraceRecorder(path, info) {
1114
+ mkdirSync(dirname2(path), { recursive: true });
1115
+ writeFileSync(
1116
+ path,
1117
+ JSON.stringify({
1118
+ v: V,
1119
+ type: "header",
1120
+ session: info.session,
1121
+ provider: info.provider,
1122
+ model: info.model,
1123
+ prompt: info.prompt,
1124
+ started_at: (/* @__PURE__ */ new Date()).toISOString(),
1125
+ argv: info.argv,
1126
+ tools: info.tools ?? []
1127
+ }) + "\n"
1128
+ );
1129
+ let i = 0;
1130
+ let turns = 0;
1131
+ let tools = 0;
1132
+ let done = false;
1133
+ const writeLine = (obj) => {
1134
+ appendFileSync(path, JSON.stringify(obj) + "\n");
1135
+ };
1136
+ return {
1137
+ path,
1138
+ record(event, messages) {
1139
+ const index = i++;
1140
+ if (event.type === "turn_end") turns++;
1141
+ if (event.type === "tool_start") tools++;
1142
+ writeLine({ v: V, type: "event", i: index, event });
1143
+ if (isSnapshotEvent(event.type)) {
1144
+ writeLine({ v: V, type: "messages", afterEvent: index, messages });
1145
+ }
1146
+ },
1147
+ finish(messages) {
1148
+ if (done) return;
1149
+ done = true;
1150
+ writeLine({
1151
+ v: V,
1152
+ type: "end",
1153
+ messageCount: messages.length,
1154
+ counts: { turns, tools }
1155
+ });
1156
+ }
1157
+ };
1158
+ }
1159
+ async function* withTrace(events, trace, messages) {
1160
+ try {
1161
+ for await (const event of events) {
1162
+ trace.record(event, messages);
1163
+ yield event;
1164
+ }
1165
+ } finally {
1166
+ trace.finish(messages);
1167
+ }
1168
+ }
1169
+ var V;
1170
+ var init_recorder = __esm({
1171
+ "src/trace/recorder.ts"() {
1172
+ "use strict";
1173
+ V = 1;
1174
+ }
1175
+ });
1176
+
1177
+ // src/tui/format.ts
1178
+ function argValue(args2) {
1179
+ const pick = args2.path ?? args2.file ?? args2.command ?? args2.cmd ?? args2.query ?? args2.text ?? args2.content;
1180
+ if (typeof pick === "string" && pick.length > 0) return pick;
1181
+ const keys = Object.keys(args2);
1182
+ if (keys.length === 0) return "";
1183
+ const first = args2[keys[0]];
1184
+ return typeof first === "string" ? first : keys.join(", ");
1185
+ }
1186
+ function shortLabel(name, args2) {
1187
+ const value = argValue(args2);
1188
+ const label = value ? `${name} ${value}` : name;
1189
+ return label.length > MAX_LABEL ? label.slice(0, MAX_LABEL - 3) + "..." : label;
1190
+ }
1191
+ function resultSummary(toolName, content, isError) {
1192
+ const firstLine = content.split("\n")[0] ?? "";
1193
+ if (isError) {
1194
+ const msg = firstLine.length > MAX_SUMMARY ? firstLine.slice(0, MAX_SUMMARY - 3) + "..." : firstLine;
1195
+ return msg || "error";
1196
+ }
1197
+ if (toolName === "read") {
1198
+ const lines = content.split("\n").length;
1199
+ return `(${lines} line${lines === 1 ? "" : "s"})`;
1200
+ }
1201
+ if (toolName === "bash") {
1202
+ const lines = content.trim() ? content.trim().split("\n").length : 0;
1203
+ return lines > 0 ? `(${lines} line${lines === 1 ? "" : "s"})` : "(ok)";
1204
+ }
1205
+ if (toolName === "write" || toolName === "edit") return "(ok)";
1206
+ const summary = firstLine.length > MAX_SUMMARY ? firstLine.slice(0, MAX_SUMMARY - 3) + "..." : firstLine;
1207
+ return summary ? summary : "(ok)";
1208
+ }
1209
+ var MAX_LABEL, MAX_SUMMARY;
1210
+ var init_format = __esm({
1211
+ "src/tui/format.ts"() {
1212
+ "use strict";
1213
+ MAX_LABEL = 80;
1214
+ MAX_SUMMARY = 100;
1215
+ }
1216
+ });
1217
+
1218
+ // src/tui/slash.ts
1219
+ function parseSlash(input) {
1220
+ if (!input.startsWith("/")) return null;
1221
+ const parts = input.slice(1).trim().split(/\s+/);
1222
+ const name = parts[0] ?? "";
1223
+ return nameToAction(name, parts.slice(1).join(" "));
1224
+ }
1225
+ function parseBare(input) {
1226
+ const word = input.trim().toLowerCase();
1227
+ if (word === "help" || word === "clear" || word === "quit" || word === "exit" || word === "q") {
1228
+ return nameToAction(word);
1229
+ }
1230
+ return null;
1231
+ }
1232
+ function nameToAction(name, arg = "") {
1233
+ if (name === "help") return { type: "help" };
1234
+ if (name === "clear") return { type: "clear" };
1235
+ if (name === "compact") return { type: "compact" };
1236
+ if (name === "context") return { type: "context" };
1237
+ if (name === "model") return { type: "model", arg: arg.trim() };
1238
+ if (name === "tools") return { type: "tools" };
1239
+ if (name === "sessions") return { type: "sessions" };
1240
+ if (name === "session") return { type: "session" };
1241
+ if (name === "quit" || name === "exit" || name === "q") return { type: "quit" };
1242
+ return { type: "unknown", name: name || "/" };
1243
+ }
1244
+ var HELP_TEXT;
1245
+ var init_slash = __esm({
1246
+ "src/tui/slash.ts"() {
1247
+ "use strict";
1248
+ HELP_TEXT = "Commands:\n /help Show this help\n /clear Clear the transcript\n /compact Summarize old history to free context\n /context Show token usage vs the window\n /model [id] List models, or switch (keeps history)\n /tools List the tools the model can call\n /sessions List saved sessions\n /session Show this session's resume command\n /quit Exit nimi";
1249
+ }
1250
+ });
1251
+
1252
+ // src/tui/App.tsx
1253
+ var App_exports = {};
1254
+ __export(App_exports, {
1255
+ default: () => App
1256
+ });
1257
+ import { useCallback, useEffect, useRef, useState } from "react";
1258
+ import { Box, Static, Text, useApp, useInput } from "ink";
1259
+ import TextInput from "ink-text-input";
1260
+ import Spinner from "ink-spinner";
1261
+ import { jsx, jsxs } from "react/jsx-runtime";
1262
+ function Header({ model: model2, sessionId }) {
1263
+ const width = process.stdout.columns ?? 60;
1264
+ const count = Math.max(20, Math.min(width - 2, DIVIDER_MAX));
1265
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
1266
+ /* @__PURE__ */ jsxs(Box, { children: [
1267
+ /* @__PURE__ */ jsx(Text, { bold: true, children: "nimi" }),
1268
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
1269
+ " ",
1270
+ "\xB7 ",
1271
+ model2,
1272
+ " \xB7 ",
1273
+ process.cwd(),
1274
+ " \xB7 ",
1275
+ sessionId.slice(-6)
1276
+ ] })
1277
+ ] }),
1278
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2500".repeat(count) })
1279
+ ] });
1280
+ }
1281
+ function renderRow(row) {
1282
+ if (row.kind === "user") {
1283
+ return /* @__PURE__ */ jsxs(Box, { marginTop: 1, children: [
1284
+ /* @__PURE__ */ jsx(Text, { bold: true, children: "\u203A " }),
1285
+ /* @__PURE__ */ jsx(Text, { children: row.text })
1286
+ ] }, row.id);
1287
+ }
1288
+ if (row.kind === "text") {
1289
+ return /* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsx(Text, { wrap: "wrap", children: row.text }) }, row.id);
1290
+ }
1291
+ return /* @__PURE__ */ jsx(Box, { marginTop: 0, children: /* @__PURE__ */ jsx(ToolRow, { row }) }, row.id);
1292
+ }
1293
+ function ToolRow({ row }) {
1294
+ if (row.status === "running") {
1295
+ return /* @__PURE__ */ jsxs(Box, { children: [
1296
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u21B3 " }),
1297
+ /* @__PURE__ */ jsx(Spinner, { type: "dots" }),
1298
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
1299
+ " ",
1300
+ row.label
1301
+ ] })
1302
+ ] });
1303
+ }
1304
+ const mark = row.status === "ok" ? "\u2713" : "\u2715";
1305
+ const color = row.status === "ok" ? "green" : "red";
1306
+ return /* @__PURE__ */ jsxs(Box, { children: [
1307
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u21B3 " }),
1308
+ /* @__PURE__ */ jsx(Text, { color, children: mark }),
1309
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
1310
+ " ",
1311
+ row.label,
1312
+ " ",
1313
+ row.summary
1314
+ ] })
1315
+ ] });
1316
+ }
1317
+ function App({ provider: provider2, model: model2, system: system2, tools, initialPrompt, yolo, sessionId, initialMessages, contextLimit }) {
1318
+ const { exit } = useApp();
1319
+ const [rows, setRows] = useState([]);
1320
+ const [draft, setDraft] = useState("");
1321
+ const [working, setWorking] = useState(false);
1322
+ const [error, setError] = useState(null);
1323
+ const [session, setSession] = useState(0);
1324
+ const [pending, setPending] = useState(null);
1325
+ const idRef = useRef(1);
1326
+ const messagesRef = useRef([...initialMessages]);
1327
+ const savedCount = useRef(initialMessages.length);
1328
+ const abortRef = useRef(null);
1329
+ const workingRef = useRef(false);
1330
+ const textIdRef = useRef(null);
1331
+ const startedRef = useRef(false);
1332
+ const alwaysAllowed = useRef(/* @__PURE__ */ new Set());
1333
+ const [activeProvider, setActiveProvider] = useState(provider2);
1334
+ const [activeModel, setActiveModel] = useState(model2);
1335
+ const nextId = useCallback(() => idRef.current++, []);
1336
+ const appendText = useCallback((delta) => {
1337
+ const id = textIdRef.current;
1338
+ if (id === null) {
1339
+ const newId = nextId();
1340
+ textIdRef.current = newId;
1341
+ setRows((prev) => [...prev, { kind: "text", id: newId, text: delta }]);
1342
+ } else {
1343
+ setRows((prev) => prev.map((r) => r.kind === "text" && r.id === id ? { ...r, text: r.text + delta } : r));
1344
+ }
1345
+ }, [nextId]);
1346
+ const canUseTool = useCallback(
1347
+ async (name, input) => {
1348
+ if (yolo || alwaysAllowed.current.has(name)) return { behavior: "allow" };
1349
+ const answer = await new Promise((resolve7) => {
1350
+ setPending({ name, input, resolve: resolve7 });
1351
+ });
1352
+ setPending(null);
1353
+ if (answer === "always") {
1354
+ alwaysAllowed.current.add(name);
1355
+ return { behavior: "allow" };
1356
+ }
1357
+ if (answer === "yes") return { behavior: "allow" };
1358
+ return { behavior: "deny", message: `User denied ${name}. Do not retry; ask what to do instead.` };
1359
+ },
1360
+ [yolo]
1361
+ );
1362
+ const submitPrompt = useCallback(
1363
+ async (prompt) => {
1364
+ const text = prompt.trim();
1365
+ if (!text || workingRef.current) return;
1366
+ setError(null);
1367
+ const slash = parseSlash(text) ?? parseBare(text);
1368
+ if (slash) {
1369
+ if (slash.type === "quit") {
1370
+ exit();
1371
+ return;
1372
+ }
1373
+ if (slash.type === "clear") {
1374
+ messagesRef.current = [];
1375
+ textIdRef.current = null;
1376
+ setRows([]);
1377
+ setSession((s) => s + 1);
1378
+ return;
1379
+ }
1380
+ if (slash.type === "model") {
1381
+ const id2 = nextId();
1382
+ if (!slash.arg) {
1383
+ const lines = MODELS.map((m) => ` ${m.id}${m.id === activeModel ? " \u2190 current" : ""}`);
1384
+ setRows((prev) => [...prev, { kind: "user", id: id2, text }, { kind: "text", id: id2 + 1, text: "Models (use /model <id>):\n" + lines.join("\n") }]);
1385
+ } else {
1386
+ const info = getModelInfo(slash.arg);
1387
+ if (!info) {
1388
+ setRows((prev) => [...prev, { kind: "user", id: id2, text }, { kind: "text", id: id2 + 1, text: `Unknown model: ${slash.arg} (try /model)` }]);
1389
+ } else {
1390
+ try {
1391
+ setActiveProvider(getProvider(info.provider));
1392
+ setActiveModel(info.id);
1393
+ setRows((prev) => [...prev, { kind: "user", id: id2, text }, { kind: "text", id: id2 + 1, text: `Switched to ${info.id} \u2014 history kept.` }]);
1394
+ } catch (e) {
1395
+ setRows((prev) => [...prev, { kind: "user", id: id2, text }, { kind: "text", id: id2 + 1, text: e instanceof Error ? e.message : String(e) }]);
1396
+ }
1397
+ }
1398
+ }
1399
+ idRef.current++;
1400
+ return;
1401
+ }
1402
+ if (slash.type === "tools") {
1403
+ const id2 = nextId();
1404
+ const lines = tools.map((t) => ` ${t.name}${t.readOnly ? " [read-only]" : " [asks permission]"}`);
1405
+ setRows((prev) => [...prev, { kind: "user", id: id2, text }, { kind: "text", id: id2 + 1, text: "Tools:\n" + lines.join("\n") }]);
1406
+ idRef.current++;
1407
+ return;
1408
+ }
1409
+ if (slash.type === "sessions") {
1410
+ const id2 = nextId();
1411
+ const ids = await listSessions();
1412
+ const lines = [];
1413
+ for (let i = ids.length - 1; i >= 0 && lines.length < 10; i--) {
1414
+ const loaded = await loadSession(ids[i]);
1415
+ if (loaded) lines.push(` ${loaded.info.id} ${loaded.info.provider}/${loaded.info.model}`);
1416
+ }
1417
+ setRows((prev) => [...prev, { kind: "user", id: id2, text }, { kind: "text", id: id2 + 1, text: lines.length > 0 ? "Sessions (resume with --session <id>):\n" + lines.join("\n") : "No saved sessions yet." }]);
1418
+ idRef.current++;
1419
+ return;
1420
+ }
1421
+ if (slash.type === "session") {
1422
+ const id2 = nextId();
1423
+ setRows((prev) => [...prev, { kind: "user", id: id2, text }, { kind: "text", id: id2 + 1, text: `Session ${sessionId} \u2014 resume with: nimi --session ${sessionId}` }]);
1424
+ idRef.current++;
1425
+ return;
1426
+ }
1427
+ if (slash.type === "context") {
1428
+ const id2 = nextId();
1429
+ const used = estimateTokens(messagesRef.current);
1430
+ const limit = getContextLimit(activeModel, contextLimit);
1431
+ const text2 = limit ? `~${used} / ${limit} tokens (${Math.round(used / limit * 100)}%) \u2014 auto-compacts at 75%` : `~${used} tokens (window unknown)`;
1432
+ setRows((prev) => [...prev, { kind: "user", id: id2, text: text2 }, { kind: "text", id: id2 + 1, text: text2 }]);
1433
+ idRef.current++;
1434
+ return;
1435
+ }
1436
+ if (slash.type === "compact") {
1437
+ if (workingRef.current) return;
1438
+ const id2 = nextId();
1439
+ if (messagesRef.current.length === 0) {
1440
+ setRows((prev) => [...prev, { kind: "user", id: id2, text }, { kind: "text", id: id2 + 1, text: "Nothing to compact." }]);
1441
+ idRef.current++;
1442
+ return;
1443
+ }
1444
+ workingRef.current = true;
1445
+ setWorking(true);
1446
+ try {
1447
+ const result = await compactMessages(messagesRef.current, activeProvider, activeModel);
1448
+ if (!result.compacted) {
1449
+ setRows((prev) => [...prev, { kind: "user", id: id2, text }, { kind: "text", id: id2 + 1, text: "History is already short." }]);
1450
+ } else {
1451
+ messagesRef.current = result.messages;
1452
+ textIdRef.current = null;
1453
+ setRows((prev) => [...prev, { kind: "user", id: id2, text }, { kind: "text", id: id2 + 1, text: "Compacted. Summary:\n" + result.summary }]);
1454
+ }
1455
+ } catch (e) {
1456
+ setRows((prev) => [...prev, { kind: "user", id: id2, text }, { kind: "text", id: id2 + 1, text: "Compact failed: " + (e instanceof Error ? e.message : String(e)) }]);
1457
+ } finally {
1458
+ workingRef.current = false;
1459
+ setWorking(false);
1460
+ }
1461
+ idRef.current++;
1462
+ return;
1463
+ }
1464
+ const id = nextId();
1465
+ if (slash.type === "help") {
1466
+ setRows((prev) => [...prev, { kind: "user", id, text }, { kind: "text", id: id + 1, text: HELP_TEXT }]);
1467
+ idRef.current++;
1468
+ } else {
1469
+ setRows((prev) => [
1470
+ ...prev,
1471
+ { kind: "user", id, text },
1472
+ { kind: "text", id: id + 1, text: `Unknown command: ${slash.name}. Try /help.` }
1473
+ ]);
1474
+ idRef.current++;
1475
+ }
1476
+ return;
1477
+ }
1478
+ const userId = nextId();
1479
+ setRows((prev) => [...prev, { kind: "user", id: userId, text }]);
1480
+ messagesRef.current.push({ role: "user", content: text });
1481
+ workingRef.current = true;
1482
+ setWorking(true);
1483
+ textIdRef.current = null;
1484
+ const controller = new AbortController();
1485
+ abortRef.current = controller;
1486
+ try {
1487
+ for await (const event of runAgent({
1488
+ provider: activeProvider,
1489
+ model: activeModel,
1490
+ system: system2,
1491
+ tools,
1492
+ messages: messagesRef.current,
1493
+ signal: controller.signal,
1494
+ contextLimit,
1495
+ canUseTool
1496
+ })) {
1497
+ if (event.type === "text_delta") {
1498
+ appendText(event.delta);
1499
+ } else if (event.type === "tool_start") {
1500
+ textIdRef.current = null;
1501
+ const id = nextId();
1502
+ const label = shortLabel(event.name, event.input);
1503
+ setRows((prev) => [
1504
+ ...prev,
1505
+ { kind: "tool", id, toolCallId: event.id, label, status: "running", summary: "" }
1506
+ ]);
1507
+ } else if (event.type === "denied") {
1508
+ textIdRef.current = null;
1509
+ } else if (event.type === "compacted") {
1510
+ textIdRef.current = null;
1511
+ const id = nextId();
1512
+ setRows((prev) => [...prev, { kind: "text", id, text: `Auto-compacted context: ${event.before} \u2192 ${event.after} messages.` }]);
1513
+ } else if (event.type === "tool_end") {
1514
+ textIdRef.current = null;
1515
+ const summary = resultSummary(event.name, event.result, event.isError);
1516
+ setRows(
1517
+ (prev) => prev.map(
1518
+ (r) => r.kind === "tool" && r.toolCallId === event.id ? { ...r, status: event.isError ? "error" : "ok", summary } : r
1519
+ )
1520
+ );
1521
+ } else if (event.type === "turn_end" || event.type === "agent_end") {
1522
+ textIdRef.current = null;
1523
+ }
1524
+ }
1525
+ } catch (err) {
1526
+ textIdRef.current = null;
1527
+ if (err instanceof Error && err.name === "AbortError") {
1528
+ const id = nextId();
1529
+ setRows((prev) => [...prev, { kind: "text", id, text: "(interrupted)" }]);
1530
+ } else {
1531
+ setError(err instanceof Error ? err.message : String(err));
1532
+ }
1533
+ } finally {
1534
+ abortRef.current = null;
1535
+ workingRef.current = false;
1536
+ setWorking(false);
1537
+ const fresh = messagesRef.current.slice(savedCount.current);
1538
+ if (fresh.length > 0) {
1539
+ savedCount.current = messagesRef.current.length;
1540
+ void appendMessages(sessionId, fresh).catch((e) => {
1541
+ setError(e instanceof Error ? e.message : String(e));
1542
+ });
1543
+ }
1544
+ }
1545
+ },
1546
+ [appendText, canUseTool, contextLimit, exit, activeModel, activeProvider, nextId, sessionId, system2, tools]
1547
+ );
1548
+ useInput((input, key) => {
1549
+ if (pending) {
1550
+ const ch = input.toLowerCase();
1551
+ if (ch === "y") pending.resolve("yes");
1552
+ else if (ch === "a") pending.resolve("always");
1553
+ else if (ch === "n" || key.escape) pending.resolve("no");
1554
+ return;
1555
+ }
1556
+ if (key.escape && workingRef.current) {
1557
+ abortRef.current?.abort();
1558
+ }
1559
+ });
1560
+ useEffect(() => {
1561
+ if (!startedRef.current && initialPrompt) {
1562
+ startedRef.current = true;
1563
+ void submitPrompt(initialPrompt);
1564
+ }
1565
+ }, [initialPrompt, submitPrompt]);
1566
+ const settled = rows.length > 0 ? rows.slice(0, -1) : [];
1567
+ const live = rows.length > 0 ? rows[rows.length - 1] : null;
1568
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
1569
+ /* @__PURE__ */ jsx(Header, { model: activeModel, sessionId }),
1570
+ rows.length === 0 && /* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: initialMessages.length > 0 ? `Resumed session \u2014 ${initialMessages.length} messages restored. Type, or /help.` : "Type a message, or /help." }) }),
1571
+ /* @__PURE__ */ jsx(Static, { items: settled, children: (row) => renderRow(row) }, session),
1572
+ live && renderRow(live),
1573
+ pending && /* @__PURE__ */ jsxs(Box, { marginTop: 1, flexDirection: "column", children: [
1574
+ /* @__PURE__ */ jsxs(Text, { color: "yellow", children: [
1575
+ "Permission needed: ",
1576
+ shortLabel(pending.name, pending.input)
1577
+ ] }),
1578
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "y = yes \xB7 a = always \xB7 n = no (esc)" })
1579
+ ] }),
1580
+ error && /* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsxs(Text, { color: "red", children: [
1581
+ "\u2715 ",
1582
+ error
1583
+ ] }) }),
1584
+ /* @__PURE__ */ jsxs(Box, { marginTop: 1, children: [
1585
+ /* @__PURE__ */ jsx(Text, { dimColor: working, children: "\u203A " }),
1586
+ /* @__PURE__ */ jsx(
1587
+ TextInput,
1588
+ {
1589
+ value: draft,
1590
+ onChange: setDraft,
1591
+ onSubmit: (value) => {
1592
+ if (pending) return;
1593
+ setDraft("");
1594
+ void submitPrompt(value);
1595
+ },
1596
+ placeholder: pending ? "answer y/a/n\u2026" : working ? "working\u2026" : "Ask nimi\u2026"
1597
+ }
1598
+ )
1599
+ ] }),
1600
+ /* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: working ? "working\u2026 esc to interrupt" : "enter send \xB7 /help /clear /quit \xB7 esc interrupt" }) })
1601
+ ] });
1602
+ }
1603
+ var DIVIDER_MAX;
1604
+ var init_App = __esm({
1605
+ "src/tui/App.tsx"() {
1606
+ "use strict";
1607
+ init_agent();
1608
+ init_compaction();
1609
+ init_models();
1610
+ init_providers();
1611
+ init_store();
1612
+ init_format();
1613
+ init_slash();
1614
+ DIVIDER_MAX = 80;
1615
+ }
1616
+ });
1617
+
1618
+ // src/modes/classic.ts
1619
+ var classic_exports = {};
1620
+ __export(classic_exports, {
1621
+ runClassic: () => runClassic
1622
+ });
1623
+ import { createInterface } from "readline";
1624
+ async function runClassic(opts) {
1625
+ let provider2 = opts.provider;
1626
+ let model2 = opts.model;
1627
+ const messages = [...opts.initialMessages];
1628
+ let saved = messages.length;
1629
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
1630
+ const canUseTool = opts.yolo ? allowAll() : createPromptPermission(rl);
1631
+ let trace = null;
1632
+ const ask = (q) => new Promise((resolve7) => rl.question(q, resolve7));
1633
+ console.log(`nimi ${model2} \xB7 session ${opts.sessionId.slice(-6)} \xB7 /help for commands`);
1634
+ if (messages.length > 0) console.log(`Resumed \u2014 ${messages.length} messages restored.`);
1635
+ if (opts.yolo) console.log("Permission prompts off (--yolo).");
1636
+ while (true) {
1637
+ const line = (await ask("\u203A ")).trim();
1638
+ if (!line) continue;
1639
+ const slash = parseSlash(line) ?? parseBare(line);
1640
+ if (slash) {
1641
+ if (slash.type === "quit") break;
1642
+ if (slash.type === "clear") {
1643
+ messages.length = 0;
1644
+ continue;
1645
+ }
1646
+ if (slash.type === "help") {
1647
+ console.log(HELP_TEXT);
1648
+ continue;
1649
+ }
1650
+ if (slash.type === "context") {
1651
+ const used = estimateTokens(messages);
1652
+ const limit = getContextLimit(model2, opts.contextLimit);
1653
+ console.log(limit ? `~${used} / ${limit} tokens` : `~${used} tokens (window unknown)`);
1654
+ continue;
1655
+ }
1656
+ if (slash.type === "compact") {
1657
+ const result = await compactMessages(messages, provider2, model2);
1658
+ if (!result.compacted) console.log("History is already short.");
1659
+ else {
1660
+ messages.length = 0;
1661
+ messages.push(...result.messages);
1662
+ console.log("Compacted. Summary:\n" + result.summary);
1663
+ }
1664
+ continue;
1665
+ }
1666
+ if (slash.type === "model") {
1667
+ if (!slash.arg) {
1668
+ for (const m of MODELS) console.log(` ${m.id}${m.id === model2 ? " \u2190 current" : ""}`);
1669
+ } else {
1670
+ const info = getModelInfo(slash.arg);
1671
+ if (!info) console.log(`Unknown model: ${slash.arg}`);
1672
+ else {
1673
+ try {
1674
+ provider2 = getProvider(info.provider);
1675
+ model2 = info.id;
1676
+ console.log(`Switched to ${model2} \u2014 history kept.`);
1677
+ } catch (e) {
1678
+ console.log(e instanceof Error ? e.message : String(e));
1679
+ }
1680
+ }
1681
+ }
1682
+ continue;
1683
+ }
1684
+ if (slash.type === "tools") {
1685
+ for (const t of ALL_TOOLS) console.log(` ${t.name}${t.readOnly ? " [read-only]" : " [asks permission]"}`);
1686
+ continue;
1687
+ }
1688
+ if (slash.type === "sessions") {
1689
+ const ids = await listSessions();
1690
+ if (ids.length === 0) console.log("No saved sessions yet.");
1691
+ for (let i = ids.length - 1; i >= 0 && i >= ids.length - 10; i--) {
1692
+ const loaded = await loadSession(ids[i]);
1693
+ if (loaded) console.log(` ${loaded.info.id} ${loaded.info.provider}/${loaded.info.model}`);
1694
+ }
1695
+ continue;
1696
+ }
1697
+ if (slash.type === "session") {
1698
+ console.log(`Session ${opts.sessionId} \u2014 resume with: nimi --session ${opts.sessionId}`);
1699
+ continue;
1700
+ }
1701
+ console.log(`Unknown command: ${slash.name}. Try /help.`);
1702
+ continue;
1703
+ }
1704
+ messages.push({ role: "user", content: line });
1705
+ if (opts.tracePath && !trace) {
1706
+ trace = createTraceRecorder(opts.tracePath, {
1707
+ session: opts.sessionId,
1708
+ provider: provider2.name,
1709
+ model: model2,
1710
+ prompt: line,
1711
+ argv: process.argv.slice(2),
1712
+ tools: opts.tools.map((t) => ({ name: t.name, readOnly: t.readOnly }))
1713
+ });
1714
+ }
1715
+ for await (const event of runAgent({ provider: provider2, model: model2, system: opts.system, tools: opts.tools, messages, contextLimit: opts.contextLimit, canUseTool })) {
1716
+ trace?.record(event, messages);
1717
+ if (event.type === "text_delta") process.stdout.write(event.delta);
1718
+ else if (event.type === "tool_start") console.log(`
1719
+ [tool] ${event.name} ${JSON.stringify(event.input)}`);
1720
+ else if (event.type === "denied") console.log(`
1721
+ [denied] ${event.name}: ${event.reason}`);
1722
+ else if (event.type === "compacted") console.log(`
1723
+ [compacted ${event.before} \u2192 ${event.after} messages]`);
1724
+ else if (event.type === "tool_end") {
1725
+ const preview = event.result.length > 300 ? event.result.slice(0, 300) + "..." : event.result;
1726
+ console.log(`
1727
+ [result] ${event.name}${event.isError ? " (error)" : ""}:
1728
+ ${preview}`);
1729
+ } else if (event.type === "turn_end" || event.type === "agent_end") process.stdout.write("\n");
1730
+ }
1731
+ const fresh = messages.slice(saved);
1732
+ if (fresh.length > 0) {
1733
+ saved = messages.length;
1734
+ await appendMessages(opts.sessionId, fresh);
1735
+ }
1736
+ }
1737
+ trace?.finish(messages);
1738
+ rl.close();
1739
+ }
1740
+ var init_classic = __esm({
1741
+ "src/modes/classic.ts"() {
1742
+ "use strict";
1743
+ init_agent();
1744
+ init_compaction();
1745
+ init_providers();
1746
+ init_models();
1747
+ init_permissions();
1748
+ init_store();
1749
+ init_tools();
1750
+ init_recorder();
1751
+ init_slash();
1752
+ }
1753
+ });
1754
+
1755
+ // src/main.tsx
1756
+ init_agent();
1757
+ import { config } from "dotenv";
1758
+ import { createInterface as createInterface2 } from "readline";
1759
+
1760
+ // src/cli/arg.ts
1761
+ var DEFAULT_PROVIDER = "meta";
1762
+ function parseArgs(argv) {
1763
+ const args2 = {
1764
+ provider: void 0,
1765
+ listSessions: false,
1766
+ classic: false,
1767
+ print: false,
1768
+ yolo: false,
1769
+ version: false,
1770
+ help: false
1771
+ };
1772
+ const positional = [];
1773
+ for (let i = 0; i < argv.length; i++) {
1774
+ const arg = argv[i];
1775
+ const eq = arg.startsWith("--") ? arg.indexOf("=") : -1;
1776
+ const flag = eq === -1 ? arg : arg.slice(0, eq);
1777
+ const inline = eq === -1 ? void 0 : arg.slice(eq + 1);
1778
+ const value = () => {
1779
+ if (inline !== void 0) return inline;
1780
+ const next = argv[++i];
1781
+ if (next === void 0 || next.startsWith("-")) {
1782
+ throw new Error(`${flag} needs a value`);
1783
+ }
1784
+ return next;
1785
+ };
1786
+ switch (flag) {
1787
+ case "--prompt":
1788
+ case "-p":
1789
+ args2.prompt = value();
1790
+ break;
1791
+ case "--provider":
1792
+ case "-z":
1793
+ args2.provider = value();
1794
+ break;
1795
+ case "--model":
1796
+ args2.model = value();
1797
+ break;
1798
+ case "--session":
1799
+ args2.session = value();
1800
+ break;
1801
+ case "--list-sessions":
1802
+ args2.listSessions = true;
1803
+ break;
1804
+ case "--context-limit": {
1805
+ const n = Number(value());
1806
+ if (!Number.isInteger(n) || n <= 0) throw new Error("--context-limit needs a positive integer");
1807
+ args2.contextLimit = n;
1808
+ break;
1809
+ }
1810
+ case "--trace":
1811
+ args2.trace = value();
1812
+ break;
1813
+ case "--classic":
1814
+ args2.classic = true;
1815
+ break;
1816
+ case "--print":
1817
+ args2.print = true;
1818
+ break;
1819
+ case "--yolo":
1820
+ args2.yolo = true;
1821
+ break;
1822
+ case "--version":
1823
+ case "-v":
1824
+ args2.version = true;
1825
+ break;
1826
+ case "--help":
1827
+ case "-h":
1828
+ args2.help = true;
1829
+ break;
1830
+ default:
1831
+ if (flag.startsWith("-")) {
1832
+ throw new Error(`Unknown option: ${flag}`);
1833
+ }
1834
+ positional.push(arg);
1835
+ }
1836
+ }
1837
+ if (positional.length > 0) {
1838
+ if (args2.prompt !== void 0) {
1839
+ throw new Error("Prompt given twice (use either -p or a positional message)");
1840
+ }
1841
+ args2.prompt = positional.join(" ");
1842
+ }
1843
+ return args2;
1844
+ }
1845
+ function printHelp() {
1846
+ console.log(`
1847
+ nimi \u2014 minimal coding agent
1848
+
1849
+ USAGE
1850
+ nimi -p "prompt" [options]
1851
+ nimi "prompt" [options]
1852
+
1853
+ OPTIONS
1854
+ -p, --prompt <text> The prompt to send to the model
1855
+ -z, --provider <name> Provider to use (default: ${DEFAULT_PROVIDER})
1856
+ --model <id> Model ID (defaults to the provider's model)
1857
+ --session <id> Resume a saved session
1858
+ --list-sessions Print saved sessions, then exit
1859
+ --context-limit <n> Override the model's context window
1860
+ --trace <path> Write a JSONL showcase trace (print mode)
1861
+ --classic Plain readline REPL instead of the TUI
1862
+ --print Force one-shot print mode even in a TTY
1863
+ --yolo Run all tools without asking
1864
+ -v, --version Show version
1865
+ -h, --help Show this help
1866
+
1867
+ PROVIDERS
1868
+ anthropic, anthropic-openai, groq, meta, deepseek
1869
+
1870
+ EXAMPLES
1871
+ nimi -p "explain this codebase"
1872
+ nimi "list all ts files" --provider deepseek
1873
+ nimi -p "read package.json" --model <id>
1874
+ `);
1875
+ }
1876
+ function printVersion() {
1877
+ console.log(`nimi ${true ? "1.0.0" : "dev"}`);
1878
+ }
1879
+
1880
+ // src/config/system-prompt.ts
1881
+ init_tools();
1882
+ import { existsSync } from "fs";
1883
+ import { readFile as readFile4 } from "fs/promises";
1884
+ import { join as join3 } from "path";
1885
+ var CONTEXT_FILES = ["AGENTS.md", "CLAUDE.md", ".context", ".nimi/context.md"];
1886
+ async function buildSystemPrompt(extra) {
1887
+ const base = `You are nimi, an autonomous coding assistant.
1888
+ You are running in: ${process.cwd()}
1889
+ Today: ${(/* @__PURE__ */ new Date()).toLocaleDateString("en-US", { weekday: "long", year: "numeric", month: "long", day: "numeric" })}
1890
+
1891
+ You have access to tools to read files, write files, run shell commands, and search code.
1892
+ Use tools proactively to understand the codebase before making changes.
1893
+ For greetings, small talk, or questions that need no codebase context, reply directly without calling tools.
1894
+ Always read a file before editing it. Verify your changes work.
1895
+ Be concise. No sycophantic openers. No unnecessary explanation.`;
1896
+ const toolsList = ALL_TOOLS.map((t) => `- ${t.name}: ${t.description}`).join("\n");
1897
+ const toolsSection = `
1898
+ Available tools:
1899
+ ${toolsList}`;
1900
+ let contextSection = "";
1901
+ for (const filename of CONTEXT_FILES) {
1902
+ const filepath = join3(process.cwd(), filename);
1903
+ if (existsSync(filepath)) {
1904
+ const content = await readFile4(filepath, "utf-8");
1905
+ contextSection += `
1906
+
1907
+ --- ${filename} ---
1908
+ ${content}`;
1909
+ }
1910
+ }
1911
+ const extraSection = extra ? `
1912
+
1913
+ ${extra}` : "";
1914
+ return base + toolsSection + contextSection + extraSection;
1915
+ }
1916
+
1917
+ // src/config/settings.ts
1918
+ import { existsSync as existsSync2 } from "fs";
1919
+ import { readFile as readFile5 } from "fs/promises";
1920
+ import { homedir } from "os";
1921
+ import { join as join4 } from "path";
1922
+ function getGlobalSettingsPath() {
1923
+ return join4(homedir(), ".nimi", "settings.json");
1924
+ }
1925
+ function getProjectSettingsPath() {
1926
+ return join4(process.cwd(), ".nimi", "settings.json");
1927
+ }
1928
+ async function loadSettings() {
1929
+ const global = await readJsonSafe(getGlobalSettingsPath());
1930
+ const project = await readJsonSafe(getProjectSettingsPath());
1931
+ const merged = { ...global, ...project };
1932
+ if (process.env.NIMI_PROVIDER) merged.provider = process.env.NIMI_PROVIDER;
1933
+ if (process.env.NIMI_MODEL) merged.model = process.env.NIMI_MODEL;
1934
+ return merged;
1935
+ }
1936
+ async function readJsonSafe(path) {
1937
+ if (!existsSync2(path)) return {};
1938
+ try {
1939
+ return JSON.parse(await readFile5(path, "utf-8"));
1940
+ } catch {
1941
+ return {};
1942
+ }
1943
+ }
1944
+
1945
+ // src/main.tsx
1946
+ init_permissions();
1947
+ init_providers();
1948
+ init_store();
1949
+ init_recorder();
1950
+ init_tools();
1951
+ import { jsx as jsx2 } from "react/jsx-runtime";
1952
+ config({ quiet: true });
1953
+ var args;
1954
+ try {
1955
+ args = parseArgs(process.argv.slice(2));
1956
+ } catch (error) {
1957
+ console.error(error instanceof Error ? error.message : String(error));
1958
+ console.error('Run "nimi --help" for usage.');
1959
+ process.exit(1);
1960
+ }
1961
+ if (args.help) {
1962
+ printHelp();
1963
+ process.exit(0);
1964
+ }
1965
+ if (args.version) {
1966
+ printVersion();
1967
+ process.exit(0);
1968
+ }
1969
+ var settings = await loadSettings();
1970
+ var provider = getProvider(args.provider ?? settings.provider ?? "meta");
1971
+ var model = args.model ?? settings.model ?? provider.defaultModel;
1972
+ var system = await buildSystemPrompt(settings.systemPromptExtra);
1973
+ if (args.listSessions) {
1974
+ const ids = await listSessions();
1975
+ if (ids.length === 0) console.log("No saved sessions yet.");
1976
+ for (let i = ids.length - 1; i >= 0; i--) {
1977
+ const loaded = await loadSession(ids[i]);
1978
+ let first = "(no prompt)";
1979
+ if (loaded) {
1980
+ for (const m of loaded.messages) {
1981
+ if (m.role === "user") {
1982
+ first = m.content.replace(/\s+/g, " ").slice(0, 60);
1983
+ break;
1984
+ }
1985
+ }
1986
+ console.log(`${loaded.info.id} ${loaded.info.created_at} [${loaded.info.provider}/${loaded.info.model}] ${first}`);
1987
+ }
1988
+ }
1989
+ process.exit(0);
1990
+ }
1991
+ if (!args.classic && (args.print || !process.stdin.isTTY || !process.stdout.isTTY)) {
1992
+ if (!args.prompt) {
1993
+ console.error('No prompt given. Run "nimi --help" for usage.');
1994
+ process.exit(1);
1995
+ }
1996
+ let messages;
1997
+ let sessionId;
1998
+ if (args.session) {
1999
+ const loaded = await loadSession(args.session);
2000
+ if (!loaded) {
2001
+ console.error(`No such session: ${args.session}. Run "--list-sessions" to see saved ids.`);
2002
+ process.exit(1);
2003
+ }
2004
+ messages = [...loaded.messages, { role: "user", content: args.prompt }];
2005
+ sessionId = loaded.info.id;
2006
+ } else {
2007
+ sessionId = newSessionId();
2008
+ messages = [{ role: "user", content: args.prompt }];
2009
+ await appendEntry(sessionId, { type: "session_info", id: sessionId, created_at: (/* @__PURE__ */ new Date()).toISOString(), model, provider: provider.name });
2010
+ }
2011
+ console.error(`Session ${sessionId} \u2014 resume with: --session ${sessionId}`);
2012
+ const before = messages.length;
2013
+ const trace = args.trace ? createTraceRecorder(args.trace, {
2014
+ session: sessionId,
2015
+ provider: provider.name,
2016
+ model,
2017
+ prompt: args.prompt,
2018
+ argv: process.argv.slice(2),
2019
+ tools: ALL_TOOLS.map((t) => ({ name: t.name, readOnly: t.readOnly }))
2020
+ }) : null;
2021
+ const rl = args.yolo ? null : createInterface2({ input: process.stdin, output: process.stdout });
2022
+ const canUseTool = args.yolo ? allowAll() : createPromptPermission(rl);
2023
+ try {
2024
+ const stream = runAgent({
2025
+ provider,
2026
+ model,
2027
+ system,
2028
+ messages,
2029
+ tools: ALL_TOOLS,
2030
+ contextLimit: args.contextLimit,
2031
+ canUseTool
2032
+ });
2033
+ for await (const event of trace ? withTrace(stream, trace, messages) : stream) {
2034
+ if (event.type === "text_delta") process.stdout.write(event.delta);
2035
+ else if (event.type === "compacted") {
2036
+ console.log(`
2037
+ [compacted ${event.before} \u2192 ${event.after} messages]`);
2038
+ } else if (event.type === "tool_start") {
2039
+ console.log(`
2040
+
2041
+ [tool] ${event.name} ${JSON.stringify(event.input)}`);
2042
+ } else if (event.type === "denied") {
2043
+ console.log(`
2044
+ [denied] ${event.name}: ${event.reason}`);
2045
+ } else if (event.type === "tool_end") {
2046
+ const preview = event.result.length > 500 ? event.result.slice(0, 500) + "..." : event.result;
2047
+ console.log(`[result] ${event.name}${event.isError ? " (error)" : ""}:
2048
+ ${preview}`);
2049
+ }
2050
+ }
2051
+ } finally {
2052
+ rl?.close();
2053
+ }
2054
+ await appendMessages(sessionId, messages.slice(before));
2055
+ process.exit(0);
2056
+ }
2057
+ var { render } = await import("ink");
2058
+ var { default: App2 } = await Promise.resolve().then(() => (init_App(), App_exports));
2059
+ var tuiMessages;
2060
+ var tuiSessionId;
2061
+ if (args.session) {
2062
+ const loaded = await loadSession(args.session);
2063
+ if (!loaded) {
2064
+ console.error(`No such session: ${args.session}. Run "--list-sessions" to see saved ids.`);
2065
+ process.exit(1);
2066
+ }
2067
+ tuiMessages = loaded.messages;
2068
+ tuiSessionId = loaded.info.id;
2069
+ } else {
2070
+ tuiSessionId = newSessionId();
2071
+ tuiMessages = [];
2072
+ await appendEntry(tuiSessionId, { type: "session_info", id: tuiSessionId, created_at: (/* @__PURE__ */ new Date()).toISOString(), model, provider: provider.name });
2073
+ }
2074
+ if (args.classic) {
2075
+ const { runClassic: runClassic2 } = await Promise.resolve().then(() => (init_classic(), classic_exports));
2076
+ await runClassic2({ provider, model, system, tools: ALL_TOOLS, sessionId: tuiSessionId, initialMessages: tuiMessages, yolo: args.yolo, contextLimit: args.contextLimit, tracePath: args.trace });
2077
+ process.exit(0);
2078
+ }
2079
+ render(
2080
+ /* @__PURE__ */ jsx2(App2, { provider, model, system, tools: ALL_TOOLS, initialPrompt: args.prompt, yolo: args.yolo, sessionId: tuiSessionId, initialMessages: tuiMessages, contextLimit: args.contextLimit })
2081
+ );