@oxygen-agent/cli 1.275.2 → 1.285.8

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 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.275.2
37
+ Version: 1.285.8
@@ -0,0 +1,30 @@
1
+ import type { Command } from "commander";
2
+ export type CommandManifestFlag = {
3
+ flags: string;
4
+ description: string;
5
+ required: boolean;
6
+ };
7
+ export type CommandManifestArgument = {
8
+ name: string;
9
+ required: boolean;
10
+ description: string;
11
+ };
12
+ export type CommandManifestEntry = {
13
+ name: string;
14
+ group: string;
15
+ description: string;
16
+ arguments: CommandManifestArgument[];
17
+ flags: CommandManifestFlag[];
18
+ spends_credits: boolean;
19
+ mutates: boolean;
20
+ json_supported: boolean;
21
+ hidden: boolean;
22
+ };
23
+ export type CommandManifest = {
24
+ binary: string;
25
+ version: string;
26
+ total: number;
27
+ exit_codes: Record<string, string>;
28
+ commands: CommandManifestEntry[];
29
+ };
30
+ export declare function buildCommandManifest(program: Command, binaryName: string): CommandManifest;
@@ -0,0 +1,70 @@
1
+ import { CLI_EXIT_CODE_TABLE, OXYGEN_VERSION } from "@oxygen/shared";
2
+ const MUTATING_VERBS = new Set([
3
+ "add", "apply", "approve", "archive", "assign", "buy", "cancel", "clear",
4
+ "configure", "connect", "create", "delete", "disable", "dispatch", "draft",
5
+ "emit", "enable", "enroll", "grant", "import", "insert", "invite", "launch",
6
+ "login", "logout", "materialize", "merge", "migrate", "order", "pause",
7
+ "publish", "purchase", "push", "record", "register", "remove", "rename",
8
+ "reorder", "reply", "rerun", "reset", "resolve", "restore", "resume",
9
+ "retry", "retype", "revoke", "rotate", "run", "save", "schedule", "select",
10
+ "send", "set", "share", "start", "stop", "subscribe", "sync", "unpublish",
11
+ "unshare", "unsubscribe", "update", "upload", "upsert", "use", "withdraw",
12
+ "write",
13
+ ]);
14
+ export function buildCommandManifest(program, binaryName) {
15
+ const commands = [];
16
+ for (const child of program.commands) {
17
+ collectCommand(child, [], commands);
18
+ }
19
+ commands.sort((a, b) => a.name.localeCompare(b.name));
20
+ return {
21
+ binary: binaryName,
22
+ version: OXYGEN_VERSION,
23
+ total: commands.length,
24
+ exit_codes: Object.fromEntries(Object.entries(CLI_EXIT_CODE_TABLE).map(([code, meaning]) => [code, meaning])),
25
+ commands,
26
+ };
27
+ }
28
+ function collectCommand(command, ancestors, out) {
29
+ const path = [...ancestors, command.name()];
30
+ const children = command.commands;
31
+ // Group commands (e.g. `oxygen tables`) exist to hold subcommands; only leaf
32
+ // commands are invocable work, so only leaves become manifest entries.
33
+ if (children.length === 0) {
34
+ out.push(toManifestEntry(command, path));
35
+ return;
36
+ }
37
+ for (const child of children) {
38
+ collectCommand(child, path, out);
39
+ }
40
+ }
41
+ function toManifestEntry(command, path) {
42
+ const options = command.options.filter((option) => !option.hidden);
43
+ const flagStrings = options.map((option) => option.flags);
44
+ const leafVerb = path[path.length - 1] ?? "";
45
+ const verbPrefix = leafVerb.split("-")[0] ?? leafVerb;
46
+ return {
47
+ name: path.join(" "),
48
+ group: path[0] ?? "",
49
+ description: command.description(),
50
+ arguments: command.registeredArguments.map((argument) => ({
51
+ name: argument.name(),
52
+ required: argument.required,
53
+ description: argument.description,
54
+ })),
55
+ flags: options.map((option) => ({
56
+ flags: option.flags,
57
+ description: option.description,
58
+ required: option.mandatory,
59
+ })),
60
+ spends_credits: flagStrings.some((flags) => flags.includes("--approved") || flags.includes("--max-credits")),
61
+ mutates: MUTATING_VERBS.has(leafVerb) || MUTATING_VERBS.has(verbPrefix),
62
+ json_supported: flagStrings.some((flags) => flags.includes("--json")),
63
+ hidden: isHiddenCommand(command),
64
+ };
65
+ }
66
+ function isHiddenCommand(command) {
67
+ // Commander keeps hidden state private; read it defensively so a library
68
+ // bump degrades to hidden:false instead of a crash.
69
+ return Boolean(command._hidden);
70
+ }
package/dist/help.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ import type { Command } from "commander";
2
+ export declare function applyOxygenHelp(program: Command, binaryName: string): void;
package/dist/help.js ADDED
@@ -0,0 +1,79 @@
1
+ import { CLI_EXIT_CODE_TABLE } from "@oxygen/shared";
2
+ // Grouped `--help` for the ~70 top-level command groups: commander's default
3
+ // registration-order wall (login, auth, profiles, ... before any GTM noun) is
4
+ // the first thing every fresh agent reads, so order it by the OXYGEN OS layer
5
+ // a command belongs to and close with an agent quickstart + the reserved
6
+ // exit-code table. Commands missing from the map land in the last group so a
7
+ // new command never disappears from help.
8
+ const HELP_GROUPS = [
9
+ {
10
+ heading: "Get started & account:",
11
+ commands: [
12
+ "login", "auth", "profiles", "logout", "update", "api-keys", "whoami",
13
+ "onboarding", "status", "orgs", "commands", "skills", "session",
14
+ "support", "feedback",
15
+ ],
16
+ },
17
+ {
18
+ heading: "Knowledge (context, wiki, recipes):",
19
+ commands: ["context", "knowledge", "prompts", "templates", "blueprints"],
20
+ },
21
+ {
22
+ heading: "Data (tables, CRM, dashboards):",
23
+ commands: [
24
+ "tables", "rows", "cells", "columns", "action-column", "enrich-column",
25
+ "enrichment", "table-runs", "table-ingestions", "runs", "crm",
26
+ "dashboards", "projects", "signup-leads",
27
+ ],
28
+ },
29
+ {
30
+ heading: "Sourcing & search:",
31
+ commands: ["sourcing", "lead-sourcing", "search", "companies", "people", "find", "tools"],
32
+ },
33
+ {
34
+ heading: "Outbound & channels:",
35
+ commands: [
36
+ "sequences", "senders", "inbox", "messages", "mailboxes",
37
+ "managed-inboxes", "deliverability", "domains", "email", "schedules",
38
+ "suppressions", "egress", "linkedin", "connections", "followers",
39
+ "viewers", "posts", "engagement", "whatsapp", "publishing", "notetaker",
40
+ "reviews",
41
+ ],
42
+ },
43
+ {
44
+ heading: "Automation, billing & admin:",
45
+ commands: [
46
+ "workflows", "integrations", "custom-integrations", "billing", "budget",
47
+ "observability", "worker", "db", "admin", "directory",
48
+ ],
49
+ },
50
+ ];
51
+ const FALLBACK_HEADING = "Other commands:";
52
+ export function applyOxygenHelp(program, binaryName) {
53
+ const headingByCommand = new Map();
54
+ for (const group of HELP_GROUPS) {
55
+ for (const name of group.commands)
56
+ headingByCommand.set(name, group.heading);
57
+ }
58
+ for (const command of program.commands) {
59
+ command.helpGroup(headingByCommand.get(command.name()) ?? FALLBACK_HEADING);
60
+ }
61
+ const exitCodeLine = Object.entries(CLI_EXIT_CODE_TABLE)
62
+ .map(([code, meaning]) => `${code} ${meaning.split(" (")[0]}`)
63
+ .join(" · ");
64
+ program.addHelpText("afterAll", [
65
+ "",
66
+ "Agent quickstart:",
67
+ ` 1. ${binaryName} login connect this terminal (or set OXYGEN_API_KEY)`,
68
+ ` 2. ${binaryName} onboarding start --json load workspace context + the skill that teaches the GTM loops`,
69
+ ` 3. ${binaryName} commands --json full machine-readable command grammar (this CLI's self-index)`,
70
+ "",
71
+ "Conventions:",
72
+ " --json prints a stable envelope {ok, data|error, meta}; errors always print it.",
73
+ " Commands that spend credits or write to external platforms are gated: live runs",
74
+ " require --approved --max-credits <n> and fail with a typed error until approved.",
75
+ " State-changing responses include a web_url deep-link to inspect the result.",
76
+ "",
77
+ `Exit codes: ${exitCodeLine}`,
78
+ ].join("\n"));
79
+ }
@@ -10,6 +10,7 @@ type RequestOptions = {
10
10
  traceId?: string;
11
11
  fetch?: typeof fetch;
12
12
  selectedOrganization?: string;
13
+ idempotencyKey?: string;
13
14
  };
