@orchagent/cli 0.3.90 → 0.3.92
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/completion.js +379 -0
- package/dist/commands/dag.js +16 -7
- package/dist/commands/delete.js +9 -4
- package/dist/commands/diff-format.js +300 -0
- package/dist/commands/diff.js +12 -131
- package/dist/commands/estimate.js +5 -2
- package/dist/commands/fork.js +7 -1
- package/dist/commands/health.js +90 -7
- package/dist/commands/index.js +6 -0
- package/dist/commands/info.js +8 -1
- package/dist/commands/init-wizard.js +225 -0
- package/dist/commands/init.js +109 -3
- package/dist/commands/login.js +8 -0
- package/dist/commands/logs.js +17 -7
- package/dist/commands/metrics.js +16 -7
- package/dist/commands/publish.js +74 -66
- package/dist/commands/replay.js +16 -7
- package/dist/commands/run.js +158 -33
- package/dist/commands/scaffold.js +213 -0
- package/dist/commands/schedule.js +112 -11
- package/dist/commands/secrets.js +16 -7
- package/dist/commands/service.js +16 -7
- package/dist/commands/skill.js +84 -8
- package/dist/commands/templates/cron-job.js +259 -0
- package/dist/commands/trace.js +16 -7
- package/dist/commands/tree.js +7 -1
- package/dist/commands/update.js +46 -9
- package/dist/commands/validate.js +264 -0
- package/dist/commands/workspace.js +16 -7
- package/dist/lib/agent-ref.js +4 -1
- package/dist/lib/browser-auth.js +1 -0
- package/dist/lib/scaffold-orchestration.js +237 -0
- package/dist/lib/validate.js +478 -0
- package/package.json +1 -1
|
@@ -0,0 +1,478 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Agent project validation engine.
|
|
4
|
+
*
|
|
5
|
+
* Runs all pre-publish checks on an agent project directory without
|
|
6
|
+
* actually publishing. Used by `orch validate` and reusable by other
|
|
7
|
+
* commands that need project validation.
|
|
8
|
+
*/
|
|
9
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
10
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
11
|
+
};
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
exports.canonicalizeManifestType = canonicalizeManifestType;
|
|
14
|
+
exports.normalizeRunMode = normalizeRunMode;
|
|
15
|
+
exports.inferExecutionEngine = inferExecutionEngine;
|
|
16
|
+
exports.validateAgentProject = validateAgentProject;
|
|
17
|
+
const promises_1 = __importDefault(require("fs/promises"));
|
|
18
|
+
const path_1 = __importDefault(require("path"));
|
|
19
|
+
const yaml_1 = __importDefault(require("yaml"));
|
|
20
|
+
const publish_1 = require("../commands/publish");
|
|
21
|
+
const bundle_1 = require("./bundle");
|
|
22
|
+
// ── Helpers ────────────────────────────────────────────────────────────
|
|
23
|
+
function canonicalizeManifestType(typeValue) {
|
|
24
|
+
const rawType = (typeValue || 'agent').trim().toLowerCase();
|
|
25
|
+
if (['prompt', 'tool', 'agent', 'skill'].includes(rawType)) {
|
|
26
|
+
return { canonicalType: rawType, rawType, valid: true };
|
|
27
|
+
}
|
|
28
|
+
if (rawType === 'agentic')
|
|
29
|
+
return { canonicalType: 'agent', rawType, valid: true };
|
|
30
|
+
if (rawType === 'code')
|
|
31
|
+
return { canonicalType: 'tool', rawType, valid: true };
|
|
32
|
+
return { canonicalType: 'agent', rawType, valid: false };
|
|
33
|
+
}
|
|
34
|
+
function normalizeRunMode(runMode) {
|
|
35
|
+
const normalized = (runMode || 'on_demand').trim().toLowerCase();
|
|
36
|
+
if (normalized === 'on_demand' || normalized === 'always_on') {
|
|
37
|
+
return { value: normalized, valid: true };
|
|
38
|
+
}
|
|
39
|
+
return { value: 'on_demand', valid: false };
|
|
40
|
+
}
|
|
41
|
+
function inferExecutionEngine(manifest, rawType) {
|
|
42
|
+
const runtimeCommand = manifest.runtime?.command?.trim();
|
|
43
|
+
const hasLoop = Boolean(manifest.loop && Object.keys(manifest.loop).length > 0);
|
|
44
|
+
if (runtimeCommand && hasLoop)
|
|
45
|
+
return { engine: null, conflict: true };
|
|
46
|
+
if (runtimeCommand)
|
|
47
|
+
return { engine: 'code_runtime', conflict: false };
|
|
48
|
+
if (hasLoop)
|
|
49
|
+
return { engine: 'managed_loop', conflict: false };
|
|
50
|
+
if (rawType === 'tool' || rawType === 'code')
|
|
51
|
+
return { engine: 'code_runtime', conflict: false };
|
|
52
|
+
if (rawType === 'agentic' || rawType === 'agent')
|
|
53
|
+
return { engine: 'managed_loop', conflict: false };
|
|
54
|
+
return { engine: 'direct_llm', conflict: false };
|
|
55
|
+
}
|
|
56
|
+
function validateNameFormat(name, issues, file) {
|
|
57
|
+
const nameRegex = /^[a-z0-9][a-z0-9-]*[a-z0-9]$/;
|
|
58
|
+
if (name.length < 2 || name.length > 50) {
|
|
59
|
+
issues.push({ level: 'error', message: 'Agent name must be 2-50 characters', file });
|
|
60
|
+
}
|
|
61
|
+
if (name !== name.toLowerCase()) {
|
|
62
|
+
issues.push({ level: 'error', message: 'Agent name must be lowercase', file });
|
|
63
|
+
}
|
|
64
|
+
if (name.length > 1 && !nameRegex.test(name)) {
|
|
65
|
+
issues.push({
|
|
66
|
+
level: 'error',
|
|
67
|
+
message: 'Agent name must contain only lowercase letters, numbers, and hyphens, and must start/end with a letter or number',
|
|
68
|
+
file,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
if (name.includes('--')) {
|
|
72
|
+
issues.push({ level: 'error', message: 'Agent name must not contain consecutive hyphens', file });
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
// ── Skill validation ───────────────────────────────────────────────────
|
|
76
|
+
function validateSkill(content, issues, metadata) {
|
|
77
|
+
metadata.isSkill = true;
|
|
78
|
+
metadata.agentType = 'skill';
|
|
79
|
+
const match = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
|
|
80
|
+
if (!match) {
|
|
81
|
+
issues.push({
|
|
82
|
+
level: 'error',
|
|
83
|
+
message: 'SKILL.md must start with YAML frontmatter (--- block)',
|
|
84
|
+
file: 'SKILL.md',
|
|
85
|
+
});
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
let frontmatter;
|
|
89
|
+
try {
|
|
90
|
+
frontmatter = yaml_1.default.parse(match[1]);
|
|
91
|
+
if (!frontmatter || typeof frontmatter !== 'object') {
|
|
92
|
+
issues.push({ level: 'error', message: 'SKILL.md frontmatter is empty or invalid YAML', file: 'SKILL.md' });
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
catch (err) {
|
|
97
|
+
issues.push({ level: 'error', message: `SKILL.md frontmatter has invalid YAML: ${err.message}`, file: 'SKILL.md' });
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
const body = match[2].trim();
|
|
101
|
+
if (!frontmatter.name) {
|
|
102
|
+
issues.push({ level: 'error', message: 'SKILL.md frontmatter must have a "name" field', file: 'SKILL.md' });
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
metadata.agentName = String(frontmatter.name);
|
|
106
|
+
validateNameFormat(String(frontmatter.name), issues, 'SKILL.md');
|
|
107
|
+
}
|
|
108
|
+
if (!frontmatter.description) {
|
|
109
|
+
issues.push({ level: 'error', message: 'SKILL.md frontmatter must have a "description" field', file: 'SKILL.md' });
|
|
110
|
+
}
|
|
111
|
+
if (!body) {
|
|
112
|
+
issues.push({ level: 'error', message: 'SKILL.md has no content after frontmatter', file: 'SKILL.md' });
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
// ── Agent validation ───────────────────────────────────────────────────
|
|
116
|
+
async function validateManifest(projectDir, manifest, issues, metadata, options) {
|
|
117
|
+
metadata.manifest = manifest;
|
|
118
|
+
// ── Name ──
|
|
119
|
+
if (!manifest.name) {
|
|
120
|
+
issues.push({ level: 'error', message: 'orchagent.json must have a "name" field', file: 'orchagent.json' });
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
metadata.agentName = manifest.name;
|
|
124
|
+
validateNameFormat(manifest.name, issues, 'orchagent.json');
|
|
125
|
+
}
|
|
126
|
+
// ── Type ──
|
|
127
|
+
const { canonicalType, rawType, valid: typeValid } = canonicalizeManifestType(manifest.type);
|
|
128
|
+
if (!typeValid) {
|
|
129
|
+
issues.push({
|
|
130
|
+
level: 'error',
|
|
131
|
+
message: `Invalid type '${manifest.type}'. Use 'prompt', 'tool', 'agent', or 'skill' (legacy: agentic, code).`,
|
|
132
|
+
file: 'orchagent.json',
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
metadata.agentType = canonicalType;
|
|
136
|
+
if (canonicalType === 'skill') {
|
|
137
|
+
issues.push({
|
|
138
|
+
level: 'error',
|
|
139
|
+
message: `Skills use SKILL.md format. Run \`orch skill create ${manifest.name || '<name>'}\` to set up the correct structure.`,
|
|
140
|
+
file: 'orchagent.json',
|
|
141
|
+
});
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
// ── Run mode ──
|
|
145
|
+
const { value: runMode, valid: runModeValid } = normalizeRunMode(manifest.run_mode);
|
|
146
|
+
if (!runModeValid) {
|
|
147
|
+
issues.push({ level: 'error', message: "run_mode must be 'on_demand' or 'always_on'", file: 'orchagent.json' });
|
|
148
|
+
}
|
|
149
|
+
metadata.runMode = runMode;
|
|
150
|
+
// ── Execution engine ──
|
|
151
|
+
const { engine: executionEngine, conflict } = inferExecutionEngine(manifest, rawType);
|
|
152
|
+
if (conflict) {
|
|
153
|
+
issues.push({ level: 'error', message: 'runtime.command and loop cannot both be set', file: 'orchagent.json' });
|
|
154
|
+
}
|
|
155
|
+
metadata.executionEngine = executionEngine || undefined;
|
|
156
|
+
metadata.callable = manifest.callable !== undefined ? Boolean(manifest.callable) : true;
|
|
157
|
+
// ── Timeout ──
|
|
158
|
+
if (manifest.timeout_seconds !== undefined) {
|
|
159
|
+
if (!Number.isInteger(manifest.timeout_seconds) || manifest.timeout_seconds <= 0) {
|
|
160
|
+
issues.push({ level: 'error', message: 'timeout_seconds must be a positive integer', file: 'orchagent.json' });
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
// ── Always-on + direct_llm incompatibility ──
|
|
164
|
+
if (runMode === 'always_on' && executionEngine === 'direct_llm') {
|
|
165
|
+
issues.push({ level: 'error', message: 'run_mode=always_on requires runtime.command or loop configuration', file: 'orchagent.json' });
|
|
166
|
+
}
|
|
167
|
+
// ── Deprecated/misused fields ──
|
|
168
|
+
if (manifest.prompt) {
|
|
169
|
+
issues.push({ level: 'warning', message: '"prompt" field in orchagent.json is ignored. Use prompt.md file instead.', file: 'orchagent.json' });
|
|
170
|
+
}
|
|
171
|
+
if (manifest.model && !manifest.default_models) {
|
|
172
|
+
issues.push({
|
|
173
|
+
level: 'warning',
|
|
174
|
+
message: '"model" field is not recognized. Use "default_models": {"anthropic": "...", "openai": "..."}',
|
|
175
|
+
file: 'orchagent.json',
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
// ── Misplaced manifest fields ──
|
|
179
|
+
const manifestFields = ['manifest_version', 'dependencies', 'max_hops', 'timeout_ms', 'per_call_downstream_cap'];
|
|
180
|
+
const misplacedFields = manifestFields.filter(f => f in manifest && !manifest.manifest);
|
|
181
|
+
if (misplacedFields.length > 0) {
|
|
182
|
+
issues.push({
|
|
183
|
+
level: 'error',
|
|
184
|
+
message: `Found manifest fields (${misplacedFields.join(', ')}) at top level. These must be nested under a "manifest" key.`,
|
|
185
|
+
file: 'orchagent.json',
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
// ── Prompt file ──
|
|
189
|
+
if (executionEngine === 'direct_llm' || executionEngine === 'managed_loop') {
|
|
190
|
+
await validatePrompt(projectDir, manifest, executionEngine, issues, metadata);
|
|
191
|
+
}
|
|
192
|
+
// ── Managed loop ──
|
|
193
|
+
if (executionEngine === 'managed_loop') {
|
|
194
|
+
validateManagedLoop(manifest, issues, metadata);
|
|
195
|
+
metadata.supportedProviders = manifest.supported_providers || ['anthropic'];
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
metadata.supportedProviders = manifest.supported_providers || ['any'];
|
|
199
|
+
}
|
|
200
|
+
// ── Schemas ──
|
|
201
|
+
await validateSchemas(projectDir, manifest, executionEngine, issues, metadata);
|
|
202
|
+
// ── Code runtime ──
|
|
203
|
+
if (executionEngine === 'code_runtime') {
|
|
204
|
+
await validateCodeRuntime(projectDir, manifest, options, issues, metadata);
|
|
205
|
+
}
|
|
206
|
+
// ── Docker flag ──
|
|
207
|
+
if (options.docker && executionEngine !== 'code_runtime') {
|
|
208
|
+
issues.push({ level: 'error', message: '--docker is only supported for code runtime agents', file: 'orchagent.json' });
|
|
209
|
+
}
|
|
210
|
+
if (options.docker) {
|
|
211
|
+
try {
|
|
212
|
+
await promises_1.default.access(path_1.default.join(projectDir, 'Dockerfile'));
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
issues.push({ level: 'error', message: '--docker flag specified but no Dockerfile found', file: 'Dockerfile' });
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
// ── Required secrets ──
|
|
219
|
+
if ((canonicalType === 'tool' || canonicalType === 'agent') && manifest.required_secrets === undefined) {
|
|
220
|
+
metadata.requiredSecrets = [];
|
|
221
|
+
issues.push({ level: 'info', message: 'No required_secrets declared — defaulting to [] (no secrets needed)', file: 'orchagent.json' });
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
metadata.requiredSecrets = manifest.required_secrets || [];
|
|
225
|
+
}
|
|
226
|
+
if (manifest.required_secrets?.includes('ORCHAGENT_SERVICE_KEY')) {
|
|
227
|
+
issues.push({
|
|
228
|
+
level: 'warning',
|
|
229
|
+
message: 'ORCHAGENT_SERVICE_KEY in required_secrets is auto-injected by the gateway. Remove it to avoid overriding the auto-injected key.',
|
|
230
|
+
file: 'orchagent.json',
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
// ── Env var scanning (code runtime) ──
|
|
234
|
+
if (executionEngine === 'code_runtime') {
|
|
235
|
+
try {
|
|
236
|
+
const undeclared = await (0, publish_1.scanUndeclaredEnvVars)(projectDir, manifest.required_secrets || []);
|
|
237
|
+
if (undeclared.length > 0) {
|
|
238
|
+
issues.push({
|
|
239
|
+
level: 'warning',
|
|
240
|
+
message: `Code references undeclared environment variables: ${undeclared.join(', ')}. Add them to required_secrets if needed.`,
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
catch { /* non-critical */ }
|
|
245
|
+
}
|
|
246
|
+
// ── Reserved port scanning (always-on code runtime) ──
|
|
247
|
+
if (runMode === 'always_on' && executionEngine === 'code_runtime') {
|
|
248
|
+
try {
|
|
249
|
+
if (await (0, publish_1.scanReservedPort)(projectDir)) {
|
|
250
|
+
issues.push({
|
|
251
|
+
level: 'warning',
|
|
252
|
+
message: 'Code appears to bind to port 8080, which is reserved by the platform health server. Use a different port.',
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
catch { /* non-critical */ }
|
|
257
|
+
}
|
|
258
|
+
// ── Bundle size estimation (code runtime with local code) ──
|
|
259
|
+
if (executionEngine === 'code_runtime' && metadata.bundleEntrypoint && !options.url) {
|
|
260
|
+
try {
|
|
261
|
+
const preview = await (0, bundle_1.previewBundle)(projectDir, {
|
|
262
|
+
entrypoint: metadata.bundleEntrypoint,
|
|
263
|
+
exclude: manifest.bundle?.exclude,
|
|
264
|
+
include: manifest.bundle?.include,
|
|
265
|
+
});
|
|
266
|
+
metadata.bundleSizeBytes = preview.totalSizeBytes;
|
|
267
|
+
metadata.bundleFileCount = preview.fileCount;
|
|
268
|
+
const maxSize = 50 * 1024 * 1024; // 50MB
|
|
269
|
+
if (preview.totalSizeBytes > maxSize) {
|
|
270
|
+
issues.push({
|
|
271
|
+
level: 'error',
|
|
272
|
+
message: `Estimated bundle size (${(preview.totalSizeBytes / 1024 / 1024).toFixed(1)}MB) exceeds 50MB limit. Add exclusions to bundle.exclude in orchagent.json.`,
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
catch { /* non-critical — bundle preview failed */ }
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
async function validatePrompt(projectDir, manifest, executionEngine, issues, metadata) {
|
|
280
|
+
const promptPath = path_1.default.join(projectDir, 'prompt.md');
|
|
281
|
+
try {
|
|
282
|
+
const prompt = await promises_1.default.readFile(promptPath, 'utf-8');
|
|
283
|
+
metadata.hasPrompt = true;
|
|
284
|
+
if (prompt.trim().length === 0) {
|
|
285
|
+
issues.push({ level: 'warning', message: 'prompt.md is empty', file: 'prompt.md' });
|
|
286
|
+
}
|
|
287
|
+
// Extract template variables for schema matching
|
|
288
|
+
const templateVars = (0, publish_1.extractTemplateVariables)(prompt);
|
|
289
|
+
metadata.templateVariables = Array.isArray(templateVars) ? templateVars : [];
|
|
290
|
+
}
|
|
291
|
+
catch (err) {
|
|
292
|
+
metadata.templateVariables = metadata.templateVariables || [];
|
|
293
|
+
if (err.code === 'ENOENT') {
|
|
294
|
+
issues.push({
|
|
295
|
+
level: 'error',
|
|
296
|
+
message: 'No prompt.md found. Create a prompt.md file with your prompt template.',
|
|
297
|
+
file: 'prompt.md',
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
function validateManagedLoop(manifest, issues, metadata) {
|
|
303
|
+
if (manifest.max_turns !== undefined) {
|
|
304
|
+
if (typeof manifest.max_turns !== 'number' || manifest.max_turns < 1 || manifest.max_turns > 50) {
|
|
305
|
+
issues.push({ level: 'error', message: 'max_turns must be a number between 1 and 50', file: 'orchagent.json' });
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
// Merge loop config (same logic as publish.ts)
|
|
309
|
+
const providedLoop = manifest.loop && typeof manifest.loop === 'object' ? { ...manifest.loop } : {};
|
|
310
|
+
if (!('max_turns' in providedLoop) && manifest.max_turns !== undefined) {
|
|
311
|
+
providedLoop.max_turns = manifest.max_turns;
|
|
312
|
+
}
|
|
313
|
+
if (!('custom_tools' in providedLoop) && manifest.custom_tools?.length) {
|
|
314
|
+
providedLoop.custom_tools = manifest.custom_tools;
|
|
315
|
+
}
|
|
316
|
+
metadata.maxTurns = providedLoop.max_turns || manifest.max_turns || 25;
|
|
317
|
+
// Validate custom_tools
|
|
318
|
+
const mergedTools = Array.isArray(providedLoop.custom_tools)
|
|
319
|
+
? providedLoop.custom_tools
|
|
320
|
+
: [];
|
|
321
|
+
metadata.customToolCount = mergedTools.length;
|
|
322
|
+
if (mergedTools.length > 0) {
|
|
323
|
+
const reservedNames = new Set(['bash', 'read_file', 'write_file', 'list_files', 'submit_result']);
|
|
324
|
+
const seenNames = new Set();
|
|
325
|
+
for (const tool of mergedTools) {
|
|
326
|
+
if (!tool.name || !tool.command) {
|
|
327
|
+
issues.push({
|
|
328
|
+
level: 'error',
|
|
329
|
+
message: `Invalid custom_tool: each tool must have 'name' and 'command' fields. Found: ${JSON.stringify(tool)}`,
|
|
330
|
+
file: 'orchagent.json',
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
else {
|
|
334
|
+
if (reservedNames.has(tool.name)) {
|
|
335
|
+
issues.push({
|
|
336
|
+
level: 'error',
|
|
337
|
+
message: `Custom tool '${tool.name}' conflicts with built-in tool name. Reserved: ${[...reservedNames].join(', ')}`,
|
|
338
|
+
file: 'orchagent.json',
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
if (seenNames.has(tool.name)) {
|
|
342
|
+
issues.push({ level: 'error', message: `Duplicate custom tool name: '${tool.name}'`, file: 'orchagent.json' });
|
|
343
|
+
}
|
|
344
|
+
seenNames.add(tool.name);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
async function validateSchemas(projectDir, manifest, executionEngine, issues, metadata) {
|
|
350
|
+
const schemaPath = path_1.default.join(projectDir, 'schema.json');
|
|
351
|
+
let schemaFromFile = false;
|
|
352
|
+
try {
|
|
353
|
+
const raw = await promises_1.default.readFile(schemaPath, 'utf-8');
|
|
354
|
+
JSON.parse(raw); // validate JSON
|
|
355
|
+
schemaFromFile = true;
|
|
356
|
+
metadata.hasSchema = true;
|
|
357
|
+
}
|
|
358
|
+
catch (err) {
|
|
359
|
+
if (err.code !== 'ENOENT') {
|
|
360
|
+
issues.push({ level: 'error', message: `Failed to parse schema.json: ${err.message}`, file: 'schema.json' });
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
// Warn about inline schemas when schema.json exists
|
|
364
|
+
if ((manifest.input_schema || manifest.output_schema) && schemaFromFile) {
|
|
365
|
+
issues.push({
|
|
366
|
+
level: 'warning',
|
|
367
|
+
message: 'Inline schemas in orchagent.json are ignored (schema.json takes priority).',
|
|
368
|
+
file: 'orchagent.json',
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
// Template variable / schema cross-check
|
|
372
|
+
const tvars = metadata.templateVariables || [];
|
|
373
|
+
if (tvars.length > 0 &&
|
|
374
|
+
(executionEngine === 'direct_llm' || executionEngine === 'managed_loop')) {
|
|
375
|
+
if (!schemaFromFile) {
|
|
376
|
+
issues.push({
|
|
377
|
+
level: 'info',
|
|
378
|
+
message: `Input schema will be auto-derived from template variables: ${tvars.join(', ')}`,
|
|
379
|
+
file: 'prompt.md',
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
else {
|
|
383
|
+
// Read schema.json to cross-check
|
|
384
|
+
try {
|
|
385
|
+
const raw = await promises_1.default.readFile(schemaPath, 'utf-8');
|
|
386
|
+
const schemas = JSON.parse(raw);
|
|
387
|
+
const inputSchema = schemas.input;
|
|
388
|
+
if (inputSchema && typeof inputSchema === 'object' && 'properties' in inputSchema) {
|
|
389
|
+
const schemaProps = Object.keys(inputSchema.properties || {});
|
|
390
|
+
const missing = tvars.filter(v => !schemaProps.includes(v));
|
|
391
|
+
const extra = schemaProps.filter(p => !tvars.includes(p));
|
|
392
|
+
if (missing.length > 0 || extra.length > 0) {
|
|
393
|
+
const parts = [];
|
|
394
|
+
if (missing.length > 0)
|
|
395
|
+
parts.push(`template uses {{${missing.join('}}, {{')}}} but schema.json doesn't define them`);
|
|
396
|
+
if (extra.length > 0)
|
|
397
|
+
parts.push(`schema.json defines ${extra.join(', ')} but template doesn't use them`);
|
|
398
|
+
issues.push({ level: 'warning', message: `Schema mismatch: ${parts.join('; ')}`, file: 'schema.json' });
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
catch { /* already reported above */ }
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
async function validateCodeRuntime(projectDir, manifest, options, issues, metadata) {
|
|
407
|
+
const entrypoint = manifest.entrypoint || (await (0, bundle_1.detectEntrypoint)(projectDir)) || null;
|
|
408
|
+
metadata.bundleEntrypoint = entrypoint;
|
|
409
|
+
if (!options.url && !entrypoint) {
|
|
410
|
+
issues.push({
|
|
411
|
+
level: 'error',
|
|
412
|
+
message: 'Tool requires either --url or an entry point file (main.py, app.py, index.js, etc.)',
|
|
413
|
+
file: 'orchagent.json',
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
// SDK detection
|
|
417
|
+
metadata.sdkCompatible = await (0, publish_1.detectSdkCompatible)(projectDir);
|
|
418
|
+
if (metadata.sdkCompatible) {
|
|
419
|
+
issues.push({ level: 'info', message: 'orchagent-sdk detected — agent will be marked as Local Ready' });
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
// ── Main entry point ───────────────────────────────────────────────────
|
|
423
|
+
async function validateAgentProject(projectDir, options = {}) {
|
|
424
|
+
const issues = [];
|
|
425
|
+
const metadata = {
|
|
426
|
+
isSkill: false,
|
|
427
|
+
hasPrompt: false,
|
|
428
|
+
hasSchema: false,
|
|
429
|
+
templateVariables: [],
|
|
430
|
+
sdkCompatible: false,
|
|
431
|
+
supportedProviders: [],
|
|
432
|
+
customToolCount: 0,
|
|
433
|
+
requiredSecrets: [],
|
|
434
|
+
};
|
|
435
|
+
// Check for SKILL.md first (takes priority, matching publish behavior)
|
|
436
|
+
const skillMdPath = path_1.default.join(projectDir, 'SKILL.md');
|
|
437
|
+
try {
|
|
438
|
+
const content = await promises_1.default.readFile(skillMdPath, 'utf-8');
|
|
439
|
+
validateSkill(content, issues, metadata);
|
|
440
|
+
return {
|
|
441
|
+
valid: issues.filter(i => i.level === 'error').length === 0,
|
|
442
|
+
issues,
|
|
443
|
+
metadata,
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
catch {
|
|
447
|
+
// No SKILL.md — continue to orchagent.json
|
|
448
|
+
}
|
|
449
|
+
// Read orchagent.json
|
|
450
|
+
const manifestPath = path_1.default.join(projectDir, 'orchagent.json');
|
|
451
|
+
let manifest;
|
|
452
|
+
try {
|
|
453
|
+
const raw = await promises_1.default.readFile(manifestPath, 'utf-8');
|
|
454
|
+
manifest = JSON.parse(raw);
|
|
455
|
+
}
|
|
456
|
+
catch (err) {
|
|
457
|
+
if (err.code === 'ENOENT') {
|
|
458
|
+
issues.push({
|
|
459
|
+
level: 'error',
|
|
460
|
+
message: 'No orchagent.json or SKILL.md found. Run `orch init` first.',
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
else {
|
|
464
|
+
issues.push({
|
|
465
|
+
level: 'error',
|
|
466
|
+
message: `Failed to parse orchagent.json: ${err.message}`,
|
|
467
|
+
file: 'orchagent.json',
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
return { valid: false, issues, metadata };
|
|
471
|
+
}
|
|
472
|
+
await validateManifest(projectDir, manifest, issues, metadata, options);
|
|
473
|
+
return {
|
|
474
|
+
valid: issues.filter(i => i.level === 'error').length === 0,
|
|
475
|
+
issues,
|
|
476
|
+
metadata,
|
|
477
|
+
};
|
|
478
|
+
}
|
package/package.json
CHANGED