@orchagent/cli 0.3.91 → 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.
@@ -4,6 +4,9 @@
4
4
  *
5
5
  * Runs when `orch init` is invoked without arguments in a TTY.
6
6
  * Uses Node.js built-in readline/promises — no extra dependencies.
7
+ *
8
+ * The wizard leads with "What do you want to build?" to directly address
9
+ * the most common new-user friction — not knowing which type/flavor to pick.
7
10
  */
8
11
  var __importDefault = (this && this.__importDefault) || function (mod) {
9
12
  return (mod && mod.__esModule) ? mod : { "default": mod };
@@ -15,6 +18,7 @@ exports.printTemplateList = printTemplateList;
15
18
  const promises_1 = __importDefault(require("readline/promises"));
16
19
  const path_1 = __importDefault(require("path"));
17
20
  exports.TEMPLATE_REGISTRY = [
21
+ { name: 'cron-job', description: 'Scheduled task — daily reports, syncs, cleanups', type: 'tool', language: 'both', runMode: 'on_demand' },
18
22
  { name: 'discord', description: 'Discord bot powered by Claude (Python)', type: 'agent', language: 'python', runMode: 'always_on' },
19
23
  { name: 'discord-js', description: 'Discord bot powered by Claude (JavaScript)', type: 'agent', language: 'javascript', runMode: 'always_on' },
20
24
  { name: 'support-agent', description: 'Multi-platform support agent (Discord/Telegram/Slack)', type: 'agent', language: 'python', runMode: 'always_on' },
@@ -54,6 +58,77 @@ async function promptSelect(rl, question, options) {
54
58
  }
55
59
  }
56
60
  // ---------------------------------------------------------------------------
61
+ // Language follow-up (shared by several use cases)
62
+ // ---------------------------------------------------------------------------
63
+ async function promptLanguage(rl) {
64
+ return promptSelect(rl, 'Language?', [
65
+ { value: 'python', label: 'Python', description: 'Recommended — broadest library support' },
66
+ { value: 'javascript', label: 'JavaScript', description: 'Node.js runtime' },
67
+ ]);
68
+ }
69
+ // ---------------------------------------------------------------------------
70
+ // "More templates" sub-flow
71
+ // ---------------------------------------------------------------------------
72
+ async function handleMoreTemplates(rl, nameForResult) {
73
+ const language = await promptLanguage(rl);
74
+ const applicableTemplates = exports.TEMPLATE_REGISTRY.filter(t => t.language === 'both' || t.language === language);
75
+ const templateOptions = [
76
+ { value: 'none', label: 'No template', description: 'Start from scratch (choose type manually)' },
77
+ ...applicableTemplates.map(t => ({
78
+ value: t.name,
79
+ label: t.name,
80
+ description: t.description,
81
+ })),
82
+ ];
83
+ const template = await promptSelect(rl, 'Pick a template:', templateOptions);
84
+ if (template !== 'none') {
85
+ const info = exports.TEMPLATE_REGISTRY.find(t => t.name === template);
86
+ rl.close();
87
+ return {
88
+ name: nameForResult,
89
+ type: info.type,
90
+ language,
91
+ template,
92
+ runMode: info.runMode,
93
+ orchestrator: false,
94
+ loop: false,
95
+ };
96
+ }
97
+ // No template — fall back to manual type selection
98
+ const type = await promptSelect(rl, 'Agent type?', [
99
+ { value: 'prompt', label: 'prompt', description: 'Single LLM call with structured I/O' },
100
+ { value: 'tool', label: 'tool', description: 'Your own code (API calls, file processing)' },
101
+ { value: 'agent', label: 'agent', description: 'Multi-step LLM reasoning with tool use' },
102
+ ]);
103
+ let runMode = 'on_demand';
104
+ let orchestrator = false;
105
+ let loop = false;
106
+ if (type === 'tool') {
107
+ runMode = await promptSelect(rl, 'Run mode?', [
108
+ { value: 'on_demand', label: 'on_demand', description: 'Run per invocation (default)' },
109
+ { value: 'always_on', label: 'always_on', description: 'Long-lived HTTP service' },
110
+ ]);
111
+ }
112
+ if (type === 'agent') {
113
+ runMode = await promptSelect(rl, 'Run mode?', [
114
+ { value: 'on_demand', label: 'on_demand', description: 'Run per invocation (default)' },
115
+ { value: 'always_on', label: 'always_on', description: 'Long-lived HTTP service' },
116
+ ]);
117
+ const subtypeOptions = [
118
+ { value: 'code', label: 'Code runtime', description: 'You write the logic (call any LLM provider)' },
119
+ { value: 'orchestrator', label: 'Orchestrator', description: 'Coordinate other agents via SDK' },
120
+ ];
121
+ if (language === 'python') {
122
+ subtypeOptions.push({ value: 'loop', label: 'Managed loop', description: 'Platform-managed LLM loop with tool use' });
123
+ }
124
+ const agentSubtype = await promptSelect(rl, 'Agent execution mode?', subtypeOptions);
125
+ orchestrator = agentSubtype === 'orchestrator';
126
+ loop = agentSubtype === 'loop';
127
+ }
128
+ rl.close();
129
+ return { name: nameForResult, type, language, template: undefined, runMode, orchestrator, loop };
130
+ }
131
+ // ---------------------------------------------------------------------------
57
132
  // Wizard
58
133
  // ---------------------------------------------------------------------------
59
134
  async function runInitWizard() {
@@ -64,77 +139,67 @@ async function runInitWizard() {
64
139
  // 1. Agent name
65
140
  const dirName = path_1.default.basename(process.cwd());
66
141
  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' },
142
+ const nameForResult = name !== dirName ? name : undefined;
143
+ // 2. What do you want to build? (use-case-driven — replaces type/template questions)
144
+ const useCase = await promptSelect(rl, 'What do you want to build?', [
145
+ { value: 'prompt', label: 'Prompt agent', description: 'Single LLM call with structured I/O (simplest)' },
146
+ { value: 'tool-py', label: 'Tool (Python)', description: 'Your code processes data (stdin/stdout JSON)' },
147
+ { value: 'tool-js', label: 'Tool (JavaScript)', description: 'Your code processes data (Node.js)' },
148
+ { value: 'cron-job', label: 'Scheduled job', description: 'Runs on a cron schedule (reports, syncs, cleanups)' },
149
+ { value: 'discord-bot', label: 'Discord bot', description: 'Always-on chatbot in Discord channels' },
150
+ { value: 'orchestrator', label: 'Orchestrator', description: 'Coordinate multiple agents via SDK' },
151
+ { value: 'agent-loop', label: 'AI agent (LLM loop)', description: 'Multi-step reasoning with tool use' },
152
+ { value: 'skill', label: 'Knowledge skill', description: 'Reusable knowledge module for other agents' },
153
+ { value: 'more', label: 'More templates...', description: 'Fan-out, pipeline, map-reduce, support agent, etc.' },
74
154
  ]);
75
- // Skill and prompt don't need language/template
76
- if (type === 'skill' || type === 'prompt') {
155
+ // --- Direct-resolution use cases (no follow-up needed) ---
156
+ if (useCase === 'prompt') {
77
157
  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
- };
158
+ return { name: nameForResult, type: 'prompt', language: 'python', template: undefined, runMode: 'on_demand', orchestrator: false, loop: false };
87
159
  }
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
- ]);
160
+ if (useCase === 'skill') {
161
+ rl.close();
162
+ return { name: nameForResult, type: 'skill', language: 'python', template: undefined, runMode: 'on_demand', orchestrator: false, loop: false };
163
+ }
164
+ if (useCase === 'tool-py') {
165
+ rl.close();
166
+ return { name: nameForResult, type: 'tool', language: 'python', template: undefined, runMode: 'on_demand', orchestrator: false, loop: false };
167
+ }
168
+ if (useCase === 'tool-js') {
169
+ rl.close();
170
+ return { name: nameForResult, type: 'tool', language: 'javascript', template: undefined, runMode: 'on_demand', orchestrator: false, loop: false };
171
+ }
172
+ if (useCase === 'agent-loop') {
173
+ rl.close();
174
+ return { name: nameForResult, type: 'agent', language: 'python', template: undefined, runMode: 'on_demand', orchestrator: false, loop: true };
115
175
  }
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' }] : []),
176
+ // --- Use cases that need a language follow-up ---
177
+ if (useCase === 'cron-job') {
178
+ const lang = await promptLanguage(rl);
179
+ rl.close();
180
+ return { name: nameForResult, type: 'tool', language: lang, template: 'cron-job', runMode: 'on_demand', orchestrator: false, loop: false };
181
+ }
182
+ if (useCase === 'discord-bot') {
183
+ const lang = await promptSelect(rl, 'Language?', [
184
+ { value: 'python', label: 'Python', description: 'Recommended — discord.py + anthropic' },
185
+ { value: 'javascript', label: 'JavaScript', description: 'discord.js + @anthropic-ai/sdk' },
124
186
  ]);
125
- orchestrator = agentSubtype === 'orchestrator';
126
- loop = agentSubtype === 'loop';
187
+ const template = lang === 'javascript' ? 'discord-js' : 'discord';
188
+ rl.close();
189
+ return { name: nameForResult, type: 'agent', language: lang, template, runMode: 'always_on', orchestrator: false, loop: false };
190
+ }
191
+ if (useCase === 'orchestrator') {
192
+ const lang = await promptLanguage(rl);
193
+ rl.close();
194
+ return { name: nameForResult, type: 'agent', language: lang, template: undefined, runMode: 'on_demand', orchestrator: true, loop: false };
127
195
  }
196
+ // --- "More templates..." sub-flow ---
197
+ if (useCase === 'more') {
198
+ return handleMoreTemplates(rl, nameForResult);
199
+ }
200
+ // Shouldn't reach here, but handle gracefully
128
201
  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
- };
202
+ return { name: nameForResult, type: 'prompt', language: 'python', template: undefined, runMode: 'on_demand', orchestrator: false, loop: false };
138
203
  }
