@orchagent/cli 0.3.83 → 0.3.85

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.
@@ -60,6 +60,13 @@ function healthColor(health) {
60
60
  return chalk_1.default.gray(health);
61
61
  }
62
62
  }
63
+ function formatServiceUrl(svc) {
64
+ const url = svc.provider_url || svc.cloud_run_url || '-';
65
+ if (url !== '-' && svc.infrastructure_provider === 'flyio') {
66
+ return `${url} ${chalk_1.default.gray('(internal — not a public endpoint)')}`;
67
+ }
68
+ return url;
69
+ }
63
70
  function severityColor(severity, message) {
64
71
  switch (severity.toUpperCase()) {
65
72
  case 'ERROR':
@@ -135,7 +142,7 @@ function registerServiceCommand(program) {
135
142
  match = agentsList.find(a => a.name === agentName && a.version === agentVersion);
136
143
  }
137
144
  if (!match) {
138
- spinner.fail('Agent not found');
145
+ spinner.stop();
139
146
  throw new errors_1.CliError(`Agent '${agentName}' (version ${agentVersion}) not found in workspace`);
140
147
  }
141
148
  agentId = match.id;
@@ -144,7 +151,7 @@ function registerServiceCommand(program) {
144
151
  catch (e) {
145
152
  if (e instanceof errors_1.CliError)
146
153
  throw e;
147
- spinner.fail('Failed to resolve agent');
154
+ spinner.stop();
148
155
  throw e;
149
156
  }
150
157
  // C-2: Show deprecation notice when --secret is used
@@ -184,7 +191,7 @@ function registerServiceCommand(program) {
184
191
  process.stdout.write(` ${chalk_1.default.bold('Name:')} ${svc.service_name}\n`);
185
192
  process.stdout.write(` ${chalk_1.default.bold('Agent:')} ${svc.agent_name}@${svc.agent_version}\n`);
186
193
  process.stdout.write(` ${chalk_1.default.bold('State:')} ${stateColor(svc.current_state)}\n`);
187
- process.stdout.write(` ${chalk_1.default.bold('URL:')} ${svc.provider_url || svc.cloud_run_url || '-'}\n`);
194
+ process.stdout.write(` ${chalk_1.default.bold('URL:')} ${formatServiceUrl(svc)}\n`);
188
195
  if (options.pin) {
189
196
  process.stdout.write(` ${chalk_1.default.bold('Pinned:')} ${chalk_1.default.yellow(`yes (won't auto-update on publish)`)}\n`);
190
197
  }
@@ -192,7 +199,7 @@ function registerServiceCommand(program) {
192
199
  process.stdout.write(chalk_1.default.gray(`View logs: orch service logs ${svc.id}\n`));
193
200
  }
194
201
  catch (e) {
195
- deploySpinner.fail('Deploy failed');
202
+ deploySpinner.stop();
196
203
  throw e;
197
204
  }
198
205
  });
@@ -311,7 +318,7 @@ function registerServiceCommand(program) {
311
318
  process.stdout.write(`${chalk_1.default.green('\u2713')} Service '${result.service.service_name}' restarted (restarts: ${result.service.restart_count})\n`);
312
319
  }
313
320
  catch (e) {
314
- spinner.fail('Restart failed');
321
+ spinner.stop();
315
322
  throw e;
316
323
  }
317
324
  });
@@ -348,7 +355,7 @@ function registerServiceCommand(program) {
348
355
  }
349
356
  process.stdout.write(` Instances: ${svc.min_instances}-${svc.max_instances}\n`);
350
357
  process.stdout.write(` Service ID: ${svc.provider_service_id || svc.cloud_run_service || '-'}\n`);
351
- process.stdout.write(` URL: ${svc.provider_url || svc.cloud_run_url || '-'}\n`);
358
+ process.stdout.write(` URL: ${formatServiceUrl(svc)}\n`);
352
359
  process.stdout.write(` Deployed: ${formatDate(svc.last_deployed_at)}\n`);
353
360
  process.stdout.write(` Last Restart: ${formatDate(svc.last_restart_at)}\n`);
354
361
  if (svc.last_error) {
@@ -414,7 +421,7 @@ function registerServiceCommand(program) {
414
421
  process.stdout.write(`${chalk_1.default.green('\u2713')} Service '${result.service.service_name}' deleted\n`);
415
422
  }
416
423
  catch (e) {
417
- spinner.fail('Delete failed');
424
+ spinner.stop();
418
425
  throw e;
419
426
  }
420
427
  });
@@ -468,7 +475,7 @@ function registerServiceCommand(program) {
468
475
  }
469
476
  }
