@edgedive/cli 0.2.0 ā 0.3.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/dist/api/client.d.ts +12 -0
- package/dist/api/client.d.ts.map +1 -1
- package/dist/api/client.js +40 -0
- package/dist/api/client.js.map +1 -1
- package/dist/commands/local.d.ts +3 -1
- package/dist/commands/local.d.ts.map +1 -1
- package/dist/commands/local.js +31 -3
- package/dist/commands/local.js.map +1 -1
- package/dist/commands/takeover.d.ts +10 -0
- package/dist/commands/takeover.d.ts.map +1 -0
- package/dist/commands/takeover.js +108 -0
- package/dist/commands/takeover.js.map +1 -0
- package/dist/constants.js +1 -1
- package/dist/constants.js.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/.turbo/turbo-build.log +0 -5
- package/.turbo/turbo-dev.log +0 -8
- package/.turbo/turbo-typecheck.log +0 -4
- package/AGENTS.md +0 -135
- package/CLAUDE.md +0 -3
- package/src/api/client.ts +0 -158
- package/src/auth/oauth-flow.ts +0 -278
- package/src/auth/pkce.ts +0 -27
- package/src/commands/local.ts +0 -245
- package/src/commands/login.ts +0 -48
- package/src/commands/logout.ts +0 -29
- package/src/config/config-manager.ts +0 -120
- package/src/constants.ts +0 -34
- package/src/index.ts +0 -58
- package/src/utils/claude-launcher.ts +0 -94
- package/src/utils/git-utils.ts +0 -179
- package/src/utils/session-downloader.ts +0 -56
- package/tsconfig.json +0 -20
package/src/api/client.ts
DELETED
|
@@ -1,158 +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
|
-
status: string;
|
|
31
|
-
agent_type: string;
|
|
32
|
-
download_urls: {
|
|
33
|
-
claude_session?: string;
|
|
34
|
-
session_metadata?: string;
|
|
35
|
-
messages?: string;
|
|
36
|
-
};
|
|
37
|
-
expires_in: number;
|
|
38
|
-
created_at: string;
|
|
39
|
-
updated_at: string;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export class EdgediveApiClient {
|
|
43
|
-
private client: AxiosInstance;
|
|
44
|
-
private configManager: ConfigManager;
|
|
45
|
-
private oauthFlow: OAuthFlow;
|
|
46
|
-
private isRefreshing: boolean = false;
|
|
47
|
-
|
|
48
|
-
constructor(configManager: ConfigManager) {
|
|
49
|
-
this.configManager = configManager;
|
|
50
|
-
this.oauthFlow = new OAuthFlow(configManager);
|
|
51
|
-
this.client = axios.create({
|
|
52
|
-
baseURL: API_CONFIG.BASE_URL,
|
|
53
|
-
timeout: TIMEOUTS.DEFAULT_REQUEST_MS,
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
// Add request interceptor to inject auth token and handle refresh
|
|
57
|
-
this.client.interceptors.request.use(async (config) => {
|
|
58
|
-
// Check if token needs refresh
|
|
59
|
-
const currentConfig = await this.configManager.load();
|
|
60
|
-
|
|
61
|
-
if (this.configManager.isTokenExpiringSoon(currentConfig) && !this.isRefreshing) {
|
|
62
|
-
this.isRefreshing = true;
|
|
63
|
-
try {
|
|
64
|
-
console.log('š Access token expiring soon, refreshing...');
|
|
65
|
-
await this.oauthFlow.refreshAccessToken();
|
|
66
|
-
console.log('ā
Token refreshed successfully');
|
|
67
|
-
} catch (error: any) {
|
|
68
|
-
console.error('ā Failed to refresh token:', error.message);
|
|
69
|
-
console.log('Please run "dive login" to re-authenticate');
|
|
70
|
-
throw error;
|
|
71
|
-
} finally {
|
|
72
|
-
this.isRefreshing = false;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const token = await this.configManager.getAccessToken();
|
|
77
|
-
if (token) {
|
|
78
|
-
config.headers.Authorization = `Bearer ${token}`;
|
|
79
|
-
}
|
|
80
|
-
return config;
|
|
81
|
-
});
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* Get session takeover information by PR URL
|
|
86
|
-
*/
|
|
87
|
-
async getTakeoverByPrUrl(prUrl: string): Promise<TakeoverResponse> {
|
|
88
|
-
try {
|
|
89
|
-
const response = await this.client.get<TakeoverResponse>(API_CONFIG.TAKEOVER_PATH, {
|
|
90
|
-
params: { pr_url: prUrl },
|
|
91
|
-
});
|
|
92
|
-
return response.data;
|
|
93
|
-
} catch (error: any) {
|
|
94
|
-
if (error.response) {
|
|
95
|
-
const errorData = error.response.data;
|
|
96
|
-
throw new Error(errorData?.error || `API request failed: ${error.response.status}`);
|
|
97
|
-
}
|
|
98
|
-
throw new Error(`API request failed: ${error.message}`);
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
async getTakeoverByLinearIssueUrl(linearIssueUrl: string): Promise<TakeoverResponse> {
|
|
103
|
-
try {
|
|
104
|
-
const response = await this.client.get<TakeoverResponse>(API_CONFIG.TAKEOVER_PATH, {
|
|
105
|
-
params: {
|
|
106
|
-
linear_issue_url: linearIssueUrl,
|
|
107
|
-
},
|
|
108
|
-
});
|
|
109
|
-
return response.data;
|
|
110
|
-
} catch (error: any) {
|
|
111
|
-
if (error.response) {
|
|
112
|
-
const errorData = error.response.data;
|
|
113
|
-
throw new Error(errorData?.error || `API request failed: ${error.response.status}`);
|
|
114
|
-
}
|
|
115
|
-
throw new Error(`API request failed: ${error.message}`);
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* Get session takeover information by Slack thread URL
|
|
121
|
-
*/
|
|
122
|
-
async getTakeoverBySlackThreadUrl(slackThreadUrl: string): Promise<TakeoverResponse> {
|
|
123
|
-
try {
|
|
124
|
-
const response = await this.client.get<TakeoverResponse>(API_CONFIG.TAKEOVER_PATH, {
|
|
125
|
-
params: {
|
|
126
|
-
slack_thread_url: slackThreadUrl,
|
|
127
|
-
},
|
|
128
|
-
});
|
|
129
|
-
return response.data;
|
|
130
|
-
} catch (error: any) {
|
|
131
|
-
if (error.response) {
|
|
132
|
-
const errorData = error.response.data;
|
|
133
|
-
throw new Error(errorData?.error || `API request failed: ${error.response.status}`);
|
|
134
|
-
}
|
|
135
|
-
throw new Error(`API request failed: ${error.message}`);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/**
|
|
140
|
-
* Upload Claude session file for an agent session
|
|
141
|
-
*/
|
|
142
|
-
async uploadClaudeSession(sessionId: string, fileContent: Buffer): Promise<void> {
|
|
143
|
-
try {
|
|
144
|
-
const uploadPath = `/api/agents/agent-sessions/${sessionId}/claude-session`;
|
|
145
|
-
await this.client.put(uploadPath, fileContent, {
|
|
146
|
-
headers: {
|
|
147
|
-
'Content-Type': 'application/octet-stream',
|
|
148
|
-
},
|
|
149
|
-
});
|
|
150
|
-
} catch (error: any) {
|
|
151
|
-
if (error.response) {
|
|
152
|
-
const errorData = error.response.data;
|
|
153
|
-
throw new Error(errorData?.error || `Upload failed: ${error.response.status}`);
|
|
154
|
-
}
|
|
155
|
-
throw new Error(`Upload failed: ${error.message}`);
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
}
|
package/src/auth/oauth-flow.ts
DELETED
|
@@ -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
|
-
}
|
package/src/commands/local.ts
DELETED
|
@@ -1,245 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Local command - download agent session for local development
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import chalk from 'chalk';
|
|
6
|
-
import { ConfigManager } from '../config/config-manager.js';
|
|
7
|
-
import { EdgediveApiClient, type TakeoverResponse } from '../api/client.js';
|
|
8
|
-
import { SessionDownloader } from '../utils/session-downloader.js';
|
|
9
|
-
import { GitUtils } from '../utils/git-utils.js';
|
|
10
|
-
import { launchClaudeSession } from '../utils/claude-launcher.js';
|
|
11
|
-
|
|
12
|
-
interface LocalOptions {
|
|
13
|
-
prUrl?: string;
|
|
14
|
-
issueUrl?: string;
|
|
15
|
-
threadUrl?: string;
|
|
16
|
-
worktree?: boolean;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export async function localCommand({
|
|
20
|
-
prUrl,
|
|
21
|
-
issueUrl,
|
|
22
|
-
threadUrl,
|
|
23
|
-
worktree,
|
|
24
|
-
}: LocalOptions): Promise<void> {
|
|
25
|
-
const configManager = new ConfigManager();
|
|
26
|
-
const apiClient = new EdgediveApiClient(configManager);
|
|
27
|
-
const downloader = new SessionDownloader();
|
|
28
|
-
|
|
29
|
-
try {
|
|
30
|
-
console.log(chalk.bold('\nš§ Edgedive Local Session Setup\n'));
|
|
31
|
-
|
|
32
|
-
// Check for Windows platform
|
|
33
|
-
if (process.platform === 'win32') {
|
|
34
|
-
console.log(chalk.red('ā Windows is not currently supported for the local command.\n'));
|
|
35
|
-
console.log(chalk.yellow('Please use macOS or Linux to work on sessions locally.\n'));
|
|
36
|
-
process.exit(1);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// Check if authenticated
|
|
40
|
-
if (!(await configManager.isAuthenticated())) {
|
|
41
|
-
console.log(chalk.red('ā You are not logged in.\n'));
|
|
42
|
-
console.log(chalk.yellow('Please run: edgedive login\n'));
|
|
43
|
-
process.exit(1);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
if (!prUrl && !issueUrl && !threadUrl) {
|
|
47
|
-
console.log(chalk.red('ā Missing session identifier.\n'));
|
|
48
|
-
console.log(
|
|
49
|
-
chalk.yellow(
|
|
50
|
-
'Provide one of:\n' +
|
|
51
|
-
' --pr-url https://github.com/owner/repo/pull/123\n' +
|
|
52
|
-
' --issue-url https://linear.app/...\n' +
|
|
53
|
-
' --thread-url https://workspace.slack.com/archives/C12345/p1234567890123456\n'
|
|
54
|
-
)
|
|
55
|
-
);
|
|
56
|
-
process.exit(1);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
let sessionData;
|
|
60
|
-
|
|
61
|
-
if (prUrl) {
|
|
62
|
-
// Validate PR URL format
|
|
63
|
-
const prUrlPattern = /^https?:\/\/github\.com\/([^\/]+)\/([^\/]+)\/pull\/(\d+)/;
|
|
64
|
-
if (!prUrlPattern.test(prUrl)) {
|
|
65
|
-
console.log(chalk.red('ā Invalid PR URL format.\n'));
|
|
66
|
-
console.log(chalk.yellow('Expected format: https://github.com/owner/repo/pull/123\n'));
|
|
67
|
-
process.exit(1);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
console.log(chalk.dim(`PR URL: ${prUrl}\n`));
|
|
71
|
-
console.log('š Fetching session information...\n');
|
|
72
|
-
sessionData = await apiClient.getTakeoverByPrUrl(prUrl);
|
|
73
|
-
} else if (issueUrl) {
|
|
74
|
-
console.log(chalk.dim(`Linear Issue URL: ${issueUrl}\n`));
|
|
75
|
-
console.log('š Fetching session information...\n');
|
|
76
|
-
sessionData = await apiClient.getTakeoverByLinearIssueUrl(issueUrl);
|
|
77
|
-
} else {
|
|
78
|
-
// Validate Slack thread URL format
|
|
79
|
-
const slackThreadPattern = /^https?:\/\/[^\/]+\.slack\.com\/archives\/([A-Z0-9]+)\/p(\d{16})/;
|
|
80
|
-
if (!slackThreadPattern.test(threadUrl!)) {
|
|
81
|
-
console.log(chalk.red('ā Invalid Slack thread URL format.\n'));
|
|
82
|
-
console.log(
|
|
83
|
-
chalk.yellow(
|
|
84
|
-
'Expected format: https://workspace.slack.com/archives/C12345/p1234567890123456\n'
|
|
85
|
-
)
|
|
86
|
-
);
|
|
87
|
-
process.exit(1);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
console.log(chalk.dim(`Slack Thread URL: ${threadUrl}\n`));
|
|
91
|
-
console.log('š Fetching session information...\n');
|
|
92
|
-
sessionData = await apiClient.getTakeoverBySlackThreadUrl(threadUrl!);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
console.log(chalk.green('ā
Found agent session!\n'));
|
|
96
|
-
if (sessionData.github_pr?.url) {
|
|
97
|
-
console.log(chalk.dim(`PR URL: ${sessionData.github_pr.url}`));
|
|
98
|
-
}
|
|
99
|
-
console.log(chalk.bold('Session Information:'));
|
|
100
|
-
console.log(chalk.dim(` Session ID: ${sessionData.session_id}`));
|
|
101
|
-
console.log(chalk.dim(` Agent Type: ${sessionData.agent_type}`));
|
|
102
|
-
console.log(chalk.dim(` Status: ${sessionData.status}`));
|
|
103
|
-
console.log(
|
|
104
|
-
chalk.dim(` Repository: ${sessionData.repository.owner}/${sessionData.repository.name}`)
|
|
105
|
-
);
|
|
106
|
-
console.log(chalk.dim(` Branch: ${sessionData.repository.branch}`));
|
|
107
|
-
|
|
108
|
-
if (sessionData.linear_issue) {
|
|
109
|
-
console.log(chalk.dim(` Linear Issue: ${sessionData.linear_issue.identifier}`));
|
|
110
|
-
console.log(chalk.dim(` Linear URL: ${sessionData.linear_issue.url}`));
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// Verify current repository matches PR source
|
|
114
|
-
const cwd = process.cwd();
|
|
115
|
-
let repoInfo;
|
|
116
|
-
|
|
117
|
-
try {
|
|
118
|
-
repoInfo = await GitUtils.verifyRepoMatches(
|
|
119
|
-
sessionData.repository.owner,
|
|
120
|
-
sessionData.repository.name,
|
|
121
|
-
cwd
|
|
122
|
-
);
|
|
123
|
-
} catch (error: any) {
|
|
124
|
-
console.error(chalk.red(`\nā ${error.message}\n`));
|
|
125
|
-
console.log(
|
|
126
|
-
chalk.yellow('Run this command from within the target repository checked out locally.')
|
|
127
|
-
);
|
|
128
|
-
process.exit(1);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
console.log(chalk.green(`ā
Using repository at ${repoInfo.rootPath}`));
|
|
132
|
-
|
|
133
|
-
// Ensure PR branch is checked out locally or in a worktree
|
|
134
|
-
let workingPath = repoInfo.rootPath;
|
|
135
|
-
try {
|
|
136
|
-
if (worktree) {
|
|
137
|
-
workingPath = await GitUtils.ensureBranchInWorktree(
|
|
138
|
-
sessionData.repository.branch,
|
|
139
|
-
repoInfo.rootPath
|
|
140
|
-
);
|
|
141
|
-
console.log(
|
|
142
|
-
chalk.green(
|
|
143
|
-
`ā
Created worktree for branch ${sessionData.repository.branch} at ${workingPath}`
|
|
144
|
-
)
|
|
145
|
-
);
|
|
146
|
-
} else {
|
|
147
|
-
await GitUtils.ensureBranchCheckedOut(sessionData.repository.branch, repoInfo.rootPath);
|
|
148
|
-
console.log(chalk.green(`ā
Checked out branch ${sessionData.repository.branch}`));
|
|
149
|
-
}
|
|
150
|
-
} catch (error: any) {
|
|
151
|
-
console.error(
|
|
152
|
-
chalk.red(
|
|
153
|
-
`\nā Failed to ${worktree ? 'create worktree for' : 'checkout'} branch ${sessionData.repository.branch}: ${error.message}\n`
|
|
154
|
-
)
|
|
155
|
-
);
|
|
156
|
-
console.log(chalk.yellow('Resolve the git issue above and rerun the local command.'));
|
|
157
|
-
process.exit(1);
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
// Download session files into Claude projects directory
|
|
161
|
-
// Use workingPath so the session goes to the correct directory (worktree path if --worktree is used)
|
|
162
|
-
const { claudeSessionId, claudeSessionPath } = await downloader.downloadSession(
|
|
163
|
-
sessionData,
|
|
164
|
-
workingPath
|
|
165
|
-
);
|
|
166
|
-
|
|
167
|
-
console.log(chalk.blue(`\nš Launching Claude session ${claudeSessionId}...\n`));
|
|
168
|
-
|
|
169
|
-
try {
|
|
170
|
-
const sessionEndInfo = await launchClaudeSession(claudeSessionId, workingPath);
|
|
171
|
-
console.log(chalk.green('\nā
Claude session closed. Happy debugging!\n'));
|
|
172
|
-
|
|
173
|
-
// Upload Claude session file back to server
|
|
174
|
-
// Use the session info from the SessionEnd hook if available, otherwise fall back to original path
|
|
175
|
-
await uploadClaudeSessionFile(
|
|
176
|
-
apiClient,
|
|
177
|
-
sessionData,
|
|
178
|
-
sessionEndInfo || { claudeSessionPath }
|
|
179
|
-
);
|
|
180
|
-
} catch (error: any) {
|
|
181
|
-
console.error(chalk.red(`\nā Failed to start Claude automatically: ${error.message}\n`));
|
|
182
|
-
console.log(
|
|
183
|
-
chalk.yellow(
|
|
184
|
-
`You can resume manually with: claude -r ${claudeSessionId} (from ${workingPath})`
|
|
185
|
-
)
|
|
186
|
-
);
|
|
187
|
-
process.exit(1);
|
|
188
|
-
}
|
|
189
|
-
} catch (error: any) {
|
|
190
|
-
console.error(chalk.red(`\nā Local command failed: ${error.message}\n`));
|
|
191
|
-
|
|
192
|
-
if (error.message.includes('401') || error.message.includes('Unauthorized')) {
|
|
193
|
-
console.log(chalk.yellow('Your session may have expired. Please run: edgedive login\n'));
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
process.exit(1);
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
/**
|
|
201
|
-
* Upload Claude session file back to Edgedive after local development
|
|
202
|
-
*/
|
|
203
|
-
async function uploadClaudeSessionFile(
|
|
204
|
-
apiClient: EdgediveApiClient,
|
|
205
|
-
sessionData: TakeoverResponse,
|
|
206
|
-
sessionInfo: { transcript_path?: string; claudeSessionPath?: string }
|
|
207
|
-
): Promise<void> {
|
|
208
|
-
try {
|
|
209
|
-
const fs = (await import('fs/promises')).default;
|
|
210
|
-
|
|
211
|
-
// Use transcript_path from SessionEnd hook if available, otherwise fall back to original path
|
|
212
|
-
const sessionFilePath = sessionInfo.transcript_path || sessionInfo.claudeSessionPath;
|
|
213
|
-
|
|
214
|
-
if (!sessionFilePath) {
|
|
215
|
-
console.log(chalk.yellow('ā ļø No Claude session file path available, skipping upload'));
|
|
216
|
-
return;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
// Check if file exists
|
|
220
|
-
try {
|
|
221
|
-
await fs.access(sessionFilePath);
|
|
222
|
-
} catch {
|
|
223
|
-
console.log(
|
|
224
|
-
chalk.yellow(`ā ļø Claude session file not found at ${sessionFilePath}, skipping upload`)
|
|
225
|
-
);
|
|
226
|
-
return;
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
console.log(chalk.blue('š¤ Uploading Claude session...'));
|
|
230
|
-
|
|
231
|
-
// Read file content
|
|
232
|
-
const fileContent = await fs.readFile(sessionFilePath);
|
|
233
|
-
|
|
234
|
-
// Upload to server
|
|
235
|
-
await apiClient.uploadClaudeSession(sessionData.session_id, fileContent);
|
|
236
|
-
|
|
237
|
-
console.log(chalk.green('ā
Claude session uploaded successfully\n'));
|
|
238
|
-
} catch (error: any) {
|
|
239
|
-
// Don't fail the whole command if upload fails
|
|
240
|
-
console.error(chalk.yellow(`ā ļø Failed to upload Claude session: ${error.message}`));
|
|
241
|
-
console.error(
|
|
242
|
-
chalk.yellow(' Your local changes are safe, but were not synced to the server.\n')
|
|
243
|
-
);
|
|
244
|
-
}
|
|
245
|
-
}
|