139
204
  catch (err) {
140
205
  rl.close();
@@ -10,6 +10,7 @@ const path_1 = __importDefault(require("path"));
10
10
  const errors_1 = require("../lib/errors");
11
11
  const init_wizard_1 = require("./init-wizard");
12
12
  const github_weekly_summary_1 = require("./templates/github-weekly-summary");
13
+ const cron_job_1 = require("./templates/cron-job");
13
14
  const support_agent_1 = require("./templates/support-agent");
14
15
  const MANIFEST_TEMPLATE = `{
15
16
  "name": "my-agent",
@@ -1566,7 +1567,7 @@ function registerInitCommand(program) {
1566
1567
  }
1567
1568
  if (options.template) {
1568
1569
  const template = options.template.trim().toLowerCase();
1569
- const validTemplates = ['fan-out', 'pipeline', 'map-reduce', 'support-agent', 'discord', 'discord-js', 'github-weekly-summary'];
1570
+ const validTemplates = ['fan-out', 'pipeline', 'map-reduce', 'support-agent', 'discord', 'discord-js', 'github-weekly-summary', 'cron-job'];
1570
1571
  if (!validTemplates.includes(template)) {
1571
1572
  throw new errors_1.CliError(`Unknown --template '${template}'. Available templates: ${validTemplates.join(', ')}`);
1572
1573
  }
@@ -1600,6 +1601,9 @@ function registerInitCommand(program) {
1600
1601
  else if (template === 'github-weekly-summary') {
1601
1602
  initMode = { type: 'agent', flavor: 'github_weekly_summary' };
1602
1603
  }
1604
+ else if (template === 'cron-job') {
1605
+ initMode = { type: 'tool', flavor: 'cron_job' };
1606
+ }
1603
1607
  }
1604
1608
  // Validate --language option
1605
1609
  const language = (options.language || 'python').trim().toLowerCase();
@@ -1949,6 +1953,72 @@ function registerInitCommand(program) {
1949
1953
  process.stdout.write(AGENT_BUILDER_HINT);
1950
1954
  return;
1951
1955
  }
1956
+ // Handle cron-job template
1957
+ if (initMode.flavor === 'cron_job') {
1958
+ const manifestPath = path_1.default.join(targetDir, 'orchagent.json');
1959
+ try {
1960
+ await promises_1.default.access(manifestPath);
1961
+ throw new errors_1.CliError(`Already initialized (orchagent.json exists in ${name ? name + '/' : 'current directory'})`);
1962
+ }
1963
+ catch (err) {
1964
+ if (err.code !== 'ENOENT') {
1965
+ throw err;
1966
+ }
1967
+ }
1968
+ const manifest = {
1969
+ name: agentName,
1970
+ type: 'tool',
1971
+ description: 'A scheduled job that runs on a cron schedule',
1972
+ run_mode: 'on_demand',
1973
+ runtime: { command: isJavaScript ? 'node main.js' : 'python main.py' },
1974
+ required_secrets: [],
1975
+ tags: ['scheduled', 'cron'],
1976
+ };
1977
+ if (isJavaScript) {
1978
+ manifest.entrypoint = 'main.js';
1979
+ }
1980
+ await promises_1.default.writeFile(manifestPath, JSON.stringify(manifest, null, 2) + '\n');
1981
+ if (isJavaScript) {
1982
+ await promises_1.default.writeFile(path_1.default.join(targetDir, 'main.js'), cron_job_1.CRON_JOB_MAIN_JS);
1983
+ await promises_1.default.writeFile(path_1.default.join(targetDir, 'package.json'), JSON.stringify({
1984
+ name: agentName,
1985
+ private: true,
1986
+ type: 'commonjs',
1987
+ dependencies: {},
1988
+ }, null, 2) + '\n');
1989
+ }
1990
+ else {
1991
+ await promises_1.default.writeFile(path_1.default.join(targetDir, 'main.py'), cron_job_1.CRON_JOB_MAIN_PY);
1992
+ }
1993
+ await promises_1.default.writeFile(path_1.default.join(targetDir, 'schema.json'), cron_job_1.CRON_JOB_SCHEMA);
1994
+ await promises_1.default.writeFile(path_1.default.join(targetDir, 'README.md'), (0, cron_job_1.cronJobReadme)(agentName));
1995
+ const prefix = name ? name + '/' : '';
1996
+ process.stdout.write(`Initialized scheduled job "${agentName}" in ${targetDir}\n`);
1997
+ process.stdout.write(`\nFiles created:\n`);
1998
+ process.stdout.write(` ${prefix}orchagent.json - Agent configuration (cron job)\n`);
1999
+ if (isJavaScript) {
2000
+ process.stdout.write(` ${prefix}main.js - Scheduled job entrypoint\n`);
2001
+ process.stdout.write(` ${prefix}package.json - npm dependencies\n`);
2002
+ }
2003
+ else {
2004
+ process.stdout.write(` ${prefix}main.py - Scheduled job entrypoint\n`);
2005
+ }
2006
+ process.stdout.write(` ${prefix}schema.json - Input/output schemas\n`);
2007
+ process.stdout.write(` ${prefix}README.md - Setup guide with cron patterns\n`);
2008
+ process.stdout.write(`\nNext steps:\n`);
2009
+ const stepNum = name ? 2 : 1;
2010
+ if (name) {
2011
+ process.stdout.write(` 1. cd ${name}\n`);
2012
+ }
2013
+ const mainFile = isJavaScript ? 'main.js' : 'main.py';
2014
+ const testCmd = isJavaScript ? 'node main.js' : 'python main.py';
2015
+ process.stdout.write(` ${stepNum}. Edit ${mainFile} with your job logic\n`);
2016
+ process.stdout.write(` ${stepNum + 1}. Test: echo '{}' | ${testCmd}\n`);
2017
+ process.stdout.write(` ${stepNum + 2}. Publish: orch publish\n`);
2018
+ process.stdout.write(` ${stepNum + 3}. Schedule: orch schedule create <org>/${agentName} --cron "0 9 * * 1"\n`);
2019
+ process.stdout.write(AGENT_BUILDER_HINT);
2020
+ return;
2021
+ }
1952
2022
  const manifestPath = path_1.default.join(targetDir, 'orchagent.json');
1953
2023
  const promptPath = path_1.default.join(targetDir, 'prompt.md');
1954
2024
  const schemaPath = path_1.default.join(targetDir, 'schema.json');
@@ -584,7 +584,7 @@ function registerPublishCommand(program) {
584
584
  .option('--skills-locked', 'Lock default skills (callers cannot override via headers)')
585
585
  .option('--docker', 'Include Dockerfile for custom environment (builds E2B template)')
586
586
  .option('--local-download', 'Allow users to download and run locally (default: server-only)')
587
- .option('--no-required-secrets', 'Skip required_secrets check for tool/agent types')
587
+ .option('--no-required-secrets', '(deprecated) No longer needed — required_secrets defaults to []')
588
588
  .option('--all', 'Publish all agents in subdirectories (dependency order)')
589
589
  .action(async (options) => {
590
590
  const cwd = process.cwd();
@@ -718,44 +718,62 @@ function registerPublishCommand(program) {
718
718
  }
719
719
  throw new errors_1.CliError(`Failed to read orchagent.json: ${err}`);
720
720
  }
721
+ // UX-1: Collect validation errors and report them all at once
722
+ const validationErrors = [];
721
723
  // Validate manifest
722
724
  if (!manifest.name) {
723
- throw new errors_1.CliError('orchagent.json must have name');
725
+ validationErrors.push('orchagent.json must have name');
724
726
  }
725
- // Validate agent name format (must match gateway rules)
726
- const agentNameRegex = /^[a-z0-9][a-z0-9-]*[a-z0-9]$/;
727
- const agentName = manifest.name;
728
- if (agentName.length < 2 || agentName.length > 50) {
729
- throw new errors_1.CliError('Agent name must be 2-50 characters');
730
- }
731
- if (agentName !== agentName.toLowerCase()) {
732
- throw new errors_1.CliError('Agent name must be lowercase');
733
- }
734
- if (agentName.length > 1 && !agentNameRegex.test(agentName)) {
735
- throw new errors_1.CliError('Agent name must contain only lowercase letters, numbers, and hyphens, and must start/end with a letter or number');
736
- }
737
- if (agentName.includes('--')) {
738
- throw new errors_1.CliError('Agent name must not contain consecutive hyphens');
727
+ else {
728
+ // Validate agent name format (must match gateway rules)
729
+ const agentNameRegex = /^[a-z0-9][a-z0-9-]*[a-z0-9]$/;
730
+ const agentName = manifest.name;
731
+ if (agentName.length < 2 || agentName.length > 50) {
732
+ validationErrors.push('Agent name must be 2-50 characters');
733
+ }
734
+ if (agentName !== agentName.toLowerCase()) {
735
+ validationErrors.push('Agent name must be lowercase');
736
+ }
737
+ if (agentName.length > 1 && !agentNameRegex.test(agentName)) {
738
+ validationErrors.push('Agent name must contain only lowercase letters, numbers, and hyphens, and must start/end with a letter or number');
739
+ }
740
+ if (agentName.includes('--')) {
741
+ validationErrors.push('Agent name must not contain consecutive hyphens');
742
+ }
739
743
  }
740
744
  const { canonicalType, rawType } = canonicalizeManifestType(manifest.type);
741
745
  const runMode = normalizeRunMode(manifest.run_mode);
742
746
  const executionEngine = inferExecutionEngineFromManifest(manifest, rawType);
743
747
  const callable = manifest.callable !== undefined ? Boolean(manifest.callable) : true;
744
748
  if (canonicalType === 'skill') {
745
- throw new errors_1.CliError("Use SKILL.md for publishing skills. Remove orchagent.json and run 'orchagent publish' from a skill directory.");
749
+ throw new errors_1.CliError('Skills use a different publishing format (SKILL.md with YAML front matter).\n\n' +
750
+ ' To publish a skill:\n' +
751
+ ' 1. Run: orchagent skill create ' + (manifest.name || '<name>') + '\n' +
752
+ ' 2. Edit the generated SKILL.md with your skill content\n' +
753
+ ' 3. Run: orchagent publish\n\n' +
754
+ ' orchagent.json is not used for skills — SKILL.md replaces it entirely.\n' +
755
+ ' See: https://orchagent.io/docs/skills');
746
756
  }
747
757
  if (runMode === 'always_on' && executionEngine === 'direct_llm') {
748
- throw new errors_1.CliError('run_mode=always_on requires runtime.command or loop configuration');
758
+ validationErrors.push('run_mode=always_on requires runtime.command or loop configuration');
749
759
  }
750
760
  if (manifest.timeout_seconds !== undefined) {
751
761
  if (!Number.isInteger(manifest.timeout_seconds) || manifest.timeout_seconds <= 0) {
752
- throw new errors_1.CliError('timeout_seconds must be a positive integer');
762
+ validationErrors.push('timeout_seconds must be a positive integer');
753
763
  }
754
764
  }
755
765
  // Warn about deprecated prompt field
756
766
  if (manifest.prompt) {
757
767
  process.stderr.write(chalk_1.default.yellow('Warning: "prompt" field in orchagent.json is ignored. Use prompt.md file instead.\n'));
758
768
  }
769
+ // UX-9: Warn about model (singular) vs default_models
770
+ if (manifest.model && !manifest.default_models) {
771
+ const modelVal = manifest.model;
772
+ process.stderr.write(chalk_1.default.yellow(`\nWarning: "model" field in orchagent.json is not recognized.\n` +
773
+ ` Use "default_models" instead to set per-provider defaults:\n\n` +
774
+ ` ${chalk_1.default.cyan(`"default_models": { "anthropic": "${modelVal}" }`)}\n\n` +
775
+ ` The model resolution order is: caller --model flag → agent default_models → platform default.\n\n`));
776
+ }
759
777
  // Auto-migrate inline schemas to schema.json
760
778
  const schemaPath = path_1.default.join(cwd, 'schema.json');
761
779
  let schemaFileExists = false;
@@ -789,20 +807,8 @@ function registerPublishCommand(program) {
789
807
  const manifestFields = ['manifest_version', 'dependencies', 'max_hops', 'timeout_ms', 'per_call_downstream_cap'];
790
808
  const misplacedFields = manifestFields.filter(f => f in manifest && !manifest.manifest);
791
809
  if (misplacedFields.length > 0) {
792
- throw new errors_1.CliError(`Found manifest fields (${misplacedFields.join(', ')}) at top level of orchagent.json.\n` +
793
- `These must be nested under a "manifest" key. Example:\n\n` +
794
- ` {\n` +
795
- ` "name": "${manifest.name}",\n` +
796
- ` "type": "${manifest.type || 'agent'}",\n` +
797
- ` "manifest": {\n` +
798
- ` "manifest_version": 1,\n` +
799
- ` "dependencies": [...],\n` +
800
- ` "max_hops": 2,\n` +
801
- ` "timeout_ms": 60000,\n` +
802
- ` "per_call_downstream_cap": 50\n` +
803
- ` }\n` +
804
- ` }\n\n` +
805
- `See docs/manifest.md for details.`);
810
+ validationErrors.push(`Found manifest fields (${misplacedFields.join(', ')}) at top level of orchagent.json. ` +
811
+ `These must be nested under a "manifest" key. See docs/manifest.md for details.`);
806
812
  }
807
813
  // Read prompt for LLM-driven engines (direct_llm + managed_loop).
808
814
  let prompt;
@@ -813,11 +819,11 @@ function registerPublishCommand(program) {
813
819
  }
814
820
  catch (err) {
815
821
  if (err.code === 'ENOENT') {
816
- throw new errors_1.CliError('No prompt.md found for this agent.\n\n' +
817
- 'Create a prompt.md file in the current directory with your prompt template.\n' +
818
- 'See: https://orchagent.io/docs/publishing');
822
+ validationErrors.push('No prompt.md found. Create a prompt.md file with your prompt template.');
823
+ }
824
+ else {
825
+ throw err;
819
826
  }
820
- throw err;
821
827
  }
822
828
  }
823
829
  // Validate managed-loop specific fields + normalize loop payload
@@ -825,7 +831,7 @@ function registerPublishCommand(program) {
825
831
  if (executionEngine === 'managed_loop') {
826
832
  if (manifest.max_turns !== undefined) {
827
833
  if (typeof manifest.max_turns !== 'number' || manifest.max_turns < 1 || manifest.max_turns > 50) {
828
- throw new errors_1.CliError('max_turns must be a number between 1 and 50');
834
+ validationErrors.push('max_turns must be a number between 1 and 50');
829
835
  }
830
836
  }
831
837
  const providedLoop = manifest.loop && typeof manifest.loop === 'object'
@@ -849,17 +855,17 @@ function registerPublishCommand(program) {
849
855
  const seenNames = new Set();
850
856
  for (const tool of mergedTools) {
851
857
  if (!tool.name || !tool.command) {
852
- throw new errors_1.CliError(`Invalid custom_tool: each tool must have 'name' and 'command' fields.\n` +
853
- `Found: ${JSON.stringify(tool)}`);
854
- }
855
- if (reservedNames.has(tool.name)) {
856
- throw new errors_1.CliError(`Custom tool '${tool.name}' conflicts with a built-in tool name.\n` +
857
- `Reserved names: ${[...reservedNames].join(', ')}`);
858
+ validationErrors.push(`Invalid custom_tool: each tool must have 'name' and 'command' fields. Found: ${JSON.stringify(tool)}`);
858
859
  }
859
- if (seenNames.has(tool.name)) {
860
- throw new errors_1.CliError(`Duplicate custom tool name: '${tool.name}'`);
860
+ else {
861
+ if (reservedNames.has(tool.name)) {
862
+ validationErrors.push(`Custom tool '${tool.name}' conflicts with a built-in tool name. Reserved names: ${[...reservedNames].join(', ')}`);
863
+ }
864
+ if (seenNames.has(tool.name)) {
865
+ validationErrors.push(`Duplicate custom tool name: '${tool.name}'`);
866
+ }
867
+ seenNames.add(tool.name);
861
868
  }
862
- seenNames.add(tool.name);
863
869
  }
864
870
  }
865
871
  if (!manifest.supported_providers) {
@@ -921,11 +927,13 @@ function registerPublishCommand(program) {
921
927
  }
922
928
  if (!options.url) {
923
929
  if (!bundleEntrypoint) {
924
- throw new errors_1.CliError('Tool requires either --url <url> or an entry point file (main.py, app.py, index.js, etc.)');
930
+ validationErrors.push('Tool requires either --url <url> or an entry point file (main.py, app.py, index.js, etc.)');
931
+ }
932
+ else {
933
+ shouldUploadBundle = true;
934
+ agentUrl = 'https://tool.internal';
935
+ process.stdout.write(`Detected code runtime entrypoint: ${bundleEntrypoint}\n`);
925
936
  }
926
- shouldUploadBundle = true;
927
- agentUrl = 'https://tool.internal';
928
- process.stdout.write(`Detected code runtime entrypoint: ${bundleEntrypoint}\n`);
929
937
  }
930
938
  let runtimeCommand = manifest.runtime?.command?.trim() || '';
931
939
  if (!runtimeCommand && manifest.run_command?.trim()) {
@@ -940,7 +948,7 @@ function registerPublishCommand(program) {
940
948
  agentUrl = agentUrl || 'https://prompt-agent.internal';
941
949
  }
942
950
  if (options.docker && executionEngine !== 'code_runtime') {
943
- throw new errors_1.CliError('--docker is only supported for code runtime agents');
951
+ validationErrors.push('--docker is only supported for code runtime agents');
944
952
  }
945
953
  // Get org info (workspace-aware — returns workspace org if workspace is active)
946
954
  const org = await (0, api_1.getOrg)(config, workspaceId);
@@ -982,23 +990,23 @@ function registerPublishCommand(program) {
982
990
  ` the field to use the default) and republish each dependency.\n\n`);
983
991
  }
984
992
  }
985
- // C-1: Block publish if tool/agent type has no required_secrets declared.
993
+ // UX-2: Default required_secrets to [] when omitted for tool/agent types.
986
994
  // Prompt and skill types are exempt (prompt agents get LLM keys from platform,
987
995
  // skills don't run standalone).
988
- // An explicit empty array (required_secrets: []) is a valid declaration
989
- // meaning "this agent deliberately needs no secrets."
990
- // Runs before dry-run so --dry-run catches the same errors as real publish (BUG-11).
991
996
  if ((canonicalType === 'tool' || canonicalType === 'agent') &&
992
- manifest.required_secrets === undefined &&
993
- options.requiredSecrets !== false) {
994
- process.stderr.write(chalk_1.default.red(`\nError: ${canonicalType} agents must declare required_secrets in orchagent.json.\n\n`) +
995
- ` Add the env vars your code needs at runtime:\n` +
996
- ` ${chalk_1.default.cyan('"required_secrets": ["ANTHROPIC_API_KEY", "MY_TOKEN"]')}\n\n` +
997
- ` If this agent genuinely needs no secrets, add an empty array:\n` +
998
- ` ${chalk_1.default.cyan('"required_secrets": []')}\n\n` +
999
- ` These are matched by name against your workspace secrets vault.\n` +
1000
- ` Use ${chalk_1.default.cyan('--no-required-secrets')} to skip this check.\n`);
1001
- const err = new errors_1.CliError('Missing required_secrets declaration', errors_1.ExitCodes.INVALID_INPUT);
997
+ manifest.required_secrets === undefined) {
998
+ manifest.required_secrets = [];
999
+ process.stderr.write(chalk_1.default.dim(` ℹ No required_secrets declared defaulting to [] (no secrets needed)\n`));
1000
+ }
1001
+ // UX-1: Report all validation errors at once
1002
+ if (validationErrors.length > 0) {
1003
+ if (validationErrors.length === 1) {
1004
+ throw new errors_1.CliError(validationErrors[0]);
1005
+ }
1006
+ const numbered = validationErrors.map((e, i) => ` ${i + 1}. ${e}`).join('\n');
1007
+ process.stderr.write(chalk_1.default.red(`\nFound ${validationErrors.length} validation errors:\n\n`) +
1008
+ numbered + '\n');
1009
+ const err = new errors_1.CliError(`Found ${validationErrors.length} validation errors:\n${numbered}`, errors_1.ExitCodes.INVALID_INPUT);
1002
1010
  err.displayed = true;
1003
1011
  throw err;
1004
1012
  }