@oxygen-agent/cli 1.1011.326 → 1.1011.440

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 (98) hide show
  1. package/README.md +1 -1
  2. package/dist/command-manifest.js +15 -4
  3. package/dist/connectors-commands.d.ts +4 -1
  4. package/dist/connectors-commands.js +84 -10
  5. package/dist/contract-flags.d.ts +2 -0
  6. package/dist/contract-flags.js +26 -11
  7. package/dist/crm-support-flags.d.ts +11 -0
  8. package/dist/crm-support-flags.js +60 -0
  9. package/dist/customer-operation-commands.d.ts +5 -0
  10. package/dist/customer-operation-commands.js +101 -0
  11. package/dist/database-query-flags.d.ts +2 -0
  12. package/dist/database-query-flags.js +6 -0
  13. package/dist/help.d.ts +3 -1
  14. package/dist/help.js +43 -0
  15. package/dist/http-client.d.ts +2 -0
  16. package/dist/http-client.js +41 -1
  17. package/dist/index.js +640 -234
  18. package/dist/knowledge-repository-commands.js +12 -14
  19. package/dist/mailbox-ramp-notice.d.ts +11 -0
  20. package/dist/mailbox-ramp-notice.js +53 -0
  21. package/dist/settings-commands.d.ts +11 -0
  22. package/dist/settings-commands.js +73 -0
  23. package/dist/skills.d.ts +5 -4
  24. package/dist/skills.js +106 -79
  25. package/dist/tool-catalog-detail-command.d.ts +7 -0
  26. package/dist/tool-catalog-detail-command.js +19 -0
  27. package/dist/tool-catalog-search-window.d.ts +6 -0
  28. package/dist/tool-catalog-search-window.js +12 -0
  29. package/dist/transcript.js +1 -0
  30. package/node_modules/@oxygen/shared/dist/bounded-read-result.d.ts +4 -0
  31. package/node_modules/@oxygen/shared/dist/bounded-read-result.js +25 -0
  32. package/node_modules/@oxygen/shared/dist/capability-discovery.d.ts +47 -1
  33. package/node_modules/@oxygen/shared/dist/capability-discovery.js +625 -39
  34. package/node_modules/@oxygen/shared/dist/connection-approval.d.ts +2 -0
  35. package/node_modules/@oxygen/shared/dist/connection-approval.js +2 -0
  36. package/node_modules/@oxygen/shared/dist/contracts/browser-operations.d.ts +2 -0
  37. package/node_modules/@oxygen/shared/dist/contracts/browser-operations.js +110 -0
  38. package/node_modules/@oxygen/shared/dist/contracts/connectors.d.ts +32 -6
  39. package/node_modules/@oxygen/shared/dist/contracts/connectors.js +56 -4
  40. package/node_modules/@oxygen/shared/dist/contracts/crm-attributes.d.ts +4 -0
  41. package/node_modules/@oxygen/shared/dist/contracts/crm-attributes.js +39 -0
  42. package/node_modules/@oxygen/shared/dist/contracts/crm-enrichment-read.d.ts +21 -0
  43. package/node_modules/@oxygen/shared/dist/contracts/crm-enrichment-read.js +15 -0
  44. package/node_modules/@oxygen/shared/dist/contracts/crm-support.d.ts +117 -0
  45. package/node_modules/@oxygen/shared/dist/contracts/crm-support.js +43 -0
  46. package/node_modules/@oxygen/shared/dist/contracts/customer-operations.d.ts +2 -0
  47. package/node_modules/@oxygen/shared/dist/contracts/customer-operations.js +131 -0
  48. package/node_modules/@oxygen/shared/dist/contracts/dashboard-tabs.d.ts +3 -0
  49. package/node_modules/@oxygen/shared/dist/contracts/dashboard-tabs.js +2 -0
  50. package/node_modules/@oxygen/shared/dist/contracts/database.d.ts +30 -0
  51. package/node_modules/@oxygen/shared/dist/contracts/database.js +29 -0
  52. package/node_modules/@oxygen/shared/dist/contracts/integration-connect.d.ts +4 -0
  53. package/node_modules/@oxygen/shared/dist/contracts/integration-connect.js +7 -0
  54. package/node_modules/@oxygen/shared/dist/contracts/knowledge-content-reads.d.ts +43 -0
  55. package/node_modules/@oxygen/shared/dist/contracts/knowledge-content-reads.js +20 -0
  56. package/node_modules/@oxygen/shared/dist/contracts/linkedin-invites-list.d.ts +12 -0
  57. package/node_modules/@oxygen/shared/dist/contracts/linkedin-invites-list.js +8 -0
  58. package/node_modules/@oxygen/shared/dist/contracts/operations.d.ts +48 -2
  59. package/node_modules/@oxygen/shared/dist/contracts/operations.js +68 -2
  60. package/node_modules/@oxygen/shared/dist/contracts/table-grid-query.d.ts +4 -0
  61. package/node_modules/@oxygen/shared/dist/contracts/table-grid-query.js +6 -0
  62. package/node_modules/@oxygen/shared/dist/contracts/table-query-grid.d.ts +21 -0
  63. package/node_modules/@oxygen/shared/dist/contracts/table-query-grid.js +27 -0
  64. package/node_modules/@oxygen/shared/dist/contracts/tool-catalog-detail.d.ts +19 -0
  65. package/node_modules/@oxygen/shared/dist/contracts/tool-catalog-detail.js +10 -0
  66. package/node_modules/@oxygen/shared/dist/contracts/tool-catalog-search-window.d.ts +12 -0
  67. package/node_modules/@oxygen/shared/dist/contracts/tool-catalog-search-window.js +7 -0
  68. package/node_modules/@oxygen/shared/dist/contracts/workspace-pages.d.ts +17 -0
  69. package/node_modules/@oxygen/shared/dist/contracts/workspace-pages.js +49 -0
  70. package/node_modules/@oxygen/shared/dist/decision-ledger-parser.d.ts +32 -0
  71. package/node_modules/@oxygen/shared/dist/decision-ledger-parser.js +303 -0
  72. package/node_modules/@oxygen/shared/dist/decision-ledger.d.ts +10 -0
  73. package/node_modules/@oxygen/shared/dist/decision-ledger.generated.d.ts +2 -0
  74. package/node_modules/@oxygen/shared/dist/decision-ledger.generated.js +717 -0
  75. package/node_modules/@oxygen/shared/dist/decision-ledger.js +20 -0
  76. package/node_modules/@oxygen/shared/dist/email-campaign-ramp.d.ts +104 -0
  77. package/node_modules/@oxygen/shared/dist/email-campaign-ramp.js +250 -0
  78. package/node_modules/@oxygen/shared/dist/mcp-event-contract.d.ts +10 -7
  79. package/node_modules/@oxygen/shared/dist/mcp-event-contract.js +11 -7
  80. package/node_modules/@oxygen/shared/dist/mcp-table-widget.d.ts +50 -0
  81. package/node_modules/@oxygen/shared/dist/mcp-table-widget.js +24 -0
  82. package/node_modules/@oxygen/shared/dist/plan-band.d.ts +8 -0
  83. package/node_modules/@oxygen/shared/dist/plan-band.js +12 -0
  84. package/node_modules/@oxygen/shared/dist/plan-limits.d.ts +20 -0
  85. package/node_modules/@oxygen/shared/dist/plan-limits.js +41 -0
  86. package/node_modules/@oxygen/shared/dist/provider-input-value.d.ts +5 -0
  87. package/node_modules/@oxygen/shared/dist/provider-input-value.js +10 -0
  88. package/node_modules/@oxygen/shared/dist/sequences.d.ts +33 -4
  89. package/node_modules/@oxygen/shared/dist/sequences.js +48 -13
  90. package/node_modules/@oxygen/shared/dist/user-capability-routing.js +17 -0
  91. package/node_modules/@oxygen/shared/dist/version.generated.d.ts +1 -1
  92. package/node_modules/@oxygen/shared/dist/version.generated.js +1 -1
  93. package/node_modules/@oxygen/shared/package.json +8 -6
  94. package/package.json +1 -1
  95. package/node_modules/@oxygen/shared/dist/copilot-skills.d.ts +0 -15
  96. package/node_modules/@oxygen/shared/dist/copilot-skills.generated.d.ts +0 -31
  97. package/node_modules/@oxygen/shared/dist/copilot-skills.generated.js +0 -41
  98. package/node_modules/@oxygen/shared/dist/copilot-skills.js +0 -6
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.326
37
+ Version: 1.1011.440
@@ -1,3 +1,4 @@
1
+ import { OPERATION_SPECS } from "@oxygen/shared/contracts/operations";
1
2
  import { CLI_EXIT_CODE_TABLE, OXYGEN_VERSION, inferUserCapabilityRoute, serializeCapabilityRoute, } from "@oxygen/shared";
