@oxygen-agent/cli 1.591.1 → 1.615.0

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 (28) hide show
  1. package/README.md +1 -1
  2. package/dist/command-manifest.d.ts +18 -0
  3. package/dist/command-manifest.js +106 -5
  4. package/dist/help.js +2 -1
  5. package/dist/index.js +273 -38
  6. package/dist/skills.d.ts +6 -0
  7. package/dist/skills.js +200 -1
  8. package/node_modules/@oxygen/shared/dist/capability-discovery.d.ts +36 -0
  9. package/node_modules/@oxygen/shared/dist/capability-discovery.js +766 -0
  10. package/node_modules/@oxygen/shared/dist/cli-result.js +4 -3
  11. package/node_modules/@oxygen/shared/dist/index.d.ts +2 -0
  12. package/node_modules/@oxygen/shared/dist/index.js +2 -0
  13. package/node_modules/@oxygen/shared/dist/pricing-sheet.d.ts +1 -1
  14. package/node_modules/@oxygen/shared/dist/pricing-sheet.js +1 -1
  15. package/node_modules/@oxygen/shared/dist/recipes.d.ts +1 -1
  16. package/node_modules/@oxygen/shared/dist/recipes.js +4 -2
  17. package/node_modules/@oxygen/shared/dist/sequence-template.js +0 -0
  18. package/node_modules/@oxygen/shared/dist/sequence-terminal-events.d.ts +47 -0
  19. package/node_modules/@oxygen/shared/dist/sequence-terminal-events.js +70 -0
  20. package/node_modules/@oxygen/shared/dist/version.d.ts +1 -1
  21. package/node_modules/@oxygen/shared/dist/version.js +1 -1
  22. package/node_modules/@oxygen/workflows/dist/graph/lint.js +3 -0
  23. package/node_modules/@oxygen/workflows/dist/graph/manifest-schema.d.ts +5 -0
  24. package/node_modules/@oxygen/workflows/dist/graph/manifest-schema.js +5 -0
  25. package/node_modules/@oxygen/workflows/dist/graph/types.d.ts +2 -0
  26. package/node_modules/@oxygen/workflows/dist/index.d.ts +4 -0
  27. package/node_modules/@oxygen/workflows/dist/index.js +5 -0
  28. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -8,8 +8,8 @@ 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
10
  import { applyOxygenHelp } from "./help.js";
11
- import { buildCommandManifest } from "./command-manifest.js";
12
- import { AGENCY_DIRECTORY_REGIONS, AGENCY_DIRECTORY_SERVICES, describeWorkflowStatusChange, formatCellForDisplay, formatPublicBudgetScopes, exitCodeForOxygenError, parseWorkflowStatusChange, isVersionGreater, isVersionLess, MAX_MCP_TOOL_NAME_LENGTH, OXYGEN_VERSION, OxygenError, parseKnowledgePageMarkdown, PLAN_LIMITS, sleep, success, TAG_KINDS_PROSE, toFailure, workflowMcpToolName, } from "@oxygen/shared";
11
+ import { buildCommandManifest, getCommandManifestEntry, searchCommandManifest, suggestCommandNames, } from "./command-manifest.js";
12
+ import { AGENCY_DIRECTORY_REGIONS, AGENCY_DIRECTORY_SERVICES, describeWorkflowStatusChange, formatCellForDisplay, formatPublicBudgetScopes, exitCodeForOxygenError, parseWorkflowStatusChange, isVersionGreater, isVersionLess, MAX_MCP_TOOL_NAME_LENGTH, OXYGEN_CAPABILITY_ROUTES, OXYGEN_VERSION, OxygenError, getCapabilityRouteMatch, inferCapabilityRoute, parseKnowledgePageMarkdown, PLAN_LIMITS, serializeCapabilityRoute, sleep, success, TAG_KINDS_PROSE, toFailure, workflowMcpToolName, } from "@oxygen/shared";
13
13
  import { TAG_COLORS } from "@oxygen/shared/select-options";
14
14
  import { inferImportColumnLabels, inferRowsFileFormat, normalizeImportColumnKey, normalizeRowsForNewTable, normalizeRowsFormat, parseRowsFileBuffer, } from "@oxygen/shared/file-import";
15
15
  import { assertRecipeBundleSafe, assertWorkflowGraphManifest, assertWorkflowManifest, buildRecipeManifest, compileWorkflowDefinition, isAnyWorkflowManifest, isRecipeManifest, isWorkflowDefinition, isWorkflowGraphManifest, isWorkflowManifest, } from "@oxygen/workflows";
@@ -24,7 +24,7 @@ import { formatAiPromptPreviewNotice } from "./column-run-notices.js";
24
24
  import { runLocalCustomHttpColumn } from "./local-custom-http-column.js";
25
25
  import { captureCurrentTranscript, collectFeedbackEnvironment, TranscriptCaptureError, } from "./transcript.js";
26
26
  import { addSessionOutput, addSessionStatus, getSessionUsage, startSession, updateSessionStep, } from "./session.js";
27
- import { doctorAgentSkills, installAgentSkills, listAgentSkills, runAutomaticSkillsInstall, } from "./skills.js";
27
+ import { doctorAgentSkills, getAgentSkill, installAgentSkills, listAgentSkills, runAutomaticSkillsInstall, searchAgentSkills, } from "./skills.js";
28
28
  import { resolveCliBinaryName } from "./runtime.js";
29
29
  import { updateCli } from "./update.js";
30
30
  import { isRecord, readErrorMessage, readOption } from "./util.js";
