darkfoo-code 0.1.3 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/main.js CHANGED
@@ -1,29 +1,1914 @@
1
- import {
2
- theme
3
- } from "./chunk-BT7IPQDS.js";
4
- import {
5
- query
6
- } from "./chunk-GQXUHUV4.js";
7
- import {
8
- BashTool,
9
- getAppState,
10
- getTools,
11
- trackedFileCount,
12
- truncate
13
- } from "./chunk-4KTJEE4A.js";
14
- import {
15
- discoverProviders,
16
- getActiveProviderName,
17
- getProvider,
18
- getProviderConfigs,
19
- loadProviderSettings,
20
- saveProviderSettings,
21
- setActiveProvider,
22
- upsertProviderConfig
23
- } from "./chunk-OBL22IIN.js";
24
- import {
25
- buildSystemPrompt
26
- } from "./chunk-VSJKCANO.js";
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropNames = Object.getOwnPropertyNames;
3
+ var __esm = (fn, res) => function __init() {
4
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
5
+ };
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+
11
+ // src/utils/theme.ts
12
+ var theme_exports = {};
13
+ __export(theme_exports, {
14
+ theme: () => theme
15
+ });
16
+ var theme;
17
+ var init_theme = __esm({
18
+ "src/utils/theme.ts"() {
19
+ "use strict";
20
+ theme = {
21
+ cyan: "#5eead4",
22
+ cyanDim: "#3ab5a0",
23
+ pink: "#f472b6",
24
+ pinkDim: "#c2588e",
25
+ purple: "#a78bfa",
26
+ green: "#4ade80",
27
+ yellow: "#fbbf24",
28
+ red: "#ef4444",
29
+ text: "#e2e8f0",
30
+ dim: "#7e8ea6",
31
+ surface: "#111827",
32
+ bg: "#0c1021"
33
+ };
34
+ }
35
+ });
36
+
37
+ // src/utils/format.ts
38
+ function addLineNumbers(lines, startLine = 1) {
39
+ const maxNum = startLine + lines.length - 1;
40
+ const width = String(maxNum).length;
41
+ return lines.map((line, i) => `${String(startLine + i).padStart(width)} ${line}`).join("\n");
42
+ }
43
+ function truncate(text, maxLen) {
44
+ if (text.length <= maxLen) return text;
45
+ return text.slice(0, maxLen - 3) + "...";
46
+ }
47
+ var init_format = __esm({
48
+ "src/utils/format.ts"() {
49
+ "use strict";
50
+ }
51
+ });
52
+
53
+ // src/state.ts
54
+ function getAppState() {
55
+ return state;
56
+ }
57
+ function updateAppState(updater) {
58
+ state = updater(state);
59
+ }
60
+ var state;
61
+ var init_state = __esm({
62
+ "src/state.ts"() {
63
+ "use strict";
64
+ state = {
65
+ planMode: false,
66
+ tasks: [],
67
+ pendingQuestion: null
68
+ };
69
+ }
70
+ });
71
+
72
+ // src/providers/ollama.ts
73
+ function normalizeToolCall(tc) {
74
+ let args = tc.function.arguments;
75
+ if (typeof args === "string") {
76
+ try {
77
+ args = JSON.parse(args);
78
+ } catch {
79
+ args = {};
80
+ }
81
+ }
82
+ return { function: { name: tc.function.name, arguments: args } };
83
+ }
84
+ var OllamaProvider;
85
+ var init_ollama = __esm({
86
+ "src/providers/ollama.ts"() {
87
+ "use strict";
88
+ OllamaProvider = class {
89
+ name = "ollama";
90
+ label;
91
+ baseUrl;
92
+ constructor(baseUrl = "http://localhost:11434", label = "Ollama") {
93
+ this.baseUrl = baseUrl.replace(/\/$/, "");
94
+ this.label = label;
95
+ }
96
+ async *chatStream(params) {
97
+ const body = {
98
+ model: params.model,
99
+ messages: params.messages,
100
+ stream: true
101
+ };
102
+ if (params.tools && params.tools.length > 0) {
103
+ body.tools = params.tools;
104
+ }
105
+ let response;
106
+ try {
107
+ response = await fetch(`${this.baseUrl}/api/chat`, {
108
+ method: "POST",
109
+ headers: { "Content-Type": "application/json" },
110
+ body: JSON.stringify(body),
111
+ signal: params.signal
112
+ });
113
+ } catch (err) {
114
+ const msg = err instanceof Error ? err.message : String(err);
115
+ yield { type: "error", error: `Failed to connect to Ollama at ${this.baseUrl}: ${msg}` };
116
+ return;
117
+ }
118
+ if (!response.ok) {
119
+ const text = await response.text().catch(() => "unknown error");
120
+ yield { type: "error", error: `Ollama error ${response.status}: ${text}` };
121
+ return;
122
+ }
123
+ if (!response.body) {
124
+ yield { type: "error", error: "No response body from Ollama" };
125
+ return;
126
+ }
127
+ const reader = response.body.getReader();
128
+ const decoder = new TextDecoder();
129
+ let buffer = "";
130
+ try {
131
+ while (true) {
132
+ const { done, value } = await reader.read();
133
+ if (done) break;
134
+ buffer += decoder.decode(value, { stream: true });
135
+ let newlineIdx;
136
+ while ((newlineIdx = buffer.indexOf("\n")) !== -1) {
137
+ const line = buffer.slice(0, newlineIdx).trim();
138
+ buffer = buffer.slice(newlineIdx + 1);
139
+ if (!line) continue;
140
+ let chunk;
141
+ try {
142
+ chunk = JSON.parse(line);
143
+ } catch {
144
+ continue;
145
+ }
146
+ if (chunk.message.content) {
147
+ yield { type: "text_delta", text: chunk.message.content };
148
+ }
149
+ if (chunk.message.tool_calls) {
150
+ for (const tc of chunk.message.tool_calls) {
151
+ yield { type: "tool_call", toolCall: normalizeToolCall(tc) };
152
+ }
153
+ }
154
+ }
155
+ }
156
+ } finally {
157
+ reader.releaseLock();
158
+ }
159
+ }
160
+ async chat(params) {
161
+ let content = "";
162
+ const toolCalls = [];
163
+ for await (const event of this.chatStream(params)) {
164
+ if (event.type === "text_delta") content += event.text;
165
+ if (event.type === "tool_call") toolCalls.push(event.toolCall);
166
+ }
167
+ return { content, toolCalls };
168
+ }
169
+ async listModels() {
170
+ try {
171
+ const res = await fetch(`${this.baseUrl}/api/tags`);
172
+ const data = await res.json();
173
+ return data.models.map((m) => ({ name: m.name, size: m.details.parameter_size, family: m.details.family }));
174
+ } catch {
175
+ return [];
176
+ }
177
+ }
178
+ async healthCheck() {
179
+ try {
180
+ const res = await fetch(`${this.baseUrl}/api/tags`, { signal: AbortSignal.timeout(5e3) });
181
+ return res.ok;
182
+ } catch {
183
+ return false;
184
+ }
185
+ }
186
+ };
187
+ }
188
+ });
189
+
190
+ // src/providers/openai-compat.ts
191
+ var OpenAICompatProvider;
192
+ var init_openai_compat = __esm({
193
+ "src/providers/openai-compat.ts"() {
194
+ "use strict";
195
+ OpenAICompatProvider = class {
196
+ name;
197
+ label;
198
+ baseUrl;
199
+ apiKey;
200
+ constructor(baseUrl, label, name = "openai", apiKey = "") {
201
+ this.baseUrl = baseUrl.replace(/\/$/, "");
202
+ this.label = label;
203
+ this.name = name;
204
+ this.apiKey = apiKey;
205
+ }
206
+ async *chatStream(params) {
207
+ const headers = { "Content-Type": "application/json" };
208
+ if (this.apiKey) headers["Authorization"] = `Bearer ${this.apiKey}`;
209
+ const messages = params.messages.map((m) => {
210
+ const msg = { role: m.role, content: m.content };
211
+ if (m.tool_calls) msg.tool_calls = m.tool_calls;
212
+ return msg;
213
+ });
214
+ const body = {
215
+ model: params.model,
216
+ messages,
217
+ stream: true
218
+ };
219
+ if (params.tools && params.tools.length > 0) {
220
+ body.tools = params.tools.map((t) => ({
221
+ type: "function",
222
+ function: {
223
+ name: t.function.name,
224
+ description: t.function.description,
225
+ parameters: t.function.parameters
226
+ }
227
+ }));
228
+ }
229
+ let response;
230
+ try {
231
+ response = await fetch(`${this.baseUrl}/v1/chat/completions`, {
232
+ method: "POST",
233
+ headers,
234
+ body: JSON.stringify(body),
235
+ signal: params.signal
236
+ });
237
+ } catch (err) {
238
+ const msg = err instanceof Error ? err.message : String(err);
239
+ yield { type: "error", error: `Failed to connect to ${this.label} at ${this.baseUrl}: ${msg}` };
240
+ return;
241
+ }
242
+ if (!response.ok) {
243
+ const text = await response.text().catch(() => "unknown error");
244
+ yield { type: "error", error: `${this.label} error ${response.status}: ${text}` };
245
+ return;
246
+ }
247
+ if (!response.body) {
248
+ yield { type: "error", error: `No response body from ${this.label}` };
249
+ return;
250
+ }
251
+ const reader = response.body.getReader();
252
+ const decoder = new TextDecoder();
253
+ let buffer = "";
254
+ const pendingToolCalls = /* @__PURE__ */ new Map();
255
+ try {
256
+ while (true) {
257
+ const { done, value } = await reader.read();
258
+ if (done) break;
259
+ buffer += decoder.decode(value, { stream: true });
260
+ let newlineIdx;
261
+ while ((newlineIdx = buffer.indexOf("\n")) !== -1) {
262
+ const line = buffer.slice(0, newlineIdx).trim();
263
+ buffer = buffer.slice(newlineIdx + 1);
264
+ if (!line || line === "data: [DONE]") continue;
265
+ if (!line.startsWith("data: ")) continue;
266
+ const jsonStr = line.slice(6);
267
+ let chunk;
268
+ try {
269
+ chunk = JSON.parse(jsonStr);
270
+ } catch {
271
+ continue;
272
+ }
273
+ const delta = chunk.choices?.[0]?.delta;
274
+ if (!delta) continue;
275
+ if (delta.content) {
276
+ yield { type: "text_delta", text: delta.content };
277
+ }
278
+ if (delta.tool_calls) {
279
+ for (const tc of delta.tool_calls) {
280
+ const idx = tc.index ?? 0;
281
+ if (!pendingToolCalls.has(idx)) {
282
+ pendingToolCalls.set(idx, { name: "", arguments: "" });
283
+ }
284
+ const pending = pendingToolCalls.get(idx);
285
+ if (tc.function?.name) pending.name = tc.function.name;
286
+ if (tc.function?.arguments) pending.arguments += tc.function.arguments;
287
+ }
288
+ }
289
+ const finishReason = chunk.choices?.[0]?.finish_reason;
290
+ if (finishReason === "tool_calls" || finishReason === "stop") {
291
+ for (const [, tc] of pendingToolCalls) {
292
+ if (tc.name) {
293
+ let args = {};
294
+ try {
295
+ args = JSON.parse(tc.arguments);
296
+ } catch {
297
+ }
298
+ yield { type: "tool_call", toolCall: { function: { name: tc.name, arguments: args } } };
299
+ }
300
+ }
301
+ pendingToolCalls.clear();
302
+ }
303
+ }
304
+ }
305
+ for (const [, tc] of pendingToolCalls) {
306
+ if (tc.name) {
307
+ let args = {};
308
+ try {
309
+ args = JSON.parse(tc.arguments);
310
+ } catch {
311
+ }
312
+ yield { type: "tool_call", toolCall: { function: { name: tc.name, arguments: args } } };
313
+ }
314
+ }
315
+ } finally {
316
+ reader.releaseLock();
317
+ }
318
+ }
319
+ async chat(params) {
320
+ let content = "";
321
+ const toolCalls = [];
322
+ for await (const event of this.chatStream(params)) {
323
+ if (event.type === "text_delta") content += event.text;
324
+ if (event.type === "tool_call") toolCalls.push(event.toolCall);
325
+ }
326
+ return { content, toolCalls };
327
+ }
328
+ async listModels() {
329
+ const headers = {};
330
+ if (this.apiKey) headers["Authorization"] = `Bearer ${this.apiKey}`;
331
+ try {
332
+ const res = await fetch(`${this.baseUrl}/v1/models`, { headers, signal: AbortSignal.timeout(5e3) });
333
+ const data = await res.json();
334
+ if (data.data) {
335
+ return data.data.map((m) => ({ name: m.id }));
336
+ }
337
+ if (data.models) {
338
+ return data.models.map((m) => ({ name: m.name || m.model || m.id || "unknown" }));
339
+ }
340
+ return [];
341
+ } catch {
342
+ return [];
343
+ }
344
+ }
345
+ async healthCheck() {
346
+ try {
347
+ const res = await fetch(`${this.baseUrl}/health`, { signal: AbortSignal.timeout(5e3) });
348
+ if (res.ok) return true;
349
+ const res2 = await fetch(`${this.baseUrl}/v1/models`, { signal: AbortSignal.timeout(5e3) });
350
+ return res2.ok;
351
+ } catch {
352
+ return false;
353
+ }
354
+ }
355
+ };
356
+ }
357
+ });
358
+
359
+ // src/providers/index.ts
360
+ var providers_exports = {};
361
+ __export(providers_exports, {
362
+ discoverProviders: () => discoverProviders,
363
+ getActiveProviderName: () => getActiveProviderName,
364
+ getProvider: () => getProvider,
365
+ getProviderByName: () => getProviderByName,
366
+ getProviderConfigs: () => getProviderConfigs,
367
+ loadProviderSettings: () => loadProviderSettings,
368
+ removeProviderConfig: () => removeProviderConfig,
369
+ saveProviderSettings: () => saveProviderSettings,
370
+ setActiveProvider: () => setActiveProvider,
371
+ upsertProviderConfig: () => upsertProviderConfig
372
+ });
373
+ import { readFile, writeFile, mkdir } from "fs/promises";
374
+ import { join } from "path";
375
+ function createProvider(config) {
376
+ switch (config.type) {
377
+ case "ollama":
378
+ return new OllamaProvider(config.baseUrl, config.label);
379
+ case "openai":
380
+ return new OpenAICompatProvider(config.baseUrl, config.label, config.name, config.apiKey);
381
+ default:
382
+ throw new Error(`Unknown provider type: ${config.type}`);
383
+ }
384
+ }
385
+ function ensureProvider(name) {
386
+ if (providerInstances.has(name)) return providerInstances.get(name);
387
+ const config = providerConfigs.find((c) => c.name === name);
388
+ if (!config) throw new Error(`Unknown provider: ${name}`);
389
+ const provider = createProvider(config);
390
+ providerInstances.set(name, provider);
391
+ return provider;
392
+ }
393
+ function getProvider() {
394
+ return ensureProvider(activeProviderName);
395
+ }
396
+ function getProviderByName(name) {
397
+ const config = providerConfigs.find((c) => c.name === name);
398
+ if (!config) return null;
399
+ return ensureProvider(name);
400
+ }
401
+ function getActiveProviderName() {
402
+ return activeProviderName;
403
+ }
404
+ function setActiveProvider(name) {
405
+ const config = providerConfigs.find((c) => c.name === name);
406
+ if (!config) throw new Error(`Unknown provider: ${name}. Available: ${providerConfigs.map((c) => c.name).join(", ")}`);
407
+ activeProviderName = name;
408
+ }
409
+ function getProviderConfigs() {
410
+ return providerConfigs;
411
+ }
412
+ function upsertProviderConfig(config) {
413
+ const idx = providerConfigs.findIndex((c) => c.name === config.name);
414
+ if (idx >= 0) {
415
+ providerConfigs[idx] = config;
416
+ } else {
417
+ providerConfigs.push(config);
418
+ }
419
+ providerInstances.delete(config.name);
420
+ }
421
+ function removeProviderConfig(name) {
422
+ const idx = providerConfigs.findIndex((c) => c.name === name);
423
+ if (idx < 0) return false;
424
+ providerConfigs.splice(idx, 1);
425
+ providerInstances.delete(name);
426
+ if (activeProviderName === name) activeProviderName = "ollama";
427
+ return true;
428
+ }
429
+ async function loadProviderSettings() {
430
+ try {
431
+ const raw = await readFile(SETTINGS_PATH, "utf-8");
432
+ const settings = JSON.parse(raw);
433
+ if (settings.providers) {
434
+ for (const custom of settings.providers) {
435
+ upsertProviderConfig(custom);
436
+ }
437
+ }
438
+ if (settings.activeProvider) {
439
+ const config = providerConfigs.find((c) => c.name === settings.activeProvider);
440
+ if (config) activeProviderName = settings.activeProvider;
441
+ }
442
+ } catch {
443
+ }
444
+ }
445
+ async function saveProviderSettings() {
446
+ let settings = {};
447
+ try {
448
+ const raw = await readFile(SETTINGS_PATH, "utf-8");
449
+ settings = JSON.parse(raw);
450
+ } catch {
451
+ }
452
+ const customProviders = providerConfigs.filter(
453
+ (c) => !DEFAULT_PROVIDERS.some((d) => d.name === c.name && d.baseUrl === c.baseUrl)
454
+ );
455
+ settings.providers = customProviders;
456
+ settings.activeProvider = activeProviderName;
457
+ await mkdir(join(process.env.HOME || "~", ".darkfoo"), { recursive: true });
458
+ await writeFile(SETTINGS_PATH, JSON.stringify(settings, null, 2), "utf-8");
459
+ }
460
+ async function discoverProviders() {
461
+ const results = await Promise.all(
462
+ providerConfigs.map(async (config) => {
463
+ const provider = ensureProvider(config.name);
464
+ const online = await provider.healthCheck();
465
+ return { config, online };
466
+ })
467
+ );
468
+ return results;
469
+ }
470
+ var SETTINGS_PATH, DEFAULT_PROVIDERS, activeProviderName, providerConfigs, providerInstances;
471
+ var init_providers = __esm({
472
+ "src/providers/index.ts"() {
473
+ "use strict";
474
+ init_ollama();
475
+ init_openai_compat();
476
+ SETTINGS_PATH = join(process.env.HOME || "~", ".darkfoo", "settings.json");
477
+ DEFAULT_PROVIDERS = [
478
+ { type: "ollama", name: "ollama", label: "Ollama", baseUrl: process.env.OLLAMA_HOST || "http://localhost:11434" },
479
+ { type: "openai", name: "llama-cpp", label: "llama.cpp", baseUrl: "http://localhost:8081" },
480
+ { type: "openai", name: "vllm", label: "vLLM", baseUrl: "http://localhost:8000" },
481
+ { type: "openai", name: "tgi", label: "TGI", baseUrl: "http://localhost:8090" },
482
+ { type: "openai", name: "lm-studio", label: "LM Studio", baseUrl: "http://localhost:1234" },
483
+ { type: "openai", name: "localai", label: "LocalAI", baseUrl: "http://localhost:8080" }
484
+ ];
485
+ activeProviderName = "ollama";
486
+ providerConfigs = [...DEFAULT_PROVIDERS];
487
+ providerInstances = /* @__PURE__ */ new Map();
488
+ }
489
+ });
490
+
491
+ // src/file-tracker.ts
492
+ function simpleHash(str) {
493
+ let hash = 0;
494
+ for (let i = 0; i < str.length; i++) {
495
+ const char = str.charCodeAt(i);
496
+ hash = (hash << 5) - hash + char;
497
+ hash |= 0;
498
+ }
499
+ return hash;
500
+ }
501
+ function markFileRead(path, content) {
502
+ readFiles.set(path, { readAt: Date.now(), contentHash: simpleHash(content) });
503
+ }
504
+ function hasFileBeenRead(path) {
505
+ return readFiles.has(path);
506
+ }
507
+ function trackedFileCount() {
508
+ return readFiles.size;
509
+ }
510
+ var readFiles;
511
+ var init_file_tracker = __esm({
512
+ "src/file-tracker.ts"() {
513
+ "use strict";
514
+ readFiles = /* @__PURE__ */ new Map();
515
+ }
516
+ });
517
+
518
+ // src/permissions.ts
519
+ import { readFile as readFile4, writeFile as writeFile5, mkdir as mkdir5 } from "fs/promises";
520
+ import { join as join5 } from "path";
521
+ async function loadSettings() {
522
+ if (cachedSettings) return cachedSettings;
523
+ try {
524
+ const raw = await readFile4(SETTINGS_PATH3, "utf-8");
525
+ cachedSettings = JSON.parse(raw);
526
+ return cachedSettings;
527
+ } catch {
528
+ cachedSettings = { permissionMode: "default", alwaysAllow: [] };
529
+ return cachedSettings;
530
+ }
531
+ }
532
+ async function checkPermission(tool, args) {
533
+ const settings = await loadSettings();
534
+ if (settings.permissionMode === "auto") return "allow";
535
+ if (tool.isReadOnly) return "allow";
536
+ const argStr = JSON.stringify(args);
537
+ const matched = settings.alwaysAllow.find(
538
+ (r) => r.tool === tool.name && (!r.pattern || argStr.includes(r.pattern))
539
+ );
540
+ if (matched) return matched.decision;
541
+ return "ask";
542
+ }
543
+ var SETTINGS_PATH3, cachedSettings;
544
+ var init_permissions = __esm({
545
+ "src/permissions.ts"() {
546
+ "use strict";
547
+ SETTINGS_PATH3 = join5(process.env.HOME || "~", ".darkfoo", "settings.json");
548
+ cachedSettings = null;
549
+ }
550
+ });
551
+
552
+ // src/query.ts
553
+ var query_exports = {};
554
+ __export(query_exports, {
555
+ query: () => query
556
+ });
557
+ import { nanoid as nanoid3 } from "nanoid";
558
+ async function* query(params) {
559
+ const { model, tools, systemPrompt, signal } = params;
560
+ const messages = [...params.messages];
561
+ const maxTurns = params.maxTurns ?? 30;
562
+ let turns = 0;
563
+ const ollamaTools = tools.map((t) => t.toOllamaToolDef());
564
+ while (turns < maxTurns) {
565
+ turns++;
566
+ const ollamaMessages = toOllamaMessages(messages, systemPrompt);
567
+ let assistantContent = "";
568
+ const toolCalls = [];
569
+ const provider = getProvider();
570
+ for await (const event of provider.chatStream({ model, messages: ollamaMessages, tools: ollamaTools, signal })) {
571
+ yield event;
572
+ if (event.type === "text_delta") {
573
+ assistantContent += event.text;
574
+ }
575
+ if (event.type === "tool_call") {
576
+ toolCalls.push(event.toolCall);
577
+ }
578
+ if (event.type === "error") {
579
+ return;
580
+ }
581
+ }
582
+ const assistantMsg = {
583
+ id: nanoid3(),
584
+ role: "assistant",
585
+ content: assistantContent,
586
+ toolCalls: toolCalls.length > 0 ? toolCalls : void 0,
587
+ timestamp: Date.now()
588
+ };
589
+ messages.push(assistantMsg);
590
+ yield { type: "assistant_message", message: assistantMsg };
591
+ if (toolCalls.length === 0) return;
592
+ const readOnlyCalls = [];
593
+ const writeCalls = [];
594
+ const unknownCalls = [];
595
+ for (const tc of toolCalls) {
596
+ const tool = tools.find((t) => t.name.toLowerCase() === tc.function.name.toLowerCase());
597
+ if (!tool) {
598
+ unknownCalls.push(tc);
599
+ } else if (tool.isReadOnly) {
600
+ readOnlyCalls.push({ tc, tool });
601
+ } else {
602
+ writeCalls.push({ tc, tool });
603
+ }
604
+ }
605
+ for (const tc of unknownCalls) {
606
+ const errOutput = `Unknown tool: ${tc.function.name}`;
607
+ yield { type: "tool_result", toolName: tc.function.name, output: errOutput, isError: true };
608
+ messages.push({ id: nanoid3(), role: "tool", content: errOutput, toolName: tc.function.name, timestamp: Date.now() });
609
+ }
610
+ if (readOnlyCalls.length > 0) {
611
+ const results = await Promise.all(
612
+ readOnlyCalls.map(async ({ tc, tool }) => {
613
+ const coercedArgs = coerceToolArgs(tc.function.arguments, tool);
614
+ try {
615
+ return { tool, result: await tool.call(coercedArgs, { cwd: process.cwd(), abortSignal: signal }) };
616
+ } catch (err) {
617
+ const msg = err instanceof Error ? err.message : String(err);
618
+ return { tool, result: { output: `Tool execution error: ${msg}`, isError: true } };
619
+ }
620
+ })
621
+ );
622
+ for (const { tool, result } of results) {
623
+ yield { type: "tool_result", toolName: tool.name, output: result.output, isError: result.isError ?? false };
624
+ messages.push({ id: nanoid3(), role: "tool", content: result.output, toolName: tool.name, timestamp: Date.now() });
625
+ }
626
+ }
627
+ for (const { tc, tool } of writeCalls) {
628
+ const coercedArgs = coerceToolArgs(tc.function.arguments, tool);
629
+ const permission = await checkPermission(tool, coercedArgs);
630
+ if (permission === "deny") {
631
+ const denied = "Tool blocked by permission rules.";
632
+ yield { type: "tool_result", toolName: tool.name, output: denied, isError: true };
633
+ messages.push({ id: nanoid3(), role: "tool", content: denied, toolName: tool.name, timestamp: Date.now() });
634
+ continue;
635
+ }
636
+ let result;
637
+ try {
638
+ result = await tool.call(coercedArgs, { cwd: process.cwd(), abortSignal: signal });
639
+ } catch (err) {
640
+ const msg = err instanceof Error ? err.message : String(err);
641
+ result = { output: `Tool execution error: ${msg}`, isError: true };
642
+ }
643
+ yield { type: "tool_result", toolName: tool.name, output: result.output, isError: result.isError ?? false };
644
+ messages.push({ id: nanoid3(), role: "tool", content: result.output, toolName: tool.name, timestamp: Date.now() });
645
+ }
646
+ }
647
+ yield { type: "error", error: `Reached maximum of ${maxTurns} tool-use turns.` };
648
+ }
649
+ function toOllamaMessages(messages, systemPrompt) {
650
+ const result = [
651
+ { role: "system", content: systemPrompt }
652
+ ];
653
+ for (const msg of messages) {
654
+ if (msg.role === "assistant") {
655
+ const ollamaMsg = {
656
+ role: "assistant",
657
+ content: msg.content
658
+ };
659
+ if (msg.toolCalls) {
660
+ ollamaMsg.tool_calls = msg.toolCalls;
661
+ }
662
+ result.push(ollamaMsg);
663
+ } else if (msg.role === "tool") {
664
+ result.push({ role: "tool", content: msg.content });
665
+ } else if (msg.role === "user") {
666
+ result.push({ role: "user", content: msg.content });
667
+ }
668
+ }
669
+ return result;
670
+ }
671
+ function coerceToolArgs(args, tool) {
672
+ const shape = tool.inputSchema.shape;
673
+ if (!shape) return args;
674
+ const coerced = { ...args };
675
+ for (const [key, val] of Object.entries(coerced)) {
676
+ if (typeof val !== "string") continue;
677
+ const fieldDef = shape[key];
678
+ if (!fieldDef) continue;
679
+ const typeName = fieldDef._def?.typeName === "ZodOptional" ? fieldDef._def?.innerType?._def?.typeName : fieldDef._def?.typeName;
680
+ if (typeName === "ZodBoolean") {
681
+ coerced[key] = val === "true";
682
+ } else if (typeName === "ZodNumber") {
683
+ const num = Number(val);
684
+ if (!isNaN(num)) coerced[key] = num;
685
+ }
686
+ }
687
+ return coerced;
688
+ }
689
+ var init_query = __esm({
690
+ "src/query.ts"() {
691
+ "use strict";
692
+ init_providers();
693
+ init_permissions();
694
+ }
695
+ });
696
+
697
+ // 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";
700
+ async function loadProjectContext(cwd) {
701
+ const parts = [];
702
+ const globalContent = await tryRead(HOME_CONTEXT);
703
+ if (globalContent) {
704
+ parts.push(`# Global instructions (~/.darkfoo/DARKFOO.md)
705
+
706
+ ${globalContent}`);
707
+ }
708
+ const visited = /* @__PURE__ */ new Set();
709
+ let dir = resolve(cwd);
710
+ while (dir && !visited.has(dir)) {
711
+ visited.add(dir);
712
+ for (const file of CONTEXT_FILES) {
713
+ const filePath = join6(dir, file);
714
+ const content = await tryRead(filePath);
715
+ if (content) {
716
+ const rel = filePath.replace(cwd + "/", "");
717
+ parts.push(`# Project instructions (${rel})
718
+
719
+ ${content}`);
720
+ }
721
+ }
722
+ const parent = dirname(dir);
723
+ if (parent === dir) break;
724
+ dir = parent;
725
+ }
726
+ const rulesDir = join6(cwd, RULES_DIR);
727
+ try {
728
+ await access(rulesDir);
729
+ const files = await readdir2(rulesDir);
730
+ for (const file of files.filter((f) => f.endsWith(".md")).sort()) {
731
+ const content = await tryRead(join6(rulesDir, file));
732
+ if (content) {
733
+ parts.push(`# Rule: ${file}
734
+
735
+ ${content}`);
736
+ }
737
+ }
738
+ } catch {
739
+ }
740
+ return parts.join("\n\n---\n\n");
741
+ }
742
+ async function tryRead(path) {
743
+ try {
744
+ return await readFile5(path, "utf-8");
745
+ } catch {
746
+ return null;
747
+ }
748
+ }
749
+ var CONTEXT_FILES, RULES_DIR, HOME_CONTEXT;
750
+ var init_context_loader = __esm({
751
+ "src/context-loader.ts"() {
752
+ "use strict";
753
+ CONTEXT_FILES = ["DARKFOO.md", ".darkfoo/DARKFOO.md"];
754
+ RULES_DIR = ".darkfoo/rules";
755
+ HOME_CONTEXT = join6(process.env.HOME || "~", ".darkfoo", "DARKFOO.md");
756
+ }
757
+ });
758
+
759
+ // src/system-prompt.ts
760
+ var system_prompt_exports = {};
761
+ __export(system_prompt_exports, {
762
+ buildSystemPrompt: () => buildSystemPrompt
763
+ });
764
+ async function buildSystemPrompt(tools, cwd) {
765
+ const toolDescriptions = tools.map((t) => {
766
+ const params = Object.keys(
767
+ t.inputSchema.shape ?? {}
768
+ );
769
+ return `### ${t.name}
770
+ ${t.description}
771
+ Parameters: ${params.join(", ")}`;
772
+ }).join("\n\n");
773
+ const projectContext = await loadProjectContext(cwd);
774
+ let prompt = `You are Darkfoo Code, a local AI coding assistant.
775
+
776
+ All text you output outside of tool use is displayed directly to the user. Use this to communicate, explain, and answer questions. You do NOT need to use a tool for every message.
777
+
778
+ IMPORTANT: Only use tools when the task genuinely requires them \u2014 reading files, writing code, running commands, searching the codebase. For conversation, greetings, explanations, opinions, or questions that don't need filesystem or shell access, just respond with text. Never call a tool with empty or meaningless arguments.
779
+
780
+ # Tools
781
+
782
+ You have access to the following tools. Call them ONLY when needed:
783
+
784
+ ${toolDescriptions}
785
+
786
+ # Tool usage guidelines
787
+
788
+ - Read files before modifying them.
789
+ - Use absolute file paths when calling tools.
790
+ - Prefer Edit for targeted changes. Use Write only for new files or complete rewrites.
791
+ - Prefer Grep over Bash with grep. Prefer Glob over Bash with find.
792
+ - If a command fails, diagnose before retrying.
793
+ - Do not make changes beyond what was asked.
794
+ - When running shell commands, quote paths with spaces.
795
+ - Use TaskCreate/TaskUpdate to track multi-step work.
796
+ - Use EnterPlanMode before non-trivial implementation tasks.
797
+
798
+ # Context
799
+
800
+ - Working directory: ${cwd}
801
+ - Date: ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}
802
+ - Platform: ${process.platform}
803
+ - You are running locally with full filesystem access.
804
+
805
+ # Style
806
+
807
+ - Be concise and direct. Lead with the answer or action.
808
+ - Use markdown formatting for code blocks, lists, and emphasis.
809
+ - When referencing code, include the file path.
810
+ `;
811
+ if (projectContext) {
812
+ prompt += `
813
+ # Project Instructions
814
+
815
+ ${projectContext}
816
+ `;
817
+ }
818
+ return prompt;
819
+ }
820
+ var init_system_prompt = __esm({
821
+ "src/system-prompt.ts"() {
822
+ "use strict";
823
+ init_context_loader();
824
+ }
825
+ });
826
+
827
+ // src/tools/types.ts
828
+ import { zodToJsonSchema } from "zod-to-json-schema";
829
+ function buildOllamaToolDef(tool) {
830
+ const raw = zodToJsonSchema(tool.inputSchema);
831
+ delete raw.$schema;
832
+ delete raw.additionalProperties;
833
+ return {
834
+ type: "function",
835
+ function: {
836
+ name: tool.name,
837
+ description: tool.description,
838
+ parameters: raw
839
+ }
840
+ };
841
+ }
842
+ var init_types = __esm({
843
+ "src/tools/types.ts"() {
844
+ "use strict";
845
+ }
846
+ });
847
+
848
+ // src/tools/bash.ts
849
+ 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";
852
+ import { nanoid as nanoid4 } from "nanoid";
853
+ import { z } from "zod";
854
+ async function runInBackground(command, cwd) {
855
+ const taskId = nanoid4(8);
856
+ await mkdir6(BG_OUTPUT_DIR, { recursive: true });
857
+ const outputPath = join7(BG_OUTPUT_DIR, `${taskId}.output`);
858
+ backgroundTasks.set(taskId, { command, status: "running", outputPath });
859
+ const proc = spawn(command, {
860
+ shell: true,
861
+ cwd,
862
+ env: { ...process.env, TERM: "dumb" },
863
+ detached: false
864
+ });
865
+ let stdout = "";
866
+ let stderr = "";
867
+ proc.stdout?.on("data", (data) => {
868
+ stdout += data.toString();
869
+ });
870
+ proc.stderr?.on("data", (data) => {
871
+ stderr += data.toString();
872
+ });
873
+ proc.on("close", async (code) => {
874
+ const output = stdout + (stderr ? `
875
+ stderr: ${stderr}` : "");
876
+ await writeFile6(outputPath, output || `(exit code ${code})`, "utf-8").catch(() => {
877
+ });
878
+ const task = backgroundTasks.get(taskId);
879
+ if (task) {
880
+ task.status = code === 0 ? "completed" : "failed";
881
+ }
882
+ });
883
+ proc.on("error", async (err) => {
884
+ await writeFile6(outputPath, `Error: ${err.message}`, "utf-8").catch(() => {
885
+ });
886
+ const task = backgroundTasks.get(taskId);
887
+ if (task) {
888
+ task.status = "failed";
889
+ }
890
+ });
891
+ return {
892
+ output: `Background task started with ID: ${taskId}
893
+ Command: ${command}
894
+ Use Bash to run: cat ${outputPath} (to check output later)`
895
+ };
896
+ }
897
+ var INPUT_SCHEMA, MAX_OUTPUT, BG_OUTPUT_DIR, backgroundTasks, BashTool;
898
+ var init_bash = __esm({
899
+ "src/tools/bash.ts"() {
900
+ "use strict";
901
+ init_types();
902
+ INPUT_SCHEMA = z.object({
903
+ command: z.string().describe("The bash command to execute"),
904
+ description: z.string().optional().describe("Short description of what this command does"),
905
+ timeout: z.number().optional().describe("Timeout in milliseconds (default 120000, max 600000)"),
906
+ run_in_background: z.boolean().optional().describe("Run in background, returning a task ID for later retrieval")
907
+ });
908
+ MAX_OUTPUT = 1e5;
909
+ BG_OUTPUT_DIR = join7(process.env.HOME || "~", ".darkfoo", "bg-tasks");
910
+ backgroundTasks = /* @__PURE__ */ new Map();
911
+ BashTool = {
912
+ name: "Bash",
913
+ description: "Execute a bash command and return its output. Use for system commands, git operations, running scripts, installing packages. Set run_in_background for long-running commands.",
914
+ inputSchema: INPUT_SCHEMA,
915
+ isReadOnly: false,
916
+ async call(input, context) {
917
+ const parsed = INPUT_SCHEMA.parse(input);
918
+ const timeout = Math.min(parsed.timeout ?? 12e4, 6e5);
919
+ if (parsed.run_in_background) {
920
+ return runInBackground(parsed.command, context.cwd);
921
+ }
922
+ return new Promise((resolve8) => {
923
+ const proc = spawn(parsed.command, {
924
+ shell: true,
925
+ cwd: context.cwd,
926
+ env: { ...process.env, TERM: "dumb" },
927
+ signal: context.abortSignal,
928
+ timeout
929
+ });
930
+ let stdout = "";
931
+ let stderr = "";
932
+ proc.stdout?.on("data", (data) => {
933
+ const chunk = data.toString();
934
+ if (stdout.length < MAX_OUTPUT) {
935
+ stdout += chunk.slice(0, MAX_OUTPUT - stdout.length);
936
+ }
937
+ });
938
+ proc.stderr?.on("data", (data) => {
939
+ const chunk = data.toString();
940
+ if (stderr.length < MAX_OUTPUT) {
941
+ stderr += chunk.slice(0, MAX_OUTPUT - stderr.length);
942
+ }
943
+ });
944
+ proc.on("error", (err) => {
945
+ resolve8({ output: `Error: ${err.message}`, isError: true });
946
+ });
947
+ proc.on("close", (code) => {
948
+ let output = stdout;
949
+ if (stdout.length >= MAX_OUTPUT) {
950
+ output += "\n... (output truncated)";
951
+ }
952
+ if (stderr) {
953
+ output += (output ? "\n" : "") + `stderr: ${stderr}`;
954
+ }
955
+ if (!output) {
956
+ output = code === 0 ? "(no output)" : `Command failed with exit code ${code}`;
957
+ }
958
+ resolve8({ output, isError: code !== 0 });
959
+ });
960
+ });
961
+ },
962
+ toOllamaToolDef() {
963
+ return buildOllamaToolDef(this);
964
+ }
965
+ };
966
+ }
967
+ });
968
+
969
+ // src/tools/read.ts
970
+ import { readFile as readFile7, stat } from "fs/promises";
971
+ import { extname, resolve as resolve2 } from "path";
972
+ import { z as z2 } from "zod";
973
+ async function readImage(filePath, size) {
974
+ const ext = extname(filePath).toLowerCase();
975
+ if (ext === ".svg") {
976
+ const content = await readFile7(filePath, "utf-8");
977
+ return { output: `SVG image (${size} bytes):
978
+ ${content.slice(0, 5e3)}` };
979
+ }
980
+ const buffer = await readFile7(filePath);
981
+ const base64 = buffer.toString("base64");
982
+ const sizeKB = (size / 1024).toFixed(1);
983
+ const dims = detectImageDimensions(buffer, ext);
984
+ const dimStr = dims ? ` ${dims.width}x${dims.height}` : "";
985
+ return {
986
+ output: `Image: ${filePath}
987
+ Format: ${ext.slice(1).toUpperCase()}${dimStr}, ${sizeKB} KB
988
+ Base64 length: ${base64.length} chars
989
+ (Image content available as binary \u2014 use Bash tools for processing if needed)`
990
+ };
991
+ }
992
+ async function readPdf(filePath) {
993
+ const { execFile: execFile6 } = await import("child_process");
994
+ return new Promise((resolve8) => {
995
+ execFile6("pdftotext", [filePath, "-"], { timeout: 15e3, maxBuffer: 5 * 1024 * 1024 }, (err, stdout) => {
996
+ if (err) {
997
+ resolve8({
998
+ output: `PDF file: ${filePath}
999
+ (Install poppler-utils for text extraction: sudo dnf install poppler-utils)`
1000
+ });
1001
+ return;
1002
+ }
1003
+ const text = stdout.trim();
1004
+ if (!text) {
1005
+ resolve8({ output: `PDF file: ${filePath}
1006
+ (No extractable text \u2014 may be image-based)` });
1007
+ return;
1008
+ }
1009
+ const lines = text.split("\n");
1010
+ const preview = lines.length > 200 ? lines.slice(0, 200).join("\n") + `
1011
+ ... (${lines.length - 200} more lines)` : text;
1012
+ resolve8({ output: `PDF: ${filePath} (${lines.length} lines extracted)
1013
+
1014
+ ${preview}` });
1015
+ });
1016
+ });
1017
+ }
1018
+ function detectImageDimensions(buffer, ext) {
1019
+ try {
1020
+ if (ext === ".png" && buffer.length >= 24) {
1021
+ return { width: buffer.readUInt32BE(16), height: buffer.readUInt32BE(20) };
1022
+ }
1023
+ if ((ext === ".jpg" || ext === ".jpeg") && buffer.length >= 2) {
1024
+ for (let i = 0; i < buffer.length - 8; i++) {
1025
+ if (buffer[i] === 255 && buffer[i + 1] === 192) {
1026
+ return { height: buffer.readUInt16BE(i + 5), width: buffer.readUInt16BE(i + 7) };
1027
+ }
1028
+ }
1029
+ }
1030
+ if (ext === ".gif" && buffer.length >= 10) {
1031
+ return { width: buffer.readUInt16LE(6), height: buffer.readUInt16LE(8) };
1032
+ }
1033
+ } catch {
1034
+ }
1035
+ return null;
1036
+ }
1037
+ var INPUT_SCHEMA2, DEFAULT_LIMIT, IMAGE_EXTS, PDF_EXT, ReadTool;
1038
+ var init_read = __esm({
1039
+ "src/tools/read.ts"() {
1040
+ "use strict";
1041
+ init_file_tracker();
1042
+ init_format();
1043
+ init_types();
1044
+ INPUT_SCHEMA2 = z2.object({
1045
+ file_path: z2.string().describe("Absolute path to the file to read"),
1046
+ offset: z2.number().optional().describe("Line number to start from (0-based). Only provide for large files"),
1047
+ limit: z2.number().optional().describe("Number of lines to read (default 2000)")
1048
+ });
1049
+ DEFAULT_LIMIT = 2e3;
1050
+ IMAGE_EXTS = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg", ".gif", ".webp", ".bmp", ".svg", ".ico"]);
1051
+ PDF_EXT = ".pdf";
1052
+ ReadTool = {
1053
+ name: "Read",
1054
+ description: "Read a file from the filesystem. Returns content with line numbers. Supports text files, images (returns base64 metadata), and basic PDF text extraction. Use offset and limit for large files.",
1055
+ inputSchema: INPUT_SCHEMA2,
1056
+ isReadOnly: true,
1057
+ async call(input, context) {
1058
+ const parsed = INPUT_SCHEMA2.parse(input);
1059
+ const filePath = resolve2(context.cwd, parsed.file_path);
1060
+ try {
1061
+ const info = await stat(filePath);
1062
+ if (info.isDirectory()) {
1063
+ return { output: `Error: ${filePath} is a directory, not a file. Use Bash with 'ls' to list directory contents.`, isError: true };
1064
+ }
1065
+ const ext = extname(filePath).toLowerCase();
1066
+ if (IMAGE_EXTS.has(ext)) {
1067
+ return readImage(filePath, info.size);
1068
+ }
1069
+ if (ext === PDF_EXT) {
1070
+ return readPdf(filePath);
1071
+ }
1072
+ const raw = await readFile7(filePath, "utf-8");
1073
+ markFileRead(filePath, raw);
1074
+ const lines = raw.split("\n");
1075
+ const offset = parsed.offset ?? 0;
1076
+ const limit = parsed.limit ?? DEFAULT_LIMIT;
1077
+ const sliced = lines.slice(offset, offset + limit);
1078
+ const numbered = addLineNumbers(sliced, offset + 1);
1079
+ let output = numbered;
1080
+ if (offset + limit < lines.length) {
1081
+ output += `
1082
+ ... (${lines.length - offset - limit} more lines)`;
1083
+ }
1084
+ return { output };
1085
+ } catch (err) {
1086
+ const msg = err instanceof Error ? err.message : String(err);
1087
+ return { output: `Error reading file: ${msg}`, isError: true };
1088
+ }
1089
+ },
1090
+ toOllamaToolDef() {
1091
+ return buildOllamaToolDef(this);
1092
+ }
1093
+ };
1094
+ }
1095
+ });
1096
+
1097
+ // src/tools/write.ts
1098
+ import { access as access2, mkdir as mkdir7, writeFile as writeFile7 } from "fs/promises";
1099
+ import { dirname as dirname2, resolve as resolve3 } from "path";
1100
+ import { z as z3 } from "zod";
1101
+ var INPUT_SCHEMA3, WriteTool;
1102
+ var init_write = __esm({
1103
+ "src/tools/write.ts"() {
1104
+ "use strict";
1105
+ init_types();
1106
+ INPUT_SCHEMA3 = z3.object({
1107
+ file_path: z3.string().describe("Absolute path to the file to write"),
1108
+ content: z3.string().describe("The content to write to the file")
1109
+ });
1110
+ WriteTool = {
1111
+ name: "Write",
1112
+ description: "Write content to a file. Creates the file if it does not exist, or overwrites it if it does. Creates parent directories as needed.",
1113
+ inputSchema: INPUT_SCHEMA3,
1114
+ isReadOnly: false,
1115
+ async call(input, context) {
1116
+ const parsed = INPUT_SCHEMA3.parse(input);
1117
+ const filePath = resolve3(context.cwd, parsed.file_path);
1118
+ try {
1119
+ let existed = false;
1120
+ try {
1121
+ await access2(filePath);
1122
+ existed = true;
1123
+ } catch {
1124
+ }
1125
+ await mkdir7(dirname2(filePath), { recursive: true });
1126
+ await writeFile7(filePath, parsed.content, "utf-8");
1127
+ const action = existed ? "Updated" : "Created";
1128
+ const lines = parsed.content.split("\n").length;
1129
+ return { output: `${action} ${filePath} (${lines} lines)` };
1130
+ } catch (err) {
1131
+ const msg = err instanceof Error ? err.message : String(err);
1132
+ return { output: `Error writing file: ${msg}`, isError: true };
1133
+ }
1134
+ },
1135
+ toOllamaToolDef() {
1136
+ return buildOllamaToolDef(this);
1137
+ }
1138
+ };
1139
+ }
1140
+ });
1141
+
1142
+ // src/tools/edit.ts
1143
+ import { readFile as readFile8, writeFile as writeFile8 } from "fs/promises";
1144
+ import { resolve as resolve4 } from "path";
1145
+ import { createPatch } from "diff";
1146
+ import { z as z4 } from "zod";
1147
+ function normalizeQuotes(s) {
1148
+ return s.replace(/[\u2018\u2019\u201A\u201B]/g, "'").replace(/[\u201C\u201D\u201E\u201F]/g, '"');
1149
+ }
1150
+ function findActualString(fileContent, searchString) {
1151
+ if (fileContent.includes(searchString)) {
1152
+ return searchString;
1153
+ }
1154
+ const normalizedSearch = normalizeQuotes(searchString);
1155
+ const normalizedFile = normalizeQuotes(fileContent);
1156
+ const idx = normalizedFile.indexOf(normalizedSearch);
1157
+ if (idx !== -1) {
1158
+ return fileContent.substring(idx, idx + normalizedSearch.length);
1159
+ }
1160
+ return null;
1161
+ }
1162
+ var INPUT_SCHEMA4, EditTool;
1163
+ var init_edit = __esm({
1164
+ "src/tools/edit.ts"() {
1165
+ "use strict";
1166
+ init_file_tracker();
1167
+ init_types();
1168
+ INPUT_SCHEMA4 = z4.object({
1169
+ file_path: z4.string().describe("Absolute path to the file to edit"),
1170
+ old_string: z4.string().describe("The exact string to find and replace"),
1171
+ new_string: z4.string().describe("The replacement string"),
1172
+ replace_all: z4.boolean().optional().describe("Replace all occurrences (default false)")
1173
+ });
1174
+ EditTool = {
1175
+ name: "Edit",
1176
+ description: "Edit a file by finding an exact string and replacing it. The old_string must be unique in the file unless replace_all is true.",
1177
+ inputSchema: INPUT_SCHEMA4,
1178
+ isReadOnly: false,
1179
+ async call(input, context) {
1180
+ const parsed = INPUT_SCHEMA4.parse(input);
1181
+ const filePath = resolve4(context.cwd, parsed.file_path);
1182
+ try {
1183
+ if (!hasFileBeenRead(filePath)) {
1184
+ return {
1185
+ output: `Warning: ${filePath} has not been read yet. Use the Read tool first to avoid overwriting unexpected content.`,
1186
+ isError: true
1187
+ };
1188
+ }
1189
+ const content = await readFile8(filePath, "utf-8");
1190
+ const actualOld = findActualString(content, parsed.old_string);
1191
+ if (!actualOld) {
1192
+ return {
1193
+ output: `Error: old_string not found in ${filePath}. Make sure you have the exact text including whitespace and indentation.`,
1194
+ isError: true
1195
+ };
1196
+ }
1197
+ const occurrences = content.split(actualOld).length - 1;
1198
+ if (occurrences > 1 && !parsed.replace_all) {
1199
+ return {
1200
+ output: `Error: old_string appears ${occurrences} times in ${filePath}. Use replace_all: true to replace all, or provide more context to make it unique.`,
1201
+ isError: true
1202
+ };
1203
+ }
1204
+ const newContent = parsed.replace_all ? content.replaceAll(actualOld, parsed.new_string) : content.replace(actualOld, parsed.new_string);
1205
+ await writeFile8(filePath, newContent, "utf-8");
1206
+ const diff = createPatch(filePath, content, newContent, "", "", { context: 3 });
1207
+ return { output: diff };
1208
+ } catch (err) {
1209
+ const msg = err instanceof Error ? err.message : String(err);
1210
+ return { output: `Error editing file: ${msg}`, isError: true };
1211
+ }
1212
+ },
1213
+ toOllamaToolDef() {
1214
+ return buildOllamaToolDef(this);
1215
+ }
1216
+ };
1217
+ }
1218
+ });
1219
+
1220
+ // src/tools/grep.ts
1221
+ import { execFile as execFile4 } from "child_process";
1222
+ import { resolve as resolve5 } from "path";
1223
+ import { z as z5 } from "zod";
1224
+ var INPUT_SCHEMA5, DEFAULT_HEAD_LIMIT, GrepTool;
1225
+ var init_grep = __esm({
1226
+ "src/tools/grep.ts"() {
1227
+ "use strict";
1228
+ init_types();
1229
+ INPUT_SCHEMA5 = z5.object({
1230
+ pattern: z5.string().describe("Regex pattern to search for"),
1231
+ path: z5.string().optional().describe("Directory or file to search in (default: cwd)"),
1232
+ glob: z5.string().optional().describe('Glob pattern to filter files (e.g. "*.ts")'),
1233
+ type: z5.string().optional().describe('File type filter (e.g. "js", "py", "rust") \u2014 more efficient than glob'),
1234
+ output_mode: z5.enum(["content", "files_with_matches", "count"]).optional().describe("Output mode (default: files_with_matches)"),
1235
+ case_insensitive: z5.boolean().optional().describe("Case insensitive search"),
1236
+ multiline: z5.boolean().optional().describe("Enable multiline mode where . matches newlines"),
1237
+ context: z5.number().optional().describe("Lines of context before and after each match"),
1238
+ head_limit: z5.number().optional().describe("Max results to return (default 250, 0 for unlimited)")
1239
+ });
1240
+ DEFAULT_HEAD_LIMIT = 250;
1241
+ GrepTool = {
1242
+ name: "Grep",
1243
+ description: "Search file contents using ripgrep. Supports regex, glob/type filtering, context lines, multiline mode, and multiple output modes.",
1244
+ inputSchema: INPUT_SCHEMA5,
1245
+ isReadOnly: true,
1246
+ async call(input, context) {
1247
+ const parsed = INPUT_SCHEMA5.parse(input);
1248
+ const searchPath = parsed.path ? resolve5(context.cwd, parsed.path) : context.cwd;
1249
+ const mode = parsed.output_mode ?? "files_with_matches";
1250
+ const headLimit = parsed.head_limit ?? DEFAULT_HEAD_LIMIT;
1251
+ const args = [
1252
+ "--hidden",
1253
+ "--glob",
1254
+ "!.git",
1255
+ "--glob",
1256
+ "!.svn",
1257
+ "--glob",
1258
+ "!node_modules",
1259
+ "--max-columns",
1260
+ "500"
1261
+ ];
1262
+ if (mode === "files_with_matches") args.push("-l");
1263
+ else if (mode === "count") args.push("-c");
1264
+ else args.push("-n");
1265
+ if (parsed.case_insensitive) args.push("-i");
1266
+ if (parsed.glob) args.push("--glob", parsed.glob);
1267
+ if (parsed.type) args.push("--type", parsed.type);
1268
+ if (parsed.multiline) {
1269
+ args.push("-U", "--multiline-dotall");
1270
+ }
1271
+ if (parsed.context && parsed.context > 0 && mode === "content") {
1272
+ args.push("-C", String(parsed.context));
1273
+ }
1274
+ if (parsed.pattern.startsWith("-")) {
1275
+ args.push("-e", parsed.pattern);
1276
+ } else {
1277
+ args.push(parsed.pattern);
1278
+ }
1279
+ args.push(searchPath);
1280
+ return new Promise((resolve8) => {
1281
+ execFile4("rg", args, { maxBuffer: 10 * 1024 * 1024, timeout: 3e4 }, (err, stdout, stderr) => {
1282
+ if (err && !stdout) {
1283
+ if (err.code === 1 || err.code === "1") {
1284
+ resolve8({ output: "No matches found." });
1285
+ return;
1286
+ }
1287
+ resolve8({ output: `Grep error: ${stderr || err.message}`, isError: true });
1288
+ return;
1289
+ }
1290
+ const lines = stdout.trim().split("\n").filter(Boolean);
1291
+ const limited = headLimit > 0 ? lines.slice(0, headLimit) : lines;
1292
+ let output = limited.join("\n");
1293
+ if (headLimit > 0 && lines.length > headLimit) {
1294
+ output += `
1295
+ ... (${lines.length - headLimit} more results)`;
1296
+ }
1297
+ if (mode === "count") {
1298
+ const total = lines.reduce((sum, line) => {
1299
+ const count = parseInt(line.split(":").pop() || "0", 10);
1300
+ return sum + (isNaN(count) ? 0 : count);
1301
+ }, 0);
1302
+ output = `${total} matches across ${lines.length} files
1303
+ ${output}`;
1304
+ }
1305
+ resolve8({ output: output || "No matches found." });
1306
+ });
1307
+ });
1308
+ },
1309
+ toOllamaToolDef() {
1310
+ return buildOllamaToolDef(this);
1311
+ }
1312
+ };
1313
+ }
1314
+ });
1315
+
1316
+ // src/tools/glob.ts
1317
+ import { execFile as execFile5 } from "child_process";
1318
+ import { resolve as resolve6 } from "path";
1319
+ import { z as z6 } from "zod";
1320
+ var INPUT_SCHEMA6, MAX_RESULTS, GlobTool;
1321
+ var init_glob = __esm({
1322
+ "src/tools/glob.ts"() {
1323
+ "use strict";
1324
+ init_types();
1325
+ INPUT_SCHEMA6 = z6.object({
1326
+ pattern: z6.string().describe('Glob pattern to match files (e.g. "**/*.ts", "src/**/*.tsx")'),
1327
+ path: z6.string().optional().describe("Directory to search in (default: cwd)")
1328
+ });
1329
+ MAX_RESULTS = 100;
1330
+ GlobTool = {
1331
+ name: "Glob",
1332
+ description: "Find files by glob pattern. Returns matching file paths sorted by modification time. Use for locating files by name or extension.",
1333
+ inputSchema: INPUT_SCHEMA6,
1334
+ isReadOnly: true,
1335
+ async call(input, context) {
1336
+ const parsed = INPUT_SCHEMA6.parse(input);
1337
+ const searchPath = parsed.path ? resolve6(context.cwd, parsed.path) : context.cwd;
1338
+ const args = [
1339
+ "--files",
1340
+ "--hidden",
1341
+ "--glob",
1342
+ parsed.pattern,
1343
+ "--glob",
1344
+ "!.git",
1345
+ "--glob",
1346
+ "!node_modules",
1347
+ "--sort=modified",
1348
+ searchPath
1349
+ ];
1350
+ return new Promise((resolve8) => {
1351
+ execFile5("rg", args, { maxBuffer: 10 * 1024 * 1024, timeout: 3e4 }, (err, stdout, stderr) => {
1352
+ if (err && !stdout) {
1353
+ if (err.code === 1 || err.code === "1") {
1354
+ resolve8({ output: "No files matched." });
1355
+ return;
1356
+ }
1357
+ resolve8({ output: `Glob error: ${stderr || err.message}`, isError: true });
1358
+ return;
1359
+ }
1360
+ const files = stdout.trim().split("\n").filter(Boolean);
1361
+ const limited = files.slice(0, MAX_RESULTS);
1362
+ let output = limited.join("\n");
1363
+ if (files.length > MAX_RESULTS) {
1364
+ output += `
1365
+ ... (${files.length - MAX_RESULTS} more files)`;
1366
+ }
1367
+ if (!output) {
1368
+ output = "No files matched.";
1369
+ }
1370
+ resolve8({ output });
1371
+ });
1372
+ });
1373
+ },
1374
+ toOllamaToolDef() {
1375
+ return buildOllamaToolDef(this);
1376
+ }
1377
+ };
1378
+ }
1379
+ });
1380
+
1381
+ // src/tools/web-fetch.ts
1382
+ import { z as z7 } from "zod";
1383
+ function stripHtml(html) {
1384
+ return html.replace(/<script[\s\S]*?<\/script>/gi, "").replace(/<style[\s\S]*?<\/style>/gi, "").replace(/<[^>]+>/g, " ").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/&nbsp;/g, " ").replace(/\s+/g, " ").replace(/\n\s*\n/g, "\n\n").trim();
1385
+ }
1386
+ var INPUT_SCHEMA7, DEFAULT_MAX_LENGTH, WebFetchTool;
1387
+ var init_web_fetch = __esm({
1388
+ "src/tools/web-fetch.ts"() {
1389
+ "use strict";
1390
+ init_types();
1391
+ INPUT_SCHEMA7 = z7.object({
1392
+ url: z7.string().describe("The URL to fetch"),
1393
+ max_length: z7.number().optional().describe("Maximum response length in characters (default 10000)")
1394
+ });
1395
+ DEFAULT_MAX_LENGTH = 1e4;
1396
+ WebFetchTool = {
1397
+ name: "WebFetch",
1398
+ description: "Fetch a URL and return its text content. Useful for reading documentation, APIs, or web pages.",
1399
+ inputSchema: INPUT_SCHEMA7,
1400
+ isReadOnly: true,
1401
+ async call(input, _context) {
1402
+ const parsed = INPUT_SCHEMA7.parse(input);
1403
+ const maxLen = parsed.max_length ?? DEFAULT_MAX_LENGTH;
1404
+ try {
1405
+ const controller = new AbortController();
1406
+ const timeout = setTimeout(() => controller.abort(), 3e4);
1407
+ const response = await fetch(parsed.url, {
1408
+ headers: {
1409
+ "User-Agent": "Darkfoo-Code/0.1 (Local AI Assistant)",
1410
+ "Accept": "text/html,text/plain,application/json,*/*"
1411
+ },
1412
+ signal: controller.signal
1413
+ });
1414
+ clearTimeout(timeout);
1415
+ if (!response.ok) {
1416
+ return { output: `HTTP ${response.status}: ${response.statusText}`, isError: true };
1417
+ }
1418
+ const contentType = response.headers.get("content-type") || "";
1419
+ let text = await response.text();
1420
+ if (contentType.includes("html")) {
1421
+ text = stripHtml(text);
1422
+ }
1423
+ if (text.length > maxLen) {
1424
+ text = text.slice(0, maxLen) + `
1425
+ ... (truncated, ${text.length} total chars)`;
1426
+ }
1427
+ return { output: text || "(empty response)" };
1428
+ } catch (err) {
1429
+ const msg = err instanceof Error ? err.message : String(err);
1430
+ return { output: `Fetch error: ${msg}`, isError: true };
1431
+ }
1432
+ },
1433
+ toOllamaToolDef() {
1434
+ return buildOllamaToolDef(this);
1435
+ }
1436
+ };
1437
+ }
1438
+ });
1439
+
1440
+ // src/tools/web-search.ts
1441
+ import { z as z8 } from "zod";
1442
+ function parseSearchResults(html, max) {
1443
+ const results = [];
1444
+ const resultBlocks = html.split('class="result__a"');
1445
+ for (let i = 1; i < resultBlocks.length && results.length < max; i++) {
1446
+ const block = resultBlocks[i];
1447
+ const hrefMatch = block.match(/href="([^"]+)"/);
1448
+ let url = hrefMatch?.[1] || "";
1449
+ const uddgMatch = url.match(/uddg=([^&]+)/);
1450
+ if (uddgMatch) {
1451
+ url = decodeURIComponent(uddgMatch[1]);
1452
+ }
1453
+ const titleMatch = block.match(/>([^<]+)<\/a>/);
1454
+ const title = titleMatch?.[1]?.trim() || "Untitled";
1455
+ const snippetSection = resultBlocks[i + 1] || block;
1456
+ const snippetMatch = snippetSection.match(/class="result__snippet"[^>]*>([^<]+)/);
1457
+ const snippet = snippetMatch?.[1]?.trim() || "";
1458
+ if (url && !url.startsWith("/")) {
1459
+ results.push({ title: stripTags(title), url, snippet: stripTags(snippet) });
1460
+ }
1461
+ }
1462
+ return results;
1463
+ }
1464
+ function stripTags(text) {
1465
+ return text.replace(/<[^>]+>/g, "").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').trim();
1466
+ }
1467
+ var INPUT_SCHEMA8, WebSearchTool;
1468
+ var init_web_search = __esm({
1469
+ "src/tools/web-search.ts"() {
1470
+ "use strict";
1471
+ init_types();
1472
+ INPUT_SCHEMA8 = z8.object({
1473
+ query: z8.string().describe("The search query"),
1474
+ max_results: z8.number().optional().describe("Maximum number of results (default 5)")
1475
+ });
1476
+ WebSearchTool = {
1477
+ name: "WebSearch",
1478
+ description: "Search the web using DuckDuckGo. Returns titles, URLs, and snippets for the top results.",
1479
+ inputSchema: INPUT_SCHEMA8,
1480
+ isReadOnly: true,
1481
+ async call(input, _context) {
1482
+ const parsed = INPUT_SCHEMA8.parse(input);
1483
+ const maxResults = parsed.max_results ?? 5;
1484
+ try {
1485
+ const encoded = encodeURIComponent(parsed.query);
1486
+ const url = `https://html.duckduckgo.com/html/?q=${encoded}`;
1487
+ const response = await fetch(url, {
1488
+ headers: {
1489
+ "User-Agent": "Darkfoo-Code/0.1 (Local AI Assistant)"
1490
+ },
1491
+ signal: AbortSignal.timeout(15e3)
1492
+ });
1493
+ if (!response.ok) {
1494
+ return { output: `Search failed: HTTP ${response.status}`, isError: true };
1495
+ }
1496
+ const html = await response.text();
1497
+ const results = parseSearchResults(html, maxResults);
1498
+ if (results.length === 0) {
1499
+ return { output: `No results found for: ${parsed.query}` };
1500
+ }
1501
+ const output = results.map((r, i) => `${i + 1}. ${r.title}
1502
+ ${r.url}
1503
+ ${r.snippet}`).join("\n\n");
1504
+ return { output };
1505
+ } catch (err) {
1506
+ const msg = err instanceof Error ? err.message : String(err);
1507
+ return { output: `Search error: ${msg}`, isError: true };
1508
+ }
1509
+ },
1510
+ toOllamaToolDef() {
1511
+ return buildOllamaToolDef(this);
1512
+ }
1513
+ };
1514
+ }
1515
+ });
1516
+
1517
+ // src/tools/agent.ts
1518
+ import { z as z9 } from "zod";
1519
+ var INPUT_SCHEMA9, MAX_TURNS, AgentTool;
1520
+ var init_agent = __esm({
1521
+ "src/tools/agent.ts"() {
1522
+ "use strict";
1523
+ init_providers();
1524
+ init_system_prompt();
1525
+ init_tools();
1526
+ init_types();
1527
+ INPUT_SCHEMA9 = z9.object({
1528
+ prompt: z9.string().describe("The task for the sub-agent to perform"),
1529
+ description: z9.string().optional().describe("Short description of what the agent will do")
1530
+ });
1531
+ MAX_TURNS = 15;
1532
+ AgentTool = {
1533
+ name: "Agent",
1534
+ description: "Launch a sub-agent to handle a complex task autonomously with its own isolated context. Use for research, multi-step exploration, or parallel work that should not pollute the main conversation.",
1535
+ inputSchema: INPUT_SCHEMA9,
1536
+ isReadOnly: false,
1537
+ async call(input, context) {
1538
+ const parsed = INPUT_SCHEMA9.parse(input);
1539
+ const tools = getTools().filter((t) => t.name !== "Agent");
1540
+ const ollamaTools = tools.map((t) => t.toOllamaToolDef());
1541
+ const model = process.env.DARKFOO_MODEL || "llama3.1:8b";
1542
+ const systemPrompt = await buildSystemPrompt(tools, context.cwd);
1543
+ const messages = [
1544
+ { role: "system", content: systemPrompt },
1545
+ { role: "user", content: parsed.prompt }
1546
+ ];
1547
+ let turns = 0;
1548
+ let finalContent = "";
1549
+ while (turns < MAX_TURNS) {
1550
+ turns++;
1551
+ let assistantContent = "";
1552
+ const toolCalls = [];
1553
+ const provider = getProvider();
1554
+ for await (const event of provider.chatStream({
1555
+ model,
1556
+ messages,
1557
+ tools: ollamaTools,
1558
+ signal: context.abortSignal
1559
+ })) {
1560
+ if (event.type === "text_delta") assistantContent += event.text;
1561
+ if (event.type === "tool_call") toolCalls.push(event.toolCall);
1562
+ if (event.type === "error") return { output: `Agent error: ${event.error}`, isError: true };
1563
+ }
1564
+ messages.push({
1565
+ role: "assistant",
1566
+ content: assistantContent,
1567
+ ...toolCalls.length > 0 ? { tool_calls: toolCalls } : {}
1568
+ });
1569
+ if (toolCalls.length === 0) {
1570
+ finalContent = assistantContent;
1571
+ break;
1572
+ }
1573
+ for (const tc of toolCalls) {
1574
+ const tool = tools.find((t) => t.name.toLowerCase() === tc.function.name.toLowerCase());
1575
+ if (!tool) {
1576
+ messages.push({ role: "tool", content: `Unknown tool: ${tc.function.name}` });
1577
+ continue;
1578
+ }
1579
+ let result;
1580
+ try {
1581
+ result = await tool.call(tc.function.arguments, context);
1582
+ } catch (err) {
1583
+ const msg = err instanceof Error ? err.message : String(err);
1584
+ result = { output: `Error: ${msg}`, isError: true };
1585
+ }
1586
+ messages.push({ role: "tool", content: result.output });
1587
+ }
1588
+ }
1589
+ if (!finalContent && turns >= MAX_TURNS) {
1590
+ finalContent = "(Agent reached max turns without final response)";
1591
+ }
1592
+ return { output: finalContent };
1593
+ },
1594
+ toOllamaToolDef() {
1595
+ return buildOllamaToolDef(this);
1596
+ }
1597
+ };
1598
+ }
1599
+ });
1600
+
1601
+ // src/tools/notebook-edit.ts
1602
+ import { readFile as readFile9, writeFile as writeFile9 } from "fs/promises";
1603
+ import { resolve as resolve7 } from "path";
1604
+ import { z as z10 } from "zod";
1605
+ var INPUT_SCHEMA10, NotebookEditTool;
1606
+ var init_notebook_edit = __esm({
1607
+ "src/tools/notebook-edit.ts"() {
1608
+ "use strict";
1609
+ init_types();
1610
+ INPUT_SCHEMA10 = z10.object({
1611
+ file_path: z10.string().describe("Path to the .ipynb notebook file"),
1612
+ cell_index: z10.number().describe("Index of the cell to edit (0-based)"),
1613
+ new_source: z10.string().describe("New source content for the cell"),
1614
+ cell_type: z10.enum(["code", "markdown"]).optional().describe("Change cell type")
1615
+ });
1616
+ NotebookEditTool = {
1617
+ name: "NotebookEdit",
1618
+ description: "Edit a Jupyter notebook (.ipynb) cell by index. Reads the notebook, modifies the specified cell, and writes it back.",
1619
+ inputSchema: INPUT_SCHEMA10,
1620
+ isReadOnly: false,
1621
+ async call(input, context) {
1622
+ const parsed = INPUT_SCHEMA10.parse(input);
1623
+ const filePath = resolve7(context.cwd, parsed.file_path);
1624
+ try {
1625
+ const raw = await readFile9(filePath, "utf-8");
1626
+ const notebook = JSON.parse(raw);
1627
+ if (parsed.cell_index < 0 || parsed.cell_index >= notebook.cells.length) {
1628
+ return {
1629
+ output: `Cell index ${parsed.cell_index} out of range (notebook has ${notebook.cells.length} cells).`,
1630
+ isError: true
1631
+ };
1632
+ }
1633
+ const cell = notebook.cells[parsed.cell_index];
1634
+ const oldSource = cell.source.join("");
1635
+ cell.source = parsed.new_source.split("\n").map(
1636
+ (line, i, arr) => i < arr.length - 1 ? line + "\n" : line
1637
+ );
1638
+ if (parsed.cell_type) {
1639
+ cell.cell_type = parsed.cell_type;
1640
+ }
1641
+ if (cell.cell_type === "code") {
1642
+ cell.outputs = [];
1643
+ cell.execution_count = null;
1644
+ }
1645
+ await writeFile9(filePath, JSON.stringify(notebook, null, 1) + "\n", "utf-8");
1646
+ return {
1647
+ output: `Updated cell ${parsed.cell_index} in ${filePath} (${cell.cell_type}). Old length: ${oldSource.length}, new length: ${parsed.new_source.length}.`
1648
+ };
1649
+ } catch (err) {
1650
+ const msg = err instanceof Error ? err.message : String(err);
1651
+ return { output: `NotebookEdit error: ${msg}`, isError: true };
1652
+ }
1653
+ },
1654
+ toOllamaToolDef() {
1655
+ return buildOllamaToolDef(this);
1656
+ }
1657
+ };
1658
+ }
1659
+ });
1660
+
1661
+ // src/tools/plan.ts
1662
+ import { z as z11 } from "zod";
1663
+ var ENTER_SCHEMA, EnterPlanModeTool, EXIT_SCHEMA, ExitPlanModeTool;
1664
+ var init_plan = __esm({
1665
+ "src/tools/plan.ts"() {
1666
+ "use strict";
1667
+ init_state();
1668
+ init_types();
1669
+ ENTER_SCHEMA = z11.object({});
1670
+ EnterPlanModeTool = {
1671
+ name: "EnterPlanMode",
1672
+ description: "Enter plan mode for designing an implementation approach before writing code. In plan mode you should explore the codebase with read-only tools (Read, Grep, Glob) and design a plan. Do NOT edit files in plan mode.",
1673
+ inputSchema: ENTER_SCHEMA,
1674
+ isReadOnly: true,
1675
+ async call(_input, _context) {
1676
+ updateAppState((s) => ({ ...s, planMode: true }));
1677
+ return {
1678
+ output: "Entered plan mode. Explore the codebase and design your approach. Use ExitPlanMode when your plan is ready for user approval. Do NOT edit files while in plan mode."
1679
+ };
1680
+ },
1681
+ toOllamaToolDef() {
1682
+ return buildOllamaToolDef(this);
1683
+ }
1684
+ };
1685
+ EXIT_SCHEMA = z11.object({
1686
+ plan: z11.string().describe("The implementation plan to present to the user for approval")
1687
+ });
1688
+ ExitPlanModeTool = {
1689
+ name: "ExitPlanMode",
1690
+ description: "Exit plan mode and present your implementation plan to the user for approval. Include the plan summary as the argument.",
1691
+ inputSchema: EXIT_SCHEMA,
1692
+ isReadOnly: true,
1693
+ async call(input, _context) {
1694
+ const parsed = EXIT_SCHEMA.parse(input);
1695
+ updateAppState((s) => ({ ...s, planMode: false }));
1696
+ return {
1697
+ output: `Plan submitted for user approval:
1698
+
1699
+ ${parsed.plan}
1700
+
1701
+ Plan mode exited. Waiting for user to approve or provide feedback.`
1702
+ };
1703
+ },
1704
+ toOllamaToolDef() {
1705
+ return buildOllamaToolDef(this);
1706
+ }
1707
+ };
1708
+ }
1709
+ });
1710
+
1711
+ // src/tools/tasks.ts
1712
+ import { nanoid as nanoid5 } from "nanoid";
1713
+ import { z as z12 } from "zod";
1714
+ var CREATE_SCHEMA, TaskCreateTool, UPDATE_SCHEMA, TaskUpdateTool, LIST_SCHEMA, TaskListTool, GET_SCHEMA, TaskGetTool;
1715
+ var init_tasks = __esm({
1716
+ "src/tools/tasks.ts"() {
1717
+ "use strict";
1718
+ init_state();
1719
+ init_types();
1720
+ CREATE_SCHEMA = z12.object({
1721
+ subject: z12.string().describe("Brief title for the task"),
1722
+ description: z12.string().describe("What needs to be done")
1723
+ });
1724
+ TaskCreateTool = {
1725
+ name: "TaskCreate",
1726
+ description: "Create a new task to track work progress. Use for multi-step tasks.",
1727
+ inputSchema: CREATE_SCHEMA,
1728
+ isReadOnly: false,
1729
+ async call(input, _context) {
1730
+ const parsed = CREATE_SCHEMA.parse(input);
1731
+ const task = {
1732
+ id: nanoid5(8),
1733
+ subject: parsed.subject,
1734
+ description: parsed.description,
1735
+ status: "pending",
1736
+ createdAt: Date.now()
1737
+ };
1738
+ updateAppState((s) => ({ ...s, tasks: [...s.tasks, task] }));
1739
+ return { output: `Task #${task.id} created: ${task.subject}` };
1740
+ },
1741
+ toOllamaToolDef() {
1742
+ return buildOllamaToolDef(this);
1743
+ }
1744
+ };
1745
+ UPDATE_SCHEMA = z12.object({
1746
+ taskId: z12.string().describe("The task ID to update"),
1747
+ status: z12.enum(["pending", "in_progress", "completed"]).optional().describe("New status"),
1748
+ subject: z12.string().optional().describe("Updated subject"),
1749
+ description: z12.string().optional().describe("Updated description")
1750
+ });
1751
+ TaskUpdateTool = {
1752
+ name: "TaskUpdate",
1753
+ description: "Update a task status or details. Mark as in_progress when starting, completed when done.",
1754
+ inputSchema: UPDATE_SCHEMA,
1755
+ isReadOnly: false,
1756
+ async call(input, _context) {
1757
+ const parsed = UPDATE_SCHEMA.parse(input);
1758
+ const state2 = getAppState();
1759
+ const task = state2.tasks.find((t) => t.id === parsed.taskId);
1760
+ if (!task) return { output: `Task ${parsed.taskId} not found.`, isError: true };
1761
+ updateAppState((s) => ({
1762
+ ...s,
1763
+ tasks: s.tasks.map((t) => {
1764
+ if (t.id !== parsed.taskId) return t;
1765
+ return {
1766
+ ...t,
1767
+ ...parsed.status && { status: parsed.status },
1768
+ ...parsed.subject && { subject: parsed.subject },
1769
+ ...parsed.description && { description: parsed.description }
1770
+ };
1771
+ })
1772
+ }));
1773
+ return { output: `Task #${parsed.taskId} updated${parsed.status ? ` \u2192 ${parsed.status}` : ""}.` };
1774
+ },
1775
+ toOllamaToolDef() {
1776
+ return buildOllamaToolDef(this);
1777
+ }
1778
+ };
1779
+ LIST_SCHEMA = z12.object({});
1780
+ TaskListTool = {
1781
+ name: "TaskList",
1782
+ description: "List all tasks with their status.",
1783
+ inputSchema: LIST_SCHEMA,
1784
+ isReadOnly: true,
1785
+ async call(_input, _context) {
1786
+ const { tasks } = getAppState();
1787
+ if (tasks.length === 0) return { output: "No tasks." };
1788
+ const statusIcon = { pending: "\u25CB", in_progress: "\u25D1", completed: "\u25CF" };
1789
+ const lines = tasks.map(
1790
+ (t) => `${statusIcon[t.status]} #${t.id} [${t.status}] ${t.subject}`
1791
+ );
1792
+ return { output: lines.join("\n") };
1793
+ },
1794
+ toOllamaToolDef() {
1795
+ return buildOllamaToolDef(this);
1796
+ }
1797
+ };
1798
+ GET_SCHEMA = z12.object({
1799
+ taskId: z12.string().describe("The task ID to retrieve")
1800
+ });
1801
+ TaskGetTool = {
1802
+ name: "TaskGet",
1803
+ description: "Get details of a specific task by ID.",
1804
+ inputSchema: GET_SCHEMA,
1805
+ isReadOnly: true,
1806
+ async call(input, _context) {
1807
+ const parsed = GET_SCHEMA.parse(input);
1808
+ const { tasks } = getAppState();
1809
+ const task = tasks.find((t) => t.id === parsed.taskId);
1810
+ if (!task) return { output: `Task ${parsed.taskId} not found.`, isError: true };
1811
+ return {
1812
+ output: [
1813
+ `Task #${task.id}`,
1814
+ `Subject: ${task.subject}`,
1815
+ `Status: ${task.status}`,
1816
+ `Description: ${task.description}`
1817
+ ].join("\n")
1818
+ };
1819
+ },
1820
+ toOllamaToolDef() {
1821
+ return buildOllamaToolDef(this);
1822
+ }
1823
+ };
1824
+ }
1825
+ });
1826
+
1827
+ // src/tools/ask.ts
1828
+ import { z as z13 } from "zod";
1829
+ var INPUT_SCHEMA11, AskUserQuestionTool;
1830
+ var init_ask = __esm({
1831
+ "src/tools/ask.ts"() {
1832
+ "use strict";
1833
+ init_state();
1834
+ init_types();
1835
+ INPUT_SCHEMA11 = z13.object({
1836
+ question: z13.string().describe("The question to ask the user")
1837
+ });
1838
+ AskUserQuestionTool = {
1839
+ name: "AskUserQuestion",
1840
+ description: "Ask the user a question when you need clarification or input before proceeding. The query loop will pause until the user responds.",
1841
+ inputSchema: INPUT_SCHEMA11,
1842
+ isReadOnly: true,
1843
+ async call(input, _context) {
1844
+ const parsed = INPUT_SCHEMA11.parse(input);
1845
+ updateAppState((s) => ({ ...s, pendingQuestion: parsed.question }));
1846
+ return {
1847
+ output: `Question posed to user: ${parsed.question}
1848
+ Waiting for user response...`
1849
+ };
1850
+ },
1851
+ toOllamaToolDef() {
1852
+ return buildOllamaToolDef(this);
1853
+ }
1854
+ };
1855
+ }
1856
+ });
1857
+
1858
+ // src/tools/index.ts
1859
+ var tools_exports = {};
1860
+ __export(tools_exports, {
1861
+ BashTool: () => BashTool,
1862
+ EditTool: () => EditTool,
1863
+ GlobTool: () => GlobTool,
1864
+ GrepTool: () => GrepTool,
1865
+ ReadTool: () => ReadTool,
1866
+ WriteTool: () => WriteTool,
1867
+ getToolByName: () => getToolByName,
1868
+ getTools: () => getTools
1869
+ });
1870
+ function getTools() {
1871
+ return [
1872
+ BashTool,
1873
+ ReadTool,
1874
+ WriteTool,
1875
+ EditTool,
1876
+ GrepTool,
1877
+ GlobTool,
1878
+ WebFetchTool,
1879
+ WebSearchTool,
1880
+ AgentTool,
1881
+ NotebookEditTool,
1882
+ EnterPlanModeTool,
1883
+ ExitPlanModeTool,
1884
+ TaskCreateTool,
1885
+ TaskUpdateTool,
1886
+ TaskListTool,
1887
+ TaskGetTool,
1888
+ AskUserQuestionTool
1889
+ ];
1890
+ }
1891
+ function getToolByName(name) {
1892
+ return getTools().find((t) => t.name.toLowerCase() === name.toLowerCase());
1893
+ }
1894
+ var init_tools = __esm({
1895
+ "src/tools/index.ts"() {
1896
+ "use strict";
1897
+ init_bash();
1898
+ init_read();
1899
+ init_write();
1900
+ init_edit();
1901
+ init_grep();
1902
+ init_glob();
1903
+ init_web_fetch();
1904
+ init_web_search();
1905
+ init_agent();
1906
+ init_notebook_edit();
1907
+ init_plan();
1908
+ init_tasks();
1909
+ init_ask();
1910
+ }
1911
+ });
27
1912
 
