oroute-cli 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/bin/oroute.cjs +2 -0
  2. package/dist/oroute.cjs +244526 -0
  3. package/package.json +27 -6
  4. package/dist/agent.d.ts +0 -11
  5. package/dist/agent.js +0 -569
  6. package/dist/agent.js.map +0 -1
  7. package/dist/commands.d.ts +0 -24
  8. package/dist/commands.js +0 -173
  9. package/dist/commands.js.map +0 -1
  10. package/dist/context.d.ts +0 -13
  11. package/dist/context.js +0 -110
  12. package/dist/context.js.map +0 -1
  13. package/dist/cost.d.ts +0 -18
  14. package/dist/cost.js +0 -80
  15. package/dist/cost.js.map +0 -1
  16. package/dist/history.d.ts +0 -20
  17. package/dist/history.js +0 -49
  18. package/dist/history.js.map +0 -1
  19. package/dist/hooks.d.ts +0 -13
  20. package/dist/hooks.js +0 -101
  21. package/dist/hooks.js.map +0 -1
  22. package/dist/index.d.ts +0 -2
  23. package/dist/index.js +0 -475
  24. package/dist/index.js.map +0 -1
  25. package/dist/markdown.d.ts +0 -4
  26. package/dist/markdown.js +0 -126
  27. package/dist/markdown.js.map +0 -1
  28. package/dist/memory.d.ts +0 -27
  29. package/dist/memory.js +0 -208
  30. package/dist/memory.js.map +0 -1
  31. package/dist/session.d.ts +0 -43
  32. package/dist/session.js +0 -166
  33. package/dist/session.js.map +0 -1
  34. package/dist/streaming.d.ts +0 -27
  35. package/dist/streaming.js +0 -201
  36. package/dist/streaming.js.map +0 -1
  37. package/dist/tools/editFile.d.ts +0 -34
  38. package/dist/tools/editFile.js +0 -40
  39. package/dist/tools/editFile.js.map +0 -1
  40. package/dist/tools/executeCommand.d.ts +0 -32
  41. package/dist/tools/executeCommand.js +0 -75
  42. package/dist/tools/executeCommand.js.map +0 -1
  43. package/dist/tools/glob.d.ts +0 -24
  44. package/dist/tools/glob.js +0 -128
  45. package/dist/tools/glob.js.map +0 -1
  46. package/dist/tools/index.d.ts +0 -40
  47. package/dist/tools/index.js +0 -28
  48. package/dist/tools/index.js.map +0 -1
  49. package/dist/tools/listDirectory.d.ts +0 -23
  50. package/dist/tools/listDirectory.js +0 -57
  51. package/dist/tools/listDirectory.js.map +0 -1
  52. package/dist/tools/notebook.d.ts +0 -7
  53. package/dist/tools/notebook.js +0 -76
  54. package/dist/tools/notebook.js.map +0 -1
  55. package/dist/tools/readFile.d.ts +0 -28
  56. package/dist/tools/readFile.js +0 -40
  57. package/dist/tools/readFile.js.map +0 -1
  58. package/dist/tools/readImage.d.ts +0 -25
  59. package/dist/tools/readImage.js +0 -52
  60. package/dist/tools/readImage.js.map +0 -1
  61. package/dist/tools/searchFiles.d.ts +0 -33
  62. package/dist/tools/searchFiles.js +0 -95
  63. package/dist/tools/searchFiles.js.map +0 -1
  64. package/dist/tools/writeFile.d.ts +0 -30
  65. package/dist/tools/writeFile.js +0 -54
  66. package/dist/tools/writeFile.js.map +0 -1
  67. package/dist/ui.d.ts +0 -30
  68. package/dist/ui.js +0 -79
  69. package/dist/ui.js.map +0 -1
  70. package/dist/update.d.ts +0 -10
  71. package/dist/update.js +0 -93
  72. package/dist/update.js.map +0 -1
