darkfoo-code 0.1.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,1740 @@
1
+ import {
2
+ theme
3
+ } from "./chunk-BT7IPQDS.js";
4
+ import {
5
+ query
6
+ } from "./chunk-GQXUHUV4.js";
7
+ import {
8
+ BashTool,
9
+ getAppState,
10
+ getTools,
11
+ trackedFileCount,
12
+ truncate
13
+ } from "./chunk-4KTJEE4A.js";
14
+ import {
15
+ discoverProviders,
16
+ getActiveProviderName,
17
+ getProvider,
18
+ getProviderConfigs,
19
+ loadProviderSettings,
20
+ saveProviderSettings,
21
+ setActiveProvider,
22
+ upsertProviderConfig
23
+ } from "./chunk-OBL22IIN.js";
24
+ import {
25
+ buildSystemPrompt
26
+ } from "./chunk-VSJKCANO.js";
27
+
28
+ // src/main.tsx
29
+ import { Command } from "commander";
30
+ import { render } from "ink";
31
+
32
+ // src/app.tsx
33
+ import { createContext, useContext } from "react";
34
+ import { jsx } from "react/jsx-runtime";
35
+ var DarkfooContext = createContext({ model: "qwen2.5-coder:32b" });
36
+ function useDarkfooContext() {
37
+ return useContext(DarkfooContext);
38
+ }
39
+ function App({ model, systemPromptOverride, children }) {
40
+ return /* @__PURE__ */ jsx(DarkfooContext.Provider, { value: { model, systemPromptOverride }, children });
41
+ }
42
+
43
+ // src/repl.tsx
44
+ import { useState as useState2, useCallback as useCallback2, useEffect, useRef } from "react";
45
+ import { Box as Box6, Text as Text6, useApp, useInput as useInput2 } from "ink";
46
+ import Spinner2 from "ink-spinner";
47
+ import { nanoid as nanoid3 } from "nanoid";
48
+
49
+ // src/components/Banner.tsx
50
+ import { Box, Text } from "ink";
51
+ import { jsx as jsx2, jsxs } from "react/jsx-runtime";
52
+ var LOGO_LINES = [
53
+ " \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 ",
54
+ " \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557 \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557 \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557 \u2588\u2588\u2551 \u2588\u2588\u2554\u255D \u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D \u2588\u2588\u2554\u2550\u2550\u2550\u2588\u2588\u2557 \u2588\u2588\u2554\u2550\u2550\u2550\u2588\u2588\u2557",
55
+ " \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D \u2588\u2588\u2588\u2588\u2588\u2554\u255D \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551",
56
+ " \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557 \u2588\u2588\u2554\u2550\u2588\u2588\u2557 \u2588\u2588\u2554\u2550\u2550\u255D \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551",
57
+ " \u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2557 \u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D \u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D",
58
+ " \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D "
59
+ ];
60
+ var SUBTITLE = " \u2584\u2580 C O D E \u2580\u2584";
61
+ var GRADIENT = [
62
+ "#5eead4",
63
+ // cyan
64
+ "#6ee0c8",
65
+ // cyan-light
66
+ "#82c8d0",
67
+ // cyan-blue
68
+ "#96b0d8",
69
+ // blue-ish
70
+ "#a78bfa",
71
+ // purple
72
+ "#c47ee8",
73
+ // purple-pink
74
+ "#f472b6"
75
+ // pink
76
+ ];
77
+ function lineColor(index) {
78
+ const t = index / (LOGO_LINES.length - 1);
79
+ const gradientIdx = Math.round(t * (GRADIENT.length - 1));
80
+ return GRADIENT[gradientIdx];
81
+ }
82
+ function gradientDivider(width) {
83
+ const chars = [];
84
+ for (let i = 0; i < width; i++) {
85
+ const t = i / (width - 1);
86
+ const gradientIdx = Math.round(t * (GRADIENT.length - 1));
87
+ chars.push({ char: "\u2501", color: GRADIENT[gradientIdx] });
88
+ }
89
+ return chars;
90
+ }
91
+ function Banner({ model, cwd }) {
92
+ const divider = gradientDivider(60);
93
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [
94
+ LOGO_LINES.map((line, i) => /* @__PURE__ */ jsx2(Text, { color: lineColor(i), bold: true, children: line }, i)),
95
+ /* @__PURE__ */ jsx2(Text, { color: theme.purple, bold: true, children: SUBTITLE }),
96
+ /* @__PURE__ */ jsx2(Text, { children: " " }),
97
+ /* @__PURE__ */ jsxs(Text, { children: [
98
+ " ",
99
+ divider.map((d, i) => /* @__PURE__ */ jsx2(Text, { color: d.color, children: d.char }, i))
100
+ ] }),
101
+ /* @__PURE__ */ jsxs(Box, { marginTop: 1, marginLeft: 2, children: [
102
+ /* @__PURE__ */ jsx2(Text, { color: theme.dim, children: "\u26A1 " }),
103
+ /* @__PURE__ */ jsx2(Text, { color: theme.cyan, bold: true, children: model }),
104
+ /* @__PURE__ */ jsx2(Text, { color: theme.dim, children: " \u2502 " }),
105
+ /* @__PURE__ */ jsx2(Text, { color: theme.text, children: cwd })
106
+ ] }),
107
+ /* @__PURE__ */ jsxs(Box, { marginLeft: 2, children: [
108
+ /* @__PURE__ */ jsx2(Text, { color: theme.dim, children: " Type a message to begin. " }),
109
+ /* @__PURE__ */ jsx2(Text, { color: theme.cyanDim, children: "Ctrl+C" }),
110
+ /* @__PURE__ */ jsx2(Text, { color: theme.dim, children: " to abort or exit." })
111
+ ] }),
112
+ /* @__PURE__ */ jsxs(Text, { children: [
113
+ " ",
114
+ divider.map((d, i) => /* @__PURE__ */ jsx2(Text, { color: d.color, children: d.char }, i))
115
+ ] })
116
+ ] });
117
+ }
118
+
119
+ // src/components/Messages.tsx
120
+ import { Box as Box2, Text as Text2 } from "ink";
121
+
122
+ // src/utils/markdown.ts
123
+ function renderMarkdown(text) {
124
+ const lines = text.split("\n");
125
+ const result = [];
126
+ let inCodeBlock = false;
127
+ let codeBlockLang = "";
128
+ for (const line of lines) {
129
+ if (line.trimStart().startsWith("```")) {
130
+ if (!inCodeBlock) {
131
+ codeBlockLang = line.trimStart().slice(3).trim();
132
+ const label = codeBlockLang ? ` ${codeBlockLang}` : "";
133
+ result.push(`\x1B[2m\x1B[38;2;94;234;212m\u2500\u2500\u2500 code${label} \u2500\u2500\u2500\x1B[0m`);
134
+ inCodeBlock = true;
135
+ } else {
136
+ result.push(`\x1B[2m\x1B[38;2;94;234;212m\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\x1B[0m`);
137
+ inCodeBlock = false;
138
+ codeBlockLang = "";
139
+ }
140
+ continue;
141
+ }
142
+ if (inCodeBlock) {
143
+ result.push(`\x1B[38;2;251;191;36m ${line}\x1B[0m`);
144
+ continue;
145
+ }
146
+ const headerMatch = line.match(/^(#{1,6})\s+(.+)/);
147
+ if (headerMatch) {
148
+ const level = headerMatch[1].length;
149
+ const text2 = headerMatch[2];
150
+ if (level <= 2) {
151
+ result.push(`\x1B[1m\x1B[38;2;94;234;212m${text2}\x1B[0m`);
152
+ } else {
153
+ result.push(`\x1B[1m${text2}\x1B[0m`);
154
+ }
155
+ continue;
156
+ }
157
+ const listMatch = line.match(/^(\s*)[*-]\s+(.+)/);
158
+ if (listMatch) {
159
+ const indent = listMatch[1] || "";
160
+ const content = renderInline(listMatch[2]);
161
+ result.push(`${indent}\x1B[38;2;94;234;212m\u2022\x1B[0m ${content}`);
162
+ continue;
163
+ }
164
+ const orderedMatch = line.match(/^(\s*)\d+\.\s+(.+)/);
165
+ if (orderedMatch) {
166
+ const indent = orderedMatch[1] || "";
167
+ const content = renderInline(orderedMatch[2]);
168
+ result.push(`${indent}${content}`);
169
+ continue;
170
+ }
171
+ if (/^---+$|^\*\*\*+$|^___+$/.test(line.trim())) {
172
+ result.push(`\x1B[2m${"\u2500".repeat(40)}\x1B[0m`);
173
+ continue;
174
+ }
175
+ result.push(renderInline(line));
176
+ }
177
+ return result.join("\n");
178
+ }
179
+ function renderInline(text) {
180
+ return text.replace(/`([^`]+)`/g, "\x1B[38;2;251;191;36m$1\x1B[0m").replace(/\*\*\*(.+?)\*\*\*/g, "\x1B[1m\x1B[3m$1\x1B[0m").replace(/\*\*(.+?)\*\*/g, "\x1B[1m$1\x1B[0m").replace(/\*(.+?)\*/g, "\x1B[3m$1\x1B[0m").replace(/\[([^\]]+)\]\(([^)]+)\)/g, "\x1B[4m\x1B[38;2;94;234;212m$1\x1B[0m");
181
+ }
182
+
183
+ // src/components/Messages.tsx
184
+ import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
185
+ function Messages({ messages }) {
186
+ return /* @__PURE__ */ jsx3(Box2, { flexDirection: "column", children: messages.map((msg) => /* @__PURE__ */ jsx3(MessageRow, { message: msg }, msg.id)) });
187
+ }
188
+ function MessageRow({ message }) {
189
+ switch (message.role) {
190
+ case "user":
191
+ return /* @__PURE__ */ jsxs2(Box2, { marginTop: 1, marginBottom: 1, children: [
192
+ /* @__PURE__ */ jsx3(Text2, { color: theme.cyan, bold: true, children: "\u276F " }),
193
+ /* @__PURE__ */ jsx3(Text2, { bold: true, color: theme.text, children: message.content })
194
+ ] });
195
+ case "assistant": {
196
+ if (!message.content && message.toolCalls) return null;
197
+ if (!message.content) return null;
198
+ const rendered = renderMarkdown(message.content);
199
+ return /* @__PURE__ */ jsx3(Box2, { flexDirection: "column", marginBottom: 1, children: /* @__PURE__ */ jsxs2(Box2, { children: [
200
+ /* @__PURE__ */ jsx3(Text2, { color: theme.cyan, children: "\u23BF " }),
201
+ /* @__PURE__ */ jsx3(Text2, { wrap: "wrap", children: rendered })
202
+ ] }) });
203
+ }
204
+ case "tool": {
205
+ const lines = message.content.split("\n");
206
+ const preview = lines.length > 8 ? lines.slice(0, 8).join("\n") + `
207
+ ... (${lines.length - 8} more lines)` : message.content;
208
+ return /* @__PURE__ */ jsx3(Box2, { flexDirection: "column", marginLeft: 2, marginBottom: 1, children: /* @__PURE__ */ jsxs2(Text2, { color: theme.dim, children: [
209
+ "\u23BF ",
210
+ truncate(preview, 1200)
211
+ ] }) });
212
+ }
213
+ default:
214
+ return null;
215
+ }
216
+ }
217
+
218
+ // src/components/ToolCall.tsx
219
+ import { Box as Box3, Text as Text3 } from "ink";
220
+ import Spinner from "ink-spinner";
221
+ import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
222
+ function ActiveToolCall({ toolName, args }) {
223
+ return /* @__PURE__ */ jsxs3(Box3, { marginLeft: 2, children: [
224
+ /* @__PURE__ */ jsx4(Text3, { color: theme.cyan, children: /* @__PURE__ */ jsx4(Spinner, { type: "dots" }) }),
225
+ /* @__PURE__ */ jsxs3(Text3, { bold: true, color: theme.yellow, children: [
226
+ " ",
227
+ toolName
228
+ ] }),
229
+ args ? /* @__PURE__ */ jsxs3(Text3, { color: theme.dim, children: [
230
+ " ",
231
+ formatToolArgs(args)
232
+ ] }) : null
233
+ ] });
234
+ }
235
+ function ToolResultDisplay({ toolName, output, isError }) {
236
+ const icon = isError ? "\u2718" : "\u2714";
237
+ const iconColor = isError ? theme.pink : theme.green;
238
+ const lines = output.split("\n");
239
+ const preview = lines.length > 6 ? lines.slice(0, 6).join("\n") + `
240
+ ... (${lines.length - 6} more lines)` : output;
241
+ return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", marginLeft: 2, marginBottom: 1, children: [
242
+ /* @__PURE__ */ jsxs3(Box3, { children: [
243
+ /* @__PURE__ */ jsxs3(Text3, { color: iconColor, children: [
244
+ icon,
245
+ " "
246
+ ] }),
247
+ /* @__PURE__ */ jsx4(Text3, { bold: true, color: theme.yellow, children: toolName })
248
+ ] }),
249
+ /* @__PURE__ */ jsx4(Box3, { marginLeft: 2, children: /* @__PURE__ */ jsxs3(Text3, { color: theme.dim, children: [
250
+ "\u23BF ",
251
+ truncate(preview, 1200)
252
+ ] }) })
253
+ ] });
254
+ }
255
+ function formatToolArgs(args) {
256
+ const entries = Object.entries(args);
257
+ if (entries.length === 0) return "";
258
+ const parts = [];
259
+ for (const [key, val] of entries.slice(0, 2)) {
260
+ const valStr = typeof val === "string" ? truncate(val, 50) : String(val);
261
+ parts.push(`${key}: ${valStr}`);
262
+ }
263
+ const extra = entries.length > 2 ? ` +${entries.length - 2} more` : "";
264
+ return `(${parts.join(", ")}${extra})`;
265
+ }
266
+
267
+ // src/components/StatusLine.tsx
268
+ import { Box as Box4, Text as Text4 } from "ink";
269
+ import { Fragment, jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
270
+ function getContextLimit(model) {
271
+ const lower = model.toLowerCase();
272
+ if (lower.includes("llama3.1")) return 131072;
273
+ if (lower.includes("llama3.2")) return 131072;
274
+ if (lower.includes("qwen")) return 32768;
275
+ if (lower.includes("gemma")) return 8192;
276
+ if (lower.includes("phi")) return 16384;
277
+ if (lower.includes("deepseek")) return 32768;
278
+ return 8192;
279
+ }
280
+ function StatusLine({ model, messageCount, tokenEstimate, isStreaming }) {
281
+ const state = getAppState();
282
+ const contextLimit = getContextLimit(model);
283
+ const usage = Math.min(tokenEstimate / contextLimit, 1);
284
+ const pct = (usage * 100).toFixed(0);
285
+ const usageColor = usage > 0.8 ? theme.pink : usage > 0.5 ? theme.yellow : theme.cyan;
286
+ const activeTasks = state.tasks.filter((t) => t.status === "in_progress").length;
287
+ return /* @__PURE__ */ jsxs4(Box4, { marginTop: 1, children: [
288
+ /* @__PURE__ */ jsxs4(Text4, { color: theme.dim, children: [
289
+ "\u2500".repeat(2),
290
+ " "
291
+ ] }),
292
+ /* @__PURE__ */ jsx5(Text4, { color: theme.cyan, bold: true, children: model.split(":")[0] }),
293
+ /* @__PURE__ */ jsx5(Text4, { color: theme.dim, children: " \u2502 " }),
294
+ /* @__PURE__ */ jsxs4(Text4, { color: theme.dim, children: [
295
+ messageCount,
296
+ " msgs"
297
+ ] }),
298
+ /* @__PURE__ */ jsx5(Text4, { color: theme.dim, children: " \u2502 " }),
299
+ /* @__PURE__ */ jsxs4(Text4, { color: usageColor, children: [
300
+ "ctx ",
301
+ pct,
302
+ "%"
303
+ ] }),
304
+ state.planMode ? /* @__PURE__ */ jsxs4(Fragment, { children: [
305
+ /* @__PURE__ */ jsx5(Text4, { color: theme.dim, children: " \u2502 " }),
306
+ /* @__PURE__ */ jsx5(Text4, { color: theme.yellow, bold: true, children: "PLAN" })
307
+ ] }) : null,
308
+ activeTasks > 0 ? /* @__PURE__ */ jsxs4(Fragment, { children: [
309
+ /* @__PURE__ */ jsx5(Text4, { color: theme.dim, children: " \u2502 " }),
310
+ /* @__PURE__ */ jsxs4(Text4, { color: theme.green, children: [
311
+ activeTasks,
312
+ " task",
313
+ activeTasks > 1 ? "s" : ""
314
+ ] })
315
+ ] }) : null,
316
+ isStreaming ? /* @__PURE__ */ jsxs4(Fragment, { children: [
317
+ /* @__PURE__ */ jsx5(Text4, { color: theme.dim, children: " \u2502 " }),
318
+ /* @__PURE__ */ jsx5(Text4, { color: theme.cyan, children: "streaming" })
319
+ ] }) : null,
320
+ /* @__PURE__ */ jsxs4(Text4, { color: theme.dim, children: [
321
+ " ",
322
+ "\u2500".repeat(2)
323
+ ] })
324
+ ] });
325
+ }
326
+
327
+ // src/components/UserInput.tsx
328
+ import { useState, useCallback } from "react";
329
+ import { Box as Box5, Text as Text5, useInput } from "ink";
330
+ import TextInput from "ink-text-input";
331
+
332
+ // src/commands/help.ts
333
+ var helpCommand = {
334
+ name: "help",
335
+ aliases: ["h", "?"],
336
+ description: "Show available commands",
337
+ async execute(_args, _context) {
338
+ const commands = getCommands();
339
+ const lines = commands.map((cmd) => {
340
+ const aliases = cmd.aliases ? ` (${cmd.aliases.map((a) => "/" + a).join(", ")})` : "";
341
+ return ` /${cmd.name}${aliases} \u2014 ${cmd.description}`;
342
+ });
343
+ const output = [
344
+ "\x1B[1m\x1B[36mAvailable Commands\x1B[0m",
345
+ "",
346
+ ...lines,
347
+ "",
348
+ "\x1B[2mPrefix with ! to run shell commands directly (e.g. !ls -la)\x1B[0m"
349
+ ].join("\n");
350
+ return { output, silent: true };
351
+ }
352
+ };
353
+
354
+ // src/commands/clear.ts
355
+ var clearCommand = {
356
+ name: "clear",
357
+ description: "Clear conversation history",
358
+ async execute(_args, context) {
359
+ context.clearMessages();
360
+ return { output: "Conversation cleared.", replaceMessages: [], silent: true };
361
+ }
362
+ };
363
+
364
+ // src/commands/exit.ts
365
+ var exitCommand = {
366
+ name: "exit",
367
+ aliases: ["quit", "q"],
368
+ description: "Exit Darkfoo Code",
369
+ async execute(_args, context) {
370
+ context.exit();
371
+ return { output: "", exit: true, silent: true };
372
+ }
373
+ };
374
+
375
+ // src/commands/model.ts
376
+ var OLLAMA_HOST = process.env.OLLAMA_HOST || "http://localhost:11434";
377
+ var modelCommand = {
378
+ name: "model",
379
+ aliases: ["m"],
380
+ description: "Switch Ollama model (usage: /model <name>)",
381
+ async execute(args, context) {
382
+ if (!args.trim()) {
383
+ try {
384
+ const res = await fetch(`${OLLAMA_HOST}/api/tags`);
385
+ const data = await res.json();
386
+ const models = data.models;
387
+ const lines = models.map((m) => {
388
+ const active = m.name === context.model ? " \x1B[36m\u2190 active\x1B[0m" : "";
389
+ return ` ${m.name} (${m.details.parameter_size}, ${m.details.family})${active}`;
390
+ });
391
+ return {
392
+ output: [
393
+ "\x1B[1m\x1B[36mAvailable Models\x1B[0m",
394
+ "",
395
+ ...lines,
396
+ "",
397
+ "\x1B[2mUsage: /model <name> to switch\x1B[0m"
398
+ ].join("\n"),
399
+ silent: true
400
+ };
401
+ } catch {
402
+ return { output: `Error: Cannot reach Ollama at ${OLLAMA_HOST}`, silent: true };
403
+ }
404
+ }
405
+ const newModel = args.trim();
406
+ context.setModel(newModel);
407
+ return { output: `Switched to model: ${newModel}`, silent: true };
408
+ }
409
+ };
410
+
411
+ // src/commands/compact.ts
412
+ import { nanoid } from "nanoid";
413
+ var compactCommand = {
414
+ name: "compact",
415
+ description: "Compress conversation history to save context",
416
+ async execute(_args, context) {
417
+ if (context.messages.length < 4) {
418
+ return { output: "Conversation is too short to compact.", silent: true };
419
+ }
420
+ const transcript = context.messages.filter((m) => m.role === "user" || m.role === "assistant" && m.content).map((m) => `${m.role}: ${m.content}`).join("\n\n");
421
+ const summaryPrompt = `Summarize this conversation concisely. Capture all key decisions, code changes, file paths mentioned, and current task state. Be thorough but brief:
422
+
423
+ ${transcript}`;
424
+ try {
425
+ const provider = getProvider();
426
+ const result = await provider.chat({
427
+ model: context.model,
428
+ messages: [{ role: "user", content: summaryPrompt }]
429
+ });
430
+ const summaryMsg = {
431
+ id: nanoid(),
432
+ role: "user",
433
+ content: `[Previous conversation summary]
434
+ ${result.content}
435
+ [End summary \u2014 conversation continues below]`,
436
+ timestamp: Date.now()
437
+ };
438
+ const before = context.messages.length;
439
+ return {
440
+ output: `Compacted ${before} messages into summary.`,
441
+ replaceMessages: [summaryMsg],
442
+ silent: true
443
+ };
444
+ } catch (err) {
445
+ const msg = err instanceof Error ? err.message : String(err);
446
+ return { output: `Compact failed: ${msg}`, silent: true };
447
+ }
448
+ }
449
+ };
450
+
451
+ // src/commands/cost.ts
452
+ var costCommand = {
453
+ name: "cost",
454
+ aliases: ["usage", "tokens"],
455
+ description: "Show token usage for this session",
456
+ async execute(_args, context) {
457
+ const { input, output } = context.tokenCounts;
458
+ const total = input + output;
459
+ const msgCount = context.messages.length;
460
+ const lines = [
461
+ "\x1B[1m\x1B[36mSession Usage\x1B[0m",
462
+ "",
463
+ ` Messages: ${msgCount}`,
464
+ ` Input tokens: ~${input.toLocaleString()}`,
465
+ ` Output tokens: ~${output.toLocaleString()}`,
466
+ ` Total tokens: ~${total.toLocaleString()}`,
467
+ "",
468
+ "\x1B[2mToken counts are estimates based on character length.\x1B[0m"
469
+ ];
470
+ return { output: lines.join("\n"), silent: true };
471
+ }
472
+ };
473
+
474
+ // src/commands/context.ts
475
+ function estimateTokens(text) {
476
+ return Math.ceil(text.length / 4);
477
+ }
478
+ var contextCommand = {
479
+ name: "context",
480
+ aliases: ["ctx"],
481
+ description: "Show context window usage",
482
+ async execute(_args, context) {
483
+ const contextLimit = getContextLimit2(context.model);
484
+ let totalTokens = 0;
485
+ const breakdown = [];
486
+ const roles = ["system", "user", "assistant", "tool"];
487
+ for (const role of roles) {
488
+ const msgs = context.messages.filter((m) => m.role === role);
489
+ const tokens = msgs.reduce((sum, m) => sum + estimateTokens(m.content), 0);
490
+ if (msgs.length > 0) {
491
+ breakdown.push({ role, tokens, count: msgs.length });
492
+ }
493
+ totalTokens += tokens;
494
+ }
495
+ const sysTokens = 2e3;
496
+ totalTokens += sysTokens;
497
+ const usage = totalTokens / contextLimit;
498
+ const barWidth = 40;
499
+ const filled = Math.round(usage * barWidth);
500
+ const bar = "\x1B[36m" + "\u2588".repeat(Math.min(filled, barWidth)) + "\x1B[0m\x1B[2m" + "\u2591".repeat(Math.max(barWidth - filled, 0)) + "\x1B[0m";
501
+ const pct = (usage * 100).toFixed(1);
502
+ const lines = [
503
+ "\x1B[1m\x1B[36mContext Window\x1B[0m",
504
+ "",
505
+ ` [${bar}] ${pct}%`,
506
+ ` ~${totalTokens.toLocaleString()} / ${contextLimit.toLocaleString()} tokens`,
507
+ "",
508
+ " Breakdown:",
509
+ ` System prompt: ~${sysTokens.toLocaleString()} tokens`,
510
+ ...breakdown.map(
511
+ (b) => ` ${b.role.padEnd(12)} ${b.count} msgs, ~${b.tokens.toLocaleString()} tokens`
512
+ ),
513
+ "",
514
+ `\x1B[2m Model: ${context.model} (${contextLimit.toLocaleString()} ctx)\x1B[0m`
515
+ ];
516
+ if (usage > 0.8) {
517
+ lines.push("", "\x1B[33m \u26A0 Context is getting full. Consider /compact to free space.\x1B[0m");
518
+ }
519
+ return { output: lines.join("\n"), silent: true };
520
+ }
521
+ };
522
+ function getContextLimit2(model) {
523
+ const lower = model.toLowerCase();
524
+ if (lower.includes("llama3.1")) return 131072;
525
+ if (lower.includes("llama3.2")) return 131072;
526
+ if (lower.includes("qwen")) return 32768;
527
+ if (lower.includes("gemma")) return 8192;
528
+ if (lower.includes("phi")) return 16384;
529
+ if (lower.includes("deepseek")) return 32768;
530
+ return 8192;
531
+ }
532
+
533
+ // src/commands/diff.ts
534
+ import { execFile } from "child_process";
535
+ var diffCommand = {
536
+ name: "diff",
537
+ description: "Show uncommitted git changes",
538
+ async execute(_args, context) {
539
+ return new Promise((resolve) => {
540
+ execFile("git", ["diff", "--stat"], { cwd: context.cwd, timeout: 1e4 }, (err, stdout, stderr) => {
541
+ if (err) {
542
+ resolve({ output: `Not a git repository or git error: ${stderr || err.message}`, silent: true });
543
+ return;
544
+ }
545
+ if (!stdout.trim()) {
546
+ execFile("git", ["diff", "--cached", "--stat"], { cwd: context.cwd, timeout: 1e4 }, (err2, staged) => {
547
+ if (err2 || !staged.trim()) {
548
+ resolve({ output: "No uncommitted changes.", silent: true });
549
+ return;
550
+ }
551
+ resolve({ output: `\x1B[1m\x1B[36mStaged changes:\x1B[0m
552
+ ${staged}`, silent: true });
553
+ });
554
+ return;
555
+ }
556
+ execFile("git", ["diff"], { cwd: context.cwd, timeout: 1e4, maxBuffer: 1024 * 1024 }, (_err3, fullDiff) => {
557
+ const output = [
558
+ "\x1B[1m\x1B[36mUncommitted Changes\x1B[0m",
559
+ "",
560
+ stdout.trim(),
561
+ "",
562
+ fullDiff ? fullDiff.slice(0, 3e3) : "",
563
+ fullDiff && fullDiff.length > 3e3 ? "\n... (truncated)" : ""
564
+ ].join("\n");
565
+ resolve({ output, silent: true });
566
+ });
567
+ });
568
+ });
569
+ }
570
+ };
571
+
572
+ // src/session.ts
573
+ import { mkdir, readdir, readFile, writeFile } from "fs/promises";
574
+ import { join } from "path";
575
+ import { nanoid as nanoid2 } from "nanoid";
576
+ var SESSIONS_DIR = join(process.env.HOME || "~", ".darkfoo", "sessions");
577
+ async function ensureDir() {
578
+ await mkdir(SESSIONS_DIR, { recursive: true });
579
+ }
580
+ function createSessionId() {
581
+ return nanoid2(12);
582
+ }
583
+ async function saveSession(id, messages, model, cwd) {
584
+ await ensureDir();
585
+ const firstUser = messages.find((m) => m.role === "user");
586
+ const title = firstUser ? firstUser.content.slice(0, 80).replace(/\n/g, " ") : "Untitled session";
587
+ const data = {
588
+ id,
589
+ model,
590
+ cwd,
591
+ title,
592
+ createdAt: messages[0]?.timestamp ?? Date.now(),
593
+ updatedAt: Date.now(),
594
+ messageCount: messages.length,
595
+ messages
596
+ };
597
+ await writeFile(join(SESSIONS_DIR, `${id}.json`), JSON.stringify(data, null, 2), "utf-8");
598
+ }
599
+ async function loadSession(id) {
600
+ try {
601
+ const raw = await readFile(join(SESSIONS_DIR, `${id}.json`), "utf-8");
602
+ return JSON.parse(raw);
603
+ } catch {
604
+ return null;
605
+ }
606
+ }
607
+ async function listSessions() {
608
+ await ensureDir();
609
+ const files = await readdir(SESSIONS_DIR);
610
+ const sessions = [];
611
+ for (const file of files) {
612
+ if (!file.endsWith(".json")) continue;
613
+ try {
614
+ const raw = await readFile(join(SESSIONS_DIR, file), "utf-8");
615
+ const data = JSON.parse(raw);
616
+ sessions.push({
617
+ id: data.id,
618
+ model: data.model,
619
+ cwd: data.cwd,
620
+ title: data.title,
621
+ createdAt: data.createdAt,
622
+ updatedAt: data.updatedAt,
623
+ messageCount: data.messageCount
624
+ });
625
+ } catch {
626
+ }
627
+ }
628
+ return sessions.sort((a, b) => b.updatedAt - a.updatedAt);
629
+ }
630
+
631
+ // src/commands/history.ts
632
+ var historyCommand = {
633
+ name: "history",
634
+ aliases: ["sessions"],
635
+ description: "List past sessions (usage: /history, /resume <id>)",
636
+ async execute(_args, _context) {
637
+ const sessions = await listSessions();
638
+ if (sessions.length === 0) {
639
+ return { output: "No saved sessions.", silent: true };
640
+ }
641
+ const lines = sessions.slice(0, 20).map((s) => {
642
+ const date = new Date(s.updatedAt).toLocaleDateString();
643
+ const time = new Date(s.updatedAt).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
644
+ return ` \x1B[36m${s.id}\x1B[0m ${date} ${time} ${s.messageCount} msgs ${s.title}`;
645
+ });
646
+ return {
647
+ output: [
648
+ "\x1B[1m\x1B[36mRecent Sessions\x1B[0m",
649
+ "",
650
+ ...lines,
651
+ "",
652
+ "\x1B[2mUsage: /resume <id> to continue a session\x1B[0m"
653
+ ].join("\n"),
654
+ silent: true
655
+ };
656
+ }
657
+ };
658
+ var resumeCommand = {
659
+ name: "resume",
660
+ description: "Resume a saved session (usage: /resume <id>)",
661
+ async execute(args, _context) {
662
+ const id = args.trim();
663
+ if (!id) {
664
+ return { output: "Usage: /resume <session-id>", silent: true };
665
+ }
666
+ const session = await loadSession(id);
667
+ if (!session) {
668
+ return { output: `Session ${id} not found.`, silent: true };
669
+ }
670
+ return {
671
+ output: `Resumed session: ${session.title} (${session.messageCount} messages)`,
672
+ replaceMessages: session.messages,
673
+ silent: true
674
+ };
675
+ }
676
+ };
677
+
678
+ // src/commands/commit.ts
679
+ import { execFile as execFile2 } from "child_process";
680
+ var commitCommand = {
681
+ name: "commit",
682
+ description: "Stage changes and commit with an AI-generated message",
683
+ async execute(args, context) {
684
+ const diff = await gitExec(["diff", "--staged"], context.cwd);
685
+ const unstaged = await gitExec(["diff"], context.cwd);
686
+ const status = await gitExec(["status", "--short"], context.cwd);
687
+ if (!status.trim()) {
688
+ return { output: "No changes to commit.", silent: true };
689
+ }
690
+ if (!diff.trim() && unstaged.trim()) {
691
+ await gitExec(["add", "-A"], context.cwd);
692
+ }
693
+ const finalDiff = await gitExec(["diff", "--staged"], context.cwd);
694
+ if (!finalDiff.trim()) {
695
+ return { output: "No changes staged for commit.", silent: true };
696
+ }
697
+ const truncatedDiff = finalDiff.slice(0, 4e3);
698
+ let commitMsg;
699
+ if (args.trim()) {
700
+ commitMsg = args.trim();
701
+ } else {
702
+ try {
703
+ const provider = getProvider();
704
+ const result = await provider.chat({
705
+ model: context.model,
706
+ messages: [{
707
+ role: "user",
708
+ content: `Generate a short, clean git commit message (one line, under 72 chars) for this diff. No quotes, no prefix like "feat:" unless it truly fits. Just the message:
709
+
710
+ ${truncatedDiff}`
711
+ }]
712
+ });
713
+ commitMsg = result.content.trim().replace(/^["']|["']$/g, "");
714
+ } catch {
715
+ return { output: "Failed to generate commit message. Use: /commit <message>", silent: true };
716
+ }
717
+ }
718
+ try {
719
+ const output = await gitExec(["commit", "-m", commitMsg], context.cwd);
720
+ return {
721
+ output: [
722
+ `\x1B[32m\u2713\x1B[0m Committed: ${commitMsg}`,
723
+ "",
724
+ output.trim()
725
+ ].join("\n"),
726
+ silent: true
727
+ };
728
+ } catch (err) {
729
+ const msg = err instanceof Error ? err.message : String(err);
730
+ return { output: `Commit failed: ${msg}`, silent: true };
731
+ }
732
+ }
733
+ };
734
+ function gitExec(args, cwd) {
735
+ return new Promise((resolve, reject) => {
736
+ execFile2("git", args, { cwd, timeout: 15e3, maxBuffer: 1024 * 1024 }, (err, stdout, stderr) => {
737
+ if (err && !stdout) {
738
+ reject(new Error(stderr || err.message));
739
+ } else {
740
+ resolve(stdout);
741
+ }
742
+ });
743
+ });
744
+ }
745
+
746
+ // src/commands/copy.ts
747
+ import { execFile as execFile3 } from "child_process";
748
+ var copyCommand = {
749
+ name: "copy",
750
+ description: "Copy the last assistant response to clipboard",
751
+ async execute(_args, context) {
752
+ const lastAssistant = [...context.messages].reverse().find((m) => m.role === "assistant" && m.content);
753
+ if (!lastAssistant) {
754
+ return { output: "No assistant response to copy.", silent: true };
755
+ }
756
+ try {
757
+ await copyToClipboard(lastAssistant.content);
758
+ return { output: "Copied to clipboard.", silent: true };
759
+ } catch (err) {
760
+ const msg = err instanceof Error ? err.message : String(err);
761
+ return { output: `Copy failed: ${msg}`, silent: true };
762
+ }
763
+ }
764
+ };
765
+ function copyToClipboard(text) {
766
+ return new Promise((resolve, reject) => {
767
+ const tools = [
768
+ { cmd: "xclip", args: ["-selection", "clipboard"] },
769
+ { cmd: "xsel", args: ["--clipboard", "--input"] },
770
+ { cmd: "wl-copy", args: [] }
771
+ ];
772
+ let attempted = 0;
773
+ const tryNext = () => {
774
+ if (attempted >= tools.length) {
775
+ reject(new Error("No clipboard tool found (install xclip, xsel, or wl-copy)"));
776
+ return;
777
+ }
778
+ const tool = tools[attempted];
779
+ attempted++;
780
+ const proc = execFile3(tool.cmd, tool.args, { timeout: 5e3 }, (err) => {
781
+ if (err) {
782
+ tryNext();
783
+ } else {
784
+ resolve();
785
+ }
786
+ });
787
+ if (proc.stdin) {
788
+ proc.stdin.write(text);
789
+ proc.stdin.end();
790
+ }
791
+ };
792
+ tryNext();
793
+ });
794
+ }
795
+
796
+ // src/commands/rewind.ts
797
+ var rewindCommand = {
798
+ name: "rewind",
799
+ aliases: ["undo"],
800
+ description: "Remove the last conversation turn (user message + assistant response)",
801
+ async execute(_args, context) {
802
+ const messages = context.messages;
803
+ if (messages.length < 2) {
804
+ return { output: "Nothing to rewind.", silent: true };
805
+ }
806
+ let cutIdx = messages.length;
807
+ for (let i = messages.length - 1; i >= 0; i--) {
808
+ if (messages[i].role === "user") {
809
+ cutIdx = i;
810
+ break;
811
+ }
812
+ }
813
+ const removed = messages.length - cutIdx;
814
+ const kept = messages.slice(0, cutIdx);
815
+ return {
816
+ output: `Rewound ${removed} message${removed !== 1 ? "s" : ""}. Last turn removed.`,
817
+ replaceMessages: kept,
818
+ silent: true
819
+ };
820
+ }
821
+ };
822
+
823
+ // src/commands/review.ts
824
+ var reviewCommand = {
825
+ name: "review",
826
+ aliases: ["code-review", "security-review"],
827
+ description: "Request an AI code review of recent changes or a file",
828
+ async execute(args, _context) {
829
+ const target = args.trim() || "the uncommitted changes in this repository";
830
+ const isSecurityReview = false;
831
+ const prompt = isSecurityReview ? `Perform a thorough security review of ${target}. Look for: injection vulnerabilities, authentication/authorization issues, data exposure, insecure defaults, dependency risks, and OWASP Top 10 concerns. Use Read, Grep, and Bash tools as needed.` : `Perform a code review of ${target}. Look for: bugs, logic errors, performance issues, code style problems, missing error handling, and potential improvements. Use Read, Grep, and Bash (for git diff) tools as needed. Be specific and actionable.`;
832
+ return { output: prompt, silent: false };
833
+ }
834
+ };
835
+
836
+ // src/commands/config.ts
837
+ import { readFile as readFile2, writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
838
+ import { join as join2 } from "path";
839
+ var SETTINGS_PATH = join2(process.env.HOME || "~", ".darkfoo", "settings.json");
840
+ var configCommand = {
841
+ name: "config",
842
+ aliases: ["settings"],
843
+ description: "View or edit settings (usage: /config, /config set <key> <value>)",
844
+ async execute(args, _context) {
845
+ const parts = args.trim().split(/\s+/);
846
+ if (!args.trim() || parts[0] === "show") {
847
+ try {
848
+ const raw = await readFile2(SETTINGS_PATH, "utf-8");
849
+ const config = JSON.parse(raw);
850
+ return {
851
+ output: [
852
+ "\x1B[1m\x1B[36mSettings\x1B[0m",
853
+ `\x1B[2m${SETTINGS_PATH}\x1B[0m`,
854
+ "",
855
+ JSON.stringify(config, null, 2)
856
+ ].join("\n"),
857
+ silent: true
858
+ };
859
+ } catch {
860
+ return {
861
+ output: [
862
+ "\x1B[1m\x1B[36mSettings\x1B[0m",
863
+ "",
864
+ "No settings file found. Default settings in use.",
865
+ `\x1B[2mSettings will be created at: ${SETTINGS_PATH}\x1B[0m`
866
+ ].join("\n"),
867
+ silent: true
868
+ };
869
+ }
870
+ }
871
+ if (parts[0] === "set" && parts.length >= 3) {
872
+ const key = parts[1];
873
+ const value = parts.slice(2).join(" ");
874
+ let config;
875
+ try {
876
+ const raw = await readFile2(SETTINGS_PATH, "utf-8");
877
+ config = JSON.parse(raw);
878
+ } catch {
879
+ config = {};
880
+ }
881
+ let parsed = value;
882
+ if (value === "true") parsed = true;
883
+ else if (value === "false") parsed = false;
884
+ else if (!isNaN(Number(value))) parsed = Number(value);
885
+ config[key] = parsed;
886
+ await mkdir2(join2(process.env.HOME || "~", ".darkfoo"), { recursive: true });
887
+ await writeFile2(SETTINGS_PATH, JSON.stringify(config, null, 2), "utf-8");
888
+ return { output: `Set ${key} = ${JSON.stringify(parsed)}`, silent: true };
889
+ }
890
+ if (parts[0] === "delete" && parts[1]) {
891
+ const key = parts[1];
892
+ let config;
893
+ try {
894
+ const raw = await readFile2(SETTINGS_PATH, "utf-8");
895
+ config = JSON.parse(raw);
896
+ } catch {
897
+ return { output: "No settings to delete from.", silent: true };
898
+ }
899
+ delete config[key];
900
+ await writeFile2(SETTINGS_PATH, JSON.stringify(config, null, 2), "utf-8");
901
+ return { output: `Deleted ${key}`, silent: true };
902
+ }
903
+ return {
904
+ output: [
905
+ "Usage:",
906
+ " /config \u2014 Show current settings",
907
+ " /config set <k> <v> \u2014 Set a value",
908
+ " /config delete <k> \u2014 Delete a key"
909
+ ].join("\n"),
910
+ silent: true
911
+ };
912
+ }
913
+ };
914
+
915
+ // src/commands/theme.ts
916
+ var THEMES = {
917
+ darkfoo: {
918
+ cyan: "#5eead4",
919
+ pink: "#f472b6",
920
+ green: "#4ade80",
921
+ yellow: "#fbbf24",
922
+ purple: "#a78bfa",
923
+ red: "#ef4444",
924
+ text: "#e2e8f0",
925
+ dim: "#7e8ea6"
926
+ },
927
+ neon: {
928
+ cyan: "#00ffff",
929
+ pink: "#ff00ff",
930
+ green: "#00ff00",
931
+ yellow: "#ffff00",
932
+ purple: "#8000ff",
933
+ red: "#ff0000",
934
+ text: "#ffffff",
935
+ dim: "#808080"
936
+ },
937
+ mono: {
938
+ cyan: "#aaaaaa",
939
+ pink: "#cccccc",
940
+ green: "#bbbbbb",
941
+ yellow: "#dddddd",
942
+ purple: "#999999",
943
+ red: "#eeeeee",
944
+ text: "#ffffff",
945
+ dim: "#666666"
946
+ },
947
+ ocean: {
948
+ cyan: "#64b5f6",
949
+ pink: "#f48fb1",
950
+ green: "#81c784",
951
+ yellow: "#fff176",
952
+ purple: "#ce93d8",
953
+ red: "#e57373",
954
+ text: "#e0e0e0",
955
+ dim: "#78909c"
956
+ }
957
+ };
958
+ var themeCommand = {
959
+ name: "theme",
960
+ description: "Switch color theme (usage: /theme <name>)",
961
+ async execute(args, _context) {
962
+ if (!args.trim()) {
963
+ const names = Object.keys(THEMES);
964
+ const lines = names.map((name2) => {
965
+ const t = THEMES[name2];
966
+ const swatch = `\x1B[38;2;${hexToRgb(t.cyan)}m\u25CF\x1B[0m\x1B[38;2;${hexToRgb(t.pink)}m\u25CF\x1B[0m\x1B[38;2;${hexToRgb(t.green)}m\u25CF\x1B[0m\x1B[38;2;${hexToRgb(t.yellow)}m\u25CF\x1B[0m\x1B[38;2;${hexToRgb(t.purple)}m\u25CF\x1B[0m`;
967
+ return ` ${name2.padEnd(10)} ${swatch}`;
968
+ });
969
+ return {
970
+ output: ["\x1B[1m\x1B[36mAvailable Themes\x1B[0m", "", ...lines, "", "\x1B[2mUsage: /theme <name>\x1B[0m"].join("\n"),
971
+ silent: true
972
+ };
973
+ }
974
+ const name = args.trim().toLowerCase();
975
+ if (!THEMES[name]) {
976
+ return { output: `Unknown theme: ${name}. Available: ${Object.keys(THEMES).join(", ")}`, silent: true };
977
+ }
978
+ const { theme: theme2 } = await import("./theme-BQAEFGVB.js");
979
+ const newTheme = THEMES[name];
980
+ Object.assign(theme2, newTheme);
981
+ return { output: `Theme switched to: ${name}`, silent: true };
982
+ }
983
+ };
984
+ function hexToRgb(hex) {
985
+ const r = parseInt(hex.slice(1, 3), 16);
986
+ const g = parseInt(hex.slice(3, 5), 16);
987
+ const b = parseInt(hex.slice(5, 7), 16);
988
+ return `${r};${g};${b}`;
989
+ }
990
+
991
+ // src/commands/export.ts
992
+ import { writeFile as writeFile3, mkdir as mkdir3 } from "fs/promises";
993
+ import { join as join3 } from "path";
994
+ var exportCommand = {
995
+ name: "export",
996
+ description: "Export conversation to markdown file (usage: /export [filename])",
997
+ async execute(args, context) {
998
+ if (context.messages.length === 0) {
999
+ return { output: "No conversation to export.", silent: true };
1000
+ }
1001
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
1002
+ const filename = args.trim() || `darkfoo-session-${timestamp}.md`;
1003
+ const exportDir = join3(process.env.HOME || "~", ".darkfoo", "exports");
1004
+ await mkdir3(exportDir, { recursive: true });
1005
+ const filePath = join3(exportDir, filename);
1006
+ const lines = [
1007
+ `# Darkfoo Code Session`,
1008
+ ``,
1009
+ `**Model:** ${context.model}`,
1010
+ `**Working Directory:** ${context.cwd}`,
1011
+ `**Date:** ${(/* @__PURE__ */ new Date()).toISOString()}`,
1012
+ `**Messages:** ${context.messages.length}`,
1013
+ ``,
1014
+ `---`,
1015
+ ``
1016
+ ];
1017
+ for (const msg of context.messages) {
1018
+ switch (msg.role) {
1019
+ case "user":
1020
+ lines.push(`## User`, ``, msg.content, ``);
1021
+ break;
1022
+ case "assistant":
1023
+ if (msg.content) {
1024
+ lines.push(`## Assistant`, ``, msg.content, ``);
1025
+ }
1026
+ if (msg.toolCalls) {
1027
+ for (const tc of msg.toolCalls) {
1028
+ lines.push(`> **Tool Call:** \`${tc.function.name}\``, `> \`\`\`json`, `> ${JSON.stringify(tc.function.arguments)}`, `> \`\`\``, ``);
1029
+ }
1030
+ }
1031
+ break;
1032
+ case "tool":
1033
+ lines.push(`> **[${msg.toolName}]** result:`, `> \`\`\``, `> ${msg.content.slice(0, 500)}`, `> \`\`\``, ``);
1034
+ break;
1035
+ }
1036
+ }
1037
+ await writeFile3(filePath, lines.join("\n"), "utf-8");
1038
+ return { output: `Exported to ${filePath}`, silent: true };
1039
+ }
1040
+ };
1041
+
1042
+ // src/commands/status.ts
1043
+ var statusCommand = {
1044
+ name: "status",
1045
+ description: "Show session status overview",
1046
+ async execute(_args, context) {
1047
+ const state = getAppState();
1048
+ const userMsgs = context.messages.filter((m) => m.role === "user").length;
1049
+ const assistantMsgs = context.messages.filter((m) => m.role === "assistant").length;
1050
+ const toolMsgs = context.messages.filter((m) => m.role === "tool").length;
1051
+ const totalChars = context.messages.reduce((sum, m) => sum + m.content.length, 0);
1052
+ const estTokens = Math.ceil(totalChars / 4);
1053
+ const tasks = state.tasks;
1054
+ const pending = tasks.filter((t) => t.status === "pending").length;
1055
+ const inProgress = tasks.filter((t) => t.status === "in_progress").length;
1056
+ const completed = tasks.filter((t) => t.status === "completed").length;
1057
+ const lines = [
1058
+ "\x1B[1m\x1B[36mSession Status\x1B[0m",
1059
+ "",
1060
+ ` Model: \x1B[36m${context.model}\x1B[0m`,
1061
+ ` Working Dir: ${context.cwd}`,
1062
+ ` Plan Mode: ${state.planMode ? "\x1B[33mActive\x1B[0m" : "Off"}`,
1063
+ "",
1064
+ " \x1B[1mMessages\x1B[0m",
1065
+ ` User: ${userMsgs}`,
1066
+ ` Assistant: ${assistantMsgs}`,
1067
+ ` Tool results: ${toolMsgs}`,
1068
+ ` Est. tokens: ~${estTokens.toLocaleString()}`,
1069
+ "",
1070
+ " \x1B[1mTasks\x1B[0m",
1071
+ ` Pending: ${pending}`,
1072
+ ` In Progress: ${inProgress}`,
1073
+ ` Completed: ${completed}`,
1074
+ "",
1075
+ ` Files tracked: ${trackedFileCount()}`
1076
+ ];
1077
+ return { output: lines.join("\n"), silent: true };
1078
+ }
1079
+ };
1080
+
1081
+ // src/commands/files.ts
1082
+ var filesCommand = {
1083
+ name: "files",
1084
+ description: "List files modified or referenced in this session",
1085
+ async execute(_args, context) {
1086
+ const readFiles = /* @__PURE__ */ new Set();
1087
+ const writtenFiles = /* @__PURE__ */ new Set();
1088
+ const editedFiles = /* @__PURE__ */ new Set();
1089
+ for (const msg of context.messages) {
1090
+ if (msg.role === "assistant" && msg.toolCalls) {
1091
+ for (const tc of msg.toolCalls) {
1092
+ const args = tc.function.arguments;
1093
+ const path = typeof args.file_path === "string" ? args.file_path : null;
1094
+ if (!path) continue;
1095
+ switch (tc.function.name) {
1096
+ case "Read":
1097
+ readFiles.add(path);
1098
+ break;
1099
+ case "Write":
1100
+ writtenFiles.add(path);
1101
+ break;
1102
+ case "Edit":
1103
+ editedFiles.add(path);
1104
+ break;
1105
+ }
1106
+ }
1107
+ }
1108
+ }
1109
+ if (readFiles.size === 0 && writtenFiles.size === 0 && editedFiles.size === 0) {
1110
+ return { output: "No files referenced in this session.", silent: true };
1111
+ }
1112
+ const lines = ["\x1B[1m\x1B[36mSession Files\x1B[0m", ""];
1113
+ if (readFiles.size > 0) {
1114
+ lines.push(" \x1B[1mRead:\x1B[0m");
1115
+ for (const f of readFiles) lines.push(` \x1B[36m${f}\x1B[0m`);
1116
+ lines.push("");
1117
+ }
1118
+ if (writtenFiles.size > 0) {
1119
+ lines.push(" \x1B[1mWritten:\x1B[0m");
1120
+ for (const f of writtenFiles) lines.push(` \x1B[32m${f}\x1B[0m`);
1121
+ lines.push("");
1122
+ }
1123
+ if (editedFiles.size > 0) {
1124
+ lines.push(" \x1B[1mEdited:\x1B[0m");
1125
+ for (const f of editedFiles) lines.push(` \x1B[33m${f}\x1B[0m`);
1126
+ lines.push("");
1127
+ }
1128
+ return { output: lines.join("\n"), silent: true };
1129
+ }
1130
+ };
1131
+
1132
+ // src/commands/brief.ts
1133
+ var briefCommand = {
1134
+ name: "brief",
1135
+ aliases: ["summary"],
1136
+ description: "Show a quick summary of the conversation so far",
1137
+ async execute(_args, context) {
1138
+ if (context.messages.length === 0) {
1139
+ return { output: "No conversation yet.", silent: true };
1140
+ }
1141
+ const userMsgs = context.messages.filter((m) => m.role === "user");
1142
+ const toolCalls = context.messages.filter((m) => m.role === "assistant" && m.toolCalls).flatMap((m) => m.toolCalls ?? []);
1143
+ const toolCounts = /* @__PURE__ */ new Map();
1144
+ for (const tc of toolCalls) {
1145
+ const name = tc.function.name;
1146
+ toolCounts.set(name, (toolCounts.get(name) || 0) + 1);
1147
+ }
1148
+ const lines = [
1149
+ "\x1B[1m\x1B[36mSession Brief\x1B[0m",
1150
+ "",
1151
+ ` Turns: ${userMsgs.length}`
1152
+ ];
1153
+ if (userMsgs.length > 0) {
1154
+ lines.push("", " \x1B[1mRecent prompts:\x1B[0m");
1155
+ const recent = userMsgs.slice(-5);
1156
+ for (const msg of recent) {
1157
+ const preview = msg.content.length > 70 ? msg.content.slice(0, 70) + "..." : msg.content;
1158
+ lines.push(` \x1B[2m\u2022\x1B[0m ${preview}`);
1159
+ }
1160
+ }
1161
+ if (toolCounts.size > 0) {
1162
+ lines.push("", " \x1B[1mTools used:\x1B[0m");
1163
+ const sorted = [...toolCounts.entries()].sort((a, b) => b[1] - a[1]);
1164
+ for (const [name, count] of sorted) {
1165
+ lines.push(` ${name}: ${count}x`);
1166
+ }
1167
+ }
1168
+ return { output: lines.join("\n"), silent: true };
1169
+ }
1170
+ };
1171
+
1172
+ // src/commands/provider.ts
1173
+ var providerCommand = {
1174
+ name: "provider",
1175
+ aliases: ["backend"],
1176
+ description: "List, switch, or add LLM providers (usage: /provider [name], /provider add <name> <url>)",
1177
+ async execute(args, _context) {
1178
+ const parts = args.trim().split(/\s+/);
1179
+ const subcommand = parts[0]?.toLowerCase();
1180
+ if (!subcommand) {
1181
+ const results = await discoverProviders();
1182
+ const active = getActiveProviderName();
1183
+ const lines = [
1184
+ "\x1B[1m\x1B[36mLLM Providers\x1B[0m",
1185
+ ""
1186
+ ];
1187
+ for (const { config, online } of results) {
1188
+ const status = online ? "\x1B[32m\u25CF online\x1B[0m" : "\x1B[31m\u25CF offline\x1B[0m";
1189
+ const isActive = config.name === active ? " \x1B[36m\u2190 active\x1B[0m" : "";
1190
+ lines.push(` ${config.name.padEnd(14)} ${config.label.padEnd(14)} ${config.baseUrl.padEnd(30)} ${status}${isActive}`);
1191
+ }
1192
+ lines.push(
1193
+ "",
1194
+ "\x1B[2mUsage:\x1B[0m",
1195
+ " /provider <name> \u2014 Switch active provider",
1196
+ " /provider add <name> <url> [label] \u2014 Add a custom provider",
1197
+ " /provider remove <name> \u2014 Remove a custom provider",
1198
+ " /provider models \u2014 List models from active provider"
1199
+ );
1200
+ return { output: lines.join("\n"), silent: true };
1201
+ }
1202
+ if (subcommand !== "add" && subcommand !== "remove" && subcommand !== "models") {
1203
+ try {
1204
+ setActiveProvider(subcommand);
1205
+ await saveProviderSettings();
1206
+ const configs = getProviderConfigs();
1207
+ const config = configs.find((c) => c.name === subcommand);
1208
+ return { output: `Switched to provider: ${config?.label || subcommand} (${config?.baseUrl})`, silent: true };
1209
+ } catch (err) {
1210
+ const msg = err instanceof Error ? err.message : String(err);
1211
+ return { output: msg, silent: true };
1212
+ }
1213
+ }
1214
+ if (subcommand === "add") {
1215
+ const name = parts[1];
1216
+ const url = parts[2];
1217
+ const label = parts.slice(3).join(" ") || name;
1218
+ if (!name || !url) {
1219
+ return { output: "Usage: /provider add <name> <url> [label]", silent: true };
1220
+ }
1221
+ const config = { type: "openai", name, label: label || name, baseUrl: url };
1222
+ upsertProviderConfig(config);
1223
+ await saveProviderSettings();
1224
+ return { output: `Added provider: ${name} \u2192 ${url}`, silent: true };
1225
+ }
1226
+ if (subcommand === "remove") {
1227
+ const name = parts[1];
1228
+ if (!name) return { output: "Usage: /provider remove <name>", silent: true };
1229
+ const { removeProviderConfig } = await import("./providers-P7Z3JXQH.js");
1230
+ if (removeProviderConfig(name)) {
1231
+ await saveProviderSettings();
1232
+ return { output: `Removed provider: ${name}`, silent: true };
1233
+ }
1234
+ return { output: `Provider not found: ${name}`, silent: true };
1235
+ }
1236
+ if (subcommand === "models") {
1237
+ const { getProvider: getProvider2 } = await import("./providers-P7Z3JXQH.js");
1238
+ const provider = getProvider2();
1239
+ const models = await provider.listModels();
1240
+ if (models.length === 0) {
1241
+ return { output: `No models found from ${provider.label}.`, silent: true };
1242
+ }
1243
+ const lines = [
1244
+ `\x1B[1m\x1B[36mModels from ${provider.label}\x1B[0m`,
1245
+ "",
1246
+ ...models.map((m) => {
1247
+ const extra = [m.size, m.family].filter(Boolean).join(", ");
1248
+ return ` ${m.name}${extra ? ` (${extra})` : ""}`;
1249
+ })
1250
+ ];
1251
+ return { output: lines.join("\n"), silent: true };
1252
+ }
1253
+ return { output: "Unknown subcommand. See /provider for usage.", silent: true };
1254
+ }
1255
+ };
1256
+
1257
+ // src/commands/index.ts
1258
+ var COMMANDS = [
1259
+ helpCommand,
1260
+ clearCommand,
1261
+ exitCommand,
1262
+ modelCommand,
1263
+ compactCommand,
1264
+ costCommand,
1265
+ contextCommand,
1266
+ diffCommand,
1267
+ historyCommand,
1268
+ resumeCommand,
1269
+ commitCommand,
1270
+ copyCommand,
1271
+ rewindCommand,
1272
+ reviewCommand,
1273
+ configCommand,
1274
+ themeCommand,
1275
+ exportCommand,
1276
+ statusCommand,
1277
+ filesCommand,
1278
+ briefCommand,
1279
+ providerCommand
1280
+ ];
1281
+ function getCommands() {
1282
+ return COMMANDS;
1283
+ }
1284
+ async function dispatchCommand(input, context) {
1285
+ if (!input.startsWith("/")) return null;
1286
+ const [rawName, ...rest] = input.slice(1).split(/\s+/);
1287
+ const name = rawName?.toLowerCase();
1288
+ if (!name) return null;
1289
+ const args = rest.join(" ");
1290
+ const cmd = COMMANDS.find(
1291
+ (c) => c.name === name || c.aliases?.includes(name)
1292
+ );
1293
+ if (!cmd) {
1294
+ return {
1295
+ output: `Unknown command: /${name}. Type /help for available commands.`,
1296
+ silent: true
1297
+ };
1298
+ }
1299
+ return cmd.execute(args, context);
1300
+ }
1301
+ function getCommandNames() {
1302
+ const names = [];
1303
+ for (const cmd of COMMANDS) {
1304
+ names.push("/" + cmd.name);
1305
+ if (cmd.aliases) {
1306
+ for (const alias of cmd.aliases) {
1307
+ names.push("/" + alias);
1308
+ }
1309
+ }
1310
+ }
1311
+ return names.sort();
1312
+ }
1313
+
1314
+ // src/components/UserInput.tsx
1315
+ import { Fragment as Fragment2, jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
1316
+ function UserInput({ value, onChange, onSubmit, disabled, history }) {
1317
+ const [historyIdx, setHistoryIdx] = useState(-1);
1318
+ const [suggestion, setSuggestion] = useState("");
1319
+ useInput((_input, key) => {
1320
+ if (disabled || !history || history.length === 0) return;
1321
+ if (key.upArrow) {
1322
+ const nextIdx = Math.min(historyIdx + 1, history.length - 1);
1323
+ setHistoryIdx(nextIdx);
1324
+ onChange(history[nextIdx]);
1325
+ } else if (key.downArrow) {
1326
+ if (historyIdx <= 0) {
1327
+ setHistoryIdx(-1);
1328
+ onChange("");
1329
+ } else {
1330
+ const nextIdx = historyIdx - 1;
1331
+ setHistoryIdx(nextIdx);
1332
+ onChange(history[nextIdx]);
1333
+ }
1334
+ }
1335
+ });
1336
+ const handleChange = useCallback(
1337
+ (val) => {
1338
+ onChange(val);
1339
+ setHistoryIdx(-1);
1340
+ if (val.startsWith("/") && val.length > 1 && !val.includes(" ")) {
1341
+ const matches = getCommandNames().filter((n) => n.startsWith(val));
1342
+ if (matches.length === 1 && matches[0] !== val) {
1343
+ setSuggestion(matches[0].slice(val.length));
1344
+ } else {
1345
+ setSuggestion("");
1346
+ }
1347
+ } else {
1348
+ setSuggestion("");
1349
+ }
1350
+ },
1351
+ [onChange]
1352
+ );
1353
+ useInput((input, key) => {
1354
+ if (key.tab && suggestion) {
1355
+ onChange(value + suggestion);
1356
+ setSuggestion("");
1357
+ }
1358
+ });
1359
+ const isBash = value.startsWith("!");
1360
+ const isCommand = value.startsWith("/");
1361
+ const borderColor = disabled ? theme.dim : isBash ? theme.yellow : isCommand ? theme.purple : theme.cyan;
1362
+ const promptChar = isBash ? "!" : "\u276F";
1363
+ const promptColor = isBash ? theme.yellow : theme.cyan;
1364
+ return /* @__PURE__ */ jsxs5(
1365
+ Box5,
1366
+ {
1367
+ borderStyle: "round",
1368
+ borderColor,
1369
+ paddingLeft: 1,
1370
+ paddingRight: 1,
1371
+ children: [
1372
+ /* @__PURE__ */ jsxs5(Text5, { color: disabled ? theme.dim : promptColor, bold: true, children: [
1373
+ promptChar,
1374
+ " "
1375
+ ] }),
1376
+ disabled ? /* @__PURE__ */ jsx6(Text5, { color: theme.dim, children: "..." }) : /* @__PURE__ */ jsxs5(Fragment2, { children: [
1377
+ /* @__PURE__ */ jsx6(
1378
+ TextInput,
1379
+ {
1380
+ value,
1381
+ onChange: handleChange,
1382
+ onSubmit: (val) => {
1383
+ if (val.trim()) {
1384
+ setSuggestion("");
1385
+ setHistoryIdx(-1);
1386
+ onSubmit(val.trim());
1387
+ }
1388
+ }
1389
+ }
1390
+ ),
1391
+ suggestion ? /* @__PURE__ */ jsx6(Text5, { color: theme.dim, children: suggestion }) : null
1392
+ ] })
1393
+ ]
1394
+ }
1395
+ );
1396
+ }
1397
+
1398
+ // src/repl.tsx
1399
+ import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
1400
+ function getContextLimit3(model) {
1401
+ const lower = model.toLowerCase();
1402
+ if (lower.includes("llama3.1")) return 131072;
1403
+ if (lower.includes("llama3.2")) return 131072;
1404
+ if (lower.includes("qwen")) return 32768;
1405
+ if (lower.includes("gemma")) return 8192;
1406
+ if (lower.includes("phi")) return 16384;
1407
+ if (lower.includes("deepseek")) return 32768;
1408
+ return 8192;
1409
+ }
1410
+ function REPL({ initialPrompt }) {
1411
+ const { model: initialModel, systemPromptOverride } = useDarkfooContext();
1412
+ const { exit } = useApp();
1413
+ const [model, setModel] = useState2(initialModel);
1414
+ const [messages, setMessages] = useState2([]);
1415
+ const [inputValue, setInputValue] = useState2("");
1416
+ const [isStreaming, setIsStreaming] = useState2(false);
1417
+ const [streamingText, setStreamingText] = useState2("");
1418
+ const [activeTool, setActiveTool] = useState2(null);
1419
+ const [toolResults, setToolResults] = useState2([]);
1420
+ const [commandOutput, setCommandOutput] = useState2(null);
1421
+ const [inputHistory, setInputHistory] = useState2([]);
1422
+ const [tokenCounts, setTokenCounts] = useState2({ input: 0, output: 0 });
1423
+ const [systemPrompt, setSystemPrompt] = useState2("");
1424
+ const abortRef = useRef(null);
1425
+ const hasRun = useRef(false);
1426
+ const sessionId = useRef(createSessionId());
1427
+ const tools = getTools();
1428
+ const cwd = process.cwd();
1429
+ useEffect(() => {
1430
+ if (systemPromptOverride) {
1431
+ setSystemPrompt(systemPromptOverride);
1432
+ } else {
1433
+ buildSystemPrompt(tools, cwd).then(setSystemPrompt);
1434
+ }
1435
+ }, []);
1436
+ const commandContext = {
1437
+ messages,
1438
+ model,
1439
+ cwd,
1440
+ setModel,
1441
+ clearMessages: () => setMessages([]),
1442
+ exit,
1443
+ tokenCounts
1444
+ };
1445
+ const addToHistory = useCallback2((input) => {
1446
+ setInputHistory((prev) => {
1447
+ const filtered = prev.filter((h) => h !== input);
1448
+ return [input, ...filtered].slice(0, 100);
1449
+ });
1450
+ }, []);
1451
+ const runQuery = useCallback2(
1452
+ async (userMessage) => {
1453
+ const userMsg = {
1454
+ id: nanoid3(),
1455
+ role: "user",
1456
+ content: userMessage,
1457
+ timestamp: Date.now()
1458
+ };
1459
+ setMessages((prev) => [...prev, userMsg]);
1460
+ setIsStreaming(true);
1461
+ setStreamingText("");
1462
+ setToolResults([]);
1463
+ setCommandOutput(null);
1464
+ const controller = new AbortController();
1465
+ abortRef.current = controller;
1466
+ const allMessages = [...messages, userMsg];
1467
+ setTokenCounts((prev) => ({
1468
+ ...prev,
1469
+ input: prev.input + Math.ceil(userMessage.length / 4)
1470
+ }));
1471
+ try {
1472
+ for await (const event of query({
1473
+ model,
1474
+ messages: allMessages,
1475
+ tools,
1476
+ systemPrompt,
1477
+ signal: controller.signal
1478
+ })) {
1479
+ if (controller.signal.aborted) break;
1480
+ switch (event.type) {
1481
+ case "text_delta":
1482
+ setStreamingText((prev) => prev + event.text);
1483
+ break;
1484
+ case "tool_call":
1485
+ setActiveTool({ name: event.toolCall.function.name, args: event.toolCall.function.arguments });
1486
+ break;
1487
+ case "tool_result":
1488
+ setActiveTool(null);
1489
+ setToolResults((prev) => [
1490
+ ...prev,
1491
+ { id: nanoid3(), toolName: event.toolName, output: event.output, isError: event.isError }
1492
+ ]);
1493
+ break;
1494
+ case "assistant_message":
1495
+ setMessages((prev) => {
1496
+ const updated = [...prev, event.message];
1497
+ saveSession(sessionId.current, updated, model, cwd).catch(() => {
1498
+ });
1499
+ return updated;
1500
+ });
1501
+ setTokenCounts((prev) => ({
1502
+ ...prev,
1503
+ output: prev.output + Math.ceil((event.message.content?.length ?? 0) / 4)
1504
+ }));
1505
+ setStreamingText("");
1506
+ break;
1507
+ case "error":
1508
+ setMessages((prev) => [
1509
+ ...prev,
1510
+ { id: nanoid3(), role: "assistant", content: `Error: ${event.error}`, timestamp: Date.now() }
1511
+ ]);
1512
+ break;
1513
+ }
1514
+ }
1515
+ } catch (err) {
1516
+ if (err.name !== "AbortError") {
1517
+ const msg = err instanceof Error ? err.message : String(err);
1518
+ setMessages((prev) => [
1519
+ ...prev,
1520
+ { id: nanoid3(), role: "assistant", content: `Error: ${msg}`, timestamp: Date.now() }
1521
+ ]);
1522
+ }
1523
+ } finally {
1524
+ setIsStreaming(false);
1525
+ setStreamingText("");
1526
+ setActiveTool(null);
1527
+ setToolResults([]);
1528
+ abortRef.current = null;
1529
+ }
1530
+ },
1531
+ [model, messages, tools, systemPrompt]
1532
+ );
1533
+ const handleSubmit = useCallback2(
1534
+ async (value) => {
1535
+ setInputValue("");
1536
+ addToHistory(value);
1537
+ setCommandOutput(null);
1538
+ const lower = value.toLowerCase().trim();
1539
+ if (lower === "exit" || lower === "quit" || lower === "q") {
1540
+ exit();
1541
+ return;
1542
+ }
1543
+ if (value.startsWith("/")) {
1544
+ const result = await dispatchCommand(value, commandContext);
1545
+ if (result) {
1546
+ if (result.replaceMessages) {
1547
+ setMessages(result.replaceMessages);
1548
+ }
1549
+ if (result.exit) return;
1550
+ if (!result.silent && result.output) {
1551
+ runQuery(result.output);
1552
+ return;
1553
+ }
1554
+ if (result.output) {
1555
+ setCommandOutput(result.output);
1556
+ }
1557
+ }
1558
+ return;
1559
+ }
1560
+ if (value.startsWith("!")) {
1561
+ const cmd = value.slice(1).trim();
1562
+ if (!cmd) return;
1563
+ setIsStreaming(true);
1564
+ setCommandOutput(null);
1565
+ try {
1566
+ const result = await BashTool.call(
1567
+ { command: cmd },
1568
+ { cwd, abortSignal: new AbortController().signal }
1569
+ );
1570
+ setCommandOutput(result.output);
1571
+ } catch (err) {
1572
+ const msg = err instanceof Error ? err.message : String(err);
1573
+ setCommandOutput(`Error: ${msg}`);
1574
+ } finally {
1575
+ setIsStreaming(false);
1576
+ }
1577
+ return;
1578
+ }
1579
+ runQuery(value);
1580
+ },
1581
+ [addToHistory, commandContext, cwd, runQuery]
1582
+ );
1583
+ useEffect(() => {
1584
+ if (initialPrompt && !hasRun.current) {
1585
+ hasRun.current = true;
1586
+ runQuery(initialPrompt);
1587
+ }
1588
+ }, [initialPrompt, runQuery]);
1589
+ const autoCompactRef = useRef(false);
1590
+ useEffect(() => {
1591
+ if (isStreaming || autoCompactRef.current || messages.length < 6) return;
1592
+ const totalChars = messages.reduce((sum, m) => sum + m.content.length, 0);
1593
+ const estTokens = Math.ceil(totalChars / 4) + 2e3;
1594
+ const limit = getContextLimit3(model);
1595
+ const usage = estTokens / limit;
1596
+ if (usage > 0.85) {
1597
+ autoCompactRef.current = true;
1598
+ setCommandOutput("\x1B[33m\u26A0 Context 85%+ full \u2014 auto-compacting...\x1B[0m");
1599
+ const transcript = messages.filter((m) => m.role === "user" || m.role === "assistant" && m.content).map((m) => `${m.role}: ${m.content}`).join("\n\n");
1600
+ getProvider().chat({
1601
+ model,
1602
+ messages: [{ role: "user", content: `Summarize concisely. Capture key decisions, code changes, file paths, and current task state:
1603
+
1604
+ ${transcript}` }]
1605
+ }).then((result) => {
1606
+ const summaryMsg = {
1607
+ id: nanoid3(),
1608
+ role: "user",
1609
+ content: `[Auto-compacted summary]
1610
+ ${result.content}
1611
+ [End summary]`,
1612
+ timestamp: Date.now()
1613
+ };
1614
+ setMessages([summaryMsg]);
1615
+ setTokenCounts({ input: Math.ceil(result.content.length / 4), output: 0 });
1616
+ setCommandOutput("\x1B[32m\u2713 Auto-compacted conversation.\x1B[0m");
1617
+ autoCompactRef.current = false;
1618
+ }).catch(() => {
1619
+ setCommandOutput("\x1B[31m\u2717 Auto-compact failed.\x1B[0m");
1620
+ autoCompactRef.current = false;
1621
+ });
1622
+ }
1623
+ }, [messages, model, isStreaming]);
1624
+ useInput2((input, key) => {
1625
+ if (key.ctrl && input === "c") {
1626
+ if (isStreaming && abortRef.current) {
1627
+ abortRef.current.abort();
1628
+ } else {
1629
+ exit();
1630
+ }
1631
+ }
1632
+ });
1633
+ return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", padding: 1, children: [
1634
+ /* @__PURE__ */ jsx7(Banner, { model, cwd }),
1635
+ /* @__PURE__ */ jsx7(Messages, { messages }),
1636
+ commandOutput ? /* @__PURE__ */ jsx7(Box6, { marginBottom: 1, marginLeft: 2, flexDirection: "column", children: /* @__PURE__ */ jsx7(Text6, { children: commandOutput }) }) : null,
1637
+ toolResults.map((tr) => /* @__PURE__ */ jsx7(ToolResultDisplay, { toolName: tr.toolName, output: tr.output, isError: tr.isError }, tr.id)),
1638
+ activeTool ? /* @__PURE__ */ jsx7(ActiveToolCall, { toolName: activeTool.name, args: activeTool.args }) : null,
1639
+ isStreaming && streamingText ? /* @__PURE__ */ jsxs6(Box6, { marginBottom: 1, children: [
1640
+ /* @__PURE__ */ jsx7(Text6, { color: theme.cyan, children: "\u23BF " }),
1641
+ /* @__PURE__ */ jsx7(Text6, { color: theme.text, wrap: "wrap", children: streamingText }),
1642
+ /* @__PURE__ */ jsxs6(Text6, { color: theme.cyan, children: [
1643
+ " ",
1644
+ /* @__PURE__ */ jsx7(Spinner2, { type: "dots" })
1645
+ ] })
1646
+ ] }) : null,
1647
+ isStreaming && !streamingText && !activeTool ? /* @__PURE__ */ jsxs6(Box6, { marginLeft: 2, children: [
1648
+ /* @__PURE__ */ jsx7(Text6, { color: theme.cyan, children: /* @__PURE__ */ jsx7(Spinner2, { type: "dots" }) }),
1649
+ /* @__PURE__ */ jsx7(Text6, { color: theme.dim, children: " Thinking..." })
1650
+ ] }) : null,
1651
+ /* @__PURE__ */ jsx7(
1652
+ UserInput,
1653
+ {
1654
+ value: inputValue,
1655
+ onChange: setInputValue,
1656
+ onSubmit: handleSubmit,
1657
+ disabled: isStreaming,
1658
+ history: inputHistory
1659
+ }
1660
+ ),
1661
+ /* @__PURE__ */ jsx7(
1662
+ StatusLine,
1663
+ {
1664
+ model,
1665
+ messageCount: messages.length,
1666
+ tokenEstimate: tokenCounts.input + tokenCounts.output,
1667
+ isStreaming
1668
+ }
1669
+ )
1670
+ ] });
1671
+ }
1672
+
1673
+ // src/main.tsx
1674
+ import { jsx as jsx8 } from "react/jsx-runtime";
1675
+ var program = new Command();
1676
+ program.name("darkfoo").description("Darkfoo Code \u2014 local AI coding assistant powered by local LLM providers").version("0.1.0").option("-m, --model <model>", "Model to use", "llama3.1:8b").option("-p, --prompt <prompt>", "Run a single prompt (non-interactive)").option("--provider <name>", "LLM provider backend (ollama, llama-cpp, vllm, tgi, etc.)").option("--system-prompt <prompt>", "Override the system prompt").action(async (options) => {
1677
+ const { model, prompt, provider, systemPrompt } = options;
1678
+ await loadProviderSettings();
1679
+ if (provider) {
1680
+ try {
1681
+ setActiveProvider(provider);
1682
+ } catch (err) {
1683
+ const msg = err instanceof Error ? err.message : String(err);
1684
+ process.stderr.write(`Error: ${msg}
1685
+ `);
1686
+ process.exit(1);
1687
+ }
1688
+ }
1689
+ if (prompt) {
1690
+ const { buildSystemPrompt: buildSystemPrompt2 } = await import("./system-prompt-YJSDZVOM.js");
1691
+ const { getTools: getTools2 } = await import("./tools-34S775OZ.js");
1692
+ const { query: query2 } = await import("./query-E6NPBSUX.js");
1693
+ const tools = getTools2();
1694
+ const sysPrompt = systemPrompt ?? await buildSystemPrompt2(tools, process.cwd());
1695
+ const { nanoid: nanoid4 } = await import("nanoid");
1696
+ const userMsg = {
1697
+ id: nanoid4(),
1698
+ role: "user",
1699
+ content: prompt,
1700
+ timestamp: Date.now()
1701
+ };
1702
+ const controller = new AbortController();
1703
+ process.on("SIGINT", () => {
1704
+ controller.abort();
1705
+ process.exit(0);
1706
+ });
1707
+ for await (const event of query2({
1708
+ model,
1709
+ messages: [userMsg],
1710
+ tools,
1711
+ systemPrompt: sysPrompt,
1712
+ signal: controller.signal
1713
+ })) {
1714
+ switch (event.type) {
1715
+ case "text_delta":
1716
+ process.stdout.write(event.text);
1717
+ break;
1718
+ case "tool_result":
1719
+ if (event.isError) {
1720
+ process.stderr.write(`
1721
+ [${event.toolName}] Error: ${event.output}
1722
+ `);
1723
+ }
1724
+ break;
1725
+ case "error":
1726
+ process.stderr.write(`
1727
+ Error: ${event.error}
1728
+ `);
1729
+ break;
1730
+ }
1731
+ }
1732
+ process.stdout.write("\n");
1733
+ process.exit(0);
1734
+ }
1735
+ const { waitUntilExit } = render(
1736
+ /* @__PURE__ */ jsx8(App, { model, systemPromptOverride: systemPrompt, children: /* @__PURE__ */ jsx8(REPL, {}) })
1737
+ );
1738
+ await waitUntilExit();
1739
+ });
1740
+ program.parse();