@axplusb/kepler 0.0.1 → 1.0.1

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 +101 -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,220 @@
1
+ /**
2
+ * OAuth Client — PKCE OAuth flow for Anthropic and other providers.
3
+ *
4
+ * Supports:
5
+ * - Device code flow (for headless environments)
6
+ * - Authorization code + PKCE
7
+ * - Token refresh
8
+ * - Credential storage in ~/.claude/credentials
9
+ */
10
+
11
+ import crypto from 'crypto';
12
+ import fs from 'fs';
13
+ import path from 'path';
14
+ import os from 'os';
15
+
16
+ export class OAuthClient {
17
+ /**
18
+ * @param {string} clientId - OAuth client ID
19
+ * @param {object} [options]
20
+ * @param {string} [options.authUrl] - authorization endpoint
21
+ * @param {string} [options.tokenUrl] - token endpoint
22
+ * @param {string} [options.deviceUrl] - device authorization endpoint
23
+ * @param {string} [options.credentialsPath] - path to store credentials
24
+ */
25
+ constructor(clientId, options = {}) {
26
+ this.clientId = clientId;
27
+ this.authUrl = options.authUrl || 'https://console.anthropic.com/oauth/authorize';
28
+ this.tokenUrl = options.tokenUrl || 'https://console.anthropic.com/oauth/token';
29
+ this.deviceUrl = options.deviceUrl || 'https://console.anthropic.com/oauth/device';
30
+ this.credentialsPath = options.credentialsPath ||
31
+ path.join(os.homedir(), '.claude', 'credentials');
32
+ }
33
+
34
+ /**
35
+ * Generate a PKCE code verifier and challenge.
36
+ * @returns {{ verifier: string, challenge: string }}
37
+ */
38
+ generatePKCE() {
39
+ const verifier = crypto.randomBytes(32)
40
+ .toString('base64url')
41
+ .replace(/[^a-zA-Z0-9]/g, '')
42
+ .substring(0, 128);
43
+
44
+ const challenge = crypto
45
+ .createHash('sha256')
46
+ .update(verifier)
47
+ .digest('base64url');
48
+
49
+ return { verifier, challenge };
50
+ }
51
+
52
+ /**
53
+ * Get the authorization URL for the PKCE flow.
54
+ * @param {object} [options]
55
+ * @param {string} [options.redirectUri] - redirect URI
56
+ * @param {string} [options.scope] - requested scope
57
+ * @returns {{ url: string, verifier: string, state: string }}
58
+ */
59
+ getAuthorizationUrl(options = {}) {
60
+ const { verifier, challenge } = this.generatePKCE();
61
+ const state = crypto.randomBytes(16).toString('hex');
62
+
63
+ const params = new URLSearchParams({
64
+ client_id: this.clientId,
65
+ response_type: 'code',
66
+ code_challenge: challenge,
67
+ code_challenge_method: 'S256',
68
+ state,
69
+ redirect_uri: options.redirectUri || 'http://localhost:9876/callback',
70
+ ...(options.scope && { scope: options.scope }),
71
+ });
72
+
73
+ return {
74
+ url: `${this.authUrl}?${params.toString()}`,
75
+ verifier,
76
+ state,
77
+ };
78
+ }
79
+
80
+ /**
81
+ * Start a device code flow (for headless environments).
82
+ * @returns {Promise<{ device_code: string, user_code: string, verification_uri: string, interval: number, expires_in: number }>}
83
+ */
84
+ async startDeviceFlow() {
85
+ const body = new URLSearchParams({
86
+ client_id: this.clientId,
87
+ });
88
+
89
+ const res = await fetch(this.deviceUrl, {
90
+ method: 'POST',
91
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
92
+ body: body.toString(),
93
+ });
94
+
95
+ if (!res.ok) {
96
+ const text = await res.text();
97
+ throw new Error(`Device flow failed (${res.status}): ${text}`);
98
+ }
99
+
100
+ return res.json();
101
+ }
102
+
103
+ /**
104
+ * Exchange an authorization code for tokens (PKCE flow).
105
+ * @param {string} code - authorization code
106
+ * @param {string} verifier - PKCE code verifier
107
+ * @param {string} [redirectUri]
108
+ * @returns {Promise<{ access_token: string, refresh_token?: string, expires_in: number }>}
109
+ */
110
+ async exchangeCode(code, verifier, redirectUri) {
111
+ const body = new URLSearchParams({
112
+ grant_type: 'authorization_code',
113
+ client_id: this.clientId,
114
+ code,
115
+ code_verifier: verifier,
116
+ redirect_uri: redirectUri || 'http://localhost:9876/callback',
117
+ });
118
+
119
+ const res = await fetch(this.tokenUrl, {
120
+ method: 'POST',
121
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
122
+ body: body.toString(),
123
+ });
124
+
125
+ if (!res.ok) {
126
+ const text = await res.text();
127
+ throw new Error(`Token exchange failed (${res.status}): ${text}`);
128
+ }
129
+
130
+ const token = await res.json();
131
+ this.saveToken(token);
132
+ return token;
133
+ }
134
+
135
+ /**
136
+ * Refresh an access token using a refresh token.
137
+ * @param {string} refreshToken
138
+ * @returns {Promise<{ access_token: string, refresh_token?: string, expires_in: number }>}
139
+ */
140
+ async refreshToken(refreshToken) {
141
+ const body = new URLSearchParams({
142
+ grant_type: 'refresh_token',
143
+ client_id: this.clientId,
144
+ refresh_token: refreshToken,
145
+ });
146
+
147
+ const res = await fetch(this.tokenUrl, {
148
+ method: 'POST',
149
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
150
+ body: body.toString(),
151
+ });
152
+
153
+ if (!res.ok) {
154
+ const text = await res.text();
155
+ throw new Error(`Token refresh failed (${res.status}): ${text}`);
156
+ }
157
+
158
+ const token = await res.json();
159
+ this.saveToken(token);
160
+ return token;
161
+ }
162
+
163
+ /**
164
+ * Get stored token from credentials file.
165
+ * @returns {object|null}
166
+ */
167
+ getStoredToken() {
168
+ try {
169
+ const raw = fs.readFileSync(this.credentialsPath, 'utf-8');
170
+ return JSON.parse(raw);
171
+ } catch {
172
+ return null;
173
+ }
174
+ }
175
+
176
+ /**
177
+ * Save a token to the credentials file.
178
+ * @param {object} token
179
+ */
180
+ saveToken(token) {
181
+ try {
182
+ const dir = path.dirname(this.credentialsPath);
183
+ fs.mkdirSync(dir, { recursive: true });
184
+
185
+ const data = {
186
+ ...token,
187
+ saved_at: new Date().toISOString(),
188
+ };
189
+
190
+ fs.writeFileSync(this.credentialsPath, JSON.stringify(data, null, 2), { mode: 0o600 });
191
+ } catch {
192
+ // Best effort
193
+ }
194
+ }
195
+
196
+ /**
197
+ * Delete stored credentials.
198
+ */
199
+ clearToken() {
200
+ try {
201
+ fs.unlinkSync(this.credentialsPath);
202
+ return true;
203
+ } catch {
204
+ return false;
205
+ }
206
+ }
207
+
208
+ /**
209
+ * Check if the stored token is expired.
210
+ * @returns {boolean}
211
+ */
212
+ isTokenExpired() {
213
+ const token = this.getStoredToken();
214
+ if (!token || !token.saved_at || !token.expires_in) return true;
215
+
216
+ const savedAt = new Date(token.saved_at).getTime();
217
+ const expiresAt = savedAt + (token.expires_in * 1000);
218
+ return Date.now() >= expiresAt;
219
+ }
220
+ }
@@ -0,0 +1,277 @@
1
+ /**
2
+ * Kepler Authentication — GitHub OAuth + config management.
3
+ * Reads/writes ~/.kepler/config.json.
4
+ */
5
+
6
+ import * as fs from 'node:fs';
7
+ import * as path from 'node:path';
8
+ import * as os from 'node:os';
9
+ import * as http from 'node:http';
10
+ import { getLoginSuccessHTML } from '../ui/banner.mjs';
11
+ import { resolveBackendUrl } from '../core/backend-url.mjs';
12
+
13
+ const CONFIG_DIR = process.env.KEPLER_HOME || process.env.ORCA_HOME || path.join(os.homedir(), '.kepler');
14
+ const CONFIG_PATH = path.join(CONFIG_DIR, 'config.json');
15
+
16
+ export class TarangAuth {
17
+ constructor() {
18
+ this._config = null;
19
+ }
20
+
21
+ /** Ensure ~/.kepler/ directory exists with secure permissions. */
22
+ _ensureConfigDir() {
23
+ if (!fs.existsSync(CONFIG_DIR)) {
24
+ fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
25
+ }
26
+ }
27
+
28
+ /** Load credentials and settings from config.json. */
29
+ loadCredentials() {
30
+ try {
31
+ if (fs.existsSync(CONFIG_PATH)) {
32
+ const raw = fs.readFileSync(CONFIG_PATH, 'utf-8');
33
+ this._config = JSON.parse(raw);
34
+ } else {
35
+ this._config = {};
36
+ }
37
+ } catch {
38
+ this._config = {};
39
+ }
40
+ return {
41
+ token: this._config.token || null,
42
+ openRouterKey: this._config.openrouter_key || process.env.OPENROUTER_API_KEY || null,
43
+ anthropicKey: this._config.anthropic_api_key || process.env.ANTHROPIC_API_KEY || null,
44
+ openaiKey: this._config.openai_api_key || process.env.OPENAI_API_KEY || null,
45
+ googleKey: this._config.google_api_key || process.env.GOOGLE_API_KEY || null,
46
+ backendUrl: resolveBackendUrl(),
47
+ mode: this._config.mode || 'auto',
48
+ gatewayType: this._config.gateway_type || 'openrouter',
49
+ models: this._config.models || {},
50
+ configuredProviders: this._config.configured_providers || [],
51
+ gatewayConfig: this._config.gateway_config || {},
52
+ };
53
+ }
54
+
55
+ /** Get the raw config object. */
56
+ getRawConfig() {
57
+ if (!this._config) this.loadCredentials();
58
+ return this._config || {};
59
+ }
60
+
61
+ /** Clear credentials — remove token and keys from config. */
62
+ logout() {
63
+ try {
64
+ if (fs.existsSync(CONFIG_PATH)) {
65
+ fs.unlinkSync(CONFIG_PATH);
66
+ }
67
+ this._config = null;
68
+ return true;
69
+ } catch {
70
+ return false;
71
+ }
72
+ }
73
+
74
+ /** Save credentials atomically (temp-file + rename). */
75
+ saveCredentials(updates) {
76
+ this._ensureConfigDir();
77
+ const current = this._config || {};
78
+ const merged = { ...current, ...updates };
79
+ const tmpPath = `${CONFIG_PATH}.tmp.${process.pid}`;
80
+ fs.writeFileSync(tmpPath, JSON.stringify(merged, null, 2), { mode: 0o600 });
81
+ fs.renameSync(tmpPath, CONFIG_PATH);
82
+ this._config = merged;
83
+ }
84
+
85
+ /** Check if user has a valid auth token. */
86
+ isAuthenticated() {
87
+ const creds = this.loadCredentials();
88
+ return !!creds.token;
89
+ }
90
+
91
+ /** Check if OpenRouter key is configured. */
92
+ hasOpenRouterKey() {
93
+ const creds = this.loadCredentials();
94
+ return !!creds.openRouterKey;
95
+ }
96
+
97
+ /** Save an API key by provider name. */
98
+ saveProviderKey(provider, key) {
99
+ const keyMap = {
100
+ openrouter: 'openrouter_key',
101
+ anthropic: 'anthropic_api_key',
102
+ openai: 'openai_api_key',
103
+ googleai: 'google_api_key',
104
+ azureopenai: 'azure_api_key',
105
+ bedrock: 'aws_access_key',
106
+ databricks: 'databricks_token',
107
+ };
108
+ const field = keyMap[provider];
109
+ if (!field) throw new Error(`Unknown provider: ${provider}`);
110
+ this.saveCredentials({ [field]: key });
111
+ }
112
+
113
+ /** Save OpenRouter API key. */
114
+ saveOpenRouterKey(key) {
115
+ this.saveProviderKey('openrouter', key);
116
+ }
117
+
118
+ /** Save Anthropic API key. */
119
+ saveAnthropicKey(key) {
120
+ this.saveProviderKey('anthropic', key);
121
+ }
122
+
123
+ /** Save OpenAI API key. */
124
+ saveOpenAIKey(key) {
125
+ this.saveProviderKey('openai', key);
126
+ }
127
+
128
+ /** Save Google AI API key. */
129
+ saveGoogleKey(key) {
130
+ this.saveProviderKey('googleai', key);
131
+ }
132
+
133
+ /** Sync settings from web backend and save locally. */
134
+ async syncSettings() {
135
+ const { fetchRemoteSettings, mergeRemoteSettings } = await import('../core/settings-sync.mjs');
136
+ const creds = this.loadCredentials();
137
+ if (!creds.token) throw new Error('Not logged in. Run `kepler login` first.');
138
+
139
+ const remote = await fetchRemoteSettings(creds.token);
140
+ if (!remote) throw new Error('Failed to fetch settings from server.');
141
+
142
+ const merged = mergeRemoteSettings(this.getRawConfig(), remote);
143
+ this.saveCredentials(merged);
144
+ return remote;
145
+ }
146
+
147
+ /** Set default mode. */
148
+ setMode(mode) {
149
+ const valid = ['local', 'remote', 'auto'];
150
+ if (!valid.includes(mode)) {
151
+ throw new Error(`Invalid mode: ${mode}. Must be one of: ${valid.join(', ')}`);
152
+ }
153
+ this.saveCredentials({ mode });
154
+ }
155
+
156
+ /** Display config (styled). */
157
+ printConfig() {
158
+ const creds = this.loadCredentials();
159
+ const GREEN = '\x1b[32m', RED = '\x1b[31m', DIM = '\x1b[2m', BOLD = '\x1b[1m', CYAN = '\x1b[36m', RESET = '\x1b[0m';
160
+ const check = `${GREEN}\u2713${RESET}`;
161
+ const cross = `${RED}\u2717${RESET}`;
162
+
163
+ const env = process.env.TARANG_ENV || process.env.NODE_ENV || 'production';
164
+
165
+ process.stderr.write(`\n${BOLD}Kepler Configuration${RESET}\n`);
166
+ process.stderr.write(`${'─'.repeat(50)}\n`);
167
+ process.stderr.write(` Auth: ${creds.token ? `${check} logged in` : `${cross} not logged in ${DIM}(/login)${RESET}`}\n`);
168
+ process.stderr.write(` Environment: ${DIM}${env}${RESET}\n`);
169
+ process.stderr.write(` Backend: ${DIM}${creds.backendUrl}${RESET}\n`);
170
+ process.stderr.write(` Mode: ${DIM}${creds.mode || 'auto'}${RESET}\n`);
171
+ process.stderr.write(` Gateway: ${DIM}${creds.gatewayType}${RESET}\n`);
172
+
173
+ const models = creds.models || {};
174
+ if (models.orchestrator || models.reasoning || models.local) {
175
+ process.stderr.write(`\n${BOLD} Models${RESET}\n`);
176
+ if (models.orchestrator) process.stderr.write(` Orchestrator: ${DIM}${models.orchestrator}${RESET}\n`);
177
+ if (models.reasoning) process.stderr.write(` Coding: ${DIM}${models.reasoning}${RESET}\n`);
178
+ if (models.local) process.stderr.write(` Local: ${DIM}${models.local}${RESET}\n`);
179
+ }
180
+
181
+ const providers = creds.configuredProviders || [];
182
+ if (providers.length > 0) {
183
+ process.stderr.write(` Providers: ${DIM}${providers.join(', ')}${RESET}\n`);
184
+ }
185
+
186
+ const raw = this.getRawConfig();
187
+ if (raw.last_synced_at) {
188
+ process.stderr.write(` Last synced: ${DIM}${new Date(raw.last_synced_at).toLocaleString()}${RESET}\n`);
189
+ }
190
+
191
+ process.stderr.write(`\n ${DIM}Run ${RESET}${CYAN}kepler sync${RESET}${DIM} to sync settings from web.${RESET}\n`);
192
+ process.stderr.write(` ${DIM}Run ${RESET}${CYAN}kepler configure${RESET}${DIM} to open settings in browser.${RESET}\n`);
193
+ process.stderr.write('\n');
194
+ }
195
+
196
+ /**
197
+ * Run login flow via web app.
198
+ *
199
+ * Flow:
200
+ * 1. CLI starts local HTTP server on random port
201
+ * 2. Opens browser to web /auth/cli?callback=http://127.0.0.1:{port}/callback
202
+ * 3. Web checks Supabase session (if none → GitHub OAuth → Supabase)
203
+ * 4. Web generates CLI token via /api/cli/token
204
+ * 5. Web redirects browser to CLI callback with token
205
+ * 6. CLI receives token, saves to ~/.kepler/config.json
206
+ */
207
+ async login() {
208
+ const { resolveWebUrl } = await import('../core/backend-url.mjs');
209
+ const webUrl = resolveWebUrl();
210
+
211
+ return new Promise((resolve, reject) => {
212
+ const server = http.createServer(async (req, res) => {
213
+ const url = new URL(req.url, `http://localhost`);
214
+
215
+ // The web app redirects here with ?token=<cli_token>
216
+ const token = url.searchParams.get('token');
217
+
218
+ if (!token) {
219
+ // Maybe an error or missing token
220
+ const error = url.searchParams.get('error');
221
+ if (error) {
222
+ res.writeHead(200, { 'Content-Type': 'text/html' });
223
+ res.end(`<html><body><h2>Login failed</h2><p>${error}</p></body></html>`);
224
+ server.close();
225
+ reject(new Error(error));
226
+ return;
227
+ }
228
+ // Ignore other requests (favicon, etc.)
229
+ res.writeHead(200);
230
+ res.end('');
231
+ return;
232
+ }
233
+
234
+ // Save the CLI token
235
+ this.saveCredentials({ token });
236
+
237
+ res.writeHead(200, { 'Content-Type': 'text/html' });
238
+ res.end(getLoginSuccessHTML());
239
+
240
+ server.close();
241
+ resolve(true);
242
+ });
243
+
244
+ server.listen(0, '127.0.0.1', () => {
245
+ const port = server.address().port;
246
+ const callbackUrl = `http://127.0.0.1:${port}/callback`;
247
+ const authUrl = `${webUrl}/auth/cli?callback=${encodeURIComponent(callbackUrl)}`;
248
+
249
+ process.stderr.write(`\n\x1b[36mOpening browser for login...\x1b[0m\n`);
250
+ process.stderr.write(`\x1b[2mIf browser doesn't open, visit:\x1b[0m\n \x1b[4m${authUrl}\x1b[0m\n\n`);
251
+
252
+ // Open browser
253
+ const openCmd = process.platform === 'darwin' ? 'open' :
254
+ process.platform === 'win32' ? 'start' : 'xdg-open';
255
+ import('node:child_process').then(({ exec }) => {
256
+ exec(`${openCmd} "${authUrl}"`, () => {});
257
+ });
258
+ });
259
+
260
+ // Timeout after 120s
261
+ setTimeout(() => {
262
+ server.close();
263
+ reject(new Error('Login timed out after 120s'));
264
+ }, 120_000);
265
+ });
266
+ }
267
+
268
+ /**
269
+ * Ensure user is authenticated, prompt login if not.
270
+ */
271
+ async ensureAuth() {
272
+ if (!this.isAuthenticated()) {
273
+ process.stderr.write('\x1b[33mNot logged in.\x1b[0m Starting login flow...\n');
274
+ await this.login();
275
+ }
276
+ }
277
+ }
@@ -0,0 +1,173 @@
1
+ /**
2
+ * CLI Argument Parser — supports all major Claude Code flags.
3
+ *
4
+ * Flags:
5
+ * --model, -m Model to use
6
+ * --permission-mode Permission mode
7
+ * --print, -p Print mode (non-interactive prompt)
8
+ * --output-format json, text, stream-json
9
+ * --system-prompt Override system prompt
10
+ * --add-dir Additional CLAUDE.md directories
11
+ * --max-turns Maximum conversation turns
12
+ * --allowedTools Comma-separated allowed tools
13
+ * --disallowedTools Comma-separated denied tools
14
+ * --verbose, -v Verbose output
15
+ * --debug, -d Debug mode
16
+ * --version Show version
17
+ * --help, -h Show help
18
+ */
19
+
20
+ export function parseArgs(args) {
21
+ const result = {
22
+ prompt: null,
23
+ model: null,
24
+ permissionMode: null,
25
+ outputFormat: null,
26
+ systemPrompt: null,
27
+ addDirs: [],
28
+ maxTurns: null,
29
+ allowedTools: null,
30
+ disallowedTools: null,
31
+ resume: false,
32
+ resumeSessionId: null,
33
+ headless: false,
34
+ freeswim: false,
35
+ verbose: false,
36
+ debug: false,
37
+ showVersion: false,
38
+ showHelp: false,
39
+ };
40
+
41
+ for (let i = 0; i < args.length; i++) {
42
+ const arg = args[i];
43
+
44
+ switch (arg) {
45
+ case '--model':
46
+ case '-m':
47
+ result.model = args[++i];
48
+ break;
49
+
50
+ case '--permission-mode':
51
+ result.permissionMode = args[++i];
52
+ break;
53
+
54
+ case '--print':
55
+ case '-p':
56
+ result.prompt = args[++i];
57
+ break;
58
+
59
+ case '--output-format':
60
+ result.outputFormat = args[++i];
61
+ break;
62
+
63
+ case '--system-prompt':
64
+ result.systemPrompt = args[++i];
65
+ break;
66
+
67
+ case '--add-dir':
68
+ result.addDirs.push(args[++i]);
69
+ break;
70
+
71
+ case '--max-turns':
72
+ result.maxTurns = parseInt(args[++i], 10);
73
+ break;
74
+
75
+ case '--allowedTools':
76
+ result.allowedTools = args[++i]?.split(',').map(s => s.trim());
77
+ break;
78
+
79
+ case '--disallowedTools':
80
+ result.disallowedTools = args[++i]?.split(',').map(s => s.trim());
81
+ break;
82
+
83
+ case '--resume':
84
+ case '--continue':
85
+ case '-r': {
86
+ result.resume = true;
87
+ // Optional: --resume <sessionId>
88
+ const next = args[i + 1];
89
+ if (next && !next.startsWith('-')) {
90
+ result.resumeSessionId = next;
91
+ i++;
92
+ }
93
+ break;
94
+ }
95
+
96
+ case '--headless':
97
+ result.headless = true;
98
+ result.freeswim = true; // headless implies skip permissions
99
+ break;
100
+
101
+ case '--freeswim-open-waters':
102
+ case '--freeswim':
103
+ case '--yes':
104
+ result.freeswim = true;
105
+ break;
106
+
107
+ case '--verbose':
108
+ case '-v':
109
+ result.verbose = true;
110
+ break;
111
+
112
+ case '--debug':
113
+ case '-d':
114
+ result.debug = true;
115
+ break;
116
+
117
+ case '--version':
118
+ result.showVersion = true;
119
+ break;
120
+
121
+ case '--help':
122
+ case '-h':
123
+ result.showHelp = true;
124
+ break;
125
+
126
+ default:
127
+ // Bare argument becomes prompt
128
+ if (!arg.startsWith('-')) {
129
+ result.prompt = arg;
130
+ }
131
+ break;
132
+ }
133
+ }
134
+
135
+ return result;
136
+ }
137
+
138
+ /**
139
+ * Print usage/help text.
140
+ * @returns {string}
141
+ */
142
+ export function getUsageText() {
143
+ return `
144
+ Usage: occ [options] [prompt]
145
+
146
+ Options:
147
+ --model, -m <model> Model to use (default: claude-sonnet-4-6)
148
+ --permission-mode <mode> Permission mode (bypassPermissions, acceptEdits, plan, auto, dontAsk)
149
+ --print, -p <prompt> Non-interactive mode: run prompt and exit
150
+ --output-format <fmt> Output format: text, json, stream-json
151
+ --system-prompt <text> Override system prompt
152
+ --add-dir <dir> Additional directory to search for CLAUDE.md
153
+ --max-turns <n> Maximum conversation turns
154
+ --allowedTools <tools> Comma-separated list of allowed tools
155
+ --disallowedTools <tools> Comma-separated list of denied tools
156
+ --resume, -r [sessionId] Resume last session (or specific session)
157
+ --continue Alias for --resume
158
+ --headless Non-interactive mode: auto-approve, JSONL output
159
+ --freeswim-open-waters Skip all approval prompts (no boundaries)
160
+ --freeswim Alias for --freeswim-open-waters
161
+ --yes Alias for --freeswim-open-waters
162
+ --verbose, -v Verbose output
163
+ --debug, -d Debug mode
164
+ --version Show version
165
+ --help, -h Show this help
166
+
167
+ Examples:
168
+ occ Start interactive REPL
169
+ occ -p "What is 2+2?" Run prompt and exit
170
+ occ -m claude-haiku-4-5 Use Haiku model
171
+ occ --debug -p "Fix bug" Debug mode with prompt
172
+ `.trim();
173
+ }