darkfoo-code 0.2.5 → 0.4.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.
Files changed (2) hide show
  1. package/dist/main.js +450 -447
  2. package/package.json +1 -1
package/dist/main.js CHANGED
@@ -18,18 +18,30 @@ var init_theme = __esm({
18
18
  "src/utils/theme.ts"() {
19
19
  "use strict";
20
20
  theme = {
21
- cyan: "#5eead4",
22
- cyanDim: "#3ab5a0",
23
- pink: "#f472b6",
24
- pinkDim: "#c2588e",
25
- purple: "#a78bfa",
21
+ cyan: "#3b82f6",
22
+ // bright blue (primary)
23
+ cyanDim: "#2563eb",
24
+ // deeper blue
25
+ pink: "#ea580c",
26
+ // burnt orange (accent)
27
+ pinkDim: "#c2410c",
28
+ // deep burnt orange
29
+ purple: "#7c6ddb",
30
+ // blue-violet (midpoint)
26
31
  green: "#4ade80",
27
- yellow: "#fbbf24",
32
+ // success green
33
+ yellow: "#f59e0b",
34
+ // warm amber
28
35
  red: "#ef4444",
36
+ // errors
29
37
  text: "#e2e8f0",
38
+ // body text
30
39
  dim: "#7e8ea6",
40
+ // secondary
31
41
  surface: "#111827",
42
+ // component backgrounds
32
43
  bg: "#0c1021"
44
+ // terminal background
33
45
  };
34
46
  }
35
47
  });
@@ -151,6 +163,13 @@ var init_ollama = __esm({
151
163
  yield { type: "tool_call", toolCall: normalizeToolCall(tc) };
152
164
  }
153
165
  }
166
+ if (chunk.done && (chunk.eval_count || chunk.prompt_eval_count)) {
167
+ yield {
168
+ type: "usage",
169
+ inputTokens: chunk.prompt_eval_count ?? 0,
170
+ outputTokens: chunk.eval_count ?? 0
171
+ };
172
+ }
154
173
  }
155
174
  }
156
175
  } finally {
@@ -299,6 +318,13 @@ var init_openai_compat = __esm({
299
318
  }
300
319
  }
301
320
  pendingToolCalls.clear();
321
+ if (chunk.usage) {
322
+ yield {
323
+ type: "usage",
324
+ inputTokens: chunk.usage.prompt_tokens ?? 0,
325
+ outputTokens: chunk.usage.completion_tokens ?? 0
326
+ };
327
+ }
302
328
  }
303
329
  }
304
330
  }
@@ -549,6 +575,88 @@ var init_permissions = __esm({
549
575
  }
550
576
  });
551
577
 
578
+ // src/hooks.ts
579
+ import { execFile as execFile4 } from "child_process";
580
+ import { readFile as readFile5 } from "fs/promises";
581
+ import { join as join6 } from "path";
582
+ async function loadHooks() {
583
+ if (cachedHooks) return cachedHooks;
584
+ try {
585
+ const raw = await readFile5(SETTINGS_PATH4, "utf-8");
586
+ const config = JSON.parse(raw);
587
+ cachedHooks = config.hooks ?? [];
588
+ return cachedHooks;
589
+ } catch {
590
+ cachedHooks = [];
591
+ return [];
592
+ }
593
+ }
594
+ async function executeHooks(event, context) {
595
+ const hooks = await loadHooks();
596
+ const matching = hooks.filter((h) => {
597
+ if (h.event !== event) return false;
598
+ if (h.toolFilter && context.toolName && !h.toolFilter.includes(context.toolName)) return false;
599
+ return true;
600
+ });
601
+ const results = [];
602
+ for (const hook of matching) {
603
+ try {
604
+ const output = await new Promise((resolve8, reject) => {
605
+ execFile4("bash", ["-c", hook.command], {
606
+ cwd: context.cwd,
607
+ timeout: 1e4,
608
+ env: {
609
+ ...process.env,
610
+ DARKFOO_EVENT: event,
611
+ DARKFOO_TOOL: context.toolName ?? ""
612
+ }
613
+ }, (err, stdout) => {
614
+ if (err) reject(err);
615
+ else resolve8(stdout.trim());
616
+ });
617
+ });
618
+ if (output) results.push(output);
619
+ } catch {
620
+ }
621
+ }
622
+ return results;
623
+ }
624
+ var SETTINGS_PATH4, cachedHooks;
625
+ var init_hooks = __esm({
626
+ "src/hooks.ts"() {
627
+ "use strict";
628
+ SETTINGS_PATH4 = join6(process.env.HOME || "~", ".darkfoo", "settings.json");
629
+ cachedHooks = null;
630
+ }
631
+ });
632
+
633
+ // src/utils/debug.ts
634
+ var debug_exports = {};
635
+ __export(debug_exports, {
636
+ debug: () => debug,
637
+ isDebugMode: () => isDebugMode,
638
+ setDebugMode: () => setDebugMode
639
+ });
640
+ function setDebugMode(on) {
641
+ debugMode = on;
642
+ }
643
+ function isDebugMode() {
644
+ return debugMode;
645
+ }
646
+ function debug(msg) {
647
+ if (debugMode) {
648
+ process.stderr.write(`[debug] ${msg}
649
+ `);
650
+ }
651
+ }
652
+ var debugMode;
653
+ var init_debug = __esm({
654
+ "src/utils/debug.ts"() {
655
+ "use strict";
656
+ debugMode = false;
657
+ }
658
+ });
659
+
552
660
  // src/query.ts
553
661
  var query_exports = {};
