@oxygen-agent/cli 1.1011.97 → 1.1011.288

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.
Files changed (58) hide show
  1. package/README.md +1 -1
  2. package/dist/admin-primary-providers-render.js +25 -5
  3. package/dist/column-run-notices.d.ts +16 -0
  4. package/dist/column-run-notices.js +57 -0
  5. package/dist/command-manifest.js +10 -1
  6. package/dist/help.d.ts +27 -1
  7. package/dist/help.js +112 -29
  8. package/dist/http-client.js +8 -0
  9. package/dist/index.js +348 -127
  10. package/node_modules/@oxygen/cli-ugc/dist/commands.js +20 -69
  11. package/node_modules/@oxygen/shared/dist/byok-connect.d.ts +6 -4
  12. package/node_modules/@oxygen/shared/dist/byok-connect.js +11 -4
  13. package/node_modules/@oxygen/shared/dist/capability-discovery.d.ts +127 -1
  14. package/node_modules/@oxygen/shared/dist/capability-discovery.js +286 -24
  15. package/node_modules/@oxygen/shared/dist/collab.d.ts +1 -1
  16. package/node_modules/@oxygen/shared/dist/collab.js +12 -0
  17. package/node_modules/@oxygen/shared/dist/column-decision.js +7 -3
  18. package/node_modules/@oxygen/shared/dist/column-output-fields.js +45 -3
  19. package/node_modules/@oxygen/shared/dist/company-enrichment-fields.d.ts +20 -13
  20. package/node_modules/@oxygen/shared/dist/company-enrichment-fields.js +29 -13
  21. package/node_modules/@oxygen/shared/dist/contracts/sender-limits.js +1 -1
  22. package/node_modules/@oxygen/shared/dist/copilot-skills.generated.d.ts +2 -2
  23. package/node_modules/@oxygen/shared/dist/copilot-skills.generated.js +2 -2
  24. package/node_modules/@oxygen/shared/dist/credit-guidance.js +12 -1
  25. package/node_modules/@oxygen/shared/dist/enrichment-intents.d.ts +37 -0
  26. package/node_modules/@oxygen/shared/dist/enrichment-intents.js +79 -44
  27. package/node_modules/@oxygen/shared/dist/linkedin-url.d.ts +7 -0
  28. package/node_modules/@oxygen/shared/dist/linkedin-url.js +9 -0
  29. package/node_modules/@oxygen/shared/dist/log.d.ts +1 -0
  30. package/node_modules/@oxygen/shared/dist/log.js +15 -0
  31. package/node_modules/@oxygen/shared/dist/operational-telemetry.d.ts +1 -0
  32. package/node_modules/@oxygen/shared/dist/operational-telemetry.js +6 -2
  33. package/node_modules/@oxygen/shared/dist/otlp-log-sink.d.ts +7 -3
  34. package/node_modules/@oxygen/shared/dist/otlp-log-sink.js +22 -1
  35. package/node_modules/@oxygen/shared/dist/plan-limits.d.ts +3 -7
  36. package/node_modules/@oxygen/shared/dist/plan-limits.js +2 -14
  37. package/node_modules/@oxygen/shared/dist/sending-schedule.d.ts +39 -0
  38. package/node_modules/@oxygen/shared/dist/sending-schedule.js +41 -0
  39. package/node_modules/@oxygen/shared/dist/sequence-lifecycle.d.ts +19 -0
  40. package/node_modules/@oxygen/shared/dist/sequence-lifecycle.js +20 -0
  41. package/node_modules/@oxygen/shared/dist/sequence-sender-variables.d.ts +35 -0
  42. package/node_modules/@oxygen/shared/dist/sequence-sender-variables.js +63 -0
  43. package/node_modules/@oxygen/shared/dist/sequences.d.ts +13 -7
  44. package/node_modules/@oxygen/shared/dist/sequences.js +26 -0
  45. package/node_modules/@oxygen/shared/dist/signup-lead-payload.d.ts +2 -0
  46. package/node_modules/@oxygen/shared/dist/signup-lead-payload.js +5 -0
  47. package/node_modules/@oxygen/shared/dist/social-post-metrics-series.js +1 -15
  48. package/node_modules/@oxygen/shared/dist/standard-webhooks.d.ts +51 -0
  49. package/node_modules/@oxygen/shared/dist/standard-webhooks.js +124 -0
  50. package/node_modules/@oxygen/shared/dist/tags.d.ts +1 -1
  51. package/node_modules/@oxygen/shared/dist/tags.js +13 -1
  52. package/node_modules/@oxygen/shared/dist/ugc.d.ts +11 -7
  53. package/node_modules/@oxygen/shared/dist/ugc.js +14 -6
  54. package/node_modules/@oxygen/shared/dist/version.generated.d.ts +1 -1
  55. package/node_modules/@oxygen/shared/dist/version.generated.js +1 -1
  56. package/node_modules/@oxygen/shared/dist/zoned-time.d.ts +39 -0
  57. package/node_modules/@oxygen/shared/dist/zoned-time.js +81 -0
  58. package/package.json +1 -1