14
15
  export declare function requestOxygen<T>(// skipcq: JS-R1005
15
16
  path: string, options?: RequestOptions): Promise<T>;
@@ -42,6 +42,17 @@ path, options = {}) {
42
42
  if (options.body) {
43
43
  headers["Content-Type"] = "application/json";
44
44
  }
45
+ // Every mutating request carries an idempotency key. Routes that support
46
+ // replay dedupe on it; others ignore the header. On a timeout the key is
47
+ // echoed in error details so a re-run (OXYGEN_IDEMPOTENCY_KEY=<key>) can
48
+ // retry without duplicating the write once the route supports replay.
49
+ const method = options.method ?? "GET";
50
+ const idempotencyKey = method === "GET"
51
+ ? undefined
52
+ : options.idempotencyKey ?? process.env.OXYGEN_IDEMPOTENCY_KEY ?? randomUUID();
53
+ if (idempotencyKey) {
54
+ headers["Idempotency-Key"] = idempotencyKey;
55
+ }
45
56
  let response;
46
57
  const timeoutMs = resolveRequestTimeoutMs(options.timeoutMs);
47
58
  const timeout = createTimeoutSignal(timeoutMs);
@@ -64,6 +75,7 @@ path, options = {}) {
64
75
  path,
65
76
  timeout_ms: timeoutMs,
66
77
  trace_id: traceId,
78
+ ...(idempotencyKey ? { idempotency_key: idempotencyKey } : {}),
67
79
  },
68
80
  exitCode: 1,
69
81
  });
@@ -72,6 +84,7 @@ path, options = {}) {
72
84
  details: {
73
85
  api_url: apiUrl,
74
86
  reason: error instanceof Error ? error.message : "unknown",
87
+ ...(idempotencyKey ? { idempotency_key: idempotencyKey } : {}),
75
88
  },
76
89
  exitCode: 1,
77
90
  });
package/dist/index.js CHANGED
@@ -7,14 +7,16 @@ import { createInterface } from "node:readline/promises";
7
7
  import { stdin as input, stdout as output } from "node:process";
8
8
  import { fileURLToPath, pathToFileURL } from "node:url";
9
9
  import { Command, Option } from "commander";
10
- import { AGENCY_DIRECTORY_REGIONS, AGENCY_DIRECTORY_SERVICES, formatCellForDisplay, formatPublicBudgetScopes, OXYGEN_VERSION, OxygenError, success, toFailure, } from "@oxygen/shared";
10
+ import { applyOxygenHelp } from "./help.js";
11
+ import { buildCommandManifest } from "./command-manifest.js";
12
+ import { AGENCY_DIRECTORY_REGIONS, AGENCY_DIRECTORY_SERVICES, formatCellForDisplay, formatPublicBudgetScopes, exitCodeForOxygenError, isVersionGreater, isVersionLess, OXYGEN_VERSION, OxygenError, parseKnowledgePageMarkdown, success, toFailure, } from "@oxygen/shared";
11
13
  import { inferImportColumnLabels, inferRowsFileFormat, normalizeImportColumnKey, normalizeRowsForNewTable, normalizeRowsFormat, parseRowsFileBuffer, } from "@oxygen/shared/file-import";
12
14
  import { assertRecipeBundleSafe, assertWorkflowManifest, buildRecipeManifest, compileWorkflowDefinition, isAnyWorkflowManifest, isRecipeManifest, isWorkflowDefinition, isWorkflowManifest, } from "@oxygen/workflows";
13
15
  import { isRecipeDefinition } from "@oxygen/recipe-sdk";
14
16
  import { createBrowserLoginSession, openBrowser } from "./browser-login.js";
15
17
  import { clearCredentials, defaultApiUrl, listCredentialProfiles, loadCredentials, normalizeApiUrl, pickProfileNameForIdentity, pickProfileNameForUserSession, resolveActiveProfile, saveCredentials, switchCredentialProfile, updateActiveOrganizationForProfile, } from "./credentials.js";
16
18
  import { ensureFreshCliForApiUrl, requestOxygen } from "./http-client.js";
17
- import { acquireMirrorLock, clearConflictFiles, deletePageFile, emptyMirrorState, findMirrorSlugByPageId, isFileDirty, listConflictFiles, markMirrorStale, mirrorExists, purgeMirror, resetMirrorForFullResync, quarantineDirtyFile, readMirrorState, releaseMirrorLock, resolveDefaultConfigDir, resolveMirrorDir, writeGeneratedIndexFile, writeGeneratedLogFile, writeMirrorState, writePageFile, } from "./knowledge-mirror.js";
19
+ import { acquireMirrorLock, clearConflictFiles, deletePageFile, emptyMirrorState, findMirrorSlugByPageId, isFileDirty, listConflictFiles, localPageSha256, markMirrorStale, mirrorExists, pageFilePath, planMirrorPush, purgeMirror, resetMirrorForFullResync, quarantineDirtyFile, readMirrorState, releaseMirrorLock, resolveDefaultConfigDir, resolveMirrorDir, writeGeneratedIndexFile, writeGeneratedLogFile, writeMirrorState, writePageFile, } from "./knowledge-mirror.js";
18
20
  import { waitForCliRun } from "./run-wait.js";
19
21
  import { runLocalCustomHttpColumn } from "./local-custom-http-column.js";
20
22
  import { captureCurrentTranscript, collectFeedbackEnvironment, TranscriptCaptureError, } from "./transcript.js";
@@ -119,12 +121,35 @@ async function handleAsyncAction(command, options, action) {
119
121
  try {
120
122
  const data = await action();
121
123
  emitSuccess(command, data, options);
124
+ writeCreditsReceipt(data);
122
125
  }
123
126
  catch (error) {
124
127
  const failure = toFailure(command, error);
125
128
  writeJson(failure);
126
129
  writeMaxCreditsHint(error);
127
- process.exitCode = error instanceof OxygenError ? error.exitCode : 1;
130
+ process.exitCode = error instanceof OxygenError ? exitCodeForOxygenError(error) : 1;
131
+ }
132
+ }
133
+ // Paid envelopes (push 3 legibility) carry a `credits` block: quote on
134
+ // dry_run, receipt on live, remaining balance on both. Mirror it as one
135
+ // stderr line so spend stays visible in a terminal without polluting the
136
+ // machine-read stdout envelope.
137
+ function writeCreditsReceipt(data) {
138
+ if (!data || typeof data !== "object" || Array.isArray(data))
139
+ return;
140
+ const credits = data.credits;
141
+ if (!credits || typeof credits !== "object" || Array.isArray(credits))
142
+ return;
143
+ const block = credits;
144
+ const remaining = typeof block.credits_remaining === "number"
145
+ ? block.credits_remaining.toLocaleString("en-US")
146
+ : null;
147
+ if (typeof block.credits_used === "number") {
148
+ process.stderr.write(`spent ${block.credits_used.toLocaleString("en-US")} credits${remaining !== null ? `, ${remaining} remaining` : ""}\n`);
149
+ return;
150
+ }
151
+ if (typeof block.estimated_credits === "number") {
152
+ process.stderr.write(`estimated ${block.estimated_credits.toLocaleString("en-US")} credits for a live run${remaining !== null ? `, ${remaining} available` : ""}\n`);
128
153
  }
129
154
  }
130
155
  // Paid live runs are refused server-side with typed spend-gate errors
@@ -1069,7 +1094,7 @@ export function createProgram() {
1069
1094
  const binaryName = resolveCliBinaryName();
1070
1095
  program
1071
1096
  .name(binaryName)
1072
- .description("CLI/API-first GTM platform for GTM tool and workflow primitives.")
1097
+ .description("Revenue infrastructure for B2B startups — agent-operated GTM: tables, enrichment, sequences, workflows, CRM, knowledge. MCP + CLI first; every state-changing action returns a web_url deep-link.")
1073
1098
  .version(OXYGEN_VERSION)
1074
1099
  .option("--profile <name>", "Use a stored CLI profile for this command.")
1075
1100
  .option("--org <organization>", "Use an organization id, Clerk org id, or slug for this command.");
@@ -1201,9 +1226,17 @@ export function createProgram() {
1201
1226
  .command("whoami")
1202
1227
  .description("Show the current Oxygen CLI identity.")
1203
1228
  .option("--json", "Print a JSON envelope.")
1229
+ .option("--summary-json", "Print a compact identity envelope with no credential-shaped fields (safe for health probes and logs).")
1204
1230
  .action(async (options) => {
1205
1231
  await handleWhoamiAction(options);
1206
1232
  });
1233
+ program
1234
+ .command("commands")
1235
+ .description("Print the machine-readable command manifest: every command with flags, spend/mutation markers, and the reserved exit-code table.")
1236
+ .option("--json", "Print a JSON envelope.")
1237
+ .action(async (options) => {
1238
+ await handleAsyncAction("commands", options, async () => buildCommandManifest(program, binaryName));
1239
+ });
1207
1240
  program
1208
1241
  .command("onboarding")
1209
1242
  .description("Onboarding helpers.")
@@ -1239,14 +1272,26 @@ export function createProgram() {
1239
1272
  requireAuth: false,
1240
1273
  enforceMinimumCliVersion: false,
1241
1274
  });
1275
+ // Explicit skew semantics: in_sync=false alone forced every ops sweep
1276
+ // to hand-interpret whether a version gap was safe (fleet friction,
1277
+ // recurring since 2026-06-18). `skew` names the direction and
1278
+ // `compatible` answers the only question that matters: is this CLI at
1279
+ // or above the server's enforced minimum?
1280
+ const minimum = server.minimum_cli_version ?? null;
1242
1281
  return {
1243
1282
  client_version: OXYGEN_VERSION,
1244
1283
  api_url: apiUrl,
1245
1284
  server_version: server.server_version,
1246
- minimum_cli_version: server.minimum_cli_version ?? null,
1285
+ minimum_cli_version: minimum,
1247
1286
  sha: server.sha,
1248
1287
  region: server.region,
1249
1288
  in_sync: server.server_version === OXYGEN_VERSION,
1289
+ skew: isVersionGreater(OXYGEN_VERSION, server.server_version)
1290
+ ? "client_ahead"
1291
+ : isVersionLess(OXYGEN_VERSION, server.server_version)
1292
+ ? "client_behind"
1293
+ : "in_sync",
1294
+ compatible: minimum === null || !isVersionLess(OXYGEN_VERSION, minimum),
1250
1295
  };
1251
1296
  });