470
477
  catch (e) {
471
- spinner.fail('Failed to update environment');
478
+ spinner.stop();
472
479
  throw e;
473
480
  }
474
481
  });
@@ -517,7 +524,7 @@ function registerServiceCommand(program) {
517
524
  }
518
525
  }
519
526
  catch (e) {
520
- spinner.fail('Failed to update environment');
527
+ spinner.stop();
521
528
  throw e;
522
529
  }
523
530
  });
@@ -594,7 +601,7 @@ function registerServiceCommand(program) {
594
601
  }
595
602
  }
596
603
  catch (e) {
597
- spinner.fail('Failed to attach secrets');
604
+ spinner.stop();
598
605
  throw e;
599
606
  }
600
607
  });
@@ -638,7 +645,7 @@ function registerServiceCommand(program) {
638
645
  }
639
646
  }
640
647
  catch (e) {
641
- spinner.fail('Failed to detach secrets');
648
+ spinner.stop();
642
649
  throw e;
643
650
  }
644
651
  });
@@ -45,7 +45,6 @@ const api_1 = require("../lib/api");
45
45
  const errors_1 = require("../lib/errors");
46
46
  const analytics_1 = require("../lib/analytics");
47
47
  const installed_1 = require("../lib/installed");
48
- const pricing_1 = require("../lib/pricing");
49
48
  const package_json_1 = __importDefault(require("../../package.json"));
50
49
  const DEFAULT_VERSION = 'latest';
51
50
  function stripFrontmatter(content) {
@@ -110,55 +109,6 @@ async function downloadSkillWithFallback(config, org, skill, version, workspaceI
110
109
  throw new errors_1.CliError(`${org}/${skill} is not a skill (type: ${skillType || 'prompt'})`);
111
110
  }
112
111
  }
113
- // Check if paid skill BEFORE attempting download
114
- if (skillMeta && (0, pricing_1.isPaidAgent)(skillMeta)) {
115
- // Paid skill - check ownership
116
- if (config.apiKey) {
117
- const callerOrg = await (0, api_1.getOrg)(config, workspaceId);
118
- const isOwner = (skillMeta.org_id && callerOrg.id === skillMeta.org_id) ||
119
- (skillMeta.org_slug && callerOrg.slug === skillMeta.org_slug);
120
- if (isOwner) {
121
- // Owner - fetch from authenticated endpoint with full content
122
- const myAgents = await (0, api_1.listMyAgents)(config, workspaceId);
123
- const matching = myAgents.filter(a => a.name === skill && a.type === 'skill');
124
- if (matching.length > 0) {
125
- let targetAgent;
126
- if (version === 'latest') {
127
- targetAgent = matching.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime())[0];
128
- }
129
- else {
130
- const found = matching.find(a => a.version === version);
131
- if (!found) {
132
- throw new api_1.ApiError(`Skill '${org}/${skill}@${version}' not found`, 404);
133
- }
134
- targetAgent = found;
135
- }
136
- // Fetch full skill data with prompt from authenticated endpoint
137
- const skillData = await (0, api_1.request)(config, 'GET', `/agents/${targetAgent.id}`);
138
- // Convert Agent to SkillDownload format
139
- return {
140
- type: skillData.type,
141
- name: skillData.name,
142
- version: skillData.version,
143
- description: skillData.description,
144
- prompt: skillData.prompt,
145
- };
146
- }
147
- }
148
- else {
149
- // Non-owner - block with helpful message
150
- const price = (0, pricing_1.formatPrice)(skillMeta);
151
- throw new errors_1.CliError(`This skill is paid (${price}) and can only be used on the server.\n\n` +
152
- `Paid skills are loaded automatically during server execution.`);
153
- }
154
- }
155
- else {
156
- // Not authenticated - block
157
- const price = (0, pricing_1.formatPrice)(skillMeta);
158
- throw new errors_1.CliError(`This skill is paid (${price}) and can only be used on the server.\n\n` +
159
- `Paid skills are loaded automatically during server execution.`);
160
- }
161
- }
162
112
  // Check if download is disabled (server-only skill)
