@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.
package/AGENT_GUIDE.md ADDED
@@ -0,0 +1,34 @@
1
+ ---
2
+ name: orchagent-cli
3
+ discovery:
4
+ full_context: "orch context"
5
+ command_contract: "orch describe <command> --json"
6
+ output:
7
+ preferred: --json
8
+ non_tty_behavior: commands with --json auto-enable JSON when stdout is non-TTY
9
+ ---
10
+
11
+ # orchagent CLI Agent Guide
12
+
13
+ Use this file as an entry point for AI agents and automation tools.
14
+
15
+ ## Discovery
16
+
17
+ - Run `orch context` to get a full, machine-readable command index in YAML-frontmatter markdown format.
18
+ - Run `orch describe <command> --json` to get argument, flag, mutation, and example metadata for one command.
19
+
20
+ ## Auth
21
+
22
+ - Set `ORCHAGENT_API_KEY` in the environment.
23
+ - Use `orch login` to create or store credentials locally.
24
+
25
+ ## I/O Conventions
26
+
27
+ - Prefer `--json` for stable parsing.
28
+ - For JSON payloads, prefer `--data @file.json` or `--data @-` (stdin) for reliability.
29
+ - Use explicit agent references (`org/name@version`) when possible.
30
+
31
+ ## Safety
32
+
33
+ - Prefer `--dry-run` for mutating commands when available.
34
+ - Inspect command contracts with `orch describe` before executing side effects.
@@ -42,6 +42,7 @@ const chalk_1 = __importDefault(require("chalk"));
42
42
  const config_1 = require("../lib/config");
43
43
  const api_1 = require("../lib/api");
44
44
  const output_1 = require("../lib/output");