1252
1297
  });
@@ -3160,6 +3205,28 @@ export function createProgram() {
3160
3205
  method: "POST",
3161
3206
  body: {},
3162
3207
  }));
3208
+ }))
3209
+ .addCommand(new Command("synthesize")
3210
+ .description("Synthesize knowledge from workspace evidence (one low-cost AI call): campaign learnings from auto-filed outreach outcomes, or a voice guide from sent messages. Previews by default; --approved writes the result.")
3211
+ .option("--kind <kind>", "What to synthesize: campaign_learnings (default) or voice.", "campaign_learnings")
3212
+ .option("--sequence <sequence_id>", "Scope campaign learnings to one sequence's learnings page. Omit for the org-wide [[outreach-learnings]] page.")
3213
+ .option("--approved", "Write the synthesized result (learnings page revision / pinned voice guide) instead of previewing.")
3214
+ .option("--body <markdown>", "With --approved: save this edited body verbatim instead of re-synthesizing.")
3215
+ .option("--title <title>", "Optional title for a newly created page/asset.")
3216
+ .option("--limit <n>", "Voice only: how many recent sent messages to learn from (max 200).")
3217
+ .option("--json", "Print a JSON envelope.")
3218
+ .action(async (options) => {
3219
+ await handleAsyncAction("knowledge synthesize", options, () => requestOxygen("/api/cli/knowledge/synthesize", {
3220
+ method: "POST",
3221
+ body: {
3222
+ kind: options.kind ?? "campaign_learnings",
3223
+ ...(readOption(options.sequence) ? { sequence: readOption(options.sequence) } : {}),
3224
+ ...(options.approved ? { approved: true } : {}),
3225
+ ...(readOption(options.body) ? { body: readOption(options.body) } : {}),
3226
+ ...(readOption(options.title) ? { title: readOption(options.title) } : {}),
3227
+ ...(readPositiveInt(options.limit) !== undefined ? { limit: readPositiveInt(options.limit) } : {}),
3228
+ },
3229
+ }));
3163
3230
  }))
3164
3231
  .addCommand(new Command("sync")
3165
3232
  .description("Clone or refresh the local knowledge mirror: server-rendered page markdown under the CLI config dir.")
@@ -3169,6 +3236,13 @@ export function createProgram() {
3169
3236
  .option("--json", "Print a JSON envelope.")
3170
3237
  .action(async (options) => {
3171
3238
  await handleAsyncAction("knowledge sync", options, () => runKnowledgeMirrorSync(options));
3239
+ }))
3240
+ .addCommand(new Command("push")
3241
+ .description("Push locally edited or newly created mirror pages back to the workspace wiki. Edits to canonical voice/brand/positioning pages become approval proposals instead of writing live.")
3242
+ .option("--dry-run", "List what would be pushed without writing anything.")
3243
+ .option("--json", "Print a JSON envelope.")
3244
+ .action(async (options) => {
3245
+ await handleAsyncAction("knowledge push", options, () => runKnowledgeMirrorPush(options));
3172
3246
  }))
3173
3247
  .addCommand(new Command("status")
3174
3248
  .description("Report the local knowledge mirror: path, page count, last sync, quarantined conflicts.")
@@ -7282,6 +7356,95 @@ export function createProgram() {
7282
7356
  });
7283
7357
  });
7284
7358
  })));
7359
+ program.addCommand(new Command("managed-inboxes")
7360
+ .description("Managed whitelabel sending inboxes on the OXYGEN-managed Cold Mail Reseller (CMR) account: subscribe a domain + N mailboxes (google/microsoft) as a recurring MONTHLY subscription billed to Oxygen credits, list/get your subscriptions, and cancel. Subscribe/cancel are approval-gated (preview → re-run with --approved --quote).")
7361
+ .addCommand(new Command("subscribe")
7362
+ .description("Subscribe a domain + mailboxes as a managed monthly CMR inbox subscription. WITHOUT --approved this prints a priced PREVIEW with a quote_id (nothing ordered or charged); re-run with --approved --quote <id> to place the order. Fails closed until the managed CMR wallet (CMR_RESELLER_API_KEY) + founder-signed pricing (CMR_MANAGED_INBOX_CREDITS_PER_MAILBOX) are configured.")
7363
+ .requiredOption("--domain <domain>", "Sending domain to register + host the mailboxes (e.g. send.acme.com).")
7364
+ .requiredOption("--provider <provider>", "Mailbox provider: google or microsoft.")
7365
+ .option("--mailboxes <json>", "JSON array of mailboxes: [{\"username\",\"first_name\",\"last_name\",\"profile_picture?\"}].")
7366
+ .option("--file <path>", "Path to a JSON file { \"mailboxes\": [...] } (alternative to --mailboxes).")
7367
+ .option("--years <n>", "Years to register the domain for (positive whole number). Defaults to 1.")
7368
+ .option("--billing <path>", "Path to a JSON file with the CMR billing address (required on the FIRST subscribe for an org that has no CMR account yet).")
7369
+ .option("--zapshield", "Add the Zapshield deliverability-protection add-on to the domain (a one-time per-domain add-on billed once its price is founder-signed; recorded but not charged until then).")
7370
+ .option("--approved", "Place the order (requires --quote). Without it, a priced preview is returned.")
7371
+ .option("--quote <id>", "The quote_id from a fresh preview. Required with --approved.")
7372
+ .option("--json", "Print a JSON envelope.")
7373
+ .action(async (options) => {
7374
+ await handleAsyncAction("managed-inboxes subscribe", options, () => {
7375
+ const domain = readOption(options.domain);
7376
+ const provider = readOption(options.provider);
7377
+ if (!domain)
7378
+ throw new Error("--domain is required.");
7379
+ if (!provider)
7380
+ throw new Error("--provider is required (google or microsoft).");
7381
+ let mailboxes;
7382
+ const mailboxesJson = readOption(options.mailboxes);
7383
+ const filePath = readOption(options.file);
7384
+ if (mailboxesJson) {
7385
+ mailboxes = JSON.parse(mailboxesJson);
7386
+ }
7387
+ else if (filePath) {
7388
+ const parsed = readJsonFileValue(resolve(filePath), "--file");
7389
+ mailboxes = parsed.mailboxes ?? [];
7390
+ }
7391
+ else {
7392
+ throw new Error("Provide --mailboxes <json> or --file <path>.");
7393
+ }
7394
+ const years = readOption(options.years);
7395
+ const billingPath = readOption(options.billing);
7396
+ const billing = billingPath ? readJsonFileValue(resolve(billingPath), "--billing") : undefined;
7397
+ const quote = readOption(options.quote);
7398
+ return requestOxygen("/api/cli/managed-inboxes/subscribe", {
7399
+ method: "POST",
7400
+ body: {
7401
+ domain,
7402
+ provider,
7403
+ mailboxes,
7404
+ ...(years ? { years: Number(years) } : {}),
7405
+ ...(billing ? { billing } : {}),
7406
+ ...(options.zapshield ? { zapshield: true } : {}),
7407
+ ...(options.approved ? { approved: true } : {}),
7408
+ ...(quote ? { quote_id: quote } : {}),
7409
+ },
7410
+ });
7411
+ });
7412
+ }))
7413
+ .addCommand(new Command("list")
7414
+ .description("List the org's managed inbox subscriptions with provider, mailbox count, monthly price, CMR lifecycle status, and internal billing posture. Read-only, 0 Oxygen credits.")
7415
+ .option("--status <status>", "Filter by status: pending, active, renewing, past_due, expired, cancelled, or failed.")
7416
+ .option("--json", "Print a JSON envelope.")
7417
+ .action(async (options) => {
7418
+ await handleAsyncAction("managed-inboxes list", options, () => {
7419
+ const params = new URLSearchParams();
7420
+ const status = readOption(options.status);
7421
+ if (status)
7422
+ params.set("status", status);
7423
+ const suffix = params.toString();
7424
+ return requestOxygen(`/api/cli/managed-inboxes${suffix ? `?${suffix}` : ""}`);
7425
+ });
7426
+ }))
7427
+ .addCommand(new Command("get")
7428
+ .description("Get one managed inbox subscription by domain (provider, mailbox count, monthly price, CMR subscription id, lifecycle status, auto-renew, period end, internal billing status). Read-only, 0 Oxygen credits.")
7429
+ .argument("<domain>", "The managed inbox domain (e.g. send.acme.com).")
7430
+ .option("--json", "Print a JSON envelope.")
7431
+ .action(async (domain, options) => {
7432
+ await handleAsyncAction("managed-inboxes get", options, () => requestOxygen(`/api/cli/managed-inboxes/${encodeURIComponent(domain)}`));
7433
+ }))
7434
+ .addCommand(new Command("cancel")
7435
+ .description("Cancel a managed inbox subscription by domain (stops CMR's recurring monthly charge and frees the domain at period end). WITHOUT --approved prints a preview; re-run with --approved to cancel. Cancelling is free — 0 Oxygen credits.")
7436
+ .argument("<domain>", "The managed inbox domain to cancel.")
7437
+ .option("--approved", "Actually cancel (otherwise a preview is returned).")
7438
+ .option("--json", "Print a JSON envelope.")
7439
+ .action(async (domain, options) => {
7440
+ await handleAsyncAction("managed-inboxes cancel", options, () => requestOxygen("/api/cli/managed-inboxes/cancel", {
7441
+ method: "POST",
7442
+ body: {
7443
+ domain,
7444
+ ...(options.approved ? { approved: true } : {}),
7445
+ },
7446
+ }));
7447
+ })));
7285
7448
  program.addCommand(new Command("mailboxes")
7286
7449
  .description("Native email sending pool: register/refresh Google/Microsoft mailboxes a campaign rotates over, pause/disable inboxes, and delegate warmup to Instantly (BYOK — Instantly bills your account, 0 Oxygen credits).")
7287
7450
  .addCommand(new Command("list")
@@ -7528,6 +7691,28 @@ export function createProgram() {
7528
7691
  const suffix = params.toString();
7529
7692
  return requestOxygen(`/api/cli/mailboxes/warmup/status${suffix ? `?${suffix}` : ""}`);
7530
7693
  });
7694
+ }))
7695
+ .addCommand(new Command("provision")
7696
+ .description("Export sending mailboxes into the Instantly workspace via Zapmail (the prerequisite for warmup), then let the worker enable warmup once the export completes. Targets the whole pool unless --mailboxes is given. Uses the org's Zapmail connection (BYOK) or the OXYGEN-managed wallet.")
7697
+ .option("--mailboxes <list>", "Comma-separated mailbox ids or addresses to provision. Omit for the whole pool.")
7698
+ .option("--connection <id>", "Zapmail connection id. Defaults to the org's active Zapmail connection (or the managed wallet).")
7699
+ .option("--force", "Re-export mailboxes already exporting/warming/active (default skips them).")
7700
+ .option("--dry-run", "Simulate without calling Zapmail (reports the mailboxes that would be exported).")
7701
+ .option("--json", "Print a JSON envelope.")
7702
+ .action(async (options) => {
7703
+ await handleAsyncAction("mailboxes warmup provision", options, () => {
7704
+ const mailboxes = readCsvOption(options.mailboxes);
7705
+ const connection = readOption(options.connection);
7706
+ return requestOxygen("/api/cli/mailboxes/warmup/provision", {
7707
+ method: "POST",
7708
+ body: {
7709
+ ...(mailboxes.length > 0 ? { mailboxes } : {}),
7710
+ ...(connection ? { connection_id: connection } : {}),
7711
+ ...(options.force ? { force: true } : {}),
7712
+ ...(options.dryRun ? { dry_run: true } : {}),
7713
+ },
7714
+ });
7715
+ });
7531
7716
  })))
