@oxygen-agent/cli 1.922.14 → 1.936.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/README.md +1 -1
  2. package/dist/admin-primary-providers-render.d.ts +18 -0
  3. package/dist/admin-primary-providers-render.js +371 -0
  4. package/dist/command-manifest.js +6 -0
  5. package/dist/functions-commands.d.ts +6 -0
  6. package/dist/functions-commands.js +56 -0
  7. package/dist/http-client.d.ts +4 -0
  8. package/dist/http-client.js +49 -2
  9. package/dist/index.js +175 -40
  10. package/dist/ugc-commands.d.ts +6 -0
  11. package/dist/ugc-commands.js +748 -0
  12. package/dist/visual-commands.d.ts +6 -0
  13. package/dist/visual-commands.js +57 -0
  14. package/dist/visual-render-wait.d.ts +3 -0
  15. package/dist/visual-render-wait.js +56 -0
  16. package/node_modules/@oxygen/shared/dist/byok-connect.d.ts +43 -0
  17. package/node_modules/@oxygen/shared/dist/byok-connect.js +84 -0
  18. package/node_modules/@oxygen/shared/dist/capability-discovery.js +26 -10
  19. package/node_modules/@oxygen/shared/dist/feature-gates.d.ts +2 -0
  20. package/node_modules/@oxygen/shared/dist/feature-gates.js +3 -0
  21. package/node_modules/@oxygen/shared/dist/index.d.ts +4 -0
  22. package/node_modules/@oxygen/shared/dist/index.js +4 -0
  23. package/node_modules/@oxygen/shared/dist/knowledge-constants.d.ts +1 -1
  24. package/node_modules/@oxygen/shared/dist/knowledge-constants.js +3 -2
  25. package/node_modules/@oxygen/shared/dist/knowledge-seed-content.js +1 -1
  26. package/node_modules/@oxygen/shared/dist/langfuse.js +17 -0
  27. package/node_modules/@oxygen/shared/dist/object-storage.d.ts +6 -0
  28. package/node_modules/@oxygen/shared/dist/object-storage.js +5 -0
  29. package/node_modules/@oxygen/shared/dist/provider-balance-signal.d.ts +113 -0
  30. package/node_modules/@oxygen/shared/dist/provider-balance-signal.js +158 -0
  31. package/node_modules/@oxygen/shared/dist/sequence-failures.d.ts +12 -0
  32. package/node_modules/@oxygen/shared/dist/sequence-failures.js +24 -0
  33. package/node_modules/@oxygen/shared/dist/sequences.d.ts +16 -0
  34. package/node_modules/@oxygen/shared/dist/sequences.js +54 -0
  35. package/node_modules/@oxygen/shared/dist/ugc.d.ts +113 -0
  36. package/node_modules/@oxygen/shared/dist/ugc.js +2 -0
  37. package/node_modules/@oxygen/shared/dist/vercel-sandbox-fetch.d.ts +9 -0
  38. package/node_modules/@oxygen/shared/dist/vercel-sandbox-fetch.js +33 -0
  39. package/node_modules/@oxygen/shared/dist/version.d.ts +1 -1
  40. package/node_modules/@oxygen/shared/dist/version.js +1 -1
  41. package/node_modules/@oxygen/shared/dist/visual-render.d.ts +30 -0
  42. package/node_modules/@oxygen/shared/dist/visual-render.js +55 -0
  43. package/node_modules/@oxygen/shared/dist/workspace-file-storage.d.ts +43 -0
  44. package/node_modules/@oxygen/shared/dist/workspace-file-storage.js +126 -0
  45. package/node_modules/@oxygen/shared/package.json +10 -0
  46. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -7,6 +7,10 @@ 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, CommanderError, Option } from "commander";
10
+ import { registerUgcCommands } from "./ugc-commands.js";
11
+ import { registerVisualCommands } from "./visual-commands.js";
12
+ import { renderPrimaryProviderBoard } from "./admin-primary-providers-render.js";
13
+ import { registerFunctionsCommands } from "./functions-commands.js";
10
14
  import { applyOxygenHelp } from "./help.js";
11
15
  import { buildCommandManifest, getCommandManifestEntry, searchCommandManifest, suggestCommandNames, } from "./command-manifest.js";
12
16
  import { AGENCY_DIRECTORY_REGIONS, AGENCY_DIRECTORY_SERVICES, COLLAB_GATE_KINDS, COLLAB_GATE_PROSE, COLLAB_SUBJECT_KINDS, COLLAB_SUBJECT_KINDS_PROSE, COLLAB_SUBJECT_LABELS, COLLAB_SUBJECT_PROSE, GATE_KIND_SUBJECTS, describeWorkflowStatusChange, formatCellForDisplay, formatCopilotPlanDuration, formatCopilotPlanSeconds, formatPublicBudgetScopes, SUBJECT_PATH_FORMS_PROSE, formatSubjectPath, exitCodeForOxygenError, parseSubjectPath, parseSubjectRef, parseWorkflowStatusChange, isVersionGreater, isVersionLess, KNOWLEDGE_BOOTSTRAP_MAX_CREDITS, MAX_MCP_TOOL_NAME_LENGTH, normalizeCopilotPlanStepStatus, OXYGEN_CAPABILITY_ROUTES, OXYGEN_VERSION, OxygenError, getCapabilityRouteMatch, inferUserCapabilityRoute, parseKnowledgePageMarkdown, PLAN_LIMITS, serializeCapabilityRoute, sleep, success, TABLE_IMPORT_ROW_LIMIT, TAG_KINDS_PROSE, toFailure, workflowMcpToolName, } from "@oxygen/shared";
@@ -456,6 +460,29 @@ function writeDryRunNotice(data) {
456
460
  process.stderr.write(`${block.message}\n`);
457
461
  if (typeof block.next_step === "string")
458
462
  process.stderr.write(`${block.next_step}\n`);
463
+ // The server's next_step quotes the MANAGED price, because the live gate's
464
+ // `billedTool` reads the descriptor and never the credential mode. On a
465
+ // customer's own key that number is not what anyone pays, so say so instead
466
+ // of leaving a managed credit estimate as the last word on a BYOK preview.
467
+ // The `--max-credits` ceiling in that line is still required by the gate, so
468
+ // the command stays copy-pasteable rather than being edited into a 400.
469
+ const byok = readByokCredentialMode(data);
470
+ if (byok) {
471
+ process.stderr.write(`That estimate is the managed-key price. This run uses credential_mode ${byok}, so Oxygen charges no credits for it — your provider bills you directly. The live gate still requires --max-credits as a ceiling.\n`);
472
+ }
473
+ }
474
+ /**
475
+ * The tool run's credential mode when the call is NOT on Oxygen's managed key
476
+ * — the one fact that decides whether a quoted credit estimate means anything.
477
+ * Null for managed runs and for payloads with no billing block at all.
478
+ */
479
+ function readByokCredentialMode(data) {
480
+ const result = asPayloadRecord(asPayloadRecord(data)?.result);
481
+ const billing = asPayloadRecord(asPayloadRecord(result?.meta)?.billing);
482
+ const mode = billing?.credential_mode;
483
+ if (typeof mode !== "string" || mode.length === 0 || mode === "managed")
484
+ return null;
485
+ return mode;
459
486
  }
460
487
  // An `oxy_live_` key can only ever answer for the one workspace it is bound to,
461
488
  // so `orgs list` returns a single row and `orgs use` refuses — both truthfully,
