darkfoo-code 0.2.5 → 0.3.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 +331 -340
  2. package/package.json +1 -1
package/dist/main.js CHANGED
@@ -151,6 +151,13 @@ var init_ollama = __esm({
151
151
  yield { type: "tool_call", toolCall: normalizeToolCall(tc) };
152
152
  }
153
153
  }
154
+ if (chunk.done && (chunk.eval_count || chunk.prompt_eval_count)) {
155
+ yield {
156
+ type: "usage",
157
+ inputTokens: chunk.prompt_eval_count ?? 0,
158
+ outputTokens: chunk.eval_count ?? 0
159
+ };
160
+ }
154
161
  }
155
162
  }
156
163
  } finally {
@@ -299,6 +306,13 @@ var init_openai_compat = __esm({
299
306
  }
300
307
  }
301
308
  pendingToolCalls.clear();
309
+ if (chunk.usage) {
310
+ yield {
311
+ type: "usage",
312
+ inputTokens: chunk.usage.prompt_tokens ?? 0,
313
+ outputTokens: chunk.usage.completion_tokens ?? 0
314
+ };
315
+ }
302
316
  }
303
317
  }
304
318
  }
@@ -549,6 +563,88 @@ var init_permissions = __esm({
549
563
  }
550
564
  });
551
565
 
566
+ // src/hooks.ts
567
+ import { execFile as execFile4 } from "child_process";
568
+ import { readFile as readFile5 } from "fs/promises";
569
+ import { join as join6 } from "path";
570
+ async function loadHooks() {
571
+ if (cachedHooks) return cachedHooks;
572
+ try {
573
+ const raw = await readFile5(SETTINGS_PATH4, "utf-8");
574
+ const config = JSON.parse(raw);
575
+ cachedHooks = config.hooks ?? [];
576
+ return cachedHooks;
577
+ } catch {
578
+ cachedHooks = [];
579
+ return [];
580
+ }
581
+ }
582
+ async function executeHooks(event, context) {
583
+ const hooks = await loadHooks();
584
+ const matching = hooks.filter((h) => {
585
+ if (h.event !== event) return false;
586
+ if (h.toolFilter && context.toolName && !h.toolFilter.includes(context.toolName)) return false;
587
+ return true;
588
+ });
589
+ const results = [];
590
+ for (const hook of matching) {
591
+ try {
592
+ const output = await new Promise((resolve8, reject) => {
593
+ execFile4("bash", ["-c", hook.command], {
594
+ cwd: context.cwd,
595
+ timeout: 1e4,
596
+ env: {
597
+ ...process.env,
598
+ DARKFOO_EVENT: event,
599
+ DARKFOO_TOOL: context.toolName ?? ""
600
+ }
601
+ }, (err, stdout) => {
602
+ if (err) reject(err);
603
+ else resolve8(stdout.trim());
604
+ });
605
+ });
606
+ if (output) results.push(output);
607
+ } catch {
608
+ }
609
+ }
610
+ return results;
611
+ }
612
+ var SETTINGS_PATH4, cachedHooks;
613
+ var init_hooks = __esm({
614
+ "src/hooks.ts"() {
615
+ "use strict";
616
+ SETTINGS_PATH4 = join6(process.env.HOME || "~", ".darkfoo", "settings.json");
617
+ cachedHooks = null;
618
+ }
619
+ });
620
+
621
+ // src/utils/debug.ts
622
+ var debug_exports = {};
623
+ __export(debug_exports, {
624
+ debug: () => debug,
625
+ isDebugMode: () => isDebugMode,
626
+ setDebugMode: () => setDebugMode
627
+ });
628
+ function setDebugMode(on) {
629
+ debugMode = on;
630
+ }
631
+ function isDebugMode() {
632
+ return debugMode;
633
+ }
634
+ function debug(msg) {
635
+ if (debugMode) {
636
+ process.stderr.write(`[debug] ${msg}
637
+ `);
638
+ }
639
+ }
640
+ var debugMode;
641
+ var init_debug = __esm({
642
+ "src/utils/debug.ts"() {
643
+ "use strict";
644
+ debugMode = false;
645
+ }
646
+ });
647
+
552
648
  // src/query.ts
553
649
  var query_exports = {};
