@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.
- package/dist/api/client.d.ts +12 -0
- package/dist/api/client.js +40 -0
- package/dist/commands/local.d.ts +3 -1
- package/dist/commands/local.js +31 -3
- package/dist/constants.js +1 -1
- package/dist/index.js +2 -0
- package/package.json +1 -1
- package/.env +0 -2
- package/.env.local +0 -2
- package/.turbo/turbo-build.log +0 -4
- 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/dist/api/client.d.ts.map +0 -1
- package/dist/api/client.js.map +0 -1
- package/dist/auth/oauth-flow.d.ts.map +0 -1
- package/dist/auth/oauth-flow.js.map +0 -1
- package/dist/auth/pkce.d.ts.map +0 -1
- package/dist/auth/pkce.js.map +0 -1
- package/dist/commands/local.d.ts.map +0 -1
- package/dist/commands/local.js.map +0 -1
- package/dist/commands/login.d.ts.map +0 -1
- package/dist/commands/login.js.map +0 -1
- package/dist/commands/logout.d.ts.map +0 -1
- package/dist/commands/logout.js.map +0 -1
- package/dist/commands/takeover.d.ts.map +0 -1
- package/dist/commands/takeover.js.map +0 -1
- package/dist/config/config-manager.d.ts.map +0 -1
- package/dist/config/config-manager.js.map +0 -1
- package/dist/constants.d.ts.map +0 -1
- package/dist/constants.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/utils/claude-launcher.d.ts.map +0 -1
- package/dist/utils/claude-launcher.js.map +0 -1
- package/dist/utils/git-utils.d.ts.map +0 -1
- package/dist/utils/git-utils.js.map +0 -1
- package/dist/utils/session-downloader.d.ts.map +0 -1
- package/dist/utils/session-downloader.js.map +0 -1
- package/src/api/client.ts +0 -202
- package/src/auth/oauth-flow.ts +0 -278
- package/src/auth/pkce.ts +0 -27
- package/src/commands/local.ts +0 -286
- 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 -62
- 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/commands/local.ts
DELETED
|
@@ -1,286 +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
|
-
taskUrl?: string;
|
|
17
|
-
sessionUrl?: string;
|
|
18
|
-
worktree?: boolean;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export async function localCommand({
|
|
22
|
-
prUrl,
|
|
23
|
-
issueUrl,
|
|
24
|
-
threadUrl,
|
|
25
|
-
taskUrl,
|
|
26
|
-
sessionUrl,
|
|
27
|
-
worktree,
|
|
28
|
-
}: LocalOptions): Promise<void> {
|
|
29
|
-
const configManager = new ConfigManager();
|
|
30
|
-
const apiClient = new EdgediveApiClient(configManager);
|
|
31
|
-
const downloader = new SessionDownloader();
|
|
32
|
-
|
|
33
|
-
try {
|
|
34
|
-
console.log(chalk.bold('\nš§ Edgedive Local Session Setup\n'));
|
|
35
|
-
|
|
36
|
-
// Check for Windows platform
|
|
37
|
-
if (process.platform === 'win32') {
|
|
38
|
-
console.log(chalk.red('ā Windows is not currently supported for the local command.\n'));
|
|
39
|
-
console.log(chalk.yellow('Please use macOS or Linux to work on sessions locally.\n'));
|
|
40
|
-
process.exit(1);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// Check if authenticated
|
|
44
|
-
if (!(await configManager.isAuthenticated())) {
|
|
45
|
-
console.log(chalk.red('ā You are not logged in.\n'));
|
|
46
|
-
console.log(chalk.yellow('Please run: edgedive login\n'));
|
|
47
|
-
process.exit(1);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
if (!prUrl && !issueUrl && !threadUrl && !taskUrl && !sessionUrl) {
|
|
51
|
-
console.log(chalk.red('ā Missing session identifier.\n'));
|
|
52
|
-
console.log(
|
|
53
|
-
chalk.yellow(
|
|
54
|
-
'Provide one of:\n' +
|
|
55
|
-
' --pr-url https://github.com/owner/repo/pull/123\n' +
|
|
56
|
-
' --issue-url https://linear.app/...\n' +
|
|
57
|
-
' --thread-url https://workspace.slack.com/archives/C12345/p1234567890123456\n' +
|
|
58
|
-
' --task-url https://app.asana.com/0/PROJECT_ID/TASK_ID\n' +
|
|
59
|
-
' --session-url https://app.edgedive.com/agent-sessions?session={session_id}\n'
|
|
60
|
-
)
|
|
61
|
-
);
|
|
62
|
-
process.exit(1);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
let sessionData;
|
|
66
|
-
|
|
67
|
-
if (prUrl) {
|
|
68
|
-
// Validate PR URL format
|
|
69
|
-
const prUrlPattern = /^https?:\/\/github\.com\/([^\/]+)\/([^\/]+)\/pull\/(\d+)/;
|
|
70
|
-
if (!prUrlPattern.test(prUrl)) {
|
|
71
|
-
console.log(chalk.red('ā Invalid PR URL format.\n'));
|
|
72
|
-
console.log(chalk.yellow('Expected format: https://github.com/owner/repo/pull/123\n'));
|
|
73
|
-
process.exit(1);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
console.log(chalk.dim(`PR URL: ${prUrl}\n`));
|
|
77
|
-
console.log('š Fetching session information...\n');
|
|
78
|
-
sessionData = await apiClient.getTakeoverByPrUrl(prUrl);
|
|
79
|
-
} else if (issueUrl) {
|
|
80
|
-
console.log(chalk.dim(`Linear Issue URL: ${issueUrl}\n`));
|
|
81
|
-
console.log('š Fetching session information...\n');
|
|
82
|
-
sessionData = await apiClient.getTakeoverByLinearIssueUrl(issueUrl);
|
|
83
|
-
} else if (taskUrl) {
|
|
84
|
-
// Validate it's an Asana URL ā let the server handle detailed format parsing
|
|
85
|
-
const asanaTaskPattern = /^https?:\/\/app\.asana\.com\//;
|
|
86
|
-
if (!asanaTaskPattern.test(taskUrl)) {
|
|
87
|
-
console.log(
|
|
88
|
-
chalk.red(
|
|
89
|
-
'ā Invalid Asana task URL. Expected a URL starting with https://app.asana.com/\n'
|
|
90
|
-
)
|
|
91
|
-
);
|
|
92
|
-
process.exit(1);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
console.log(chalk.dim(`Asana Task URL: ${taskUrl}\n`));
|
|
96
|
-
console.log('š Fetching session information...\n');
|
|
97
|
-
sessionData = await apiClient.getTakeoverByAsanaTaskUrl(taskUrl);
|
|
98
|
-
} else if (sessionUrl) {
|
|
99
|
-
// Validate session URL format
|
|
100
|
-
const sessionUrlPattern = /^https?:\/\/[^\/]+\/agent-sessions\?session=([a-f0-9-]+)/i;
|
|
101
|
-
if (!sessionUrlPattern.test(sessionUrl)) {
|
|
102
|
-
console.log(chalk.red('ā Invalid session URL format.\n'));
|
|
103
|
-
console.log(
|
|
104
|
-
chalk.yellow(
|
|
105
|
-
'Expected format: https://app.edgedive.com/agent-sessions?session={session_id}\n'
|
|
106
|
-
)
|
|
107
|
-
);
|
|
108
|
-
process.exit(1);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
console.log(chalk.dim(`Session URL: ${sessionUrl}\n`));
|
|
112
|
-
console.log('š Fetching session information...\n');
|
|
113
|
-
sessionData = await apiClient.getTakeoverBySessionUrl(sessionUrl);
|
|
114
|
-
} else {
|
|
115
|
-
// Validate Slack thread URL format
|
|
116
|
-
const slackThreadPattern = /^https?:\/\/[^\/]+\.slack\.com\/archives\/([A-Z0-9]+)\/p(\d{16})/;
|
|
117
|
-
if (!slackThreadPattern.test(threadUrl!)) {
|
|
118
|
-
console.log(chalk.red('ā Invalid Slack thread URL format.\n'));
|
|
119
|
-
console.log(
|
|
120
|
-
chalk.yellow(
|
|
121
|
-
'Expected format: https://workspace.slack.com/archives/C12345/p1234567890123456\n'
|
|
122
|
-
)
|
|
123
|
-
);
|
|
124
|
-
process.exit(1);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
console.log(chalk.dim(`Slack Thread URL: ${threadUrl}\n`));
|
|
128
|
-
console.log('š Fetching session information...\n');
|
|
129
|
-
sessionData = await apiClient.getTakeoverBySlackThreadUrl(threadUrl!);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
console.log(chalk.green('ā
Found agent session!\n'));
|
|
133
|
-
if (sessionData.github_pr?.url) {
|
|
134
|
-
console.log(chalk.dim(`PR URL: ${sessionData.github_pr.url}`));
|
|
135
|
-
}
|
|
136
|
-
console.log(chalk.bold('Session Information:'));
|
|
137
|
-
console.log(chalk.dim(` Session ID: ${sessionData.session_id}`));
|
|
138
|
-
console.log(chalk.dim(` Agent Type: ${sessionData.agent_type}`));
|
|
139
|
-
console.log(chalk.dim(` Status: ${sessionData.status}`));
|
|
140
|
-
console.log(
|
|
141
|
-
chalk.dim(` Repository: ${sessionData.repository.owner}/${sessionData.repository.name}`)
|
|
142
|
-
);
|
|
143
|
-
console.log(chalk.dim(` Branch: ${sessionData.repository.branch}`));
|
|
144
|
-
|
|
145
|
-
if (sessionData.linear_issue) {
|
|
146
|
-
console.log(chalk.dim(` Linear Issue: ${sessionData.linear_issue.identifier}`));
|
|
147
|
-
console.log(chalk.dim(` Linear URL: ${sessionData.linear_issue.url}`));
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
if (sessionData.asana_task) {
|
|
151
|
-
console.log(chalk.dim(` Asana Task: ${sessionData.asana_task.url}`));
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
// Verify current repository matches PR source
|
|
155
|
-
const cwd = process.cwd();
|
|
156
|
-
let repoInfo;
|
|
157
|
-
|
|
158
|
-
try {
|
|
159
|
-
repoInfo = await GitUtils.verifyRepoMatches(
|
|
160
|
-
sessionData.repository.owner,
|
|
161
|
-
sessionData.repository.name,
|
|
162
|
-
cwd
|
|
163
|
-
);
|
|
164
|
-
} catch (error: any) {
|
|
165
|
-
console.error(chalk.red(`\nā ${error.message}\n`));
|
|
166
|
-
console.log(
|
|
167
|
-
chalk.yellow('Run this command from within the target repository checked out locally.')
|
|
168
|
-
);
|
|
169
|
-
process.exit(1);
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
console.log(chalk.green(`ā
Using repository at ${repoInfo.rootPath}`));
|
|
173
|
-
|
|
174
|
-
// Ensure PR branch is checked out locally or in a worktree
|
|
175
|
-
let workingPath = repoInfo.rootPath;
|
|
176
|
-
try {
|
|
177
|
-
if (worktree) {
|
|
178
|
-
workingPath = await GitUtils.ensureBranchInWorktree(
|
|
179
|
-
sessionData.repository.branch,
|
|
180
|
-
repoInfo.rootPath
|
|
181
|
-
);
|
|
182
|
-
console.log(
|
|
183
|
-
chalk.green(
|
|
184
|
-
`ā
Created worktree for branch ${sessionData.repository.branch} at ${workingPath}`
|
|
185
|
-
)
|
|
186
|
-
);
|
|
187
|
-
} else {
|
|
188
|
-
await GitUtils.ensureBranchCheckedOut(sessionData.repository.branch, repoInfo.rootPath);
|
|
189
|
-
console.log(chalk.green(`ā
Checked out branch ${sessionData.repository.branch}`));
|
|
190
|
-
}
|
|
191
|
-
} catch (error: any) {
|
|
192
|
-
console.error(
|
|
193
|
-
chalk.red(
|
|
194
|
-
`\nā Failed to ${worktree ? 'create worktree for' : 'checkout'} branch ${sessionData.repository.branch}: ${error.message}\n`
|
|
195
|
-
)
|
|
196
|
-
);
|
|
197
|
-
console.log(chalk.yellow('Resolve the git issue above and rerun the local command.'));
|
|
198
|
-
process.exit(1);
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// Download session files into Claude projects directory
|
|
202
|
-
// Use workingPath so the session goes to the correct directory (worktree path if --worktree is used)
|
|
203
|
-
const { claudeSessionId, claudeSessionPath } = await downloader.downloadSession(
|
|
204
|
-
sessionData,
|
|
205
|
-
workingPath
|
|
206
|
-
);
|
|
207
|
-
|
|
208
|
-
console.log(chalk.blue(`\nš Launching Claude session ${claudeSessionId}...\n`));
|
|
209
|
-
|
|
210
|
-
try {
|
|
211
|
-
const sessionEndInfo = await launchClaudeSession(claudeSessionId, workingPath);
|
|
212
|
-
console.log(chalk.green('\nā
Claude session closed. Happy debugging!\n'));
|
|
213
|
-
|
|
214
|
-
// Upload Claude session file back to server
|
|
215
|
-
// Use the session info from the SessionEnd hook if available, otherwise fall back to original path
|
|
216
|
-
await uploadClaudeSessionFile(
|
|
217
|
-
apiClient,
|
|
218
|
-
sessionData,
|
|
219
|
-
sessionEndInfo || { claudeSessionPath }
|
|
220
|
-
);
|
|
221
|
-
} catch (error: any) {
|
|
222
|
-
console.error(chalk.red(`\nā Failed to start Claude automatically: ${error.message}\n`));
|
|
223
|
-
console.log(
|
|
224
|
-
chalk.yellow(
|
|
225
|
-
`You can resume manually with: claude -r ${claudeSessionId} (from ${workingPath})`
|
|
226
|
-
)
|
|
227
|
-
);
|
|
228
|
-
process.exit(1);
|
|
229
|
-
}
|
|
230
|
-
} catch (error: any) {
|
|
231
|
-
console.error(chalk.red(`\nā Local command failed: ${error.message}\n`));
|
|
232
|
-
|
|
233
|
-
if (error.message.includes('401') || error.message.includes('Unauthorized')) {
|
|
234
|
-
console.log(chalk.yellow('Your session may have expired. Please run: edgedive login\n'));
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
process.exit(1);
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
/**
|
|
242
|
-
* Upload Claude session file back to Edgedive after local development
|
|
243
|
-
*/
|
|
244
|
-
async function uploadClaudeSessionFile(
|
|
245
|
-
apiClient: EdgediveApiClient,
|
|
246
|
-
sessionData: TakeoverResponse,
|
|
247
|
-
sessionInfo: { transcript_path?: string; claudeSessionPath?: string }
|
|
248
|
-
): Promise<void> {
|
|
249
|
-
try {
|
|
250
|
-
const fs = (await import('fs/promises')).default;
|
|
251
|
-
|
|
252
|
-
// Use transcript_path from SessionEnd hook if available, otherwise fall back to original path
|
|
253
|
-
const sessionFilePath = sessionInfo.transcript_path || sessionInfo.claudeSessionPath;
|
|
254
|
-
|
|
255
|
-
if (!sessionFilePath) {
|
|
256
|
-
console.log(chalk.yellow('ā ļø No Claude session file path available, skipping upload'));
|
|
257
|
-
return;
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
// Check if file exists
|
|
261
|
-
try {
|
|
262
|
-
await fs.access(sessionFilePath);
|
|
263
|
-
} catch {
|
|
264
|
-
console.log(
|
|
265
|
-
chalk.yellow(`ā ļø Claude session file not found at ${sessionFilePath}, skipping upload`)
|
|
266
|
-
);
|
|
267
|
-
return;
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
console.log(chalk.blue('š¤ Uploading Claude session...'));
|
|
271
|
-
|
|
272
|
-
// Read file content
|
|
273
|
-
const fileContent = await fs.readFile(sessionFilePath);
|
|
274
|
-
|
|
275
|
-
// Upload to server
|
|
276
|
-
await apiClient.uploadClaudeSession(sessionData.session_id, fileContent);
|
|
277
|
-
|
|
278
|
-
console.log(chalk.green('ā
Claude session uploaded successfully\n'));
|
|
279
|
-
} catch (error: any) {
|
|
280
|
-
// Don't fail the whole command if upload fails
|
|
281
|
-
console.error(chalk.yellow(`ā ļø Failed to upload Claude session: ${error.message}`));
|
|
282
|
-
console.error(
|
|
283
|
-
chalk.yellow(' Your local changes are safe, but were not synced to the server.\n')
|
|
284
|
-
);
|
|
285
|
-
}
|
|
286
|
-
}
|
package/src/commands/login.ts
DELETED
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Login command - initiates OAuth flow
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import chalk from 'chalk';
|
|
6
|
-
import { ConfigManager } from '../config/config-manager.js';
|
|
7
|
-
import { OAuthFlow } from '../auth/oauth-flow.js';
|
|
8
|
-
|
|
9
|
-
export async function loginCommand(): Promise<void> {
|
|
10
|
-
const configManager = new ConfigManager();
|
|
11
|
-
const oauthFlow = new OAuthFlow(configManager);
|
|
12
|
-
|
|
13
|
-
try {
|
|
14
|
-
console.log(chalk.bold('\nš Edgedive CLI Login\n'));
|
|
15
|
-
|
|
16
|
-
// Check if already authenticated
|
|
17
|
-
if (await configManager.isAuthenticated()) {
|
|
18
|
-
console.log(chalk.yellow('ā You are already logged in!'));
|
|
19
|
-
console.log(chalk.dim(`Config: ${configManager.getConfigPath()}\n`));
|
|
20
|
-
|
|
21
|
-
const { default: inquirer } = await import('inquirer');
|
|
22
|
-
const { reauth } = await inquirer.prompt([
|
|
23
|
-
{
|
|
24
|
-
type: 'confirm',
|
|
25
|
-
name: 'reauth',
|
|
26
|
-
message: 'Do you want to re-authenticate?',
|
|
27
|
-
default: false,
|
|
28
|
-
},
|
|
29
|
-
]);
|
|
30
|
-
|
|
31
|
-
if (!reauth) {
|
|
32
|
-
process.exit(0);
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// Start OAuth flow
|
|
37
|
-
const tokenResponse = await oauthFlow.authorize();
|
|
38
|
-
|
|
39
|
-
console.log(chalk.green.bold('ā
Successfully logged in!\n'));
|
|
40
|
-
console.log(chalk.dim(`Token type: ${tokenResponse.token_type}`));
|
|
41
|
-
console.log(chalk.dim(`Expires in: ${Math.floor(tokenResponse.expires_in / 86400)} days`));
|
|
42
|
-
console.log(chalk.dim(`Config saved to: ${configManager.getConfigPath()}\n`));
|
|
43
|
-
process.exit(0);
|
|
44
|
-
} catch (error: any) {
|
|
45
|
-
console.error(chalk.red(`\nā Login failed: ${error.message}\n`));
|
|
46
|
-
process.exit(1);
|
|
47
|
-
}
|
|
48
|
-
}
|
package/src/commands/logout.ts
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Logout command - removes stored credentials
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import chalk from 'chalk';
|
|
6
|
-
import { ConfigManager } from '../config/config-manager.js';
|
|
7
|
-
|
|
8
|
-
export async function logoutCommand(): Promise<void> {
|
|
9
|
-
const configManager = new ConfigManager();
|
|
10
|
-
|
|
11
|
-
try {
|
|
12
|
-
console.log(chalk.bold('\nš Edgedive CLI Logout\n'));
|
|
13
|
-
|
|
14
|
-
// Check if authenticated
|
|
15
|
-
if (!(await configManager.isAuthenticated())) {
|
|
16
|
-
console.log(chalk.yellow('You are not logged in.\n'));
|
|
17
|
-
return;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
// Delete config
|
|
21
|
-
await configManager.delete();
|
|
22
|
-
|
|
23
|
-
console.log(chalk.green('ā
Successfully logged out!\n'));
|
|
24
|
-
console.log(chalk.dim(`Removed config from: ${configManager.getConfigPath()}\n`));
|
|
25
|
-
} catch (error: any) {
|
|
26
|
-
console.error(chalk.red(`\nā Logout failed: ${error.message}\n`));
|
|
27
|
-
process.exit(1);
|
|
28
|
-
}
|
|
29
|
-
}
|
|
@@ -1,120 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Configuration manager for storing OAuth tokens and CLI settings
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import fs from 'fs/promises';
|
|
6
|
-
import path from 'path';
|
|
7
|
-
import os from 'os';
|
|
8
|
-
import { LOCAL_CONFIG } from '../constants.js';
|
|
9
|
-
|
|
10
|
-
export interface EdgediveConfig {
|
|
11
|
-
accessToken?: string;
|
|
12
|
-
tokenType?: string;
|
|
13
|
-
expiresAt?: number;
|
|
14
|
-
scope?: string;
|
|
15
|
-
refreshToken?: string;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export class ConfigManager {
|
|
19
|
-
private configPath: string;
|
|
20
|
-
|
|
21
|
-
constructor() {
|
|
22
|
-
const homeDir = os.homedir();
|
|
23
|
-
const configDir = path.join(homeDir, LOCAL_CONFIG.CONFIG_DIR);
|
|
24
|
-
this.configPath = path.join(configDir, LOCAL_CONFIG.CONFIG_FILE);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Load configuration from disk
|
|
29
|
-
*/
|
|
30
|
-
async load(): Promise<EdgediveConfig> {
|
|
31
|
-
try {
|
|
32
|
-
const data = await fs.readFile(this.configPath, 'utf-8');
|
|
33
|
-
return JSON.parse(data);
|
|
34
|
-
} catch (error: any) {
|
|
35
|
-
if (error.code === 'ENOENT') {
|
|
36
|
-
return {}; // File doesn't exist, return empty config
|
|
37
|
-
}
|
|
38
|
-
throw new Error(`Failed to load config: ${error.message}`);
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Save configuration to disk
|
|
44
|
-
*/
|
|
45
|
-
async save(config: EdgediveConfig): Promise<void> {
|
|
46
|
-
try {
|
|
47
|
-
const configDir = path.dirname(this.configPath);
|
|
48
|
-
await fs.mkdir(configDir, { recursive: true, mode: 0o700 });
|
|
49
|
-
await fs.writeFile(this.configPath, JSON.stringify(config, null, 2), { mode: 0o600 });
|
|
50
|
-
} catch (error: any) {
|
|
51
|
-
throw new Error(`Failed to save config: ${error.message}`);
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Delete configuration file
|
|
57
|
-
*/
|
|
58
|
-
async delete(): Promise<void> {
|
|
59
|
-
try {
|
|
60
|
-
await fs.unlink(this.configPath);
|
|
61
|
-
} catch (error: any) {
|
|
62
|
-
if (error.code !== 'ENOENT') {
|
|
63
|
-
throw new Error(`Failed to delete config: ${error.message}`);
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Check if user is authenticated
|
|
70
|
-
*/
|
|
71
|
-
async isAuthenticated(): Promise<boolean> {
|
|
72
|
-
const config = await this.load();
|
|
73
|
-
|
|
74
|
-
// If we have an access token that's not expired, we're authenticated
|
|
75
|
-
if (config.accessToken && (!config.expiresAt || config.expiresAt > Date.now())) {
|
|
76
|
-
return true;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// If we have a refresh token, we can refresh, so consider authenticated
|
|
80
|
-
if (config.refreshToken) {
|
|
81
|
-
return true;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
return false;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Get access token (even if expired - let the API client handle refresh)
|
|
89
|
-
*/
|
|
90
|
-
async getAccessToken(): Promise<string | null> {
|
|
91
|
-
const config = await this.load();
|
|
92
|
-
return config.accessToken || null;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* Get refresh token
|
|
97
|
-
*/
|
|
98
|
-
async getRefreshToken(): Promise<string | null> {
|
|
99
|
-
const config = await this.load();
|
|
100
|
-
return config.refreshToken || null;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Check if token is expired or will expire soon (within 5 minutes)
|
|
105
|
-
*/
|
|
106
|
-
isTokenExpiringSoon(config: EdgediveConfig): boolean {
|
|
107
|
-
if (!config.expiresAt) {
|
|
108
|
-
return false;
|
|
109
|
-
}
|
|
110
|
-
// Consider token expiring if less than 5 minutes remaining
|
|
111
|
-
return config.expiresAt < Date.now() + 5 * 60 * 1000;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Get configuration directory path
|
|
116
|
-
*/
|
|
117
|
-
getConfigPath(): string {
|
|
118
|
-
return this.configPath;
|
|
119
|
-
}
|
|
120
|
-
}
|
package/src/constants.ts
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import 'dotenv/config';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Edgedive CLI Constants
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
// OAuth configuration
|
|
8
|
-
export const OAUTH_CONFIG = {
|
|
9
|
-
CLIENT_ID: 'edgedive-cli',
|
|
10
|
-
REDIRECT_URI: 'http://localhost:8765/callback',
|
|
11
|
-
CALLBACK_PORT: 8765,
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
// API configuration
|
|
15
|
-
export const API_CONFIG = {
|
|
16
|
-
BASE_URL: process.env.EDGEDIVE_API_URL || 'https://api.edgedive.com',
|
|
17
|
-
AUTHORIZE_PATH: '/api/oauth/authorize',
|
|
18
|
-
TOKEN_PATH: '/api/oauth/token',
|
|
19
|
-
TAKEOVER_PATH: '/api/agents/agent-sessions/takeover',
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
// Local configuration
|
|
23
|
-
export const LOCAL_CONFIG = {
|
|
24
|
-
CONFIG_DIR: '.edgedive',
|
|
25
|
-
CONFIG_FILE: 'config.json',
|
|
26
|
-
CLAUDE_DIR: '.claude',
|
|
27
|
-
CLAUDE_PROJECTS_DIR: 'projects',
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
// HTTP timeouts
|
|
31
|
-
export const TIMEOUTS = {
|
|
32
|
-
DEFAULT_REQUEST_MS: 30000,
|
|
33
|
-
CALLBACK_SERVER_MS: 300000, // 5 minutes for OAuth flow
|
|
34
|
-
};
|
package/src/index.ts
DELETED
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Edgedive CLI - Local agent session management
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { Command } from 'commander';
|
|
8
|
-
import dotenv from 'dotenv';
|
|
9
|
-
import { createRequire } from 'module';
|
|
10
|
-
|
|
11
|
-
const require = createRequire(import.meta.url);
|
|
12
|
-
const packageJson = require('../package.json');
|
|
13
|
-
|
|
14
|
-
dotenv.config();
|
|
15
|
-
import { loginCommand } from './commands/login.js';
|
|
16
|
-
import { logoutCommand } from './commands/logout.js';
|
|
17
|
-
import { localCommand } from './commands/local.js';
|
|
18
|
-
|
|
19
|
-
const program = new Command();
|
|
20
|
-
|
|
21
|
-
program
|
|
22
|
-
.name('edgedive')
|
|
23
|
-
.description('Edgedive CLI for local agent session management')
|
|
24
|
-
.version(packageJson.version);
|
|
25
|
-
|
|
26
|
-
program
|
|
27
|
-
.command('login')
|
|
28
|
-
.description('Authenticate with Edgedive via OAuth')
|
|
29
|
-
.action(async () => {
|
|
30
|
-
await loginCommand();
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
program
|
|
34
|
-
.command('logout')
|
|
35
|
-
.description('Remove stored credentials')
|
|
36
|
-
.action(async () => {
|
|
37
|
-
await logoutCommand();
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
program
|
|
41
|
-
.command('local')
|
|
42
|
-
.description('Prepare an agent session locally')
|
|
43
|
-
.option('--pr-url <prUrl>', 'GitHub PR URL to load locally')
|
|
44
|
-
.option('--issue-url <issueUrl>', 'Linear issue URL to load locally')
|
|
45
|
-
.option('--thread-url <threadUrl>', 'Slack thread URL to load locally')
|
|
46
|
-
.option('--task-url <taskUrl>', 'Asana task URL to load locally')
|
|
47
|
-
.option('--session-url <sessionUrl>', 'Session share URL to load locally')
|
|
48
|
-
.option('--worktree', 'Create branch in a worktree instead of checking out')
|
|
49
|
-
.action(
|
|
50
|
-
async (options: {
|
|
51
|
-
prUrl?: string;
|
|
52
|
-
issueUrl?: string;
|
|
53
|
-
threadUrl?: string;
|
|
54
|
-
taskUrl?: string;
|
|
55
|
-
sessionUrl?: string;
|
|
56
|
-
worktree?: boolean;
|
|
57
|
-
}) => {
|
|
58
|
-
await localCommand(options);
|
|
59
|
-
}
|
|
60
|
-
);
|
|
61
|
-
|
|
62
|
-
program.parse();
|
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
import { spawn } from 'child_process';
|
|
2
|
-
import * as fs from 'fs/promises';
|
|
3
|
-
import * as path from 'path';
|
|
4
|
-
import * as os from 'os';
|
|
5
|
-
|
|
6
|
-
const getUserShell = (): string => process.env.SHELL || '/bin/zsh';
|
|
7
|
-
|
|
8
|
-
export interface SessionEndInfo {
|
|
9
|
-
session_id: string;
|
|
10
|
-
transcript_path: string;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Creates a temporary SessionEnd hook script to capture final session information
|
|
15
|
-
* This allows us to get the actual session ID after Claude exits, even if it was compacted
|
|
16
|
-
*/
|
|
17
|
-
async function createSessionEndHook(outputPath: string): Promise<string> {
|
|
18
|
-
const hookScript = `#!/bin/bash
|
|
19
|
-
# SessionEnd hook to capture final session info for Edgedive upload
|
|
20
|
-
# Reads session data from stdin and writes to a file
|
|
21
|
-
|
|
22
|
-
# Read the JSON payload from stdin
|
|
23
|
-
payload=$(cat)
|
|
24
|
-
|
|
25
|
-
# Write to output file
|
|
26
|
-
echo "$payload" > "${outputPath}"
|
|
27
|
-
`;
|
|
28
|
-
|
|
29
|
-
const hookPath = path.join(os.tmpdir(), `edgedive-session-end-hook-${Date.now()}.sh`);
|
|
30
|
-
await fs.writeFile(hookPath, hookScript, { mode: 0o755 });
|
|
31
|
-
return hookPath;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export async function launchClaudeSession(
|
|
35
|
-
claudeSessionId: string,
|
|
36
|
-
workspacePath: string
|
|
37
|
-
): Promise<SessionEndInfo | null> {
|
|
38
|
-
const sessionInfoPath = path.join(os.tmpdir(), `edgedive-session-info-${Date.now()}.json`);
|
|
39
|
-
let hookPath: string | null = null;
|
|
40
|
-
|
|
41
|
-
try {
|
|
42
|
-
// Create the SessionEnd hook
|
|
43
|
-
hookPath = await createSessionEndHook(sessionInfoPath);
|
|
44
|
-
|
|
45
|
-
await new Promise<void>((resolve, reject) => {
|
|
46
|
-
const shell = getUserShell();
|
|
47
|
-
// Set the CLAUDE_SESSION_END_HOOK environment variable to our hook script
|
|
48
|
-
const child = spawn(shell, ['-i', '-c', `claude -r ${claudeSessionId} --settings '{"includeCoAuthoredBy": false}'`], {
|
|
49
|
-
cwd: workspacePath,
|
|
50
|
-
stdio: 'inherit',
|
|
51
|
-
env: {
|
|
52
|
-
...process.env,
|
|
53
|
-
CLAUDE_SESSION_END_HOOK: hookPath!,
|
|
54
|
-
},
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
child.on('error', (error: any) => {
|
|
58
|
-
if (error.code === 'ENOENT') {
|
|
59
|
-
reject(
|
|
60
|
-
new Error(
|
|
61
|
-
'`claude` command not found. Install the Claude CLI and ensure it is available on your PATH.'
|
|
62
|
-
)
|
|
63
|
-
);
|
|
64
|
-
} else {
|
|
65
|
-
reject(error);
|
|
66
|
-
}
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
child.on('exit', (code) => {
|
|
70
|
-
if (code === 0) {
|
|
71
|
-
resolve();
|
|
72
|
-
} else {
|
|
73
|
-
reject(new Error(`claude -r exited with code ${code}`));
|
|
74
|
-
}
|
|
75
|
-
});
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
// Read the session info captured by the hook
|
|
79
|
-
try {
|
|
80
|
-
const sessionInfoJson = await fs.readFile(sessionInfoPath, 'utf-8');
|
|
81
|
-
const sessionInfo = JSON.parse(sessionInfoJson) as SessionEndInfo;
|
|
82
|
-
return sessionInfo;
|
|
83
|
-
} catch (error) {
|
|
84
|
-
// Hook didn't write session info - return null
|
|
85
|
-
return null;
|
|
86
|
-
}
|
|
87
|
-
} finally {
|
|
88
|
-
// Cleanup temporary files
|
|
89
|
-
if (hookPath) {
|
|
90
|
-
await fs.unlink(hookPath).catch(() => {});
|
|
91
|
-
}
|
|
92
|
-
await fs.unlink(sessionInfoPath).catch(() => {});
|
|
93
|
-
}
|
|
94
|
-
}
|