@edgedive/cli 0.2.1 ā 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/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/.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/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
|
@@ -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
|
-
}
|
package/src/utils/git-utils.ts
DELETED
|
@@ -1,179 +0,0 @@
|
|
|
1
|
-
import { promisify } from 'util';
|
|
2
|
-
import { exec as execCb } from 'child_process';
|
|
3
|
-
import * as path from 'path';
|
|
4
|
-
import * as fs from 'fs/promises';
|
|
5
|
-
|
|
6
|
-
const exec = promisify(execCb);
|
|
7
|
-
|
|
8
|
-
export interface RepoVerification {
|
|
9
|
-
rootPath: string;
|
|
10
|
-
remoteUrl: string;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
function parseGithubRemote(remoteUrl: string): { owner: string; repo: string } | null {
|
|
14
|
-
const patterns = [/github\.com[:/](?<owner>[^/]+)\/(?<repo>[^/.]+)(?:\.git)?$/];
|
|
15
|
-
|
|
16
|
-
for (const pattern of patterns) {
|
|
17
|
-
const match = remoteUrl.match(pattern);
|
|
18
|
-
if (match && match.groups) {
|
|
19
|
-
return {
|
|
20
|
-
owner: match.groups.owner.toLowerCase(),
|
|
21
|
-
repo: match.groups.repo.toLowerCase(),
|
|
22
|
-
};
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
return null;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
async function runGit(command: string, cwd: string): Promise<string> {
|
|
30
|
-
const { stdout } = await exec(`git ${command}`, { cwd });
|
|
31
|
-
return stdout.trim();
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export class GitUtils {
|
|
35
|
-
static async getRepoRoot(cwd: string): Promise<string> {
|
|
36
|
-
return runGit('rev-parse --show-toplevel', cwd);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
static async verifyRepoMatches(
|
|
40
|
-
expectedOwner: string,
|
|
41
|
-
expectedName: string,
|
|
42
|
-
cwd: string
|
|
43
|
-
): Promise<RepoVerification> {
|
|
44
|
-
const rootPath = await this.getRepoRoot(cwd);
|
|
45
|
-
const remoteUrl = await runGit('remote get-url origin', rootPath);
|
|
46
|
-
|
|
47
|
-
const parsed = parseGithubRemote(remoteUrl);
|
|
48
|
-
if (!parsed) {
|
|
49
|
-
throw new Error(
|
|
50
|
-
'Could not parse the origin remote URL. Ensure this repository is linked to GitHub.'
|
|
51
|
-
);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const matchesOwner = parsed.owner === expectedOwner.toLowerCase();
|
|
55
|
-
const matchesRepo = parsed.repo === expectedName.toLowerCase();
|
|
56
|
-
|
|
57
|
-
if (!matchesOwner || !matchesRepo) {
|
|
58
|
-
throw new Error(
|
|
59
|
-
`This repository (${parsed.owner}/${parsed.repo}) does not match the PR source ${expectedOwner}/${expectedName}.`
|
|
60
|
-
);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
return { rootPath, remoteUrl };
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
static async ensureBranchCheckedOut(branch: string, rootPath: string): Promise<void> {
|
|
67
|
-
// Get current branch
|
|
68
|
-
const currentBranch = await runGit('rev-parse --abbrev-ref HEAD', rootPath);
|
|
69
|
-
|
|
70
|
-
// Check if local branch exists
|
|
71
|
-
const localBranchExists = await exec(`git rev-parse --verify ${branch}`, { cwd: rootPath })
|
|
72
|
-
.then(() => true)
|
|
73
|
-
.catch(() => false);
|
|
74
|
-
|
|
75
|
-
// Try to fetch from remote (non-blocking - remote branch may not exist yet)
|
|
76
|
-
let remoteBranchExists = false;
|
|
77
|
-
try {
|
|
78
|
-
await runGit(`fetch origin ${branch}`, rootPath);
|
|
79
|
-
// Verify the remote branch exists
|
|
80
|
-
await runGit(`rev-parse --verify origin/${branch}`, rootPath);
|
|
81
|
-
remoteBranchExists = true;
|
|
82
|
-
} catch {
|
|
83
|
-
// Remote branch doesn't exist or fetch failed, continue anyway
|
|
84
|
-
remoteBranchExists = false;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// If already on the target branch
|
|
88
|
-
if (currentBranch === branch) {
|
|
89
|
-
// Pull from remote if it exists
|
|
90
|
-
if (remoteBranchExists) {
|
|
91
|
-
await runGit(`pull origin ${branch}`, rootPath);
|
|
92
|
-
}
|
|
93
|
-
return;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// If local branch exists, check it out
|
|
97
|
-
if (localBranchExists) {
|
|
98
|
-
await runGit(`checkout ${branch}`, rootPath);
|
|
99
|
-
// Pull from remote if it exists
|
|
100
|
-
if (remoteBranchExists) {
|
|
101
|
-
await runGit(`pull origin ${branch}`, rootPath);
|
|
102
|
-
}
|
|
103
|
-
return;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// Local branch doesn't exist, create it
|
|
107
|
-
if (remoteBranchExists) {
|
|
108
|
-
// Create from remote
|
|
109
|
-
await runGit(`checkout -b ${branch} origin/${branch}`, rootPath);
|
|
110
|
-
} else {
|
|
111
|
-
// Remote doesn't exist, create from current HEAD
|
|
112
|
-
await runGit(`checkout -b ${branch}`, rootPath);
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* Ensure a branch exists in a git worktree at a sibling directory.
|
|
118
|
-
* Returns the path to the worktree.
|
|
119
|
-
*/
|
|
120
|
-
static async ensureBranchInWorktree(branch: string, rootPath: string): Promise<string> {
|
|
121
|
-
// Sanitize branch name for directory: replace slashes with dashes
|
|
122
|
-
const sanitizedBranchName = branch.replace(/\//g, '-');
|
|
123
|
-
|
|
124
|
-
// Calculate worktree path as sibling directory
|
|
125
|
-
const parentDir = path.dirname(rootPath);
|
|
126
|
-
const worktreePath = path.join(parentDir, sanitizedBranchName);
|
|
127
|
-
|
|
128
|
-
// Check if worktree already exists
|
|
129
|
-
try {
|
|
130
|
-
await fs.access(worktreePath);
|
|
131
|
-
// Worktree directory exists, verify it's a valid git worktree
|
|
132
|
-
try {
|
|
133
|
-
await runGit('rev-parse --git-dir', worktreePath);
|
|
134
|
-
// It's a valid git directory, return the path
|
|
135
|
-
return worktreePath;
|
|
136
|
-
} catch {
|
|
137
|
-
// Directory exists but is not a git worktree
|
|
138
|
-
throw new Error(
|
|
139
|
-
`Directory ${worktreePath} already exists but is not a git worktree. ` +
|
|
140
|
-
`Please remove it or choose a different branch name.`
|
|
141
|
-
);
|
|
142
|
-
}
|
|
143
|
-
} catch {
|
|
144
|
-
// Worktree doesn't exist, we'll create it
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// Check if local branch exists
|
|
148
|
-
const localBranchExists = await exec(`git rev-parse --verify ${branch}`, { cwd: rootPath })
|
|
149
|
-
.then(() => true)
|
|
150
|
-
.catch(() => false);
|
|
151
|
-
|
|
152
|
-
// Try to fetch from remote
|
|
153
|
-
let remoteBranchExists = false;
|
|
154
|
-
try {
|
|
155
|
-
await runGit(`fetch origin ${branch}`, rootPath);
|
|
156
|
-
await runGit(`rev-parse --verify origin/${branch}`, rootPath);
|
|
157
|
-
remoteBranchExists = true;
|
|
158
|
-
} catch {
|
|
159
|
-
remoteBranchExists = false;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
// Create worktree based on what exists
|
|
163
|
-
if (localBranchExists) {
|
|
164
|
-
// Local branch exists, create worktree from it
|
|
165
|
-
await runGit(`worktree add "${worktreePath}" ${branch}`, rootPath);
|
|
166
|
-
} else if (remoteBranchExists) {
|
|
167
|
-
// Remote branch exists, create worktree tracking it
|
|
168
|
-
await runGit(
|
|
169
|
-
`worktree add --track -b ${branch} "${worktreePath}" origin/${branch}`,
|
|
170
|
-
rootPath
|
|
171
|
-
);
|
|
172
|
-
} else {
|
|
173
|
-
// Neither exists, create new branch in worktree from current HEAD
|
|
174
|
-
await runGit(`worktree add -b ${branch} "${worktreePath}"`, rootPath);
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
return worktreePath;
|
|
178
|
-
}
|
|
179
|
-
}
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Utility for downloading agent session files from S3
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import fs from 'fs/promises';
|
|
6
|
-
import path from 'path';
|
|
7
|
-
import os from 'os';
|
|
8
|
-
import axios from 'axios';
|
|
9
|
-
import { LOCAL_CONFIG, TIMEOUTS } from '../constants.js';
|
|
10
|
-
import type { TakeoverResponse } from '../api/client.js';
|
|
11
|
-
|
|
12
|
-
export class SessionDownloader {
|
|
13
|
-
private sanitizeWorkspacePath(workspacePath: string): string {
|
|
14
|
-
return workspacePath.replace(/[\\/_.]/g, '-');
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
private getClaudeProjectDir(workspacePath: string): string {
|
|
18
|
-
const homeDir = os.homedir();
|
|
19
|
-
const sanitized = this.sanitizeWorkspacePath(workspacePath);
|
|
20
|
-
return path.join(homeDir, LOCAL_CONFIG.CLAUDE_DIR, LOCAL_CONFIG.CLAUDE_PROJECTS_DIR, sanitized);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
private async downloadFile(url: string, targetPath: string): Promise<void> {
|
|
24
|
-
try {
|
|
25
|
-
const response = await axios.get(url, {
|
|
26
|
-
responseType: 'arraybuffer',
|
|
27
|
-
timeout: TIMEOUTS.DEFAULT_REQUEST_MS,
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
await fs.writeFile(targetPath, Buffer.from(response.data));
|
|
31
|
-
} catch (error: any) {
|
|
32
|
-
throw new Error(`Failed to download file to ${targetPath}: ${error.message}`);
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
async downloadSession(
|
|
37
|
-
sessionData: TakeoverResponse,
|
|
38
|
-
workspacePath: string
|
|
39
|
-
): Promise<{ claudeSessionId: string; claudeSessionPath: string }> {
|
|
40
|
-
let claudeSessionId = sessionData.session_id;
|
|
41
|
-
|
|
42
|
-
if (!sessionData.download_urls.claude_session) {
|
|
43
|
-
throw new Error('No Claude session file available for this agent session.');
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const claudeProjectDir = this.getClaudeProjectDir(workspacePath);
|
|
47
|
-
await fs.mkdir(claudeProjectDir, { recursive: true });
|
|
48
|
-
|
|
49
|
-
const claudeSessionPath = path.join(claudeProjectDir, `${claudeSessionId}.jsonl`);
|
|
50
|
-
await this.downloadFile(sessionData.download_urls.claude_session, claudeSessionPath);
|
|
51
|
-
|
|
52
|
-
console.log(`\nš Restored Claude session to: ${claudeSessionPath}\n`);
|
|
53
|
-
|
|
54
|
-
return { claudeSessionId, claudeSessionPath };
|
|
55
|
-
}
|
|
56
|
-
}
|
package/tsconfig.json
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"target": "ES2022",
|
|
4
|
-
"module": "ESNext",
|
|
5
|
-
"lib": ["ES2022"],
|
|
6
|
-
"moduleResolution": "node",
|
|
7
|
-
"outDir": "./dist",
|
|
8
|
-
"rootDir": "./src",
|
|
9
|
-
"strict": true,
|
|
10
|
-
"esModuleInterop": true,
|
|
11
|
-
"skipLibCheck": true,
|
|
12
|
-
"forceConsistentCasingInFileNames": true,
|
|
13
|
-
"resolveJsonModule": true,
|
|
14
|
-
"declaration": true,
|
|
15
|
-
"declarationMap": true,
|
|
16
|
-
"sourceMap": true
|
|
17
|
-
},
|
|
18
|
-
"include": ["src/**/*"],
|
|
19
|
-
"exclude": ["node_modules", "dist"]
|
|
20
|
-
}
|