554
650
  __export(query_exports, {
@@ -563,6 +659,7 @@ async function* query(params) {
563
659
  const ollamaTools = tools.map((t) => t.toOllamaToolDef());
564
660
  while (turns < maxTurns) {
565
661
  turns++;
662
+ debug(`query turn ${turns}/${maxTurns}, ${messages.length} messages`);
566
663
  const ollamaMessages = toOllamaMessages(messages, systemPrompt);
567
664
  let assistantContent = "";
568
665
  const toolCalls = [];
@@ -588,7 +685,11 @@ async function* query(params) {
588
685
  };
589
686
  messages.push(assistantMsg);
590
687
  yield { type: "assistant_message", message: assistantMsg };
591
- if (toolCalls.length === 0) return;
688
+ if (toolCalls.length === 0) {
689
+ debug("no tool calls, query complete");
690
+ return;
691
+ }
692
+ debug(`${toolCalls.length} tool calls: ${toolCalls.map((tc) => tc.function.name).join(", ")}`);
592
693
  const readOnlyCalls = [];
593
694
  const writeCalls = [];
594
695
  const unknownCalls = [];
@@ -611,10 +712,14 @@ async function* query(params) {
611
712
  const results = await Promise.all(
612
713
  readOnlyCalls.map(async ({ tc, tool }) => {
613
714
  const coercedArgs = coerceToolArgs(tc.function.arguments, tool);
715
+ await executeHooks("pre_tool", { toolName: tool.name, cwd: process.cwd() });
614
716
  try {
615
- return { tool, result: await tool.call(coercedArgs, { cwd: process.cwd(), abortSignal: signal }) };
717
+ const result = await tool.call(coercedArgs, { cwd: process.cwd(), abortSignal: signal });
718
+ await executeHooks("post_tool", { toolName: tool.name, cwd: process.cwd() });
719
+ return { tool, result };
616
720
  } catch (err) {
617
721
  const msg = err instanceof Error ? err.message : String(err);
722
+ await executeHooks("post_tool", { toolName: tool.name, cwd: process.cwd() });
618
723
  return { tool, result: { output: `Tool execution error: ${msg}`, isError: true } };
619
724
  }
620
725
  })
@@ -625,6 +730,12 @@ async function* query(params) {
625
730
  }
626
731
  }
627
732
  for (const { tc, tool } of writeCalls) {
733
+ if (getAppState().planMode) {
734
+ const blocked = `Tool "${tool.name}" is blocked in plan mode. Use ExitPlanMode first.`;
735
+ yield { type: "tool_result", toolName: tool.name, output: blocked, isError: true };
736
+ messages.push({ id: nanoid3(), role: "tool", content: blocked, toolName: tool.name, timestamp: Date.now() });
737
+ continue;
738
+ }
628
739
  const coercedArgs = coerceToolArgs(tc.function.arguments, tool);
629
740
  const permission = await checkPermission(tool, coercedArgs);
630
741
  if (permission === "deny") {
@@ -633,6 +744,7 @@ async function* query(params) {
633
744
  messages.push({ id: nanoid3(), role: "tool", content: denied, toolName: tool.name, timestamp: Date.now() });
634
745
  continue;
635
746
  }
747
+ await executeHooks("pre_tool", { toolName: tool.name, cwd: process.cwd() });
636
748
  let result;
637
749
  try {
638
750
  result = await tool.call(coercedArgs, { cwd: process.cwd(), abortSignal: signal });
@@ -640,6 +752,7 @@ async function* query(params) {
640
752
  const msg = err instanceof Error ? err.message : String(err);
641
753
  result = { output: `Tool execution error: ${msg}`, isError: true };
642
754
  }
755
+ await executeHooks("post_tool", { toolName: tool.name, cwd: process.cwd() });
643
756
  yield { type: "tool_result", toolName: tool.name, output: result.output, isError: result.isError ?? false };
644
757
  messages.push({ id: nanoid3(), role: "tool", content: result.output, toolName: tool.name, timestamp: Date.now() });
645
758
  }
@@ -691,12 +804,15 @@ var init_query = __esm({
691
804
  "use strict";
692
805
  init_providers();
693
806
  init_permissions();
807
+ init_hooks();
808
+ init_state();
809
+ init_debug();
694
810
  }
695
811
  });
696
812
 
697
813
  // 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";
814
+ import { readFile as readFile6, readdir as readdir2, access } from "fs/promises";
815
+ import { join as join7, dirname, resolve } from "path";
700
816
  async function loadProjectContext(cwd) {
701
817
  const parts = [];
702
818
  const globalContent = await tryRead(HOME_CONTEXT);
@@ -704,13 +820,27 @@ async function loadProjectContext(cwd) {
704
820
  parts.push(`# Global instructions (~/.darkfoo/DARKFOO.md)
705
821
 
706
822
  ${globalContent}`);
823
+ }
824
+ const userRulesDir = join7(process.env.HOME || "~", ".darkfoo", "rules");
825
+ try {
826
+ await access(userRulesDir);
827
+ const userRuleFiles = await readdir2(userRulesDir);
828
+ for (const file of userRuleFiles.filter((f) => f.endsWith(".md")).sort()) {
829
+ const content = await tryRead(join7(userRulesDir, file));
830
+ if (content) {
831
+ parts.push(`# User rule: ${file}
832
+
833
+ ${content}`);
834
+ }
835
+ }
836
+ } catch {
707
837
  }
708
838
  const visited = /* @__PURE__ */ new Set();
709
839
  let dir = resolve(cwd);
710
840
  while (dir && !visited.has(dir)) {
711
841
  visited.add(dir);
712
842
  for (const file of CONTEXT_FILES) {
713
- const filePath = join6(dir, file);
843
+ const filePath = join7(dir, file);
714
844
  const content = await tryRead(filePath);
715
845
  if (content) {
716
846
  const rel = filePath.replace(cwd + "/", "");
@@ -723,12 +853,12 @@ ${content}`);
723
853
  if (parent === dir) break;
724
854
  dir = parent;
725
855
  }
726
- const rulesDir = join6(cwd, RULES_DIR);
856
+ const rulesDir = join7(cwd, RULES_DIR);
727
857
  try {
728
858
  await access(rulesDir);
729
859
  const files = await readdir2(rulesDir);
730
860
  for (const file of files.filter((f) => f.endsWith(".md")).sort()) {
731
- const content = await tryRead(join6(rulesDir, file));
861
+ const content = await tryRead(join7(rulesDir, file));
732
862
  if (content) {
733
863
  parts.push(`# Rule: ${file}
734
864
 
@@ -741,7 +871,7 @@ ${content}`);
741
871
  }
742
872
  async function tryRead(path) {
743
873
  try {
744
- return await readFile5(path, "utf-8");
874
+ return await readFile6(path, "utf-8");
745
875
  } catch {
746
876
  return null;
747
877
  }
@@ -752,7 +882,7 @@ var init_context_loader = __esm({
752
882
  "use strict";
753
883
  CONTEXT_FILES = ["DARKFOO.md", ".darkfoo/DARKFOO.md"];
754
884
  RULES_DIR = ".darkfoo/rules";
755
- HOME_CONTEXT = join6(process.env.HOME || "~", ".darkfoo", "DARKFOO.md");
885
+ HOME_CONTEXT = join7(process.env.HOME || "~", ".darkfoo", "DARKFOO.md");
756
886
  }
757
887
  });
758
888
 
@@ -847,14 +977,14 @@ var init_types = __esm({
847
977
 
848
978
  // src/tools/bash.ts
849
979
  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";
980
+ import { writeFile as writeFile6, mkdir as mkdir6, readFile as readFile7 } from "fs/promises";
981
+ import { join as join8 } from "path";
852
982
  import { nanoid as nanoid4 } from "nanoid";
853
983
  import { z } from "zod";
854
984
  async function runInBackground(command, cwd) {
855
985
  const taskId = nanoid4(8);
856
986
  await mkdir6(BG_OUTPUT_DIR, { recursive: true });
857
- const outputPath = join7(BG_OUTPUT_DIR, `${taskId}.output`);
987
+ const outputPath = join8(BG_OUTPUT_DIR, `${taskId}.output`);
858
988
  backgroundTasks.set(taskId, { command, status: "running", outputPath });
859
989
  const proc = spawn(command, {
860
990
  shell: true,
@@ -906,7 +1036,7 @@ var init_bash = __esm({
906
1036
  run_in_background: z.boolean().optional().describe("Run in background, returning a task ID for later retrieval")
907
1037
  });
908
1038
  MAX_OUTPUT = 1e5;
909
- BG_OUTPUT_DIR = join7(process.env.HOME || "~", ".darkfoo", "bg-tasks");
1039
+ BG_OUTPUT_DIR = join8(process.env.HOME || "~", ".darkfoo", "bg-tasks");
910
1040
  backgroundTasks = /* @__PURE__ */ new Map();
911
1041
  BashTool = {
912
1042
  name: "Bash",
@@ -967,17 +1097,17 @@ var init_bash = __esm({
967
1097
  });
968
1098
 
969
1099
  // src/tools/read.ts
970
- import { readFile as readFile7, stat } from "fs/promises";
1100
+ import { readFile as readFile8, stat } from "fs/promises";
971
1101
  import { extname, resolve as resolve2 } from "path";
972
1102
  import { z as z2 } from "zod";
973
1103
  async function readImage(filePath, size) {
974
1104
  const ext = extname(filePath).toLowerCase();
975
1105
  if (ext === ".svg") {
976
- const content = await readFile7(filePath, "utf-8");
1106
+ const content = await readFile8(filePath, "utf-8");
977
1107
  return { output: `SVG image (${size} bytes):
978
1108
  ${content.slice(0, 5e3)}` };
979
1109
  }
980
- const buffer = await readFile7(filePath);
1110
+ const buffer = await readFile8(filePath);
981
1111
  const base64 = buffer.toString("base64");
982
1112
  const sizeKB = (size / 1024).toFixed(1);
983
1113
  const dims = detectImageDimensions(buffer, ext);
@@ -990,9 +1120,9 @@ Base64 length: ${base64.length} chars
990
1120
  };
991
1121
  }
992
1122
  async function readPdf(filePath) {
993
- const { execFile: execFile6 } = await import("child_process");
1123
+ const { execFile: execFile7 } = await import("child_process");
994
1124
  return new Promise((resolve8) => {
995
- execFile6("pdftotext", [filePath, "-"], { timeout: 15e3, maxBuffer: 5 * 1024 * 1024 }, (err, stdout) => {
1125
+ execFile7("pdftotext", [filePath, "-"], { timeout: 15e3, maxBuffer: 5 * 1024 * 1024 }, (err, stdout) => {
996
1126
  if (err) {
997
1127
  resolve8({
998
1128
  output: `PDF file: ${filePath}
@@ -1069,7 +1199,7 @@ var init_read = __esm({
1069
1199
  if (ext === PDF_EXT) {
1070
1200
  return readPdf(filePath);
1071
1201
  }
1072
- const raw = await readFile7(filePath, "utf-8");
1202
+ const raw = await readFile8(filePath, "utf-8");
1073
1203
  markFileRead(filePath, raw);
1074
1204
  const lines = raw.split("\n");
1075
1205
  const offset = parsed.offset ?? 0;
@@ -1140,7 +1270,7 @@ var init_write = __esm({
1140
1270
  });
1141
1271
 
1142
1272
  // src/tools/edit.ts
1143
- import { readFile as readFile8, writeFile as writeFile8 } from "fs/promises";
1273
+ import { readFile as readFile9, writeFile as writeFile8 } from "fs/promises";
1144
1274
  import { resolve as resolve4 } from "path";
1145
1275
  import { createPatch } from "diff";
1146
1276
  import { z as z4 } from "zod";
@@ -1186,7 +1316,7 @@ var init_edit = __esm({
1186
1316
  isError: true
1187
1317
  };
1188
1318
  }
1189
- const content = await readFile8(filePath, "utf-8");
1319
+ const content = await readFile9(filePath, "utf-8");
1190
1320
  const actualOld = findActualString(content, parsed.old_string);
1191
1321
  if (!actualOld) {
1192
1322
  return {
@@ -1218,7 +1348,7 @@ var init_edit = __esm({
1218
1348
  });
1219
1349
 
1220
1350
  // src/tools/grep.ts
1221
- import { execFile as execFile4 } from "child_process";
1351
+ import { execFile as execFile5 } from "child_process";
1222
1352
  import { resolve as resolve5 } from "path";
1223
1353
  import { z as z5 } from "zod";
1224
1354
  var INPUT_SCHEMA5, DEFAULT_HEAD_LIMIT, GrepTool;
@@ -1278,7 +1408,7 @@ var init_grep = __esm({
1278
1408
  }
1279
1409
  args.push(searchPath);
1280
1410
  return new Promise((resolve8) => {
1281
- execFile4("rg", args, { maxBuffer: 10 * 1024 * 1024, timeout: 3e4 }, (err, stdout, stderr) => {
1411
+ execFile5("rg", args, { maxBuffer: 10 * 1024 * 1024, timeout: 3e4 }, (err, stdout, stderr) => {
1282
1412
  if (err && !stdout) {
1283
1413
  if (err.code === 1 || err.code === "1") {
1284
1414
  resolve8({ output: "No matches found." });
@@ -1314,7 +1444,7 @@ ${output}`;
1314
1444
  });
1315
1445
 
1316
1446
  // src/tools/glob.ts
1317
- import { execFile as execFile5 } from "child_process";
1447
+ import { execFile as execFile6 } from "child_process";
1318
1448
  import { resolve as resolve6 } from "path";
1319
1449
  import { z as z6 } from "zod";
1320
1450
  var INPUT_SCHEMA6, MAX_RESULTS, GlobTool;
@@ -1348,7 +1478,7 @@ var init_glob = __esm({
1348
1478
  searchPath
1349
1479
  ];
1350
1480
  return new Promise((resolve8) => {
1351
- execFile5("rg", args, { maxBuffer: 10 * 1024 * 1024, timeout: 3e4 }, (err, stdout, stderr) => {
1481
+ execFile6("rg", args, { maxBuffer: 10 * 1024 * 1024, timeout: 3e4 }, (err, stdout, stderr) => {
1352
1482
  if (err && !stdout) {
1353
1483
  if (err.code === 1 || err.code === "1") {
1354
1484
  resolve8({ output: "No files matched." });
@@ -1599,7 +1729,7 @@ var init_agent = __esm({
1599
1729
  });
1600
1730
 
1601
1731
  // src/tools/notebook-edit.ts
1602
- import { readFile as readFile9, writeFile as writeFile9 } from "fs/promises";
1732
+ import { readFile as readFile10, writeFile as writeFile9 } from "fs/promises";
1603
1733
  import { resolve as resolve7 } from "path";
1604
1734
  import { z as z10 } from "zod";
1605
1735
  var INPUT_SCHEMA10, NotebookEditTool;
@@ -1622,7 +1752,7 @@ var init_notebook_edit = __esm({
1622
1752
  const parsed = INPUT_SCHEMA10.parse(input);
1623
1753
  const filePath = resolve7(context.cwd, parsed.file_path);
1624
1754
  try {
1625
- const raw = await readFile9(filePath, "utf-8");
1755
+ const raw = await readFile10(filePath, "utf-8");
1626
1756
  const notebook = JSON.parse(raw);
1627
1757
  if (parsed.cell_index < 0 || parsed.cell_index >= notebook.cells.length) {
1628
1758
  return {
@@ -1921,8 +2051,8 @@ var DarkfooContext = createContext({ model: "qwen2.5-coder:32b" });
1921
2051
  function useDarkfooContext() {
1922
2052
  return useContext(DarkfooContext);
1923
2053
  }
1924
- function App({ model, systemPromptOverride, children }) {
1925
- return /* @__PURE__ */ jsx(DarkfooContext.Provider, { value: { model, systemPromptOverride }, children });
2054
+ function App({ model, systemPromptOverride, maxTurns, initialMessages, initialSessionId, children }) {
2055
+ return /* @__PURE__ */ jsx(DarkfooContext.Provider, { value: { model, systemPromptOverride, maxTurns, initialMessages, initialSessionId }, children });
1926
2056
  }
1927
2057
 
1928
2058
  // src/repl.tsx
@@ -1935,128 +2065,25 @@ init_theme();
1935
2065
  import { memo as memo2 } from "react";
1936
2066
  import { Box as Box2, Text as Text2 } from "ink";
1937
2067
 
1938
- // src/components/Fox.tsx
1939
- init_theme();
2068
+ // src/components/Mascot.tsx
1940
2069
  import { memo } from "react";
1941
2070
  import { Box, Text } from "ink";
1942
2071
  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 = {
2072
+ var MOOD_COLORS = {
2018
2073
  idle: "#5eead4",
2019
2074
  thinking: "#a78bfa",
2020
2075
  working: "#fbbf24",
2021
2076
  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"
2077
+ error: "#f472b6"
2038
2078
  };
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) + "'" })
2079
+ var Mascot = memo(function Mascot2({ mood = "idle" }) {
2080
+ const color = MOOD_COLORS[mood];
2081
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", alignItems: "center", children: [
2082
+ /* @__PURE__ */ jsx2(Text, { color, children: " \u259F\u2588\u2588\u2599" }),
2083
+ /* @__PURE__ */ jsx2(Text, { color, children: " \u2590\u2588\u2588\u2588\u2588\u258C" }),
2084
+ /* @__PURE__ */ jsx2(Text, { color, children: " \u259C\u2588\u2588\u259B" })
2058
2085
  ] });
2059
- }
2086
+ });
2060
2087
 
2061
2088
  // src/components/Banner.tsx
2062
2089
  import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
@@ -2088,26 +2115,20 @@ function gradientBar(width) {
2088
2115
  color: GRADIENT[Math.round(i / (width - 1) * (GRADIENT.length - 1))]
2089
2116
  }));
2090
2117
  }
2091
- var Banner = memo2(function Banner2({ model, cwd, providerName, providerOnline }) {
2118
+ var Banner = memo2(function Banner2({ model, cwd, providerName, providerOnline, mood }) {
2092
2119
  const bar = gradientBar(60);
2093
2120
  const statusTag = providerOnline ? "[connected]" : "[offline]";
2094
2121
  const statusColor = providerOnline ? theme.green ?? "#4ade80" : theme.pink ?? "#f472b6";
2095
2122
  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 })
2100
- ] }),
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." })
2104
- ] })
2123
+ /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", alignItems: "center", children: [
2124
+ LOGO_LINES.map((line, i) => /* @__PURE__ */ jsx3(Text2, { color: lineColor(i), bold: true, children: line }, i)),
2125
+ /* @__PURE__ */ jsx3(Text2, { color: theme.purple ?? "#a78bfa", bold: true, children: SUBTITLE })
2105
2126
  ] }),
2106
- /* @__PURE__ */ jsx3(Text2, { children: " " }),
2107
- /* @__PURE__ */ jsxs2(Text2, { children: [
2127
+ /* @__PURE__ */ jsx3(Box2, { justifyContent: "center", marginTop: 1, children: /* @__PURE__ */ jsx3(Mascot, { mood }) }),
2128
+ /* @__PURE__ */ jsx3(Box2, { marginTop: 1, children: /* @__PURE__ */ jsxs2(Text2, { children: [
2108
2129
  " ",
2109
2130
  bar.map((d, i) => /* @__PURE__ */ jsx3(Text2, { color: d.color, children: d.char }, i))
2110
- ] }),
2131
+ ] }) }),
2111
2132
  /* @__PURE__ */ jsxs2(Box2, { marginTop: 1, marginLeft: 2, children: [
2112
2133
  /* @__PURE__ */ jsx3(Text2, { color: theme.dim ?? "#7e8ea6", children: "model " }),
2113
2134
  model ? /* @__PURE__ */ jsx3(Text2, { color: theme.cyan ?? "#5eead4", bold: true, children: model }) : /* @__PURE__ */ jsx3(Text2, { color: theme.dim ?? "#7e8ea6", children: "--" })
@@ -2435,12 +2456,15 @@ import { nanoid } from "nanoid";
2435
2456
  var compactCommand = {
2436
2457
  name: "compact",
2437
2458
  description: "Compress conversation history to save context",
2438
- async execute(_args, context) {
2459
+ async execute(args, context) {
2439
2460
  if (context.messages.length < 4) {
2440
2461
  return { output: "Conversation is too short to compact.", silent: true };
2441
2462
  }
2442
2463
  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:
2464
+ const focus = args.trim();
2465
+ const summaryPrompt = focus ? `Summarize this conversation, focusing especially on: ${focus}. Capture key decisions, code changes, file paths, and current state:
2466
+
2467
+ ${transcript}` : `Summarize this conversation concisely. Capture all key decisions, code changes, file paths mentioned, and current task state. Be thorough but brief:
2444
2468
 
2445
2469
  ${transcript}`;
2446
2470
  try {
@@ -2514,7 +2538,7 @@ var contextCommand = {
2514
2538
  }
2515
2539
  totalTokens += tokens;
2516
2540
  }
2517
- const sysTokens = 2e3;
2541
+ const sysTokens = context.systemPrompt ? estimateTokens(context.systemPrompt) : 2e3;
2518
2542
  totalTokens += sysTokens;
2519
2543
  const usage = totalTokens / contextLimit;
2520
2544
  const barWidth = 40;
@@ -2528,7 +2552,7 @@ var contextCommand = {
2528
2552
  ` ~${totalTokens.toLocaleString()} / ${contextLimit.toLocaleString()} tokens`,
2529
2553
  "",
2530
2554
  " Breakdown:",
2531
- ` System prompt: ~${sysTokens.toLocaleString()} tokens`,
2555
+ ` System prompt: ${context.systemPrompt ? "" : "~"}${sysTokens.toLocaleString()} tokens`,
2532
2556
  ...breakdown.map(
2533
2557
  (b) => ` ${b.role.padEnd(12)} ${b.count} msgs, ~${b.tokens.toLocaleString()} tokens`
2534
2558
  ),
@@ -2618,6 +2642,13 @@ async function saveSession(id, messages, model, cwd) {
2618
2642
  };
2619
2643
  await writeFile2(join2(SESSIONS_DIR, `${id}.json`), JSON.stringify(data, null, 2), "utf-8");
2620
2644
  }
2645
+ async function renameSession(id, title) {
2646
+ const session = await loadSession(id);
2647
+ if (!session) return;
2648
+ session.title = title;
2649
+ session.updatedAt = Date.now();
2650
+ await writeFile2(join2(SESSIONS_DIR, `${id}.json`), JSON.stringify(session, null, 2), "utf-8");
2651
+ }
2621
2652
  async function loadSession(id) {
2622
2653
  try {
2623
2654
  const raw = await readFile2(join2(SESSIONS_DIR, `${id}.json`), "utf-8");
@@ -3280,143 +3311,36 @@ var providerCommand = {
3280
3311
  }
3281
3312
  };
3282
3313
 
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" };
3314
+ // src/commands/rename.ts
3315
+ var renameCommand = {
3316
+ name: "rename",
3317
+ description: "Rename the current session (usage: /rename <title>)",
3318
+ async execute(args, context) {
3319
+ const title = args.trim();
3320
+ if (!title) {
3321
+ return { output: "Usage: /rename <new session title>", silent: true };
3322
+ }
3323
+ await renameSession(context.sessionId, title);
3324
+ return { output: `Session renamed to: ${title}`, silent: true };
3382
3325
  }
3383
3326
  };
3384
- var foxCommand = {
3385
- name: "fox",
3386
- aliases: ["companion", "buddy"],
3387
- description: "Check on your fox companion (usage: /fox, /fox name <name>)",
3327
+
3328
+ // src/commands/plan.ts
3329
+ init_state();
3330
+ var planCommand = {
3331
+ name: "plan",
3332
+ description: "Enter plan mode to explore and design before implementing (usage: /plan <description>)",
3388
3333
  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";
3334
+ const description = args.trim();
3335
+ if (!description) {
3336
+ return { output: "Usage: /plan <what you want to plan>", silent: true };
3337
+ }
3338
+ updateAppState((s) => ({ ...s, planMode: true }));
3339
+ return {
3340
+ output: `Enter plan mode. Explore the codebase and design a plan for: ${description}`,
3341
+ silent: false,
3342
+ foxMood: "thinking"
3400
3343
  };
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
3344
  }
3421
3345
  };
3422
3346
 
@@ -3443,10 +3367,8 @@ var COMMANDS = [
3443
3367
  filesCommand,
3444
3368
  briefCommand,
3445
3369
  providerCommand,
3446
- petCommand,
3447
- feedCommand,
3448
- restCommand,
3449
- foxCommand
3370
+ renameCommand,
3371
+ planCommand
3450
3372
  ];
3451
3373
  function getCommands() {
3452
3374
  return COMMANDS;
@@ -3572,6 +3494,7 @@ init_system_prompt();
3572
3494
  init_providers();
3573
3495
  init_tools();
3574
3496
  init_bash();
3497
+ init_hooks();
3575
3498
  init_theme();
3576
3499
  import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
3577
3500
  function getContextLimit3(model) {
@@ -3585,10 +3508,10 @@ function getContextLimit3(model) {
3585
3508
  return 8192;
3586
3509
  }
3587
3510
  function REPL({ initialPrompt }) {
3588
- const { model: initialModel, systemPromptOverride } = useDarkfooContext();
3511
+ const { model: initialModel, systemPromptOverride, maxTurns, initialMessages, initialSessionId } = useDarkfooContext();
3589
3512
  const { exit } = useApp();
3590
3513
  const [model, setModel] = useState2(initialModel);
3591
- const [messages, setMessages] = useState2([]);
3514
+ const [messages, setMessages] = useState2(initialMessages ?? []);
3592
3515
  const [inputValue, setInputValue] = useState2("");
3593
3516
  const [isStreaming, setIsStreaming] = useState2(false);
3594
3517
  const [streamingText, setStreamingText] = useState2("");
@@ -3598,11 +3521,11 @@ function REPL({ initialPrompt }) {
3598
3521
  const [inputHistory, setInputHistory] = useState2([]);
3599
3522
  const [tokenCounts, setTokenCounts] = useState2({ input: 0, output: 0 });
3600
3523
  const [systemPrompt, setSystemPrompt] = useState2("");
3601
- const [foxMood, setFoxMood] = useState2("idle");
3524
+ const [mascotMood, setMascotMood] = useState2("idle");
3602
3525
  const [providerOnline, setProviderOnline] = useState2(true);
3603
3526
  const abortRef = useRef(null);
3604
3527
  const hasRun = useRef(false);
3605
- const sessionId = useRef(createSessionId());
3528
+ const sessionId = useRef(initialSessionId ?? createSessionId());
3606
3529
  const messagesRef = useRef(messages);
3607
3530
  messagesRef.current = messages;
3608
3531
  const modelRef = useRef(model);
@@ -3618,6 +3541,10 @@ function REPL({ initialPrompt }) {
3618
3541
  buildSystemPrompt(tools, cwd).then(setSystemPrompt);
3619
3542
  }
3620
3543
  }, []);
3544
+ useEffect(() => {
3545
+ executeHooks("session_start", { cwd }).catch(() => {
3546
+ });
3547
+ }, []);
3621
3548
  useEffect(() => {
3622
3549
  const provider = getProvider();
3623
3550
  provider.healthCheck().then((ok) => {
@@ -3655,12 +3582,14 @@ function REPL({ initialPrompt }) {
3655
3582
  messages,
3656
3583
  model,
3657
3584
  cwd,
3585
+ sessionId: sessionId.current,
3658
3586
  setModel,
3659
3587
  clearMessages,
3660
3588
  exit,
3661
- tokenCounts
3589
+ tokenCounts,
3590
+ systemPrompt
3662
3591
  });
3663
- commandContextRef.current = { messages, model, cwd, setModel, clearMessages, exit, tokenCounts };
3592
+ commandContextRef.current = { messages, model, cwd, sessionId: sessionId.current, setModel, clearMessages, exit, tokenCounts, systemPrompt };
3664
3593
  const addToHistory = useCallback2((input) => {
3665
3594
  setInputHistory((prev) => {
3666
3595
  const filtered = prev.filter((h) => h !== input);
@@ -3680,42 +3609,45 @@ function REPL({ initialPrompt }) {
3680
3609
  setStreamingText("");
3681
3610
  setToolResults([]);
3682
3611
  setCommandOutput(null);
3683
- setFoxMood("thinking");
3612
+ setMascotMood("thinking");
3684
3613
  const controller = new AbortController();
3685
3614
  abortRef.current = controller;
3686
3615
  const allMessages = [...messagesRef.current, userMsg];
3687
3616
  const currentModel = modelRef.current;
3688
3617
  const currentSystemPrompt = systemPromptRef.current;
3689
- setTokenCounts((prev) => ({
3690
- ...prev,
3691
- input: prev.input + Math.ceil(userMessage.length / 4)
3692
- }));
3693
3618
  try {
3694
3619
  for await (const event of query({
3695
3620
  model: currentModel,
3696
3621
  messages: allMessages,
3697
3622
  tools,
3698
3623
  systemPrompt: currentSystemPrompt,
3699
- signal: controller.signal
3624
+ signal: controller.signal,
3625
+ maxTurns
3700
3626
  })) {
3701
3627
  if (controller.signal.aborted) break;
3702
3628
  switch (event.type) {
3703
3629
  case "text_delta":
3704
3630
  setStreamingText((prev) => prev + event.text);
3705
- setFoxMood("idle");
3631
+ setMascotMood("idle");
3706
3632
  break;
3707
3633
  case "tool_call":
3708
3634
  setActiveTool({ name: event.toolCall.function.name, args: event.toolCall.function.arguments });
3709
- setFoxMood("working");
3635
+ setMascotMood("working");
3710
3636
  break;
3711
3637
  case "tool_result":
3712
3638
  setActiveTool(null);
3713
- setFoxMood(event.isError ? "error" : "working");
3639
+ setMascotMood(event.isError ? "error" : "working");
3714
3640
  setToolResults((prev) => [
3715
3641
  ...prev,
3716
3642
  { id: nanoid6(), toolName: event.toolName, output: event.output, isError: event.isError }
3717
3643
  ]);
3718
3644
  break;
3645
+ case "usage":
3646
+ setTokenCounts((prev) => ({
3647
+ input: prev.input + event.inputTokens,
3648
+ output: prev.output + event.outputTokens
3649
+ }));
3650
+ break;
3719
3651
  case "assistant_message":
3720
3652
  setMessages((prev) => {
3721
3653
  const updated = [...prev, event.message];
@@ -3723,14 +3655,10 @@ function REPL({ initialPrompt }) {
3723
3655
  });
3724
3656
  return updated;
3725
3657
  });
3726
- setTokenCounts((prev) => ({
3727
- ...prev,
3728
- output: prev.output + Math.ceil((event.message.content?.length ?? 0) / 4)
3729
- }));
3730
3658
  setStreamingText("");
3731
3659
  break;
3732
3660
  case "error":
3733
- setFoxMood("error");
3661
+ setMascotMood("error");
3734
3662
  setMessages((prev) => [
3735
3663
  ...prev,
3736
3664
  { id: nanoid6(), role: "assistant", content: `Error: ${event.error}`, timestamp: Date.now() }
@@ -3751,8 +3679,8 @@ function REPL({ initialPrompt }) {
3751
3679
  setStreamingText("");
3752
3680
  setActiveTool(null);
3753
3681
  setToolResults([]);
3754
- setFoxMood((prev) => prev === "error" ? "error" : "success");
3755
- setTimeout(() => setFoxMood("idle"), 2e3);
3682
+ setMascotMood((prev) => prev === "error" ? "error" : "success");
3683
+ setTimeout(() => setMascotMood("idle"), 2e3);
3756
3684
  abortRef.current = null;
3757
3685
  }
3758
3686
  },
@@ -3775,8 +3703,8 @@ function REPL({ initialPrompt }) {
3775
3703
  setMessages(result.replaceMessages);
3776
3704
  }
3777
3705
  if (result.foxMood) {
3778
- setFoxMood(result.foxMood);
3779
- setTimeout(() => setFoxMood("idle"), 3e3);
3706
+ setMascotMood(result.foxMood);
3707
+ setTimeout(() => setMascotMood("idle"), 3e3);
3780
3708
  }
3781
3709
  if (result.exit) return;
3782
3710
  if (!result.silent && result.output) {
@@ -3863,7 +3791,7 @@ ${result.content}
3863
3791
  }
3864
3792
  });
3865
3793
  return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", padding: 1, children: [
3866
- /* @__PURE__ */ jsx8(Banner, { model, cwd, providerName: getActiveProviderName(), providerOnline }),
3794
+ /* @__PURE__ */ jsx8(Banner, { model, cwd, providerName: getActiveProviderName(), providerOnline, mood: mascotMood }),
3867
3795
  /* @__PURE__ */ jsx8(Messages, { messages }),
3868
3796
  commandOutput ? /* @__PURE__ */ jsx8(Box7, { marginBottom: 1, marginLeft: 2, flexDirection: "column", children: /* @__PURE__ */ jsx8(Text7, { children: commandOutput }) }) : null,
3869
3797
  toolResults.map((tr) => /* @__PURE__ */ jsx8(ToolResultDisplay, { toolName: tr.toolName, output: tr.output, isError: tr.isError }, tr.id)),
@@ -3877,19 +3805,16 @@ ${result.content}
3877
3805
  /* @__PURE__ */ jsx8(Text7, { color: theme.cyan, children: "..." }),
3878
3806
  /* @__PURE__ */ jsx8(Text7, { color: theme.dim, children: " Thinking" })
3879
3807
  ] }) : 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
- ] }),
3808
+ /* @__PURE__ */ jsx8(
3809
+ UserInput,
3810
+ {
3811
+ value: inputValue,
3812
+ onChange: setInputValue,
3813
+ onSubmit: handleSubmit,
3814
+ disabled: isStreaming,
3815
+ history: inputHistory
3816
+ }
3817
+ ),
3893
3818
  /* @__PURE__ */ jsx8(
3894
3819
  StatusLine,
3895
3820
  {
@@ -3906,8 +3831,13 @@ ${result.content}
3906
3831
  init_providers();
3907
3832
  import { jsx as jsx9 } from "react/jsx-runtime";
3908
3833
  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) => {
3834
+ program.name("darkfoo").description("Darkfoo Code \u2014 local AI coding assistant powered by local LLM providers").version("0.3.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
3835
  const { model, prompt, provider, systemPrompt } = options;
3836
+ if (options.debug) {
3837
+ const { setDebugMode: setDebugMode2 } = await Promise.resolve().then(() => (init_debug(), debug_exports));
3838
+ setDebugMode2(true);
3839
+ }
3840
+ const maxTurns = parseInt(options.maxTurns, 10) || 30;
3911
3841
  await loadProviderSettings();
3912
3842
  if (provider) {
3913
3843
  try {
@@ -3956,19 +3886,35 @@ program.name("darkfoo").description("Darkfoo Code \u2014 local AI coding assista
3956
3886
  controller.abort();
3957
3887
  process.exit(0);
3958
3888
  });
3889
+ const jsonMode = options.outputFormat === "json";
3890
+ let jsonContent = "";
3891
+ const jsonToolCalls = [];
3892
+ const jsonToolResults = [];
3959
3893
  for await (const event of query2({
3960
3894
  model: resolvedModel,
3961
3895
  messages: [userMsg],
3962
3896
  tools,
3963
3897
  systemPrompt: sysPrompt,
3964
- signal: controller.signal
3898
+ signal: controller.signal,
3899
+ maxTurns
3965
3900
  })) {
3966
3901
  switch (event.type) {
3967
3902
  case "text_delta":
3968
- process.stdout.write(event.text);
3903
+ if (jsonMode) {
3904
+ jsonContent += event.text;
3905
+ } else {
3906
+ process.stdout.write(event.text);
3907
+ }
3908
+ break;
3909
+ case "tool_call":
3910
+ if (jsonMode) {
3911
+ jsonToolCalls.push({ name: event.toolCall.function.name, arguments: event.toolCall.function.arguments });
3912
+ }
3969
3913
  break;
3970
3914
  case "tool_result":
3971
- if (event.isError) {
3915
+ if (jsonMode) {
3916
+ jsonToolResults.push({ tool: event.toolName, output: event.output, isError: event.isError });
3917
+ } else if (event.isError) {
3972
3918
  process.stderr.write(`
3973
3919
  [${event.toolName}] Error: ${event.output}
3974
3920
  `);
@@ -3981,11 +3927,56 @@ Error: ${event.error}
3981
3927
  break;
3982
3928
  }
3983
3929
  }
3984
- process.stdout.write("\n");
3930
+ if (jsonMode) {
3931
+ const result = { role: "assistant", content: jsonContent };
3932
+ if (jsonToolCalls.length > 0) result.toolCalls = jsonToolCalls;
3933
+ if (jsonToolResults.length > 0) result.toolResults = jsonToolResults;
3934
+ process.stdout.write(JSON.stringify(result) + "\n");
3935
+ } else {
3936
+ process.stdout.write("\n");
3937
+ }
3985
3938
  process.exit(0);
3986
3939
  }
3940
+ let initialMessages;
3941
+ let initialSessionId;
3942
+ if (options.continue) {
3943
+ const sessions = await listSessions();
3944
+ if (sessions.length > 0) {
3945
+ const session = await loadSession(sessions[0].id);
3946
+ if (session) {
3947
+ initialMessages = session.messages;
3948
+ initialSessionId = session.id;
3949
+ process.stderr.write(`Resuming session: ${session.title}
3950
+ `);
3951
+ }
3952
+ } else {
3953
+ process.stderr.write("No previous sessions found.\n");
3954
+ }
3955
+ } else if (options.resume) {
3956
+ const session = await loadSession(options.resume);
3957
+ if (session) {
3958
+ initialMessages = session.messages;
3959
+ initialSessionId = session.id;
3960
+ process.stderr.write(`Resuming session: ${session.title}
3961
+ `);
3962
+ } else {
3963
+ process.stderr.write(`Session not found: ${options.resume}
3964
+ `);
3965
+ process.exit(1);
3966
+ }
3967
+ }
3987
3968
  const { waitUntilExit } = render(
3988
- /* @__PURE__ */ jsx9(App, { model: resolvedModel, systemPromptOverride: systemPrompt, children: /* @__PURE__ */ jsx9(REPL, {}) })
3969
+ /* @__PURE__ */ jsx9(
3970
+ App,
3971
+ {
3972
+ model: resolvedModel,
3973
+ systemPromptOverride: systemPrompt,
3974
+ maxTurns,
3975
+ initialMessages,
3976
+ initialSessionId,
3977
+ children: /* @__PURE__ */ jsx9(REPL, {})
3978
+ }
3979
+ )
3989
3980
  );
3990
3981
  await waitUntilExit();
3991
3982
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "darkfoo-code",
3
- "version": "0.2.5",
3
+ "version": "0.3.0",
4
4
  "description": "Darkfoo Code — local AI coding assistant powered by Ollama, vLLM, llama.cpp, and other LLM providers",
5
5
  "type": "module",
6
6
  "license": "MIT",