@oxygen-agent/cli 1.275.1 → 1.284.3

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/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.")
@@ -5721,6 +5795,7 @@ export function createProgram() {
5721
5795
  .option("--profile-views-per-day <n>", "Daily profile views cap.")
5722
5796
  .option("--follows-per-day <n>", "Daily follows cap.")
5723
5797
  .option("--likes-per-day <n>", "Daily likes cap.")
5798
+ .option("--comments-per-day <n>", "Daily comments cap.")
5724
5799
  .option("--total-actions-per-day <n>", "Daily cap across all send/action types.")
5725
5800
  .option("--relations-reads-per-day <n>", "Daily cap on relations/connections list reads (scrape protection).")
5726
5801
  .option("--messages-reads-per-day <n>", "Daily cap on chat and message-history reads.")
@@ -7281,6 +7356,95 @@ export function createProgram() {
7281
7356
  });
7282
7357
  });
7283
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
+ })));
7284
7448
  program.addCommand(new Command("mailboxes")
7285
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).")
7286
7450
  .addCommand(new Command("list")
@@ -7527,6 +7691,28 @@ export function createProgram() {
7527
7691
  const suffix = params.toString();
7528
7692
  return requestOxygen(`/api/cli/mailboxes/warmup/status${suffix ? `?${suffix}` : ""}`);
7529
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
+ });
7530
7716
  })))
7531
7717
  .addCommand(new Command("order")
7532
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.")
@@ -8194,6 +8380,7 @@ export function createProgram() {
8194
8380
  .action(async (options) => {
8195
8381
  await handleAsyncAction("skills install", options, () => installAgentSkills(options));
8196
8382
  }));
8383
+ applyOxygenHelp(program, binaryName);
8197
8384
  return program;
8198
8385
  }