@@ -554,7 +581,12 @@ function writeCreditsReceipt(data) {
554
581
  return;
555
582
  }
556
583
  if (typeof block.estimated_credits === "number") {
557
- process.stderr.write(`estimated ${block.estimated_credits.toLocaleString("en-US")} credits for a live run${remaining !== null ? `, ${remaining} available` : ""}\n`);
584
+ // A BYOK preview spends zero Oxygen credits, so quoting the managed
585
+ // estimate as "for a live run" is simply wrong; name the mode instead.
586
+ const byok = readByokCredentialMode(data);
587
+ process.stderr.write(byok
588
+ ? `estimated 0 Oxygen credits for a live run on your own key (${byok})${remaining !== null ? `, ${remaining} available` : ""}\n`
589
+ : `estimated ${block.estimated_credits.toLocaleString("en-US")} credits for a live run${remaining !== null ? `, ${remaining} available` : ""}\n`);
558
590
  }
559
591
  }
560
592
  // A disabled workflow is a customer's automation at zero, and `status:
@@ -1636,6 +1668,8 @@ function buildPublishingPostsListPath(options) {
1636
1668
  approval_status: options.approvalStatus,
1637
1669
  tag: options.tag,
1638
1670
  provider: options.provider,
1671
+ sender_account_id: options.sender,
1672
+ cursor: options.cursor,
1639
1673
  limit: options.limit,
1640
1674
  };
1641
1675
  for (const [key, value] of Object.entries(filters)) {
@@ -3606,7 +3640,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
3606
3640
  }));
3607
3641
  program
3608
3642
  .command("projects")
3609
- .description("Manage table projects. Inspect contents with `oxygen tables list --project <project>`.")
3643
+ .description("Manage table folders (projects). Inspect contents with `oxygen tables list --project <project>`.")
3610
3644
  .addCommand(new Command("list")
3611
3645
  .description("List table projects in the current tenant database.")
3612
3646
  .option("--json", "Print a JSON envelope.")
@@ -3643,11 +3677,12 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
3643
3677
  });
3644
3678
  }))
3645
3679
  .addCommand(new Command("delete")
3646
- .description("Delete an empty non-General table project. Defaults to dry-run; --live requires --confirm.")
3680
+ .description("Delete a non-General folder and its tables. Restoring any deleted table with `oxygen tables restore` before the purge date also restores the folder. Defaults to dry-run; --live requires --confirm and the preview's table IDs.")
3647
3681
  .argument("<project>", "Project slug or id.")
3648
3682
  .option("--dry-run", "Preview whether the project can be deleted without writing. Default.")
3649
3683
  .option("--live", "Delete the project after inspecting the dry-run preview. Requires --confirm.")
3650
3684
  .option("--confirm", "Confirm the live delete.")
3685
+ .option("--expected-table-ids <ids>", "Comma-separated table IDs from the preview. Required for nonempty folders; refuses if contents changed.")
3651
3686
  .option("--json", "Print a JSON envelope.")
3652
3687
  .action(async (project, options) => {
3653
3688
  await handleAsyncAction("projects delete", options, () => {
@@ -3660,6 +3695,9 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
3660
3695
  body: {
3661
3696
  mode,
3662
3697
  ...(mode === "live" ? { confirm: true } : {}),
3698
+ ...(options.expectedTableIds !== undefined
3699
+ ? { expected_table_ids: splitCommaList(options.expectedTableIds) }
3700
+ : {}),
3663
3701
  },
3664
3702
  });
3665
3703
  });
@@ -3761,6 +3799,8 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
3761
3799
  .option("--approval-status <status>", "Filter by draft, needs_approval, approved, or rejected.")
3762
3800
  .option("--tag <tag>", "Only posts carrying this workspace tag.")
3763
3801
  .option("--provider <provider>", "Filter by provider: linkedin, x, instagram, tiktok, facebook, or youtube.")
3802
+ .option("--sender <sender_account_id>", "Only posts belonging to this connected sender in the active workspace.")
3803
+ .option("--cursor <cursor>", "Continue from next_cursor with the same filters.")
3764
3804
  .option("--limit <n>", "Maximum posts to return.")
3765
3805
  .option("--json", "Print a JSON envelope.")
3766
3806
  .action(async (options) => {
@@ -3855,7 +3895,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
3855
3895
  }));
3856
3896
  }))
3857
3897
  .addCommand(new Command("delete")
3858
- .description("Delete a never-attempted post from the queue (draft, scheduled, or queued with no publish attempts). A post that was ever attempted — published, publishing, failed, or re-armed after a provider error — is refused: cancel it instead, so its attempt history is kept. To remove a LinkedIn post that already went live, use `oxygen posts delete`.")
3898
+ .description("Permanently delete the OXYGEN copy and its settled history, keeping the live platform post intact. Supports never-attempted draft/scheduled/queued posts, settled canceled unpublished posts, and settled published copies. Published deletions stay excluded from background discovery. Cancel failed or re-armed posts first. In-flight publishing, replies, amplification, and active amplification spend accounting remain protected. Live LinkedIn deletion is a separate action using `oxygen posts delete`.")
3859
3899
  .argument("<post_id>", "Scheduled post id.")
3860
3900
  .option("--json", "Print a JSON envelope.")
3861
3901
  .action(async (postId, options) => {
@@ -5034,7 +5074,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
5034
5074
  .addCommand(new Command("create")
5035
5075
  .description("Create a real Postgres-backed workspace table. Free — 0 Oxygen credits. To create a table and import a file in one step, use `tables import --create <name>`.")
5036
5076
  .argument("<name>", "Display name for the table.")
5037
- .requiredOption("--columns-json <json>", 'JSON array of column definitions, e.g. [{"key":"name","label":"Name","dataType":"text"},{"key":"domain","label":"Domain","dataType":"text"}]. Inspect an existing shape with `oxygen tables describe <table>`.')
5077
+ .requiredOption("--columns-json <json>", 'Required nonempty JSON array of column definitions, e.g. [{"key":"name","label":"Name","dataType":"text"},{"key":"domain","label":"Domain","dataType":"text"}]. Inspect an existing shape with `oxygen tables describe <table>`.')
5038
5078
  .option("--project <project>", "Project id or slug. Defaults to General.")
5039
5079
  .option("--json", "Print a JSON envelope.")
5040
5080
  .action(async (name, options) => {
@@ -6280,11 +6320,46 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
6280
6320
  program
6281
6321
  .command("knowledge")
6282
6322
  .description("Company knowledge wiki: slug-addressed pages, a [[wikilink]] graph, and a decision log.")
6323
+ .addCommand(new Command("folders")
6324
+ .description("Browse, create, and move nested folders for knowledge pages; slugs and wikilinks stay stable.")
6325
+ .addCommand(new Command("list")
6326
+ .description("List workspace folders and page locations, including empty folders. Read-only, 0 credits.")
6327
+ .option("--json", "Print a JSON envelope.")
6328
+ .action(async (options) => {
6329
+ await handleAsyncAction("knowledge folders list", options, () => requestOxygen("/api/cli/knowledge/folders"));
6330
+ }))
6331
+ .addCommand(new Command("create")
6332
+ .description("Create a persistent knowledge folder, optionally inside another folder.")
6333
+ .requiredOption("--name <name>", "Folder name.")
6334
+ .option("--parent <folder_id>", "Parent folder UUID from knowledge folders list. Omit for root.")
6335
+ .option("--json", "Print a JSON envelope.")
6336
+ .action(async (options) => {
6337
+ await handleAsyncAction("knowledge folders create", options, () => requestOxygen("/api/cli/knowledge/folders/create", {
6338
+ method: "POST",
6339
+ body: {
6340
+ name: options.name,
6341
+ ...(readOption(options.parent) ? { parentId: readOption(options.parent) } : {}),
6342
+ },
6343
+ }));
6344
+ }))
6345
+ .addCommand(new Command("move")
6346
+ .description("Move a folder and its contents to another folder or the workspace root.")
6347
+ .argument("<id>", "Folder UUID from knowledge folders list.")
6348
+ .requiredOption("--parent <folder_id|root>", "Destination folder UUID, or root.")
6349
+ .option("--json", "Print a JSON envelope.")
6350
+ .action(async (id, options) => {
6351
+ await handleAsyncAction("knowledge folders move", options, () => requestOxygen("/api/cli/knowledge/folders/move", {
6352
+ method: "POST",
6353
+ body: { id, parentId: options.parent === "root" ? null : options.parent },
6354
+ }));
6355
+ })))
6283
6356
  .addCommand(new Command("page")
6284
6357
  .description("Knowledge wiki pages (slug-addressed, revision-guarded).")
6285
6358
  .addCommand(new Command("upsert")
6286
6359
  .description("Create or update a knowledge wiki page.")
6287
6360
  .option("--slug <slug>", "Stable page slug to create or address. Omit to create by title.")
6361
+ .option("--folder <folder_id>", "Folder UUID, or root to remove folder placement. Omit to preserve the current folder.")
6362
+ .option("--create-only", "Fail if the page already exists instead of updating it.")
6288
6363
  .option("--id <page_id>", "Existing page UUID to update. Omit to create.")
6289
6364
  .option("--type <type>", "Page type (positioning, competitor, research_note, playbook, strategy, other). Defaults to other on create.")
6290
6365
  .option("--title <title>", "Page title. Required on create.")
@@ -6307,7 +6382,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
6307
6382
  });
6308
6383
  }))
6309
6384
  .addCommand(new Command("get")
6310
- .description("Read one knowledge wiki page by slug or UUID.")
6385
+ .description("Read one knowledge wiki page by slug or UUID. Read-only, 0 credits.")
6311
6386
  .argument("<slug_or_id>", "Page slug or UUID.")
6312
6387
  .option("--json", "Print a JSON envelope.")
6313
6388
  .action(async (slugOrId, options) => {
@@ -6317,7 +6392,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
6317
6392
  }));
6318
6393
  }))