163
113
  if (skillMeta && skillMeta.allow_local_download === false) {
164
114
  if (config.apiKey) {
@@ -204,11 +154,6 @@ async function downloadSkillWithFallback(config, org, skill, version, workspaceI
204
154
  // If download fails but metadata exists, it might be a 403 for other reasons
205
155
  if (err instanceof api_1.ApiError && err.status === 403) {
206
156
  const payload = err.payload;
207
- if (payload?.error?.code === 'PAID_AGENT_SERVER_ONLY') {
208
- const price = payload.error.price_per_call_cents || 0;
209
- throw new errors_1.CliError(`This skill costs $${(price / 100).toFixed(2)}/call and runs on server only.\n\n` +
210
- `Use: orchagent run ${org}/${skill}@${version} --data '{...}'`);
211
- }
212
157
  if (payload?.error?.code === 'DOWNLOAD_DISABLED') {
213
158
  throw new errors_1.CliError(`This skill is server-only and cannot be downloaded.\n\n` +
214
159
  `Skills are loaded automatically during server execution via 'orchagent run'.`);
@@ -259,18 +204,18 @@ async function downloadSkillWithFallback(config, org, skill, version, workspaceI
259
204
  }
260
205
  function registerSkillCommand(program) {
261
206
  const skill = program.command('skill').description('Manage and install skills');
262
- // orch skill list (deprecated)
207
+ skill.action(() => { skill.help(); });
208
+ // orch skill list
263
209
  skill
264
210
  .command('list')
265
- .description('(Deprecated) Use "orchagent search --type skills" instead')
211
+ .description('Browse available skills')
266
212
  .option('--json', 'Output raw JSON')
267
213
  .action(async () => {
268
- process.stdout.write('The "skill list" command has been replaced by "search".\n\n' +
269
- 'Usage:\n' +
270
- ' orchagent search <query> --type skills Search skills by keyword\n' +
271
- ' orchagent search --popular --type skills Top skills by stars\n' +
272
- ' orchagent search --recent --type skills Most recently published\n\n' +
273
- 'View all skills at: https://orchagent.io/explore\n');
214
+ process.stdout.write('Browse and discover skills at: https://orchagent.io/explore\n\n' +
215
+ 'Install a skill:\n' +
216
+ ' orch skill install <org>/<skill-name>\n\n' +
217
+ 'View installed skills:\n' +
218
+ ' orch update --check\n');
274
219
  process.exit(0);
275
220
  });
276
221
  // orch skill create [name]
@@ -514,7 +514,7 @@ async function runPromptFixtureTests(agentDir, fixtures, verbose, config) {
514
514
  const detected = await (0, llm_1.detectLlmKey)(['any'], config);
515
515
  if (!detected) {
516
516
  throw new errors_1.CliError('No LLM key found for fixture tests.\n' +
517
- 'Set an environment variable (e.g., OPENAI_API_KEY) or run `orchagent keys add <provider>`');
517
+ 'Set an environment variable (e.g., OPENAI_API_KEY) or run `orch secrets set <PROVIDER>_API_KEY <key>`');
518
518
  }
519
519
  const { provider, key, model: serverModel } = detected;
520
520
  const model = serverModel ?? (0, llm_1.getDefaultModel)(provider);
package/dist/lib/api.js CHANGED
@@ -65,8 +65,6 @@ exports.getEnvironment = getEnvironment;
65
65
  exports.createEnvironment = createEnvironment;
66
66
  exports.deleteEnvironment = deleteEnvironment;
67
67
  exports.setWorkspaceDefaultEnvironment = setWorkspaceDefaultEnvironment;
68
- exports.getCreditsBalance = getCreditsBalance;
69
- exports.createCreditCheckout = createCreditCheckout;
70
68
  exports.listAgentKeys = listAgentKeys;
71
69
  exports.createAgentKey = createAgentKey;
72
70
  exports.deleteAgentKey = deleteAgentKey;
@@ -531,16 +529,6 @@ async function setWorkspaceDefaultEnvironment(config, workspaceId, environmentId
531
529
  headers: { 'Content-Type': 'application/json' },
532
530
  });
533
531
  }
534
- // Billing API functions
535
- async function getCreditsBalance(config) {
536
- return request(config, 'GET', '/billing/credits');
537
- }
538
- async function createCreditCheckout(config, amountCents) {
539
- return request(config, 'POST', '/billing/add-credits', {
540
- body: JSON.stringify({ amount_cents: amountCents }),
541
- headers: { 'Content-Type': 'application/json' },
542
- });
543
- }
544
532
  async function listAgentKeys(config, agentId) {
545
533
  return request(config, 'GET', `/agents/${agentId}/keys`);
546
534
  }
@@ -6,8 +6,10 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.checkNodeVersion = checkNodeVersion;
7
7
  exports.checkCliVersion = checkCliVersion;
8
8
  exports.checkGitAvailable = checkGitAvailable;
9
+ exports.checkDualInstallation = checkDualInstallation;
9
10
  exports.runEnvironmentChecks = runEnvironmentChecks;
10
11
  const child_process_1 = require("child_process");
12
+ const fs_1 = require("fs");
11
13
  const package_json_1 = __importDefault(require("../../../../package.json"));
12
14
  const update_notifier_1 = require("../../update-notifier");
13
15
  const REQUIRED_NODE_MAJOR = 18;
@@ -142,6 +144,148 @@ async function checkGitAvailable() {
142
144
  };
143
145
  }
144
146
  }
147
+ /**
148
+ * Find all binary paths for a given command name using `which -a`.
149
+ * Returns an empty array if the command is not found.
150
+ * Note: Uses execSync with a hardcoded binary name — no user input, safe from injection.
151
+ */
152
+ function findAllBinaryPaths(binaryName) {
153
+ try {
154
+ const output = (0, child_process_1.execSync)(`which -a ${binaryName}`, {
155
+ encoding: 'utf-8',
156
+ timeout: 5000,
157
+ stdio: ['pipe', 'pipe', 'pipe'],
158
+ });
159
+ return output.trim().split('\n').filter(Boolean);
160
+ }
161
+ catch {
162
+ return [];
163
+ }
164
+ }
165
+ /**
166
+ * Get the CLI version from a binary path by running it with --version.
167
+ * Uses execFileSync (no shell) to avoid injection via path characters.
168
+ */
169
+ function getVersionFromBinary(binPath) {
170
+ try {
171
+ const output = (0, child_process_1.execFileSync)(binPath, ['--version'], {
172
+ encoding: 'utf-8',
173
+ timeout: 5000,
174
+ stdio: ['pipe', 'pipe', 'pipe'],
175
+ });
176
+ const match = output.match(/(\d+\.\d+\.\d+)/);
177
+ return match ? match[1] : null;
178
+ }
179
+ catch {
180
+ return null;
181
+ }
182
+ }
183
+ /**
184
+ * Resolve a path through symlinks. Returns the original path if resolution fails.
185
+ */
186
+ function safeRealpathSync(p) {
187
+ try {
188
+ return (0, fs_1.realpathSync)(p);
189
+ }
190
+ catch {
191
+ return p;
192
+ }
193
+ }
194
+ /**
195
+ * Detect multiple CLI installations at different paths/versions (BUG-008).
196
+ *
197
+ * System-level (/usr/local/bin/orchagent) and user-level (~/.npm-global/bin/orch)
198
+ * installs can coexist silently. `npm update -g` updates one but not the other,
199
+ * leaving the user running an outdated version without knowing it.
200
+ */
201
+ async function checkDualInstallation() {
202
+ // Skip on Windows (which -a not available)
203
+ if (process.platform === 'win32') {
204
+ return {
205
+ category: 'environment',
206
+ name: 'dual_installation',
207
+ status: 'info',
208
+ message: 'Installation path check skipped (Windows)',
209
+ details: { skipped: true, reason: 'Windows not supported' },
210
+ };
211
+ }
212
+ try {
213
+ const binaryNames = ['orch', 'orchagent'];
214
+ const installations = new Map();
215
+ for (const binary of binaryNames) {
216
+ const paths = findAllBinaryPaths(binary);
217
+ for (const binPath of paths) {
218
+ const realPath = safeRealpathSync(binPath);
219
+ // Deduplicate by resolved real path
220
+ if (installations.has(realPath))
221
+ continue;
222
+ const version = getVersionFromBinary(binPath) || 'unknown';
223
+ installations.set(realPath, {
224
+ path: binPath,
225
+ realPath,
226
+ version,
227
+ binary,
228
+ });
229
+ }
230
+ }
231
+ if (installations.size <= 1) {
232
+ return {
233
+ category: 'environment',
234
+ name: 'dual_installation',
235
+ status: 'success',
236
+ message: 'Single CLI installation',
237
+ details: {
238
+ installationCount: installations.size,
239
+ installations: [...installations.values()],
240
+ },
241
+ };
242
+ }
243
+ // Multiple installations found
244
+ const allInstalls = [...installations.values()];
245
+ const versions = new Set(allInstalls.map((i) => i.version));
246
+ const versionsDiffer = versions.size > 1;
247
+ const pathList = allInstalls
248
+ .map((i) => `${i.path} (v${i.version})`)
249
+ .join(', ');
250
+ if (versionsDiffer) {
251
+ return {
252
+ category: 'environment',
253
+ name: 'dual_installation',
254
+ status: 'warning',
255
+ message: `Multiple CLI versions found: ${pathList}`,
256
+ fix: 'Remove the outdated installation. Run `which -a orch orchagent` to see all paths, then remove the older binary',
257
+ details: {
258
+ installationCount: installations.size,
259
+ versionMismatch: true,
260
+ installations: allInstalls,
261
+ },
262
+ };
263
+ }
264
+ // Same version at multiple paths — informational only
265
+ return {
266
+ category: 'environment',
267
+ name: 'dual_installation',
268
+ status: 'info',
269
+ message: `Multiple CLI paths (same version v${allInstalls[0].version}): ${pathList}`,
270
+ details: {
271
+ installationCount: installations.size,
272
+ versionMismatch: false,
273
+ installations: allInstalls,
274
+ },
275
+ };
276
+ }
277
+ catch (err) {
278
+ return {
279
+ category: 'environment',
280
+ name: 'dual_installation',
281
+ status: 'info',
282
+ message: 'Could not check for duplicate installations',
283
+ details: {
284
+ error: err instanceof Error ? err.message : 'unknown error',
285
+ },
286
+ };
287
+ }
288
+ }
145
289
  /**
146
290
  * Run all environment checks.
147
291
  */
@@ -150,6 +294,7 @@ async function runEnvironmentChecks() {
150
294
  checkNodeVersion(),
151
295
  checkCliVersion(),
152
296
  checkGitAvailable(),
297
+ checkDualInstallation(),
153
298
  ]);
154
299
  return results;
155
300
  }
