darkfoo-code 0.1.3 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,1023 +0,0 @@
1
- import {
2
- getProvider
3
- } from "./chunk-OBL22IIN.js";
4
- import {
5
- buildSystemPrompt
6
- } from "./chunk-VSJKCANO.js";
7
-
8
- // src/tools/bash.ts
9
- import { spawn } from "child_process";
10
- import { writeFile, mkdir, readFile } from "fs/promises";
11
- import { join } from "path";
12
- import { nanoid } from "nanoid";
13
- import { z } from "zod";
14
-
15
- // src/tools/types.ts
16
- import { zodToJsonSchema } from "zod-to-json-schema";
17
- function buildOllamaToolDef(tool) {
18
- const raw = zodToJsonSchema(tool.inputSchema);
19
- delete raw.$schema;
20
- delete raw.additionalProperties;
21
- return {
22
- type: "function",
23
- function: {
24
- name: tool.name,
25
- description: tool.description,
26
- parameters: raw
27
- }
28
- };
29
- }
30
-
31
- // src/tools/bash.ts
32
- var INPUT_SCHEMA = z.object({
33
- command: z.string().describe("The bash command to execute"),
34
- description: z.string().optional().describe("Short description of what this command does"),
35
- timeout: z.number().optional().describe("Timeout in milliseconds (default 120000, max 600000)"),
36
- run_in_background: z.boolean().optional().describe("Run in background, returning a task ID for later retrieval")
37
- });
38
- var MAX_OUTPUT = 1e5;
39
- var BG_OUTPUT_DIR = join(process.env.HOME || "~", ".darkfoo", "bg-tasks");
40
- var backgroundTasks = /* @__PURE__ */ new Map();
41
- var BashTool = {
42
- name: "Bash",
43
- 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.",
44
- inputSchema: INPUT_SCHEMA,
45
- isReadOnly: false,
46
- async call(input, context) {
47
- const parsed = INPUT_SCHEMA.parse(input);
48
- const timeout = Math.min(parsed.timeout ?? 12e4, 6e5);
49
- if (parsed.run_in_background) {
50
- return runInBackground(parsed.command, context.cwd);
51
- }
52
- return new Promise((resolve7) => {
53
- const proc = spawn(parsed.command, {
54
- shell: true,
55
- cwd: context.cwd,
56
- env: { ...process.env, TERM: "dumb" },
57
- signal: context.abortSignal,
58
- timeout
59
- });
60
- let stdout = "";
61
- let stderr = "";
62
- proc.stdout?.on("data", (data) => {
63
- const chunk = data.toString();
64
- if (stdout.length < MAX_OUTPUT) {
65
- stdout += chunk.slice(0, MAX_OUTPUT - stdout.length);
66
- }
67
- });
68
- proc.stderr?.on("data", (data) => {
69
- const chunk = data.toString();
70
- if (stderr.length < MAX_OUTPUT) {
71
- stderr += chunk.slice(0, MAX_OUTPUT - stderr.length);
72
- }
73
- });
74
- proc.on("error", (err) => {
75
- resolve7({ output: `Error: ${err.message}`, isError: true });
76
- });
77
- proc.on("close", (code) => {
78
- let output = stdout;
79
- if (stdout.length >= MAX_OUTPUT) {
80
- output += "\n... (output truncated)";
81
- }
82
- if (stderr) {
83
- output += (output ? "\n" : "") + `stderr: ${stderr}`;
84
- }
85
- if (!output) {
86
- output = code === 0 ? "(no output)" : `Command failed with exit code ${code}`;
87
- }
88
- resolve7({ output, isError: code !== 0 });
89
- });
90
- });
91
- },
92
- toOllamaToolDef() {
93
- return buildOllamaToolDef(this);
94
- }
95
- };
96
- async function runInBackground(command, cwd) {
97
- const taskId = nanoid(8);
98
- await mkdir(BG_OUTPUT_DIR, { recursive: true });
99
- const outputPath = join(BG_OUTPUT_DIR, `${taskId}.output`);
100
- backgroundTasks.set(taskId, { command, status: "running", outputPath });
101
- const proc = spawn(command, {
102
- shell: true,
103
- cwd,
104
- env: { ...process.env, TERM: "dumb" },
105
- detached: false
106
- });
107
- let stdout = "";
108
- let stderr = "";
109
- proc.stdout?.on("data", (data) => {
110
- stdout += data.toString();
111
- });
112
- proc.stderr?.on("data", (data) => {
113
- stderr += data.toString();
114
- });
115
- proc.on("close", async (code) => {
116
- const output = stdout + (stderr ? `
117
- stderr: ${stderr}` : "");
118
- await writeFile(outputPath, output || `(exit code ${code})`, "utf-8").catch(() => {
119
- });
120
- const task = backgroundTasks.get(taskId);
121
- if (task) {
122
- task.status = code === 0 ? "completed" : "failed";
123
- }
124
- });
125
- proc.on("error", async (err) => {
126
- await writeFile(outputPath, `Error: ${err.message}`, "utf-8").catch(() => {
127
- });
128
- const task = backgroundTasks.get(taskId);
129
- if (task) {
130
- task.status = "failed";
131
- }
132
- });
133
- return {
134
- output: `Background task started with ID: ${taskId}
135
- Command: ${command}
136
- Use Bash to run: cat ${outputPath} (to check output later)`
137
- };
138
- }
139
-
140
- // src/tools/read.ts
141
- import { readFile as readFile2, stat } from "fs/promises";
142
- import { extname, resolve } from "path";
143
- import { z as z2 } from "zod";
144
-
145
- // src/file-tracker.ts
146
- var readFiles = /* @__PURE__ */ new Map();
147
- function simpleHash(str) {
148
- let hash = 0;
149
- for (let i = 0; i < str.length; i++) {
150
- const char = str.charCodeAt(i);
151
- hash = (hash << 5) - hash + char;
152
- hash |= 0;
153
- }
154
- return hash;
155
- }
156
- function markFileRead(path, content) {
157
- readFiles.set(path, { readAt: Date.now(), contentHash: simpleHash(content) });
158
- }
159
- function hasFileBeenRead(path) {
160
- return readFiles.has(path);
161
- }
162
- function trackedFileCount() {
163
- return readFiles.size;
164
- }
165
-
166
- // src/utils/format.ts
167
- function addLineNumbers(lines, startLine = 1) {
168
- const maxNum = startLine + lines.length - 1;
169
- const width = String(maxNum).length;
170
- return lines.map((line, i) => `${String(startLine + i).padStart(width)} ${line}`).join("\n");
171
- }
172
- function truncate(text, maxLen) {
173
- if (text.length <= maxLen) return text;
174
- return text.slice(0, maxLen - 3) + "...";
175
- }
176
-
177
- // src/tools/read.ts
178
- var INPUT_SCHEMA2 = z2.object({
179
- file_path: z2.string().describe("Absolute path to the file to read"),
180
- offset: z2.number().optional().describe("Line number to start from (0-based). Only provide for large files"),
181
- limit: z2.number().optional().describe("Number of lines to read (default 2000)")
182
- });
183
- var DEFAULT_LIMIT = 2e3;
184
- var IMAGE_EXTS = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg", ".gif", ".webp", ".bmp", ".svg", ".ico"]);
185
- var PDF_EXT = ".pdf";
186
- var ReadTool = {
187
- name: "Read",
188
- 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.",
189
- inputSchema: INPUT_SCHEMA2,
190
- isReadOnly: true,
191
- async call(input, context) {
192
- const parsed = INPUT_SCHEMA2.parse(input);
193
- const filePath = resolve(context.cwd, parsed.file_path);
194
- try {
195
- const info = await stat(filePath);
196
- if (info.isDirectory()) {
197
- return { output: `Error: ${filePath} is a directory, not a file. Use Bash with 'ls' to list directory contents.`, isError: true };
198
- }
199
- const ext = extname(filePath).toLowerCase();
200
- if (IMAGE_EXTS.has(ext)) {
201
- return readImage(filePath, info.size);
202
- }
203
- if (ext === PDF_EXT) {
204
- return readPdf(filePath);
205
- }
206
- const raw = await readFile2(filePath, "utf-8");
207
- markFileRead(filePath, raw);
208
- const lines = raw.split("\n");
209
- const offset = parsed.offset ?? 0;
210
- const limit = parsed.limit ?? DEFAULT_LIMIT;
211
- const sliced = lines.slice(offset, offset + limit);
212
- const numbered = addLineNumbers(sliced, offset + 1);
213
- let output = numbered;
214
- if (offset + limit < lines.length) {
215
- output += `
216
- ... (${lines.length - offset - limit} more lines)`;
217
- }
218
- return { output };
219
- } catch (err) {
220
- const msg = err instanceof Error ? err.message : String(err);
221
- return { output: `Error reading file: ${msg}`, isError: true };
222
- }
223
- },
224
- toOllamaToolDef() {
225
- return buildOllamaToolDef(this);
226
- }
227
- };
228
- async function readImage(filePath, size) {
229
- const ext = extname(filePath).toLowerCase();
230
- if (ext === ".svg") {
231
- const content = await readFile2(filePath, "utf-8");
232
- return { output: `SVG image (${size} bytes):
233
- ${content.slice(0, 5e3)}` };
234
- }
235
- const buffer = await readFile2(filePath);
236
- const base64 = buffer.toString("base64");
237
- const sizeKB = (size / 1024).toFixed(1);
238
- const dims = detectImageDimensions(buffer, ext);
239
- const dimStr = dims ? ` ${dims.width}x${dims.height}` : "";
240
- return {
241
- output: `Image: ${filePath}
242
- Format: ${ext.slice(1).toUpperCase()}${dimStr}, ${sizeKB} KB
243
- Base64 length: ${base64.length} chars
244
- (Image content available as binary \u2014 use Bash tools for processing if needed)`
245
- };
246
- }
247
- async function readPdf(filePath) {
248
- const { execFile: execFile3 } = await import("child_process");
249
- return new Promise((resolve7) => {
250
- execFile3("pdftotext", [filePath, "-"], { timeout: 15e3, maxBuffer: 5 * 1024 * 1024 }, (err, stdout) => {
251
- if (err) {
252
- resolve7({
253
- output: `PDF file: ${filePath}
254
- (Install poppler-utils for text extraction: sudo dnf install poppler-utils)`
255
- });
256
- return;
257
- }
258
- const text = stdout.trim();
259
- if (!text) {
260
- resolve7({ output: `PDF file: ${filePath}
261
- (No extractable text \u2014 may be image-based)` });
262
- return;
263
- }
264
- const lines = text.split("\n");
265
- const preview = lines.length > 200 ? lines.slice(0, 200).join("\n") + `
266
- ... (${lines.length - 200} more lines)` : text;
267
- resolve7({ output: `PDF: ${filePath} (${lines.length} lines extracted)
268
-
269
- ${preview}` });
270
- });
271
- });
272
- }
273
- function detectImageDimensions(buffer, ext) {
274
- try {
275
- if (ext === ".png" && buffer.length >= 24) {
276
- return { width: buffer.readUInt32BE(16), height: buffer.readUInt32BE(20) };
277
- }
278
- if ((ext === ".jpg" || ext === ".jpeg") && buffer.length >= 2) {
279
- for (let i = 0; i < buffer.length - 8; i++) {
280
- if (buffer[i] === 255 && buffer[i + 1] === 192) {
281
- return { height: buffer.readUInt16BE(i + 5), width: buffer.readUInt16BE(i + 7) };
282
- }
283
- }
284
- }
285
- if (ext === ".gif" && buffer.length >= 10) {
286
- return { width: buffer.readUInt16LE(6), height: buffer.readUInt16LE(8) };
287
- }
288
- } catch {
289
- }
290
- return null;
291
- }
292
-
293
- // src/tools/write.ts
294
- import { access, mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
295
- import { dirname, resolve as resolve2 } from "path";
296
- import { z as z3 } from "zod";
297
- var INPUT_SCHEMA3 = z3.object({
298
- file_path: z3.string().describe("Absolute path to the file to write"),
299
- content: z3.string().describe("The content to write to the file")
300
- });
301
- var WriteTool = {
302
- name: "Write",
303
- 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.",
304
- inputSchema: INPUT_SCHEMA3,
305
- isReadOnly: false,
306
- async call(input, context) {
307
- const parsed = INPUT_SCHEMA3.parse(input);
308
- const filePath = resolve2(context.cwd, parsed.file_path);
309
- try {
310
- let existed = false;
311
- try {
312
- await access(filePath);
313
- existed = true;
314
- } catch {
315
- }
316
- await mkdir2(dirname(filePath), { recursive: true });
317
- await writeFile2(filePath, parsed.content, "utf-8");
318
- const action = existed ? "Updated" : "Created";
319
- const lines = parsed.content.split("\n").length;
320
- return { output: `${action} ${filePath} (${lines} lines)` };
321
- } catch (err) {
322
- const msg = err instanceof Error ? err.message : String(err);
323
- return { output: `Error writing file: ${msg}`, isError: true };
324
- }
325
- },
326
- toOllamaToolDef() {
327
- return buildOllamaToolDef(this);
328
- }
329
- };
330
-
331
- // src/tools/edit.ts
332
- import { readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
333
- import { resolve as resolve3 } from "path";
334
- import { createPatch } from "diff";
335
- import { z as z4 } from "zod";
336
- var INPUT_SCHEMA4 = z4.object({
337
- file_path: z4.string().describe("Absolute path to the file to edit"),
338
- old_string: z4.string().describe("The exact string to find and replace"),
339
- new_string: z4.string().describe("The replacement string"),
340
- replace_all: z4.boolean().optional().describe("Replace all occurrences (default false)")
341
- });
342
- function normalizeQuotes(s) {
343
- return s.replace(/[\u2018\u2019\u201A\u201B]/g, "'").replace(/[\u201C\u201D\u201E\u201F]/g, '"');
344
- }
345
- function findActualString(fileContent, searchString) {
346
- if (fileContent.includes(searchString)) {
347
- return searchString;
348
- }
349
- const normalizedSearch = normalizeQuotes(searchString);
350
- const normalizedFile = normalizeQuotes(fileContent);
351
- const idx = normalizedFile.indexOf(normalizedSearch);
352
- if (idx !== -1) {
353
- return fileContent.substring(idx, idx + normalizedSearch.length);
354
- }
355
- return null;
356
- }
357
- var EditTool = {
358
- name: "Edit",
359
- 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.",
360
- inputSchema: INPUT_SCHEMA4,
361
- isReadOnly: false,
362
- async call(input, context) {
363
- const parsed = INPUT_SCHEMA4.parse(input);
364
- const filePath = resolve3(context.cwd, parsed.file_path);
365
- try {
366
- if (!hasFileBeenRead(filePath)) {
367
- return {
368
- output: `Warning: ${filePath} has not been read yet. Use the Read tool first to avoid overwriting unexpected content.`,
369
- isError: true
370
- };
371
- }
372
- const content = await readFile3(filePath, "utf-8");
373
- const actualOld = findActualString(content, parsed.old_string);
374
- if (!actualOld) {
375
- return {
376
- output: `Error: old_string not found in ${filePath}. Make sure you have the exact text including whitespace and indentation.`,
377
- isError: true
378
- };
379
- }
380
- const occurrences = content.split(actualOld).length - 1;
381
- if (occurrences > 1 && !parsed.replace_all) {
382
- return {
383
- output: `Error: old_string appears ${occurrences} times in ${filePath}. Use replace_all: true to replace all, or provide more context to make it unique.`,
384
- isError: true
385
- };
386
- }
387
- const newContent = parsed.replace_all ? content.replaceAll(actualOld, parsed.new_string) : content.replace(actualOld, parsed.new_string);
388
- await writeFile3(filePath, newContent, "utf-8");
389
- const diff = createPatch(filePath, content, newContent, "", "", { context: 3 });
390
- return { output: diff };
391
- } catch (err) {
392
- const msg = err instanceof Error ? err.message : String(err);
393
- return { output: `Error editing file: ${msg}`, isError: true };
394
- }
395
- },
396
- toOllamaToolDef() {
397
- return buildOllamaToolDef(this);
398
- }
399
- };
400
-
401
- // src/tools/grep.ts
402
- import { execFile } from "child_process";
403
- import { resolve as resolve4 } from "path";
404
- import { z as z5 } from "zod";
405
- var INPUT_SCHEMA5 = z5.object({
406
- pattern: z5.string().describe("Regex pattern to search for"),
407
- path: z5.string().optional().describe("Directory or file to search in (default: cwd)"),
408
- glob: z5.string().optional().describe('Glob pattern to filter files (e.g. "*.ts")'),
409
- type: z5.string().optional().describe('File type filter (e.g. "js", "py", "rust") \u2014 more efficient than glob'),
410
- output_mode: z5.enum(["content", "files_with_matches", "count"]).optional().describe("Output mode (default: files_with_matches)"),
411
- case_insensitive: z5.boolean().optional().describe("Case insensitive search"),
412
- multiline: z5.boolean().optional().describe("Enable multiline mode where . matches newlines"),
413
- context: z5.number().optional().describe("Lines of context before and after each match"),
414
- head_limit: z5.number().optional().describe("Max results to return (default 250, 0 for unlimited)")
415
- });
416
- var DEFAULT_HEAD_LIMIT = 250;
417
- var GrepTool = {
418
- name: "Grep",
419
- description: "Search file contents using ripgrep. Supports regex, glob/type filtering, context lines, multiline mode, and multiple output modes.",
420
- inputSchema: INPUT_SCHEMA5,
421
- isReadOnly: true,
422
- async call(input, context) {
423
- const parsed = INPUT_SCHEMA5.parse(input);
424
- const searchPath = parsed.path ? resolve4(context.cwd, parsed.path) : context.cwd;
425
- const mode = parsed.output_mode ?? "files_with_matches";
426
- const headLimit = parsed.head_limit ?? DEFAULT_HEAD_LIMIT;
427
- const args = [
428
- "--hidden",
429
- "--glob",
430
- "!.git",
431
- "--glob",
432
- "!.svn",
433
- "--glob",
434
- "!node_modules",
435
- "--max-columns",
436
- "500"
437
- ];
438
- if (mode === "files_with_matches") args.push("-l");
439
- else if (mode === "count") args.push("-c");
440
- else args.push("-n");
441
- if (parsed.case_insensitive) args.push("-i");
442
- if (parsed.glob) args.push("--glob", parsed.glob);
443
- if (parsed.type) args.push("--type", parsed.type);
444
- if (parsed.multiline) {
445
- args.push("-U", "--multiline-dotall");
446
- }
447
- if (parsed.context && parsed.context > 0 && mode === "content") {
448
- args.push("-C", String(parsed.context));
449
- }
450
- if (parsed.pattern.startsWith("-")) {
451
- args.push("-e", parsed.pattern);
452
- } else {
453
- args.push(parsed.pattern);
454
- }
455
- args.push(searchPath);
456
- return new Promise((resolve7) => {
457
- execFile("rg", args, { maxBuffer: 10 * 1024 * 1024, timeout: 3e4 }, (err, stdout, stderr) => {
458
- if (err && !stdout) {
459
- if (err.code === 1 || err.code === "1") {
460
- resolve7({ output: "No matches found." });
461
- return;
462
- }
463
- resolve7({ output: `Grep error: ${stderr || err.message}`, isError: true });
464
- return;
465
- }
466
- const lines = stdout.trim().split("\n").filter(Boolean);
467
- const limited = headLimit > 0 ? lines.slice(0, headLimit) : lines;
468
- let output = limited.join("\n");
469
- if (headLimit > 0 && lines.length > headLimit) {
470
- output += `
471
- ... (${lines.length - headLimit} more results)`;
472
- }
473
- if (mode === "count") {
474
- const total = lines.reduce((sum, line) => {
475
- const count = parseInt(line.split(":").pop() || "0", 10);
476
- return sum + (isNaN(count) ? 0 : count);
477
- }, 0);
478
- output = `${total} matches across ${lines.length} files
479
- ${output}`;
480
- }
481
- resolve7({ output: output || "No matches found." });
482
- });
483
- });
484
- },
485
- toOllamaToolDef() {
486
- return buildOllamaToolDef(this);
487
- }
488
- };
489
-
490
- // src/tools/glob.ts
491
- import { execFile as execFile2 } from "child_process";
492
- import { resolve as resolve5 } from "path";
493
- import { z as z6 } from "zod";
494
- var INPUT_SCHEMA6 = z6.object({
495
- pattern: z6.string().describe('Glob pattern to match files (e.g. "**/*.ts", "src/**/*.tsx")'),
496
- path: z6.string().optional().describe("Directory to search in (default: cwd)")
497
- });
498
- var MAX_RESULTS = 100;
499
- var GlobTool = {
500
- name: "Glob",
501
- description: "Find files by glob pattern. Returns matching file paths sorted by modification time. Use for locating files by name or extension.",
502
- inputSchema: INPUT_SCHEMA6,
503
- isReadOnly: true,
504
- async call(input, context) {
505
- const parsed = INPUT_SCHEMA6.parse(input);
506
- const searchPath = parsed.path ? resolve5(context.cwd, parsed.path) : context.cwd;
507
- const args = [
508
- "--files",
509
- "--hidden",
510
- "--glob",
511
- parsed.pattern,
512
- "--glob",
513
- "!.git",
514
- "--glob",
515
- "!node_modules",
516
- "--sort=modified",
517
- searchPath
518
- ];
519
- return new Promise((resolve7) => {
520
- execFile2("rg", args, { maxBuffer: 10 * 1024 * 1024, timeout: 3e4 }, (err, stdout, stderr) => {
521
- if (err && !stdout) {
522
- if (err.code === 1 || err.code === "1") {
523
- resolve7({ output: "No files matched." });
524
- return;
525
- }
526
- resolve7({ output: `Glob error: ${stderr || err.message}`, isError: true });
527
- return;
528
- }
529
- const files = stdout.trim().split("\n").filter(Boolean);
530
- const limited = files.slice(0, MAX_RESULTS);
531
- let output = limited.join("\n");
532
- if (files.length > MAX_RESULTS) {
533
- output += `
534
- ... (${files.length - MAX_RESULTS} more files)`;
535
- }
536
- if (!output) {
537
- output = "No files matched.";
538
- }
539
- resolve7({ output });
540
- });
541
- });
542
- },
543
- toOllamaToolDef() {
544
- return buildOllamaToolDef(this);
545
- }
546
- };
547
-
548
- // src/tools/web-fetch.ts
549
- import { z as z7 } from "zod";
550
- var INPUT_SCHEMA7 = z7.object({
551
- url: z7.string().describe("The URL to fetch"),
552
- max_length: z7.number().optional().describe("Maximum response length in characters (default 10000)")
553
- });
554
- var DEFAULT_MAX_LENGTH = 1e4;
555
- var WebFetchTool = {
556
- name: "WebFetch",
557
- description: "Fetch a URL and return its text content. Useful for reading documentation, APIs, or web pages.",
558
- inputSchema: INPUT_SCHEMA7,
559
- isReadOnly: true,
560
- async call(input, _context) {
561
- const parsed = INPUT_SCHEMA7.parse(input);
562
- const maxLen = parsed.max_length ?? DEFAULT_MAX_LENGTH;
563
- try {
564
- const controller = new AbortController();
565
- const timeout = setTimeout(() => controller.abort(), 3e4);
566
- const response = await fetch(parsed.url, {
567
- headers: {
568
- "User-Agent": "Darkfoo-Code/0.1 (Local AI Assistant)",
569
- "Accept": "text/html,text/plain,application/json,*/*"
570
- },
571
- signal: controller.signal
572
- });
573
- clearTimeout(timeout);
574
- if (!response.ok) {
575
- return { output: `HTTP ${response.status}: ${response.statusText}`, isError: true };
576
- }
577
- const contentType = response.headers.get("content-type") || "";
578
- let text = await response.text();
579
- if (contentType.includes("html")) {
580
- text = stripHtml(text);
581
- }
582
- if (text.length > maxLen) {
583
- text = text.slice(0, maxLen) + `
584
- ... (truncated, ${text.length} total chars)`;
585
- }
586
- return { output: text || "(empty response)" };
587
- } catch (err) {
588
- const msg = err instanceof Error ? err.message : String(err);
589
- return { output: `Fetch error: ${msg}`, isError: true };
590
- }
591
- },
592
- toOllamaToolDef() {
593
- return buildOllamaToolDef(this);
594
- }
595
- };
596
- function stripHtml(html) {
597
- 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();
598
- }
599
-
600
- // src/tools/web-search.ts
601
- import { z as z8 } from "zod";
602
- var INPUT_SCHEMA8 = z8.object({
603
- query: z8.string().describe("The search query"),
604
- max_results: z8.number().optional().describe("Maximum number of results (default 5)")
605
- });
606
- var WebSearchTool = {
607
- name: "WebSearch",
608
- description: "Search the web using DuckDuckGo. Returns titles, URLs, and snippets for the top results.",
609
- inputSchema: INPUT_SCHEMA8,
610
- isReadOnly: true,
611
- async call(input, _context) {
612
- const parsed = INPUT_SCHEMA8.parse(input);
613
- const maxResults = parsed.max_results ?? 5;
614
- try {
615
- const encoded = encodeURIComponent(parsed.query);
616
- const url = `https://html.duckduckgo.com/html/?q=${encoded}`;
617
- const response = await fetch(url, {
618
- headers: {
619
- "User-Agent": "Darkfoo-Code/0.1 (Local AI Assistant)"
620
- },
621
- signal: AbortSignal.timeout(15e3)
622
- });
623
- if (!response.ok) {
624
- return { output: `Search failed: HTTP ${response.status}`, isError: true };
625
- }
626
- const html = await response.text();
627
- const results = parseSearchResults(html, maxResults);
628
- if (results.length === 0) {
629
- return { output: `No results found for: ${parsed.query}` };
630
- }
631
- const output = results.map((r, i) => `${i + 1}. ${r.title}
632
- ${r.url}
633
- ${r.snippet}`).join("\n\n");
634
- return { output };
635
- } catch (err) {
636
- const msg = err instanceof Error ? err.message : String(err);
637
- return { output: `Search error: ${msg}`, isError: true };
638
- }
639
- },
640
- toOllamaToolDef() {
641
- return buildOllamaToolDef(this);
642
- }
643
- };
644
- function parseSearchResults(html, max) {
645
- const results = [];
646
- const resultBlocks = html.split('class="result__a"');
647
- for (let i = 1; i < resultBlocks.length && results.length < max; i++) {
648
- const block = resultBlocks[i];
649
- const hrefMatch = block.match(/href="([^"]+)"/);
650
- let url = hrefMatch?.[1] || "";
651
- const uddgMatch = url.match(/uddg=([^&]+)/);
652
- if (uddgMatch) {
653
- url = decodeURIComponent(uddgMatch[1]);
654
- }
655
- const titleMatch = block.match(/>([^<]+)<\/a>/);
656
- const title = titleMatch?.[1]?.trim() || "Untitled";
657
- const snippetSection = resultBlocks[i + 1] || block;
658
- const snippetMatch = snippetSection.match(/class="result__snippet"[^>]*>([^<]+)/);
659
- const snippet = snippetMatch?.[1]?.trim() || "";
660
- if (url && !url.startsWith("/")) {
661
- results.push({ title: stripTags(title), url, snippet: stripTags(snippet) });
662
- }
663
- }
664
- return results;
665
- }
666
- function stripTags(text) {
667
- return text.replace(/<[^>]+>/g, "").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').trim();
668
- }
669
-
670
- // src/tools/agent.ts
671
- import { z as z9 } from "zod";
672
- var INPUT_SCHEMA9 = z9.object({
673
- prompt: z9.string().describe("The task for the sub-agent to perform"),
674
- description: z9.string().optional().describe("Short description of what the agent will do")
675
- });
676
- var MAX_TURNS = 15;
677
- var AgentTool = {
678
- name: "Agent",
679
- 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.",
680
- inputSchema: INPUT_SCHEMA9,
681
- isReadOnly: false,
682
- async call(input, context) {
683
- const parsed = INPUT_SCHEMA9.parse(input);
684
- const tools = getTools().filter((t) => t.name !== "Agent");
685
- const ollamaTools = tools.map((t) => t.toOllamaToolDef());
686
- const model = process.env.DARKFOO_MODEL || "llama3.1:8b";
687
- const systemPrompt = await buildSystemPrompt(tools, context.cwd);
688
- const messages = [
689
- { role: "system", content: systemPrompt },
690
- { role: "user", content: parsed.prompt }
691
- ];
692
- let turns = 0;
693
- let finalContent = "";
694
- while (turns < MAX_TURNS) {
695
- turns++;
696
- let assistantContent = "";
697
- const toolCalls = [];
698
- const provider = getProvider();
699
- for await (const event of provider.chatStream({
700
- model,
701
- messages,
702
- tools: ollamaTools,
703
- signal: context.abortSignal
704
- })) {
705
- if (event.type === "text_delta") assistantContent += event.text;
706
- if (event.type === "tool_call") toolCalls.push(event.toolCall);
707
- if (event.type === "error") return { output: `Agent error: ${event.error}`, isError: true };
708
- }
709
- messages.push({
710
- role: "assistant",
711
- content: assistantContent,
712
- ...toolCalls.length > 0 ? { tool_calls: toolCalls } : {}
713
- });
714
- if (toolCalls.length === 0) {
715
- finalContent = assistantContent;
716
- break;
717
- }
718
- for (const tc of toolCalls) {
719
- const tool = tools.find((t) => t.name.toLowerCase() === tc.function.name.toLowerCase());
720
- if (!tool) {
721
- messages.push({ role: "tool", content: `Unknown tool: ${tc.function.name}` });
722
- continue;
723
- }
724
- let result;
725
- try {
726
- result = await tool.call(tc.function.arguments, context);
727
- } catch (err) {
728
- const msg = err instanceof Error ? err.message : String(err);
729
- result = { output: `Error: ${msg}`, isError: true };
730
- }
731
- messages.push({ role: "tool", content: result.output });
732
- }
733
- }
734
- if (!finalContent && turns >= MAX_TURNS) {
735
- finalContent = "(Agent reached max turns without final response)";
736
- }
737
- return { output: finalContent };
738
- },
739
- toOllamaToolDef() {
740
- return buildOllamaToolDef(this);
741
- }
742
- };
743
-
744
- // src/tools/notebook-edit.ts
745
- import { readFile as readFile4, writeFile as writeFile4 } from "fs/promises";
746
- import { resolve as resolve6 } from "path";
747
- import { z as z10 } from "zod";
748
- var INPUT_SCHEMA10 = z10.object({
749
- file_path: z10.string().describe("Path to the .ipynb notebook file"),
750
- cell_index: z10.number().describe("Index of the cell to edit (0-based)"),
751
- new_source: z10.string().describe("New source content for the cell"),
752
- cell_type: z10.enum(["code", "markdown"]).optional().describe("Change cell type")
753
- });
754
- var NotebookEditTool = {
755
- name: "NotebookEdit",
756
- description: "Edit a Jupyter notebook (.ipynb) cell by index. Reads the notebook, modifies the specified cell, and writes it back.",
757
- inputSchema: INPUT_SCHEMA10,
758
- isReadOnly: false,
759
- async call(input, context) {
760
- const parsed = INPUT_SCHEMA10.parse(input);
761
- const filePath = resolve6(context.cwd, parsed.file_path);
762
- try {
763
- const raw = await readFile4(filePath, "utf-8");
764
- const notebook = JSON.parse(raw);
765
- if (parsed.cell_index < 0 || parsed.cell_index >= notebook.cells.length) {
766
- return {
767
- output: `Cell index ${parsed.cell_index} out of range (notebook has ${notebook.cells.length} cells).`,
768
- isError: true
769
- };
770
- }
771
- const cell = notebook.cells[parsed.cell_index];
772
- const oldSource = cell.source.join("");
773
- cell.source = parsed.new_source.split("\n").map(
774
- (line, i, arr) => i < arr.length - 1 ? line + "\n" : line
775
- );
776
- if (parsed.cell_type) {
777
- cell.cell_type = parsed.cell_type;
778
- }
779
- if (cell.cell_type === "code") {
780
- cell.outputs = [];
781
- cell.execution_count = null;
782
- }
783
- await writeFile4(filePath, JSON.stringify(notebook, null, 1) + "\n", "utf-8");
784
- return {
785
- output: `Updated cell ${parsed.cell_index} in ${filePath} (${cell.cell_type}). Old length: ${oldSource.length}, new length: ${parsed.new_source.length}.`
786
- };
787
- } catch (err) {
788
- const msg = err instanceof Error ? err.message : String(err);
789
- return { output: `NotebookEdit error: ${msg}`, isError: true };
790
- }
791
- },
792
- toOllamaToolDef() {
793
- return buildOllamaToolDef(this);
794
- }
795
- };
796
-
797
- // src/tools/plan.ts
798
- import { z as z11 } from "zod";
799
-
800
- // src/state.ts
801
- var state = {
802
- planMode: false,
803
- tasks: [],
804
- pendingQuestion: null
805
- };
806
- function getAppState() {
807
- return state;
808
- }
809
- function updateAppState(updater) {
810
- state = updater(state);
811
- }
812
-
813
- // src/tools/plan.ts
814
- var ENTER_SCHEMA = z11.object({});
815
- var EnterPlanModeTool = {
816
- name: "EnterPlanMode",
817
- 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.",
818
- inputSchema: ENTER_SCHEMA,
819
- isReadOnly: true,
820
- async call(_input, _context) {
821
- updateAppState((s) => ({ ...s, planMode: true }));
822
- return {
823
- 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."
824
- };
825
- },
826
- toOllamaToolDef() {
827
- return buildOllamaToolDef(this);
828
- }
829
- };
830
- var EXIT_SCHEMA = z11.object({
831
- plan: z11.string().describe("The implementation plan to present to the user for approval")
832
- });
833
- var ExitPlanModeTool = {
834
- name: "ExitPlanMode",
835
- description: "Exit plan mode and present your implementation plan to the user for approval. Include the plan summary as the argument.",
836
- inputSchema: EXIT_SCHEMA,
837
- isReadOnly: true,
838
- async call(input, _context) {
839
- const parsed = EXIT_SCHEMA.parse(input);
840
- updateAppState((s) => ({ ...s, planMode: false }));
841
- return {
842
- output: `Plan submitted for user approval:
843
-
844
- ${parsed.plan}
845
-
846
- Plan mode exited. Waiting for user to approve or provide feedback.`
847
- };
848
- },
849
- toOllamaToolDef() {
850
- return buildOllamaToolDef(this);
851
- }
852
- };
853
-
854
- // src/tools/tasks.ts
855
- import { nanoid as nanoid2 } from "nanoid";
856
- import { z as z12 } from "zod";
857
- var CREATE_SCHEMA = z12.object({
858
- subject: z12.string().describe("Brief title for the task"),
859
- description: z12.string().describe("What needs to be done")
860
- });
861
- var TaskCreateTool = {
862
- name: "TaskCreate",
863
- description: "Create a new task to track work progress. Use for multi-step tasks.",
864
- inputSchema: CREATE_SCHEMA,
865
- isReadOnly: false,
866
- async call(input, _context) {
867
- const parsed = CREATE_SCHEMA.parse(input);
868
- const task = {
869
- id: nanoid2(8),
870
- subject: parsed.subject,
871
- description: parsed.description,
872
- status: "pending",
873
- createdAt: Date.now()
874
- };
875
- updateAppState((s) => ({ ...s, tasks: [...s.tasks, task] }));
876
- return { output: `Task #${task.id} created: ${task.subject}` };
877
- },
878
- toOllamaToolDef() {
879
- return buildOllamaToolDef(this);
880
- }
881
- };
882
- var UPDATE_SCHEMA = z12.object({
883
- taskId: z12.string().describe("The task ID to update"),
884
- status: z12.enum(["pending", "in_progress", "completed"]).optional().describe("New status"),
885
- subject: z12.string().optional().describe("Updated subject"),
886
- description: z12.string().optional().describe("Updated description")
887
- });
888
- var TaskUpdateTool = {
889
- name: "TaskUpdate",
890
- description: "Update a task status or details. Mark as in_progress when starting, completed when done.",
891
- inputSchema: UPDATE_SCHEMA,
892
- isReadOnly: false,
893
- async call(input, _context) {
894
- const parsed = UPDATE_SCHEMA.parse(input);
895
- const state2 = getAppState();
896
- const task = state2.tasks.find((t) => t.id === parsed.taskId);
897
- if (!task) return { output: `Task ${parsed.taskId} not found.`, isError: true };
898
- updateAppState((s) => ({
899
- ...s,
900
- tasks: s.tasks.map((t) => {
901
- if (t.id !== parsed.taskId) return t;
902
- return {
903
- ...t,
904
- ...parsed.status && { status: parsed.status },
905
- ...parsed.subject && { subject: parsed.subject },
906
- ...parsed.description && { description: parsed.description }
907
- };
908
- })
909
- }));
910
- return { output: `Task #${parsed.taskId} updated${parsed.status ? ` \u2192 ${parsed.status}` : ""}.` };
911
- },
912
- toOllamaToolDef() {
913
- return buildOllamaToolDef(this);
914
- }
915
- };
916
- var LIST_SCHEMA = z12.object({});
917
- var TaskListTool = {
918
- name: "TaskList",
919
- description: "List all tasks with their status.",
920
- inputSchema: LIST_SCHEMA,
921
- isReadOnly: true,
922
- async call(_input, _context) {
923
- const { tasks } = getAppState();
924
- if (tasks.length === 0) return { output: "No tasks." };
925
- const statusIcon = { pending: "\u25CB", in_progress: "\u25D1", completed: "\u25CF" };
926
- const lines = tasks.map(
927
- (t) => `${statusIcon[t.status]} #${t.id} [${t.status}] ${t.subject}`
928
- );
929
- return { output: lines.join("\n") };
930
- },
931
- toOllamaToolDef() {
932
- return buildOllamaToolDef(this);
933
- }
934
- };
935
- var GET_SCHEMA = z12.object({
936
- taskId: z12.string().describe("The task ID to retrieve")
937
- });
938
- var TaskGetTool = {
939
- name: "TaskGet",
940
- description: "Get details of a specific task by ID.",
941
- inputSchema: GET_SCHEMA,
942
- isReadOnly: true,
943
- async call(input, _context) {
944
- const parsed = GET_SCHEMA.parse(input);
945
- const { tasks } = getAppState();
946
- const task = tasks.find((t) => t.id === parsed.taskId);
947
- if (!task) return { output: `Task ${parsed.taskId} not found.`, isError: true };
948
- return {
949
- output: [
950
- `Task #${task.id}`,
951
- `Subject: ${task.subject}`,
952
- `Status: ${task.status}`,
953
- `Description: ${task.description}`
954
- ].join("\n")
955
- };
956
- },
957
- toOllamaToolDef() {
958
- return buildOllamaToolDef(this);
959
- }
960
- };
961
-
962
- // src/tools/ask.ts
963
- import { z as z13 } from "zod";
964
- var INPUT_SCHEMA11 = z13.object({
965
- question: z13.string().describe("The question to ask the user")
966
- });
967
- var AskUserQuestionTool = {
968
- name: "AskUserQuestion",
969
- description: "Ask the user a question when you need clarification or input before proceeding. The query loop will pause until the user responds.",
970
- inputSchema: INPUT_SCHEMA11,
971
- isReadOnly: true,
972
- async call(input, _context) {
973
- const parsed = INPUT_SCHEMA11.parse(input);
974
- updateAppState((s) => ({ ...s, pendingQuestion: parsed.question }));
975
- return {
976
- output: `Question posed to user: ${parsed.question}
977
- Waiting for user response...`
978
- };
979
- },
980
- toOllamaToolDef() {
981
- return buildOllamaToolDef(this);
982
- }
983
- };
984
-
985
- // src/tools/index.ts
986
- function getTools() {
987
- return [
988
- BashTool,
989
- ReadTool,
990
- WriteTool,
991
- EditTool,
992
- GrepTool,
993
- GlobTool,
994
- WebFetchTool,
995
- WebSearchTool,
996
- AgentTool,
997
- NotebookEditTool,
998
- EnterPlanModeTool,
999
- ExitPlanModeTool,
1000
- TaskCreateTool,
1001
- TaskUpdateTool,
1002
- TaskListTool,
1003
- TaskGetTool,
1004
- AskUserQuestionTool
1005
- ];
1006
- }
1007
- function getToolByName(name) {
1008
- return getTools().find((t) => t.name.toLowerCase() === name.toLowerCase());
1009
- }
1010
-
1011
- export {
1012
- truncate,
1013
- getAppState,
1014
- trackedFileCount,
1015
- BashTool,
1016
- ReadTool,
1017
- WriteTool,
1018
- EditTool,
1019
- GrepTool,
1020
- GlobTool,
1021
- getTools,
1022
- getToolByName
1023
- };