@orchagent/cli 0.3.119 → 0.3.120

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.
@@ -57,6 +57,7 @@ const config_1 = require("../lib/config");
57
57
  const resolve_agent_1 = require("../lib/resolve-agent");
58
58
  const api_1 = require("../lib/api");
59
59
  const errors_1 = require("../lib/errors");
60
+ const sanitize_1 = require("../lib/sanitize");
60
61
  const json_input_1 = require("../lib/json-input");
61
62
  const output_1 = require("../lib/output");
62
63
  const spinner_1 = require("../lib/spinner");
@@ -124,6 +125,8 @@ function resolveExecutionEngine(agentData) {
124
125
  }
125
126
  // ─── Validation helpers ─────────────────────────────────────────────────────
126
127
  async function validateFilePath(filePath) {
128
+ // DX-29: reject ../ traversal in file paths
129
+ (0, sanitize_1.rejectPathTraversal)(filePath, 'file path');
127
130
  const stat = await promises_1.default.stat(filePath);
128
131
  if (stat.isDirectory()) {
129
132
  throw new errors_1.CliError(`Cannot upload a directory for cloud execution: ${filePath}\n\n` +
@@ -381,6 +384,8 @@ async function readKeyedFiles(args) {
381
384
  const parsed = isKeyedFileArg(arg);
382
385
  if (!parsed)
383
386
  continue;
387
+ // DX-29: reject ../ traversal in file paths
388
+ (0, sanitize_1.rejectPathTraversal)(parsed.filePath, 'file path');
384
389
  const resolved = path_1.default.resolve(parsed.filePath);
385
390
  let stat;
386
391
  try {
@@ -403,6 +408,8 @@ const MOUNT_SKIP_DIRS = new Set([
403
408
  const MOUNT_MAX_DEPTH = 15;
404
409
  const MOUNT_MAX_FILES = 500;
405
410
  async function mountDirectory(dirPath) {
411
+ // DX-29: reject ../ traversal in mount paths
412
+ (0, sanitize_1.rejectPathTraversal)(dirPath, 'mount path');
406
413
  const resolved = path_1.default.resolve(dirPath);
407
414
  let stat;
408
415
  try {
@@ -1856,7 +1863,7 @@ async function executeCloud(agentRef, file, options) {
1856
1863
  runtime: agentMeta.runtime ?? null,
1857
1864
  loop: agentMeta.loop ?? null,
1858
1865
  });
1859
- // Pre-flight: check required secrets before running (F-18)
1866
+ // Pre-flight: check required secrets before running (F-18, DX-3)
1860
1867
  // Only for sandbox-backed engines where secrets are injected as env vars
1861
1868
  if (cloudEngine !== 'direct_llm') {
1862
1869
  const agentRequiredSecrets = agentMeta.required_secrets;
@@ -1867,12 +1874,15 @@ async function executeCloud(agentRef, file, options) {
1867
1874
  const existingNames = new Set(secretsResult.secrets.map((s) => s.name));
1868
1875
  const missing = agentRequiredSecrets.filter((s) => !existingNames.has(s));
1869
1876
  if (missing.length > 0) {
1870
- throw new errors_1.CliError(`Agent requires secrets not found in workspace '${org}':\n` +
1877
+ const err = new errors_1.CliError(`Agent requires ${missing.length} secret${missing.length === 1 ? '' : 's'} not found in workspace '${org}':\n` +
1871
1878
  missing.map((s) => ` - ${s}`).join('\n') + '\n\n' +
1872
1879
  `Set them before running:\n` +
1873
1880
  missing.map((s) => ` orch secrets set ${s} <value>`).join('\n') + '\n\n' +
1874
1881
  `Secrets are injected as environment variables into the agent sandbox.\n` +
1875
- `View existing secrets: orch secrets list`);
1882
+ `View existing secrets: orch secrets list`, errors_1.ExitCodes.INVALID_INPUT);
1883
+ err.code = errors_1.ErrorCodes.MISSING_SECRETS;
1884
+ err.hint = `Set missing secrets: ${missing.map(s => `orch secrets set ${s} <value>`).join(', ')}`;
1885
+ throw err;
1876
1886
  }
1877
1887
  }
1878
1888
  }
@@ -2297,6 +2307,14 @@ async function executeCloud(agentRef, file, options) {
2297
2307
  const sandboxExitCode = typeof payload === 'object' && payload
2298
2308
  ? payload.metadata?.sandbox_exit_code
2299
2309
  : undefined;
2310
+ // DX-1: Read failure classification from gateway
2311
+ const userMessage = typeof payload === 'object' && payload
2312
+ ? payload.error?.user_message
2313
+ : undefined;
2314
+ // DX-2: Read stderr tail from gateway for immediate debugging
2315
+ const stderrTail = typeof payload === 'object' && payload
2316
+ ? payload.error?.stderr_tail
2317
+ : undefined;
2300
2318
  // Detect platform errors that surface as SANDBOX_ERROR (BUG-11)
2301
2319
  const lowerMessage = (message || '').toLowerCase();
2302
2320
  const isPlatformError = /\b403\b/.test(message || '') ||
@@ -2304,13 +2322,16 @@ async function executeCloud(agentRef, file, options) {
2304
2322
  lowerMessage.includes('proxy token') ||
2305
2323
  lowerMessage.includes('orchagent_service_key') ||
2306
2324
  lowerMessage.includes('orchagent_billing_org');
2307
- // Categorize: platform > gateway category > exit_code heuristic > neutral
2325
+ // DX-1: Prefer user_message from gateway when available
2308
2326
  let attribution;
2309
2327
  if (isPlatformError) {
2310
2328
  attribution =
2311
2329
  `This may be a platform configuration issue, not an error in the agent's code.\n` +
2312
2330
  `If this persists, contact support with the ref below.`;
2313
2331
  }
2332
+ else if (userMessage) {
2333
+ attribution = userMessage;
2334
+ }
2314
2335
  else if (errorCategory === 'code_error' || (!errorCategory && sandboxExitCode != null && sandboxExitCode !== 0)) {
2315
2336
  attribution =
2316
2337
  `This is an error in the agent's code, not the platform.\n` +
@@ -2322,21 +2343,30 @@ async function executeCloud(agentRef, file, options) {
2322
2343
  `Check requirements.txt and environment configuration.`;
2323
2344
  }
2324
2345
  else {
2325
- // No category from gateway, no exit code — can't determine blame
2326
2346
  attribution = `Agent execution failed. Check agent logs for details.`;
2327
2347
  }
2348
+ // DX-2: Show stderr tail for immediate debugging
2349
+ const stderrSection = stderrTail
2350
+ ? `\n\n--- stderr (last lines) ---\n${stderrTail}`
2351
+ : '';
2328
2352
  throw new errors_1.CliError(`${message}\n\n` +
2329
2353
  attribution +
2354
+ stderrSection +
2330
2355
  (hint ? `\n\nHint: ${hint}` : '') +
2356
+ (requestId ? `\n\nFull logs: orch logs ${requestId.slice(0, 8)}` : '') +
2331
2357
  refSuffix);
2332
2358
  }
2333
2359
  if (errorCode === 'SANDBOX_TIMEOUT') {
2334
2360
  spinner?.stop();
2361
+ // DX-1: Use user_message from gateway when available
2362
+ const userMessage = typeof payload === 'object' && payload
2363
+ ? payload.error?.user_message
2364
+ : undefined;
2335
2365
  throw new errors_1.CliError(`${message}\n\n` +
2336
- `The agent did not complete in time. Try:\n` +
2337
- ` - Simplifying the input\n` +
2338
- ` - Using a smaller dataset\n` +
2339
- ` - Contacting the agent author to increase the timeout` +
2366
+ (userMessage || `The agent did not complete in time. Try:\n` +
2367
+ ` - Simplifying the input\n` +
2368
+ ` - Using a smaller dataset\n` +
2369
+ ` - Contacting the agent author to increase the timeout`) +
2340
2370
  refSuffix);
2341
2371
  }
2342
2372
  if (errorCode === 'MISSING_SECRETS') {
@@ -13,6 +13,7 @@ const api_1 = require("../lib/api");
13
13
  const errors_1 = require("../lib/errors");
14
14
  const json_input_1 = require("../lib/json-input");
15
15
  const output_1 = require("../lib/output");
16
+ const list_options_1 = require("../lib/list-options");
16
17
  const agent_ref_1 = require("../lib/agent-ref");
17
18
  const api_2 = require("../lib/api");
18
19
  // ============================================
@@ -88,6 +89,9 @@ function registerScheduleCommand(program) {
88
89
  .option('--agent <name>', 'Filter by agent name')
89
90
  .option('--type <type>', 'Filter by type (cron or webhook)')
90
91
  .option('--json', 'Output as JSON')
92
+ .option('--fields <fields>', 'Comma-separated fields to include in JSON output (implies --json)')
93
+ .option('--limit <n>', 'Maximum number of schedules to return (default: 100)')
94
+ .option('--offset <n>', 'Number of schedules to skip')
91
95
  .action(async (options) => {
92
96
  const config = await (0, config_1.getResolvedConfig)();
93
97
  if (!config.apiKey) {
@@ -99,11 +103,24 @@ function registerScheduleCommand(program) {
99
103
  params.set('agent_name', options.agent);
100
104
  if (options.type)
101
105
  params.set('schedule_type', options.type);
102
- params.set('limit', '100');
106
+ params.set('limit', options.limit ?? '100');
107
+ if (options.offset) {
108
+ const offset = parseInt(options.offset, 10);
109
+ if (!isNaN(offset) && offset > 0)
110
+ params.set('offset', String(offset));
111
+ }
103
112
  const qs = params.toString() ? `?${params.toString()}` : '';
104
113
  const result = await (0, api_1.request)(config, 'GET', `/workspaces/${workspaceId}/schedules${qs}`);
105
- if (options.json) {
106
- (0, output_1.printJson)(result);
114
+ // --fields implies --json
115
+ const useJson = options.json || !!options.fields;
116
+ if (useJson) {
117
+ if (options.fields) {
118
+ const fields = (0, list_options_1.parseFields)(options.fields);
119
+ (0, output_1.printJson)({ ...result, schedules: (0, list_options_1.filterFields)(result.schedules, fields) });
120
+ }
121
+ else {
122
+ (0, output_1.printJson)(result);
123
+ }
107
124
  return;
108
125
  }
109
126
  if (result.schedules.length === 0) {
@@ -561,7 +578,9 @@ function registerScheduleCommand(program) {
561
578
  .option('--workspace <slug>', 'Workspace slug (default: current workspace)')
562
579
  .option('--status <status>', 'Filter by status (completed, failed, running, timeout)')
563
580
  .option('--limit <n>', 'Number of runs to show (default: 20)', '20')
581
+ .option('--offset <n>', 'Number of runs to skip')
564
582
  .option('--json', 'Output as JSON')
583
+ .option('--fields <fields>', 'Comma-separated fields to include in JSON output (implies --json)')
565
584
  .action(async (scheduleId, options) => {
566
585
  const config = await (0, config_1.getResolvedConfig)();
567
586
  if (!config.apiKey) {
@@ -572,10 +591,23 @@ function registerScheduleCommand(program) {
572
591
  params.set('limit', options.limit);
573
592
  if (options.status)
574
593
  params.set('status', options.status);
594
+ if (options.offset) {
595
+ const offset = parseInt(options.offset, 10);
596
+ if (!isNaN(offset) && offset > 0)
597
+ params.set('offset', String(offset));
598
+ }
575
599
  const qs = `?${params.toString()}`;
576
600
  const result = await (0, api_1.request)(config, 'GET', `/workspaces/${workspaceId}/schedules/${scheduleId}/runs${qs}`);
577
- if (options.json) {
578
- (0, output_1.printJson)(result);
601
+ // --fields implies --json
602
+ const useJson = options.json || !!options.fields;
603
+ if (useJson) {
604
+ if (options.fields) {
605
+ const fields = (0, list_options_1.parseFields)(options.fields);
606
+ (0, output_1.printJson)({ ...result, runs: (0, list_options_1.filterFields)(result.runs, fields) });
607
+ }
608
+ else {
609
+ (0, output_1.printJson)(result);
610
+ }
579
611
  return;
580
612
  }
581
613
  if (!result.runs.length) {
@@ -0,0 +1,78 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registerSchemaCommand = registerSchemaCommand;
4
+ const config_1 = require("../lib/config");
5
+ const api_1 = require("../lib/api");
6
+ const agent_ref_1 = require("../lib/agent-ref");
7
+ const errors_1 = require("../lib/errors");
8
+ const info_1 = require("./info");
9
+ function registerSchemaCommand(program) {
10
+ program
11
+ .command('schema <agent>')
12
+ .description('Show agent input/output schemas as machine-readable JSON')
13
+ .option('--input-only', 'Show only the input schema')
14
+ .option('--output-only', 'Show only the output schema')
15
+ .option('--full', 'Show full agent spec (type, execution_engine, schemas, custom_tools, secrets)')
16
+ .action(async (agentArg, options) => {
17
+ const config = await (0, config_1.getResolvedConfig)();
18
+ const parsed = (0, agent_ref_1.parseAgentRef)(agentArg);
19
+ const configFile = await (0, config_1.loadConfig)();
20
+ const org = parsed.org ?? configFile.workspace ?? config.defaultOrg;
21
+ if (!org) {
22
+ throw new errors_1.CliError('Missing org. Use org/agent format or set default org.');
23
+ }
24
+ const { agent, version } = parsed;
25
+ const workspaceId = await (0, api_1.resolveWorkspaceIdForOrg)(config, org);
26
+ const agentData = await (0, info_1.getAgentInfo)(config, org, agent, version, workspaceId);
27
+ const ref = `${org}/${agent}@${agentData.version || version}`;
28
+ if (options.inputOnly) {
29
+ process.stdout.write(JSON.stringify(agentData.input_schema || {}, null, 2) + '\n');
30
+ return;
31
+ }
32
+ if (options.outputOnly) {
33
+ process.stdout.write(JSON.stringify(agentData.output_schema || {}, null, 2) + '\n');
34
+ return;
35
+ }
36
+ if (options.full) {
37
+ const output = {
38
+ agent: ref,
39
+ type: agentData.type,
40
+ callable: agentData.callable ?? false,
41
+ supported_providers: agentData.supported_providers,
42
+ input_schema: agentData.input_schema || {},
43
+ output_schema: agentData.output_schema || {},
44
+ };
45
+ if (agentData.description) {
46
+ output.description = agentData.description;
47
+ }
48
+ if (agentData.required_secrets && agentData.required_secrets.length > 0) {
49
+ output.required_secrets = agentData.required_secrets;
50
+ }
51
+ if (agentData.optional_secrets && agentData.optional_secrets.length > 0) {
52
+ output.optional_secrets = agentData.optional_secrets;
53
+ }
54
+ if (agentData.dependencies && agentData.dependencies.length > 0) {
55
+ output.dependencies = agentData.dependencies;
56
+ }
57
+ if (agentData.default_skills && agentData.default_skills.length > 0) {
58
+ output.default_skills = agentData.default_skills;
59
+ }
60
+ if (agentData.custom_tools && agentData.custom_tools.length > 0) {
61
+ output.custom_tools = agentData.custom_tools;
62
+ }
63
+ if (agentData.environment) {
64
+ output.environment = agentData.environment;
65
+ }
66
+ process.stdout.write(JSON.stringify(output, null, 2) + '\n');
67
+ return;
68
+ }
69
+ // Default: show both schemas with agent ref
70
+ const output = {
71
+ agent: ref,
72
+ type: agentData.type,
73
+ input_schema: agentData.input_schema || {},
74
+ output_schema: agentData.output_schema || {},
75
+ };
76
+ process.stdout.write(JSON.stringify(output, null, 2) + '\n');
77
+ });
78
+ }
@@ -10,6 +10,8 @@ const config_1 = require("../lib/config");
10
10
  const api_1 = require("../lib/api");
11
11
  const errors_1 = require("../lib/errors");
12
12
  const output_1 = require("../lib/output");
13
+ const list_options_1 = require("../lib/list-options");
14
+ const sanitize_1 = require("../lib/sanitize");
13
15
  // Known LLM key names → provider. Must stay in sync with gateway's
14
16
  // _PROVIDER_TO_SECRET_NAME (db.py) and publish.ts PROVIDER_TO_SECRET_NAME.
15
17
  const LLM_KEY_NAME_TO_PROVIDER = {
@@ -49,6 +51,8 @@ function formatDate(iso) {
49
51
  return new Date(iso).toLocaleString();
50
52
  }
51
53
  function validateSecretName(name) {
54
+ // DX-29: reject control chars before format check
55
+ (0, sanitize_1.rejectControlChars)(name, 'secret name');
52
56
  if (!name || name.length > 128) {
53
57
  throw new errors_1.CliError('Secret name must be 1-128 characters.');
54
58
  }
@@ -78,6 +82,7 @@ function registerSecretsCommand(program) {
78
82
  .description('List secrets in your workspace (names and metadata, never values)')
79
83
  .option('--workspace <slug>', 'Workspace slug (default: current workspace)')
80
84
  .option('--json', 'Output as JSON')
85
+ .option('--fields <fields>', 'Comma-separated fields to include in JSON output (implies --json)')
81
86
  .action(async (options) => {
82
87
  const config = await (0, config_1.getResolvedConfig)();
83
88
  if (!config.apiKey) {
@@ -85,8 +90,16 @@ function registerSecretsCommand(program) {
85
90
  }
86
91
  const workspaceId = await resolveWorkspaceId(config, options.workspace);
87
92
  const result = await (0, api_1.request)(config, 'GET', `/workspaces/${workspaceId}/secrets`);
88
- if (options.json) {
89
- (0, output_1.printJson)(result);
93
+ // --fields implies --json
94
+ const useJson = options.json || !!options.fields;
95
+ if (useJson) {
96
+ if (options.fields) {
97
+ const fields = (0, list_options_1.parseFields)(options.fields);
98
+ (0, output_1.printJson)({ ...result, secrets: (0, list_options_1.filterFields)(result.secrets, fields) });
99
+ }
100
+ else {
101
+ (0, output_1.printJson)(result);
102
+ }
90
103
  return;
91
104
  }
92
105
  if (result.secrets.length === 0) {
@@ -127,6 +140,8 @@ function registerSecretsCommand(program) {
127
140
  throw new errors_1.CliError('Missing API key. Run `orch login` first.');
128
141
  }
129
142
  validateSecretName(name);
143
+ // DX-29: strip dangerous control chars from secret values (keep \n, \r, \t for PEM keys etc.)
144
+ value = (0, sanitize_1.sanitizeSecretValue)(value);
130
145
  if (!value) {
131
146
  throw new errors_1.CliError('Secret value cannot be empty.');
132
147
  }
@@ -10,6 +10,7 @@ const config_1 = require("../lib/config");
10
10
  const api_1 = require("../lib/api");
11
11
  const errors_1 = require("../lib/errors");
12
12
  const output_1 = require("../lib/output");
13
+ const list_options_1 = require("../lib/list-options");
13
14
  const spinner_1 = require("../lib/spinner");
14
15
  // ============================================
15
16
  // HELPERS
@@ -227,6 +228,9 @@ function registerServiceCommand(program) {
227
228
  .option('--workspace <slug>', 'Workspace slug (default: current workspace)')
228
229
  .option('--status <state>', 'Filter by state')
229
230
  .option('--json', 'Output as JSON')
231
+ .option('--fields <fields>', 'Comma-separated fields to include in JSON output (implies --json)')
232
+ .option('--limit <n>', 'Maximum number of services to return (default: 100)')
233
+ .option('--offset <n>', 'Number of services to skip')
230
234
  .action(async (options) => {
231
235
  const config = await (0, config_1.getResolvedConfig)();
232
236
  if (!config.apiKey) {
@@ -236,11 +240,24 @@ function registerServiceCommand(program) {
236
240
  const params = new URLSearchParams();
237
241
  if (options.status)
238
242
  params.set('status', options.status);
239
- params.set('limit', '100');
243
+ params.set('limit', options.limit ?? '100');
244
+ if (options.offset) {
245
+ const offset = parseInt(options.offset, 10);
246
+ if (!isNaN(offset) && offset > 0)
247
+ params.set('offset', String(offset));
248
+ }
240
249
  const qs = params.toString() ? `?${params.toString()}` : '';
241
250
  const result = await (0, api_1.request)(config, 'GET', `/workspaces/${workspaceId}/services${qs}`);
242
- if (options.json) {
243
- (0, output_1.printJson)(result);
251
+ // --fields implies --json
252
+ const useJson = options.json || !!options.fields;
253
+ if (useJson) {
254
+ if (options.fields) {
255
+ const fields = (0, list_options_1.parseFields)(options.fields);
256
+ (0, output_1.printJson)({ ...result, services: (0, list_options_1.filterFields)(result.services, fields) });
257
+ }
258
+ else {
259
+ (0, output_1.printJson)(result);
260
+ }
244
261
  return;
245
262
  }
246
263
  if (!result.services.length) {
@@ -42,6 +42,7 @@ const config_1 = require("../lib/config");
42
42
  const api_1 = require("../lib/api");
43
43
  const errors_1 = require("../lib/errors");
44
44
  const output_1 = require("../lib/output");
45
+ const list_options_1 = require("../lib/list-options");
45
46
  // ============================================
46
47
  // HELPERS
47
48
  // ============================================
@@ -171,6 +172,7 @@ function registerStorageCommand(program) {
171
172
  .option('--limit <n>', 'Max keys to return (default: 100)', '100')
172
173
  .option('--cursor <cursor>', 'Pagination cursor from previous response')
173
174
  .option('--json', 'Output as JSON')
175
+ .option('--fields <fields>', 'Comma-separated fields to include in JSON output (implies --json)')
174
176
  .action(async (namespace, options) => {
175
177
  const config = await (0, config_1.getResolvedConfig)();
176
178
  if (!config.apiKey) {
@@ -178,11 +180,14 @@ function registerStorageCommand(program) {
178
180
  }
179
181
  const workspaceId = await resolveWorkspaceId(config, options.workspace);
180
182
  const headers = { 'X-Workspace-Id': workspaceId };
183
+ // --fields implies --json
184
+ const useJson = options.json || !!options.fields;
185
+ const parsedFields = options.fields ? (0, list_options_1.parseFields)(options.fields) : undefined;
181
186
  if (!namespace) {
182
187
  // List namespaces
183
188
  const result = await (0, api_1.request)(config, 'GET', '/storage', { headers });
184
- if (options.json) {
185
- (0, output_1.printJson)(result);
189
+ if (useJson) {
190
+ (0, output_1.printJson)(parsedFields ? (0, list_options_1.filterFields)(result, parsedFields) : result);
186
191
  return;
187
192
  }
188
193
  if (result.namespaces.length === 0) {
@@ -203,8 +208,8 @@ function registerStorageCommand(program) {
203
208
  if (options.cursor)
204
209
  path += `&cursor=${encodeURIComponent(options.cursor)}`;
205
210
  const result = await (0, api_1.request)(config, 'GET', path, { headers });
206
- if (options.json) {
207
- (0, output_1.printJson)(result);
211
+ if (useJson) {
212
+ (0, output_1.printJson)(parsedFields ? (0, list_options_1.filterFields)(result, parsedFields) : result);
208
213
  return;
209
214
  }
210
215
  if (result.keys.length === 0) {
package/dist/index.js CHANGED
@@ -47,6 +47,7 @@ const errors_1 = require("./lib/errors");
47
47
  const suggest_1 = require("./lib/suggest");
48
48
  const analytics_1 = require("./lib/analytics");
49
49
  const config_1 = require("./lib/config");
50
+ const output_1 = require("./lib/output");
50
51
  const spinner_1 = require("./lib/spinner");
51
52
  const update_notifier_1 = require("./lib/update-notifier");
52
53
  const package_json_1 = __importDefault(require("../package.json"));
@@ -82,12 +83,30 @@ async function main() {
82
83
  if (config.no_progress) {
83
84
  (0, spinner_1.setProgressEnabled)(false);
84
85
  }
85
- // Parse args - hook will set noProgress if --no-progress flag is passed
86
- program.hook('preAction', () => {
86
+ // Parse args - hook handles --no-progress and TTY auto-detection
87
+ program.hook('preAction', (_thisCommand, actionCommand) => {
87
88
  const opts = program.opts();
88
89
  if (opts.progress === false) {
89
90
  (0, spinner_1.setProgressEnabled)(false);
90
91
  }
92
+ // TTY auto-detection: non-TTY → auto-enable JSON on commands that support it
93
+ // This lets piped output (orch agents | jq .) and agent consumers get JSON automatically.
94
+ // Override: ORCHAGENT_OUTPUT=text forces human-readable even in non-TTY.
95
+ const hasJsonOption = actionCommand.options.some((o) => o.long === '--json');
96
+ if (hasJsonOption && actionCommand.getOptionValue('json') === undefined) {
97
+ if ((0, output_1.shouldAutoJson)()) {
98
+ actionCommand.setOptionValue('json', true);
99
+ (0, spinner_1.setProgressEnabled)(false);
100
+ }
101
+ }
102
+ // Track JSON mode globally so exitWithError can output structured errors
103
+ if (actionCommand.getOptionValue('json')) {
104
+ (0, output_1.setJsonMode)(true);
105
+ }
106
+ // Also disable progress spinners in non-TTY (even if command has no --json option)
107
+ if (!process.stdout.isTTY) {
108
+ (0, spinner_1.setProgressEnabled)(false);
109
+ }
91
110
  });
92
111
  await program.parseAsync(process.argv);
93
112
  }
@@ -2,7 +2,10 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.parseAgentRef = parseAgentRef;
4
4
  const errors_1 = require("./errors");
5
+ const sanitize_1 = require("./sanitize");
5
6
  function parseAgentRef(value, defaultVersion = 'latest') {
7
+ // DX-29: reject unsafe chars in the raw input before parsing
8
+ (0, sanitize_1.rejectResourceIdChars)(value, 'agent reference');
6
9
  const [ref, versionPart] = value.split('@');
7
10
  const version = versionPart?.trim() || defaultVersion;
8
11
  const segments = ref.split('/');