7532
7717
  .addCommand(new Command("order")
7533
7718
  .description("Order NEW sending mailboxes (domain + inboxes + warmup) on the OXYGEN-managed Zapmail wallet, billed to Oxygen credits. Without --approved returns a priced preview + quote_id. FAILS CLOSED (409) until managed ordering is enabled.")
@@ -8195,6 +8380,7 @@ export function createProgram() {
8195
8380
  .action(async (options) => {
8196
8381
  await handleAsyncAction("skills install", options, () => installAgentSkills(options));
8197
8382
  }));
8383
+ applyOxygenHelp(program, binaryName);
8198
8384
  return program;
8199
8385
  }
8200
8386
  /**
@@ -10772,6 +10958,27 @@ async function handleWhoamiAction(options) {
10772
10958
  enforceMinimumCliVersion: false,
10773
10959
  });
10774
10960
  const context = await resolveActiveProfileWithSource();
10961
+ // Compact, credential-free identity shape for health probes and ops
10962
+ // sweeps: the full envelope carries apiKey token prefix/suffix metadata,
10963
+ // which forced every automated consumer to sanitize before logging
10964
+ // (recurring fleet friction since 2026-06-18).
10965
+ if (options.summaryJson) {
10966
+ writeJson(success("whoami", {
10967
+ user: { id: identity.user.id, email: identity.user.email },
10968
+ organization: {
10969
+ id: identity.organization.id,
10970
+ name: identity.organization.name,
10971
+ slug: identity.organization.slug,
10972
+ },
10973
+ auth_type: identity.authType ?? identity.apiKey?.kind ?? null,
10974
+ onboarding_complete: identity.onboarding?.complete ?? null,
10975
+ profile: context.resolution.exists ? context.resolution.name : null,
10976
+ profile_source: context.source,
10977
+ api_url: context.resolution.credentials?.apiUrl ?? null,
10978
+ cli_version: OXYGEN_VERSION,
10979
+ }));
10980
+ return;
10981
+ }
10775
10982
  if (context.resolution.exists) {
10776
10983
  const existingCredentials = context.resolution.credentials;
10777
10984
  const cached = existingCredentials?.identity;
@@ -10807,7 +11014,7 @@ async function handleWhoamiAction(options) {
10807
11014
  catch (error) {
10808
11015
  const failure = toFailure("whoami", error);
10809
11016
  writeJson(failure);
10810
- process.exitCode = error instanceof OxygenError ? error.exitCode : 1;
11017
+ process.exitCode = error instanceof OxygenError ? exitCodeForOxygenError(error) : 1;
10811
11018
  }
10812
11019
  }
10813
11020
  async function handleOnboardingStartAction(options) {
@@ -10822,7 +11029,7 @@ async function handleOnboardingStartAction(options) {
10822
11029
  catch (error) {
10823
11030
  const failure = toFailure("onboarding start", error);
10824
11031
  writeJson(failure);
10825
- process.exitCode = error instanceof OxygenError ? error.exitCode : 1;
11032
+ process.exitCode = error instanceof OxygenError ? exitCodeForOxygenError(error) : 1;
10826
11033
  }
10827
11034
  }
10828
11035
  async function handleOnboardingResetAction(options) {
@@ -10846,7 +11053,7 @@ async function handleOnboardingResetAction(options) {
10846
11053
  catch (error) {
10847
11054
  const failure = toFailure("onboarding reset", error);
10848
11055
  writeJson(failure);
10849
- process.exitCode = error instanceof OxygenError ? error.exitCode : 1;
11056
+ process.exitCode = error instanceof OxygenError ? exitCodeForOxygenError(error) : 1;
10850
11057
  }
10851
11058
  }
10852
11059
  function formatOnboardingReset(data) {
@@ -10904,7 +11111,7 @@ async function handleLoginAction(options) {
10904
11111
  catch (error) {
10905
11112
  const failure = toFailure("login", error);
10906
11113
  writeJson(failure);
10907
- process.exitCode = error instanceof OxygenError ? error.exitCode : 1;
11114
+ process.exitCode = error instanceof OxygenError ? exitCodeForOxygenError(error) : 1;
10908
11115
  }
10909
11116
  }
10910
11117
  async function handleAuthUseTokenAction(options) {
@@ -10934,7 +11141,7 @@ async function handleAuthUseTokenAction(options) {
10934
11141
  writeJson(failure);
10935
11142
  else
10936
11143
  process.stderr.write(`${failure.error.message}\n`);
10937
- process.exitCode = error instanceof OxygenError ? error.exitCode : 1;
11144
+ process.exitCode = error instanceof OxygenError ? exitCodeForOxygenError(error) : 1;
10938
11145
  }
10939
11146
  }
10940
11147
  async function handleAuthDoctorAction(options) {
@@ -10949,7 +11156,7 @@ async function handleAuthDoctorAction(options) {
10949
11156
  catch (error) {
10950
11157
  const failure = toFailure("auth doctor", error);
10951
11158
  writeJson(failure);
10952
- process.exitCode = error instanceof OxygenError ? error.exitCode : 1;
11159
+ process.exitCode = error instanceof OxygenError ? exitCodeForOxygenError(error) : 1;
10953
11160
  }
10954
11161
  }
10955
11162
  async function runAuthDoctor() {
@@ -11040,7 +11247,7 @@ async function handleOrgUseAction(organization, options, command) {
11040
11247
  catch (error) {
11041
11248
  const failure = toFailure(command, error);
11042
11249
  writeJson(failure);
11043
- process.exitCode = error instanceof OxygenError ? error.exitCode : 1;
11250
+ process.exitCode = error instanceof OxygenError ? exitCodeForOxygenError(error) : 1;
11044
11251
  }
11045
11252
  }
11046
11253
  async function handleProfilesListAction(options) {
@@ -11059,7 +11266,7 @@ async function handleProfilesListAction(options) {
11059
11266
  catch (error) {
11060
11267
  const failure = toFailure("profiles list", error);
11061
11268
  writeJson(failure);
11062
- process.exitCode = error instanceof OxygenError ? error.exitCode : 1;
11269
+ process.exitCode = error instanceof OxygenError ? exitCodeForOxygenError(error) : 1;
11063
11270
  }
11064
11271
  }
11065
11272
  async function handleProfilesUseAction(profile, options) {
@@ -11080,7 +11287,7 @@ async function handleProfilesUseAction(profile, options) {
11080
11287
  catch (error) {
11081
11288
  const failure = toFailure("profiles use", error);
11082
11289
  writeJson(failure);
11083
- process.exitCode = error instanceof OxygenError ? error.exitCode : 1;
11290
+ process.exitCode = error instanceof OxygenError ? exitCodeForOxygenError(error) : 1;
11084
11291
  }
11085
11292
  }
11086
11293
  async function handleProfilesEnvAction(profile, options) {
@@ -11119,7 +11326,7 @@ async function handleProfilesEnvAction(profile, options) {
11119
11326
  catch (error) {
11120
11327
  const failure = toFailure("profiles env", error);
11121
11328
  writeJson(failure);
11122
- process.exitCode = error instanceof OxygenError ? error.exitCode : 1;
11329
+ process.exitCode = error instanceof OxygenError ? exitCodeForOxygenError(error) : 1;
11123
11330
  }
11124
11331
  }
11125
11332
  async function handleProfilesCurrentAction(options) {
@@ -11143,7 +11350,7 @@ async function handleProfilesCurrentAction(options) {
11143
11350
  catch (error) {
11144
11351
  const failure = toFailure("profiles current", error);
11145
11352
  writeJson(failure);
11146
- process.exitCode = error instanceof OxygenError ? error.exitCode : 1;
11353
+ process.exitCode = error instanceof OxygenError ? exitCodeForOxygenError(error) : 1;
11147
11354
  }
11148
11355
  }
11149
11356
  function shellQuote(value) {
@@ -11172,7 +11379,7 @@ async function handleLogoutAction(options) {
11172
11379
  catch (error) {
11173
11380
  const failure = toFailure("logout", error);
11174
11381
  writeJson(failure);
11175
- process.exitCode = error instanceof OxygenError ? error.exitCode : 1;
11382
+ process.exitCode = error instanceof OxygenError ? exitCodeForOxygenError(error) : 1;
11176
11383
  }
11177
11384
  }
11178
11385
  async function handleUpdateAction(options) {
@@ -11187,7 +11394,7 @@ async function handleUpdateAction(options) {
11187
11394
  catch (error) {
11188
11395
  const failure = toFailure("update", error);
11189
11396
  writeJson(failure);
11190
- process.exitCode = error instanceof OxygenError ? error.exitCode : 1;
11397
+ process.exitCode = error instanceof OxygenError ? exitCodeForOxygenError(error) : 1;
11191
11398
  }
11192
11399
  }
11193
11400
  function buildApiKeyCreateBody(options) {
@@ -11798,7 +12005,7 @@ async function handleSequenceSignalAction(sequence, options) {
11798
12005
  const failure = toFailure("sequences signal", error);
11799
12006
  writeJson(failure);
11800
12007
  writeMaxCreditsHint(error);
11801
- process.exitCode = error instanceof OxygenError ? error.exitCode : 1;
12008
+ process.exitCode = error instanceof OxygenError ? exitCodeForOxygenError(error) : 1;
11802
12009
  }
11803
12010
  }
11804
12011
  async function handleSequenceVariantsAction(sequence, options) {
@@ -11835,7 +12042,7 @@ async function handleSequenceVariantsAction(sequence, options) {
11835
12042
  const failure = toFailure("sequences variants", error);
11836
12043
  writeJson(failure);
11837
12044
  writeMaxCreditsHint(error);
11838
- process.exitCode = error instanceof OxygenError ? error.exitCode : 1;
12045
+ process.exitCode = error instanceof OxygenError ? exitCodeForOxygenError(error) : 1;
11839
12046
  }
11840
12047
  }
11841
12048
  function formatVariantCell(value) {
@@ -11966,7 +12173,7 @@ async function handleTablesTidySuggestAction(table, options) {
11966
12173
  const failure = toFailure("tables tidy-suggest", error);
11967
12174
  writeJson(failure);
11968
12175
  writeMaxCreditsHint(error);
11969
- process.exitCode = error instanceof OxygenError ? error.exitCode : 1;
12176
+ process.exitCode = error instanceof OxygenError ? exitCodeForOxygenError(error) : 1;
11970
12177
  }
11971
12178
  }
11972
12179
  function formatTidyCount(value) {
@@ -12349,7 +12556,7 @@ async function handleSupportAdminWorkflowAction(ticketId, options) {
12349
12556
  catch (error) {
12350
12557
  const failure = toFailure("support admin workflow", error);
12351
12558
  writeJson(failure);
12352
- process.exitCode = error instanceof OxygenError ? error.exitCode : 1;
12559
+ process.exitCode = error instanceof OxygenError ? exitCodeForOxygenError(error) : 1;
12353
12560
  }
12354
12561
  }
12355
12562
  async function handleSupportAdminUpdateAction(ticketId, options) {
@@ -12365,7 +12572,7 @@ async function handleSupportAdminUpdateAction(ticketId, options) {
12365
12572
  catch (error) {
12366
12573
  const failure = toFailure("support admin update", error);
12367
12574
  writeJson(failure);
12368
- process.exitCode = error instanceof OxygenError ? error.exitCode : 1;
12575
+ process.exitCode = error instanceof OxygenError ? exitCodeForOxygenError(error) : 1;
12369
12576
  }
12370
12577
  }
12371
12578
  function formatSupportAdminWorkflowStep(styles, label, status, artifactLabel, artifact) {
@@ -12916,6 +13123,152 @@ async function runKnowledgeMirrorSync(options) {
12916
13123
  releaseMirrorLock(target.dir);
12917
13124
  }
12918
13125
  }
13126
+ // One page's push: parse the local file, guard identity, upsert with the manifest
13127
+ // revision as the optimistic-concurrency base, and translate the two expected
13128
+ // gate outcomes (approval proposal, revision conflict) into statuses.
13129
+ async function pushOneMirrorPage(dir, state, candidate) {
13130
+ const { slug, kind, record } = candidate;
13131
+ let contents;
13132
+ try {
13133
+ contents = readFileSync(pageFilePath(dir, slug), "utf8");
13134
+ }
13135
+ catch {
13136
+ return { slug, kind, status: "skipped", reason: "file_unreadable" };
13137
+ }
13138
+ const parsed = parseKnowledgePageMarkdown(contents);
13139
+ if (record && parsed.id && parsed.id !== record.id) {
13140
+ return { slug, kind, status: "skipped", reason: "id_mismatch_with_manifest" };
13141
+ }
13142
+ if (parsed.slug && parsed.slug !== slug) {
13143
+ return { slug, kind, status: "skipped", reason: "frontmatter_slug_differs_from_filename" };
13144
+ }
13145
+ const body = {
13146
+ slug,
13147
+ body: parsed.body,
13148
+ ...(parsed.title ? { title: parsed.title } : {}),
13149
+ ...(parsed.type ? { type: parsed.type } : {}),
13150
+ ...(parsed.status ? { status: parsed.status } : {}),
13151
+ ...(parsed.tags ? { tags: parsed.tags } : {}),
13152
+ // A frontmatter block owns the summary line: pushing an emptied summary
13153
+ // clears it server-side; a plain new file leaves summary untouched.
13154
+ ...(parsed.hasFrontmatter ? { summary: parsed.summary } : {}),
13155
+ ...(record ? { expected_revision: record.revision } : {}),
13156
+ };
13157
+ try {
13158
+ const data = await requestOxygen("/api/cli/knowledge/pages/upsert", { method: "POST", body });
13159
+ const page = data.page ?? {};
13160
+ if (typeof page.id === "string" && typeof page.revision === "number") {
13161
+ // Record the pushed bytes as the manifest truth so the follow-up sync
13162
+ // overwrites this file with the server rendering instead of quarantining
13163
+ // the very edit we just pushed.
13164
+ const localSha = localPageSha256(dir, slug);
13165
+ if (localSha) {
13166
+ state.pages[slug] = { id: page.id, revision: page.revision, content_sha256: localSha };
13167
+ }
13168
+ }
13169
+ return {
13170
+ slug,
13171
+ kind,
13172
+ status: "pushed",
13173
+ ...(typeof page.revision === "number" ? { revision: page.revision } : {}),
13174
+ };
13175
+ }
13176
+ catch (error) {
13177
+ if (error instanceof OxygenError && error.code === "knowledge_approval_required") {
13178
+ const details = (error.details ?? {});
13179
+ return {
13180
+ slug,
13181
+ kind,
13182
+ status: "proposal_created",
13183
+ ...(typeof details.proposalId === "string" ? { proposal_id: details.proposalId } : {}),
13184
+ ...(typeof details.proposalSlug === "string" ? { proposal_slug: details.proposalSlug } : {}),
13185
+ reason: typeof details.reason === "string" ? details.reason : "sensitive_page",
13186
+ };
13187
+ }
13188
+ if (error instanceof OxygenError && error.code === "knowledge_page_conflict") {
13189
+ return { slug, kind, status: "conflict", reason: "revision_moved_run_knowledge_sync_first" };
13190
+ }
13191
+ if (error instanceof OxygenError) {
13192
+ return { slug, kind, status: "failed", reason: error.code };
13193
+ }
13194
+ throw error;
13195
+ }
13196
+ }
13197
+ // `oxygen knowledge push` — the write half of the local mirror: locally edited
13198
+ // (dirty) and brand-new mirror files upsert back to the workspace wiki through
13199
+ // the same gate as every other write surface, then a delta sync converges the
13200
+ // mirror onto the server rendering. This is what lets a local agent (Claude
13201
+ // Code, Codex) work the wiki as plain markdown files.
13202
+ async function runKnowledgeMirrorPush(options) {
13203
+ const target = await resolveKnowledgeMirrorTarget();
13204
+ const state = readMirrorState(target.dir);
13205
+ if (!state) {
13206
+ throw new OxygenError("knowledge_mirror_missing", "No local knowledge mirror exists for this organization yet. Run `oxygen knowledge sync` first.", {
13207
+ details: { mirror_path: target.dir },
13208
+ exitCode: 1,
13209
+ });
13210
+ }
13211
+ const plan = planMirrorPush(target.dir, state);
13212
+ if (options.dryRun) {
13213
+ return {
13214
+ dry_run: true,
13215
+ candidates: plan.candidates.map(({ slug, kind, record }) => ({
13216
+ slug,
13217
+ kind,
13218
+ ...(record ? { base_revision: record.revision } : {}),
13219
+ })),
13220
+ skipped: plan.skipped,
13221
+ mirror_path: target.dir,
13222
+ web_url: target.webUrl,
13223
+ };
13224
+ }
13225
+ // Hold the mirror lock through the push phase so a concurrent sync can't
13226
+ // rewrite files or the manifest mid-push; released before the convergence
13227
+ // sync below, which takes the lock itself.
13228
+ if (!acquireMirrorLock(target.dir)) {
13229
+ throw new OxygenError("knowledge_sync_locked", "Another knowledge sync is running for this mirror. Retry shortly; a crashed sync's lock self-clears after 10 minutes.", {
13230
+ details: { mirror_path: target.dir },
13231
+ exitCode: 1,
13232
+ });
13233
+ }
13234
+ const outcomes = [];
13235
+ let pushedCount = 0;
13236
+ try {
13237
+ for (const candidate of plan.candidates) {
13238
+ outcomes.push(await pushOneMirrorPage(target.dir, state, candidate));
13239
+ }
13240
+ pushedCount = outcomes.filter((outcome) => outcome.status === "pushed").length;
13241
+ if (pushedCount > 0) {
13242
+ writeMirrorState(target.dir, state);
13243
+ }
13244
+ }
13245
+ finally {
13246
+ releaseMirrorLock(target.dir);
13247
+ }
13248
+ // Converge: pull the server's rendering of everything just pushed (plus any
13249
+ // proposal draft pages the gate filed) so local bytes match the server again.
13250
+ let syncedAfter = false;
13251
+ if (pushedCount > 0 || outcomes.some((outcome) => outcome.status === "proposal_created")) {
13252
+ await runKnowledgeMirrorSync({});
13253
+ syncedAfter = true;
13254
+ }
13255
+ return {
13256
+ pushed: pushedCount,
13257
+ proposals: outcomes.filter((outcome) => outcome.status === "proposal_created"),
13258
+ conflicts: outcomes.filter((outcome) => outcome.status === "conflict"),
13259
+ failures: outcomes.filter((outcome) => outcome.status === "failed"),
13260
+ skipped: [
13261
+ ...plan.skipped,
13262
+ ...outcomes
13263
+ .filter((outcome) => outcome.status === "skipped")
13264
+ .map(({ slug, reason }) => ({ file: `${slug}.md`, reason: reason ?? "skipped" })),
13265
+ ],
13266
+ outcomes,
13267
+ synced_after: syncedAfter,
13268
+ mirror_path: target.dir,
13269
+ web_url: target.webUrl,
13270
+ };
13271
+ }
12919
13272
  async function runKnowledgeMirrorStatus(options) {
12920
13273
  const target = await resolveKnowledgeMirrorTarget();
12921
13274
  const clearedConflicts = options.clearConflicts ? clearConflictFiles(target.dir) : undefined;
@@ -12949,7 +13302,7 @@ async function handleKnowledgeMirrorPrintPathAction() {
12949
13302
  catch (error) {
12950
13303
  const failure = toFailure("knowledge status", error);
12951
13304
  writeJson(failure);
12952
- process.exitCode = error instanceof OxygenError ? error.exitCode : 1;
13305
+ process.exitCode = error instanceof OxygenError ? exitCodeForOxygenError(error) : 1;
12953
13306
  }
12954
13307
  }
12955
13308
  // Knowledge page routes accept either a UUID (`id`) or a slug (`slug`); split
@@ -13194,4 +13547,13 @@ function muteTokenEcho() {
13194
13547
  return () => undefined;
13195
13548
  }
13196
13549
  }
13550
+ // Piping CLI output into a consumer that exits early (`oxygen whoami --json |
13551
+ // head -c 20`) otherwise crashes with a raw EPIPE stack after the consumer
13552
+ // closes the pipe; a closed pipe is normal shell behavior, not an error.
13553
+ process.stdout.on("error", (error) => {
13554
+ if (error.code === "EPIPE")
13555
+ process.exit(0);
13556
+ process.stderr.write(`stdout error: ${error.message}\n`);
13557
+ process.exit(1);
13558
+ });
13197
13559
  await createProgram().parseAsync(process.argv);
@@ -92,6 +92,28 @@ export declare function acquireMirrorLock(dir: string, options?: {
92
92
  export declare function releaseMirrorLock(dir: string): void;
93
93
  export declare function writeGeneratedIndexFile(dir: string, index: MirrorIndexSnapshot): void;
94
94
  export declare function writeGeneratedLogFile(dir: string, entries: MirrorLogEntry[]): void;
95
+ export type MirrorPushCandidate = {
96
+ slug: string;
97
+ /** dirty = tracked page whose local bytes drifted; new = untracked local file. */
98
+ kind: "dirty" | "new";
99
+ path: string;
100
+ /** Manifest record for dirty pages — its revision is the optimistic-concurrency base. */
101
+ record?: MirrorPageRecord;
102
+ };
103
+ export type MirrorPushPlan = {
104
+ candidates: MirrorPushCandidate[];
105
+ skipped: Array<{
106
+ file: string;
107
+ reason: "invalid_slug" | "reserved_slug" | "generated_file";
108
+ }>;
109
+ };
110
+ /**
111
+ * Scan the mirror for pushable local state: tracked pages whose bytes drifted
112
+ * from the manifest sha (edited locally) and untracked `.md` files (created
113
+ * locally). Generated index/log summaries and reserved slugs are skipped.
114
+ * Read-only — the push command decides what to do with the plan.
115
+ */
116
+ export declare function planMirrorPush(dir: string, state: MirrorState | null): MirrorPushPlan;
95
117
  /** Delete the whole mirror (pages, generated files, sidecar). Safe on a missing dir. */
