@orchagent/cli 0.3.90 → 0.3.91

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,160 @@
1
+ "use strict";
2
+ /**
3
+ * Interactive wizard for `orch init`.
4
+ *
5
+ * Runs when `orch init` is invoked without arguments in a TTY.
6
+ * Uses Node.js built-in readline/promises — no extra dependencies.
7
+ */
8
+ var __importDefault = (this && this.__importDefault) || function (mod) {
9
+ return (mod && mod.__esModule) ? mod : { "default": mod };
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.TEMPLATE_REGISTRY = void 0;
13
+ exports.runInitWizard = runInitWizard;
14
+ exports.printTemplateList = printTemplateList;
15
+ const promises_1 = __importDefault(require("readline/promises"));
16
+ const path_1 = __importDefault(require("path"));
17
+ exports.TEMPLATE_REGISTRY = [
18
+ { name: 'discord', description: 'Discord bot powered by Claude (Python)', type: 'agent', language: 'python', runMode: 'always_on' },
19
+ { name: 'discord-js', description: 'Discord bot powered by Claude (JavaScript)', type: 'agent', language: 'javascript', runMode: 'always_on' },
20
+ { name: 'support-agent', description: 'Multi-platform support agent (Discord/Telegram/Slack)', type: 'agent', language: 'python', runMode: 'always_on' },
21
+ { name: 'fan-out', description: 'Parallel orchestration — call agents concurrently', type: 'agent', language: 'both', runMode: 'on_demand' },
22
+ { name: 'pipeline', description: 'Sequential orchestration — chain agents in series', type: 'agent', language: 'both', runMode: 'on_demand' },
23
+ { name: 'map-reduce', description: 'Map-reduce orchestration — split, process, aggregate', type: 'agent', language: 'both', runMode: 'on_demand' },
24
+ { name: 'github-weekly-summary', description: 'GitHub activity analyser with Discord delivery', type: 'agent', language: 'python', runMode: 'on_demand' },
25
+ ];
26
+ // ---------------------------------------------------------------------------
27
+ // Prompt helpers (readline-based, no dependencies)
28
+ // ---------------------------------------------------------------------------
29
+ function createInterface() {
30
+ return promises_1.default.createInterface({
31
+ input: process.stdin,
32
+ output: process.stderr, // prompts on stderr so stdout stays clean for piping
33
+ });
34
+ }
35
+ async function promptText(rl, question, defaultValue) {
36
+ const suffix = defaultValue ? ` (${defaultValue})` : '';
37
+ const answer = await rl.question(` ${question}${suffix}: `);
38
+ return answer.trim() || defaultValue || '';
39
+ }
40
+ async function promptSelect(rl, question, options) {
41
+ process.stderr.write(`\n ${question}\n`);
42
+ for (let i = 0; i < options.length; i++) {
43
+ const opt = options[i];
44
+ process.stderr.write(` ${i + 1}) ${opt.label} — ${opt.description}\n`);
45
+ }
46
+ process.stderr.write('\n');
47
+ while (true) {
48
+ const answer = await rl.question(` Choice [1-${options.length}]: `);
49
+ const num = parseInt(answer.trim(), 10);
50
+ if (num >= 1 && num <= options.length) {
51
+ return options[num - 1].value;
52
+ }
53
+ process.stderr.write(` Please enter a number between 1 and ${options.length}.\n`);
54
+ }
55
+ }
56
+ // ---------------------------------------------------------------------------
57
+ // Wizard
58
+ // ---------------------------------------------------------------------------
59
+ async function runInitWizard() {
60
+ const rl = createInterface();
61
+ try {
62
+ process.stderr.write('\n orch init — interactive setup\n');
63
+ process.stderr.write(' ─────────────────────────────\n\n');
64
+ // 1. Agent name
65
+ const dirName = path_1.default.basename(process.cwd());
66
+ const name = await promptText(rl, 'Agent name', dirName);
67
+ const createSubdir = name !== dirName;
68
+ // 2. Agent type
69
+ const type = await promptSelect(rl, 'What type of agent?', [
70
+ { value: 'prompt', label: 'prompt', description: 'Single LLM call with structured I/O' },
71
+ { value: 'tool', label: 'tool', description: 'Your own code (API calls, file processing)' },
72
+ { value: 'agent', label: 'agent', description: 'Multi-step LLM reasoning with tool use' },
73
+ { value: 'skill', label: 'skill', description: 'Knowledge module for other agents' },
74
+ ]);
75
+ // Skill and prompt don't need language/template
76
+ if (type === 'skill' || type === 'prompt') {
77
+ rl.close();
78
+ return {
79
+ name: createSubdir ? name : undefined,
80
+ type,
81
+ language: 'python',
82
+ template: undefined,
83
+ runMode: 'on_demand',
84
+ orchestrator: false,
85
+ loop: false,
86
+ };
87
+ }
88
+ // 3. Language (tool/agent only)
89
+ const language = await promptSelect(rl, 'Language?', [
90
+ { value: 'python', label: 'Python', description: 'Recommended — broadest library support' },
91
+ { value: 'javascript', label: 'JavaScript', description: 'Node.js runtime' },
92
+ ]);
93
+ // 4. Template (optional)
94
+ const applicableTemplates = exports.TEMPLATE_REGISTRY.filter(t => {
95
+ if (t.language !== 'both' && t.language !== language)
96
+ return false;
97
+ return true;
98
+ });
99
+ const templateOptions = [
100
+ { value: 'none', label: 'No template', description: 'Start from scratch' },
101
+ ...applicableTemplates.map(t => ({
102
+ value: t.name,
103
+ label: t.name,
104
+ description: t.description,
105
+ })),
106
+ ];
107
+ const template = await promptSelect(rl, 'Start from a template?', templateOptions);
108
+ // 5. Run mode (only if no template selected — templates set their own)
109
+ let runMode = 'on_demand';
110
+ if (template === 'none') {
111
+ runMode = await promptSelect(rl, 'Run mode?', [
112
+ { value: 'on_demand', label: 'on_demand', description: 'Run per invocation (default)' },
113
+ { value: 'always_on', label: 'always_on', description: 'Long-lived HTTP service' },
114
+ ]);
115
+ }
116
+ // 6. Agent subtype (only for agent type, no template)
117
+ let orchestrator = false;
118
+ let loop = false;
119
+ if (type === 'agent' && template === 'none') {
120
+ const agentSubtype = await promptSelect(rl, 'Agent execution mode?', [
121
+ { value: 'code', label: 'Code runtime', description: 'You write the logic (call any LLM provider)' },
122
+ { value: 'orchestrator', label: 'Orchestrator', description: 'Coordinate other agents via SDK' },
123
+ ...(language === 'python' ? [{ value: 'loop', label: 'Managed loop', description: 'Platform-managed LLM loop with tool use' }] : []),
124
+ ]);
125
+ orchestrator = agentSubtype === 'orchestrator';
126
+ loop = agentSubtype === 'loop';
127
+ }
128
+ rl.close();
129
+ return {
130
+ name: createSubdir ? name : undefined,
131
+ type,
132
+ language,
133
+ template: template === 'none' ? undefined : template,
134
+ runMode,
135
+ orchestrator,
136
+ loop,
137
+ };
138
+ }
139
+ catch (err) {
140
+ rl.close();
141
+ throw err;
142
+ }
143
+ }
144
+ // ---------------------------------------------------------------------------
145
+ // List templates (for --list-templates flag)
146
+ // ---------------------------------------------------------------------------
147
+ function printTemplateList() {
148
+ process.stdout.write('\nAvailable templates:\n\n');
149
+ const nameWidth = Math.max(...exports.TEMPLATE_REGISTRY.map(t => t.name.length)) + 2;
150
+ const langWidth = 12;
151
+ process.stdout.write(` ${'TEMPLATE'.padEnd(nameWidth)}${'LANGUAGE'.padEnd(langWidth)}${'RUN MODE'.padEnd(12)}DESCRIPTION\n`);
152
+ process.stdout.write(` ${'─'.repeat(nameWidth)}${'─'.repeat(langWidth)}${'─'.repeat(12)}${'─'.repeat(40)}\n`);
153
+ for (const t of exports.TEMPLATE_REGISTRY) {
154
+ const lang = t.language === 'both' ? 'py / js' : t.language === 'python' ? 'python' : 'javascript';
155
+ process.stdout.write(` ${t.name.padEnd(nameWidth)}${lang.padEnd(langWidth)}${t.runMode.padEnd(12)}${t.description}\n`);
156
+ }
157
+ process.stdout.write('\nUsage:\n');
158
+ process.stdout.write(' orch init my-agent --template <name>\n');
159
+ process.stdout.write(' orch init my-agent --template <name> --language javascript\n\n');
160
+ }
@@ -4,9 +4,11 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.registerInitCommand = registerInitCommand;
7
+ const commander_1 = require("commander");
7
8
  const promises_1 = __importDefault(require("fs/promises"));
8
9
  const path_1 = __importDefault(require("path"));
9
10
  const errors_1 = require("../lib/errors");
11
+ const init_wizard_1 = require("./init-wizard");
10
12
  const github_weekly_summary_1 = require("./templates/github-weekly-summary");
11
13
  const support_agent_1 = require("./templates/support-agent");
12
14
  const MANIFEST_TEMPLATE = `{
@@ -1498,15 +1500,49 @@ function resolveInitFlavor(typeOption) {
1498
1500
  function registerInitCommand(program) {
1499
1501
  program
1500
1502
  .command('init')
1501
- .description('Initialize a new agent project')
1503
+ .description('Initialize a new agent project (interactive wizard when called without arguments)')
1502
1504
  .argument('[name]', 'Agent name (default: current directory name)')
1503
1505
  .option('--type <type>', 'Type: prompt, tool, agent, or skill (legacy aliases: agentic, code)', 'prompt')
1504
1506
  .option('--orchestrator', 'Create an orchestrator agent with dependency scaffolding and SDK boilerplate')
1505
1507
  .option('--run-mode <mode>', 'Run mode for agents: on_demand or always_on', 'on_demand')
1506
1508
  .option('--language <lang>', 'Language: python or javascript (default: python)', 'python')
1507
1509
  .option('--loop', 'Use platform-managed LLM loop instead of code runtime (requires --type agent)')
1508
- .option('--template <name>', 'Start from a template (available: fan-out, pipeline, map-reduce, support-agent, discord, discord-js, github-weekly-summary)')
1510
+ .option('--template <name>', 'Start from a template (use --list-templates to see options)')
1511
+ .option('--list-templates', 'Show available templates with descriptions')
1509
1512
  .action(async (name, options) => {
1513
+ // --list-templates: print and exit
1514
+ if (options.listTemplates) {
1515
+ (0, init_wizard_1.printTemplateList)();
1516
+ return;
1517
+ }
1518
+ // Interactive wizard: no name, TTY, and no explicit flags that indicate non-interactive intent
1519
+ const rawArgs = process.argv.slice(2);
1520
+ const initArgIndex = rawArgs.indexOf('init');
1521
+ const argsAfterInit = initArgIndex >= 0 ? rawArgs.slice(initArgIndex + 1) : [];
1522
+ const hasExplicitFlags = argsAfterInit.some(a => a.startsWith('--'));
1523
+ const hasNameArg = name !== undefined;
1524
+ if (!hasNameArg && !hasExplicitFlags && process.stdin.isTTY) {
1525
+ const wizard = await (0, init_wizard_1.runInitWizard)();
1526
+ // Re-invoke the action with wizard results by constructing args
1527
+ const wizardArgs = ['node', 'orch', 'init'];
1528
+ if (wizard.name)
1529
+ wizardArgs.push(wizard.name);
1530
+ wizardArgs.push('--type', wizard.type);
1531
+ wizardArgs.push('--language', wizard.language);
1532
+ wizardArgs.push('--run-mode', wizard.runMode);
1533
+ if (wizard.template)
1534
+ wizardArgs.push('--template', wizard.template);
1535
+ if (wizard.orchestrator)
1536
+ wizardArgs.push('--orchestrator');
1537
+ if (wizard.loop)
1538
+ wizardArgs.push('--loop');
1539
+ // Create a fresh program to run with wizard args
1540
+ const wizardProgram = new commander_1.Command();
1541
+ wizardProgram.exitOverride();
1542
+ registerInitCommand(wizardProgram);
1543
+ await wizardProgram.parseAsync(wizardArgs);
1544
+ return;
1545
+ }
1510
1546
  const cwd = process.cwd();
1511
1547
  let runMode = (options.runMode || 'on_demand').trim().toLowerCase();
1512
1548
  if (!['on_demand', 'always_on'].includes(runMode)) {
@@ -56,6 +56,7 @@ async function keyBasedLogin(apiKey) {
56
56
  const resolved = await (0, config_1.getResolvedConfig)({ api_key: apiKey });
57
57
  const org = await (0, api_1.getOrg)(resolved);
58
58
  const existing = await (0, config_1.loadConfig)();
59
+ const isFirstLogin = !existing.api_key;
59
60
  const nextConfig = {
60
61
  ...existing,
61
62
  api_key: apiKey,
@@ -67,12 +68,16 @@ async function keyBasedLogin(apiKey) {
67
68
  await (0, config_1.saveConfig)(nextConfig);
68
69
  await (0, analytics_1.track)('cli_login', { method: 'key' });
69
70
  process.stdout.write(`✓ Logged in to ${org.slug}\n`);
71
+ if (isFirstLogin) {
72
+ process.stdout.write('\n Tip: Run `orch doctor` to verify your setup.\n\n');
73
+ }
70
74
  }
71
75
  /**
72
76
  * Login via browser OAuth flow.
73
77
  */
74
78
  async function browserBasedLogin(port) {
75
79
  const existing = await (0, config_1.loadConfig)();
80
+ const isFirstLogin = !existing.api_key;
76
81
  const resolved = await (0, config_1.getResolvedConfig)();
77
82
  process.stdout.write('Opening browser for authentication...\n');
78
83
  try {
@@ -88,6 +93,9 @@ async function browserBasedLogin(port) {
88
93
  await (0, config_1.saveConfig)(nextConfig);
89
94
  await (0, analytics_1.track)('cli_login', { method: 'browser' });
90
95
  process.stdout.write(`\n✓ Logged in to ${result.orgSlug}\n`);
96
+ if (isFirstLogin) {
97
+ process.stdout.write('\n Tip: Run `orch doctor` to verify your setup.\n\n');
98
+ }
91
99
  }
92
100
  catch (err) {
93
101
  if (err instanceof errors_1.CliError) {
@@ -16,15 +16,25 @@ const output_1 = require("../lib/output");
16
16
  async function resolveWorkspaceId(config, slug) {
17
17
  const configFile = await (0, config_1.loadConfig)();
18
18
  const targetSlug = slug ?? configFile.workspace;
19
- if (!targetSlug) {
20
- throw new errors_1.CliError('No workspace specified. Use --workspace <slug> or run `orch workspace use <slug>` first.');
21
- }
22
19
  const response = await (0, api_1.request)(config, 'GET', '/workspaces');
23
- const workspace = response.workspaces.find((w) => w.slug === targetSlug);
24
- if (!workspace) {
25
- throw new errors_1.CliError(`Workspace '${targetSlug}' not found.`);
20
+ if (targetSlug) {
21
+ const workspace = response.workspaces.find((w) => w.slug === targetSlug);
22
+ if (!workspace) {
23
+ throw new errors_1.CliError(`Workspace '${targetSlug}' not found.`);
24
+ }
25
+ return workspace.id;
26
+ }
27
+ // No workspace specified — auto-select if user has exactly one
28
+ if (response.workspaces.length === 0) {
29
+ throw new errors_1.CliError('No workspaces found. Create one with `orch workspace create <name>`.');
30
+ }
31
+ if (response.workspaces.length === 1) {
32
+ return response.workspaces[0].id;
26
33
  }
27
- return workspace.id;
34
+ // Multiple workspaces — list them and ask the user to pick
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>`.');
28
38
  }
