@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.
- package/README.md +1 -1
- package/dist/command-manifest.js +15 -4
- package/dist/connectors-commands.d.ts +4 -1
- package/dist/connectors-commands.js +84 -10
- package/dist/contract-flags.d.ts +2 -0
- package/dist/contract-flags.js +26 -11
- package/dist/crm-support-flags.d.ts +11 -0
- package/dist/crm-support-flags.js +60 -0
- package/dist/customer-operation-commands.d.ts +5 -0
- package/dist/customer-operation-commands.js +101 -0
- package/dist/database-query-flags.d.ts +2 -0
- package/dist/database-query-flags.js +6 -0
- package/dist/help.d.ts +3 -1
- package/dist/help.js +43 -0
- package/dist/http-client.d.ts +2 -0
- package/dist/http-client.js +41 -1
- package/dist/index.js +640 -234
- package/dist/knowledge-repository-commands.js +12 -14
- package/dist/mailbox-ramp-notice.d.ts +11 -0
- package/dist/mailbox-ramp-notice.js +53 -0
- package/dist/settings-commands.d.ts +11 -0
- package/dist/settings-commands.js +73 -0
- package/dist/skills.d.ts +5 -4
- package/dist/skills.js +106 -79
- package/dist/tool-catalog-detail-command.d.ts +7 -0
- package/dist/tool-catalog-detail-command.js +19 -0
- package/dist/tool-catalog-search-window.d.ts +6 -0
- package/dist/tool-catalog-search-window.js +12 -0
- package/dist/transcript.js +1 -0
- package/node_modules/@oxygen/shared/dist/bounded-read-result.d.ts +4 -0
- package/node_modules/@oxygen/shared/dist/bounded-read-result.js +25 -0
- package/node_modules/@oxygen/shared/dist/capability-discovery.d.ts +47 -1
- package/node_modules/@oxygen/shared/dist/capability-discovery.js +625 -39
- package/node_modules/@oxygen/shared/dist/connection-approval.d.ts +2 -0
- package/node_modules/@oxygen/shared/dist/connection-approval.js +2 -0
- package/node_modules/@oxygen/shared/dist/contracts/browser-operations.d.ts +2 -0
- package/node_modules/@oxygen/shared/dist/contracts/browser-operations.js +110 -0
- package/node_modules/@oxygen/shared/dist/contracts/connectors.d.ts +32 -6
- package/node_modules/@oxygen/shared/dist/contracts/connectors.js +56 -4
- package/node_modules/@oxygen/shared/dist/contracts/crm-attributes.d.ts +4 -0
- package/node_modules/@oxygen/shared/dist/contracts/crm-attributes.js +39 -0
- package/node_modules/@oxygen/shared/dist/contracts/crm-enrichment-read.d.ts +21 -0
- package/node_modules/@oxygen/shared/dist/contracts/crm-enrichment-read.js +15 -0
- package/node_modules/@oxygen/shared/dist/contracts/crm-support.d.ts +117 -0
- package/node_modules/@oxygen/shared/dist/contracts/crm-support.js +43 -0
- package/node_modules/@oxygen/shared/dist/contracts/customer-operations.d.ts +2 -0
- package/node_modules/@oxygen/shared/dist/contracts/customer-operations.js +131 -0
- package/node_modules/@oxygen/shared/dist/contracts/dashboard-tabs.d.ts +3 -0
- package/node_modules/@oxygen/shared/dist/contracts/dashboard-tabs.js +2 -0
- package/node_modules/@oxygen/shared/dist/contracts/database.d.ts +30 -0
- package/node_modules/@oxygen/shared/dist/contracts/database.js +29 -0
- package/node_modules/@oxygen/shared/dist/contracts/integration-connect.d.ts +4 -0
- package/node_modules/@oxygen/shared/dist/contracts/integration-connect.js +7 -0
- package/node_modules/@oxygen/shared/dist/contracts/knowledge-content-reads.d.ts +43 -0
- package/node_modules/@oxygen/shared/dist/contracts/knowledge-content-reads.js +20 -0
- package/node_modules/@oxygen/shared/dist/contracts/linkedin-invites-list.d.ts +12 -0
- package/node_modules/@oxygen/shared/dist/contracts/linkedin-invites-list.js +8 -0
- package/node_modules/@oxygen/shared/dist/contracts/operations.d.ts +48 -2
- package/node_modules/@oxygen/shared/dist/contracts/operations.js +68 -2
- package/node_modules/@oxygen/shared/dist/contracts/table-grid-query.d.ts +4 -0
- package/node_modules/@oxygen/shared/dist/contracts/table-grid-query.js +6 -0
- package/node_modules/@oxygen/shared/dist/contracts/table-query-grid.d.ts +21 -0
- package/node_modules/@oxygen/shared/dist/contracts/table-query-grid.js +27 -0
- package/node_modules/@oxygen/shared/dist/contracts/tool-catalog-detail.d.ts +19 -0
- package/node_modules/@oxygen/shared/dist/contracts/tool-catalog-detail.js +10 -0
- package/node_modules/@oxygen/shared/dist/contracts/tool-catalog-search-window.d.ts +12 -0
- package/node_modules/@oxygen/shared/dist/contracts/tool-catalog-search-window.js +7 -0
- package/node_modules/@oxygen/shared/dist/contracts/workspace-pages.d.ts +17 -0
- package/node_modules/@oxygen/shared/dist/contracts/workspace-pages.js +49 -0
- package/node_modules/@oxygen/shared/dist/decision-ledger-parser.d.ts +32 -0
- package/node_modules/@oxygen/shared/dist/decision-ledger-parser.js +303 -0
- package/node_modules/@oxygen/shared/dist/decision-ledger.d.ts +10 -0
- package/node_modules/@oxygen/shared/dist/decision-ledger.generated.d.ts +2 -0
- package/node_modules/@oxygen/shared/dist/decision-ledger.generated.js +717 -0
- package/node_modules/@oxygen/shared/dist/decision-ledger.js +20 -0
- package/node_modules/@oxygen/shared/dist/email-campaign-ramp.d.ts +104 -0
- package/node_modules/@oxygen/shared/dist/email-campaign-ramp.js +250 -0
- package/node_modules/@oxygen/shared/dist/mcp-event-contract.d.ts +10 -7
- package/node_modules/@oxygen/shared/dist/mcp-event-contract.js +11 -7
- package/node_modules/@oxygen/shared/dist/mcp-table-widget.d.ts +50 -0
- package/node_modules/@oxygen/shared/dist/mcp-table-widget.js +24 -0
- package/node_modules/@oxygen/shared/dist/plan-band.d.ts +8 -0
- package/node_modules/@oxygen/shared/dist/plan-band.js +12 -0
- package/node_modules/@oxygen/shared/dist/plan-limits.d.ts +20 -0
- package/node_modules/@oxygen/shared/dist/plan-limits.js +41 -0
- package/node_modules/@oxygen/shared/dist/provider-input-value.d.ts +5 -0
- package/node_modules/@oxygen/shared/dist/provider-input-value.js +10 -0
- package/node_modules/@oxygen/shared/dist/sequences.d.ts +33 -4
- package/node_modules/@oxygen/shared/dist/sequences.js +48 -13
- package/node_modules/@oxygen/shared/dist/user-capability-routing.js +17 -0
- package/node_modules/@oxygen/shared/dist/version.generated.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/version.generated.js +1 -1
- package/node_modules/@oxygen/shared/package.json +8 -6
- package/package.json +1 -1
- package/node_modules/@oxygen/shared/dist/copilot-skills.d.ts +0 -15
- package/node_modules/@oxygen/shared/dist/copilot-skills.generated.d.ts +0 -31
- package/node_modules/@oxygen/shared/dist/copilot-skills.generated.js +0 -41
- package/node_modules/@oxygen/shared/dist/copilot-skills.js +0 -6
package/README.md
CHANGED
package/dist/command-manifest.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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-
|
|
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
|
|
31
|
-
+ "`connectors list` shows them; `connectors
|
|
32
|
-
+ "
|
|
33
|
-
+ "
|
|
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
|
|
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 }), {
|
package/dist/contract-flags.d.ts
CHANGED
|
@@ -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.
|
package/dist/contract-flags.js
CHANGED
|
@@ -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"
|
|
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
|
-
|
|
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:
|
|
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"
|
|
149
|
-
return value
|
|
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
|
|
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:
|
|
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,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
|
|
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>'`.
|
package/dist/http-client.d.ts
CHANGED
|
@@ -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;
|