@@ -289,6 +289,7 @@ async function handleAsyncAction(command, options, action) {
289
289
  try {
290
290
  const data = await action();
291
291
  emitSuccess(command, data, options);
292
+ writeDryRunNotice(data);
292
293
  writeCreditsReceipt(data);
293
294
  }
294
295
  catch (error) {
@@ -304,6 +305,24 @@ function emitCliFailure(command, error) {
304
305
  writeMaxCreditsHint(error);
305
306
  process.exitCode = error instanceof OxygenError ? exitCodeForOxygenError(error) : 1;
306
307
  }
308
+ // A dry run's stdout envelope looks like a successful result — same shape, same
309
+ // `ok: true` — so in a terminal the only tell that nothing was fetched was
310
+ // `meta.mode` buried inside the payload. That is how a working provider key gets
311
+ // reported as broken: the preview comes back empty and reads as a failed live
312
+ // call. Mirror the server's preview banner on stderr, the same stdout/stderr
313
+ // split the credits receipt uses, so the machine-read envelope stays clean.
314
+ function writeDryRunNotice(data) {
315
+ if (!data || typeof data !== "object" || Array.isArray(data))
316
+ return;
317
+ const preview = data.preview;
318
+ if (!preview || typeof preview !== "object" || Array.isArray(preview))
319
+ return;
320
+ const block = preview;
321
+ if (typeof block.message === "string")
322
+ process.stderr.write(`${block.message}\n`);
323
+ if (typeof block.next_step === "string")
324
+ process.stderr.write(`${block.next_step}\n`);
325
+ }
307
326
  // Paid envelopes (push 3 legibility) carry a `credits` block: quote on
308
327
  // dry_run, receipt on live, remaining balance on both. Mirror it as one
309
328
  // stderr line so spend stays visible in a terminal without polluting the
@@ -2508,13 +2527,78 @@ export function createProgram() {
2508
2527
  .action(async (options) => {
2509
2528
  await handleAsyncAction("home standup", options, () => requestOxygen("/api/cli/home/standup"));
2510
2529
  });
2511
- program
2530
+ const commandsCommand = program
2512
2531
  .command("commands")
2513
- .description("Print the machine-readable command manifest: every command with flags, spend/mutation markers, and the reserved exit-code table.")
2532
+ .description("Discover CLI commands. With no subcommand, print the backwards-compatible full manifest.")
2514
2533
  .option("--json", "Print a JSON envelope.")
2515
2534
  .action(async (options) => {
2516
2535
  await handleAsyncAction("commands", options, async () => buildCommandManifest(program, binaryName));
2517
2536
  });
2537
+ commandsCommand
2538
+ .addCommand(new Command("search")
2539
+ .description("Find a bounded set of CLI commands from a natural-language outcome.")
2540
+ .argument("<query...>", "Outcome or capability to find.")
2541
+ .option("--limit <n>", "Maximum results (default 10, max 25).")
2542
+ .option("--json", "Print a JSON envelope.")
2543
+ .action(async (queryParts, options) => {
2544
+ const outputOptions = { ...options, json: options.json || Boolean(commandsCommand.opts().json) };
2545
+ await handleAsyncAction("commands search", outputOptions, async () => {
2546
+ const manifest = buildCommandManifest(program, binaryName);
2547
+ return searchCommandManifest(manifest, queryParts.join(" "), readPositiveInt(options.limit) ?? 10);
2548
+ });
2549
+ }))
2550
+ .addCommand(new Command("get")
2551
+ .description("Hydrate one exact CLI command with its arguments, flags, and safety markers.")
2552
+ .argument("<command...>", "Exact command name returned by commands search.")
2553
+ .option("--json", "Print a JSON envelope.")
2554
+ .action(async (commandParts, options) => {
2555
+ const outputOptions = { ...options, json: options.json || Boolean(commandsCommand.opts().json) };
2556
+ await handleAsyncAction("commands get", outputOptions, async () => {
2557
+ const manifest = buildCommandManifest(program, binaryName);
2558
+ const exactName = commandParts.join(" ");
2559
+ const command = getCommandManifestEntry(manifest, exactName);
2560
+ if (!command) {
2561
+ throw new OxygenError("command_not_found", `Unknown Oxygen command: ${exactName}`, {
2562
+ details: { did_you_mean: suggestCommandNames(manifest, exactName) },
2563
+ exitCode: 2,
2564
+ });
2565
+ }
2566
+ return command;
2567
+ });
2568
+ }));
2569
+ program
2570
+ .command("capabilities")
2571
+ .description("Route an outcome to its owning OXYGEN layer and hosted primitive.")
2572
+ .addCommand(new Command("search")
2573
+ .description("Find the owning capability, boundary, gateways, and next hydration step.")
2574
+ .argument("<query...>", "Natural-language GTM outcome.")
2575
+ .option("--json", "Print a JSON envelope.")
2576
+ .action(async (queryParts, options) => {
2577
+ await handleAsyncAction("capabilities search", options, async () => {
2578
+ const query = queryParts.join(" ");
2579
+ return {
2580
+ query,
2581
+ route: serializeCapabilityRoute(inferCapabilityRoute(query)),
2582
+ hint: "Hydrate one exact CLI command with `oxygen commands get <exact-command> --json`, or one MCP tool with `oxygen_capabilities_schema`.",
2583
+ };
2584
+ });
2585
+ }))
2586
+ .addCommand(new Command("get")
2587
+ .description("Hydrate one exact capability card by id.")
2588
+ .argument("<capability-id>", "Capability id returned by capabilities search.")
2589
+ .option("--json", "Print a JSON envelope.")
2590
+ .action(async (capabilityId, options) => {
2591
+ await handleAsyncAction("capabilities get", options, async () => {
2592
+ const route = getCapabilityRouteMatch(capabilityId);
2593
+ if (!route) {
2594
+ throw new OxygenError("capability_not_found", `Unknown OXYGEN capability: ${capabilityId}`, {
2595
+ details: { available_ids: OXYGEN_CAPABILITY_ROUTES.map((card) => card.id) },
2596
+ exitCode: 2,
2597
+ });
2598
+ }
2599
+ return serializeCapabilityRoute(route);
2600
+ });
2601
+ }));
2518
2602
  program
2519
2603
  .command("status")
2520
2604
  .description("Compare the local Oxygen CLI version against the active profile's deployed Oxygen API.")
@@ -3066,7 +3150,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
3066
3150
  .addCommand(new Command("resolve")
3067
3151
  .description("Resolve @handles, LinkedIn profile/company links, provider ids, and URNs — through a connected sender (notify=false, free) or the managed scraper (--source scraper, paid, no sender/window needed).")
3068
3152
  .option("--account <account>", "LinkedIn sender id, connection id, or Unipile account id. Required unless --source scraper.")
3069
- .option("--source <source>", "Resolution source: sender (default, free) or scraper (managed cookieless lookup, ~10 credits per identity, needs --approved --max-credits).")
3153
+ .option("--source <source>", "Resolution source: sender (default, free) or scraper (managed cookieless lookup; fetch current cost from tools get/dry-run; needs --approved --max-credits).")
3070
3154
  .option("--approved", "Approve the paid scraper lookups (required with --source scraper).")
3071
3155
  .option("--max-credits <n>", "Credit ceiling for scraper lookups (required with --source scraper).")
3072
3156
  .option("--text <text>", "Post text containing @identifiers or LinkedIn URLs.")
@@ -3660,8 +3744,8 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
3660
3744
  }));
3661
3745
  })))
3662
3746
  .addCommand(new Command("search")
3663
- .description("Search CRM records by identity or record label.")
3664
- .argument("<query>", "Domain, email, LinkedIn URL, or record name to search for.")
3747
+ .description("Search CRM records by identity or record label. Takes ONE query; narrow the objects with --object, not a second argument.")
3748
+ .argument("<query>", "Domain, email, LinkedIn URL, or record name to search for. Quote it if it contains spaces; to restrict to one object use --object companies.")
3665
3749
  .option("--objects <objects>", "Comma-separated CRM object slugs to search. Defaults to all configured objects.")
3666
3750
  .option("--object <object>", "Alias for --objects; every sibling crm command spells it singular.")
3667
3751
  .option("--limit <limit>", "Maximum records to return.")
@@ -5826,7 +5910,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
5826
5910
  });
5827
5911
  }))