29
39
  function formatDate(iso) {
30
40
  if (!iso)
@@ -15,15 +15,24 @@ const output_1 = require("../lib/output");
15
15
  async function resolveWorkspaceId(config, slug) {
16
16
  const configFile = await (0, config_1.loadConfig)();
17
17
  const targetSlug = slug ?? configFile.workspace;
18
- if (!targetSlug) {
19
- throw new errors_1.CliError('No workspace specified. Use --workspace <slug> or run `orch workspace use <slug>` first.');
20
- }
21
18
  const response = await (0, api_1.request)(config, 'GET', '/workspaces');
22
- const workspace = response.workspaces.find((w) => w.slug === targetSlug);
23
- if (!workspace) {
24
- throw new errors_1.CliError(`Workspace '${targetSlug}' not found.`);
19
+ if (targetSlug) {
20
+ const workspace = response.workspaces.find((w) => w.slug === targetSlug);
21
+ if (!workspace) {
22
+ throw new errors_1.CliError(`Workspace '${targetSlug}' not found.`);
23
+ }
24
+ return workspace.id;
25
+ }
26
+ // No workspace specified — auto-select if user has exactly one
27
+ if (response.workspaces.length === 0) {
28
+ throw new errors_1.CliError('No workspaces found. Create one with `orch workspace create <name>`.');
29
+ }
30
+ if (response.workspaces.length === 1) {
31
+ return response.workspaces[0].id;
25
32
  }
26
- return workspace.id;
33
+ const slugs = response.workspaces.map((w) => w.slug).join(', ');
34
+ throw new errors_1.CliError(`Multiple workspaces available: ${slugs}\n` +
35
+ 'Specify one with --workspace <slug> or run `orch workspace use <slug>`.');
27
36
  }
28
37
  function formatDuration(ms) {
29
38
  if (ms === 0)
@@ -15,15 +15,24 @@ const output_1 = require("../lib/output");
15
15
  async function resolveWorkspaceId(config, slug) {
16
16
  const configFile = await (0, config_1.loadConfig)();
17
17
  const targetSlug = slug ?? configFile.workspace;
18
- if (!targetSlug) {
19
- throw new errors_1.CliError('No workspace specified. Use --workspace <slug> or run `orch workspace use <slug>` first.');
20
- }
21
18
  const response = await (0, api_1.request)(config, 'GET', '/workspaces');
22
- const workspace = response.workspaces.find((w) => w.slug === targetSlug);
23
- if (!workspace) {
24
- throw new errors_1.CliError(`Workspace '${targetSlug}' not found.`);
19
+ if (targetSlug) {
20
+ const workspace = response.workspaces.find((w) => w.slug === targetSlug);
21
+ if (!workspace) {
22
+ throw new errors_1.CliError(`Workspace '${targetSlug}' not found.`);
23
+ }
24
+ return workspace.id;
25
+ }
26
+ // No workspace specified — auto-select if user has exactly one
27
+ if (response.workspaces.length === 0) {
28
+ throw new errors_1.CliError('No workspaces found. Create one with `orch workspace create <name>`.');
29
+ }
30
+ if (response.workspaces.length === 1) {
31
+ return response.workspaces[0].id;
25
32
  }
26
- return workspace.id;
33
+ const slugs = response.workspaces.map((w) => w.slug).join(', ');
34
+ throw new errors_1.CliError(`Multiple workspaces available: ${slugs}\n` +
35
+ 'Specify one with --workspace <slug> or run `orch workspace use <slug>`.');
27
36
  }
28
37
  function isUuid(value) {
29
38
  return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value);
@@ -38,6 +38,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
39
  exports.localCommandForEntrypoint = localCommandForEntrypoint;
40
40
  exports.validateInputSchema = validateInputSchema;
41
+ exports.tryParseJsonObject = tryParseJsonObject;
41
42
  exports.isKeyedFileArg = isKeyedFileArg;
42
43
  exports.readKeyedFiles = readKeyedFiles;
43
44
  exports.mountDirectory = mountDirectory;
@@ -264,6 +265,26 @@ async function readStdin() {
264
265
  return null;
265
266
  return Buffer.concat(chunks);
266
267
  }
268
+ /**
269
+ * Try to parse a Buffer as a JSON object (not array).
270
+ * Returns the parsed object on success, null on failure or if the content
271
+ * is not a JSON object (e.g. array, string, number).
272
+ */
273
+ function tryParseJsonObject(buf) {
274
+ const text = buf.toString('utf8').trim();
275
+ if (!text.startsWith('{'))
276
+ return null;
277
+ try {
278
+ const parsed = JSON.parse(text);
279
+ if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
280
+ return parsed;
281
+ }
282
+ return null;
283
+ }
284
+ catch {
285
+ return null;
286
+ }
287
+ }
267
288
  async function buildMultipartBody(filePaths, metadata) {
268
289
  if (!filePaths || filePaths.length === 0) {
269
290
  const stdinData = await readStdin();
@@ -2127,13 +2148,39 @@ async function executeCloud(agentRef, file, options) {
2127
2148
  sourceLabel = multipart.sourceLabel;
2128
2149
  }
2129
2150
  else if (llmCredentials) {
2130
- body = JSON.stringify({ llm_credentials: llmCredentials });
2151
+ // Check for piped JSON stdin to merge with credentials
2152
+ const stdinData = await readStdin();
2153
+ const stdinJson = stdinData ? tryParseJsonObject(stdinData) : null;
2154
+ if (stdinJson) {
2155
+ stdinJson.llm_credentials = llmCredentials;
2156
+ warnInputSchemaErrors(stdinJson, agentMeta.input_schema);
2157
+ body = JSON.stringify(stdinJson);
2158
+ sourceLabel = 'stdin';
2159
+ }
2160
+ else {
2161
+ body = JSON.stringify({ llm_credentials: llmCredentials });
2162
+ }
2131
2163
  headers['Content-Type'] = 'application/json';
2132
2164
  }
2133
2165
  else {
2134
- const multipart = await buildMultipartBody(undefined, options.metadata);
2135
- body = multipart.body;
2136
- sourceLabel = multipart.sourceLabel;
2166
+ // No --data, no --file, no --metadata — check for piped JSON stdin
2167
+ const stdinData = await readStdin();
2168
+ if (stdinData) {
2169
+ const stdinJson = tryParseJsonObject(stdinData);
2170
+ if (stdinJson) {
2171
+ warnInputSchemaErrors(stdinJson, agentMeta.input_schema);
2172
+ body = JSON.stringify(stdinJson);
2173
+ headers['Content-Type'] = 'application/json';
2174
+ sourceLabel = 'stdin';
2175
+ }
2176
+ else {
2177
+ // Non-JSON stdin — send as binary file attachment
2178
+ const form = new FormData();
2179
+ form.append('files[]', new Blob([new Uint8Array(stdinData)]), 'stdin');
2180
+ body = form;
2181
+ sourceLabel = 'stdin';
2182
+ }
2183
+ }
2137
2184
  }
2138
2185
  } // end of non-injection path
2139
2186
  const verboseQs = options.verbose ? '?verbose=true' : '';
@@ -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' },
@@ -217,6 +237,10 @@ function registerScheduleCommand(program) {
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>
@@ -498,4 +541,25 @@ function registerScheduleCommand(program) {
498
541
  process.stdout.write(`${table.toString()}\n`);
499
542
  process.stdout.write(chalk_1.default.gray(`\n${result.total} run${result.total !== 1 ? 's' : ''} total\n`));
500
543
  });
544
+ // orch schedule test-alert <schedule-id>
545
+ schedule
546
+ .command('test-alert <schedule-id>')
547
+ .description('Send a test alert to the schedule\'s configured webhook URL')
548
+ .option('--workspace <slug>', 'Workspace slug (default: current workspace)')
549
+ .action(async (partialScheduleId, options) => {
550
+ const config = await (0, config_1.getResolvedConfig)();
551
+ if (!config.apiKey) {
552
+ throw new errors_1.CliError('Missing API key. Run `orch login` first.');
553
+ }
554
+ const workspaceId = await resolveWorkspaceId(config, options.workspace);
555
+ const scheduleId = await resolveScheduleId(config, partialScheduleId, workspaceId);
556
+ process.stdout.write('Sending test alert...\n');
557
+ const result = await (0, api_1.request)(config, 'POST', `/workspaces/${workspaceId}/schedules/${scheduleId}/test-alert`);
558
+ if (result.success) {
559
+ process.stdout.write(chalk_1.default.green('\u2713') + ' Test alert delivered successfully\n');
560
+ }
561
+ else {
562
+ process.stdout.write(chalk_1.default.red('\u2717') + ' Test alert delivery failed\n');
563
+ }
564
+ });
501
565
  }
@@ -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)