@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.
- package/dist/commands/diff-format.js +300 -0
- package/dist/commands/diff.js +2 -129
- package/dist/commands/health.js +90 -7
- package/dist/commands/index.js +4 -0
- package/dist/commands/init-wizard.js +129 -64
- package/dist/commands/init.js +71 -1
- package/dist/commands/publish.js +74 -66
- package/dist/commands/run.js +107 -29
- package/dist/commands/scaffold.js +213 -0
- package/dist/commands/schedule.js +40 -3
- package/dist/commands/templates/cron-job.js +259 -0
- package/dist/commands/update.js +46 -9
- package/dist/commands/validate.js +264 -0
- package/dist/lib/scaffold-orchestration.js +237 -0
- package/dist/lib/validate.js +478 -0
- package/package.json +1 -1
|
@@ -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
|
|
68
|
-
// 2.
|
|
69
|
-
const
|
|
70
|
-
{ value: 'prompt', label: '
|
|
71
|
-
{ value: 'tool', label: '
|
|
72
|
-
{ value: '
|
|
73
|
-
{ value: '
|
|
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
|
-
//
|
|
76
|
-
if (
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
{
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
//
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
126
|
-
|
|
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();
|
package/dist/commands/init.js
CHANGED
|
@@ -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');
|
package/dist/commands/publish.js
CHANGED
|
@@ -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', '
|
|
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
|
-
|
|
725
|
+
validationErrors.push('orchagent.json must have name');
|
|
724
726
|
}
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
793
|
-
`These must be nested under a "manifest" key.
|
|
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
|
-
|
|
817
|
-
|
|
818
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
860
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
994
|
-
process.stderr.write(chalk_1.default.
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
const
|
|
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
|
}
|