@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.
- package/README.md +82 -0
- package/package.json +36 -4
- package/pulse/app/activity/page.tsx +190 -0
- package/pulse/app/api/activity/route.ts +138 -0
- package/pulse/app/api/costs/route.ts +88 -0
- package/pulse/app/api/export/route.ts +77 -0
- package/pulse/app/api/history/route.ts +11 -0
- package/pulse/app/api/import/route.ts +31 -0
- package/pulse/app/api/memory/route.ts +52 -0
- package/pulse/app/api/plans/route.ts +9 -0
- package/pulse/app/api/projects/[slug]/route.ts +96 -0
- package/pulse/app/api/projects/route.ts +121 -0
- package/pulse/app/api/sessions/[id]/replay/route.ts +20 -0
- package/pulse/app/api/sessions/[id]/route.ts +31 -0
- package/pulse/app/api/sessions/route.ts +112 -0
- package/pulse/app/api/settings/route.ts +14 -0
- package/pulse/app/api/stats/route.ts +143 -0
- package/pulse/app/api/todos/route.ts +9 -0
- package/pulse/app/api/tools/route.ts +160 -0
- package/pulse/app/costs/page.tsx +179 -0
- package/pulse/app/export/page.tsx +465 -0
- package/pulse/app/favicon.ico +0 -0
- package/pulse/app/globals.css +263 -0
- package/pulse/app/help/page.tsx +142 -0
- package/pulse/app/history/page.tsx +157 -0
- package/pulse/app/layout.tsx +46 -0
- package/pulse/app/memory/page.tsx +365 -0
- package/pulse/app/overview-client.tsx +393 -0
- package/pulse/app/page.tsx +14 -0
- package/pulse/app/plans/page.tsx +308 -0
- package/pulse/app/projects/[slug]/page.tsx +390 -0
- package/pulse/app/projects/page.tsx +110 -0
- package/pulse/app/sessions/[id]/page.tsx +243 -0
- package/pulse/app/sessions/page.tsx +39 -0
- package/pulse/app/settings/page.tsx +188 -0
- package/pulse/app/todos/page.tsx +211 -0
- package/pulse/app/tools/page.tsx +249 -0
- package/pulse/cli.js +159 -0
- package/pulse/components/activity/day-of-week-chart.tsx +35 -0
- package/pulse/components/activity/streak-card.tsx +36 -0
- package/pulse/components/costs/cache-efficiency-panel.tsx +76 -0
- package/pulse/components/costs/cost-by-project-chart.tsx +48 -0
- package/pulse/components/costs/cost-over-time-chart.tsx +95 -0
- package/pulse/components/costs/model-token-table.tsx +60 -0
- package/pulse/components/global-search.tsx +193 -0
- package/pulse/components/keyboard-nav-provider.tsx +23 -0
- package/pulse/components/layout/bottom-nav.tsx +52 -0
- package/pulse/components/layout/client-layout.tsx +31 -0
- package/pulse/components/layout/sidebar-context.tsx +50 -0
- package/pulse/components/layout/sidebar.tsx +182 -0
- package/pulse/components/layout/top-bar.tsx +121 -0
- package/pulse/components/overview/activity-heatmap.tsx +107 -0
- package/pulse/components/overview/conversation-table.tsx +148 -0
- package/pulse/components/overview/model-breakdown-donut.tsx +95 -0
- package/pulse/components/overview/peak-hours-chart.tsx +87 -0
- package/pulse/components/overview/project-activity-donut.tsx +96 -0
- package/pulse/components/overview/stat-card.tsx +102 -0
- package/pulse/components/overview/usage-over-time-chart.tsx +166 -0
- package/pulse/components/projects/project-card.tsx +175 -0
- package/pulse/components/sessions/replay/assistant-markdown.tsx +94 -0
- package/pulse/components/sessions/replay/compaction-card.tsx +25 -0
- package/pulse/components/sessions/replay/session-sidebar.tsx +231 -0
- package/pulse/components/sessions/replay/token-accumulation-chart.tsx +98 -0
- package/pulse/components/sessions/replay/tool-call-badge.tsx +127 -0
- package/pulse/components/sessions/replay/turn-cards.tsx +220 -0
- package/pulse/components/sessions/replay/user-tool-result.tsx +158 -0
- package/pulse/components/sessions/session-badges.tsx +49 -0
- package/pulse/components/sessions/session-table.tsx +299 -0
- package/pulse/components/theme-provider.tsx +44 -0
- package/pulse/components/tools/feature-adoption-table.tsx +58 -0
- package/pulse/components/tools/mcp-server-panel.tsx +45 -0
- package/pulse/components/tools/tool-ranking-chart.tsx +57 -0
- package/pulse/components/tools/version-history-table.tsx +32 -0
- package/pulse/components/ui/alert.tsx +66 -0
- package/pulse/components/ui/badge.tsx +48 -0
- package/pulse/components/ui/breadcrumb.tsx +109 -0
- package/pulse/components/ui/button.tsx +64 -0
- package/pulse/components/ui/calendar.tsx +220 -0
- package/pulse/components/ui/card.tsx +92 -0
- package/pulse/components/ui/command.tsx +158 -0
- package/pulse/components/ui/dialog.tsx +158 -0
- package/pulse/components/ui/input.tsx +21 -0
- package/pulse/components/ui/popover.tsx +89 -0
- package/pulse/components/ui/progress.tsx +31 -0
- package/pulse/components/ui/select.tsx +190 -0
- package/pulse/components/ui/separator.tsx +28 -0
- package/pulse/components/ui/sheet.tsx +143 -0
- package/pulse/components/ui/skeleton.tsx +13 -0
- package/pulse/components/ui/table.tsx +116 -0
- package/pulse/components/ui/tabs.tsx +91 -0
- package/pulse/components/ui/tooltip.tsx +57 -0
- package/pulse/components/use-global-keyboard-nav.ts +79 -0
- package/pulse/components.json +23 -0
- package/pulse/eslint.config.mjs +18 -0
- package/pulse/lib/claude-reader.ts +594 -0
- package/pulse/lib/decode.ts +129 -0
- package/pulse/lib/pricing.ts +102 -0
- package/pulse/lib/replay-parser.ts +165 -0
- package/pulse/lib/tool-categories.ts +127 -0
- package/pulse/lib/utils.ts +6 -0
- package/pulse/next-env.d.ts +6 -0
- package/pulse/next.config.ts +16 -0
- package/pulse/package.json +45 -0
- package/pulse/postcss.config.mjs +7 -0
- package/pulse/public/activity.png +0 -0
- package/pulse/public/cc-lens.png +0 -0
- package/pulse/public/command-k.png +0 -0
- package/pulse/public/costs.png +0 -0
- package/pulse/public/dashboard-dark.png +0 -0
- package/pulse/public/dashboard-white.png +0 -0
- package/pulse/public/export.png +0 -0
- package/pulse/public/file.svg +1 -0
- package/pulse/public/globe.svg +1 -0
- package/pulse/public/next.svg +1 -0
- package/pulse/public/projects.png +0 -0
- package/pulse/public/session-chat.png +0 -0
- package/pulse/public/todos.png +0 -0
- package/pulse/public/tools.png +0 -0
- package/pulse/public/vercel.svg +1 -0
- package/pulse/public/window.svg +1 -0
- package/pulse/tsconfig.json +34 -0
- package/pulse/types/claude.ts +294 -0
- package/src/agents/loader.mjs +89 -0
- package/src/agents/parser.mjs +98 -0
- package/src/agents/teams.mjs +123 -0
- package/src/auth/oauth.mjs +220 -0
- package/src/auth/tarang-auth.mjs +277 -0
- package/src/config/cli-args.mjs +173 -0
- package/src/config/env.mjs +263 -0
- package/src/config/settings.mjs +132 -0
- package/src/context/ast-parser.mjs +298 -0
- package/src/context/bm25.mjs +85 -0
- package/src/context/retriever.mjs +270 -0
- package/src/context/skeleton.mjs +134 -0
- package/src/core/agent-loop.mjs +480 -0
- package/src/core/approval.mjs +273 -0
- package/src/core/backend-url.mjs +57 -0
- package/src/core/cache.mjs +105 -0
- package/src/core/callback-client.mjs +149 -0
- package/src/core/checkpoints.mjs +142 -0
- package/src/core/context-manager.mjs +198 -0
- package/src/core/headless.mjs +168 -0
- package/src/core/hooks-manager.mjs +87 -0
- package/src/core/jsonl-writer.mjs +351 -0
- package/src/core/local-agent.mjs +429 -0
- package/src/core/local-store.mjs +325 -0
- package/src/core/mode-selector.mjs +51 -0
- package/src/core/output-filter.mjs +177 -0
- package/src/core/paths.mjs +98 -0
- package/src/core/pricing.mjs +314 -0
- package/src/core/providers.mjs +219 -0
- package/src/core/rate-limiter.mjs +119 -0
- package/src/core/safety.mjs +200 -0
- package/src/core/scheduler.mjs +173 -0
- package/src/core/session-manager.mjs +317 -0
- package/src/core/session.mjs +143 -0
- package/src/core/settings-sync.mjs +85 -0
- package/src/core/stagnation.mjs +57 -0
- package/src/core/stream-client.mjs +367 -0
- package/src/core/streaming.mjs +182 -0
- package/src/core/system-prompt.mjs +135 -0
- package/src/core/tool-executor.mjs +725 -0
- package/src/hooks/engine.mjs +162 -0
- package/src/index.mjs +370 -0
- package/src/mcp/client.mjs +253 -0
- package/src/mcp/transport-shttp.mjs +130 -0
- package/src/mcp/transport-sse.mjs +131 -0
- package/src/mcp/transport-ws.mjs +134 -0
- package/src/permissions/checker.mjs +57 -0
- package/src/permissions/command-classifier.mjs +573 -0
- package/src/permissions/injection-check.mjs +60 -0
- package/src/permissions/path-check.mjs +102 -0
- package/src/permissions/prompt.mjs +73 -0
- package/src/permissions/sandbox.mjs +112 -0
- package/src/plugins/loader.mjs +138 -0
- package/src/skills/loader.mjs +147 -0
- package/src/skills/runner.mjs +55 -0
- package/src/telemetry/index.mjs +96 -0
- package/src/terminal/agents.mjs +177 -0
- package/src/terminal/analytics.mjs +292 -0
- package/src/terminal/ansi.mjs +421 -0
- package/src/terminal/main.mjs +150 -0
- package/src/terminal/repl.mjs +1484 -0
- package/src/terminal/tool-display.mjs +58 -0
- package/src/tools/agent.mjs +137 -0
- package/src/tools/ask-user.mjs +61 -0
- package/src/tools/bash.mjs +148 -0
- package/src/tools/cron-create.mjs +120 -0
- package/src/tools/cron-delete.mjs +49 -0
- package/src/tools/cron-list.mjs +37 -0
- package/src/tools/edit.mjs +82 -0
- package/src/tools/enter-worktree.mjs +69 -0
- package/src/tools/exit-worktree.mjs +57 -0
- package/src/tools/glob.mjs +117 -0
- package/src/tools/grep.mjs +129 -0
- package/src/tools/lint.mjs +71 -0
- package/src/tools/ls.mjs +58 -0
- package/src/tools/lsp.mjs +115 -0
- package/src/tools/multi-edit.mjs +94 -0
- package/src/tools/notebook-edit.mjs +96 -0
- package/src/tools/read-mcp-resource.mjs +57 -0
- package/src/tools/read.mjs +138 -0
- package/src/tools/registry.mjs +132 -0
- package/src/tools/remote-trigger.mjs +84 -0
- package/src/tools/send-message.mjs +64 -0
- package/src/tools/skill.mjs +52 -0
- package/src/tools/test-runner.mjs +49 -0
- package/src/tools/todo-write.mjs +68 -0
- package/src/tools/tool-search.mjs +77 -0
- package/src/tools/web-fetch.mjs +65 -0
- package/src/tools/web-search.mjs +89 -0
- package/src/tools/write.mjs +55 -0
- package/src/ui/banner.mjs +237 -0
- package/src/ui/commands.mjs +499 -0
- package/src/ui/formatter.mjs +379 -0
- package/src/ui/markdown.mjs +278 -0
- package/src/ui/slash-commands.mjs +258 -0
- 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
|
+
* Orca Authentication — GitHub OAuth + config management.
|
|
3
|
+
* Reads/writes ~/.orca/config.json (shared with Python CLI).
|
|
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 = path.join(os.homedir(), '.orca');
|
|
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 ~/.orca/ 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 `orca 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}Orca 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}orca sync${RESET}${DIM} to sync settings from web.${RESET}\n`);
|
|
192
|
+
process.stderr.write(` ${DIM}Run ${RESET}${CYAN}orca 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 ~/.orca/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
|
+
}
|