agent-relay 3.2.18 → 3.2.22
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/bin/agent-relay-broker-darwin-arm64 +0 -0
- package/bin/agent-relay-broker-darwin-x64 +0 -0
- package/bin/agent-relay-broker-linux-arm64 +0 -0
- package/bin/agent-relay-broker-linux-x64 +0 -0
- package/dist/index.cjs +233 -55
- package/dist/src/cli/commands/cloud.d.ts +1 -9
- package/dist/src/cli/commands/cloud.d.ts.map +1 -1
- package/dist/src/cli/commands/cloud.js +326 -323
- package/dist/src/cli/commands/cloud.js.map +1 -1
- package/dist/src/cli/commands/connect.d.ts.map +1 -1
- package/dist/src/cli/commands/connect.js +6 -10
- package/dist/src/cli/commands/connect.js.map +1 -1
- package/package.json +16 -10
- package/packages/acp-bridge/package.json +2 -2
- package/packages/brand/README.md +36 -0
- package/packages/brand/brand.css +226 -0
- package/packages/brand/package.json +20 -0
- package/packages/cloud/dist/api-client.d.ts +33 -0
- package/packages/cloud/dist/api-client.d.ts.map +1 -0
- package/packages/cloud/dist/api-client.js +123 -0
- package/packages/cloud/dist/api-client.js.map +1 -0
- package/packages/cloud/dist/auth.d.ts +13 -0
- package/packages/cloud/dist/auth.d.ts.map +1 -0
- package/packages/cloud/dist/auth.js +248 -0
- package/packages/cloud/dist/auth.js.map +1 -0
- package/packages/cloud/dist/index.d.ts +5 -0
- package/packages/cloud/dist/index.d.ts.map +1 -0
- package/packages/cloud/dist/index.js +5 -0
- package/packages/cloud/dist/index.js.map +1 -0
- package/packages/cloud/dist/types.d.ts +73 -0
- package/packages/cloud/dist/types.d.ts.map +1 -0
- package/packages/cloud/dist/types.js +19 -0
- package/packages/cloud/dist/types.js.map +1 -0
- package/packages/cloud/dist/workflows.d.ts +34 -0
- package/packages/cloud/dist/workflows.d.ts.map +1 -0
- package/packages/cloud/dist/workflows.js +389 -0
- package/packages/cloud/dist/workflows.js.map +1 -0
- package/packages/cloud/package.json +44 -0
- package/packages/cloud/src/api-client.ts +169 -0
- package/packages/cloud/src/auth.ts +314 -0
- package/packages/cloud/src/index.ts +41 -0
- package/packages/cloud/src/types.ts +97 -0
- package/packages/cloud/src/workflows.ts +539 -0
- package/packages/cloud/tsconfig.json +21 -0
- package/packages/config/package.json +1 -1
- package/packages/hooks/package.json +4 -4
- package/packages/memory/package.json +2 -2
- package/packages/openclaw/package.json +2 -2
- package/packages/policy/package.json +2 -2
- package/packages/sdk/dist/workflows/__tests__/e2big-and-verify.test.d.ts +2 -0
- package/packages/sdk/dist/workflows/__tests__/e2big-and-verify.test.d.ts.map +1 -0
- package/packages/sdk/dist/workflows/__tests__/e2big-and-verify.test.js +62 -0
- package/packages/sdk/dist/workflows/__tests__/e2big-and-verify.test.js.map +1 -0
- package/packages/sdk/dist/workflows/cli.js +46 -2
- package/packages/sdk/dist/workflows/cli.js.map +1 -1
- package/packages/sdk/dist/workflows/file-db.d.ts +2 -0
- package/packages/sdk/dist/workflows/file-db.d.ts.map +1 -1
- package/packages/sdk/dist/workflows/file-db.js +20 -3
- package/packages/sdk/dist/workflows/file-db.js.map +1 -1
- package/packages/sdk/dist/workflows/runner.d.ts +10 -1
- package/packages/sdk/dist/workflows/runner.d.ts.map +1 -1
- package/packages/sdk/dist/workflows/runner.js +233 -50
- package/packages/sdk/dist/workflows/runner.js.map +1 -1
- package/packages/sdk/package.json +2 -2
- package/packages/sdk/src/__tests__/resume-fallback.test.ts +415 -0
- package/packages/sdk/src/__tests__/workflow-runner.test.ts +73 -2
- package/packages/sdk/src/workflows/__tests__/e2big-and-verify.test.ts +117 -0
- package/packages/sdk/src/workflows/cli.ts +53 -2
- package/packages/sdk/src/workflows/file-db.ts +22 -3
- package/packages/sdk/src/workflows/runner.ts +283 -49
- package/packages/sdk-py/pyproject.toml +1 -1
- package/packages/sdk-swift/Sources/AgentRelaySDK/RelayObserver.swift +2 -0
- 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 +2 -2
|
@@ -1,390 +1,393 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import os from 'node:os';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
-
import
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
4
|
+
import { InvalidArgumentError } from 'commander';
|
|
5
|
+
import { CLI_AUTH_CONFIG } from '@agent-relay/config/cli-auth-config';
|
|
6
|
+
import { ensureAuthenticated, authorizedApiFetch, readStoredAuth, clearStoredAuth, defaultApiUrl, AUTH_FILE_PATH, REFRESH_WINDOW_MS, runWorkflow, getRunStatus, getRunLogs, syncWorkflowPatch, } from '@agent-relay/cloud';
|
|
7
|
+
import { runInteractiveSession } from '../lib/ssh-interactive.js';
|
|
8
|
+
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
9
|
+
const color = {
|
|
10
|
+
cyan: (s) => `\x1b[36m${s}\x1b[0m`,
|
|
11
|
+
green: (s) => `\x1b[32m${s}\x1b[0m`,
|
|
12
|
+
yellow: (s) => `\x1b[33m${s}\x1b[0m`,
|
|
13
|
+
red: (s) => `\x1b[31m${s}\x1b[0m`,
|
|
14
|
+
dim: (s) => `\x1b[2m${s}\x1b[0m`,
|
|
15
|
+
};
|
|
12
16
|
function defaultExit(code) {
|
|
13
17
|
process.exit(code);
|
|
14
18
|
}
|
|
15
|
-
async function defaultPrompt(question) {
|
|
16
|
-
const rl = readline.createInterface({
|
|
17
|
-
input: process.stdin,
|
|
18
|
-
output: process.stdout,
|
|
19
|
-
});
|
|
20
|
-
return await new Promise((resolve) => {
|
|
21
|
-
rl.question(question, (answer) => {
|
|
22
|
-
rl.close();
|
|
23
|
-
resolve(answer.trim());
|
|
24
|
-
});
|
|
25
|
-
});
|
|
26
|
-
}
|
|
27
|
-
async function defaultOpenExternal(url) {
|
|
28
|
-
if (process.platform === 'darwin') {
|
|
29
|
-
await execFileAsync('open', [url]);
|
|
30
|
-
return;
|
|
31
|
-
}
|
|
32
|
-
if (process.platform === 'win32') {
|
|
33
|
-
await execFileAsync('cmd', ['/c', 'start', '', url]);
|
|
34
|
-
return;
|
|
35
|
-
}
|
|
36
|
-
await execFileAsync('xdg-open', [url]);
|
|
37
|
-
}
|
|
38
|
-
function createDefaultApiClient() {
|
|
39
|
-
return createCloudApiClient();
|
|
40
|
-
}
|
|
41
19
|
function withDefaults(overrides = {}) {
|
|
42
20
|
return {
|
|
43
|
-
createApiClient: createDefaultApiClient,
|
|
44
|
-
getDataDir: () => process.env.AGENT_RELAY_DATA_DIR || path.join(os.homedir(), '.local', 'share', 'agent-relay'),
|
|
45
|
-
getHostname: () => os.hostname(),
|
|
46
|
-
randomHex: (bytes) => randomBytes(bytes).toString('hex'),
|
|
47
|
-
now: () => new Date(),
|
|
48
|
-
openExternal: defaultOpenExternal,
|
|
49
|
-
prompt: defaultPrompt,
|
|
50
21
|
log: (...args) => console.log(...args),
|
|
51
22
|
error: (...args) => console.error(...args),
|
|
52
23
|
exit: defaultExit,
|
|
53
24
|
...overrides,
|
|
54
25
|
};
|
|
55
26
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
27
|
+
const PROVIDER_ALIASES = {
|
|
28
|
+
claude: 'anthropic',
|
|
29
|
+
codex: 'openai',
|
|
30
|
+
gemini: 'google',
|
|
31
|
+
};
|
|
32
|
+
const PROVIDER_HELP_TEXT = Object.keys(CLI_AUTH_CONFIG)
|
|
33
|
+
.sort()
|
|
34
|
+
.map((id) => {
|
|
35
|
+
const alias = Object.entries(PROVIDER_ALIASES).find(([, target]) => target === id);
|
|
36
|
+
return alias ? `${id} (alias: ${alias[0]})` : id;
|
|
37
|
+
})
|
|
38
|
+
.join(', ');
|
|
39
|
+
function normalizeProvider(providerArg) {
|
|
40
|
+
const providerInput = providerArg.toLowerCase().trim();
|
|
41
|
+
return PROVIDER_ALIASES[providerInput] || providerInput;
|
|
42
|
+
}
|
|
43
|
+
function parsePositiveInteger(value) {
|
|
44
|
+
const parsed = Number.parseInt(value, 10);
|
|
45
|
+
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
46
|
+
throw new InvalidArgumentError('Expected a positive integer.');
|
|
59
47
|
}
|
|
60
|
-
|
|
61
|
-
return JSON.parse(raw);
|
|
48
|
+
return parsed;
|
|
62
49
|
}
|
|
63
|
-
function
|
|
64
|
-
|
|
50
|
+
function parseNonNegativeInteger(value) {
|
|
51
|
+
const parsed = Number.parseInt(value, 10);
|
|
52
|
+
if (!Number.isInteger(parsed) || parsed < 0) {
|
|
53
|
+
throw new InvalidArgumentError('Expected a non-negative integer.');
|
|
54
|
+
}
|
|
55
|
+
return parsed;
|
|
65
56
|
}
|
|
66
|
-
function
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
57
|
+
function parseWorkflowFileType(value) {
|
|
58
|
+
if (value === 'yaml' || value === 'ts' || value === 'py') {
|
|
59
|
+
return value;
|
|
60
|
+
}
|
|
61
|
+
throw new InvalidArgumentError('Expected workflow type to be one of: yaml, ts, py');
|
|
62
|
+
}
|
|
63
|
+
function sleep(ms) {
|
|
64
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
73
65
|
}
|
|
74
|
-
function
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
66
|
+
async function getErrorDetails(response) {
|
|
67
|
+
let body;
|
|
68
|
+
try {
|
|
69
|
+
body = await response.text();
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return response.statusText;
|
|
73
|
+
}
|
|
74
|
+
if (!body)
|
|
75
|
+
return response.statusText;
|
|
76
|
+
try {
|
|
77
|
+
const json = JSON.parse(body);
|
|
78
|
+
return json.error || json.message || response.statusText;
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return body;
|
|
79
82
|
}
|
|
80
|
-
return config;
|
|
81
83
|
}
|
|
84
|
+
// ── Command registration ─────────────────────────────────────────────────────
|
|
82
85
|
export function registerCloudCommands(program, overrides = {}) {
|
|
83
86
|
const deps = withDefaults(overrides);
|
|
84
87
|
const cloudCommand = program
|
|
85
88
|
.command('cloud')
|
|
86
|
-
.description('Cloud account and
|
|
87
|
-
|
|
89
|
+
.description('Cloud account, provider auth, and workflow commands');
|
|
90
|
+
// ── login ──────────────────────────────────────────────────────────────────
|
|
88
91
|
cloudCommand
|
|
89
|
-
.command('
|
|
90
|
-
.description('
|
|
91
|
-
.option('--
|
|
92
|
-
.option('--
|
|
92
|
+
.command('login')
|
|
93
|
+
.description('Authenticate with Agent Relay Cloud via browser')
|
|
94
|
+
.option('--api-url <url>', 'Cloud API base URL')
|
|
95
|
+
.option('--force', 'Force re-authentication even if already logged in')
|
|
93
96
|
.action(async (options) => {
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
97
|
+
const apiUrl = options.apiUrl || defaultApiUrl();
|
|
98
|
+
if (!options.force) {
|
|
99
|
+
const existing = await readStoredAuth();
|
|
100
|
+
if (existing && existing.apiUrl === apiUrl) {
|
|
101
|
+
const expiresAt = Date.parse(existing.accessTokenExpiresAt);
|
|
102
|
+
if (!Number.isNaN(expiresAt) && expiresAt - Date.now() > REFRESH_WINDOW_MS) {
|
|
103
|
+
deps.log(`Already logged in to ${existing.apiUrl}`);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
101
107
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
108
|
+
await ensureAuthenticated(apiUrl, { force: options.force });
|
|
109
|
+
});
|
|
110
|
+
// ── logout ─────────────────────────────────────────────────────────────────
|
|
111
|
+
cloudCommand
|
|
112
|
+
.command('logout')
|
|
113
|
+
.description('Clear stored cloud credentials')
|
|
114
|
+
.action(async () => {
|
|
115
|
+
const auth = await readStoredAuth();
|
|
116
|
+
if (!auth) {
|
|
117
|
+
deps.log('Not logged in.');
|
|
118
|
+
return;
|
|
106
119
|
}
|
|
107
|
-
deps.log('');
|
|
108
|
-
deps.log('Agent Relay Cloud - Link Machine');
|
|
109
|
-
deps.log('');
|
|
110
|
-
deps.log(`Machine: ${machineName}`);
|
|
111
|
-
deps.log(`ID: ${machineId}`);
|
|
112
|
-
deps.log('');
|
|
113
|
-
const tempCode = deps.randomHex(16);
|
|
114
|
-
fs.writeFileSync(tempCodePath, tempCode);
|
|
115
|
-
const authUrl = `${stripApiSuffix(cloudUrl)}/cloud/link?code=${tempCode}` +
|
|
116
|
-
`&machine=${encodeURIComponent(machineId)}&name=${encodeURIComponent(machineName)}`;
|
|
117
|
-
deps.log('Open this URL in your browser to authenticate:');
|
|
118
|
-
deps.log('');
|
|
119
|
-
deps.log(` ${authUrl}`);
|
|
120
|
-
deps.log('');
|
|
121
120
|
try {
|
|
122
|
-
|
|
123
|
-
|
|
121
|
+
const revokeUrl = new URL('api/v1/auth/token/revoke', auth.apiUrl.endsWith('/') ? auth.apiUrl : `${auth.apiUrl}/`);
|
|
122
|
+
await fetch(revokeUrl, {
|
|
123
|
+
method: 'POST',
|
|
124
|
+
headers: { 'content-type': 'application/json' },
|
|
125
|
+
body: JSON.stringify({ token: auth.refreshToken }),
|
|
126
|
+
});
|
|
124
127
|
}
|
|
125
128
|
catch {
|
|
126
|
-
|
|
129
|
+
// best-effort revoke
|
|
127
130
|
}
|
|
128
|
-
|
|
129
|
-
deps.log('
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
131
|
+
await clearStoredAuth();
|
|
132
|
+
deps.log('Logged out.');
|
|
133
|
+
});
|
|
134
|
+
// ── whoami ─────────────────────────────────────────────────────────────────
|
|
135
|
+
cloudCommand
|
|
136
|
+
.command('whoami')
|
|
137
|
+
.description('Show current authentication status')
|
|
138
|
+
.option('--api-url <url>', 'Cloud API base URL')
|
|
139
|
+
.action(async (options) => {
|
|
140
|
+
const apiUrl = options.apiUrl || defaultApiUrl();
|
|
141
|
+
const auth = await ensureAuthenticated(apiUrl);
|
|
142
|
+
const { response } = await authorizedApiFetch(auth, '/api/v1/auth/whoami', {
|
|
143
|
+
method: 'GET',
|
|
144
|
+
});
|
|
145
|
+
const payload = (await response.json().catch(() => null));
|
|
146
|
+
if (!response.ok || !payload?.authenticated) {
|
|
147
|
+
throw new Error(payload?.error || 'Failed to resolve auth status');
|
|
148
|
+
}
|
|
149
|
+
deps.log(`API URL: ${auth.apiUrl}`);
|
|
150
|
+
deps.log(`Auth source: ${payload.source}`);
|
|
151
|
+
deps.log(`Subject type: ${payload.subjectType ?? 'session'}`);
|
|
152
|
+
deps.log(`User: ${payload.user.name || '(no name)'}${payload.user.email ? ` <${payload.user.email}>` : ''}`);
|
|
153
|
+
deps.log(`Organization: ${payload.currentOrganization.name}`);
|
|
154
|
+
deps.log(`Workspace: ${payload.currentWorkspace.name}`);
|
|
155
|
+
deps.log(`Scopes: ${payload.scopes.length > 0 ? payload.scopes.join(', ') : '(none)'}`);
|
|
156
|
+
deps.log(`Token file: ${AUTH_FILE_PATH}`);
|
|
157
|
+
});
|
|
158
|
+
// ── connect ────────────────────────────────────────────────────────────────
|
|
159
|
+
cloudCommand
|
|
160
|
+
.command('connect')
|
|
161
|
+
.description('Connect a provider via interactive SSH session')
|
|
162
|
+
.argument('<provider>', `Provider to connect (${PROVIDER_HELP_TEXT})`)
|
|
163
|
+
.option('--api-url <url>', 'Cloud API base URL')
|
|
164
|
+
.option('--language <language>', 'Sandbox language/image', 'typescript')
|
|
165
|
+
.option('--timeout <seconds>', 'Connection timeout in seconds', parsePositiveInteger, 300)
|
|
166
|
+
.action(async (providerArg, options) => {
|
|
167
|
+
const timeoutMs = options.timeout * 1000;
|
|
168
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
169
|
+
throw new Error('This command requires an interactive terminal (TTY).');
|
|
170
|
+
}
|
|
171
|
+
const provider = normalizeProvider(providerArg);
|
|
172
|
+
const providerConfig = CLI_AUTH_CONFIG[provider];
|
|
173
|
+
if (!providerConfig) {
|
|
174
|
+
const known = Object.keys(CLI_AUTH_CONFIG).sort();
|
|
175
|
+
throw new Error(`Unknown provider: ${providerArg}. Supported providers: ${known.join(', ')}`);
|
|
176
|
+
}
|
|
177
|
+
const apiUrl = options.apiUrl || defaultApiUrl();
|
|
178
|
+
const io = {
|
|
179
|
+
log: deps.log,
|
|
180
|
+
error: deps.error,
|
|
181
|
+
};
|
|
182
|
+
io.log('');
|
|
183
|
+
io.log(color.cyan('═══════════════════════════════════════════════════'));
|
|
184
|
+
io.log(color.cyan(' Provider Authentication (Daytona Connect)'));
|
|
185
|
+
io.log(color.cyan('═══════════════════════════════════════════════════'));
|
|
186
|
+
io.log('');
|
|
187
|
+
io.log(`Provider: ${providerConfig.displayName} (${provider})`);
|
|
188
|
+
io.log(`Language: ${color.dim(options.language)}`);
|
|
189
|
+
io.log(color.dim(`Cloud: ${apiUrl}`));
|
|
190
|
+
io.log('');
|
|
191
|
+
io.log('Requesting sandbox from cloud...');
|
|
192
|
+
let auth = await ensureAuthenticated(apiUrl);
|
|
193
|
+
const { response: createResponse, auth: refreshedAuth } = await authorizedApiFetch(auth, '/api/v1/cli/auth', {
|
|
194
|
+
method: 'POST',
|
|
195
|
+
body: JSON.stringify({ provider, language: options.language }),
|
|
196
|
+
});
|
|
197
|
+
auth = refreshedAuth;
|
|
198
|
+
const start = (await createResponse.json().catch(() => null));
|
|
199
|
+
if (!createResponse.ok || !start?.sessionId) {
|
|
200
|
+
const detail = start?.error || start?.message || `${createResponse.status} ${createResponse.statusText}`;
|
|
201
|
+
throw new Error(detail);
|
|
202
|
+
}
|
|
203
|
+
const sshPort = typeof start.ssh?.port === 'string'
|
|
204
|
+
? Number.parseInt(start.ssh.port, 10)
|
|
205
|
+
: start.ssh?.port;
|
|
206
|
+
if (!start.ssh?.host || !sshPort || !start.ssh.user || !start.ssh.password) {
|
|
207
|
+
throw new Error('Cloud returned invalid SSH session details.');
|
|
135
208
|
}
|
|
136
|
-
|
|
137
|
-
|
|
209
|
+
io.log(color.green('✓ Sandbox ready'));
|
|
210
|
+
io.log(color.dim(` SSH: ${start.ssh.user}@${start.ssh.host}:${sshPort}`));
|
|
211
|
+
io.log('');
|
|
212
|
+
io.log(color.yellow('Connecting via SSH...'));
|
|
213
|
+
io.log(color.dim(` Running: ${start.remoteCommand}`));
|
|
214
|
+
io.log('');
|
|
215
|
+
let sessionResult;
|
|
138
216
|
try {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
}
|
|
153
|
-
deps.log('');
|
|
154
|
-
deps.log('Machine linked successfully!');
|
|
155
|
-
deps.log('');
|
|
156
|
-
deps.log('Your broker will now sync with Agent Relay Cloud.');
|
|
157
|
-
deps.log('Run `agent-relay up` to start with cloud sync enabled.');
|
|
158
|
-
deps.log('');
|
|
217
|
+
sessionResult = await runInteractiveSession({
|
|
218
|
+
ssh: {
|
|
219
|
+
host: start.ssh.host,
|
|
220
|
+
port: sshPort,
|
|
221
|
+
user: start.ssh.user,
|
|
222
|
+
password: start.ssh.password,
|
|
223
|
+
},
|
|
224
|
+
remoteCommand: start.remoteCommand,
|
|
225
|
+
successPatterns: providerConfig.successPatterns || [],
|
|
226
|
+
errorPatterns: providerConfig.errorPatterns || [],
|
|
227
|
+
timeoutMs,
|
|
228
|
+
io,
|
|
229
|
+
});
|
|
159
230
|
}
|
|
160
|
-
catch (
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
231
|
+
catch (error) {
|
|
232
|
+
throw new Error(`Failed to connect via SSH: ${error instanceof Error ? error.message : String(error)}`);
|
|
233
|
+
}
|
|
234
|
+
io.log('');
|
|
235
|
+
const success = sessionResult.authDetected;
|
|
236
|
+
io.log('Finalizing authentication with cloud...');
|
|
237
|
+
const { response: completeResponse } = await authorizedApiFetch(auth, '/api/v1/cli/auth/complete', {
|
|
238
|
+
method: 'POST',
|
|
239
|
+
body: JSON.stringify({ sessionId: start.sessionId, success }),
|
|
240
|
+
});
|
|
241
|
+
if (!completeResponse.ok) {
|
|
242
|
+
throw new Error(await getErrorDetails(completeResponse));
|
|
164
243
|
}
|
|
244
|
+
if (!success) {
|
|
245
|
+
const exitCode = sessionResult.exitCode;
|
|
246
|
+
if (typeof exitCode === 'number' && exitCode !== 0) {
|
|
247
|
+
io.error(color.red(`Remote auth command exited with code ${exitCode}.`));
|
|
248
|
+
}
|
|
249
|
+
if (sessionResult.exitCode === 127) {
|
|
250
|
+
io.log(color.yellow(`The ${providerConfig.displayName} CLI ("${providerConfig.command}") is not installed on the sandbox.`));
|
|
251
|
+
io.log(color.dim('Check the sandbox snapshot includes the required CLI tools.'));
|
|
252
|
+
}
|
|
253
|
+
throw new Error(`Provider auth for ${provider} did not complete successfully`);
|
|
254
|
+
}
|
|
255
|
+
io.log('');
|
|
256
|
+
io.log(color.green('═══════════════════════════════════════════════════'));
|
|
257
|
+
io.log(color.green(' Authentication Complete!'));
|
|
258
|
+
io.log(color.green('═══════════════════════════════════════════════════'));
|
|
259
|
+
io.log('');
|
|
260
|
+
io.log(`${providerConfig.displayName} credentials are now stored and encrypted.`);
|
|
261
|
+
io.log(color.dim('Your workflows will automatically use these credentials.'));
|
|
262
|
+
io.log('');
|
|
165
263
|
});
|
|
264
|
+
// ── run ────────────────────────────────────────────────────────────────────
|
|
166
265
|
cloudCommand
|
|
167
|
-
.command('
|
|
168
|
-
.description('
|
|
169
|
-
.
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
266
|
+
.command('run')
|
|
267
|
+
.description('Submit a workflow run')
|
|
268
|
+
.argument('<workflow>', 'Workflow file path or inline workflow content')
|
|
269
|
+
.option('--api-url <url>', 'Cloud API base URL')
|
|
270
|
+
.option('--file-type <type>', 'Workflow type: yaml, ts, or py', parseWorkflowFileType)
|
|
271
|
+
.option('--sync-code', 'Upload the current working directory before running')
|
|
272
|
+
.option('--no-sync-code', 'Skip uploading the current working directory')
|
|
273
|
+
.option('--json', 'Print raw JSON response', false)
|
|
274
|
+
.action(async (workflow, options) => {
|
|
275
|
+
const result = await runWorkflow(workflow, options);
|
|
276
|
+
if (options.json) {
|
|
277
|
+
deps.log(JSON.stringify(result, null, 2));
|
|
174
278
|
return;
|
|
175
279
|
}
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
deps.log(
|
|
181
|
-
deps.log(
|
|
182
|
-
deps.log(`
|
|
183
|
-
deps.log('');
|
|
184
|
-
deps.log('Note: The API key has been removed locally. To fully revoke access,');
|
|
185
|
-
deps.log('visit your Agent Relay Cloud dashboard and remove this machine.');
|
|
186
|
-
deps.log('');
|
|
280
|
+
deps.log(`Run created: ${result.runId}`);
|
|
281
|
+
if (typeof result.sandboxId === 'string') {
|
|
282
|
+
deps.log(`Sandbox: ${result.sandboxId}`);
|
|
283
|
+
}
|
|
284
|
+
deps.log(`Status: ${result.status}`);
|
|
285
|
+
deps.log(`\nView logs: agent-relay cloud logs ${result.runId} --follow`);
|
|
286
|
+
deps.log(`Sync code: agent-relay cloud sync ${result.runId}`);
|
|
187
287
|
});
|
|
288
|
+
// ── status ─────────────────────────────────────────────────────────────────
|
|
188
289
|
cloudCommand
|
|
189
290
|
.command('status')
|
|
190
|
-
.description('
|
|
191
|
-
.
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
deps.log(
|
|
198
|
-
deps.log('');
|
|
199
|
-
deps.log('Run `agent-relay cloud link` to connect to Agent Relay Cloud.');
|
|
200
|
-
deps.log('');
|
|
291
|
+
.description('Fetch workflow run status')
|
|
292
|
+
.argument('<runId>', 'Workflow run id')
|
|
293
|
+
.option('--api-url <url>', 'Cloud API base URL')
|
|
294
|
+
.option('--json', 'Print raw JSON response', false)
|
|
295
|
+
.action(async (runId, options) => {
|
|
296
|
+
const result = await getRunStatus(runId, options);
|
|
297
|
+
if (options.json) {
|
|
298
|
+
deps.log(JSON.stringify(result, null, 2));
|
|
201
299
|
return;
|
|
202
300
|
}
|
|
203
|
-
deps.log(
|
|
204
|
-
deps.log(
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
deps.log(` ID: ${config.machineId}`);
|
|
208
|
-
deps.log(` Cloud URL: ${config.cloudUrl}`);
|
|
209
|
-
deps.log(` Linked: ${new Date(config.linkedAt).toLocaleString()}`);
|
|
210
|
-
deps.log('');
|
|
211
|
-
try {
|
|
212
|
-
const client = deps.createApiClient();
|
|
213
|
-
const online = await client.checkConnection({
|
|
214
|
-
cloudUrl: config.cloudUrl,
|
|
215
|
-
apiKey: config.apiKey,
|
|
216
|
-
});
|
|
217
|
-
deps.log(` Cloud connection: ${online ? 'Online' : 'Error (API key may be invalid)'}`);
|
|
218
|
-
}
|
|
219
|
-
catch (err) {
|
|
220
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
221
|
-
deps.log(` Cloud connection: Offline (${message})`);
|
|
222
|
-
}
|
|
223
|
-
deps.log('');
|
|
224
|
-
});
|
|
225
|
-
cloudCommand
|
|
226
|
-
.command('sync')
|
|
227
|
-
.description('Manually sync credentials from cloud')
|
|
228
|
-
.action(async () => {
|
|
229
|
-
const dataDir = deps.getDataDir();
|
|
230
|
-
const { configPath, credentialsPath } = getPaths(dataDir);
|
|
231
|
-
const config = ensureLinked(configPath, deps);
|
|
232
|
-
deps.log('Syncing credentials from cloud...');
|
|
233
|
-
try {
|
|
234
|
-
const client = deps.createApiClient();
|
|
235
|
-
const credentials = await client.syncCredentials({
|
|
236
|
-
cloudUrl: config.cloudUrl,
|
|
237
|
-
apiKey: config.apiKey,
|
|
238
|
-
});
|
|
239
|
-
deps.log('');
|
|
240
|
-
deps.log(`Synced ${credentials.length} provider credentials:`);
|
|
241
|
-
for (const credential of credentials) {
|
|
242
|
-
deps.log(` - ${credential.provider}`);
|
|
243
|
-
}
|
|
244
|
-
fs.writeFileSync(credentialsPath, JSON.stringify(credentials, null, 2));
|
|
245
|
-
fs.chmodSync(credentialsPath, 0o600);
|
|
246
|
-
deps.log('');
|
|
247
|
-
deps.log('Credentials synced successfully');
|
|
248
|
-
deps.log('');
|
|
301
|
+
deps.log(`Run: ${result.runId ?? runId}`);
|
|
302
|
+
deps.log(`Status: ${result.status ?? 'unknown'}`);
|
|
303
|
+
if (typeof result.sandboxId === 'string') {
|
|
304
|
+
deps.log(`Sandbox: ${result.sandboxId}`);
|
|
249
305
|
}
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
deps.error(`Failed to sync: ${message}`);
|
|
253
|
-
deps.exit(1);
|
|
306
|
+
if (typeof result.updatedAt === 'string') {
|
|
307
|
+
deps.log(`Updated: ${result.updatedAt}`);
|
|
254
308
|
}
|
|
255
309
|
});
|
|
310
|
+
// ── logs ───────────────────────────────────────────────────────────────────
|
|
256
311
|
cloudCommand
|
|
257
|
-
.command('
|
|
258
|
-
.description('
|
|
259
|
-
.
|
|
260
|
-
.
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
312
|
+
.command('logs')
|
|
313
|
+
.description('Read workflow run logs')
|
|
314
|
+
.argument('<runId>', 'Workflow run id')
|
|
315
|
+
.option('--api-url <url>', 'Cloud API base URL')
|
|
316
|
+
.option('--follow', 'Poll until the run is done', false)
|
|
317
|
+
.option('--poll-interval <seconds>', 'Polling interval while following', parsePositiveInteger, 2)
|
|
318
|
+
.option('--offset <bytes>', 'Start reading logs from a byte offset', parseNonNegativeInteger, 0)
|
|
319
|
+
.option('--agent <name>', 'Read logs for a specific agent')
|
|
320
|
+
.option('--sandbox-id <sandboxId>', 'Read logs for a specific step sandbox')
|
|
321
|
+
.option('--json', 'Print raw JSON responses', false)
|
|
322
|
+
.action(async (runId, options) => {
|
|
323
|
+
let offset = options.offset ?? 0;
|
|
324
|
+
const sandboxId = options.agent ?? options.sandboxId;
|
|
325
|
+
while (true) {
|
|
326
|
+
const result = await getRunLogs(runId, {
|
|
327
|
+
apiUrl: options.apiUrl,
|
|
328
|
+
offset,
|
|
329
|
+
sandboxId,
|
|
269
330
|
});
|
|
270
331
|
if (options.json) {
|
|
271
|
-
deps.log(JSON.stringify(
|
|
272
|
-
return;
|
|
332
|
+
deps.log(JSON.stringify(result, null, 2));
|
|
273
333
|
}
|
|
274
|
-
if (
|
|
275
|
-
|
|
276
|
-
deps.log('Make sure brokers are running on linked machines.');
|
|
277
|
-
return;
|
|
334
|
+
else if (result.content) {
|
|
335
|
+
process.stdout.write(result.content);
|
|
278
336
|
}
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
deps.log('NAME STATUS BROKER MACHINE');
|
|
283
|
-
deps.log('─'.repeat(65));
|
|
284
|
-
const byBroker = new Map();
|
|
285
|
-
for (const agent of agents) {
|
|
286
|
-
const current = byBroker.get(agent.brokerName) || [];
|
|
287
|
-
current.push(agent);
|
|
288
|
-
byBroker.set(agent.brokerName, current);
|
|
337
|
+
offset = result.offset;
|
|
338
|
+
if (!options.follow || result.done) {
|
|
339
|
+
break;
|
|
289
340
|
}
|
|
290
|
-
|
|
291
|
-
for (const agent of brokerAgents) {
|
|
292
|
-
const machine = (agent.machineId || '').substring(0, 20);
|
|
293
|
-
deps.log(formatTableRow([
|
|
294
|
-
{ value: agent.name, width: 15 },
|
|
295
|
-
{ value: agent.status, width: 8 },
|
|
296
|
-
{ value: brokerName, width: 18 },
|
|
297
|
-
{ value: machine },
|
|
298
|
-
]));
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
deps.log('');
|
|
302
|
-
deps.log(`Total: ${agents.length} agents on ${byBroker.size} machines`);
|
|
303
|
-
deps.log('');
|
|
304
|
-
}
|
|
305
|
-
catch (err) {
|
|
306
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
307
|
-
deps.error(`Failed to fetch agents: ${message}`);
|
|
308
|
-
deps.exit(1);
|
|
341
|
+
await sleep((options.pollInterval ?? 2) * 1000);
|
|
309
342
|
}
|
|
310
343
|
});
|
|
344
|
+
// ── sync ───────────────────────────────────────────────────────────────────
|
|
311
345
|
cloudCommand
|
|
312
|
-
.command('
|
|
313
|
-
.description('
|
|
314
|
-
.argument('<
|
|
315
|
-
.
|
|
316
|
-
.option('--
|
|
317
|
-
.
|
|
318
|
-
|
|
319
|
-
const
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
cloudUrl: config.cloudUrl,
|
|
326
|
-
apiKey: config.apiKey,
|
|
327
|
-
});
|
|
328
|
-
const targetAgent = allAgents.find((candidate) => candidate.name === agent);
|
|
329
|
-
if (!targetAgent) {
|
|
330
|
-
deps.error(`Agent "${agent}" not found.`);
|
|
331
|
-
deps.log('Available agents:');
|
|
332
|
-
for (const availableAgent of allAgents) {
|
|
333
|
-
deps.log(` - ${availableAgent.name} (on ${availableAgent.brokerName})`);
|
|
334
|
-
}
|
|
335
|
-
deps.exit(1);
|
|
336
|
-
return;
|
|
337
|
-
}
|
|
338
|
-
await client.sendMessage({
|
|
339
|
-
cloudUrl: config.cloudUrl,
|
|
340
|
-
apiKey: config.apiKey,
|
|
341
|
-
targetBrokerId: targetAgent.brokerId,
|
|
342
|
-
targetAgent: agent,
|
|
343
|
-
from: options.from,
|
|
344
|
-
content: message,
|
|
345
|
-
});
|
|
346
|
-
deps.log('');
|
|
347
|
-
deps.log(`Message sent to ${agent} on ${targetAgent.brokerName}`);
|
|
348
|
-
deps.log('');
|
|
346
|
+
.command('sync')
|
|
347
|
+
.description('Download and apply code changes from a completed workflow run')
|
|
348
|
+
.argument('<runId>', 'Workflow run id')
|
|
349
|
+
.option('--api-url <url>', 'Cloud API base URL')
|
|
350
|
+
.option('--dir <path>', 'Local directory to apply the patch to', '.')
|
|
351
|
+
.option('--dry-run', 'Download and display the patch without applying', false)
|
|
352
|
+
.action(async (runId, options) => {
|
|
353
|
+
const targetDir = path.resolve(options.dir ?? '.');
|
|
354
|
+
deps.log(`Fetching patch for run ${runId}...`);
|
|
355
|
+
const result = await syncWorkflowPatch(runId, { apiUrl: options.apiUrl });
|
|
356
|
+
if (!result.hasChanges) {
|
|
357
|
+
deps.log('No changes to sync — the workflow did not modify any files.');
|
|
358
|
+
return;
|
|
349
359
|
}
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
deps.
|
|
360
|
+
if (options.dryRun) {
|
|
361
|
+
deps.log('\n--- Patch (dry run) ---');
|
|
362
|
+
process.stdout.write(result.patch);
|
|
363
|
+
deps.log('\n--- End patch ---');
|
|
364
|
+
return;
|
|
354
365
|
}
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
.
|
|
358
|
-
.
|
|
359
|
-
.option('--json', 'Output as JSON')
|
|
360
|
-
.action(async (options) => {
|
|
361
|
-
const dataDir = deps.getDataDir();
|
|
362
|
-
const { configPath } = getPaths(dataDir);
|
|
363
|
-
const config = ensureLinked(configPath, deps);
|
|
366
|
+
const { execSync } = await import('node:child_process');
|
|
367
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cloud-sync-'));
|
|
368
|
+
const tmpPatch = path.join(tmpDir, 'changes.patch');
|
|
369
|
+
fs.writeFileSync(tmpPatch, result.patch, { mode: 0o600 });
|
|
364
370
|
try {
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
371
|
+
const stat = execSync(`git apply --stat "${tmpPatch}"`, {
|
|
372
|
+
cwd: targetDir,
|
|
373
|
+
encoding: 'utf-8',
|
|
374
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
375
|
+
});
|
|
376
|
+
if (stat.trim()) {
|
|
377
|
+
deps.log('\nFiles changed by agent:');
|
|
378
|
+
deps.log(stat);
|
|
373
379
|
}
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
deps.log(
|
|
380
|
-
deps.log(` Linked: ${new Date(config.linkedAt).toLocaleString()}`);
|
|
381
|
-
deps.log('');
|
|
382
|
-
deps.log('Note: To see all linked brokers, visit your cloud dashboard.');
|
|
383
|
-
deps.log('');
|
|
380
|
+
execSync(`git apply "${tmpPatch}"`, {
|
|
381
|
+
cwd: targetDir,
|
|
382
|
+
encoding: 'utf-8',
|
|
383
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
384
|
+
});
|
|
385
|
+
deps.log('Patch applied successfully.');
|
|
384
386
|
}
|
|
385
387
|
catch (err) {
|
|
386
388
|
const message = err instanceof Error ? err.message : String(err);
|
|
387
|
-
deps.error(`Failed: ${message}`);
|
|
389
|
+
deps.error(`Failed to apply patch: ${message}`);
|
|
390
|
+
deps.error(`Patch saved to: ${tmpPatch}`);
|
|
388
391
|
deps.exit(1);
|
|
389
392
|
}
|
|
390
393
|
});
|