package/README.md CHANGED
@@ -34,4 +34,4 @@ oxygen update
34
34
 
35
35
  For product documentation, visit https://oxygen-agent.com/docs. For support, visit https://oxygen-agent.com.
36
36
 
37
- Version: 1.1011.97
37
+ Version: 1.1011.288
@@ -165,6 +165,10 @@ export function renderPrimaryProviderBoard(payload, options = {}) {
165
165
  ((num(summary.fallback_providers) ?? 0) > 0
166
166
  ? ` · ${count(summary.fallback_providers)} default-chain fallback${(num(summary.fallback_providers) ?? 0) === 1 ? "" : "s"}` +
167
167
  ((num(summary.fallback_critical) ?? 0) > 0 ? ` (${count(summary.fallback_critical)} critical)` : "")
168
+ : "") +
169
+ ((num(summary.outside_chain_providers) ?? 0) > 0
170
+ ? ` · ${count(summary.outside_chain_providers)} managed outside every chain` +
171
+ ((num(summary.outside_chain_critical) ?? 0) > 0 ? ` (${count(summary.outside_chain_critical)} critical)` : "")
168
172
  : ""));
169
173
  out(`COGS (managed primaries): 30d ${usd(summary.cost_30d_usd)} · 7d ${usd(summary.cost_7d_usd)}`);
170
174
  out(renderGuards(obj(board.guards)));
@@ -189,17 +193,23 @@ export function renderPrimaryProviderBoard(payload, options = {}) {
189
193
  }
190
194
  // A dry fallback account is invisible until a primary fails, which is
191
195
  // exactly when nobody has time to notice it — so it renders here too.
192
- const fallbacks = arr(board.default_chain_fallbacks)
193
- .map((row) => obj(row))
194
- .filter((row) => row !== null)
195
- .sort((a, b) => postureRank(str(obj(a.posture)?.level)) - postureRank(str(obj(b.posture)?.level)) ||
196
- (str(a.provider) ?? "").localeCompare(str(b.provider) ?? ""));
196
+ const fallbacks = rowsWorstFirst(board.default_chain_fallbacks);
197
197
  if (fallbacks.length > 0) {
198
198
  out("");
199
199
  out("Default-chain fallbacks (not primary anywhere — a routed chain reaches them when its primary fails):");
200
200
  for (const row of fallbacks)
201
201
  out(renderProvider(row, now, binary));
202
202
  }
203
+ // Managed data providers no routed chain reaches. Direct tool calls and product
204
+ // features still spend OXYGEN's key there, and until this group existed a dead
205
+ // key or a dry wallet on one of them showed on no surface at all.
206
+ const outsideChains = rowsWorstFirst(board.managed_outside_chains);
207
+ if (outsideChains.length > 0) {
208
+ out("");
209
+ out("Managed outside every chain (OXYGEN holds the key; direct tool calls and product features reach them, no routed chain does):");
210
+ for (const row of outsideChains)
211
+ out(renderProvider(row, now, binary));
212
+ }
203
213
  const intents = arr(board.intents_without_primary)