5828
5912
  .addCommand(new Command("preflight")
5829
- .description("Preflight a blueprint (slug, local file, or shared URL) against this workspace.")
5913
+ .description("Preflight a blueprint (slug, local file, or shared URL) against this workspace. Price-aware seeds return a current runtime descriptor upper bound, cap adequacy, and an exact zero-credit apply command.")
5830
5914
  .argument("[slug]", "Blueprint slug (for stored or seed blueprints).")
5831
5915
  .option("--file <path>", "Read a blueprint envelope from a local JSON file.")
5832
5916
  .option("--from-url <url>", "Fetch a shared blueprint envelope from a public Oxygen share URL.")
@@ -5987,6 +6071,8 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
5987
6071
  "",
5988
6072
  "Safety:",
5989
6073
  " list, describe, and preflight use 0 credits, make no provider calls, and do not change workspace state.",
6074
+ " Price-aware preflights fetch runtime descriptor pricing, label upper bounds versus exact-shape dry runs,",
6075
+ " and return the exact apply command that preserves the validated inputs.",
5990
6076
  " export has the same workspace safety; --out only writes the named local file.",
5991
6077
  " apply also uses 0 credits and makes no provider calls or external writes, but it creates workspace",
5992
6078
  " tables, prompts, and a disabled workflow. Its response reports future per-run credit ceilings.",
@@ -6450,6 +6536,11 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
6450
6536
  .option("--research-mode <mode>", "Research columns: strict (answer only from the sources) or estimate (reason to a figure from them).")
6451
6537
  .option("--research-results <n>", "Research columns: how many search results to ground each row on (1-25).")
6452
6538
  .option("--research-engine <engine>", "Research columns: pin the search provider (exa, parallel, or firecrawl).")
6539
+ // Declaring BOTH forms leaves the default undefined (Commander only
6540
+ // defaults to true when --no- is declared alone), so an update that
6541
+ // doesn't mention visibility leaves it untouched.
6542
+ .option("--always-show", "Keep this column visible even when it holds no values.")
6543
+ .option("--no-always-show", "Stop pinning this column, so it collapses again while empty.")
6453
6544
  .option("--dry-run", "Return the would-be merged definition without writing.")
6454
6545
  .option("--json", "Print a JSON envelope.")
6455
6546
  .action(async (table, column, options) => {
@@ -6494,6 +6585,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
6494
6585
  ...(definition ? { definition } : {}),
6495
6586
  ...(definitionUnset.length > 0 ? { definition_unset: definitionUnset } : {}),
6496
6587
  ...(readOption(options.dataType) ? { data_type: readOption(options.dataType) } : {}),
6588
+ ...(typeof options.alwaysShow === "boolean" ? { always_show: options.alwaysShow } : {}),
6497
6589
  ...(options.dryRun ? { dry_run: true } : {}),
6498
6590
  },
6499
6591
  });
@@ -7377,7 +7469,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
7377
7469
  })));
7378
7470
  program
7379
7471
  .command("billing")
7380
- .description("Plan and managed credit commands. Spend splits into FLEXIBLE (ad-hoc: enrichment, AI, automation — drawn from your free-to-spend balance) and FIXED recurring per-resource monthly commitments blocked out of it; see `billing commitments`. THREE CLOCKS, deliberately different: the CREDIT CYCLE that `billing allowance` reports against (your plan's monthly grant window); each resource's own COMMITMENT RENEWAL, anchored to the day you connected it, so `billing commitments --json` next_due_at rarely matches the cycle end; and your SUBSCRIPTION PERIOD in `billing balance` (annual plans span many credit cycles). A number from one clock will not reconcile against another.")
7472
+ .description("Plan and managed credit commands. Spend splits into FLEXIBLE (ad-hoc: enrichment, AI, automation — drawn from your free-to-spend balance) and FIXED recurring per-resource monthly commitments blocked out of it; see `billing commitments`. THREE CLOCKS, deliberately different: the CREDIT CYCLE that `billing allowance` reports against (your plan's monthly grant window); each resource's own COMMITMENT RENEWAL, anchored to the day you connected it, so `billing commitments --json` next_due_at rarely matches the cycle end; and your SUBSCRIPTION PERIOD in `billing balance` (annual plans span many credit cycles). A number from one clock will not reconcile against another. Failed-payment grace, suspension, and recovery: https://oxygen-agent.com/docs/safety/billing.")
7381
7473
  .addCommand(new Command("change")
7382
7474
  .description("Preview an upgrade or downgrade and return a Stripe confirmation link. Nothing changes until confirmed in Stripe.")
7383
7475
  .requiredOption("--to <tier>", "Target plan: starter, pro, or team.")
@@ -7389,7 +7481,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
7389
7481
  }));
7390
7482
  }))