28
1913
  // src/main.tsx
29
1914
  import { Command } from "commander";
@@ -44,9 +1929,10 @@ function App({ model, systemPromptOverride, children }) {
44
1929
  import { useState as useState2, useCallback as useCallback2, useEffect, useRef } from "react";
45
1930
  import { Box as Box6, Text as Text6, useApp, useInput as useInput2 } from "ink";
46
1931
  import Spinner2 from "ink-spinner";
47
- import { nanoid as nanoid3 } from "nanoid";
1932
+ import { nanoid as nanoid6 } from "nanoid";
48
1933
 
49
1934
  // src/components/Banner.tsx
1935
+ init_theme();
50
1936
  import { Box, Text } from "ink";
51
1937
  import { jsx as jsx2, jsxs } from "react/jsx-runtime";
52
1938
  var LOGO_LINES = [
@@ -117,6 +2003,8 @@ function Banner({ model, cwd }) {
117
2003
  }
118
2004
 
119
2005
  // src/components/Messages.tsx
2006
+ init_theme();
2007
+ init_format();
120
2008
  import { Box as Box2, Text as Text2 } from "ink";
121
2009
 
122
2010
  // src/utils/markdown.ts
@@ -216,6 +2104,8 @@ function MessageRow({ message }) {
216
2104
  }
217
2105
 
218
2106
  // src/components/ToolCall.tsx
2107
+ init_theme();
2108
+ init_format();
219
2109
  import { Box as Box3, Text as Text3 } from "ink";
220
2110
  import Spinner from "ink-spinner";
221
2111
  import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
@@ -265,6 +2155,8 @@ function formatToolArgs(args) {
265
2155
  }
266
2156
 
267
2157
  // src/components/StatusLine.tsx
2158
+ init_theme();
2159
+ init_state();
268
2160
  import { Box as Box4, Text as Text4 } from "ink";
269
2161
  import { Fragment, jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
270
2162
  function getContextLimit(model) {
@@ -278,12 +2170,12 @@ function getContextLimit(model) {
278
2170
  return 8192;
279
2171
  }
280
2172
  function StatusLine({ model, messageCount, tokenEstimate, isStreaming }) {
281
- const state = getAppState();
2173
+ const state2 = getAppState();
282
2174
  const contextLimit = getContextLimit(model);
283
2175
  const usage = Math.min(tokenEstimate / contextLimit, 1);
284
2176
  const pct = (usage * 100).toFixed(0);
285
2177
  const usageColor = usage > 0.8 ? theme.pink : usage > 0.5 ? theme.yellow : theme.cyan;
286
- const activeTasks = state.tasks.filter((t) => t.status === "in_progress").length;
2178
+ const activeTasks = state2.tasks.filter((t) => t.status === "in_progress").length;
287
2179
  return /* @__PURE__ */ jsxs4(Box4, { marginTop: 1, children: [
288
2180
  /* @__PURE__ */ jsxs4(Text4, { color: theme.dim, children: [
289
2181
  "\u2500".repeat(2),
@@ -301,7 +2193,7 @@ function StatusLine({ model, messageCount, tokenEstimate, isStreaming }) {
301
2193
  pct,
302
2194
  "%"
303
2195
  ] }),
304
- state.planMode ? /* @__PURE__ */ jsxs4(Fragment, { children: [
2196
+ state2.planMode ? /* @__PURE__ */ jsxs4(Fragment, { children: [
305
2197
  /* @__PURE__ */ jsx5(Text4, { color: theme.dim, children: " \u2502 " }),
306
2198
  /* @__PURE__ */ jsx5(Text4, { color: theme.yellow, bold: true, children: "PLAN" })
307
2199
  ] }) : null,
@@ -325,6 +2217,7 @@ function StatusLine({ model, messageCount, tokenEstimate, isStreaming }) {
325
2217
  }
326
2218
 
327
2219
  // src/components/UserInput.tsx
2220
+ init_theme();
328
2221
  import { useState, useCallback } from "react";
329
2222
  import { Box as Box5, Text as Text5, useInput } from "ink";
330
2223
  import TextInput from "ink-text-input";
@@ -409,6 +2302,7 @@ var modelCommand = {
409
2302
  };
410
2303
 
411
2304
  // src/commands/compact.ts
2305
+ init_providers();
412
2306
  import { nanoid } from "nanoid";
413
2307
  var compactCommand = {
414
2308
  name: "compact",
@@ -536,19 +2430,19 @@ var diffCommand = {
536
2430
  name: "diff",
537
2431
  description: "Show uncommitted git changes",
538
2432
  async execute(_args, context) {
539
- return new Promise((resolve) => {
2433
+ return new Promise((resolve8) => {
540
2434
  execFile("git", ["diff", "--stat"], { cwd: context.cwd, timeout: 1e4 }, (err, stdout, stderr) => {
541
2435
  if (err) {
542
- resolve({ output: `Not a git repository or git error: ${stderr || err.message}`, silent: true });
2436
+ resolve8({ output: `Not a git repository or git error: ${stderr || err.message}`, silent: true });
543
2437
  return;
544
2438
  }
545
2439
  if (!stdout.trim()) {
546
2440
  execFile("git", ["diff", "--cached", "--stat"], { cwd: context.cwd, timeout: 1e4 }, (err2, staged) => {
547
2441
  if (err2 || !staged.trim()) {
548
- resolve({ output: "No uncommitted changes.", silent: true });
2442
+ resolve8({ output: "No uncommitted changes.", silent: true });
549
2443
  return;
550
2444
  }
551
- resolve({ output: `\x1B[1m\x1B[36mStaged changes:\x1B[0m
2445
+ resolve8({ output: `\x1B[1m\x1B[36mStaged changes:\x1B[0m
552
2446
  ${staged}`, silent: true });
553
2447
  });
554
2448
  return;
@@ -562,7 +2456,7 @@ ${staged}`, silent: true });
562
2456
  fullDiff ? fullDiff.slice(0, 3e3) : "",
563
2457
  fullDiff && fullDiff.length > 3e3 ? "\n... (truncated)" : ""
564
2458
  ].join("\n");
565
- resolve({ output, silent: true });
2459
+ resolve8({ output, silent: true });
566
2460
  });
567
2461
  });
568
2462
  });
@@ -570,12 +2464,12 @@ ${staged}`, silent: true });
570
2464
  };
571
2465
 
572
2466
  // src/session.ts
573
- import { mkdir, readdir, readFile, writeFile } from "fs/promises";
574
- import { join } from "path";
2467
+ import { mkdir as mkdir2, readdir, readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
2468
+ import { join as join2 } from "path";
575
2469
  import { nanoid as nanoid2 } from "nanoid";
576
- var SESSIONS_DIR = join(process.env.HOME || "~", ".darkfoo", "sessions");
2470
+ var SESSIONS_DIR = join2(process.env.HOME || "~", ".darkfoo", "sessions");
577
2471
  async function ensureDir() {
578
- await mkdir(SESSIONS_DIR, { recursive: true });
2472
+ await mkdir2(SESSIONS_DIR, { recursive: true });
579
2473
  }
580
2474
  function createSessionId() {
581
2475
  return nanoid2(12);
@@ -594,11 +2488,11 @@ async function saveSession(id, messages, model, cwd) {
594
2488
  messageCount: messages.length,
595
2489
  messages
596
2490
  };
597
- await writeFile(join(SESSIONS_DIR, `${id}.json`), JSON.stringify(data, null, 2), "utf-8");
2491
+ await writeFile2(join2(SESSIONS_DIR, `${id}.json`), JSON.stringify(data, null, 2), "utf-8");
598
2492
  }
599
2493
  async function loadSession(id) {
600
2494
  try {
601
- const raw = await readFile(join(SESSIONS_DIR, `${id}.json`), "utf-8");
2495
+ const raw = await readFile2(join2(SESSIONS_DIR, `${id}.json`), "utf-8");
602
2496
  return JSON.parse(raw);
603
2497
  } catch {
604
2498
  return null;
@@ -611,7 +2505,7 @@ async function listSessions() {
611
2505
  for (const file of files) {
612
2506
  if (!file.endsWith(".json")) continue;
613
2507
  try {
614
- const raw = await readFile(join(SESSIONS_DIR, file), "utf-8");
2508
+ const raw = await readFile2(join2(SESSIONS_DIR, file), "utf-8");
615
2509
  const data = JSON.parse(raw);
616
2510
  sessions.push({
617
2511
  id: data.id,
@@ -676,6 +2570,7 @@ var resumeCommand = {
676
2570
  };
677
2571
 
678
2572
  // src/commands/commit.ts
2573
+ init_providers();
679
2574
  import { execFile as execFile2 } from "child_process";
680
2575
  var commitCommand = {
681
2576
  name: "commit",
@@ -732,12 +2627,12 @@ ${truncatedDiff}`
732
2627
  }
733
2628
  };
734
2629
  function gitExec(args, cwd) {
735
- return new Promise((resolve, reject) => {
2630
+ return new Promise((resolve8, reject) => {
736
2631
  execFile2("git", args, { cwd, timeout: 15e3, maxBuffer: 1024 * 1024 }, (err, stdout, stderr) => {
737
2632
  if (err && !stdout) {
738
2633
  reject(new Error(stderr || err.message));
739
2634
  } else {
740
- resolve(stdout);
2635
+ resolve8(stdout);
741
2636
  }
742
2637
  });
743
2638
  });
@@ -763,7 +2658,7 @@ var copyCommand = {
763
2658
  }
764
2659
  };
765
2660
  function copyToClipboard(text) {
766
- return new Promise((resolve, reject) => {
2661
+ return new Promise((resolve8, reject) => {
767
2662
  const tools = [
768
2663
  { cmd: "xclip", args: ["-selection", "clipboard"] },
769
2664
  { cmd: "xsel", args: ["--clipboard", "--input"] },
@@ -781,7 +2676,7 @@ function copyToClipboard(text) {
781
2676
  if (err) {
782
2677
  tryNext();
783
2678
  } else {
784
- resolve();
2679
+ resolve8();
785
2680
  }
786
2681
  });
787
2682
  if (proc.stdin) {
@@ -834,9 +2729,9 @@ var reviewCommand = {
834
2729
  };
835
2730
 
836
2731
  // src/commands/config.ts
837
- import { readFile as readFile2, writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
838
- import { join as join2 } from "path";
839
- var SETTINGS_PATH = join2(process.env.HOME || "~", ".darkfoo", "settings.json");
2732
+ import { readFile as readFile3, writeFile as writeFile3, mkdir as mkdir3 } from "fs/promises";
2733
+ import { join as join3 } from "path";
2734
+ var SETTINGS_PATH2 = join3(process.env.HOME || "~", ".darkfoo", "settings.json");
840
2735
  var configCommand = {
841
2736
  name: "config",
842
2737
  aliases: ["settings"],
@@ -845,12 +2740,12 @@ var configCommand = {
845
2740
  const parts = args.trim().split(/\s+/);
846
2741
  if (!args.trim() || parts[0] === "show") {
847
2742
  try {
848
- const raw = await readFile2(SETTINGS_PATH, "utf-8");
2743
+ const raw = await readFile3(SETTINGS_PATH2, "utf-8");
849
2744
  const config = JSON.parse(raw);
850
2745
  return {
851
2746
  output: [
852
2747
  "\x1B[1m\x1B[36mSettings\x1B[0m",
853
- `\x1B[2m${SETTINGS_PATH}\x1B[0m`,
2748
+ `\x1B[2m${SETTINGS_PATH2}\x1B[0m`,
854
2749
  "",
855
2750
  JSON.stringify(config, null, 2)
856
2751
  ].join("\n"),
@@ -862,7 +2757,7 @@ var configCommand = {
862
2757
  "\x1B[1m\x1B[36mSettings\x1B[0m",
863
2758
  "",
864
2759
  "No settings file found. Default settings in use.",
865
- `\x1B[2mSettings will be created at: ${SETTINGS_PATH}\x1B[0m`
2760
+ `\x1B[2mSettings will be created at: ${SETTINGS_PATH2}\x1B[0m`
866
2761
  ].join("\n"),
867
2762
  silent: true
868
2763
  };
@@ -873,7 +2768,7 @@ var configCommand = {
873
2768
  const value = parts.slice(2).join(" ");
874
2769
  let config;
875
2770
  try {
876
- const raw = await readFile2(SETTINGS_PATH, "utf-8");
2771
+ const raw = await readFile3(SETTINGS_PATH2, "utf-8");
877
2772
  config = JSON.parse(raw);
878
2773
  } catch {
879
2774
  config = {};
@@ -883,21 +2778,21 @@ var configCommand = {
883
2778
  else if (value === "false") parsed = false;
884
2779
  else if (!isNaN(Number(value))) parsed = Number(value);
885
2780
  config[key] = parsed;
886
- await mkdir2(join2(process.env.HOME || "~", ".darkfoo"), { recursive: true });
887
- await writeFile2(SETTINGS_PATH, JSON.stringify(config, null, 2), "utf-8");
2781
+ await mkdir3(join3(process.env.HOME || "~", ".darkfoo"), { recursive: true });
2782
+ await writeFile3(SETTINGS_PATH2, JSON.stringify(config, null, 2), "utf-8");
888
2783
  return { output: `Set ${key} = ${JSON.stringify(parsed)}`, silent: true };
889
2784
  }
890
2785
  if (parts[0] === "delete" && parts[1]) {
891
2786
  const key = parts[1];
892
2787
  let config;
893
2788
  try {
894
- const raw = await readFile2(SETTINGS_PATH, "utf-8");
2789
+ const raw = await readFile3(SETTINGS_PATH2, "utf-8");
895
2790
  config = JSON.parse(raw);
896
2791
  } catch {
897
2792
  return { output: "No settings to delete from.", silent: true };
898
2793
  }
899
2794
  delete config[key];
900
- await writeFile2(SETTINGS_PATH, JSON.stringify(config, null, 2), "utf-8");
2795
+ await writeFile3(SETTINGS_PATH2, JSON.stringify(config, null, 2), "utf-8");
901
2796
  return { output: `Deleted ${key}`, silent: true };
902
2797
  }
903
2798
  return {
@@ -975,7 +2870,7 @@ var themeCommand = {
975
2870
  if (!THEMES[name]) {
976
2871
  return { output: `Unknown theme: ${name}. Available: ${Object.keys(THEMES).join(", ")}`, silent: true };
977
2872
  }
978
- const { theme: theme2 } = await import("./theme-BQAEFGVB.js");
2873
+ const { theme: theme2 } = await Promise.resolve().then(() => (init_theme(), theme_exports));
979
2874
  const newTheme = THEMES[name];
980
2875
  Object.assign(theme2, newTheme);
981
2876
  return { output: `Theme switched to: ${name}`, silent: true };
@@ -989,8 +2884,8 @@ function hexToRgb(hex) {
989
2884
  }
990
2885
 
991
2886
  // src/commands/export.ts
992
- import { writeFile as writeFile3, mkdir as mkdir3 } from "fs/promises";
993
- import { join as join3 } from "path";
2887
+ import { writeFile as writeFile4, mkdir as mkdir4 } from "fs/promises";
2888
+ import { join as join4 } from "path";
994
2889
  var exportCommand = {
995
2890
  name: "export",
996
2891
  description: "Export conversation to markdown file (usage: /export [filename])",
@@ -1000,9 +2895,9 @@ var exportCommand = {
1000
2895
  }
1001
2896
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
1002
2897
  const filename = args.trim() || `darkfoo-session-${timestamp}.md`;
1003
- const exportDir = join3(process.env.HOME || "~", ".darkfoo", "exports");
1004
- await mkdir3(exportDir, { recursive: true });
1005
- const filePath = join3(exportDir, filename);
2898
+ const exportDir = join4(process.env.HOME || "~", ".darkfoo", "exports");
2899
+ await mkdir4(exportDir, { recursive: true });
2900
+ const filePath = join4(exportDir, filename);
1006
2901
  const lines = [
1007
2902
  `# Darkfoo Code Session`,
1008
2903
  ``,
@@ -1034,23 +2929,25 @@ var exportCommand = {
1034
2929
  break;
1035
2930
  }
1036
2931
  }
1037
- await writeFile3(filePath, lines.join("\n"), "utf-8");
2932
+ await writeFile4(filePath, lines.join("\n"), "utf-8");
1038
2933
  return { output: `Exported to ${filePath}`, silent: true };
1039
2934
  }
1040
2935
  };
1041
2936
 
1042
2937
  // src/commands/status.ts
2938
+ init_file_tracker();
2939
+ init_state();
1043
2940
  var statusCommand = {
1044
2941
  name: "status",
1045
2942
  description: "Show session status overview",
1046
2943
  async execute(_args, context) {
1047
- const state = getAppState();
2944
+ const state2 = getAppState();
1048
2945
  const userMsgs = context.messages.filter((m) => m.role === "user").length;
1049
2946
  const assistantMsgs = context.messages.filter((m) => m.role === "assistant").length;
1050
2947
  const toolMsgs = context.messages.filter((m) => m.role === "tool").length;
1051
2948
  const totalChars = context.messages.reduce((sum, m) => sum + m.content.length, 0);
1052
2949
  const estTokens = Math.ceil(totalChars / 4);
1053
- const tasks = state.tasks;
2950
+ const tasks = state2.tasks;
1054
2951
  const pending = tasks.filter((t) => t.status === "pending").length;
1055
2952
  const inProgress = tasks.filter((t) => t.status === "in_progress").length;
1056
2953
  const completed = tasks.filter((t) => t.status === "completed").length;
@@ -1059,7 +2956,7 @@ var statusCommand = {
1059
2956
  "",
1060
2957
  ` Model: \x1B[36m${context.model}\x1B[0m`,
1061
2958
  ` Working Dir: ${context.cwd}`,
1062
- ` Plan Mode: ${state.planMode ? "\x1B[33mActive\x1B[0m" : "Off"}`,
2959
+ ` Plan Mode: ${state2.planMode ? "\x1B[33mActive\x1B[0m" : "Off"}`,
1063
2960
  "",
1064
2961
  " \x1B[1mMessages\x1B[0m",
1065
2962
  ` User: ${userMsgs}`,
@@ -1083,7 +2980,7 @@ var filesCommand = {
1083
2980
  name: "files",
1084
2981
  description: "List files modified or referenced in this session",
1085
2982
  async execute(_args, context) {
1086
- const readFiles = /* @__PURE__ */ new Set();
2983
+ const readFiles2 = /* @__PURE__ */ new Set();
1087
2984
  const writtenFiles = /* @__PURE__ */ new Set();
1088
2985
  const editedFiles = /* @__PURE__ */ new Set();
1089
2986
  for (const msg of context.messages) {
@@ -1094,7 +2991,7 @@ var filesCommand = {
1094
2991
  if (!path) continue;
1095
2992
  switch (tc.function.name) {
1096
2993
  case "Read":
1097
- readFiles.add(path);
2994
+ readFiles2.add(path);
1098
2995
  break;
1099
2996
  case "Write":
1100
2997
  writtenFiles.add(path);
@@ -1106,13 +3003,13 @@ var filesCommand = {
1106
3003
  }
1107
3004
  }
1108
3005
  }
1109
- if (readFiles.size === 0 && writtenFiles.size === 0 && editedFiles.size === 0) {
3006
+ if (readFiles2.size === 0 && writtenFiles.size === 0 && editedFiles.size === 0) {
1110
3007
  return { output: "No files referenced in this session.", silent: true };
1111
3008
  }
1112
3009
  const lines = ["\x1B[1m\x1B[36mSession Files\x1B[0m", ""];
1113
- if (readFiles.size > 0) {
3010
+ if (readFiles2.size > 0) {
1114
3011
  lines.push(" \x1B[1mRead:\x1B[0m");
1115
- for (const f of readFiles) lines.push(` \x1B[36m${f}\x1B[0m`);
3012
+ for (const f of readFiles2) lines.push(` \x1B[36m${f}\x1B[0m`);
1116
3013
  lines.push("");
1117
3014
  }
1118
3015
  if (writtenFiles.size > 0) {
@@ -1170,6 +3067,7 @@ var briefCommand = {
1170
3067
  };
1171
3068
 
1172
3069
  // src/commands/provider.ts
3070
+ init_providers();
1173
3071
  var providerCommand = {
1174
3072
  name: "provider",
1175
3073
  aliases: ["backend"],
@@ -1226,15 +3124,15 @@ var providerCommand = {
1226
3124
  if (subcommand === "remove") {
1227
3125
  const name = parts[1];
1228
3126
  if (!name) return { output: "Usage: /provider remove <name>", silent: true };
1229
- const { removeProviderConfig } = await import("./providers-P7Z3JXQH.js");
1230
- if (removeProviderConfig(name)) {
3127
+ const { removeProviderConfig: removeProviderConfig2 } = await Promise.resolve().then(() => (init_providers(), providers_exports));
3128
+ if (removeProviderConfig2(name)) {
1231
3129
  await saveProviderSettings();
1232
3130
  return { output: `Removed provider: ${name}`, silent: true };
1233
3131
  }
1234
3132
  return { output: `Provider not found: ${name}`, silent: true };
1235
3133
  }
1236
3134
  if (subcommand === "models") {
1237
- const { getProvider: getProvider2 } = await import("./providers-P7Z3JXQH.js");
3135
+ const { getProvider: getProvider2 } = await Promise.resolve().then(() => (init_providers(), providers_exports));
1238
3136
  const provider = getProvider2();
1239
3137
  const models = await provider.listModels();
1240
3138
  if (models.length === 0) {
@@ -1396,6 +3294,12 @@ function UserInput({ value, onChange, onSubmit, disabled, history }) {
1396
3294
  }
1397
3295
 
1398
3296
  // src/repl.tsx
3297
+ init_query();
3298
+ init_system_prompt();
3299
+ init_providers();
3300
+ init_tools();
3301
+ init_bash();
3302
+ init_theme();
1399
3303
  import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
1400
3304
  function getContextLimit3(model) {
1401
3305
  const lower = model.toLowerCase();
@@ -1451,7 +3355,7 @@ function REPL({ initialPrompt }) {
1451
3355
  const runQuery = useCallback2(
1452
3356
  async (userMessage) => {
1453
3357
  const userMsg = {
1454
- id: nanoid3(),
3358
+ id: nanoid6(),
1455
3359
  role: "user",
1456
3360
  content: userMessage,
1457
3361
  timestamp: Date.now()
@@ -1488,7 +3392,7 @@ function REPL({ initialPrompt }) {
1488
3392
  setActiveTool(null);
1489
3393
  setToolResults((prev) => [
1490
3394
  ...prev,
1491
- { id: nanoid3(), toolName: event.toolName, output: event.output, isError: event.isError }
3395
+ { id: nanoid6(), toolName: event.toolName, output: event.output, isError: event.isError }
1492
3396
  ]);
1493
3397
  break;
1494
3398
  case "assistant_message":
@@ -1507,7 +3411,7 @@ function REPL({ initialPrompt }) {
1507
3411
  case "error":
1508
3412
  setMessages((prev) => [
1509
3413
  ...prev,
1510
- { id: nanoid3(), role: "assistant", content: `Error: ${event.error}`, timestamp: Date.now() }
3414
+ { id: nanoid6(), role: "assistant", content: `Error: ${event.error}`, timestamp: Date.now() }
1511
3415
  ]);
1512
3416
  break;
1513
3417
  }
@@ -1517,7 +3421,7 @@ function REPL({ initialPrompt }) {
1517
3421
  const msg = err instanceof Error ? err.message : String(err);
1518
3422
  setMessages((prev) => [
1519
3423
  ...prev,
1520
- { id: nanoid3(), role: "assistant", content: `Error: ${msg}`, timestamp: Date.now() }
3424
+ { id: nanoid6(), role: "assistant", content: `Error: ${msg}`, timestamp: Date.now() }
1521
3425
  ]);
1522
3426
  }
1523
3427
  } finally {
@@ -1604,7 +3508,7 @@ function REPL({ initialPrompt }) {
1604
3508
  ${transcript}` }]
1605
3509
  }).then((result) => {
1606
3510
  const summaryMsg = {
1607
- id: nanoid3(),
3511
+ id: nanoid6(),
1608
3512
  role: "user",
1609
3513
  content: `[Auto-compacted summary]
1610
3514
  ${result.content}
@@ -1671,9 +3575,10 @@ ${result.content}
1671
3575
  }
1672
3576
 
1673
3577
  // src/main.tsx
3578
+ init_providers();
1674
3579
  import { jsx as jsx8 } from "react/jsx-runtime";
1675
3580
  var program = new Command();
1676
- program.name("darkfoo").description("Darkfoo Code \u2014 local AI coding assistant powered by local LLM providers").version("0.1.0").option("-m, --model <model>", "Model to use", "llama3.1:8b").option("-p, --prompt <prompt>", "Run a single prompt (non-interactive)").option("--provider <name>", "LLM provider backend (ollama, llama-cpp, vllm, tgi, etc.)").option("--system-prompt <prompt>", "Override the system prompt").action(async (options) => {
3581
+ program.name("darkfoo").description("Darkfoo Code \u2014 local AI coding assistant powered by local LLM providers").version("0.1.4").option("-m, --model <model>", "Model to use", "llama3.1:8b").option("-p, --prompt <prompt>", "Run a single prompt (non-interactive)").option("--provider <name>", "LLM provider backend (ollama, llama-cpp, vllm, tgi, etc.)").option("--system-prompt <prompt>", "Override the system prompt").action(async (options) => {
1677
3582
  const { model, prompt, provider, systemPrompt } = options;
1678
3583
  await loadProviderSettings();
1679
3584
  if (provider) {
@@ -1687,14 +3592,14 @@ program.name("darkfoo").description("Darkfoo Code \u2014 local AI coding assista
1687
3592
  }
1688
3593
  }
1689
3594
  if (prompt) {
1690
- const { buildSystemPrompt: buildSystemPrompt2 } = await import("./system-prompt-YJSDZVOM.js");
1691
- const { getTools: getTools2 } = await import("./tools-34S775OZ.js");
1692
- const { query: query2 } = await import("./query-E6NPBSUX.js");
3595
+ const { buildSystemPrompt: buildSystemPrompt2 } = await Promise.resolve().then(() => (init_system_prompt(), system_prompt_exports));
3596
+ const { getTools: getTools2 } = await Promise.resolve().then(() => (init_tools(), tools_exports));
3597
+ const { query: query2 } = await Promise.resolve().then(() => (init_query(), query_exports));
1693
3598
  const tools = getTools2();
1694
3599
  const sysPrompt = systemPrompt ?? await buildSystemPrompt2(tools, process.cwd());
1695
- const { nanoid: nanoid4 } = await import("nanoid");
3600
+ const { nanoid: nanoid7 } = await import("nanoid");
1696
3601
  const userMsg = {
1697
- id: nanoid4(),
3602
+ id: nanoid7(),
1698
3603
  role: "user",
1699
3604
  content: prompt,
1700
3605
  timestamp: Date.now()