@axplusb/kepler 0.0.1 → 1.0.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 (218) hide show
  1. package/README.md +82 -0
  2. package/package.json +36 -4
  3. package/pulse/app/activity/page.tsx +190 -0
  4. package/pulse/app/api/activity/route.ts +138 -0
  5. package/pulse/app/api/costs/route.ts +88 -0
  6. package/pulse/app/api/export/route.ts +77 -0
  7. package/pulse/app/api/history/route.ts +11 -0
  8. package/pulse/app/api/import/route.ts +31 -0
  9. package/pulse/app/api/memory/route.ts +52 -0
  10. package/pulse/app/api/plans/route.ts +9 -0
  11. package/pulse/app/api/projects/[slug]/route.ts +96 -0
  12. package/pulse/app/api/projects/route.ts +121 -0
  13. package/pulse/app/api/sessions/[id]/replay/route.ts +20 -0
  14. package/pulse/app/api/sessions/[id]/route.ts +31 -0
  15. package/pulse/app/api/sessions/route.ts +112 -0
  16. package/pulse/app/api/settings/route.ts +14 -0
  17. package/pulse/app/api/stats/route.ts +143 -0
  18. package/pulse/app/api/todos/route.ts +9 -0
  19. package/pulse/app/api/tools/route.ts +160 -0
  20. package/pulse/app/costs/page.tsx +179 -0
  21. package/pulse/app/export/page.tsx +465 -0
  22. package/pulse/app/favicon.ico +0 -0
  23. package/pulse/app/globals.css +263 -0
  24. package/pulse/app/help/page.tsx +142 -0
  25. package/pulse/app/history/page.tsx +157 -0
  26. package/pulse/app/layout.tsx +46 -0
  27. package/pulse/app/memory/page.tsx +365 -0
  28. package/pulse/app/overview-client.tsx +393 -0
  29. package/pulse/app/page.tsx +14 -0
  30. package/pulse/app/plans/page.tsx +308 -0
  31. package/pulse/app/projects/[slug]/page.tsx +390 -0
  32. package/pulse/app/projects/page.tsx +110 -0
  33. package/pulse/app/sessions/[id]/page.tsx +243 -0
  34. package/pulse/app/sessions/page.tsx +39 -0
  35. package/pulse/app/settings/page.tsx +188 -0
  36. package/pulse/app/todos/page.tsx +211 -0
  37. package/pulse/app/tools/page.tsx +249 -0
  38. package/pulse/cli.js +159 -0
  39. package/pulse/components/activity/day-of-week-chart.tsx +35 -0
  40. package/pulse/components/activity/streak-card.tsx +36 -0
  41. package/pulse/components/costs/cache-efficiency-panel.tsx +76 -0
  42. package/pulse/components/costs/cost-by-project-chart.tsx +48 -0
  43. package/pulse/components/costs/cost-over-time-chart.tsx +95 -0
  44. package/pulse/components/costs/model-token-table.tsx +60 -0
  45. package/pulse/components/global-search.tsx +193 -0
  46. package/pulse/components/keyboard-nav-provider.tsx +23 -0
  47. package/pulse/components/layout/bottom-nav.tsx +52 -0
  48. package/pulse/components/layout/client-layout.tsx +31 -0
  49. package/pulse/components/layout/sidebar-context.tsx +50 -0
  50. package/pulse/components/layout/sidebar.tsx +182 -0
  51. package/pulse/components/layout/top-bar.tsx +121 -0
  52. package/pulse/components/overview/activity-heatmap.tsx +107 -0
  53. package/pulse/components/overview/conversation-table.tsx +148 -0
  54. package/pulse/components/overview/model-breakdown-donut.tsx +95 -0
  55. package/pulse/components/overview/peak-hours-chart.tsx +87 -0
  56. package/pulse/components/overview/project-activity-donut.tsx +96 -0
  57. package/pulse/components/overview/stat-card.tsx +102 -0
  58. package/pulse/components/overview/usage-over-time-chart.tsx +166 -0
  59. package/pulse/components/projects/project-card.tsx +175 -0
  60. package/pulse/components/sessions/replay/assistant-markdown.tsx +94 -0
  61. package/pulse/components/sessions/replay/compaction-card.tsx +25 -0
  62. package/pulse/components/sessions/replay/session-sidebar.tsx +231 -0
  63. package/pulse/components/sessions/replay/token-accumulation-chart.tsx +98 -0
  64. package/pulse/components/sessions/replay/tool-call-badge.tsx +127 -0
  65. package/pulse/components/sessions/replay/turn-cards.tsx +220 -0
  66. package/pulse/components/sessions/replay/user-tool-result.tsx +158 -0
  67. package/pulse/components/sessions/session-badges.tsx +49 -0
  68. package/pulse/components/sessions/session-table.tsx +299 -0
  69. package/pulse/components/theme-provider.tsx +44 -0
  70. package/pulse/components/tools/feature-adoption-table.tsx +58 -0
  71. package/pulse/components/tools/mcp-server-panel.tsx +45 -0
  72. package/pulse/components/tools/tool-ranking-chart.tsx +57 -0
  73. package/pulse/components/tools/version-history-table.tsx +32 -0
  74. package/pulse/components/ui/alert.tsx +66 -0
  75. package/pulse/components/ui/badge.tsx +48 -0
  76. package/pulse/components/ui/breadcrumb.tsx +109 -0
  77. package/pulse/components/ui/button.tsx +64 -0
  78. package/pulse/components/ui/calendar.tsx +220 -0
  79. package/pulse/components/ui/card.tsx +92 -0
  80. package/pulse/components/ui/command.tsx +158 -0
  81. package/pulse/components/ui/dialog.tsx +158 -0
  82. package/pulse/components/ui/input.tsx +21 -0
  83. package/pulse/components/ui/popover.tsx +89 -0
  84. package/pulse/components/ui/progress.tsx +31 -0
  85. package/pulse/components/ui/select.tsx +190 -0
  86. package/pulse/components/ui/separator.tsx +28 -0
  87. package/pulse/components/ui/sheet.tsx +143 -0
  88. package/pulse/components/ui/skeleton.tsx +13 -0
  89. package/pulse/components/ui/table.tsx +116 -0
  90. package/pulse/components/ui/tabs.tsx +91 -0
  91. package/pulse/components/ui/tooltip.tsx +57 -0
  92. package/pulse/components/use-global-keyboard-nav.ts +79 -0
  93. package/pulse/components.json +23 -0
  94. package/pulse/eslint.config.mjs +18 -0
  95. package/pulse/lib/claude-reader.ts +594 -0
  96. package/pulse/lib/decode.ts +129 -0
  97. package/pulse/lib/pricing.ts +102 -0
  98. package/pulse/lib/replay-parser.ts +165 -0
  99. package/pulse/lib/tool-categories.ts +127 -0
  100. package/pulse/lib/utils.ts +6 -0
  101. package/pulse/next-env.d.ts +6 -0
  102. package/pulse/next.config.ts +16 -0
  103. package/pulse/package.json +45 -0
  104. package/pulse/postcss.config.mjs +7 -0
  105. package/pulse/public/activity.png +0 -0
  106. package/pulse/public/cc-lens.png +0 -0
  107. package/pulse/public/command-k.png +0 -0
  108. package/pulse/public/costs.png +0 -0
  109. package/pulse/public/dashboard-dark.png +0 -0
  110. package/pulse/public/dashboard-white.png +0 -0
  111. package/pulse/public/export.png +0 -0
  112. package/pulse/public/file.svg +1 -0
  113. package/pulse/public/globe.svg +1 -0
  114. package/pulse/public/next.svg +1 -0
  115. package/pulse/public/projects.png +0 -0
  116. package/pulse/public/session-chat.png +0 -0
  117. package/pulse/public/todos.png +0 -0
  118. package/pulse/public/tools.png +0 -0
  119. package/pulse/public/vercel.svg +1 -0
  120. package/pulse/public/window.svg +1 -0
  121. package/pulse/tsconfig.json +34 -0
  122. package/pulse/types/claude.ts +294 -0
  123. package/src/agents/loader.mjs +89 -0
  124. package/src/agents/parser.mjs +98 -0
  125. package/src/agents/teams.mjs +123 -0
  126. package/src/auth/oauth.mjs +220 -0
  127. package/src/auth/tarang-auth.mjs +277 -0
  128. package/src/config/cli-args.mjs +173 -0
  129. package/src/config/env.mjs +263 -0
  130. package/src/config/settings.mjs +132 -0
  131. package/src/context/ast-parser.mjs +298 -0
  132. package/src/context/bm25.mjs +85 -0
  133. package/src/context/retriever.mjs +270 -0
  134. package/src/context/skeleton.mjs +134 -0
  135. package/src/core/agent-loop.mjs +480 -0
  136. package/src/core/approval.mjs +273 -0
  137. package/src/core/backend-url.mjs +57 -0
  138. package/src/core/cache.mjs +105 -0
  139. package/src/core/callback-client.mjs +149 -0
  140. package/src/core/checkpoints.mjs +142 -0
  141. package/src/core/context-manager.mjs +198 -0
  142. package/src/core/headless.mjs +168 -0
  143. package/src/core/hooks-manager.mjs +87 -0
  144. package/src/core/jsonl-writer.mjs +351 -0
  145. package/src/core/local-agent.mjs +429 -0
  146. package/src/core/local-store.mjs +325 -0
  147. package/src/core/mode-selector.mjs +51 -0
  148. package/src/core/output-filter.mjs +177 -0
  149. package/src/core/paths.mjs +98 -0
  150. package/src/core/pricing.mjs +314 -0
  151. package/src/core/providers.mjs +219 -0
  152. package/src/core/rate-limiter.mjs +119 -0
  153. package/src/core/safety.mjs +200 -0
  154. package/src/core/scheduler.mjs +173 -0
  155. package/src/core/session-manager.mjs +317 -0
  156. package/src/core/session.mjs +143 -0
  157. package/src/core/settings-sync.mjs +85 -0
  158. package/src/core/stagnation.mjs +57 -0
  159. package/src/core/stream-client.mjs +367 -0
  160. package/src/core/streaming.mjs +182 -0
  161. package/src/core/system-prompt.mjs +135 -0
  162. package/src/core/tool-executor.mjs +725 -0
  163. package/src/hooks/engine.mjs +162 -0
  164. package/src/index.mjs +370 -0
  165. package/src/mcp/client.mjs +253 -0
  166. package/src/mcp/transport-shttp.mjs +130 -0
  167. package/src/mcp/transport-sse.mjs +131 -0
  168. package/src/mcp/transport-ws.mjs +134 -0
  169. package/src/permissions/checker.mjs +57 -0
  170. package/src/permissions/command-classifier.mjs +573 -0
  171. package/src/permissions/injection-check.mjs +60 -0
  172. package/src/permissions/path-check.mjs +102 -0
  173. package/src/permissions/prompt.mjs +73 -0
  174. package/src/permissions/sandbox.mjs +112 -0
  175. package/src/plugins/loader.mjs +138 -0
  176. package/src/skills/loader.mjs +147 -0
  177. package/src/skills/runner.mjs +55 -0
  178. package/src/telemetry/index.mjs +96 -0
  179. package/src/terminal/agents.mjs +177 -0
  180. package/src/terminal/analytics.mjs +292 -0
  181. package/src/terminal/ansi.mjs +421 -0
  182. package/src/terminal/main.mjs +150 -0
  183. package/src/terminal/repl.mjs +1484 -0
  184. package/src/terminal/tool-display.mjs +58 -0
  185. package/src/tools/agent.mjs +137 -0
  186. package/src/tools/ask-user.mjs +61 -0
  187. package/src/tools/bash.mjs +148 -0
  188. package/src/tools/cron-create.mjs +120 -0
  189. package/src/tools/cron-delete.mjs +49 -0
  190. package/src/tools/cron-list.mjs +37 -0
  191. package/src/tools/edit.mjs +82 -0
  192. package/src/tools/enter-worktree.mjs +69 -0
  193. package/src/tools/exit-worktree.mjs +57 -0
  194. package/src/tools/glob.mjs +117 -0
  195. package/src/tools/grep.mjs +129 -0
  196. package/src/tools/lint.mjs +71 -0
  197. package/src/tools/ls.mjs +58 -0
  198. package/src/tools/lsp.mjs +115 -0
  199. package/src/tools/multi-edit.mjs +94 -0
  200. package/src/tools/notebook-edit.mjs +96 -0
  201. package/src/tools/read-mcp-resource.mjs +57 -0
  202. package/src/tools/read.mjs +138 -0
  203. package/src/tools/registry.mjs +132 -0
  204. package/src/tools/remote-trigger.mjs +84 -0
  205. package/src/tools/send-message.mjs +64 -0
  206. package/src/tools/skill.mjs +52 -0
  207. package/src/tools/test-runner.mjs +49 -0
  208. package/src/tools/todo-write.mjs +68 -0
  209. package/src/tools/tool-search.mjs +77 -0
  210. package/src/tools/web-fetch.mjs +65 -0
  211. package/src/tools/web-search.mjs +89 -0
  212. package/src/tools/write.mjs +55 -0
  213. package/src/ui/banner.mjs +237 -0
  214. package/src/ui/commands.mjs +499 -0
  215. package/src/ui/formatter.mjs +379 -0
  216. package/src/ui/markdown.mjs +278 -0
  217. package/src/ui/slash-commands.mjs +258 -0
  218. package/index.js +0 -1
