@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,162 @@
1
+ /**
2
+ * Hook Engine — pre/post tool use and stop hooks.
3
+ *
4
+ * Based on Claude Code's hooks system (6 event types):
5
+ * - PreToolUse: can block tool execution
6
+ * - PostToolUse: can modify results
7
+ * - Stop: can prevent the agent from stopping
8
+ * - Notification: inform external systems
9
+ * - PrePrompt: modify user input
10
+ * - PostResponse: modify assistant output
11
+ *
12
+ * Hooks are defined in settings.json under the "hooks" key.
13
+ */
14
+
15
+ import { execSync } from 'child_process';
16
+
17
+ export class HookEngine {
18
+ /**
19
+ * @param {object} hooksConfig - hooks configuration from settings
20
+ */
21
+ constructor(hooksConfig = {}) {
22
+ this.hooks = hooksConfig;
23
+ }
24
+
25
+ /**
26
+ * Run pre-tool-use hooks. Returns { allow, message }.
27
+ * If any hook returns deny, the tool call is blocked.
28
+ *
29
+ * @param {string} toolName - name of the tool being called
30
+ * @param {object} input - tool input arguments
31
+ * @returns {Promise<{allow: boolean, message?: string}>}
32
+ */
33
+ async runPreToolUse(toolName, input) {
34
+ const hooks = this._getHooks('PreToolUse');
35
+ for (const hook of hooks) {
36
+ // Check if hook applies to this tool
37
+ if (hook.toolName && hook.toolName !== toolName) continue;
38
+
39
+ const result = await this._executeHook(hook, {
40
+ event: 'PreToolUse',
41
+ toolName,
42
+ input,
43
+ });
44
+
45
+ if (result?.decision === 'deny' || result?.decision === 'block') {
46
+ return { allow: false, message: result.message || `Blocked by hook: ${hook.name || 'unnamed'}` };
47
+ }
48
+ }
49
+ return { allow: true };
50
+ }
51
+
52
+ /**
53
+ * Run post-tool-use hooks. Can modify the result.
54
+ *
55
+ * @param {string} toolName - name of the tool that was called
56
+ * @param {*} result - tool execution result
57
+ * @returns {Promise<*>} possibly modified result
58
+ */
59
+ async runPostToolUse(toolName, result) {
60
+ const hooks = this._getHooks('PostToolUse');
61
+ let current = result;
62
+ for (const hook of hooks) {
63
+ if (hook.toolName && hook.toolName !== toolName) continue;
64
+
65
+ const hookResult = await this._executeHook(hook, {
66
+ event: 'PostToolUse',
67
+ toolName,
68
+ result: current,
69
+ });
70
+
71
+ if (hookResult?.modifiedResult !== undefined) {
72
+ current = hookResult.modifiedResult;
73
+ }
74
+ }
75
+ return current;
76
+ }
77
+
78
+ /**
79
+ * Run stop hooks. Returns true if stop should proceed, false to continue.
80
+ *
81
+ * @returns {Promise<boolean>} whether to allow stopping
82
+ */
83
+ async runStop() {
84
+ const hooks = this._getHooks('Stop');
85
+ for (const hook of hooks) {
86
+ const result = await this._executeHook(hook, { event: 'Stop' });
87
+ if (result?.preventStop) {
88
+ return false; // do not stop
89
+ }
90
+ }
91
+ return true; // allow stop
92
+ }
93
+
94
+ /**
95
+ * Run notification hooks (fire-and-forget).
96
+ * @param {string} event - notification event name
97
+ * @param {object} data - event data
98
+ */
99
+ async runNotification(event, data) {
100
+ const hooks = this._getHooks('Notification');
101
+ for (const hook of hooks) {
102
+ try {
103
+ await this._executeHook(hook, { event, ...data });
104
+ } catch {
105
+ // Notifications are best-effort
106
+ }
107
+ }
108
+ }
109
+
110
+ /**
111
+ * Get hooks for a given event type.
112
+ * @param {string} eventType
113
+ * @returns {Array}
114
+ */
115
+ _getHooks(eventType) {
116
+ if (!this.hooks || !this.hooks[eventType]) return [];
117
+ const hooks = this.hooks[eventType];
118
+ return Array.isArray(hooks) ? hooks : [hooks];
119
+ }
120
+
121
+ /**
122
+ * Execute a single hook. Supports command (shell) and function hooks.
123
+ *
124
+ * @param {object} hook - hook definition
125
+ * @param {object} context - execution context
126
+ * @returns {Promise<object|null>}
127
+ */
128
+ async _executeHook(hook, context) {
129
+ try {
130
+ if (hook.command) {
131
+ const env = {
132
+ ...process.env,
133
+ HOOK_EVENT: context.event,
134
+ HOOK_TOOL: context.toolName || '',
135
+ HOOK_INPUT: JSON.stringify(context.input || {}),
136
+ };
137
+ const output = execSync(hook.command, {
138
+ encoding: 'utf-8',
139
+ timeout: hook.timeout || 10000,
140
+ env,
141
+ });
142
+ try {
143
+ return JSON.parse(output.trim());
144
+ } catch {
145
+ return { output: output.trim() };
146
+ }
147
+ }
148
+
149
+ if (typeof hook.handler === 'function') {
150
+ return await hook.handler(context);
151
+ }
152
+
153
+ return null;
154
+ } catch (err) {
155
+ if (hook.failOpen !== false) {
156
+ // Default: fail open (allow)
157
+ return null;
158
+ }
159
+ return { decision: 'deny', message: `Hook error: ${err.message}` };
160
+ }
161
+ }
162
+ }
package/src/index.mjs ADDED
@@ -0,0 +1,370 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * @devtarang/orca v1.0.1 — Orca AI Coding Agent CLI
4
+ *
5
+ * Phase 3: Hybrid local/remote/auto + advanced features.
6
+ */
7
+
8
+ // Load .env file from cwd or ~/.orca/.env
9
+ import { readFileSync, existsSync } from 'node:fs';
10
+ import { join } from 'node:path';
11
+ import { homedir } from 'node:os';
12
+
13
+ for (const envPath of [join(process.cwd(), '.env'), join(homedir(), '.orca', '.env')]) {
14
+ if (existsSync(envPath)) {
15
+ for (const line of readFileSync(envPath, 'utf-8').split('\n')) {
16
+ const match = line.match(/^\s*([\w]+)\s*=\s*(.+?)\s*$/);
17
+ if (match && !process.env[match[1]]) {
18
+ process.env[match[1]] = match[2];
19
+ }
20
+ }
21
+ break;
22
+ }
23
+ }
24
+
25
+ import { TarangStreamClient, EVENT_TYPES } from './core/stream-client.mjs';
26
+ import { LocalAgent } from './core/local-agent.mjs';
27
+ import { createToolExecutor } from './core/tool-executor.mjs';
28
+ import { TarangAuth } from './auth/tarang-auth.mjs';
29
+ import { ApprovalManager } from './core/approval.mjs';
30
+ import { SessionManager } from './core/session-manager.mjs';
31
+ import { EventFormatter } from './ui/formatter.mjs';
32
+ import { handleSlashCommand, COMMANDS } from './ui/slash-commands.mjs';
33
+ import { selectMode } from './core/mode-selector.mjs';
34
+ import { printBanner, printProjectInfo, printHints, printAuthStatus, printStyledConfig, printGoodbye } from './ui/banner.mjs';
35
+ import { ContextRetriever } from './context/retriever.mjs';
36
+ import { loadSettings } from './config/settings.mjs';
37
+
38
+ const VERSION = '1.0.1';
39
+
40
+ // ── Arg Parsing (consolidated from index.mjs + cli-args.mjs) ──
41
+
42
+ function parseArgs(argv) {
43
+ const args = {
44
+ // Commands
45
+ command: null, instruction: null,
46
+ // Tarang mode flags
47
+ verbose: false, yes: false, plan: false, strict: false,
48
+ local: false, remote: false, debug: false,
49
+ version: false, help: false,
50
+ // Config subcommand flags
51
+ showConfig: false, openRouterKey: null, anthropicKey: null,
52
+ backendUrl: null, mode: null,
53
+ // Extended flags (from cli-args.mjs)
54
+ permissionMode: null,
55
+ outputFormat: null,
56
+ systemPrompt: null,
57
+ addDirs: [],
58
+ maxTurns: null,
59
+ allowedTools: null,
60
+ disallowedTools: null,
61
+ };
62
+ let i = 0;
63
+ while (i < argv.length) {
64
+ const arg = argv[i];
65
+ switch (arg) {
66
+ // Version / help
67
+ case '--version': case '-V': args.version = true; break;
68
+ case '--help': case '-h': args.help = true; break;
69
+ // Behavior flags
70
+ case '--verbose': case '-v': args.verbose = true; break;
71
+ case '--debug': case '-d': args.debug = true; args.verbose = true; break;
72
+ case '--yes': case '-y': args.yes = true; break;
73
+ case '--plan': args.plan = true; break;
74
+ case '--strict': args.strict = true; break;
75
+ // Mode flags
76
+ case '--local': args.local = true; break;
77
+ case '--remote': args.remote = true; break;
78
+ case '--mode': args.mode = argv[++i]; break;
79
+ // Commands
80
+ case 'login': args.command = 'login'; break;
81
+ case 'resume': args.command = 'resume'; break;
82
+ case 'config': args.command = 'config'; break;
83
+ case 'configure': args.command = 'configure'; break;
84
+ case 'sync': args.command = 'sync'; break;
85
+ // Config flags
86
+ case '--show': args.showConfig = true; break;
87
+ case '--openrouter-key': case '-k': args.openRouterKey = argv[++i]; break;
88
+ case '--anthropic-key': args.anthropicKey = argv[++i]; break;
89
+ case '--openai-key': args.openaiKey = argv[++i]; break;
90
+ case '--google-key': args.googleKey = argv[++i]; break;
91
+ case '--gateway': args.gateway = argv[++i]; break;
92
+ // --backend-url removed: use TARANG_ENV
93
+ // --model removed: use tarang configure (web settings)
94
+ // Extended flags
95
+ case '--permission-mode': args.permissionMode = argv[++i]; break;
96
+ case '--print': case '-p': args.instruction = argv[++i]; break;
97
+ case '--output-format': args.outputFormat = argv[++i]; break;
98
+ case '--system-prompt': args.systemPrompt = argv[++i]; break;
99
+ case '--add-dir': args.addDirs.push(argv[++i]); break;
100
+ case '--max-turns': args.maxTurns = parseInt(argv[++i], 10); break;
101
+ case '--allowedTools': args.allowedTools = argv[++i]?.split(',').map(s => s.trim()); break;
102
+ case '--disallowedTools': args.disallowedTools = argv[++i]?.split(',').map(s => s.trim()); break;
103
+ default:
104
+ if (!arg.startsWith('-') && !args.command && !args.instruction) args.instruction = arg;
105
+ break;
106
+ }
107
+ i++;
108
+ }
109
+ if (!args.verbose && process.env.TARANG_VERBOSE === '1') args.verbose = true;
110
+ if (!args.yes && process.env.TARANG_YES === '1') args.yes = true;
111
+ return args;
112
+ }
113
+
114
+ function printUsage() {
115
+ printBanner();
116
+
117
+ const B = '\x1b[1m', C = '\x1b[36m', D = '\x1b[2m', G = '\x1b[32m', R = '\x1b[0m';
118
+
119
+ process.stderr.write(`${B}USAGE${R}\n`);
120
+ process.stderr.write(` ${C}orca "instruction"${R} Execute instruction\n`);
121
+ process.stderr.write(` ${C}orca${R} Interactive mode (REPL)\n`);
122
+ process.stderr.write(` ${C}orca login${R} Authenticate via GitHub OAuth\n`);
123
+ process.stderr.write(` ${C}orca configure${R} Open settings in browser\n`);
124
+ process.stderr.write(` ${C}orca config --show${R} Display local configuration\n`);
125
+ process.stderr.write(` ${C}orca resume${R} Resume a paused session\n`);
126
+ process.stderr.write('\n');
127
+ process.stderr.write(`${B}MODE FLAGS${R}\n`);
128
+ process.stderr.write(` ${G}--local${R} Direct LLM API ${D}(<100ms, offline)${R}\n`);
129
+ process.stderr.write(` ${G}--remote${R} SSE backend ${D}(multi-agent orchestration)${R}\n`);
130
+ process.stderr.write(` ${G}--mode <auto|local|remote>${R} Set mode explicitly\n`);
131
+ process.stderr.write(` ${D}(default: auto-select based on task complexity)${R}\n`);
132
+ process.stderr.write('\n');
133
+ process.stderr.write(`${B}MODEL FLAGS${R}\n`);
134
+ process.stderr.write(` ${G}--system-prompt <text>${R} Override system prompt\n`);
135
+ process.stderr.write(` ${G}--max-turns <n>${R} Maximum conversation turns\n`);
136
+ process.stderr.write(` ${D}Models are configured via: orca configure${R}\n`);
137
+ process.stderr.write('\n');
138
+ process.stderr.write(`${B}PERMISSION FLAGS${R}\n`);
139
+ process.stderr.write(` ${G}--yes, -y${R} Auto-approve all operations\n`);
140
+ process.stderr.write(` ${G}--plan${R} Read-only mode (block all writes)\n`);
141
+ process.stderr.write(` ${G}--strict${R} Deny tools not in allowed list\n`);
142
+ process.stderr.write(` ${G}--permission-mode <mode>${R} Permission mode ${D}(auto, plan, strict)${R}\n`);
143
+ process.stderr.write(` ${G}--allowedTools <tools>${R} Comma-separated allowed tools\n`);
144
+ process.stderr.write(` ${G}--disallowedTools <tools>${R} Comma-separated denied tools\n`);
145
+ process.stderr.write('\n');
146
+ process.stderr.write(`${B}OUTPUT FLAGS${R}\n`);
147
+ process.stderr.write(` ${G}--print, -p <prompt>${R} Non-interactive: run prompt and exit\n`);
148
+ process.stderr.write(` ${G}--output-format <fmt>${R} Output format: text, json, stream-json\n`);
149
+ process.stderr.write(` ${G}--verbose, -v${R} Show tool details and thinking\n`);
150
+ process.stderr.write(` ${G}--debug, -d${R} Debug mode ${D}(implies verbose)${R}\n`);
151
+ process.stderr.write('\n');
152
+ process.stderr.write(`${B}OTHER FLAGS${R}\n`);
153
+ process.stderr.write(` ${G}--version, -V${R} Show version\n`);
154
+ process.stderr.write(` ${G}--help, -h${R} Show this help\n`);
155
+ process.stderr.write(` ${G}--add-dir <dir>${R} Additional CLAUDE.md directory\n`);
156
+ process.stderr.write('\n');
157
+ process.stderr.write(`${B}SLASH COMMANDS${R} ${D}(interactive mode)${R}\n`);
158
+ for (const [k, v] of Object.entries(COMMANDS)) {
159
+ process.stderr.write(` ${C}${k.padEnd(14)}${R} ${v.description}\n`);
160
+ }
161
+ process.stderr.write('\n');
162
+ }
163
+
164
+ // ── Execute ─────────────────────────────────────────────────
165
+
166
+ async function executeInstruction(executor, instruction, formatter, sessionMgr) {
167
+ sessionMgr.start(instruction);
168
+ for await (const event of executor) {
169
+ formatter.render(event);
170
+ if (event.type === 'session_info') sessionMgr.setSessionInfo(event.data);
171
+ if (event.type === 'tool_call' || event.type === 'tool_request') sessionMgr.recordToolCall(event.data?.tool);
172
+ if (event.type === 'complete') sessionMgr.complete(event.data?.summary);
173
+ if (event.type === 'error' && event.data?.fatal) sessionMgr.fail(event.data?.message);
174
+ if (event.type === 'cancelled') sessionMgr.cancel();
175
+ if (event.type === 'paused') sessionMgr.pause();
176
+ }
177
+ }
178
+
179
+ // ── REPL ────────────────────────────────────────────────────
180
+
181
+ // Inline REPL removed — now delegates to startTerminalRepl() from terminal/repl.mjs
182
+ // which has full markdown rendering, turn summaries, cost display, and ANSI UI.
183
+
184
+ import { startTerminalRepl } from './terminal/repl.mjs';
185
+
186
+ // ── Main ────────────────────────────────────────────────────
187
+
188
+ async function main() {
189
+ const args = parseArgs(process.argv.slice(2));
190
+ if (args.version) { console.log(`@devtarang/orca ${VERSION}`); process.exit(0); }
191
+ if (args.help) { printUsage(); process.exit(0); }
192
+
193
+ const auth = new TarangAuth();
194
+
195
+ if (args.command === 'login') {
196
+ printBanner();
197
+ process.stderr.write('\x1b[1mAuthentication\x1b[0m\n\n');
198
+ await auth.login();
199
+ process.stderr.write('\n\x1b[32m✓ Login successful!\x1b[0m\n');
200
+ // Sync settings from web after login
201
+ try {
202
+ process.stderr.write('\x1b[2mSyncing settings from server...\x1b[0m\n');
203
+ const remote = await auth.syncSettings();
204
+ process.stderr.write(`\x1b[32m✓ Settings synced\x1b[0m ${remote.gateway_type ? `(gateway: ${remote.gateway_type})` : ''}\n`);
205
+ } catch {
206
+ process.stderr.write('\x1b[2mSettings sync skipped — configure at devtarang.ai/dashboard/settings\x1b[0m\n');
207
+ }
208
+ process.stderr.write('\n');
209
+ // Fall through to REPL — user starts working right away
210
+ }
211
+
212
+ if (args.command === 'sync') {
213
+ printBanner();
214
+ process.stderr.write('\x1b[1mSyncing settings...\x1b[0m\n\n');
215
+ try {
216
+ const remote = await auth.syncSettings();
217
+ process.stderr.write(`\x1b[32m✓ Gateway:\x1b[0m ${remote.gateway_type}\n`);
218
+ if (remote.models?.orchestrator) process.stderr.write(`\x1b[32m✓ Orchestrator:\x1b[0m ${remote.models.orchestrator}\n`);
219
+ if (remote.models?.reasoning) process.stderr.write(`\x1b[32m✓ Coding:\x1b[0m ${remote.models.reasoning}\n`);
220
+ if (remote.models?.local) process.stderr.write(`\x1b[32m✓ Local:\x1b[0m ${remote.models.local}\n`);
221
+ if (remote.configured_providers?.length) process.stderr.write(`\x1b[32m✓ Providers:\x1b[0m ${remote.configured_providers.join(', ')}\n`);
222
+ process.stderr.write('\n\x1b[32m✓ Settings saved to ~/.orca/config.json\x1b[0m\n');
223
+ } catch (err) {
224
+ process.stderr.write(`\x1b[31m✗ ${err.message}\x1b[0m\n`);
225
+ }
226
+ process.exit(0);
227
+ }
228
+
229
+ if (args.command === 'configure') {
230
+ printBanner();
231
+ const { resolveWebUrl } = await import('./core/backend-url.mjs');
232
+ const webUrl = resolveWebUrl();
233
+ const settingsUrl = `${webUrl}/dashboard/settings?tab=providers&source=cli`;
234
+ process.stderr.write('\x1b[36mOpening settings in browser...\x1b[0m\n');
235
+ process.stderr.write(`\x1b[2m${settingsUrl}\x1b[0m\n`);
236
+ const openCmd = process.platform === 'darwin' ? 'open' :
237
+ process.platform === 'win32' ? 'start' : 'xdg-open';
238
+ const { exec } = await import('node:child_process');
239
+ exec(`${openCmd} "${settingsUrl}"`, () => {});
240
+ process.stderr.write('\n\x1b[2mConfigure your provider, models, and CLI preferences in the browser.\x1b[0m\n');
241
+ process.stderr.write('\x1b[2mChanges sync automatically to the backend.\x1b[0m\n');
242
+ process.exit(0);
243
+ }
244
+
245
+ if (args.command === 'config') {
246
+ let changed = false;
247
+ if (args.openRouterKey) { auth.saveOpenRouterKey(args.openRouterKey); process.stderr.write('\x1b[32m✓ OpenRouter key saved.\x1b[0m\n'); changed = true; }
248
+ if (args.anthropicKey) { auth.saveAnthropicKey(args.anthropicKey); process.stderr.write('\x1b[32m✓ Anthropic key saved.\x1b[0m\n'); changed = true; }
249
+ if (args.openaiKey) { auth.saveOpenAIKey(args.openaiKey); process.stderr.write('\x1b[32m✓ OpenAI key saved.\x1b[0m\n'); changed = true; }
250
+ if (args.googleKey) { auth.saveGoogleKey(args.googleKey); process.stderr.write('\x1b[32m✓ Google AI key saved.\x1b[0m\n'); changed = true; }
251
+ if (args.gateway) { auth.saveCredentials({ gateway_type: args.gateway }); process.stderr.write(`\x1b[32m✓ Gateway set to ${args.gateway}\x1b[0m\n`); changed = true; }
252
+ if (args.mode) { auth.setMode(args.mode); process.stderr.write(`\x1b[32m✓ Mode set to ${args.mode}\x1b[0m\n`); changed = true; }
253
+ if (args.showConfig || !changed) {
254
+ auth.printConfig();
255
+ }
256
+ process.exit(0);
257
+ }
258
+
259
+ // Load settings (user ~/.claude/settings.json + project .claude/settings.json + local)
260
+ const settings = await loadSettings();
261
+
262
+ /** Re-read credentials from disk (so /login updates take effect). */
263
+ function freshCreds() {
264
+ const c = auth.loadCredentials();
265
+ return {
266
+ token: process.env.TARANG_TOKEN || c.token,
267
+ openRouterKey: process.env.OPENROUTER_API_KEY || c.openRouterKey,
268
+ anthropicKey: process.env.ANTHROPIC_API_KEY || c.anthropicKey,
269
+ openaiKey: process.env.OPENAI_API_KEY || c.openaiKey,
270
+ googleKey: process.env.GOOGLE_API_KEY || c.googleKey,
271
+ backendUrl: c.backendUrl,
272
+ gatewayType: c.gatewayType,
273
+ models: c.models,
274
+ };
275
+ }
276
+
277
+ // Initial load — used for settings and startup checks
278
+ const initCreds = freshCreds();
279
+
280
+ // Apply settings as defaults (CLI flags override settings)
281
+ if (!args.verbose && settings.debugMode) args.verbose = true;
282
+ if (!args.permissionMode && settings.permissions?.defaultMode !== 'default') {
283
+ args.permissionMode = settings.permissions.defaultMode;
284
+ }
285
+
286
+ const toolExecutor = createToolExecutor();
287
+ const approval = new ApprovalManager({ autoApprove: args.yes, planMode: args.plan });
288
+ const formatter = new EventFormatter({ verbose: args.verbose });
289
+ const sessionMgr = new SessionManager();
290
+ const contextRetriever = new ContextRetriever(process.cwd());
291
+
292
+ /** Create an executor (local or remote) for a given instruction. */
293
+ async function createExecutor(instruction, messages = null) {
294
+ // Always re-read credentials so /login changes are picked up
295
+ const creds = freshCreds();
296
+ const mode = await selectMode(instruction, args, { ...creds, backendUrl: creds.backendUrl });
297
+
298
+ if (mode === 'local') {
299
+ // Model priority: CLI flag > synced web setting > settings.json > default
300
+ const localModel = creds.models?.local || settings.model || 'anthropic/claude-sonnet-4-6';
301
+ if (args.verbose) process.stderr.write(`\x1b[2m[mode] local (${localModel})\x1b[0m\n`);
302
+
303
+ // Pick the right API key based on the model/gateway
304
+ let apiKey = creds.anthropicKey;
305
+ let orKey = creds.openRouterKey;
306
+ if (creds.gatewayType === 'openai') apiKey = creds.openaiKey;
307
+ if (creds.gatewayType === 'googleai') apiKey = creds.googleKey;
308
+
309
+ return new LocalAgent({
310
+ apiKey,
311
+ openRouterKey: orKey,
312
+ model: localModel,
313
+ toolExecutor,
314
+ verbose: args.verbose,
315
+ cwd: process.cwd(),
316
+ systemPromptOverride: args.systemPrompt,
317
+ maxTurns: args.maxTurns,
318
+ stagnationDetection: settings.stagnationDetection,
319
+ stagnationThreshold: settings.stagnationThreshold,
320
+ }).execute(instruction, { cwd: process.cwd() });
321
+ } else {
322
+ if (args.verbose) process.stderr.write('\x1b[2m[mode] remote\x1b[0m\n');
323
+
324
+ // Retrieve BM25 context to send to backend
325
+ let indexedContext = {};
326
+ try {
327
+ const chunks = contextRetriever.retrieve(instruction, 8);
328
+ if (chunks.length > 0) {
329
+ indexedContext = {
330
+ indexed: chunks.map(c => ({ id: c.id, score: c.score, text: c.text })),
331
+ };
332
+ if (args.verbose) process.stderr.write(`\x1b[2m[context] ${chunks.length} chunks from BM25 index\x1b[0m\n`);
333
+ }
334
+ } catch {
335
+ // No index available — send without context
336
+ }
337
+
338
+ const client = new TarangStreamClient({
339
+ baseUrl: creds.backendUrl, token: creds.token, toolExecutor,
340
+ verbose: args.verbose, approvalManager: approval,
341
+ });
342
+ // Cancel on SIGINT but don't exit (REPL handles exit)
343
+ const sigHandler = async () => { await client.cancel().catch(() => {}); };
344
+ process.once('SIGINT', sigHandler);
345
+ return client.execute(instruction, { cwd: process.cwd(), ...indexedContext }, messages);
346
+ }
347
+ }
348
+
349
+ if (args.command === 'resume') {
350
+ const state = sessionMgr.loadState();
351
+ if (!state || state.status === 'completed') { process.stderr.write('No resumable session.\n'); process.exit(1); }
352
+ const resumeCreds = freshCreds();
353
+ const client = new TarangStreamClient({ baseUrl: resumeCreds.backendUrl, token: resumeCreds.token, toolExecutor, verbose: args.verbose, approvalManager: approval });
354
+ client.currentTaskId = state.task_id;
355
+ await client.resume();
356
+ for await (const event of client.execute(state.instruction)) formatter.render(event);
357
+ process.exit(0);
358
+ }
359
+
360
+ if (args.instruction) {
361
+ const exec = await createExecutor(args.instruction);
362
+ await executeInstruction(exec, args.instruction, formatter, sessionMgr);
363
+ process.stdout.write('\n');
364
+ process.exit(0);
365
+ }
366
+
367
+ await startTerminalRepl();
368
+ }
369
+
370
+ main().catch(err => { process.stderr.write(`\x1b[31mFatal: ${err.message}\x1b[0m\n`); process.exit(1); });