darkfoo-code 0.1.3 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/main.js +2354 -227
- 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: "[ ]", in_progress: "[~]", completed: "[x]" };
|
|
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";
|
|
@@ -41,14 +1926,219 @@ function App({ model, systemPromptOverride, children }) {
|
|
|
41
1926
|
}
|
|
42
1927
|
|
|
43
1928
|
// src/repl.tsx
|
|
44
|
-
import { useState as
|
|
45
|
-
import { Box as
|
|
1929
|
+
import { useState as useState3, useCallback as useCallback2, useEffect as useEffect2, useRef } from "react";
|
|
1930
|
+
import { Box as Box7, Text as Text7, 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();
|
|
1936
|
+
import { Box as Box2, Text as Text2 } from "ink";
|
|
1937
|
+
|
|
1938
|
+
// src/components/Fox.tsx
|
|
1939
|
+
init_theme();
|
|
1940
|
+
import { useState, useEffect } from "react";
|
|
50
1941
|
import { Box, Text } from "ink";
|
|
51
1942
|
import { jsx as jsx2, jsxs } from "react/jsx-runtime";
|
|
1943
|
+
var IDLE_FRAMES = [
|
|
1944
|
+
[
|
|
1945
|
+
" /\\_/\\ ",
|
|
1946
|
+
" ( o.o ) ",
|
|
1947
|
+
" > ^ < ",
|
|
1948
|
+
" /| |\\ ",
|
|
1949
|
+
"(_| |_)",
|
|
1950
|
+
" ~ "
|
|
1951
|
+
],
|
|
1952
|
+
[
|
|
1953
|
+
" /\\_/\\ ",
|
|
1954
|
+
" ( o.o ) ",
|
|
1955
|
+
" > ^ < ",
|
|
1956
|
+
" /| |\\ ",
|
|
1957
|
+
"(_| |_)",
|
|
1958
|
+
" ~ "
|
|
1959
|
+
],
|
|
1960
|
+
[
|
|
1961
|
+
" /\\_/\\ ",
|
|
1962
|
+
" ( o.o ) ",
|
|
1963
|
+
" > ^ < ",
|
|
1964
|
+
" /| |\\ ",
|
|
1965
|
+
"(_| |_)",
|
|
1966
|
+
" ~ "
|
|
1967
|
+
],
|
|
1968
|
+
[
|
|
1969
|
+
" /\\_/\\ ",
|
|
1970
|
+
" ( o.o ) ",
|
|
1971
|
+
" > ^ < ",
|
|
1972
|
+
" /| |\\ ",
|
|
1973
|
+
"(_| |_)",
|
|
1974
|
+
" ~ "
|
|
1975
|
+
]
|
|
1976
|
+
];
|
|
1977
|
+
var THINKING_FRAMES = [
|
|
1978
|
+
[
|
|
1979
|
+
" /\\_/\\ ",
|
|
1980
|
+
" ( o.o ) ",
|
|
1981
|
+
" > ^ < ",
|
|
1982
|
+
" | | ",
|
|
1983
|
+
" |___| ",
|
|
1984
|
+
" . . "
|
|
1985
|
+
],
|
|
1986
|
+
[
|
|
1987
|
+
" /\\_/\\ ",
|
|
1988
|
+
" ( o.- ) ",
|
|
1989
|
+
" > ^ < ",
|
|
1990
|
+
" | | ",
|
|
1991
|
+
" |___| ",
|
|
1992
|
+
" . . . "
|
|
1993
|
+
],
|
|
1994
|
+
[
|
|
1995
|
+
" /\\_/\\ ",
|
|
1996
|
+
" ( -.o ) ",
|
|
1997
|
+
" > ^ < ",
|
|
1998
|
+
" | | ",
|
|
1999
|
+
" |___| ",
|
|
2000
|
+
" . . . . "
|
|
2001
|
+
],
|
|
2002
|
+
[
|
|
2003
|
+
" /\\_/\\ ",
|
|
2004
|
+
" ( o.o ) ",
|
|
2005
|
+
" > ^ < ",
|
|
2006
|
+
" | | ",
|
|
2007
|
+
" |___| ",
|
|
2008
|
+
" . . . "
|
|
2009
|
+
]
|
|
2010
|
+
];
|
|
2011
|
+
var WORKING_FRAMES = [
|
|
2012
|
+
[
|
|
2013
|
+
" /\\_/\\ ",
|
|
2014
|
+
" ( >.< ) ",
|
|
2015
|
+
" > ^ < ",
|
|
2016
|
+
" /| |\\ ",
|
|
2017
|
+
"(_| |_)",
|
|
2018
|
+
" ~/ "
|
|
2019
|
+
],
|
|
2020
|
+
[
|
|
2021
|
+
" /\\_/\\ ",
|
|
2022
|
+
" ( >.< ) ",
|
|
2023
|
+
" > ^ < ",
|
|
2024
|
+
" /| |\\ ",
|
|
2025
|
+
"(_| |_)",
|
|
2026
|
+
" /~ "
|
|
2027
|
+
]
|
|
2028
|
+
];
|
|
2029
|
+
var SUCCESS_FRAMES = [
|
|
2030
|
+
[
|
|
2031
|
+
" /\\_/\\ ",
|
|
2032
|
+
" ( ^.^ ) ",
|
|
2033
|
+
" > w < ",
|
|
2034
|
+
" /| |\\ ",
|
|
2035
|
+
"(_| |_)",
|
|
2036
|
+
" \\~/ "
|
|
2037
|
+
],
|
|
2038
|
+
[
|
|
2039
|
+
" /\\_/\\ ",
|
|
2040
|
+
" ( ^.^ ) ",
|
|
2041
|
+
" > w < ",
|
|
2042
|
+
" /| |\\ ",
|
|
2043
|
+
" (_|_|_) ",
|
|
2044
|
+
" \\~/ "
|
|
2045
|
+
]
|
|
2046
|
+
];
|
|
2047
|
+
var ERROR_FRAMES = [
|
|
2048
|
+
[
|
|
2049
|
+
" /\\_/\\ ",
|
|
2050
|
+
" ( ;.; ) ",
|
|
2051
|
+
" > n < ",
|
|
2052
|
+
" | | ",
|
|
2053
|
+
" |___| ",
|
|
2054
|
+
" . "
|
|
2055
|
+
]
|
|
2056
|
+
];
|
|
2057
|
+
var GREETING_FRAMES = [
|
|
2058
|
+
[
|
|
2059
|
+
" /\\_/\\ ",
|
|
2060
|
+
" ( ^.^ )/",
|
|
2061
|
+
" > w < ",
|
|
2062
|
+
" /| | ",
|
|
2063
|
+
"(_| |) ",
|
|
2064
|
+
" \\~/ "
|
|
2065
|
+
],
|
|
2066
|
+
[
|
|
2067
|
+
" /\\_/\\ ",
|
|
2068
|
+
" ( ^.^ ) ",
|
|
2069
|
+
" > w <\\ ",
|
|
2070
|
+
" /| | ",
|
|
2071
|
+
"(_| |) ",
|
|
2072
|
+
" \\~/ "
|
|
2073
|
+
]
|
|
2074
|
+
];
|
|
2075
|
+
var FRAME_SETS = {
|
|
2076
|
+
idle: IDLE_FRAMES,
|
|
2077
|
+
thinking: THINKING_FRAMES,
|
|
2078
|
+
working: WORKING_FRAMES,
|
|
2079
|
+
success: SUCCESS_FRAMES,
|
|
2080
|
+
error: ERROR_FRAMES,
|
|
2081
|
+
greeting: GREETING_FRAMES
|
|
2082
|
+
};
|
|
2083
|
+
var FRAME_SPEEDS = {
|
|
2084
|
+
idle: 600,
|
|
2085
|
+
thinking: 400,
|
|
2086
|
+
working: 250,
|
|
2087
|
+
success: 500,
|
|
2088
|
+
error: 1e3,
|
|
2089
|
+
greeting: 400
|
|
2090
|
+
};
|
|
2091
|
+
var MOOD_COLORS = {
|
|
2092
|
+
idle: theme.cyan ?? "#5eead4",
|
|
2093
|
+
thinking: theme.purple ?? "#a78bfa",
|
|
2094
|
+
working: theme.yellow ?? "#fbbf24",
|
|
2095
|
+
success: theme.green ?? "#4ade80",
|
|
2096
|
+
error: theme.pink ?? "#f472b6",
|
|
2097
|
+
greeting: theme.cyan ?? "#5eead4"
|
|
2098
|
+
};
|
|
2099
|
+
var ACCENT_COLORS = {
|
|
2100
|
+
idle: theme.text ?? "#e2e8f0",
|
|
2101
|
+
thinking: theme.cyan ?? "#5eead4",
|
|
2102
|
+
working: theme.cyan ?? "#5eead4",
|
|
2103
|
+
success: theme.green ?? "#4ade80",
|
|
2104
|
+
error: theme.pink ?? "#f472b6",
|
|
2105
|
+
greeting: theme.green ?? "#4ade80"
|
|
2106
|
+
};
|
|
2107
|
+
function Fox({ mood = "idle" }) {
|
|
2108
|
+
const [frameIdx, setFrameIdx] = useState(0);
|
|
2109
|
+
const frames = FRAME_SETS[mood];
|
|
2110
|
+
const speed = FRAME_SPEEDS[mood];
|
|
2111
|
+
useEffect(() => {
|
|
2112
|
+
setFrameIdx(0);
|
|
2113
|
+
const interval = setInterval(() => {
|
|
2114
|
+
setFrameIdx((prev) => (prev + 1) % frames.length);
|
|
2115
|
+
}, speed);
|
|
2116
|
+
return () => clearInterval(interval);
|
|
2117
|
+
}, [mood, frames.length, speed]);
|
|
2118
|
+
const frame = frames[frameIdx] ?? frames[0];
|
|
2119
|
+
const bodyColor = MOOD_COLORS[mood];
|
|
2120
|
+
const accentColor = ACCENT_COLORS[mood];
|
|
2121
|
+
return /* @__PURE__ */ jsx2(Box, { flexDirection: "column", children: frame.map((line, i) => /* @__PURE__ */ jsx2(Text, { color: i <= 1 ? accentColor : bodyColor, children: line }, i)) });
|
|
2122
|
+
}
|
|
2123
|
+
function FoxBubble({ text }) {
|
|
2124
|
+
if (!text) return null;
|
|
2125
|
+
const maxWidth = 30;
|
|
2126
|
+
const display = text.length > maxWidth ? text.slice(0, maxWidth - 3) + "..." : text;
|
|
2127
|
+
const padding = display.length;
|
|
2128
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginLeft: 1, children: [
|
|
2129
|
+
/* @__PURE__ */ jsx2(Text, { color: theme.dim ?? "#7e8ea6", children: "." + "-".repeat(padding + 2) + "." }),
|
|
2130
|
+
/* @__PURE__ */ jsxs(Text, { color: theme.dim ?? "#7e8ea6", children: [
|
|
2131
|
+
"| ",
|
|
2132
|
+
/* @__PURE__ */ jsx2(Text, { color: theme.text ?? "#e2e8f0", children: display }),
|
|
2133
|
+
" |"
|
|
2134
|
+
] }),
|
|
2135
|
+
/* @__PURE__ */ jsx2(Text, { color: theme.dim ?? "#7e8ea6", children: "'" + "-".repeat(padding + 2) + "'" }),
|
|
2136
|
+
/* @__PURE__ */ jsx2(Text, { color: theme.dim ?? "#7e8ea6", children: "/" })
|
|
2137
|
+
] });
|
|
2138
|
+
}
|
|
2139
|
+
|
|
2140
|
+
// src/components/Banner.tsx
|
|
2141
|
+
import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
52
2142
|
var LOGO_LINES = [
|
|
53
2143
|
" \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 ",
|
|
54
2144
|
" \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557 \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557 \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557 \u2588\u2588\u2551 \u2588\u2588\u2554\u255D \u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D \u2588\u2588\u2554\u2550\u2550\u2550\u2588\u2588\u2557 \u2588\u2588\u2554\u2550\u2550\u2550\u2588\u2588\u2557",
|
|
@@ -57,67 +2147,71 @@ var LOGO_LINES = [
|
|
|
57
2147
|
" \u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2557 \u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D \u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D",
|
|
58
2148
|
" \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D "
|
|
59
2149
|
];
|
|
60
|
-
var SUBTITLE = "
|
|
2150
|
+
var SUBTITLE = " -- C O D E --";
|
|
61
2151
|
var GRADIENT = [
|
|
62
2152
|
"#5eead4",
|
|
63
|
-
// cyan
|
|
64
2153
|
"#6ee0c8",
|
|
65
|
-
// cyan-light
|
|
66
2154
|
"#82c8d0",
|
|
67
|
-
// cyan-blue
|
|
68
2155
|
"#96b0d8",
|
|
69
|
-
// blue-ish
|
|
70
2156
|
"#a78bfa",
|
|
71
|
-
// purple
|
|
72
2157
|
"#c47ee8",
|
|
73
|
-
// purple-pink
|
|
74
2158
|
"#f472b6"
|
|
75
|
-
// pink
|
|
76
2159
|
];
|
|
77
2160
|
function lineColor(index) {
|
|
78
2161
|
const t = index / (LOGO_LINES.length - 1);
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
2162
|
+
return GRADIENT[Math.round(t * (GRADIENT.length - 1))];
|
|
2163
|
+
}
|
|
2164
|
+
function gradientBar(width) {
|
|
2165
|
+
return Array.from({ length: width }, (_, i) => ({
|
|
2166
|
+
char: "\u2501",
|
|
2167
|
+
color: GRADIENT[Math.round(i / (width - 1) * (GRADIENT.length - 1))]
|
|
2168
|
+
}));
|
|
2169
|
+
}
|
|
2170
|
+
function Banner({ model, cwd, providerName, providerOnline }) {
|
|
2171
|
+
const bar = gradientBar(60);
|
|
2172
|
+
const statusTag = providerOnline ? "[connected]" : "[offline]";
|
|
2173
|
+
const statusColor = providerOnline ? theme.green ?? "#4ade80" : theme.pink ?? "#f472b6";
|
|
2174
|
+
return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", marginBottom: 1, children: [
|
|
2175
|
+
/* @__PURE__ */ jsxs2(Box2, { children: [
|
|
2176
|
+
/* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
|
|
2177
|
+
LOGO_LINES.map((line, i) => /* @__PURE__ */ jsx3(Text2, { color: lineColor(i), bold: true, children: line }, i)),
|
|
2178
|
+
/* @__PURE__ */ jsx3(Text2, { color: theme.purple ?? "#a78bfa", bold: true, children: SUBTITLE })
|
|
2179
|
+
] }),
|
|
2180
|
+
/* @__PURE__ */ jsxs2(Box2, { marginLeft: 2, flexDirection: "row", alignItems: "flex-end", children: [
|
|
2181
|
+
/* @__PURE__ */ jsx3(Fox, { mood: "greeting" }),
|
|
2182
|
+
/* @__PURE__ */ jsx3(FoxBubble, { text: "Ready to work." })
|
|
2183
|
+
] })
|
|
2184
|
+
] }),
|
|
2185
|
+
/* @__PURE__ */ jsx3(Text2, { children: " " }),
|
|
2186
|
+
/* @__PURE__ */ jsxs2(Text2, { children: [
|
|
98
2187
|
" ",
|
|
99
|
-
|
|
2188
|
+
bar.map((d, i) => /* @__PURE__ */ jsx3(Text2, { color: d.color, children: d.char }, i))
|
|
100
2189
|
] }),
|
|
101
|
-
/* @__PURE__ */
|
|
102
|
-
/* @__PURE__ */
|
|
103
|
-
/* @__PURE__ */
|
|
104
|
-
/* @__PURE__ */ jsx2(Text, { color: theme.dim, children: " \u2502 " }),
|
|
105
|
-
/* @__PURE__ */ jsx2(Text, { color: theme.text, children: cwd })
|
|
2190
|
+
/* @__PURE__ */ jsxs2(Box2, { marginTop: 1, marginLeft: 2, children: [
|
|
2191
|
+
/* @__PURE__ */ jsx3(Text2, { color: theme.dim ?? "#7e8ea6", children: "model " }),
|
|
2192
|
+
/* @__PURE__ */ jsx3(Text2, { color: theme.cyan ?? "#5eead4", bold: true, children: model })
|
|
106
2193
|
] }),
|
|
107
|
-
/* @__PURE__ */
|
|
108
|
-
/* @__PURE__ */
|
|
109
|
-
/* @__PURE__ */
|
|
110
|
-
/* @__PURE__ */
|
|
2194
|
+
/* @__PURE__ */ jsxs2(Box2, { marginLeft: 2, children: [
|
|
2195
|
+
/* @__PURE__ */ jsx3(Text2, { color: theme.dim ?? "#7e8ea6", children: "via " }),
|
|
2196
|
+
/* @__PURE__ */ jsx3(Text2, { color: theme.text ?? "#e2e8f0", children: providerName }),
|
|
2197
|
+
/* @__PURE__ */ jsx3(Text2, { color: theme.dim ?? "#7e8ea6", children: " " }),
|
|
2198
|
+
/* @__PURE__ */ jsx3(Text2, { color: statusColor, children: statusTag })
|
|
111
2199
|
] }),
|
|
112
|
-
/* @__PURE__ */
|
|
2200
|
+
/* @__PURE__ */ jsxs2(Box2, { marginLeft: 2, children: [
|
|
2201
|
+
/* @__PURE__ */ jsx3(Text2, { color: theme.dim ?? "#7e8ea6", children: "cwd " }),
|
|
2202
|
+
/* @__PURE__ */ jsx3(Text2, { color: theme.text ?? "#e2e8f0", children: cwd })
|
|
2203
|
+
] }),
|
|
2204
|
+
/* @__PURE__ */ jsx3(Box2, { marginTop: 1, children: /* @__PURE__ */ jsxs2(Text2, { children: [
|
|
113
2205
|
" ",
|
|
114
|
-
|
|
115
|
-
] })
|
|
2206
|
+
bar.map((d, i) => /* @__PURE__ */ jsx3(Text2, { color: d.color, children: d.char }, i))
|
|
2207
|
+
] }) })
|
|
116
2208
|
] });
|
|
117
2209
|
}
|
|
118
2210
|
|
|
119
2211
|
// src/components/Messages.tsx
|
|
120
|
-
|
|
2212
|
+
init_theme();
|
|
2213
|
+
init_format();
|
|
2214
|
+
import { Box as Box3, Text as Text3 } from "ink";
|
|
121
2215
|
|
|
122
2216
|
// src/utils/markdown.ts
|
|
123
2217
|
function renderMarkdown(text) {
|
|
@@ -181,31 +2275,31 @@ function renderInline(text) {
|
|
|
181
2275
|
}
|
|
182
2276
|
|
|
183
2277
|
// src/components/Messages.tsx
|
|
184
|
-
import { jsx as
|
|
2278
|
+
import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
185
2279
|
function Messages({ messages }) {
|
|
186
|
-
return /* @__PURE__ */
|
|
2280
|
+
return /* @__PURE__ */ jsx4(Box3, { flexDirection: "column", children: messages.map((msg) => /* @__PURE__ */ jsx4(MessageRow, { message: msg }, msg.id)) });
|
|
187
2281
|
}
|
|
188
2282
|
function MessageRow({ message }) {
|
|
189
2283
|
switch (message.role) {
|
|
190
2284
|
case "user":
|
|
191
|
-
return /* @__PURE__ */
|
|
192
|
-
/* @__PURE__ */
|
|
193
|
-
/* @__PURE__ */
|
|
2285
|
+
return /* @__PURE__ */ jsxs3(Box3, { marginTop: 1, marginBottom: 1, children: [
|
|
2286
|
+
/* @__PURE__ */ jsx4(Text3, { color: theme.cyan, bold: true, children: "\u276F " }),
|
|
2287
|
+
/* @__PURE__ */ jsx4(Text3, { bold: true, color: theme.text, children: message.content })
|
|
194
2288
|
] });
|
|
195
2289
|
case "assistant": {
|
|
196
2290
|
if (!message.content && message.toolCalls) return null;
|
|
197
2291
|
if (!message.content) return null;
|
|
198
2292
|
const rendered = renderMarkdown(message.content);
|
|
199
|
-
return /* @__PURE__ */
|
|
200
|
-
/* @__PURE__ */
|
|
201
|
-
/* @__PURE__ */
|
|
2293
|
+
return /* @__PURE__ */ jsx4(Box3, { flexDirection: "column", marginBottom: 1, children: /* @__PURE__ */ jsxs3(Box3, { children: [
|
|
2294
|
+
/* @__PURE__ */ jsx4(Text3, { color: theme.cyan, children: "\u23BF " }),
|
|
2295
|
+
/* @__PURE__ */ jsx4(Text3, { wrap: "wrap", children: rendered })
|
|
202
2296
|
] }) });
|
|
203
2297
|
}
|
|
204
2298
|
case "tool": {
|
|
205
2299
|
const lines = message.content.split("\n");
|
|
206
2300
|
const preview = lines.length > 8 ? lines.slice(0, 8).join("\n") + `
|
|
207
2301
|
... (${lines.length - 8} more lines)` : message.content;
|
|
208
|
-
return /* @__PURE__ */
|
|
2302
|
+
return /* @__PURE__ */ jsx4(Box3, { flexDirection: "column", marginLeft: 2, marginBottom: 1, children: /* @__PURE__ */ jsxs3(Text3, { color: theme.dim, children: [
|
|
209
2303
|
"\u23BF ",
|
|
210
2304
|
truncate(preview, 1200)
|
|
211
2305
|
] }) });
|
|
@@ -216,17 +2310,19 @@ function MessageRow({ message }) {
|
|
|
216
2310
|
}
|
|
217
2311
|
|
|
218
2312
|
// src/components/ToolCall.tsx
|
|
219
|
-
|
|
2313
|
+
init_theme();
|
|
2314
|
+
init_format();
|
|
2315
|
+
import { Box as Box4, Text as Text4 } from "ink";
|
|
220
2316
|
import Spinner from "ink-spinner";
|
|
221
|
-
import { jsx as
|
|
2317
|
+
import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
222
2318
|
function ActiveToolCall({ toolName, args }) {
|
|
223
|
-
return /* @__PURE__ */
|
|
224
|
-
/* @__PURE__ */
|
|
225
|
-
/* @__PURE__ */
|
|
2319
|
+
return /* @__PURE__ */ jsxs4(Box4, { marginLeft: 2, children: [
|
|
2320
|
+
/* @__PURE__ */ jsx5(Text4, { color: theme.cyan, children: /* @__PURE__ */ jsx5(Spinner, { type: "dots" }) }),
|
|
2321
|
+
/* @__PURE__ */ jsxs4(Text4, { bold: true, color: theme.yellow, children: [
|
|
226
2322
|
" ",
|
|
227
2323
|
toolName
|
|
228
2324
|
] }),
|
|
229
|
-
args ? /* @__PURE__ */
|
|
2325
|
+
args ? /* @__PURE__ */ jsxs4(Text4, { color: theme.dim, children: [
|
|
230
2326
|
" ",
|
|
231
2327
|
formatToolArgs(args)
|
|
232
2328
|
] }) : null
|
|
@@ -238,15 +2334,15 @@ function ToolResultDisplay({ toolName, output, isError }) {
|
|
|
238
2334
|
const lines = output.split("\n");
|
|
239
2335
|
const preview = lines.length > 6 ? lines.slice(0, 6).join("\n") + `
|
|
240
2336
|
... (${lines.length - 6} more lines)` : output;
|
|
241
|
-
return /* @__PURE__ */
|
|
242
|
-
/* @__PURE__ */
|
|
243
|
-
/* @__PURE__ */
|
|
2337
|
+
return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", marginLeft: 2, marginBottom: 1, children: [
|
|
2338
|
+
/* @__PURE__ */ jsxs4(Box4, { children: [
|
|
2339
|
+
/* @__PURE__ */ jsxs4(Text4, { color: iconColor, children: [
|
|
244
2340
|
icon,
|
|
245
2341
|
" "
|
|
246
2342
|
] }),
|
|
247
|
-
/* @__PURE__ */
|
|
2343
|
+
/* @__PURE__ */ jsx5(Text4, { bold: true, color: theme.yellow, children: toolName })
|
|
248
2344
|
] }),
|
|
249
|
-
/* @__PURE__ */
|
|
2345
|
+
/* @__PURE__ */ jsx5(Box4, { marginLeft: 2, children: /* @__PURE__ */ jsxs4(Text4, { color: theme.dim, children: [
|
|
250
2346
|
"\u23BF ",
|
|
251
2347
|
truncate(preview, 1200)
|
|
252
2348
|
] }) })
|
|
@@ -265,8 +2361,10 @@ function formatToolArgs(args) {
|
|
|
265
2361
|
}
|
|
266
2362
|
|
|
267
2363
|
// src/components/StatusLine.tsx
|
|
268
|
-
|
|
269
|
-
|
|
2364
|
+
init_theme();
|
|
2365
|
+
init_state();
|
|
2366
|
+
import { Box as Box5, Text as Text5 } from "ink";
|
|
2367
|
+
import { Fragment, jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
270
2368
|
function getContextLimit(model) {
|
|
271
2369
|
const lower = model.toLowerCase();
|
|
272
2370
|
if (lower.includes("llama3.1")) return 131072;
|
|
@@ -278,46 +2376,46 @@ function getContextLimit(model) {
|
|
|
278
2376
|
return 8192;
|
|
279
2377
|
}
|
|
280
2378
|
function StatusLine({ model, messageCount, tokenEstimate, isStreaming }) {
|
|
281
|
-
const
|
|
2379
|
+
const state2 = getAppState();
|
|
282
2380
|
const contextLimit = getContextLimit(model);
|
|
283
2381
|
const usage = Math.min(tokenEstimate / contextLimit, 1);
|
|
284
2382
|
const pct = (usage * 100).toFixed(0);
|
|
285
2383
|
const usageColor = usage > 0.8 ? theme.pink : usage > 0.5 ? theme.yellow : theme.cyan;
|
|
286
|
-
const activeTasks =
|
|
287
|
-
return /* @__PURE__ */
|
|
288
|
-
/* @__PURE__ */
|
|
2384
|
+
const activeTasks = state2.tasks.filter((t) => t.status === "in_progress").length;
|
|
2385
|
+
return /* @__PURE__ */ jsxs5(Box5, { marginTop: 1, children: [
|
|
2386
|
+
/* @__PURE__ */ jsxs5(Text5, { color: theme.dim, children: [
|
|
289
2387
|
"\u2500".repeat(2),
|
|
290
2388
|
" "
|
|
291
2389
|
] }),
|
|
292
|
-
/* @__PURE__ */
|
|
293
|
-
/* @__PURE__ */
|
|
294
|
-
/* @__PURE__ */
|
|
2390
|
+
/* @__PURE__ */ jsx6(Text5, { color: theme.cyan, bold: true, children: model.split(":")[0] }),
|
|
2391
|
+
/* @__PURE__ */ jsx6(Text5, { color: theme.dim, children: " \u2502 " }),
|
|
2392
|
+
/* @__PURE__ */ jsxs5(Text5, { color: theme.dim, children: [
|
|
295
2393
|
messageCount,
|
|
296
2394
|
" msgs"
|
|
297
2395
|
] }),
|
|
298
|
-
/* @__PURE__ */
|
|
299
|
-
/* @__PURE__ */
|
|
2396
|
+
/* @__PURE__ */ jsx6(Text5, { color: theme.dim, children: " \u2502 " }),
|
|
2397
|
+
/* @__PURE__ */ jsxs5(Text5, { color: usageColor, children: [
|
|
300
2398
|
"ctx ",
|
|
301
2399
|
pct,
|
|
302
2400
|
"%"
|
|
303
2401
|
] }),
|
|
304
|
-
|
|
305
|
-
/* @__PURE__ */
|
|
306
|
-
/* @__PURE__ */
|
|
2402
|
+
state2.planMode ? /* @__PURE__ */ jsxs5(Fragment, { children: [
|
|
2403
|
+
/* @__PURE__ */ jsx6(Text5, { color: theme.dim, children: " \u2502 " }),
|
|
2404
|
+
/* @__PURE__ */ jsx6(Text5, { color: theme.yellow, bold: true, children: "PLAN" })
|
|
307
2405
|
] }) : null,
|
|
308
|
-
activeTasks > 0 ? /* @__PURE__ */
|
|
309
|
-
/* @__PURE__ */
|
|
310
|
-
/* @__PURE__ */
|
|
2406
|
+
activeTasks > 0 ? /* @__PURE__ */ jsxs5(Fragment, { children: [
|
|
2407
|
+
/* @__PURE__ */ jsx6(Text5, { color: theme.dim, children: " \u2502 " }),
|
|
2408
|
+
/* @__PURE__ */ jsxs5(Text5, { color: theme.green, children: [
|
|
311
2409
|
activeTasks,
|
|
312
2410
|
" task",
|
|
313
2411
|
activeTasks > 1 ? "s" : ""
|
|
314
2412
|
] })
|
|
315
2413
|
] }) : null,
|
|
316
|
-
isStreaming ? /* @__PURE__ */
|
|
317
|
-
/* @__PURE__ */
|
|
318
|
-
/* @__PURE__ */
|
|
2414
|
+
isStreaming ? /* @__PURE__ */ jsxs5(Fragment, { children: [
|
|
2415
|
+
/* @__PURE__ */ jsx6(Text5, { color: theme.dim, children: " \u2502 " }),
|
|
2416
|
+
/* @__PURE__ */ jsx6(Text5, { color: theme.cyan, children: "streaming" })
|
|
319
2417
|
] }) : null,
|
|
320
|
-
/* @__PURE__ */
|
|
2418
|
+
/* @__PURE__ */ jsxs5(Text5, { color: theme.dim, children: [
|
|
321
2419
|
" ",
|
|
322
2420
|
"\u2500".repeat(2)
|
|
323
2421
|
] })
|
|
@@ -325,8 +2423,9 @@ function StatusLine({ model, messageCount, tokenEstimate, isStreaming }) {
|
|
|
325
2423
|
}
|
|
326
2424
|
|
|
327
2425
|
// src/components/UserInput.tsx
|
|
328
|
-
|
|
329
|
-
import {
|
|
2426
|
+
init_theme();
|
|
2427
|
+
import { useState as useState2, useCallback } from "react";
|
|
2428
|
+
import { Box as Box6, Text as Text6, useInput } from "ink";
|
|
330
2429
|
import TextInput from "ink-text-input";
|
|
331
2430
|
|
|
332
2431
|
// src/commands/help.ts
|
|
@@ -409,6 +2508,7 @@ var modelCommand = {
|
|
|
409
2508
|
};
|
|
410
2509
|
|
|
411
2510
|
// src/commands/compact.ts
|
|
2511
|
+
init_providers();
|
|
412
2512
|
import { nanoid } from "nanoid";
|
|
413
2513
|
var compactCommand = {
|
|
414
2514
|
name: "compact",
|
|
@@ -514,7 +2614,7 @@ var contextCommand = {
|
|
|
514
2614
|
`\x1B[2m Model: ${context.model} (${contextLimit.toLocaleString()} ctx)\x1B[0m`
|
|
515
2615
|
];
|
|
516
2616
|
if (usage > 0.8) {
|
|
517
|
-
lines.push("", "\x1B[33m
|
|
2617
|
+
lines.push("", "\x1B[33m [!] Context is getting full. Consider /compact to free space.\x1B[0m");
|
|
518
2618
|
}
|
|
519
2619
|
return { output: lines.join("\n"), silent: true };
|
|
520
2620
|
}
|
|
@@ -536,19 +2636,19 @@ var diffCommand = {
|
|
|
536
2636
|
name: "diff",
|
|
537
2637
|
description: "Show uncommitted git changes",
|
|
538
2638
|
async execute(_args, context) {
|
|
539
|
-
return new Promise((
|
|
2639
|
+
return new Promise((resolve8) => {
|
|
540
2640
|
execFile("git", ["diff", "--stat"], { cwd: context.cwd, timeout: 1e4 }, (err, stdout, stderr) => {
|
|
541
2641
|
if (err) {
|
|
542
|
-
|
|
2642
|
+
resolve8({ output: `Not a git repository or git error: ${stderr || err.message}`, silent: true });
|
|
543
2643
|
return;
|
|
544
2644
|
}
|
|
545
2645
|
if (!stdout.trim()) {
|
|
546
2646
|
execFile("git", ["diff", "--cached", "--stat"], { cwd: context.cwd, timeout: 1e4 }, (err2, staged) => {
|
|
547
2647
|
if (err2 || !staged.trim()) {
|
|
548
|
-
|
|
2648
|
+
resolve8({ output: "No uncommitted changes.", silent: true });
|
|
549
2649
|
return;
|
|
550
2650
|
}
|
|
551
|
-
|
|
2651
|
+
resolve8({ output: `\x1B[1m\x1B[36mStaged changes:\x1B[0m
|
|
552
2652
|
${staged}`, silent: true });
|
|
553
2653
|
});
|
|
554
2654
|
return;
|
|
@@ -562,7 +2662,7 @@ ${staged}`, silent: true });
|
|
|
562
2662
|
fullDiff ? fullDiff.slice(0, 3e3) : "",
|
|
563
2663
|
fullDiff && fullDiff.length > 3e3 ? "\n... (truncated)" : ""
|
|
564
2664
|
].join("\n");
|
|
565
|
-
|
|
2665
|
+
resolve8({ output, silent: true });
|
|
566
2666
|
});
|
|
567
2667
|
});
|
|
568
2668
|
});
|
|
@@ -570,12 +2670,12 @@ ${staged}`, silent: true });
|
|
|
570
2670
|
};
|
|
571
2671
|
|
|
572
2672
|
// src/session.ts
|
|
573
|
-
import { mkdir, readdir, readFile, writeFile } from "fs/promises";
|
|
574
|
-
import { join } from "path";
|
|
2673
|
+
import { mkdir as mkdir2, readdir, readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
|
|
2674
|
+
import { join as join2 } from "path";
|
|
575
2675
|
import { nanoid as nanoid2 } from "nanoid";
|
|
576
|
-
var SESSIONS_DIR =
|
|
2676
|
+
var SESSIONS_DIR = join2(process.env.HOME || "~", ".darkfoo", "sessions");
|
|
577
2677
|
async function ensureDir() {
|
|
578
|
-
await
|
|
2678
|
+
await mkdir2(SESSIONS_DIR, { recursive: true });
|
|
579
2679
|
}
|
|
580
2680
|
function createSessionId() {
|
|
581
2681
|
return nanoid2(12);
|
|
@@ -594,11 +2694,11 @@ async function saveSession(id, messages, model, cwd) {
|
|
|
594
2694
|
messageCount: messages.length,
|
|
595
2695
|
messages
|
|
596
2696
|
};
|
|
597
|
-
await
|
|
2697
|
+
await writeFile2(join2(SESSIONS_DIR, `${id}.json`), JSON.stringify(data, null, 2), "utf-8");
|
|
598
2698
|
}
|
|
599
2699
|
async function loadSession(id) {
|
|
600
2700
|
try {
|
|
601
|
-
const raw = await
|
|
2701
|
+
const raw = await readFile2(join2(SESSIONS_DIR, `${id}.json`), "utf-8");
|
|
602
2702
|
return JSON.parse(raw);
|
|
603
2703
|
} catch {
|
|
604
2704
|
return null;
|
|
@@ -611,7 +2711,7 @@ async function listSessions() {
|
|
|
611
2711
|
for (const file of files) {
|
|
612
2712
|
if (!file.endsWith(".json")) continue;
|
|
613
2713
|
try {
|
|
614
|
-
const raw = await
|
|
2714
|
+
const raw = await readFile2(join2(SESSIONS_DIR, file), "utf-8");
|
|
615
2715
|
const data = JSON.parse(raw);
|
|
616
2716
|
sessions.push({
|
|
617
2717
|
id: data.id,
|
|
@@ -676,6 +2776,7 @@ var resumeCommand = {
|
|
|
676
2776
|
};
|
|
677
2777
|
|
|
678
2778
|
// src/commands/commit.ts
|
|
2779
|
+
init_providers();
|
|
679
2780
|
import { execFile as execFile2 } from "child_process";
|
|
680
2781
|
var commitCommand = {
|
|
681
2782
|
name: "commit",
|
|
@@ -719,7 +2820,7 @@ ${truncatedDiff}`
|
|
|
719
2820
|
const output = await gitExec(["commit", "-m", commitMsg], context.cwd);
|
|
720
2821
|
return {
|
|
721
2822
|
output: [
|
|
722
|
-
`\x1B[32m\
|
|
2823
|
+
`\x1B[32m[ok]\x1B[0m Committed: ${commitMsg}`,
|
|
723
2824
|
"",
|
|
724
2825
|
output.trim()
|
|
725
2826
|
].join("\n"),
|
|
@@ -732,12 +2833,12 @@ ${truncatedDiff}`
|
|
|
732
2833
|
}
|
|
733
2834
|
};
|
|
734
2835
|
function gitExec(args, cwd) {
|
|
735
|
-
return new Promise((
|
|
2836
|
+
return new Promise((resolve8, reject) => {
|
|
736
2837
|
execFile2("git", args, { cwd, timeout: 15e3, maxBuffer: 1024 * 1024 }, (err, stdout, stderr) => {
|
|
737
2838
|
if (err && !stdout) {
|
|
738
2839
|
reject(new Error(stderr || err.message));
|
|
739
2840
|
} else {
|
|
740
|
-
|
|
2841
|
+
resolve8(stdout);
|
|
741
2842
|
}
|
|
742
2843
|
});
|
|
743
2844
|
});
|
|
@@ -763,7 +2864,7 @@ var copyCommand = {
|
|
|
763
2864
|
}
|
|
764
2865
|
};
|
|
765
2866
|
function copyToClipboard(text) {
|
|
766
|
-
return new Promise((
|
|
2867
|
+
return new Promise((resolve8, reject) => {
|
|
767
2868
|
const tools = [
|
|
768
2869
|
{ cmd: "xclip", args: ["-selection", "clipboard"] },
|
|
769
2870
|
{ cmd: "xsel", args: ["--clipboard", "--input"] },
|
|
@@ -781,7 +2882,7 @@ function copyToClipboard(text) {
|
|
|
781
2882
|
if (err) {
|
|
782
2883
|
tryNext();
|
|
783
2884
|
} else {
|
|
784
|
-
|
|
2885
|
+
resolve8();
|
|
785
2886
|
}
|
|
786
2887
|
});
|
|
787
2888
|
if (proc.stdin) {
|
|
@@ -834,9 +2935,9 @@ var reviewCommand = {
|
|
|
834
2935
|
};
|
|
835
2936
|
|
|
836
2937
|
// src/commands/config.ts
|
|
837
|
-
import { readFile as
|
|
838
|
-
import { join as
|
|
839
|
-
var
|
|
2938
|
+
import { readFile as readFile3, writeFile as writeFile3, mkdir as mkdir3 } from "fs/promises";
|
|
2939
|
+
import { join as join3 } from "path";
|
|
2940
|
+
var SETTINGS_PATH2 = join3(process.env.HOME || "~", ".darkfoo", "settings.json");
|
|
840
2941
|
var configCommand = {
|
|
841
2942
|
name: "config",
|
|
842
2943
|
aliases: ["settings"],
|
|
@@ -845,12 +2946,12 @@ var configCommand = {
|
|
|
845
2946
|
const parts = args.trim().split(/\s+/);
|
|
846
2947
|
if (!args.trim() || parts[0] === "show") {
|
|
847
2948
|
try {
|
|
848
|
-
const raw = await
|
|
2949
|
+
const raw = await readFile3(SETTINGS_PATH2, "utf-8");
|
|
849
2950
|
const config = JSON.parse(raw);
|
|
850
2951
|
return {
|
|
851
2952
|
output: [
|
|
852
2953
|
"\x1B[1m\x1B[36mSettings\x1B[0m",
|
|
853
|
-
`\x1B[2m${
|
|
2954
|
+
`\x1B[2m${SETTINGS_PATH2}\x1B[0m`,
|
|
854
2955
|
"",
|
|
855
2956
|
JSON.stringify(config, null, 2)
|
|
856
2957
|
].join("\n"),
|
|
@@ -862,7 +2963,7 @@ var configCommand = {
|
|
|
862
2963
|
"\x1B[1m\x1B[36mSettings\x1B[0m",
|
|
863
2964
|
"",
|
|
864
2965
|
"No settings file found. Default settings in use.",
|
|
865
|
-
`\x1B[2mSettings will be created at: ${
|
|
2966
|
+
`\x1B[2mSettings will be created at: ${SETTINGS_PATH2}\x1B[0m`
|
|
866
2967
|
].join("\n"),
|
|
867
2968
|
silent: true
|
|
868
2969
|
};
|
|
@@ -873,7 +2974,7 @@ var configCommand = {
|
|
|
873
2974
|
const value = parts.slice(2).join(" ");
|
|
874
2975
|
let config;
|
|
875
2976
|
try {
|
|
876
|
-
const raw = await
|
|
2977
|
+
const raw = await readFile3(SETTINGS_PATH2, "utf-8");
|
|
877
2978
|
config = JSON.parse(raw);
|
|
878
2979
|
} catch {
|
|
879
2980
|
config = {};
|
|
@@ -883,21 +2984,21 @@ var configCommand = {
|
|
|
883
2984
|
else if (value === "false") parsed = false;
|
|
884
2985
|
else if (!isNaN(Number(value))) parsed = Number(value);
|
|
885
2986
|
config[key] = parsed;
|
|
886
|
-
await
|
|
887
|
-
await
|
|
2987
|
+
await mkdir3(join3(process.env.HOME || "~", ".darkfoo"), { recursive: true });
|
|
2988
|
+
await writeFile3(SETTINGS_PATH2, JSON.stringify(config, null, 2), "utf-8");
|
|
888
2989
|
return { output: `Set ${key} = ${JSON.stringify(parsed)}`, silent: true };
|
|
889
2990
|
}
|
|
890
2991
|
if (parts[0] === "delete" && parts[1]) {
|
|
891
2992
|
const key = parts[1];
|
|
892
2993
|
let config;
|
|
893
2994
|
try {
|
|
894
|
-
const raw = await
|
|
2995
|
+
const raw = await readFile3(SETTINGS_PATH2, "utf-8");
|
|
895
2996
|
config = JSON.parse(raw);
|
|
896
2997
|
} catch {
|
|
897
2998
|
return { output: "No settings to delete from.", silent: true };
|
|
898
2999
|
}
|
|
899
3000
|
delete config[key];
|
|
900
|
-
await
|
|
3001
|
+
await writeFile3(SETTINGS_PATH2, JSON.stringify(config, null, 2), "utf-8");
|
|
901
3002
|
return { output: `Deleted ${key}`, silent: true };
|
|
902
3003
|
}
|
|
903
3004
|
return {
|
|
@@ -963,7 +3064,7 @@ var themeCommand = {
|
|
|
963
3064
|
const names = Object.keys(THEMES);
|
|
964
3065
|
const lines = names.map((name2) => {
|
|
965
3066
|
const t = THEMES[name2];
|
|
966
|
-
const swatch = `\x1B[38;2;${hexToRgb(t.cyan)}m
|
|
3067
|
+
const swatch = `\x1B[38;2;${hexToRgb(t.cyan)}m#\x1B[0m\x1B[38;2;${hexToRgb(t.pink)}m#\x1B[0m\x1B[38;2;${hexToRgb(t.green)}m#\x1B[0m\x1B[38;2;${hexToRgb(t.yellow)}m#\x1B[0m\x1B[38;2;${hexToRgb(t.purple)}m#\x1B[0m`;
|
|
967
3068
|
return ` ${name2.padEnd(10)} ${swatch}`;
|
|
968
3069
|
});
|
|
969
3070
|
return {
|
|
@@ -975,7 +3076,7 @@ var themeCommand = {
|
|
|
975
3076
|
if (!THEMES[name]) {
|
|
976
3077
|
return { output: `Unknown theme: ${name}. Available: ${Object.keys(THEMES).join(", ")}`, silent: true };
|
|
977
3078
|
}
|
|
978
|
-
const { theme: theme2 } = await
|
|
3079
|
+
const { theme: theme2 } = await Promise.resolve().then(() => (init_theme(), theme_exports));
|
|
979
3080
|
const newTheme = THEMES[name];
|
|
980
3081
|
Object.assign(theme2, newTheme);
|
|
981
3082
|
return { output: `Theme switched to: ${name}`, silent: true };
|
|
@@ -989,8 +3090,8 @@ function hexToRgb(hex) {
|
|
|
989
3090
|
}
|
|
990
3091
|
|
|
991
3092
|
// src/commands/export.ts
|
|
992
|
-
import { writeFile as
|
|
993
|
-
import { join as
|
|
3093
|
+
import { writeFile as writeFile4, mkdir as mkdir4 } from "fs/promises";
|
|
3094
|
+
import { join as join4 } from "path";
|
|
994
3095
|
var exportCommand = {
|
|
995
3096
|
name: "export",
|
|
996
3097
|
description: "Export conversation to markdown file (usage: /export [filename])",
|
|
@@ -1000,9 +3101,9 @@ var exportCommand = {
|
|
|
1000
3101
|
}
|
|
1001
3102
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
1002
3103
|
const filename = args.trim() || `darkfoo-session-${timestamp}.md`;
|
|
1003
|
-
const exportDir =
|
|
1004
|
-
await
|
|
1005
|
-
const filePath =
|
|
3104
|
+
const exportDir = join4(process.env.HOME || "~", ".darkfoo", "exports");
|
|
3105
|
+
await mkdir4(exportDir, { recursive: true });
|
|
3106
|
+
const filePath = join4(exportDir, filename);
|
|
1006
3107
|
const lines = [
|
|
1007
3108
|
`# Darkfoo Code Session`,
|
|
1008
3109
|
``,
|
|
@@ -1034,23 +3135,25 @@ var exportCommand = {
|
|
|
1034
3135
|
break;
|
|
1035
3136
|
}
|
|
1036
3137
|
}
|
|
1037
|
-
await
|
|
3138
|
+
await writeFile4(filePath, lines.join("\n"), "utf-8");
|
|
1038
3139
|
return { output: `Exported to ${filePath}`, silent: true };
|
|
1039
3140
|
}
|
|
1040
3141
|
};
|
|
1041
3142
|
|
|
1042
3143
|
// src/commands/status.ts
|
|
3144
|
+
init_file_tracker();
|
|
3145
|
+
init_state();
|
|
1043
3146
|
var statusCommand = {
|
|
1044
3147
|
name: "status",
|
|
1045
3148
|
description: "Show session status overview",
|
|
1046
3149
|
async execute(_args, context) {
|
|
1047
|
-
const
|
|
3150
|
+
const state2 = getAppState();
|
|
1048
3151
|
const userMsgs = context.messages.filter((m) => m.role === "user").length;
|
|
1049
3152
|
const assistantMsgs = context.messages.filter((m) => m.role === "assistant").length;
|
|
1050
3153
|
const toolMsgs = context.messages.filter((m) => m.role === "tool").length;
|
|
1051
3154
|
const totalChars = context.messages.reduce((sum, m) => sum + m.content.length, 0);
|
|
1052
3155
|
const estTokens = Math.ceil(totalChars / 4);
|
|
1053
|
-
const tasks =
|
|
3156
|
+
const tasks = state2.tasks;
|
|
1054
3157
|
const pending = tasks.filter((t) => t.status === "pending").length;
|
|
1055
3158
|
const inProgress = tasks.filter((t) => t.status === "in_progress").length;
|
|
1056
3159
|
const completed = tasks.filter((t) => t.status === "completed").length;
|
|
@@ -1059,7 +3162,7 @@ var statusCommand = {
|
|
|
1059
3162
|
"",
|
|
1060
3163
|
` Model: \x1B[36m${context.model}\x1B[0m`,
|
|
1061
3164
|
` Working Dir: ${context.cwd}`,
|
|
1062
|
-
` Plan Mode: ${
|
|
3165
|
+
` Plan Mode: ${state2.planMode ? "\x1B[33mActive\x1B[0m" : "Off"}`,
|
|
1063
3166
|
"",
|
|
1064
3167
|
" \x1B[1mMessages\x1B[0m",
|
|
1065
3168
|
` User: ${userMsgs}`,
|
|
@@ -1083,7 +3186,7 @@ var filesCommand = {
|
|
|
1083
3186
|
name: "files",
|
|
1084
3187
|
description: "List files modified or referenced in this session",
|
|
1085
3188
|
async execute(_args, context) {
|
|
1086
|
-
const
|
|
3189
|
+
const readFiles2 = /* @__PURE__ */ new Set();
|
|
1087
3190
|
const writtenFiles = /* @__PURE__ */ new Set();
|
|
1088
3191
|
const editedFiles = /* @__PURE__ */ new Set();
|
|
1089
3192
|
for (const msg of context.messages) {
|
|
@@ -1094,7 +3197,7 @@ var filesCommand = {
|
|
|
1094
3197
|
if (!path) continue;
|
|
1095
3198
|
switch (tc.function.name) {
|
|
1096
3199
|
case "Read":
|
|
1097
|
-
|
|
3200
|
+
readFiles2.add(path);
|
|
1098
3201
|
break;
|
|
1099
3202
|
case "Write":
|
|
1100
3203
|
writtenFiles.add(path);
|
|
@@ -1106,13 +3209,13 @@ var filesCommand = {
|
|
|
1106
3209
|
}
|
|
1107
3210
|
}
|
|
1108
3211
|
}
|
|
1109
|
-
if (
|
|
3212
|
+
if (readFiles2.size === 0 && writtenFiles.size === 0 && editedFiles.size === 0) {
|
|
1110
3213
|
return { output: "No files referenced in this session.", silent: true };
|
|
1111
3214
|
}
|
|
1112
3215
|
const lines = ["\x1B[1m\x1B[36mSession Files\x1B[0m", ""];
|
|
1113
|
-
if (
|
|
3216
|
+
if (readFiles2.size > 0) {
|
|
1114
3217
|
lines.push(" \x1B[1mRead:\x1B[0m");
|
|
1115
|
-
for (const f of
|
|
3218
|
+
for (const f of readFiles2) lines.push(` \x1B[36m${f}\x1B[0m`);
|
|
1116
3219
|
lines.push("");
|
|
1117
3220
|
}
|
|
1118
3221
|
if (writtenFiles.size > 0) {
|
|
@@ -1170,6 +3273,7 @@ var briefCommand = {
|
|
|
1170
3273
|
};
|
|
1171
3274
|
|
|
1172
3275
|
// src/commands/provider.ts
|
|
3276
|
+
init_providers();
|
|
1173
3277
|
var providerCommand = {
|
|
1174
3278
|
name: "provider",
|
|
1175
3279
|
aliases: ["backend"],
|
|
@@ -1185,7 +3289,7 @@ var providerCommand = {
|
|
|
1185
3289
|
""
|
|
1186
3290
|
];
|
|
1187
3291
|
for (const { config, online } of results) {
|
|
1188
|
-
const status = online ? "\x1B[32m
|
|
3292
|
+
const status = online ? "\x1B[32m[online]\x1B[0m" : "\x1B[31m[offline]\x1B[0m";
|
|
1189
3293
|
const isActive = config.name === active ? " \x1B[36m\u2190 active\x1B[0m" : "";
|
|
1190
3294
|
lines.push(` ${config.name.padEnd(14)} ${config.label.padEnd(14)} ${config.baseUrl.padEnd(30)} ${status}${isActive}`);
|
|
1191
3295
|
}
|
|
@@ -1226,15 +3330,15 @@ var providerCommand = {
|
|
|
1226
3330
|
if (subcommand === "remove") {
|
|
1227
3331
|
const name = parts[1];
|
|
1228
3332
|
if (!name) return { output: "Usage: /provider remove <name>", silent: true };
|
|
1229
|
-
const { removeProviderConfig } = await
|
|
1230
|
-
if (
|
|
3333
|
+
const { removeProviderConfig: removeProviderConfig2 } = await Promise.resolve().then(() => (init_providers(), providers_exports));
|
|
3334
|
+
if (removeProviderConfig2(name)) {
|
|
1231
3335
|
await saveProviderSettings();
|
|
1232
3336
|
return { output: `Removed provider: ${name}`, silent: true };
|
|
1233
3337
|
}
|
|
1234
3338
|
return { output: `Provider not found: ${name}`, silent: true };
|
|
1235
3339
|
}
|
|
1236
3340
|
if (subcommand === "models") {
|
|
1237
|
-
const { getProvider: getProvider2 } = await
|
|
3341
|
+
const { getProvider: getProvider2 } = await Promise.resolve().then(() => (init_providers(), providers_exports));
|
|
1238
3342
|
const provider = getProvider2();
|
|
1239
3343
|
const models = await provider.listModels();
|
|
1240
3344
|
if (models.length === 0) {
|
|
@@ -1312,10 +3416,10 @@ function getCommandNames() {
|
|
|
1312
3416
|
}
|
|
1313
3417
|
|
|
1314
3418
|
// src/components/UserInput.tsx
|
|
1315
|
-
import { Fragment as Fragment2, jsx as
|
|
3419
|
+
import { Fragment as Fragment2, jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
1316
3420
|
function UserInput({ value, onChange, onSubmit, disabled, history }) {
|
|
1317
|
-
const [historyIdx, setHistoryIdx] =
|
|
1318
|
-
const [suggestion, setSuggestion] =
|
|
3421
|
+
const [historyIdx, setHistoryIdx] = useState2(-1);
|
|
3422
|
+
const [suggestion, setSuggestion] = useState2("");
|
|
1319
3423
|
useInput((_input, key) => {
|
|
1320
3424
|
if (disabled || !history || history.length === 0) return;
|
|
1321
3425
|
if (key.upArrow) {
|
|
@@ -1361,20 +3465,20 @@ function UserInput({ value, onChange, onSubmit, disabled, history }) {
|
|
|
1361
3465
|
const borderColor = disabled ? theme.dim : isBash ? theme.yellow : isCommand ? theme.purple : theme.cyan;
|
|
1362
3466
|
const promptChar = isBash ? "!" : "\u276F";
|
|
1363
3467
|
const promptColor = isBash ? theme.yellow : theme.cyan;
|
|
1364
|
-
return /* @__PURE__ */
|
|
1365
|
-
|
|
3468
|
+
return /* @__PURE__ */ jsxs6(
|
|
3469
|
+
Box6,
|
|
1366
3470
|
{
|
|
1367
3471
|
borderStyle: "round",
|
|
1368
3472
|
borderColor,
|
|
1369
3473
|
paddingLeft: 1,
|
|
1370
3474
|
paddingRight: 1,
|
|
1371
3475
|
children: [
|
|
1372
|
-
/* @__PURE__ */
|
|
3476
|
+
/* @__PURE__ */ jsxs6(Text6, { color: disabled ? theme.dim : promptColor, bold: true, children: [
|
|
1373
3477
|
promptChar,
|
|
1374
3478
|
" "
|
|
1375
3479
|
] }),
|
|
1376
|
-
disabled ? /* @__PURE__ */
|
|
1377
|
-
/* @__PURE__ */
|
|
3480
|
+
disabled ? /* @__PURE__ */ jsx7(Text6, { color: theme.dim, children: "..." }) : /* @__PURE__ */ jsxs6(Fragment2, { children: [
|
|
3481
|
+
/* @__PURE__ */ jsx7(
|
|
1378
3482
|
TextInput,
|
|
1379
3483
|
{
|
|
1380
3484
|
value,
|
|
@@ -1388,7 +3492,7 @@ function UserInput({ value, onChange, onSubmit, disabled, history }) {
|
|
|
1388
3492
|
}
|
|
1389
3493
|
}
|
|
1390
3494
|
),
|
|
1391
|
-
suggestion ? /* @__PURE__ */
|
|
3495
|
+
suggestion ? /* @__PURE__ */ jsx7(Text6, { color: theme.dim, children: suggestion }) : null
|
|
1392
3496
|
] })
|
|
1393
3497
|
]
|
|
1394
3498
|
}
|
|
@@ -1396,7 +3500,14 @@ function UserInput({ value, onChange, onSubmit, disabled, history }) {
|
|
|
1396
3500
|
}
|
|
1397
3501
|
|
|
1398
3502
|
// src/repl.tsx
|
|
1399
|
-
|
|
3503
|
+
init_providers();
|
|
3504
|
+
init_query();
|
|
3505
|
+
init_system_prompt();
|
|
3506
|
+
init_providers();
|
|
3507
|
+
init_tools();
|
|
3508
|
+
init_bash();
|
|
3509
|
+
init_theme();
|
|
3510
|
+
import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
1400
3511
|
function getContextLimit3(model) {
|
|
1401
3512
|
const lower = model.toLowerCase();
|
|
1402
3513
|
if (lower.includes("llama3.1")) return 131072;
|
|
@@ -1410,29 +3521,34 @@ function getContextLimit3(model) {
|
|
|
1410
3521
|
function REPL({ initialPrompt }) {
|
|
1411
3522
|
const { model: initialModel, systemPromptOverride } = useDarkfooContext();
|
|
1412
3523
|
const { exit } = useApp();
|
|
1413
|
-
const [model, setModel] =
|
|
1414
|
-
const [messages, setMessages] =
|
|
1415
|
-
const [inputValue, setInputValue] =
|
|
1416
|
-
const [isStreaming, setIsStreaming] =
|
|
1417
|
-
const [streamingText, setStreamingText] =
|
|
1418
|
-
const [activeTool, setActiveTool] =
|
|
1419
|
-
const [toolResults, setToolResults] =
|
|
1420
|
-
const [commandOutput, setCommandOutput] =
|
|
1421
|
-
const [inputHistory, setInputHistory] =
|
|
1422
|
-
const [tokenCounts, setTokenCounts] =
|
|
1423
|
-
const [systemPrompt, setSystemPrompt] =
|
|
3524
|
+
const [model, setModel] = useState3(initialModel);
|
|
3525
|
+
const [messages, setMessages] = useState3([]);
|
|
3526
|
+
const [inputValue, setInputValue] = useState3("");
|
|
3527
|
+
const [isStreaming, setIsStreaming] = useState3(false);
|
|
3528
|
+
const [streamingText, setStreamingText] = useState3("");
|
|
3529
|
+
const [activeTool, setActiveTool] = useState3(null);
|
|
3530
|
+
const [toolResults, setToolResults] = useState3([]);
|
|
3531
|
+
const [commandOutput, setCommandOutput] = useState3(null);
|
|
3532
|
+
const [inputHistory, setInputHistory] = useState3([]);
|
|
3533
|
+
const [tokenCounts, setTokenCounts] = useState3({ input: 0, output: 0 });
|
|
3534
|
+
const [systemPrompt, setSystemPrompt] = useState3("");
|
|
3535
|
+
const [foxMood, setFoxMood] = useState3("idle");
|
|
3536
|
+
const [providerOnline, setProviderOnline] = useState3(true);
|
|
1424
3537
|
const abortRef = useRef(null);
|
|
1425
3538
|
const hasRun = useRef(false);
|
|
1426
3539
|
const sessionId = useRef(createSessionId());
|
|
1427
3540
|
const tools = getTools();
|
|
1428
3541
|
const cwd = process.cwd();
|
|
1429
|
-
|
|
3542
|
+
useEffect2(() => {
|
|
1430
3543
|
if (systemPromptOverride) {
|
|
1431
3544
|
setSystemPrompt(systemPromptOverride);
|
|
1432
3545
|
} else {
|
|
1433
3546
|
buildSystemPrompt(tools, cwd).then(setSystemPrompt);
|
|
1434
3547
|
}
|
|
1435
3548
|
}, []);
|
|
3549
|
+
useEffect2(() => {
|
|
3550
|
+
getProvider().healthCheck().then((ok) => setProviderOnline(ok)).catch(() => setProviderOnline(false));
|
|
3551
|
+
}, [model]);
|
|
1436
3552
|
const commandContext = {
|
|
1437
3553
|
messages,
|
|
1438
3554
|
model,
|
|
@@ -1451,7 +3567,7 @@ function REPL({ initialPrompt }) {
|
|
|
1451
3567
|
const runQuery = useCallback2(
|
|
1452
3568
|
async (userMessage) => {
|
|
1453
3569
|
const userMsg = {
|
|
1454
|
-
id:
|
|
3570
|
+
id: nanoid6(),
|
|
1455
3571
|
role: "user",
|
|
1456
3572
|
content: userMessage,
|
|
1457
3573
|
timestamp: Date.now()
|
|
@@ -1461,6 +3577,7 @@ function REPL({ initialPrompt }) {
|
|
|
1461
3577
|
setStreamingText("");
|
|
1462
3578
|
setToolResults([]);
|
|
1463
3579
|
setCommandOutput(null);
|
|
3580
|
+
setFoxMood("thinking");
|
|
1464
3581
|
const controller = new AbortController();
|
|
1465
3582
|
abortRef.current = controller;
|
|
1466
3583
|
const allMessages = [...messages, userMsg];
|
|
@@ -1480,15 +3597,18 @@ function REPL({ initialPrompt }) {
|
|
|
1480
3597
|
switch (event.type) {
|
|
1481
3598
|
case "text_delta":
|
|
1482
3599
|
setStreamingText((prev) => prev + event.text);
|
|
3600
|
+
setFoxMood("idle");
|
|
1483
3601
|
break;
|
|
1484
3602
|
case "tool_call":
|
|
1485
3603
|
setActiveTool({ name: event.toolCall.function.name, args: event.toolCall.function.arguments });
|
|
3604
|
+
setFoxMood("working");
|
|
1486
3605
|
break;
|
|
1487
3606
|
case "tool_result":
|
|
1488
3607
|
setActiveTool(null);
|
|
3608
|
+
setFoxMood(event.isError ? "error" : "working");
|
|
1489
3609
|
setToolResults((prev) => [
|
|
1490
3610
|
...prev,
|
|
1491
|
-
{ id:
|
|
3611
|
+
{ id: nanoid6(), toolName: event.toolName, output: event.output, isError: event.isError }
|
|
1492
3612
|
]);
|
|
1493
3613
|
break;
|
|
1494
3614
|
case "assistant_message":
|
|
@@ -1505,9 +3625,10 @@ function REPL({ initialPrompt }) {
|
|
|
1505
3625
|
setStreamingText("");
|
|
1506
3626
|
break;
|
|
1507
3627
|
case "error":
|
|
3628
|
+
setFoxMood("error");
|
|
1508
3629
|
setMessages((prev) => [
|
|
1509
3630
|
...prev,
|
|
1510
|
-
{ id:
|
|
3631
|
+
{ id: nanoid6(), role: "assistant", content: `Error: ${event.error}`, timestamp: Date.now() }
|
|
1511
3632
|
]);
|
|
1512
3633
|
break;
|
|
1513
3634
|
}
|
|
@@ -1517,7 +3638,7 @@ function REPL({ initialPrompt }) {
|
|
|
1517
3638
|
const msg = err instanceof Error ? err.message : String(err);
|
|
1518
3639
|
setMessages((prev) => [
|
|
1519
3640
|
...prev,
|
|
1520
|
-
{ id:
|
|
3641
|
+
{ id: nanoid6(), role: "assistant", content: `Error: ${msg}`, timestamp: Date.now() }
|
|
1521
3642
|
]);
|
|
1522
3643
|
}
|
|
1523
3644
|
} finally {
|
|
@@ -1525,6 +3646,8 @@ function REPL({ initialPrompt }) {
|
|
|
1525
3646
|
setStreamingText("");
|
|
1526
3647
|
setActiveTool(null);
|
|
1527
3648
|
setToolResults([]);
|
|
3649
|
+
setFoxMood((prev) => prev === "error" ? "error" : "success");
|
|
3650
|
+
setTimeout(() => setFoxMood("idle"), 2e3);
|
|
1528
3651
|
abortRef.current = null;
|
|
1529
3652
|
}
|
|
1530
3653
|
},
|
|
@@ -1580,14 +3703,14 @@ function REPL({ initialPrompt }) {
|
|
|
1580
3703
|
},
|
|
1581
3704
|
[addToHistory, commandContext, cwd, runQuery]
|
|
1582
3705
|
);
|
|
1583
|
-
|
|
3706
|
+
useEffect2(() => {
|
|
1584
3707
|
if (initialPrompt && !hasRun.current) {
|
|
1585
3708
|
hasRun.current = true;
|
|
1586
3709
|
runQuery(initialPrompt);
|
|
1587
3710
|
}
|
|
1588
3711
|
}, [initialPrompt, runQuery]);
|
|
1589
3712
|
const autoCompactRef = useRef(false);
|
|
1590
|
-
|
|
3713
|
+
useEffect2(() => {
|
|
1591
3714
|
if (isStreaming || autoCompactRef.current || messages.length < 6) return;
|
|
1592
3715
|
const totalChars = messages.reduce((sum, m) => sum + m.content.length, 0);
|
|
1593
3716
|
const estTokens = Math.ceil(totalChars / 4) + 2e3;
|
|
@@ -1595,7 +3718,7 @@ function REPL({ initialPrompt }) {
|
|
|
1595
3718
|
const usage = estTokens / limit;
|
|
1596
3719
|
if (usage > 0.85) {
|
|
1597
3720
|
autoCompactRef.current = true;
|
|
1598
|
-
setCommandOutput("\x1B[33m
|
|
3721
|
+
setCommandOutput("\x1B[33m[!] Context 85%+ full \u2014 auto-compacting...\x1B[0m");
|
|
1599
3722
|
const transcript = messages.filter((m) => m.role === "user" || m.role === "assistant" && m.content).map((m) => `${m.role}: ${m.content}`).join("\n\n");
|
|
1600
3723
|
getProvider().chat({
|
|
1601
3724
|
model,
|
|
@@ -1604,7 +3727,7 @@ function REPL({ initialPrompt }) {
|
|
|
1604
3727
|
${transcript}` }]
|
|
1605
3728
|
}).then((result) => {
|
|
1606
3729
|
const summaryMsg = {
|
|
1607
|
-
id:
|
|
3730
|
+
id: nanoid6(),
|
|
1608
3731
|
role: "user",
|
|
1609
3732
|
content: `[Auto-compacted summary]
|
|
1610
3733
|
${result.content}
|
|
@@ -1613,10 +3736,10 @@ ${result.content}
|
|
|
1613
3736
|
};
|
|
1614
3737
|
setMessages([summaryMsg]);
|
|
1615
3738
|
setTokenCounts({ input: Math.ceil(result.content.length / 4), output: 0 });
|
|
1616
|
-
setCommandOutput("\x1B[32m
|
|
3739
|
+
setCommandOutput("\x1B[32m[ok] Auto-compacted conversation.\x1B[0m");
|
|
1617
3740
|
autoCompactRef.current = false;
|
|
1618
3741
|
}).catch(() => {
|
|
1619
|
-
setCommandOutput("\x1B[31m
|
|
3742
|
+
setCommandOutput("\x1B[31m[err] Auto-compact failed.\x1B[0m");
|
|
1620
3743
|
autoCompactRef.current = false;
|
|
1621
3744
|
});
|
|
1622
3745
|
}
|
|
@@ -1630,35 +3753,38 @@ ${result.content}
|
|
|
1630
3753
|
}
|
|
1631
3754
|
}
|
|
1632
3755
|
});
|
|
1633
|
-
return /* @__PURE__ */
|
|
1634
|
-
/* @__PURE__ */
|
|
1635
|
-
/* @__PURE__ */
|
|
1636
|
-
commandOutput ? /* @__PURE__ */
|
|
1637
|
-
toolResults.map((tr) => /* @__PURE__ */
|
|
1638
|
-
activeTool ? /* @__PURE__ */
|
|
1639
|
-
isStreaming && streamingText ? /* @__PURE__ */
|
|
1640
|
-
/* @__PURE__ */
|
|
1641
|
-
/* @__PURE__ */
|
|
1642
|
-
/* @__PURE__ */
|
|
3756
|
+
return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", padding: 1, children: [
|
|
3757
|
+
/* @__PURE__ */ jsx8(Banner, { model, cwd, providerName: getActiveProviderName(), providerOnline }),
|
|
3758
|
+
/* @__PURE__ */ jsx8(Messages, { messages }),
|
|
3759
|
+
commandOutput ? /* @__PURE__ */ jsx8(Box7, { marginBottom: 1, marginLeft: 2, flexDirection: "column", children: /* @__PURE__ */ jsx8(Text7, { children: commandOutput }) }) : null,
|
|
3760
|
+
toolResults.map((tr) => /* @__PURE__ */ jsx8(ToolResultDisplay, { toolName: tr.toolName, output: tr.output, isError: tr.isError }, tr.id)),
|
|
3761
|
+
activeTool ? /* @__PURE__ */ jsx8(ActiveToolCall, { toolName: activeTool.name, args: activeTool.args }) : null,
|
|
3762
|
+
isStreaming && streamingText ? /* @__PURE__ */ jsxs7(Box7, { marginBottom: 1, children: [
|
|
3763
|
+
/* @__PURE__ */ jsx8(Text7, { color: theme.cyan, children: "\u23BF " }),
|
|
3764
|
+
/* @__PURE__ */ jsx8(Text7, { color: theme.text, wrap: "wrap", children: streamingText }),
|
|
3765
|
+
/* @__PURE__ */ jsxs7(Text7, { color: theme.cyan, children: [
|
|
1643
3766
|
" ",
|
|
1644
|
-
/* @__PURE__ */
|
|
3767
|
+
/* @__PURE__ */ jsx8(Spinner2, { type: "dots" })
|
|
1645
3768
|
] })
|
|
1646
3769
|
] }) : null,
|
|
1647
|
-
isStreaming && !streamingText && !activeTool ? /* @__PURE__ */
|
|
1648
|
-
/* @__PURE__ */
|
|
1649
|
-
/* @__PURE__ */
|
|
3770
|
+
isStreaming && !streamingText && !activeTool ? /* @__PURE__ */ jsxs7(Box7, { marginLeft: 2, children: [
|
|
3771
|
+
/* @__PURE__ */ jsx8(Text7, { color: theme.cyan, children: /* @__PURE__ */ jsx8(Spinner2, { type: "dots" }) }),
|
|
3772
|
+
/* @__PURE__ */ jsx8(Text7, { color: theme.dim, children: " Thinking..." })
|
|
1650
3773
|
] }) : null,
|
|
1651
|
-
/* @__PURE__ */
|
|
1652
|
-
|
|
1653
|
-
{
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
3774
|
+
/* @__PURE__ */ jsxs7(Box7, { children: [
|
|
3775
|
+
/* @__PURE__ */ jsx8(Fox, { mood: foxMood }),
|
|
3776
|
+
/* @__PURE__ */ jsx8(Box7, { flexDirection: "column", flexGrow: 1, marginLeft: 1, children: /* @__PURE__ */ jsx8(
|
|
3777
|
+
UserInput,
|
|
3778
|
+
{
|
|
3779
|
+
value: inputValue,
|
|
3780
|
+
onChange: setInputValue,
|
|
3781
|
+
onSubmit: handleSubmit,
|
|
3782
|
+
disabled: isStreaming,
|
|
3783
|
+
history: inputHistory
|
|
3784
|
+
}
|
|
3785
|
+
) })
|
|
3786
|
+
] }),
|
|
3787
|
+
/* @__PURE__ */ jsx8(
|
|
1662
3788
|
StatusLine,
|
|
1663
3789
|
{
|
|
1664
3790
|
model,
|
|
@@ -1671,9 +3797,10 @@ ${result.content}
|
|
|
1671
3797
|
}
|
|
1672
3798
|
|
|
1673
3799
|
// src/main.tsx
|
|
1674
|
-
|
|
3800
|
+
init_providers();
|
|
3801
|
+
import { jsx as jsx9 } from "react/jsx-runtime";
|
|
1675
3802
|
var program = new Command();
|
|
1676
|
-
program.name("darkfoo").description("Darkfoo Code \u2014 local AI coding assistant powered by local LLM providers").version("0.
|
|
3803
|
+
program.name("darkfoo").description("Darkfoo Code \u2014 local AI coding assistant powered by local LLM providers").version("0.2.0").option("-m, --model <model>", "Model to use", "llama3.1:8b").option("-p, --prompt <prompt>", "Run a single prompt (non-interactive)").option("--provider <name>", "LLM provider backend (ollama, llama-cpp, vllm, tgi, etc.)").option("--system-prompt <prompt>", "Override the system prompt").action(async (options) => {
|
|
1677
3804
|
const { model, prompt, provider, systemPrompt } = options;
|
|
1678
3805
|
await loadProviderSettings();
|
|
1679
3806
|
if (provider) {
|
|
@@ -1687,14 +3814,14 @@ program.name("darkfoo").description("Darkfoo Code \u2014 local AI coding assista
|
|
|
1687
3814
|
}
|
|
1688
3815
|
}
|
|
1689
3816
|
if (prompt) {
|
|
1690
|
-
const { buildSystemPrompt: buildSystemPrompt2 } = await
|
|
1691
|
-
const { getTools: getTools2 } = await
|
|
1692
|
-
const { query: query2 } = await
|
|
3817
|
+
const { buildSystemPrompt: buildSystemPrompt2 } = await Promise.resolve().then(() => (init_system_prompt(), system_prompt_exports));
|
|
3818
|
+
const { getTools: getTools2 } = await Promise.resolve().then(() => (init_tools(), tools_exports));
|
|
3819
|
+
const { query: query2 } = await Promise.resolve().then(() => (init_query(), query_exports));
|
|
1693
3820
|
const tools = getTools2();
|
|
1694
3821
|
const sysPrompt = systemPrompt ?? await buildSystemPrompt2(tools, process.cwd());
|
|
1695
|
-
const { nanoid:
|
|
3822
|
+
const { nanoid: nanoid7 } = await import("nanoid");
|
|
1696
3823
|
const userMsg = {
|
|
1697
|
-
id:
|
|
3824
|
+
id: nanoid7(),
|
|
1698
3825
|
role: "user",
|
|
1699
3826
|
content: prompt,
|
|
1700
3827
|
timestamp: Date.now()
|
|
@@ -1733,7 +3860,7 @@ Error: ${event.error}
|
|
|
1733
3860
|
process.exit(0);
|
|
1734
3861
|
}
|
|
1735
3862
|
const { waitUntilExit } = render(
|
|
1736
|
-
/* @__PURE__ */
|
|
3863
|
+
/* @__PURE__ */ jsx9(App, { model, systemPromptOverride: systemPrompt, children: /* @__PURE__ */ jsx9(REPL, {}) })
|
|
1737
3864
|
);
|
|
1738
3865
|
await waitUntilExit();
|
|
1739
3866
|
});
|