6319
6394
  .addCommand(new Command("list")
6320
- .description("List knowledge wiki pages.")
6395
+ .description("List knowledge wiki pages. Read-only, 0 credits.")
6321
6396
  .option("--type <type>", "Filter by page type.")
6322
6397
  .option("--status <status>", "Filter by draft, active, or archived.")
6323
6398
  .option("--tags <csv>", "Comma-separated tags that must be present.")
@@ -6365,7 +6440,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
6365
6440
  });
6366
6441
  }))
6367
6442
  .addCommand(new Command("revisions")
6368
- .description("List the revision history of a knowledge wiki page, newest first.")
6443
+ .description("List the revision history of a knowledge wiki page, newest first. Read-only, 0 credits.")
6369
6444
  .argument("<slug_or_id>", "Page slug or UUID.")
6370
6445
  .option("--limit <n>", "Maximum revisions to return.")
6371
6446
  .option("--json", "Print a JSON envelope.")
@@ -6382,7 +6457,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
6382
6457
  });
6383
6458
  }))
6384
6459
  .addCommand(new Command("revision")
6385
- .description("Read one full revision snapshot of a knowledge wiki page.")
6460
+ .description("Read one full revision snapshot of a knowledge wiki page. Read-only, 0 credits.")
6386
6461
  .argument("<slug_or_id>", "Page slug or UUID.")
6387
6462
  .argument("<revision>", "Revision number.")
6388
6463
  .option("--json", "Print a JSON envelope.")
@@ -6404,7 +6479,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
6404
6479
  });
6405
6480
  })))
6406
6481
  .addCommand(new Command("search")
6407
- .description("Full-text search across knowledge wiki pages, ranked by relevance.")
6482
+ .description("Search wiki pages by meaning and keywords. Read-only, 0 credits. Plain language uses hybrid retrieval when available; check match_type and semantic_status. Quotes, OR, and exclusions stay lexical. If the platform-funded embedding allowance is unavailable, keyword search still works.")
6408
6483
  .argument("<query>", "Search text. Supports quoted phrases, OR, and -exclude.")
6409
6484
  .option("--type <type>", "Filter by page type.")
6410
6485
  .option("--status <status>", "Filter by draft, active, or archived.")
@@ -6418,7 +6493,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
6418
6493
  }));
6419
6494
  }))
6420
6495
  .addCommand(new Command("index")
6421
- .description("Compact wiki index: each page's slug, one-liner, tags, link degree, plus type/status counts over the whole wiki. Lists the 200 most recently updated pages by default.")
6496
+ .description("Compact wiki index: each page's slug, one-liner, tags, link degree, plus type/status counts over the whole wiki. Lists the 200 most recently updated pages by default. Read-only, 0 credits. semantic.budget is Oxygen's platform-funded embedding safety allowance; its dollar and query counters are Oxygen's spend, never charges against your credits.")
6422
6497
  .option("--limit <n>", "Maximum pages to list. Defaults to 200; hard cap is 1000. Counts always cover the whole wiki.")
6423
6498
  .option("--all", "List every page instead of the most recently updated window.")
6424
6499
  .option("--json", "Print a JSON envelope.")
@@ -6438,7 +6513,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
6438
6513
  });
6439
6514
  }))
6440
6515
  .addCommand(new Command("graph")
6441
- .description("Knowledge graph of pages and their [[wikilink]] edges. Pass --center to render one page's local neighborhood instead of the whole graph.")
6516
+ .description("Knowledge graph of pages and their [[wikilink]] edges. Pass --center to render one page's local neighborhood instead of the whole graph. Read-only, 0 credits.")
6442
6517
  .option("--max-nodes <n>", "Maximum nodes to include in the graph.")
6443
6518
  .option("--center <slug_or_id>", "Center the graph on one page and show only its neighborhood (local mode).")
6444
6519
  .option("--depth <n>", "Local-graph hop depth (1-3, default 1). Only applies with --center.")
@@ -6450,7 +6525,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
6450
6525
  }));
6451
6526
  }))
6452
6527
  .addCommand(new Command("lint")
6453
- .description("Structural wiki health report: orphans (unfilled seed stubs excluded), dead-ends, unresolved links, contradictions (disputed pages, conflicting trust tags, competing live-copy pages), missing canonicals, untagged pages, duplicate titles, oversized hubs, unfilled seed stubs (total plus the most-linked-to), researchable stubs, decisions missing a reversal condition, stale pages (untouched or superseded by newer sources), oversized bodies, and aged proposals.")
6528
+ .description("Read-only structural wiki health report, 0 credits: orphans (unfilled seed stubs excluded), dead-ends, unresolved links, contradictions (disputed pages, conflicting trust tags, competing live-copy pages), missing canonicals, untagged pages, duplicate titles, oversized hubs, unfilled seed stubs (total plus the most-linked-to), researchable stubs, decisions missing a reversal condition, stale pages (untouched or superseded by newer sources), oversized bodies, and aged proposals.")
6454
6529
  .option("--json", "Print a JSON envelope.")
6455
6530
  .action(async (options) => {
6456
6531
  await handleAsyncAction("knowledge lint", options, () => requestOxygen("/api/cli/knowledge/lint"));
@@ -6471,7 +6546,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
6471
6546
  .addCommand(new Command("log")
6472
6547
  .description("Knowledge decision and activity log.")
6473
6548
  .addCommand(new Command("list")
6474
- .description("List knowledge log entries, newest first.")
6549
+ .description("List knowledge log entries, newest first. Read-only, 0 credits.")
6475
6550
  .option("--cursor <cursor>", "Pagination cursor from a previous page.")
6476
6551
  .option("--limit <n>", "Maximum log entries to return.")
6477
6552
  .option("--json", "Print a JSON envelope.")
@@ -6495,7 +6570,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
6495
6570
  }));
6496
6571
  })))