204
214
  .map((row) => obj(row))
205
215
  .filter((row) => row !== null);
@@ -234,6 +244,14 @@ export function renderPrimaryProviderBoard(payload, options = {}) {
234
244
  }
235
245
  return lines.join("\n");
236
246
  }
247
+ /** Provider rows from one board group, worst posture first, then by name. */
248
+ function rowsWorstFirst(value) {
249
+ return arr(value)
250
+ .map((row) => obj(row))
251
+ .filter((row) => row !== null)
252
+ .sort((a, b) => postureRank(str(obj(a.posture)?.level)) - postureRank(str(obj(b.posture)?.level)) ||
253
+ (str(a.provider) ?? "").localeCompare(str(b.provider) ?? ""));
254
+ }
237
255
  const INTENT_KIND_COPY = {
238
256
  fallback_only: "fallback only — paid routed steps exist but none is seeded primary (the real gap):",
239
257
  preflight_only: "preflight only — every step is a free sizing/count probe (by design):",
@@ -285,6 +303,8 @@ function renderProvider(row, now, binary) {
285
303
  const healthStatus = str(health.latest_status) ?? (probe === "none" ? "unmonitored" : "no data");
286
304
  const healthDetail = [
287
305
  `${probe} probe`,
306
+ // No zero-credit read exists for this vendor; the reasons below say why.
307
+ str(health.unwatchable) ? "structurally unwatchable" : null,
288
308
  ago(health.last_checked_at, now),
289
309
  num(health.uptime_pct_96h) === null ? null : `${pct(health.uptime_pct_96h)} up 96h`,
290
310
  (num(health.degraded_pct_96h) ?? 0) > 0 ? `${pct(health.degraded_pct_96h)} degraded` : null,
@@ -9,6 +9,16 @@
9
9
  * live run, or a preview the server could not produce).
10
10
  */
11
11
  export declare function formatAiPromptPreviewNotice(data: unknown): string | null;
12
+ /**
13
+ * `columns run --dry-run` and `enrich-column preview` carry
14
+ * `provider_availability.lanes`: whether Oxygen's managed account behind each
15
+ * lane can take a call right now (OXP-1.14.12). On 2026-09-27 a BounceBan
16
+ * column's dry run printed nothing while that account was benched and the
17
+ * approved run then deferred forever. One line for the verdict (with the fix
18
+ * when no lane can run), then one line per lane that cannot run. `--json`
19
+ * readers get the fields; nothing is printed while every lane can run.
20
+ */
21
+ export declare function formatProviderLaneNotices(availability: unknown): string[];
12
22
  /**
13
23
  * Advisory reference findings from `columns add` / `update` / `rename`.
14
24
  *
@@ -19,3 +29,9 @@ export declare function formatAiPromptPreviewNotice(data: unknown): string | nul
19
29
  * sibling's reference. Printed to stderr so `--json` piping stays clean.
20
30
  */
21
31
  export declare function formatColumnReferenceNotices(data: unknown): string[];
32
+ /**
33
+ * The inputs `columns add --capability` mapped, in `--input` syntax, or the
34
+ * warning and command for one it could not find. The envelope carries both for
35
+ * `--json` readers; this is the line a human sees on stderr.
36
+ */
37
+ export declare function formatEnrichmentInputMappingNotice(data: unknown): string[];
@@ -30,11 +30,41 @@ export function formatAiPromptPreviewNotice(data) {
30
30
  const truncation = preview.truncated === true
31
31
  ? ` — truncated to ${shown.length} of ${chars} chars`
32
32
  : "";
33
+ const state = isRecord(preview.state) && isRecord(preview.state.row)
34
+ ? JSON.stringify(preview.state.row, null, 2)
35
+ : null;
33
36
  return [
34
37
  `prompt preview — row ${rowId} (${source}), before grounding${truncation}:`,
35
38
  shown,
39
+ ...(state ? [`row values sent separately in classification state:\n${state.slice(0, 4000)}${state.length > 4000 ? "… (truncated)" : ""}`] : []),
36
40
  ].join("\n");
37
41
  }
42
+ /**
43
+ * `columns run --dry-run` and `enrich-column preview` carry
44
+ * `provider_availability.lanes`: whether Oxygen's managed account behind each
45
+ * lane can take a call right now (OXP-1.14.12). On 2026-09-27 a BounceBan
46
+ * column's dry run printed nothing while that account was benched and the
47
+ * approved run then deferred forever. One line for the verdict (with the fix
48
+ * when no lane can run), then one line per lane that cannot run. `--json`
49
+ * readers get the fields; nothing is printed while every lane can run.
50
+ */
51
+ export function formatProviderLaneNotices(availability) {
52
+ if (!isRecord(availability) || !Array.isArray(availability.lanes))
53
+ return [];
54
+ const lines = [];
55
+ for (const lane of availability.lanes) {
56
+ if (!isRecord(lane) || (lane.availability !== "degraded" && lane.availability !== "unknown"))
57
+ continue;
58
+ const state = lane.availability === "unknown" ? "availability unknown" : "unavailable";
59
+ const detail = readRecordString(lane, "message") ?? readRecordString(lane, "next_action");
60
+ lines.push(`! ${readRecordString(lane, "tool_id") ?? "a lane"} ${state}${detail ? `: ${detail}` : ""}`);
61
+ }
62
+ const message = readRecordString(availability, "message");
63
+ if (lines.length === 0 || !message)
64
+ return lines;
65
+ const fix = availability.will_produce_results === false ? readRecordString(availability, "next_action") : null;
66
+ return [`! ${message}${fix ? ` ${fix}` : ""}`, ...lines];
67
+ }
38
68
  /**
39
69
  * Advisory reference findings from `columns add` / `update` / `rename`.
40
70
  *
@@ -57,3 +87,30 @@ export function formatColumnReferenceNotices(data) {
57
87
  }
58
88
  return notices;
59
89
  }
90
+ /**
91
+ * The inputs `columns add --capability` mapped, in `--input` syntax, or the
92
+ * warning and command for one it could not find. The envelope carries both for
93
+ * `--json` readers; this is the line a human sees on stderr.
94
+ */
95
+ export function formatEnrichmentInputMappingNotice(data) {
96
+ if (!isRecord(data) || !isRecord(data.input_mapping))
97
+ return [];
98
+ const notices = [];
99
+ const mapped = isRecord(data.input_mapping.mapped) ? Object.entries(data.input_mapping.mapped) : [];
100
+ if (mapped.length > 0) {
101
+ const origin = data.input_mapping.source === "auto_detected" ? "Inputs mapped from this table" : "Inputs";
102
+ notices.push(`${origin}: ${mapped.map(([input, reads]) => `${input}=${String(reads)}`).join(", ")}.`);
103
+ }
104
+ const gap = Array.isArray(data.warnings)
105
+ ? data.warnings.find((entry) => isRecord(entry) && entry.code === "enrichment_inputs_unmapped")
106
+ : undefined;
107
+ if (isRecord(gap)) {
108
+ const message = readRecordString(gap, "message");
109
+ if (message)
110
+ notices.push(`warning: ${message}`);
111
+ const nextAction = readRecordString(data, "next_action");
112
+ if (nextAction)
113
+ notices.push(nextAction);
114
+ }
115
+ return notices;
116
+ }
@@ -25,8 +25,9 @@ const COMMAND_SEARCH_STOP_WORDS = new Set([
25
25
  // leaf in the tree using that verb, so the default (mutates:false) would have
26
26
  // advertised the most consequential collaboration write as a read.
27
27
  const MUTATING_VERBS = new Set([
28
+ "forget-whatsapp-chat",
28
29
  "start-whatsapp",
29
- "brand-approve", "revoke-invite", "confirm",
30
+ "revoke-invite", "confirm",
30
31
  // Exact leaf: `ugc creators set-target` writes one creator's posting deal.
31
32
  "set-target",
32
33
  "infrastructure-exit",
@@ -116,6 +117,7 @@ const MUTATING_COMMANDS = new Set([
116
117
  // zero-credit. Keep those exceptions exact so discovery never invents spend.
117
118
  const ZERO_CREDIT_APPROVAL_COMMANDS = new Set([
118
119
  "inbox delete-message",
120
+ "inbox forget-whatsapp-chat",
119
121
  "tables watcher preview",
120
122
  "knowledge repositories connect",
121
123
  "knowledge repositories resolve",
@@ -129,6 +131,12 @@ const ZERO_CREDIT_APPROVAL_COMMANDS = new Set([
129
131
  "ugc sponsorship cancel",
130
132
  // A Function binding stores a cap for future runs; creating it executes nothing.
131
133
  "functions bind",
134
+ // Same shape: a draft Sequence stores its LinkedIn --max-credits ceiling for a
135
+ // later approved `sequences start`, which carries its own cap. Creating or
136
+ // editing one spends nothing; the manifest said otherwise while MCP called
137
+ // the same tools free (blind user eval, 2026-09-29).
138
+ "sequences create",
139
+ "sequences update",
132
140
  // Starting an attended session itself is free. This legacy compatibility
133
141
  // flag neither authorizes nor caps later inference; only `copilot send` can
134
142
  // trigger the billed model loop.
@@ -166,6 +174,7 @@ const READ_ONLY_COMMANDS = new Set([
166
174
  // calling the command without its confirmation flag writes anything.
167
175
  const PREVIEW_BY_DEFAULT_COMMANDS = new Set([
168
176
  "inbox delete-message",
177
+ "inbox forget-whatsapp-chat",
169
178
  "tables watcher preview",
170
179
  "ugc creators invite",
171
180
  "ugc creators send-invite",
package/dist/help.d.ts CHANGED
@@ -1,4 +1,5 @@
1
- import type { Command } from "commander";
1
+ import type { Command, CommanderError } from "commander";
2
+ import { type CliFailure } from "@oxygen/shared/cli-result";
2
3
  export declare function applyOxygenHelp(program: Command, binaryName: string): void;
3
4
  /**
4
5
  * Turn commander's own edit-distance suggester on for EVERY command.
@@ -28,4 +29,29 @@ export declare function enableCommandSuggestions(command: Command): void;
28
29
  * all, or when the flag is not a long option.
29
30
  */
30
31
  export declare function unknownOptionHint(program: Command, argv: readonly string[], message: string): string | null;
32
+ /**
33
+ * Drop the help flag when it follows an operand that names no subcommand.
34
+ *
35
+ * Commander answers `--help` before it checks the operand, so `oxygen sequence
36
+ * --help` printed the ROOT help and exited 0: a typo'd command read as success,
37
+ * and the agent took the root help for the group it meant (blind user eval,
38
+ * 2026-09-29). Without the flag the same argv fails as the unknown command it
39
+ * is, with commander's suggestion, our hint and exit 2. Only a group that takes
40
+ * no positional argument qualifies, so `sequences get <id> --help` still helps.
41
+ */
42
+ export declare function withoutHelpForUnknownCommand(program: Command, argv: readonly string[]): string[];
31
43
  export declare function unknownCommandHint(program: Command, argv: readonly string[]): string | null;
44
+ /**
45
+ * The failure envelope and exit code for a commander usage error, or null when
46
+ * the error is not one (`--help` and `--version` exit 0). Commander refuses
47
+ * before any action runs, so the envelope every handler prints never covered
48
+ * these: `ugc creators accept` without `--linkedin-url` gave `--json` callers an
49
+ * empty stdout and exit 1 (UGC E2E pass, 2026-09-29). `hint` is the first hint
50
+ * already written to stderr; it becomes `details.next_step`.
51
+ */
52
+ export declare function commanderUsageFailure(program: Command, argv: readonly string[], error: CommanderError, hint: string | null): {
53
+ envelope: CliFailure;
54
+ exitCode: number;
55
+ } | null;
56
+ /** `--json` anywhere before a `--` terminator, where commander still reads options. */
57
+ export declare function argvRequestsJson(argv: readonly string[]): boolean;
package/dist/help.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { CLI_EXIT_CODE_TABLE } from "@oxygen/shared";
2
+ import { exitCodeForErrorCode, failure } from "@oxygen/shared/cli-result";
2
3
  // Grouped `--help` for the ~70 top-level command groups: commander's default
3
4
  // registration-order wall (login, auth, profiles, ... before any GTM noun) is
4
5
  // the first thing every fresh agent reads, so order it by the OXYGEN OS layer
@@ -225,7 +226,7 @@ export function unknownOptionHint(program, argv, message) {
225
226
  const flag = /unknown option '(--[^']+)'/.exec(message)?.[1];
226
227
  if (!flag)
227
228
  return null;
228
- const [command, path] = resolveCommandPath(program, argv);
229
+ const { command, path } = resolveCommandPath(program, argv);
229
230
  const longs = new Set();
230
231
  for (let current = command; current; current = current.parent) {
231
232
  for (const option of current.options)
@@ -245,45 +246,127 @@ export function unknownOptionHint(program, argv, message) {
245
246
  }
246
247
  return `Hint: \`${path}\` has no \`${bare}\`. ${helpLine}`;
247
248
  }
248
- /** Walk the command path in argv (stopping at the first option) and return the deepest command with its printable path. */
249
+ /**
250
+ * Walk the command path in argv and return the deepest command, its printable
251
+ * path, and the first token that is no subcommand of it (`next`). Root options
252
+ * and their values (`--org <id>`, `--profile <name>`) are stepped over, since
253
+ * agents routinely pass them first; any other option ends the walk, because
254
+ * options below the root are commander's business.
255
+ */
249
256
  function resolveCommandPath(program, argv) {
250
- let current = program;
257
+ let command = program;
251
258
  let path = program.name();
252
- for (const token of argv) {
253
- if (token.startsWith("-"))
254
- break;
255
- const child = current.commands.find((candidate) => candidate.name() === token || candidate.aliases().includes(token));
259
+ for (let index = 0; index < argv.length; index += 1) {
260
+ const token = argv[index];
261
+ if (token.startsWith("-")) {
262
+ const flag = token.split("=")[0];
263
+ const option = program.options.find((candidate) => candidate.long === flag || candidate.short === flag);
264
+ if (!option)
265
+ break;
266
+ const value = argv[index + 1];
267
+ if (!token.includes("=") && (option.required || (option.optional && value !== undefined && !value.startsWith("-"))))
268
+ index += 1;
269
+ continue;
270
+ }
271
+ const child = command.commands.find((candidate) => candidate.name() === token || candidate.aliases().includes(token));
256
272
  if (!child)
257
- break;
258
- current = child;
273
+ return { command, path, next: token };
274
+ command = child;
259
275
  path = `${path} ${token}`;
260
276
  }
261
- return [current, path];
277
+ return { command, path };
278
+ }
279
+ /**
280
+ * Drop the help flag when it follows an operand that names no subcommand.
281
+ *
282
+ * Commander answers `--help` before it checks the operand, so `oxygen sequence
283
+ * --help` printed the ROOT help and exited 0: a typo'd command read as success,
284
+ * and the agent took the root help for the group it meant (blind user eval,
285
+ * 2026-09-29). Without the flag the same argv fails as the unknown command it
286
+ * is, with commander's suggestion, our hint and exit 2. Only a group that takes
287
+ * no positional argument qualifies, so `sequences get <id> --help` still helps.
288
+ */
289
+ export function withoutHelpForUnknownCommand(program, argv) {
290
+ const args = argv.slice(2);
291
+ const end = args.indexOf("--");
292
+ const scanned = end < 0 ? args : args.slice(0, end);
293
+ const isHelp = (token) => token === "--help" || token === "-h";
294
+ if (!scanned.some(isHelp))
295
+ return [...argv];
296
+ const { command, next } = resolveCommandPath(program, args);
297
+ if (next === undefined || command.commands.length === 0 || command.registeredArguments.length > 0)
298
+ return [...argv];
299
+ return [...argv.slice(0, 2), ...args.filter((token, index) => !(index < scanned.length && isHelp(token)))];
262
300
  }
263
301
  export function unknownCommandHint(program, argv) {
264
- let current = program;
265
- let path = program.name();
266
- for (const token of argv) {
267
- // Options and their values are commander's business; only the command path
268
- // is walked here.
269
- if (token.startsWith("-"))
270
- break;
271
- const child = current.commands.find((candidate) => candidate.name() === token || candidate.aliases().includes(token));
272
- if (child) {
273
- current = child;
274
- path = `${path} ${token}`;
275
- continue;
276
- }
277
- if (current.commands.length === 0)
278
- return null;
279
- return buildUnknownCommandHint(current, path, token);
280
- }
281
- return null;
302
+ const { command, path, next } = resolveCommandPath(program, argv);
303
+ if (next === undefined || command.commands.length === 0)
304
+ return null;
305
+ return buildUnknownCommandHint(command, path, next);
306
+ }
307
+ /**
308
+ * Commander's refusals, each as the `missing_*` / `invalid_*` code the exit
309
+ * table classes as 2 (usage). `commander.help` is a command group run without
310
+ * a subcommand (help on stderr); `commander.error` is a command's own
311
+ * `command.error(...)`.
312
+ */
313
+ const COMMANDER_USAGE_CODES = {
314
+ "commander.missingMandatoryOptionValue": "missing_required_option",
315
+ "commander.optionMissingArgument": "missing_option_value",
316
+ "commander.missingArgument": "missing_argument",
317
+ "commander.help": "missing_subcommand",
318
+ "commander.unknownOption": "invalid_option",
319
+ "commander.unknownCommand": "invalid_command",
320
+ "commander.excessArguments": "invalid_arguments",
321
+ "commander.invalidArgument": "invalid_argument",
322
+ "commander.conflictingOption": "invalid_option_combination",
323
+ "commander.error": "invalid_request",
324
+ };
325
+ /**
326
+ * The failure envelope and exit code for a commander usage error, or null when
327
+ * the error is not one (`--help` and `--version` exit 0). Commander refuses
328
+ * before any action runs, so the envelope every handler prints never covered
329
+ * these: `ugc creators accept` without `--linkedin-url` gave `--json` callers an
330
+ * empty stdout and exit 1 (UGC E2E pass, 2026-09-29). `hint` is the first hint
331
+ * already written to stderr; it becomes `details.next_step`.
332
+ */
333
+ export function commanderUsageFailure(program, argv, error, hint) {
334
+ if (error.exitCode === 0)
335
+ return null;
336
+ const code = COMMANDER_USAGE_CODES[error.code];
337
+ if (!code)
338
+ return null;
339
+ const { path } = resolveCommandPath(program, argv);
340
+ const message = error.code === "commander.help"
341
+ ? `\`${path}\` needs a subcommand.`
342
+ : error.message.replace(/^error:\s*/, "");
343
+ const option = /option '(-[^' ]+)/.exec(message)?.[1];
344
+ const helpFor = error.code === "commander.help" ? "subcommands" : "options";
345
+ return {
346
+ envelope: failure(path.split(" ").slice(1).join(" ") || program.name(), {
347
+ code,
348
+ message,
349
+ details: {
350
+ commander_code: error.code,
351
+ ...(option ? { option } : {}),
352
+ next_step: hint?.replace(/^Hint:\s*/, "") ?? `Run \`${path} --help\` for its ${helpFor}.`,
353
+ },
354
+ }),
355
+ exitCode: exitCodeForErrorCode(code),
356
+ };
357
+ }
358
+ /** `--json` anywhere before a `--` terminator, where commander still reads options. */
359
+ export function argvRequestsJson(argv) {
360
+ const end = argv.indexOf("--");
361
+ return (end < 0 ? argv : argv.slice(0, end)).includes("--json");
282
362
  }
283
363
  function buildUnknownCommandHint(parent, path, token) {
284
364
  const names = parent.commands.map((command) => command.name()).filter((name) => name !== "help");
285
365
  const lower = token.toLowerCase();
286
- const synonym = (COMMAND_SYNONYMS[lower] ?? []).find((candidate) => names.includes(candidate));
366
+ // A read intent on a budget must never point at the mutating `set` command.
367
+ const synonym = path.endsWith(" budget") && lower === "get" && names.includes("list")
368
+ ? "list"
369
+ : (COMMAND_SYNONYMS[lower] ?? []).find((candidate) => names.includes(candidate));
287
370
  // A unique prefix is the other guess commander cannot make: `tables desc`
288
371
  // scores too far from `describe` on edit distance, but it names exactly one
289
372
  // command.
@@ -5,6 +5,11 @@ import { defaultApiUrl, loadCredentialsWithEndpoint } from "./credentials.js";
5
5
  import { assertResolvedApiUrl, cliEndpointDetails, describeCliEndpoint, isProdApiUrl, resolveCliUpdateGuidance, } from "./runtime.js";
6
6
  const DEFAULT_REQUEST_TIMEOUT_MS = 120_000;
7
7
  const CLI_COMPATIBILITY_CHECK_TIMEOUT_MS = 5_000;
8
+ // A write that fails in transit may already have been applied before the
9
+ // connection dropped or the client gave up, so a blind retry can repeat it.
10
+ // Replay on the Idempotency-Key is per route (most routes ignore the header),
11
+ // so this must not promise that a same-key retry is safe.
12
+ const UNCONFIRMED_WRITE_NEXT_STEP = "This change may already have been applied. Check the current state (for example with the matching list or get command) before you retry.";
8
13
  const cliCompatibilityCheckKeys = new Set();
9
14
  export async function requestOxygen(// skipcq: JS-R1005
10
15
  path, options = {}) {
@@ -99,6 +104,7 @@ path, options = {}) {
99
104
  catch (error) {
100
105
  if (error instanceof OxygenError)
101
106
  throw error;
107
+ const unconfirmedWrite = method === "GET" ? {} : { next_step: UNCONFIRMED_WRITE_NEXT_STEP };
102
108
  if (timeout.timedOut() || (timeoutMs !== undefined && isAbortError(error))) {
103
109
  throw new OxygenError("network_timeout", "Oxygen API request timed out.", {
104
110
  details: {
@@ -107,6 +113,7 @@ path, options = {}) {
107
113
  timeout_ms: timeoutMs,
108
114
  trace_id: traceId,
109
115
  ...(idempotencyKey ? { idempotency_key: idempotencyKey } : {}),
116
+ ...unconfirmedWrite,
110
117
  },
111
118
  exitCode: 1,
112
119
  });
@@ -116,6 +123,7 @@ path, options = {}) {
116
123
  api_url: apiUrl,
117
124
  reason: error instanceof Error ? error.message : "unknown",
118
125
  ...(idempotencyKey ? { idempotency_key: idempotencyKey } : {}),
126
+ ...unconfirmedWrite,
119
127
  },
120
128
  exitCode: 1,
121
129
  });