554
662
  __export(query_exports, {
@@ -563,6 +671,7 @@ async function* query(params) {
563
671
  const ollamaTools = tools.map((t) => t.toOllamaToolDef());
564
672
  while (turns < maxTurns) {
565
673
  turns++;
674
+ debug(`query turn ${turns}/${maxTurns}, ${messages.length} messages`);
566
675
  const ollamaMessages = toOllamaMessages(messages, systemPrompt);
567
676
  let assistantContent = "";
568
677
  const toolCalls = [];
@@ -588,7 +697,11 @@ async function* query(params) {
588
697
  };
589
698
  messages.push(assistantMsg);
590
699
  yield { type: "assistant_message", message: assistantMsg };
591
- if (toolCalls.length === 0) return;
700
+ if (toolCalls.length === 0) {
701
+ debug("no tool calls, query complete");
702
+ return;
703
+ }
704
+ debug(`${toolCalls.length} tool calls: ${toolCalls.map((tc) => tc.function.name).join(", ")}`);
592
705
  const readOnlyCalls = [];
593
706
  const writeCalls = [];
594
707
  const unknownCalls = [];
@@ -611,10 +724,14 @@ async function* query(params) {
611
724
  const results = await Promise.all(
612
725
  readOnlyCalls.map(async ({ tc, tool }) => {
613
726
  const coercedArgs = coerceToolArgs(tc.function.arguments, tool);
727
+ await executeHooks("pre_tool", { toolName: tool.name, cwd: process.cwd() });
614
728
  try {
615
- return { tool, result: await tool.call(coercedArgs, { cwd: process.cwd(), abortSignal: signal }) };
729
+ const result = await tool.call(coercedArgs, { cwd: process.cwd(), abortSignal: signal });
730
+ await executeHooks("post_tool", { toolName: tool.name, cwd: process.cwd() });
731
+ return { tool, result };
616
732
  } catch (err) {
617
733
  const msg = err instanceof Error ? err.message : String(err);
734
+ await executeHooks("post_tool", { toolName: tool.name, cwd: process.cwd() });
618
735
  return { tool, result: { output: `Tool execution error: ${msg}`, isError: true } };
619
736
  }
620
737
  })
@@ -625,6 +742,12 @@ async function* query(params) {
625
742
  }
626
743
  }
627
744
  for (const { tc, tool } of writeCalls) {
745
+ if (getAppState().planMode) {
746
+ const blocked = `Tool "${tool.name}" is blocked in plan mode. Use ExitPlanMode first.`;
747
+ yield { type: "tool_result", toolName: tool.name, output: blocked, isError: true };
748
+ messages.push({ id: nanoid3(), role: "tool", content: blocked, toolName: tool.name, timestamp: Date.now() });
749
+ continue;
750
+ }
628
751
  const coercedArgs = coerceToolArgs(tc.function.arguments, tool);
629
752
  const permission = await checkPermission(tool, coercedArgs);
630
753
  if (permission === "deny") {
@@ -633,6 +756,7 @@ async function* query(params) {
633
756
  messages.push({ id: nanoid3(), role: "tool", content: denied, toolName: tool.name, timestamp: Date.now() });
634
757
  continue;
635
758
  }
759
+ await executeHooks("pre_tool", { toolName: tool.name, cwd: process.cwd() });
636
760
  let result;
637
761
  try {
638
762
  result = await tool.call(coercedArgs, { cwd: process.cwd(), abortSignal: signal });
@@ -640,6 +764,7 @@ async function* query(params) {
640
764
  const msg = err instanceof Error ? err.message : String(err);
641
765
  result = { output: `Tool execution error: ${msg}`, isError: true };
642
766
  }
767
+ await executeHooks("post_tool", { toolName: tool.name, cwd: process.cwd() });
643
768
  yield { type: "tool_result", toolName: tool.name, output: result.output, isError: result.isError ?? false };
644
769
  messages.push({ id: nanoid3(), role: "tool", content: result.output, toolName: tool.name, timestamp: Date.now() });
645
770
  }
@@ -691,12 +816,15 @@ var init_query = __esm({
691
816
  "use strict";
692
817
  init_providers();
693
818
  init_permissions();
819
+ init_hooks();
820
+ init_state();
821
+ init_debug();
694
822
  }
695
823
  });
696
824
 
697
825
  // src/context-loader.ts
698
- import { readFile as readFile5, readdir as readdir2, access } from "fs/promises";
699
- import { join as join6, dirname, resolve } from "path";
826
+ import { readFile as readFile6, readdir as readdir2, access } from "fs/promises";
827
+ import { join as join7, dirname, resolve } from "path";
700
828
  async function loadProjectContext(cwd) {
701
829
  const parts = [];
702
830
  const globalContent = await tryRead(HOME_CONTEXT);
@@ -704,13 +832,27 @@ async function loadProjectContext(cwd) {
704
832
  parts.push(`# Global instructions (~/.darkfoo/DARKFOO.md)
705
833
 
706
834
  ${globalContent}`);
835
+ }
836
+ const userRulesDir = join7(process.env.HOME || "~", ".darkfoo", "rules");
837
+ try {
838
+ await access(userRulesDir);
839
+ const userRuleFiles = await readdir2(userRulesDir);
840
+ for (const file of userRuleFiles.filter((f) => f.endsWith(".md")).sort()) {
841
+ const content = await tryRead(join7(userRulesDir, file));
842
+ if (content) {
843
+ parts.push(`# User rule: ${file}
844
+
845
+ ${content}`);
846
+ }
847
+ }
848
+ } catch {
707
849
  }
708
850
  const visited = /* @__PURE__ */ new Set();
709
851
  let dir = resolve(cwd);
710
852
  while (dir && !visited.has(dir)) {
711
853
  visited.add(dir);
712
854
  for (const file of CONTEXT_FILES) {
713
- const filePath = join6(dir, file);
855
+ const filePath = join7(dir, file);
714
856
  const content = await tryRead(filePath);
715
857
  if (content) {
716
858
  const rel = filePath.replace(cwd + "/", "");
@@ -723,12 +865,12 @@ ${content}`);
723
865
  if (parent === dir) break;
724
866
  dir = parent;
725
867
  }
726
- const rulesDir = join6(cwd, RULES_DIR);
868
+ const rulesDir = join7(cwd, RULES_DIR);
727
869
  try {
728
870
  await access(rulesDir);
729
871
  const files = await readdir2(rulesDir);
730
872
  for (const file of files.filter((f) => f.endsWith(".md")).sort()) {
731
- const content = await tryRead(join6(rulesDir, file));
873
+ const content = await tryRead(join7(rulesDir, file));
732
874
  if (content) {
733
875
  parts.push(`# Rule: ${file}
734
876
 
@@ -741,7 +883,7 @@ ${content}`);
741
883
  }
742
884
  async function tryRead(path) {
743
885
  try {
744
- return await readFile5(path, "utf-8");
886
+ return await readFile6(path, "utf-8");
745
887
  } catch {
746
888
  return null;
747
889
  }
@@ -752,7 +894,7 @@ var init_context_loader = __esm({
752
894
  "use strict";
753
895
  CONTEXT_FILES = ["DARKFOO.md", ".darkfoo/DARKFOO.md"];
754
896
  RULES_DIR = ".darkfoo/rules";
755
- HOME_CONTEXT = join6(process.env.HOME || "~", ".darkfoo", "DARKFOO.md");
897
+ HOME_CONTEXT = join7(process.env.HOME || "~", ".darkfoo", "DARKFOO.md");
756
898
  }
757
899
  });
758
900
 
@@ -847,14 +989,14 @@ var init_types = __esm({
847
989
 
848
990
  // src/tools/bash.ts
849
991
  import { spawn } from "child_process";
850
- import { writeFile as writeFile6, mkdir as mkdir6, readFile as readFile6 } from "fs/promises";
851
- import { join as join7 } from "path";
992
+ import { writeFile as writeFile6, mkdir as mkdir6, readFile as readFile7 } from "fs/promises";
993
+ import { join as join8 } from "path";
852
994
  import { nanoid as nanoid4 } from "nanoid";
853
995
  import { z } from "zod";
854
996
  async function runInBackground(command, cwd) {
855
997
  const taskId = nanoid4(8);
856
998
  await mkdir6(BG_OUTPUT_DIR, { recursive: true });
857
- const outputPath = join7(BG_OUTPUT_DIR, `${taskId}.output`);
999
+ const outputPath = join8(BG_OUTPUT_DIR, `${taskId}.output`);
858
1000
  backgroundTasks.set(taskId, { command, status: "running", outputPath });
859
1001
  const proc = spawn(command, {
860
1002
  shell: true,
@@ -906,7 +1048,7 @@ var init_bash = __esm({
906
1048
  run_in_background: z.boolean().optional().describe("Run in background, returning a task ID for later retrieval")
907
1049
  });
908
1050
  MAX_OUTPUT = 1e5;
909
- BG_OUTPUT_DIR = join7(process.env.HOME || "~", ".darkfoo", "bg-tasks");
1051
+ BG_OUTPUT_DIR = join8(process.env.HOME || "~", ".darkfoo", "bg-tasks");
910
1052
  backgroundTasks = /* @__PURE__ */ new Map();
911
1053
  BashTool = {
912
1054
  name: "Bash",
@@ -967,17 +1109,17 @@ var init_bash = __esm({
967
1109
  });
968
1110
 
969
1111
  // src/tools/read.ts
970
- import { readFile as readFile7, stat } from "fs/promises";
1112
+ import { readFile as readFile8, stat } from "fs/promises";
971
1113
  import { extname, resolve as resolve2 } from "path";
972
1114
  import { z as z2 } from "zod";
973
1115
  async function readImage(filePath, size) {
974
1116
  const ext = extname(filePath).toLowerCase();
975
1117
  if (ext === ".svg") {
976
- const content = await readFile7(filePath, "utf-8");
1118
+ const content = await readFile8(filePath, "utf-8");
977
1119
  return { output: `SVG image (${size} bytes):
978
1120
  ${content.slice(0, 5e3)}` };
979
1121
  }
980
- const buffer = await readFile7(filePath);
1122
+ const buffer = await readFile8(filePath);
981
1123
  const base64 = buffer.toString("base64");
982
1124
  const sizeKB = (size / 1024).toFixed(1);
983
1125
  const dims = detectImageDimensions(buffer, ext);
@@ -990,9 +1132,9 @@ Base64 length: ${base64.length} chars
990
1132
  };
991
1133
  }
992
1134
  async function readPdf(filePath) {
993
- const { execFile: execFile6 } = await import("child_process");
1135
+ const { execFile: execFile7 } = await import("child_process");
994
1136
  return new Promise((resolve8) => {
995
- execFile6("pdftotext", [filePath, "-"], { timeout: 15e3, maxBuffer: 5 * 1024 * 1024 }, (err, stdout) => {
1137
+ execFile7("pdftotext", [filePath, "-"], { timeout: 15e3, maxBuffer: 5 * 1024 * 1024 }, (err, stdout) => {
996
1138
  if (err) {
997
1139
  resolve8({
998
1140
  output: `PDF file: ${filePath}
@@ -1069,7 +1211,7 @@ var init_read = __esm({
1069
1211
  if (ext === PDF_EXT) {
1070
1212
  return readPdf(filePath);
1071
1213
  }
1072
- const raw = await readFile7(filePath, "utf-8");
1214
+ const raw = await readFile8(filePath, "utf-8");
1073
1215
  markFileRead(filePath, raw);
1074
1216
  const lines = raw.split("\n");
1075
1217
  const offset = parsed.offset ?? 0;
@@ -1140,7 +1282,7 @@ var init_write = __esm({
1140
1282
  });
1141
1283
 
1142
1284
  // src/tools/edit.ts
1143
- import { readFile as readFile8, writeFile as writeFile8 } from "fs/promises";
1285
+ import { readFile as readFile9, writeFile as writeFile8 } from "fs/promises";
1144
1286
  import { resolve as resolve4 } from "path";
1145
1287
  import { createPatch } from "diff";
1146
1288
  import { z as z4 } from "zod";
@@ -1186,7 +1328,7 @@ var init_edit = __esm({
1186
1328
  isError: true
1187
1329
  };
1188
1330
  }
1189
- const content = await readFile8(filePath, "utf-8");
1331
+ const content = await readFile9(filePath, "utf-8");
1190
1332
  const actualOld = findActualString(content, parsed.old_string);
1191
1333
  if (!actualOld) {
1192
1334
  return {
@@ -1218,7 +1360,7 @@ var init_edit = __esm({
1218
1360
  });
1219
1361
 
1220
1362
  // src/tools/grep.ts
1221
- import { execFile as execFile4 } from "child_process";
1363
+ import { execFile as execFile5 } from "child_process";
1222
1364
  import { resolve as resolve5 } from "path";
1223
1365
  import { z as z5 } from "zod";
1224
1366
  var INPUT_SCHEMA5, DEFAULT_HEAD_LIMIT, GrepTool;
@@ -1278,7 +1420,7 @@ var init_grep = __esm({
1278
1420
  }
1279
1421
  args.push(searchPath);
1280
1422
  return new Promise((resolve8) => {
1281
- execFile4("rg", args, { maxBuffer: 10 * 1024 * 1024, timeout: 3e4 }, (err, stdout, stderr) => {
1423
+ execFile5("rg", args, { maxBuffer: 10 * 1024 * 1024, timeout: 3e4 }, (err, stdout, stderr) => {
1282
1424
  if (err && !stdout) {
1283
1425
  if (err.code === 1 || err.code === "1") {
1284
1426
  resolve8({ output: "No matches found." });
@@ -1314,7 +1456,7 @@ ${output}`;
1314
1456
  });
1315
1457
 
1316
1458
  // src/tools/glob.ts
1317
- import { execFile as execFile5 } from "child_process";
1459
+ import { execFile as execFile6 } from "child_process";
1318
1460
  import { resolve as resolve6 } from "path";
1319
1461
  import { z as z6 } from "zod";
1320
1462
  var INPUT_SCHEMA6, MAX_RESULTS, GlobTool;
@@ -1348,7 +1490,7 @@ var init_glob = __esm({
1348
1490
  searchPath
1349
1491
  ];
1350
1492
  return new Promise((resolve8) => {
1351
- execFile5("rg", args, { maxBuffer: 10 * 1024 * 1024, timeout: 3e4 }, (err, stdout, stderr) => {
1493
+ execFile6("rg", args, { maxBuffer: 10 * 1024 * 1024, timeout: 3e4 }, (err, stdout, stderr) => {
1352
1494
  if (err && !stdout) {
1353
1495
  if (err.code === 1 || err.code === "1") {
1354
1496
  resolve8({ output: "No files matched." });
@@ -1599,7 +1741,7 @@ var init_agent = __esm({
1599
1741
  });
1600
1742
 
1601
1743
  // src/tools/notebook-edit.ts
1602
- import { readFile as readFile9, writeFile as writeFile9 } from "fs/promises";
1744
+ import { readFile as readFile10, writeFile as writeFile9 } from "fs/promises";
1603
1745
  import { resolve as resolve7 } from "path";
1604
1746
  import { z as z10 } from "zod";
1605
1747
  var INPUT_SCHEMA10, NotebookEditTool;
@@ -1622,7 +1764,7 @@ var init_notebook_edit = __esm({
1622
1764
  const parsed = INPUT_SCHEMA10.parse(input);
1623
1765
  const filePath = resolve7(context.cwd, parsed.file_path);
1624
1766
  try {
1625
- const raw = await readFile9(filePath, "utf-8");
1767
+ const raw = await readFile10(filePath, "utf-8");
1626
1768
  const notebook = JSON.parse(raw);
1627
1769
  if (parsed.cell_index < 0 || parsed.cell_index >= notebook.cells.length) {
1628
1770
  return {
@@ -1921,145 +2063,21 @@ var DarkfooContext = createContext({ model: "qwen2.5-coder:32b" });
1921
2063
  function useDarkfooContext() {
1922
2064
  return useContext(DarkfooContext);
1923
2065
  }
1924
- function App({ model, systemPromptOverride, children }) {
1925
- return /* @__PURE__ */ jsx(DarkfooContext.Provider, { value: { model, systemPromptOverride }, children });
2066
+ function App({ model, systemPromptOverride, maxTurns, initialMessages, initialSessionId, children }) {
2067
+ return /* @__PURE__ */ jsx(DarkfooContext.Provider, { value: { model, systemPromptOverride, maxTurns, initialMessages, initialSessionId }, children });
1926
2068
  }
1927
2069
 
1928
2070
  // src/repl.tsx
1929
2071
  import { useState as useState2, useCallback as useCallback2, useEffect, useRef } from "react";
1930
- import { Box as Box7, Text as Text7, useApp, useInput as useInput2 } from "ink";
2072
+ import { Box as Box6, Text as Text6, useApp, useInput as useInput2 } from "ink";
1931
2073
  import { nanoid as nanoid6 } from "nanoid";
1932
2074
 
1933
2075
  // src/components/Banner.tsx
1934
2076
  init_theme();
1935
- import { memo as memo2 } from "react";
1936
- import { Box as Box2, Text as Text2 } from "ink";
1937
-
1938
- // src/components/Fox.tsx
1939
- init_theme();
1940
2077
  import { memo } from "react";
1941
2078
  import { Box, Text } from "ink";
1942
2079
  import { jsx as jsx2, jsxs } from "react/jsx-runtime";
1943
- var FRAMES = {
1944
- idle: [
1945
- " /\\_/\\ ",
1946
- " ( o.o ) ",
1947
- " > ^ < ",
1948
- " /| |\\ ",
1949
- " (_| |_)",
1950
- " ~ "
1951
- ],
1952
- thinking: [
1953
- " /\\_/\\ ",
1954
- " ( o.- ) ",
1955
- " > ^ < ",
1956
- " | | ",
1957
- " |___| ",
1958
- " . . . "
1959
- ],
1960
- working: [
1961
- " /\\_/\\ ",
1962
- " ( >.< ) ",
1963
- " > ^ < ",
1964
- " /| |\\ ",
1965
- " (_| |_)",
1966
- " ~\\ "
1967
- ],
1968
- success: [
1969
- " /\\_/\\ ",
1970
- " ( ^.^ ) ",
1971
- " > w < ",
1972
- " /| |\\ ",
1973
- " (_| |_)",
1974
- " \\~/ * "
1975
- ],
1976
- error: [
1977
- " /\\_/\\ ",
1978
- " ( ;.; ) ",
1979
- " > n < ",
1980
- " | | ",
1981
- " |___| ",
1982
- " "
1983
- ],
1984
- greeting: [
1985
- " /\\_/\\ ",
1986
- " ( ^.^ )/",
1987
- " > w < ",
1988
- " /| | ",
1989
- " (_| |) ",
1990
- " \\~/ "
1991
- ],
1992
- pet: [
1993
- " /\\_/\\ ",
1994
- " ( ^w^ ) ",
1995
- " > ~ < ",
1996
- " /| |\\ ",
1997
- " (_| |_)",
1998
- " \\~/ * "
1999
- ],
2000
- eating: [
2001
- " /\\_/\\ ",
2002
- " ( >o< ) ",
2003
- " > ~ < ",
2004
- " /| |\\ ",
2005
- " (_| |_)",
2006
- " \\~/ "
2007
- ],
2008
- sleeping: [
2009
- " /\\_/\\ ",
2010
- " ( -.- ) ",
2011
- " > ~ < ",
2012
- " | | ",
2013
- " |___| ",
2014
- " z z z "
2015
- ]
2016
- };
2017
- var BODY_COLORS = {
2018
- idle: "#5eead4",
2019
- thinking: "#a78bfa",
2020
- working: "#fbbf24",
2021
- success: "#4ade80",
2022
- error: "#f472b6",
2023
- greeting: "#5eead4",
2024
- pet: "#f472b6",
2025
- eating: "#fbbf24",
2026
- sleeping: "#7e8ea6"
2027
- };
2028
- var FACE_COLORS = {
2029
- idle: "#e2e8f0",
2030
- thinking: "#5eead4",
2031
- working: "#5eead4",
2032
- success: "#4ade80",
2033
- error: "#f472b6",
2034
- greeting: "#4ade80",
2035
- pet: "#f472b6",
2036
- eating: "#fbbf24",
2037
- sleeping: "#7e8ea6"
2038
- };
2039
- var Fox = memo(function Fox2({ mood = "idle" }) {
2040
- const frame = FRAMES[mood];
2041
- const bodyColor = BODY_COLORS[mood];
2042
- const faceColor = FACE_COLORS[mood];
2043
- return /* @__PURE__ */ jsx2(Box, { flexDirection: "column", children: frame.map((line, i) => /* @__PURE__ */ jsx2(Text, { color: i <= 1 ? faceColor : bodyColor, children: line }, i)) });
2044
- });
2045
- function FoxBubble({ text }) {
2046
- if (!text) return null;
2047
- const maxW = 28;
2048
- const display = text.length > maxW ? text.slice(0, maxW - 2) + ".." : text;
2049
- const w = display.length;
2050
- return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
2051
- /* @__PURE__ */ jsx2(Text, { color: theme.dim ?? "#7e8ea6", children: "." + "-".repeat(w + 2) + "." }),
2052
- /* @__PURE__ */ jsxs(Text, { color: theme.dim ?? "#7e8ea6", children: [
2053
- "| ",
2054
- /* @__PURE__ */ jsx2(Text, { color: theme.text ?? "#e2e8f0", children: display }),
2055
- " |"
2056
- ] }),
2057
- /* @__PURE__ */ jsx2(Text, { color: theme.dim ?? "#7e8ea6", children: "'" + "-".repeat(w + 2) + "'" })
2058
- ] });
2059
- }
2060
-
2061
- // src/components/Banner.tsx
2062
- import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
2080
+ var version = "0.4.0";
2063
2081
  var LOGO_LINES = [
2064
2082
  " \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 ",
2065
2083
  " \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",
@@ -2070,14 +2088,21 @@ var LOGO_LINES = [
2070
2088
  ];
2071
2089
  var SUBTITLE = " -- C O D E --";
2072
2090
  var GRADIENT = [
2073
- "#5eead4",
2074
- "#6ee0c8",
2075
- "#82c8d0",
2076
- "#96b0d8",
2077
- "#a78bfa",
2078
- "#c47ee8",
2079
- "#f472b6"
2091
+ "#3b82f6",
2092
+ "#5a72e0",
2093
+ "#7c6ddb",
2094
+ "#9a5ec0",
2095
+ "#b8509a",
2096
+ "#d46030",
2097
+ "#ea580c"
2080
2098
  ];
2099
+ var MOOD_COLORS = {
2100
+ idle: "#3b82f6",
2101
+ thinking: "#7c6ddb",
2102
+ working: "#f59e0b",
2103
+ success: "#4ade80",
2104
+ error: "#ea580c"
2105
+ };
2081
2106
  function lineColor(index) {
2082
2107
  const t = index / (LOGO_LINES.length - 1);
2083
2108
  return GRADIENT[Math.round(t * (GRADIENT.length - 1))];
@@ -2088,43 +2113,51 @@ function gradientBar(width) {
2088
2113
  color: GRADIENT[Math.round(i / (width - 1) * (GRADIENT.length - 1))]
2089
2114
  }));
2090
2115
  }
2091
- var Banner = memo2(function Banner2({ model, cwd, providerName, providerOnline }) {
2116
+ var Banner = memo(function Banner2({ model, cwd, providerName, providerOnline, mood = "idle" }) {
2092
2117
  const bar = gradientBar(60);
2093
2118
  const statusTag = providerOnline ? "[connected]" : "[offline]";
2094
2119
  const statusColor = providerOnline ? theme.green ?? "#4ade80" : theme.pink ?? "#f472b6";
2095
- return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", marginBottom: 1, children: [
2096
- /* @__PURE__ */ jsxs2(Box2, { children: [
2097
- /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
2098
- LOGO_LINES.map((line, i) => /* @__PURE__ */ jsx3(Text2, { color: lineColor(i), bold: true, children: line }, i)),
2099
- /* @__PURE__ */ jsx3(Text2, { color: theme.purple ?? "#a78bfa", bold: true, children: SUBTITLE })
2120
+ const mascotColor = MOOD_COLORS[mood] ?? MOOD_COLORS.idle;
2121
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [
2122
+ /* @__PURE__ */ jsxs(Box, { children: [
2123
+ /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
2124
+ LOGO_LINES.map((line, i) => /* @__PURE__ */ jsx2(Text, { color: lineColor(i), bold: true, children: line }, i)),
2125
+ /* @__PURE__ */ jsx2(Text, { color: theme.purple ?? "#a78bfa", bold: true, children: SUBTITLE }),
2126
+ /* @__PURE__ */ jsxs(Text, { color: theme.dim ?? "#7e8ea6", children: [
2127
+ " v",
2128
+ version
2129
+ ] })
2100
2130
  ] }),
2101
- /* @__PURE__ */ jsxs2(Box2, { marginLeft: 2, flexDirection: "row", alignItems: "flex-end", children: [
2102
- /* @__PURE__ */ jsx3(Fox, { mood: "greeting" }),
2103
- /* @__PURE__ */ jsx3(FoxBubble, { text: "Ready to work." })
2131
+ /* @__PURE__ */ jsxs(Box, { marginLeft: 2, flexDirection: "column", justifyContent: "flex-end", children: [
2132
+ /* @__PURE__ */ jsx2(Text, { color: mascotColor, children: " \u2584\u2588\u2588\u2584" }),
2133
+ /* @__PURE__ */ jsx2(Text, { color: mascotColor, children: " \u2590\u2588\u25E6\u25E6\u2588\u258C" }),
2134
+ /* @__PURE__ */ jsx2(Text, { color: mascotColor, children: " \u2590\u258C\u25C6\u25C6\u2590\u258C" }),
2135
+ /* @__PURE__ */ jsx2(Text, { color: mascotColor, children: " \u2590\u258C\u2584\u2584\u2590\u258C" }),
2136
+ /* @__PURE__ */ jsx2(Text, { color: mascotColor, children: " \u2580\u2580\u2580\u2580" })
2104
2137
  ] })
2105
2138
  ] }),
2106
- /* @__PURE__ */ jsx3(Text2, { children: " " }),
2107
- /* @__PURE__ */ jsxs2(Text2, { children: [
2139
+ /* @__PURE__ */ jsx2(Text, { children: " " }),
2140
+ /* @__PURE__ */ jsxs(Text, { children: [
2108
2141
  " ",
2109
- bar.map((d, i) => /* @__PURE__ */ jsx3(Text2, { color: d.color, children: d.char }, i))
2142
+ bar.map((d, i) => /* @__PURE__ */ jsx2(Text, { color: d.color, children: d.char }, i))
2110
2143
  ] }),
2111
- /* @__PURE__ */ jsxs2(Box2, { marginTop: 1, marginLeft: 2, children: [
2112
- /* @__PURE__ */ jsx3(Text2, { color: theme.dim ?? "#7e8ea6", children: "model " }),
2113
- model ? /* @__PURE__ */ jsx3(Text2, { color: theme.cyan ?? "#5eead4", bold: true, children: model }) : /* @__PURE__ */ jsx3(Text2, { color: theme.dim ?? "#7e8ea6", children: "--" })
2144
+ /* @__PURE__ */ jsxs(Box, { marginTop: 1, marginLeft: 2, children: [
2145
+ /* @__PURE__ */ jsx2(Text, { color: theme.dim ?? "#7e8ea6", children: "model " }),
2146
+ model ? /* @__PURE__ */ jsx2(Text, { color: theme.cyan ?? "#5eead4", bold: true, children: model }) : /* @__PURE__ */ jsx2(Text, { color: theme.dim ?? "#7e8ea6", children: "--" })
2114
2147
  ] }),
2115
- /* @__PURE__ */ jsxs2(Box2, { marginLeft: 2, children: [
2116
- /* @__PURE__ */ jsx3(Text2, { color: theme.dim ?? "#7e8ea6", children: "via " }),
2117
- /* @__PURE__ */ jsx3(Text2, { color: theme.text ?? "#e2e8f0", children: providerName }),
2118
- /* @__PURE__ */ jsx3(Text2, { color: theme.dim ?? "#7e8ea6", children: " " }),
2119
- /* @__PURE__ */ jsx3(Text2, { color: statusColor, children: statusTag })
2148
+ /* @__PURE__ */ jsxs(Box, { marginLeft: 2, children: [
2149
+ /* @__PURE__ */ jsx2(Text, { color: theme.dim ?? "#7e8ea6", children: "via " }),
2150
+ /* @__PURE__ */ jsx2(Text, { color: theme.text ?? "#e2e8f0", children: providerName }),
2151
+ /* @__PURE__ */ jsx2(Text, { color: theme.dim ?? "#7e8ea6", children: " " }),
2152
+ /* @__PURE__ */ jsx2(Text, { color: statusColor, children: statusTag })
2120
2153
  ] }),
2121
- /* @__PURE__ */ jsxs2(Box2, { marginLeft: 2, children: [
2122
- /* @__PURE__ */ jsx3(Text2, { color: theme.dim ?? "#7e8ea6", children: "cwd " }),
2123
- /* @__PURE__ */ jsx3(Text2, { color: theme.text ?? "#e2e8f0", children: cwd })
2154
+ /* @__PURE__ */ jsxs(Box, { marginLeft: 2, children: [
2155
+ /* @__PURE__ */ jsx2(Text, { color: theme.dim ?? "#7e8ea6", children: "cwd " }),
2156
+ /* @__PURE__ */ jsx2(Text, { color: theme.text ?? "#e2e8f0", children: cwd })
2124
2157
  ] }),
2125
- /* @__PURE__ */ jsx3(Box2, { marginTop: 1, children: /* @__PURE__ */ jsxs2(Text2, { children: [
2158
+ /* @__PURE__ */ jsx2(Box, { marginTop: 1, children: /* @__PURE__ */ jsxs(Text, { children: [
2126
2159
  " ",
2127
- bar.map((d, i) => /* @__PURE__ */ jsx3(Text2, { color: d.color, children: d.char }, i))
2160
+ bar.map((d, i) => /* @__PURE__ */ jsx2(Text, { color: d.color, children: d.char }, i))
2128
2161
  ] }) })
2129
2162
  ] });
2130
2163
  });
@@ -2132,8 +2165,8 @@ var Banner = memo2(function Banner2({ model, cwd, providerName, providerOnline }
2132
2165
  // src/components/Messages.tsx
2133
2166
  init_theme();
2134
2167
  init_format();
2135
- import { memo as memo3 } from "react";
2136
- import { Box as Box3, Text as Text3 } from "ink";
2168
+ import { memo as memo2 } from "react";
2169
+ import { Box as Box2, Text as Text2 } from "ink";
2137
2170
 
2138
2171
  // src/utils/markdown.ts
2139
2172
  function renderMarkdown(text) {
@@ -2197,31 +2230,31 @@ function renderInline(text) {
2197
2230
  }
2198
2231
 
2199
2232
  // src/components/Messages.tsx
2200
- import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
2201
- var Messages = memo3(function Messages2({ messages }) {
2202
- return /* @__PURE__ */ jsx4(Box3, { flexDirection: "column", children: messages.map((msg) => /* @__PURE__ */ jsx4(MessageRow, { message: msg }, msg.id)) });
2233
+ import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
2234
+ var Messages = memo2(function Messages2({ messages }) {
2235
+ return /* @__PURE__ */ jsx3(Box2, { flexDirection: "column", children: messages.map((msg) => /* @__PURE__ */ jsx3(MessageRow, { message: msg }, msg.id)) });
2203
2236
  });
2204
2237
  function MessageRow({ message }) {
2205
2238
  switch (message.role) {
2206
2239
  case "user":
2207
- return /* @__PURE__ */ jsxs3(Box3, { marginTop: 1, marginBottom: 1, children: [
2208
- /* @__PURE__ */ jsx4(Text3, { color: theme.cyan, bold: true, children: "\u276F " }),
2209
- /* @__PURE__ */ jsx4(Text3, { bold: true, color: theme.text, children: message.content })
2240
+ return /* @__PURE__ */ jsxs2(Box2, { marginTop: 1, marginBottom: 1, children: [
2241
+ /* @__PURE__ */ jsx3(Text2, { color: theme.cyan, bold: true, children: "\u276F " }),
2242
+ /* @__PURE__ */ jsx3(Text2, { bold: true, color: theme.text, children: message.content })
2210
2243
  ] });
2211
2244
  case "assistant": {
2212
2245
  if (!message.content && message.toolCalls) return null;
2213
2246
  if (!message.content) return null;
2214
2247
  const rendered = renderMarkdown(message.content);
2215
- return /* @__PURE__ */ jsx4(Box3, { flexDirection: "column", marginBottom: 1, children: /* @__PURE__ */ jsxs3(Box3, { children: [
2216
- /* @__PURE__ */ jsx4(Text3, { color: theme.cyan, children: "\u23BF " }),
2217
- /* @__PURE__ */ jsx4(Text3, { wrap: "wrap", children: rendered })
2248
+ return /* @__PURE__ */ jsx3(Box2, { flexDirection: "column", marginBottom: 1, children: /* @__PURE__ */ jsxs2(Box2, { children: [
2249
+ /* @__PURE__ */ jsx3(Text2, { color: theme.cyan, children: "\u23BF " }),
2250
+ /* @__PURE__ */ jsx3(Text2, { wrap: "wrap", children: rendered })
2218
2251
  ] }) });
2219
2252
  }
2220
2253
  case "tool": {
2221
2254
  const lines = message.content.split("\n");
2222
2255
  const preview = lines.length > 8 ? lines.slice(0, 8).join("\n") + `
2223
2256
  ... (${lines.length - 8} more lines)` : message.content;
2224
- return /* @__PURE__ */ jsx4(Box3, { flexDirection: "column", marginLeft: 2, marginBottom: 1, children: /* @__PURE__ */ jsxs3(Text3, { color: theme.dim, children: [
2257
+ return /* @__PURE__ */ jsx3(Box2, { flexDirection: "column", marginLeft: 2, marginBottom: 1, children: /* @__PURE__ */ jsxs2(Text2, { color: theme.dim, children: [
2225
2258
  "\u23BF ",
2226
2259
  truncate(preview, 1200)
2227
2260
  ] }) });
@@ -2234,16 +2267,16 @@ function MessageRow({ message }) {
2234
2267
  // src/components/ToolCall.tsx
2235
2268
  init_theme();
2236
2269
  init_format();
2237
- import { Box as Box4, Text as Text4 } from "ink";
2238
- import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
2270
+ import { Box as Box3, Text as Text3 } from "ink";
2271
+ import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
2239
2272
  function ActiveToolCall({ toolName, args }) {
2240
- return /* @__PURE__ */ jsxs4(Box4, { marginLeft: 2, children: [
2241
- /* @__PURE__ */ jsx5(Text4, { color: theme.cyan, children: "..." }),
2242
- /* @__PURE__ */ jsxs4(Text4, { bold: true, color: theme.yellow, children: [
2273
+ return /* @__PURE__ */ jsxs3(Box3, { marginLeft: 2, children: [
2274
+ /* @__PURE__ */ jsx4(Text3, { color: theme.cyan, children: "..." }),
2275
+ /* @__PURE__ */ jsxs3(Text3, { bold: true, color: theme.yellow, children: [
2243
2276
  " ",
2244
2277
  toolName
2245
2278
  ] }),
2246
- args ? /* @__PURE__ */ jsxs4(Text4, { color: theme.dim, children: [
2279
+ args ? /* @__PURE__ */ jsxs3(Text3, { color: theme.dim, children: [
2247
2280
  " ",
2248
2281
  formatToolArgs(args)
2249
2282
  ] }) : null
@@ -2255,15 +2288,15 @@ function ToolResultDisplay({ toolName, output, isError }) {
2255
2288
  const lines = output.split("\n");
2256
2289
  const preview = lines.length > 6 ? lines.slice(0, 6).join("\n") + `
2257
2290
  ... (${lines.length - 6} more lines)` : output;
2258
- return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", marginLeft: 2, marginBottom: 1, children: [
2259
- /* @__PURE__ */ jsxs4(Box4, { children: [
2260
- /* @__PURE__ */ jsxs4(Text4, { color: iconColor, children: [
2291
+ return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", marginLeft: 2, marginBottom: 1, children: [
2292
+ /* @__PURE__ */ jsxs3(Box3, { children: [
2293
+ /* @__PURE__ */ jsxs3(Text3, { color: iconColor, children: [
2261
2294
  icon,
2262
2295
  " "
2263
2296
  ] }),
2264
- /* @__PURE__ */ jsx5(Text4, { bold: true, color: theme.yellow, children: toolName })
2297
+ /* @__PURE__ */ jsx4(Text3, { bold: true, color: theme.yellow, children: toolName })
2265
2298
  ] }),
2266
- /* @__PURE__ */ jsx5(Box4, { marginLeft: 2, children: /* @__PURE__ */ jsxs4(Text4, { color: theme.dim, children: [
2299
+ /* @__PURE__ */ jsx4(Box3, { marginLeft: 2, children: /* @__PURE__ */ jsxs3(Text3, { color: theme.dim, children: [
2267
2300
  "\u23BF ",
2268
2301
  truncate(preview, 1200)
2269
2302
  ] }) })
@@ -2284,9 +2317,9 @@ function formatToolArgs(args) {
2284
2317
  // src/components/StatusLine.tsx
2285
2318
  init_theme();
2286
2319
  init_state();
2287
- import { memo as memo4 } from "react";
2288
- import { Box as Box5, Text as Text5 } from "ink";
2289
- import { Fragment, jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
2320
+ import { memo as memo3 } from "react";
2321
+ import { Box as Box4, Text as Text4 } from "ink";
2322
+ import { Fragment, jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
2290
2323
  function getContextLimit(model) {
2291
2324
  const lower = model.toLowerCase();
2292
2325
  if (lower.includes("llama3.1")) return 131072;
@@ -2297,47 +2330,47 @@ function getContextLimit(model) {
2297
2330
  if (lower.includes("deepseek")) return 32768;
2298
2331
  return 8192;
2299
2332
  }
2300
- var StatusLine = memo4(function StatusLine2({ model, messageCount, tokenEstimate, isStreaming }) {
2333
+ var StatusLine = memo3(function StatusLine2({ model, messageCount, tokenEstimate, isStreaming }) {
2301
2334
  const state2 = getAppState();
2302
2335
  const contextLimit = getContextLimit(model);
2303
2336
  const usage = Math.min(tokenEstimate / contextLimit, 1);
2304
2337
  const pct = (usage * 100).toFixed(0);
2305
2338
  const usageColor = usage > 0.8 ? theme.pink : usage > 0.5 ? theme.yellow : theme.cyan;
2306
2339
  const activeTasks = state2.tasks.filter((t) => t.status === "in_progress").length;
2307
- return /* @__PURE__ */ jsxs5(Box5, { marginTop: 1, children: [
2308
- /* @__PURE__ */ jsxs5(Text5, { color: theme.dim, children: [
2340
+ return /* @__PURE__ */ jsxs4(Box4, { marginTop: 1, children: [
2341
+ /* @__PURE__ */ jsxs4(Text4, { color: theme.dim, children: [
2309
2342
  "\u2500".repeat(2),
2310
2343
  " "
2311
2344
  ] }),
2312
- /* @__PURE__ */ jsx6(Text5, { color: theme.cyan, bold: true, children: model ? model.split(":")[0] : "--" }),
2313
- /* @__PURE__ */ jsx6(Text5, { color: theme.dim, children: " \u2502 " }),
2314
- /* @__PURE__ */ jsxs5(Text5, { color: theme.dim, children: [
2345
+ /* @__PURE__ */ jsx5(Text4, { color: theme.cyan, bold: true, children: model ? model.split(":")[0] : "--" }),
2346
+ /* @__PURE__ */ jsx5(Text4, { color: theme.dim, children: " \u2502 " }),
2347
+ /* @__PURE__ */ jsxs4(Text4, { color: theme.dim, children: [
2315
2348
  messageCount,
2316
2349
  " msgs"
2317
2350
  ] }),
2318
- /* @__PURE__ */ jsx6(Text5, { color: theme.dim, children: " \u2502 " }),
2319
- /* @__PURE__ */ jsxs5(Text5, { color: usageColor, children: [
2351
+ /* @__PURE__ */ jsx5(Text4, { color: theme.dim, children: " \u2502 " }),
2352
+ /* @__PURE__ */ jsxs4(Text4, { color: usageColor, children: [
2320
2353
  "ctx ",
2321
2354
  pct,
2322
2355
  "%"
2323
2356
  ] }),
2324
- state2.planMode ? /* @__PURE__ */ jsxs5(Fragment, { children: [
2325
- /* @__PURE__ */ jsx6(Text5, { color: theme.dim, children: " \u2502 " }),
2326
- /* @__PURE__ */ jsx6(Text5, { color: theme.yellow, bold: true, children: "PLAN" })
2357
+ state2.planMode ? /* @__PURE__ */ jsxs4(Fragment, { children: [
2358
+ /* @__PURE__ */ jsx5(Text4, { color: theme.dim, children: " \u2502 " }),
2359
+ /* @__PURE__ */ jsx5(Text4, { color: theme.yellow, bold: true, children: "PLAN" })
2327
2360
  ] }) : null,
2328
- activeTasks > 0 ? /* @__PURE__ */ jsxs5(Fragment, { children: [
2329
- /* @__PURE__ */ jsx6(Text5, { color: theme.dim, children: " \u2502 " }),
2330
- /* @__PURE__ */ jsxs5(Text5, { color: theme.green, children: [
2361
+ activeTasks > 0 ? /* @__PURE__ */ jsxs4(Fragment, { children: [
2362
+ /* @__PURE__ */ jsx5(Text4, { color: theme.dim, children: " \u2502 " }),
2363
+ /* @__PURE__ */ jsxs4(Text4, { color: theme.green, children: [
2331
2364
  activeTasks,
2332
2365
  " task",
2333
2366
  activeTasks > 1 ? "s" : ""
2334
2367
  ] })
2335
2368
  ] }) : null,
2336
- isStreaming ? /* @__PURE__ */ jsxs5(Fragment, { children: [
2337
- /* @__PURE__ */ jsx6(Text5, { color: theme.dim, children: " \u2502 " }),
2338
- /* @__PURE__ */ jsx6(Text5, { color: theme.cyan, children: "streaming" })
2369
+ isStreaming ? /* @__PURE__ */ jsxs4(Fragment, { children: [
2370
+ /* @__PURE__ */ jsx5(Text4, { color: theme.dim, children: " \u2502 " }),
2371
+ /* @__PURE__ */ jsx5(Text4, { color: theme.cyan, children: "streaming" })
2339
2372
  ] }) : null,
2340
- /* @__PURE__ */ jsxs5(Text5, { color: theme.dim, children: [
2373
+ /* @__PURE__ */ jsxs4(Text4, { color: theme.dim, children: [
2341
2374
  " ",
2342
2375
  "\u2500".repeat(2)
2343
2376
  ] })
@@ -2346,8 +2379,8 @@ var StatusLine = memo4(function StatusLine2({ model, messageCount, tokenEstimate
2346
2379
 
2347
2380
  // src/components/UserInput.tsx
2348
2381
  init_theme();
2349
- import { useState, useCallback, memo as memo5 } from "react";
2350
- import { Box as Box6, Text as Text6, useInput } from "ink";
2382
+ import { useState, useCallback, memo as memo4 } from "react";
2383
+ import { Box as Box5, Text as Text5, useInput } from "ink";
2351
2384
  import TextInput from "ink-text-input";
2352
2385
 
2353
2386
  // src/commands/help.ts
@@ -2435,12 +2468,15 @@ import { nanoid } from "nanoid";
2435
2468
  var compactCommand = {
2436
2469
  name: "compact",
2437
2470
  description: "Compress conversation history to save context",
2438
- async execute(_args, context) {
2471
+ async execute(args, context) {
2439
2472
  if (context.messages.length < 4) {
2440
2473
  return { output: "Conversation is too short to compact.", silent: true };
2441
2474
  }
2442
2475
  const transcript = context.messages.filter((m) => m.role === "user" || m.role === "assistant" && m.content).map((m) => `${m.role}: ${m.content}`).join("\n\n");
2443
- const summaryPrompt = `Summarize this conversation concisely. Capture all key decisions, code changes, file paths mentioned, and current task state. Be thorough but brief:
2476
+ const focus = args.trim();
2477
+ const summaryPrompt = focus ? `Summarize this conversation, focusing especially on: ${focus}. Capture key decisions, code changes, file paths, and current state:
2478
+
2479
+ ${transcript}` : `Summarize this conversation concisely. Capture all key decisions, code changes, file paths mentioned, and current task state. Be thorough but brief:
2444
2480
 
2445
2481
  ${transcript}`;
2446
2482
  try {
@@ -2514,7 +2550,7 @@ var contextCommand = {
2514
2550
  }
2515
2551
  totalTokens += tokens;
2516
2552
  }
2517
- const sysTokens = 2e3;
2553
+ const sysTokens = context.systemPrompt ? estimateTokens(context.systemPrompt) : 2e3;
2518
2554
  totalTokens += sysTokens;
2519
2555
  const usage = totalTokens / contextLimit;
2520
2556
  const barWidth = 40;
@@ -2528,7 +2564,7 @@ var contextCommand = {
2528
2564
  ` ~${totalTokens.toLocaleString()} / ${contextLimit.toLocaleString()} tokens`,
2529
2565
  "",
2530
2566
  " Breakdown:",
2531
- ` System prompt: ~${sysTokens.toLocaleString()} tokens`,
2567
+ ` System prompt: ${context.systemPrompt ? "" : "~"}${sysTokens.toLocaleString()} tokens`,
2532
2568
  ...breakdown.map(
2533
2569
  (b) => ` ${b.role.padEnd(12)} ${b.count} msgs, ~${b.tokens.toLocaleString()} tokens`
2534
2570
  ),
@@ -2618,6 +2654,13 @@ async function saveSession(id, messages, model, cwd) {
2618
2654
  };
2619
2655
  await writeFile2(join2(SESSIONS_DIR, `${id}.json`), JSON.stringify(data, null, 2), "utf-8");
2620
2656
  }
2657
+ async function renameSession(id, title) {
2658
+ const session = await loadSession(id);
2659
+ if (!session) return;
2660
+ session.title = title;
2661
+ session.updatedAt = Date.now();
2662
+ await writeFile2(join2(SESSIONS_DIR, `${id}.json`), JSON.stringify(session, null, 2), "utf-8");
2663
+ }
2621
2664
  async function loadSession(id) {
2622
2665
  try {
2623
2666
  const raw = await readFile2(join2(SESSIONS_DIR, `${id}.json`), "utf-8");
@@ -3280,143 +3323,36 @@ var providerCommand = {
3280
3323
  }
3281
3324
  };
3282
3325
 
3283
- // src/fox-state.ts
3284
- var DEFAULT_STATS = {
3285
- name: "DarkFox",
3286
- happiness: 70,
3287
- hunger: 80,
3288
- energy: 90,
3289
- timesPetted: 0,
3290
- timesFed: 0,
3291
- createdAt: Date.now()
3292
- };
3293
- var stats = { ...DEFAULT_STATS };
3294
- var lastDecayTime = Date.now();
3295
- function getFoxStats() {
3296
- decay();
3297
- return { ...stats };
3298
- }
3299
- function setFoxName(name) {
3300
- stats.name = name;
3301
- }
3302
- function petFox() {
3303
- decay();
3304
- stats.happiness = Math.min(100, stats.happiness + 15);
3305
- stats.timesPetted++;
3306
- const reactions = [
3307
- `${stats.name} nuzzles your hand.`,
3308
- `${stats.name} purrs softly.`,
3309
- `${stats.name}'s tail wags happily.`,
3310
- `${stats.name} leans into the pets.`,
3311
- `${stats.name} rolls over for belly rubs.`,
3312
- `${stats.name} makes a happy chirping sound.`,
3313
- `${stats.name} bumps your hand with their nose.`
3314
- ];
3315
- if (stats.happiness >= 95) {
3316
- return `${stats.name} is absolutely overjoyed! Maximum floof achieved.`;
3317
- }
3318
- return reactions[stats.timesPetted % reactions.length];
3319
- }
3320
- function feedFox() {
3321
- decay();
3322
- stats.hunger = Math.min(100, stats.hunger + 25);
3323
- stats.happiness = Math.min(100, stats.happiness + 5);
3324
- stats.timesFed++;
3325
- const foods = [
3326
- "a small cookie",
3327
- "some berries",
3328
- "a piece of fish",
3329
- "a tiny sandwich",
3330
- "some trail mix",
3331
- "a warm dumpling",
3332
- "a bit of cheese"
3333
- ];
3334
- const food = foods[stats.timesFed % foods.length];
3335
- if (stats.hunger >= 95) {
3336
- return `${stats.name} nibbles ${food} contentedly. Completely stuffed!`;
3337
- }
3338
- return `${stats.name} happily munches on ${food}.`;
3339
- }
3340
- function restFox() {
3341
- decay();
3342
- stats.energy = Math.min(100, stats.energy + 30);
3343
- stats.happiness = Math.min(100, stats.happiness + 5);
3344
- return `${stats.name} curls up for a quick nap... Energy restored!`;
3345
- }
3346
- function decay() {
3347
- const now = Date.now();
3348
- const elapsed = (now - lastDecayTime) / 6e4;
3349
- if (elapsed < 1) return;
3350
- lastDecayTime = now;
3351
- const minutes = Math.floor(elapsed);
3352
- stats.hunger = Math.max(0, stats.hunger - minutes * 1.5);
3353
- stats.energy = Math.max(0, stats.energy - minutes * 0.5);
3354
- if (stats.hunger < 30) stats.happiness = Math.max(0, stats.happiness - minutes * 1);
3355
- if (stats.energy < 20) stats.happiness = Math.max(0, stats.happiness - minutes * 0.5);
3356
- }
3357
-
3358
- // src/commands/fox.ts
3359
- var petCommand = {
3360
- name: "pet",
3361
- description: "Pet your fox companion",
3362
- async execute(_args, _context) {
3363
- const reaction = petFox();
3364
- return { output: reaction, silent: true, foxMood: "pet" };
3365
- }
3366
- };
3367
- var feedCommand = {
3368
- name: "feed",
3369
- description: "Feed your fox companion",
3370
- async execute(_args, _context) {
3371
- const reaction = feedFox();
3372
- return { output: reaction, silent: true, foxMood: "eating" };
3373
- }
3374
- };
3375
- var restCommand = {
3376
- name: "rest",
3377
- aliases: ["sleep", "nap"],
3378
- description: "Let your fox take a nap",
3379
- async execute(_args, _context) {
3380
- const reaction = restFox();
3381
- return { output: reaction, silent: true, foxMood: "sleeping" };
3326
+ // src/commands/rename.ts
3327
+ var renameCommand = {
3328
+ name: "rename",
3329
+ description: "Rename the current session (usage: /rename <title>)",
3330
+ async execute(args, context) {
3331
+ const title = args.trim();
3332
+ if (!title) {
3333
+ return { output: "Usage: /rename <new session title>", silent: true };
3334
+ }
3335
+ await renameSession(context.sessionId, title);
3336
+ return { output: `Session renamed to: ${title}`, silent: true };
3382
3337
  }
3383
3338
  };
3384
- var foxCommand = {
3385
- name: "fox",
3386
- aliases: ["companion", "buddy"],
3387
- description: "Check on your fox companion (usage: /fox, /fox name <name>)",
3339
+
3340
+ // src/commands/plan.ts
3341
+ init_state();
3342
+ var planCommand = {
3343
+ name: "plan",
3344
+ description: "Enter plan mode to explore and design before implementing (usage: /plan <description>)",
3388
3345
  async execute(args, _context) {
3389
- const parts = args.trim().split(/\s+/);
3390
- if (parts[0] === "name" && parts[1]) {
3391
- const newName = parts.slice(1).join(" ");
3392
- setFoxName(newName);
3393
- return { output: `Your fox is now named "${newName}".`, silent: true };
3394
- }
3395
- const stats2 = getFoxStats();
3396
- const bar = (val) => {
3397
- const filled = Math.round(val / 5);
3398
- const empty = 20 - filled;
3399
- return "\x1B[36m" + "#".repeat(filled) + "\x1B[0m\x1B[2m" + "-".repeat(empty) + "\x1B[0m";
3346
+ const description = args.trim();
3347
+ if (!description) {
3348
+ return { output: "Usage: /plan <what you want to plan>", silent: true };
3349
+ }
3350
+ updateAppState((s) => ({ ...s, planMode: true }));
3351
+ return {
3352
+ output: `Enter plan mode. Explore the codebase and design a plan for: ${description}`,
3353
+ silent: false,
3354
+ foxMood: "thinking"
3400
3355
  };
3401
- const age = Math.floor((Date.now() - stats2.createdAt) / 6e4);
3402
- const ageStr = age < 60 ? `${age}m` : `${Math.floor(age / 60)}h ${age % 60}m`;
3403
- const lines = [
3404
- `\x1B[1m\x1B[36m${stats2.name}\x1B[0m`,
3405
- "",
3406
- ` Happiness [${bar(stats2.happiness)}] ${Math.round(stats2.happiness)}%`,
3407
- ` Hunger [${bar(stats2.hunger)}] ${Math.round(stats2.hunger)}%`,
3408
- ` Energy [${bar(stats2.energy)}] ${Math.round(stats2.energy)}%`,
3409
- "",
3410
- ` Times petted: ${stats2.timesPetted}`,
3411
- ` Times fed: ${stats2.timesFed}`,
3412
- ` Age: ${ageStr}`,
3413
- "",
3414
- "\x1B[2m /pet \u2014 Pet your fox",
3415
- " /feed \u2014 Feed your fox",
3416
- " /rest \u2014 Let your fox nap",
3417
- " /fox name <name> \u2014 Rename\x1B[0m"
3418
- ];
3419
- return { output: lines.join("\n"), silent: true };
3420
3356
  }
3421
3357
  };
3422
3358
 
@@ -3443,10 +3379,8 @@ var COMMANDS = [
3443
3379
  filesCommand,
3444
3380
  briefCommand,
3445
3381
  providerCommand,
3446
- petCommand,
3447
- feedCommand,
3448
- restCommand,
3449
- foxCommand
3382
+ renameCommand,
3383
+ planCommand
3450
3384
  ];
3451
3385
  function getCommands() {
3452
3386
  return COMMANDS;
@@ -3482,8 +3416,8 @@ function getCommandNames() {
3482
3416
  }
3483
3417
 
3484
3418
  // src/components/UserInput.tsx
3485
- import { Fragment as Fragment2, jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
3486
- var UserInput = memo5(function UserInput2({ value, onChange, onSubmit, disabled, history }) {
3419
+ import { Fragment as Fragment2, jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
3420
+ var UserInput = memo4(function UserInput2({ value, onChange, onSubmit, disabled, history }) {
3487
3421
  const [historyIdx, setHistoryIdx] = useState(-1);
3488
3422
  const [suggestion, setSuggestion] = useState("");
3489
3423
  useInput((_input, key) => {
@@ -3531,20 +3465,20 @@ var UserInput = memo5(function UserInput2({ value, onChange, onSubmit, disabled,
3531
3465
  const borderColor = disabled ? theme.dim : isBash ? theme.yellow : isCommand ? theme.purple : theme.cyan;
3532
3466
  const promptChar = isBash ? "!" : "\u276F";
3533
3467
  const promptColor = isBash ? theme.yellow : theme.cyan;
3534
- return /* @__PURE__ */ jsxs6(
3535
- Box6,
3468
+ return /* @__PURE__ */ jsxs5(
3469
+ Box5,
3536
3470
  {
3537
3471
  borderStyle: "round",
3538
3472
  borderColor,
3539
3473
  paddingLeft: 1,
3540
3474
  paddingRight: 1,
3541
3475
  children: [
3542
- /* @__PURE__ */ jsxs6(Text6, { color: disabled ? theme.dim : promptColor, bold: true, children: [
3476
+ /* @__PURE__ */ jsxs5(Text5, { color: disabled ? theme.dim : promptColor, bold: true, children: [
3543
3477
  promptChar,
3544
3478
  " "
3545
3479
  ] }),
3546
- disabled ? /* @__PURE__ */ jsx7(Text6, { color: theme.dim, children: "..." }) : /* @__PURE__ */ jsxs6(Fragment2, { children: [
3547
- /* @__PURE__ */ jsx7(
3480
+ disabled ? /* @__PURE__ */ jsx6(Text5, { color: theme.dim, children: "..." }) : /* @__PURE__ */ jsxs5(Fragment2, { children: [
3481
+ /* @__PURE__ */ jsx6(
3548
3482
  TextInput,
3549
3483
  {
3550
3484
  value,
@@ -3558,7 +3492,7 @@ var UserInput = memo5(function UserInput2({ value, onChange, onSubmit, disabled,
3558
3492
  }
3559
3493
  }
3560
3494
  ),
3561
- suggestion ? /* @__PURE__ */ jsx7(Text6, { color: theme.dim, children: suggestion }) : null
3495
+ suggestion ? /* @__PURE__ */ jsx6(Text5, { color: theme.dim, children: suggestion }) : null
3562
3496
  ] })
3563
3497
  ]
3564
3498
  }
@@ -3572,8 +3506,9 @@ init_system_prompt();
3572
3506
  init_providers();
3573
3507
  init_tools();
3574
3508
  init_bash();
3509
+ init_hooks();
3575
3510
  init_theme();
3576
- import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
3511
+ import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
3577
3512
  function getContextLimit3(model) {
3578
3513
  const lower = model.toLowerCase();
3579
3514
  if (lower.includes("llama3.1")) return 131072;
@@ -3585,10 +3520,10 @@ function getContextLimit3(model) {
3585
3520
  return 8192;
3586
3521
  }
3587
3522
  function REPL({ initialPrompt }) {
3588
- const { model: initialModel, systemPromptOverride } = useDarkfooContext();
3523
+ const { model: initialModel, systemPromptOverride, maxTurns, initialMessages, initialSessionId } = useDarkfooContext();
3589
3524
  const { exit } = useApp();
3590
3525
  const [model, setModel] = useState2(initialModel);
3591
- const [messages, setMessages] = useState2([]);
3526
+ const [messages, setMessages] = useState2(initialMessages ?? []);
3592
3527
  const [inputValue, setInputValue] = useState2("");
3593
3528
  const [isStreaming, setIsStreaming] = useState2(false);
3594
3529
  const [streamingText, setStreamingText] = useState2("");
@@ -3598,11 +3533,11 @@ function REPL({ initialPrompt }) {
3598
3533
  const [inputHistory, setInputHistory] = useState2([]);
3599
3534
  const [tokenCounts, setTokenCounts] = useState2({ input: 0, output: 0 });
3600
3535
  const [systemPrompt, setSystemPrompt] = useState2("");
3601
- const [foxMood, setFoxMood] = useState2("idle");
3536
+ const [mascotMood, setMascotMood] = useState2("idle");
3602
3537
  const [providerOnline, setProviderOnline] = useState2(true);
3603
3538
  const abortRef = useRef(null);
3604
3539
  const hasRun = useRef(false);
3605
- const sessionId = useRef(createSessionId());
3540
+ const sessionId = useRef(initialSessionId ?? createSessionId());
3606
3541
  const messagesRef = useRef(messages);
3607
3542
  messagesRef.current = messages;
3608
3543
  const modelRef = useRef(model);
@@ -3618,6 +3553,10 @@ function REPL({ initialPrompt }) {
3618
3553
  buildSystemPrompt(tools, cwd).then(setSystemPrompt);
3619
3554
  }
3620
3555
  }, []);
3556
+ useEffect(() => {
3557
+ executeHooks("session_start", { cwd }).catch(() => {
3558
+ });
3559
+ }, []);
3621
3560
  useEffect(() => {
3622
3561
  const provider = getProvider();
3623
3562
  provider.healthCheck().then((ok) => {
@@ -3655,12 +3594,14 @@ function REPL({ initialPrompt }) {
3655
3594
  messages,
3656
3595
  model,
3657
3596
  cwd,
3597
+ sessionId: sessionId.current,
3658
3598
  setModel,
3659
3599
  clearMessages,
3660
3600
  exit,
3661
- tokenCounts
3601
+ tokenCounts,
3602
+ systemPrompt
3662
3603
  });
3663
- commandContextRef.current = { messages, model, cwd, setModel, clearMessages, exit, tokenCounts };
3604
+ commandContextRef.current = { messages, model, cwd, sessionId: sessionId.current, setModel, clearMessages, exit, tokenCounts, systemPrompt };
3664
3605
  const addToHistory = useCallback2((input) => {
3665
3606
  setInputHistory((prev) => {
3666
3607
  const filtered = prev.filter((h) => h !== input);
@@ -3680,42 +3621,45 @@ function REPL({ initialPrompt }) {
3680
3621
  setStreamingText("");
3681
3622
  setToolResults([]);
3682
3623
  setCommandOutput(null);
3683
- setFoxMood("thinking");
3624
+ setMascotMood("thinking");
3684
3625
  const controller = new AbortController();
3685
3626
  abortRef.current = controller;
3686
3627
  const allMessages = [...messagesRef.current, userMsg];
3687
3628
  const currentModel = modelRef.current;
3688
3629
  const currentSystemPrompt = systemPromptRef.current;
3689
- setTokenCounts((prev) => ({
3690
- ...prev,
3691
- input: prev.input + Math.ceil(userMessage.length / 4)
3692
- }));
3693
3630
  try {
3694
3631
  for await (const event of query({
3695
3632
  model: currentModel,
3696
3633
  messages: allMessages,
3697
3634
  tools,
3698
3635
  systemPrompt: currentSystemPrompt,
3699
- signal: controller.signal
3636
+ signal: controller.signal,
3637
+ maxTurns
3700
3638
  })) {
3701
3639
  if (controller.signal.aborted) break;
3702
3640
  switch (event.type) {
3703
3641
  case "text_delta":
3704
3642
  setStreamingText((prev) => prev + event.text);
3705
- setFoxMood("idle");
3643
+ setMascotMood("idle");
3706
3644
  break;
3707
3645
  case "tool_call":
3708
3646
  setActiveTool({ name: event.toolCall.function.name, args: event.toolCall.function.arguments });
3709
- setFoxMood("working");
3647
+ setMascotMood("working");
3710
3648
  break;
3711
3649
  case "tool_result":
3712
3650
  setActiveTool(null);
3713
- setFoxMood(event.isError ? "error" : "working");
3651
+ setMascotMood(event.isError ? "error" : "working");
3714
3652
  setToolResults((prev) => [
3715
3653
  ...prev,
3716
3654
  { id: nanoid6(), toolName: event.toolName, output: event.output, isError: event.isError }
3717
3655
  ]);
3718
3656
  break;
3657
+ case "usage":
3658
+ setTokenCounts((prev) => ({
3659
+ input: prev.input + event.inputTokens,
3660
+ output: prev.output + event.outputTokens
3661
+ }));
3662
+ break;
3719
3663
  case "assistant_message":
3720
3664
  setMessages((prev) => {
3721
3665
  const updated = [...prev, event.message];
@@ -3723,14 +3667,10 @@ function REPL({ initialPrompt }) {
3723
3667
  });
3724
3668
  return updated;
3725
3669
  });
3726
- setTokenCounts((prev) => ({
3727
- ...prev,
3728
- output: prev.output + Math.ceil((event.message.content?.length ?? 0) / 4)
3729
- }));
3730
3670
  setStreamingText("");
3731
3671
  break;
3732
3672
  case "error":
3733
- setFoxMood("error");
3673
+ setMascotMood("error");
3734
3674
  setMessages((prev) => [
3735
3675
  ...prev,
3736
3676
  { id: nanoid6(), role: "assistant", content: `Error: ${event.error}`, timestamp: Date.now() }
@@ -3751,8 +3691,8 @@ function REPL({ initialPrompt }) {
3751
3691
  setStreamingText("");
3752
3692
  setActiveTool(null);
3753
3693
  setToolResults([]);
3754
- setFoxMood((prev) => prev === "error" ? "error" : "success");
3755
- setTimeout(() => setFoxMood("idle"), 2e3);
3694
+ setMascotMood((prev) => prev === "error" ? "error" : "success");
3695
+ setTimeout(() => setMascotMood("idle"), 2e3);
3756
3696
  abortRef.current = null;
3757
3697
  }
3758
3698
  },
@@ -3775,8 +3715,8 @@ function REPL({ initialPrompt }) {
3775
3715
  setMessages(result.replaceMessages);
3776
3716
  }
3777
3717
  if (result.foxMood) {
3778
- setFoxMood(result.foxMood);
3779
- setTimeout(() => setFoxMood("idle"), 3e3);
3718
+ setMascotMood(result.foxMood);
3719
+ setTimeout(() => setMascotMood("idle"), 3e3);
3780
3720
  }
3781
3721
  if (result.exit) return;
3782
3722
  if (!result.silent && result.output) {
@@ -3862,35 +3802,32 @@ ${result.content}
3862
3802
  }
3863
3803
  }
3864
3804
  });
3865
- return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", padding: 1, children: [
3866
- /* @__PURE__ */ jsx8(Banner, { model, cwd, providerName: getActiveProviderName(), providerOnline }),
3867
- /* @__PURE__ */ jsx8(Messages, { messages }),
3868
- commandOutput ? /* @__PURE__ */ jsx8(Box7, { marginBottom: 1, marginLeft: 2, flexDirection: "column", children: /* @__PURE__ */ jsx8(Text7, { children: commandOutput }) }) : null,
3869
- toolResults.map((tr) => /* @__PURE__ */ jsx8(ToolResultDisplay, { toolName: tr.toolName, output: tr.output, isError: tr.isError }, tr.id)),
3870
- activeTool ? /* @__PURE__ */ jsx8(ActiveToolCall, { toolName: activeTool.name, args: activeTool.args }) : null,
3871
- isStreaming && streamingText ? /* @__PURE__ */ jsxs7(Box7, { marginBottom: 1, children: [
3872
- /* @__PURE__ */ jsx8(Text7, { color: theme.cyan, children: "\u23BF " }),
3873
- /* @__PURE__ */ jsx8(Text7, { color: theme.text, wrap: "wrap", children: streamingText }),
3874
- /* @__PURE__ */ jsx8(Text7, { color: theme.dim, children: " ..." })
3805
+ return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", padding: 1, children: [
3806
+ /* @__PURE__ */ jsx7(Banner, { model, cwd, providerName: getActiveProviderName(), providerOnline, mood: mascotMood }),
3807
+ /* @__PURE__ */ jsx7(Messages, { messages }),
3808
+ commandOutput ? /* @__PURE__ */ jsx7(Box6, { marginBottom: 1, marginLeft: 2, flexDirection: "column", children: /* @__PURE__ */ jsx7(Text6, { children: commandOutput }) }) : null,
3809
+ toolResults.map((tr) => /* @__PURE__ */ jsx7(ToolResultDisplay, { toolName: tr.toolName, output: tr.output, isError: tr.isError }, tr.id)),
3810
+ activeTool ? /* @__PURE__ */ jsx7(ActiveToolCall, { toolName: activeTool.name, args: activeTool.args }) : null,
3811
+ isStreaming && streamingText ? /* @__PURE__ */ jsxs6(Box6, { marginBottom: 1, children: [
3812
+ /* @__PURE__ */ jsx7(Text6, { color: theme.cyan, children: "\u23BF " }),
3813
+ /* @__PURE__ */ jsx7(Text6, { color: theme.text, wrap: "wrap", children: streamingText }),
3814
+ /* @__PURE__ */ jsx7(Text6, { color: theme.dim, children: " ..." })
3875
3815
  ] }) : null,
3876
- isStreaming && !streamingText && !activeTool ? /* @__PURE__ */ jsxs7(Box7, { marginLeft: 2, children: [
3877
- /* @__PURE__ */ jsx8(Text7, { color: theme.cyan, children: "..." }),
3878
- /* @__PURE__ */ jsx8(Text7, { color: theme.dim, children: " Thinking" })
3816
+ isStreaming && !streamingText && !activeTool ? /* @__PURE__ */ jsxs6(Box6, { marginLeft: 2, children: [
3817
+ /* @__PURE__ */ jsx7(Text6, { color: theme.cyan, children: "..." }),
3818
+ /* @__PURE__ */ jsx7(Text6, { color: theme.dim, children: " Thinking" })
3879
3819
  ] }) : null,
3880
- /* @__PURE__ */ jsxs7(Box7, { children: [
3881
- /* @__PURE__ */ jsx8(Box7, { flexDirection: "column", flexGrow: 1, children: /* @__PURE__ */ jsx8(
3882
- UserInput,
3883
- {
3884
- value: inputValue,
3885
- onChange: setInputValue,
3886
- onSubmit: handleSubmit,
3887
- disabled: isStreaming,
3888
- history: inputHistory
3889
- }
3890
- ) }),
3891
- /* @__PURE__ */ jsx8(Box7, { marginLeft: 1, children: /* @__PURE__ */ jsx8(Fox, { mood: foxMood }) })
3892
- ] }),
3893
- /* @__PURE__ */ jsx8(
3820
+ /* @__PURE__ */ jsx7(
3821
+ UserInput,
3822
+ {
3823
+ value: inputValue,
3824
+ onChange: setInputValue,
3825
+ onSubmit: handleSubmit,
3826
+ disabled: isStreaming,
3827
+ history: inputHistory
3828
+ }
3829
+ ),
3830
+ /* @__PURE__ */ jsx7(
3894
3831
  StatusLine,
3895
3832
  {
3896
3833
  model,
@@ -3904,10 +3841,15 @@ ${result.content}
3904
3841
 
3905
3842
  // src/main.tsx
3906
3843
  init_providers();
3907
- import { jsx as jsx9 } from "react/jsx-runtime";
3844
+ import { jsx as jsx8 } from "react/jsx-runtime";
3908
3845
  var program = new Command();
3909
- program.name("darkfoo").description("Darkfoo Code \u2014 local AI coding assistant powered by local LLM providers").version("0.2.1").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) => {
3846
+ program.name("darkfoo").description("Darkfoo Code \u2014 local AI coding assistant powered by local LLM providers").version("0.4.0").option("-m, --model <model>", "Model to use", "llama3.1:8b").option("-p, --prompt <prompt>", "Run a single prompt (non-interactive)").option("-c, --continue", "Resume the most recent session").option("--resume <id>", "Resume a specific session by ID").option("--max-turns <n>", "Maximum tool-use turns per query", "30").option("--debug", "Enable debug logging to stderr").option("--output-format <format>", "Output format for non-interactive mode (text, json)").option("--provider <name>", "LLM provider backend (ollama, llama-cpp, vllm, tgi, etc.)").option("--system-prompt <prompt>", "Override the system prompt").action(async (options) => {
3910
3847
  const { model, prompt, provider, systemPrompt } = options;
3848
+ if (options.debug) {
3849
+ const { setDebugMode: setDebugMode2 } = await Promise.resolve().then(() => (init_debug(), debug_exports));
3850
+ setDebugMode2(true);
3851
+ }
3852
+ const maxTurns = parseInt(options.maxTurns, 10) || 30;
3911
3853
  await loadProviderSettings();
3912
3854
  if (provider) {
3913
3855
  try {
@@ -3956,19 +3898,35 @@ program.name("darkfoo").description("Darkfoo Code \u2014 local AI coding assista
3956
3898
  controller.abort();
3957
3899
  process.exit(0);
3958
3900
  });
3901
+ const jsonMode = options.outputFormat === "json";
3902
+ let jsonContent = "";
3903
+ const jsonToolCalls = [];
3904
+ const jsonToolResults = [];
3959
3905
  for await (const event of query2({
3960
3906
  model: resolvedModel,
3961
3907
  messages: [userMsg],
3962
3908
  tools,
3963
3909
  systemPrompt: sysPrompt,
3964
- signal: controller.signal
3910
+ signal: controller.signal,
3911
+ maxTurns
3965
3912
  })) {
3966
3913
  switch (event.type) {
3967
3914
  case "text_delta":
3968
- process.stdout.write(event.text);
3915
+ if (jsonMode) {
3916
+ jsonContent += event.text;
3917
+ } else {
3918
+ process.stdout.write(event.text);
3919
+ }
3920
+ break;
3921
+ case "tool_call":
3922
+ if (jsonMode) {
3923
+ jsonToolCalls.push({ name: event.toolCall.function.name, arguments: event.toolCall.function.arguments });
3924
+ }
3969
3925
  break;
3970
3926
  case "tool_result":
3971
- if (event.isError) {
3927
+ if (jsonMode) {
3928
+ jsonToolResults.push({ tool: event.toolName, output: event.output, isError: event.isError });
3929
+ } else if (event.isError) {
3972
3930
  process.stderr.write(`
3973
3931
  [${event.toolName}] Error: ${event.output}
3974
3932
  `);
@@ -3981,11 +3939,56 @@ Error: ${event.error}
3981
3939
  break;
3982
3940
  }
3983
3941
  }
3984
- process.stdout.write("\n");
3942
+ if (jsonMode) {
3943
+ const result = { role: "assistant", content: jsonContent };
3944
+ if (jsonToolCalls.length > 0) result.toolCalls = jsonToolCalls;
3945
+ if (jsonToolResults.length > 0) result.toolResults = jsonToolResults;
3946
+ process.stdout.write(JSON.stringify(result) + "\n");
3947
+ } else {
3948
+ process.stdout.write("\n");
3949
+ }
3985
3950
  process.exit(0);
3986
3951
  }
3952
+ let initialMessages;
3953
+ let initialSessionId;
3954
+ if (options.continue) {
3955
+ const sessions = await listSessions();
3956
+ if (sessions.length > 0) {
3957
+ const session = await loadSession(sessions[0].id);
3958
+ if (session) {
3959
+ initialMessages = session.messages;
3960
+ initialSessionId = session.id;
3961
+ process.stderr.write(`Resuming session: ${session.title}
3962
+ `);
3963
+ }
3964
+ } else {
3965
+ process.stderr.write("No previous sessions found.\n");
3966
+ }
3967
+ } else if (options.resume) {
3968
+ const session = await loadSession(options.resume);
3969
+ if (session) {
3970
+ initialMessages = session.messages;
3971
+ initialSessionId = session.id;
3972
+ process.stderr.write(`Resuming session: ${session.title}
3973
+ `);
3974
+ } else {
3975
+ process.stderr.write(`Session not found: ${options.resume}
3976
+ `);
3977
+ process.exit(1);
3978
+ }
3979
+ }
3987
3980
  const { waitUntilExit } = render(
3988
- /* @__PURE__ */ jsx9(App, { model: resolvedModel, systemPromptOverride: systemPrompt, children: /* @__PURE__ */ jsx9(REPL, {}) })
3981
+ /* @__PURE__ */ jsx8(
3982
+ App,
3983
+ {
3984
+ model: resolvedModel,
3985
+ systemPromptOverride: systemPrompt,
3986
+ maxTurns,
3987
+ initialMessages,
3988
+ initialSessionId,
3989
+ children: /* @__PURE__ */ jsx8(REPL, {})
3990
+ }
3991
+ )
3989
3992
  );
3990
3993
  await waitUntilExit();
3991
3994
  });