7391
7483
  .addCommand(new Command("balance")
7392
- .description("Show the current plan and managed credit balance: available and reserved credits, FIXED credits committed to recurring per-resource charges, and the FLEXIBLE free-to-spend remainder. Credits are Oxygen's native unit; the plan's price and $-per-credit are at https://oxygen-agent.com/billing.")
7484
+ .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. 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.")
7393
7485
  .option("--json", "Print a JSON envelope.")
7394
7486
  .action(async (options) => {
7395
7487
  await handleAsyncAction("billing balance", options, () => requestOxygen("/api/cli/billing/balance"));
@@ -7774,8 +7866,8 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
7774
7866
  .command("admin")
7775
7867
  .description("Staff-only commands.")
7776
7868
  .addCommand(new Command("kpis")
7777
- .description("Show company acquisition, matured seven-day trial conversion, MRR/ARR, and churn. OXYGEN staff only; workspace admin role alone does not grant access.")
7778
- .addOption(new Option("--range <range>", "Range for acquisition, seven-day trial cohorts, and churn; current MRR/ARR remain point-in-time.")
7869
+ .description("Show company acquisition with self-reported signup-channel attribution, signup-to-CLI/MCP activation, matured seven-day trial conversion, positive-paid MRR/ARR, and paid churn separated from trial loss. OXYGEN staff only; workspace admin role alone does not grant access.")
7870
+ .addOption(new Option("--range <range>", "Range for acquisition, actor-and-surface-tracked activation, seven-day trial cohorts, and churn; current MRR/ARR and scheduled cancellations remain point-in-time.")
7779
7871
  .choices(["7d", "30d", "90d"])
7780
7872
  .default("30d"))
7781
7873
  .option("--json", "Print a JSON envelope.")
@@ -8676,10 +8768,11 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
8676
8768
  .command("tools")
8677
8769
  .description("Tool catalog commands.")
8678
8770
  .addCommand(new Command("search")
8679
- .description("Search the tool catalog. A response listing partial_sources means an optional catalog source timed out and totals may be understated — rerun for the complete catalog.")
8771
+ .description("Search a bounded, compact provider-operation catalog; hydrate one result with tools get. A response listing partial_sources means an optional catalog source timed out and totals may be understated — rerun for the complete catalog.")
8680
8772
  .argument("[query]", "Search text.")
8681
- .option("--verbosity <verbosity>", "minimal, summary, or full. Defaults to summary. Use minimal for high-fanout discovery sweeps that need to stay under the MCP token budget.")
8773
+ .option("--verbosity <verbosity>", "minimal, summary, or full. Defaults to minimal; hydrate one result with tools get.")
8682
8774
  .option("--terse", "Alias for --verbosity minimal.")
8775
+ .option("--all", "Return the complete matching catalog. Explicit because the default is bounded to 10.")
8683
8776
  .option("--only-runnable", "Only return tools runnable by the active organization.")
8684
8777
  .option("--workflow-eligible", "Only return tools a hosted workflow step may call, each annotated with the canonical `workflow_effect` its manifest step must declare. This is a narrower set than the table-column catalog — use it when authoring a workflow manifest so lint cannot reject a tool at apply time.")
8685
8778
  .option("--no-access-check", "Skip per-tool availability checks for a fast complete-catalog listing. Tools are returned without availability info; pair with --terse for discovery sweeps.")
@@ -8695,6 +8788,8 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
8695
8788
  const verbosity = options.terse ? "minimal" : readOption(options.verbosity);
8696
8789
  if (verbosity)
8697
8790
  params.set("verbosity", verbosity);
8791
+ if (options.all)
8792
+ params.set("all", "true");
8698
8793
  if (options.onlyRunnable)
8699
8794
  params.set("only_runnable", "true");
8700
8795
  if (options.workflowEligible)
@@ -10861,24 +10956,57 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
10861
10956
  const suffix = params.toString();
10862
10957
  return requestOxygen(`/api/cli/sequences${suffix ? `?${suffix}` : ""}`);
10863
10958
  });
10959
+ }))
10960
+ .addCommand(new Command("send")
10961
+ .description("Initiate one net-new LinkedIn message through a one-recipient, one-step hosted Sequence. First call without a sequence id: pass --recipient, --text/--text-file, and --sender to create the inert draft and preview (no message, no credits). After showing that preview, re-run with the returned sequence id plus --approved --max-credits N. Existing-thread replies stay in `oxygen inbox send`.")
10962
+ .argument("[sequence]", "Sequence id returned by the preview call. Omit when creating the preview; required with --approved.")
10963
+ .option("--recipient <url-or-id>", "LinkedIn personal-profile URL or provider member id. Preview call only.")
10964
+ .option("--recipient-name <name>", "Optional display name stored on the enrollment. Preview call only.")
10965
+ .option("--text <text>", "Message body. Preview call only; use either --text or --text-file.")
10966
+ .option("--text-file <path>", "Read the message body from a file. Preview call only; use either --text-file or --text.")
10967
+ .option("--sender <ref>", "Connected LinkedIn sender id, connection id, or Unipile account id. List choices with `oxygen senders list`.")
10968
+ .option("--approved", "Approve the previewed Sequence for live dispatch. Requires the returned sequence id and --max-credits.")
10969
+ .option("--max-credits <n>", "Positive credit ceiling for the live LinkedIn dispatch.")
10970
+ .option("--json", "Print a JSON envelope.")
10971
+ .action(async (sequence, options) => {
10972
+ await handleAsyncAction("sequences send", options, () => {
10973
+ const sequenceId = readOption(sequence);
10974
+ const maxCredits = readPositiveNumber(options.maxCredits);
10975
+ if (sequenceId) {
10976
+ if (readOption(options.recipient) || readOption(options.recipientName) || readOption(options.text) || readOption(options.textFile) || readOption(options.sender)) {
10977
+ throw new OxygenError("conflicting_flags", "With a sequence id, pass only --approved and --max-credits; recipient, copy, and sender are fixed by the previewed draft.", { exitCode: 1 });
10978
+ }
10979
+ return requestOxygen("/api/cli/sequences/send", { method: "POST", body: { sequence: sequenceId, ...(options.approved ? { approved: true } : {}), ...(maxCredits !== undefined ? { max_credits: maxCredits } : {}) } });
10980
+ }
10981
+ if (options.approved)
10982
+ throw new OxygenError("sequence_preview_required", "Preview first without --approved; then pass the returned sequence id with --approved --max-credits N.", { exitCode: 1 });
10983
+ const recipient = readOption(options.recipient);
10984
+ const sender = readOption(options.sender);
10985
+ const text = readPublishingPostText(options, true);
10986
+ if (!recipient)
10987
+ throw new OxygenError("invalid_request", "--recipient is required for the preview call.", { exitCode: 1 });
10988
+ if (!sender)
10989
+ throw new OxygenError("invalid_request", "--sender is required for the preview call; list choices with `oxygen senders list`.", { exitCode: 1 });
10990
+ return requestOxygen("/api/cli/sequences/send", { method: "POST", body: { recipient, text, sender, ...(readOption(options.recipientName) ? { recipient_name: readOption(options.recipientName) } : {}) } });
10991
+ });
10864
10992
  }))
10865
10993
  .addCommand(new Command("workflows")
10866
- .description("Configure the sequencer's editable reply workflows: internal CRM routing and positive-reply Microsoft Teams notifications.")
10994
+ .description("Compatibility-only maintenance for existing sequencer presets. Do not create new Teams notifications here: use `oxygen workflows` (web: Sequence → Launch → Connected workflows) with a sequence event trigger and Microsoft Teams Send Message.")
10867
10995
  .addCommand(new Command("list")
10868
- .description("List reply workflows with armed state, editable config, and workflow deep-links.")
10996
+ .description("List the two legacy sequencer presets with armed state, editable config, and ordinary workflow deep-links.")
10869
10997
  .option("--json", "Print a JSON envelope.")
10870
10998
  .action(async (options) => {
10871
10999
  await handleAsyncAction("sequences workflows list", options, () => requestOxygen("/api/cli/sequencer/reply-workflows"));
10872
11000
  }))
10873
11001
  .addCommand(new Command("configure")
10874
- .description("Preview or save one editable reply workflow. Live arming requires --armed --approved and a positive per-delivery --max-credits cap; Microsoft Teams requires at least 20.02.")
10875
- .argument("<template>", "crm-lead-stage-router or sequencer-positive-reply-teams.")
11002
+ .description("Preview or maintain an existing legacy preset. New Teams notifications must use an ordinary connected workflow; this command remains only for compatibility. Omit --live for a zero-write preview. Live arming requires --armed --approved and a positive per-delivery --max-credits cap.")
11003
+ .argument("<template>", "crm-lead-stage-router or the legacy sequencer-positive-reply-teams preset (existing configurations only).")
10876
11004
  .option("--config-file <path>", "JSON object containing the managed workflow configuration.")
10877
11005
  .option("--armed", "Arm the workflow.")
10878
11006
  .option("--disarmed", "Disarm the workflow.")
10879
11007
  .option("--live", "Persist the configuration. Omit for a dry-run preview.")
10880
11008
  .option("--approved", "Explicitly approve standing authority for this exact workflow revision.")
10881
- .option("--max-credits <n>", "Hard per-delivery credit ceiling (Teams requires at least 20.02).")
11009
+ .option("--max-credits <n>", "Hard per-delivery credit ceiling (the legacy Teams preset requires at least 0.02).")
10882
11010
  .option("--json", "Print a JSON envelope.")
10883
11011
  .action(async (template, options) => {
10884
11012
  await handleAsyncAction("sequences workflows configure", options, () => {
@@ -10907,15 +11035,15 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
10907
11035
  });
10908
11036
  })))
10909
11037
  .addCommand(new Command("hubspot-sync")
10910
- .description("Map actual sequencer events to contact datetime fields in the connected HubSpot portal. Event-driven and workspace-wide; no polling interval, direction, row batch, or operator credit-cap controls.")
11038
+ .description("Map actual sequencer events to contact datetime fields in the connected HubSpot portal. Reuses native or provider-managed authorization and only requests known-missing property access if a preview needs new fields.")
10911
11039
  .addCommand(new Command("show")
10912
- .description("Fetch supported sequencer events, the connected portal's writable contact datetime fields, current mappings, and enabled state.")
11040
+ .description("Show the connected portal and fetch its writable contact datetime fields, supported sequencer events, current mappings, and enabled state.")
10913
11041
  .option("--json", "Print a JSON envelope.")
10914
11042
  .action(async (options) => {
10915
11043
  await handleAsyncAction("sequences hubspot-sync show", options, () => requestOxygen("/api/cli/sequencer/hubspot-sync"));
10916
11044
  }))
10917
11045
  .addCommand(new Command("configure")
10918
- .description("Preview or save event → HubSpot property mappings. A live enable requires --approved after preview; missing recommended Oxygen properties are created on enable.")
11046
+ .description("Preview or save event → HubSpot property mappings. Existing fields need no new grant; creating missing properties requires --approved and HubSpot property-creation access.")
10919
11047
  .option("--mappings-file <path>", "JSON object mapping every sequencer event key to a HubSpot contact datetime-property internal name; use an empty string to disable an event.")
10920
11048
  .option("--armed", "Enable future event delivery.")
10921
11049
  .option("--disarmed", "Disable future event delivery.")
@@ -11392,7 +11520,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
11392
11520
  program.addCommand(new Command("voice")
11393
11521
  .description("The call channel. `tasks` works the call queue — leads waiting for a HUMAN to dial, queued by sequence call steps, CRM records, table rows, and replies. Claiming takes a lease so two reps never dial the same prospect. Nothing here places a call: dialing is a separate, metered, guardrail-gated action. Consumes 0 credits.")
11394
11522
  .addCommand(new Command("tasks")
11395
- .description("Work the call queue: queue | list | claim | complete | skip | override.\n\nWhat blocks a dial, and what you can do about it:\n calling_window lead's local time outside 09:00-20:00 waivable per lead\n unknown_timezone no timezone on the lead, so the window\n cannot be checked (fails closed) waivable per lead\n daily_cap the chosen number hit today's dial cap waivable per lead\n suppressed on the workspace do-not-call list NEVER waivable\n destination_blocked your numbers cannot reach that country NEVER waivable\n\n`list` reports dialable, block_reason and block_detail per lead, plus\ndialable_count and blocked_by_reason for the page. Waive one with `override`.")
11523
+ .description("Work the call queue: queue | list | claim | complete | extend | skip | override.\n\nA call that nobody answers is dispositioned for you from the call itself, which\nalso resumes any sequence enrollment parked on it — so `complete` and `skip`\nreport already_recorded:true rather than failing when that has happened. You log\nonly what a person said.\n\nWhat blocks a dial, and what you can do about it:\n calling_window lead's local time outside 09:00-20:00 waivable per lead\n unknown_timezone no timezone on the lead, so the window\n cannot be checked (fails closed) waivable per lead\n daily_cap the chosen number hit today's dial cap waivable per lead\n suppressed on the workspace do-not-call list NEVER waivable\n destination_blocked your numbers cannot reach that country NEVER waivable\n\n`list` reports dialable, block_reason and block_detail per lead, plus\ndialable_count and blocked_by_reason for the page. Waive one with `override`.")
11396
11524
  .addCommand(new Command("queue")
11397
11525
  .description("Put a lead INTO the call queue. Idempotent: a lead who already has an open task comes back with created:false rather than a duplicate, because the queue guarantees one open call per person. Queuing is not dialing — it writes a row a human later acts on. Consumes 0 credits.")
11398
11526
  .requiredOption("--phone <e164>", "The lead's number in E.164 (a leading + then 2-15 digits).")
@@ -11552,6 +11680,25 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
11552
11680
  },
11553
11681
  });
11554
11682
  });
11683
+ }))
11684
+ .addCommand(new Command("extend")
11685
+ .description("Renew a claimed task's 15-minute lease. Completing needs a live lease, so a longer call must heartbeat or its outcome and notes are rejected as stale.")
11686
+ .requiredOption("--task <id>", "The claimed task's id.")
11687
+ .requiredOption("--lease <token>", "The lease_token returned by `claim`.")
11688
+ .option("--json", "Print a JSON envelope.")
11689
+ .action(async (options) => {
11690
+ await handleAsyncAction("voice tasks extend", options, () => {
11691
+ const task = readOption(options.task);
11692
+ if (!task)
11693
+ throw new Error("--task is required.");
11694
+ const lease = readOption(options.lease);
11695
+ if (!lease)
11696
+ throw new Error("--lease is required.");
11697
+ return requestOxygen("/api/cli/voice/tasks", {
11698
+ method: "POST",
11699
+ body: { action: "extend", task_id: task, lease_token: lease },
11700
+ });
11701
+ });
11555
11702
  }))
