@oxygen-agent/cli 1.1011.855 → 1.1011.1091

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 (71) hide show
  1. package/README.md +1 -1
  2. package/dist/admin-lead-magnet-commands.d.ts +7 -0
  3. package/dist/admin-lead-magnet-commands.js +128 -0
  4. package/dist/command-manifest.js +29 -0
  5. package/dist/crm-support-flags.d.ts +1 -0
  6. package/dist/crm-support-flags.js +5 -1
  7. package/dist/customer-operation-commands.js +30 -1
  8. package/dist/functions-commands.js +1 -1
  9. package/dist/help.js +10 -2
  10. package/dist/http-client.d.ts +4 -0
  11. package/dist/http-client.js +21 -10
  12. package/dist/index.js +444 -208
  13. package/dist/sender-limits-flags.d.ts +7 -0
  14. package/dist/sender-limits-flags.js +13 -0
  15. package/dist/workspace-utility-flags.d.ts +7 -0
  16. package/dist/workspace-utility-flags.js +45 -0
  17. package/node_modules/@oxygen/shared/dist/attempt-provenance.d.ts +22 -0
  18. package/node_modules/@oxygen/shared/dist/attempt-provenance.js +83 -0
  19. package/node_modules/@oxygen/shared/dist/billing-anchors.d.ts +6 -0
  20. package/node_modules/@oxygen/shared/dist/billing-anchors.js +7 -1
  21. package/node_modules/@oxygen/shared/dist/capability-discovery.d.ts +6 -0
  22. package/node_modules/@oxygen/shared/dist/capability-discovery.js +279 -13
  23. package/node_modules/@oxygen/shared/dist/cli-result.js +6 -0
  24. package/node_modules/@oxygen/shared/dist/contracts/browser-operations.js +21 -9
  25. package/node_modules/@oxygen/shared/dist/contracts/crm-attributes.d.ts +7 -1
  26. package/node_modules/@oxygen/shared/dist/contracts/crm-attributes.js +5 -1
  27. package/node_modules/@oxygen/shared/dist/contracts/crm-support.d.ts +14 -0
  28. package/node_modules/@oxygen/shared/dist/contracts/crm-support.js +4 -0
  29. package/node_modules/@oxygen/shared/dist/contracts/customer-operations.js +13 -11
  30. package/node_modules/@oxygen/shared/dist/contracts/limits-read.js +11 -1
  31. package/node_modules/@oxygen/shared/dist/contracts/operations.d.ts +6 -0
  32. package/node_modules/@oxygen/shared/dist/contracts/publishing-reads.js +1 -0
  33. package/node_modules/@oxygen/shared/dist/contracts/schema.js +10 -6
  34. package/node_modules/@oxygen/shared/dist/contracts/spintax.d.ts +18 -0
  35. package/node_modules/@oxygen/shared/dist/contracts/spintax.js +11 -0
  36. package/node_modules/@oxygen/shared/dist/contracts/table-row-removal.d.ts +8 -0
  37. package/node_modules/@oxygen/shared/dist/contracts/table-row-removal.js +21 -0
  38. package/node_modules/@oxygen/shared/dist/contracts/workspace-utilities.d.ts +162 -0
  39. package/node_modules/@oxygen/shared/dist/contracts/workspace-utilities.js +46 -0
  40. package/node_modules/@oxygen/shared/dist/credit-topup-review.d.ts +11 -0
  41. package/node_modules/@oxygen/shared/dist/credit-topup-review.js +3 -0
  42. package/node_modules/@oxygen/shared/dist/emailguard-placement-probe.d.ts +70 -0
  43. package/node_modules/@oxygen/shared/dist/emailguard-placement-probe.js +161 -0
  44. package/node_modules/@oxygen/shared/dist/future-signup-events.d.ts +1 -1
  45. package/node_modules/@oxygen/shared/dist/future-signup-events.js +4 -0
  46. package/node_modules/@oxygen/shared/dist/knowledge-seed-content.js +2 -2
  47. package/node_modules/@oxygen/shared/dist/linkedin-quota-denial.d.ts +48 -0
  48. package/node_modules/@oxygen/shared/dist/linkedin-quota-denial.js +94 -0
  49. package/node_modules/@oxygen/shared/dist/partner-coupon-bonus.d.ts +45 -0
  50. package/node_modules/@oxygen/shared/dist/partner-coupon-bonus.js +57 -0
  51. package/node_modules/@oxygen/shared/dist/provider-balance-signal.js +3 -2
  52. package/node_modules/@oxygen/shared/dist/table-filter-operators.d.ts +6 -0
  53. package/node_modules/@oxygen/shared/dist/table-filter-operators.js +50 -0
  54. package/node_modules/@oxygen/shared/dist/telemetry.js +4 -0
  55. package/node_modules/@oxygen/shared/dist/version.d.ts +1 -1
  56. package/node_modules/@oxygen/shared/dist/version.generated.d.ts +1 -1
  57. package/node_modules/@oxygen/shared/dist/version.generated.js +1 -1
  58. package/node_modules/@oxygen/shared/dist/version.js +1 -1
  59. package/node_modules/@oxygen/shared/dist/whatsapp-sender-limits.d.ts +42 -0
  60. package/node_modules/@oxygen/shared/dist/whatsapp-sender-limits.js +68 -0
  61. package/node_modules/@oxygen/shared/dist/workflow-trigger-metadata.d.ts +15 -0
  62. package/node_modules/@oxygen/shared/dist/workflow-trigger-metadata.js +33 -0
  63. package/node_modules/@oxygen/shared/package.json +5 -5
  64. package/node_modules/@oxygen/workflows/dist/index.d.ts +1 -0
  65. package/node_modules/@oxygen/workflows/dist/index.js +1 -0
  66. package/node_modules/@oxygen/workflows/dist/recipe-timestamp.d.ts +22 -0
  67. package/node_modules/@oxygen/workflows/dist/recipe-timestamp.js +72 -0
  68. package/node_modules/@oxygen/workflows/dist/usage-estimate.js +9 -5
  69. package/package.json +1 -1
  70. package/node_modules/@oxygen/shared/dist/vercel-sandbox-fetch.d.ts +0 -9
  71. package/node_modules/@oxygen/shared/dist/vercel-sandbox-fetch.js +0 -33
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.855
37
+ Version: 1.1011.1091
@@ -0,0 +1,7 @@
1
+ import { Command } from "commander";
2
+ type Handle = (command: string, options: {
3
+ json?: boolean;
4
+ }, action: () => Promise<unknown>) => Promise<void>;
5
+ /** `oxygen admin lead-magnets`: the staff CMS for the public /resources catalog, the same rows /admin/lead-magnets edits. */
6
+ export declare function registerAdminLeadMagnetCommands(program: Command, handle: Handle): void;
7
+ export {};
@@ -0,0 +1,128 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { basename, extname, resolve } from "node:path";
3
+ import { Option } from "commander";
4
+ import { OxygenError } from "@oxygen/shared/cli-result";
5
+ import { resolveLiveDryRunMode } from "./cli-values.js";
6
+ import { requestOxygen } from "./http-client.js";
7
+ // The resource JSON documented in docs/marketing-resources.md, mapped to the API body.
8
+ const FILE_FIELDS = {
9
+ slug: "slug", title: "title", description: "description", imageUrl: "image_url", imageAlt: "image_alt",
10
+ redirectUrl: "redirect_url", status: "status", sortOrder: "sort_order",
11
+ };
12
+ const COVER_TYPES = { ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".webp": "image/webp" };
13
+ // The server's own ceiling (LEAD_MAGNET_MAX_IMAGE_BYTES), checked first so an oversized file is never sent.
14
+ const MAX_COVER_BYTES = 4 * 1024 * 1024;
15
+ const usageError = (message) => new OxygenError("invalid_request", message, { exitCode: 2 });
16
+ /** `oxygen admin lead-magnets`: the staff CMS for the public /resources catalog, the same rows /admin/lead-magnets edits. */
17
+ export function registerAdminLeadMagnetCommands(program, handle) {
18
+ const admin = program.commands.find((command) => command.name() === "admin");
19
+ if (!admin)
20
+ throw new Error("Admin commands must be registered first.");
21
+ const leadMagnets = admin
22
+ .command("lead-magnets")
23
+ .description("Publish, update and unpublish the email-gated lead magnets (resources) on the public /resources catalog. Staff only.");
24
+ leadMagnets
25
+ .command("list")
26
+ .description("List every lead magnet (resource) on /resources, drafts included, with status and public URL. Staff only, read-only.")
27
+ .option("--json", "Print a JSON envelope.")
28
+ .action((options) => handle("admin lead-magnets list", options, () => requestOxygen("/api/cli/admin/lead-magnets")));
29
+ leadMagnets
30
+ .command("get")
31
+ .description("Show one lead magnet (resource) by slug: every field, draft or published, and its public /resources URL. Staff only, read-only.")
32
+ .argument("<slug>", "The resource's slug (its /resources/<slug> path) or id.")
33
+ .option("--json", "Print a JSON envelope.")
34
+ .action((slug, options) => handle("admin lead-magnets get", options, () => requestOxygen(`/api/cli/admin/lead-magnets/${encodeURIComponent(slug)}`)));
35
+ leadMagnets
36
+ .command("upsert")
37
+ .description("Publish or update the lead magnet (resource) at a slug on /resources from a resource JSON file and/or flags; --cover uploads a local image. Dry run by default: prints the exact row and changed fields, uploads and writes nothing. --live applies it, and a published resource is public at once. Staff only.")
38
+ .option("--file <path>", "Resource JSON with slug, title, description, imageUrl, imageAlt, redirectUrl, status, sortOrder. Flags override its values.")
39
+ .option("--slug <slug>", "Permanent public slug (/resources/<slug>): lowercase letters, digits and hyphens. A new slug creates another resource.")
40
+ .option("--title <text>", "Title, up to 160 characters.")
41
+ .option("--description <text>", "Brief description, up to 600 characters.")
42
+ .option("--redirect-url <url>", "HTTPS link the visitor opens after entering an email.")
43
+ .option("--cover <path>", "Local PNG, JPEG or WebP cover image, up to 4 MB, uploaded to public storage on --live.")
44
+ .option("--image-url <url>", "HTTPS URL of an already hosted cover image, instead of --cover.")
45
+ .option("--image-alt <text>", "Cover alt text. Defaults to the title.")
46
+ .addOption(new Option("--status <status>", "published lists it on /resources; draft hides it. A new resource defaults to draft.").choices(["published", "draft"]))
47
+ .option("--sort-order <n>", "Catalog position, lowest first. Defaults to 0.")
48
+ .option("--dry-run", "Preview without uploading or writing (the default).")
49
+ .option("--live", "Upload the cover and write the resource.")
50
+ .option("--json", "Print a JSON envelope.")
51
+ .action((options) => handle("admin lead-magnets upsert", options, () => upsertLeadMagnet(options)));
52
+ leadMagnets
53
+ .command("unpublish")
54
+ .description("Unpublish a lead magnet (resource): set it to draft, which removes it and its page from /resources and keeps its email records. Dry run by default; --live applies it. Staff only.")
55
+ .argument("<slug>", "The resource's slug.")
56
+ .option("--dry-run", "Preview without writing (the default).")
57
+ .option("--live", "Unpublish it.")
58
+ .option("--json", "Print a JSON envelope.")
59
+ .action((slug, options) => handle("admin lead-magnets unpublish", options, () => requestOxygen(`/api/cli/admin/lead-magnets/${encodeURIComponent(slug)}`, {
60
+ method: "POST",
61
+ body: { status: "draft", mode: resolveLiveDryRunMode(options) },
62
+ })));
63
+ }
64
+ async function upsertLeadMagnet(options) {
65
+ const mode = resolveLiveDryRunMode(options);
66
+ if (options.cover !== undefined && options.imageUrl !== undefined) {
67
+ throw new OxygenError("conflicting_flags", "Pass either --cover or --image-url, not both.", { exitCode: 1 });
68
+ }
69
+ const fromFile = options.file === undefined ? {} : await readResourceFile(options.file);
70
+ if (options.slug !== undefined && fromFile.slug !== undefined && fromFile.slug !== options.slug) {
71
+ throw usageError(`--slug ${options.slug} does not match the file's slug ${String(fromFile.slug)}. The slug is the resource's identity: drop --slug or fix the file.`);
72
+ }
73
+ const flags = {
74
+ slug: options.slug, title: options.title, description: options.description, redirect_url: options.redirectUrl,
75
+ image_url: options.imageUrl, image_alt: options.imageAlt, status: options.status,
76
+ sort_order: options.sortOrder === undefined ? undefined : readSortOrder(options.sortOrder),
77
+ };
78
+ const { slug, ...fields } = { ...fromFile, ...Object.fromEntries(Object.entries(flags).filter(([, value]) => value !== undefined)) };
79
+ if (typeof slug !== "string" || !slug)
80
+ throw usageError("Name the resource with --slug, or a slug in --file.");
81
+ // A local cover replaces any imageUrl the file carries.
82
+ if (options.cover !== undefined)
83
+ delete fields.image_url;
84
+ const body = { ...fields, mode };
85
+ if (options.cover === undefined) {
86
+ return requestOxygen(`/api/cli/admin/lead-magnets/${encodeURIComponent(slug)}`, { method: "POST", body });
87
+ }
88
+ const form = new FormData();
89
+ form.append("payload", JSON.stringify(body));
90
+ form.append("image", await readCover(options.cover));
91
+ return requestOxygen(`/api/cli/admin/lead-magnets/${encodeURIComponent(slug)}`, { method: "POST", formData: form });
92
+ }
93
+ async function readResourceFile(path) {
94
+ let parsed;
95
+ try {
96
+ parsed = JSON.parse(await readFile(resolve(path), "utf8"));
97
+ }
98
+ catch (error) {
99
+ throw usageError(`--file must be a readable JSON file: ${error instanceof Error ? error.message : String(error)}`);
100
+ }
101
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
102
+ throw usageError("--file must hold one JSON object.");
103
+ const unknown = Object.keys(parsed).filter((key) => !Object.hasOwn(FILE_FIELDS, key));
104
+ if (unknown.length > 0) {
105
+ throw usageError(`--file has unknown fields: ${unknown.join(", ")}. A resource file has ${Object.keys(FILE_FIELDS).join(", ")}.`);
106
+ }
107
+ return Object.fromEntries(Object.entries(parsed).map(([key, value]) => [FILE_FIELDS[key], value]));
108
+ }
109
+ async function readCover(path) {
110
+ const type = COVER_TYPES[extname(path).toLowerCase()];
111
+ if (!type)
112
+ throw usageError("--cover must be a .png, .jpg, .jpeg or .webp image.");
113
+ let bytes;
114
+ try {
115
+ bytes = await readFile(resolve(path));
116
+ }
117
+ catch (error) {
118
+ throw usageError(`--cover could not be read: ${error instanceof Error ? error.message : String(error)}`);
119
+ }
120
+ if (bytes.byteLength > MAX_COVER_BYTES)
121
+ throw usageError("--cover must be 4 MB or smaller.");
122
+ return new File([bytes], basename(path), { type });
123
+ }
124
+ function readSortOrder(value) {
125
+ if (!/^-?\d+$/.test(value.trim()))
126
+ throw usageError("--sort-order must be a whole number.");
127
+ return Number(value);
128
+ }
@@ -78,6 +78,9 @@ const MUTATING_VERBS = new Set([
78
78
  // Thread field, while a future `field-get` would be a read.
79
79
  "field-set",
80
80
  "file", "forward", "grant", "harvest", "history", "import", "insert", "interrupt", "invite",
81
+ // `crm keep` clears a seeded sample record's marker for good: from then on the
82
+ // record counts in pipeline totals. A verb that reads like "leave as is".
83
+ "keep",
81
84
  "label-add", "label-remove", "launch", "log", "login", "logout", "mark-all-read", "mark-read", "materialize", "merge",
82
85
  "migrate",
83
86
  "move", "note", "off", "onboard", "order", "pause", "pin", "priority", "provision",
@@ -101,10 +104,18 @@ const MUTATING_VERBS = new Set([
101
104
  // status persists state and can pause the provider; reconnect replaces its
102
105
  // connection and can restart provider history. Other status commands are reads.
103
106
  const MUTATING_COMMANDS = new Set([
107
+ // These balance reads can post an eligible due grant or expire due sponsored
108
+ // credits. The exact exceptions disclose writes without inventing spend.
109
+ "billing balance",
110
+ "billing seats",
104
111
  // A default preview is free; approval restores provider seats and billing.
105
112
  "managed-inboxes recover",
106
113
  "mailboxes warmup status",
107
114
  "mailboxes warmup reconnect",
115
+ // Approval creates a provider consent link; status can attach its receipt.
116
+ "mailboxes warmup google",
117
+ // Immediately moves a provider proxy and may pause or resume the sender.
118
+ "senders proxy-country",
108
119
  // A noun leaf with its own action (not a group), so the default would
109
120
  // advertise a STANDING per-viewer paid-enrichment toggle as a read. `enrichment`
110
121
  // itself is not a MUTATING_VERBS entry because the other two `enrichment`
@@ -129,6 +140,10 @@ const MUTATING_COMMANDS = new Set([
129
140
  // mode, but a small number of approved operations are explicitly
130
141
  // zero-credit. Keep those exceptions exact so discovery never invents spend.
131
142
  const ZERO_CREDIT_APPROVAL_COMMANDS = new Set([
143
+ // The compatibility --approved flag never bypasses canonical proposal review.
144
+ // Wiki revisions and canonical pin changes are free (Knowledge audit, 2026-10-06).
145
+ "knowledge page upsert",
146
+ "knowledge page pin",
132
147
  "inbox delete-message",
133
148
  "inbox forget-whatsapp-chat",
134
149
  "tables watcher preview",
@@ -163,12 +178,18 @@ const ZERO_CREDIT_APPROVAL_COMMANDS = new Set([
163
178
  // progress are covered by the existing monthly mailbox commitment.
164
179
  "mailboxes connect-oauth",
165
180
  "mailboxes sync",
181
+ // Separate Google consent always has a zero-credit ceiling; enrollment bills
182
+ // through its own warmup enable command.
183
+ "mailboxes warmup google",
166
184
  // Previews without --approved; with it, removes a program no creator ever
167
185
  // joined. No provider call, no credits.
168
186
  "ugc programs delete",
169
187
  // This approval only reopens exact fingerprint-bound tenant rows. It never
170
188
  // calls the provider, dispatches an action, or touches the credit ledger.
171
189
  "sequences recover-capacity",
190
+ // Provider reads are free; approval only records an exact match locally.
191
+ // Neither branch resends or writes to the provider.
192
+ "sequences reconcile",
172
193
  // Credential replacement is destructive but does not execute the graph,
173
194
  // call a provider, or spend Oxygen credits.
174
195
  "workflows webhooks rotate",
@@ -182,6 +203,10 @@ const ZERO_CREDIT_APPROVAL_COMMANDS = new Set([
182
203
  // one. Keep the exception exact so genuine `run` actions remain mutations.
183
204
  const READ_ONLY_COMMANDS = new Set([
184
205
  "workflows run",
206
+ // `history` is a mutating verb for `whatsapp history` (it backfills a chat);
207
+ // these two only read a row's and a cell's recorded changes.
208
+ "rows history",
209
+ "cells history",
185
210
  // Inspect existing creator receipts; never generate or import again.
186
211
  "ugc posts draft-status",
187
212
  "ugc posts history",
@@ -190,6 +215,9 @@ const READ_ONLY_COMMANDS = new Set([
190
215
  // invocation still previews, so machine-readable discovery must not imply that
191
216
  // calling the command without its confirmation flag writes anything.
192
217
  const PREVIEW_BY_DEFAULT_COMMANDS = new Set([
218
+ "mailboxes warmup google",
219
+ "deliverability placement-test run",
220
+ "deliverability placement-test send",
193
221
  "managed-inboxes recover",
194
222
  // /api/cli/find/run treats an omitted mode as dry_run for every Find capability.
195
223
  "find email",
@@ -222,6 +250,7 @@ const PREVIEW_BY_DEFAULT_COMMANDS = new Set([
222
250
  // No action reads progress; an action previews until --approved.
223
251
  "mailboxes sync",
224
252
  "sequences recover-capacity",
253
+ "sequences reconcile",
225
254
  "support admin done",
226
255
  "support admin reply",
227
256
  // A bare call reads the Plain workspace and prints the plan; --apply writes.
@@ -8,4 +8,5 @@ export declare function readCrmPhotoFields(options: {
8
8
  }): Record<string, unknown>;
9
9
  export declare function readCrmFileLabel(options: object): Record<string, unknown>;
10
10
  export declare function buildCrmObjectsPath(options: object): string;
11
+ export declare function buildCrmRecordPath(object: string, rowId: string, options: object): string;
11
12
  export declare function buildCrmConnectionsPath(object: string, rowId: string, options: object): string;
@@ -1,5 +1,5 @@
1
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";
2
+ import { CRM_CONNECTIONS_QUERY_CONTRACT, CRM_NOTE_EDIT_CONTRACT, CRM_OBJECTS_QUERY_CONTRACT, CRM_PHOTOS_CONTRACT, CRM_FILE_COMPLETE_CONTRACT, CRM_RECORD_QUERY_CONTRACT } from "@oxygen/shared/contracts/crm-support";
3
3
  import { validateContract } from "@oxygen/shared/contracts/schema";
4
4
  import { addContractFlags, readContractFlags } from "./contract-flags.js";
5
5
  import { assertModeFlagsExclusive } from "./cli-values.js";
@@ -19,6 +19,7 @@ function replaceFlags(command, attributes, contract, overlay) {
19
19
  export function registerCrmSupportFlags(program) {
20
20
  const crm = child(program, "crm");
21
21
  replaceFlags(child(crm, "objects"), [], CRM_OBJECTS_QUERY_CONTRACT);
22
+ replaceFlags(child(crm, "get"), [], CRM_RECORD_QUERY_CONTRACT);
22
23
  replaceFlags(child(crm, "connections"), ["conversationLimit", "sequenceLimit"], CRM_CONNECTIONS_QUERY_CONTRACT);
23
24
  replaceFlags(child(child(crm, "notes"), "update"), ["body", "title", "pin", "unpin"], CRM_NOTE_EDIT_CONTRACT, NOTE_OVERLAY);
24
25
  replaceFlags(child(crm, "photos"), ["limit", "live"], CRM_PHOTOS_CONTRACT);
@@ -55,6 +56,9 @@ function queryPath(path, query) {
55
56
  return params.size ? `${path}?${params}` : path;
56
57
  }
57
58
  export function buildCrmObjectsPath(options) { return queryPath("/api/cli/crm/objects", checkedFlags(options, CRM_OBJECTS_QUERY_CONTRACT)); }
59
+ export function buildCrmRecordPath(object, rowId, options) {
60
+ return queryPath(`/api/cli/crm/objects/${encodeURIComponent(object)}/records/${encodeURIComponent(rowId)}`, checkedFlags(options, CRM_RECORD_QUERY_CONTRACT));
61
+ }
58
62
  export function buildCrmConnectionsPath(object, rowId, options) {
59
63
  return queryPath(`/api/cli/crm/objects/${encodeURIComponent(object)}/records/${encodeURIComponent(rowId)}/connections`, checkedFlags(options, CRM_CONNECTIONS_QUERY_CONTRACT));
60
64
  }
@@ -32,6 +32,10 @@ const GROUP_PURPOSES = {
32
32
  onboarding: "Save workspace discovery choices and continue onboarding.",
33
33
  };
34
34
  const OPERATION_FLAGS = {
35
+ "tables rows remove-selection": {
36
+ table: { help: "Table id or slug." },
37
+ row_ids: { help: "Comma-separated exact row IDs. Choose this or --selection-json." },
38
+ },
35
39
  "tables favorite": { favorite: { negateHelp: "Unstar the selected Table or folder (favorite: false)." } },
36
40
  "tables favorites": { details: { negateHelp: "Return stored star keys without names or links (details: false)." } },
37
41
  };
@@ -92,7 +96,7 @@ export function registerCustomerOperationCommands(program, handle) {
92
96
  command.action((options, selected) => handle(spec.cli.path.join(" "), { ...options, json: selected.optsWithGlobals().json === true }, async () => {
93
97
  let input;
94
98
  try {
95
- input = readContractFlags(options, contract, overlay);
99
+ input = readContractFlags(ancestorConsumedOptions(command, selected, options), contract, overlay);
96
100
  if (spec.multipart && options.file !== undefined) {
97
101
  if (input[spec.multipart.contentField] !== undefined)
98
102
  throw new OxygenError("invalid_request", "Choose --file or the base64 content flag, not both.", { exitCode: 2 });
@@ -139,6 +143,31 @@ export function registerCustomerOperationCommands(program, handle) {
139
143
  }));
140
144
  }
141
145
  }
146
+ /**
147
+ * Commander lets an ancestor that declares the same flag consume it, so a value
148
+ * the user typed for this leaf lands on the parent and the leaf reads nothing:
149
+ * `sequences contacts rows --limit 1` sent no limit at all, because the native
150
+ * `sequences contacts` list owns `--limit` too, and the server returned its
151
+ * default page (OXP-1.11.67). Take such a value back only when the user typed it
152
+ * (value source `cli`); an ancestor's own default never becomes this leaf's input.
153
+ */
154
+ function ancestorConsumedOptions(leaf, selected, options) {
155
+ const merged = { ...options };
156
+ for (const option of leaf.options) {
157
+ const key = option.attributeName();
158
+ if (merged[key] !== undefined)
159
+ continue;
160
+ for (let ancestor = selected.parent; ancestor; ancestor = ancestor.parent) {
161
+ const owned = ancestor.options.find((candidate) => candidate.long === option.long);
162
+ if (!owned)
163
+ continue;
164
+ if (ancestor.getOptionValueSource(owned.attributeName()) === "cli")
165
+ merged[key] = ancestor.opts()[owned.attributeName()];
166
+ break;
167
+ }
168
+ }
169
+ return merged;
170
+ }
142
171
  function uploadContentType(filePath) {
143
172
  const types = {
144
173
  ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".webp": "image/webp", ".svg": "image/svg+xml",
@@ -42,7 +42,7 @@ export function registerFunctionsCommands(program, handle) {
42
42
  .requiredOption("--expected-draft-hash <hash>", "function.draft.contentHash returned by draft or describe; rejects concurrent edits.")
43
43
  .option("--json", "Print a JSON envelope.")
44
44
  .action((ref, options) => handle("functions publish", options, () => requestOxygen("/api/cli/functions", { method: "POST", body: { operation: "publish", function: ref, expected_draft_hash: options.expectedDraftHash } })));
45
- functions.command("runs <function>").description("Inspect a Function's latest 100 invocations, the table that called each one, and their captured versions. For a call that did not complete, creditsUsed can show the estimate; `table-runs provider-summary <run_id>` reports the credits actually captured.")
45
+ functions.command("runs <function>").description("Inspect a Function's latest 100 invocations, the table that called each one, and their captured versions and bindings: each run's overwrite policy, output mapping and run condition as they were when it ran. Per row, the calling table's Function cell records overwrite_policy, written_columns, overwritten_columns (filled cells it replaced) and kept_columns (filled cells empty_only left). For a call that did not complete, creditsUsed can show the estimate; `table-runs provider-summary <run_id>` reports the credits actually captured.")
46
46
  .option("--json", "Print a JSON envelope.")
47
47
  .action((ref, options) => handle("functions runs", options, () => requestOxygen(`/api/cli/functions/runs?${new URLSearchParams({ function: ref })}`)));
48
48
  functions.command("rename <function> <name>").description("Rename a Function while preserving its identity, callers and published execution.")
package/dist/help.js CHANGED
@@ -141,6 +141,7 @@ export function applyOxygenHelp(program, binaryName) {
141
141
  "",
142
142
  "Agent quickstart:",
143
143
  ` 1. ${binaryName} login connect this terminal (or set OXYGEN_API_KEY)`,
144
+ ` ${binaryName} whoami --summary-json confirm the active workspace and identity (0 credits)`,
144
145
  // `login` auto-installs the skills, but an agent authenticating with
145
146
  // OXYGEN_API_KEY never runs it — without this step nothing on any
146
147
  // first-touch surface names the skill that teaches the GTM loops.
@@ -163,8 +164,9 @@ export function applyOxygenHelp(program, binaryName) {
163
164
  "Conventions:",
164
165
  " --json prints a stable envelope {ok, data|error, meta}; errors always print it.",
165
166
  " Free Sequence launch previews omit --approved and --max-credits.",
166
- " Credit-spending commands require --approved --max-credits <n>; zero-credit",
167
- " external writes require --approved plus the command's preview-bound plan token.",
167
+ " Credit-spending commands require --approved --max-credits <n>.",
168
+ " Zero-credit external writes require --approved; supply a preview-bound plan",
169
+ " token when the command requires one (see its preview and --help).",
168
170
  " State-changing responses include a web_url deep-link to inspect the result.",
169
171
  ].join("\n"));
170
172
  program.addHelpText("afterAll", `\nExit codes: ${exitCodeLine}`);
@@ -360,6 +362,12 @@ export function unknownCommandHint(program, argv) {
360
362
  const { command, path, next } = resolveCommandPath(program, argv);
361
363
  if (next === undefined || command.commands.length === 0)
362
364
  return null;
365
+ const identityExists = program.commands.some((candidate) => candidate.name() === "whoami");
366
+ const currentWorkspace = (command.parent === program && command.name() === "workspace" && next === "current")
367
+ || (command === program && next === "workspaces" && argv.some((token, index) => token === "workspaces" && argv[index + 1] === "current"));
368
+ if (identityExists && currentWorkspace) {
369
+ return `Hint: Active workspace and identity: \`${program.name()} whoami --summary-json\` (read-only, 0 credits).`;
370
+ }
363
371
  return buildUnknownCommandHint(command, path, next);
364
372
  }
365
373
  /**
@@ -6,6 +6,10 @@ type RequestOptions = {
6
6
  /** Stream a native authenticated download to an exclusively created file. */
7
7
  binaryOutputPath?: string;
8
8
  method?: "GET" | "POST" | "PATCH" | "DELETE";
9
+ /** A native read that uses POST for its selection payload. Never set on a write. */
10
+ readOnly?: boolean;
11
+ /** Only for audited preview routes that request no product mutation. */
12
+ previewOnly?: boolean;
9
13
  body?: Record<string, unknown>;
10
14
  formData?: FormData;
11
15
  credentials?: StoredCredentials;
@@ -11,6 +11,7 @@ const CLI_COMPATIBILITY_CHECK_TIMEOUT_MS = 5_000;
11
11
  // Replay on the Idempotency-Key is per route (most routes ignore the header),
12
12
  // so this must not promise that a same-key retry is safe.
13
13
  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.";
14
+ const FAILED_READ_NEXT_STEP = "This read failed. Check your connection; it is safe to retry the same read.";
14
15
  const cliCompatibilityCheckKeys = new Set();
15
16
  export async function requestOxygen(// skipcq: JS-R1005
16
17
  path, options = {}) {
@@ -75,7 +76,13 @@ path, options = {}) {
75
76
  // echoed in error details so a re-run (OXYGEN_IDEMPOTENCY_KEY=<key>) can
76
77
  // retry without duplicating the write once the route supports replay.
77
78
  const method = options.method ?? "GET";
78
- const idempotencyKey = method === "GET"
79
+ const readOnly = method === "GET" || options.readOnly === true;
80
+ const recovery = {
81
+ next_step: options.previewOnly
82
+ ? "This was a preview; no changes were requested. Retry the preview when Oxygen is reachable."
83
+ : readOnly ? FAILED_READ_NEXT_STEP : UNCONFIRMED_WRITE_NEXT_STEP,
84
+ };
85
+ const idempotencyKey = readOnly
79
86
  ? undefined
80
87
  : options.idempotencyKey ?? process.env.OXYGEN_IDEMPOTENCY_KEY ?? randomUUID();
81
88
  if (idempotencyKey) {
@@ -108,7 +115,6 @@ path, options = {}) {
108
115
  catch (error) {
109
116
  if (error instanceof OxygenError)
110
117
  throw error;
111
- const unconfirmedWrite = method === "GET" ? {} : { next_step: UNCONFIRMED_WRITE_NEXT_STEP };
112
118
  if (timeout.timedOut() || (timeoutMs !== undefined && isAbortError(error))) {
113
119
  throw new OxygenError("network_timeout", "Oxygen API request timed out.", {
114
120
  details: {
@@ -117,9 +123,9 @@ path, options = {}) {
117
123
  timeout_ms: timeoutMs,
118
124
  trace_id: traceId,
119
125
  ...(idempotencyKey ? { idempotency_key: idempotencyKey } : {}),
120
- ...unconfirmedWrite,
126
+ ...recovery,
121
127
  },
122
- exitCode: 1,
128
+ exitCode: 8,
123
129
  });
124
130
  }
125
131
  throw new OxygenError("network_error", "Unable to reach Oxygen API.", {
@@ -127,9 +133,9 @@ path, options = {}) {
127
133
  api_url: apiUrl,
128
134
  reason: error instanceof Error ? error.message : "unknown",
129
135
  ...(idempotencyKey ? { idempotency_key: idempotencyKey } : {}),
130
- ...unconfirmedWrite,
136
+ ...recovery,
131
137
  },
132
- exitCode: 1,
138
+ exitCode: 5,
133
139
  });
134
140
  }
135
141
  finally {
@@ -141,12 +147,17 @@ path, options = {}) {
141
147
  // calling the gateway's page an invalid envelope.
142
148
  if (!(error instanceof OxygenError) || ![502, 503, 504].includes(response.status))
143
149
  throw error;
144
- throw new OxygenError("invalid_response", `Oxygen API did not answer in time (HTTP ${response.status}); the outcome is unknown.`, {
150
+ const message = options.previewOnly
151
+ ? `Oxygen API did not answer in time (HTTP ${response.status}); the preview was not returned.`
152
+ : readOnly
153
+ ? `Oxygen API did not answer in time (HTTP ${response.status}); the read was not returned.`
154
+ : `Oxygen API did not answer in time (HTTP ${response.status}); the outcome is unknown.`;
155
+ throw new OxygenError("invalid_response", message, {
145
156
  details: {
146
157
  status: response.status,
147
158
  trace_id: response.headers.get("x-oxygen-trace-id") ?? traceId,
148
159
  ...(idempotencyKey ? { idempotency_key: idempotencyKey } : {}),
149
- ...(method === "GET" ? {} : { next_step: UNCONFIRMED_WRITE_NEXT_STEP }),
160
+ ...recovery,
150
161
  },
151
162
  exitCode: 1,
152
163
  });
@@ -173,11 +184,11 @@ path, options = {}) {
173
184
  if (error.code === "cli_update_required")
174
185
  throw await recoverFromCliFloorRejection(error, endpoint);
175
186
  // Any other answer to a write may have written something; never re-run the command after it.
176
- if (method !== "GET")
187
+ if (!readOnly)
177
188
  recordMutatingRequestAnswered();
178
189
  throw error;
179
190
  }
180
- if (method !== "GET")
191
+ if (!readOnly)
181
192
  recordMutatingRequestAnswered();
182
193
  return envelope.data;
183
194
  }