6497
6572
  .addCommand(new Command("proposals")
6498
- .description("Draft change proposals against wiki pages, awaiting human review.")
6573
+ .description("Draft change proposals against wiki pages, awaiting human review. Read-only, 0 credits.")
6499
6574
  .option("--status <status>", "open, decided, or all. Defaults to open.")
6500
6575
  .option("--json", "Print a JSON envelope.")
6501
6576
  .action(async (options) => {
@@ -8756,7 +8831,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
8756
8831
  }));
8757
8832
  }))
8758
8833
  .addCommand(new Command("balance")
8759
- .description("Show the current plan, subscription entitlement, and managed credit balance: available and reserved credits, FIXED credits committed to recurring per-resource charges, and the FLEXIBLE free-to-spend remainder. `renewals_past_due` lists infrastructure renewals (managed-inbox domains, warm-ups) OXYGEN could NOT charge this month, what they cost, and the exact top-up that clears them — nothing is cancelled and billing retries automatically after a top-up. `warnings` also flags a recurring_shortfall when your connected infrastructure costs more per month than the plan grants. After failed-payment grace expires, mutations stop while read/export and billing recovery remain available. Recovery: https://oxygen-agent.com/billing. Policy: https://oxygen-agent.com/docs/safety/billing.")
8834
+ .description("Show the current plan, subscription entitlement, and managed credit balance: available and reserved credits, FIXED credits committed to recurring per-resource charges, and the FLEXIBLE free-to-spend remainder. `next_renewal` answers \"will I be charged?\": read `will_charge` first — true only when this workspace's own Stripe plan is going to charge its card, in which case `charge_at` and the plan's monthly list price in money say when and roughly how much (the exact amount, with any promotion code or tax, is on the invoice); `period_ends_at` is the trial or period end either way, `cancellation_scheduled` says whether a cancellation is already set, and `stop_command` names the one command that changes it (`billing cancel`, undone by `billing resume`) or is null when nothing here can stop it (staff-invoiced, billed through another workspace, not active). `renewals_past_due` lists infrastructure renewals (managed-inbox domains, warm-ups) OXYGEN could NOT charge this month, what they cost, and the exact top-up that clears them — nothing is cancelled and billing retries automatically after a top-up. `warnings` also flags a recurring_shortfall when your connected infrastructure costs more per month than the plan grants. After failed-payment grace expires, mutations stop while read/export and billing recovery remain available. Recovery: https://oxygen-agent.com/billing. Policy: https://oxygen-agent.com/docs/safety/billing.")
8760
8835
  .option("--json", "Print a JSON envelope.")
8761
8836
  .action(async (options) => {
8762
8837
  await handleAsyncAction("billing balance", options, () => requestOxygen("/api/cli/billing/balance"));
@@ -8768,7 +8843,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
8768
8843
  await handleAsyncAction("billing commitments", options, () => requestOxygen("/api/cli/billing/commitments"));
8769
8844
  }))
8770
8845
  .addCommand(new Command("seats")
8771
- .description("Show sending capacity per channel: how many seats are purchased, how many are grandfathered, how many senders are connected, and how many are left. Sending seats are what let you CONNECT a sender — a LinkedIn account, WhatsApp number, phone number, or email mailbox — and they are billed in dollars on their own subscription, separate from your plan and separate from credits. You do not need a plan to buy seats. A grandfathered value of null means unlimited for that channel. Email mailbox allocation is read from this workspace's own database; when it cannot be read, the email seat's allocated/available/can_connect read null and `email_sender_allocation` is `tenant_unavailable`. Read-only, 0 Oxygen credits.")
8846
+ .description("Show sending capacity per channel: how many seats are purchased, how many are grandfathered, how many senders are connected, and how many are left. Sending seats are what let you CONNECT a sender — a LinkedIn account, WhatsApp number, phone number, or email mailbox — and they are billed in dollars on their own subscription, separate from your plan and separate from credits. You do not need a plan to buy seats. Seats belong to the billing owner: a workspace linked to another organization's plan with `orgs billing-link` connects senders against that organization's seats and buys them there. A grandfathered value of null means unlimited for that channel. Email mailbox allocation is read from this workspace's own database; when it cannot be read, the email seat's allocated/available/can_connect read null and `email_sender_allocation` is `tenant_unavailable`. Read-only, 0 Oxygen credits.")
8772
8847
  .option("--json", "Print a JSON envelope.")
8773
8848
  .action(async (options) => {
8774
8849
  await handleAsyncAction("billing seats", options, () => requestOxygen("/api/cli/billing/seats"));
@@ -9233,10 +9308,48 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
9233
9308
  });
9234
9309
  }))
9235
9310
  .addCommand(new Command("primary-providers")
9236
- .description("Board of the PRIMARY managed external data providers (enrichment, people/company search, web search, scraping, signals, AI-column web grounding, LLM inference): per provider health probe, 7d traffic, balance, 30d/7d COGS, rate policy, spend ceilings, breaker, and posture, plus the platform runaway guards. Read-only — never calls a provider. Staff only.")
9311
+ .description("Board of the PRIMARY managed external data providers (enrichment, people/company search, web search, scraping, signals, AI-column web grounding, LLM inference): per provider health probe, 7d traffic, balance, 30d/7d COGS, rate policy, spend ceilings, breaker, and posture, plus the platform runaway guards and how old each snapshot is. Reads the snapshots the crons write; --refresh re-runs those same zero-credit snapshots first. Staff only.")
9312
+ .option("--refresh", "Re-run the zero-credit snapshot work the three crons run (provider health probes, managed balances, cost snapshot) before reading the board. No paid provider call and no credit spend; takes up to a few minutes.")
9313
+ .option("--stages <csv>", "Limit --refresh to these stages: health, balances, costs. Defaults to all three.")
9237
9314
  .option("--json", "Print a JSON envelope.")
9238
9315
  .action(async (options) => {
9239
- await handleAsyncAction("admin primary-providers", options, () => requestOxygen("/api/cli/admin/primary-providers"));
9316
+ const stages = readCsvOption(options.stages);
9317
+ if (stages.length > 0 && !options.refresh) {
9318
+ // Fail with the envelope rather than reading a board the caller
9319
+ // believes it just refreshed.
9320
+ emitCliFailure("admin primary-providers", new OxygenError("invalid_request", "--stages only applies to --refresh. Add --refresh to re-run those snapshots.", { exitCode: 2 }));
9321
+ return;
9322
+ }
9323
+ let failed = false;
9324
+ const board = await requestOxygen("/api/cli/admin/primary-providers", options.refresh
9325
+ ? {
9326
+ method: "POST",
9327
+ // The route budgets maxDuration = 300 and the balance stage
9328
+ // walks every fetcher sequentially at 15s each, so the
9329
+ // default 120s client budget aborts a refresh the server
9330
+ // finishes — the operator reads a failure for work that
9331
+ // succeeded and re-runs the outbound probes.
9332
+ timeoutMs: 300_000,
9333
+ body: { refresh: true, ...(stages.length > 0 ? { stages } : {}) },
9334
+ }
9335
+ : undefined).catch((error) => {
9336
+ emitCliFailure("admin primary-providers", error);
9337
+ failed = true;
9338
+ return null;
9339
+ });
9340
+ // Only a transport failure ends the command silently; an empty
9341
+ // payload still gets rendered (or enveloped) rather than exiting 0
9342
+ // with nothing printed.
9343
+ if (failed)
9344
+ return;
9345
+ if (options.json) {
9346
+ emitSuccess("admin primary-providers", board, options);
9347
+ return;
9348
+ }
9349
+ // Without this the default invocation printed ~1,500 lines of raw
9350
+ // JSON while the MCP tool printed a worst-first summary of the same
9351
+ // payload — same board, two different products.
9352
+ process.stdout.write(`${renderPrimaryProviderBoard(board, { binary: binaryName })}\n`);
9240
9353
  }))
