@orchagent/cli 0.3.54 → 0.3.55

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.
@@ -0,0 +1,401 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.registerServiceCommand = registerServiceCommand;
7
+ const cli_table3_1 = __importDefault(require("cli-table3"));
8
+ const chalk_1 = __importDefault(require("chalk"));
9
+ const config_1 = require("../lib/config");
10
+ const api_1 = require("../lib/api");
11
+ const errors_1 = require("../lib/errors");
12
+ const output_1 = require("../lib/output");
13
+ const spinner_1 = require("../lib/spinner");
14
+ // ============================================
15
+ // HELPERS
16
+ // ============================================
17
+ async function resolveWorkspaceId(config, slug) {
18
+ const configFile = await (0, config_1.loadConfig)();
19
+ const targetSlug = slug ?? configFile.workspace;
20
+ if (!targetSlug) {
21
+ throw new errors_1.CliError('No workspace specified. Use --workspace <slug> or run `orch workspace use <slug>` first.');
22
+ }
23
+ const response = await (0, api_1.request)(config, 'GET', '/workspaces');
24
+ const workspace = response.workspaces.find((w) => w.slug === targetSlug);
25
+ if (!workspace) {
26
+ throw new errors_1.CliError(`Workspace '${targetSlug}' not found.`);
27
+ }
28
+ return workspace.id;
29
+ }
30
+ function formatDate(iso) {
31
+ if (!iso)
32
+ return '-';
33
+ return new Date(iso).toLocaleString();
34
+ }
35
+ function stateColor(state) {
36
+ switch (state) {
37
+ case 'running':
38
+ return chalk_1.default.green(state);
39
+ case 'provisioning':
40
+ return chalk_1.default.yellow(state);
41
+ case 'unhealthy':
42
+ case 'failed':
43
+ return chalk_1.default.red(state);
44
+ case 'deleting':
45
+ case 'deleted':
46
+ return chalk_1.default.gray(state);
47
+ default:
48
+ return state;
49
+ }
50
+ }
51
+ function healthColor(health) {
52
+ switch (health) {
53
+ case 'healthy':
54
+ return chalk_1.default.green(health);
55
+ case 'degraded':
56
+ return chalk_1.default.yellow(health);
57
+ case 'unhealthy':
58
+ return chalk_1.default.red(health);
59
+ default:
60
+ return chalk_1.default.gray(health);
61
+ }
62
+ }
63
+ function severityColor(severity, message) {
64
+ switch (severity.toUpperCase()) {
65
+ case 'ERROR':
66
+ case 'CRITICAL':
67
+ return chalk_1.default.red(message);
68
+ case 'WARNING':
69
+ return chalk_1.default.yellow(message);
70
+ case 'INFO':
71
+ return chalk_1.default.white(message);
72
+ default:
73
+ return chalk_1.default.gray(message);
74
+ }
75
+ }
76
+ // ============================================
77
+ // COMMAND REGISTRATION
78
+ // ============================================
79
+ function registerServiceCommand(program) {
80
+ const service = program
81
+ .command('service')
82
+ .description('Manage always-on automation services');
83
+ // orch service deploy <org/agent[@version]>
84
+ service
85
+ .command('deploy <agent>')
86
+ .description('Deploy an always-on automation service')
87
+ .option('--workspace <slug>', 'Workspace slug (default: current workspace)')
88
+ .option('--name <service-name>', 'Service name (default: agent name)')
89
+ .option('--min-instances <n>', 'Minimum instances (default: 1)', '1')
90
+ .option('--max-instances <n>', 'Maximum instances (default: 1)', '1')
91
+ .option('--env <KEY=VALUE>', 'Environment variable (repeatable)', collectKeyValue, {})
92
+ .option('--secret <NAME>', 'Workspace secret name (repeatable)', collectArray, [])
93
+ .option('--command <cmd>', 'Override entrypoint command')
94
+ .option('--arg <value>', 'Command argument (repeatable)', collectArray, [])
95
+ .option('--json', 'Output as JSON')
96
+ .action(async (agentArg, options) => {
97
+ const config = await (0, config_1.getResolvedConfig)();
98
+ if (!config.apiKey) {
99
+ throw new errors_1.CliError('Missing API key. Run `orch login` first.');
100
+ }
101
+ const workspaceId = await resolveWorkspaceId(config, options.workspace);
102
+ // Parse agent ref: org/agent[@version]
103
+ const parts = agentArg.split('/');
104
+ if (parts.length !== 2) {
105
+ throw new errors_1.CliError('Agent must be in format: org/agent[@version]');
106
+ }
107
+ const [, agentPart] = parts;
108
+ const atIndex = agentPart.indexOf('@');
109
+ const agentName = atIndex >= 0 ? agentPart.slice(0, atIndex) : agentPart;
110
+ const agentVersion = atIndex >= 0 ? agentPart.slice(atIndex + 1) : 'latest';
111
+ const serviceName = options.name || agentName;
112
+ const minInstances = parseInt(options.minInstances, 10);
113
+ const maxInstances = parseInt(options.maxInstances, 10);
114
+ if (isNaN(minInstances) || isNaN(maxInstances)) {
115
+ throw new errors_1.CliError('--min-instances and --max-instances must be numbers');
116
+ }
117
+ // First, resolve the agent to get agent_id
118
+ const spinner = (0, spinner_1.createSpinner)('Resolving agent...');
119
+ spinner.start();
120
+ let agentId;
121
+ try {
122
+ // List agents for the workspace via the correct gateway endpoint
123
+ const agentsList = await (0, api_1.request)(config, 'GET', `/agents?workspace_id=${workspaceId}`);
124
+ let match;
125
+ if (agentVersion === 'latest') {
126
+ // Filter all matching agents by name, sort by created_at desc to get newest
127
+ const candidates = agentsList
128
+ .filter(a => a.name === agentName)
129
+ .sort((a, b) => (b.created_at ?? '').localeCompare(a.created_at ?? ''));
130
+ match = candidates[0];
131
+ }
132
+ else {
133
+ match = agentsList.find(a => a.name === agentName && a.version === agentVersion);
134
+ }
135
+ if (!match) {
136
+ spinner.fail('Agent not found');
137
+ throw new errors_1.CliError(`Agent '${agentName}' (version ${agentVersion}) not found in workspace`);
138
+ }
139
+ agentId = match.id;
140
+ spinner.succeed('Agent resolved');
141
+ }
142
+ catch (e) {
143
+ if (e instanceof errors_1.CliError)
144
+ throw e;
145
+ spinner.fail('Failed to resolve agent');
146
+ throw e;
147
+ }
148
+ const deploySpinner = (0, spinner_1.createSpinner)('Deploying service...');
149
+ deploySpinner.start();
150
+ try {
151
+ const result = await (0, api_1.request)(config, 'POST', `/workspaces/${workspaceId}/services`, {
152
+ body: JSON.stringify({
153
+ agent_id: agentId,
154
+ agent_name: agentName,
155
+ agent_version: agentVersion,
156
+ service_name: serviceName,
157
+ min_instances: minInstances,
158
+ max_instances: maxInstances,
159
+ command: options.command || null,
160
+ args: options.arg.length > 0 ? options.arg : null,
161
+ env: Object.keys(options.env).length > 0 ? options.env : null,
162
+ secret_names: options.secret.length > 0 ? options.secret : null,
163
+ }),
164
+ headers: { 'Content-Type': 'application/json' },
165
+ });
166
+ deploySpinner.succeed('Service deployed');
167
+ if (options.json) {
168
+ (0, output_1.printJson)(result.service);
169
+ return;
170
+ }
171
+ const svc = result.service;
172
+ process.stdout.write(`\n${chalk_1.default.green('\u2713')} Service deployed successfully\n\n`);
173
+ process.stdout.write(` ${chalk_1.default.bold('ID:')} ${svc.id}\n`);
174
+ process.stdout.write(` ${chalk_1.default.bold('Name:')} ${svc.service_name}\n`);
175
+ process.stdout.write(` ${chalk_1.default.bold('Agent:')} ${svc.agent_name}@${svc.agent_version}\n`);
176
+ process.stdout.write(` ${chalk_1.default.bold('State:')} ${stateColor(svc.current_state)}\n`);
177
+ process.stdout.write(` ${chalk_1.default.bold('URL:')} ${svc.cloud_run_url || '-'}\n`);
178
+ process.stdout.write(`\n`);
179
+ process.stdout.write(chalk_1.default.gray(`View logs: orch service logs ${svc.id}\n`));
180
+ }
181
+ catch (e) {
182
+ deploySpinner.fail('Deploy failed');
183
+ throw e;
184
+ }
185
+ });
186
+ // orch service list
187
+ service
188
+ .command('list')
189
+ .description('List automation services in your workspace')
190
+ .option('--workspace <slug>', 'Workspace slug (default: current workspace)')
191
+ .option('--status <state>', 'Filter by state')
192
+ .option('--json', 'Output as JSON')
193
+ .action(async (options) => {
194
+ const config = await (0, config_1.getResolvedConfig)();
195
+ if (!config.apiKey) {
196
+ throw new errors_1.CliError('Missing API key. Run `orch login` first.');
197
+ }
198
+ const workspaceId = await resolveWorkspaceId(config, options.workspace);
199
+ const params = new URLSearchParams();
200
+ if (options.status)
201
+ params.set('status', options.status);
202
+ params.set('limit', '100');
203
+ const qs = params.toString() ? `?${params.toString()}` : '';
204
+ const result = await (0, api_1.request)(config, 'GET', `/workspaces/${workspaceId}/services${qs}`);
205
+ if (options.json) {
206
+ (0, output_1.printJson)(result);
207
+ return;
208
+ }
209
+ if (!result.services.length) {
210
+ process.stdout.write('No services found.\n');
211
+ process.stdout.write(chalk_1.default.gray('Deploy one with: orch service deploy <org/agent>\n'));
212
+ return;
213
+ }
214
+ const table = new cli_table3_1.default({
215
+ head: [
216
+ chalk_1.default.bold('Name'),
217
+ chalk_1.default.bold('Agent'),
218
+ chalk_1.default.bold('State'),
219
+ chalk_1.default.bold('Health'),
220
+ chalk_1.default.bold('Restarts'),
221
+ chalk_1.default.bold('Deployed'),
222
+ ],
223
+ });
224
+ for (const svc of result.services) {
225
+ const stateLabel = svc.auto_paused_at
226
+ ? chalk_1.default.bgRed.white(' CRASH-LOOP ')
227
+ : stateColor(svc.current_state);
228
+ const restartsLabel = svc.consecutive_restart_failures > 0
229
+ ? `${svc.restart_count} (${chalk_1.default.red(String(svc.consecutive_restart_failures))} fails)`
230
+ : String(svc.restart_count);
231
+ table.push([
232
+ svc.service_name,
233
+ `${svc.agent_name}@${svc.agent_version}`,
234
+ stateLabel,
235
+ healthColor(svc.health_status),
236
+ restartsLabel,
237
+ formatDate(svc.last_deployed_at),
238
+ ]);
239
+ }
240
+ process.stdout.write(`${table.toString()}\n`);
241
+ process.stdout.write(chalk_1.default.gray(`\n${result.total} service${result.total !== 1 ? 's' : ''}\n`));
242
+ });
243
+ // orch service logs <service-id>
244
+ service
245
+ .command('logs <service-id>')
246
+ .description('Fetch recent logs for a service')
247
+ .option('--workspace <slug>', 'Workspace slug (default: current workspace)')
248
+ .option('--limit <n>', 'Number of log lines (default: 100)', '100')
249
+ .option('--since <timestamp>', 'Only show logs after this ISO timestamp')
250
+ .option('--json', 'Output as JSON')
251
+ .action(async (serviceId, options) => {
252
+ const config = await (0, config_1.getResolvedConfig)();
253
+ if (!config.apiKey) {
254
+ throw new errors_1.CliError('Missing API key. Run `orch login` first.');
255
+ }
256
+ const workspaceId = await resolveWorkspaceId(config, options.workspace);
257
+ const params = new URLSearchParams();
258
+ params.set('limit', options.limit);
259
+ if (options.since)
260
+ params.set('since', options.since);
261
+ const qs = `?${params.toString()}`;
262
+ const result = await (0, api_1.request)(config, 'GET', `/workspaces/${workspaceId}/services/${serviceId}/logs${qs}`);
263
+ if (options.json) {
264
+ (0, output_1.printJson)(result);
265
+ return;
266
+ }
267
+ if (!result.logs.length) {
268
+ process.stdout.write('No logs found.\n');
269
+ return;
270
+ }
271
+ for (const entry of result.logs) {
272
+ const ts = entry.timestamp ? new Date(entry.timestamp).toISOString().replace('T', ' ').replace('Z', '') : '???';
273
+ const sev = entry.severity.padEnd(7);
274
+ process.stdout.write(`${chalk_1.default.gray(ts)} ${severityColor(entry.severity, sev)} ${entry.message}\n`);
275
+ }
276
+ });
277
+ // orch service restart <service-id>
278
+ service
279
+ .command('restart <service-id>')
280
+ .description('Restart an automation service')
281
+ .option('--workspace <slug>', 'Workspace slug (default: current workspace)')
282
+ .option('--json', 'Output as JSON')
283
+ .action(async (serviceId, options) => {
284
+ const config = await (0, config_1.getResolvedConfig)();
285
+ if (!config.apiKey) {
286
+ throw new errors_1.CliError('Missing API key. Run `orch login` first.');
287
+ }
288
+ const workspaceId = await resolveWorkspaceId(config, options.workspace);
289
+ const spinner = (0, spinner_1.createSpinner)('Restarting service...');
290
+ spinner.start();
291
+ try {
292
+ const result = await (0, api_1.request)(config, 'POST', `/workspaces/${workspaceId}/services/${serviceId}/restart`);
293
+ spinner.succeed('Service restarted');
294
+ if (options.json) {
295
+ (0, output_1.printJson)(result.service);
296
+ return;
297
+ }
298
+ process.stdout.write(`${chalk_1.default.green('\u2713')} Service '${result.service.service_name}' restarted (restarts: ${result.service.restart_count})\n`);
299
+ }
300
+ catch (e) {
301
+ spinner.fail('Restart failed');
302
+ throw e;
303
+ }
304
+ });
305
+ // orch service info <service-id>
306
+ service
307
+ .command('info <service-id>')
308
+ .description('Show detailed service information with events')
309
+ .option('--workspace <slug>', 'Workspace slug (default: current workspace)')
310
+ .option('--json', 'Output as JSON')
311
+ .action(async (serviceId, options) => {
312
+ const config = await (0, config_1.getResolvedConfig)();
313
+ if (!config.apiKey) {
314
+ throw new errors_1.CliError('Missing API key. Run `orch login` first.');
315
+ }
316
+ const workspaceId = await resolveWorkspaceId(config, options.workspace);
317
+ const result = await (0, api_1.request)(config, 'GET', `/workspaces/${workspaceId}/services/${serviceId}`);
318
+ if (options.json) {
319
+ (0, output_1.printJson)(result.service);
320
+ return;
321
+ }
322
+ const svc = result.service;
323
+ process.stdout.write(`\n${chalk_1.default.bold('Service Details')}\n\n`);
324
+ process.stdout.write(` ID: ${svc.id}\n`);
325
+ process.stdout.write(` Name: ${svc.service_name}\n`);
326
+ process.stdout.write(` Agent: ${svc.agent_name}@${svc.agent_version}\n`);
327
+ process.stdout.write(` State: ${stateColor(svc.current_state)}\n`);
328
+ process.stdout.write(` Health: ${healthColor(svc.health_status)}\n`);
329
+ if (svc.auto_paused_at) {
330
+ process.stdout.write(` ${chalk_1.default.bgRed.white(' CRASH-LOOP ')} auto-paused at ${formatDate(svc.auto_paused_at)}\n`);
331
+ }
332
+ process.stdout.write(` Restarts: ${svc.restart_count}\n`);
333
+ if (svc.consecutive_restart_failures > 0) {
334
+ process.stdout.write(` Fail Streak: ${chalk_1.default.red(String(svc.consecutive_restart_failures))} / ${svc.max_restart_failures}\n`);
335
+ }
336
+ process.stdout.write(` Instances: ${svc.min_instances}-${svc.max_instances}\n`);
337
+ process.stdout.write(` Cloud Run: ${svc.cloud_run_service || '-'}\n`);
338
+ process.stdout.write(` URL: ${svc.cloud_run_url || '-'}\n`);
339
+ process.stdout.write(` Deployed: ${formatDate(svc.last_deployed_at)}\n`);
340
+ process.stdout.write(` Last Restart: ${formatDate(svc.last_restart_at)}\n`);
341
+ if (svc.last_error) {
342
+ process.stdout.write(` Last Error: ${chalk_1.default.red(svc.last_error)}\n`);
343
+ }
344
+ if (svc.alert_webhook_url) {
345
+ process.stdout.write(` Alert URL: ${svc.alert_webhook_url.slice(0, 50)}...\n`);
346
+ }
347
+ // Events timeline
348
+ const events = svc.events || [];
349
+ if (events.length > 0) {
350
+ process.stdout.write(`\n${chalk_1.default.bold('Recent Events')}\n`);
351
+ for (const e of events.slice(0, 10)) {
352
+ const color = e.event_type.includes('fail') || e.event_type.includes('paused') ? chalk_1.default.red : chalk_1.default.gray;
353
+ process.stdout.write(` ${chalk_1.default.gray(formatDate(e.created_at))} ${color(e.event_type)} ${e.message || ''}\n`);
354
+ }
355
+ }
356
+ process.stdout.write('\n');
357
+ });
358
+ // orch service delete <service-id>
359
+ service
360
+ .command('delete <service-id>')
361
+ .description('Delete an automation service')
362
+ .option('--workspace <slug>', 'Workspace slug (default: current workspace)')
363
+ .option('--json', 'Output as JSON')
364
+ .action(async (serviceId, options) => {
365
+ const config = await (0, config_1.getResolvedConfig)();
366
+ if (!config.apiKey) {
367
+ throw new errors_1.CliError('Missing API key. Run `orch login` first.');
368
+ }
369
+ const workspaceId = await resolveWorkspaceId(config, options.workspace);
370
+ const spinner = (0, spinner_1.createSpinner)('Deleting service...');
371
+ spinner.start();
372
+ try {
373
+ const result = await (0, api_1.request)(config, 'DELETE', `/workspaces/${workspaceId}/services/${serviceId}`);
374
+ spinner.succeed('Service deleted');
375
+ if (options.json) {
376
+ (0, output_1.printJson)(result);
377
+ return;
378
+ }
379
+ process.stdout.write(`${chalk_1.default.green('\u2713')} Service '${result.service.service_name}' deleted\n`);
380
+ }
381
+ catch (e) {
382
+ spinner.fail('Delete failed');
383
+ throw e;
384
+ }
385
+ });
386
+ }
387
+ // ============================================
388
+ // OPTION COLLECTORS
389
+ // ============================================
390
+ function collectKeyValue(value, previous) {
391
+ const idx = value.indexOf('=');
392
+ if (idx < 0) {
393
+ throw new errors_1.CliError(`Invalid env format: '${value}'. Use KEY=VALUE.`);
394
+ }
395
+ previous[value.slice(0, idx)] = value.slice(idx + 1);
396
+ return previous;
397
+ }
398
+ function collectArray(value, previous) {
399
+ previous.push(value);
400
+ return previous;
401
+ }
@@ -0,0 +1,135 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.registerTransferCommand = registerTransferCommand;
7
+ const promises_1 = __importDefault(require("readline/promises"));
8
+ const chalk_1 = __importDefault(require("chalk"));
9
+ const config_1 = require("../lib/config");
10
+ const api_1 = require("../lib/api");
11
+ const errors_1 = require("../lib/errors");
12
+ const analytics_1 = require("../lib/analytics");
13
+ const output_1 = require("../lib/output");
14
+ async function promptText(message) {
15
+ const rl = promises_1.default.createInterface({
16
+ input: process.stdin,
17
+ output: process.stdout,
18
+ });
19
+ const answer = await rl.question(message);
20
+ rl.close();
21
+ return answer.trim();
22
+ }
23
+ function registerTransferCommand(program) {
24
+ program
25
+ .command('transfer <agent-name>')
26
+ .description('Transfer an agent to another workspace')
27
+ .requiredOption('--to <workspace-slug>', 'Target workspace slug')
28
+ .option('-y, --yes', 'Skip confirmation prompt')
29
+ .option('--dry-run', 'Show what would be transferred without making changes')
30
+ .option('--json', 'Output result as JSON')
31
+ .addHelpText('after', `
32
+ Examples:
33
+ orch transfer my-agent --to team-workspace # Transfer agent to another workspace
34
+ orch transfer my-agent --to team-workspace --dry-run # Preview transfer
35
+ orch transfer my-agent --to team-workspace --yes # Skip confirmation
36
+ `)
37
+ .action(async (agentName, options) => {
38
+ const config = await (0, config_1.getResolvedConfig)();
39
+ if (!config.apiKey) {
40
+ throw new errors_1.CliError('Not logged in. Run `orchagent login` first.');
41
+ }
42
+ process.stdout.write('Finding agent and workspaces...\n');
43
+ // Fetch workspaces and agents in parallel
44
+ const [workspacesResponse, agents] = await Promise.all([
45
+ (0, api_1.request)(config, 'GET', '/workspaces'),
46
+ (0, api_1.listMyAgents)(config),
47
+ ]);
48
+ // Find the target workspace by slug
49
+ const targetWorkspace = workspacesResponse.workspaces.find((w) => w.slug === options.to);
50
+ if (!targetWorkspace) {
51
+ throw new errors_1.CliError(`Workspace '${options.to}' not found. Run \`orchagent workspace list\` to see available workspaces.`);
52
+ }
53
+ // Find the agent by name
54
+ const matching = agents.filter((a) => a.name === agentName);
55
+ if (matching.length === 0) {
56
+ throw new errors_1.CliError(`Agent '${agentName}' not found in current workspace.`);
57
+ }
58
+ // Use the most recent version to get the agent ID
59
+ const agent = matching.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime())[0];
60
+ // Check transfer eligibility
61
+ const check = await (0, api_1.checkAgentTransfer)(config, agent.id, targetWorkspace.id);
62
+ // Show transfer summary
63
+ process.stdout.write(`\n${chalk_1.default.bold('Agent:')} ${agent.name}\n`);
64
+ process.stdout.write(`${chalk_1.default.bold('Target workspace:')} ${targetWorkspace.name} (${targetWorkspace.slug})\n`);
65
+ const { details } = check;
66
+ process.stdout.write(`${chalk_1.default.bold('Versions:')} ${details.version_count}\n`);
67
+ if (details.grants_count > 0) {
68
+ process.stdout.write(`${chalk_1.default.bold('Grants to revoke:')} ${details.grants_count}\n`);
69
+ }
70
+ if (details.keys_count > 0) {
71
+ process.stdout.write(`${chalk_1.default.bold('Keys to delete:')} ${details.keys_count}\n`);
72
+ }
73
+ if (details.schedules_count > 0) {
74
+ process.stdout.write(`${chalk_1.default.bold('Schedules to disable:')} ${details.schedules_count}\n`);
75
+ }
76
+ process.stdout.write('\n');
77
+ // Show warnings
78
+ if (check.warnings.length > 0) {
79
+ for (const warning of check.warnings) {
80
+ process.stdout.write(chalk_1.default.yellow(`Warning: ${warning}\n`));
81
+ }
82
+ process.stdout.write('\n');
83
+ }
84
+ // Show blockers and exit if any
85
+ if (check.blockers.length > 0) {
86
+ for (const blocker of check.blockers) {
87
+ process.stdout.write(chalk_1.default.red(`Blocker: ${blocker}\n`));
88
+ }
89
+ process.stdout.write(chalk_1.default.red('\nTransfer cannot proceed due to blockers above.\n'));
90
+ process.exit(1);
91
+ }
92
+ // Handle dry-run
93
+ if (options.dryRun) {
94
+ process.stdout.write('DRY RUN - No changes will be made\n\n');
95
+ process.stdout.write(`Would transfer: ${agent.name} (${details.version_count} version(s))\n`);
96
+ process.stdout.write(`Target: ${targetWorkspace.name} (${targetWorkspace.slug})\n`);
97
+ if (details.grants_count > 0 || details.keys_count > 0 || details.schedules_count > 0) {
98
+ process.stdout.write(chalk_1.default.yellow(`Cleanup: ${details.grants_count} grant(s) revoked, ${details.keys_count} key(s) deleted, ${details.schedules_count} schedule(s) disabled\n`));
99
+ }
100
+ process.stdout.write('\nNo changes made (dry run)\n');
101
+ return;
102
+ }
103
+ // Prompt for confirmation
104
+ if (!options.yes) {
105
+ process.stdout.write(chalk_1.default.yellow('This will transfer the agent and all its versions to the target workspace.\n'));
106
+ process.stdout.write(chalk_1.default.yellow('Existing grants, keys, and schedules in the source workspace will be cleaned up.\n\n'));
107
+ const confirmName = await promptText(`Type "${agent.name}" to confirm transfer: `);
108
+ if (confirmName !== agent.name) {
109
+ process.stdout.write(chalk_1.default.red('\nTransfer cancelled. Name did not match.\n'));
110
+ process.exit(1);
111
+ }
112
+ }
113
+ // Perform transfer
114
+ process.stdout.write('Transferring agent...\n');
115
+ const result = await (0, api_1.transferAgent)(config, agent.id, {
116
+ target_workspace_id: targetWorkspace.id,
117
+ confirmation_name: agent.name,
118
+ });
119
+ await (0, analytics_1.track)('cli_transfer', {
120
+ agent_name: result.agent_name,
121
+ versions_transferred: result.versions_transferred,
122
+ target_workspace: result.target_workspace.slug,
123
+ });
124
+ if (options.json) {
125
+ (0, output_1.printJson)(result);
126
+ return;
127
+ }
128
+ process.stdout.write(`\n${chalk_1.default.green('+')} Transferred ${result.agent_name} (${result.versions_transferred} version(s))\n`);
129
+ process.stdout.write(` From: ${result.source_workspace.name} (${result.source_workspace.slug})\n`);
130
+ process.stdout.write(` To: ${result.target_workspace.name} (${result.target_workspace.slug})\n`);
131
+ if (result.cleanup.grants_revoked > 0 || result.cleanup.keys_deleted > 0 || result.cleanup.schedules_disabled > 0) {
132
+ process.stdout.write(chalk_1.default.gray(`\nCleanup: ${result.cleanup.grants_revoked} grant(s) revoked, ${result.cleanup.keys_deleted} key(s) deleted, ${result.cleanup.schedules_disabled} schedule(s) disabled\n`));
133
+ }
134
+ });
135
+ }
package/dist/lib/api.js CHANGED
@@ -54,6 +54,8 @@ exports.getAgentWithFallback = getAgentWithFallback;
54
54
  exports.downloadCodeBundleAuthenticated = downloadCodeBundleAuthenticated;