8199
8386
  /**
@@ -10771,6 +10958,27 @@ async function handleWhoamiAction(options) {
10771
10958
  enforceMinimumCliVersion: false,
10772
10959
  });
10773
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
+ }
10774
10982
  if (context.resolution.exists) {
10775
10983
  const existingCredentials = context.resolution.credentials;
10776
10984
  const cached = existingCredentials?.identity;
@@ -10806,7 +11014,7 @@ async function handleWhoamiAction(options) {
10806
11014
  catch (error) {
10807
11015
  const failure = toFailure("whoami", error);
10808
11016
  writeJson(failure);
10809
- process.exitCode = error instanceof OxygenError ? error.exitCode : 1;
11017
+ process.exitCode = error instanceof OxygenError ? exitCodeForOxygenError(error) : 1;
10810
11018
  }
10811
11019
  }
10812
11020
  async function handleOnboardingStartAction(options) {
@@ -10821,7 +11029,7 @@ async function handleOnboardingStartAction(options) {
10821
11029
  catch (error) {
10822
11030
  const failure = toFailure("onboarding start", error);
10823
11031
  writeJson(failure);
10824
- process.exitCode = error instanceof OxygenError ? error.exitCode : 1;
11032
+ process.exitCode = error instanceof OxygenError ? exitCodeForOxygenError(error) : 1;
10825
11033
  }
10826
11034
  }
10827
11035
  async function handleOnboardingResetAction(options) {
@@ -10845,7 +11053,7 @@ async function handleOnboardingResetAction(options) {
10845
11053
  catch (error) {
10846
11054
  const failure = toFailure("onboarding reset", error);
10847
11055
  writeJson(failure);
10848
- process.exitCode = error instanceof OxygenError ? error.exitCode : 1;
11056
+ process.exitCode = error instanceof OxygenError ? exitCodeForOxygenError(error) : 1;
10849
11057
  }
10850
11058
  }
10851
11059
  function formatOnboardingReset(data) {
@@ -10903,7 +11111,7 @@ async function handleLoginAction(options) {
10903
11111
  catch (error) {
10904
11112
  const failure = toFailure("login", error);
10905
11113
  writeJson(failure);
10906
- process.exitCode = error instanceof OxygenError ? error.exitCode : 1;
11114
+ process.exitCode = error instanceof OxygenError ? exitCodeForOxygenError(error) : 1;
10907
11115
  }
10908
11116
  }
10909
11117
  async function handleAuthUseTokenAction(options) {
@@ -10933,7 +11141,7 @@ async function handleAuthUseTokenAction(options) {
10933
11141
  writeJson(failure);
10934
11142
  else
10935
11143
  process.stderr.write(`${failure.error.message}\n`);
10936
- process.exitCode = error instanceof OxygenError ? error.exitCode : 1;
11144
+ process.exitCode = error instanceof OxygenError ? exitCodeForOxygenError(error) : 1;
10937
11145
  }
10938
11146
  }
10939
11147
  async function handleAuthDoctorAction(options) {
@@ -10948,7 +11156,7 @@ async function handleAuthDoctorAction(options) {
10948
11156
  catch (error) {
10949
11157
  const failure = toFailure("auth doctor", error);
10950
11158
  writeJson(failure);
10951
- process.exitCode = error instanceof OxygenError ? error.exitCode : 1;
11159
+ process.exitCode = error instanceof OxygenError ? exitCodeForOxygenError(error) : 1;
10952
11160
  }
10953
11161
  }
10954
11162
  async function runAuthDoctor() {
@@ -11039,7 +11247,7 @@ async function handleOrgUseAction(organization, options, command) {
11039
11247
  catch (error) {
11040
11248
  const failure = toFailure(command, error);
11041
11249
  writeJson(failure);
11042
- process.exitCode = error instanceof OxygenError ? error.exitCode : 1;
11250
+ process.exitCode = error instanceof OxygenError ? exitCodeForOxygenError(error) : 1;
11043
11251
  }
11044
11252
  }
11045
11253
  async function handleProfilesListAction(options) {
@@ -11058,7 +11266,7 @@ async function handleProfilesListAction(options) {
11058
11266
  catch (error) {
11059
11267
  const failure = toFailure("profiles list", error);
11060
11268
  writeJson(failure);
11061
- process.exitCode = error instanceof OxygenError ? error.exitCode : 1;
11269
+ process.exitCode = error instanceof OxygenError ? exitCodeForOxygenError(error) : 1;
11062
11270
  }
11063
11271
  }
11064
11272
  async function handleProfilesUseAction(profile, options) {
@@ -11079,7 +11287,7 @@ async function handleProfilesUseAction(profile, options) {
11079
11287
  catch (error) {
11080
11288
  const failure = toFailure("profiles use", error);
11081
11289
  writeJson(failure);
11082
- process.exitCode = error instanceof OxygenError ? error.exitCode : 1;
11290
+ process.exitCode = error instanceof OxygenError ? exitCodeForOxygenError(error) : 1;
11083
11291
  }
11084
11292
  }
11085
11293
  async function handleProfilesEnvAction(profile, options) {
@@ -11118,7 +11326,7 @@ async function handleProfilesEnvAction(profile, options) {
11118
11326
  catch (error) {
11119
11327
  const failure = toFailure("profiles env", error);
11120
11328
  writeJson(failure);
11121
- process.exitCode = error instanceof OxygenError ? error.exitCode : 1;
11329
+ process.exitCode = error instanceof OxygenError ? exitCodeForOxygenError(error) : 1;
11122
11330
  }
11123
11331
  }
11124
11332
  async function handleProfilesCurrentAction(options) {
@@ -11142,7 +11350,7 @@ async function handleProfilesCurrentAction(options) {
11142
11350
  catch (error) {
11143
11351
  const failure = toFailure("profiles current", error);
11144
11352
  writeJson(failure);
11145
- process.exitCode = error instanceof OxygenError ? error.exitCode : 1;
11353
+ process.exitCode = error instanceof OxygenError ? exitCodeForOxygenError(error) : 1;
11146
11354
  }
11147
11355
  }
11148
11356
  function shellQuote(value) {
@@ -11171,7 +11379,7 @@ async function handleLogoutAction(options) {
11171
11379
  catch (error) {
11172
11380
  const failure = toFailure("logout", error);
11173
11381
  writeJson(failure);
11174
- process.exitCode = error instanceof OxygenError ? error.exitCode : 1;
11382
+ process.exitCode = error instanceof OxygenError ? exitCodeForOxygenError(error) : 1;
11175
11383
  }
11176
11384
  }
11177
11385
  async function handleUpdateAction(options) {
@@ -11186,7 +11394,7 @@ async function handleUpdateAction(options) {
11186
11394
  catch (error) {
11187
11395
  const failure = toFailure("update", error);
11188
11396
  writeJson(failure);
11189
- process.exitCode = error instanceof OxygenError ? error.exitCode : 1;
11397
+ process.exitCode = error instanceof OxygenError ? exitCodeForOxygenError(error) : 1;
11190
11398
  }
11191
11399
  }
11192
11400
  function buildApiKeyCreateBody(options) {
@@ -11797,7 +12005,7 @@ async function handleSequenceSignalAction(sequence, options) {
11797
12005
  const failure = toFailure("sequences signal", error);
11798
12006
  writeJson(failure);
11799
12007
  writeMaxCreditsHint(error);
11800
- process.exitCode = error instanceof OxygenError ? error.exitCode : 1;
12008
+ process.exitCode = error instanceof OxygenError ? exitCodeForOxygenError(error) : 1;
11801
12009
  }
11802
12010
  }
11803
12011
  async function handleSequenceVariantsAction(sequence, options) {
@@ -11834,7 +12042,7 @@ async function handleSequenceVariantsAction(sequence, options) {
11834
12042
  const failure = toFailure("sequences variants", error);
11835
12043
  writeJson(failure);
11836
12044
  writeMaxCreditsHint(error);
11837
- process.exitCode = error instanceof OxygenError ? error.exitCode : 1;
12045
+ process.exitCode = error instanceof OxygenError ? exitCodeForOxygenError(error) : 1;
11838
12046
  }
11839
12047
  }
11840
12048
  function formatVariantCell(value) {
@@ -11965,7 +12173,7 @@ async function handleTablesTidySuggestAction(table, options) {
11965
12173
  const failure = toFailure("tables tidy-suggest", error);
11966
12174
  writeJson(failure);
11967
12175
  writeMaxCreditsHint(error);
11968
- process.exitCode = error instanceof OxygenError ? error.exitCode : 1;
12176
+ process.exitCode = error instanceof OxygenError ? exitCodeForOxygenError(error) : 1;
11969
12177
  }
11970
12178
  }
11971
12179
  function formatTidyCount(value) {
@@ -12348,7 +12556,7 @@ async function handleSupportAdminWorkflowAction(ticketId, options) {
12348
12556
  catch (error) {
12349
12557
  const failure = toFailure("support admin workflow", error);
12350
12558
  writeJson(failure);
12351
- process.exitCode = error instanceof OxygenError ? error.exitCode : 1;
12559
+ process.exitCode = error instanceof OxygenError ? exitCodeForOxygenError(error) : 1;
12352
12560
  }
12353
12561
  }
12354
12562
  async function handleSupportAdminUpdateAction(ticketId, options) {
@@ -12364,7 +12572,7 @@ async function handleSupportAdminUpdateAction(ticketId, options) {
12364
12572
  catch (error) {
12365
12573
  const failure = toFailure("support admin update", error);
12366
12574
  writeJson(failure);
12367
- process.exitCode = error instanceof OxygenError ? error.exitCode : 1;
12575
+ process.exitCode = error instanceof OxygenError ? exitCodeForOxygenError(error) : 1;
12368
12576
  }
12369
12577
  }
12370
12578
  function formatSupportAdminWorkflowStep(styles, label, status, artifactLabel, artifact) {
@@ -12915,6 +13123,152 @@ async function runKnowledgeMirrorSync(options) {
12915
13123
  releaseMirrorLock(target.dir);
12916
13124
  }
12917
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
+ }
12918
13272
  async function runKnowledgeMirrorStatus(options) {
12919
13273
  const target = await resolveKnowledgeMirrorTarget();
12920
13274
  const clearedConflicts = options.clearConflicts ? clearConflictFiles(target.dir) : undefined;
@@ -12948,7 +13302,7 @@ async function handleKnowledgeMirrorPrintPathAction() {
12948
13302
  catch (error) {
12949
13303
  const failure = toFailure("knowledge status", error);
12950
13304
  writeJson(failure);
12951
- process.exitCode = error instanceof OxygenError ? error.exitCode : 1;
13305
+ process.exitCode = error instanceof OxygenError ? exitCodeForOxygenError(error) : 1;
12952
13306
  }
12953
13307
  }
12954
13308
  // Knowledge page routes accept either a UUID (`id`) or a slug (`slug`); split
@@ -13030,6 +13384,7 @@ options) {
13030
13384
  setLimit("profile_views_per_day", options.profileViewsPerDay);
13031
13385
  setLimit("follows_per_day", options.followsPerDay);
13032
13386
  setLimit("likes_per_day", options.likesPerDay);
13387
+ setLimit("comments_per_day", options.commentsPerDay);
13033
13388
  setLimit("total_actions_per_day", options.totalActionsPerDay);
13034
13389
  setLimit("relations_reads_per_day", options.relationsReadsPerDay);
13035
13390
  setLimit("messages_reads_per_day", options.messagesReadsPerDay);
@@ -13192,4 +13547,13 @@ function muteTokenEcho() {
13192
13547
  return () => undefined;
13193
13548
  }
13194
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
+ });
13195
13559
  await createProgram().parseAsync(process.argv);