11556
11703
  .addCommand(new Command("skip")
11557
11704
  .description("Drop a task out of the queue without dialing (bad data, a rep pass, a guardrail block). Needs no lease — the guardrail gate skips tasks it never claimed.")
@@ -11850,7 +11997,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
11850
11997
  });
11851
11998
  })));
11852
11999
  program.addCommand(new Command("managed-inboxes")
11853
- .description("Whitelabel sending inboxes bought through OXYGEN: subscribe a domain + N mailboxes (google/microsoft/azure) as a recurring MONTHLY subscription billed in USD to your Oxygen Email Infrastructure subscription, list/get your subscriptions, verify that Oxygen/Stripe/the vendor agree, and cancel. Subscribe/cancel are approval-gated (preview → re-run with --approved --quote). The vendor is chosen for you; --vendor pins one.")
12000
+ .description("Whitelabel sending inboxes bought through OXYGEN: subscribe a domain + N mailboxes (google/microsoft/azure) as a recurring MONTHLY subscription billed in USD to your Oxygen Email Infrastructure subscription, add-inboxes to a domain you already own, list/get your subscriptions, verify that Oxygen/Stripe/the vendor agree, and cancel. Subscribe/add-inboxes/cancel are approval-gated (preview → re-run with --approved --quote). The vendor is chosen for you; --vendor pins one.")
11854
12001
  .addCommand(new Command("verify")
11855
12002
  .description("Check that OXYGEN, STRIPE, and the VENDOR agree about what this org is buying. The truth about a managed inbox lives in three systems — what the customer asked for, what they are charged, and what is actually running — and a 200 from any one of them proves nothing. Reports every disagreement with WHO IS LOSING MONEY while it stands (customer_overbilled first, then oxygen_pays). Read-only, no writes, 0 Oxygen credits.")
11856
12003
  .option("--json", "Print a JSON envelope.")
@@ -11864,7 +12011,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
11864
12011
  await handleAsyncAction("managed-inboxes registrant", options, () => requestOxygen("/api/cli/managed-inboxes/registrant"));
11865
12012
  }))
