@orchagent/cli 0.3.90 → 0.3.92

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,213 @@
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.registerScaffoldCommand = registerScaffoldCommand;
7
+ const promises_1 = __importDefault(require("fs/promises"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const config_1 = require("../lib/config");
10
+ const api_1 = require("../lib/api");
11
+ const agent_ref_1 = require("../lib/agent-ref");
12
+ const errors_1 = require("../lib/errors");
13
+ const analytics_1 = require("../lib/analytics");
14
+ const output_1 = require("../lib/output");
15
+ const scaffold_orchestration_1 = require("../lib/scaffold-orchestration");
16
+ async function fileExists(filePath) {
17
+ try {
18
+ await promises_1.default.access(filePath);
19
+ return true;
20
+ }
21
+ catch {
22
+ return false;
23
+ }
24
+ }
25
+ function asCanonicalType(value) {
26
+ const normalized = String(value || 'agent').trim().toLowerCase();
27
+ if (normalized === 'agentic')
28
+ return 'agent';
29
+ if (normalized === 'code')
30
+ return 'tool';
31
+ return normalized;
32
+ }
33
+ function asObject(value) {
34
+ if (!value || typeof value !== 'object' || Array.isArray(value))
35
+ return undefined;
36
+ return value;
37
+ }
38
+ function formatDependencyNotFound(org, agent, version) {
39
+ return `Dependency agent not found: ${org}/${agent}@${version}`;
40
+ }
41
+ function throwDependencyResolutionError(err, org, agent, version) {
42
+ if (err instanceof api_1.ApiError) {
43
+ if (err.status === 404) {
44
+ throw new errors_1.CliError(formatDependencyNotFound(org, agent, version));
45
+ }
46
+ if (err.status === 401) {
47
+ throw new errors_1.CliError(`Authentication required to resolve ${org}/${agent}@${version}. Run \`orch login\` first.`);
48
+ }
49
+ throw new errors_1.CliError(`Failed to resolve ${org}/${agent}@${version}: ${err.message} (HTTP ${err.status})`);
50
+ }
51
+ throw err;
52
+ }
53
+ function assertDependencyIsCallable(agent, org, name, version) {
54
+ const canonicalType = asCanonicalType(agent.type);
55
+ if (canonicalType === 'skill') {
56
+ throw new errors_1.CliError(`Dependency ${org}/${name}@${version} is a skill. Skills are not callable agents.`);
57
+ }
58
+ if (agent.callable === false) {
59
+ throw new errors_1.CliError(`Dependency ${org}/${name}@${version} has callable: false and cannot be used for orchestration.`);
60
+ }
61
+ }
62
+ async function resolveDependencies(config, rawRefs) {
63
+ const workspaceByOrg = new Map();
64
+ const workspaceForOrg = async (org) => {
65
+ if (!workspaceByOrg.has(org)) {
66
+ workspaceByOrg.set(org, (0, api_1.resolveWorkspaceIdForOrg)(config, org).catch(() => undefined));
67
+ }
68
+ return workspaceByOrg.get(org);
69
+ };
70
+ const resolved = await Promise.all(rawRefs.map(async (rawRef) => {
71
+ const parsed = (0, agent_ref_1.parseAgentRef)(rawRef);
72
+ const org = parsed.org || config.defaultOrg;
73
+ if (!org) {
74
+ throw new errors_1.CliError(`Missing org in '${rawRef}'. Use org/agent[@version] format or set default org with \`orch config set default_org <org>\`.`);
75
+ }
76
+ const workspaceId = await workspaceForOrg(org);
77
+ let depAgent;
78
+ try {
79
+ depAgent = await (0, api_1.getAgentWithFallback)(config, org, parsed.agent, parsed.version, workspaceId);
80
+ }
81
+ catch (err) {
82
+ throwDependencyResolutionError(err, org, parsed.agent, parsed.version);
83
+ }
84
+ const pinnedVersion = depAgent.version || parsed.version;
85
+ assertDependencyIsCallable(depAgent, org, parsed.agent, pinnedVersion);
86
+ return {
87
+ org,
88
+ name: parsed.agent,
89
+ version: pinnedVersion,
90
+ description: typeof depAgent.description === 'string' ? depAgent.description : undefined,
91
+ inputSchema: asObject(depAgent.input_schema),
92
+ };
93
+ }));
94
+ const deduped = (0, scaffold_orchestration_1.dedupeOrchestrationDependencies)(resolved);
95
+ if (deduped.conflicts.length > 0) {
96
+ const details = deduped.conflicts
97
+ .map((conflict) => `- ${conflict.id}: ${conflict.versions.join(', ')}`)
98
+ .join('\n');
99
+ throw new errors_1.CliError(`Conflicting dependency versions provided:\n${details}\nUse a single version per dependency.`);
100
+ }
101
+ return {
102
+ dependencies: deduped.dependencies,
103
+ duplicates: deduped.duplicates,
104
+ };
105
+ }
106
+ async function ensureOutputDirectory(outputDir) {
107
+ const exists = await fileExists(outputDir);
108
+ if (!exists) {
109
+ await promises_1.default.mkdir(outputDir, { recursive: true });
110
+ return;
111
+ }
112
+ const stat = await promises_1.default.stat(outputDir);
113
+ if (!stat.isDirectory()) {
114
+ throw new errors_1.CliError(`Output path exists but is not a directory: ${outputDir}`);
115
+ }
116
+ }
117
+ function registerScaffoldCommand(program) {
118
+ const scaffold = program
119
+ .command('scaffold')
120
+ .description('Scaffold projects from existing agents')
121
+ .action(() => { scaffold.help(); });
122
+ scaffold
123
+ .command('orchestration <agents...>')
124
+ .description('Generate a managed-loop orchestrator scaffold from published dependency agents')
125
+ .option('--profile <name>', 'Use API key from named profile')
126
+ .option('--name <name>', 'Orchestrator name (default: output directory name)')
127
+ .option('--output <dir>', 'Output directory (default: current directory)')
128
+ .option('--force', 'Overwrite existing scaffold files')
129
+ .option('--json', 'Print scaffold summary as JSON')
130
+ .action(async (agents, options) => {
131
+ const config = await (0, config_1.getResolvedConfig)({}, options.profile);
132
+ const outputDir = path_1.default.resolve(options.output || process.cwd());
133
+ await ensureOutputDirectory(outputDir);
134
+ const manifestName = (options.name || path_1.default.basename(outputDir)).trim();
135
+ const nameErrors = (0, scaffold_orchestration_1.validateScaffoldAgentName)(manifestName);
136
+ if (nameErrors.length > 0) {
137
+ throw new errors_1.CliError(nameErrors.join('\n'));
138
+ }
139
+ const manifestPath = path_1.default.join(outputDir, 'orchagent.json');
140
+ const promptPath = path_1.default.join(outputDir, 'prompt.md');
141
+ const schemaPath = path_1.default.join(outputDir, 'schema.json');
142
+ const targetFiles = [manifestPath, promptPath, schemaPath];
143
+ if (!options.force) {
144
+ const existing = (await Promise.all(targetFiles.map(async (filePath) => (await fileExists(filePath) ? filePath : null)))).filter((filePath) => Boolean(filePath));
145
+ if (existing.length > 0) {
146
+ const rel = existing.map((f) => path_1.default.relative(outputDir, f));
147
+ throw new errors_1.CliError(`Refusing to overwrite existing files in ${outputDir}: ${rel.join(', ')}. Re-run with --force to overwrite.`);
148
+ }
149
+ }
150
+ const { dependencies, duplicates } = await resolveDependencies(config, agents);
151
+ if (dependencies.length === 0) {
152
+ throw new errors_1.CliError('No dependency agents were resolved.');
153
+ }
154
+ const customTools = (0, scaffold_orchestration_1.buildOrchestrationCustomTools)(dependencies);
155
+ const manifest = (0, scaffold_orchestration_1.buildOrchestrationManifest)({
156
+ name: manifestName,
157
+ dependencies,
158
+ customTools,
159
+ });
160
+ const prompt = (0, scaffold_orchestration_1.buildOrchestrationPrompt)({
161
+ name: manifestName,
162
+ dependencies,
163
+ customTools,
164
+ });
165
+ const schema = (0, scaffold_orchestration_1.buildOrchestrationSchema)();
166
+ await promises_1.default.writeFile(manifestPath, JSON.stringify(manifest, null, 2) + '\n');
167
+ await promises_1.default.writeFile(promptPath, prompt);
168
+ await promises_1.default.writeFile(schemaPath, JSON.stringify(schema, null, 2) + '\n');
169
+ await (0, analytics_1.track)('cli_scaffold_orchestration', {
170
+ dependency_count: dependencies.length,
171
+ duplicates_removed: duplicates.length,
172
+ output_custom: Boolean(options.output),
173
+ force: Boolean(options.force),
174
+ json: Boolean(options.json),
175
+ });
176
+ if (options.json) {
177
+ (0, output_1.printJson)({
178
+ name: manifestName,
179
+ output_dir: outputDir,
180
+ dependencies: dependencies.map((dep) => (0, scaffold_orchestration_1.dependencyRef)(dep)),
181
+ custom_tools: customTools.map((tool) => tool.name),
182
+ files: ['orchagent.json', 'prompt.md', 'schema.json'],
183
+ duplicates_removed: duplicates,
184
+ });
185
+ return;
186
+ }
187
+ process.stdout.write(`Scaffolded orchestrator "${manifestName}" in ${outputDir}\n`);
188
+ process.stdout.write('\nDependencies:\n');
189
+ for (const dep of dependencies) {
190
+ process.stdout.write(` - ${(0, scaffold_orchestration_1.dependencyRef)(dep)}\n`);
191
+ }
192
+ if (duplicates.length > 0) {
193
+ process.stdout.write('\nRemoved duplicate dependency refs:\n');
194
+ for (const dup of duplicates) {
195
+ process.stdout.write(` - ${dup}\n`);
196
+ }
197
+ }
198
+ process.stdout.write('\nFiles written:\n');
199
+ process.stdout.write(' - orchagent.json\n');
200
+ process.stdout.write(' - prompt.md\n');
201
+ process.stdout.write(' - schema.json\n');
202
+ process.stdout.write('\nNext steps:\n');
203
+ if (path_1.default.resolve(outputDir) !== path_1.default.resolve(process.cwd())) {
204
+ process.stdout.write(` 1. cd ${outputDir}\n`);
205
+ process.stdout.write(' 2. Review prompt.md and schema.json\n');
206
+ process.stdout.write(' 3. Publish: orch publish\n');
207
+ }
208
+ else {
209
+ process.stdout.write(' 1. Review prompt.md and schema.json\n');
210
+ process.stdout.write(' 2. Publish: orch publish\n');
211
+ }
212
+ });
213
+ }
@@ -19,15 +19,24 @@ const api_2 = require("../lib/api");
19
19
  async function resolveWorkspaceId(config, slug) {
20
20
  const configFile = await (0, config_1.loadConfig)();
21
21
  const targetSlug = slug ?? configFile.workspace;
22
- if (!targetSlug) {
23
- throw new errors_1.CliError('No workspace specified. Use --workspace <slug> or run `orch workspace use <slug>` first.');
24
- }
25
22
  const response = await (0, api_1.request)(config, 'GET', '/workspaces');
26
- const workspace = response.workspaces.find((w) => w.slug === targetSlug);
27
- if (!workspace) {
28
- throw new errors_1.CliError(`Workspace '${targetSlug}' not found.`);
23
+ if (targetSlug) {
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
+ // No workspace specified — auto-select if user has exactly one
31
+ if (response.workspaces.length === 0) {
32
+ throw new errors_1.CliError('No workspaces found. Create one with `orch workspace create <name>`.');
29
33
  }
30
- return workspace.id;
34
+ if (response.workspaces.length === 1) {
35
+ return response.workspaces[0].id;
36
+ }
37
+ const slugs = response.workspaces.map((w) => w.slug).join(', ');
38
+ throw new errors_1.CliError(`Multiple workspaces available: ${slugs}\n` +
39
+ 'Specify one with --workspace <slug> or run `orch workspace use <slug>`.');
31
40
  }
32
41
  function formatDate(iso) {
33
42
  if (!iso)
@@ -148,6 +157,8 @@ function registerScheduleCommand(program) {
148
157
  .option('--input <json>', 'Input data as JSON string')
149
158
  .option('--provider <provider>', 'LLM provider (anthropic, openai, gemini)')
150
159
  .option('--pin-version', 'Pin to this version (disable auto-update on publish)')
160
+ .option('--alert-webhook <url>', 'Webhook URL to POST on failure (HTTPS required)')
161
+ .option('--alert-on-failure-count <n>', 'Number of consecutive failures before alerting (default: 3)', parseInt)
151
162
  .option('--workspace <slug>', 'Workspace slug (default: current workspace)')
152
163
  .action(async (agentArg, options) => {
153
164
  const config = await (0, config_1.getResolvedConfig)();
@@ -162,8 +173,13 @@ function registerScheduleCommand(program) {
162
173
  }
163
174
  const workspaceId = await resolveWorkspaceId(config, options.workspace);
164
175
  const ref = (0, agent_ref_1.parseAgentRef)(agentArg);
176
+ const configFile = await (0, config_1.loadConfig)();
177
+ const org = ref.org ?? configFile.workspace ?? config.defaultOrg;
178
+ if (!org) {
179
+ throw new errors_1.CliError('Missing org. Use org/agent format or set default org.');
180
+ }
165
181
  // Resolve agent to get the ID (pass workspace context for private agents)
166
- const agent = await (0, api_2.getAgentWithFallback)(config, ref.org, ref.agent, ref.version, workspaceId);
182
+ const agent = await (0, api_2.getAgentWithFallback)(config, org, ref.agent, ref.version, workspaceId);
167
183
  // Parse input data
168
184
  let inputData;
169
185
  if (options.input) {
@@ -190,6 +206,10 @@ function registerScheduleCommand(program) {
190
206
  body.llm_provider = options.provider;
191
207
  if (options.pinVersion)
192
208
  body.auto_update = false;
209
+ if (options.alertWebhook)
210
+ body.alert_webhook_url = options.alertWebhook;
211
+ if (options.alertOnFailureCount)
212
+ body.alert_on_failure_count = options.alertOnFailureCount;
193
213
  const result = await (0, api_1.request)(config, 'POST', `/workspaces/${workspaceId}/schedules`, {
194
214
  body: JSON.stringify(body),
195
215
  headers: { 'Content-Type': 'application/json' },
@@ -210,13 +230,17 @@ function registerScheduleCommand(program) {
210
230
  }
211
231
  else {
212
232
  if (s.webhook_url) {
213
- process.stdout.write(`\n ${chalk_1.default.bold('Webhook URL')} (save this — secret shown once):\n`);
233
+ process.stdout.write(`\n ${chalk_1.default.bold('Webhook URL')} (save this — retrieve later with ${chalk_1.default.cyan('orch schedule info --reveal')}):\n`);
214
234
  process.stdout.write(` ${s.webhook_url}\n`);
215
235
  }
216
236
  }
217
237
  if (s.llm_provider) {
218
238
  process.stdout.write(` Provider: ${s.llm_provider}\n`);
219
239
  }
240
+ if (s.alert_webhook_url) {
241
+ process.stdout.write(` Alert URL: ${s.alert_webhook_url}\n`);
242
+ process.stdout.write(` Alert after: ${s.alert_on_failure_count ?? 3} consecutive failures\n`);
243
+ }
220
244
  process.stdout.write('\n');
221
245
  });
222
246
  // orch schedule update <schedule-id>
@@ -231,6 +255,9 @@ function registerScheduleCommand(program) {
231
255
  .option('--disable', 'Disable the schedule')
232
256
  .option('--auto-update', 'Enable auto-update on publish')
233
257
  .option('--pin-version', 'Pin to current version (disable auto-update)')
258
+ .option('--alert-webhook <url>', 'Webhook URL to POST on failure (HTTPS required)')
259
+ .option('--alert-on-failure-count <n>', 'Number of consecutive failures before alerting', parseInt)
260
+ .option('--clear-alert-webhook', 'Remove the alert webhook URL')
234
261
  .option('--workspace <slug>', 'Workspace slug (default: current workspace)')
235
262
  .action(async (scheduleId, options) => {
236
263
  const config = await (0, config_1.getResolvedConfig)();
@@ -243,6 +270,9 @@ function registerScheduleCommand(program) {
243
270
  if (options.autoUpdate && options.pinVersion) {
244
271
  throw new errors_1.CliError('Cannot use both --auto-update and --pin-version');
245
272
  }
273
+ if (options.alertWebhook && options.clearAlertWebhook) {
274
+ throw new errors_1.CliError('Cannot use both --alert-webhook and --clear-alert-webhook');
275
+ }
246
276
  const workspaceId = await resolveWorkspaceId(config, options.workspace);
247
277
  const updates = {};
248
278
  if (options.cron)
@@ -259,6 +289,12 @@ function registerScheduleCommand(program) {
259
289
  updates.auto_update = true;
260
290
  if (options.pinVersion)
261
291
  updates.auto_update = false;
292
+ if (options.alertWebhook)
293
+ updates.alert_webhook_url = options.alertWebhook;
294
+ if (options.alertOnFailureCount)
295
+ updates.alert_on_failure_count = options.alertOnFailureCount;
296
+ if (options.clearAlertWebhook)
297
+ updates.alert_webhook_url = '';
262
298
  if (options.input) {
263
299
  try {
264
300
  updates.input_data = JSON.parse(options.input);
@@ -285,6 +321,13 @@ function registerScheduleCommand(program) {
285
321
  if (s.next_run_at) {
286
322
  process.stdout.write(` Next: ${formatDate(s.next_run_at)}\n`);
287
323
  }
324
+ if (s.alert_webhook_url) {
325
+ process.stdout.write(` Alert: ${s.alert_webhook_url}\n`);
326
+ process.stdout.write(` After: ${s.alert_on_failure_count ?? 3} failures\n`);
327
+ }
328
+ else if (options.clearAlertWebhook) {
329
+ process.stdout.write(` Alert: ${chalk_1.default.gray('removed')}\n`);
330
+ }
288
331
  process.stdout.write('\n');
289
332
  });
290
333
  // orch schedule delete <schedule-id>
@@ -401,8 +444,11 @@ function registerScheduleCommand(program) {
401
444
  if (s.webhook_url) {
402
445
  process.stdout.write(` Webhook: ${s.webhook_url}\n`);
403
446
  }
404
- else if (s.schedule_type === 'webhook' && !options.reveal) {
405
- process.stdout.write(` Webhook: ${chalk_1.default.gray('(redacteduse --reveal to show)')}\n`);
447
+ else if (s.schedule_type === 'webhook' && options.reveal) {
448
+ process.stdout.write(` Webhook: ${chalk_1.default.red('Failed to reveal you may need owner permissions')}\n`);
449
+ }
450
+ else if (s.schedule_type === 'webhook') {
451
+ process.stdout.write(` Webhook: ${chalk_1.default.gray('(redacted — use --reveal to show, or regenerate-webhook if lost)')}\n`);
406
452
  }
407
453
  process.stdout.write(` Enabled: ${s.enabled ? chalk_1.default.green('yes') : chalk_1.default.red('no')}\n`);
408
454
  process.stdout.write(` Auto-update: ${s.auto_update === false ? chalk_1.default.yellow('pinned') : chalk_1.default.green('yes')}\n`);
@@ -498,4 +544,59 @@ function registerScheduleCommand(program) {
498
544
  process.stdout.write(`${table.toString()}\n`);
499
545
  process.stdout.write(chalk_1.default.gray(`\n${result.total} run${result.total !== 1 ? 's' : ''} total\n`));
500
546
  });
547
+ // orch schedule test-alert <schedule-id>
548
+ schedule
549
+ .command('test-alert <schedule-id>')
550
+ .description('Send a test alert to the schedule\'s configured webhook URL')
551
+ .option('--workspace <slug>', 'Workspace slug (default: current workspace)')
552
+ .action(async (partialScheduleId, options) => {
553
+ const config = await (0, config_1.getResolvedConfig)();
554
+ if (!config.apiKey) {
555
+ throw new errors_1.CliError('Missing API key. Run `orch login` first.');
556
+ }
557
+ const workspaceId = await resolveWorkspaceId(config, options.workspace);
558
+ const scheduleId = await resolveScheduleId(config, partialScheduleId, workspaceId);
559
+ process.stdout.write('Sending test alert...\n');
560
+ const result = await (0, api_1.request)(config, 'POST', `/workspaces/${workspaceId}/schedules/${scheduleId}/test-alert`);
561
+ if (result.success) {
562
+ process.stdout.write(chalk_1.default.green('\u2713') + ' Test alert delivered successfully\n');
563
+ }
564
+ else {
565
+ process.stdout.write(chalk_1.default.red('\u2717') + ' Test alert delivery failed\n');
566
+ }
567
+ });
568
+ // orch schedule regenerate-webhook <schedule-id>
569
+ schedule
570
+ .command('regenerate-webhook <schedule-id>')
571
+ .description('Regenerate the webhook secret (invalidates old URL)')
572
+ .option('--workspace <slug>', 'Workspace slug (default: current workspace)')
573
+ .option('-y, --yes', 'Skip confirmation prompt')
574
+ .action(async (partialScheduleId, options) => {
575
+ const config = await (0, config_1.getResolvedConfig)();
576
+ if (!config.apiKey) {
577
+ throw new errors_1.CliError('Missing API key. Run `orch login` first.');
578
+ }
579
+ const workspaceId = await resolveWorkspaceId(config, options.workspace);
580
+ const scheduleId = await resolveScheduleId(config, partialScheduleId, workspaceId);
581
+ if (!options.yes) {
582
+ const rl = promises_1.default.createInterface({
583
+ input: process.stdin,
584
+ output: process.stdout,
585
+ });
586
+ const answer = await rl.question(chalk_1.default.yellow('Warning: This will invalidate the current webhook URL.\n') +
587
+ 'Any integrations using the old URL will stop working.\n' +
588
+ 'Regenerate webhook secret? (y/N): ');
589
+ rl.close();
590
+ if (answer.trim().toLowerCase() !== 'y' && answer.trim().toLowerCase() !== 'yes') {
591
+ process.stdout.write('Cancelled.\n');
592
+ return;
593
+ }
594
+ }
595
+ const result = await (0, api_1.request)(config, 'POST', `/workspaces/${workspaceId}/schedules/${scheduleId}/regenerate-webhook`);
596
+ process.stdout.write(chalk_1.default.green('\u2713') + ' Webhook secret regenerated\n\n');
597
+ process.stdout.write(` ${chalk_1.default.bold('New Webhook URL')} (save this — retrieve later with ${chalk_1.default.cyan('orch schedule info --reveal')}):\n`);
598
+ process.stdout.write(` ${result.webhook_url}\n\n`);
599
+ process.stdout.write(chalk_1.default.yellow(' The old webhook URL no longer works.\n'));
600
+ process.stdout.write('\n');
601
+ });
501
602
  }
@@ -17,15 +17,24 @@ const SECRET_NAME_REGEX = /^[A-Z][A-Z0-9_]*$/;
17
17
  async function resolveWorkspaceId(config, slug) {
18
18
  const configFile = await (0, config_1.loadConfig)();
19
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
20
  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.`);
21
+ if (targetSlug) {
22
+ const workspace = response.workspaces.find((w) => w.slug === targetSlug);
23
+ if (!workspace) {
24
+ throw new errors_1.CliError(`Workspace '${targetSlug}' not found.`);
25
+ }
26
+ return workspace.id;
27
+ }
28
+ // No workspace specified — auto-select if user has exactly one
29
+ if (response.workspaces.length === 0) {
30
+ throw new errors_1.CliError('No workspaces found. Create one with `orch workspace create <name>`.');
31
+ }
32
+ if (response.workspaces.length === 1) {
33
+ return response.workspaces[0].id;
27
34
  }
28
- return workspace.id;
35
+ const slugs = response.workspaces.map((w) => w.slug).join(', ');
36
+ throw new errors_1.CliError(`Multiple workspaces available: ${slugs}\n` +
37
+ 'Specify one with --workspace <slug> or run `orch workspace use <slug>`.');
29
38
  }
30
39
  function formatDate(iso) {
31
40
  if (!iso)
@@ -17,15 +17,24 @@ const spinner_1 = require("../lib/spinner");
17
17
  async function resolveWorkspaceId(config, slug) {
18
18
  const configFile = await (0, config_1.loadConfig)();
19
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
20
  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.`);
21
+ if (targetSlug) {
22
+ const workspace = response.workspaces.find((w) => w.slug === targetSlug);
23
+ if (!workspace) {
24
+ throw new errors_1.CliError(`Workspace '${targetSlug}' not found.`);
25
+ }
26
+ return workspace.id;
27
+ }
28
+ // No workspace specified — auto-select if user has exactly one
29
+ if (response.workspaces.length === 0) {
30
+ throw new errors_1.CliError('No workspaces found. Create one with `orch workspace create <name>`.');
31
+ }
32
+ if (response.workspaces.length === 1) {
33
+ return response.workspaces[0].id;
27
34
  }
28
- return workspace.id;
35
+ const slugs = response.workspaces.map((w) => w.slug).join(', ');
36
+ throw new errors_1.CliError(`Multiple workspaces available: ${slugs}\n` +
37
+ 'Specify one with --workspace <slug> or run `orch workspace use <slug>`.');
29
38
  }
30
39
  function formatDate(iso) {
31
40
  if (!iso)
@@ -40,11 +40,13 @@ exports.registerSkillCommand = registerSkillCommand;
40
40
  const promises_1 = __importDefault(require("fs/promises"));
41
41
  const path_1 = __importDefault(require("path"));
42
42
  const os_1 = __importDefault(require("os"));
43
+ const chalk_1 = __importDefault(require("chalk"));
43
44
  const config_1 = require("../lib/config");
44
45
  const api_1 = require("../lib/api");
45
46
  const errors_1 = require("../lib/errors");
46
47
  const analytics_1 = require("../lib/analytics");
47
48
  const installed_1 = require("../lib/installed");
49
+ const agents_1 = require("./agents");
48
50
  const package_json_1 = __importDefault(require("../../package.json"));
49
51
  const DEFAULT_VERSION = 'latest';
50
52
  function stripFrontmatter(content) {
@@ -208,15 +210,89 @@ function registerSkillCommand(program) {
208
210
  // orch skill list
209
211
  skill
210
212
  .command('list')
211
- .description('Browse available skills')
213
+ .description('List your published and installed skills')
212
214
  .option('--json', 'Output raw JSON')
213
- .action(async () => {
214
- process.stdout.write('Install a skill:\n' +
215
- ' orch skill install <org>/<skill-name>\n\n' +
216
- 'View installed skills:\n' +
217
- ' orch update --check\n\n' +
218
- 'Learn more: https://docs.orchagent.io/skills\n');
219
- process.exit(0);
215
+ .action(async (options) => {
216
+ const config = await (0, config_1.getResolvedConfig)();
217
+ const jsonMode = options.json === true;
218
+ // Fetch published skills (only if authenticated)
219
+ let publishedSkills = [];
220
+ if (config.apiKey) {
221
+ const configFile = await (0, config_1.loadConfig)();
222
+ const orgSlug = configFile.workspace ?? config.defaultOrg;
223
+ const workspaceId = orgSlug ? await (0, api_1.resolveWorkspaceIdForOrg)(config, orgSlug) : undefined;
224
+ const allAgents = await (0, api_1.listMyAgents)(config, workspaceId);
225
+ publishedSkills = allAgents.filter(a => a.type === 'skill');
226
+ }
227
+ // Fetch locally installed skills
228
+ const installed = await (0, installed_1.getInstalled)();
229
+ // JSON output
230
+ if (jsonMode) {
231
+ const { agents: latestSkills } = (0, agents_1.latestOnly)(publishedSkills);
232
+ process.stdout.write(JSON.stringify({
233
+ published: latestSkills,
234
+ installed,
235
+ }, null, 2) + '\n');
236
+ return;
237
+ }
238
+ // Empty state
239
+ if (publishedSkills.length === 0 && installed.length === 0) {
240
+ process.stdout.write('No skills found.\n\n' +
241
+ 'Install a skill:\n' +
242
+ ' orch skill install <org>/<skill-name>\n\n' +
243
+ 'Create a skill:\n' +
244
+ ' orch skill create <name>\n');
245
+ return;
246
+ }
247
+ // Published skills table
248
+ if (publishedSkills.length > 0) {
249
+ const { agents: latestSkills, versionCounts } = (0, agents_1.latestOnly)(publishedSkills);
250
+ const Table = (await Promise.resolve().then(() => __importStar(require('cli-table3')))).default;
251
+ const table = new Table({
252
+ head: [
253
+ chalk_1.default.bold('Skill'),
254
+ chalk_1.default.bold('Version'),
255
+ chalk_1.default.bold('Description'),
256
+ ],
257
+ });
258
+ for (const skill of latestSkills) {
259
+ const desc = skill.description
260
+ ? skill.description.length > 60
261
+ ? skill.description.slice(0, 57) + '...'
262
+ : skill.description
263
+ : '-';
264
+ let version = skill.version;
265
+ const count = versionCounts.get(skill.name) ?? 1;
266
+ if (count > 1) {
267
+ version = `${skill.version} (${count} total)`;
268
+ }
269
+ table.push([skill.name, version, desc]);
270
+ }
271
+ process.stdout.write(`Published Skills\n${table.toString()}\n`);
272
+ process.stdout.write(`\n${latestSkills.length} skill${latestSkills.length === 1 ? '' : 's'}`);
273
+ if (publishedSkills.length > latestSkills.length) {
274
+ process.stdout.write(` (${publishedSkills.length} versions total)`);
275
+ }
276
+ process.stdout.write('\n');
277
+ }
278
+ // Installed skills table
279
+ if (installed.length > 0) {
280
+ const Table = (await Promise.resolve().then(() => __importStar(require('cli-table3')))).default;
281
+ const table = new Table({
282
+ head: [
283
+ chalk_1.default.bold('Skill'),
284
+ chalk_1.default.bold('Version'),
285
+ chalk_1.default.bold('Tool'),
286
+ chalk_1.default.bold('Scope'),
287
+ ],
288
+ });
289
+ for (const entry of installed) {
290
+ table.push([entry.agent, entry.version, entry.format, entry.scope]);
291
+ }
292
+ if (publishedSkills.length > 0)
293
+ process.stdout.write('\n');
294
+ process.stdout.write(`Installed Skills\n${table.toString()}\n`);
295
+ }
220
296
  });
221
297
  // orch skill create [name]
222
298
  skill