45
+ const list_options_1 = require("../lib/list-options");
45
46
  /**
46
47
  * Given a list of agents, return only the latest version of each agent name.
47
48
  * "Latest" = highest created_at timestamp (most recently published).
@@ -73,6 +74,9 @@ function registerAgentsCommand(program) {
73
74
  .option('--filter <text>', 'Filter by name')
74
75
  .option('--all-versions', 'Show all versions (default: latest only)')
75
76
  .option('--json', 'Output raw JSON')
77
+ .option('--fields <fields>', 'Comma-separated fields to include in JSON output (implies --json)')
78
+ .option('--limit <n>', 'Maximum number of items to return')
79
+ .option('--offset <n>', 'Number of items to skip')
76
80
  .action(async (options) => {
77
81
  const config = await (0, config_1.getResolvedConfig)();
78
82
  // Resolve workspace context
@@ -95,8 +99,17 @@ function registerAgentsCommand(program) {
95
99
  displayAgents = grouped.agents;
96
100
  versionCounts = grouped.versionCounts;
97
101
  }
98
- if (options.json) {
99
- (0, output_1.printJson)(displayAgents);
102
+ // Apply client-side limit/offset
103
+ const limit = (0, list_options_1.parseIntOption)(options.limit);
104
+ const offset = (0, list_options_1.parseIntOption)(options.offset);
105
+ if (limit != null || offset != null) {
106
+ displayAgents = (0, list_options_1.applyLimitOffset)(displayAgents, limit, offset);
107
+ }
108
+ // --fields implies --json
109
+ const useJson = options.json || !!options.fields;
110
+ if (useJson) {
111
+ const fields = options.fields ? (0, list_options_1.parseFields)(options.fields) : undefined;
112
+ (0, output_1.printJson)(fields ? (0, list_options_1.filterFields)(displayAgents, fields) : displayAgents);
100
113
  return;
101
114
  }
102
115
  if (displayAgents.length === 0) {
@@ -0,0 +1,76 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.buildContextDocument = buildContextDocument;
7
+ exports.registerContextCommand = registerContextCommand;
8
+ const yaml_1 = __importDefault(require("yaml"));
9
+ const package_json_1 = __importDefault(require("../../package.json"));
10
+ const command_introspection_1 = require("../lib/command-introspection");
11
+ function toContextCommands(program) {
12
+ return (0, command_introspection_1.buildCliCommandMetadata)(program).map((command) => ({
13
+ name: command.name,
14
+ description: command.description,
15
+ flags: (0, command_introspection_1.collectFlagNames)(command),
16
+ mutations: command.mutations,
17
+ ...(command.dryRun ? { dry_run: true } : {}),
18
+ }));
19
+ }
20
+ function buildGuideBody(commands) {
21
+ const commandIndex = commands
22
+ .map((command) => `- \`${command.name}\` - ${command.description || 'No description available'}`)
23
+ .join('\n');
24
+ const mutatingCommands = commands
25
+ .filter((command) => command.mutations)
26
+ .map((command) => `- \`${command.name}\`${command.dry_run ? ' (supports --dry-run)' : ''}`)
27
+ .join('\n');
28
+ return `# orchagent CLI - Agent Guide
29
+
30
+ ## Authentication
31
+ Set \`ORCHAGENT_API_KEY\` in the environment. You can create a key with \`orch login\` or in the orchagent dashboard.
32
+
33
+ ## Discovery
34
+ - Use \`orch context\` for a full command index plus machine-readable frontmatter.
35
+ - Use \`orch describe <command> --json\` for detailed metadata on one command.
36
+
37
+ ## Output Conventions
38
+ - Prefer \`--json\` when your caller needs structured output.
39
+ - Commands that support \`--json\` automatically switch to JSON in non-TTY output.
40
+ - For JSON inputs, prefer \`--data @file.json\` or \`--data @-\` (stdin) over shell-escaped inline blobs.
41
+
42
+ ## Common Patterns
43
+ - Use explicit agent references when possible: \`org/name@version\`.
44
+ - Use \`--dry-run\` before mutating operations whenever available.
45
+ - For large automation flows, inspect command contracts with \`orch describe\` before execution.
46
+
47
+ ## Top-Level Commands
48
+ ${commandIndex}
49
+
50
+ ## Mutating Commands
51
+ ${mutatingCommands || '- None detected'}
52
+ `;
53
+ }
54
+ function buildContextDocument(program) {
55
+ const commands = toContextCommands(program);
56
+ const version = program.version() || package_json_1.default.version;
57
+ const frontmatter = {
58
+ name: 'orchagent-cli',
59
+ version,
60
+ commands,
61
+ };
62
+ const body = buildGuideBody(commands).trim();
63
+ return `---\n${yaml_1.default.stringify(frontmatter).trim()}\n---\n\n${body}\n`;
64
+ }
65
+ function registerContextCommand(program) {
66
+ program
67
+ .command('context')
68
+ .description('Print an embedded CLI guide for AI agents (YAML frontmatter + markdown)')
69
+ .addHelpText('after', `
70
+ Examples:
71
+ orch context
72
+ `)
73
+ .action(() => {
74
+ process.stdout.write(buildContextDocument(program));
75
+ });
76
+ }
@@ -0,0 +1,131 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registerDescribeCommand = registerDescribeCommand;
4
+ const command_introspection_1 = require("../lib/command-introspection");
5
+ const errors_1 = require("../lib/errors");
6
+ const output_1 = require("../lib/output");
7
+ function toDescribeOutput(command) {
8
+ return {
9
+ command: command.path,
10
+ description: command.description,
11
+ usage: `orch ${command.usage}`,
12
+ arguments: command.arguments.map((argument) => ({
13
+ name: argument.name,
14
+ required: argument.required,
15
+ variadic: argument.variadic,
16
+ format: argument.format,
17
+ ...(argument.description ? { description: argument.description } : {}),
18
+ })),
19
+ flags: command.flags.map((flag) => ({
20
+ name: flag.name,
21
+ type: flag.type,
22
+ required: flag.required,
23
+ ...(flag.valueRequired ? { value_required: true } : {}),
24
+ description: flag.description,
25
+ ...(flag.short ? { short: flag.short } : {}),
26
+ ...(flag.alias ? { alias: flag.alias } : {}),
27
+ ...(flag.choices ? { choices: flag.choices } : {}),
28
+ ...(flag.defaultValue !== undefined ? { default: flag.defaultValue } : {}),
29
+ })),
30
+ mutations: command.mutations,
31
+ dry_run: command.dryRun,
32
+ examples: command.examples,
33
+ aliases: command.aliases,
34
+ ...(command.subcommands.length > 0
35
+ ? {
36
+ subcommands: command.subcommands.map((subcommand) => ({
37
+ command: subcommand.path,
38
+ description: subcommand.description,
39
+ usage: `orch ${subcommand.usage}`,
40
+ mutations: subcommand.mutations,
41
+ dry_run: subcommand.dryRun,
42
+ })),
43
+ }
44
+ : {}),
45
+ };
46
+ }
47
+ function renderMarkdown(command) {
48
+ const argumentLines = command.arguments.length > 0
49
+ ? command.arguments
50
+ .map((argument) => `- \`${argument.name}\` (${argument.required ? 'required' : 'optional'}, ${argument.format})${argument.description ? ` - ${argument.description}` : ''}`)
51
+ .join('\n')
52
+ : '- None';
53
+ const flagLines = command.flags.length > 0
54
+ ? command.flags
55
+ .map((flag) => {
56
+ const parts = [`\`${flag.name}\``];
57
+ if (flag.alias)
58
+ parts.push(`alias: \`${flag.alias}\``);
59
+ if (flag.short)
60
+ parts.push(`short: \`${flag.short}\``);
61
+ parts.push(`type: ${flag.type}`);
62
+ return `- ${parts.join(', ')} - ${flag.description}`;
63
+ })
64
+ .join('\n')
65
+ : '- None';
66
+ const subcommandLines = command.subcommands.length > 0
67
+ ? command.subcommands
68
+ .map((subcommand) => `- \`${subcommand.path}\` - ${subcommand.description}`)
69
+ .join('\n')
70
+ : '- None';
71
+ const exampleLines = command.examples.length > 0
72
+ ? command.examples.map((example) => `- \`${example}\``).join('\n')
73
+ : '- None';
74
+ return `# orch describe: ${command.path}
75
+
76
+ ## Description
77
+ ${command.description || 'No description available'}
78
+
79
+ ## Usage
80
+ \`orch ${command.usage}\`
81
+
82
+ ## Mutations
83
+ ${command.mutations ? 'Yes' : 'No'}
84
+
85
+ ## Dry Run
86
+ ${command.dryRun ? 'Supported' : 'Not supported'}
87
+
88
+ ## Arguments
89
+ ${argumentLines}
90
+
91
+ ## Flags
92
+ ${flagLines}
93
+
94
+ ## Subcommands
95
+ ${subcommandLines}
96
+
97
+ ## Examples
98
+ ${exampleLines}
99
+ `;
100
+ }
101
+ function resolveCommand(program, commandPath) {
102
+ const allCommands = (0, command_introspection_1.buildCliCommandMetadata)(program);
103
+ const match = (0, command_introspection_1.findCommandMetadata)(allCommands, commandPath);
104
+ if (match)
105
+ return match;
106
+ const query = commandPath.join(' ');
107
+ const err = new errors_1.CliError(`Unknown command "${query}"`, errors_1.ExitCodes.NOT_FOUND);
108
+ err.code = errors_1.ErrorCodes.NOT_FOUND;
109
+ err.hint = 'Run "orch context" to list available commands.';
110
+ throw err;
111
+ }
112
+ function registerDescribeCommand(program) {
113
+ program
114
+ .command('describe <command...>')
115
+ .description('Show machine-readable metadata for a single command')
116
+ .option('--json', 'Output machine-readable JSON (default)')
117
+ .option('--markdown', 'Output markdown instead of JSON')
118
+ .addHelpText('after', `
119
+ Examples:
120
+ orch describe run --json
121
+ orch describe schedule create --json
122
+ `)
123
+ .action((commandPath, options) => {
124
+ const command = resolveCommand(program, commandPath);
125
+ if (options.markdown) {
126
+ process.stdout.write(renderMarkdown(command));
127
+ return;
128
+ }
129
+ (0, output_1.printJson)(toDescribeOutput(command));
130
+ });
131
+ }
@@ -46,6 +46,9 @@ const dag_1 = require("./dag");
46
46
  const completion_1 = require("./completion");
47
47
  const validate_1 = require("./validate");
48
48
  const storage_1 = require("./storage");
49
+ const schema_1 = require("./schema");
50
+ const context_1 = require("./context");
51
+ const describe_1 = require("./describe");
49
52
  function registerCommands(program) {
50
53
  (0, login_1.registerLoginCommand)(program);
51
54
  (0, logout_1.registerLogoutCommand)(program);
@@ -92,4 +95,7 @@ function registerCommands(program) {
92
95
  (0, completion_1.registerCompletionCommand)(program);
93
96
  (0, validate_1.registerValidateCommand)(program);
94
97
  (0, storage_1.registerStorageCommand)(program);
98
+ (0, schema_1.registerSchemaCommand)(program);
99
+ (0, context_1.registerContextCommand)(program);
100
+ (0, describe_1.registerDescribeCommand)(program);
95
101
  }
@@ -3,12 +3,14 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.getAgentInfo = getAgentInfo;
6
7
  exports.registerInfoCommand = registerInfoCommand;
7
8
  const chalk_1 = __importDefault(require("chalk"));
8
9
  const config_1 = require("../lib/config");
9
10
  const api_1 = require("../lib/api");
10
11
  const agent_ref_1 = require("../lib/agent-ref");
11
12
  const errors_1 = require("../lib/errors");
13
+ const list_options_1 = require("../lib/list-options");
12
14
  function formatSchema(schema, indent = ' ') {
13
15
  const lines = [];
14
16
  const props = schema.properties || {};
@@ -150,6 +152,7 @@ function registerInfoCommand(program) {
150
152
  .command('info <agent>')
151
153
  .description('Show agent details including inputs and outputs')
152
154
  .option('--json', 'Output as JSON')
155
+ .option('--fields <fields>', 'Comma-separated fields to include in JSON output (implies --json)')
153
156
  .action(async (agentArg, options) => {
154
157
  const config = await (0, config_1.getResolvedConfig)();
155
158
  const parsed = (0, agent_ref_1.parseAgentRef)(agentArg);
@@ -163,13 +166,16 @@ function registerInfoCommand(program) {
163
166
  const workspaceId = await (0, api_1.resolveWorkspaceIdForOrg)(config, org);
164
167
  // Fetch agent metadata
165
168
  const agentData = await getAgentInfo(config, org, agent, version, workspaceId);
166
- if (options.json) {
169
+ // --fields implies --json
170
+ if (options.json || options.fields) {
167
171
  // Don't expose internal routing URLs in JSON output
168
172
  const output = { ...agentData };
169
173
  if (output.url?.includes('.internal')) {
170
174
  delete output.url;
171
175
  }
172
- process.stdout.write(JSON.stringify(output, null, 2) + '\n');
176
+ const fields = options.fields ? (0, list_options_1.parseFields)(options.fields) : undefined;
177
+ const data = fields ? (0, list_options_1.filterFields)(output, fields) : output;
178
+ process.stdout.write(JSON.stringify(data, null, 2) + '\n');
173
179
  return;
174
180
  }
175
181
  // Display agent info
@@ -27,10 +27,12 @@ function registerLoginCommand(program) {
27
27
  .option('--key <key>', 'API key (for CI/CD, non-interactive)')
28
28
  .option('--port <port>', `Localhost port for browser callback (default: ${DEFAULT_AUTH_PORT})`, String(DEFAULT_AUTH_PORT))
29
29
  .action(async (options) => {
30
- const providedKey = options.key || process.env.ORCHAGENT_API_KEY;
31
- // If key provided via flag or env var, use existing key-based flow
32
- if (providedKey) {
33
- await keyBasedLogin(providedKey);
30
+ // If key provided via --key flag, use key-based flow (for CI/CD)
31
+ // Note: ORCHAGENT_API_KEY env var is intentionally NOT checked here
32
+ // it's for runtime API auth, not for login. Otherwise `orch login`
33
+ // can never reach the browser flow when the env var is set.
34
+ if (options.key) {
35
+ await keyBasedLogin(options.key);
34
36
  return;
35
37
  }
36
38
  // Check if running in non-interactive mode (no TTY)
@@ -61,13 +63,18 @@ async function keyBasedLogin(apiKey) {
61
63
  ...existing,
62
64
  api_key: apiKey,
63
65
  api_url: resolved.apiUrl,
64
- default_org: existing.default_org ?? org.slug,
66
+ default_org: org.slug,
65
67
  };
66
68
  // Clear workspace from previous account — workspaces are account-specific
67
69
  delete nextConfig.workspace;
68
70
  await (0, config_1.saveConfig)(nextConfig);
69
71
  await (0, analytics_1.track)('cli_login', { method: 'key' });
70
72
  process.stdout.write(`✓ Logged in to ${org.slug}\n`);
73
+ if (process.env.ORCHAGENT_API_KEY) {
74
+ process.stderr.write('\nWarning: ORCHAGENT_API_KEY is set in your environment.\n' +
75
+ 'The env var overrides your login credentials. Unset it with:\n' +
76
+ ' unset ORCHAGENT_API_KEY\n');
77
+ }
71
78
  if (isFirstLogin) {
72
79
  process.stdout.write('\n Tip: Run `orch doctor` to verify your setup.\n\n');
73
80
  }
@@ -86,13 +93,18 @@ async function browserBasedLogin(port) {
86
93
  ...existing,
87
94
  api_key: result.apiKey,
88
95
  api_url: resolved.apiUrl,
89
- default_org: existing.default_org ?? result.orgSlug,
96
+ default_org: result.orgSlug,
90
97
  };
91
98
  // Clear workspace from previous account — workspaces are account-specific
92
99
  delete nextConfig.workspace;
93
100
  await (0, config_1.saveConfig)(nextConfig);
94
101
  await (0, analytics_1.track)('cli_login', { method: 'browser' });
95
102
  process.stdout.write(`\n✓ Logged in to ${result.orgSlug}\n`);
103
+ if (process.env.ORCHAGENT_API_KEY) {
104
+ process.stderr.write('\nWarning: ORCHAGENT_API_KEY is set in your environment.\n' +
105
+ 'The env var overrides your login credentials. Unset it with:\n' +
106
+ ' unset ORCHAGENT_API_KEY\n');
107
+ }
96
108
  if (isFirstLogin) {
97
109
  process.stdout.write('\n Tip: Run `orch doctor` to verify your setup.\n\n');
98
110
  }
@@ -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
  // ============================================
14
15
  // HELPERS
15
16
  // ============================================
@@ -102,6 +103,16 @@ async function findServiceForAgent(config, workspaceId, agentName) {
102
103
  return null;
103
104
  }
104
105
  }
106
+ /** Find all active (non-deleted) always-on services in the workspace. */
107
+ async function findActiveServices(config, workspaceId) {
108
+ try {
109
+ const result = await (0, api_1.request)(config, 'GET', `/workspaces/${workspaceId}/services?limit=100`);
110
+ return result.services.filter((s) => s.current_state !== 'deleted');
111
+ }
112
+ catch {
113
+ return [];
114
+ }
115
+ }
105
116
  /** Fetch and display logs from an always-on service. */
