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/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