9241
9354
  .addCommand(new Command("spend")
9242
9355
  .description("Global managed-provider spend limiter: burn, ceilings, breakers. Staff only.")
@@ -10450,7 +10563,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
10450
10563
  }));
10451
10564
  program
10452
10565
  .command("find")
10453
- .description("One-shot contact and company lookups over enrichment waterfalls (no table). dry_run previews for free; live spends and needs --max-credits.")
10566
+ .description("One-shot contact and company lookups over enrichment waterfalls (no table). dry_run previews the plan for free (which lanes would run, in order, and what each costs — not the answer); live spends and needs --max-credits.")
10454
10567
  .addCommand(new Command("email")
10455
10568
  .description("Find a person's work email via an input-aware multi-provider waterfall: a LinkedIn URL, name+domain, or first/last+domain each select the best provider profile, with an email-pattern pre-step and verification. dry_run shows the resolved profile + chain for free.")
10456
10569
  .option("--linkedin-url <url>", "Person LinkedIn profile URL (strongest signal).")
@@ -10459,7 +10572,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
10459
10572
  .option("--last-name <name>", "Person last name.")
10460
10573
  .option("--company-domain <domain>", "Company apex domain, e.g. acme.com.")
10461
10574
  .option("--company-name <name>", "Company name.")
10462
- .option("--mode <mode>", "dry_run (default) or live. live spends credits.")
10575
+ .option("--mode <mode>", "dry_run (default) previews the plan and spends nothing; live spends credits and returns the answer.")
10463
10576
  .option("--max-credits <credits>", "Spend ceiling. Required for --mode live.")
10464
10577
  .option("--json", "Print a JSON envelope.")
10465
10578
  .action(async (options) => {
@@ -10473,7 +10586,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
10473
10586
  .option("--company-domain <domain>", "Company apex domain.")
10474
10587
  .option("--company-name <name>", "Company name.")
10475
10588
  .option("--verify", "Verify the found number with ClearoutPhone — adds line_type/carrier and keeps the number on a non-verdict (only a genuine 'not valid' is discarded).")
10476
- .option("--mode <mode>", "dry_run (default) or live. live spends credits.")
10589
+ .option("--mode <mode>", "dry_run (default) previews the plan and spends nothing; live spends credits and returns the answer.")
10477
10590
  .option("--max-credits <credits>", "Spend ceiling. Required for --mode live.")
10478
10591
  .option("--json", "Print a JSON envelope.")
10479
10592
  .action(async (options) => {
@@ -10486,7 +10599,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
10486
10599
  .option("--company-domain <domain>", "Company apex domain.")
10487
10600
  .option("--company-name <name>", "Company name.")
10488
10601
  .option("--company-linkedin-url <url>", "Company LinkedIn URL.")
10489
- .option("--mode <mode>", "dry_run (default) or live. live spends credits.")
10602
+ .option("--mode <mode>", "dry_run (default) previews the plan and spends nothing; live spends credits and returns the answer.")
10490
10603
  .option("--max-credits <credits>", "Spend ceiling. Required for --mode live.")
10491
10604
  .option("--json", "Print a JSON envelope.")
10492
10605
  .action(async (options) => {
@@ -10498,7 +10611,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
10498
10611
  .option("--name <name>", "Company name.")
10499
10612
  .option("--linkedin-url <url>", "Company LinkedIn URL.")
10500
10613
  .option("--fields <fields>", "Comma-separated company fields. Defaults to domain,linkedin_url,headcount,industry.")
10501
- .option("--mode <mode>", "dry_run (default) or live. live spends credits.")
10614
+ .option("--mode <mode>", "dry_run (default) previews the plan and spends nothing; live spends credits and returns the answer.")
10502
10615
  .option("--max-credits <credits>", "Spend ceiling. Required for --mode live.")
10503
10616
  .option("--json", "Print a JSON envelope.")
10504
10617
  .action(async (options) => {
@@ -10510,7 +10623,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
10510
10623
  .addCommand(new Command("email")
10511
10624
  .description("Verify one or more email addresses. MillionVerifier answers first; addresses on catch-all (accept-all) domains, which it can only flag as risky, escalate automatically to BounceBan to get a real answer. Returns valid / invalid / catch_all / unknown per address.")
10512
10625
  .argument("<emails...>", "One or more email addresses to verify.")
10513
- .option("--mode <mode>", "dry_run (default) or live. live spends credits.")
10626
+ .option("--mode <mode>", "dry_run (default) previews the plan and spends nothing; live spends credits and returns the answer.")
10514
10627
  .option("--approved", "Same as --mode live. Accepted because the global help footer names --approved as the way to authorize a credit-spending command.")
10515
10628
  .option("--max-credits <credits>", "Spend ceiling for the whole run. Required for --mode live. Run the free dry_run first — it returns estimate.recommended_max_credits, the value that guarantees every catch-all address still gets escalated.")
10516
10629
  .option("--json", "Print a JSON envelope.")
@@ -11330,7 +11443,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
11330
11443
  });
11331
11444
  }))
11332
11445
  .addCommand(new Command("delete")
11333
- .description("Delete a LinkedIn post the connected account authored — a REAL, irreversible public write. Refuses without --approved (exit 7). --post is the composite social_id returned when the post was created (or by `oxygen posts get`), NOT the activity URN. To remove a post that is still only SCHEDULED in Oxygen, use `oxygen publishing posts delete` instead.")
11446
+ .description("Delete a LinkedIn post the connected account authored — a REAL, irreversible public write. Refuses without --approved (exit 7). --post is the composite social_id returned when the post was created (or by `oxygen posts get`), NOT the activity URN. To remove only the OXYGEN copy, including a settled published copy, while keeping LinkedIn intact, use `oxygen publishing posts delete`.")
11334
11447
  .requiredOption("--post <social_id>", "Composite post social_id from `oxygen posts get` (the id returned when the post was created).")
11335
11448
  .option("--account <ref>", "Sender account that authored the post (sender id, connection id, or Unipile account id). Omit for the org default.")
11336
11449
  .option("--approved", "Actually delete the post. Without it the command refuses and deletes nothing.")
@@ -12411,7 +12524,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
12411
12524
  program.addCommand(new Command("messages")
12412
12525
  .description("Cross-channel message corpus: every individual email + LinkedIn + WhatsApp message as one searchable stream (Postgres full-text search over bodies, keyset paginated by recency), plus reply-rate/campaign analytics. Message-level, unlike inbox (conversation-level triage).")
12413
12526
  .addCommand(new Command("query")
12414
- .description("Query messages across channels newest first, or relevance-ranked when -q is set. --channel all merges email + LinkedIn + WhatsApp (narrow with --channels); a campaign filter (--sequence-id) restricts to email. Filter by direction, account, contact, status, and date range.")
12527
+ .description("Query messages across channels newest first, or relevance-ranked when -q is set. --channel all merges email + LinkedIn + WhatsApp (narrow with --channels); a campaign filter (--sequence-id) restricts to email. Filter by direction, account, contact, status, and date range. Scope: the Messages store (Unibox conversations, every native email since v1.927.0 linked to its campaign; earlier sends without a provider thread id are not). The complete send ledger of a sequence is `sequences events --kind sent`; its counts are `sequences stats`.")
12415
12528
  .option("-q, --query <text>", "Full-text search over message bodies (relevance-ranked).")
12416
12529
  .option("--channel <channel>", "Channel: all (merged, default), email, linkedin, or whatsapp.")
12417
12530
  .option("--channels <list>", "channel=all only: comma-separated channels to include (email,linkedin,whatsapp). Empty = all three.")
@@ -12763,14 +12876,14 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
12763
12876
  .option("--email-definition-file <path>", "Path to a JSON file with the email content spec (subjects/bodies/delays/subsequences) compiled to an Instantly campaign on start.")
12764
12877
  .option("--max-credits <n>", "Credit cap for the LinkedIn track (also set when starting).")
12765
12878
  .option("--max-live-sends <n>", "External-action ceiling for 0-credit email/WhatsApp/CRM-task tracks (positive integer). Required to start any of them live.")
12766
- .option("--max-emails-per-mailbox-per-day <n>", "Per-mailbox daily email cap (positive integer) applied across the sending pool.")
12879
+ .option("--max-emails-per-mailbox-per-day <n>", "Per-mailbox daily email cap (positive integer) applied across the sending pool; also sets each mailbox's send pace (send window ÷ cap, catching up when a slot is missed). Daily capacity = this cap × sendable mailboxes; `sequences stats` reports it under daily_budget.")
12767
12880
  .option("--send-window-file <path>", "Path to a JSON file with the sequence-level email send window: { timezone, days?, start, end, timezone_mode?, recipient_timezone_column? }.")
12768
12881
  .option("--max-emails-per-day <n>", "Sequence-wide daily live-send fleet cap (positive integer) across every sender/mailbox.")
12769
12882
  .option("--max-new-enrollments-per-day <n>", "Daily drip cap on NEW first-touch leads the planner starts (positive integer).")
12770
12883
  .option("--sequence-prioritization <mode>", "Under a tight daily budget, serve 'followups' or 'new_leads' first.")
12771
12884
  .option("--esp-matching <mode>", "Native-email ESP matching: 'prefer' (DEFAULT) biases toward a mailbox on the recipient's own provider, falling back to any; 'off' rotates mailboxes freely; 'strict' requires a same-provider mailbox and defers the send when none exists.")
12772
12885
  .option("--sender-failover <mode>", "What happens when an enrollment's LinkedIn/WhatsApp sender goes unavailable: 'wait' (default) resumes when the sender recovers; 'rebind' moves UNTOUCHED enrollments (no thread, no pending invite, no lead binding) to the least-loaded healthy sender in the pool after ~5h of confirmed outage.")
12773
- .option("--email-min-gap-minutes <n>", "Minimum minutes between two live emails from the SAME mailbox for this sequence (Instantly's 'time gap between emails'; integer 0-720, 0 = none). A humanization gap that only WIDENS the derived per-mailbox spacing — it never bypasses the daily caps or the send window.")
12886
+ .option("--email-min-gap-minutes <n>", "Minimum minutes between two live emails from the SAME mailbox for this sequence (Instantly's 'time gap between emails'; integer 0-720, 0 = none). A humanization FLOOR only: Oxygen already spaces each mailbox's sends evenly across its send window (window length ÷ per-mailbox daily cap, e.g. 9h ÷ 15 = 36 min, tightening through the day to catch up on any lost slot), so it only binds when it exceeds the derived spacing — it also floors the late-day catch-up, so 12 keeps every gap ≥12 min. It never bypasses the daily caps or the send window. To send MORE per day, raise --max-emails-per-mailbox-per-day, add mailboxes, or widen the send window.")
12774
12887
  .option("--opportunity-value <usd>", "Estimated USD value of one positive-reply opportunity (>= 0). Analytics-only: sequence stats multiply it by the positive-reply count to report pipeline $ (stats.opportunities). Never gates a send or spends a credit.")
12775
12888
  .option("--mailboxes <ids>", "Per-sequence sending-account allowlist for native email (Instantly's 'Accounts to use'): comma-separated mailbox ids. Native email sends rotate ONLY over these mailboxes instead of the whole pool. Omit for the whole pool. Note: a narrow allowlist plus --esp-matching strict can starve sends when no allowed same-provider mailbox exists (surfaced as blocked defer reasons in stats).")
12776
12889
  .option("--sender-profiles <ids>", "Send from these sender profiles (unified LinkedIn/WhatsApp/inbox identities): comma-separated profile ids. The server resolves each into its senders + inboxes. Live start is blocked if a selected profile lacks an account for a channel the journey uses.")
@@ -12865,14 +12978,14 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
12865
12978
  .option("--clear-email", "Remove the email binding from the sequence (draft only).")
12866
12979
  .option("--max-credits <n>", "Credit cap for the LinkedIn track (draft only).")
12867
12980
  .option("--max-live-sends <n>", "Draft-time external-action ceiling for 0-credit email/WhatsApp/CRM-task tracks (positive integer). After first start, change it through `sequences start`.")
12868
- .option("--max-emails-per-mailbox-per-day <n>", "Per-mailbox daily email cap (positive integer) applied across the sending pool.")
12981
+ .option("--max-emails-per-mailbox-per-day <n>", "Per-mailbox daily email cap (positive integer) applied across the sending pool; also sets each mailbox's send pace (send window ÷ cap, catching up when a slot is missed). Daily capacity = this cap × sendable mailboxes; `sequences stats` reports it under daily_budget.")
12869
12982
  .option("--send-window-file <path>", "Path to a JSON file with the sequence-level email send window: { timezone, days?, start, end, timezone_mode?, recipient_timezone_column? }.")
12870
12983
  .option("--max-emails-per-day <n>", "Sequence-wide daily live-send fleet cap (positive integer) across every sender/mailbox.")
12871
12984
  .option("--max-new-enrollments-per-day <n>", "Daily drip cap on NEW first-touch leads the planner starts (positive integer).")
12872
12985
  .option("--sequence-prioritization <mode>", "Under a tight daily budget, serve 'followups' or 'new_leads' first.")
12873
12986
  .option("--esp-matching <mode>", "Native-email ESP matching: 'prefer' (DEFAULT) biases toward a mailbox on the recipient's own provider, falling back to any; 'off' rotates mailboxes freely; 'strict' requires a same-provider mailbox and defers the send when none exists.")
12874
12987
  .option("--sender-failover <mode>", "What happens when an enrollment's LinkedIn/WhatsApp sender goes unavailable: 'wait' (default) resumes when the sender recovers; 'rebind' moves UNTOUCHED enrollments (no thread, no pending invite, no lead binding) to the least-loaded healthy sender in the pool after ~5h of confirmed outage.")
12875
- .option("--email-min-gap-minutes <n>", "Minimum minutes between two live emails from the SAME mailbox for this sequence (Instantly's 'time gap between emails'; integer 0-720, 0 = none). A humanization gap that only WIDENS the derived per-mailbox spacing — it never bypasses the daily caps or the send window.")
12988
+ .option("--email-min-gap-minutes <n>", "Minimum minutes between two live emails from the SAME mailbox for this sequence (Instantly's 'time gap between emails'; integer 0-720, 0 = none). A humanization FLOOR only: Oxygen already spaces each mailbox's sends evenly across its send window (window length ÷ per-mailbox daily cap, e.g. 9h ÷ 15 = 36 min, tightening through the day to catch up on any lost slot), so it only binds when it exceeds the derived spacing — it also floors the late-day catch-up, so 12 keeps every gap ≥12 min. It never bypasses the daily caps or the send window. To send MORE per day, raise --max-emails-per-mailbox-per-day, add mailboxes, or widen the send window.")
12876
12989
  .option("--opportunity-value <usd>", "Estimated USD value of one positive-reply opportunity (>= 0). Analytics-only: sequence stats multiply it by the positive-reply count to report pipeline $ (stats.opportunities). Never gates a send or spends a credit.")
12877
12990
  .option("--mailboxes <ids>", "Per-sequence sending-account allowlist for native email (Instantly's 'Accounts to use'): comma-separated mailbox ids. Native email sends rotate ONLY over these mailboxes instead of the whole pool. Note: a narrow allowlist plus --esp-matching strict can starve sends when no allowed same-provider mailbox exists (surfaced as blocked defer reasons in stats).")
12878
12991
  .option("--sender-profiles <ids>", "Send from these sender profiles (unified LinkedIn/WhatsApp/inbox identities): comma-separated profile ids. The server resolves each into its senders + inboxes. Live start is blocked if a selected profile lacks an account for a channel the journey uses.")
@@ -13090,7 +13203,9 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
13090
13203
  .description("List a launched campaign's table-backed contacts with full source variables, durable engagement stage, and factual or assigned sender. Contacts removed from this campaign are hidden; source rows remain intact.")
13091
13204
  .argument("<sequence>", "Sequence id or slug.")
13092
13205
  .option("--contact-state <state>", "Filter: all, not_contacted, contacted, connected, replied, or positive_reply.", "all")
13093
- .option("--limit <n>", "Maximum contacts to return (1-500).", "100")
13206
+ .option("--limit <n>", "Maximum contacts per page (1-500). Page with --cursor (next_cursor from the previous page) or --offset.", "100")
13207
+ .option("--cursor <c>", "Pagination cursor from the previous page's next_cursor.")
13208
+ .option("--offset <n>", "Skip this many contacts before the page (0-based); an absolute alternative to --cursor.")
13094
13209
  .option("--json", "Print a JSON envelope.")
13095
13210
  .action(async (sequence, options) => {
13096
13211
  await handleAsyncAction("sequences contacts", options, () => {
@@ -13106,6 +13221,20 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
13106
13221
  contact_state: contactState,
13107
13222
  limit: String(limit),
13108
13223
  });
13224
+ const cursor = readOption(options.cursor);
13225
+ if (cursor)
13226
+ params.set("cursor", cursor);
13227
+ const offsetRaw = readOption(options.offset);
13228
+ if (offsetRaw !== null) {
13229
+ const offset = Number(offsetRaw);
13230
+ if (!Number.isInteger(offset) || offset < 0) {
13231
+ throw new OxygenError("invalid_offset", "--offset must be a non-negative integer.", {
13232
+ details: { offset: offsetRaw },
13233
+ exitCode: 2,
13234
+ });
13235
+ }
13236
+ params.set("offset", String(offset));
13237
+ }
13109
13238
  return requestOxygen(`/api/cli/sequences/${encodeURIComponent(sequence)}/contacts?${params.toString()}`);
13110
13239
  });
13111
13240
  }))
@@ -13198,7 +13327,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
13198
13327
  });
13199
13328
  }))
