@orchagent/cli 0.3.57 → 0.3.59

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.
@@ -37,7 +37,6 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
37
37
  };
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
39
  exports.registerGitHubCommand = registerGitHubCommand;
40
- const http_1 = __importDefault(require("http"));
41
40
  const open_1 = __importDefault(require("open"));
42
41
  const cli_table3_1 = __importDefault(require("cli-table3"));
43
42
  const chalk_1 = __importDefault(require("chalk"));
@@ -47,195 +46,10 @@ const api_1 = require("../lib/api");
47
46
  const errors_1 = require("../lib/errors");
48
47
  const analytics_1 = require("../lib/analytics");
49
48
  const output_1 = require("../lib/output");
50
- const DEFAULT_AUTH_PORT = 8375;
51
- const AUTH_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
49
+ const POLL_INTERVAL_MS = 1500;
50
+ const POLL_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
51
+ const SETTINGS_REDIRECT_BASE = 'https://orchagent.io/settings';
52
52
  // Helper functions
53
- function successHtml() {
54
- return `<!DOCTYPE html>
55
- <html>
56
- <head>
57
- <meta charset="utf-8">
58
- <title>orchagent CLI - GitHub Connected</title>
59
- <style>
60
- body {
61
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
62
- display: flex;
63
- align-items: center;
64
- justify-content: center;
65
- min-height: 100vh;
66
- margin: 0;
67
- background: #0a0a0a;
68
- color: #fafafa;
69
- }
70
- .container {
71
- text-align: center;
72
- padding: 2rem;
73
- }
74
- .icon {
75
- width: 64px;
76
- height: 64px;
77
- background: rgba(34, 197, 94, 0.1);
78
- border-radius: 50%;
79
- display: flex;
80
- align-items: center;
81
- justify-content: center;
82
- margin: 0 auto 1.5rem;
83
- }
84
- .icon svg {
85
- width: 32px;
86
- height: 32px;
87
- color: #22c55e;
88
- }
89
- h1 {
90
- font-size: 1.5rem;
91
- margin: 0 0 0.5rem;
92
- }
93
- p {
94
- color: #a1a1aa;
95
- margin: 0;
96
- }
97
- </style>
98
- </head>
99
- <body>
100
- <div class="container">
101
- <div class="icon">
102
- <svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
103
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
104
- </svg>
105
- </div>
106
- <h1>GitHub Connected</h1>
107
- <p>You can close this tab and return to your terminal.</p>
108
- </div>
109
- </body>
110
- </html>`;
111
- }
112
- function errorHtml(message) {
113
- return `<!DOCTYPE html>
114
- <html>
115
- <head>
116
- <meta charset="utf-8">
117
- <title>orchagent CLI - GitHub Connection Error</title>
118
- <style>
119
- body {
120
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
121
- display: flex;
122
- align-items: center;
123
- justify-content: center;
124
- min-height: 100vh;
125
- margin: 0;
126
- background: #0a0a0a;
127
- color: #fafafa;
128
- }
129
- .container {
130
- text-align: center;
131
- padding: 2rem;
132
- }
133
- .icon {
134
- width: 64px;
135
- height: 64px;
136
- background: rgba(239, 68, 68, 0.1);
137
- border-radius: 50%;
138
- display: flex;
139
- align-items: center;
140
- justify-content: center;
141
- margin: 0 auto 1.5rem;
142
- }
143
- .icon svg {
144
- width: 32px;
145
- height: 32px;
146
- color: #ef4444;
147
- }
148
- h1 {
149
- font-size: 1.5rem;
150
- margin: 0 0 0.5rem;
151
- }
152
- p {
153
- color: #a1a1aa;
154
- margin: 0;
155
- }
156
- </style>
157
- </head>
158
- <body>
159
- <div class="container">
160
- <div class="icon">
161
- <svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
162
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
163
- </svg>
164
- </div>
165
- <h1>Connection Error</h1>
166
- <p>${message}</p>
167
- </div>
168
- </body>
169
- </html>`;
170
- }
171
- async function waitForGitHubCallback(port, timeoutMs) {
172
- return new Promise((resolve, reject) => {
173
- let resolved = false;
174
- let server = null;
175
- const cleanup = () => {
176
- if (server) {
177
- server.close();
178
- server = null;
179
- }
180
- };
181
- const timeout = setTimeout(() => {
182
- if (!resolved) {
183
- resolved = true;
184
- cleanup();
185
- reject(new errors_1.CliError('GitHub authentication timed out. Please try again.'));
186
- }
187
- }, timeoutMs);
188
- server = http_1.default.createServer((req, res) => {
189
- const url = new URL(req.url || '/', `http://127.0.0.1:${port}`);
190
- if (url.pathname !== '/callback') {
191
- res.writeHead(404);
192
- res.end('Not Found');
193
- return;
194
- }
195
- const code = url.searchParams.get('code');
196
- const state = url.searchParams.get('state');
197
- const error = url.searchParams.get('error');
198
- if (error) {
199
- res.writeHead(400, { 'Content-Type': 'text/html' });
200
- res.end(errorHtml(url.searchParams.get('error_description') || error));
201
- if (!resolved) {
202
- resolved = true;
203
- clearTimeout(timeout);
204
- cleanup();
205
- reject(new errors_1.CliError(`GitHub authorization failed: ${error}`));
206
- }
207
- return;
208
- }
209
- if (!code || !state) {
210
- res.writeHead(400, { 'Content-Type': 'text/html' });
211
- res.end(errorHtml('Missing code or state parameter'));
212
- return;
213
- }
214
- res.writeHead(200, { 'Content-Type': 'text/html' });
215
- res.end(successHtml());
216
- if (!resolved) {
217
- resolved = true;
218
- clearTimeout(timeout);
219
- cleanup();
220
- resolve({ code, state });
221
- }
222
- });
223
- server.on('error', (err) => {
224
- if (!resolved) {
225
- resolved = true;
226
- clearTimeout(timeout);
227
- cleanup();
228
- if (err.code === 'EADDRINUSE') {
229
- reject(new errors_1.CliError(`Port ${port} is already in use. Try a different port with --port.`));
230
- }
231
- else {
232
- reject(new errors_1.CliError(`Failed to start auth server: ${err.message}`));
233
- }
234
- }
235
- });
236
- server.listen(port, '127.0.0.1');
237
- });
238
- }
239
53
  async function promptForSelection(items) {
240
54
  if (!process.stdin.isTTY) {
241
55
  return null;
@@ -271,38 +85,54 @@ async function promptForSelection(items) {
271
85
  });
272
86
  });
