@edgedive/cli 0.2.1 → 0.3.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 (53) hide show
  1. package/dist/api/client.d.ts +12 -0
  2. package/dist/api/client.js +40 -0
  3. package/dist/commands/local.d.ts +3 -1
  4. package/dist/commands/local.js +31 -3
  5. package/dist/constants.js +1 -1
  6. package/dist/index.js +2 -0
  7. package/package.json +1 -1
  8. package/.env +0 -2
  9. package/.env.local +0 -2
  10. package/.turbo/turbo-build.log +0 -4
  11. package/.turbo/turbo-dev.log +0 -8
  12. package/.turbo/turbo-typecheck.log +0 -4
  13. package/AGENTS.md +0 -135
  14. package/CLAUDE.md +0 -3
  15. package/dist/api/client.d.ts.map +0 -1
  16. package/dist/api/client.js.map +0 -1
  17. package/dist/auth/oauth-flow.d.ts.map +0 -1
  18. package/dist/auth/oauth-flow.js.map +0 -1
  19. package/dist/auth/pkce.d.ts.map +0 -1
  20. package/dist/auth/pkce.js.map +0 -1
  21. package/dist/commands/local.d.ts.map +0 -1
  22. package/dist/commands/local.js.map +0 -1
  23. package/dist/commands/login.d.ts.map +0 -1
  24. package/dist/commands/login.js.map +0 -1
  25. package/dist/commands/logout.d.ts.map +0 -1
  26. package/dist/commands/logout.js.map +0 -1
  27. package/dist/commands/takeover.d.ts.map +0 -1
  28. package/dist/commands/takeover.js.map +0 -1
  29. package/dist/config/config-manager.d.ts.map +0 -1
  30. package/dist/config/config-manager.js.map +0 -1
  31. package/dist/constants.d.ts.map +0 -1
  32. package/dist/constants.js.map +0 -1
  33. package/dist/index.d.ts.map +0 -1
  34. package/dist/index.js.map +0 -1
  35. package/dist/utils/claude-launcher.d.ts.map +0 -1
  36. package/dist/utils/claude-launcher.js.map +0 -1
  37. package/dist/utils/git-utils.d.ts.map +0 -1
  38. package/dist/utils/git-utils.js.map +0 -1
  39. package/dist/utils/session-downloader.d.ts.map +0 -1
  40. package/dist/utils/session-downloader.js.map +0 -1
  41. package/src/api/client.ts +0 -202
  42. package/src/auth/oauth-flow.ts +0 -278
  43. package/src/auth/pkce.ts +0 -27
  44. package/src/commands/local.ts +0 -286
  45. package/src/commands/login.ts +0 -48
  46. package/src/commands/logout.ts +0 -29
  47. package/src/config/config-manager.ts +0 -120
  48. package/src/constants.ts +0 -34
  49. package/src/index.ts +0 -62
  50. package/src/utils/claude-launcher.ts +0 -94
  51. package/src/utils/git-utils.ts +0 -179
  52. package/src/utils/session-downloader.ts +0 -56
  53. package/tsconfig.json +0 -20