55
55
  exports.checkAgentDelete = checkAgentDelete;
56
56
  exports.deleteAgent = deleteAgent;
57
+ exports.checkAgentTransfer = checkAgentTransfer;
58
+ exports.transferAgent = transferAgent;
57
59
  exports.previewAgentVersion = previewAgentVersion;
58
60
  exports.reportInstall = reportInstall;
59
61
  exports.fetchUserProfile = fetchUserProfile;
@@ -248,7 +250,7 @@ async function fetchLlmKeys(config) {
248
250
  return result.keys;
249
251
  }
250
252
  /**
251
- * Download a tool bundle for local execution.
253
+ * Download a code-runtime bundle for local execution.
252
254
  */
253
255
  async function downloadCodeBundle(config, org, agent, version) {
254
256
  const response = await safeFetch(`${config.apiUrl.replace(/\/$/, '')}/public/agents/${org}/${agent}/${version}/bundle`);
@@ -366,6 +368,21 @@ async function deleteAgent(config, agentId, confirmationName) {
366
368
  const params = confirmationName ? `?confirmation_name=${encodeURIComponent(confirmationName)}` : '';
367
369
  return request(config, 'DELETE', `/agents/${agentId}${params}`);
368
370
  }
371
+ /**
372
+ * Check if an agent can be transferred to another workspace.
373
+ */
374
+ async function checkAgentTransfer(config, agentId, targetWorkspaceId) {
375
+ return request(config, 'GET', `/agents/${agentId}/transfer-check?target_workspace_id=${encodeURIComponent(targetWorkspaceId)}`);
376
+ }
377
+ /**
378
+ * Transfer an agent to another workspace.
379
+ */
380
+ async function transferAgent(config, agentId, data) {
381
+ return request(config, 'POST', `/agents/${agentId}/transfer`, {
382
+ body: JSON.stringify(data),
383
+ headers: { 'Content-Type': 'application/json' },
384
+ });
385
+ }
369
386
  /**
370
387
  * Preview the next version number for an agent.
371
388
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orchagent/cli",
3
- "version": "0.3.54",
3
+ "version": "0.3.55",
4
4
  "description": "Command-line interface for orchagent — deploy and run AI agents for your team",
5
5
  "license": "MIT",
6
6
  "author": "orchagent <hello@orchagent.io>",