96
118
  export declare function purgeMirror(dir: string): void;
97
119
  /**
@@ -327,6 +327,62 @@ function writeGeneratedFile(dir, name, lines) {
327
327
  function oneLine(value) {
328
328
  return value.replace(/\s+/g, " ").trim();
329
329
  }
330
+ // Slugs the server write path refuses (RESERVED_KNOWLEDGE_SLUGS) — an untracked
331
+ // local file under one of these names can never push. A TRACKED page under one
332
+ // (legacy rows may predate the reservation) still pushes by id.
333
+ const PUSH_RESERVED_SLUGS = new Set(["index", "log", "schema", "company-profile", "readme"]);
334
+ /**
335
+ * Scan the mirror for pushable local state: tracked pages whose bytes drifted
336
+ * from the manifest sha (edited locally) and untracked `.md` files (created
337
+ * locally). Generated index/log summaries and reserved slugs are skipped.
338
+ * Read-only — the push command decides what to do with the plan.
339
+ */
340
+ export function planMirrorPush(dir, state) {
341
+ const plan = { candidates: [], skipped: [] };
342
+ let entries = [];
343
+ try {
344
+ entries = readdirSync(dir);
345
+ }
346
+ catch {
347
+ return plan; // no mirror directory — nothing to push
348
+ }
349
+ for (const entry of entries.sort((a, b) => a.localeCompare(b))) {
350
+ if (!entry.endsWith(".md"))
351
+ continue;
352
+ const slug = entry.slice(0, -3);
353
+ const path = join(dir, entry);
354
+ const record = state?.pages[slug];
355
+ if (record) {
356
+ if (isFileDirty(dir, slug, record.content_sha256)) {
357
+ plan.candidates.push({ slug, kind: "dirty", path, record });
358
+ }
359
+ continue;
360
+ }
361
+ if (slug.length > MIRROR_SLUG_MAX_LENGTH || !MIRROR_SLUG_PATTERN.test(slug)) {
362
+ plan.skipped.push({ file: entry, reason: "invalid_slug" });
363
+ continue;
364
+ }
365
+ // Untracked generated summaries (index.md / log.md before their slugs are
366
+ // ever page-backed) carry the marker header — never push those.
367
+ let head = "";
368
+ try {
369
+ head = readFileSync(path, "utf8").slice(0, GENERATED_MIRROR_FILE_HEADER.length);
370
+ }
371
+ catch {
372
+ continue;
373
+ }
374
+ if (head === GENERATED_MIRROR_FILE_HEADER) {
375
+ plan.skipped.push({ file: entry, reason: "generated_file" });
376
+ continue;
377
+ }
378
+ if (PUSH_RESERVED_SLUGS.has(slug)) {
379
+ plan.skipped.push({ file: entry, reason: "reserved_slug" });
380
+ continue;
381
+ }
382
+ plan.candidates.push({ slug, kind: "new", path });
383
+ }
384
+ return plan;
385
+ }
330
386
  // ── Purge ───────────────────────────────────────────────────────────────────────