273
87
  }
88
+ function sleep(ms) {
89
+ return new Promise((resolve) => setTimeout(resolve, ms));
90
+ }
274
91
  // Command implementations
275
- async function connectGitHub(config, port) {
276
- const redirectUri = `http://127.0.0.1:${port}/callback`;
277
- // Step 1: Initialize the GitHub OAuth flow
278
- const initResponse = await (0, api_1.request)(config, 'POST', '/github/connect/init', {
279
- body: JSON.stringify({ redirect_uri: redirectUri }),
92
+ async function connectGitHub(config) {
93
+ // Step 1: Initialize the GitHub App install flow
94
+ const initResponse = await (0, api_1.request)(config, 'POST', '/github/install/init', {
95
+ body: JSON.stringify({ redirect_uri: SETTINGS_REDIRECT_BASE }),
280
96
  headers: { 'Content-Type': 'application/json' },
281
97
  });
282
- // Step 2: Start local server to receive callback
283
- const callbackPromise = waitForGitHubCallback(port, AUTH_TIMEOUT_MS);
284
- // Step 3: Open browser
285
- process.stdout.write('Opening browser for GitHub authentication...\n');
98
+ // Step 2: Open browser for GitHub App installation
99
+ process.stdout.write('Opening browser to install the GitHub App...\n');
286
100
  try {
287
- await (0, open_1.default)(initResponse.auth_url);
101
+ await (0, open_1.default)(initResponse.install_url);
288
102
  }
289
103
  catch {
290
- process.stdout.write(`\nPlease open this URL in your browser:\n${initResponse.auth_url}\n\n`);
104
+ // Headless or browser unavailable - print URL for manual copy/paste
105
+ process.stdout.write(`\nCould not open browser automatically.\n`);
106
+ process.stdout.write(`Please open this URL in your browser:\n\n`);
107
+ process.stdout.write(` ${initResponse.install_url}\n\n`);
291
108
  }
292
- // Step 4: Wait for callback
293
- const { code, state } = await callbackPromise;
294
- // Step 5: Exchange code for connection
295
- const callbackResponse = await (0, api_1.request)(config, 'POST', '/github/connect/callback', {
296
- body: JSON.stringify({ code, state }),
297
- headers: { 'Content-Type': 'application/json' },
298
- });
299
- await (0, analytics_1.track)('cli_github_connect', { success: true });
300
- process.stdout.write(`\nConnected to GitHub as ${chalk_1.default.bold(callbackResponse.username)}\n`);
109
+ // Step 3: Poll for completion
110
+ process.stdout.write('Waiting for GitHub App installation...\n');
111
+ const startTime = Date.now();
112
+ while (Date.now() - startTime < POLL_TIMEOUT_MS) {
113
+ await sleep(POLL_INTERVAL_MS);
114
+ const status = await (0, api_1.request)(config, 'GET', `/github/install/status?state=${encodeURIComponent(initResponse.state)}`);
115
+ if (status.status === 'completed') {
116
+ await (0, analytics_1.track)('cli_github_connect', { success: true });
117
+ process.stdout.write('\n');
118
+ process.stdout.write(`Connected to GitHub as ${chalk_1.default.bold(status.github_account_login)}\n`);
119
+ if (status.github_account_type) {
120
+ process.stdout.write(` Type: ${status.github_account_type}\n`);
121
+ }
122
+ return;
123
+ }
124
+ if (status.status === 'failed') {
125
+ await (0, analytics_1.track)('cli_github_connect', { success: false });
126
+ throw new errors_1.CliError(`GitHub App installation failed: ${status.error_message || 'Unknown error'}`);
127
+ }
128
+ // Still pending - continue polling
129
+ }
130
+ throw new errors_1.CliError('GitHub App installation timed out after 5 minutes. Please try again.');
301
131
  }
302
132
  async function disconnectGitHub(config) {
303
- await (0, api_1.request)(config, 'DELETE', '/github/disconnect');
133
+ await (0, api_1.request)(config, 'DELETE', '/github/uninstall');
304
134
  await (0, analytics_1.track)('cli_github_disconnect');
305
- process.stdout.write('Disconnected from GitHub.\n');
135
+ process.stdout.write('GitHub App uninstalled.\n');
306
136
  }
307
137
  async function getGitHubStatus(config, json) {
308
138
  const connection = await (0, api_1.request)(config, 'GET', '/github/connection');
@@ -317,52 +147,19 @@ async function getGitHubStatus(config, json) {
317
147
  }
318
148
  process.stdout.write(`GitHub Status:\n\n`);
319
149
  process.stdout.write(` Connected: ${chalk_1.default.green('Yes')}\n`);
320
- process.stdout.write(` Username: ${chalk_1.default.bold(connection.username)}\n`);
321
- if (connection.connected_at) {
322
- const date = new Date(connection.connected_at).toLocaleDateString();
150
+ process.stdout.write(` Account: ${chalk_1.default.bold(connection.github_account_login)}\n`);
151
+ if (connection.github_account_type) {
152
+ process.stdout.write(` Type: ${connection.github_account_type === 'User' ? 'User' : 'Organization'}\n`);
153
+ }
154
+ if (connection.installed_at) {
155
+ const date = new Date(connection.installed_at).toLocaleDateString();
323
156
  process.stdout.write(` Since: ${date}\n`);
324
157
  }
325
- if (connection.scopes?.length) {
326
- process.stdout.write(` Scopes: ${connection.scopes.join(', ')}\n`);
158
+ if (connection.suspended_at) {
159
+ process.stdout.write(` Status: ${chalk_1.default.yellow('Suspended')} (since ${new Date(connection.suspended_at).toLocaleDateString()})\n`);
327
160
  }
328
161
  process.stdout.write('\n');
329
162
  }
330
- async function listGitHubRepos(config, search, json) {
331
- const params = new URLSearchParams();
332
- if (search) {
333
- params.append('search', search);
334
- }
335
- const queryStr = params.toString();
336
- const repos = await (0, api_1.request)(config, 'GET', `/github/repos${queryStr ? `?${queryStr}` : ''}`);
337
- await (0, analytics_1.track)('cli_github_list', { search: !!search, count: repos.length });
338
- if (json) {
339
- (0, output_1.printJson)(repos);
340
- return;
341
- }
342
- if (repos.length === 0) {
343
- process.stdout.write('No repositories found.\n');
344
- if (search) {
345
- process.stdout.write(`\nTry a different search term or run without --search to see all repos.\n`);
346
- }
347
- return;
348
- }
349
- const table = new cli_table3_1.default({
350
- head: [
351
- chalk_1.default.bold('Repository'),
352
- chalk_1.default.bold('Private'),
353
- chalk_1.default.bold('Last Pushed'),
354
- ],
355
- });
356
- repos.forEach((repo) => {
357
- const visibility = repo.private ? chalk_1.default.yellow('Yes') : chalk_1.default.green('No');
358
- const pushed = repo.pushed_at
359
- ? new Date(repo.pushed_at).toLocaleDateString()
360
- : '-';
361
- table.push([repo.full_name, visibility, pushed]);
362
- });
363
- process.stdout.write(`${table.toString()}\n`);
364
- process.stdout.write(`\nFound ${repos.length} repositor${repos.length === 1 ? 'y' : 'ies'}.\n`);
365
- }
366
163
  async function scanGitHubRepo(config, repo, json) {
367
164
  // Parse owner/repo format
368
165
  const parts = repo.split('/');
@@ -371,7 +168,8 @@ async function scanGitHubRepo(config, repo, json) {
371
168
  `Use owner/repo format, e.g.: orchagent github scan myorg/myrepo`);
372
169
  }
373
170
  const [owner, repoName] = parts;
374
- const results = await (0, api_1.request)(config, 'GET', `/github/repos/${owner}/${repoName}/scan`);
171
+ const response = await (0, api_1.request)(config, 'GET', `/github/repos/${owner}/${repoName}/scan`);
172
+ const results = response.items;
375
173
  await (0, analytics_1.track)('cli_github_scan', { repo, found: results.length });
376
174
  if (json) {
377
175
  (0, output_1.printJson)(results);
@@ -392,7 +190,7 @@ async function scanGitHubRepo(config, repo, json) {
392
190
  ],
393
191
  });
394
192
  results.forEach((item) => {
395
- const typeLabel = item.type === 'skill' ? chalk_1.default.cyan('skill') : chalk_1.default.magenta('agent');
193
+ const typeLabel = item.type === 'skill' ? chalk_1.default.cyan('skill') : chalk_1.default.magenta(item.type);
396
194
  table.push([typeLabel, item.path, item.name || '-']);
397
195
  });
398
196
  process.stdout.write(`${table.toString()}\n`);
@@ -401,13 +199,12 @@ async function scanGitHubRepo(config, repo, json) {
401
199
  return results;
402
200
  }
403
201
  async function importFromGitHub(config, repo, options) {
404
- // Parse owner/repo format
202
+ // Validate owner/repo format
405
203
  const parts = repo.split('/');
406
204
  if (parts.length !== 2) {
407
205
  throw new errors_1.CliError(`Invalid repository format: ${repo}\n\n` +
408
206
  `Use owner/repo format, e.g.: orchagent github import myorg/myrepo`);
409
207
  }
410
- const [owner, repoName] = parts;
411
208
  let selectedPath = options.path;
412
209
  // If no path specified, scan first and let user choose
413
210
  if (!selectedPath) {
@@ -433,8 +230,7 @@ async function importFromGitHub(config, repo, options) {
433
230
  const isPublic = options.private ? false : true;
434
231
  const importResult = await (0, api_1.request)(config, 'POST', '/github/import', {
435
232
  body: JSON.stringify({
436
- owner,
437
- repo: repoName,
233
+ repo,
438
234
  path: selectedPath,
439
235
  is_public: isPublic,
440
236
  name: options.name,
@@ -445,21 +241,48 @@ async function importFromGitHub(config, repo, options) {
445
241
  repo,
446
242
  path: selectedPath,
447
243
  is_public: isPublic,
448
- type: importResult.type,
244
+ type: importResult.agent.type,
449
245
  });
450
246
  if (options.json) {
451
247
  (0, output_1.printJson)(importResult);
452
248
  return;
453
249
  }
454
250
  process.stdout.write('\n');
455
- process.stdout.write(`Imported ${chalk_1.default.bold(importResult.name)} from ${repo}\n`);
251
+ process.stdout.write(`Imported ${chalk_1.default.bold(importResult.agent.name)} from ${repo}\n`);
456
252
  process.stdout.write('\n');
457
- process.stdout.write(` Agent: ${importResult.org_slug}/${importResult.name}\n`);
458
- process.stdout.write(` Version: ${importResult.version}\n`);
459
- process.stdout.write(` Type: ${importResult.type}\n`);
253
+ process.stdout.write(` Agent: ${importResult.agent.name}\n`);
254
+ process.stdout.write(` Version: ${importResult.agent.version}\n`);
255
+ process.stdout.write(` Type: ${importResult.agent.type}\n`);
460
256
  process.stdout.write(` Public: ${isPublic ? chalk_1.default.green('Yes') : chalk_1.default.yellow('No')}\n`);
461
257
  process.stdout.write('\n');
462
- process.stdout.write(`View at: https://orchagent.io/${importResult.org_slug}/${importResult.name}\n`);
258
+ }
259
+ async function getSyncConfig(config, agentId, options) {
260
+ // If --set-auto-publish is specified, update config first
261
+ if (options.setAutoPublish !== undefined) {
262
+ const autoPublish = options.setAutoPublish === 'true';
263
+ await (0, api_1.request)(config, 'PATCH', `/github/agents/${agentId}/sync-config`, {
264
+ body: JSON.stringify({ auto_publish: autoPublish }),
265
+ headers: { 'Content-Type': 'application/json' },
266
+ });
267
+ process.stdout.write(`Updated auto_publish to ${chalk_1.default.bold(String(autoPublish))} for agent ${agentId}\n`);
268
+ return;
269
+ }
270
+ const syncConfig = await (0, api_1.request)(config, 'GET', `/github/agents/${agentId}/sync-config`);
271
+ if (options.json) {
272
+ (0, output_1.printJson)(syncConfig);
273
+ return;
274
+ }
275
+ process.stdout.write(`Sync Config for ${chalk_1.default.bold(agentId)}:\n\n`);
276
+ process.stdout.write(` Auto-publish: ${syncConfig.auto_publish ? chalk_1.default.green('enabled') : chalk_1.default.yellow('disabled')}\n`);
277
+ if (syncConfig.sync_status) {
278
+ process.stdout.write(` Sync status: ${syncConfig.sync_status}\n`);
279
+ }
280
+ process.stdout.write('\n');
281
+ }
282
+ async function approveSync(config, agentId) {
283
+ await (0, api_1.request)(config, 'POST', `/github/agents/${agentId}/approve`);
284
+ await (0, analytics_1.track)('cli_github_approve', { agent_id: agentId });
285
+ process.stdout.write(`Approved pending sync for agent ${chalk_1.default.bold(agentId)}.\n`);
463
286
  }
464
287
  // Command registration
465
288
  function registerGitHubCommand(program) {
@@ -468,22 +291,17 @@ function registerGitHubCommand(program) {
468
291
  .description('Connect to GitHub and import agents');
469
292
  github
470
293
  .command('connect')
471
- .description('Connect your GitHub account via browser OAuth')
472
- .option('--port <port>', `Localhost port for callback (default: ${DEFAULT_AUTH_PORT})`, String(DEFAULT_AUTH_PORT))
473
- .action(async (options) => {
294
+ .description('Install the GitHub App to connect your account')
295
+ .action(async () => {
474
296
  const config = await (0, config_1.getResolvedConfig)();
475
297
  if (!config.apiKey) {
476
298
  throw new errors_1.CliError('Missing API key. Run `orchagent login` first.');
477
299
  }
478
- const port = parseInt(options.port || String(DEFAULT_AUTH_PORT), 10);
479
- if (isNaN(port) || port < 1024 || port > 65535) {
480
- throw new errors_1.CliError('Port must be a number between 1024 and 65535');
481
- }
482
- await connectGitHub(config, port);
300
+ await connectGitHub(config);
483
301
  });
484
302
  github
485
303
  .command('disconnect')
486
- .description('Remove GitHub connection')
304
+ .description('Remove GitHub App installation')
487
305
  .action(async () => {
488
306
  const config = await (0, config_1.getResolvedConfig)();
489
307
  if (!config.apiKey) {
@@ -502,18 +320,6 @@ function registerGitHubCommand(program) {
502
320
  }
503
321
  await getGitHubStatus(config, options.json || false);
504
322
  });
505
- github
506
- .command('list')
507
- .description('List accessible GitHub repositories')
508
- .option('--search <query>', 'Filter repositories by name')
509
- .option('--json', 'Output raw JSON')
510
- .action(async (options) => {
511
- const config = await (0, config_1.getResolvedConfig)();
512
- if (!config.apiKey) {
513
- throw new errors_1.CliError('Missing API key. Run `orchagent login` first.');
514
- }
515
- await listGitHubRepos(config, options.search, options.json || false);
516
- });
517
323
  github
518
324
  .command('scan <repo>')
519
325
  .description('Scan a repository for agents and skills')
@@ -540,4 +346,29 @@ function registerGitHubCommand(program) {
540
346
  }
541
347
  await importFromGitHub(config, repo, options);
542
348
  });
349
+ github
350
+ .command('sync-config <agent>')
351
+ .description('View or update sync configuration for a GitHub-linked agent')
352
+ .option('--set-auto-publish <value>', 'Set auto_publish (true or false)')
353
+ .option('--json', 'Output raw JSON')
354
+ .action(async (agent, options) => {
355
+ const config = await (0, config_1.getResolvedConfig)();
356
+ if (!config.apiKey) {
357
+ throw new errors_1.CliError('Missing API key. Run `orchagent login` first.');
358
+ }
359
+ if (options.setAutoPublish !== undefined && options.setAutoPublish !== 'true' && options.setAutoPublish !== 'false') {
360
+ throw new errors_1.CliError('--set-auto-publish must be "true" or "false"');
361
+ }
362
+ await getSyncConfig(config, agent, options);
363
+ });
364
+ github
365
+ .command('approve <agent_id>')
366
+ .description('Approve a pending GitHub sync for an agent')
367
+ .action(async (agentId) => {
368
+ const config = await (0, config_1.getResolvedConfig)();
369
+ if (!config.apiKey) {
370
+ throw new errors_1.CliError('Missing API key. Run `orchagent login` first.');
371
+ }
372
+ await approveSync(config, agentId);
373
+ });
543
374
  }
@@ -32,6 +32,7 @@ const agent_keys_1 = require("./agent-keys");
32
32
  const schedule_1 = require("./schedule");
33
33
  const service_1 = require("./service");
34
34
  const transfer_1 = require("./transfer");
35
+ const pull_1 = require("./pull");
35
36
  function registerCommands(program) {
36
37
  (0, login_1.registerLoginCommand)(program);
37
38
  (0, whoami_1.registerWhoamiCommand)(program);
@@ -64,4 +65,5 @@ function registerCommands(program) {
64
65
  (0, schedule_1.registerScheduleCommand)(program);
65
66
  (0, service_1.registerServiceCommand)(program);
66
67
  (0, transfer_1.registerTransferCommand)(program);
68
+ (0, pull_1.registerPullCommand)(program);
67
69
  }
@@ -0,0 +1,419 @@
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.registerPullCommand = registerPullCommand;
7
+ const chalk_1 = __importDefault(require("chalk"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const promises_1 = __importDefault(require("fs/promises"));
10
+ const os_1 = __importDefault(require("os"));
11
+ const child_process_1 = require("child_process");
12
+ const config_1 = require("../lib/config");
13
+ const api_1 = require("../lib/api");
14
+ const errors_1 = require("../lib/errors");
15
+ const analytics_1 = require("../lib/analytics");
16
+ const output_1 = require("../lib/output");
17
+ // ─── Helpers ────────────────────────────────────────────────────────────────
18
+ function parsePullRef(value) {
19
+ const [ref, versionPart] = value.split('@');
20
+ const version = versionPart?.trim() || 'latest';
21
+ const segments = ref.split('/');
22
+ if (segments.length === 1) {
23
+ return { agent: segments[0], version };
24
+ }
25
+ if (segments.length === 2) {
26
+ return { org: segments[0], agent: segments[1], version };
27
+ }
28
+ throw new errors_1.CliError('Invalid agent reference. Use org/agent[@version] or agent[@version] format.');
29
+ }
30
+ function canonicalType(typeValue) {
31
+ const normalized = (typeValue || 'agent').toLowerCase();
32
+ return normalized === 'skill' ? 'skill' : 'agent';
33
+ }
34
+ function resolveEngine(data) {
35
+ const ee = data.execution_engine;
36
+ if (ee === 'direct_llm' || ee === 'managed_loop' || ee === 'code_runtime') {
37
+ return ee;
38
+ }
39
+ const normalized = (data.type || '').toLowerCase();
40
+ if (normalized === 'tool' || normalized === 'code')
41
+ return 'code_runtime';
42
+ if (normalized === 'agentic')
43
+ return 'managed_loop';
44
+ return 'direct_llm';
45
+ }
46
+ // ─── Agent Resolution ───────────────────────────────────────────────────────
47
+ async function resolveAgent(config, org, agent, version) {
48
+ // 1. Try public download endpoint
49
+ try {
50
+ const data = await (0, api_1.publicRequest)(config, `/public/agents/${org}/${agent}/${version}/download`);
51
+ return {
52
+ name: data.name,
53
+ version: data.version,
54
+ description: data.description,
55
+ type: data.type || 'agent',
56
+ run_mode: data.run_mode,
57
+ execution_engine: data.execution_engine,
58
+ callable: data.callable,
59
+ prompt: data.prompt,
60
+ input_schema: data.input_schema,
61
+ output_schema: data.output_schema,
62
+ supported_providers: data.supported_providers,
63
+ default_models: data.default_models,
64
+ default_skills: data.default_skills,
65
+ skills_locked: data.skills_locked,
66
+ source_url: data.source_url,
67
+ pip_package: data.pip_package,
68
+ run_command: data.run_command,
69
+ entrypoint: data.entrypoint,
70
+ has_bundle: data.has_bundle,
71
+ source: 'public_download',
72
+ };
73
+ }
74
+ catch (err) {
75
+ // 2. Handle 403 (server-only / download-disabled)
76
+ if (err instanceof api_1.ApiError && err.status === 403) {
77
+ const payload = err.payload;
78
+ const errorCode = payload?.error?.code;
79
+ if (errorCode === 'PAID_AGENT_SERVER_ONLY' || errorCode === 'DOWNLOAD_DISABLED') {
80
+ // Try authenticated owner path
81
+ if (config.apiKey) {
82
+ const ownerData = await tryOwnerFallback(config, org, agent, version);
83
+ if (ownerData)
84
+ return { ...ownerData, source: 'owner_authenticated' };
85
+ }
86
+ // Not owner - block with message
87
+ if (errorCode === 'PAID_AGENT_SERVER_ONLY') {
88
+ throw new errors_1.CliError(`This agent is paid and runs on server only.\n\n` +
89
+ `Use cloud execution: orch run ${org}/${agent}@${version} --data '{...}'`);
90
+ }
91
+ throw new errors_1.CliError(`This agent is server-only and cannot be downloaded.\n\n` +
92
+ `Use cloud execution: orch run ${org}/${agent}@${version} --data '{...}'`);
93
+ }
94
+ }
95
+ // 3. Handle 404 - try private authenticated fallback
96
+ if (!(err instanceof api_1.ApiError) || err.status !== 404)
97
+ throw err;
98
+ }
99
+ // 4. Private agent fallback (authenticated)
100
+ if (!config.apiKey) {
101
+ throw new api_1.ApiError(`Agent '${org}/${agent}@${version}' not found`, 404);
102
+ }
103
+ const userOrg = await (0, api_1.getOrg)(config);
104
+ if (userOrg.slug !== org) {
105
+ throw new api_1.ApiError(`Agent '${org}/${agent}@${version}' not found`, 404);
106
+ }
107
+ const data = await resolveFromMyAgents(config, agent, version, org);
108
+ if (!data) {
109
+ throw new api_1.ApiError(`Agent '${org}/${agent}@${version}' not found`, 404);
110
+ }
111
+ return { ...data, source: 'private_authenticated' };
112
+ }
113
+ async function tryOwnerFallback(config, org, agent, version) {
114
+ try {
115
+ const myAgents = await (0, api_1.listMyAgents)(config);
116
+ let match;
117
+ if (version === 'latest') {
118
+ match = myAgents
119
+ .filter(a => a.name === agent && a.org_slug === org)
120
+ .sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime())[0];
121
+ }
122
+ else {
123
+ match = myAgents.find(a => a.name === agent && a.version === version && a.org_slug === org);
124
+ }
125
+ if (!match)
126
+ return null;
127
+ const agentData = await (0, api_1.request)(config, 'GET', `/agents/${match.id}`);
128
+ return mapAgentToPullData(agentData);
129
+ }
130
+ catch {
131
+ return null;
132
+ }
133
+ }
134
+ async function resolveFromMyAgents(config, agent, version, org) {
135
+ const agents = await (0, api_1.listMyAgents)(config);
136
+ const matching = agents.filter(a => a.name === agent);
137
+ if (matching.length === 0)
138
+ return null;
139
+ let target;
140
+ if (version === 'latest') {
141
+ target = matching.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime())[0];
142
+ }
143
+ else {
144
+ const found = matching.find(a => a.version === version);
145
+ if (!found)
146
+ return null;
147
+ target = found;
148
+ }
149
+ const agentData = await (0, api_1.request)(config, 'GET', `/agents/${target.id}`);
150
+ return mapAgentToPullData(agentData);
151
+ }
152
+ function mapAgentToPullData(agent) {
153
+ return {
154
+ name: agent.name,
155
+ version: agent.version,
156
+ description: agent.description,
157
+ type: agent.type,
158
+ run_mode: agent.run_mode ?? null,
159
+ execution_engine: agent.execution_engine ?? null,
160
+ callable: agent.callable,
161
+ prompt: agent.prompt,
162
+ input_schema: agent.input_schema,
163
+ output_schema: agent.output_schema,
164
+ supported_providers: agent.supported_providers,
165
+ default_models: agent.default_models,
166
+ tags: agent.tags,
167
+ default_skills: agent.default_skills,
168
+ skills_locked: agent.skills_locked,
169
+ source_url: agent.source_url,
170
+ pip_package: agent.pip_package,
171
+ run_command: agent.run_command,
172
+ entrypoint: agent.entrypoint,
173
+ has_bundle: !!agent.code_bundle_url,
174
+ manifest: agent.manifest,
175
+ agentId: agent.id,
176
+ };
177
+ }
178
+ // ─── Manifest Reconstruction ────────────────────────────────────────────────
179
+ function buildManifest(data) {
180
+ const manifest = {
181
+ name: data.name,
182
+ description: data.description || '',
183
+ type: canonicalType(data.type) === 'skill' ? 'skill' : 'agent',
184
+ };
185
+ if (data.run_mode)
186
+ manifest.run_mode = data.run_mode;
187
+ if (data.callable !== undefined)
188
+ manifest.callable = data.callable;
189
+ if (data.tags && data.tags.length > 0)
190
+ manifest.tags = data.tags;
191
+ if (data.supported_providers && data.supported_providers.length > 0) {
192
+ // Don't include if it's just ['any'] (the default)
193
+ if (!(data.supported_providers.length === 1 && data.supported_providers[0] === 'any')) {
194
+ manifest.supported_providers = data.supported_providers;
195
+ }
196
+ }
197
+ if (data.default_models && Object.keys(data.default_models).length > 0) {
198
+ manifest.default_models = data.default_models;
199
+ }
200
+ // Skills
201
+ if (data.default_skills && data.default_skills.length > 0) {
202
+ manifest.default_skills = data.default_skills;
203
+ }
204
+ if (data.skills_locked !== undefined && data.skills_locked) {
205
+ manifest.skills_locked = true;
206
+ }
207
+ // Engine-specific fields
208
+ const engine = resolveEngine(data);
209
+ if (engine === 'code_runtime') {
210
+ if (data.entrypoint && data.entrypoint !== 'sandbox_main.py') {
211
+ manifest.entrypoint = data.entrypoint;
212
+ }
213
+ if (data.source_url)
214
+ manifest.source_url = data.source_url;
215
+ if (data.pip_package)
216
+ manifest.pip_package = data.pip_package;
217
+ if (data.run_command)
218
+ manifest.run_command = data.run_command;
219
+ }
220
+ // Include orchestration manifest if present (for dependencies, etc.)
221
+ if (data.manifest && typeof data.manifest === 'object') {
222
+ const m = { ...data.manifest };
223
+ // Clean up fields that are already top-level
224
+ delete m.runtime;
225
+ delete m.loop;
226
+ if (Object.keys(m).length > 0) {
227
+ manifest.manifest = m;
228
+ }
229
+ }
230
+ return manifest;
231
+ }
232
+ // ─── Bundle Download + Extraction ───────────────────────────────────────────
233
+ async function downloadBundle(config, org, agent, version, agentId) {
234
+ try {
235
+ return await (0, api_1.downloadCodeBundle)(config, org, agent, version);
236
+ }
237
+ catch (err) {
238
+ if (!(err instanceof api_1.ApiError) || err.status !== 404)
239
+ throw err;
240
+ }
241
+ if (config.apiKey && agentId) {
242
+ try {
243
+ return await (0, api_1.downloadCodeBundleAuthenticated)(config, agentId);
244
+ }
245
+ catch (err) {
246
+ if (!(err instanceof api_1.ApiError) || err.status !== 404)
247
+ throw err;
248
+ }
249
+ }
250
+ return null;
251
+ }
252
+ async function unzipBundle(zipPath, destDir) {
253
+ return new Promise((resolve, reject) => {
254
+ const proc = (0, child_process_1.spawn)('unzip', ['-q', zipPath, '-d', destDir], {
255
+ stdio: ['ignore', 'pipe', 'pipe'],
256
+ });
257
+ let stderr = '';
258
+ proc.stderr?.on('data', (chunk) => {
259
+ stderr += chunk.toString();
260
+ });
261
+ proc.on('close', (code) => {
262
+ if (code !== 0) {
263
+ const detail = stderr.trim() || `exit code ${code}`;
264
+ reject(new errors_1.CliError(`Failed to extract bundle: ${detail}`));
265
+ }
266
+ else {
267
+ resolve();
268
+ }
269
+ });
270
+ proc.on('error', (err) => {
271
+ reject(new errors_1.CliError(`Failed to run unzip: ${err.message}. Make sure unzip is installed.`));
272
+ });
273
+ });
274
+ }
275
+ // ─── Command ────────────────────────────────────────────────────────────────
276
+ function registerPullCommand(program) {
277
+ program
278
+ .command('pull <agent>')
279
+ .description('Pull a published agent into a local project directory')
280
+ .option('-o, --output <path>', 'Output directory (default: ./<agent-name>/)')
281
+ .option('--overwrite', 'Replace existing output directory contents')
282
+ .option('--json', 'Print machine-readable result summary')
283
+ .addHelpText('after', `
284
+ Examples:
285
+ orch pull acme/my-agent
286
+ orch pull acme/my-agent@v2
287
+ orch pull my-agent --output ./custom-dir
288
+ orch pull acme/my-agent --overwrite
289
+ orch pull acme/my-agent --json
290
+ `)
291
+ .action(async (agentRef, options) => {
292
+ const write = (message) => {
293
+ if (!options.json)
294
+ process.stdout.write(message);
295
+ };
296
+ const config = await (0, config_1.getResolvedConfig)();
297
+ const parsed = parsePullRef(agentRef);
298
+ // Resolve org from workspace / defaultOrg fallback
299
+ const configFile = await (0, config_1.loadConfig)();
300
+ const org = parsed.org ?? configFile.workspace ?? config.defaultOrg;
301
+ if (!org) {
302
+ throw new errors_1.CliError('Missing org. Use org/agent[@version] format, or set a default org with:\n' +
303
+ ' orch config set default-org <org>');
304
+ }
305
+ write(`Resolving ${org}/${parsed.agent}@${parsed.version}...\n`);
306
+ // Resolve agent data
307
+ const data = await resolveAgent(config, org, parsed.agent, parsed.version);
308
+ // Reject skills
309
+ if (canonicalType(data.type) === 'skill') {
310
+ throw new errors_1.CliError("This is a skill. Use 'orch skill install <ref>' instead.");
311
+ }
312
+ // Resolve output path
313
+ const outputDir = path_1.default.resolve(options.output || `./${data.name}`);
314
+ // Check if output path already exists
315
+ try {
316
+ const stat = await promises_1.default.stat(outputDir);
317
+ if (stat.isFile()) {
318
+ throw new errors_1.CliError(`Output path '${outputDir}' is a file. Please specify a directory.`);
319
+ }
320
+ if (!options.overwrite) {
321
+ throw new errors_1.CliError(`Output directory '${outputDir}' already exists.\n` +
322
+ `Use --overwrite to replace its contents.`);
323
+ }
324
+ // Overwrite: clear and recreate
325
+ await promises_1.default.rm(outputDir, { recursive: true });
326
+ }
327
+ catch (err) {
328
+ if (err.code !== 'ENOENT') {
329
+ if (err instanceof errors_1.CliError)
330
+ throw err;
331
+ throw err;
332
+ }
333
+ }
334
+ await promises_1.default.mkdir(outputDir, { recursive: true });
335
+ const engine = resolveEngine(data);
336
+ const filesWritten = [];
337
+ const warnings = [];
338
+ let bundleExtracted = false;
339
+ // Write orchagent.json
340
+ const manifest = buildManifest(data);
341
+ await promises_1.default.writeFile(path_1.default.join(outputDir, 'orchagent.json'), JSON.stringify(manifest, null, 2) + '\n');
342
+ filesWritten.push('orchagent.json');
343
+ // Write prompt.md (for prompt-driven engines)
344
+ if (data.prompt && (engine === 'direct_llm' || engine === 'managed_loop')) {
345
+ await promises_1.default.writeFile(path_1.default.join(outputDir, 'prompt.md'), data.prompt);
346
+ filesWritten.push('prompt.md');
347
+ }
348
+ // Write schema.json (if schemas exist)
349
+ if (data.input_schema || data.output_schema) {
350
+ const schema = {};
351
+ if (data.input_schema)
352
+ schema.input = data.input_schema;
353
+ if (data.output_schema)
354
+ schema.output = data.output_schema;
355
+ await promises_1.default.writeFile(path_1.default.join(outputDir, 'schema.json'), JSON.stringify(schema, null, 2) + '\n');
356
+ filesWritten.push('schema.json');
357
+ }
358
+ // Bundle download for code_runtime agents
359
+ if (engine === 'code_runtime' && data.has_bundle) {
360
+ write('Downloading code bundle...\n');
361
+ const bundle = await downloadBundle(config, org, data.name, data.version, data.agentId);
362
+ if (bundle) {
363
+ const tempDir = path_1.default.join(os_1.default.tmpdir(), `orchagent-pull-${Date.now()}`);
364
+ const zipPath = path_1.default.join(tempDir, 'bundle.zip');
365
+ try {
366
+ await promises_1.default.mkdir(tempDir, { recursive: true });
367
+ await promises_1.default.writeFile(zipPath, bundle);
368
+ await unzipBundle(zipPath, outputDir);
369
+ bundleExtracted = true;
370
+ write('Bundle extracted.\n');
371
+ }
372
+ finally {
373
+ await promises_1.default.rm(tempDir, { recursive: true, force: true }).catch(() => { });
374
+ }
375
+ }
376
+ else {
377
+ warnings.push('No downloadable bundle available for this version.');
378
+ }
379
+ }
380
+ else if (engine === 'code_runtime' && !data.has_bundle) {
381
+ warnings.push('No downloadable bundle available for this version.');
382
+ }
383
+ // Track analytics
384
+ await (0, analytics_1.track)('cli_pull', {
385
+ org,
386
+ agent: parsed.agent,
387
+ version: data.version,
388
+ engine,
389
+ source: data.source,
390
+ });
391
+ // Output
392
+ const resolvedRef = `${org}/${data.name}@${data.version}`;
393
+ if (options.json) {
394
+ const result = {
395
+ success: true,
396
+ requested_ref: `${org}/${parsed.agent}@${parsed.version}`,
397
+ resolved_ref: resolvedRef,
398
+ output_dir: outputDir,
399
+ engine,
400
+ source: data.source,
401
+ files_written: filesWritten,
402
+ bundle_extracted: bundleExtracted,
403
+ warnings,
404
+ };
405
+ (0, output_1.printJson)(result);
406
+ return;
407
+ }
408
+ write(`\n${chalk_1.default.green('\u2713')} Pulled ${resolvedRef}\n`);
409
+ write(` Output: ${outputDir}\n`);
410
+ write(` Engine: ${engine}\n`);
411
+ write(` Files: ${filesWritten.join(', ')}\n`);
412
+ if (bundleExtracted) {
413
+ write(` Bundle: extracted\n`);
414
+ }
415
+ for (const w of warnings) {
416
+ write(` ${chalk_1.default.yellow('Warning:')} ${w}\n`);
417
+ }
418
+ });
419
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orchagent/cli",
3
- "version": "0.3.57",
3
+ "version": "0.3.59",
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>",