13200
13329
  .addCommand(new Command("stats")
13201
- .description("Show one sequence's persisted launch status and current operational state separately, including reasons, open work, lifetime sent/terminal-failed/deferral counts, and the funnel.")
13330
+ .description("Show one sequence's persisted launch status and current operational state separately, including reasons, open work, lifetime sent/terminal-failed/deferral counts, and the funnel. daily_budget carries today's sends against the caps; operational.reasons carries why work is waiting (outside_send_window, mailbox_spacing, daily_send_cap_reached, ...). For a per-day send series use `sequences analytics --sequence <id> --range <window>`.")
13202
13331
  .argument("<sequence>", "Sequence id or slug.")
13203
13332
  .option("--json", "Print a JSON envelope.")
13204
13333
  .action(async (sequence, options) => {
@@ -14147,7 +14276,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
14147
14276
  program.addCommand(new Command("mailboxes")
14148
14277
  .description("Native email sending pool: register/refresh Google/Microsoft mailboxes (including secure local credential-file transfer), pause/disable inboxes, connect EmailGuard monitoring, and inspect/control OXYGEN Warm-up. Fresh managed InboxKit Google/Microsoft/Azure orders activate exact-scope warm-up automatically after provisioning under their approved default-on add-on. Google reuses the managed credential just in time; Microsoft/Azure uses native export. Standalone warm-up plans and credit caps are for BYOK/imported mailboxes, managed opt-outs, or later separate enrollment. OXYGEN Warm-up never owns campaign dispatch. Safe import preflight: run `oxygen mailboxes compatibility --catalog-only --json`, then `oxygen mailboxes import --file <path> --validate-only --json` (add the credential source flags shown by import help when the file contains credentials).")
14149
14278
  .addCommand(new Command("list")
14150
- .description("List the org's sending mailboxes with provider, status, warmup state, source (managed = bought through Oxygen, byok = bring-your-own), worker-owned connectionRepair and warmupRepair state, and a pool overview (including counts by source). mode=automatic means OXYGEN owns the next bounded retry — do not ask for browser consent or disable/re-enable an existing warm-up seat. To see only Google/Microsoft mailboxes that still need OAuth connection, including attempt budgets and manual remedies, use `oxygen mailboxes oauth-health --json`.")
14279
+ .description("List the org's sending mailboxes with provider, status, warmup state, source (managed = bought through Oxygen, byok = bring-your-own), worker-owned connectionRepair and warmupRepair state, and a pool overview (including counts by source). Read each mailbox's warmupTruth for warm-up (state, last_send, pause, dispatch, next_action with the exact command); warmupState is only the stored rail token and reads `error` for a provider-paused seat. mode=automatic means OXYGEN owns the next bounded retry — do not ask for browser consent or disable/re-enable an existing warm-up seat. To see only Google/Microsoft mailboxes that still need OAuth connection, including attempt budgets and manual remedies, use `oxygen mailboxes oauth-health --json`.")
14151
14280
  .option("--status <status>", "Filter by status: active, paused, disabled, or provisioning (ordered, still being set up).")
14152
14281
  .option("--tag <tags>", "Comma-separated workspace tags — matches mailboxes carrying ANY of these tags (see `oxygen tags list`).")
14153
14282
  .option("--json", "Print a JSON envelope.")
@@ -14870,7 +14999,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
14870
14999
  });
14871
15000
  }))