106
117
  async function showServiceLogs(config, workspaceId, service, limit, json) {
107
118
  const params = new URLSearchParams();
@@ -140,8 +151,10 @@ function registerLogsCommand(program) {
140
151
  .option('--workspace <slug>', 'Workspace slug (default: current workspace)')
141
152
  .option('--status <status>', 'Filter by status: running, completed, failed, timeout')
142
153
  .option('--limit <n>', 'Number of runs to show (default: 20)', '20')
154
+ .option('--offset <n>', 'Number of runs to skip')
143
155
  .option('--live', 'Show live logs from always-on service (skips run history)')
144
156
  .option('--json', 'Output as JSON')
157
+ .option('--fields <fields>', 'Comma-separated fields to include in JSON output (implies --json)')
145
158
  .action(async (target, options) => {
146
159
  const config = await (0, config_1.getResolvedConfig)();
147
160
  if (!config.apiKey) {
@@ -208,12 +221,19 @@ async function listRuns(config, workspaceId, agentName, options) {
208
221
  params.set('status', options.status);
209
222
  const limit = parseInt(options.limit ?? '20', 10);
210
223
  params.set('limit', String(Math.min(Math.max(1, limit), 200)));
224
+ if (options.offset) {
225
+ const offset = parseInt(options.offset, 10);
226
+ if (!isNaN(offset) && offset > 0)
227
+ params.set('offset', String(offset));
228
+ }
211
229
  const qs = params.toString() ? `?${params.toString()}` : '';
212
230
  const [result, service] = await Promise.all([
213
231
  (0, api_1.request)(config, 'GET', `/workspaces/${workspaceId}/runs${qs}`),
214
232
  servicePromise,
215
233
  ]);
216
- if (options.json) {
234
+ // --fields implies --json
235
+ const useJson = options.json || !!options.fields;
236
+ if (useJson) {
217
237
  const payload = { ...result };
218
238
  if (service) {
219
239
  payload.service = {
@@ -223,15 +243,29 @@ async function listRuns(config, workspaceId, agentName, options) {
223
243
  health_status: service.health_status,
224
244
  };
225
245
  }
246
+ if (options.fields) {
247
+ const fields = (0, list_options_1.parseFields)(options.fields);
248
+ payload.runs = (0, list_options_1.filterFields)(payload.runs, fields);
249
+ }
226
250
  (0, output_1.printJson)(payload);
227
251
  return;
228
252
  }
229
253
  if (result.runs.length === 0 && !service) {
230
254
  if (agentName) {
231
255
  process.stdout.write(`No runs found for agent '${agentName}'.\n`);
256
+ process.stdout.write(chalk_1.default.gray(`\nIf this is an always-on service, try: orch logs ${agentName} --live\n`));
232
257
  }
233
258
  else {
234
259
  process.stdout.write('No runs found in this workspace.\n');
260
+ // Check if the workspace has active services the user might want logs for
261
+ const activeServices = await findActiveServices(config, workspaceId);
262
+ if (activeServices.length > 0) {
263
+ process.stdout.write(chalk_1.default.yellow('\nThis workspace has always-on services. To view service logs:\n'));
264
+ for (const svc of activeServices) {
265
+ process.stdout.write(` ${chalk_1.default.cyan('orch logs ' + svc.agent_name + ' --live')} (${svc.service_name})\n`);
266
+ }
267
+ process.stdout.write(chalk_1.default.gray('\nOr use: orch service logs <service-id>\n'));
268
+ }
235
269
  }
236
270
  return;
237
271
  }
@@ -43,6 +43,8 @@ exports.scanReservedPort = scanReservedPort;
43
43
  exports.detectSdkCompatible = detectSdkCompatible;
44
44
  exports.checkDependencies = checkDependencies;
45
45
  exports.checkWorkspaceLlmKeys = checkWorkspaceLlmKeys;
46
+ exports.prePublishSecretsCheck = prePublishSecretsCheck;
47
+ exports.checkRequiredSecrets = checkRequiredSecrets;
46
48
  exports.batchPublish = batchPublish;
47
49
  exports.registerPublishCommand = registerPublishCommand;
48
50
  const promises_1 = __importDefault(require("fs/promises"));
@@ -58,6 +60,7 @@ const analytics_1 = require("../lib/analytics");
58
60
  const bundle_1 = require("../lib/bundle");
59
61
  const key_store_1 = require("../lib/key-store");
60
62
  const batch_publish_1 = require("../lib/batch-publish");
63
+ const llm_1 = require("../lib/llm");
61
64
  /**
62
65
  * Extract template placeholders from a prompt template.
63
66
  * Matches double-brace patterns like {{variable}}.
@@ -517,6 +520,97 @@ async function checkWorkspaceLlmKeys(config, workspaceId, workspaceSlug, executi
517
520
  ` Cloud runs will fail until you add keys.\n` +
518
521
  ` Add a key: ${chalk_1.default.cyan(`orch secrets set ${exampleSecretName} <key>`)}\n`);
519
522
  }
523
+ /**
524
+ * Pre-publish check: verify that all required_secrets exist in the workspace
525
+ * BEFORE publishing. This lets users set missing secrets before wasting time
526
+ * on the publish step (DX-3).
527
+ *
528
+ * In TTY mode: warns and asks for confirmation to continue.
529
+ * In non-TTY mode: warns but does not block (for CI/CD pipelines).
530
+ * API errors are silently swallowed (non-fatal).
531
+ *
532
+ * Returns the list of missing secret names (empty if all present or check failed).
533
+ */
534
+ async function prePublishSecretsCheck(config, workspaceId, workspaceSlug, requiredSecrets) {
535
+ if (!requiredSecrets.length)
536
+ return [];
537
+ let secrets;
538
+ try {
539
+ const result = await (0, api_1.request)(config, 'GET', `/workspaces/${workspaceId}/secrets`);
540
+ secrets = result.secrets;
541
+ if (!Array.isArray(secrets))
542
+ return [];
543
+ }
544
+ catch {
545
+ return []; // Can't reach API — skip pre-check, gateway will catch at runtime
546
+ }
547
+ const existingNames = new Set(secrets.map(s => s.name));
548
+ const missing = requiredSecrets.filter(name => !existingNames.has(name));
549
+ if (missing.length === 0)
550
+ return [];
551
+ // Show prominent warning about missing secrets
552
+ process.stderr.write(chalk_1.default.yellow.bold(`\n⚠ ${missing.length} required secret${missing.length === 1 ? '' : 's'} missing from workspace '${workspaceSlug}':\n`));
553
+ for (const name of missing) {
554
+ process.stderr.write(` ${chalk_1.default.yellow('•')} ${chalk_1.default.bold(name)}\n`);
555
+ }
556
+ process.stderr.write(`\n Set them now:\n`);
557
+ for (const name of missing) {
558
+ process.stderr.write(` ${chalk_1.default.cyan(`orch secrets set ${name} <value>`)}\n`);
559
+ }
560
+ process.stderr.write(chalk_1.default.gray(`\n Without these secrets, the agent will fail at runtime.\n`));
561
+ // In TTY mode, ask user to confirm they want to continue publishing
562
+ if (process.stdin.isTTY && process.stderr.isTTY) {
563
+ const readline = await Promise.resolve().then(() => __importStar(require('readline/promises')));
564
+ const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
565
+ const answer = await rl.question(chalk_1.default.yellow('\n Publish anyway? (y/N): '));
566
+ rl.close();
567
+ if (answer.toLowerCase() !== 'y' && answer.toLowerCase() !== 'yes') {
568
+ process.stderr.write('\nPublish cancelled. Set the missing secrets first.\n');
569
+ process.exit(0);
570
+ }
571
+ }
572
+ else {
573
+ process.stderr.write(chalk_1.default.gray(` (non-interactive — continuing with publish)\n\n`));
574
+ }
575
+ return missing;
576
+ }
577
+ /**
578
+ * After a successful publish, check whether the workspace has all the secrets
579
+ * declared in required_secrets. Warns about any missing ones so the user can
580
+ * set them before running the agent.
581
+ *
582
+ * Best-effort: API errors are silently swallowed (the agent is already published).
583
+ */
584
+ async function checkRequiredSecrets(config, workspaceId, workspaceSlug, requiredSecrets) {
585
+ if (!requiredSecrets.length)
586
+ return;
587
+ let secrets;
588
+ try {
589
+ const result = await (0, api_1.request)(config, 'GET', `/workspaces/${workspaceId}/secrets`);
590
+ secrets = result.secrets;
591
+ if (!Array.isArray(secrets))
592
+ return;
593
+ }
594
+ catch {
595
+ return; // Can't reach API — fall back to showing all secrets as instructions
596
+ }
597
+ const existingNames = new Set(secrets.map(s => s.name));
598
+ const missing = requiredSecrets.filter(name => !existingNames.has(name));
599
+ if (missing.length === 0) {
600
+ // All secrets present — brief confirmation
601
+ process.stderr.write(chalk_1.default.green(`\n✓ All ${requiredSecrets.length} required secret${requiredSecrets.length === 1 ? '' : 's'} found in workspace '${workspaceSlug}'\n`));
602
+ return;
603
+ }
604
+ // Some secrets missing — warn with set commands
605
+ process.stderr.write(chalk_1.default.yellow(`\n⚠ ${missing.length} required secret${missing.length === 1 ? '' : 's'} not set in workspace '${workspaceSlug}':\n`));
606
+ for (const name of missing) {
607
+ process.stderr.write(` ${chalk_1.default.yellow(name)}\n`);
608
+ }
609
+ process.stderr.write(`\n Set before running:\n`);
610
+ for (const name of missing) {
611
+ process.stderr.write(` ${chalk_1.default.cyan(`orch secrets set ${name} <value>`)}\n`);
612
+ }
613
+ }
520
614
  /**
521
615
  * Batch publish all agents found in subdirectories, in dependency order.
522
616
  * Discovers orchagent.json/SKILL.md in immediate subdirectories,
@@ -883,6 +977,13 @@ function registerPublishCommand(program) {
883
977
  ` ${chalk_1.default.cyan(`"default_models": { "anthropic": "${modelVal}" }`)}\n\n` +
884
978
  ` The model resolution order is: caller --model flag → agent default_models → platform default.\n\n`));
885
979
  }
980
+ // DX-17: Validate model IDs in default_models
981
+ if (manifest.default_models && typeof manifest.default_models === 'object') {
982
+ const modelWarnings = (0, llm_1.validateModelIds)(manifest.default_models);
983
+ for (const w of modelWarnings) {
984
+ process.stderr.write(chalk_1.default.yellow(`Warning: ${w.message}\n`));
985
+ }
986
+ }
886
987
  // Auto-migrate inline schemas to schema.json
887
988
  const schemaPath = path_1.default.join(cwd, 'schema.json');
888
989
  let schemaFileExists = false;
@@ -1336,6 +1437,9 @@ function registerPublishCommand(program) {
1336
1437
  ` env var to detect the reserved port.\n\n`);
1337
1438
  }
1338
1439
  }
1440
+ // Pre-publish: check required secrets exist in workspace (DX-3)
1441
+ // Fail fast — warn before spending time on the publish API call
1442
+ await prePublishSecretsCheck(config, workspaceId || org.id, org.slug, manifest.required_secrets || []);
1339
1443
  // Create the agent (server auto-assigns version)
1340
1444
  let result;
1341
1445
  try {
@@ -1542,18 +1646,9 @@ function registerPublishCommand(program) {
1542
1646
  }
1543
1647
  // Warn if workspace has no LLM vault keys for this agent's providers (UX-13-01)
1544
1648
  await checkWorkspaceLlmKeys(config, workspaceId || org.id, org.slug, executionEngine, supportedProviders);
1545
- // Show required secrets with setup instructions (F-18)
1546
- if (manifest.required_secrets?.length) {
1547
- process.stdout.write(`\nRequired secrets:\n`);
1548
- for (const secret of manifest.required_secrets) {
1549
- process.stdout.write(` ${secret}\n`);
1550
- }
1551
- process.stdout.write(`\nSet secrets before running:\n`);
1552
- for (const secret of manifest.required_secrets) {
1553
- process.stdout.write(` orch secrets set ${secret} <value>\n`);
1554
- }
1555
- process.stdout.write(`\nView existing secrets: ${chalk_1.default.cyan('orch secrets list')}\n`);
1556
- }
1649
+ // Post-publish: confirm secrets status (DX-13, DX-3)
1650
+ // Shows green check when all present, yellow warning when missing
1651
+ await checkRequiredSecrets(config, workspaceId || org.id, org.slug, manifest.required_secrets || []);
1557
1652
  // Show security review result if available
1558
1653
  const secReview = result.security_review;
1559
1654
  if (secReview?.verdict) {