11866
12013
  .addCommand(new Command("subscribe")
11867
- .description("Subscribe a domain + mailboxes as a managed monthly inbox subscription. WITHOUT --approved this prints a priced PREVIEW with a quote_id (nothing ordered, nothing charged); re-run with --approved --quote <id> to place the order. Prices come from the vendor's LIVE rate card and LIVE domain price, and are refused if they sit below vendor cost. Fails closed until the vendor key + founder-signed per-platform pricing are configured.")
12014
+ .description("Subscribe a NEW domain + mailboxes as a managed monthly inbox subscription. WITHOUT --approved this prints a priced PREVIEW with a quote_id (nothing ordered, nothing charged); re-run with --approved --quote <id> to place the order. Prices come from the vendor's LIVE rate card and LIVE domain price, and are refused if they sit below vendor cost. Fails closed until the vendor key + founder-signed per-platform pricing are configured. To add mailboxes to a domain you ALREADY own use `managed-inboxes add-inboxes` — this command always registers a new domain and fails on one you own.")
11868
12015
  .argument("[domain]", "Sending domain to register + host the mailboxes (e.g. send.acme.com). May also be passed as --domain.")
11869
12016
  .option("--domain <domain>", "Sending domain (alternative to the positional argument).")
11870
12017
  .requiredOption("--provider <provider>", "Mailbox PLATFORM: google, microsoft, or azure. (google/microsoft cap at 5 mailboxes per domain; azure allows 100.)")
@@ -11929,6 +12076,75 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
11929
12076
  },
11930
12077
  });
11931
12078
  });
12079
+ }))
12080
+ .addCommand(new Command("add-inboxes")
12081
+ .description("Add mailboxes to a managed domain you ALREADY own — no new domain is registered and there is NO domain registration charge, only the extra inboxes' monthly rate (plus their add-ons). WITHOUT --approved this prints a priced PREVIEW with a quote_id and orders nothing; re-run with --approved --quote <id> to place the order. Capped per domain by the platform the domain was bought on (5 google/microsoft, 100 azure) COUNTING the inboxes already on it. To register a NEW domain use `managed-inboxes subscribe` instead.")
12082
+ .argument("[domain]", "A managed domain this workspace already owns (e.g. send.acme.com). May also be passed as --domain.")
12083
+ .option("--domain <domain>", "The managed inbox domain (alternative to the positional argument).")
12084
+ .option("--mailboxes <json>", "JSON array of mailboxes: [{\"username\",\"first_name\",\"last_name\"}]. The vendor stamps the names on each mailbox, so real names belong here.")
12085
+ .option("--file <path>", "Path to a JSON file { \"mailboxes\": [...] } (alternative to --mailboxes).")
12086
+ .option("--count <n>", "Shorthand for --mailboxes: how many inboxes to add. Requires --prefix.")
12087
+ .option("--prefix <base>", "Shorthand username base for --count: `--count 3 --prefix ada` adds ada1, ada2, ada3 with placeholder names (Ada 1, Ada 2, Ada 3). Pass --mailboxes/--file instead when the inboxes need real human names.")
12088
+ .option("--approved", "Place the order (requires --quote). Without it, a priced preview is returned.")
12089
+ .option("--quote <id>", "The quote_id from a fresh preview. Required with --approved.")
12090
+ .option("--json", "Print a JSON envelope.")
12091
+ .action(async (domainArg, options) => {
12092
+ await handleAsyncAction("managed-inboxes add-inboxes", options, () => {
12093
+ const domain = requireDomainArg(domainArg, options.domain);
12094
+ let mailboxes;
12095
+ const mailboxesJson = readOption(options.mailboxes);
12096
+ const filePath = readOption(options.file);
12097
+ const prefix = readOption(options.prefix);
12098
+ const count = readPositiveInt(options.count);
12099
+ if (mailboxesJson) {
12100
+ mailboxes = JSON.parse(mailboxesJson);
12101
+ }
12102
+ else if (filePath) {
12103
+ const parsed = readJsonFileValue(resolve(filePath), "--file");
12104
+ mailboxes = parsed.mailboxes ?? [];
12105
+ }
12106
+ else if (count !== undefined || prefix) {
12107
+ if (count === undefined || !prefix) {
12108
+ throw new Error("--count and --prefix must be passed together (e.g. --count 3 --prefix ada).");
12109
+ }
12110
+ // The vendor stamps a first/last name on every mailbox, so there is no
12111
+ // name-less order shape to fall back on — the shorthand invents
12112
+ // placeholders rather than pretending names are optional. Anyone who
12113
+ // wants real human identities on the inboxes passes --mailboxes/--file.
12114
+ const base = prefix.toLowerCase();
12115
+ const label = `${base.charAt(0).toUpperCase()}${base.slice(1)}`;
12116
+ mailboxes = Array.from({ length: count }, (_entry, index) => ({
12117
+ username: `${base}${index + 1}`,
12118
+ first_name: label,
12119
+ last_name: String(index + 1),
12120
+ }));
12121
+ }
12122
+ else {
12123
+ throw new Error("Provide --mailboxes <json>, --file <path>, or --count <n> --prefix <base>.");
12124
+ }
12125
+ const quote = readOption(options.quote);
12126
+ // Refuse locally rather than spending a round trip on a PAID path: the
12127
+ // route requires the quote that priced this exact expansion, so
12128
+ // `--approved` alone can only ever come back as a 400.
12129
+ if (options.approved && !quote) {
12130
+ // Typed, not a bare Error: a bare throw surfaces as
12131
+ // `unexpected_error`, which an agent cannot tell apart from a
12132
+ // real failure on a PAID command. Refusing to order is an
12133
+ // ordinary, expected outcome and must say so in its code.
12134
+ throw new OxygenError("invalid_request", "--approved requires --quote <id> from a fresh preview. Re-run without --approved to get one.", { exitCode: 2 });
12135
+ }
12136
+ return requestOxygen(`/api/cli/managed-inboxes/${encodeURIComponent(domain)}/mailboxes`, {
12137
+ method: "POST",
12138
+ body: {
12139
+ // Always an explicit mailboxes[]: --count/--prefix is client-side
12140
+ // sugar, so the server sees exactly one shape and the preview it
12141
+ // prices is the list that gets ordered.
12142
+ mailboxes,
12143
+ ...(options.approved ? { approved: true } : {}),
12144
+ ...(quote ? { quote_id: quote } : {}),
12145
+ },
12146
+ });
12147
+ });
11932
12148
  }))