14872
15001
  .addCommand(new Command("status")
14873
- .description("Read warmup analytics back from the rail each mailbox is enrolled on and update its state in the pool. 0 Oxygen credits and no campaign sends, but it pauses a mailbox whose warmup has run away. Targets the whole pool unless --mailboxes is given.")
15002
+ .description("Read warmup analytics back from the rail each mailbox is enrolled on and refresh each mailbox's warmupTruth (state, last send, pause reason, dispatch counters, next_action). A read: 0 Oxygen credits, no campaign sends, no change to your configuration; the one write OXYGEN may make is its own breaker pausing a mailbox proven to be dispatching far above its ramp. Targets the whole pool unless --mailboxes is given. For a per-mailbox read without a rail sync, use `oxygen mailboxes get <address> --json`.")
14874
15003
  .option("--mailboxes <list>", "Comma-separated mailbox ids or addresses to sync. Omit to sync the whole pool.")
14875
15004
  .option("--provider <name>", "Optional machine routing filter. Omit it for OXYGEN Warm-up; pass trulyinbox only to read inboxes still warming on the retired rail.")
14876
15005
  .option("--dry-run", "Skip the provider call (mailboxes marked pending).")
@@ -14912,9 +15041,9 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
14912
15041
  }));
14913
15042
  })));