@@ -54,7 +54,7 @@ function renderLlmSection(checks, verbose) {
54
54
  // Build location text
55
55
  let location;
56
56
  if (serverVal === null) {
57
- location = localVal ? 'Server unknown, local configured' : 'Server unknown, not local';
57
+ location = localVal ? 'Local configured (vault keys not checked)' : 'Not configured locally (vault keys used for cloud runs)';
58
58
  }
59
59
  else if (serverVal && localVal) {
60
60
  location = 'Configured (server + local)';
@@ -45,6 +45,8 @@ class CliError extends Error {
45
45
  exitCode;
46
46
  cause;
47
47
  responseBody;
48
+ /** When true, exitWithError skips printing — the message was already shown (e.g. via spinner.fail). */
49
+ displayed;
48
50
  constructor(message, exitCode = 1) {
49
51
  super(message);
50
52
  this.exitCode = exitCode;
@@ -78,7 +80,12 @@ async function exitWithError(err) {
78
80
  }
79
81
  // Flush PostHog before exiting
80
82
  await (0, analytics_1.shutdownPostHog)();
81
- process.stderr.write(`${message}\n`);
83
+ // Skip printing if the error was already shown (e.g. by spinner.fail)
84
+ const alreadyDisplayed = (err instanceof CliError && err.displayed) ||
85
+ (err instanceof Error && err._displayed);
86
+ if (!alreadyDisplayed) {
87
+ process.stderr.write(`${message}\n`);
88
+ }
82
89
  if (err instanceof CliError) {
83
90
  process.exit(err.exitCode);
84
91
  }
@@ -96,6 +96,11 @@ async function withSpinner(text, fn, options) {
96
96
  ? options.failText(err)
97
97
  : options?.failText || (err instanceof Error ? err.message : 'Failed');
98
98
  spinner.fail(failMsg);
99
+ // Mark as already displayed so exitWithError doesn't print again
100
+ if (err instanceof Error) {
101
+ ;
102
+ err._displayed = true;
103
+ }
99
104
  throw err;
100
105
  }
101
106
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orchagent/cli",
3
- "version": "0.3.83",
3
+ "version": "0.3.85",
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>",
@@ -25,7 +25,8 @@
25
25
  "node": ">=18"
26
26
  },
27
27
  "bin": {
28
- "orch": "dist/index.js"
28
+ "orch": "dist/index.js",
29
+ "orchagent": "dist/index.js"
29
30
  },
30
31
  "files": [
31
32
  "dist",
@@ -1,22 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.isPaidAgent = isPaidAgent;
4
- exports.formatPrice = formatPrice;
5
- function isPaidAgent(agent) {
6
- // Fail-closed: per_call mode with missing or >0 price is paid
7
- if (agent.pricing_mode === 'per_call') {
8
- const price = agent.price_per_call_cents;
9
- return price === null || price === undefined || price > 0;
10
- }
11
- return false;
12
- }
13
- function formatPrice(agent) {
14
- if (!isPaidAgent(agent)) {
15
- return 'FREE';
16
- }
17
- const price = agent.price_per_call_cents;
18
- if (!price) {
19
- return 'PAID (server-only)'; // Fail-closed message
20
- }
21
- return `$${(price / 100).toFixed(2)}/call`;
22
- }