@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.
@@ -584,7 +584,7 @@ function registerPublishCommand(program) {
584
584
  .option('--skills-locked', 'Lock default skills (callers cannot override via headers)')
585
585
  .option('--docker', 'Include Dockerfile for custom environment (builds E2B template)')
586
586
  .option('--local-download', 'Allow users to download and run locally (default: server-only)')
587
- .option('--no-required-secrets', 'Skip required_secrets check for tool/agent types')
587
+ .option('--no-required-secrets', '(deprecated) No longer needed — required_secrets defaults to []')
588
588
  .option('--all', 'Publish all agents in subdirectories (dependency order)')
589
589
  .action(async (options) => {
590
590
  const cwd = process.cwd();
@@ -718,44 +718,62 @@ function registerPublishCommand(program) {
718
718
  }
719
719
  throw new errors_1.CliError(`Failed to read orchagent.json: ${err}`);
720
720
  }
721
+ // UX-1: Collect validation errors and report them all at once
722
+ const validationErrors = [];
721
723
  // Validate manifest
722
724
  if (!manifest.name) {
723
- throw new errors_1.CliError('orchagent.json must have name');
725
+ validationErrors.push('orchagent.json must have name');
724
726
  }
725
- // Validate agent name format (must match gateway rules)
726
- const agentNameRegex = /^[a-z0-9][a-z0-9-]*[a-z0-9]$/;
727
- const agentName = manifest.name;
728
- if (agentName.length < 2 || agentName.length > 50) {
729
- throw new errors_1.CliError('Agent name must be 2-50 characters');
730
- }
731
- if (agentName !== agentName.toLowerCase()) {
732
- throw new errors_1.CliError('Agent name must be lowercase');
733
- }
734
- if (agentName.length > 1 && !agentNameRegex.test(agentName)) {
735
- throw new errors_1.CliError('Agent name must contain only lowercase letters, numbers, and hyphens, and must start/end with a letter or number');
736
- }
737
- if (agentName.includes('--')) {
738
- throw new errors_1.CliError('Agent name must not contain consecutive hyphens');
727
+ else {
728
+ // Validate agent name format (must match gateway rules)
729
+ const agentNameRegex = /^[a-z0-9][a-z0-9-]*[a-z0-9]$/;
730
+ const agentName = manifest.name;
731
+ if (agentName.length < 2 || agentName.length > 50) {
732
+ validationErrors.push('Agent name must be 2-50 characters');
733
+ }
734
+ if (agentName !== agentName.toLowerCase()) {
735
+ validationErrors.push('Agent name must be lowercase');
736
+ }
737
+ if (agentName.length > 1 && !agentNameRegex.test(agentName)) {
738
+ validationErrors.push('Agent name must contain only lowercase letters, numbers, and hyphens, and must start/end with a letter or number');
739
+ }
740
+ if (agentName.includes('--')) {
741
+ validationErrors.push('Agent name must not contain consecutive hyphens');
742
+ }
739
743
  }
740
744
  const { canonicalType, rawType } = canonicalizeManifestType(manifest.type);
741
745
  const runMode = normalizeRunMode(manifest.run_mode);
742
746
  const executionEngine = inferExecutionEngineFromManifest(manifest, rawType);
743
747
  const callable = manifest.callable !== undefined ? Boolean(manifest.callable) : true;
744
748
  if (canonicalType === 'skill') {
745
- throw new errors_1.CliError("Use SKILL.md for publishing skills. Remove orchagent.json and run 'orchagent publish' from a skill directory.");
749
+ throw new errors_1.CliError('Skills use a different publishing format (SKILL.md with YAML front matter).\n\n' +
750
+ ' To publish a skill:\n' +
751
+ ' 1. Run: orchagent skill create ' + (manifest.name || '<name>') + '\n' +
752
+ ' 2. Edit the generated SKILL.md with your skill content\n' +
753
+ ' 3. Run: orchagent publish\n\n' +
754
+ ' orchagent.json is not used for skills — SKILL.md replaces it entirely.\n' +
755
+ ' See: https://orchagent.io/docs/skills');
746
756
  }
747
757
  if (runMode === 'always_on' && executionEngine === 'direct_llm') {
748
- throw new errors_1.CliError('run_mode=always_on requires runtime.command or loop configuration');
758
+ validationErrors.push('run_mode=always_on requires runtime.command or loop configuration');
749
759
  }
750
760
  if (manifest.timeout_seconds !== undefined) {
751
761
  if (!Number.isInteger(manifest.timeout_seconds) || manifest.timeout_seconds <= 0) {
752
- throw new errors_1.CliError('timeout_seconds must be a positive integer');
762
+ validationErrors.push('timeout_seconds must be a positive integer');
753
763
  }
754
764
  }
755
765
  // Warn about deprecated prompt field
756
766
  if (manifest.prompt) {
757
767
  process.stderr.write(chalk_1.default.yellow('Warning: "prompt" field in orchagent.json is ignored. Use prompt.md file instead.\n'));
758
768
  }
769
+ // UX-9: Warn about model (singular) vs default_models
770
+ if (manifest.model && !manifest.default_models) {
771
+ const modelVal = manifest.model;
772
+ process.stderr.write(chalk_1.default.yellow(`\nWarning: "model" field in orchagent.json is not recognized.\n` +
773
+ ` Use "default_models" instead to set per-provider defaults:\n\n` +
774
+ ` ${chalk_1.default.cyan(`"default_models": { "anthropic": "${modelVal}" }`)}\n\n` +
775
+ ` The model resolution order is: caller --model flag → agent default_models → platform default.\n\n`));
776
+ }
759
777
  // Auto-migrate inline schemas to schema.json
760
778
  const schemaPath = path_1.default.join(cwd, 'schema.json');
761
779
  let schemaFileExists = false;
@@ -789,20 +807,8 @@ function registerPublishCommand(program) {
789
807
  const manifestFields = ['manifest_version', 'dependencies', 'max_hops', 'timeout_ms', 'per_call_downstream_cap'];
790
808
  const misplacedFields = manifestFields.filter(f => f in manifest && !manifest.manifest);
791
809
  if (misplacedFields.length > 0) {
792
- throw new errors_1.CliError(`Found manifest fields (${misplacedFields.join(', ')}) at top level of orchagent.json.\n` +
793
- `These must be nested under a "manifest" key. Example:\n\n` +
794
- ` {\n` +
795
- ` "name": "${manifest.name}",\n` +
796
- ` "type": "${manifest.type || 'agent'}",\n` +
797
- ` "manifest": {\n` +
798
- ` "manifest_version": 1,\n` +
799
- ` "dependencies": [...],\n` +
800
- ` "max_hops": 2,\n` +
801
- ` "timeout_ms": 60000,\n` +
802
- ` "per_call_downstream_cap": 50\n` +
803
- ` }\n` +
804
- ` }\n\n` +
805
- `See docs/manifest.md for details.`);
810
+ validationErrors.push(`Found manifest fields (${misplacedFields.join(', ')}) at top level of orchagent.json. ` +
811
+ `These must be nested under a "manifest" key. See docs/manifest.md for details.`);
806
812
  }
807
813
  // Read prompt for LLM-driven engines (direct_llm + managed_loop).
808
814
  let prompt;
@@ -813,11 +819,11 @@ function registerPublishCommand(program) {
813
819
  }
814
820
  catch (err) {
815
821
  if (err.code === 'ENOENT') {
816
- throw new errors_1.CliError('No prompt.md found for this agent.\n\n' +
817
- 'Create a prompt.md file in the current directory with your prompt template.\n' +
818
- 'See: https://orchagent.io/docs/publishing');
822
+ validationErrors.push('No prompt.md found. Create a prompt.md file with your prompt template.');
823
+ }
824
+ else {
825
+ throw err;
819
826
  }
820
- throw err;
821
827
  }
822
828
  }
823
829
  // Validate managed-loop specific fields + normalize loop payload
@@ -825,7 +831,7 @@ function registerPublishCommand(program) {
825
831
  if (executionEngine === 'managed_loop') {
826
832
  if (manifest.max_turns !== undefined) {
827
833
  if (typeof manifest.max_turns !== 'number' || manifest.max_turns < 1 || manifest.max_turns > 50) {
828
- throw new errors_1.CliError('max_turns must be a number between 1 and 50');
834
+ validationErrors.push('max_turns must be a number between 1 and 50');
829
835
  }
830
836
  }
831
837
  const providedLoop = manifest.loop && typeof manifest.loop === 'object'
@@ -849,17 +855,17 @@ function registerPublishCommand(program) {
849
855
  const seenNames = new Set();
850
856
  for (const tool of mergedTools) {
851
857
  if (!tool.name || !tool.command) {
852
- throw new errors_1.CliError(`Invalid custom_tool: each tool must have 'name' and 'command' fields.\n` +
853
- `Found: ${JSON.stringify(tool)}`);
854
- }
855
- if (reservedNames.has(tool.name)) {
856
- throw new errors_1.CliError(`Custom tool '${tool.name}' conflicts with a built-in tool name.\n` +
857
- `Reserved names: ${[...reservedNames].join(', ')}`);
858
+ validationErrors.push(`Invalid custom_tool: each tool must have 'name' and 'command' fields. Found: ${JSON.stringify(tool)}`);
858
859
  }
859
- if (seenNames.has(tool.name)) {
860
- throw new errors_1.CliError(`Duplicate custom tool name: '${tool.name}'`);
860
+ else {
861
+ if (reservedNames.has(tool.name)) {
862
+ validationErrors.push(`Custom tool '${tool.name}' conflicts with a built-in tool name. Reserved names: ${[...reservedNames].join(', ')}`);
863
+ }
864
+ if (seenNames.has(tool.name)) {
865
+ validationErrors.push(`Duplicate custom tool name: '${tool.name}'`);
866
+ }
867
+ seenNames.add(tool.name);
861
868
  }
862
- seenNames.add(tool.name);
863
869
  }
864
870
  }
865
871
  if (!manifest.supported_providers) {
@@ -921,11 +927,13 @@ function registerPublishCommand(program) {
921
927
  }
922
928
  if (!options.url) {
923
929
  if (!bundleEntrypoint) {
924
- throw new errors_1.CliError('Tool requires either --url <url> or an entry point file (main.py, app.py, index.js, etc.)');
930
+ validationErrors.push('Tool requires either --url <url> or an entry point file (main.py, app.py, index.js, etc.)');
931
+ }
932
+ else {
933
+ shouldUploadBundle = true;
934
+ agentUrl = 'https://tool.internal';
935
+ process.stdout.write(`Detected code runtime entrypoint: ${bundleEntrypoint}\n`);
925
936
  }
926
- shouldUploadBundle = true;
927
- agentUrl = 'https://tool.internal';
928
- process.stdout.write(`Detected code runtime entrypoint: ${bundleEntrypoint}\n`);
929
937
  }
930
938
  let runtimeCommand = manifest.runtime?.command?.trim() || '';
931
939
  if (!runtimeCommand && manifest.run_command?.trim()) {
@@ -940,7 +948,7 @@ function registerPublishCommand(program) {
940
948
  agentUrl = agentUrl || 'https://prompt-agent.internal';
941
949
  }
942
950
  if (options.docker && executionEngine !== 'code_runtime') {
943
- throw new errors_1.CliError('--docker is only supported for code runtime agents');
951
+ validationErrors.push('--docker is only supported for code runtime agents');
944
952
  }
945
953
  // Get org info (workspace-aware — returns workspace org if workspace is active)
946
954
  const org = await (0, api_1.getOrg)(config, workspaceId);
@@ -982,23 +990,23 @@ function registerPublishCommand(program) {
982
990
  ` the field to use the default) and republish each dependency.\n\n`);
983
991
  }
984
992
  }
985
- // C-1: Block publish if tool/agent type has no required_secrets declared.
993
+ // UX-2: Default required_secrets to [] when omitted for tool/agent types.
986
994
  // Prompt and skill types are exempt (prompt agents get LLM keys from platform,
987
995
  // skills don't run standalone).
988
- // An explicit empty array (required_secrets: []) is a valid declaration
989
- // meaning "this agent deliberately needs no secrets."
990
- // Runs before dry-run so --dry-run catches the same errors as real publish (BUG-11).
991
996
  if ((canonicalType === 'tool' || canonicalType === 'agent') &&
992
- manifest.required_secrets === undefined &&
993
- options.requiredSecrets !== false) {
994
- process.stderr.write(chalk_1.default.red(`\nError: ${canonicalType} agents must declare required_secrets in orchagent.json.\n\n`) +
995
- ` Add the env vars your code needs at runtime:\n` +
996
- ` ${chalk_1.default.cyan('"required_secrets": ["ANTHROPIC_API_KEY", "MY_TOKEN"]')}\n\n` +
997
- ` If this agent genuinely needs no secrets, add an empty array:\n` +
998
- ` ${chalk_1.default.cyan('"required_secrets": []')}\n\n` +
999
- ` These are matched by name against your workspace secrets vault.\n` +
1000
- ` Use ${chalk_1.default.cyan('--no-required-secrets')} to skip this check.\n`);
1001
- const err = new errors_1.CliError('Missing required_secrets declaration', errors_1.ExitCodes.INVALID_INPUT);
997
+ manifest.required_secrets === undefined) {
998
+ manifest.required_secrets = [];
999
+ process.stderr.write(chalk_1.default.dim(` ℹ No required_secrets declared defaulting to [] (no secrets needed)\n`));
1000
+ }
1001
+ // UX-1: Report all validation errors at once
1002
+ if (validationErrors.length > 0) {
1003
+ if (validationErrors.length === 1) {
1004
+ throw new errors_1.CliError(validationErrors[0]);
1005
+ }
1006
+ const numbered = validationErrors.map((e, i) => ` ${i + 1}. ${e}`).join('\n');
1007
+ process.stderr.write(chalk_1.default.red(`\nFound ${validationErrors.length} validation errors:\n\n`) +
1008
+ numbered + '\n');
1009
+ const err = new errors_1.CliError(`Found ${validationErrors.length} validation errors:\n${numbered}`, errors_1.ExitCodes.INVALID_INPUT);
1002
1010
  err.displayed = true;
1003
1011
  throw err;
1004
1012
  }
@@ -15,15 +15,24 @@ const output_1 = require("../lib/output");
15
15
  async function resolveWorkspaceId(config, slug) {
16
16
  const configFile = await (0, config_1.loadConfig)();
17
17
  const targetSlug = slug ?? configFile.workspace;
18
- if (!targetSlug) {
19
- throw new errors_1.CliError('No workspace specified. Use --workspace <slug> or run `orch workspace use <slug>` first.');
20
- }
21
18
  const response = await (0, api_1.request)(config, 'GET', '/workspaces');
22
- const workspace = response.workspaces.find((w) => w.slug === targetSlug);
23
- if (!workspace) {
24
- throw new errors_1.CliError(`Workspace '${targetSlug}' not found.`);
19
+ if (targetSlug) {
20
+ const workspace = response.workspaces.find((w) => w.slug === targetSlug);
21
+ if (!workspace) {
22
+ throw new errors_1.CliError(`Workspace '${targetSlug}' not found.`);
23
+ }
24
+ return workspace.id;
25
+ }
26
+ // No workspace specified — auto-select if user has exactly one
27
+ if (response.workspaces.length === 0) {
28
+ throw new errors_1.CliError('No workspaces found. Create one with `orch workspace create <name>`.');
29
+ }
30
+ if (response.workspaces.length === 1) {
31
+ return response.workspaces[0].id;
25
32
  }
26
- return workspace.id;
33
+ const slugs = response.workspaces.map((w) => w.slug).join(', ');
34
+ throw new errors_1.CliError(`Multiple workspaces available: ${slugs}\n` +
35
+ 'Specify one with --workspace <slug> or run `orch workspace use <slug>`.');
27
36
  }
28
37
  function isUuid(value) {
29
38
  return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value);
@@ -37,7 +37,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
37
37
  };
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
39
  exports.localCommandForEntrypoint = localCommandForEntrypoint;
40
+ exports.inferFileField = inferFileField;
40
41
  exports.validateInputSchema = validateInputSchema;
42
+ exports.tryParseJsonObject = tryParseJsonObject;
41
43
  exports.isKeyedFileArg = isKeyedFileArg;
42
44
  exports.readKeyedFiles = readKeyedFiles;
43
45
  exports.mountDirectory = mountDirectory;
@@ -155,6 +157,13 @@ function warnIfLocalPathReference(jsonBody) {
155
157
  // If parsing fails, skip the warning
156
158
  }
157
159
  }
160
+ /**
161
+ * Infer which schema field should receive file content.
162
+ *
163
+ * Returns the detected field name, or null when the schema has properties
164
+ * but none of the heuristics can determine the right field.
165
+ * Returns 'content' (default) only when there is no schema to check against.
166
+ */
158
167
  function inferFileField(inputSchema) {
159
168
  if (!inputSchema || typeof inputSchema !== 'object')
160
169
  return 'content';
@@ -162,6 +171,7 @@ function inferFileField(inputSchema) {
162
171
  if (!props || typeof props !== 'object')
163
172
  return 'content';
164
173
  const properties = props;
174
+ // 1. Check well-known content field names
165
175
  for (const field of CONTENT_FIELD_NAMES) {
166
176
  if (properties[field] && properties[field].type === 'string')
167
177
  return field;
@@ -170,12 +180,41 @@ function inferFileField(inputSchema) {
170
180
  const stringProps = Object.entries(properties)
171
181
  .filter(([, v]) => v.type === 'string')
172
182
  .map(([k]) => k);
183
+ // 2. Only one string property in the schema — use it
173
184
  if (stringProps.length === 1)
174
185
  return stringProps[0];
186
+ // 3. Only one required string property — use it
175
187
  const requiredStrings = stringProps.filter(k => required.includes(k));
176
188
  if (requiredStrings.length === 1)
177
189
  return requiredStrings[0];
178
- return 'content';
190
+ // 4. Schema exists but detection is ambiguous — return null so callers
191
+ // can surface a clear error instead of silently using the wrong field
192
+ return null;
193
+ }
194
+ /**
195
+ * Resolve the file field name, throwing a clear error when auto-detection fails.
196
+ * Used at call sites where a file is being injected into a JSON payload.
197
+ */
198
+ function resolveFileField(fileFieldOption, inputSchema) {
199
+ if (fileFieldOption)
200
+ return fileFieldOption;
201
+ const detected = inferFileField(inputSchema);
202
+ if (detected !== null)
203
+ return detected;
204
+ // Detection failed — build a helpful error message
205
+ const props = inputSchema?.properties;
206
+ const stringFields = props
207
+ ? Object.entries(props).filter(([, v]) => v.type === 'string').map(([k]) => k)
208
+ : [];
209
+ const fieldList = stringFields.length > 0
210
+ ? `String fields in schema: ${stringFields.map(f => `"${f}"`).join(', ')}`
211
+ : 'No string fields found in schema';
212
+ throw new errors_1.CliError(`Could not determine which input field to use for file content.\n\n` +
213
+ `${fieldList}\n\n` +
214
+ `Specify the field explicitly:\n` +
215
+ ` orch run <agent> --file-field <field> input.json\n` +
216
+ ` orch run <agent> --data @input.json\n` +
217
+ ` orch run <agent> --file <field>=input.json`);
179
218
  }
180
219
  function applySchemaDefaults(body, schema) {
181
220
  if (!schema)
@@ -264,6 +303,26 @@ async function readStdin() {
264
303
  return null;
265
304
  return Buffer.concat(chunks);
266
305
  }
306
+ /**
307
+ * Try to parse a Buffer as a JSON object (not array).
308
+ * Returns the parsed object on success, null on failure or if the content
309
+ * is not a JSON object (e.g. array, string, number).
310
+ */
311
+ function tryParseJsonObject(buf) {
312
+ const text = buf.toString('utf8').trim();
313
+ if (!text.startsWith('{'))
314
+ return null;
315
+ try {
316
+ const parsed = JSON.parse(text);
317
+ if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
318
+ return parsed;
319
+ }
320
+ return null;
321
+ }
322
+ catch {
323
+ return null;
324
+ }
325
+ }
267
326
  async function buildMultipartBody(filePaths, metadata) {
268
327
  if (!filePaths || filePaths.length === 0) {
269
328
  const stdinData = await readStdin();
@@ -2016,7 +2075,7 @@ async function executeCloud(agentRef, file, options) {
2016
2075
  const resolvedBody = await resolveJsonBody(options.data);
2017
2076
  const bodyObj = JSON.parse(resolvedBody);
2018
2077
  if (cloudEngine !== 'code_runtime') {
2019
- const fieldName = options.fileField || inferFileField(agentMeta.input_schema);
2078
+ const fieldName = resolveFileField(options.fileField, agentMeta.input_schema);
2020
2079
  if (filePaths.length === 1) {
2021
2080
  await validateFilePath(filePaths[0]);
2022
2081
  bodyObj[fieldName] = await promises_1.default.readFile(filePaths[0], 'utf-8');
@@ -2072,7 +2131,7 @@ async function executeCloud(agentRef, file, options) {
2072
2131
  headers['Content-Type'] = 'application/json';
2073
2132
  }
2074
2133
  else if ((filePaths.length > 0 || options.metadata) && cloudEngine !== 'code_runtime') {
2075
- const fieldName = options.fileField || inferFileField(agentMeta.input_schema);
2134
+ const fieldName = resolveFileField(options.fileField, agentMeta.input_schema);
2076
2135
  let bodyObj = {};
2077
2136
  if (options.metadata) {
2078
2137
  try {
@@ -2127,13 +2186,39 @@ async function executeCloud(agentRef, file, options) {
2127
2186
  sourceLabel = multipart.sourceLabel;
2128
2187
  }
2129
2188
  else if (llmCredentials) {
2130
- body = JSON.stringify({ llm_credentials: llmCredentials });
2189
+ // Check for piped JSON stdin to merge with credentials
2190
+ const stdinData = await readStdin();
2191
+ const stdinJson = stdinData ? tryParseJsonObject(stdinData) : null;
2192
+ if (stdinJson) {
2193
+ stdinJson.llm_credentials = llmCredentials;
2194
+ warnInputSchemaErrors(stdinJson, agentMeta.input_schema);
2195
+ body = JSON.stringify(stdinJson);
2196
+ sourceLabel = 'stdin';
2197
+ }
2198
+ else {
2199
+ body = JSON.stringify({ llm_credentials: llmCredentials });
2200
+ }
2131
2201
  headers['Content-Type'] = 'application/json';
2132
2202
  }
2133
2203
  else {
2134
- const multipart = await buildMultipartBody(undefined, options.metadata);
2135
- body = multipart.body;
2136
- sourceLabel = multipart.sourceLabel;
2204
+ // No --data, no --file, no --metadata — check for piped JSON stdin
2205
+ const stdinData = await readStdin();
2206
+ if (stdinData) {
2207
+ const stdinJson = tryParseJsonObject(stdinData);
2208
+ if (stdinJson) {
2209
+ warnInputSchemaErrors(stdinJson, agentMeta.input_schema);
2210
+ body = JSON.stringify(stdinJson);
2211
+ headers['Content-Type'] = 'application/json';
2212
+ sourceLabel = 'stdin';
2213
+ }
2214
+ else {
2215
+ // Non-JSON stdin — send as binary file attachment
2216
+ const form = new FormData();
2217
+ form.append('files[]', new Blob([new Uint8Array(stdinData)]), 'stdin');
2218
+ body = form;
2219
+ sourceLabel = 'stdin';
2220
+ }
2221
+ }
2137
2222
  }
2138
2223
  } // end of non-injection path
2139
2224
  const verboseQs = options.verbose ? '?verbose=true' : '';
@@ -2157,8 +2242,11 @@ async function executeCloud(agentRef, file, options) {
2157
2242
  ? { spinner: null, dispose: () => { } }
2158
2243
  : (0, spinner_1.createElapsedSpinner)(`Running ${org}/${parsed.agent}@${parsed.version}...`);
2159
2244
  spinner?.start();
2160
- // Streamed sandbox runs can take longer; use 10 min timeout.
2161
- const timeoutMs = wantStream ? 600000 : undefined;
2245
+ // Streamed sandbox runs can take longer; use 10 min timeout (or --wait-timeout).
2246
+ const waitTimeoutSec = options.waitTimeout ? parseInt(options.waitTimeout, 10) : undefined;
2247
+ const timeoutMs = wantStream
2248
+ ? (waitTimeoutSec && waitTimeoutSec > 0 ? waitTimeoutSec * 1000 : 600000)
2249
+ : undefined;
2162
2250
  let response;
2163
2251
  try {
2164
2252
  response = await (0, api_1.safeFetchWithRetryForCalls)(url, {
@@ -2328,38 +2416,74 @@ async function executeCloud(agentRef, file, options) {
2328
2416
  process.stderr.write(chalk_1.default.gray(`\nStreaming ${org}/${parsed.agent}@${parsed.version}:\n`));
2329
2417
  }
2330
2418
  let progressErrorShown = false;
2331
- for await (const { event, data } of parseSSE(response.body)) {
2332
- if (event === 'progress') {
2333
- try {
2334
- const parsed = JSON.parse(data);
2335
- renderProgress(parsed, !!options.verbose);
2336
- if (parsed.type === 'error') {
2337
- progressErrorShown = true;
2419
+ let streamTimedOut = false;
2420
+ try {
2421
+ for await (const { event, data } of parseSSE(response.body)) {
2422
+ if (event === 'progress') {
2423
+ try {
2424
+ const parsed = JSON.parse(data);
2425
+ renderProgress(parsed, !!options.verbose);
2426
+ if (parsed.type === 'error') {
2427
+ progressErrorShown = true;
2428
+ }
2429
+ }
2430
+ catch {
2431
+ // ignore malformed progress events
2338
2432
  }
2339
2433
  }
2340
- catch {
2341
- // ignore malformed progress events
2342
- }
2343
- }
2344
- else if (event === 'result') {
2345
- try {
2346
- finalPayload = JSON.parse(data);
2434
+ else if (event === 'result') {
2435
+ try {
2436
+ finalPayload = JSON.parse(data);
2437
+ }
2438
+ catch {
2439
+ finalPayload = data;
2440
+ }
2347
2441
  }
2348
- catch {
2349
- finalPayload = data;
2442
+ else if (event === 'error') {
2443
+ hadError = true;
2444
+ try {
2445
+ finalPayload = JSON.parse(data);
2446
+ }
2447
+ catch {
2448
+ finalPayload = data;
2449
+ }
2350
2450
  }
2351
2451
  }
2352
- else if (event === 'error') {
2353
- hadError = true;
2354
- try {
2355
- finalPayload = JSON.parse(data);
2356
- }
2357
- catch {
2358
- finalPayload = data;
2359
- }
2452
+ }
2453
+ catch (streamErr) {
2454
+ // BUG-6: Detect timeout/abort errors — the server-side job may still be running.
2455
+ const errName = streamErr instanceof DOMException ? streamErr.name
2456
+ : streamErr instanceof Error ? streamErr.name
2457
+ : '';
2458
+ if (errName === 'TimeoutError' || errName === 'AbortError') {
2459
+ streamTimedOut = true;
2460
+ }
2461
+ else {
2462
+ throw streamErr;
2360
2463
  }
2361
2464
  }
2362
2465
  process.stderr.write('\n');
2466
+ // BUG-6: When the stream timed out, the run is likely still in progress on server.
2467
+ if (streamTimedOut) {
2468
+ const runId = response.headers?.get?.('x-run-id');
2469
+ process.stderr.write(chalk_1.default.yellow('\nRun still in progress on server — the CLI stopped waiting.\n') +
2470
+ (runId
2471
+ ? chalk_1.default.yellow(`Check status with: orch logs ${runId}\n`)
2472
+ : chalk_1.default.yellow('Check recent runs with: orch runs\n')) +
2473
+ (options.waitTimeout
2474
+ ? ''
2475
+ : chalk_1.default.gray('Tip: Use --wait-timeout <seconds> to wait longer.\n')));
2476
+ await (0, analytics_1.track)('cli_run', {
2477
+ agent: `${org}/${parsed.agent}@${parsed.version}`,
2478
+ input_type: hasInjection ? 'file_injection' : unkeyedFileArgs.length > 0 ? 'file' : options.data ? 'json' : 'empty',
2479
+ mode: 'cloud',
2480
+ streamed: true,
2481
+ timed_out: true,
2482
+ });
2483
+ const err = new errors_1.CliError('CLI wait timeout — run still in progress on server', errors_1.ExitCodes.TIMEOUT);
2484
+ err.displayed = true;
2485
+ throw err;
2486
+ }
2363
2487
  await (0, analytics_1.track)('cli_run', {
2364
2488
  agent: `${org}/${parsed.agent}@${parsed.version}`,
2365
2489
  input_type: hasInjection ? 'file_injection' : unkeyedFileArgs.length > 0 ? 'file' : options.data ? 'json' : 'empty',
@@ -2747,6 +2871,7 @@ function registerRunCommand(program) {
2747
2871
  .option('--skills-only <skills>', 'Use only these skills')
2748
2872
  .option('--no-skills', 'Ignore default skills')
2749
2873
  .option('--no-stream', 'Disable real-time streaming for stream-capable sandbox runs')
2874
+ .option('--wait-timeout <seconds>', 'Max seconds to wait for streaming result (default: 600)')
2750
2875
  // Cloud-only options
2751
2876
  .option('--endpoint <endpoint>', 'Override agent endpoint (cloud only)')
2752
2877
  .option('--tenant <tenant>', 'Tenant identifier for multi-tenant callers (cloud only)')