package/package.json CHANGED
@@ -1,17 +1,32 @@
1
1
  {
2
2
  "name": "oroute-cli",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "O'Route CLI — AI API auto-routing for 13 models from your terminal",
5
5
  "type": "module",
6
6
  "bin": {
7
- "oroute": "./dist/index.js"
7
+ "oroute": "./bin/oroute.cjs"
8
8
  },
9
9
  "files": [
10
- "dist/**/*"
10
+ "bin/oroute.cjs",
11
+ "dist/oroute.cjs"
11
12
  ],
13
+ "publishConfig": {
14
+ "access": "public"
15
+ },
12
16
  "keywords": [
13
- "ai", "cli", "llm", "routing", "claude", "gpt", "gemini",
14
- "deepseek", "qwen", "anthropic", "openai", "agent", "coding"
17
+ "ai",
18
+ "cli",
19
+ "llm",
20
+ "routing",
21
+ "claude",
22
+ "gpt",
23
+ "gemini",
24
+ "deepseek",
25
+ "qwen",
26
+ "anthropic",
27
+ "openai",
28
+ "agent",
29
+ "coding"
15
30
  ],
16
31
  "author": "O'Route <netclaus@gmail.com>",
17
32
  "license": "MIT",
@@ -21,13 +36,19 @@
21
36
  "url": "https://github.com/netclaus/oroute"
22
37
  },
23
38
  "scripts": {
24
- "build": "tsc",
39
+ "build": "tsc && node esbuild.config.mjs",
40
+ "build:bundle": "node esbuild.config.mjs",
25
41
  "dev": "tsx src/index.ts",
26
42
  "prepublishOnly": "npm run build"
27
43
  },
28
44
  "devDependencies": {
29
45
  "@types/node": "^22.19.15",
46
+ "esbuild": "^0.27.4",
30
47
  "tsx": "^4.21.0",
31
48
  "typescript": "^5.9.3"
49
+ },
50
+ "dependencies": {
51
+ "@modelcontextprotocol/sdk": "^1.27.1",
52
+ "pdf-parse": "1.1.1"
32
53
  }
33
54
  }
