agent-relay 2.1.26 → 2.1.27-beta.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/index.cjs +2 -2
- package/dist/src/cli/index.d.ts.map +1 -1
- package/dist/src/cli/index.js +530 -18
- package/dist/src/cli/index.js.map +1 -1
- package/package.json +18 -18
- package/packages/acp-bridge/package.json +2 -2
- package/packages/api-types/package.json +1 -1
- package/packages/benchmark/package.json +5 -5
- package/packages/bridge/package.json +7 -7
- package/packages/cli-tester/package.json +1 -1
- package/packages/config/package.json +2 -2
- package/packages/continuity/package.json +2 -2
- package/packages/daemon/package.json +12 -12
- package/packages/hooks/package.json +4 -4
- package/packages/mcp/package.json +5 -5
- package/packages/memory/package.json +2 -2
- package/packages/policy/package.json +2 -2
- package/packages/protocol/package.json +1 -1
- package/packages/resiliency/package.json +1 -1
- package/packages/sdk/dist/client.d.ts +1 -1
- package/packages/sdk/dist/client.js +2 -2
- package/packages/sdk/package.json +3 -3
- package/packages/sdk/src/client.ts +2 -2
- package/packages/spawner/package.json +1 -1
- package/packages/state/package.json +1 -1
- package/packages/storage/package.json +2 -2
- package/packages/telemetry/package.json +1 -1
- package/packages/trajectory/package.json +2 -2
- package/packages/user-directory/package.json +2 -2
- package/packages/utils/package.json +3 -3
- package/packages/wrapper/dist/client.js +1 -1
- package/packages/wrapper/package.json +6 -6
- package/packages/wrapper/src/client.test.ts +1 -1
- package/packages/wrapper/src/client.ts +1 -1
package/dist/index.cjs
CHANGED
|
@@ -33354,8 +33354,8 @@ var RelayClient = class _RelayClient {
|
|
|
33354
33354
|
const result = await this.requestResponse(
|
|
33355
33355
|
"SPAWN",
|
|
33356
33356
|
payload,
|
|
33357
|
-
|
|
33358
|
-
//
|
|
33357
|
+
6e4
|
|
33358
|
+
// 60 second timeout for spawn
|
|
33359
33359
|
);
|
|
33360
33360
|
return toSpawnResult(result);
|
|
33361
33361
|
} catch (e) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/cli/index.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;;;;;GAgBG;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/cli/index.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;;;;;GAgBG;AA+VH;;;;GAIG;AACH,wBAAsB,oBAAoB,CAAC,OAAO,CAAC,EAAE;IAAE,MAAM,CAAC,EAAE,OAAO,CAAA;CAAE,GAAG,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,SAAS,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC,CAqC7H"}
|
package/dist/src/cli/index.js
CHANGED
|
@@ -24,6 +24,7 @@ import { RelayPtyOrchestrator, getTmuxPath } from '@agent-relay/wrapper';
|
|
|
24
24
|
import { AgentSpawner, readWorkersMetadata, getWorkerLogsDir, selectShadowCli, ensureMcpPermissions } from '@agent-relay/bridge';
|
|
25
25
|
import { generateAgentName, checkForUpdatesInBackground, checkForUpdates } from '@agent-relay/utils';
|
|
26
26
|
import { getShadowForAgent, getProjectPaths, loadRuntimeConfig } from '@agent-relay/config';
|
|
27
|
+
import { CLI_AUTH_CONFIG, stripAnsiCodes } from '@agent-relay/config/cli-auth-config';
|
|
27
28
|
import { createStorageAdapter } from '@agent-relay/storage/adapter';
|
|
28
29
|
import { initTelemetry, track, enableTelemetry, disableTelemetry, getStatus, isDisabledByEnv, } from '@agent-relay/telemetry';
|
|
29
30
|
import { installMcpConfig } from '@agent-relay/mcp';
|
|
@@ -3592,17 +3593,468 @@ program
|
|
|
3592
3593
|
console.log(`Profiling ${agentName}... Press Ctrl+C to stop.`);
|
|
3593
3594
|
});
|
|
3594
3595
|
// ============================================================================
|
|
3595
|
-
//
|
|
3596
|
+
// auth - SSH-based interactive provider authentication (cloud workspaces)
|
|
3596
3597
|
// ============================================================================
|
|
3597
3598
|
program
|
|
3598
|
-
.command('
|
|
3599
|
-
.description('
|
|
3600
|
-
.option('--workspace <id>', 'Workspace ID to
|
|
3601
|
-
.option('--
|
|
3602
|
-
.option('--
|
|
3603
|
-
.option('--session-cookie <cookie>', 'Session cookie for authentication (deprecated, use --token)')
|
|
3599
|
+
.command('auth <provider>')
|
|
3600
|
+
.description('Authenticate a provider CLI in a cloud workspace over SSH (interactive)')
|
|
3601
|
+
.option('--workspace <id>', 'Workspace ID to authenticate in')
|
|
3602
|
+
.option('--token <token>', 'One-time CLI token from dashboard (skips cloud config requirement)')
|
|
3603
|
+
.option('--cloud-url <url>', 'Cloud API URL (overrides linked config and AGENT_RELAY_CLOUD_URL)')
|
|
3604
3604
|
.option('--timeout <seconds>', 'Timeout in seconds (default: 300)', '300')
|
|
3605
|
-
.action(async (options) => {
|
|
3605
|
+
.action(async (providerArg, options) => {
|
|
3606
|
+
const cyan = (s) => `\x1b[36m${s}\x1b[0m`;
|
|
3607
|
+
const green = (s) => `\x1b[32m${s}\x1b[0m`;
|
|
3608
|
+
const yellow = (s) => `\x1b[33m${s}\x1b[0m`;
|
|
3609
|
+
const red = (s) => `\x1b[31m${s}\x1b[0m`;
|
|
3610
|
+
const dim = (s) => `\x1b[2m${s}\x1b[0m`;
|
|
3611
|
+
const timeoutSeconds = parseInt(options.timeout, 10);
|
|
3612
|
+
if (!Number.isFinite(timeoutSeconds) || timeoutSeconds <= 0) {
|
|
3613
|
+
console.log(red(`Invalid --timeout value: ${options.timeout}`));
|
|
3614
|
+
process.exit(1);
|
|
3615
|
+
}
|
|
3616
|
+
const TIMEOUT_MS = timeoutSeconds * 1000;
|
|
3617
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
3618
|
+
console.log(red('This command requires an interactive terminal (TTY).'));
|
|
3619
|
+
console.log(dim('Run it directly in your terminal (not piped/redirected).'));
|
|
3620
|
+
process.exit(1);
|
|
3621
|
+
}
|
|
3622
|
+
const providerInput = providerArg.toLowerCase().trim();
|
|
3623
|
+
const providerMap = {
|
|
3624
|
+
claude: 'anthropic',
|
|
3625
|
+
codex: 'openai',
|
|
3626
|
+
gemini: 'google',
|
|
3627
|
+
};
|
|
3628
|
+
const provider = providerMap[providerInput] || providerInput;
|
|
3629
|
+
const providerConfig = CLI_AUTH_CONFIG[provider];
|
|
3630
|
+
if (!providerConfig) {
|
|
3631
|
+
const known = Object.keys(CLI_AUTH_CONFIG).sort();
|
|
3632
|
+
console.log(red(`Unknown provider: ${providerArg}`));
|
|
3633
|
+
console.log('');
|
|
3634
|
+
console.log('Examples:');
|
|
3635
|
+
console.log(` ${cyan('npx agent-relay auth claude --workspace=<ID>')}`);
|
|
3636
|
+
console.log(` ${cyan('npx agent-relay auth codex --workspace=<ID>')}`);
|
|
3637
|
+
console.log(` ${cyan('npx agent-relay auth gemini --workspace=<ID>')}`);
|
|
3638
|
+
console.log('');
|
|
3639
|
+
console.log('Supported provider ids:');
|
|
3640
|
+
console.log(` ${known.join(', ')}`);
|
|
3641
|
+
process.exit(1);
|
|
3642
|
+
}
|
|
3643
|
+
const shellEscape = (s) => {
|
|
3644
|
+
// Basic POSIX shell escaping for args (remote exec uses a shell).
|
|
3645
|
+
if (s.length === 0)
|
|
3646
|
+
return "''";
|
|
3647
|
+
if (/^[a-zA-Z0-9_\\/\\-\\.=:]+$/.test(s))
|
|
3648
|
+
return s;
|
|
3649
|
+
return `'${s.replace(/'/g, `'\"'\"'`)}'`;
|
|
3650
|
+
};
|
|
3651
|
+
const remoteCommandFallback = [providerConfig.command, ...providerConfig.args].map(shellEscape).join(' ');
|
|
3652
|
+
// When --token is provided (from dashboard CLI command), skip cloud config requirement.
|
|
3653
|
+
// The token authenticates directly with the /start endpoint.
|
|
3654
|
+
const cliToken = options.token;
|
|
3655
|
+
let cloudConfig = {};
|
|
3656
|
+
if (!cliToken) {
|
|
3657
|
+
const dataDir = process.env.AGENT_RELAY_DATA_DIR || path.join(homedir(), '.local', 'share', 'agent-relay');
|
|
3658
|
+
const configPath = path.join(dataDir, 'cloud-config.json');
|
|
3659
|
+
if (!fs.existsSync(configPath)) {
|
|
3660
|
+
console.log(red('Cloud config not found.'));
|
|
3661
|
+
console.log(dim(`Expected: ${configPath}`));
|
|
3662
|
+
console.log('');
|
|
3663
|
+
console.log(`Run ${cyan('agent-relay cloud link')} first to link this machine to Agent Relay Cloud.`);
|
|
3664
|
+
process.exit(1);
|
|
3665
|
+
}
|
|
3666
|
+
try {
|
|
3667
|
+
cloudConfig = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
3668
|
+
}
|
|
3669
|
+
catch (err) {
|
|
3670
|
+
console.log(red(`Failed to read cloud config: ${err instanceof Error ? err.message : String(err)}`));
|
|
3671
|
+
process.exit(1);
|
|
3672
|
+
}
|
|
3673
|
+
if (!cloudConfig.apiKey) {
|
|
3674
|
+
console.log(red('Cloud config is missing apiKey.'));
|
|
3675
|
+
console.log(dim(`Config path: ${configPath}`));
|
|
3676
|
+
console.log(`Re-link with ${cyan('agent-relay cloud link')}.`);
|
|
3677
|
+
process.exit(1);
|
|
3678
|
+
}
|
|
3679
|
+
}
|
|
3680
|
+
const CLOUD_URL = (options.cloudUrl || process.env.AGENT_RELAY_CLOUD_URL || cloudConfig.cloudUrl || 'https://agent-relay.com')
|
|
3681
|
+
.replace(/\/$/, '');
|
|
3682
|
+
const requestedWorkspaceId = options.workspace || process.env.WORKSPACE_ID;
|
|
3683
|
+
console.log('');
|
|
3684
|
+
console.log(cyan('═══════════════════════════════════════════════════'));
|
|
3685
|
+
console.log(cyan(' Provider Authentication (SSH)'));
|
|
3686
|
+
console.log(cyan('═══════════════════════════════════════════════════'));
|
|
3687
|
+
console.log('');
|
|
3688
|
+
console.log(`Provider: ${providerConfig.displayName} (${provider})`);
|
|
3689
|
+
console.log(`Workspace: ${requestedWorkspaceId ? `${requestedWorkspaceId.slice(0, 8)}...` : '(default)'}`);
|
|
3690
|
+
console.log(dim(`Cloud: ${CLOUD_URL}`));
|
|
3691
|
+
console.log('');
|
|
3692
|
+
// Step 1: Request SSH session info from cloud.
|
|
3693
|
+
console.log('Requesting SSH session from cloud...');
|
|
3694
|
+
let start;
|
|
3695
|
+
try {
|
|
3696
|
+
const headers = { 'Content-Type': 'application/json' };
|
|
3697
|
+
if (!cliToken && cloudConfig.apiKey) {
|
|
3698
|
+
headers['Authorization'] = `Bearer ${cloudConfig.apiKey}`;
|
|
3699
|
+
}
|
|
3700
|
+
const response = await fetch(`${CLOUD_URL}/api/auth/ssh/start`, {
|
|
3701
|
+
method: 'POST',
|
|
3702
|
+
headers,
|
|
3703
|
+
body: JSON.stringify({
|
|
3704
|
+
provider,
|
|
3705
|
+
workspaceId: requestedWorkspaceId,
|
|
3706
|
+
...(cliToken && { token: cliToken }),
|
|
3707
|
+
}),
|
|
3708
|
+
});
|
|
3709
|
+
if (!response.ok) {
|
|
3710
|
+
let details = response.statusText;
|
|
3711
|
+
try {
|
|
3712
|
+
const json = await response.json();
|
|
3713
|
+
details = json.error || json.message || details;
|
|
3714
|
+
}
|
|
3715
|
+
catch {
|
|
3716
|
+
try {
|
|
3717
|
+
details = await response.text();
|
|
3718
|
+
}
|
|
3719
|
+
catch {
|
|
3720
|
+
// ignore
|
|
3721
|
+
}
|
|
3722
|
+
}
|
|
3723
|
+
console.log(red(`Failed to start SSH auth session: ${details || response.statusText}`));
|
|
3724
|
+
process.exit(1);
|
|
3725
|
+
}
|
|
3726
|
+
start = await response.json();
|
|
3727
|
+
}
|
|
3728
|
+
catch (err) {
|
|
3729
|
+
console.log(red(`Failed to connect to cloud API: ${err instanceof Error ? err.message : String(err)}`));
|
|
3730
|
+
process.exit(1);
|
|
3731
|
+
}
|
|
3732
|
+
const sshPort = typeof start.ssh?.port === 'string' ? parseInt(start.ssh.port, 10) : start.ssh?.port;
|
|
3733
|
+
if (!start.sessionId || !start.workspaceId || !start.ssh?.host || !sshPort || !start.ssh.user || !start.ssh.password) {
|
|
3734
|
+
console.log(red('Cloud returned invalid SSH session details.'));
|
|
3735
|
+
process.exit(1);
|
|
3736
|
+
}
|
|
3737
|
+
const remoteCommand = (typeof start.command === 'string' && start.command.trim().length > 0)
|
|
3738
|
+
? start.command.trim()
|
|
3739
|
+
: remoteCommandFallback;
|
|
3740
|
+
console.log(green('✓ SSH session created'));
|
|
3741
|
+
if (start.workspaceName) {
|
|
3742
|
+
console.log(`Workspace: ${cyan(start.workspaceName)} (${start.workspaceId.slice(0, 8)}...)`);
|
|
3743
|
+
}
|
|
3744
|
+
else {
|
|
3745
|
+
console.log(`Workspace: ${start.workspaceId.slice(0, 8)}...`);
|
|
3746
|
+
}
|
|
3747
|
+
console.log(dim(` SSH: ${start.ssh.user}@${start.ssh.host}:${sshPort}`));
|
|
3748
|
+
console.log(dim(` Command: ${remoteCommand}`));
|
|
3749
|
+
console.log('');
|
|
3750
|
+
const { Client } = await import('ssh2');
|
|
3751
|
+
const net = await import('node:net');
|
|
3752
|
+
const TUNNEL_PORT = 1455;
|
|
3753
|
+
const sshClient = new Client();
|
|
3754
|
+
let sshReady = false;
|
|
3755
|
+
const tunnel = { server: null };
|
|
3756
|
+
const sshReadyPromise = new Promise((resolve, reject) => {
|
|
3757
|
+
sshClient.on('ready', () => {
|
|
3758
|
+
sshReady = true;
|
|
3759
|
+
// Set up port forwarding for OAuth callbacks: localhost:1455 -> workspace:1455
|
|
3760
|
+
tunnel.server = net.createServer((localSocket) => {
|
|
3761
|
+
sshClient.forwardOut('127.0.0.1', TUNNEL_PORT, 'localhost', TUNNEL_PORT, (err, stream) => {
|
|
3762
|
+
if (err) {
|
|
3763
|
+
localSocket.end();
|
|
3764
|
+
return;
|
|
3765
|
+
}
|
|
3766
|
+
localSocket.pipe(stream).pipe(localSocket);
|
|
3767
|
+
});
|
|
3768
|
+
});
|
|
3769
|
+
tunnel.server.on('error', (err) => {
|
|
3770
|
+
if (err.code === 'EADDRINUSE') {
|
|
3771
|
+
console.log(dim(`Note: Port ${TUNNEL_PORT} in use, OAuth callbacks may not work.`));
|
|
3772
|
+
}
|
|
3773
|
+
// Non-fatal: resolve anyway so the auth command still runs.
|
|
3774
|
+
// Device-flow providers don't need port forwarding.
|
|
3775
|
+
resolve();
|
|
3776
|
+
});
|
|
3777
|
+
tunnel.server.listen(TUNNEL_PORT, '127.0.0.1', () => {
|
|
3778
|
+
resolve();
|
|
3779
|
+
});
|
|
3780
|
+
});
|
|
3781
|
+
sshClient.on('error', (err) => {
|
|
3782
|
+
let msg;
|
|
3783
|
+
if (err.message.includes('Authentication')) {
|
|
3784
|
+
msg = 'SSH authentication failed.';
|
|
3785
|
+
}
|
|
3786
|
+
else if (err.message.includes('ECONNREFUSED')) {
|
|
3787
|
+
msg = `Cannot connect to SSH server at ${start.ssh.host}:${sshPort}. Is the workspace running and SSH enabled?`;
|
|
3788
|
+
}
|
|
3789
|
+
else if (err.message.includes('ENOTFOUND') || err.message.includes('getaddrinfo')) {
|
|
3790
|
+
msg = `Cannot resolve hostname: ${start.ssh.host}. Check network connectivity.`;
|
|
3791
|
+
}
|
|
3792
|
+
else if (err.message.includes('ETIMEDOUT')) {
|
|
3793
|
+
msg = `Connection timed out to ${start.ssh.host}:${sshPort}. Is the workspace running?`;
|
|
3794
|
+
}
|
|
3795
|
+
else {
|
|
3796
|
+
msg = `SSH error: ${err.message}`;
|
|
3797
|
+
}
|
|
3798
|
+
reject(new Error(msg));
|
|
3799
|
+
});
|
|
3800
|
+
sshClient.on('close', () => {
|
|
3801
|
+
if (!sshReady) {
|
|
3802
|
+
reject(new Error(`SSH connection to ${start.ssh.host}:${sshPort} closed unexpectedly.`));
|
|
3803
|
+
}
|
|
3804
|
+
});
|
|
3805
|
+
});
|
|
3806
|
+
console.log(yellow('Connecting via SSH...'));
|
|
3807
|
+
console.log(dim(` Tunnel: localhost:${TUNNEL_PORT} → workspace:${TUNNEL_PORT}`));
|
|
3808
|
+
console.log(dim(` Running: ${remoteCommand}`));
|
|
3809
|
+
console.log('');
|
|
3810
|
+
try {
|
|
3811
|
+
sshClient.connect({
|
|
3812
|
+
host: start.ssh.host,
|
|
3813
|
+
port: sshPort,
|
|
3814
|
+
username: start.ssh.user,
|
|
3815
|
+
password: start.ssh.password,
|
|
3816
|
+
readyTimeout: 10000,
|
|
3817
|
+
// Workspace containers use ephemeral SSH hosts; skip host key checking.
|
|
3818
|
+
hostVerifier: () => true,
|
|
3819
|
+
});
|
|
3820
|
+
await Promise.race([
|
|
3821
|
+
sshReadyPromise,
|
|
3822
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error('SSH connection timeout')), 15000)),
|
|
3823
|
+
]);
|
|
3824
|
+
}
|
|
3825
|
+
catch (err) {
|
|
3826
|
+
console.log(red(`Failed to connect via SSH: ${err instanceof Error ? err.message : String(err)}`));
|
|
3827
|
+
if (tunnel.server)
|
|
3828
|
+
tunnel.server.close();
|
|
3829
|
+
sshClient.end();
|
|
3830
|
+
process.exit(1);
|
|
3831
|
+
}
|
|
3832
|
+
// Get success/error patterns from CLI_AUTH_CONFIG for auto-detection
|
|
3833
|
+
const successPatterns = providerConfig.successPatterns || [];
|
|
3834
|
+
const errorPatterns = providerConfig.errorPatterns || [];
|
|
3835
|
+
const execInteractive = async (cmd, timeoutMs) => {
|
|
3836
|
+
return await new Promise((resolve, reject) => {
|
|
3837
|
+
const cols = process.stdout.columns || 80;
|
|
3838
|
+
const rows = process.stdout.rows || 24;
|
|
3839
|
+
const term = process.env.TERM || 'xterm-256color';
|
|
3840
|
+
sshClient.exec(cmd, { pty: { term, cols, rows } }, (err, stream) => {
|
|
3841
|
+
if (err)
|
|
3842
|
+
return reject(err);
|
|
3843
|
+
let exitCode = null;
|
|
3844
|
+
let exitSignal = null;
|
|
3845
|
+
let authDetected = false;
|
|
3846
|
+
let outputBuffer = ''; // Rolling buffer for pattern matching
|
|
3847
|
+
const stdin = process.stdin;
|
|
3848
|
+
const stdout = process.stdout;
|
|
3849
|
+
const stderr = process.stderr;
|
|
3850
|
+
const wasRaw = stdin.isRaw ?? false;
|
|
3851
|
+
try {
|
|
3852
|
+
stdin.setRawMode?.(true);
|
|
3853
|
+
}
|
|
3854
|
+
catch {
|
|
3855
|
+
// ignore
|
|
3856
|
+
}
|
|
3857
|
+
stdin.resume();
|
|
3858
|
+
const onStdinData = (data) => {
|
|
3859
|
+
stream.write(data);
|
|
3860
|
+
};
|
|
3861
|
+
stdin.on('data', onStdinData);
|
|
3862
|
+
const cleanup = () => {
|
|
3863
|
+
stdin.off('data', onStdinData);
|
|
3864
|
+
stdout.off('resize', onResize);
|
|
3865
|
+
try {
|
|
3866
|
+
stdin.setRawMode?.(wasRaw);
|
|
3867
|
+
}
|
|
3868
|
+
catch {
|
|
3869
|
+
// ignore
|
|
3870
|
+
}
|
|
3871
|
+
stdin.pause();
|
|
3872
|
+
};
|
|
3873
|
+
// Auto-close the session when auth success is detected
|
|
3874
|
+
const closeOnAuthSuccess = () => {
|
|
3875
|
+
authDetected = true;
|
|
3876
|
+
// Brief delay so the user sees the success message
|
|
3877
|
+
setTimeout(() => {
|
|
3878
|
+
cleanup();
|
|
3879
|
+
clearTimeout(timer);
|
|
3880
|
+
try {
|
|
3881
|
+
stream.close();
|
|
3882
|
+
}
|
|
3883
|
+
catch {
|
|
3884
|
+
// ignore
|
|
3885
|
+
}
|
|
3886
|
+
}, 1500);
|
|
3887
|
+
};
|
|
3888
|
+
stream.on('data', (data) => {
|
|
3889
|
+
stdout.write(data);
|
|
3890
|
+
// Accumulate output for pattern matching (keep last 2KB to avoid memory growth)
|
|
3891
|
+
const text = data.toString();
|
|
3892
|
+
outputBuffer += text;
|
|
3893
|
+
if (outputBuffer.length > 2048) {
|
|
3894
|
+
outputBuffer = outputBuffer.slice(-2048);
|
|
3895
|
+
}
|
|
3896
|
+
// Check for auth success patterns
|
|
3897
|
+
if (!authDetected && successPatterns.length > 0) {
|
|
3898
|
+
const clean = stripAnsiCodes(outputBuffer);
|
|
3899
|
+
for (const pattern of successPatterns) {
|
|
3900
|
+
if (pattern.test(clean)) {
|
|
3901
|
+
closeOnAuthSuccess();
|
|
3902
|
+
break;
|
|
3903
|
+
}
|
|
3904
|
+
}
|
|
3905
|
+
}
|
|
3906
|
+
});
|
|
3907
|
+
stream.stderr.on('data', (data) => {
|
|
3908
|
+
stderr.write(data);
|
|
3909
|
+
});
|
|
3910
|
+
const onResize = () => {
|
|
3911
|
+
try {
|
|
3912
|
+
stream.setWindow(stdout.rows || 24, stdout.columns || 80, 0, 0);
|
|
3913
|
+
}
|
|
3914
|
+
catch {
|
|
3915
|
+
// ignore
|
|
3916
|
+
}
|
|
3917
|
+
};
|
|
3918
|
+
stdout.on('resize', onResize);
|
|
3919
|
+
const timer = setTimeout(() => {
|
|
3920
|
+
cleanup();
|
|
3921
|
+
try {
|
|
3922
|
+
stream.close();
|
|
3923
|
+
}
|
|
3924
|
+
catch {
|
|
3925
|
+
// ignore
|
|
3926
|
+
}
|
|
3927
|
+
reject(new Error(`Authentication timed out after ${Math.floor(timeoutMs / 1000)}s`));
|
|
3928
|
+
}, timeoutMs);
|
|
3929
|
+
stream.on('exit', (code, signal) => {
|
|
3930
|
+
if (typeof code === 'number')
|
|
3931
|
+
exitCode = code;
|
|
3932
|
+
if (typeof signal === 'string')
|
|
3933
|
+
exitSignal = signal;
|
|
3934
|
+
});
|
|
3935
|
+
stream.on('close', () => {
|
|
3936
|
+
clearTimeout(timer);
|
|
3937
|
+
cleanup();
|
|
3938
|
+
resolve({ exitCode, exitSignal, authDetected });
|
|
3939
|
+
});
|
|
3940
|
+
stream.on('error', (e) => {
|
|
3941
|
+
clearTimeout(timer);
|
|
3942
|
+
cleanup();
|
|
3943
|
+
reject(e instanceof Error ? e : new Error(String(e)));
|
|
3944
|
+
});
|
|
3945
|
+
});
|
|
3946
|
+
});
|
|
3947
|
+
};
|
|
3948
|
+
let execResult = null;
|
|
3949
|
+
let execError = null;
|
|
3950
|
+
try {
|
|
3951
|
+
console.log(yellow('Starting interactive authentication...'));
|
|
3952
|
+
console.log(dim('Follow the prompts below. The session will close automatically when auth completes.'));
|
|
3953
|
+
console.log('');
|
|
3954
|
+
execResult = await execInteractive(remoteCommand, TIMEOUT_MS);
|
|
3955
|
+
}
|
|
3956
|
+
catch (err) {
|
|
3957
|
+
execError = err instanceof Error ? err : new Error(String(err));
|
|
3958
|
+
console.log('');
|
|
3959
|
+
console.log(red(`Remote auth command failed: ${execError.message}`));
|
|
3960
|
+
}
|
|
3961
|
+
finally {
|
|
3962
|
+
if (tunnel.server)
|
|
3963
|
+
tunnel.server.close();
|
|
3964
|
+
sshClient.end();
|
|
3965
|
+
}
|
|
3966
|
+
// Step 2: Notify cloud completion (cloud will verify and persist credentials).
|
|
3967
|
+
console.log('');
|
|
3968
|
+
console.log('Finalizing authentication with cloud...');
|
|
3969
|
+
// Auth is successful if: success patterns were detected, OR the process exited cleanly (code 0)
|
|
3970
|
+
const success = execError === null && (execResult?.authDetected === true || execResult?.exitCode === 0);
|
|
3971
|
+
const providerForComplete = (typeof start.provider === 'string' && start.provider.trim().length > 0)
|
|
3972
|
+
? start.provider.trim()
|
|
3973
|
+
: provider;
|
|
3974
|
+
try {
|
|
3975
|
+
const response = await fetch(`${CLOUD_URL}/api/auth/ssh/complete`, {
|
|
3976
|
+
method: 'POST',
|
|
3977
|
+
headers: {
|
|
3978
|
+
'Authorization': `Bearer ${cloudConfig.apiKey}`,
|
|
3979
|
+
'Content-Type': 'application/json',
|
|
3980
|
+
},
|
|
3981
|
+
body: JSON.stringify({ sessionId: start.sessionId, workspaceId: start.workspaceId, provider: providerForComplete, success }),
|
|
3982
|
+
});
|
|
3983
|
+
if (!response.ok) {
|
|
3984
|
+
let details = response.statusText;
|
|
3985
|
+
try {
|
|
3986
|
+
const json = await response.json();
|
|
3987
|
+
details = json.error || json.message || details;
|
|
3988
|
+
}
|
|
3989
|
+
catch {
|
|
3990
|
+
try {
|
|
3991
|
+
details = await response.text();
|
|
3992
|
+
}
|
|
3993
|
+
catch {
|
|
3994
|
+
// ignore
|
|
3995
|
+
}
|
|
3996
|
+
}
|
|
3997
|
+
console.log(red(`Failed to complete auth session: ${details || response.statusText}`));
|
|
3998
|
+
process.exit(1);
|
|
3999
|
+
}
|
|
4000
|
+
}
|
|
4001
|
+
catch (err) {
|
|
4002
|
+
console.log(red(`Failed to complete auth session: ${err instanceof Error ? err.message : String(err)}`));
|
|
4003
|
+
process.exit(1);
|
|
4004
|
+
}
|
|
4005
|
+
if (!success) {
|
|
4006
|
+
const exitCode = execResult?.exitCode;
|
|
4007
|
+
if (typeof exitCode === 'number' && exitCode !== 0) {
|
|
4008
|
+
console.log('');
|
|
4009
|
+
console.log(red(`Remote auth command exited with code ${exitCode}.`));
|
|
4010
|
+
}
|
|
4011
|
+
process.exit(1);
|
|
4012
|
+
}
|
|
4013
|
+
console.log('');
|
|
4014
|
+
console.log(green('═══════════════════════════════════════════════════'));
|
|
4015
|
+
console.log(green(' Authentication Complete!'));
|
|
4016
|
+
console.log(green('═══════════════════════════════════════════════════'));
|
|
4017
|
+
console.log('');
|
|
4018
|
+
console.log(`${providerConfig.displayName} is now connected to workspace ${start.workspaceId.slice(0, 8)}...`);
|
|
4019
|
+
console.log('');
|
|
4020
|
+
});
|
|
4021
|
+
// ============================================================================
|
|
4022
|
+
// cli-auth - SSH tunnel helper for provider authentication (Claude, Codex, Cursor, etc.)
|
|
4023
|
+
// ============================================================================
|
|
4024
|
+
// Provider display names for CLI output
|
|
4025
|
+
const CLI_AUTH_DISPLAY_NAMES = {
|
|
4026
|
+
anthropic: 'Claude',
|
|
4027
|
+
openai: 'Codex',
|
|
4028
|
+
google: 'Gemini',
|
|
4029
|
+
cursor: 'Cursor',
|
|
4030
|
+
copilot: 'GitHub Copilot',
|
|
4031
|
+
opencode: 'OpenCode',
|
|
4032
|
+
droid: 'Droid',
|
|
4033
|
+
};
|
|
4034
|
+
// CLI command names per provider (used in help text)
|
|
4035
|
+
const CLI_AUTH_COMMAND_NAMES = {
|
|
4036
|
+
anthropic: 'claude',
|
|
4037
|
+
openai: 'codex',
|
|
4038
|
+
google: 'gemini',
|
|
4039
|
+
cursor: 'cursor',
|
|
4040
|
+
copilot: 'copilot',
|
|
4041
|
+
opencode: 'opencode',
|
|
4042
|
+
droid: 'droid',
|
|
4043
|
+
};
|
|
4044
|
+
// Provider alias mapping (CLI name → config key)
|
|
4045
|
+
const CLI_AUTH_PROVIDER_MAP = {
|
|
4046
|
+
claude: 'anthropic',
|
|
4047
|
+
codex: 'openai',
|
|
4048
|
+
gemini: 'google',
|
|
4049
|
+
};
|
|
4050
|
+
/**
|
|
4051
|
+
* Shared action handler for cli-auth and its provider-specific aliases.
|
|
4052
|
+
*/
|
|
4053
|
+
async function runCliAuth(providerArg, options) {
|
|
4054
|
+
// Resolve provider alias
|
|
4055
|
+
const provider = CLI_AUTH_PROVIDER_MAP[providerArg.toLowerCase()] || providerArg.toLowerCase();
|
|
4056
|
+
const displayName = CLI_AUTH_DISPLAY_NAMES[provider] || provider;
|
|
4057
|
+
const cliName = CLI_AUTH_COMMAND_NAMES[provider] || provider;
|
|
3606
4058
|
const TIMEOUT_MS = parseInt(options.timeout, 10) * 1000;
|
|
3607
4059
|
const CLOUD_URL = options.cloudUrl.replace(/\/$/, '');
|
|
3608
4060
|
const TUNNEL_PORT = 1455;
|
|
@@ -3614,25 +4066,26 @@ program
|
|
|
3614
4066
|
const dim = (s) => `\x1b[2m${s}\x1b[0m`;
|
|
3615
4067
|
console.log('');
|
|
3616
4068
|
console.log(cyan('═══════════════════════════════════════════════════'));
|
|
3617
|
-
console.log(cyan(
|
|
4069
|
+
console.log(cyan(` ${displayName} Authentication Helper`));
|
|
3618
4070
|
console.log(cyan('═══════════════════════════════════════════════════'));
|
|
3619
4071
|
console.log('');
|
|
3620
4072
|
if (!options.workspace) {
|
|
3621
4073
|
console.log(red('Missing --workspace parameter.'));
|
|
3622
4074
|
console.log('');
|
|
3623
|
-
console.log(
|
|
4075
|
+
console.log(`To connect ${displayName}, follow these steps:`);
|
|
3624
4076
|
console.log('');
|
|
3625
4077
|
console.log(' 1. Go to the Agent Relay dashboard');
|
|
3626
|
-
console.log(
|
|
4078
|
+
console.log(` 2. Click "Connect with ${displayName}" (Settings → AI Providers)`);
|
|
3627
4079
|
console.log(' 3. Copy the command shown (it includes the workspace ID and token)');
|
|
3628
4080
|
console.log(' 4. Run the command in your terminal');
|
|
3629
4081
|
console.log('');
|
|
3630
4082
|
console.log('The command will look like:');
|
|
3631
|
-
console.log(cyan(
|
|
4083
|
+
console.log(cyan(` npx agent-relay cli-auth ${cliName} --workspace=<ID> --token=<TOKEN>`));
|
|
3632
4084
|
console.log('');
|
|
3633
4085
|
process.exit(1);
|
|
3634
4086
|
}
|
|
3635
4087
|
const workspaceId = options.workspace;
|
|
4088
|
+
console.log(`Provider: ${displayName}`);
|
|
3636
4089
|
console.log(`Workspace: ${workspaceId.slice(0, 8)}...`);
|
|
3637
4090
|
// Get tunnel info from cloud API
|
|
3638
4091
|
console.log('Getting workspace connection info...');
|
|
@@ -3644,18 +4097,19 @@ program
|
|
|
3644
4097
|
if (!options.token && !options.sessionCookie) {
|
|
3645
4098
|
console.log(red('Missing --token parameter.'));
|
|
3646
4099
|
console.log('');
|
|
3647
|
-
console.log(
|
|
4100
|
+
console.log(`The token is provided by the dashboard when you click "Connect with ${displayName}".`);
|
|
3648
4101
|
console.log('Copy the complete command from the dashboard and paste it here.');
|
|
3649
4102
|
console.log('');
|
|
3650
4103
|
process.exit(1);
|
|
3651
4104
|
}
|
|
3652
4105
|
let tunnelInfo;
|
|
3653
4106
|
try {
|
|
3654
|
-
// Build URL with token query
|
|
4107
|
+
// Build URL with token and provider query parameters
|
|
3655
4108
|
const tunnelInfoUrl = new URL(`${CLOUD_URL}/api/auth/codex-helper/tunnel-info/${workspaceId}`);
|
|
3656
4109
|
if (options.token) {
|
|
3657
4110
|
tunnelInfoUrl.searchParams.set('token', options.token);
|
|
3658
4111
|
}
|
|
4112
|
+
tunnelInfoUrl.searchParams.set('provider', provider);
|
|
3659
4113
|
const response = await fetch(tunnelInfoUrl.toString(), {
|
|
3660
4114
|
method: 'GET',
|
|
3661
4115
|
headers,
|
|
@@ -3784,8 +4238,8 @@ program
|
|
|
3784
4238
|
console.log('');
|
|
3785
4239
|
console.log(cyan(tunnelInfo.authUrl));
|
|
3786
4240
|
console.log('');
|
|
3787
|
-
console.log(dim(
|
|
3788
|
-
console.log(dim(
|
|
4241
|
+
console.log(dim(`The browser will redirect to localhost:${TUNNEL_PORT}, which tunnels to the workspace.`));
|
|
4242
|
+
console.log(dim(`The ${displayName} CLI in the workspace will receive the callback and complete auth.`));
|
|
3789
4243
|
console.log('');
|
|
3790
4244
|
}
|
|
3791
4245
|
else {
|
|
@@ -3800,11 +4254,12 @@ program
|
|
|
3800
4254
|
while (!authenticated && (Date.now() - startTime) < TIMEOUT_MS) {
|
|
3801
4255
|
await new Promise(resolve => setTimeout(resolve, 3000));
|
|
3802
4256
|
try {
|
|
3803
|
-
// Build URL with token for authentication
|
|
4257
|
+
// Build URL with token and provider for authentication
|
|
3804
4258
|
const authStatusUrl = new URL(`${CLOUD_URL}/api/auth/codex-helper/auth-status/${workspaceId}`);
|
|
3805
4259
|
if (options.token) {
|
|
3806
4260
|
authStatusUrl.searchParams.set('token', options.token);
|
|
3807
4261
|
}
|
|
4262
|
+
authStatusUrl.searchParams.set('provider', provider);
|
|
3808
4263
|
const statusResponse = await fetch(authStatusUrl.toString(), { method: 'GET', headers, credentials: 'include' });
|
|
3809
4264
|
if (statusResponse.ok) {
|
|
3810
4265
|
const statusData = await statusResponse.json();
|
|
@@ -3832,7 +4287,7 @@ program
|
|
|
3832
4287
|
console.log(green(' Authentication Complete!'));
|
|
3833
4288
|
console.log(green('═══════════════════════════════════════════════════'));
|
|
3834
4289
|
console.log('');
|
|
3835
|
-
console.log(
|
|
4290
|
+
console.log(`Your ${displayName} account is now connected to the workspace.`);
|
|
3836
4291
|
console.log('You can close this terminal and return to the dashboard.');
|
|
3837
4292
|
console.log('');
|
|
3838
4293
|
}
|
|
@@ -3844,6 +4299,63 @@ program
|
|
|
3844
4299
|
console.log('the callback. Check if the SSH tunnel was working correctly.');
|
|
3845
4300
|
process.exit(1);
|
|
3846
4301
|
}
|
|
4302
|
+
}
|
|
4303
|
+
// Shared options for all cli-auth commands
|
|
4304
|
+
const cliAuthOptions = {
|
|
4305
|
+
workspace: '--workspace <id>',
|
|
4306
|
+
cloudUrl: '--cloud-url <url>',
|
|
4307
|
+
token: '--token <token>',
|
|
4308
|
+
sessionCookie: '--session-cookie <cookie>',
|
|
4309
|
+
timeout: '--timeout <seconds>',
|
|
4310
|
+
};
|
|
4311
|
+
const defaultCloudUrl = process.env.AGENT_RELAY_CLOUD_URL || 'https://agent-relay.com';
|
|
4312
|
+
// cli-auth <provider> - Generic command
|
|
4313
|
+
program
|
|
4314
|
+
.command('cli-auth <provider>')
|
|
4315
|
+
.description('Connect a provider via SSH tunnel to workspace (Claude, Codex, Cursor, etc.)')
|
|
4316
|
+
.option(cliAuthOptions.workspace, 'Workspace ID to connect to')
|
|
4317
|
+
.option(cliAuthOptions.cloudUrl, 'Cloud API URL', defaultCloudUrl)
|
|
4318
|
+
.option(cliAuthOptions.token, 'CLI authentication token (from dashboard)')
|
|
4319
|
+
.option(cliAuthOptions.sessionCookie, 'Session cookie for authentication (deprecated, use --token)')
|
|
4320
|
+
.option(cliAuthOptions.timeout, 'Timeout in seconds (default: 300)', '300')
|
|
4321
|
+
.action(async (providerArg, options) => {
|
|
4322
|
+
await runCliAuth(providerArg, options);
|
|
4323
|
+
});
|
|
4324
|
+
// codex-auth - Backward-compatible alias
|
|
4325
|
+
program
|
|
4326
|
+
.command('codex-auth')
|
|
4327
|
+
.description('Connect Codex via SSH tunnel to workspace (alias for cli-auth codex)')
|
|
4328
|
+
.option(cliAuthOptions.workspace, 'Workspace ID to connect to')
|
|
4329
|
+
.option(cliAuthOptions.cloudUrl, 'Cloud API URL', defaultCloudUrl)
|
|
4330
|
+
.option(cliAuthOptions.token, 'CLI authentication token (from dashboard)')
|
|
4331
|
+
.option(cliAuthOptions.sessionCookie, 'Session cookie for authentication (deprecated, use --token)')
|
|
4332
|
+
.option(cliAuthOptions.timeout, 'Timeout in seconds (default: 300)', '300')
|
|
4333
|
+
.action(async (options) => {
|
|
4334
|
+
await runCliAuth('codex', options);
|
|
4335
|
+
});
|
|
4336
|
+
// claude-auth - Alias for Claude/Anthropic
|
|
4337
|
+
program
|
|
4338
|
+
.command('claude-auth')
|
|
4339
|
+
.description('Connect Claude via SSH tunnel to workspace (alias for cli-auth claude)')
|
|
4340
|
+
.option(cliAuthOptions.workspace, 'Workspace ID to connect to')
|
|
4341
|
+
.option(cliAuthOptions.cloudUrl, 'Cloud API URL', defaultCloudUrl)
|
|
4342
|
+
.option(cliAuthOptions.token, 'CLI authentication token (from dashboard)')
|
|
4343
|
+
.option(cliAuthOptions.sessionCookie, 'Session cookie for authentication (deprecated, use --token)')
|
|
4344
|
+
.option(cliAuthOptions.timeout, 'Timeout in seconds (default: 300)', '300')
|
|
4345
|
+
.action(async (options) => {
|
|
4346
|
+
await runCliAuth('claude', options);
|
|
4347
|
+
});
|
|
4348
|
+
// cursor-auth - Alias for Cursor
|
|
4349
|
+
program
|
|
4350
|
+
.command('cursor-auth')
|
|
4351
|
+
.description('Connect Cursor via SSH tunnel to workspace (alias for cli-auth cursor)')
|
|
4352
|
+
.option(cliAuthOptions.workspace, 'Workspace ID to connect to')
|
|
4353
|
+
.option(cliAuthOptions.cloudUrl, 'Cloud API URL', defaultCloudUrl)
|
|
4354
|
+
.option(cliAuthOptions.token, 'CLI authentication token (from dashboard)')
|
|
4355
|
+
.option(cliAuthOptions.sessionCookie, 'Session cookie for authentication (deprecated, use --token)')
|
|
4356
|
+
.option(cliAuthOptions.timeout, 'Timeout in seconds (default: 300)', '300')
|
|
4357
|
+
.action(async (options) => {
|
|
4358
|
+
await runCliAuth('cursor', options);
|
|
3847
4359
|
});
|
|
3848
4360
|
// init - First-time setup wizard for Agent Relay
|
|
3849
4361
|
async function runInit(options) {
|