2
3
  const DEFAULT_COMMAND_SEARCH_LIMIT = 10;
3
4
  const MAX_COMMAND_SEARCH_LIMIT = 25;
@@ -173,6 +174,13 @@ const READ_ONLY_COMMANDS = new Set([
173
174
  // invocation still previews, so machine-readable discovery must not imply that
174
175
  // calling the command without its confirmation flag writes anything.
175
176
  const PREVIEW_BY_DEFAULT_COMMANDS = new Set([
177
+ // /api/cli/find/run treats an omitted mode as dry_run for every Find capability.
178
+ "find email",
179
+ "find personal-email",
180
+ "find phone",
181
+ "find linkedin",
182
+ "find person",
183
+ "find company",
176
184
  "inbox delete-message",
177
185
  "inbox forget-whatsapp-chat",
178
186
  "tables watcher preview",
@@ -334,6 +342,7 @@ function toManifestEntry(command, path) {
334
342
  const leafVerb = path[path.length - 1] ?? "";
335
343
  const verbPrefix = leafVerb.split("-")[0] ?? leafVerb;
336
344
  const commandName = path.join(" ");
345
+ const operations = OPERATION_SPECS.filter((spec) => spec.cli.path.join(" ") === commandName);
337
346
  return {
338
347
  name: commandName,
339
348
  group: path[0] ?? "",
@@ -348,7 +357,9 @@ function toManifestEntry(command, path) {
348
357
  description: option.description,
349
358
  required: option.mandatory,
350
359
  })),
351
- spends_credits: !ZERO_CREDIT_APPROVAL_COMMANDS.has(commandName) &&
360
+ // Some native commands read without flags and write with an explicit grant.
361
+ // Their metadata must include every available action's authorization posture.
362
+ spends_credits: operations.length ? operations.some((spec) => spec.spends) : (!ZERO_CREDIT_APPROVAL_COMMANDS.has(commandName) &&
352
363
  (flagStrings.some((flags) => flags.includes("--approved") ||
353
364
  flags.includes("--max-credits") ||
354
365
  // Legacy Copilot compatibility plus any future explicit budget flag.
@@ -361,11 +372,11 @@ function toManifestEntry(command, path) {
361
372
  // cap flag exists for either, so the flag heuristic alone would misreport
362
373
  // them as free.
363
374
  (path[0] === "copilot" &&
364
- (leafVerb === "send" || leafVerb === "approve"))),
365
- mutates: !READ_ONLY_COMMANDS.has(commandName)
375
+ (leafVerb === "send" || leafVerb === "approve")))),
376
+ mutates: operations.length ? operations.some((spec) => spec.mutates) : (!READ_ONLY_COMMANDS.has(commandName)
366
377
  && (MUTATING_COMMANDS.has(commandName)
367
378
  || MUTATING_VERBS.has(leafVerb)
368
- || MUTATING_VERBS.has(verbPrefix)),
379
+ || MUTATING_VERBS.has(verbPrefix))),
369
380
  preview_by_default: flagStrings.some((flags) => flags.includes("--live")) ||
370
381
  PREVIEW_BY_DEFAULT_COMMANDS.has(commandName),
371
382
  json_supported: flagStrings.some((flags) => flags.includes("--json")),
@@ -5,7 +5,10 @@ type JsonOptions = {
5
5
  };
6
6
  type Handle = (command: string, options: JsonOptions, action: () => Promise<unknown>) => Promise<void>;
7
7
  export declare const CONNECTORS_FLAG_OVERLAY: ContractFlagOverlay;
8
- /** The PATCH body for the flags given; one of the two workspace flags is required. */
8
+ export declare const CONNECTOR_EVENTS_REVOKE_FLAG_OVERLAY: ContractFlagOverlay;
9
+ /** The revoke body for the flags given; --subscription is required. */
10
+ export declare function buildConnectorEventsRevokeBody(options: Record<string, unknown>): Record<string, unknown>;
11
+ /** The PATCH body for the flags given; a workspace or a credit ceiling flag is required. */
9
12
  export declare function buildConnectorUpdateBody(options: Record<string, unknown>): Record<string, unknown>;
10
13
  export declare function registerConnectorsCommands(program: Command, handle: Handle): void;
11
14
  export {};
@@ -5,45 +5,117 @@
5
5
  // disagree about what can be set.
6
6
  import { Command } from "commander";
7
7
  import { OxygenError } from "@oxygen/shared/cli-result";
8
- import { CONNECTOR_UPDATE_OPERATION } from "@oxygen/shared/contracts/connectors";
8
+ import { CONNECTOR_EVENTS_REVOKE_OPERATION, CONNECTOR_UPDATE_OPERATION } from "@oxygen/shared/contracts/connectors";
9
9
  import { operationPath } from "@oxygen/shared/contracts/operations";
10
10
  import { addContractFlags, readContractFlags } from "./contract-flags.js";
11
11
  import { requestOxygen } from "./http-client.js";
12
12
  const update = CONNECTOR_UPDATE_OPERATION;
13
+ const revokeEvents = CONNECTOR_EVENTS_REVOKE_OPERATION;
13
14
  export const CONNECTORS_FLAG_OVERLAY = {
14
15
  default_workspace: {
15
16
  help: "The workspace this app should work in without asking: its id, slug or exact name from `orgs list`. You must be a member.",
16
17
  valueName: "<workspace>",
17
18
  },
19
+ credit_ceiling: {
20
+ help: "The most credits this app may spend in any 24 hours while it holds OXYGEN event subscriptions (0 = no paid calls then). --clear-credit-ceiling restores the plan default.",
21
+ valueName: "<credits>",
22
+ },
23
+ approve_above_default: {
24
+ help: "Approve a --credit-ceiling above your plan's default.",
25
+ negateHelp: "Do not approve a ceiling above the default (the default behaviour).",
26
+ },
27
+ };
28
+ export const CONNECTOR_EVENTS_REVOKE_FLAG_OVERLAY = {
29
+ subscription: {
30
+ help: "The subscription id from `connectors events get <app>`, or `all` for every live one.",
31
+ valueName: "<id|all>",
32
+ },
33
+ workspace: {
34
+ help: "Only this workspace's subscriptions (id, slug or exact name). Omit for all your workspaces.",
35
+ valueName: "<workspace>",
36
+ },
18
37
  };
19
- /** The PATCH body for the flags given; one of the two workspace flags is required. */
38
+ /** The revoke body for the flags given; --subscription is required. */
39
+ export function buildConnectorEventsRevokeBody(options) {
40
+ const body = readContractFlags(options, revokeEvents.contract, CONNECTOR_EVENTS_REVOKE_FLAG_OVERLAY);
41
+ if (typeof body.subscription !== "string") {
42
+ throw new OxygenError("invalid_request", "Pass --subscription <id> (from `connectors events get <app>`), or --subscription all to revoke every live one.", { exitCode: 1 });
43
+ }
44
+ return body;
45
+ }
46
+ /** The PATCH body for the flags given; a workspace or a credit ceiling flag is required. */
20
47
  export function buildConnectorUpdateBody(options) {
21
48
  const body = readContractFlags(options, update.contract, CONNECTORS_FLAG_OVERLAY);
22
- if (!("default_workspace" in body)) {
23
- throw new OxygenError("invalid_request", "Pass --default-workspace <workspace> to set where the app works, or --clear-default-workspace to have it ask each time.", { exitCode: 1 });
49
+ if (!("default_workspace" in body) && !("credit_ceiling" in body)) {
50
+ throw new OxygenError("invalid_request", "Pass --default-workspace <workspace> (or --clear-default-workspace) to set where the app works, or --credit-ceiling <credits> (or --clear-credit-ceiling) to set what it may spend in 24 hours.", { exitCode: 1 });
51
+ }
52
+ if ("approve_above_default" in body && !("credit_ceiling" in body)) {
53
+ throw new OxygenError("invalid_request", "--approve-above-default only goes with --credit-ceiling (or --clear-credit-ceiling).", { exitCode: 1 });
24
54
  }
25
55
  return body;
26
56
  }
27
57
  export function registerConnectorsCommands(program, handle) {
28
58
  const connectors = program
29
59
  .command("connectors")
30
- .description("Connected apps: ChatGPT, Claude, Codex and any other app you connected to OXYGEN with OAuth, and the default workspace each one works in without asking. "
31
- + "`connectors list` shows them; `connectors set <app> --default-workspace <workspace>` changes that app only. "
32
- + "Picking a workspace inside an app changes the same setting, and the latest choice wins. This CLI keeps its own workspace (`orgs use`). "
33
- + "Web: https://oxygen-agent.com/settings/connected-apps.");
60
+ .description("Connected apps (ChatGPT, Claude, Codex… connected to OXYGEN with OAuth): the workspace each one works in, the OXYGEN events it is subscribed to, and what it may spend. "
61
+ + "`connectors list` shows them; `connectors events get <app>` shows an app's event subscriptions and what OXYGEN sent it, and `connectors events revoke` stops them. "
62
+ + "`connectors set <app>` changes one app only: --default-workspace (picking a workspace inside the app changes the same setting; latest wins; this CLI keeps its own, `orgs use`) "
63
+ + "and --credit-ceiling (while it holds event subscriptions, its paid calls stay under this many credits in any 24 hours; the plan default otherwise). "
64
+ + "Web: the Connected apps page, /settings/connected-apps; every response carries its `web_url`.");
34
65
  connectors.addCommand(new Command("list")
35
- .description("List your connected apps with each one's default workspace, when it was last used, and the workspaces you can choose from. Read-only, 0 credits.")
66
+ .description("List your connected apps with each one's default workspace, its live event subscriptions, its daily credit ceiling and what it spent in the last 24 hours, when it was last used, and the workspaces you can choose from. Read-only, 0 credits.")
36
67
  .option("--json", "Print a JSON envelope.")
37
68
  .action(async (options) => {
38
69
  await handle("connectors list", options, () => requestOxygen("/api/cli/connectors"));
39
70
  }));
40
71
  connectors.addCommand(new Command("get")
41
- .description("Show one connected app and its default workspace. Read-only, 0 credits.")
72
+ .description("Show one connected app: its default workspace, its daily credit ceiling and what it spent in the last 24 hours in each workspace where it spends, and how many event subscriptions it holds. Read-only, 0 credits.")
42
73
  .argument("<connection>", update.cli.argument?.description ?? "The app's id or name.")
43
74
  .option("--json", "Print a JSON envelope.")
44
75
  .action(async (connection, options) => {
45
76
  await handle("connectors get", options, () => requestOxygen(`/api/cli/connectors/${encodeURIComponent(connection)}`));
46
77
  }));
78
+ const events = new Command("events")
79
+ .description("The events OXYGEN sends one connected app: what it subscribed to (a reply was classified, a run settled, an approval is waiting…), "
80
+ + "each subscription's filters, status and expiry, and what OXYGEN delivered to it and how each delivery went (never the content). "
81
+ + "`get` reads them; `revoke` stops them. Only apps connected with OAuth, such as ChatGPT, subscribe.");
82
+ events.addCommand(new Command("get")
83
+ .description("Show one connected app's event subscriptions (filters, status, granted, renew-by and expiry, workspace) and its recent deliveries (status, attempts, failure category, times; never content). Ended subscriptions stay listed for 7 days; `active_subscriptions` counts the live ones. Read-only, 0 credits.")
84
+ .argument("<connection>", update.cli.argument?.description ?? "The app's id or name.")
85
+ .option("--workspace <workspace>", "Only this workspace (id, slug or exact name). Omit for all your workspaces.")
86
+ .option("--deliveries <n>", "Recent deliveries to show per subscription (0-50, default 10).")
87
+ .option("--json", "Print a JSON envelope.")
88
+ .addHelpText("after", `
89
+ Examples:
90
+ oxygen connectors events get ChatGPT
91
+ oxygen connectors events get ChatGPT --workspace "Acme GTM" --deliveries 20
92
+ `)
93
+ .action(async (connection, options) => {
94
+ const query = new URLSearchParams();
95
+ if (options.workspace?.trim())
96
+ query.set("workspace", options.workspace.trim());
97
+ if (options.deliveries?.trim())
98
+ query.set("deliveries", options.deliveries.trim());
99
+ const queryString = query.toString();
100
+ const suffix = queryString ? `?${queryString}` : "";
101
+ await handle("connectors events get", options, () => requestOxygen(`/api/cli/connectors/${encodeURIComponent(connection)}/events${suffix}`));
102
+ }));
103
+ events.addCommand(addContractFlags(new Command(revokeEvents.cli.path.at(-1) ?? "revoke")
104
+ .description(revokeEvents.cli.summary)
105
+ .argument(`<${revokeEvents.cli.argument?.name ?? "connection"}>`, revokeEvents.cli.argument?.description), revokeEvents.contract, CONNECTOR_EVENTS_REVOKE_FLAG_OVERLAY)
106
+ .option("--json", "Print a JSON envelope.")
107
+ .addHelpText("after", `
108
+ Examples:
109
+ oxygen connectors events revoke ChatGPT --subscription all
110
+ oxygen connectors events revoke ChatGPT --subscription <id> --workspace "Acme GTM"
111
+ `)
112
+ .action(async (connection, options) => {
113
+ await handle("connectors events revoke", options, () => requestOxygen(operationPath(revokeEvents, { connection }), {
114
+ method: revokeEvents.method,
115
+ body: buildConnectorEventsRevokeBody(options),
116
+ }));
117
+ }));
118
+ connectors.addCommand(events);
47
119
  connectors.addCommand(addContractFlags(new Command(update.cli.path.at(-1) ?? "set")
48
120
  .description(update.cli.summary)
49
121
  .argument(`<${update.cli.argument?.name ?? "connection"}>`, update.cli.argument?.description), update.contract, CONNECTORS_FLAG_OVERLAY)
@@ -53,6 +125,8 @@ Examples:
53
125
  oxygen connectors list
54
126
  oxygen connectors set ChatGPT --default-workspace "Acme GTM"
55
127
  oxygen connectors set <id> --clear-default-workspace
128
+ oxygen connectors set ChatGPT --credit-ceiling 300
129
+ oxygen connectors set ChatGPT --credit-ceiling 50000 --approve-above-default
56
130
  `)
57
131
  .action(async (connection, options) => {
58
132
  await handle("connectors set", options, () => requestOxygen(operationPath(update, { connection }), {
@@ -10,6 +10,8 @@ export type ContractFlagOverlayEntry = {
10
10
  aliases?: readonly string[];
11
11
  /** Boolean: help for the `--no-<flag>` form. */
12
12
  negateHelp?: string;
13
+ /** Boolean: do not generate the `--no-<flag>` form, for a field whose `false` changes nothing. */
14
+ noNegate?: boolean;
13
15
  /**
14
16
  * Boolean as two flags, e.g. `--warmup-restart` (true) / `--warmup-disable`
15
17
  * (false). When both are given, `off` wins.
@@ -24,7 +24,7 @@ function attribute(flag) {
24
24
  return flag.replace(/-([a-z0-9])/g, (_match, char) => char.toUpperCase());
25
25
  }
26
26
  function isObject(property) {
27
- return "type" in property && property.type === "object" && property.properties !== undefined;
27
+ return "type" in property && property.type === "object";
28
28
  }
29
29
  function contractFlagSpecs(properties, overlay, parentPath = [], parentFlag = "") {
30
30
  const specs = [];
@@ -37,11 +37,14 @@ function contractFlagSpecs(properties, overlay, parentPath = [], parentFlag = ""
37
37
  const flag = entry.flag ?? `${parentFlag}${kebab(name)}`;
38
38
  if (isObject(property)) {
39
39
  const fields = Object.values(property.properties ?? {});
40
- const splat = entry.flatten || (fields.length <= MAX_SPLAT_FIELDS && !fields.some((field) => isObject(unwrapNullable(field).property)));
40
+ const splat = entry.flatten || (fields.length > 0 && property.additionalProperties !== true && fields.length <= MAX_SPLAT_FIELDS && !fields.some((field) => isObject(unwrapNullable(field).property)));
41
41
  if (splat)
42
42
  specs.push(...contractFlagSpecs(property.properties ?? {}, overlay, path, entry.flatten ? "" : `${flag}-`));
43
43
  else
44
- specs.push({ kind: "json", path, flag: `${flag}-json`, entry });
44
+ specs.push({ kind: "json", path, flag: `${flag}-json`, property, entry });
45
+ }
46
+ else if (!("type" in property) || (property.type === "array" && !("type" in property.items && ["string", "number", "integer", "boolean"].includes(property.items.type)))) {
47
+ specs.push({ kind: "json", path, flag: `${flag}-json`, property, entry });
45
48
  }
46
49
  else if ("type" in property && property.type === "boolean") {
47
50
  specs.push(entry.pair ? { kind: "pair", path, on: entry.pair.on, off: entry.pair.off, entry } : { kind: "boolean", path, flag, entry });
@@ -84,7 +87,9 @@ export function addContractFlags(command, contract, overlay = {}) {
84
87
  }
85
88
  if (spec.kind === "boolean") {
86
89
  command.addOption(new Option(`--${spec.flag}`, helpFor(spec, property)));
87
- command.addOption(new Option(`--no-${spec.flag}`, spec.entry.negateHelp ?? `Sends ${spec.path.join(".")}: false.`));
90
+ if (!spec.entry.noNegate) {
91
+ command.addOption(new Option(`--no-${spec.flag}`, spec.entry.negateHelp ?? `Sends ${spec.path.join(".")}: false.`));
92
+ }
88
93
  continue;
89
94
  }
90
95
  const valueName = spec.entry.valueName ?? (spec.kind === "json" ? "<json>" : "<value>");
@@ -95,7 +100,7 @@ export function addContractFlags(command, contract, overlay = {}) {
95
100
  return command;
96
101
  }
97
102
  function invalid(flag, value, message, code = "invalid_number") {
98
- return new OxygenError(code, message, { details: { flag: `--${flag}`, value }, exitCode: 1 });
103
+ return new OxygenError(code, message, { details: { flag: `--${flag}`, value }, exitCode: 2 });
99
104
  }
100
105
  function parseScalar(property, raw, flag) {
101
106
  if (!("type" in property))
@@ -119,6 +124,13 @@ function parseScalar(property, raw, flag) {
119
124
  if (property.type === "string" && property.enum && !property.enum.includes(raw)) {
120
125
  throw invalid(flag, raw, `--${flag} must be one of: ${property.enum.join(", ")}.`, "invalid_option");
121
126
  }
127
+ if (property.type === "boolean") {
128
+ if (raw === "true")
129
+ return true;
130
+ if (raw === "false")
131
+ return false;
132
+ throw invalid(flag, raw, `--${flag} values must be true or false.`, "invalid_option");
133
+ }
122
134
  return raw;
123
135
  }
124
136
  function readValue(spec, raw) {
@@ -145,8 +157,8 @@ function setPath(body, path, value) {
145
157
  function stringOption(options, flag, aliases = []) {
146
158
  for (const name of [flag, ...aliases]) {
147
159
  const value = options[attribute(name)];
148
- if (typeof value === "string" && value.trim())
149
- return value.trim();
160
+ if (typeof value === "string")
161
+ return value;
150
162
  }
151
163
  return undefined;
152
164
  }
@@ -175,11 +187,14 @@ export function readContractFlags(options, contract, overlay = {}) {
175
187
  parsed = JSON.parse(raw);
176
188
  }
177
189
  catch {
178
- throw invalid(spec.flag, raw, `--${spec.flag} must be a JSON object.`, "invalid_json");
190
+ throw invalid(spec.flag, raw, `--${spec.flag} must be valid JSON.`, "invalid_json");
179
191
  }
180
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
192
+ if ("type" in spec.property && spec.property.type === "object" && (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))) {
181
193
  throw invalid(spec.flag, raw, `--${spec.flag} must be a JSON object.`, "invalid_json");
182
194
  }
195
+ if ("type" in spec.property && spec.property.type === "array" && !Array.isArray(parsed)) {
196
+ throw invalid(spec.flag, raw, `--${spec.flag} must be a JSON array.`, "invalid_json");
197
+ }
183
198
  setPath(body, spec.path, parsed);
184
199
  given.add(spec.flag);
185
200
  }
@@ -196,7 +211,7 @@ export function readContractFlags(options, contract, overlay = {}) {
196
211
  }
197
212
  else if (options[attribute(spec.flag)] === true) {
198
213
  if (given.has(spec.target)) {
199
- throw new OxygenError("invalid_request", `Pass either --${spec.target} or --${spec.flag}, not both.`, { exitCode: 1 });
214
+ throw new OxygenError("invalid_request", `Pass either --${spec.target} or --${spec.flag}, not both.`, { exitCode: 2 });
200
215
  }
201
216
  setPath(body, spec.path, null);
202
217
  }
@@ -210,7 +225,7 @@ export function contractFlagNames(contract, overlay = {}) {
210
225
  case "pair":
211
226
  return [`--${spec.on}`, `--${spec.off}`];
212
227
  case "boolean":
213
- return [`--${spec.flag}`, `--no-${spec.flag}`];
228
+ return spec.entry.noNegate ? [`--${spec.flag}`] : [`--${spec.flag}`, `--no-${spec.flag}`];
214
229
  case "clear":
215
230
  return [`--${spec.flag}`];
216
231
  default:
@@ -0,0 +1,11 @@
1
+ import type { Command } from "commander";
2
+ /** Existing command names, positional arguments, formatters and actions stay owned by index.ts. */
3
+ export declare function registerCrmSupportFlags(program: Command): void;
4
+ export declare function readCrmNoteEditFields(options: object): Record<string, unknown>;
5
+ export declare function readCrmPhotoFields(options: {
6
+ live?: boolean;
7
+ dryRun?: boolean;
8
+ }): Record<string, unknown>;
9
+ export declare function readCrmFileLabel(options: object): Record<string, unknown>;
10
+ export declare function buildCrmObjectsPath(options: object): string;
11
+ export declare function buildCrmConnectionsPath(object: string, rowId: string, options: object): string;
@@ -0,0 +1,60 @@
1
+ import { OxygenError } from "@oxygen/shared/cli-result";
2
+ import { CRM_CONNECTIONS_QUERY_CONTRACT, CRM_NOTE_EDIT_CONTRACT, CRM_OBJECTS_QUERY_CONTRACT, CRM_PHOTOS_CONTRACT, CRM_FILE_COMPLETE_CONTRACT } from "@oxygen/shared/contracts/crm-support";
3
+ import { validateContract } from "@oxygen/shared/contracts/schema";
4
+ import { addContractFlags, readContractFlags } from "./contract-flags.js";
5
+ import { assertModeFlagsExclusive } from "./cli-values.js";
6
+ const NOTE_OVERLAY = { pinned: { pair: { on: "pin", onHelp: "Pin the note.", off: "unpin", offHelp: "Unpin the note." } } };
7
+ function child(parent, name) { return parent?.commands.find((command) => command.name() === name); }
8
+ function replaceFlags(command, attributes, contract, overlay) {
9
+ if (!command)
10
+ throw new Error("Register CRM commands before their shared flags.");
11
+ const retained = command.options.filter((option) => !attributes.includes(option.attributeName()));
12
+ // Commander exposes this mutable runtime collection as readonly in its types.
13
+ // Replace only the flags this leaf owns before parsing the existing commands.
14
+ const options = command.options;
15
+ options.splice(0, options.length, ...retained);
16
+ addContractFlags(command, contract, overlay);
17
+ }
18
+ /** Existing command names, positional arguments, formatters and actions stay owned by index.ts. */
19
+ export function registerCrmSupportFlags(program) {
20
+ const crm = child(program, "crm");
21
+ replaceFlags(child(crm, "objects"), [], CRM_OBJECTS_QUERY_CONTRACT);
22
+ replaceFlags(child(crm, "connections"), ["conversationLimit", "sequenceLimit"], CRM_CONNECTIONS_QUERY_CONTRACT);
23
+ replaceFlags(child(child(crm, "notes"), "update"), ["body", "title", "pin", "unpin"], CRM_NOTE_EDIT_CONTRACT, NOTE_OVERLAY);
24
+ replaceFlags(child(crm, "photos"), ["limit", "live"], CRM_PHOTOS_CONTRACT);
25
+ replaceFlags(child(child(crm, "files"), "attach"), ["label"], CRM_FILE_COMPLETE_CONTRACT);
26
+ }
27
+ function checkedFlags(options, contract, overlay) {
28
+ const value = readContractFlags(options, contract, overlay);
29
+ const checked = validateContract(contract, value, { unknown: "reject", overMax: "reject" });
30
+ if (!checked.ok)
31
+ throw new OxygenError("invalid_request", "Invalid CRM command fields.", { exitCode: 1, details: { fields: checked.issues.map((issue) => issue.path) } });
32
+ return checked.value;
33
+ }
34
+ export function readCrmNoteEditFields(options) {
35
+ const value = checkedFlags(options, CRM_NOTE_EDIT_CONTRACT, NOTE_OVERLAY);
36
+ // Preserve the existing --title "" spelling in addition to --clear-title.
37
+ if (options.title === "")
38
+ value.title = null;
39
+ return value;
40
+ }
41
+ export function readCrmPhotoFields(options) {
42
+ assertModeFlagsExclusive(options);
43
+ const value = checkedFlags(options, CRM_PHOTOS_CONTRACT);
44
+ if (options.dryRun)
45
+ value.mode = "dry_run";
46
+ if (value.mode === "dry_run" && value.live === true)
47
+ throw new OxygenError("invalid_request", "Pass either dry_run or live.", { exitCode: 1 });
48
+ if (value.mode === undefined && value.live !== true)
49
+ value.mode = "dry_run";
50
+ return value;
51
+ }
52
+ export function readCrmFileLabel(options) { return checkedFlags(options, CRM_FILE_COMPLETE_CONTRACT); }
53
+ function queryPath(path, query) {
54
+ const params = new URLSearchParams(Object.entries(query).map(([key, value]) => [key, String(value)]));
55
+ return params.size ? `${path}?${params}` : path;
56
+ }
57
+ export function buildCrmObjectsPath(options) { return queryPath("/api/cli/crm/objects", checkedFlags(options, CRM_OBJECTS_QUERY_CONTRACT)); }
58
+ export function buildCrmConnectionsPath(object, rowId, options) {
59
+ return queryPath(`/api/cli/crm/objects/${encodeURIComponent(object)}/records/${encodeURIComponent(rowId)}/connections`, checkedFlags(options, CRM_CONNECTIONS_QUERY_CONTRACT));
60
+ }
@@ -0,0 +1,5 @@
1
+ import { Command } from "commander";
2
+ /** Register each domain's selected operation as a native command with exact typed flags. */
3
+ export declare function registerCustomerOperationCommands(program: Command, handle: (command: string, options: {
4
+ json?: boolean;
5
+ }, action: () => Promise<unknown>) => Promise<void>): void;
@@ -0,0 +1,101 @@
1
+ import { readFile, writeFile } from "node:fs/promises";
2
+ import { basename, extname, resolve } from "node:path";
3
+ import { OxygenError } from "@oxygen/shared/cli-result";
4
+ import { CLI_OPERATIONS, operationFormData, operationInputContract, operationRequest } from "@oxygen/shared/contracts/operations";
5
+ import { unwrapNullable, validateContract } from "@oxygen/shared/contracts/schema";
6
+ import { addContractFlags, readContractFlags } from "./contract-flags.js";
7
+ import { requestOxygen } from "./http-client.js";
8
+ /** Register each domain's selected operation as a native command with exact typed flags. */
9
+ export function registerCustomerOperationCommands(program, handle) {
10
+ for (const spec of CLI_OPERATIONS) {
11
+ let parent = program;
12
+ const words = [...spec.cli.path];
13
+ const leaf = words.pop();
14
+ for (const word of words) {
15
+ let child = parent.commands.find((command) => command.name() === word);
16
+ if (!child)
17
+ child = parent.command(word).description(`${word} workspace operations.`);
18
+ parent = child;
19
+ }
20
+ // Never hide an existing owner behind a second Commander command.
21
+ if (parent.commands.some((command) => command.name() === leaf)) {
22
+ throw new Error(`Duplicate native operation command ${spec.cli.path.join(" ")}`);
23
+ }
24
+ const contract = operationInputContract(spec);
25
+ const command = addContractFlags(parent.command(leaf).description(spec.cli.summary), contract)
26
+ .option("--json", "Print a JSON envelope.");
27
+ if (spec.binary)
28
+ command.requiredOption("--output <path>", "Write the exact downloaded bytes to this local file.");
29
+ if (spec.multipart)
30
+ command.option("--file <path>", "Upload bytes from this local file. Omit the base64 flag; file name and content type default from the path.");
31
+ for (const name of contract.required ?? []) {
32
+ const property = contract.properties[name];
33
+ const flag = `--${name.replace(/_/g, "-")}`;
34
+ const option = command.options.find((candidate) => candidate.long === flag || candidate.long === `${flag}-json`);
35
+ const multipartAlternative = spec.multipart && [spec.multipart.contentField, spec.multipart.nameField, spec.multipart.typeField].includes(name);
36
+ if (multipartAlternative) {
37
+ if (option)
38
+ option.description = `${option.description} Required unless supplied by --file.`.trim();
39
+ }
40
+ else if (unwrapNullable(property).nullable) {
41
+ if (option)
42
+ option.description = `${option.description} Required: use this flag or --clear-${name.replace(/_/g, "-")}.`.trim();
43
+ }
44
+ else if (option) {
45
+ // The negative boolean spelling uses the same attribute, so an explicit
46
+ // false satisfies this requirement just as true does.
47
+ option.makeOptionMandatory();
48
+ }
49
+ }
50
+ command.action((options, selected) => handle(spec.cli.path.join(" "), { ...options, json: selected.optsWithGlobals().json === true }, async () => {
51
+ const input = readContractFlags(options, contract);
52
+ if (spec.multipart && options.file !== undefined) {
53
+ if (input[spec.multipart.contentField] !== undefined)
54
+ throw new OxygenError("invalid_request", "Choose --file or the base64 content flag, not both.", { exitCode: 2 });
55
+ const filePath = String(options.file);
56
+ input[spec.multipart.contentField] = (await readFile(filePath)).toString("base64");
57
+ input[spec.multipart.nameField] ??= basename(filePath);
58
+ input[spec.multipart.typeField] ??= uploadContentType(filePath);
59
+ }
60
+ const validation = validateContract(contract, input, { unknown: "reject", overMax: "reject" });
61
+ if (!validation.ok)
62
+ throw new OxygenError("invalid_request", validation.issues.map((issue) => issue.message).join(" "), { exitCode: 2 });
63
+ const request = operationRequest(spec, input);
64
+ if (spec.binary) {
65
+ if (spec.binary.stream)
66
+ return requestOxygen(request.path, { method: request.method, binaryOutputPath: resolve(String(options.output)) });
67
+ const bytes = await requestOxygen(request.path, { method: request.method, binaryMaxBytes: spec.binary.maxBytes });
68
+ const output = resolve(String(options.output));
69
+ try {
70
+ await writeFile(output, bytes, { flag: "wx" });
71
+ }
72
+ catch (error) {
73
+ if (error && typeof error === "object" && "code" in error && error.code === "EEXIST") {
74
+ throw new OxygenError("output_exists", "The output file already exists. Choose another --output path.", { exitCode: 2 });
75
+ }
76
+ throw error;
77
+ }
78
+ return { path: output, bytes: bytes.byteLength };
79
+ }
80
+ if (spec.multipart) {
81
+ let formData;
82
+ try {
83
+ formData = operationFormData(spec, request.body ?? {});
84
+ }
85
+ catch (error) {
86
+ throw new OxygenError("invalid_request", error instanceof Error ? error.message : "Invalid upload.", { exitCode: 2 });
87
+ }
88
+ return requestOxygen(request.path, { method: request.method, formData });
89
+ }
90
+ return requestOxygen(request.path, { method: request.method, ...(request.body ? { body: request.body } : {}) });
91
+ }));
92
+ }
93
+ }
94
+ function uploadContentType(filePath) {
95
+ const types = {
96
+ ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".webp": "image/webp", ".svg": "image/svg+xml",
97
+ ".csv": "text/csv", ".json": "application/json", ".jsonl": "application/x-ndjson",
98
+ ".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
99
+ };
100
+ return types[extname(filePath).toLowerCase()] ?? "application/octet-stream";
101
+ }
@@ -0,0 +1,2 @@
1
+ import type { ContractFlagOverlay } from "./contract-flags.js";
2
+ export declare const DATABASE_QUERY_FLAG_OVERLAY: ContractFlagOverlay;
@@ -0,0 +1,6 @@
1
+ import { readPositiveInt } from "./cli-values.js";
2
+ export const DATABASE_QUERY_FLAG_OVERLAY = {
3
+ sql: { omit: true }, // Keep the existing required --sql option.
4
+ max_rows: { valueName: "<n>", parse: readPositiveInt }, // Preserve legacy coercion/defaults.
5
+ max_chars: { valueName: "<n>" },
6
+ };
package/dist/help.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { Command, CommanderError } from "commander";
1
+ import { CommanderError, type Command } from "commander";
2
2
  import { type CliFailure } from "@oxygen/shared/cli-result";
3
3
  export declare function applyOxygenHelp(program: Command, binaryName: string): void;
4
4
  /**
@@ -12,6 +12,8 @@ export declare function applyOxygenHelp(program: Command, binaryName: string): v
12
12
  * `installCommanderExitOverride` recurses.
13
13
  */
14
14
  export declare function enableCommandSuggestions(command: Command): void;
15
+ /** A declared flag following a value option is a missing value, not literal data. */
16
+ export declare function validateRequiredOptionValues(program: Command, selected: Command): void;
15
17
  /**
16
18
  * The extra line printed after `error: unknown command '<x>'`.
17
19
  *
package/dist/help.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { CommanderError } from "commander";
1
2
  import { CLI_EXIT_CODE_TABLE } from "@oxygen/shared";
2
3
  import { exitCodeForErrorCode, failure } from "@oxygen/shared/cli-result";
3
4
  // Grouped `--help` for the ~70 top-level command groups: commander's default
@@ -20,6 +21,12 @@ const HELP_GROUPS = [
20
21
  "status",
21
22
  "home",
22
23
  "orgs",
24
+ // Where each connected app (ChatGPT, Claude…) works by default: read
25
+ // beside `orgs`, where a founder asking "which workspace" looks first.
26
+ "connectors",
27
+ // Workspace settings (the sidebar pages everyone sees): the 2026-09-30
28
+ // blind baseline typed `oxygen settings --help` before anything else.
29
+ "settings",
23
30
  "commands",
24
31
  "skills",
25
32
  "session",
@@ -175,6 +182,38 @@ export function enableCommandSuggestions(command) {
175
182
  for (const child of command.commands)
176
183
  enableCommandSuggestions(child);
177
184
  }
185
+ /** A declared flag following a value option is a missing value, not literal data. */
186
+ export function validateRequiredOptionValues(program, selected) {
187
+ const options = new Map();
188
+ for (let current = selected; current; current = current.parent) {
189
+ for (const option of current.options) {
190
+ if (option.long && !options.has(option.long))
191
+ options.set(option.long, option);
192
+ if (option.short && !options.has(option.short))
193
+ options.set(option.short, option);
194
+ }
195
+ }
196
+ // Installed Commander writes this exact property before command dispatch;
197
+ // its public typings omit it. Validate only that property at this seam.
198
+ const rawArgs = Reflect.get(program, "rawArgs");
199
+ if (!Array.isArray(rawArgs) || !rawArgs.every((value) => typeof value === "string")) {
200
+ throw new CommanderError(2, "commander.invalidArgument", "Cannot validate the command's original option arguments.");
201
+ }
202
+ const argv = rawArgs;
203
+ for (let index = 0; index < argv.length; index += 1) {
204
+ const token = argv[index];
205
+ if (token === "--")
206
+ break;
207
+ const option = options.get(token);
208
+ if (!option?.required)
209
+ continue;
210
+ const next = argv[index + 1];
211
+ if (next === undefined || next === "--" || options.has(next.split("=")[0])) {
212
+ throw new CommanderError(2, "commander.optionMissingArgument", `option '${option.flags}' argument missing. To use an option name as literal text, pass ${option.long ?? option.short}=<value>.`);
213
+ }
214
+ index += 1;
215
+ }
216
+ }
178
217
  /**
179
218
  * Verbs a user reaches for that this CLI spells differently.
180
219
  *
@@ -205,6 +244,10 @@ const COMMAND_SYNONYMS = {
205
244
  exec: ["run", "start", "call"],
206
245
  execute: ["run", "start", "call"],
207
246
  find: ["search", "query", "list"],
247
+ // The product's own copy calls a Sequence a campaign; a blind agent's first
248
+ // guess was `oxygen campaigns` (Plain T-220 baseline, 2026-09-30).
249
+ campaign: ["sequences"],
250
+ campaigns: ["sequences"],
208
251
  };
209
252
  /**
210
253
  * The extra line printed after `error: unknown command '<x>'`.
@@ -3,6 +3,8 @@ import { type CliEndpoint } from "./runtime.js";
3
3
  type RequestOptions = {
4
4
  /** Internal bounded file download; never follows a caller-supplied URL. */
5
5
  binaryMaxBytes?: number;
6
+ /** Stream a native authenticated download to an exclusively created file. */
7
+ binaryOutputPath?: string;
6
8
  method?: "GET" | "POST" | "PATCH" | "DELETE";
7
9
  body?: Record<string, unknown>;
8
10
  formData?: FormData;