11933
12149
  .addCommand(new Command("list")
11934
12150
  .description("List the org's managed inbox orders: vendor, platform, live inbox count, lifecycle status, internal billing posture, and each order's FULL monthly cost — the inbox line plus the warm-up and inbox-placement add-ons billing per inbox on top of it (`orders[].total_monthly_credits`, `total_monthly_credits` across the workspace). Read-only, 0 Oxygen credits.")
@@ -11982,7 +12198,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
11982
12198
  program.addCommand(new Command("mailboxes")
11983
12199
  .description("Native email sending pool: register/refresh Google/Microsoft mailboxes (including secure local Hypertide transfer), pause/disable inboxes, connect EmailGuard monitoring, and run managed TrulyInbox warmup with explicit plans and credit caps. 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).")
11984
12200
  .addCommand(new Command("list")
11985
- .description("List the org's sending mailboxes with provider, status, warmup state, and a pool overview.")
12201
+ .description("List the org's sending mailboxes with provider, status, warmup state, source (managed = bought through Oxygen, byok = bring-your-own), and a pool overview (including counts by source).")
11986
12202
  .option("--status <status>", "Filter by status: active, paused, or disabled.")
11987
12203
  .option("--json", "Print a JSON envelope.")
11988
12204
  .action(async (options) => {
@@ -11996,7 +12212,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
11996
12212
  });
11997
12213
  }))
11998
12214
  .addCommand(new Command("get")
11999
- .description("Get one sending mailbox's detail (provider, status, daily cap, warmup state, auth mode) plus a one-row pool summary. An ineligible native-send transport includes transport_reason + transport_hint; it does not by itself block TrulyInbox warmup or EmailGuard monitoring. <mailbox> accepts a mailbox id or email address.")
12215
+ .description("Get one sending mailbox's detail (provider, status, daily cap, warmup state, auth mode, and source — managed vs bring-your-own) plus a one-row pool summary. An ineligible native-send transport includes transport_reason + transport_hint; it does not by itself block TrulyInbox warmup or EmailGuard monitoring. <mailbox> accepts a mailbox id or email address.")
12000
12216
  .argument("<mailbox>", "Mailbox id or email address.")
12001
12217
  .option("--json", "Print a JSON envelope.")
12002
12218
  .action(async (mailbox, options) => {
@@ -12009,7 +12225,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
12009
12225
  await handleAsyncAction("mailboxes health", options, () => requestOxygen("/api/cli/mailboxes/health"));
12010
12226
  }))
12011
12227
  .addCommand(new Command("compatibility")
12012
- .description("Read-only compatibility report for every selected mailbox: origin vendor, real infrastructure tier (including InboxKit Azure), mailbox auth, TrulyInbox warmup path, EmailGuard monitoring path, and the exact next action. Use --catalog-only for the compact, workspace-independent import contract with no mailbox rows. JSON data.compatibility contains checked mailbox rows; data.provider_matrix always contains the complete current 18-pair product matrix even when mailboxes are filtered; data.import_methods and data.import_fields describe every supported ingestion path; data.non_transferable_auth names credentials that must be reconnected; data.summary rolls up states; data.web_url opens the pool. States distinguish credential_required, consent_required, and vendor_blocked. Never decrypts a credential or calls a downstream provider.")
12228
+ .description("Read-only compatibility report for every selected mailbox: origin vendor, real infrastructure tier (including InboxKit Azure), OXYGEN native-send connection quality, TrulyInbox warmup path, EmailGuard monitoring path, and exact next actions. Native send distinguishes destination-bound OAuth, provider-managed API transport, admin delegation, and authorization_required. Use --catalog-only for the compact workspace-independent import contract. JSON data.compatibility contains checked rows; data.import_methods and data.import_fields describe accepted transfer inputs; data.provider_matrix is the complete current 18-pair product matrix; data.non_transferable_auth names credentials that must be reconnected; data.summary rolls up states; data.web_url opens the pool. Downstream states distinguish credential_required, consent_required, and vendor_blocked. Never decrypts a credential or calls a downstream provider.")
12013
12229
  .option("--mailboxes <list>", "Comma-separated mailbox ids or addresses. Omit for the whole pool.")
12014
12230
  .option("--catalog-only", "Return only the bounded import-method, field, auth-boundary, and 18-pair provider catalogs; do not read or return workspace mailbox rows.")
12015
12231
  .option("--json", "Print a JSON envelope.")
@@ -12242,16 +12458,16 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
12242
12458
  });
12243
12459
  }))
12244
12460
  .addCommand(new Command("connect-oauth")
12245
- .description("Zapmail- or InboxKit-provisioned pools — not Hypertide imports. Preview native OAuth provisioning by default; pass --approved after reviewing the mailbox/domain scope to start external vendor writes. A successful preview describes the requested scope and safety envelope; it can still contain zero eligible mailboxes or only already-authorized no-ops. For InboxKit, first confirm mailboxes oauth-health returns remedy connect_oauth_inboxkit, then use mailboxes compatibility --mailboxes <list> to verify origin and auth state. --vendor zapmail (default) hands the pool to Zapmail's Custom OAuth export; --vendor inboxkit requests Workspace-admin consent per mailbox, but only for domains where Oxygen's OAuth client id is already approved — an unapproved domain sends no mailbox consent, and a domain that has never connected a mailbox fires one canary until it lands (--no-canary to fan out immediately). Manual InboxKit live requests are capped at 10 potential mailbox consent writes; use --mailboxes to split wider proven-domain or no-canary scopes. Newly provisioned InboxKit mailboxes are retried by the durable worker. This authorization action uses 0 Oxygen credits; existing vendor or subscription billing is unchanged. Pass --status <id> to poll.")
12461
+ .description("Connect Google/Microsoft mailboxes to OXYGEN native send with fresh destination-bound OAuth. Preview by default; pass --approved only after reviewing exact candidates. --vendor oxygen handles imported, Hypertide, external, or manual mailboxes: it requires an exact list (max 10), returns one OXYGEN browser link per candidate, and stores a grant only after that exact mailbox completes provider consent/MFA. Source tokens, passwords, authenticator seeds, and one-time codes never transfer. --vendor zapmail (default) hands a provisioned pool to Zapmail Custom OAuth; --vendor inboxkit requests domain-gated consent with one canary on unproven domains and a 10-write live cap. All paths cost 0 Oxygen credits. Poll --status <id>; connected means an encrypted refresh token actually landed, never merely that a request was accepted.")
12246
12462
  .option("--provider <provider>", "Mailbox provider to provision: google or microsoft.")
12247
- .option("--vendor <vendor>", "Which provisioning path to use: zapmail (default) or inboxkit.")
12248
- .option("--mailboxes <list>", "Comma-separated mailbox addresses to provision. Omit for the whole pool; manual InboxKit live runs must resolve to at most 10 potential consent writes.")
12463
+ .option("--vendor <vendor>", "Authorization path: oxygen for imported/manual mailboxes, zapmail (default), or inboxkit.")
12464
+ .option("--mailboxes <list>", "Comma-separated mailbox addresses. Required for vendor=oxygen (max 10); omit for vendor-provisioned whole-pool flows.")
12249
12465
  .option("--domains <list>", "Comma-separated sending domains to limit an inboxkit run to. Omit to cover every domain in the pool.")
12250
12466
  .option("--no-canary", "inboxkit only: fan out to every eligible mailbox on a domain that has never connected one, instead of firing a single canary first.")
12251
12467
  .option("--connection <id>", "Zapmail connection id. Defaults to the org's active Zapmail connection.")
12252
- .option("--status <id>", "Poll a previously started run (Zapmail export id or InboxKit run id) instead of starting a new one.")
12468
+ .option("--status <id>", "Poll a previously started Zapmail, InboxKit, or OXYGEN direct-consent run.")
12253
12469
  .option("--dry-run", "Preview the requested mailbox/domain scope and external writes without changing vendor or ledger state. This is the default for starts; a successful preview can still have zero eligible mailboxes, so check InboxKit candidates with oauth-health before approval.")
12254
- .option("--approved", "Start live vendor OAuth provisioning after inspecting the dry-run preview.")
12470
+ .option("--approved", "Start the reviewed authorization run. For vendor=oxygen this records expiring browser steps; provider consent still occurs only when the user opens each link.")
12255
12471
  .option("--json", "Print a JSON envelope.")
12256
12472
  .action(async (options) => {
12257
12473
  await handleAsyncAction("mailboxes connect-oauth", options, () => {
@@ -12295,7 +12511,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
12295
12511
  });
12296
12512
  }))
