darkfoo-code 0.1.2 → 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/bin/darkfoo.mjs +3 -1
- package/dist/main.js +1991 -86
- package/package.json +1 -1
- package/dist/chunk-4KTJEE4A.js +0 -1023
- package/dist/chunk-BT7IPQDS.js +0 -19
- package/dist/chunk-GQXUHUV4.js +0 -171
- package/dist/chunk-OBL22IIN.js +0 -400
- package/dist/chunk-VSJKCANO.js +0 -117
- package/dist/providers-P7Z3JXQH.js +0 -24
- package/dist/query-E6NPBSUX.js +0 -7
- package/dist/system-prompt-YJSDZVOM.js +0 -6
- package/dist/theme-BQAEFGVB.js +0 -6
- package/dist/tools-34S775OZ.js +0 -22
package/dist/main.js
CHANGED
|
@@ -1,29 +1,1914 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").replace(/ /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(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/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
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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((
|
|
2433
|
+
return new Promise((resolve8) => {
|
|
540
2434
|
execFile("git", ["diff", "--stat"], { cwd: context.cwd, timeout: 1e4 }, (err, stdout, stderr) => {
|
|
541
2435
|
if (err) {
|
|
542
|
-
|
|
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
|
-
|
|
2442
|
+
resolve8({ output: "No uncommitted changes.", silent: true });
|
|
549
2443
|
return;
|
|
550
2444
|
}
|
|
551
|
-
|
|
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
|
-
|
|
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 =
|
|
2470
|
+
var SESSIONS_DIR = join2(process.env.HOME || "~", ".darkfoo", "sessions");
|
|
577
2471
|
async function ensureDir() {
|
|
578
|
-
await
|
|
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
|
|
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
|
|
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
|
|
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((
|
|
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
|
-
|
|
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((
|
|
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
|
-
|
|
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
|
|
838
|
-
import { join as
|
|
839
|
-
var
|
|
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
|
|
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${
|
|
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: ${
|
|
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
|
|
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
|
|
887
|
-
await
|
|
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
|
|
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
|
|
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
|
|
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
|
|
993
|
-
import { join as
|
|
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 =
|
|
1004
|
-
await
|
|
1005
|
-
const filePath =
|
|
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
|
|
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
|
|
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 =
|
|
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: ${
|
|
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
|
|
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
|
-
|
|
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 (
|
|
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 (
|
|
3010
|
+
if (readFiles2.size > 0) {
|
|
1114
3011
|
lines.push(" \x1B[1mRead:\x1B[0m");
|
|
1115
|
-
for (const f of
|
|
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
|
|
1230
|
-
if (
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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.
|
|
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
|
|
1691
|
-
const { getTools: getTools2 } = await
|
|
1692
|
-
const { query: query2 } = await
|
|
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:
|
|
3600
|
+
const { nanoid: nanoid7 } = await import("nanoid");
|
|
1696
3601
|
const userMsg = {
|
|
1697
|
-
id:
|
|
3602
|
+
id: nanoid7(),
|
|
1698
3603
|
role: "user",
|
|
1699
3604
|
content: prompt,
|
|
1700
3605
|
timestamp: Date.now()
|