@@ -0,0 +1,429 @@
1
+ /**
2
+ * Local Agent — T18: Direct LLM API calls, <100ms startup, offline.
3
+ * Replaces the SSE backend for --local mode.
4
+ * Yields events matching the same format as TarangStreamClient.
5
+ */
6
+
7
+ import { ContextRetriever } from '../context/retriever.mjs';
8
+ import { createStagnationTracker, stagnationMessage } from './stagnation.mjs';
9
+
10
+ const MAX_ITERATIONS = 50;
11
+
12
+ /** Tool schemas for the LLM — proper parameter definitions. */
13
+ const TOOL_SCHEMAS = [
14
+ {
15
+ name: 'shell',
16
+ description: 'Run a shell command and return stdout/stderr. Use for: installing packages, running tests, builds, git operations, or any terminal command.',
17
+ input_schema: {
18
+ type: 'object',
19
+ properties: {
20
+ command: { type: 'string', description: 'The shell command to execute' },
21
+ timeout: { type: 'number', description: 'Timeout in milliseconds (default: 120000)' },
22
+ },
23
+ required: ['command'],
24
+ },
25
+ },
26
+ {
27
+ name: 'read_file',
28
+ description: 'Read a file and return its contents. Supports line ranges for large files.',
29
+ input_schema: {
30
+ type: 'object',
31
+ properties: {
32
+ file_path: { type: 'string', description: 'Path to the file (relative to project root)' },
33
+ offset: { type: 'number', description: 'Start line number (1-based, optional)' },
34
+ limit: { type: 'number', description: 'Number of lines to read (optional)' },
35
+ },
36
+ required: ['file_path'],
37
+ },
38
+ },
39
+ {
40
+ name: 'write_file',
41
+ description: 'Create or overwrite a file with the given content. Parent directories are created automatically.',
42
+ input_schema: {
43
+ type: 'object',
44
+ properties: {
45
+ file_path: { type: 'string', description: 'Path to the file (relative to project root)' },
46
+ content: { type: 'string', description: 'The full file content to write' },
47
+ },
48
+ required: ['file_path', 'content'],
49
+ },
50
+ },
51
+ {
52
+ name: 'edit_file',
53
+ description: 'Search for a string in a file and replace it. The old_string must match exactly (including whitespace).',
54
+ input_schema: {
55
+ type: 'object',
56
+ properties: {
57
+ file_path: { type: 'string', description: 'Path to the file' },
58
+ old_string: { type: 'string', description: 'Exact string to find in the file' },
59
+ new_string: { type: 'string', description: 'Replacement string' },
60
+ },
61
+ required: ['file_path', 'old_string', 'new_string'],
62
+ },
63
+ },
64
+ {
65
+ name: 'list_files',
66
+ description: 'List files matching a glob pattern. Returns file paths relative to project root.',
67
+ input_schema: {
68
+ type: 'object',
69
+ properties: {
70
+ pattern: { type: 'string', description: 'Glob pattern (e.g., "src/**/*.ts", "*.json")' },
71
+ path: { type: 'string', description: 'Directory to search in (default: project root)' },
72
+ },
73
+ required: ['pattern'],
74
+ },
75
+ },
76
+ {
77
+ name: 'search_code',
78
+ description: 'Search file contents using a regex pattern. Returns matching lines with file paths and line numbers.',
79
+ input_schema: {
80
+ type: 'object',
81
+ properties: {
82
+ pattern: { type: 'string', description: 'Regex pattern to search for' },
83
+ path: { type: 'string', description: 'Directory or file to search in (default: project root)' },
84
+ include: { type: 'string', description: 'File glob filter (e.g., "*.ts")' },
85
+ },
86
+ required: ['pattern'],
87
+ },
88
+ },
89
+ {
90
+ name: 'search_files',
91
+ description: 'Search for files by name pattern. Returns matching file paths.',
92
+ input_schema: {
93
+ type: 'object',
94
+ properties: {
95
+ query: { type: 'string', description: 'Filename pattern to search for' },
96
+ path: { type: 'string', description: 'Directory to search in (default: project root)' },
97
+ },
98
+ required: ['query'],
99
+ },
100
+ },
101
+ {
102
+ name: 'read_files',
103
+ description: 'Read multiple files at once (batch). More efficient than multiple read_file calls.',
104
+ input_schema: {
105
+ type: 'object',
106
+ properties: {
107
+ file_paths: {
108
+ type: 'array',
109
+ items: { type: 'string' },
110
+ description: 'Array of file paths to read',
111
+ },
112
+ },
113
+ required: ['file_paths'],
114
+ },
115
+ },
116
+ {
117
+ name: 'delete_file',
118
+ description: 'Delete a file from the project.',
119
+ input_schema: {
120
+ type: 'object',
121
+ properties: {
122
+ file_path: { type: 'string', description: 'Path to the file to delete' },
123
+ },
124
+ required: ['file_path'],
125
+ },
126
+ },
127
+ {
128
+ name: 'get_file_info',
129
+ description: 'Get file metadata: size, modification time, type, permissions.',
130
+ input_schema: {
131
+ type: 'object',
132
+ properties: {
133
+ file_path: { type: 'string', description: 'Path to the file' },
134
+ },
135
+ required: ['file_path'],
136
+ },
137
+ },
138
+ {
139
+ name: 'validate_file',
140
+ description: 'Check file syntax. Runs language-specific checks (node --check for JS, py_compile for Python).',
141
+ input_schema: {
142
+ type: 'object',
143
+ properties: {
144
+ file_path: { type: 'string', description: 'Path to the file to validate' },
145
+ },
146
+ required: ['file_path'],
147
+ },
148
+ },
149
+ {
150
+ name: 'validate_build',
151
+ description: 'Run the project build command. Auto-detects: npm run build, make, cargo build, etc.',
152
+ input_schema: {
153
+ type: 'object',
154
+ properties: {
155
+ command: { type: 'string', description: 'Build command override (optional — auto-detected if omitted)' },
156
+ },
157
+ },
158
+ },
159
+ {
160
+ name: 'lint_check',
161
+ description: 'Run linter on a file. Uses ruff for Python, eslint for JS/TS.',
162
+ input_schema: {
163
+ type: 'object',
164
+ properties: {
165
+ file_path: { type: 'string', description: 'Path to the file to lint' },
166
+ },
167
+ required: ['file_path'],
168
+ },
169
+ },
170
+ {
171
+ name: 'validate_structure',
172
+ description: 'Check that a list of expected files exist in the project.',
173
+ input_schema: {
174
+ type: 'object',
175
+ properties: {
176
+ files: {
177
+ type: 'array',
178
+ items: { type: 'string' },
179
+ description: 'Array of file paths that should exist',
180
+ },
181
+ },
182
+ required: ['files'],
183
+ },
184
+ },
185
+ ];
186
+
187
+ export class LocalAgent {
188
+ constructor({
189
+ apiKey,
190
+ model,
191
+ toolExecutor,
192
+ verbose = false,
193
+ openRouterKey = null,
194
+ cwd = null,
195
+ systemPromptOverride = null,
196
+ maxTurns = null,
197
+ stagnationDetection = false,
198
+ stagnationThreshold = 3,
199
+ }) {
200
+ this.apiKey = apiKey;
201
+ this.openRouterKey = openRouterKey;
202
+ this.model = model || 'claude-sonnet-4-20250514';
203
+ this.toolExecutor = toolExecutor;
204
+ this.verbose = verbose;
205
+ this.cwd = cwd || process.cwd();
206
+ this.retriever = new ContextRetriever(this.cwd);
207
+ this.systemPromptOverride = systemPromptOverride;
208
+ this.maxTurns = maxTurns || MAX_ITERATIONS;
209
+ this.stagnationDetection = stagnationDetection;
210
+ this.stagnationThreshold = stagnationThreshold;
211
+ this._cancelled = false;
212
+ }
213
+
214
+ async *execute(instruction, context = {}) {
215
+ this._cancelled = false;
216
+ const startTime = Date.now();
217
+ let toolCount = 0;
218
+
219
+ yield { type: 'status', data: { message: `Local mode: ${this.model}` } };
220
+
221
+ // Retrieve relevant code context via BM25 index
222
+ let retrievedContext = [];
223
+ try {
224
+ retrievedContext = this.retriever.retrieve(instruction, 8);
225
+ if (retrievedContext.length > 0) {
226
+ yield { type: 'status', data: { message: `Context: ${retrievedContext.length} relevant chunks from index` } };
227
+ }
228
+ } catch {
229
+ // Index may not exist yet — that's fine, continue without context
230
+ }
231
+
232
+ const tools = this._buildToolDefs();
233
+ const systemPrompt = this._buildSystemPrompt(context, retrievedContext);
234
+ const messages = [{ role: 'user', content: instruction }];
235
+
236
+ const stagnation = createStagnationTracker({
237
+ enabled: this.stagnationDetection,
238
+ threshold: this.stagnationThreshold,
239
+ });
240
+
241
+ for (let i = 0; i < this.maxTurns; i++) {
242
+ if (this._cancelled) {
243
+ yield { type: 'cancelled', data: { reason: 'User cancelled' } };
244
+ return;
245
+ }
246
+
247
+ let response;
248
+ try {
249
+ response = await this._callLLM(systemPrompt, messages, tools);
250
+ } catch (err) {
251
+ yield { type: 'error', data: { message: `LLM API error: ${err.message}`, fatal: true } };
252
+ return;
253
+ }
254
+
255
+ const { content, stopReason } = response;
256
+
257
+ // Process content blocks
258
+ let hasToolUse = false;
259
+ const assistantContent = [];
260
+
261
+ for (const block of content) {
262
+ if (block.type === 'text') {
263
+ yield { type: 'content', data: { text: block.text } };
264
+ assistantContent.push(block);
265
+ } else if (block.type === 'tool_use') {
266
+ hasToolUse = true;
267
+ toolCount++;
268
+ const { id, name, input } = block;
269
+
270
+ const stagnationResult = stagnation.record(name, input);
271
+ if (stagnationResult.detected) {
272
+ const message = stagnationMessage(name, stagnationResult.count);
273
+ yield { type: 'stagnation', data: { tool: name, count: stagnationResult.count, message } };
274
+ assistantContent.push(block);
275
+ messages.push({ role: 'assistant', content: assistantContent.slice() });
276
+ messages.push({
277
+ role: 'user',
278
+ content: [{ type: 'tool_result', tool_use_id: id, content: message }],
279
+ });
280
+ continue;
281
+ }
282
+
283
+ yield { type: 'tool_call', data: { call_id: id, tool: name, args: input } };
284
+
285
+ // Execute locally
286
+ let result;
287
+ try {
288
+ result = await this.toolExecutor.execute(name, input || {});
289
+ } catch (err) {
290
+ result = { success: false, output: `Error: ${err.message}` };
291
+ }
292
+
293
+ yield { type: 'tool_done', data: { tool: name, duration_ms: 0 } };
294
+
295
+ assistantContent.push(block);
296
+ messages.push({ role: 'assistant', content: assistantContent.slice() });
297
+ messages.push({
298
+ role: 'user',
299
+ content: [{ type: 'tool_result', tool_use_id: id, content: result.output || JSON.stringify(result) }],
300
+ });
301
+ }
302
+ }
303
+
304
+ if (!hasToolUse || stopReason === 'end_turn') {
305
+ const duration = (Date.now() - startTime) / 1000;
306
+ yield { type: 'complete', data: { summary: 'Done (local)', changes: toolCount, duration_s: duration } };
307
+ return;
308
+ }
309
+ }
310
+
311
+ yield { type: 'error', data: { message: `Max turns (${this.maxTurns}) reached.` } };
312
+ yield { type: 'complete', data: { summary: 'Aborted (max turns)', changes: toolCount, duration_s: (Date.now() - startTime) / 1000 } };
313
+ }
314
+
315
+ async _callLLM(systemPrompt, messages, tools) {
316
+ const isClaude = this.model.startsWith('claude') || this.model.startsWith('anthropic/claude');
317
+
318
+ // Use Anthropic direct API only for Claude models when we have an Anthropic key
319
+ if (isClaude && this.apiKey && this.apiKey.startsWith('sk-ant-')) {
320
+ return this._callClaude(systemPrompt, messages, tools);
321
+ }
322
+
323
+ // Everything else goes through OpenRouter (DeepSeek, GPT, Gemini, or Claude via OR)
324
+ if (this.openRouterKey) {
325
+ return this._callOpenRouter(systemPrompt, messages, tools);
326
+ }
327
+
328
+ if (this.apiKey) {
329
+ return this._callClaude(systemPrompt, messages, tools);
330
+ }
331
+
332
+ throw new Error('No API key configured. Set ANTHROPIC_API_KEY or configure OpenRouter key.');
333
+ }
334
+
335
+ async _callClaude(systemPrompt, messages, tools) {
336
+ const resp = await fetch('https://api.anthropic.com/v1/messages', {
337
+ method: 'POST',
338
+ headers: {
339
+ 'x-api-key': this.apiKey,
340
+ 'anthropic-version': '2023-06-01',
341
+ 'content-type': 'application/json',
342
+ },
343
+ body: JSON.stringify({
344
+ model: this.model,
345
+ system: systemPrompt,
346
+ messages,
347
+ tools: tools.length > 0 ? tools : undefined,
348
+ max_tokens: 8192,
349
+ }),
350
+ });
351
+ if (!resp.ok) {
352
+ const text = await resp.text().catch(() => '');
353
+ throw new Error(`Claude API ${resp.status}: ${text.slice(0, 200)}`);
354
+ }
355
+ const data = await resp.json();
356
+ return { content: data.content || [], stopReason: data.stop_reason };
357
+ }
358
+
359
+ async _callOpenRouter(systemPrompt, messages, tools) {
360
+ // OpenRouter requires provider prefix (e.g. anthropic/claude-sonnet-4-20250514)
361
+ let model = this.model;
362
+ if (model.startsWith('claude') && !model.includes('/')) {
363
+ model = `anthropic/${model}`;
364
+ }
365
+
366
+ const orMessages = [{ role: 'system', content: systemPrompt }, ...messages];
367
+ const resp = await fetch('https://openrouter.ai/api/v1/chat/completions', {
368
+ method: 'POST',
369
+ headers: {
370
+ 'Authorization': `Bearer ${this.openRouterKey}`,
371
+ 'Content-Type': 'application/json',
372
+ },
373
+ body: JSON.stringify({
374
+ model,
375
+ messages: orMessages,
376
+ tools: tools.length > 0 ? tools.map(t => ({ type: 'function', function: { name: t.name, description: t.description, parameters: t.input_schema } })) : undefined,
377
+ }),
378
+ });
379
+ if (!resp.ok) {
380
+ const text = await resp.text().catch(() => '');
381
+ throw new Error(`OpenRouter API ${resp.status}: ${text.slice(0, 200)}`);
382
+ }
383
+ const data = await resp.json();
384
+ const choice = data.choices?.[0];
385
+ const content = [];
386
+ if (choice?.message?.content) content.push({ type: 'text', text: choice.message.content });
387
+ if (choice?.message?.tool_calls) {
388
+ for (const tc of choice.message.tool_calls) {
389
+ content.push({ type: 'tool_use', id: tc.id, name: tc.function.name, input: JSON.parse(tc.function.arguments || '{}') });
390
+ }
391
+ }
392
+ return { content, stopReason: choice?.finish_reason === 'stop' ? 'end_turn' : 'tool_use' };
393
+ }
394
+
395
+ _buildToolDefs() {
396
+ return TOOL_SCHEMAS;
397
+ }
398
+
399
+ _buildSystemPrompt(context, retrievedContext = null) {
400
+ if (this.systemPromptOverride) {
401
+ return this.systemPromptOverride;
402
+ }
403
+
404
+ const parts = [
405
+ 'You are Orca, an AI coding agent running in local mode.',
406
+ 'You have access to tools for reading, writing, and executing code.',
407
+ 'Use tools to accomplish the user\'s request. Be concise and direct.',
408
+ ];
409
+ if (context.cwd) parts.push(`Working directory: ${context.cwd}`);
410
+ if (context.gitBranch) parts.push(`Git branch: ${context.gitBranch}`);
411
+
412
+ if (retrievedContext && retrievedContext.length > 0) {
413
+ parts.push('');
414
+ parts.push('== RELEVANT CODE CONTEXT (from project index) ==');
415
+ for (const chunk of retrievedContext) {
416
+ parts.push(`--- ${chunk.id} (score: ${chunk.score.toFixed(2)}) ---`);
417
+ parts.push(chunk.text);
418
+ parts.push('');
419
+ }
420
+ parts.push('== END CONTEXT ==');
421
+ parts.push('');
422
+ parts.push('Use the above context to understand the codebase. Read full files with read_file when you need more detail.');
423
+ }
424
+
425
+ return parts.join('\n');
426
+ }
427
+
428
+ cancel() { this._cancelled = true; }
429
+ }