package/dist/agent.d.ts DELETED
@@ -1,11 +0,0 @@
1
- export interface AgentConfig {
2
- apiKey?: string;
3
- apiUrl?: string;
4
- model?: string;
5
- confirmBeforeWrite?: boolean;
6
- confirmBeforeExecute?: boolean;
7
- skipConfirmations?: boolean;
8
- planMode?: boolean;
9
- }
10
- /** Main agent chat loop with tool use */
11
- export declare function agentChat(config: AgentConfig): Promise<void>;
package/dist/agent.js DELETED
@@ -1,569 +0,0 @@
1
- /**
2
- * Agent loop — send messages with tools, handle tool_use, stream responses.
3
- * Integrates: A1 Streaming, A2 Context, A3 Markdown, A4 History, A5 Commands, A6 Multi-line, B1 Git, B6 Cost
4
- * B2 Glob, B3 Notebook, B4 ReadImage, B7 Session, C1 Parallel, C3 Hooks, C5 Memory
5
- */
6
- import * as os from 'node:os';
7
- import * as fs from 'node:fs';
8
- import * as path from 'node:path';
9
- import * as readline from 'node:readline';
10
- import { readFileTool, readFileDefinition, writeFileTool, writeFileDefinition, generateDiff, listDirectoryTool, listDirectoryDefinition, searchFilesTool, searchFilesDefinition, executeCommandTool, executeCommandDefinition, editFileTool, editFileDefinition, generateEditDiff, globTool, globDefinition, readImageTool, readImageDefinition, } from './tools/index.js';
11
- import { GREEN, GRAY, BOLD, DIM, RESET, YELLOW, RED, printToolUse, printToolResult, printError, printSuccess, confirm, createSpinner, } from './ui.js';
12
- import { renderMarkdown } from './markdown.js';
13
- import { loadProjectContext, buildContextString } from './context.js';
14
- import { estimateTokens, TOKEN_THRESHOLD, compressHistory } from './history.js';
15
- import { parseSSEStream, isSSEResponse } from './streaming.js';
16
- import { handleSlashCommand } from './commands.js';
17
- import { createSessionStats, updateStats, printExitSummary } from './cost.js';
18
- import { loadHooks, runHooks } from './hooks.js';
19
- import { buildMemoryContext } from './memory.js';
20
- import { autoSaveSession } from './session.js';
21
- const TOOL_DEFINITIONS = [
22
- readFileDefinition,
23
- writeFileDefinition,
24
- listDirectoryDefinition,
25
- searchFilesDefinition,
26
- executeCommandDefinition,
27
- editFileDefinition,
28
- globDefinition,
29
- readImageDefinition,
30
- ];
31
- /** C1: Tools that are safe to run in parallel (read-only, no side effects) */
32
- const PARALLEL_SAFE_TOOLS = new Set([
33
- 'read_file', 'list_directory', 'search_files', 'glob', 'read_image',
34
- ]);
35
- /** Max chars to send as tool result to API (prevent token overflow) */
36
- const MAX_TOOL_RESULT_CHARS = 4000;
37
- /** Detect if a directory is a project (has package.json, .git, etc.) */
38
- function detectProject(dir) {
39
- const markers = ['package.json', 'Cargo.toml', 'pyproject.toml', 'go.mod', '.git'];
40
- for (const m of markers) {
41
- if (fs.existsSync(path.join(dir, m))) {
42
- if (m === 'package.json') {
43
- try {
44
- const pkg = JSON.parse(fs.readFileSync(path.join(dir, m), 'utf-8'));
45
- return { isProject: true, name: pkg.name ?? path.basename(dir) };
46
- }
47
- catch {
48
- return { isProject: true, name: path.basename(dir) };
49
- }
50
- }
51
- return { isProject: true, name: path.basename(dir) };
52
- }
53
- }
54
- return { isProject: false };
55
- }
56
- /** Find nearest project directory by walking up */
57
- function findProjectRoot(startDir) {
58
- let dir = startDir;
59
- for (let i = 0; i < 10; i++) {
60
- if (detectProject(dir).isProject)
61
- return dir;
62
- const parent = path.dirname(dir);
63
- if (parent === dir)
64
- break;
65
- dir = parent;
66
- }
67
- return null;
68
- }
69
- function buildSystemPrompt(cwd, contextStr, memoryStr) {
70
- const project = detectProject(cwd);
71
- const projectInfo = project.isProject
72
- ? `\nProject: ${project.name} (${cwd})`
73
- : '\nNo project detected in current directory.';
74
- const contextSection = contextStr
75
- ? `\n\n--- Project Context ---\n${contextStr}\n--- End Context ---`
76
- : '';
77
- const memorySection = memoryStr
78
- ? `\n\n${memoryStr}`
79
- : '';
80
- return `You are O'Route CLI Agent, an expert developer assistant with file system access.
81
-
82
- Current working directory: ${cwd}${projectInfo}
83
- OS: ${os.platform()} ${os.arch()}
84
- Node: ${process.version}${contextSection}${memorySection}
85
-
86
- You have access to file system tools:
87
- - read_file: Read file contents (including .ipynb notebooks)
88
- - write_file: Create or overwrite files
89
- - edit_file: Replace a specific string in a file (str_replace)
90
- - list_directory: List directory tree structure (use "." for current dir)
91
- - search_files: Search for text patterns in files (grep-like)
92
- - execute_command: Run whitelisted shell commands (npm, pnpm, git, node, etc.)
93
- - glob: Find files matching glob patterns (e.g. "**/*.tsx")
94
- - read_image: Read image files for visual analysis
95
-
96
- Guidelines:
97
- - Always use "." as the path for list_directory to show current project, not absolute paths.
98
- - Keep tool results concise. If a directory has many files, use depth=1.
99
- - When the user writes in Korean, respond in Korean.
100
- - Be concise but thorough.
101
- - If a task requires multiple steps, execute them one at a time.
102
- - Always explain what you're doing before using tools.
103
- - Format responses with markdown for readability.`;
104
- }
105
- /** Truncate tool result to prevent token overflow */
106
- function truncateResult(result) {
107
- if (result.length <= MAX_TOOL_RESULT_CHARS)
108
- return result;
109
- return result.slice(0, MAX_TOOL_RESULT_CHARS) + `\n... (truncated, ${result.length - MAX_TOOL_RESULT_CHARS} chars omitted)`;
110
- }
111
- /** Execute a single tool call and return the result */
112
- async function executeTool(tool, cwd, config) {
113
- const confirmWrite = config.skipConfirmations ? false : (config.confirmBeforeWrite !== false);
114
- const confirmExec = config.skipConfirmations ? false : (config.confirmBeforeExecute !== false);
115
- try {
116
- switch (tool.name) {
117
- case 'read_file': {
118
- const input = tool.input;
119
- printToolUse('read_file', input.path);
120
- const result = readFileTool(input, cwd);
121
- printToolResult(result.content);
122
- return truncateResult(JSON.stringify({ content: result.content, lines: result.lines, size: result.size }));
123
- }
124
- case 'write_file': {
125
- const input = tool.input;
126
- printToolUse('write_file', input.path);
127
- const diff = generateDiff(input.path, input.content, cwd);
128
- console.log(`${GRAY}${diff}${RESET}`);
129
- if (confirmWrite) {
130
- const ok = await confirm(`Write to ${input.path}?`);
131
- if (!ok)
132
- return JSON.stringify({ success: false, reason: 'User declined' });
133
- }
134
- const result = writeFileTool(input, cwd);
135
- printSuccess(`Written ${result.bytesWritten} bytes${result.isNew ? ' (new file)' : ''}`);
136
- return JSON.stringify(result);
137
- }
138
- case 'list_directory': {
139
- const input = tool.input;
140
- printToolUse('list_directory', input.path);
141
- const tree = listDirectoryTool(input, cwd);
142
- printToolResult(tree);
143
- return truncateResult(tree);
144
- }
145
- case 'search_files': {
146
- const input = tool.input;
147
- printToolUse('search_files', `"${input.pattern}" in ${input.path}`);
148
- const results = searchFilesTool(input, cwd);
149
- const formatted = results.map(r => `${r.file}:${r.line}: ${r.content}`).join('\n');
150
- printToolResult(formatted || 'No matches found.');
151
- return truncateResult(JSON.stringify(results));
152
- }
153
- case 'execute_command': {
154
- const input = tool.input;
155
- printToolUse('execute_command', input.command);
156
- if (config.planMode) {
157
- console.log(`${YELLOW} [PLAN MODE] Would execute: ${input.command}${RESET}`);
158
- return JSON.stringify({ stdout: '[plan mode — not executed]', stderr: '', exitCode: 0 });
159
- }
160
- if (confirmExec) {
161
- const ok = await confirm(`Execute: ${input.command}`);
162
- if (!ok)
163
- return JSON.stringify({ stdout: '', stderr: 'User declined', exitCode: -1 });
164
- }
165
- const result = executeCommandTool(input, cwd);
166
- if (result.stdout)
167
- printToolResult(result.stdout);
168
- if (result.stderr)
169
- console.log(`${RED}${result.stderr}${RESET}`);
170
- if (result.exitCode === 0)
171
- printSuccess('Command completed');
172
- else
173
- console.log(`${YELLOW} Exit code: ${result.exitCode}${RESET}`);
174
- return truncateResult(JSON.stringify(result));
175
- }
176
- case 'edit_file': {
177
- const input = tool.input;
178
- printToolUse('edit_file', input.path);
179
- const diff = generateEditDiff(input);
180
- console.log(`${GRAY}${diff}${RESET}`);
181
- if (config.planMode) {
182
- console.log(`${YELLOW} [PLAN MODE] Would edit: ${input.path}${RESET}`);
183
- return JSON.stringify({ success: true, note: 'plan mode — not executed' });
184
- }
185
- if (confirmWrite) {
186
- const ok = await confirm(`Edit ${input.path}?`);
187
- if (!ok)
188
- return JSON.stringify({ success: false, reason: 'User declined' });
189
- }
190
- const result = editFileTool(input, cwd);
191
- printSuccess('Edit applied');
192
- return JSON.stringify(result);
193
- }
194
- case 'glob': {
195
- const input = tool.input;
196
- printToolUse('glob', input.pattern);
197
- const results = globTool(input, cwd);
198
- const formatted = results.join('\n');
199
- printToolResult(formatted || 'No matches found.');
200
- return truncateResult(JSON.stringify({ matches: results, count: results.length }));
201
- }
202
- case 'read_image': {
203
- const input = tool.input;
204
- printToolUse('read_image', input.path);
205
- const result = readImageTool(input, cwd);
206
- printSuccess(`Read image: ${result.media_type}, ${(result.size / 1024).toFixed(1)}KB`);
207
- return JSON.stringify(result);
208
- }
209
- default:
210
- return JSON.stringify({ error: `Unknown tool: ${tool.name}` });
211
- }
212
- }
213
- catch (err) {
214
- const msg = err instanceof Error ? err.message : String(err);
215
- printError(msg);
216
- return JSON.stringify({ error: msg });
217
- }
218
- }
219
- /** Execute tool with hooks (C3) */
220
- async function executeToolWithHooks(tool, cwd, config, hooks) {
221
- // C3: Run PreToolUse hooks
222
- const shouldProceed = runHooks('PreToolUse', hooks, tool.name, tool.input, cwd);
223
- if (!shouldProceed) {
224
- return JSON.stringify({ error: 'Skipped by PreToolUse hook' });
225
- }
226
- const result = await executeTool(tool, cwd, config);
227
- // C3: Run PostToolUse hooks
228
- runHooks('PostToolUse', hooks, tool.name, tool.input, cwd);
229
- return result;
230
- }
231
- /** Execute tool from streamed content block */
232
- async function executeStreamedTool(block, cwd, config, hooks) {
233
- const toolBlock = {
234
- type: 'tool_use',
235
- id: block.id ?? '',
236
- name: block.name ?? '',
237
- input: block.input ?? {},
238
- };
239
- return executeToolWithHooks(toolBlock, cwd, config, hooks);
240
- }
241
- /** C1: Execute tools with parallel optimization for read-only tools */
242
- async function executeToolsParallel(blocks, cwd, config, hooks) {
243
- const results = [];
244
- // Group consecutive parallel-safe tools
245
- let i = 0;
246
- while (i < blocks.length) {
247
- const block = blocks[i];
248
- if (PARALLEL_SAFE_TOOLS.has(block.name ?? '')) {
249
- // Collect consecutive parallel-safe tools
250
- const parallelBatch = [block];
251
- let j = i + 1;
252
- while (j < blocks.length && PARALLEL_SAFE_TOOLS.has(blocks[j].name ?? '')) {
253
- parallelBatch.push(blocks[j]);
254
- j++;
255
- }
256
- if (parallelBatch.length > 1) {
257
- // Execute in parallel
258
- const batchResults = await Promise.all(parallelBatch.map(b => executeStreamedTool(b, cwd, config, hooks)));
259
- for (let k = 0; k < parallelBatch.length; k++) {
260
- results.push({
261
- type: 'tool_result',
262
- tool_use_id: parallelBatch[k].id ?? '',
263
- content: batchResults[k],
264
- });
265
- }
266
- }
267
- else {
268
- // Single tool, execute normally
269
- const result = await executeStreamedTool(block, cwd, config, hooks);
270
- results.push({
271
- type: 'tool_result',
272
- tool_use_id: block.id ?? '',
273
- content: result,
274
- });
275
- }
276
- i = j;
277
- }
278
- else {
279
- // Sequential tool (write, execute, edit)
280
- const result = await executeStreamedTool(block, cwd, config, hooks);
281
- results.push({
282
- type: 'tool_result',
283
- tool_use_id: block.id ?? '',
284
- content: result,
285
- });
286
- i++;
287
- }
288
- }
289
- return results;
290
- }
291
- /** A6: Read multi-line input (between """ or ''') */
292
- async function readMultiLineInput(rl, delimiter) {
293
- const lines = [];
294
- return new Promise(resolve => {
295
- const handler = (line) => {
296
- if (line.trim() === delimiter) {
297
- rl.removeListener('line', handler);
298
- resolve(lines.join('\n'));
299
- return;
300
- }
301
- lines.push(line);
302
- process.stdout.write(`${GRAY}...${RESET} `);
303
- };
304
- process.stdout.write(`${GRAY}...${RESET} `);
305
- rl.on('line', handler);
306
- });
307
- }
308
- /** Main agent chat loop with tool use */
309
- export async function agentChat(config) {
310
- const apiUrl = config.apiUrl ?? 'http://localhost:3001';
311
- let model = config.model ?? 'auto';
312
- let cwd = process.cwd();
313
- const homeDir = os.homedir();
314
- // Auto-detect project or warn if in home directory
315
- if (cwd === homeDir) {
316
- console.log(`${YELLOW} Warning: Running from home directory.${RESET}`);
317
- console.log(`${GRAY} Tip: Run from a project folder, or specify: oroute /path/to/project${RESET}`);
318
- console.log();
319
- const projectRoot = findProjectRoot(cwd);
320
- if (projectRoot && projectRoot !== homeDir) {
321
- console.log(`${GREEN} Found project: ${projectRoot}${RESET}`);
322
- process.chdir(projectRoot);
323
- cwd = projectRoot;
324
- }
325
- }
326
- const project = detectProject(cwd);
327
- // A2: Load project context
328
- const projectContext = loadProjectContext(cwd);
329
- const contextStr = buildContextString(projectContext);
330
- // C5: Load memory context
331
- const memoryStr = buildMemoryContext(cwd);
332
- // C3: Load hooks
333
- const hooks = loadHooks(cwd);
334
- // B6: Initialize session stats
335
- let sessionStats = createSessionStats();
336
- console.log(`${GREEN}${BOLD} O'Route Agent${RESET} v0.2.0`);
337
- console.log(`${GRAY} model: ${model} (auto-routed) · agent mode${RESET}`);
338
- if (project.isProject) {
339
- console.log(`${GRAY} project: ${project.name}${RESET}`);
340
- }
341
- // B1: Show git info
342
- if (projectContext.gitBranch) {
343
- console.log(`${GRAY} git: ${projectContext.gitBranch}${projectContext.gitStatus ? ` (${projectContext.gitStatus})` : ''}${RESET}`);
344
- }
345
- console.log(`${GRAY} cwd: ${cwd}${RESET}`);
346
- if (config.skipConfirmations) {
347
- console.log(`${YELLOW} --yes mode: confirmations skipped${RESET}`);
348
- }
349
- if (config.planMode) {
350
- console.log(`${YELLOW} --plan mode: tools shown but not executed${RESET}`);
351
- }
352
- console.log(`${GRAY} Type /help for commands, """ for multi-line${RESET}`);
353
- console.log();
354
- let messages = [];
355
- const systemPrompt = buildSystemPrompt(cwd, contextStr, memoryStr);
356
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
357
- const promptInput = () => new Promise(resolve => rl.question(`${GREEN}>${RESET} `, answer => resolve(answer.trim())));
358
- while (true) {
359
- const rawInput = await promptInput();
360
- // A6: Multi-line input
361
- let input = rawInput;
362
- if (rawInput === '"""' || rawInput === "'''") {
363
- input = await readMultiLineInput(rl, rawInput);
364
- if (!input)
365
- continue;
366
- }
367
- if (!input)
368
- continue;
369
- // A5: Slash commands
370
- const cmdResult = handleSlashCommand(input, {
371
- messages,
372
- stats: sessionStats,
373
- model,
374
- cwd,
375
- apiUrl,
376
- apiKey: config.apiKey ?? '',
377
- setModel: (m) => { model = m; },
378
- setCwd: (d) => { cwd = d; },
379
- setMessages: (m) => { messages = m; },
380
- setStats: (s) => { sessionStats = s; },
381
- });
382
- if (cmdResult === 'exit') {
383
- rl.close();
384
- // B7: Auto-save session on exit
385
- autoSaveSession(messages, sessionStats, cwd);
386
- printExitSummary(sessionStats);
387
- break;
388
- }
389
- if (cmdResult === 'handled')
390
- continue;
391
- // exit/quit without slash
392
- if (input === 'exit' || input === 'quit') {
393
- rl.close();
394
- autoSaveSession(messages, sessionStats, cwd);
395
- printExitSummary(sessionStats);
396
- break;
397
- }
398
- messages.push({ role: 'user', content: input });
399
- // A4: Auto-compress history before API call
400
- const estimated = estimateTokens(messages);
401
- if (estimated > TOKEN_THRESHOLD) {
402
- const compressed = compressHistory(messages);
403
- if (compressed.compressed) {
404
- messages = compressed.messages;
405
- console.log(`${DIM} (history compressed to ${messages.length} messages)${RESET}`);
406
- }
407
- }
408
- // Agent loop: send -> process tool_use -> send results -> repeat
409
- let continueLoop = true;
410
- let retryCount = 0;
411
- const maxRetries = 3;
412
- while (continueLoop) {
413
- const spinner = createSpinner('Thinking...');
414
- try {
415
- const res = await fetch(`${apiUrl}/v1/messages`, {
416
- method: 'POST',
417
- headers: {
418
- 'Authorization': `Bearer ${config.apiKey}`,
419
- 'Content-Type': 'application/json',
420
- },
421
- body: JSON.stringify({
422
- model,
423
- max_tokens: 8192,
424
- system: systemPrompt,
425
- messages,
426
- tools: TOOL_DEFINITIONS,
427
- stream: true,
428
- }),
429
- signal: AbortSignal.timeout(120000),
430
- });
431
- spinner.stop();
432
- if (!res.ok) {
433
- const errBody = await res.text().catch(() => '');
434
- // Rate limit — wait and retry
435
- if (res.status === 429 && retryCount < maxRetries) {
436
- retryCount++;
437
- const waitSec = retryCount * 3;
438
- console.log(`${YELLOW} Rate limited. Retrying in ${waitSec}s... (${retryCount}/${maxRetries})${RESET}`);
439
- await new Promise(r => setTimeout(r, waitSec * 1000));
440
- continue;
441
- }
442
- // Parse error message
443
- let errMsg = `HTTP ${res.status}`;
444
- try {
445
- const errObj = JSON.parse(errBody);
446
- errMsg = typeof errObj.error === 'string'
447
- ? errObj.error
448
- : errObj.error?.message ?? errMsg;
449
- }
450
- catch { /* use default */ }
451
- printError(errMsg);
452
- messages.pop();
453
- continueLoop = false;
454
- continue;
455
- }
456
- retryCount = 0;
457
- // A1: Check if response is SSE streaming or regular JSON
458
- if (isSSEResponse(res)) {
459
- // Streaming response
460
- let firstText = true;
461
- const streamResult = await parseSSEStream(res, (text) => {
462
- if (firstText) {
463
- console.log(); // newline before first text
464
- firstText = false;
465
- }
466
- process.stdout.write(text);
467
- });
468
- // After streaming text is done
469
- if (!firstText) {
470
- console.log(); // newline after streaming text
471
- }
472
- // B6: Update stats
473
- sessionStats = updateStats(sessionStats, streamResult.usage, streamResult.routingMetadata?.provider);
474
- // Build content blocks for history
475
- const contentBlocksForHistory = [];
476
- const toolBlocks = [];
477
- for (const block of streamResult.contentBlocks) {
478
- if (block.type === 'text') {
479
- contentBlocksForHistory.push({ type: 'text', text: block.text ?? '' });
480
- }
481
- else if (block.type === 'tool_use') {
482
- contentBlocksForHistory.push({
483
- type: 'tool_use',
484
- id: block.id ?? '',
485
- name: block.name ?? '',
486
- input: block.input ?? {},
487
- });
488
- toolBlocks.push(block);
489
- }
490
- }
491
- // Add assistant message to history
492
- messages.push({ role: 'assistant', content: contentBlocksForHistory });
493
- // C1: Execute tools (parallel for read-only, sequential for writes)
494
- if (toolBlocks.length > 0) {
495
- const toolResults = await executeToolsParallel(toolBlocks, cwd, config, hooks);
496
- messages.push({ role: 'user', content: toolResults });
497
- }
498
- else {
499
- // Show metadata
500
- const meta = streamResult.routingMetadata;
501
- if (meta) {
502
- console.log(`${DIM} - ${meta.provider} · L${meta.taskComplexity} · ${meta.totalLatencyMs.toFixed(0)}ms · ${streamResult.usage.input_tokens}+${streamResult.usage.output_tokens} tokens${RESET}`);
503
- }
504
- console.log();
505
- continueLoop = false;
506
- }
507
- if (streamResult.stopReason === 'end_turn' && toolBlocks.length === 0) {
508
- continueLoop = false;
509
- }
510
- }
511
- else {
512
- // Non-streaming fallback (regular JSON response)
513
- const data = (await res.json());
514
- // B6: Update stats
515
- sessionStats = updateStats(sessionStats, data.usage, data.routing_metadata?.provider);
516
- // Process response content blocks
517
- const toolResults = [];
518
- for (const block of data.content) {
519
- if (block.type === 'text') {
520
- const text = block.text;
521
- if (text) {
522
- console.log();
523
- // A3: Render markdown
524
- console.log(renderMarkdown(text));
525
- }
526
- }
527
- else if (block.type === 'tool_use') {
528
- const toolBlock = block;
529
- const result = await executeToolWithHooks(toolBlock, cwd, config, hooks);
530
- toolResults.push({
531
- type: 'tool_result',
532
- tool_use_id: toolBlock.id,
533
- content: result,
534
- });
535
- }
536
- }
537
- // Add assistant message to history
538
- messages.push({ role: 'assistant', content: data.content });
539
- // If there were tool uses, send results back
540
- if (toolResults.length > 0) {
541
- messages.push({ role: 'user', content: toolResults });
542
- }
543
- else {
544
- // No tool use, show metadata and stop
545
- const meta = data.routing_metadata;
546
- if (meta) {
547
- console.log();
548
- console.log(`${DIM} - ${meta.provider} · L${meta.taskComplexity} · ${meta.totalLatencyMs.toFixed(0)}ms · ${data.usage.input_tokens}+${data.usage.output_tokens} tokens${RESET}`);
549
- }
550
- console.log();
551
- continueLoop = false;
552
- }
553
- // Check stop reason
554
- if (data.stop_reason === 'end_turn' && toolResults.length === 0) {
555
- continueLoop = false;
556
- }
557
- }
558
- }
559
- catch (err) {
560
- spinner.stop();
561
- const msg = err instanceof Error ? err.message : 'unknown error';
562
- printError(`Request failed: ${msg}`);
563
- messages.pop();
564
- continueLoop = false;
565
- }
566
- }
567
- }
568
- }
569
- //# sourceMappingURL=agent.js.map