12297
12513
  .addCommand(new Command("oauth-health")
12298
- .description("Show the delegation-stuck sending mailboxes across both transports (google and microsoft): every inbox still on auth_mode=delegation with no OAuth grant, each naming the remedy that applies (connect_oauth for Zapmail-linked rows, connect_oauth_inboxkit for InboxKit-provisioned ones, domain_delegation for the rest) and, for Zapmail rows, its Custom OAuth budget (attempts in the last 7 days, remaining re-export posts before Zapmail's 3-per-mailbox-per-7-day cap, last error, and next safe retry time). Read-only — 0 Oxygen credits.")
12514
+ .description("Show every Google/Microsoft inbox with neither a per-mailbox OAuth token nor covered Google delegation. Remedies are connect_oauth for Zapmail, connect_oauth_inboxkit for InboxKit, and connect_oauth_oxygen for imported/manual mailboxes. Zapmail rows include their 3-per-7-day export budget; OXYGEN direct rows require exact browser consent and may invoke provider MFA. Read-only — 0 Oxygen credits.")
12299
12515
  .option("--json", "Print a JSON envelope.")
12300
12516
  .action(async (options) => {
12301
12517
  await handleAsyncAction("mailboxes oauth-health", options, () => requestOxygen("/api/cli/mailboxes/oauth-health"));
@@ -13225,7 +13441,8 @@ Run completion:
13225
13441
  params.set("tag", tag);
13226
13442
  const qs = params.toString() ? `?${params.toString()}` : "";
13227
13443
  const data = await requestOxygen(`/api/cli/workflows${qs}`);
13228
- writeDisabledWorkflowNotices(data);
13444
+ if (!options.json)
13445
+ writeDisabledWorkflowNotices(data);
13229
13446
  return data;
13230
13447
  });
13231
13448
  }))
@@ -13240,7 +13457,8 @@ Run completion:
13240
13457
  method: "POST",
13241
13458
  body: { workflow },
13242
13459
  });
13243
- writeDisabledWorkflowNotices(data);
13460
+ if (!options.json)
13461
+ writeDisabledWorkflowNotices(data);
13244
13462
  return prepareWorkflowCliOutput(data, options);
13245
13463
  });
13246
13464
  }))
@@ -13735,11 +13953,28 @@ Run completion:
13735
13953
  .command("skills")
13736
13954
  .description("Agent skill discovery and installation commands.")
13737
13955
  .addCommand(new Command("list")
13738
- .description("List Oxygen agent skills available from the public skill index.")
13956
+ .description("List Oxygen agent skills available from the hosted skill index.")
13739
13957
  .option("--api-url <url>", "Oxygen app URL. Defaults to OXYGEN_API_URL or https://oxygen-agent.com.")
13740
13958
  .option("--json", "Print a JSON envelope.")
13741
13959
  .action(async (options) => {
13742
13960
  await handleAsyncAction("skills list", options, () => listAgentSkills(options));
13961
+ }))
13962
+ .addCommand(new Command("search")
13963
+ .description("Find a bounded set of Oxygen skills and matching documents from an outcome.")
13964
+ .argument("<query...>", "Outcome, primitive, provider, or operating task.")
13965
+ .option("--limit <n>", "Maximum results (default 5, max 10).")
13966
+ .option("--api-url <url>", "Oxygen app URL. Defaults to OXYGEN_API_URL or https://oxygen-agent.com.")
13967
+ .option("--json", "Print a JSON envelope.")
13968
+ .action(async (queryParts, options) => {
13969
+ await handleAsyncAction("skills search", options, () => searchAgentSkills(queryParts.join(" "), options));
13970
+ }))
13971
+ .addCommand(new Command("get")
13972
+ .description("Hydrate one exact Oxygen skill entrypoint and its document index.")
13973
+ .argument("<skill-name>", "Exact name returned by skills search.")
13974
+ .option("--api-url <url>", "Oxygen app URL. Defaults to OXYGEN_API_URL or https://oxygen-agent.com.")
13975
+ .option("--json", "Print a JSON envelope.")
13976
+ .action(async (skillName, options) => {
13977
+ await handleAsyncAction("skills get", options, () => getAgentSkill(skillName, options));
13743
13978
  }))
13744
13979
  .addCommand(new Command("doctor")
13745
13980
  .description("Check Oxygen skill index reachability and local installer prerequisites.")
package/dist/skills.d.ts CHANGED
@@ -14,6 +14,10 @@ export type SkillsListOptions = {
14
14
  json?: boolean;
15
15
  apiUrl?: string;
16
16
  };
17
+ export type SkillsSearchOptions = SkillsListOptions & {
18
+ limit?: string;
19
+ };
20
+ export type SkillsGetOptions = SkillsListOptions;
17
21
  export type SkillsDoctorOptions = {
18
22
  json?: boolean;
19
23
  apiUrl?: string;
@@ -58,6 +62,8 @@ export declare function resolveSkillsInstallSource(options: {
58
62
  apiUrl: string;
59
63
  } & SkillsSourceRuntime): Promise<SkillsInstallSource>;
60
64
  export declare function listAgentSkills(options: SkillsListOptions, runtime?: SkillsSourceRuntime): Promise<Record<string, unknown>>;
65
+ export declare function searchAgentSkills(query: string, options: SkillsSearchOptions, runtime?: SkillsSourceRuntime): Promise<Record<string, unknown>>;
66
+ export declare function getAgentSkill(skillName: string, options: SkillsGetOptions, runtime?: SkillsSourceRuntime): Promise<Record<string, unknown>>;
61
67
  export declare function doctorAgentSkills(options: SkillsDoctorOptions, runtime?: SkillsSourceRuntime): Promise<Record<string, unknown>>;
62
68
  export declare function installAgentSkills(// skipcq: JS-R1005
63
69
  options: SkillsInstallOptions, runtime?: {