@orchagent/cli 0.3.84 → 0.3.85

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.
@@ -37,13 +37,6 @@ exports.claudeCodeAdapter = {
37
37
  errors.push('Agent has no prompt content');
38
38
  return { canConvert: false, warnings, errors };
39
39
  }
40
- // Warnings for fields that can't be fully mapped
41
- if (agent.input_schema) {
42
- warnings.push('input_schema will be described in the prompt body, not enforced');
43
- }
44
- if (agent.output_schema) {
45
- warnings.push('output_schema will be described in the prompt body, not enforced');
46
- }
47
40
  return { canConvert: true, warnings, errors };
48
41
  },
49
42
  convert(agent) {
@@ -52,8 +45,11 @@ exports.claudeCodeAdapter = {
52
45
  const frontmatter = {
53
46
  name: normalizedName,
54
47
  description: agent.description || `Delegatable agent: ${agent.name}`,
55
- tools: 'Read, Glob, Grep', // Safe defaults - read-only
56
48
  };
49
+ // Only include tools for agent-type (prompt and skill types are single LLM calls)
50
+ if (agent.type === 'agent') {
51
+ frontmatter.tools = 'Read, Glob, Grep'; // Safe defaults - read-only
52
+ }
57
53
  // Map model if specified
58
54
  if (agent.default_models?.anthropic) {
59
55
  const modelAlias = (0, utils_1.mapModelToAlias)(agent.default_models.anthropic);
@@ -123,6 +123,133 @@ function main() {
123
123
 
124
124
  main();
125
125
  `;
126
+ const ALWAYS_ON_TEMPLATE_PY = `"""
127
+ orchagent always-on service entrypoint.
128
+
129
+ Runs a long-lived HTTP server that handles requests over HTTP.
130
+ This is the standard pattern for always-on services on orchagent.
131
+
132
+ IMPORTANT: Port 8080 is reserved by the platform health server.
133
+ Use a different port (default: 3000).
134
+
135
+ Local development:
136
+ python main.py
137
+ """
138
+
139
+ import json
140
+ import os
141
+ from http.server import HTTPServer, BaseHTTPRequestHandler
142
+
143
+ PORT = int(os.environ.get("PORT", "3000"))
144
+
145
+
146
+ class Handler(BaseHTTPRequestHandler):
147
+ def do_POST(self):
148
+ content_length = int(self.headers.get("Content-Length", 0))
149
+ body = self.rfile.read(content_length)
150
+ try:
151
+ data = json.loads(body) if body else {}
152
+ except json.JSONDecodeError:
153
+ self._respond(400, {"error": "Invalid JSON"})
154
+ return
155
+
156
+ user_input = data.get("input", "")
157
+
158
+ # --- Your logic here ---
159
+ # To use workspace secrets, add them to "required_secrets" in orchagent.json:
160
+ # "required_secrets": ["MY_API_KEY"]
161
+ # Then access via: os.environ["MY_API_KEY"]
162
+ result = f"Received: {user_input}"
163
+ # --- End your logic ---
164
+
165
+ self._respond(200, {"result": result})
166
+
167
+ def do_GET(self):
168
+ if self.path == "/health":
169
+ self._respond(200, {"status": "ok"})
170
+ return
171
+ self._respond(200, {"status": "running"})
172
+
173
+ def _respond(self, code, body):
174
+ self.send_response(code)
175
+ self.send_header("Content-Type", "application/json")
176
+ self.end_headers()
177
+ self.wfile.write(json.dumps(body).encode())
178
+
179
+ def log_message(self, format, *args):
180
+ print(f"[{self.log_date_time_string()}] {format % args}")
181
+
182
+
183
+ if __name__ == "__main__":
184
+ server = HTTPServer(("0.0.0.0", PORT), Handler)
185
+ print(f"Always-on service listening on port {PORT}")
186
+ server.serve_forever()
187
+ `;
188
+ const ALWAYS_ON_TEMPLATE_JS = `/**
189
+ * orchagent always-on service entrypoint.
190
+ *
191
+ * Runs a long-lived HTTP server that handles requests over HTTP.
192
+ * This is the standard pattern for always-on services on orchagent.
193
+ *
194
+ * IMPORTANT: Port 8080 is reserved by the platform health server.
195
+ * Use a different port (default: 3000).
196
+ *
197
+ * Local development:
198
+ * node main.js
199
+ */
200
+
201
+ const http = require('http');
202
+
203
+ const PORT = parseInt(process.env.PORT || '3000', 10);
204
+
205
+ const server = http.createServer((req, res) => {
206
+ if (req.method === 'GET') {
207
+ if (req.url === '/health') {
208
+ res.writeHead(200, { 'Content-Type': 'application/json' });
209
+ res.end(JSON.stringify({ status: 'ok' }));
210
+ return;
211
+ }
212
+ res.writeHead(200, { 'Content-Type': 'application/json' });
213
+ res.end(JSON.stringify({ status: 'running' }));
214
+ return;
215
+ }
216
+
217
+ if (req.method === 'POST') {
218
+ let body = '';
219
+ req.on('data', chunk => { body += chunk; });
220
+ req.on('end', () => {
221
+ let data;
222
+ try {
223
+ data = body ? JSON.parse(body) : {};
224
+ } catch {
225
+ res.writeHead(400, { 'Content-Type': 'application/json' });
226
+ res.end(JSON.stringify({ error: 'Invalid JSON' }));
227
+ return;
228
+ }
229
+
230
+ const input = data.input || '';
231
+
232
+ // --- Your logic here ---
233
+ // To use workspace secrets, add them to "required_secrets" in orchagent.json:
234
+ // "required_secrets": ["MY_API_KEY"]
235
+ // Then access via: process.env.MY_API_KEY
236
+ const result = \`Received: \${input}\`;
237
+ // --- End your logic ---
238
+
239
+ res.writeHead(200, { 'Content-Type': 'application/json' });
240
+ res.end(JSON.stringify({ result }));
241
+ });
242
+ return;
243
+ }
244
+
245
+ res.writeHead(405, { 'Content-Type': 'application/json' });
246
+ res.end(JSON.stringify({ error: 'Method not allowed' }));
247
+ });
248
+
249
+ server.listen(PORT, '0.0.0.0', () => {
250
+ console.log(\`Always-on service listening on port \${PORT}\`);
251
+ });
252
+ `;
126
253
  const DISCORD_MAIN_JS = `/**
127
254
  * Discord bot agent — powered by Claude.
128
255
  *
@@ -266,6 +393,7 @@ DISCORD_CHANNEL_IDS=
266
393
  # MODEL=claude-sonnet-4-5-20250929
267
394
  # MAX_TOKENS=1024
268
395
  `;
396
+ const AGENT_BUILDER_HINT = `\n Tip: orch skill install orchagent-public/agent-builder — gives your AI the full platform builder reference\n`;
269
397
  function readmeTemplate(agentName, flavor) {
270
398
  if (flavor === 'support_agent') {
271
399
  return `# ${agentName}
@@ -852,6 +980,11 @@ function registerInitCommand(program) {
852
980
  throw new errors_1.CliError('JavaScript agent-type agents are not yet supported. Use --type tool for JavaScript agents.');
853
981
  }
854
982
  // JS orchestrators are now supported via the orchagent-sdk npm package
983
+ // Block --language for types that don't create runtime files
984
+ if (isJavaScript && (initMode.type === 'prompt' || initMode.type === 'skill')) {
985
+ throw new errors_1.CliError(`The --language flag has no effect for ${initMode.type} types (no runtime files are created). ` +
986
+ 'Use --type tool or --type agent to create a project with runtime scaffolding.');
987
+ }
855
988
  if (options.template) {
856
989
  const template = options.template.trim().toLowerCase();
857
990
  const validTemplates = ['support-agent', 'discord', 'discord-js', 'github-weekly-summary'];
@@ -988,7 +1121,7 @@ function registerInitCommand(program) {
988
1121
  process.stdout.write(` ${s + 2}. Copy .env.example to .env and add platform tokens\n`);
989
1122
  process.stdout.write(` ${s + 3}. Test locally: pip install -r requirements.txt && python main.py\n`);
990
1123
  process.stdout.write(` ${s + 4}. Deploy: orch publish && orch service deploy\n`);
991
- process.stdout.write(`\n Skill: orch skill install orchagent-public/agent-builder — gives your AI the full platform builder reference\n`);
1124
+ process.stdout.write(AGENT_BUILDER_HINT);
992
1125
  return;
993
1126
  }
994
1127
  // Handle github-weekly-summary template separately (own file set + output)
@@ -1044,7 +1177,7 @@ function registerInitCommand(program) {
1044
1177
  process.stdout.write(` ${s + 3}. orch run <org>/${agentName} Test it\n`);
1045
1178
  process.stdout.write(` ${s + 4}. orch schedule create <org>/${agentName} --cron "0 9 * * 1" Schedule weekly\n`);
1046
1179
  process.stdout.write(`\n See README.md for full setup guide.\n`);
1047
- process.stdout.write(`\n Skill: orch skill install orchagent-public/agent-builder — gives your AI the full platform builder reference\n`);
1180
+ process.stdout.write(AGENT_BUILDER_HINT);
1048
1181
  return;
1049
1182
  }
1050
1183
  // Handle discord-js template separately (JS Discord bot)
@@ -1094,7 +1227,7 @@ function registerInitCommand(program) {
1094
1227
  process.stdout.write(` ${stepNum + 2}. Copy .env.example to .env and fill in your tokens\n`);
1095
1228
  process.stdout.write(` ${stepNum + 3}. Test locally: npm install && node main.js\n`);
1096
1229
  process.stdout.write(` ${stepNum + 4}. Deploy: orch publish\n`);
1097
- process.stdout.write(`\n Skill: orch skill install orchagent-public/agent-builder — gives your AI the full platform builder reference\n`);
1230
+ process.stdout.write(AGENT_BUILDER_HINT);
1098
1231
  return;
1099
1232
  }
1100
1233
  const manifestPath = path_1.default.join(targetDir, 'orchagent.json');
@@ -1182,7 +1315,7 @@ function registerInitCommand(program) {
1182
1315
  }
1183
1316
  else if (initMode.flavor === 'code_runtime') {
1184
1317
  if (isJavaScript) {
1185
- await promises_1.default.writeFile(path_1.default.join(targetDir, 'main.js'), CODE_TEMPLATE_JS);
1318
+ await promises_1.default.writeFile(path_1.default.join(targetDir, 'main.js'), runMode === 'always_on' ? ALWAYS_ON_TEMPLATE_JS : CODE_TEMPLATE_JS);
1186
1319
  await promises_1.default.writeFile(path_1.default.join(targetDir, 'package.json'), JSON.stringify({
1187
1320
  name: agentName,
1188
1321
  private: true,
@@ -1191,7 +1324,7 @@ function registerInitCommand(program) {
1191
1324
  }, null, 2) + '\n');
1192
1325
  }
1193
1326
  else {
1194
- await promises_1.default.writeFile(path_1.default.join(targetDir, 'main.py'), CODE_TEMPLATE_PY);
1327
+ await promises_1.default.writeFile(path_1.default.join(targetDir, 'main.py'), runMode === 'always_on' ? ALWAYS_ON_TEMPLATE_PY : CODE_TEMPLATE_PY);
1195
1328
  }
1196
1329
  await promises_1.default.writeFile(schemaPath, SCHEMA_TEMPLATE);
1197
1330
  }
@@ -1226,12 +1359,13 @@ function registerInitCommand(program) {
1226
1359
  process.stdout.write(` ${prefix}.env.example - Environment variables template\n`);
1227
1360
  }
1228
1361
  else if (initMode.flavor === 'code_runtime') {
1362
+ const entrypointDesc = runMode === 'always_on' ? 'Always-on HTTP server' : 'Agent entrypoint (stdin/stdout JSON)';
1229
1363
  if (isJavaScript) {
1230
- process.stdout.write(` ${prefix}main.js - Agent entrypoint (stdin/stdout JSON)\n`);
1364
+ process.stdout.write(` ${prefix}main.js - ${entrypointDesc}\n`);
1231
1365
  process.stdout.write(` ${prefix}package.json - npm dependencies\n`);
1232
1366
  }
1233
1367
  else {
1234
- process.stdout.write(` ${prefix}main.py - Agent entrypoint (stdin/stdout JSON)\n`);
1368
+ process.stdout.write(` ${prefix}main.py - ${entrypointDesc}\n`);
1235
1369
  }
1236
1370
  }
1237
1371
  else {
@@ -1274,7 +1408,15 @@ function registerInitCommand(program) {
1274
1408
  if (name) {
1275
1409
  process.stdout.write(` 1. cd ${name}\n`);
1276
1410
  }
1277
- if (isJavaScript) {
1411
+ if (runMode === 'always_on') {
1412
+ const mainFile = isJavaScript ? 'main.js' : 'main.py';
1413
+ const testCmd = isJavaScript ? 'node main.js' : 'python main.py';
1414
+ process.stdout.write(` ${stepNum}. Edit ${mainFile} with your service logic\n`);
1415
+ process.stdout.write(` ${stepNum + 1}. Test locally: ${testCmd}\n`);
1416
+ process.stdout.write(` ${stepNum + 2}. Publish: orch publish\n`);
1417
+ process.stdout.write(` ${stepNum + 3}. Deploy: orch service deploy\n`);
1418
+ }
1419
+ else if (isJavaScript) {
1278
1420
  process.stdout.write(` ${stepNum}. Edit main.js with your agent logic\n`);
1279
1421
  process.stdout.write(` ${stepNum + 1}. Edit schema.json with your input/output schemas\n`);
1280
1422
  process.stdout.write(` ${stepNum + 2}. Test: echo '{"input": "test"}' | node main.js\n`);
@@ -1296,6 +1438,6 @@ function registerInitCommand(program) {
1296
1438
  process.stdout.write(` ${stepNum + 1}. Edit schema.json with your input/output schemas\n`);
1297
1439
  process.stdout.write(` ${stepNum + 2}. Run: orchagent publish\n`);
1298
1440
  }
1299
- process.stdout.write(`\n Skill: orch skill install orchagent-public/agent-builder — gives your AI the full platform builder reference\n`);
1441
+ process.stdout.write(AGENT_BUILDER_HINT);
1300
1442
  });
1301
1443
  }
@@ -56,10 +56,14 @@ function formatDuration(ms) {
56
56
  return `${(ms / 1000).toFixed(1)}s`;
57
57
  return `${(ms / 60000).toFixed(1)}m`;
58
58
  }
59
- /** Detect if a string looks like a UUID (run ID) */
59
+ /** Detect if a string looks like a full UUID (run ID) */
60
60
  function isUuid(value) {
61
61
  return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value);
62
62
  }
63
+ /** Detect if a string looks like a short UUID prefix (8+ hex chars) */
64
+ function isShortUuid(value) {
65
+ return /^[0-9a-f]{7,}$/i.test(value) && !value.includes('/');
66
+ }
63
67
  // ============================================
64
68
  // COMMAND REGISTRATION
65
69
  // ============================================
@@ -77,16 +81,38 @@ function registerLogsCommand(program) {
77
81
  throw new errors_1.CliError('Missing API key. Run `orch login` first.');
78
82
  }
79
83
  const workspaceId = await resolveWorkspaceId(config, options.workspace);
80
- // If target looks like a UUID, show detailed logs for that run
84
+ // If target looks like a UUID (full or short prefix), show detailed logs for that run
81
85
  if (target && isUuid(target)) {
82
86
  await showRunLogs(config, workspaceId, target, options.json);
83
87
  return;
84
88
  }
85
- // Otherwise list runs, optionally filtered by agent name
86
- await listRuns(config, workspaceId, target, options);
89
+ if (target && isShortUuid(target)) {
90
+ // Short UUID prefix — find the matching run from the list
91
+ const fullId = await resolveShortRunId(config, workspaceId, target);
92
+ await showRunLogs(config, workspaceId, fullId, options.json);
93
+ return;
94
+ }
95
+ // Otherwise list runs, optionally filtered by agent name.
96
+ // Strip org prefix if provided (e.g. "joe/my-agent" → "my-agent")
97
+ const agentFilter = target?.includes('/') ? target.split('/').pop() : target;
98
+ await listRuns(config, workspaceId, agentFilter, options);
87
99
  });
88
100
  }
89
101
  // ============================================
102
+ // SHORT RUN ID RESOLUTION
103
+ // ============================================
104
+ async function resolveShortRunId(config, workspaceId, shortId) {
105
+ // Server-side prefix matching — searches ALL runs, not just the last 200
106
+ const result = await (0, api_1.request)(config, 'GET', `/workspaces/${workspaceId}/runs?limit=200&run_id_prefix=${encodeURIComponent(shortId)}`);
107
+ if (result.runs.length === 0) {
108
+ throw new errors_1.CliError(`No run found matching '${shortId}'.`);
109
+ }
110
+ if (result.runs.length > 1) {
111
+ throw new errors_1.CliError(`Ambiguous run ID '${shortId}' — matches ${result.runs.length} runs. Use more characters to narrow it down.`);
112
+ }
113
+ return result.runs[0].id;
114
+ }
115
+ // ============================================
90
116
  // LIST RUNS
91
117
  // ============================================
92
118
  async function listRuns(config, workspaceId, agentName, options) {
@@ -161,6 +187,16 @@ async function showRunLogs(config, workspaceId, runId, json) {
161
187
  const exitLabel = result.exit_code === 0 ? chalk_1.default.green(String(result.exit_code)) : chalk_1.default.red(String(result.exit_code));
162
188
  process.stdout.write(`Exit code: ${exitLabel}\n`);
163
189
  }
190
+ // Input data
191
+ if (result.input_data != null && Object.keys(result.input_data).length > 0) {
192
+ process.stdout.write('\n' + chalk_1.default.bold.blue('--- input ---') + '\n' +
193
+ JSON.stringify(result.input_data, null, 2) + '\n');
194
+ }
195
+ // Output data
196
+ if (result.output_data != null && Object.keys(result.output_data).length > 0) {
197
+ process.stdout.write('\n' + chalk_1.default.bold.green('--- output ---') + '\n' +
198
+ JSON.stringify(result.output_data, null, 2) + '\n');
199
+ }
164
200
  // Error message
165
201
  if (result.error_message) {
166
202
  process.stdout.write('\n' + chalk_1.default.red.bold('Error:\n') + chalk_1.default.red(result.error_message) + '\n');
@@ -39,6 +39,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
39
39
  exports.extractTemplateVariables = extractTemplateVariables;
40
40
  exports.deriveInputSchema = deriveInputSchema;
41
41
  exports.scanUndeclaredEnvVars = scanUndeclaredEnvVars;
42
+ exports.scanReservedPort = scanReservedPort;
42
43
  exports.detectSdkCompatible = detectSdkCompatible;
43
44
  exports.checkDependencies = checkDependencies;
44
45
  exports.registerPublishCommand = registerPublishCommand;
@@ -167,6 +168,68 @@ async function scanUndeclaredEnvVars(agentDir, requiredSecrets) {
167
168
  // Return env vars that are referenced but not declared or auto-injected
168
169
  return [...found].filter(v => !declared.has(v) && !autoInjected.has(v)).sort();
169
170
  }
171
+ /**
172
+ * Scan code files for patterns that bind to port 8080 — the platform reserves
173
+ * this port for the always-on service health server. Returns true if a likely
174
+ * port-8080 binding is detected.
175
+ */
176
+ async function scanReservedPort(agentDir) {
177
+ // Patterns that indicate binding to port 8080 in Python / JS / TS
178
+ const pyPatterns = [
179
+ /\.listen\s*\(\s*8080\b/, // server.listen(8080
180
+ /\.run\s*\([^)]*port\s*=\s*8080\b/, // app.run(port=8080
181
+ /\.run\s*\([^)]*host\s*=\s*['"][^'"]*['"],?\s*8080\b/, // app.run("0.0.0.0", 8080
182
+ /bind\s*\(\s*\(?['"][^'"]*['"]\s*,\s*8080\b/, // bind(("0.0.0.0", 8080
183
+ /PORT\s*=\s*8080\b/, // PORT = 8080
184
+ /port\s*=\s*8080\b/, // port=8080
185
+ ];
186
+ const jsPatterns = [
187
+ /\.listen\s*\(\s*8080\b/, // app.listen(8080
188
+ /port\s*[:=]\s*8080\b/, // port: 8080 or port = 8080
189
+ /PORT\s*[:=]\s*8080\b/, // PORT = 8080
190
+ ];
191
+ async function scanDir(dir, depth) {
192
+ let entries;
193
+ try {
194
+ entries = await promises_1.default.readdir(dir, { withFileTypes: true });
195
+ if (!entries || !Array.isArray(entries))
196
+ return false;
197
+ }
198
+ catch {
199
+ return false;
200
+ }
201
+ for (const entry of entries) {
202
+ const name = entry.name;
203
+ const fullPath = path_1.default.join(dir, name);
204
+ if (entry.isDirectory() && depth < 2 && !name.startsWith('.') && name !== 'node_modules' && name !== '__pycache__' && name !== 'venv' && name !== '.venv') {
205
+ if (await scanDir(fullPath, depth + 1))
206
+ return true;
207
+ }
208
+ else if (entry.isFile() && name.endsWith('.py')) {
209
+ try {
210
+ const content = await promises_1.default.readFile(fullPath, 'utf-8');
211
+ for (const re of pyPatterns) {
212
+ if (re.test(content))
213
+ return true;
214
+ }
215
+ }
216
+ catch { /* skip */ }
217
+ }
218
+ else if (entry.isFile() && (name.endsWith('.js') || name.endsWith('.ts'))) {
219
+ try {
220
+ const content = await promises_1.default.readFile(fullPath, 'utf-8');
221
+ for (const re of jsPatterns) {
222
+ if (re.test(content))
223
+ return true;
224
+ }
225
+ }
226
+ catch { /* skip */ }
227
+ }
228
+ }
229
+ return false;
230
+ }
231
+ return scanDir(agentDir, 0);
232
+ }
170
233
  /**
171
234
  * Check if orchagent-sdk is listed in requirements.txt or pyproject.toml
172
235
  */
@@ -319,11 +382,8 @@ function inferExecutionEngineFromManifest(manifest, rawType) {
319
382
  return 'managed_loop';
320
383
  if (rawType === 'tool' || rawType === 'code')
321
384
  return 'code_runtime';
322
- if (rawType === 'agentic')
323
- return 'managed_loop';
324
- if (rawType === 'agent' && (manifest.custom_tools?.length || manifest.max_turns)) {
385
+ if (rawType === 'agentic' || rawType === 'agent')
325
386
  return 'managed_loop';
326
- }
327
387
  return 'direct_llm';
328
388
  }
329
389
  function commandForEntrypoint(entrypoint) {
@@ -485,6 +545,7 @@ function registerPublishCommand(program) {
485
545
  description: skillData.frontmatter.description,
486
546
  prompt: skillData.body,
487
547
  is_public: false,
548
+ callable: false,
488
549
  supported_providers: ['any'],
489
550
  default_skills: skillsFromFlag,
490
551
  skills_locked: options.skillsLocked || undefined,
@@ -542,7 +603,7 @@ function registerPublishCommand(program) {
542
603
  const { canonicalType, rawType } = canonicalizeManifestType(manifest.type);
543
604
  const runMode = normalizeRunMode(manifest.run_mode);
544
605
  const executionEngine = inferExecutionEngineFromManifest(manifest, rawType);
545
- const callable = Boolean(manifest.callable);
606
+ const callable = manifest.callable !== undefined ? Boolean(manifest.callable) : true;
546
607
  if (canonicalType === 'skill') {
547
608
  throw new errors_1.CliError("Use SKILL.md for publishing skills. Remove orchagent.json and run 'orchagent publish' from a skill directory.");
548
609
  }
@@ -767,12 +828,13 @@ function registerPublishCommand(program) {
767
828
  ` Publish each dependency first, then re-run this publish.\n\n`);
768
829
  }
769
830
  if (notCallable.length > 0) {
770
- process.stderr.write(chalk_1.default.yellow(`\n⚠ Dependencies not marked as callable:\n`));
831
+ process.stderr.write(chalk_1.default.yellow(`\n⚠ Dependencies have callable: false:\n`));
771
832
  for (const dep of notCallable) {
772
833
  process.stderr.write(chalk_1.default.yellow(` - ${dep.ref}\n`));
773
834
  }
774
- process.stderr.write(`\n Agents must have callable: true in orchagent.json to be invoked\n` +
775
- ` by orchestrators. Update and republish each dependency.\n\n`);
835
+ process.stderr.write(`\n These agents have explicitly set callable: false, which blocks\n` +
836
+ ` agent-to-agent calls at runtime. Set callable: true (or remove\n` +
837
+ ` the field to use the default) and republish each dependency.\n\n`);
776
838
  }
777
839
  }
778
840
  // Handle dry-run for agents
@@ -883,18 +945,35 @@ function registerPublishCommand(program) {
883
945
  ` (Platform-injected vars like LLM API keys are already excluded.)\n\n`);
884
946
  }
885
947
  }
948
+ // Warn if always_on code binds to port 8080 (reserved for platform health server)
949
+ if (runMode === 'always_on' && executionEngine === 'code_runtime') {
950
+ const usesReservedPort = await scanReservedPort(cwd);
951
+ if (usesReservedPort) {
952
+ process.stderr.write(chalk_1.default.yellow(`\n⚠ Your code appears to bind to port 8080, which is reserved by the\n`) +
953
+ chalk_1.default.yellow(` platform health server for always-on services.\n\n`) +
954
+ ` Your service will crash with EADDRINUSE at runtime.\n` +
955
+ ` Use a different port (e.g. 3000) or read the ORCHAGENT_HEALTH_PORT\n` +
956
+ ` env var to detect the reserved port.\n\n`);
957
+ }
958
+ }
886
959
  // C-1: Block publish if tool/agent type has no required_secrets declared.
887
960
  // Prompt and skill types are exempt (prompt agents get LLM keys from platform,
888
961
  // skills don't run standalone).
962
+ // An explicit empty array (required_secrets: []) is a valid declaration
963
+ // meaning "this agent deliberately needs no secrets."
889
964
  if ((canonicalType === 'tool' || canonicalType === 'agent') &&
890
- (!manifest.required_secrets || manifest.required_secrets.length === 0) &&
965
+ manifest.required_secrets === undefined &&
891
966
  options.requiredSecrets !== false) {
892
967
  process.stderr.write(chalk_1.default.red(`\nError: ${canonicalType} agents must declare required_secrets in orchagent.json.\n\n`) +
893
968
  ` Add the env vars your code needs at runtime:\n` +
894
969
  ` ${chalk_1.default.cyan('"required_secrets": ["ANTHROPIC_API_KEY", "MY_TOKEN"]')}\n\n` +
970
+ ` If this agent genuinely needs no secrets, add an empty array:\n` +
971
+ ` ${chalk_1.default.cyan('"required_secrets": []')}\n\n` +
895
972
  ` These are matched by name against your workspace secrets vault.\n` +
896
973
  ` Use ${chalk_1.default.cyan('--no-required-secrets')} to skip this check.\n`);
897
- throw new errors_1.CliError('Missing required_secrets declaration', errors_1.ExitCodes.INVALID_INPUT);
974
+ const err = new errors_1.CliError('Missing required_secrets declaration', errors_1.ExitCodes.INVALID_INPUT);
975
+ err.displayed = true;
976
+ throw err;
898
977
  }
899
978
  // Create the agent (server auto-assigns version)
900
979
  let result;
@@ -1125,6 +1204,5 @@ function registerPublishCommand(program) {
1125
1204
  process.stdout.write(`\nNote: Hosted code execution is in beta. Contact support for full deployment.\n`);
1126
1205
  }
1127
1206
  process.stdout.write(`\nView analytics and usage: https://orchagent.io/dashboard\n`);
1128
- process.stdout.write(`\nSkill: orch skill install orchagent-public/agent-builder — gives your AI the full platform builder reference\n`);
1129
1207
  });
1130
1208
  }
@@ -29,7 +29,13 @@ function parsePullRef(value) {
29
29
  }
30
30
  function canonicalType(typeValue) {
31
31
  const normalized = (typeValue || 'agent').toLowerCase();
32
- return normalized === 'skill' ? 'skill' : 'agent';
32
+ if (['prompt', 'tool', 'agent', 'skill'].includes(normalized))
33
+ return normalized;
34
+ if (normalized === 'code')
35
+ return 'tool';
36
+ if (normalized === 'agentic')
37
+ return 'agent';
38
+ return 'agent';
33
39
  }
34
40
  function resolveEngine(data) {
35
41
  const ee = data.execution_engine;
@@ -200,7 +206,7 @@ function buildManifest(data) {
200
206
  const manifest = {
201
207
  name: data.name,
202
208
  description: data.description || '',
203
- type: canonicalType(data.type) === 'skill' ? 'skill' : 'agent',
209
+ type: canonicalType(data.type),
204
210
  };
205
211
  if (data.run_mode)
206
212
  manifest.run_mode = data.run_mode;
@@ -675,7 +675,7 @@ async function executePromptLocally(agentData, inputData, skillPrompts = [], con
675
675
  if (allProviders.length === 0) {
676
676
  const providers = providersToCheck.join(', ');
677
677
  throw new errors_1.CliError(`No LLM key found for: ${providers}\n` +
678
- `Set an environment variable (e.g., OPENAI_API_KEY), run 'orchagent keys add <provider>', or configure in web dashboard`);
678
+ `Set an environment variable (e.g., OPENAI_API_KEY), run 'orch secrets set <PROVIDER>_API_KEY <key>', or configure in web dashboard`);
679
679
  }
680
680
  if (modelOverride && !providerOverride && allProviders.length > 1) {
681
681
  process.stderr.write(`Warning: --model specified without --provider. The model '${modelOverride}' will be used for all ${allProviders.length} fallback providers, which may cause errors if the model is incompatible.\n` +
@@ -702,7 +702,7 @@ async function executePromptLocally(agentData, inputData, skillPrompts = [], con
702
702
  if (!detected) {
703
703
  const providers = providersToCheck.join(', ');
704
704
  throw new errors_1.CliError(`No LLM key found for: ${providers}\n` +
705
- `Set an environment variable (e.g., OPENAI_API_KEY), run 'orchagent keys add <provider>', or configure in web dashboard`);
705
+ `Set an environment variable (e.g., OPENAI_API_KEY), run 'orch secrets set <PROVIDER>_API_KEY <key>', or configure in web dashboard`);
706
706
  }
707
707
  const { provider, key, model: serverModel } = detected;
708
708
  const model = modelOverride || serverModel || agentData.default_models?.[provider] || (0, llm_1.getDefaultModel)(provider);
@@ -736,7 +736,7 @@ async function executeAgentLocally(agentDir, prompt, inputData, outputSchema, cu
736
736
  if (allProviders.length === 0) {
737
737
  const providers = providersToCheck.join(', ');
738
738
  throw new errors_1.CliError(`No LLM key found for: ${providers}\n` +
739
- `Set an environment variable (e.g., ANTHROPIC_API_KEY), run 'orchagent keys add <provider>', or configure in web dashboard`);
739
+ `Set an environment variable (e.g., ANTHROPIC_API_KEY), run 'orch secrets set <PROVIDER>_API_KEY <key>', or configure in web dashboard`);
740
740
  }
741
741
  const primary = allProviders[0];
742
742
  const model = modelOverride || primary.model || (0, llm_1.getDefaultModel)(primary.provider);
@@ -1775,12 +1775,6 @@ async function executeCloud(agentRef, file, options) {
1775
1775
  ...(options.model && { model: options.model }),
1776
1776
  };
1777
1777
  }
1778
- else if (cloudEngine !== 'code_runtime') {
1779
- const searchedProviders = effectiveProvider ? [effectiveProvider] : supportedProviders;
1780
- const providerList = searchedProviders.join(', ');
1781
- process.stderr.write(`Warning: No LLM key found for provider(s): ${providerList}\n` +
1782
- `Set an env var (e.g., OPENAI_API_KEY), run 'orchagent keys add <provider>', use --key, or configure in web dashboard\n\n`);
1783
- }
1784
1778
  if (options.skills) {
1785
1779
  headers['X-OrchAgent-Skills'] = options.skills;
1786
1780
  }
@@ -2027,9 +2021,20 @@ async function executeCloud(agentRef, file, options) {
2027
2021
  const hint = typeof payload === 'object' && payload
2028
2022
  ? payload.error?.hint
2029
2023
  : undefined;
2024
+ // Detect platform errors that surface as SANDBOX_ERROR (BUG-11)
2025
+ const lowerMessage = (message || '').toLowerCase();
2026
+ const isPlatformError = /\b403\b/.test(message || '') ||
2027
+ /\b401\b/.test(message || '') ||
2028
+ lowerMessage.includes('proxy token') ||
2029
+ lowerMessage.includes('orchagent_service_key') ||
2030
+ lowerMessage.includes('orchagent_billing_org');
2031
+ const attribution = isPlatformError
2032
+ ? `This may be a platform configuration issue, not an error in the agent's code.\n` +
2033
+ `If this persists, contact support with the ref below.`
2034
+ : `This is an error in the agent's code, not the platform.\n` +
2035
+ `Check the agent code and requirements, then republish.`;
2030
2036
  throw new errors_1.CliError(`${message}\n\n` +
2031
- `This is an error in the agent's code, not the platform.\n` +
2032
- `Check the agent code and requirements, then republish.` +
2037
+ attribution +
2033
2038
  (hint ? `\n\nHint: ${hint}` : '') +
2034
2039
  refSuffix);
2035
2040
  }
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.registerScheduleCommand = registerScheduleCommand;
7
7
  const cli_table3_1 = __importDefault(require("cli-table3"));
8
8
  const chalk_1 = __importDefault(require("chalk"));
9
+ const promises_1 = __importDefault(require("readline/promises"));
9
10
  const config_1 = require("../lib/config");
10
11
  const api_1 = require("../lib/api");
11
12
  const errors_1 = require("../lib/errors");
@@ -64,7 +65,8 @@ async function resolveScheduleId(config, partialId, workspaceId) {
64
65
  function registerScheduleCommand(program) {
65
66
  const schedule = program
66
67
  .command('schedule')
67
- .description('Manage scheduled agent runs (cron and webhooks)');
68
+ .description('Manage scheduled agent runs (cron and webhooks)')
69
+ .action(() => { schedule.help(); });
68
70
  // orch schedule list
69
71
  schedule
70
72
  .command('list')
@@ -282,6 +284,7 @@ function registerScheduleCommand(program) {
282
284
  schedule
283
285
  .command('delete <schedule-id>')
284
286
  .description('Delete a schedule')
287
+ .option('-y, --yes', 'Skip confirmation prompt')
285
288
  .option('--workspace <slug>', 'Workspace slug (default: current workspace)')
286
289
  .action(async (scheduleId, options) => {
287
290
  const config = await (0, config_1.getResolvedConfig)();
@@ -289,6 +292,18 @@ function registerScheduleCommand(program) {
289
292
  throw new errors_1.CliError('Missing API key. Run `orch login` first.');
290
293
  }
291
294
  const workspaceId = await resolveWorkspaceId(config, options.workspace);
295
+ if (!options.yes) {
296
+ const rl = promises_1.default.createInterface({
297
+ input: process.stdin,
298
+ output: process.stdout,
299
+ });
300
+ const answer = await rl.question(`Delete schedule ${scheduleId}? (y/N): `);
301
+ rl.close();
302
+ if (answer.trim().toLowerCase() !== 'y' && answer.trim().toLowerCase() !== 'yes') {
303
+ process.stdout.write('Cancelled.\n');
304
+ return;
305
+ }
306
+ }
292
307
  await (0, api_1.request)(config, 'DELETE', `/workspaces/${workspaceId}/schedules/${scheduleId}`);
293
308
  process.stdout.write(chalk_1.default.green('\u2713') + ` Schedule ${scheduleId} deleted\n`);
294
309
  });
@@ -338,6 +353,7 @@ function registerScheduleCommand(program) {
338
353
  .command('info <schedule-id>')
339
354
  .description('Show detailed schedule information with recent runs and events')
340
355
  .option('--workspace <slug>', 'Workspace slug (default: current workspace)')
356
+ .option('--reveal', 'Show full webhook URL (for webhook schedules)')
341
357
  .option('--json', 'Output as JSON')
342
358
  .action(async (partialScheduleId, options) => {
343
359
  const config = await (0, config_1.getResolvedConfig)();
@@ -346,8 +362,9 @@ function registerScheduleCommand(program) {
346
362
  }
347
363
  const workspaceId = await resolveWorkspaceId(config, options.workspace);
348
364
  const scheduleId = await resolveScheduleId(config, partialScheduleId, workspaceId);
365
+ const revealParam = options.reveal ? '?reveal_webhook=true' : '';
349
366
  const [scheduleRes, runsRes, eventsRes] = await Promise.all([
350
- (0, api_1.request)(config, 'GET', `/workspaces/${workspaceId}/schedules/${scheduleId}`),
367
+ (0, api_1.request)(config, 'GET', `/workspaces/${workspaceId}/schedules/${scheduleId}${revealParam}`),
351
368
  (0, api_1.request)(config, 'GET', `/workspaces/${workspaceId}/schedules/${scheduleId}/runs?limit=5`),
352
369
  (0, api_1.request)(config, 'GET', `/workspaces/${workspaceId}/schedules/${scheduleId}/events?limit=5`),
353
370
  ]);
@@ -364,6 +381,12 @@ function registerScheduleCommand(program) {
364
381
  process.stdout.write(` Cron: ${s.cron_expression}\n`);
365
382
  process.stdout.write(` Timezone: ${s.timezone}\n`);
366
383
  }
384
+ if (s.webhook_url) {
385
+ process.stdout.write(` Webhook: ${s.webhook_url}\n`);
386
+ }
387
+ else if (s.schedule_type === 'webhook' && !options.reveal) {
388
+ process.stdout.write(` Webhook: ${chalk_1.default.gray('(redacted — use --reveal to show)')}\n`);
389
+ }
367
390
  process.stdout.write(` Enabled: ${s.enabled ? chalk_1.default.green('yes') : chalk_1.default.red('no')}\n`);
368
391
  process.stdout.write(` Auto-update: ${s.auto_update === false ? chalk_1.default.yellow('pinned') : chalk_1.default.green('yes')}\n`);
369
392
  if (s.auto_disabled_at) {
@@ -374,6 +397,10 @@ function registerScheduleCommand(program) {
374
397
  if (s.next_run_at) {
375
398
  process.stdout.write(` Next Run: ${formatDate(s.next_run_at)}\n`);
376
399
  }
400
+ if (s.input_data && Object.keys(s.input_data).length > 0) {
401
+ const inputStr = JSON.stringify(s.input_data);
402
+ process.stdout.write(` Input: ${inputStr.length > 100 ? inputStr.slice(0, 100) + '...' : inputStr}\n`);
403
+ }
377
404
  if (s.alert_webhook_url) {
378
405
  process.stdout.write(` Alert URL: ${s.alert_webhook_url.slice(0, 50)}...\n`);
379
406
  }
@@ -54,7 +54,8 @@ async function findSecretByName(config, workspaceId, name) {
54
54
  function registerSecretsCommand(program) {
55
55
  const secrets = program
56
56
  .command('secrets')
57
- .description('Manage workspace secrets (injected as env vars into agent sandboxes)');
57
+ .description('Manage workspace secrets (injected as env vars into agent sandboxes)')
58
+ .action(() => { secrets.help(); });
58
59
  // orch secrets list
59
60
  secrets
60
61
  .command('list')
@@ -60,6 +60,13 @@ function healthColor(health) {
60
60
  return chalk_1.default.gray(health);
61
61
  }
62
62
  }
63
+ function formatServiceUrl(svc) {
64
+ const url = svc.provider_url || svc.cloud_run_url || '-';
65
+ if (url !== '-' && svc.infrastructure_provider === 'flyio') {
66
+ return `${url} ${chalk_1.default.gray('(internal — not a public endpoint)')}`;
67
+ }
68
+ return url;
69
+ }
63
70
  function severityColor(severity, message) {
64
71
  switch (severity.toUpperCase()) {
65
72
  case 'ERROR':
@@ -184,7 +191,7 @@ function registerServiceCommand(program) {
184
191
  process.stdout.write(` ${chalk_1.default.bold('Name:')} ${svc.service_name}\n`);
185
192
  process.stdout.write(` ${chalk_1.default.bold('Agent:')} ${svc.agent_name}@${svc.agent_version}\n`);
186
193
  process.stdout.write(` ${chalk_1.default.bold('State:')} ${stateColor(svc.current_state)}\n`);
187
- process.stdout.write(` ${chalk_1.default.bold('URL:')} ${svc.provider_url || svc.cloud_run_url || '-'}\n`);
194
+ process.stdout.write(` ${chalk_1.default.bold('URL:')} ${formatServiceUrl(svc)}\n`);
188
195
  if (options.pin) {
189
196
  process.stdout.write(` ${chalk_1.default.bold('Pinned:')} ${chalk_1.default.yellow(`yes (won't auto-update on publish)`)}\n`);
190
197
  }
@@ -192,7 +199,7 @@ function registerServiceCommand(program) {
192
199
  process.stdout.write(chalk_1.default.gray(`View logs: orch service logs ${svc.id}\n`));
193
200
  }
194
201
  catch (e) {
195
- deploySpinner.fail('Deploy failed');
202
+ deploySpinner.stop();
196
203
  throw e;
197
204
  }
198
205
  });
@@ -348,7 +355,7 @@ function registerServiceCommand(program) {
348
355
  }
349
356
  process.stdout.write(` Instances: ${svc.min_instances}-${svc.max_instances}\n`);
350
357
  process.stdout.write(` Service ID: ${svc.provider_service_id || svc.cloud_run_service || '-'}\n`);
351
- process.stdout.write(` URL: ${svc.provider_url || svc.cloud_run_url || '-'}\n`);
358
+ process.stdout.write(` URL: ${formatServiceUrl(svc)}\n`);
352
359
  process.stdout.write(` Deployed: ${formatDate(svc.last_deployed_at)}\n`);
353
360
  process.stdout.write(` Last Restart: ${formatDate(svc.last_restart_at)}\n`);
354
361
  if (svc.last_error) {
@@ -204,18 +204,18 @@ async function downloadSkillWithFallback(config, org, skill, version, workspaceI
204
204
  }
205
205
  function registerSkillCommand(program) {
206
206
  const skill = program.command('skill').description('Manage and install skills');
207
- // orch skill list (deprecated)
207
+ skill.action(() => { skill.help(); });
208
+ // orch skill list
208
209
  skill
209
210
  .command('list')
210
- .description('(Deprecated) Use "orchagent search --type skills" instead')
211
+ .description('Browse available skills')
211
212
  .option('--json', 'Output raw JSON')
212
213
  .action(async () => {
213
- process.stdout.write('The "skill list" command has been replaced by "search".\n\n' +
214
- 'Usage:\n' +
215
- ' orchagent search <query> --type skills Search skills by keyword\n' +
216
- ' orchagent search --popular --type skills Top skills by stars\n' +
217
- ' orchagent search --recent --type skills Most recently published\n\n' +
218
- 'View all skills at: https://orchagent.io/explore\n');
214
+ process.stdout.write('Browse and discover skills at: https://orchagent.io/explore\n\n' +
215
+ 'Install a skill:\n' +
216
+ ' orch skill install <org>/<skill-name>\n\n' +
217
+ 'View installed skills:\n' +
218
+ ' orch update --check\n');
219
219
  process.exit(0);
220
220
  });
221
221
  // orch skill create [name]
@@ -514,7 +514,7 @@ async function runPromptFixtureTests(agentDir, fixtures, verbose, config) {
514
514
  const detected = await (0, llm_1.detectLlmKey)(['any'], config);
515
515
  if (!detected) {
516
516
  throw new errors_1.CliError('No LLM key found for fixture tests.\n' +
517
- 'Set an environment variable (e.g., OPENAI_API_KEY) or run `orchagent keys add <provider>`');
517
+ 'Set an environment variable (e.g., OPENAI_API_KEY) or run `orch secrets set <PROVIDER>_API_KEY <key>`');
518
518
  }
519
519
  const { provider, key, model: serverModel } = detected;
520
520
  const model = serverModel ?? (0, llm_1.getDefaultModel)(provider);
@@ -6,8 +6,10 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.checkNodeVersion = checkNodeVersion;
7
7
  exports.checkCliVersion = checkCliVersion;
8
8
  exports.checkGitAvailable = checkGitAvailable;
9
+ exports.checkDualInstallation = checkDualInstallation;
9
10
  exports.runEnvironmentChecks = runEnvironmentChecks;
10
11
  const child_process_1 = require("child_process");
12
+ const fs_1 = require("fs");
11
13
  const package_json_1 = __importDefault(require("../../../../package.json"));
12
14
  const update_notifier_1 = require("../../update-notifier");
13
15
  const REQUIRED_NODE_MAJOR = 18;
@@ -142,6 +144,148 @@ async function checkGitAvailable() {
142
144
  };
143
145
  }
144
146
  }
147
+ /**
148
+ * Find all binary paths for a given command name using `which -a`.
149
+ * Returns an empty array if the command is not found.
150
+ * Note: Uses execSync with a hardcoded binary name — no user input, safe from injection.
151
+ */
152
+ function findAllBinaryPaths(binaryName) {
153
+ try {
154
+ const output = (0, child_process_1.execSync)(`which -a ${binaryName}`, {
155
+ encoding: 'utf-8',
156
+ timeout: 5000,
157
+ stdio: ['pipe', 'pipe', 'pipe'],
158
+ });
159
+ return output.trim().split('\n').filter(Boolean);
160
+ }
161
+ catch {
162
+ return [];
163
+ }
164
+ }
165
+ /**
166
+ * Get the CLI version from a binary path by running it with --version.
167
+ * Uses execFileSync (no shell) to avoid injection via path characters.
168
+ */
169
+ function getVersionFromBinary(binPath) {
170
+ try {
171
+ const output = (0, child_process_1.execFileSync)(binPath, ['--version'], {
172
+ encoding: 'utf-8',
173
+ timeout: 5000,
174
+ stdio: ['pipe', 'pipe', 'pipe'],
175
+ });
176
+ const match = output.match(/(\d+\.\d+\.\d+)/);
177
+ return match ? match[1] : null;
178
+ }
179
+ catch {
180
+ return null;
181
+ }
182
+ }
183
+ /**
184
+ * Resolve a path through symlinks. Returns the original path if resolution fails.
185
+ */
186
+ function safeRealpathSync(p) {
187
+ try {
188
+ return (0, fs_1.realpathSync)(p);
189
+ }
190
+ catch {
191
+ return p;
192
+ }
193
+ }
194
+ /**
195
+ * Detect multiple CLI installations at different paths/versions (BUG-008).
196
+ *
197
+ * System-level (/usr/local/bin/orchagent) and user-level (~/.npm-global/bin/orch)
198
+ * installs can coexist silently. `npm update -g` updates one but not the other,
199
+ * leaving the user running an outdated version without knowing it.
200
+ */
201
+ async function checkDualInstallation() {
202
+ // Skip on Windows (which -a not available)
203
+ if (process.platform === 'win32') {
204
+ return {
205
+ category: 'environment',
206
+ name: 'dual_installation',
207
+ status: 'info',
208
+ message: 'Installation path check skipped (Windows)',
209
+ details: { skipped: true, reason: 'Windows not supported' },
210
+ };
211
+ }
212
+ try {
213
+ const binaryNames = ['orch', 'orchagent'];
214
+ const installations = new Map();
215
+ for (const binary of binaryNames) {
216
+ const paths = findAllBinaryPaths(binary);
217
+ for (const binPath of paths) {
218
+ const realPath = safeRealpathSync(binPath);
219
+ // Deduplicate by resolved real path
220
+ if (installations.has(realPath))
221
+ continue;
222
+ const version = getVersionFromBinary(binPath) || 'unknown';
223
+ installations.set(realPath, {
224
+ path: binPath,
225
+ realPath,
226
+ version,
227
+ binary,
228
+ });
229
+ }
230
+ }
231
+ if (installations.size <= 1) {
232
+ return {
233
+ category: 'environment',
234
+ name: 'dual_installation',
235
+ status: 'success',
236
+ message: 'Single CLI installation',
237
+ details: {
238
+ installationCount: installations.size,
239
+ installations: [...installations.values()],
240
+ },
241
+ };
242
+ }
243
+ // Multiple installations found
244
+ const allInstalls = [...installations.values()];
245
+ const versions = new Set(allInstalls.map((i) => i.version));
246
+ const versionsDiffer = versions.size > 1;
247
+ const pathList = allInstalls
248
+ .map((i) => `${i.path} (v${i.version})`)
249
+ .join(', ');
250
+ if (versionsDiffer) {
251
+ return {
252
+ category: 'environment',
253
+ name: 'dual_installation',
254
+ status: 'warning',
255
+ message: `Multiple CLI versions found: ${pathList}`,
256
+ fix: 'Remove the outdated installation. Run `which -a orch orchagent` to see all paths, then remove the older binary',
257
+ details: {
258
+ installationCount: installations.size,
259
+ versionMismatch: true,
260
+ installations: allInstalls,
261
+ },
262
+ };
263
+ }
264
+ // Same version at multiple paths — informational only
265
+ return {
266
+ category: 'environment',
267
+ name: 'dual_installation',
268
+ status: 'info',
269
+ message: `Multiple CLI paths (same version v${allInstalls[0].version}): ${pathList}`,
270
+ details: {
271
+ installationCount: installations.size,
272
+ versionMismatch: false,
273
+ installations: allInstalls,
274
+ },
275
+ };
276
+ }
277
+ catch (err) {
278
+ return {
279
+ category: 'environment',
280
+ name: 'dual_installation',
281
+ status: 'info',
282
+ message: 'Could not check for duplicate installations',
283
+ details: {
284
+ error: err instanceof Error ? err.message : 'unknown error',
285
+ },
286
+ };
287
+ }
288
+ }
145
289
  /**
146
290
  * Run all environment checks.
147
291
  */
@@ -150,6 +294,7 @@ async function runEnvironmentChecks() {
150
294
  checkNodeVersion(),
151
295
  checkCliVersion(),
152
296
  checkGitAvailable(),
297
+ checkDualInstallation(),
153
298
  ]);
154
299
  return results;
155
300
  }
@@ -54,7 +54,7 @@ function renderLlmSection(checks, verbose) {
54
54
  // Build location text
55
55
  let location;
56
56
  if (serverVal === null) {
57
- location = localVal ? 'Server unknown, local configured' : 'Server unknown, not local';
57
+ location = localVal ? 'Local configured (vault keys not checked)' : 'Not configured locally (vault keys used for cloud runs)';
58
58
  }
59
59
  else if (serverVal && localVal) {
60
60
  location = 'Configured (server + local)';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orchagent/cli",
3
- "version": "0.3.84",
3
+ "version": "0.3.85",
4
4
  "description": "Command-line interface for orchagent — deploy and run AI agents for your team",
5
5
  "license": "MIT",
6
6
  "author": "orchagent <hello@orchagent.io>",
@@ -25,7 +25,8 @@
25
25
  "node": ">=18"
26
26
  },
27
27
  "bin": {
28
- "orch": "dist/index.js"
28
+ "orch": "dist/index.js",
29
+ "orchagent": "dist/index.js"
29
30
  },
30
31
  "files": [
31
32
  "dist",