331
387
  /** Delete the whole mirror (pages, generated files, sidecar). Safe on a missing dir. */
332
388
  export function purgeMirror(dir) {
@@ -26,6 +26,8 @@ export type PricingPlanDefinition = {
26
26
  };
27
27
  export type PricingPlanMetadata = Record<string, unknown> | null | undefined;
28
28
  export declare const CONTACT_SALES_URL = "https://cal.com/tim-scheuer-mxbib9/45";
29
+ export declare const TRIAL_PERIOD_DAYS = 7;
30
+ export declare const TRIAL_CREDIT_GRANT = 1000;
29
31
  export declare const BASE_PRICING_PLANS: {
30
32
  readonly free: {
31
33
  readonly tier: "free";
@@ -2,6 +2,12 @@ export const WEEKLY_USAGE_WINDOW_DAYS = 7;
2
2
  export const BILLING_CURRENCIES = ["usd"];
3
3
  export const DEFAULT_BILLING_CURRENCY = "usd";
4
4
  export const CONTACT_SALES_URL = "https://cal.com/tim-scheuer-mxbib9/45";
5
+ // Card-required free trial: every new signup starts a Stripe trial on the Starter
6
+ // plan, gets TRIAL_CREDIT_GRANT credits (a value-demonstration budget sized to the
7
+ // guided first outcome — NOT the full plan allowance), and converts to paid on
8
+ // credit exhaustion or when the trial clock elapses, whichever comes first.
9
+ export const TRIAL_PERIOD_DAYS = 7;
10
+ export const TRIAL_CREDIT_GRANT = 1_000;
5
11
  export const BASE_PRICING_PLANS = {
6
12
  free: {
7
13
  tier: "free",
@@ -30,6 +30,9 @@ export declare class OxygenError extends Error {
30
30
  exitCode?: number;
31
31
  });
32
32
  }
33
+ export declare const CLI_EXIT_CODE_TABLE: Readonly<Record<number, string>>;
34
+ export declare function exitCodeForErrorCode(code: string): number;
35
+ export declare function exitCodeForOxygenError(error: OxygenError): number;
33
36
  export declare function success<T>(command: string, data: T, version?: string, minimumCliVersion?: string): CliSuccess<T>;
34
37
  export declare function failure(command: string, error: {
35
38
  code: string;
@@ -19,6 +19,71 @@ export class OxygenError extends Error {
19
19
  this.exitCode = options.exitCode ?? 1;
20
20
  }
21
21
  }
22
+ // Reserved CLI exit codes, so shell agents can branch on failure class without
23
+ // parsing the JSON envelope. Documented in `oxygen --help` and in the
24
+ // `oxygen commands --json` manifest; treat as a stable contract.
25
+ //
26
+ // 0 ok · 1 unclassified error · 2 usage / input validation · 3 auth or CLI
27
+ // compatibility (not_authenticated, cli_update_required) · 4 not found ·
28
+ // 5 provider / server failure · 6 rate limit (details.retry_after_seconds
29
+ // says how long to wait) · 7 approval / spend gate (re-run with --approved
30
+ // --max-credits <n>) · 8 timeout.
31
+ export const CLI_EXIT_CODE_TABLE = {
32
+ 0: "ok",
33
+ 1: "unclassified error",
34
+ 2: "usage / input validation",
35
+ 3: "auth or CLI compatibility",
36
+ 4: "not found",
37
+ 5: "provider or server failure",
38
+ 6: "rate limited (wait details.retry_after_seconds, then retry)",
39
+ 7: "approval / spend gate (re-run with --approved --max-credits <n>)",
40
+ 8: "timeout",
41
+ };
42
+ export function exitCodeForErrorCode(code) {
43
+ switch (code) {
44
+ case "not_authenticated":
45
+ case "unauthorized":
46
+ case "invalid_token":
47
+ case "cli_update_required":
48
+ return 3;
49
+ case "rate_limit_exceeded":
50
+ return 6;
51
+ case "max_credits_required":
52
+ case "approval_required":
53
+ case "spend_cap_required":
54
+ case "spend_cap_too_low":
55
+ case "insufficient_credits":
56
+ case "workspace_credit_cap_exceeded":
57
+ case "byok_requires_paid_plan":
58
+ return 7;
59
+ case "network_timeout":
60
+ return 8;
61
+ case "provider_error":
62
+ case "upstream_error":
63
+ case "internal_error":
64
+ case "server_error":
65
+ return 5;
66
+ default:
67
+ break;
68
+ }
69
+ if (code === "not_found" || code.endsWith("_not_found"))
70
+ return 4;
71
+ if (code === "timeout" || code.endsWith("_timeout"))
72
+ return 8;
73
+ if (code.startsWith("invalid_")
74
+ || code.startsWith("missing_")
75
+ || code === "unknown_fields"
76
+ || code === "validation_failed") {
77
+ return 2;
78
+ }
79
+ return 1;
80
+ }
81
+ // exitCode 1 is the legacy constructor default at ~170 OxygenError call sites;
82
+ // treat a stored 1 as "unset" and derive the reserved code from the typed
83
+ // error code instead. An explicit non-1 exitCode option always wins.
84
+ export function exitCodeForOxygenError(error) {
85
+ return error.exitCode !== 1 ? error.exitCode : exitCodeForErrorCode(error.code);
86
+ }
22
87
  export function success(command, data, version = OXYGEN_VERSION, minimumCliVersion = OXYGEN_MINIMUM_CLI_VERSION) {
23
88
  return {
24
89
  ok: true,
@@ -0,0 +1,12 @@
1
+ export type DeprecationRegistryEntry = {
2
+ /** What is deprecated, precisely enough to grep for. */
3
+ surface: string;
4
+ /** OXYGEN_VERSION at which the surface became deprecated. */
5
+ deprecated_in: string;
6
+ /** Semver deadline, or "floor-bump" (see header). */
7
+ sunset_version: string;
8
+ /** Where the surface lives and what removal entails. */
9
+ note: string;
10
+ };
11
+ export declare const FLOOR_BUMP_SENTINEL = "floor-bump";
12
+ export declare const DEPRECATION_REGISTRY: DeprecationRegistryEntry[];
@@ -0,0 +1,58 @@
1
+ // Deprecation sunset registry — the build-enforced list of deprecated agent
2
+ // surfaces and when each one must actually be removed.
3
+ //
4
+ // Why this exists: deprecated aliases ("kept for old clients") historically
5
+ // outlive every intention to remove them because nothing ever fails. The
6
+ // registry inverts that: packages/shared/src/index.test.ts asserts that
7
+ // OXYGEN_VERSION has NOT passed any entry's semver `sunset_version`. Once the
8
+ // version crosses a sunset, the build goes red until the surface is deleted —
9
+ // and removal means deleting the surface AND its registry entry in the same
10
+ // PR.
11
+ //
12
+ // sunset_version values:
13
+ // - "1.x.y" semver — hard deadline, gated by the test.
14
+ // - "floor-bump" — sentinel for surfaces that can only be removed when
15
+ // OXYGEN_MINIMUM_CLI_VERSION next rises past the clients that still use
16
+ // them (old CLIs on users' machines send/read these). The test validates
17
+ // the entry's shape but skips the version gate; whoever bumps the CLI
18
+ // floor must sweep the "floor-bump" entries in the same change.
19
+ export const FLOOR_BUMP_SENTINEL = "floor-bump";
20
+ export const DEPRECATION_REGISTRY = [
21
+ {
22
+ surface: "oxygen_templates_* MCP alias tools",
23
+ deprecated_in: "1.80.0",
24
+ sunset_version: "1.290.0",
25
+ note: "Deprecated alias tree of oxygen_prompts_* (packages/mcp-server/src/tools/" +
26
+ "prompt-template-tools.ts, prefix \"oxygen_templates\"). Removal: drop the " +
27
+ "alias prefix from the generated tools and the CLI `templates` alias " +
28
+ "command tree, then update the pinned tool counts.",
29
+ },
30
+ {
31
+ surface: "oxygen.linkedin-inbox widget alias",
32
+ deprecated_in: "1.226.0",
33
+ sunset_version: "1.290.0",
34
+ note: "Legacy alias of oxygen.unibox kept so MCP clients that cached " +
35
+ "ui://oxygen/linkedin-inbox keep resolving (packages/mcp-server/src/widgets/" +
36
+ "definitions.ts). Removal: delete the alias widget definition and any " +
37
+ "bindings that still point at it.",
38
+ },
39
+ {
40
+ surface: "CLI deepLink/deep_link response keys",
41
+ deprecated_in: "1.8.2",
42
+ sunset_version: FLOOR_BUMP_SENTINEL,
43
+ note: "web_url has been the canonical deep-link key since at least v1.8.2; " +
44
+ "/api/cli responses still duplicate it as deepLink/deep_link because " +
45
+ "CLIs at or below the current minimum version read the legacy keys. " +
46
+ "Remove the duplicated keys (and the CLI readers) when " +
47
+ "OXYGEN_MINIMUM_CLI_VERSION next rises past those clients.",
48
+ },
49
+ {
50
+ surface: "CLI --return/--org legacy flags",
51
+ deprecated_in: "1.0.0",
52
+ sunset_version: FLOOR_BUMP_SENTINEL,
53
+ note: "Legacy aliases of --return-mode/--org-id on `oxygen tools run` " +
54
+ "(packages/cli/src/index.ts); aliased since before versioned commits. " +
55
+ "Remove the flags and their request-body mappings when " +
56
+ "OXYGEN_MINIMUM_CLI_VERSION next rises past CLIs that send them.",
57
+ },
58
+ ];
@@ -21,3 +21,28 @@ export type KnowledgePageRenderInput = {
21
21
  * verbatim with exactly one trailing newline.
22
22
  */
23
23
  export declare function renderKnowledgePageMarkdown(page: KnowledgePageRenderInput): string;
24
+ export type ParsedKnowledgePageMarkdown = {
25
+ /** True when the file opened with a `---` frontmatter block (any keys). */
26
+ hasFrontmatter: boolean;
27
+ /** Raw scalar frontmatter values by key (tags stay in their `[a, b]` form here). */
28
+ frontmatter: Record<string, string>;
29
+ id: string | null;
30
+ slug: string | null;
31
+ type: string | null;
32
+ title: string | null;
33
+ status: string | null;
34
+ tags: string[] | null;
35
+ summary: string | null;
36
+ revision: number | null;
37
+ /** Body text below the frontmatter (whole file when none), no trailing newline. */
38
+ body: string;
39
+ };
40
+ /**
41
+ * Parse a mirror page file back into its fields — the inverse of
42
+ * renderKnowledgePageMarkdown, tolerant of human/agent edits (`oxygen knowledge
43
+ * push` runs this on locally edited files). Line-based like the renderer's
44
+ * contract: `key: value` pairs between `---` fences, tags as `[a, b]`. A file
45
+ * without a frontmatter block parses as pure body; a plain-markdown title is
46
+ * derived from the first `# heading` so brand-new local files can push.
47
+ */
48
+ export declare function parseKnowledgePageMarkdown(contents: string): ParsedKnowledgePageMarkdown;
@@ -55,3 +55,77 @@ export function renderKnowledgePageMarkdown(page) {
55
55
  const body = (page.body ?? "").replace(/\r\n/g, "\n").replace(/\n+$/, "");
56
56
  return `${lines.join("\n")}\n${body}\n`;
57
57
  }
58
+ /**
59
+ * Parse a mirror page file back into its fields — the inverse of
60
+ * renderKnowledgePageMarkdown, tolerant of human/agent edits (`oxygen knowledge
61
+ * push` runs this on locally edited files). Line-based like the renderer's
62
+ * contract: `key: value` pairs between `---` fences, tags as `[a, b]`. A file
63
+ * without a frontmatter block parses as pure body; a plain-markdown title is
64
+ * derived from the first `# heading` so brand-new local files can push.
65
+ */
66
+ export function parseKnowledgePageMarkdown(contents) {
67
+ const text = contents.replace(/\r\n/g, "\n");
68
+ const lines = text.split("\n");
69
+ const parsed = {
70
+ hasFrontmatter: false,
71
+ frontmatter: {},
72
+ id: null,
73
+ slug: null,
74
+ type: null,
75
+ title: null,
76
+ status: null,
77
+ tags: null,
78
+ summary: null,
79
+ revision: null,
80
+ body: "",
81
+ };
82
+ let bodyStart = 0;
83
+ if (lines[0]?.trim() === "---") {
84
+ const closing = lines.findIndex((line, index) => index > 0 && line.trim() === "---");
85
+ if (closing > 0) {
86
+ parsed.hasFrontmatter = true;
87
+ for (const line of lines.slice(1, closing)) {
88
+ const separator = line.indexOf(":");
89
+ if (separator <= 0)
90
+ continue;
91
+ const key = line.slice(0, separator).trim();
92
+ if (!/^[A-Za-z0-9_-]+$/.test(key))
93
+ continue;
94
+ parsed.frontmatter[key] = line.slice(separator + 1).trim();
95
+ }
96
+ bodyStart = closing + 1;
97
+ // The renderer emits exactly one blank line after the closing fence.
98
+ if (lines[bodyStart] === "")
99
+ bodyStart += 1;
100
+ }
101
+ }
102
+ parsed.body = lines.slice(bodyStart).join("\n").replace(/\n+$/, "");
103
+ const fm = parsed.frontmatter;
104
+ const str = (key) => {
105
+ const value = fm[key];
106
+ return value !== undefined && value !== "" ? value : null;
107
+ };
108
+ parsed.id = str("id");
109
+ parsed.slug = str("slug");
110
+ parsed.type = str("type");
111
+ parsed.title = str("title");
112
+ parsed.status = str("status");
113
+ parsed.summary = str("summary");
114
+ if (fm.tags !== undefined) {
115
+ const inner = fm.tags.replace(/^\[/, "").replace(/\]$/, "");
116
+ parsed.tags = inner
117
+ .split(",")
118
+ .map((tag) => tag.trim())
119
+ .filter((tag) => tag.length > 0);
120
+ }
121
+ if (fm.revision !== undefined) {
122
+ const revision = Number.parseInt(fm.revision, 10);
123
+ parsed.revision = Number.isFinite(revision) ? revision : null;
124
+ }
125
+ if (!parsed.title) {
126
+ const heading = lines.slice(bodyStart).find((line) => /^#\s+\S/.test(line));
127
+ if (heading)
128
+ parsed.title = heading.replace(/^#\s+/, "").trim() || null;
129
+ }
130
+ return parsed;
131
+ }
@@ -1,2 +1,2 @@
1
- export declare const OXYGEN_VERSION = "1.275.2";
1
+ export declare const OXYGEN_VERSION = "1.285.8";
2
2
  export declare const OXYGEN_MINIMUM_CLI_VERSION = "1.181.0";
@@ -1,4 +1,4 @@
1
- export const OXYGEN_VERSION = "1.275.2";
1
+ export const OXYGEN_VERSION = "1.285.8";
2
2
  // Bump this only when deployed CLI/API contracts require a newer CLI.
3
3
  // 1.181.0: paid table action runs and background columns run require
4
4
  // approved=true in addition to max_credits; older CLIs cannot send the flag.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxygen-agent/cli",
3
- "version": "1.275.2",
3
+ "version": "1.285.8",
4
4
  "private": false,
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",