package/src/api/client.ts DELETED
@@ -1,202 +0,0 @@
1
- /**
2
- * Edgedive API client for making authenticated requests
3
- */
4
-
5
- import axios, { AxiosInstance } from 'axios';
6
- import { API_CONFIG, TIMEOUTS } from '../constants.js';
7
- import { ConfigManager } from '../config/config-manager.js';
8
- import { OAuthFlow } from '../auth/oauth-flow.js';
9
-
10
- export interface TakeoverResponse {
11
- session_id: string;
12
- tenant_id: string;
13
- repository: {
14
- owner: string;
15
- name: string;
16
- branch: string;
17
- base_branch: string;
18
- };
19
- github_pr: {
20
- owner: string;
21
- name: string;
22
- number: number;
23
- url: string;
24
- };
25
- linear_issue?: {
26
- id: string;
27
- identifier: string;
28
- url: string;
29
- };
30
- asana_task?: {
31
- gid: string;
32
- url: string;
33
- };
34
- status: string;
35
- agent_type: string;
36
- download_urls: {
37
- claude_session?: string;
38
- session_metadata?: string;
39
- messages?: string;
40
- };
41
- expires_in: number;
42
- created_at: string;
43
- updated_at: string;
44
- }
45
-
46
- export class EdgediveApiClient {
47
- private client: AxiosInstance;
48
- private configManager: ConfigManager;
49
- private oauthFlow: OAuthFlow;
50
- private isRefreshing: boolean = false;
51
-
52
- constructor(configManager: ConfigManager) {
53
- this.configManager = configManager;
54
- this.oauthFlow = new OAuthFlow(configManager);
55
- this.client = axios.create({
56
- baseURL: API_CONFIG.BASE_URL,
57
- timeout: TIMEOUTS.DEFAULT_REQUEST_MS,
58
- });
59
-
60
- // Add request interceptor to inject auth token and handle refresh
61
- this.client.interceptors.request.use(async (config) => {
62
- // Check if token needs refresh
63
- const currentConfig = await this.configManager.load();
64
-
65
- if (this.configManager.isTokenExpiringSoon(currentConfig) && !this.isRefreshing) {
66
- this.isRefreshing = true;
67
- try {
68
- console.log('🔄 Access token expiring soon, refreshing...');
69
- await this.oauthFlow.refreshAccessToken();
70
- console.log('✅ Token refreshed successfully');
71
- } catch (error: any) {
72
- console.error('❌ Failed to refresh token:', error.message);
73
- console.log('Please run "dive login" to re-authenticate');
74
- throw error;
75
- } finally {
76
- this.isRefreshing = false;
77
- }
78
- }
79
-
80
- const token = await this.configManager.getAccessToken();
81
- if (token) {
82
- config.headers.Authorization = `Bearer ${token}`;
83
- }
84
- return config;
85
- });
86
- }
87
-
88
- /**
89
- * Get session takeover information by PR URL
90
- */
91
- async getTakeoverByPrUrl(prUrl: string): Promise<TakeoverResponse> {
92
- try {
93
- const response = await this.client.get<TakeoverResponse>(API_CONFIG.TAKEOVER_PATH, {
94
- params: { pr_url: prUrl },
95
- });
96
- return response.data;
97
- } catch (error: any) {
98
- if (error.response) {
99
- const errorData = error.response.data;
100
- throw new Error(errorData?.error || `API request failed: ${error.response.status}`);
101
- }
102
- throw new Error(`API request failed: ${error.message}`);
103
- }
104
- }
105
-
106
- async getTakeoverByLinearIssueUrl(linearIssueUrl: string): Promise<TakeoverResponse> {
107
- try {
108
- const response = await this.client.get<TakeoverResponse>(API_CONFIG.TAKEOVER_PATH, {
109
- params: {
110
- linear_issue_url: linearIssueUrl,
111
- },
112
- });
113
- return response.data;
114
- } catch (error: any) {
115
- if (error.response) {
116
- const errorData = error.response.data;
117
- throw new Error(errorData?.error || `API request failed: ${error.response.status}`);
118
- }
119
- throw new Error(`API request failed: ${error.message}`);
120
- }
121
- }
122
-
123
- /**
124
- * Get session takeover information by Slack thread URL
125
- */
126
- async getTakeoverBySlackThreadUrl(slackThreadUrl: string): Promise<TakeoverResponse> {
127
- try {
128
- const response = await this.client.get<TakeoverResponse>(API_CONFIG.TAKEOVER_PATH, {
129
- params: {
130
- slack_thread_url: slackThreadUrl,
131
- },
132
- });
133
- return response.data;
134
- } catch (error: any) {
135
- if (error.response) {
136
- const errorData = error.response.data;
137
- throw new Error(errorData?.error || `API request failed: ${error.response.status}`);
138
- }
139
- throw new Error(`API request failed: ${error.message}`);
140
- }
141
- }
142
-
143
- /**
144
- * Get session takeover information by Asana task URL
145
- */
146
- async getTakeoverByAsanaTaskUrl(asanaTaskUrl: string): Promise<TakeoverResponse> {
147
- try {
148
- const response = await this.client.get<TakeoverResponse>(API_CONFIG.TAKEOVER_PATH, {
149
- params: {
150
- asana_task_url: asanaTaskUrl,
151
- },
152
- });
153
- return response.data;
154
- } catch (error: any) {
155
- if (error.response) {
156
- const errorData = error.response.data;
157
- throw new Error(errorData?.error || `API request failed: ${error.response.status}`);
158
- }
159
- throw new Error(`API request failed: ${error.message}`);
160
- }
161
- }
162
-
163
- /**
164
- * Get session takeover information by session URL (share link)
165
- */
166
- async getTakeoverBySessionUrl(sessionUrl: string): Promise<TakeoverResponse> {
167
- try {
168
- const response = await this.client.get<TakeoverResponse>(API_CONFIG.TAKEOVER_PATH, {
169
- params: {
170
- session_url: sessionUrl,
171
- },
172
- });
173
- return response.data;
174
- } catch (error: any) {
175
- if (error.response) {
176
- const errorData = error.response.data;
177
- throw new Error(errorData?.error || `API request failed: ${error.response.status}`);
178
- }
179
- throw new Error(`API request failed: ${error.message}`);
180
- }
181
- }
182
-
183
- /**
184
- * Upload Claude session file for an agent session
185
- */
186
- async uploadClaudeSession(sessionId: string, fileContent: Buffer): Promise<void> {
187
- try {
188
- const uploadPath = `/api/agents/agent-sessions/${sessionId}/claude-session`;
189
- await this.client.put(uploadPath, fileContent, {
190
- headers: {
191
- 'Content-Type': 'application/octet-stream',
192
- },
193
- });
194
- } catch (error: any) {
195
- if (error.response) {
196
- const errorData = error.response.data;
197
- throw new Error(errorData?.error || `Upload failed: ${error.response.status}`);
198
- }
199
- throw new Error(`Upload failed: ${error.message}`);
200
- }
201
- }
202
- }
@@ -1,278 +0,0 @@
1
- /**
2
- * OAuth 2.0 authorization flow with PKCE
3
- */
4
-
5
- import http from 'http';
6
- import url from 'url';
7
- import axios from 'axios';
8
- import open from 'open';
9
- import { generateCodeChallenge, generateCodeVerifier } from './pkce.js';
10
- import { API_CONFIG, OAUTH_CONFIG, TIMEOUTS } from '../constants.js';
11
- import { ConfigManager } from '../config/config-manager.js';
12
-
13
- export interface OAuthTokenResponse {
14
- access_token: string;
15
- token_type: string;
16
- expires_in: number;
17
- scope: string;
18
- refresh_token: string;
19
- }
20
-
21
- export class OAuthFlow {
22
- private configManager: ConfigManager;
23
-
24
- constructor(configManager: ConfigManager) {
25
- this.configManager = configManager;
26
- }
27
-
28
- /**
29
- * Initiate OAuth flow and wait for user authorization
30
- */
31
- async authorize(): Promise<OAuthTokenResponse> {
32
- // Generate PKCE parameters
33
- const codeVerifier = generateCodeVerifier();
34
- const codeChallenge = generateCodeChallenge(codeVerifier);
35
- const state = generateCodeVerifier(); // Use random string for state
36
-
37
- // Build authorization URL
38
- const authUrl = new URL(API_CONFIG.AUTHORIZE_PATH, API_CONFIG.BASE_URL);
39
- authUrl.searchParams.set('client_id', OAUTH_CONFIG.CLIENT_ID);
40
- authUrl.searchParams.set('redirect_uri', OAUTH_CONFIG.REDIRECT_URI);
41
- authUrl.searchParams.set('code_challenge', codeChallenge);
42
- authUrl.searchParams.set('state', state);
43
- authUrl.searchParams.set('scope', 'read write');
44
-
45
- console.log('\n🔐 Starting OAuth authorization flow...\n');
46
- console.log('Opening browser for authorization...');
47
- console.log(`If the browser doesn't open, visit: ${authUrl.toString()}\n`);
48
-
49
- // Start local callback server
50
- const authCode = await this.startCallbackServer(state, authUrl.toString());
51
-
52
- // Exchange authorization code for access token
53
- console.log('\n✅ Authorization successful! Exchanging code for token...\n');
54
- const tokenResponse = await this.exchangeCodeForToken(authCode, codeVerifier);
55
-
56
- // Save token to config
57
- const expiresAt = Date.now() + tokenResponse.expires_in * 1000;
58
- await this.configManager.save({
59
- accessToken: tokenResponse.access_token,
60
- tokenType: tokenResponse.token_type,
61
- expiresAt,
62
- scope: tokenResponse.scope,
63
- refreshToken: tokenResponse.refresh_token,
64
- });
65
-
66
- return tokenResponse;
67
- }
68
-
69
- /**
70
- * Start local HTTP server to receive OAuth callback
71
- */
72
- private async startCallbackServer(expectedState: string, authUrl: string): Promise<string> {
73
- return new Promise((resolve, reject) => {
74
- let resolved = false;
75
- const timeout = setTimeout(() => {
76
- if (!resolved) {
77
- server.close();
78
- reject(new Error('OAuth flow timed out'));
79
- }
80
- }, TIMEOUTS.CALLBACK_SERVER_MS);
81
-
82
- const server = http.createServer((req, res) => {
83
- if (!req.url) {
84
- return;
85
- }
86
-
87
- const parsedUrl = url.parse(req.url, true);
88
-
89
- if (parsedUrl.pathname === '/callback') {
90
- const { code, state, error } = parsedUrl.query;
91
-
92
- // Handle error response
93
- if (error) {
94
- resolved = true;
95
- clearTimeout(timeout);
96
- res.writeHead(400, { 'Content-Type': 'text/html; charset=utf-8' });
97
- res.end(`
98
- <html>
99
- <body>
100
- <h1>Authorization Failed</h1>
101
- <p>Error: ${error}</p>
102
- <p>You can close this window.</p>
103
- </body>
104
- </html>
105
- `);
106
- server.close();
107
- reject(new Error(`OAuth error: ${error}`));
108
- return;
109
- }
110
-
111
- // Validate state
112
- if (state !== expectedState) {
113
- resolved = true;
114
- clearTimeout(timeout);
115
- res.writeHead(400, { 'Content-Type': 'text/html; charset=utf-8' });
116
- res.end(`
117
- <html>
118
- <body>
119
- <h1>Authorization Failed</h1>
120
- <p>Invalid state parameter. Possible CSRF attack.</p>
121
- <p>You can close this window.</p>
122
- </body>
123
- </html>
124
- `);
125
- server.close();
126
- reject(new Error('Invalid state parameter'));
127
- return;
128
- }
129
-
130
- // Success - got authorization code
131
- if (code && typeof code === 'string') {
132
- resolved = true;
133
- clearTimeout(timeout);
134
- res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
135
- res.end(`
136
- <html>
137
- <head>
138
- <meta charset="utf-8" />
139
- </head>
140
- <body>
141
- <h1>✅ Authorization Successful!</h1>
142
- <p>You can close this window and return to the terminal.</p>
143
- <script>setTimeout(() => window.close(), 2000);</script>
144
- </body>
145
- </html>
146
- `);
147
- server.close();
148
- resolve(code);
149
- return;
150
- }
151
-
152
- // Missing code
153
- resolved = true;
154
- clearTimeout(timeout);
155
- res.writeHead(400, { 'Content-Type': 'text/html; charset=utf-8' });
156
- res.end(`
157
- <html>
158
- <body>
159
- <h1>Authorization Failed</h1>
160
- <p>Missing authorization code.</p>
161
- <p>You can close this window.</p>
162
- </body>
163
- </html>
164
- `);
165
- server.close();
166
- reject(new Error('Missing authorization code'));
167
- }
168
- });
169
-
170
- server.listen(OAUTH_CONFIG.CALLBACK_PORT, () => {
171
- // Open browser for authorization using the provided PKCE parameters
172
- void open(authUrl, { wait: false })
173
- .then((child) => {
174
- if (child && typeof child.unref === 'function') {
175
- child.unref();
176
- }
177
- })
178
- .catch(() => {
179
- // Silently fail if browser can't be opened
180
- });
181
- });
182
-
183
- server.on('error', (err) => {
184
- if (!resolved) {
185
- resolved = true;
186
- clearTimeout(timeout);
187
- reject(err);
188
- }
189
- });
190
- });
191
- }
192
-
193
- /**
194
- * Exchange authorization code for access token
195
- */
196
- private async exchangeCodeForToken(
197
- code: string,
198
- codeVerifier: string
199
- ): Promise<OAuthTokenResponse> {
200
- try {
201
- const tokenUrl = new URL(API_CONFIG.TOKEN_PATH, API_CONFIG.BASE_URL);
202
-
203
- const response = await axios.post<OAuthTokenResponse>(
204
- tokenUrl.toString(),
205
- new URLSearchParams({
206
- grant_type: 'authorization_code',
207
- code,
208
- code_verifier: codeVerifier,
209
- }),
210
- {
211
- headers: {
212
- 'Content-Type': 'application/x-www-form-urlencoded',
213
- },
214
- timeout: TIMEOUTS.DEFAULT_REQUEST_MS,
215
- }
216
- );
217
-
218
- return response.data;
219
- } catch (error: any) {
220
- if (error.response) {
221
- throw new Error(
222
- `Failed to exchange code for token: ${error.response.data?.error || error.message}`
223
- );
224
- }
225
- throw new Error(`Failed to exchange code for token: ${error.message}`);
226
- }
227
- }
228
-
229
- /**
230
- * Refresh access token using refresh token
231
- */
232
- async refreshAccessToken(): Promise<OAuthTokenResponse | null> {
233
- try {
234
- const refreshToken = await this.configManager.getRefreshToken();
235
- if (!refreshToken) {
236
- return null;
237
- }
238
-
239
- const tokenUrl = new URL(API_CONFIG.TOKEN_PATH, API_CONFIG.BASE_URL);
240
-
241
- const response = await axios.post<OAuthTokenResponse>(
242
- tokenUrl.toString(),
243
- new URLSearchParams({
244
- grant_type: 'refresh_token',
245
- refresh_token: refreshToken,
246
- }),
247
- {
248
- headers: {
249
- 'Content-Type': 'application/x-www-form-urlencoded',
250
- },
251
- timeout: TIMEOUTS.DEFAULT_REQUEST_MS,
252
- }
253
- );
254
-
255
- // Save new tokens to config
256
- const expiresAt = Date.now() + response.data.expires_in * 1000;
257
- await this.configManager.save({
258
- accessToken: response.data.access_token,
259
- tokenType: response.data.token_type,
260
- expiresAt,
261
- scope: response.data.scope,
262
- refreshToken: response.data.refresh_token,
263
- });
264
-
265
- return response.data;
266
- } catch (error: any) {
267
- // If refresh fails, clear the config so user needs to login again
268
- await this.configManager.delete();
269
-
270
- if (error.response) {
271
- throw new Error(
272
- `Failed to refresh token: ${error.response.data?.error || error.message}`
273
- );
274
- }
275
- throw new Error(`Failed to refresh token: ${error.message}`);
276
- }
277
- }
278
- }
package/src/auth/pkce.ts DELETED
@@ -1,27 +0,0 @@
1
- /**
2
- * PKCE (Proof Key for Code Exchange) utilities for OAuth 2.0
3
- */
4
-
5
- import crypto from 'crypto';
6
-
7
- /**
8
- * Generate a cryptographically random code verifier
9
- */
10
- export function generateCodeVerifier(): string {
11
- return base64URLEncode(crypto.randomBytes(32));
12
- }
13
-
14
- /**
15
- * Generate a code challenge from a code verifier using SHA256
16
- */
17
- export function generateCodeChallenge(verifier: string): string {
18
- const hash = crypto.createHash('sha256').update(verifier).digest();
19
- return base64URLEncode(hash);
20
- }
21
-
22
- /**
23
- * Base64 URL encode (without padding)
24
- */
25
- function base64URLEncode(buffer: Buffer): string {
26
- return buffer.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
27
- }