14914
15043
  program.addCommand(new Command("deliverability")
14915
- .description("External email deliverability: fleet reputation health and DIRECTIONAL inbox-placement (spam) tests via EmailGuard, Zapmail, or an explicitly configured SendKit dev canary. This group creates one-off placement probes; for continuous EmailGuard account monitoring use `oxygen mailboxes emailguard connect`. Placement tests are approval-gated paid runs (managed bills Oxygen credits; BYOK = 0 Oxygen credits — Zapmail BYOK bills your Zapmail wallet ~$2/test). SendKit is never auto-selected or generally available: it is a 0-credit, one-test canary for the configured eligible connected sender only.")
15044
+ .description("External email deliverability: fleet reputation health and DIRECTIONAL inbox-placement (spam) tests via EmailGuard, Zapmail, or an explicitly configured SendKit dev canary. Start with `placement-test list` and `placement-test get` to inspect existing results for 0 credits without creating or sending a test. For continuous EmailGuard account monitoring use `oxygen mailboxes emailguard connect`. Placement tests are approval-gated paid runs (managed bills Oxygen credits; BYOK = 0 Oxygen credits — Zapmail BYOK bills your Zapmail wallet ~$2/test). SendKit is never auto-selected or generally available: it is a 0-credit, one-test canary for the configured eligible connected sender only.")
14916
15045
  .addCommand(new Command("placement-test")
14917
- .description("Directional inbox-placement (spam) tests: create, separately approve EmailGuard's exact seed send, then poll results.")
15046
+ .description("Inspect saved directional inbox-placement results with list/get (0 credits; no test email is sent). New tests use preview and approval through run/send.")
14918
15047
  .addCommand(new Command("run")
14919
15048
  .description("Create a placement test for one sending mailbox. Without --approved this returns a cost PREVIEW. EmailGuard creation returns exact seeds + phrase but sends nothing; next run `placement-test send <id>` to preview and approve that external email. Zapmail owns its probe delivery and completes async (2-24h). SendKit requires explicit --provider sendkit plus --subject and --body, and is available only for the configured dev canary with an eligible connected sender. Its approval creates AND sends one probe with no separate send step: preview first, then re-run the same mailbox, provider, subject, and body with --approved --max-credits 0 --plan <plan_hash>.")
14920
15049
  .argument("<mailbox>", "Sending mailbox address to test (e.g. ada@send.acme.com).")
@@ -14969,7 +15098,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
14969
15098
  }));
14970
15099
  }))
14971
15100
  .addCommand(new Command("list")
14972
- .description("List recent directional inbox-placement tests with status, health provider, billing mode, seed set, score, and credits used.")
15101
+ .description("Inspect recent directional inbox-placement tests, their status, results, and credits used. The returned web link opens Accounts; use get for an exact result link. Costs 0 credits and never creates or sends a test; active tests may refresh their observations.")
14973
15102
  .option("--limit <n>", "Max rows (default 50, cap 200).")
14974
15103
  .option("--json", "Print a JSON envelope.")
14975
15104
  .action(async (options) => {
@@ -14979,7 +15108,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
14979
15108
  });
14980
15109
  }))
14981
15110
  .addCommand(new Command("get")
14982
- .description("Get one directional inbox-placement test by id — status, health provider, seed inboxes, per-provider results, and score. Zapmail results land async (2-24h); keep polling while status is awaiting_results.")
15111
+ .description("Inspect one saved directional inbox-placement test by id: status, seed counts, per-provider results, and a web result link. Costs 0 credits and never creates or sends a test; active tests may refresh their observations. Zapmail results land async (2-24h); keep polling while status is awaiting_results.")
14983
15112
  .argument("<id>", "Placement test id.")
14984
15113
  .option("--json", "Print a JSON envelope.")
14985
15114
  .action(async (id, options) => {
@@ -16185,7 +16314,7 @@ Full trigger schema: oxygen workflows schema --subject trigger --json
16185
16314
  }
16186
16315
  }))
16187
16316
  .addCommand(new Command("mcp")
16188
- .description("Publish workflows as dynamic MCP tools (oxygen_workflow_<slug>) — the 'Clay Functions' pattern.")
16317
+ .description("Expose hosted Workflows as dynamic MCP tools (oxygen_workflow_<slug>). For reusable table Functions, use oxygen functions.")
16189
16318
  .addCommand(new Command("enable")
16190
16319
  .description("Publish an active workflow as a callable MCP tool so any MCP client can run it. The published tool appears to MCP clients as oxygen_workflow_<slug>.")
16191
16320
  .argument("<workflow>", "Workflow id, slug, or name.")
@@ -16286,6 +16415,9 @@ Full trigger schema: oxygen workflows schema --subject trigger --json
16286
16415
  await handleAsyncAction("skills install", options, () => installAgentSkills(options));
16287
16416
  }));
16288
16417
  applyOxygenHelp(program, binaryName);
16418
+ registerVisualCommands(program, handleAsyncAction);
16419
+ registerFunctionsCommands(program, handleAsyncAction);
16420
+ registerUgcCommands(program, handleAsyncAction);
16289
16421
  return program;
16290
16422
  }
16291
16423
  /**
@@ -23463,7 +23595,10 @@ function buildContextAssetUpsertBody(options) {
23463
23595
  function buildKnowledgePageUpsertBody(options) {
23464
23596
  const tags = readCsvOption(options.tags);
23465
23597
  const expectedRevision = readPositiveInt(options.expectedRevision);
23598
+ const folder = readOption(options.folder);
23466
23599
  return {
23600
+ ...(folder ? { folderId: folder === "root" ? null : folder } : {}),
23601
+ ...(options.createOnly ? { createOnly: true } : {}),
23467
23602
  ...(readOption(options.slug) ? { slug: readOption(options.slug) } : {}),
23468
23603
  ...(readOption(options.id) ? { id: readOption(options.id) } : {}),
23469
23604
  ...(readOption(options.type) ? { type: readOption(options.type) } : {}),
@@ -0,0 +1,6 @@
1
+ import { Command } from "commander";
2
+ type Handle = (command: string, options: {
3
+ json?: boolean;
4
+ }, action: () => Promise<unknown>) => Promise<void>;
5
+ export declare function registerUgcCommands(program: Command, handle: Handle): void;
6
+ export {};