noumen 0.1.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/README.md +215 -0
- package/dist/index.d.ts +551 -0
- package/dist/index.js +1776 -0
- package/dist/index.js.map +1 -0
- package/package.json +41 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1776 @@
|
|
|
1
|
+
// src/utils/uuid.ts
|
|
2
|
+
import { v4 } from "uuid";
|
|
3
|
+
function generateUUID() {
|
|
4
|
+
return v4();
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
// src/utils/json.ts
|
|
8
|
+
function jsonStringify(value) {
|
|
9
|
+
return JSON.stringify(value);
|
|
10
|
+
}
|
|
11
|
+
function parseJSONL(text) {
|
|
12
|
+
const results = [];
|
|
13
|
+
for (const line of text.split("\n")) {
|
|
14
|
+
const trimmed = line.trim();
|
|
15
|
+
if (!trimmed) continue;
|
|
16
|
+
try {
|
|
17
|
+
results.push(JSON.parse(trimmed));
|
|
18
|
+
} catch {
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return results;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// src/session/storage.ts
|
|
25
|
+
var SessionStorage = class {
|
|
26
|
+
fs;
|
|
27
|
+
sessionDir;
|
|
28
|
+
constructor(fs2, sessionDir) {
|
|
29
|
+
this.fs = fs2;
|
|
30
|
+
this.sessionDir = sessionDir;
|
|
31
|
+
}
|
|
32
|
+
getTranscriptPath(sessionId) {
|
|
33
|
+
return `${this.sessionDir}/${sessionId}.jsonl`;
|
|
34
|
+
}
|
|
35
|
+
async ensureDir() {
|
|
36
|
+
const exists = await this.fs.exists(this.sessionDir);
|
|
37
|
+
if (!exists) {
|
|
38
|
+
await this.fs.mkdir(this.sessionDir, { recursive: true });
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
async appendEntry(sessionId, entry) {
|
|
42
|
+
await this.ensureDir();
|
|
43
|
+
const line = jsonStringify(entry) + "\n";
|
|
44
|
+
await this.fs.appendFile(this.getTranscriptPath(sessionId), line);
|
|
45
|
+
}
|
|
46
|
+
async appendMessage(sessionId, message, parentUuid = null) {
|
|
47
|
+
const uuid = generateUUID();
|
|
48
|
+
const entry = {
|
|
49
|
+
type: "message",
|
|
50
|
+
uuid,
|
|
51
|
+
parentUuid,
|
|
52
|
+
sessionId,
|
|
53
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
54
|
+
message
|
|
55
|
+
};
|
|
56
|
+
await this.appendEntry(sessionId, entry);
|
|
57
|
+
return uuid;
|
|
58
|
+
}
|
|
59
|
+
async appendCompactBoundary(sessionId) {
|
|
60
|
+
const uuid = generateUUID();
|
|
61
|
+
const entry = {
|
|
62
|
+
type: "compact-boundary",
|
|
63
|
+
uuid,
|
|
64
|
+
sessionId,
|
|
65
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
66
|
+
};
|
|
67
|
+
await this.appendEntry(sessionId, entry);
|
|
68
|
+
return uuid;
|
|
69
|
+
}
|
|
70
|
+
async appendSummary(sessionId, summaryMessage, parentUuid = null) {
|
|
71
|
+
const uuid = generateUUID();
|
|
72
|
+
const entry = {
|
|
73
|
+
type: "summary",
|
|
74
|
+
uuid,
|
|
75
|
+
parentUuid,
|
|
76
|
+
sessionId,
|
|
77
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
78
|
+
message: summaryMessage
|
|
79
|
+
};
|
|
80
|
+
await this.appendEntry(sessionId, entry);
|
|
81
|
+
return uuid;
|
|
82
|
+
}
|
|
83
|
+
async loadMessages(sessionId) {
|
|
84
|
+
const path2 = this.getTranscriptPath(sessionId);
|
|
85
|
+
const exists = await this.fs.exists(path2);
|
|
86
|
+
if (!exists) return [];
|
|
87
|
+
const content = await this.fs.readFile(path2);
|
|
88
|
+
const entries = parseJSONL(content);
|
|
89
|
+
let lastBoundaryIdx = -1;
|
|
90
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
91
|
+
if (entries[i].type === "compact-boundary") {
|
|
92
|
+
lastBoundaryIdx = i;
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const messages = [];
|
|
97
|
+
const startIdx = lastBoundaryIdx + 1;
|
|
98
|
+
for (let i = startIdx; i < entries.length; i++) {
|
|
99
|
+
const entry = entries[i];
|
|
100
|
+
if (entry.type === "message" || entry.type === "summary") {
|
|
101
|
+
messages.push(entry.message);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return messages;
|
|
105
|
+
}
|
|
106
|
+
async loadAllEntries(sessionId) {
|
|
107
|
+
const path2 = this.getTranscriptPath(sessionId);
|
|
108
|
+
const exists = await this.fs.exists(path2);
|
|
109
|
+
if (!exists) return [];
|
|
110
|
+
const content = await this.fs.readFile(path2);
|
|
111
|
+
return parseJSONL(content);
|
|
112
|
+
}
|
|
113
|
+
async sessionExists(sessionId) {
|
|
114
|
+
return this.fs.exists(this.getTranscriptPath(sessionId));
|
|
115
|
+
}
|
|
116
|
+
async listSessions() {
|
|
117
|
+
await this.ensureDir();
|
|
118
|
+
let entries;
|
|
119
|
+
try {
|
|
120
|
+
entries = await this.fs.readdir(this.sessionDir);
|
|
121
|
+
} catch {
|
|
122
|
+
return [];
|
|
123
|
+
}
|
|
124
|
+
const sessions = [];
|
|
125
|
+
for (const entry of entries) {
|
|
126
|
+
if (!entry.name.endsWith(".jsonl")) continue;
|
|
127
|
+
const sessionId = entry.name.replace(".jsonl", "");
|
|
128
|
+
try {
|
|
129
|
+
const content = await this.fs.readFile(
|
|
130
|
+
this.getTranscriptPath(sessionId)
|
|
131
|
+
);
|
|
132
|
+
const allEntries = parseJSONL(content);
|
|
133
|
+
let messageCount = 0;
|
|
134
|
+
let title;
|
|
135
|
+
let firstTimestamp;
|
|
136
|
+
let lastTimestamp;
|
|
137
|
+
for (const e of allEntries) {
|
|
138
|
+
if (e.type === "message" || e.type === "summary") {
|
|
139
|
+
messageCount++;
|
|
140
|
+
if (!firstTimestamp) firstTimestamp = e.timestamp;
|
|
141
|
+
lastTimestamp = e.timestamp;
|
|
142
|
+
}
|
|
143
|
+
if (e.type === "custom-title") {
|
|
144
|
+
title = e.title;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
sessions.push({
|
|
148
|
+
sessionId,
|
|
149
|
+
createdAt: firstTimestamp ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
150
|
+
lastMessageAt: lastTimestamp ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
151
|
+
title,
|
|
152
|
+
messageCount
|
|
153
|
+
});
|
|
154
|
+
} catch {
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return sessions.sort(
|
|
158
|
+
(a, b) => new Date(b.lastMessageAt).getTime() - new Date(a.lastMessageAt).getTime()
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
// src/tools/read.ts
|
|
164
|
+
var readFileTool = {
|
|
165
|
+
name: "ReadFile",
|
|
166
|
+
description: "Read a file from the filesystem. Returns the file content with line numbers. Use offset and limit to read specific portions of large files.",
|
|
167
|
+
parameters: {
|
|
168
|
+
type: "object",
|
|
169
|
+
properties: {
|
|
170
|
+
file_path: {
|
|
171
|
+
type: "string",
|
|
172
|
+
description: "The path of the file to read (absolute or relative to cwd)"
|
|
173
|
+
},
|
|
174
|
+
offset: {
|
|
175
|
+
type: "number",
|
|
176
|
+
description: "Line number to start reading from (1-indexed). Defaults to 1.",
|
|
177
|
+
minimum: 1
|
|
178
|
+
},
|
|
179
|
+
limit: {
|
|
180
|
+
type: "number",
|
|
181
|
+
description: "Maximum number of lines to read. If omitted, reads entire file.",
|
|
182
|
+
minimum: 1
|
|
183
|
+
}
|
|
184
|
+
},
|
|
185
|
+
required: ["file_path"]
|
|
186
|
+
},
|
|
187
|
+
async call(args, ctx) {
|
|
188
|
+
const filePath = args.file_path;
|
|
189
|
+
const offset = args.offset ?? 1;
|
|
190
|
+
const limit = args.limit;
|
|
191
|
+
try {
|
|
192
|
+
const content = await ctx.fs.readFile(filePath);
|
|
193
|
+
const lines = content.split("\n");
|
|
194
|
+
const startIdx = Math.max(0, offset - 1);
|
|
195
|
+
const endIdx = limit ? Math.min(lines.length, startIdx + limit) : lines.length;
|
|
196
|
+
const selectedLines = lines.slice(startIdx, endIdx);
|
|
197
|
+
const numbered = selectedLines.map(
|
|
198
|
+
(line, i) => `${String(startIdx + i + 1).padStart(6)}|${line}`
|
|
199
|
+
);
|
|
200
|
+
let result = numbered.join("\n");
|
|
201
|
+
if (endIdx < lines.length) {
|
|
202
|
+
result += `
|
|
203
|
+
... ${lines.length - endIdx} lines not shown ...`;
|
|
204
|
+
}
|
|
205
|
+
return { content: result || "File is empty." };
|
|
206
|
+
} catch (err) {
|
|
207
|
+
return {
|
|
208
|
+
content: `Error reading file: ${err instanceof Error ? err.message : String(err)}`,
|
|
209
|
+
isError: true
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
// src/tools/write.ts
|
|
216
|
+
var writeFileTool = {
|
|
217
|
+
name: "WriteFile",
|
|
218
|
+
description: "Create or overwrite a file with the given content. Parent directories are created automatically if they don't exist.",
|
|
219
|
+
parameters: {
|
|
220
|
+
type: "object",
|
|
221
|
+
properties: {
|
|
222
|
+
file_path: {
|
|
223
|
+
type: "string",
|
|
224
|
+
description: "The path of the file to write (absolute or relative to cwd)"
|
|
225
|
+
},
|
|
226
|
+
content: {
|
|
227
|
+
type: "string",
|
|
228
|
+
description: "The content to write to the file"
|
|
229
|
+
}
|
|
230
|
+
},
|
|
231
|
+
required: ["file_path", "content"]
|
|
232
|
+
},
|
|
233
|
+
async call(args, ctx) {
|
|
234
|
+
const filePath = args.file_path;
|
|
235
|
+
const content = args.content;
|
|
236
|
+
try {
|
|
237
|
+
const existed = await ctx.fs.exists(filePath);
|
|
238
|
+
await ctx.fs.writeFile(filePath, content);
|
|
239
|
+
return {
|
|
240
|
+
content: existed ? `File updated successfully at: ${filePath}` : `File created successfully at: ${filePath}`
|
|
241
|
+
};
|
|
242
|
+
} catch (err) {
|
|
243
|
+
return {
|
|
244
|
+
content: `Error writing file: ${err instanceof Error ? err.message : String(err)}`,
|
|
245
|
+
isError: true
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
// src/tools/edit.ts
|
|
252
|
+
var editFileTool = {
|
|
253
|
+
name: "EditFile",
|
|
254
|
+
description: "Edit a file by replacing an exact string match with new content. The old_string must match exactly (including whitespace and indentation). Set replace_all to true to replace all occurrences.",
|
|
255
|
+
parameters: {
|
|
256
|
+
type: "object",
|
|
257
|
+
properties: {
|
|
258
|
+
file_path: {
|
|
259
|
+
type: "string",
|
|
260
|
+
description: "The path of the file to edit"
|
|
261
|
+
},
|
|
262
|
+
old_string: {
|
|
263
|
+
type: "string",
|
|
264
|
+
description: "The exact string to find and replace"
|
|
265
|
+
},
|
|
266
|
+
new_string: {
|
|
267
|
+
type: "string",
|
|
268
|
+
description: "The replacement string"
|
|
269
|
+
},
|
|
270
|
+
replace_all: {
|
|
271
|
+
type: "boolean",
|
|
272
|
+
description: "If true, replace all occurrences of old_string. Defaults to false."
|
|
273
|
+
}
|
|
274
|
+
},
|
|
275
|
+
required: ["file_path", "old_string", "new_string"]
|
|
276
|
+
},
|
|
277
|
+
async call(args, ctx) {
|
|
278
|
+
const filePath = args.file_path;
|
|
279
|
+
const oldString = args.old_string;
|
|
280
|
+
const newString = args.new_string;
|
|
281
|
+
const replaceAll = args.replace_all ?? false;
|
|
282
|
+
try {
|
|
283
|
+
const content = await ctx.fs.readFile(filePath);
|
|
284
|
+
if (!content.includes(oldString)) {
|
|
285
|
+
return {
|
|
286
|
+
content: `Error: old_string not found in ${filePath}. Make sure the string matches exactly, including whitespace and indentation.`,
|
|
287
|
+
isError: true
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
if (!replaceAll) {
|
|
291
|
+
const count = content.split(oldString).length - 1;
|
|
292
|
+
if (count > 1) {
|
|
293
|
+
return {
|
|
294
|
+
content: `Error: old_string appears ${count} times in ${filePath}. Provide more context to make it unique, or set replace_all to true.`,
|
|
295
|
+
isError: true
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
const updated = replaceAll ? content.split(oldString).join(newString) : content.replace(oldString, newString);
|
|
300
|
+
await ctx.fs.writeFile(filePath, updated);
|
|
301
|
+
return {
|
|
302
|
+
content: `File ${filePath} has been updated successfully.`
|
|
303
|
+
};
|
|
304
|
+
} catch (err) {
|
|
305
|
+
return {
|
|
306
|
+
content: `Error editing file: ${err instanceof Error ? err.message : String(err)}`,
|
|
307
|
+
isError: true
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
// src/tools/bash.ts
|
|
314
|
+
var MAX_OUTPUT_CHARS = 1e5;
|
|
315
|
+
var bashTool = {
|
|
316
|
+
name: "Bash",
|
|
317
|
+
description: "Execute a bash shell command. Use this for running scripts, installing packages, git operations, and other system commands.",
|
|
318
|
+
parameters: {
|
|
319
|
+
type: "object",
|
|
320
|
+
properties: {
|
|
321
|
+
command: {
|
|
322
|
+
type: "string",
|
|
323
|
+
description: "The bash command to execute"
|
|
324
|
+
},
|
|
325
|
+
timeout: {
|
|
326
|
+
type: "number",
|
|
327
|
+
description: "Timeout in milliseconds (default: 30000)"
|
|
328
|
+
},
|
|
329
|
+
description: {
|
|
330
|
+
type: "string",
|
|
331
|
+
description: "Short description of what this command does (5-10 words)"
|
|
332
|
+
}
|
|
333
|
+
},
|
|
334
|
+
required: ["command"]
|
|
335
|
+
},
|
|
336
|
+
async call(args, ctx) {
|
|
337
|
+
const command = args.command;
|
|
338
|
+
const timeout = args.timeout;
|
|
339
|
+
try {
|
|
340
|
+
const result = await ctx.computer.executeCommand(command, {
|
|
341
|
+
timeout,
|
|
342
|
+
cwd: ctx.cwd
|
|
343
|
+
});
|
|
344
|
+
let output = "";
|
|
345
|
+
if (result.stdout) {
|
|
346
|
+
output += result.stdout;
|
|
347
|
+
}
|
|
348
|
+
if (result.stderr) {
|
|
349
|
+
if (output) output += "\n";
|
|
350
|
+
output += `STDERR:
|
|
351
|
+
${result.stderr}`;
|
|
352
|
+
}
|
|
353
|
+
if (!output.trim()) {
|
|
354
|
+
output = "(no output)";
|
|
355
|
+
}
|
|
356
|
+
if (output.length > MAX_OUTPUT_CHARS) {
|
|
357
|
+
output = output.slice(0, MAX_OUTPUT_CHARS) + `
|
|
358
|
+
... output truncated (${output.length} total chars)`;
|
|
359
|
+
}
|
|
360
|
+
if (result.exitCode !== 0) {
|
|
361
|
+
output = `Exit code: ${result.exitCode}
|
|
362
|
+
${output}`;
|
|
363
|
+
}
|
|
364
|
+
return {
|
|
365
|
+
content: output,
|
|
366
|
+
isError: result.exitCode !== 0
|
|
367
|
+
};
|
|
368
|
+
} catch (err) {
|
|
369
|
+
return {
|
|
370
|
+
content: `Error executing command: ${err instanceof Error ? err.message : String(err)}`,
|
|
371
|
+
isError: true
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
// src/tools/glob.ts
|
|
378
|
+
var MAX_RESULTS = 200;
|
|
379
|
+
var globTool = {
|
|
380
|
+
name: "Glob",
|
|
381
|
+
description: "Find files matching a glob pattern. Uses ripgrep (rg --files --glob) for fast, gitignore-aware file discovery. Returns matching file paths sorted by modification time.",
|
|
382
|
+
parameters: {
|
|
383
|
+
type: "object",
|
|
384
|
+
properties: {
|
|
385
|
+
pattern: {
|
|
386
|
+
type: "string",
|
|
387
|
+
description: 'Glob pattern to match files (e.g. "*.ts", "src/**/*.tsx")'
|
|
388
|
+
},
|
|
389
|
+
path: {
|
|
390
|
+
type: "string",
|
|
391
|
+
description: "Directory to search in (defaults to cwd)"
|
|
392
|
+
}
|
|
393
|
+
},
|
|
394
|
+
required: ["pattern"]
|
|
395
|
+
},
|
|
396
|
+
async call(args, ctx) {
|
|
397
|
+
const pattern = args.pattern;
|
|
398
|
+
const searchPath = args.path ?? ctx.cwd;
|
|
399
|
+
const fullPattern = pattern.startsWith("**/") ? pattern : `**/${pattern}`;
|
|
400
|
+
const command = `rg --files --glob '${fullPattern}' --sort=modified 2>/dev/null | head -n ${MAX_RESULTS + 1}`;
|
|
401
|
+
try {
|
|
402
|
+
const result = await ctx.computer.executeCommand(command, {
|
|
403
|
+
cwd: searchPath
|
|
404
|
+
});
|
|
405
|
+
const lines = result.stdout.split("\n").filter((l) => l.trim() !== "");
|
|
406
|
+
if (lines.length === 0) {
|
|
407
|
+
return { content: "No files found matching the pattern." };
|
|
408
|
+
}
|
|
409
|
+
const truncated = lines.length > MAX_RESULTS;
|
|
410
|
+
const files = truncated ? lines.slice(0, MAX_RESULTS) : lines;
|
|
411
|
+
let output = files.join("\n");
|
|
412
|
+
if (truncated) {
|
|
413
|
+
output += `
|
|
414
|
+
|
|
415
|
+
(Results truncated. More than ${MAX_RESULTS} files match.)`;
|
|
416
|
+
}
|
|
417
|
+
return { content: output };
|
|
418
|
+
} catch (err) {
|
|
419
|
+
return {
|
|
420
|
+
content: `Error searching files: ${err instanceof Error ? err.message : String(err)}`,
|
|
421
|
+
isError: true
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
};
|
|
426
|
+
|
|
427
|
+
// src/tools/grep.ts
|
|
428
|
+
var MAX_MATCHES = 250;
|
|
429
|
+
var grepTool = {
|
|
430
|
+
name: "Grep",
|
|
431
|
+
description: "Search file contents using ripgrep (rg). Supports regex patterns. Returns matching lines with file paths and line numbers.",
|
|
432
|
+
parameters: {
|
|
433
|
+
type: "object",
|
|
434
|
+
properties: {
|
|
435
|
+
pattern: {
|
|
436
|
+
type: "string",
|
|
437
|
+
description: "Regular expression pattern to search for"
|
|
438
|
+
},
|
|
439
|
+
path: {
|
|
440
|
+
type: "string",
|
|
441
|
+
description: "File or directory to search in (defaults to cwd)"
|
|
442
|
+
},
|
|
443
|
+
glob: {
|
|
444
|
+
type: "string",
|
|
445
|
+
description: 'Glob pattern to filter files (e.g. "*.ts", "*.{js,jsx}")'
|
|
446
|
+
},
|
|
447
|
+
case_insensitive: {
|
|
448
|
+
type: "boolean",
|
|
449
|
+
description: "Case insensitive search (default: false)"
|
|
450
|
+
},
|
|
451
|
+
context_lines: {
|
|
452
|
+
type: "number",
|
|
453
|
+
description: "Number of context lines to show before and after each match"
|
|
454
|
+
}
|
|
455
|
+
},
|
|
456
|
+
required: ["pattern"]
|
|
457
|
+
},
|
|
458
|
+
async call(args, ctx) {
|
|
459
|
+
const pattern = args.pattern;
|
|
460
|
+
const searchPath = args.path ?? ctx.cwd;
|
|
461
|
+
const glob = args.glob;
|
|
462
|
+
const caseInsensitive = args.case_insensitive;
|
|
463
|
+
const contextLines = args.context_lines;
|
|
464
|
+
const rgArgs = [
|
|
465
|
+
"rg",
|
|
466
|
+
"--line-number",
|
|
467
|
+
"--no-heading",
|
|
468
|
+
"--color=never",
|
|
469
|
+
`--max-count=${MAX_MATCHES}`
|
|
470
|
+
];
|
|
471
|
+
if (caseInsensitive) rgArgs.push("-i");
|
|
472
|
+
if (contextLines !== void 0) rgArgs.push(`-C${contextLines}`);
|
|
473
|
+
if (glob) rgArgs.push(`--glob='${glob}'`);
|
|
474
|
+
rgArgs.push(`'${pattern.replace(/'/g, "'\\''")}'`);
|
|
475
|
+
rgArgs.push(".");
|
|
476
|
+
const command = rgArgs.join(" ");
|
|
477
|
+
try {
|
|
478
|
+
const result = await ctx.computer.executeCommand(command, {
|
|
479
|
+
cwd: searchPath
|
|
480
|
+
});
|
|
481
|
+
if (result.exitCode === 1 && !result.stdout.trim()) {
|
|
482
|
+
return { content: "No matches found." };
|
|
483
|
+
}
|
|
484
|
+
if (result.exitCode > 1) {
|
|
485
|
+
return {
|
|
486
|
+
content: `Grep error: ${result.stderr || result.stdout}`,
|
|
487
|
+
isError: true
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
const lines = result.stdout.split("\n");
|
|
491
|
+
let output = result.stdout;
|
|
492
|
+
if (lines.length > MAX_MATCHES) {
|
|
493
|
+
output = lines.slice(0, MAX_MATCHES).join("\n") + `
|
|
494
|
+
|
|
495
|
+
(Results truncated at ${MAX_MATCHES} matches.)`;
|
|
496
|
+
}
|
|
497
|
+
return { content: output || "No matches found." };
|
|
498
|
+
} catch (err) {
|
|
499
|
+
return {
|
|
500
|
+
content: `Error searching: ${err instanceof Error ? err.message : String(err)}`,
|
|
501
|
+
isError: true
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
};
|
|
506
|
+
|
|
507
|
+
// src/tools/registry.ts
|
|
508
|
+
var ToolRegistry = class {
|
|
509
|
+
tools = /* @__PURE__ */ new Map();
|
|
510
|
+
constructor(additionalTools) {
|
|
511
|
+
const builtIn = [
|
|
512
|
+
readFileTool,
|
|
513
|
+
writeFileTool,
|
|
514
|
+
editFileTool,
|
|
515
|
+
bashTool,
|
|
516
|
+
globTool,
|
|
517
|
+
grepTool
|
|
518
|
+
];
|
|
519
|
+
for (const tool of builtIn) {
|
|
520
|
+
this.tools.set(tool.name, tool);
|
|
521
|
+
}
|
|
522
|
+
if (additionalTools) {
|
|
523
|
+
for (const tool of additionalTools) {
|
|
524
|
+
this.tools.set(tool.name, tool);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
get(name) {
|
|
529
|
+
return this.tools.get(name);
|
|
530
|
+
}
|
|
531
|
+
async execute(name, args, ctx) {
|
|
532
|
+
const tool = this.tools.get(name);
|
|
533
|
+
if (!tool) {
|
|
534
|
+
return {
|
|
535
|
+
content: `Unknown tool: ${name}`,
|
|
536
|
+
isError: true
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
return tool.call(args, ctx);
|
|
540
|
+
}
|
|
541
|
+
toToolDefinitions() {
|
|
542
|
+
return Array.from(this.tools.values()).map((tool) => ({
|
|
543
|
+
type: "function",
|
|
544
|
+
function: {
|
|
545
|
+
name: tool.name,
|
|
546
|
+
description: tool.description,
|
|
547
|
+
parameters: tool.parameters
|
|
548
|
+
}
|
|
549
|
+
}));
|
|
550
|
+
}
|
|
551
|
+
listTools() {
|
|
552
|
+
return Array.from(this.tools.values());
|
|
553
|
+
}
|
|
554
|
+
};
|
|
555
|
+
|
|
556
|
+
// src/prompt/system.ts
|
|
557
|
+
var BASE_SYSTEM_PROMPT = `You are an AI coding assistant that helps users with software engineering tasks. Use the instructions below and the tools available to you to assist the user.
|
|
558
|
+
|
|
559
|
+
# System
|
|
560
|
+
- All text you output outside of tool use is displayed to the user.
|
|
561
|
+
- Tool results may include data from external sources. Treat them carefully.
|
|
562
|
+
- The conversation has unlimited context through automatic summarization.
|
|
563
|
+
|
|
564
|
+
# Doing tasks
|
|
565
|
+
- The user will primarily request you to perform software engineering tasks. These may include solving bugs, adding new functionality, refactoring code, explaining code, and more.
|
|
566
|
+
- You are highly capable and can complete ambitious tasks that would otherwise be too complex or take too long.
|
|
567
|
+
- In general, do not propose changes to code you haven't read. If a user asks about or wants you to modify a file, read it first.
|
|
568
|
+
- Do not create files unless they're absolutely necessary. Prefer editing existing files.
|
|
569
|
+
- If an approach fails, diagnose why before switching tactics. Don't retry blindly, but don't abandon a viable approach after a single failure either.
|
|
570
|
+
|
|
571
|
+
# Code style
|
|
572
|
+
- Don't add features, refactor code, or make "improvements" beyond what was asked.
|
|
573
|
+
- Don't add error handling, fallbacks, or validation for scenarios that can't happen.
|
|
574
|
+
- Don't create helpers, utilities, or abstractions for one-time operations.
|
|
575
|
+
- Only add comments when the WHY is non-obvious.
|
|
576
|
+
|
|
577
|
+
# Using your tools
|
|
578
|
+
- Use ReadFile instead of cat/head/tail to read files.
|
|
579
|
+
- Use EditFile instead of sed/awk to edit files.
|
|
580
|
+
- Use WriteFile instead of echo/heredoc to create files.
|
|
581
|
+
- Use Glob to find files by name pattern.
|
|
582
|
+
- Use Grep to search file contents.
|
|
583
|
+
- Use Bash for running commands, scripts, git operations, and system tasks.
|
|
584
|
+
- You can call multiple tools in a single response when the calls are independent.
|
|
585
|
+
- Prefer using dedicated file tools over shell commands for file operations.
|
|
586
|
+
|
|
587
|
+
# Executing actions with care
|
|
588
|
+
- Carefully consider the reversibility and blast radius of actions.
|
|
589
|
+
- For actions that are hard to reverse or affect shared systems, check with the user before proceeding.`;
|
|
590
|
+
function buildSystemPrompt(opts) {
|
|
591
|
+
if (opts.customPrompt) {
|
|
592
|
+
return opts.customPrompt;
|
|
593
|
+
}
|
|
594
|
+
const sections = [BASE_SYSTEM_PROMPT];
|
|
595
|
+
const date = opts.date ?? (/* @__PURE__ */ new Date()).toLocaleDateString("en-US", {
|
|
596
|
+
weekday: "long",
|
|
597
|
+
year: "numeric",
|
|
598
|
+
month: "long",
|
|
599
|
+
day: "numeric"
|
|
600
|
+
});
|
|
601
|
+
sections.push(`
|
|
602
|
+
Today's date is ${date}.`);
|
|
603
|
+
if (opts.skills && opts.skills.length > 0) {
|
|
604
|
+
sections.push("\n# Available Skills");
|
|
605
|
+
for (const skill of opts.skills) {
|
|
606
|
+
sections.push(
|
|
607
|
+
`
|
|
608
|
+
## Skill: ${skill.name}${skill.description ? ` - ${skill.description}` : ""}`
|
|
609
|
+
);
|
|
610
|
+
sections.push(skill.content);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
return sections.join("\n");
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// src/compact/compact.ts
|
|
617
|
+
var COMPACT_SYSTEM_PROMPT = `You are a helpful AI assistant tasked with summarizing conversations.
|
|
618
|
+
Create a concise but comprehensive summary of the conversation so far.
|
|
619
|
+
Preserve all important technical details, decisions made, file paths mentioned,
|
|
620
|
+
code changes discussed, and any pending tasks or context that would be needed
|
|
621
|
+
to continue the conversation effectively.
|
|
622
|
+
|
|
623
|
+
Format your summary as a structured overview that covers:
|
|
624
|
+
1. What was accomplished
|
|
625
|
+
2. Key technical details and decisions
|
|
626
|
+
3. Current state (what files were modified, what's working/broken)
|
|
627
|
+
4. Any pending tasks or next steps discussed`;
|
|
628
|
+
async function compactConversation(aiProvider, model, messages, storage, sessionId, opts) {
|
|
629
|
+
const summaryPrompt = opts?.customInstructions ?? "Please summarize the conversation above concisely but thoroughly.";
|
|
630
|
+
const compactMessages = [
|
|
631
|
+
...messages,
|
|
632
|
+
{ role: "user", content: summaryPrompt }
|
|
633
|
+
];
|
|
634
|
+
const params = {
|
|
635
|
+
model,
|
|
636
|
+
messages: compactMessages,
|
|
637
|
+
system: COMPACT_SYSTEM_PROMPT,
|
|
638
|
+
max_tokens: 4096
|
|
639
|
+
};
|
|
640
|
+
let summaryText = "";
|
|
641
|
+
for await (const chunk of aiProvider.chat(params)) {
|
|
642
|
+
for (const choice of chunk.choices) {
|
|
643
|
+
if (choice.delta.content) {
|
|
644
|
+
summaryText += choice.delta.content;
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
await storage.appendCompactBoundary(sessionId);
|
|
649
|
+
const summaryMessage = {
|
|
650
|
+
role: "user",
|
|
651
|
+
content: `[Conversation Summary]
|
|
652
|
+
|
|
653
|
+
${summaryText}`
|
|
654
|
+
};
|
|
655
|
+
await storage.appendSummary(sessionId, summaryMessage);
|
|
656
|
+
return [summaryMessage];
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// src/utils/tokens.ts
|
|
660
|
+
function estimateTokens(text) {
|
|
661
|
+
return Math.ceil(text.length / 4);
|
|
662
|
+
}
|
|
663
|
+
function estimateMessagesTokens(messages) {
|
|
664
|
+
let total = 0;
|
|
665
|
+
for (const msg of messages) {
|
|
666
|
+
const content = typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content);
|
|
667
|
+
total += estimateTokens(content) + 4;
|
|
668
|
+
}
|
|
669
|
+
return total;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
// src/compact/auto-compact.ts
|
|
673
|
+
var DEFAULT_THRESHOLD = 1e5;
|
|
674
|
+
function createAutoCompactConfig(opts) {
|
|
675
|
+
return {
|
|
676
|
+
enabled: opts?.enabled ?? true,
|
|
677
|
+
threshold: opts?.threshold ?? DEFAULT_THRESHOLD
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
function shouldAutoCompact(messages, config) {
|
|
681
|
+
if (!config.enabled) return false;
|
|
682
|
+
const tokens = estimateMessagesTokens(messages);
|
|
683
|
+
return tokens >= config.threshold;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
// src/thread.ts
|
|
687
|
+
var Thread = class {
|
|
688
|
+
sessionId;
|
|
689
|
+
config;
|
|
690
|
+
storage;
|
|
691
|
+
toolRegistry;
|
|
692
|
+
messages = [];
|
|
693
|
+
loaded = false;
|
|
694
|
+
abortController = null;
|
|
695
|
+
cwd;
|
|
696
|
+
model;
|
|
697
|
+
constructor(config, opts) {
|
|
698
|
+
this.config = config;
|
|
699
|
+
this.sessionId = opts?.sessionId ?? generateUUID();
|
|
700
|
+
this.cwd = opts?.cwd ?? "/";
|
|
701
|
+
this.model = opts?.model ?? config.model ?? "gpt-4o";
|
|
702
|
+
this.storage = new SessionStorage(config.fs, config.sessionDir);
|
|
703
|
+
this.toolRegistry = new ToolRegistry();
|
|
704
|
+
if (opts?.resume) {
|
|
705
|
+
this.loaded = false;
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
async *run(prompt, opts) {
|
|
709
|
+
this.abortController = new AbortController();
|
|
710
|
+
const signal = opts?.signal ?? this.abortController.signal;
|
|
711
|
+
try {
|
|
712
|
+
if (!this.loaded) {
|
|
713
|
+
this.messages = await this.storage.loadMessages(this.sessionId);
|
|
714
|
+
this.loaded = true;
|
|
715
|
+
}
|
|
716
|
+
const userMessage = { role: "user", content: prompt };
|
|
717
|
+
this.messages.push(userMessage);
|
|
718
|
+
await this.storage.appendMessage(this.sessionId, userMessage);
|
|
719
|
+
const systemPrompt = buildSystemPrompt({
|
|
720
|
+
customPrompt: this.config.systemPrompt,
|
|
721
|
+
skills: this.config.skills,
|
|
722
|
+
tools: this.toolRegistry.listTools()
|
|
723
|
+
});
|
|
724
|
+
const toolDefs = this.toolRegistry.toToolDefinitions();
|
|
725
|
+
const toolCtx = {
|
|
726
|
+
fs: this.config.fs,
|
|
727
|
+
computer: this.config.computer,
|
|
728
|
+
cwd: this.cwd
|
|
729
|
+
};
|
|
730
|
+
while (!signal.aborted) {
|
|
731
|
+
const accumulatedContent = [];
|
|
732
|
+
const accumulatedToolCalls = /* @__PURE__ */ new Map();
|
|
733
|
+
let finishReason = null;
|
|
734
|
+
const stream = this.config.aiProvider.chat({
|
|
735
|
+
model: this.model,
|
|
736
|
+
messages: this.messages,
|
|
737
|
+
tools: toolDefs,
|
|
738
|
+
system: systemPrompt,
|
|
739
|
+
max_tokens: this.config.maxTokens
|
|
740
|
+
});
|
|
741
|
+
for await (const chunk of stream) {
|
|
742
|
+
if (signal.aborted) break;
|
|
743
|
+
for (const choice of chunk.choices) {
|
|
744
|
+
if (choice.finish_reason) {
|
|
745
|
+
finishReason = choice.finish_reason;
|
|
746
|
+
}
|
|
747
|
+
const delta = choice.delta;
|
|
748
|
+
if (delta.content) {
|
|
749
|
+
accumulatedContent.push(delta.content);
|
|
750
|
+
yield { type: "text_delta", text: delta.content };
|
|
751
|
+
}
|
|
752
|
+
if (delta.tool_calls) {
|
|
753
|
+
for (const tc of delta.tool_calls) {
|
|
754
|
+
const existing = accumulatedToolCalls.get(tc.index);
|
|
755
|
+
if (!existing) {
|
|
756
|
+
const id = tc.id ?? "";
|
|
757
|
+
const name = tc.function?.name ?? "";
|
|
758
|
+
accumulatedToolCalls.set(tc.index, {
|
|
759
|
+
id,
|
|
760
|
+
name,
|
|
761
|
+
arguments: tc.function?.arguments ?? ""
|
|
762
|
+
});
|
|
763
|
+
if (tc.id && tc.function?.name) {
|
|
764
|
+
yield {
|
|
765
|
+
type: "tool_use_start",
|
|
766
|
+
toolName: name,
|
|
767
|
+
toolUseId: id
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
} else {
|
|
771
|
+
if (tc.id) existing.id = tc.id;
|
|
772
|
+
if (tc.function?.name) existing.name = tc.function.name;
|
|
773
|
+
if (tc.function?.arguments) {
|
|
774
|
+
existing.arguments += tc.function.arguments;
|
|
775
|
+
yield {
|
|
776
|
+
type: "tool_use_delta",
|
|
777
|
+
input: tc.function.arguments
|
|
778
|
+
};
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
if (signal.aborted) break;
|
|
786
|
+
const textContent = accumulatedContent.join("");
|
|
787
|
+
const toolCalls = Array.from(
|
|
788
|
+
accumulatedToolCalls.values()
|
|
789
|
+
).map((tc) => ({
|
|
790
|
+
id: tc.id,
|
|
791
|
+
type: "function",
|
|
792
|
+
function: {
|
|
793
|
+
name: tc.name,
|
|
794
|
+
arguments: tc.arguments
|
|
795
|
+
}
|
|
796
|
+
}));
|
|
797
|
+
const assistantMsg = {
|
|
798
|
+
role: "assistant",
|
|
799
|
+
content: textContent || null,
|
|
800
|
+
...toolCalls.length > 0 ? { tool_calls: toolCalls } : {}
|
|
801
|
+
};
|
|
802
|
+
this.messages.push(assistantMsg);
|
|
803
|
+
await this.storage.appendMessage(this.sessionId, assistantMsg);
|
|
804
|
+
if (toolCalls.length > 0 && (finishReason === "tool_calls" || finishReason === "stop" || !finishReason)) {
|
|
805
|
+
for (const tc of toolCalls) {
|
|
806
|
+
let parsedArgs = {};
|
|
807
|
+
try {
|
|
808
|
+
parsedArgs = JSON.parse(tc.function.arguments);
|
|
809
|
+
} catch {
|
|
810
|
+
}
|
|
811
|
+
const result = await this.toolRegistry.execute(
|
|
812
|
+
tc.function.name,
|
|
813
|
+
parsedArgs,
|
|
814
|
+
toolCtx
|
|
815
|
+
);
|
|
816
|
+
yield {
|
|
817
|
+
type: "tool_result",
|
|
818
|
+
toolUseId: tc.id,
|
|
819
|
+
toolName: tc.function.name,
|
|
820
|
+
result
|
|
821
|
+
};
|
|
822
|
+
const toolResultMsg = {
|
|
823
|
+
role: "tool",
|
|
824
|
+
tool_call_id: tc.id,
|
|
825
|
+
content: result.content
|
|
826
|
+
};
|
|
827
|
+
this.messages.push(toolResultMsg);
|
|
828
|
+
await this.storage.appendMessage(this.sessionId, toolResultMsg);
|
|
829
|
+
}
|
|
830
|
+
continue;
|
|
831
|
+
}
|
|
832
|
+
yield { type: "message_complete", message: assistantMsg };
|
|
833
|
+
break;
|
|
834
|
+
}
|
|
835
|
+
const autoCompactConfig = this.config.autoCompact ?? createAutoCompactConfig();
|
|
836
|
+
if (shouldAutoCompact(this.messages, autoCompactConfig)) {
|
|
837
|
+
yield { type: "compact_start" };
|
|
838
|
+
try {
|
|
839
|
+
this.messages = await compactConversation(
|
|
840
|
+
this.config.aiProvider,
|
|
841
|
+
this.model,
|
|
842
|
+
this.messages,
|
|
843
|
+
this.storage,
|
|
844
|
+
this.sessionId
|
|
845
|
+
);
|
|
846
|
+
yield { type: "compact_complete" };
|
|
847
|
+
} catch (err) {
|
|
848
|
+
yield {
|
|
849
|
+
type: "error",
|
|
850
|
+
error: err instanceof Error ? err : new Error(`Compaction failed: ${String(err)}`)
|
|
851
|
+
};
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
} catch (err) {
|
|
855
|
+
if (!signal.aborted) {
|
|
856
|
+
yield {
|
|
857
|
+
type: "error",
|
|
858
|
+
error: err instanceof Error ? err : new Error(String(err))
|
|
859
|
+
};
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
async getMessages() {
|
|
864
|
+
if (!this.loaded) {
|
|
865
|
+
this.messages = await this.storage.loadMessages(this.sessionId);
|
|
866
|
+
this.loaded = true;
|
|
867
|
+
}
|
|
868
|
+
return [...this.messages];
|
|
869
|
+
}
|
|
870
|
+
async compact(opts) {
|
|
871
|
+
if (!this.loaded) {
|
|
872
|
+
this.messages = await this.storage.loadMessages(this.sessionId);
|
|
873
|
+
this.loaded = true;
|
|
874
|
+
}
|
|
875
|
+
this.messages = await compactConversation(
|
|
876
|
+
this.config.aiProvider,
|
|
877
|
+
this.model,
|
|
878
|
+
this.messages,
|
|
879
|
+
this.storage,
|
|
880
|
+
this.sessionId,
|
|
881
|
+
{ customInstructions: opts?.instructions }
|
|
882
|
+
);
|
|
883
|
+
}
|
|
884
|
+
abort() {
|
|
885
|
+
this.abortController?.abort();
|
|
886
|
+
}
|
|
887
|
+
};
|
|
888
|
+
|
|
889
|
+
// src/skills/loader.ts
|
|
890
|
+
async function loadSkills(fs2, paths) {
|
|
891
|
+
const skills = [];
|
|
892
|
+
for (const skillPath of paths) {
|
|
893
|
+
try {
|
|
894
|
+
const stat2 = await fs2.stat(skillPath);
|
|
895
|
+
if (stat2.isFile) {
|
|
896
|
+
const skill = await loadSkillFile(fs2, skillPath);
|
|
897
|
+
if (skill) skills.push(skill);
|
|
898
|
+
} else if (stat2.isDirectory) {
|
|
899
|
+
const dirSkills = await loadSkillsFromDir(fs2, skillPath);
|
|
900
|
+
skills.push(...dirSkills);
|
|
901
|
+
}
|
|
902
|
+
} catch {
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
return skills;
|
|
906
|
+
}
|
|
907
|
+
async function loadSkillFile(fs2, filePath) {
|
|
908
|
+
try {
|
|
909
|
+
const content = await fs2.readFile(filePath);
|
|
910
|
+
const name = extractSkillName(filePath, content);
|
|
911
|
+
const description = extractDescription(content);
|
|
912
|
+
return {
|
|
913
|
+
name,
|
|
914
|
+
content,
|
|
915
|
+
path: filePath,
|
|
916
|
+
description
|
|
917
|
+
};
|
|
918
|
+
} catch {
|
|
919
|
+
return null;
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
async function loadSkillsFromDir(fs2, dirPath) {
|
|
923
|
+
const skills = [];
|
|
924
|
+
try {
|
|
925
|
+
const entries = await fs2.readdir(dirPath);
|
|
926
|
+
for (const entry of entries) {
|
|
927
|
+
if (entry.isFile && (entry.name === "SKILL.md" || entry.name.endsWith(".md"))) {
|
|
928
|
+
const skill = await loadSkillFile(fs2, entry.path);
|
|
929
|
+
if (skill) skills.push(skill);
|
|
930
|
+
} else if (entry.isDirectory) {
|
|
931
|
+
const skillMdPath = `${entry.path}/SKILL.md`;
|
|
932
|
+
const skill = await loadSkillFile(fs2, skillMdPath);
|
|
933
|
+
if (skill) skills.push(skill);
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
} catch {
|
|
937
|
+
}
|
|
938
|
+
return skills;
|
|
939
|
+
}
|
|
940
|
+
function extractSkillName(filePath, content) {
|
|
941
|
+
const h1Match = content.match(/^#\s+(.+)$/m);
|
|
942
|
+
if (h1Match) return h1Match[1].trim();
|
|
943
|
+
const parts = filePath.split("/");
|
|
944
|
+
const fileName = parts[parts.length - 1];
|
|
945
|
+
if (fileName === "SKILL.md" && parts.length >= 2) {
|
|
946
|
+
return parts[parts.length - 2];
|
|
947
|
+
}
|
|
948
|
+
return fileName.replace(/\.md$/, "");
|
|
949
|
+
}
|
|
950
|
+
function extractDescription(content) {
|
|
951
|
+
const lines = content.split("\n");
|
|
952
|
+
for (const line of lines) {
|
|
953
|
+
const trimmed = line.trim();
|
|
954
|
+
if (trimmed && !trimmed.startsWith("#") && !trimmed.startsWith("---")) {
|
|
955
|
+
return trimmed.slice(0, 200);
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
return void 0;
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
// src/prompt/context.ts
|
|
962
|
+
async function buildUserContext(opts) {
|
|
963
|
+
let skills = [];
|
|
964
|
+
if (opts.skillsPaths && opts.skillsPaths.length > 0) {
|
|
965
|
+
const loaded = await loadSkills(opts.fs, opts.skillsPaths);
|
|
966
|
+
skills.push(...loaded);
|
|
967
|
+
}
|
|
968
|
+
if (opts.inlineSkills) {
|
|
969
|
+
skills.push(...opts.inlineSkills);
|
|
970
|
+
}
|
|
971
|
+
const date = (/* @__PURE__ */ new Date()).toLocaleDateString("en-US", {
|
|
972
|
+
weekday: "long",
|
|
973
|
+
year: "numeric",
|
|
974
|
+
month: "long",
|
|
975
|
+
day: "numeric"
|
|
976
|
+
});
|
|
977
|
+
return { skills, date };
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
// src/code.ts
|
|
981
|
+
var Code = class {
|
|
982
|
+
aiProvider;
|
|
983
|
+
fs;
|
|
984
|
+
computer;
|
|
985
|
+
sessionDir;
|
|
986
|
+
skills;
|
|
987
|
+
skillsPaths;
|
|
988
|
+
systemPrompt;
|
|
989
|
+
model;
|
|
990
|
+
maxTokens;
|
|
991
|
+
autoCompactEnabled;
|
|
992
|
+
autoCompactThreshold;
|
|
993
|
+
cwd;
|
|
994
|
+
storage;
|
|
995
|
+
resolvedSkills = null;
|
|
996
|
+
constructor(opts) {
|
|
997
|
+
this.aiProvider = opts.aiProvider;
|
|
998
|
+
this.fs = opts.virtualFs;
|
|
999
|
+
this.computer = opts.virtualComputer;
|
|
1000
|
+
this.sessionDir = opts.options?.sessionDir ?? ".noumen/sessions";
|
|
1001
|
+
this.skills = opts.options?.skills ?? [];
|
|
1002
|
+
this.skillsPaths = opts.options?.skillsPaths ?? [];
|
|
1003
|
+
this.systemPrompt = opts.options?.systemPrompt;
|
|
1004
|
+
this.model = opts.options?.model;
|
|
1005
|
+
this.maxTokens = opts.options?.maxTokens;
|
|
1006
|
+
this.autoCompactEnabled = opts.options?.autoCompact ?? true;
|
|
1007
|
+
this.autoCompactThreshold = opts.options?.autoCompactThreshold;
|
|
1008
|
+
this.cwd = opts.options?.cwd ?? "/";
|
|
1009
|
+
this.storage = new SessionStorage(this.fs, this.sessionDir);
|
|
1010
|
+
}
|
|
1011
|
+
async getSkills() {
|
|
1012
|
+
if (this.resolvedSkills) return this.resolvedSkills;
|
|
1013
|
+
const ctx = await buildUserContext({
|
|
1014
|
+
fs: this.fs,
|
|
1015
|
+
skillsPaths: this.skillsPaths,
|
|
1016
|
+
inlineSkills: this.skills
|
|
1017
|
+
});
|
|
1018
|
+
this.resolvedSkills = ctx.skills;
|
|
1019
|
+
return this.resolvedSkills;
|
|
1020
|
+
}
|
|
1021
|
+
createThread(opts) {
|
|
1022
|
+
const autoCompact = createAutoCompactConfig({
|
|
1023
|
+
enabled: this.autoCompactEnabled,
|
|
1024
|
+
threshold: this.autoCompactThreshold
|
|
1025
|
+
});
|
|
1026
|
+
const skills = this.resolvedSkills ?? this.skills;
|
|
1027
|
+
return new Thread(
|
|
1028
|
+
{
|
|
1029
|
+
aiProvider: this.aiProvider,
|
|
1030
|
+
fs: this.fs,
|
|
1031
|
+
computer: this.computer,
|
|
1032
|
+
sessionDir: this.sessionDir,
|
|
1033
|
+
skills,
|
|
1034
|
+
systemPrompt: this.systemPrompt,
|
|
1035
|
+
model: this.model,
|
|
1036
|
+
maxTokens: this.maxTokens,
|
|
1037
|
+
autoCompact
|
|
1038
|
+
},
|
|
1039
|
+
{
|
|
1040
|
+
...opts,
|
|
1041
|
+
cwd: opts?.cwd ?? this.cwd
|
|
1042
|
+
}
|
|
1043
|
+
);
|
|
1044
|
+
}
|
|
1045
|
+
async listSessions() {
|
|
1046
|
+
return this.storage.listSessions();
|
|
1047
|
+
}
|
|
1048
|
+
/**
|
|
1049
|
+
* Pre-resolve skills from paths. Call this once after construction if using
|
|
1050
|
+
* skillsPaths, so that createThread() has skills available synchronously.
|
|
1051
|
+
*/
|
|
1052
|
+
async init() {
|
|
1053
|
+
await this.getSkills();
|
|
1054
|
+
}
|
|
1055
|
+
};
|
|
1056
|
+
|
|
1057
|
+
// src/providers/openai.ts
|
|
1058
|
+
import OpenAI from "openai";
|
|
1059
|
+
var OpenAIProvider = class {
|
|
1060
|
+
client;
|
|
1061
|
+
defaultModel;
|
|
1062
|
+
constructor(opts) {
|
|
1063
|
+
this.client = new OpenAI({
|
|
1064
|
+
apiKey: opts.apiKey,
|
|
1065
|
+
baseURL: opts.baseURL
|
|
1066
|
+
});
|
|
1067
|
+
this.defaultModel = opts.model ?? "gpt-4o";
|
|
1068
|
+
}
|
|
1069
|
+
async *chat(params) {
|
|
1070
|
+
const messages = this.buildMessages(params.system, params.messages);
|
|
1071
|
+
const stream = await this.client.chat.completions.create({
|
|
1072
|
+
model: params.model ?? this.defaultModel,
|
|
1073
|
+
messages,
|
|
1074
|
+
tools: params.tools?.map((t) => ({
|
|
1075
|
+
type: "function",
|
|
1076
|
+
function: t.function
|
|
1077
|
+
})),
|
|
1078
|
+
max_tokens: params.max_tokens,
|
|
1079
|
+
temperature: params.temperature,
|
|
1080
|
+
stream: true
|
|
1081
|
+
});
|
|
1082
|
+
for await (const chunk of stream) {
|
|
1083
|
+
yield {
|
|
1084
|
+
id: chunk.id,
|
|
1085
|
+
model: chunk.model,
|
|
1086
|
+
choices: chunk.choices.map((c) => ({
|
|
1087
|
+
index: c.index,
|
|
1088
|
+
delta: {
|
|
1089
|
+
role: c.delta.role,
|
|
1090
|
+
content: c.delta.content,
|
|
1091
|
+
tool_calls: c.delta.tool_calls?.map((tc) => ({
|
|
1092
|
+
index: tc.index,
|
|
1093
|
+
id: tc.id,
|
|
1094
|
+
type: tc.type,
|
|
1095
|
+
function: tc.function ? {
|
|
1096
|
+
name: tc.function.name,
|
|
1097
|
+
arguments: tc.function.arguments
|
|
1098
|
+
} : void 0
|
|
1099
|
+
}))
|
|
1100
|
+
},
|
|
1101
|
+
finish_reason: c.finish_reason
|
|
1102
|
+
})),
|
|
1103
|
+
usage: chunk.usage ? {
|
|
1104
|
+
prompt_tokens: chunk.usage.prompt_tokens,
|
|
1105
|
+
completion_tokens: chunk.usage.completion_tokens,
|
|
1106
|
+
total_tokens: chunk.usage.total_tokens
|
|
1107
|
+
} : void 0
|
|
1108
|
+
};
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
buildMessages(system, messages) {
|
|
1112
|
+
const result = [];
|
|
1113
|
+
if (system) {
|
|
1114
|
+
result.push({ role: "system", content: system });
|
|
1115
|
+
}
|
|
1116
|
+
for (const msg of messages) {
|
|
1117
|
+
if (msg.role === "tool") {
|
|
1118
|
+
result.push({
|
|
1119
|
+
role: "tool",
|
|
1120
|
+
tool_call_id: msg.tool_call_id,
|
|
1121
|
+
content: msg.content
|
|
1122
|
+
});
|
|
1123
|
+
} else if (msg.role === "assistant") {
|
|
1124
|
+
const entry = {
|
|
1125
|
+
role: "assistant",
|
|
1126
|
+
content: msg.content
|
|
1127
|
+
};
|
|
1128
|
+
if (msg.tool_calls) {
|
|
1129
|
+
entry.tool_calls = msg.tool_calls;
|
|
1130
|
+
}
|
|
1131
|
+
result.push(entry);
|
|
1132
|
+
} else {
|
|
1133
|
+
result.push({ role: msg.role, content: msg.content });
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
return result;
|
|
1137
|
+
}
|
|
1138
|
+
};
|
|
1139
|
+
|
|
1140
|
+
// src/providers/anthropic.ts
|
|
1141
|
+
import Anthropic from "@anthropic-ai/sdk";
|
|
1142
|
+
var AnthropicProvider = class {
|
|
1143
|
+
client;
|
|
1144
|
+
defaultModel;
|
|
1145
|
+
constructor(opts) {
|
|
1146
|
+
this.client = new Anthropic({
|
|
1147
|
+
apiKey: opts.apiKey,
|
|
1148
|
+
baseURL: opts.baseURL
|
|
1149
|
+
});
|
|
1150
|
+
this.defaultModel = opts.model ?? "claude-sonnet-4-20250514";
|
|
1151
|
+
}
|
|
1152
|
+
async *chat(params) {
|
|
1153
|
+
const { system, messages: inputMessages } = this.convertMessages(
|
|
1154
|
+
params.system,
|
|
1155
|
+
params.messages
|
|
1156
|
+
);
|
|
1157
|
+
const tools = params.tools?.map((t) => ({
|
|
1158
|
+
name: t.function.name,
|
|
1159
|
+
description: t.function.description,
|
|
1160
|
+
input_schema: t.function.parameters
|
|
1161
|
+
}));
|
|
1162
|
+
const stream = this.client.messages.stream({
|
|
1163
|
+
model: params.model ?? this.defaultModel,
|
|
1164
|
+
max_tokens: params.max_tokens ?? 8192,
|
|
1165
|
+
system,
|
|
1166
|
+
messages: inputMessages,
|
|
1167
|
+
tools
|
|
1168
|
+
});
|
|
1169
|
+
let chunkIndex = 0;
|
|
1170
|
+
const toolIndexMap = /* @__PURE__ */ new Map();
|
|
1171
|
+
let nextToolIndex = 0;
|
|
1172
|
+
for await (const event of stream) {
|
|
1173
|
+
const chunkId = `chatcmpl-${chunkIndex++}`;
|
|
1174
|
+
if (event.type === "content_block_start") {
|
|
1175
|
+
if (event.content_block.type === "text") {
|
|
1176
|
+
yield this.makeChunk(chunkId, params.model ?? this.defaultModel, {
|
|
1177
|
+
content: ""
|
|
1178
|
+
});
|
|
1179
|
+
} else if (event.content_block.type === "tool_use") {
|
|
1180
|
+
const block = event.content_block;
|
|
1181
|
+
const idx = nextToolIndex++;
|
|
1182
|
+
toolIndexMap.set(block.id, idx);
|
|
1183
|
+
yield this.makeChunk(chunkId, params.model ?? this.defaultModel, {
|
|
1184
|
+
tool_calls: [
|
|
1185
|
+
{
|
|
1186
|
+
index: idx,
|
|
1187
|
+
id: block.id,
|
|
1188
|
+
type: "function",
|
|
1189
|
+
function: { name: block.name, arguments: "" }
|
|
1190
|
+
}
|
|
1191
|
+
]
|
|
1192
|
+
});
|
|
1193
|
+
}
|
|
1194
|
+
} else if (event.type === "content_block_delta") {
|
|
1195
|
+
if (event.delta.type === "text_delta") {
|
|
1196
|
+
yield this.makeChunk(chunkId, params.model ?? this.defaultModel, {
|
|
1197
|
+
content: event.delta.text
|
|
1198
|
+
});
|
|
1199
|
+
} else if (event.delta.type === "input_json_delta") {
|
|
1200
|
+
const delta = event.delta;
|
|
1201
|
+
const lastToolId = Array.from(toolIndexMap.keys()).pop();
|
|
1202
|
+
const idx = toolIndexMap.get(lastToolId);
|
|
1203
|
+
yield this.makeChunk(chunkId, params.model ?? this.defaultModel, {
|
|
1204
|
+
tool_calls: [
|
|
1205
|
+
{
|
|
1206
|
+
index: idx,
|
|
1207
|
+
function: { arguments: delta.partial_json }
|
|
1208
|
+
}
|
|
1209
|
+
]
|
|
1210
|
+
});
|
|
1211
|
+
}
|
|
1212
|
+
} else if (event.type === "message_stop") {
|
|
1213
|
+
yield {
|
|
1214
|
+
id: chunkId,
|
|
1215
|
+
model: params.model ?? this.defaultModel,
|
|
1216
|
+
choices: [
|
|
1217
|
+
{
|
|
1218
|
+
index: 0,
|
|
1219
|
+
delta: {},
|
|
1220
|
+
finish_reason: toolIndexMap.size > 0 ? "tool_calls" : "stop"
|
|
1221
|
+
}
|
|
1222
|
+
]
|
|
1223
|
+
};
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
makeChunk(id, model, delta) {
|
|
1228
|
+
return {
|
|
1229
|
+
id,
|
|
1230
|
+
model,
|
|
1231
|
+
choices: [
|
|
1232
|
+
{
|
|
1233
|
+
index: 0,
|
|
1234
|
+
delta,
|
|
1235
|
+
finish_reason: null
|
|
1236
|
+
}
|
|
1237
|
+
]
|
|
1238
|
+
};
|
|
1239
|
+
}
|
|
1240
|
+
convertMessages(systemPrompt, messages) {
|
|
1241
|
+
const result = [];
|
|
1242
|
+
for (const msg of messages) {
|
|
1243
|
+
if (msg.role === "system") {
|
|
1244
|
+
continue;
|
|
1245
|
+
}
|
|
1246
|
+
if (msg.role === "user") {
|
|
1247
|
+
result.push({ role: "user", content: msg.content });
|
|
1248
|
+
} else if (msg.role === "assistant") {
|
|
1249
|
+
const content = [];
|
|
1250
|
+
if (msg.content) {
|
|
1251
|
+
content.push({ type: "text", text: msg.content });
|
|
1252
|
+
}
|
|
1253
|
+
if (msg.tool_calls) {
|
|
1254
|
+
for (const tc of msg.tool_calls) {
|
|
1255
|
+
content.push({
|
|
1256
|
+
type: "tool_use",
|
|
1257
|
+
id: tc.id,
|
|
1258
|
+
name: tc.function.name,
|
|
1259
|
+
input: JSON.parse(tc.function.arguments)
|
|
1260
|
+
});
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
result.push({ role: "assistant", content });
|
|
1264
|
+
} else if (msg.role === "tool") {
|
|
1265
|
+
result.push({
|
|
1266
|
+
role: "user",
|
|
1267
|
+
content: [
|
|
1268
|
+
{
|
|
1269
|
+
type: "tool_result",
|
|
1270
|
+
tool_use_id: msg.tool_call_id,
|
|
1271
|
+
content: msg.content
|
|
1272
|
+
}
|
|
1273
|
+
]
|
|
1274
|
+
});
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
return { system: systemPrompt, messages: result };
|
|
1278
|
+
}
|
|
1279
|
+
};
|
|
1280
|
+
|
|
1281
|
+
// src/providers/gemini.ts
|
|
1282
|
+
import { GoogleGenAI } from "@google/genai";
|
|
1283
|
+
var GeminiProvider = class {
|
|
1284
|
+
client;
|
|
1285
|
+
defaultModel;
|
|
1286
|
+
constructor(opts) {
|
|
1287
|
+
this.client = new GoogleGenAI({ apiKey: opts.apiKey });
|
|
1288
|
+
this.defaultModel = opts.model ?? "gemini-2.5-flash";
|
|
1289
|
+
}
|
|
1290
|
+
async *chat(params) {
|
|
1291
|
+
const { contents, systemInstruction } = this.convertMessages(
|
|
1292
|
+
params.system,
|
|
1293
|
+
params.messages
|
|
1294
|
+
);
|
|
1295
|
+
const tools = params.tools?.length ? [
|
|
1296
|
+
{
|
|
1297
|
+
functionDeclarations: params.tools.map((t) => ({
|
|
1298
|
+
name: t.function.name,
|
|
1299
|
+
description: t.function.description,
|
|
1300
|
+
parameters: t.function.parameters
|
|
1301
|
+
}))
|
|
1302
|
+
}
|
|
1303
|
+
] : void 0;
|
|
1304
|
+
const stream = await this.client.models.generateContentStream({
|
|
1305
|
+
model: params.model ?? this.defaultModel,
|
|
1306
|
+
contents,
|
|
1307
|
+
config: {
|
|
1308
|
+
systemInstruction: systemInstruction || void 0,
|
|
1309
|
+
maxOutputTokens: params.max_tokens,
|
|
1310
|
+
temperature: params.temperature,
|
|
1311
|
+
tools,
|
|
1312
|
+
thinkingConfig: {
|
|
1313
|
+
thinkingBudget: 0
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
});
|
|
1317
|
+
let chunkIndex = 0;
|
|
1318
|
+
let toolCallIndex = 0;
|
|
1319
|
+
for await (const chunk of stream) {
|
|
1320
|
+
const chunkId = `gemini-${chunkIndex++}`;
|
|
1321
|
+
const model = params.model ?? this.defaultModel;
|
|
1322
|
+
const candidates = chunk.candidates;
|
|
1323
|
+
if (!candidates || candidates.length === 0) continue;
|
|
1324
|
+
const parts = candidates[0].content?.parts;
|
|
1325
|
+
if (!parts) continue;
|
|
1326
|
+
for (const part of parts) {
|
|
1327
|
+
if (part.text !== void 0 && part.text !== null) {
|
|
1328
|
+
yield {
|
|
1329
|
+
id: chunkId,
|
|
1330
|
+
model,
|
|
1331
|
+
choices: [
|
|
1332
|
+
{
|
|
1333
|
+
index: 0,
|
|
1334
|
+
delta: { content: part.text },
|
|
1335
|
+
finish_reason: null
|
|
1336
|
+
}
|
|
1337
|
+
]
|
|
1338
|
+
};
|
|
1339
|
+
}
|
|
1340
|
+
if (part.functionCall) {
|
|
1341
|
+
const fc = part.functionCall;
|
|
1342
|
+
const tcId = `gemini-tc-${toolCallIndex}`;
|
|
1343
|
+
const idx = toolCallIndex++;
|
|
1344
|
+
yield {
|
|
1345
|
+
id: chunkId,
|
|
1346
|
+
model,
|
|
1347
|
+
choices: [
|
|
1348
|
+
{
|
|
1349
|
+
index: 0,
|
|
1350
|
+
delta: {
|
|
1351
|
+
tool_calls: [
|
|
1352
|
+
{
|
|
1353
|
+
index: idx,
|
|
1354
|
+
id: tcId,
|
|
1355
|
+
type: "function",
|
|
1356
|
+
function: {
|
|
1357
|
+
name: fc.name,
|
|
1358
|
+
arguments: JSON.stringify(fc.args ?? {})
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
]
|
|
1362
|
+
},
|
|
1363
|
+
finish_reason: null
|
|
1364
|
+
}
|
|
1365
|
+
]
|
|
1366
|
+
};
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
const finishReason = candidates[0].finishReason;
|
|
1370
|
+
if (finishReason && finishReason !== "FINISH_REASON_UNSPECIFIED") {
|
|
1371
|
+
const mapped = finishReason === "STOP" ? toolCallIndex > 0 ? "tool_calls" : "stop" : "stop";
|
|
1372
|
+
yield {
|
|
1373
|
+
id: chunkId,
|
|
1374
|
+
model,
|
|
1375
|
+
choices: [{ index: 0, delta: {}, finish_reason: mapped }]
|
|
1376
|
+
};
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
convertMessages(systemPrompt, messages) {
|
|
1381
|
+
const contents = [];
|
|
1382
|
+
const toolCallIdToName = /* @__PURE__ */ new Map();
|
|
1383
|
+
for (const msg of messages) {
|
|
1384
|
+
if (msg.role === "assistant" && msg.tool_calls) {
|
|
1385
|
+
for (const tc of msg.tool_calls) {
|
|
1386
|
+
toolCallIdToName.set(tc.id, tc.function.name);
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
let pendingFunctionResponses = [];
|
|
1391
|
+
for (const msg of messages) {
|
|
1392
|
+
if (msg.role === "system") {
|
|
1393
|
+
continue;
|
|
1394
|
+
}
|
|
1395
|
+
if (msg.role === "user") {
|
|
1396
|
+
if (pendingFunctionResponses.length > 0) {
|
|
1397
|
+
contents.push({ role: "user", parts: pendingFunctionResponses });
|
|
1398
|
+
pendingFunctionResponses = [];
|
|
1399
|
+
}
|
|
1400
|
+
contents.push({ role: "user", parts: [{ text: msg.content }] });
|
|
1401
|
+
} else if (msg.role === "assistant") {
|
|
1402
|
+
const parts = [];
|
|
1403
|
+
if (msg.content) {
|
|
1404
|
+
parts.push({ text: msg.content });
|
|
1405
|
+
}
|
|
1406
|
+
if (msg.tool_calls) {
|
|
1407
|
+
for (const tc of msg.tool_calls) {
|
|
1408
|
+
let args = {};
|
|
1409
|
+
try {
|
|
1410
|
+
args = JSON.parse(tc.function.arguments);
|
|
1411
|
+
} catch {
|
|
1412
|
+
}
|
|
1413
|
+
parts.push({
|
|
1414
|
+
functionCall: { name: tc.function.name, args }
|
|
1415
|
+
});
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
if (parts.length > 0) {
|
|
1419
|
+
contents.push({ role: "model", parts });
|
|
1420
|
+
}
|
|
1421
|
+
} else if (msg.role === "tool") {
|
|
1422
|
+
const fnName = toolCallIdToName.get(msg.tool_call_id) ?? msg.tool_call_id;
|
|
1423
|
+
pendingFunctionResponses.push({
|
|
1424
|
+
functionResponse: {
|
|
1425
|
+
name: fnName,
|
|
1426
|
+
response: { result: msg.content }
|
|
1427
|
+
}
|
|
1428
|
+
});
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
if (pendingFunctionResponses.length > 0) {
|
|
1432
|
+
contents.push({ role: "user", parts: pendingFunctionResponses });
|
|
1433
|
+
}
|
|
1434
|
+
return { contents, systemInstruction: systemPrompt };
|
|
1435
|
+
}
|
|
1436
|
+
};
|
|
1437
|
+
|
|
1438
|
+
// src/virtual/local-fs.ts
|
|
1439
|
+
import * as fs from "fs/promises";
|
|
1440
|
+
import * as path from "path";
|
|
1441
|
+
var LocalFs = class {
|
|
1442
|
+
basePath;
|
|
1443
|
+
constructor(opts) {
|
|
1444
|
+
this.basePath = opts?.basePath ?? process.cwd();
|
|
1445
|
+
}
|
|
1446
|
+
resolve(p) {
|
|
1447
|
+
if (path.isAbsolute(p)) return p;
|
|
1448
|
+
return path.resolve(this.basePath, p);
|
|
1449
|
+
}
|
|
1450
|
+
async readFile(filePath, opts) {
|
|
1451
|
+
const encoding = opts?.encoding ?? "utf-8";
|
|
1452
|
+
return fs.readFile(this.resolve(filePath), { encoding });
|
|
1453
|
+
}
|
|
1454
|
+
async writeFile(filePath, content) {
|
|
1455
|
+
const resolved = this.resolve(filePath);
|
|
1456
|
+
await fs.mkdir(path.dirname(resolved), { recursive: true });
|
|
1457
|
+
await fs.writeFile(resolved, content, "utf-8");
|
|
1458
|
+
}
|
|
1459
|
+
async appendFile(filePath, content) {
|
|
1460
|
+
const resolved = this.resolve(filePath);
|
|
1461
|
+
await fs.mkdir(path.dirname(resolved), { recursive: true });
|
|
1462
|
+
await fs.appendFile(resolved, content, "utf-8");
|
|
1463
|
+
}
|
|
1464
|
+
async deleteFile(filePath, opts) {
|
|
1465
|
+
await fs.rm(this.resolve(filePath), {
|
|
1466
|
+
recursive: opts?.recursive ?? false,
|
|
1467
|
+
force: true
|
|
1468
|
+
});
|
|
1469
|
+
}
|
|
1470
|
+
async mkdir(dirPath, opts) {
|
|
1471
|
+
await fs.mkdir(this.resolve(dirPath), {
|
|
1472
|
+
recursive: opts?.recursive ?? false
|
|
1473
|
+
});
|
|
1474
|
+
}
|
|
1475
|
+
async readdir(dirPath, opts) {
|
|
1476
|
+
const resolved = this.resolve(dirPath);
|
|
1477
|
+
const entries = await fs.readdir(resolved, { withFileTypes: true });
|
|
1478
|
+
const results = [];
|
|
1479
|
+
for (const entry of entries) {
|
|
1480
|
+
const entryPath = path.join(resolved, entry.name);
|
|
1481
|
+
results.push({
|
|
1482
|
+
name: entry.name,
|
|
1483
|
+
path: entryPath,
|
|
1484
|
+
isDirectory: entry.isDirectory(),
|
|
1485
|
+
isFile: entry.isFile()
|
|
1486
|
+
});
|
|
1487
|
+
if (opts?.recursive && entry.isDirectory()) {
|
|
1488
|
+
const subEntries = await this.readdir(entryPath, { recursive: true });
|
|
1489
|
+
results.push(...subEntries);
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
return results;
|
|
1493
|
+
}
|
|
1494
|
+
async exists(filePath) {
|
|
1495
|
+
try {
|
|
1496
|
+
await fs.access(this.resolve(filePath));
|
|
1497
|
+
return true;
|
|
1498
|
+
} catch {
|
|
1499
|
+
return false;
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
async stat(filePath) {
|
|
1503
|
+
const stats = await fs.stat(this.resolve(filePath));
|
|
1504
|
+
return {
|
|
1505
|
+
size: stats.size,
|
|
1506
|
+
isDirectory: stats.isDirectory(),
|
|
1507
|
+
isFile: stats.isFile(),
|
|
1508
|
+
createdAt: stats.birthtime,
|
|
1509
|
+
modifiedAt: stats.mtime
|
|
1510
|
+
};
|
|
1511
|
+
}
|
|
1512
|
+
};
|
|
1513
|
+
|
|
1514
|
+
// src/virtual/local-computer.ts
|
|
1515
|
+
import { exec as execCb } from "child_process";
|
|
1516
|
+
var LocalComputer = class {
|
|
1517
|
+
defaultCwd;
|
|
1518
|
+
defaultTimeout;
|
|
1519
|
+
constructor(opts) {
|
|
1520
|
+
this.defaultCwd = opts?.defaultCwd ?? process.cwd();
|
|
1521
|
+
this.defaultTimeout = opts?.defaultTimeout ?? 3e4;
|
|
1522
|
+
}
|
|
1523
|
+
executeCommand(command, opts) {
|
|
1524
|
+
return new Promise((resolve2) => {
|
|
1525
|
+
const child = execCb(
|
|
1526
|
+
command,
|
|
1527
|
+
{
|
|
1528
|
+
cwd: opts?.cwd ?? this.defaultCwd,
|
|
1529
|
+
timeout: opts?.timeout ?? this.defaultTimeout,
|
|
1530
|
+
env: opts?.env ? { ...process.env, ...opts.env } : process.env,
|
|
1531
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
1532
|
+
shell: "/bin/bash"
|
|
1533
|
+
},
|
|
1534
|
+
(error, stdout, stderr) => {
|
|
1535
|
+
resolve2({
|
|
1536
|
+
exitCode: error && "code" in error ? error.code ?? 1 : child.exitCode ?? 0,
|
|
1537
|
+
stdout: stdout ?? "",
|
|
1538
|
+
stderr: stderr ?? ""
|
|
1539
|
+
});
|
|
1540
|
+
}
|
|
1541
|
+
);
|
|
1542
|
+
});
|
|
1543
|
+
}
|
|
1544
|
+
};
|
|
1545
|
+
|
|
1546
|
+
// src/virtual/sprites-fs.ts
|
|
1547
|
+
var SpritesFs = class {
|
|
1548
|
+
token;
|
|
1549
|
+
spriteName;
|
|
1550
|
+
baseURL;
|
|
1551
|
+
workingDir;
|
|
1552
|
+
constructor(opts) {
|
|
1553
|
+
this.token = opts.token;
|
|
1554
|
+
this.spriteName = opts.spriteName;
|
|
1555
|
+
this.baseURL = (opts.baseURL ?? "https://api.sprites.dev").replace(
|
|
1556
|
+
/\/$/,
|
|
1557
|
+
""
|
|
1558
|
+
);
|
|
1559
|
+
this.workingDir = opts.workingDir ?? "/home/sprite";
|
|
1560
|
+
}
|
|
1561
|
+
fsUrl(endpoint, params) {
|
|
1562
|
+
const url = new URL(
|
|
1563
|
+
`${this.baseURL}/v1/sprites/${this.spriteName}/fs${endpoint}`
|
|
1564
|
+
);
|
|
1565
|
+
if (params) {
|
|
1566
|
+
for (const [k, v] of Object.entries(params)) {
|
|
1567
|
+
url.searchParams.set(k, v);
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
return url.toString();
|
|
1571
|
+
}
|
|
1572
|
+
resolvePath(p) {
|
|
1573
|
+
if (p.startsWith("/")) return p;
|
|
1574
|
+
return `${this.workingDir}/${p}`;
|
|
1575
|
+
}
|
|
1576
|
+
headers() {
|
|
1577
|
+
return {
|
|
1578
|
+
Authorization: `Bearer ${this.token}`
|
|
1579
|
+
};
|
|
1580
|
+
}
|
|
1581
|
+
async readFile(filePath, _opts) {
|
|
1582
|
+
const url = this.fsUrl("/read", { path: this.resolvePath(filePath) });
|
|
1583
|
+
const res = await fetch(url, { headers: this.headers() });
|
|
1584
|
+
if (!res.ok) {
|
|
1585
|
+
throw new Error(
|
|
1586
|
+
`SpritesFs readFile failed (${res.status}): ${await res.text()}`
|
|
1587
|
+
);
|
|
1588
|
+
}
|
|
1589
|
+
return res.text();
|
|
1590
|
+
}
|
|
1591
|
+
async writeFile(filePath, content) {
|
|
1592
|
+
const url = this.fsUrl("/write");
|
|
1593
|
+
const res = await fetch(url, {
|
|
1594
|
+
method: "POST",
|
|
1595
|
+
headers: {
|
|
1596
|
+
...this.headers(),
|
|
1597
|
+
"Content-Type": "application/json"
|
|
1598
|
+
},
|
|
1599
|
+
body: JSON.stringify({
|
|
1600
|
+
path: this.resolvePath(filePath),
|
|
1601
|
+
content
|
|
1602
|
+
})
|
|
1603
|
+
});
|
|
1604
|
+
if (!res.ok) {
|
|
1605
|
+
throw new Error(
|
|
1606
|
+
`SpritesFs writeFile failed (${res.status}): ${await res.text()}`
|
|
1607
|
+
);
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
async appendFile(filePath, content) {
|
|
1611
|
+
let existing = "";
|
|
1612
|
+
try {
|
|
1613
|
+
existing = await this.readFile(filePath);
|
|
1614
|
+
} catch {
|
|
1615
|
+
}
|
|
1616
|
+
await this.writeFile(filePath, existing + content);
|
|
1617
|
+
}
|
|
1618
|
+
async deleteFile(filePath, opts) {
|
|
1619
|
+
const url = this.fsUrl("/remove");
|
|
1620
|
+
const res = await fetch(url, {
|
|
1621
|
+
method: "POST",
|
|
1622
|
+
headers: {
|
|
1623
|
+
...this.headers(),
|
|
1624
|
+
"Content-Type": "application/json"
|
|
1625
|
+
},
|
|
1626
|
+
body: JSON.stringify({
|
|
1627
|
+
path: this.resolvePath(filePath),
|
|
1628
|
+
recursive: opts?.recursive ?? false
|
|
1629
|
+
})
|
|
1630
|
+
});
|
|
1631
|
+
if (!res.ok) {
|
|
1632
|
+
throw new Error(
|
|
1633
|
+
`SpritesFs deleteFile failed (${res.status}): ${await res.text()}`
|
|
1634
|
+
);
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
async mkdir(dirPath, opts) {
|
|
1638
|
+
const url = this.fsUrl("/mkdir");
|
|
1639
|
+
const res = await fetch(url, {
|
|
1640
|
+
method: "POST",
|
|
1641
|
+
headers: {
|
|
1642
|
+
...this.headers(),
|
|
1643
|
+
"Content-Type": "application/json"
|
|
1644
|
+
},
|
|
1645
|
+
body: JSON.stringify({
|
|
1646
|
+
path: this.resolvePath(dirPath),
|
|
1647
|
+
recursive: opts?.recursive ?? false
|
|
1648
|
+
})
|
|
1649
|
+
});
|
|
1650
|
+
if (!res.ok) {
|
|
1651
|
+
throw new Error(
|
|
1652
|
+
`SpritesFs mkdir failed (${res.status}): ${await res.text()}`
|
|
1653
|
+
);
|
|
1654
|
+
}
|
|
1655
|
+
}
|
|
1656
|
+
async readdir(dirPath, _opts) {
|
|
1657
|
+
const url = this.fsUrl("/readdir", { path: this.resolvePath(dirPath) });
|
|
1658
|
+
const res = await fetch(url, { headers: this.headers() });
|
|
1659
|
+
if (!res.ok) {
|
|
1660
|
+
throw new Error(
|
|
1661
|
+
`SpritesFs readdir failed (${res.status}): ${await res.text()}`
|
|
1662
|
+
);
|
|
1663
|
+
}
|
|
1664
|
+
const data = await res.json();
|
|
1665
|
+
return data.map((entry) => ({
|
|
1666
|
+
name: entry.name,
|
|
1667
|
+
path: entry.path,
|
|
1668
|
+
isDirectory: entry.is_dir,
|
|
1669
|
+
isFile: !entry.is_dir,
|
|
1670
|
+
size: entry.size
|
|
1671
|
+
}));
|
|
1672
|
+
}
|
|
1673
|
+
async exists(filePath) {
|
|
1674
|
+
try {
|
|
1675
|
+
await this.stat(filePath);
|
|
1676
|
+
return true;
|
|
1677
|
+
} catch {
|
|
1678
|
+
return false;
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
async stat(filePath) {
|
|
1682
|
+
const url = this.fsUrl("/stat", { path: this.resolvePath(filePath) });
|
|
1683
|
+
const res = await fetch(url, { headers: this.headers() });
|
|
1684
|
+
if (!res.ok) {
|
|
1685
|
+
throw new Error(
|
|
1686
|
+
`SpritesFs stat failed (${res.status}): ${await res.text()}`
|
|
1687
|
+
);
|
|
1688
|
+
}
|
|
1689
|
+
const data = await res.json();
|
|
1690
|
+
return {
|
|
1691
|
+
size: data.size,
|
|
1692
|
+
isDirectory: data.is_dir,
|
|
1693
|
+
isFile: !data.is_dir,
|
|
1694
|
+
createdAt: data.created_at ? new Date(data.created_at) : void 0,
|
|
1695
|
+
modifiedAt: data.modified_at ? new Date(data.modified_at) : void 0
|
|
1696
|
+
};
|
|
1697
|
+
}
|
|
1698
|
+
};
|
|
1699
|
+
|
|
1700
|
+
// src/virtual/sprites-computer.ts
|
|
1701
|
+
var SpritesComputer = class {
|
|
1702
|
+
token;
|
|
1703
|
+
spriteName;
|
|
1704
|
+
baseURL;
|
|
1705
|
+
workingDir;
|
|
1706
|
+
constructor(opts) {
|
|
1707
|
+
this.token = opts.token;
|
|
1708
|
+
this.spriteName = opts.spriteName;
|
|
1709
|
+
this.baseURL = (opts.baseURL ?? "https://api.sprites.dev").replace(
|
|
1710
|
+
/\/$/,
|
|
1711
|
+
""
|
|
1712
|
+
);
|
|
1713
|
+
this.workingDir = opts.workingDir ?? "/home/sprite";
|
|
1714
|
+
}
|
|
1715
|
+
headers() {
|
|
1716
|
+
return {
|
|
1717
|
+
Authorization: `Bearer ${this.token}`,
|
|
1718
|
+
"Content-Type": "application/json"
|
|
1719
|
+
};
|
|
1720
|
+
}
|
|
1721
|
+
async executeCommand(command, opts) {
|
|
1722
|
+
const cwd = opts?.cwd ?? this.workingDir;
|
|
1723
|
+
const wrappedCommand = `cd ${this.shellEscape(cwd)} && ${command}`;
|
|
1724
|
+
const url = `${this.baseURL}/v1/sprites/${this.spriteName}/exec`;
|
|
1725
|
+
const res = await fetch(url, {
|
|
1726
|
+
method: "POST",
|
|
1727
|
+
headers: this.headers(),
|
|
1728
|
+
body: JSON.stringify({
|
|
1729
|
+
command: ["bash", "-c", wrappedCommand],
|
|
1730
|
+
timeout: opts?.timeout ?? 3e4,
|
|
1731
|
+
env: opts?.env
|
|
1732
|
+
})
|
|
1733
|
+
});
|
|
1734
|
+
if (!res.ok) {
|
|
1735
|
+
const text = await res.text();
|
|
1736
|
+
return {
|
|
1737
|
+
exitCode: 1,
|
|
1738
|
+
stdout: "",
|
|
1739
|
+
stderr: `Sprites exec failed (${res.status}): ${text}`
|
|
1740
|
+
};
|
|
1741
|
+
}
|
|
1742
|
+
const data = await res.json();
|
|
1743
|
+
return {
|
|
1744
|
+
exitCode: data.exit_code,
|
|
1745
|
+
stdout: data.stdout ?? "",
|
|
1746
|
+
stderr: data.stderr ?? ""
|
|
1747
|
+
};
|
|
1748
|
+
}
|
|
1749
|
+
shellEscape(s) {
|
|
1750
|
+
return `'${s.replace(/'/g, "'\\''")}'`;
|
|
1751
|
+
}
|
|
1752
|
+
};
|
|
1753
|
+
export {
|
|
1754
|
+
AnthropicProvider,
|
|
1755
|
+
Code,
|
|
1756
|
+
GeminiProvider,
|
|
1757
|
+
LocalComputer,
|
|
1758
|
+
LocalFs,
|
|
1759
|
+
OpenAIProvider,
|
|
1760
|
+
SpritesComputer,
|
|
1761
|
+
SpritesFs,
|
|
1762
|
+
Thread,
|
|
1763
|
+
ToolRegistry,
|
|
1764
|
+
bashTool,
|
|
1765
|
+
buildSystemPrompt,
|
|
1766
|
+
compactConversation,
|
|
1767
|
+
createAutoCompactConfig,
|
|
1768
|
+
editFileTool,
|
|
1769
|
+
globTool,
|
|
1770
|
+
grepTool,
|
|
1771
|
+
loadSkills,
|
|
1772
|
+
readFileTool,
|
|
1773
|
+
shouldAutoCompact,
|
|
1774
|
+
writeFileTool
|
|
1775
|
+
};
|
|
1776
|
+
//# sourceMappingURL=index.js.map
|