@oxygen-agent/cli 1.1014.86 → 1.1014.119
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/command-manifest.js +4 -1
- package/dist/help.js +5 -0
- package/dist/index.js +133 -80
- package/dist/skills.js +2 -0
- package/node_modules/@oxygen/shared/dist/capability-discovery.js +122 -0
- package/node_modules/@oxygen/shared/dist/contracts/browser-operations.js +14 -4
- package/node_modules/@oxygen/shared/dist/contracts/customer-operations.js +6 -5
- package/node_modules/@oxygen/shared/dist/contracts/knowledge-sync.d.ts +19 -0
- package/node_modules/@oxygen/shared/dist/contracts/knowledge-sync.js +8 -0
- package/node_modules/@oxygen/shared/dist/contracts/publishing-reads.d.ts +7 -0
- package/node_modules/@oxygen/shared/dist/contracts/schema.d.ts +3 -0
- package/node_modules/@oxygen/shared/dist/contracts/schema.js +3 -0
- package/node_modules/@oxygen/shared/dist/contracts/table-feed-read.d.ts +1 -0
- package/node_modules/@oxygen/shared/dist/contracts/table-row-read.d.ts +2 -0
- package/node_modules/@oxygen/shared/dist/credit-gate.js +2 -1
- package/node_modules/@oxygen/shared/dist/mcp-event-contract.d.ts +12 -0
- package/node_modules/@oxygen/shared/dist/otlp-log-sink.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/otlp-log-sink.js +1 -0
- package/node_modules/@oxygen/shared/dist/plan-capabilities.d.ts +9 -2
- package/node_modules/@oxygen/shared/dist/plan-capabilities.js +14 -1
- package/node_modules/@oxygen/shared/dist/sending-schedule.d.ts +7 -0
- package/node_modules/@oxygen/shared/dist/sending-schedule.js +10 -0
- package/node_modules/@oxygen/shared/dist/user-capability-routing.d.ts +2 -0
- package/node_modules/@oxygen/shared/dist/user-capability-routing.js +8 -0
- package/node_modules/@oxygen/shared/dist/version.generated.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/version.generated.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
package/dist/command-manifest.js
CHANGED
|
@@ -111,8 +111,11 @@ const MUTATING_COMMANDS = new Set([
|
|
|
111
111
|
// The ledger audit reads that same balance first, with the same conditional writes.
|
|
112
112
|
"billing audit",
|
|
113
113
|
// Creates an approval or Checkout link; a first plan's Checkout can start the
|
|
114
|
-
// first-month offer window. The plan changes only after
|
|
114
|
+
// first-month offer window. The plan changes only after confirmation.
|
|
115
115
|
"billing change",
|
|
116
|
+
// No amount is a read; an amount/pack creates Stripe billing resources.
|
|
117
|
+
// The manifest describes all invocations, without inventing a credit debit.
|
|
118
|
+
"billing topup",
|
|
116
119
|
"billing retain",
|
|
117
120
|
// --approved records a domain's renewal exit, or with --revoke resumes renewals.
|
|
118
121
|
"managed-inboxes exit",
|
package/dist/help.js
CHANGED
|
@@ -453,6 +453,11 @@ export function argvRequestsJson(argv) {
|
|
|
453
453
|
}
|
|
454
454
|
function buildUnknownCommandHint(parent, path, token) {
|
|
455
455
|
const names = parent.commands.map((command) => command.name()).filter((name) => name !== "help");
|
|
456
|
+
const firstWord = token.trim().split(/\s+/)[0] ?? "";
|
|
457
|
+
if (/\s/.test(token) && parent.commands.some((command) => command.name() === firstWord || command.aliases().includes(firstWord))) {
|
|
458
|
+
const name = path.split(" ").slice(1).concat(firstWord).join(" ");
|
|
459
|
+
return `Hint: The subcommand and its options arrived as one argument. Pass them as separate shell arguments; quote individual values only. Inspect the grammar with \`${path.split(" ")[0]} commands get '${name}' --json\`.`;
|
|
460
|
+
}
|
|
456
461
|
const lower = token.toLowerCase();
|
|
457
462
|
// A read intent on a budget must never point at the mutating `set` command.
|
|
458
463
|
const synonym = path.endsWith(" budget") && lower === "get" && names.includes("list")
|
package/dist/index.js
CHANGED
|
@@ -83,6 +83,7 @@ import { SEQUENCE_LIFECYCLES, sequenceDisplayStatus } from "@oxygen/shared/seque
|
|
|
83
83
|
import { lookupColumnDataType } from "@oxygen/shared/table-field-types";
|
|
84
84
|
import { FILTER_TREE_OPERATORS } from "@oxygen/shared/table-filter-operators";
|
|
85
85
|
import { tableRunSummary } from "@oxygen/shared/table-run-summary";
|
|
86
|
+
import { capabilityDiscoveryRecovery } from "@oxygen/shared/user-capability-routing";
|
|
86
87
|
import { assertRecipeBundleSafe, assertPortableWorkflowDefinition, assertWorkflowGraphManifest, assertWorkflowManifest, buildRecipeManifest, compileWorkflowDefinition, isAnyWorkflowManifest, isRecipeManifest, isWorkflowDefinition, isWorkflowGraphManifest, isWorkflowManifest, hashPortableWorkflowGraphManifest, WORKFLOW_GRAPH_COMPILER_VERSION, WORKFLOW_GRAPH_MANIFEST_VERSION, } from "@oxygen/workflows";
|
|
87
88
|
import { Command, CommanderError, Option } from "commander";
|
|
88
89
|
import { execFileSync } from "node:child_process";
|
|
@@ -1645,14 +1646,6 @@ function writeMaxCreditsHint(error) {
|
|
|
1645
1646
|
if (reservation === null)
|
|
1646
1647
|
return;
|
|
1647
1648
|
const nextStep = readDetailsString(error.details, "next_step");
|
|
1648
|
-
// A free workspace connecting LinkedIn is sent to the plan, not to a top-up (2026-10-11).
|
|
1649
|
-
if (readDetailsString(error.details, "remedy") === "subscribe") {
|
|
1650
|
-
const upgradeUrl = readDetailsString(error.details, "upgrade_url");
|
|
1651
|
-
process.stderr.write(`hint: this connect reserves ${reservation} credits a month; subscribe to the Oxygen plan, then connect again\n`);
|
|
1652
|
-
if (upgradeUrl)
|
|
1653
|
-
process.stderr.write(`hint: choose a plan at ${upgradeUrl}\n`);
|
|
1654
|
-
return;
|
|
1655
|
-
}
|
|
1656
1649
|
const topupUrl = readDetailsString(error.details, "topup_url");
|
|
1657
1650
|
process.stderr.write(`hint: this connect reserves ${reservation} credits a month; add credits, then connect again\n`);
|
|
1658
1651
|
if (nextStep)
|
|
@@ -3676,6 +3669,7 @@ export function createProgram() {
|
|
|
3676
3669
|
const program = new Command();
|
|
3677
3670
|
const binaryName = resolveCliBinaryName();
|
|
3678
3671
|
const directoryDocsUrl = `${defaultApiUrl()}/docs/agencies`;
|
|
3672
|
+
const billingDocsUrl = `${defaultApiUrl()}/docs/safety/billing`;
|
|
3679
3673
|
program
|
|
3680
3674
|
.name(binaryName)
|
|
3681
3675
|
.description("Revenue infrastructure for B2B startups — agent-operated GTM: tables, enrichment, sequences, workflows, CRM, knowledge. Everything here also runs in the web app and over MCP; every state-changing action returns a web_url deep-link.")
|
|
@@ -3955,10 +3949,13 @@ export function createProgram() {
|
|
|
3955
3949
|
.action(async (queryParts, options) => {
|
|
3956
3950
|
await handleAsyncAction("capabilities search", options, async () => {
|
|
3957
3951
|
const query = queryParts.join(" ");
|
|
3952
|
+
const route = inferUserCapabilityRoute(query);
|
|
3958
3953
|
return {
|
|
3959
3954
|
query,
|
|
3960
|
-
route: serializeCapabilityRoute(
|
|
3961
|
-
hint:
|
|
3955
|
+
route: serializeCapabilityRoute(route),
|
|
3956
|
+
hint: route
|
|
3957
|
+
? "Hydrate one exact CLI command with `oxygen commands get <exact-command> --json`, or one MCP tool with `oxygen_capabilities_schema`."
|
|
3958
|
+
: capabilityDiscoveryRecovery(query, "cli"),
|
|
3962
3959
|
alternate_search_hint: `If this route does not fit the outcome, run \`oxygen skills search ${JSON.stringify(query)} --json\` for a narrower operational play.`,
|
|
3963
3960
|
};
|
|
3964
3961
|
});
|
|
@@ -8532,9 +8529,8 @@ Examples:
|
|
|
8532
8529
|
.option("--force", "Re-arm past an existing once-ever marker, the one deliberate way to research the same workspace twice. Without --live it only previews the re-arm for free: nothing is cleared or written. With --live it replaces the marker with a new pass and spends again.")
|
|
8533
8530
|
.option("--json", "Print a JSON envelope.")
|
|
8534
8531
|
.action(async (options) => {
|
|
8535
|
-
await handleAsyncAction("knowledge bootstrap", options, () =>
|
|
8536
|
-
|
|
8537
|
-
body: {
|
|
8532
|
+
await handleAsyncAction("knowledge bootstrap", options, () => {
|
|
8533
|
+
const parameters = {
|
|
8538
8534
|
mode: options.live || options.approved ? "live" : "dry_run",
|
|
8539
8535
|
...(readOption(options.domain) ? { domain: readOption(options.domain) } : {}),
|
|
8540
8536
|
...(readOption(options.linkedin) ? { linkedin_url: readOption(options.linkedin) } : {}),
|
|
@@ -8542,8 +8538,11 @@ Examples:
|
|
|
8542
8538
|
? { max_credits: readPositiveNumber(options.maxCredits) }
|
|
8543
8539
|
: {}),
|
|
8544
8540
|
...(options.force ? { force: true } : {}),
|
|
8545
|
-
}
|
|
8546
|
-
|
|
8541
|
+
};
|
|
8542
|
+
return parameters.mode === "live"
|
|
8543
|
+
? requestOxygen("/api/cli/knowledge/bootstrap", { method: "POST", body: parameters })
|
|
8544
|
+
: requestOxygen(knowledgeReadPath("/api/cli/knowledge/bootstrap", parameters));
|
|
8545
|
+
});
|
|
8547
8546
|
})
|
|
8548
8547
|
.addCommand(new Command("status")
|
|
8549
8548
|
.description("Report the once-ever bootstrap marker for this workspace: status (queued, running, completed, partial, skipped, failed, or never attempted), when it ran, its system run id, the credits it actually spent, the domain researched, the reason it stopped, and the pages it wrote. Free, reads only, and the honest way to find out whether the automatic pass already happened before you spend on a manual one.")
|
|
@@ -8556,10 +8555,7 @@ Examples:
|
|
|
8556
8555
|
...options,
|
|
8557
8556
|
json: options.json || Boolean(knowledgeBootstrapCommand.opts().json),
|
|
8558
8557
|
};
|
|
8559
|
-
await handleAsyncAction("knowledge bootstrap status", outputOptions, () => requestOxygen("/api/cli/knowledge/bootstrap"
|
|
8560
|
-
method: "POST",
|
|
8561
|
-
body: { status: true },
|
|
8562
|
-
}));
|
|
8558
|
+
await handleAsyncAction("knowledge bootstrap status", outputOptions, () => requestOxygen("/api/cli/knowledge/bootstrap?status=true"));
|
|
8563
8559
|
}));
|
|
8564
8560
|
program
|
|
8565
8561
|
.command("knowledge")
|
|
@@ -8696,13 +8692,10 @@ Examples:
|
|
|
8696
8692
|
.action(async (slugOrId, options) => {
|
|
8697
8693
|
await handleAsyncAction("knowledge page revisions", options, () => {
|
|
8698
8694
|
const limit = readPositiveInt(options.limit);
|
|
8699
|
-
return requestOxygen("/api/cli/knowledge/pages/revisions", {
|
|
8700
|
-
|
|
8701
|
-
|
|
8702
|
-
|
|
8703
|
-
...(limit !== undefined ? { limit } : {}),
|
|
8704
|
-
},
|
|
8705
|
-
});
|
|
8695
|
+
return requestOxygen(knowledgeReadPath("/api/cli/knowledge/pages/revisions", {
|
|
8696
|
+
...knowledgePageRefBody(slugOrId),
|
|
8697
|
+
...(limit !== undefined ? { limit } : {}),
|
|
8698
|
+
}));
|
|
8706
8699
|
});
|
|
8707
8700
|
}))
|
|
8708
8701
|
.addCommand(new Command("revision")
|
|
@@ -8718,13 +8711,10 @@ Examples:
|
|
|
8718
8711
|
details: { revision },
|
|
8719
8712
|
});
|
|
8720
8713
|
}
|
|
8721
|
-
return requestOxygen("/api/cli/knowledge/pages/revisions/get", {
|
|
8722
|
-
|
|
8723
|
-
|
|
8724
|
-
|
|
8725
|
-
revision: revisionNumber,
|
|
8726
|
-
},
|
|
8727
|
-
});
|
|
8714
|
+
return requestOxygen(knowledgeReadPath("/api/cli/knowledge/pages/revisions/get", {
|
|
8715
|
+
...knowledgePageRefBody(slugOrId),
|
|
8716
|
+
revision: revisionNumber,
|
|
8717
|
+
}));
|
|
8728
8718
|
});
|
|
8729
8719
|
})))
|
|
8730
8720
|
.addCommand(new Command("search")
|
|
@@ -8770,10 +8760,7 @@ Examples:
|
|
|
8770
8760
|
.option("--depth <n>", "Local-graph hop depth (1-3, default 1). Only applies with --center.")
|
|
8771
8761
|
.option("--json", "Print a JSON envelope.")
|
|
8772
8762
|
.action(async (options) => {
|
|
8773
|
-
await handleAsyncAction("knowledge graph", options, () => requestOxygen("/api/cli/knowledge/graph",
|
|
8774
|
-
method: "POST",
|
|
8775
|
-
body: buildKnowledgeGraphBody(options),
|
|
8776
|
-
}));
|
|
8763
|
+
await handleAsyncAction("knowledge graph", options, () => requestOxygen(knowledgeReadPath("/api/cli/knowledge/graph", buildKnowledgeGraphBody(options))));
|
|
8777
8764
|
}))
|
|
8778
8765
|
.addCommand(new Command("lint")
|
|
8779
8766
|
.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.")
|
|
@@ -8802,10 +8789,7 @@ Examples:
|
|
|
8802
8789
|
.option("--limit <n>", "Maximum log entries to return.")
|
|
8803
8790
|
.option("--json", "Print a JSON envelope.")
|
|
8804
8791
|
.action(async (options) => {
|
|
8805
|
-
await handleAsyncAction("knowledge log", options, () => requestOxygen("/api/cli/knowledge/log",
|
|
8806
|
-
method: "POST",
|
|
8807
|
-
body: buildKnowledgeLogListBody(options),
|
|
8808
|
-
}));
|
|
8792
|
+
await handleAsyncAction("knowledge log", options, () => requestOxygen(knowledgeReadPath("/api/cli/knowledge/log", buildKnowledgeLogListBody(options))));
|
|
8809
8793
|
}))
|
|
8810
8794
|
.addCommand(new Command("append")
|
|
8811
8795
|
.description("Append an entry to the knowledge log.")
|
|
@@ -8823,14 +8807,16 @@ Examples:
|
|
|
8823
8807
|
.addCommand(new Command("proposals")
|
|
8824
8808
|
.description("Draft change proposals against wiki pages, awaiting human review. Read-only, 0 credits.")
|
|
8825
8809
|
.option("--status <status>", "open, decided, or all. Defaults to open.")
|
|
8810
|
+
.option("--limit <n>", "Maximum proposals to return. Defaults to 50; server caps at 200.")
|
|
8826
8811
|
.option("--json", "Print a JSON envelope.")
|
|
8827
8812
|
.action(async (options) => {
|
|
8828
|
-
await handleAsyncAction("knowledge proposals", options, () =>
|
|
8829
|
-
|
|
8830
|
-
|
|
8813
|
+
await handleAsyncAction("knowledge proposals", options, () => {
|
|
8814
|
+
const limit = readPositiveInt(options.limit);
|
|
8815
|
+
return requestOxygen(knowledgeReadPath("/api/cli/knowledge/proposals", {
|
|
8831
8816
|
...(readOption(options.status) ? { status: readOption(options.status) } : {}),
|
|
8832
|
-
|
|
8833
|
-
|
|
8817
|
+
...(limit !== undefined ? { limit } : {}),
|
|
8818
|
+
}));
|
|
8819
|
+
});
|
|
8834
8820
|
})
|
|
8835
8821
|
.addCommand(new Command("approve")
|
|
8836
8822
|
.description("Approve a knowledge proposal and apply it to the target page.")
|
|
@@ -10869,13 +10855,30 @@ Examples:
|
|
|
10869
10855
|
.addCommand(new Command("retry-failed")
|
|
10870
10856
|
.description("Requeue failed items for a durable table action run. Items with an unconfirmed external effect stay blocked until their destination is verified and explicitly approved.")
|
|
10871
10857
|
.argument("<run_id>", "Table action run UUID.")
|
|
10858
|
+
.option("--dry-run", "Read the exact retry scope and approval hash without requeueing.")
|
|
10859
|
+
.option("--approved", "Approve the inspected retry scope and additional-credit ceiling.")
|
|
10860
|
+
.option("--retry-plan-hash <hash>", "Hash returned by the retry dry run.")
|
|
10861
|
+
.option("--max-additional-credits <n>", "Fresh credit allowance for this recovery; never raises the original lifetime ceiling.")
|
|
10872
10862
|
.option("--approved-effect-unknown", "After a human verifies every affected destination, explicitly allow re-executing items whose prior external effect could not be confirmed.")
|
|
10873
10863
|
.option("--json", "Print a JSON envelope.")
|
|
10874
10864
|
.action(async (runId, options) => {
|
|
10875
|
-
|
|
10876
|
-
|
|
10877
|
-
|
|
10878
|
-
|
|
10865
|
+
if (options.dryRun && (options.approved || options.retryPlanHash || options.maxAdditionalCredits !== undefined || options.approvedEffectUnknown)) {
|
|
10866
|
+
throw new OxygenError("invalid_request", "Use --dry-run separately from retry approval flags.", { exitCode: 2 });
|
|
10867
|
+
}
|
|
10868
|
+
const maxAdditionalCredits = options.maxAdditionalCredits === undefined ? undefined : Number(options.maxAdditionalCredits);
|
|
10869
|
+
if (maxAdditionalCredits !== undefined && (!options.maxAdditionalCredits?.trim() || !Number.isFinite(maxAdditionalCredits) || maxAdditionalCredits < 0)) {
|
|
10870
|
+
throw new OxygenError("invalid_request", "--max-additional-credits must be a nonnegative number.", { exitCode: 2 });
|
|
10871
|
+
}
|
|
10872
|
+
await handleAsyncAction("table-runs retry-failed", options, () => options.dryRun
|
|
10873
|
+
? requestOxygen(`/api/cli/table-action-runs/${encodeURIComponent(runId)}/retry-failed`, { method: "GET" })
|
|
10874
|
+
: requestOxygen(`/api/cli/table-action-runs/${encodeURIComponent(runId)}/retry-failed`, {
|
|
10875
|
+
method: "POST", body: {
|
|
10876
|
+
...(options.approvedEffectUnknown ? { approved_effect_unknown: true } : {}),
|
|
10877
|
+
...(options.approved !== undefined ? { approved: options.approved } : {}),
|
|
10878
|
+
...(options.retryPlanHash !== undefined ? { retry_plan_hash: options.retryPlanHash } : {}),
|
|
10879
|
+
...(maxAdditionalCredits !== undefined ? { max_additional_credits: maxAdditionalCredits } : {}),
|
|
10880
|
+
},
|
|
10881
|
+
}));
|
|
10879
10882
|
}));
|
|
10880
10883
|
program
|
|
10881
10884
|
.command("table-ingestions")
|
|
@@ -11486,7 +11489,7 @@ Examples:
|
|
|
11486
11489
|
.description("Plan-tier limits, spend-safety defaults, per-channel sending limits, and storage capacity posture. `limits show --section capacity` returns storage alone, including which Table is full.")
|
|
11487
11490
|
.addHelpText("after", "\nRun `oxygen limits show --json` to see your current plan's API and webhook rates, Table and workspace storage headroom, and the next scheduled change. The $999 and $1,999 rows-per-Table increases remain held until capacity testing passes.\n")
|
|
11488
11491
|
.addCommand(addContractFlags(new Command("show")
|
|
11489
|
-
.description("Compare every plan size: price, monthly credits, volume bonus and price per 100 credits (`plan_bands`). Show your plan band and its limits beside every plan size's (API rate, webhook ingress, storage, import file size, columns per auto-run, signals feed, spend defaults incl. Agent and Copilot caps, fastest workflow schedule), the per-channel sending limits every plan shares (emails per mailbox, WhatsApp, LinkedIn, calls) and its workflow Code-step and run-attempt limits, plus Tables capacity usage naming the largest Table. Each plan size's storage says whether it is in force, scheduled to change on a date, or awaiting its rows-per-Table capacity test, with the values it moves to. `spend_safety_defaults.enterprise_agent_run_default` gives Enterprise the $1,999 band’s in-force Agent default with no contract override; `agent_run_approved_max_credits` is the maximum for an explicitly approved higher run. `spend_safety_defaults.applies_to` says what each spend default bounds: an unattended trigger delivery, a table auto-run batch, one Agent run, or the attended Copilot. `web_url` opens the same view in the web app (Settings → Plan limits), including any scheduled change. Read-only; spends no credits. Recovery:
|
|
11492
|
+
.description("Compare every plan size: price, monthly credits, volume bonus and price per 100 credits (`plan_bands`). Show your plan band and its limits beside every plan size's (API rate, webhook ingress, storage, import file size, columns per auto-run, signals feed, spend defaults incl. Agent and Copilot caps, fastest workflow schedule), the per-channel sending limits every plan shares (emails per mailbox, WhatsApp, LinkedIn, calls) and its workflow Code-step and run-attempt limits, plus Tables capacity usage naming the largest Table. Each plan size's storage says whether it is in force, scheduled to change on a date, or awaiting its rows-per-Table capacity test, with the values it moves to. `spend_safety_defaults.enterprise_agent_run_default` gives Enterprise the $1,999 band’s in-force Agent default with no contract override; `agent_run_approved_max_credits` is the maximum for an explicitly approved higher run. `spend_safety_defaults.applies_to` says what each spend default bounds: an unattended trigger delivery, a table auto-run batch, one Agent run, or the attended Copilot. `web_url` opens the same view in the web app (Settings → Plan limits), including any scheduled change. Read-only; spends no credits. Recovery: " + billingDocsUrl + "."), LIMITS_READ_QUERY_CONTRACT)
|
|
11490
11493
|
.option("--json", "Print a JSON envelope.")
|
|
11491
11494
|
.action(async (options) => {
|
|
11492
11495
|
await handleAsyncAction("limits show", options, () => {
|
|
@@ -11529,9 +11532,9 @@ Examples:
|
|
|
11529
11532
|
})));
|
|
11530
11533
|
program
|
|
11531
11534
|
.command("billing")
|
|
11532
|
-
.description("Plan and managed credit commands. Credits reserved but not yet charged are listed hold by hold with `billing usage --meter holds`. 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. On a plan that pays for several workspaces, `billing usage --group-by workspace` totals what each one spent. To price a month of outreach before building it (leads, enrichment, sequence steps, senders), run `billing estimate`. Failed-payment grace, suspension, and recovery:
|
|
11535
|
+
.description("Plan and managed credit commands. Credits reserved but not yet charged are listed hold by hold with `billing usage --meter holds`. 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. On a plan that pays for several workspaces, `billing usage --group-by workspace` totals what each one spent. To price a month of outreach before building it (leads, enrichment, sequence steps, senders), run `billing estimate`. Failed-payment grace, suspension, and recovery: " + billingDocsUrl + ".")
|
|
11533
11536
|
.addCommand(new Command("change")
|
|
11534
|
-
.description("Create an approval link (`approval_url`) to start or change a plan;
|
|
11537
|
+
.description("Create an approval link (`approval_url`) to start or change a plan; no charge or plan change until confirmation. First purchase needs an admin/owner; existing-plan changes allow any unrestricted member. Compare all six prices, credits and yearly terms with `billing balance` first. Use --interval month --term year for twelve discounted monthly payments, renewing for another year: the $99 plan costs $82.50 per month before tax, not $990 upfront. Credits arrive monthly after successful payment. On a workspace that has never had a plan, the Checkout for any monthly plan from $99 up carries the first-month offer, half its first month, while its 24-hour window is open (`offer`; `billing balance` lists every size under `first_month_offer.plans`): `amount_due_now` is then the discounted first month and `next_invoice_total` the regular price, both before tax. Creating that Checkout starts the window if nothing has started it yet. Upgrades during an annual commitment apply immediately with proration and keep its end date; downgrades and switching to a monthly commitment wait for the term end. Switching from a monthly commitment to yearly starts at the next monthly boundary. Legacy prepaid yearly upgrades that keep their cadence apply immediately with proration; downgrades and term or cadence changes wait until the paid year ends. Omit interval and term to preserve the current contract. For an existing subscription, a yearly preview returns charges, start and term-end dates; nothing changes until separate confirmation.")
|
|
11535
11538
|
.requiredOption("--to <plan>",
|
|
11536
11539
|
// Derived from the catalog, never retyped: this help text is the only
|
|
11537
11540
|
// place a customer's agent learns which plans exist, and it named
|
|
@@ -11563,7 +11566,7 @@ Examples:
|
|
|
11563
11566
|
}));
|
|
11564
11567
|
}))
|
|
11565
11568
|
.addCommand(new Command("balance")
|
|
11566
|
-
.description("Compare all six subscription sizes in `available_plans`: monthly prices, yearly paid-monthly prices, credit allowances and savings. `annual_monthly_billing_available` says whether yearly paid monthly is offered; `annual_monthly_terms` explains the 12-month commitment, annual renewal, remaining payments after cancellation and plan-change timing. `first_month_offer` is the first-month offer this workspace can still claim: every size it applies to (`plans`, from `minimum_plan` up) with its first-month and regular price before tax, and `ends_at`, null until its 24-hour window starts. The window starts the first time the workspace meets the offer: the app showing it, signup from a browser that saw it on the website, or creating its Checkout with `billing change`. Also show the current plan, commitment, 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 subscription's recurring amount 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). `billing_interval` is payment cadence; `commitment` distinguishes yearly paid monthly from monthly and legacy prepaid yearly plans. Credits are granted monthly; `next_credit_grant_at` is the next grant. For a yearly commitment, cancellation stops annual renewal but remaining monthly payments continue until commitment.ends_at. `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. `partner_bonus` says whether this workspace earns the agency partner bonus (+15% credits a month for a published directory partner on $99 or above), how many credits, and why not when it does not; the bonus arrives as its own ledger line (`billing usage --category partner_bonus`), never inside plan.monthly_credits. A workspace linked to another organization (`orgs billing-link`) reads its billing at that owner: the balance is the owner's pool, `shared_billing` is true and `billing_owner_organization_id` names the owner. After failed-payment grace expires, mutations stop while read/export and billing recovery remain available. Recovery:
|
|
11569
|
+
.description("Compare all six subscription sizes in `available_plans`: monthly prices, yearly paid-monthly prices, credit allowances and savings. `annual_monthly_billing_available` says whether yearly paid monthly is offered; `annual_monthly_terms` explains the 12-month commitment, annual renewal, remaining payments after cancellation and plan-change timing. `first_month_offer` is the first-month offer this workspace can still claim: every size it applies to (`plans`, from `minimum_plan` up) with its first-month and regular price before tax, and `ends_at`, null until its 24-hour window starts. The window starts the first time the workspace meets the offer: the app showing it, signup from a browser that saw it on the website, or creating its Checkout with `billing change`. Also show the current plan, commitment, 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 subscription's recurring amount 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). `billing_interval` is payment cadence; `commitment` distinguishes yearly paid monthly from monthly and legacy prepaid yearly plans. Credits are granted monthly; `next_credit_grant_at` is the next grant. For a yearly commitment, cancellation stops annual renewal but remaining monthly payments continue until commitment.ends_at. `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. `partner_bonus` says whether this workspace earns the agency partner bonus (+15% credits a month for a published directory partner on $99 or above), how many credits, and why not when it does not; the bonus arrives as its own ledger line (`billing usage --category partner_bonus`), never inside plan.monthly_credits. A workspace linked to another organization (`orgs billing-link`) reads its billing at that owner: the balance is the owner's pool, `shared_billing` is true and `billing_owner_organization_id` names the owner. After failed-payment grace expires, mutations stop while read/export and billing recovery remain available. Recovery: " + defaultApiUrl() + "/billing. Policy: " + billingDocsUrl + ". 0 credits; reserves nothing. Due plan grants or credit expiry may apply.")
|
|
11567
11570
|
.option("--json", "Print a JSON envelope.")
|
|
11568
11571
|
.action(async (options) => {
|
|
11569
11572
|
await handleAsyncAction("billing balance", options, () => requestOxygen("/api/cli/billing/balance"));
|
|
@@ -11634,7 +11637,7 @@ Examples:
|
|
|
11634
11637
|
await handleAsyncAction("billing allowance", options, () => requestOxygen("/api/cli/billing/allowance"));
|
|
11635
11638
|
}))
|
|
11636
11639
|
.addCommand(new Command("usage")
|
|
11637
|
-
.description("Show credit ledger events (each tagged with its FIXED/FLEXIBLE spend_class), workflow step usage (automation_actions: credits per step, credits billed; writing rows is free), calls with your own API keys (byok_calls: the platform fee per call and the fee credits this month;
|
|
11640
|
+
.description("Show credit ledger events (each tagged with its FIXED/FLEXIBLE spend_class), workflow step usage (automation_actions: credits per step, credits billed; writing rows is free), calls with your own API keys (byok_calls: the platform fee per successful call and the fee credits this month; vendor charges follow your provider's terms), or connected LinkedIn and WhatsApp accounts (provider_seats: the rolling-30d peak and its implied connectivity cost; what each account is charged is its credit reservation in `billing commitments`). On a plan that pays for several workspaces (an agency covering client workspaces), `--group-by workspace` totals the credits each workspace spent (`by_workspace`) and `--workspace <id|slug>` narrows the ledger to one of them; a linked client workspace sees only its own spend. managed_credits_used counts captured credits; for where credits went by category, provider or tool, with shares, run `billing audit`.")
|
|
11638
11641
|
.option("--meter <meter>", "credits, automation_actions, byok_calls, provider_seats (rolling-30d peak connected accounts and the flat per-account charge; each account's reservation is listed by `billing commitments`), or holds (each held reservation behind reserved_credits in this workspace: credits, run, why it is held (an unmarked LinkedIn lookup hold cannot say whether its result was unknown or delivered), and automatic_return: LinkedIn lookup holds from 24 hours to 30 days old and explicitly unknown LeadMagic email-validation holds older than 24 hours are eligible while automatic return is switched on, and an hourly release returns it on a later pass with no fixed deadline; read-only, newest first, --days up to 365 and --limit apply). Defaults to credits.")
|
|
11639
11642
|
.option("--days <n>", "Lookback window in days. Defaults to 30.")
|
|
11640
11643
|
.option("--from <iso>", "Only include ledger events at or after this ISO timestamp.")
|
|
@@ -11790,7 +11793,7 @@ Examples:
|
|
|
11790
11793
|
await handleAsyncAction("billing retain", options, () => requestOxygen("/api/cli/billing/cancel", { method: "POST", body: { action: "retain", offer_id: options.offer } }));
|
|
11791
11794
|
}))
|
|
11792
11795
|
.addCommand(new Command("topup")
|
|
11793
|
-
.description("
|
|
11796
|
+
.description("No amount: read-only price quote, no checkout. With --credits or a pack: create Stripe Checkout and possibly billing resources; no charge until payment. Read tax_basis and review tax and the final total in Checkout. Prices are per 100 credits (`usd_per_100_credits`); credits arrive after confirmed payment and never expire. Plan credits cost less. `usd_per_1k_credits` is a deprecated name for the same per-100 price, not a price per 1,000 credits. `default_credits` is the web form's initial custom selection, not a purchase or minimum.")
|
|
11794
11797
|
.argument("[pack]", "A pack id from the list shown when you run without an amount.")
|
|
11795
11798
|
.option("--credits <n>", "Credits to buy (800-100,000 in 100-credit increments).")
|
|
11796
11799
|
.option("--json", "Print a JSON envelope.")
|
|
@@ -13411,7 +13414,7 @@ Examples:
|
|
|
13411
13414
|
.command("enrich-column")
|
|
13412
13415
|
.description("High-level table enrichment helpers. To see every enrichment a table's columns support, with per-row prices, before choosing one, run `tools search --for-table <table>` (0 credits).")
|
|
13413
13416
|
.addCommand(new Command("preview")
|
|
13414
|
-
.description("Preflight an enrichment column without provider calls or credit usage. A lane on your own key (BYOK) is quoted at OXYGEN's platform fee per call,
|
|
13417
|
+
.description("Preflight an enrichment column without provider calls or credit usage. A lane on your own key (BYOK) is quoted at OXYGEN's platform fee per successful call, including a successful miss; vendor charges follow your provider's terms (provider_cost_breakdown[].byok_platform_fee_credits_per_call).")
|
|
13415
13418
|
.argument("<table>", "Table id or slug.")
|
|
13416
13419
|
.option("--source-column <column>", "Source column key or id. For mobile_phone this is the LinkedIn URL column.")
|
|
13417
13420
|
.option("--full-name-column <column>", "Column key or id containing the person's full name for work_email. Pair with company-domain/name for Prospeo, LeadMagic, Hunter, Dropleads, ContactOut, BetterContact, or People Data Labs.")
|
|
@@ -13503,7 +13506,7 @@ Examples:
|
|
|
13503
13506
|
.option("--company-domain <domain>", "Company apex domain, e.g. acme.com.")
|
|
13504
13507
|
.option("--company-name <name>", "Company name.")
|
|
13505
13508
|
.option("--mode <mode>", "dry_run (default) previews the plan and spends nothing; live spends credits and returns the answer.")
|
|
13506
|
-
.option("--max-credits <credits>", "Spend ceiling in Oxygen credits
|
|
13509
|
+
.option("--max-credits <credits>", "Spend ceiling in Oxygen credits, including the platform fee on each successful own-key call, even on a miss. Your provider may separately bill your account under its own rules, outside this ceiling (cap own-key calls with `oxygen limits provider set`). Required for --mode live. Run the free dry_run first: it returns estimate.recommended_max_credits, the ceiling that lets the WHOLE resolved waterfall run, plus estimate.min_credits and a per-leg breakdown. When the resolved chain runs the guessed-address pass (the name+domain and first/last+domain profiles, not the LinkedIn-URL one) the block also carries estimate.pattern_guess_verification_credits — the verification checks that pass bills before any named provider is dialled. A lower ceiling does NOT stop the run: each lane priced above what is left of it is refused on its own (spend_cap_too_low) and the waterfall advances, so a cheaper lane further down the chain can still run and bill.")
|
|
13507
13510
|
.option("--json", "Print a JSON envelope.")
|
|
13508
13511
|
.action(async (options) => {
|
|
13509
13512
|
await handleAsyncAction("find email", options, () => requestOxygen("/api/cli/find/run", { method: "POST", body: buildFindBody("email", options) }));
|
|
@@ -13516,7 +13519,7 @@ Examples:
|
|
|
13516
13519
|
.option("--company-domain <domain>", "Company apex domain, e.g. acme.com.")
|
|
13517
13520
|
.option("--company-name <name>", "Company name.")
|
|
13518
13521
|
.option("--mode <mode>", "dry_run (default) previews the plan and spends nothing; live spends credits and returns the answer.")
|
|
13519
|
-
.option("--max-credits <credits>", "Spend ceiling in Oxygen credits
|
|
13522
|
+
.option("--max-credits <credits>", "Spend ceiling in Oxygen credits, including the platform fee on each successful own-key call, even on a miss. Your provider may separately bill your account under its own rules, outside this ceiling (cap own-key calls with `oxygen limits provider set`). Required for --mode live. Run the free dry_run first: it returns estimate.recommended_max_credits, the ceiling that lets the WHOLE resolved waterfall run, plus estimate.min_credits and a per-leg breakdown. A lower ceiling does NOT stop the run: each lane priced above what is left of it is refused on its own (spend_cap_too_low) and the waterfall advances, so a cheaper lane further down the chain can still run and bill.")
|
|
13520
13523
|
.option("--json", "Print a JSON envelope.")
|
|
13521
13524
|
.action(async (options) => {
|
|
13522
13525
|
await handleAsyncAction("find personal-email", options, () => requestOxygen("/api/cli/find/run", { method: "POST", body: buildFindBody("personal_email", options) }));
|
|
@@ -13530,7 +13533,7 @@ Examples:
|
|
|
13530
13533
|
.option("--company-name <name>", "Company name.")
|
|
13531
13534
|
.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).")
|
|
13532
13535
|
.option("--mode <mode>", "dry_run (default) previews the plan and spends nothing; live spends credits and returns the answer.")
|
|
13533
|
-
.option("--max-credits <credits>", "Spend ceiling in Oxygen credits
|
|
13536
|
+
.option("--max-credits <credits>", "Spend ceiling in Oxygen credits, including the platform fee on each successful own-key call, even on a miss. Your provider may separately bill your account under its own rules, outside this ceiling (cap own-key calls with `oxygen limits provider set`). Required for --mode live. Run the free dry_run first: it returns estimate.recommended_max_credits, the ceiling that lets the WHOLE resolved waterfall run, plus estimate.min_credits and a per-leg breakdown. A lower ceiling does NOT stop the run: each lane priced above what is left of it is refused on its own (spend_cap_too_low) and the waterfall advances, so a cheaper lane further down the chain can still run and bill.")
|
|
13534
13537
|
.option("--json", "Print a JSON envelope.")
|
|
13535
13538
|
.action(async (options) => {
|
|
13536
13539
|
await handleAsyncAction("find phone", options, () => requestOxygen("/api/cli/find/run", { method: "POST", body: buildFindBody("phone", options) }));
|
|
@@ -13543,7 +13546,7 @@ Examples:
|
|
|
13543
13546
|
.option("--company-name <name>", "Company name.")
|
|
13544
13547
|
.option("--company-linkedin-url <url>", "Company LinkedIn URL.")
|
|
13545
13548
|
.option("--mode <mode>", "dry_run (default) previews the plan and spends nothing; live spends credits and returns the answer.")
|
|
13546
|
-
.option("--max-credits <credits>", "Spend ceiling in Oxygen credits
|
|
13549
|
+
.option("--max-credits <credits>", "Spend ceiling in Oxygen credits, including the platform fee on each successful own-key call, even on a miss. Your provider may separately bill your account under its own rules, outside this ceiling (cap own-key calls with `oxygen limits provider set`). Required for --mode live. Run the free dry_run first: it returns estimate.recommended_max_credits, the ceiling that lets the WHOLE resolved waterfall run, plus estimate.min_credits and a per-leg breakdown. A lower ceiling does NOT stop the run: each lane priced above what is left of it is refused on its own (spend_cap_too_low) and the waterfall advances, so a cheaper lane further down the chain can still run and bill.")
|
|
13547
13550
|
.option("--json", "Print a JSON envelope.")
|
|
13548
13551
|
.action(async (options) => {
|
|
13549
13552
|
await handleAsyncAction("find linkedin", options, () => requestOxygen("/api/cli/find/run", { method: "POST", body: buildFindBody("linkedin", options) }));
|
|
@@ -13561,7 +13564,7 @@ Examples:
|
|
|
13561
13564
|
.option("--company-name <name>", "Company name.")
|
|
13562
13565
|
.option("--include-raw", "Also return the provider's raw profile payload (profile_raw) for fields the profile block does not carry.")
|
|
13563
13566
|
.option("--mode <mode>", "dry_run (default) previews the plan and spends nothing; live spends credits and returns the profile.")
|
|
13564
|
-
.option("--max-credits <credits>", "Spend ceiling in Oxygen credits
|
|
13567
|
+
.option("--max-credits <credits>", "Spend ceiling in Oxygen credits, including the platform fee on each successful own-key call, even on a miss. Your provider may separately bill your account under its own rules, outside this ceiling (cap own-key calls with `oxygen limits provider set`). Required for --mode live. Run the free dry_run first: estimate.recommended_max_credits funds the profile lookup plus, for an email or name input, every lane of the LinkedIn URL waterfall that resolves it.")
|
|
13565
13568
|
.option("--json", "Print a JSON envelope.")
|
|
13566
13569
|
.action(async (options) => {
|
|
13567
13570
|
await handleAsyncAction("find person", options, () => requestOxygen("/api/cli/find/run", { method: "POST", body: buildFindBody("person", options) }));
|
|
@@ -13575,7 +13578,7 @@ Examples:
|
|
|
13575
13578
|
.option("--list-fields", "Print the field catalog (category, description, provider count, per-row credits: free / fixed / ~estimated) and exit. Needs no identity; spends nothing.")
|
|
13576
13579
|
.option("--check-technologies <names>", "Comma-separated technology names for the technology_check field, answered yes/no against the detected stack (e.g. hubspot,salesforce).")
|
|
13577
13580
|
.option("--mode <mode>", "dry_run (default) previews the plan and spends nothing; live spends credits and returns the answer.")
|
|
13578
|
-
.option("--max-credits <credits>", "Spend ceiling in Oxygen credits
|
|
13581
|
+
.option("--max-credits <credits>", "Spend ceiling in Oxygen credits, including the platform fee on each successful own-key call, even on a miss. Your provider may separately bill your account under its own rules, outside this ceiling (cap own-key calls with `oxygen limits provider set`). Required for --mode live; 0 can still call providers on zero-credit lanes and reuse any priced lane this workspace already has a cached answer for (the other priced lanes are skipped as credit_ceiling_reached). Run the free dry_run first: it returns estimate.recommended_max_credits, the ceiling that funds every lane in the plan, and estimate.min_credits, what the plan costs if each field is answered by its first lane.")
|
|
13579
13582
|
.option("--json", "Print a JSON envelope.")
|
|
13580
13583
|
.action(async (options) => {
|
|
13581
13584
|
await handleAsyncAction("find company", options, () => requestOxygen("/api/cli/find/run", { method: "POST", body: buildFindBody("company", options) }));
|
|
@@ -16468,6 +16471,8 @@ Examples:
|
|
|
16468
16471
|
.option("--schedule-hours-start <HH:MM>", "Sending schedule start, local to --schedule-timezone, e.g. 09:00.")
|
|
16469
16472
|
.option("--schedule-hours-end <HH:MM>", "Sending schedule end (exclusive), e.g. 17:00. An email step's own send_window (on the step in --steps-file) still overrides it for that step. LinkedIn sends also stay inside each LinkedIn account's working hours (default 07:00-22:00 in the account's timezone; set with `oxygen senders limits set`); WhatsApp accounts and email mailboxes have no hours of their own.")
|
|
16470
16473
|
.option("--send-window-file <path>", "Email-only window JSON: { timezone, days?, start, end, timezone_mode?, recipient_timezone_column? }; days are ISO 1-7. New sequences default to weekdays 09:00-17:00 UTC. Use it to keep email on different hours from LinkedIn/WhatsApp, or to send in each recipient's own timezone (timezone_mode recipient, which --schedule-* then keeps); for one schedule across channels, use --schedule-* instead.")
|
|
16474
|
+
.option("--linkedin-invite-schedule <json>", 'LinkedIn invites only: { "timezone": "Europe/Berlin", "days": [1,2,3,4,5,6,7], "start": "09:00", "end": "17:00" }. Replaces the general Sequence schedule for invites; account working hours still apply. null restores the general schedule.')
|
|
16475
|
+
.option("--linkedin-message-schedule <json>", 'LinkedIn messages and InMail only: { "timezone": "Europe/Berlin", "days": [1,2,3,4,5], "start": "10:00", "end": "16:00" }. Replaces the general Sequence schedule for messages; account working hours still apply. null restores the general schedule.')
|
|
16471
16476
|
.option("--linkedin-timezone <tz>", "LinkedIn/WhatsApp-only sending schedule: IANA zone, e.g. Europe/Zurich. Set with --linkedin-days, --linkedin-hours-start and --linkedin-hours-end; for LinkedIn the window narrows each account's working hours, for WhatsApp it is the only hours limit. For one schedule across channels, use --schedule-* instead.")
|
|
16472
16477
|
.option("--linkedin-days <days>", "LinkedIn sending schedule days: mon-fri, mon,wed,fri, or ISO numbers 1-7 (1 = Monday).")
|
|
16473
16478
|
.option("--linkedin-hours-start <HH:MM>", "LinkedIn sending schedule start, local to --linkedin-timezone, e.g. 06:45.")
|
|
@@ -29604,6 +29609,12 @@ function buildKnowledgePageListBody(options) {
|
|
|
29604
29609
|
...(limit !== undefined ? { limit } : {}),
|
|
29605
29610
|
};
|
|
29606
29611
|
}
|
|
29612
|
+
function knowledgeReadPath(path, parameters) {
|
|
29613
|
+
const query = new URLSearchParams(Object.entries(parameters)
|
|
29614
|
+
.filter(([, value]) => value !== undefined)
|
|
29615
|
+
.map(([key, value]) => [key, String(value)]));
|
|
29616
|
+
return query.size ? `${path}?${query}` : path;
|
|
29617
|
+
}
|
|
29607
29618
|
function buildKnowledgeGraphBody(options) {
|
|
29608
29619
|
const maxNodes = readPositiveInt(options.maxNodes);
|
|
29609
29620
|
const center = readOption(options.center);
|
|
@@ -29649,6 +29660,7 @@ const KNOWLEDGE_SYNC_DEFAULT_TTL_MINUTES = 15;
|
|
|
29649
29660
|
// 500 batches × the server's 100-page default is far above any v1 wiki; the cap
|
|
29650
29661
|
// only exists so a misbehaving server can never wedge the CLI in a loop.
|
|
29651
29662
|
const KNOWLEDGE_SYNC_MAX_BATCHES = 500;
|
|
29663
|
+
const KNOWLEDGE_SYNC_CATALOG_MAX_BATCHES = 500;
|
|
29652
29664
|
// Envelope error codes meaning this org can no longer be read with the stored
|
|
29653
29665
|
// token (revoked membership, expired key, org mismatch). The mirror is a local
|
|
29654
29666
|
// cache of tenant data and must not outlive access to it.
|
|
@@ -29779,6 +29791,43 @@ function applyKnowledgeSyncPage(dir, state, page, counters) {
|
|
|
29779
29791
|
};
|
|
29780
29792
|
counters.synced += 1;
|
|
29781
29793
|
}
|
|
29794
|
+
async function readKnowledgeMirrorCatalog(requestOrg, mirrorPath) {
|
|
29795
|
+
let cursor = null;
|
|
29796
|
+
const seenCursors = new Set();
|
|
29797
|
+
const pages = new Map();
|
|
29798
|
+
for (let batch = 0; batch < KNOWLEDGE_SYNC_CATALOG_MAX_BATCHES; batch += 1) {
|
|
29799
|
+
const query = new URLSearchParams({ max_chars: "48000", limit: "1000", ...(cursor ? { cursor } : {}) });
|
|
29800
|
+
const window = await requestOxygen(`/api/cli/knowledge/index?${query}`, requestOrg);
|
|
29801
|
+
if (typeof window.has_more !== "boolean" || !Number.isInteger(window.max_chars) || window.max_chars < 2000 || window.max_chars > 48000 ||
|
|
29802
|
+
!Array.isArray(window.index?.pages) || window.index.capped !== window.has_more ||
|
|
29803
|
+
(window.next_cursor !== null && typeof window.next_cursor !== "string") || (!window.has_more && window.next_cursor !== null)) {
|
|
29804
|
+
throw new OxygenError("knowledge_sync_catalog_invalid", "The knowledge catalog did not return a complete bounded continuation receipt. The generated index was left unchanged.", {
|
|
29805
|
+
details: { catalog_cursor: cursor, mirror_path: mirrorPath }, exitCode: 1,
|
|
29806
|
+
});
|
|
29807
|
+
}
|
|
29808
|
+
for (const page of window.index.pages) {
|
|
29809
|
+
if (!page || typeof page.slug !== "string" || !page.slug || pages.has(page.slug)) {
|
|
29810
|
+
throw new OxygenError("knowledge_sync_catalog_changed", "The catalog repeated or omitted a page slug during live traversal. Retry sync to restart the catalog; the generated index was left unchanged.", {
|
|
29811
|
+
details: { catalog_cursor: cursor, mirror_path: mirrorPath }, exitCode: 1,
|
|
29812
|
+
});
|
|
29813
|
+
}
|
|
29814
|
+
pages.set(page.slug, page);
|
|
29815
|
+
}
|
|
29816
|
+
if (!window.has_more) {
|
|
29817
|
+
return { ...window, index: { ...window.index, pages: [...pages.values()] } };
|
|
29818
|
+
}
|
|
29819
|
+
if (!window.next_cursor || seenCursors.has(window.next_cursor)) {
|
|
29820
|
+
throw new OxygenError("knowledge_sync_catalog_stalled", "The catalog cursor did not advance; the generated index was left unchanged. Retry sync to restart the catalog from the preserved page watermark.", {
|
|
29821
|
+
details: { catalog_cursor: cursor, next_cursor: window.next_cursor, mirror_path: mirrorPath }, exitCode: 1,
|
|
29822
|
+
});
|
|
29823
|
+
}
|
|
29824
|
+
cursor = window.next_cursor;
|
|
29825
|
+
seenCursors.add(cursor);
|
|
29826
|
+
}
|
|
29827
|
+
throw new OxygenError("knowledge_sync_catalog_incomplete", "The catalog batch cap was reached before its terminal receipt. The generated index was left unchanged; retry sync to restart the catalog from the preserved page watermark.", {
|
|
29828
|
+
details: { catalog_cursor: cursor, mirror_path: mirrorPath, batch_cap: KNOWLEDGE_SYNC_CATALOG_MAX_BATCHES }, exitCode: 1,
|
|
29829
|
+
});
|
|
29830
|
+
}
|
|
29782
29831
|
// `orgOverride` targets a specific org (used by `sync --all`): it names the mirror
|
|
29783
29832
|
// directory explicitly and pins X-Oxygen-Organization on every request in the
|
|
29784
29833
|
// loop, so each workspace clones into its own mirror regardless of the cached
|
|
@@ -29842,11 +29891,12 @@ options, orgOverride) {
|
|
|
29842
29891
|
lastSeen = { updated_at: page.updated_at, id: page.id };
|
|
29843
29892
|
}
|
|
29844
29893
|
if (!delta.has_more) {
|
|
29845
|
-
//
|
|
29846
|
-
//
|
|
29847
|
-
//
|
|
29848
|
-
|
|
29849
|
-
|
|
29894
|
+
// The native SQL watermark retains timestamp precision that the public
|
|
29895
|
+
// updated_at Date can lose. Older servers lack last_cursor; retain their
|
|
29896
|
+
// compatibility fallback without claiming it preserves SQL precision.
|
|
29897
|
+
if (delta.last_cursor)
|
|
29898
|
+
state.cursor = delta.last_cursor;
|
|
29899
|
+
else if (lastSeen)
|
|
29850
29900
|
state.cursor = `${lastSeen.updated_at}|${lastSeen.id}`;
|
|
29851
29901
|
completed = true;
|
|
29852
29902
|
break;
|
|
@@ -29868,19 +29918,18 @@ options, orgOverride) {
|
|
|
29868
29918
|
exitCode: 1,
|
|
29869
29919
|
});
|
|
29870
29920
|
}
|
|
29871
|
-
//
|
|
29872
|
-
//
|
|
29873
|
-
|
|
29874
|
-
|
|
29875
|
-
|
|
29876
|
-
|
|
29877
|
-
|
|
29878
|
-
|
|
29879
|
-
|
|
29880
|
-
|
|
29881
|
-
if (!state.pages.index)
|
|
29921
|
+
// Save page progress before summary reads so a failed catalog can resume its
|
|
29922
|
+
// delta without declaring a fresh, fully completed mirror.
|
|
29923
|
+
state.last_sync_at = null;
|
|
29924
|
+
writeMirrorState(target.dir, state);
|
|
29925
|
+
// Traverse every native catalog window, including shared-only continuations.
|
|
29926
|
+
// Its scope excludes archived/proposal pages; it is a live keyset, not a
|
|
29927
|
+
// snapshot of every cloned page. Real `index`/`log` page bytes take priority.
|
|
29928
|
+
const index = state.pages.index ? null : await readKnowledgeMirrorCatalog(requestOrg, target.dir);
|
|
29929
|
+
const logPage = state.pages.log ? null : await requestOxygen("/api/cli/knowledge/log", requestOrg);
|
|
29930
|
+
if (index)
|
|
29882
29931
|
writeGeneratedIndexFile(target.dir, index.index);
|
|
29883
|
-
if (
|
|
29932
|
+
if (logPage)
|
|
29884
29933
|
writeGeneratedLogFile(target.dir, logPage.entries);
|
|
29885
29934
|
state.last_sync_at = new Date().toISOString();
|
|
29886
29935
|
writeMirrorState(target.dir, state);
|
|
@@ -30569,6 +30618,10 @@ function readSequenceSettings(options) {
|
|
|
30569
30618
|
if (linkedinSchedule) {
|
|
30570
30619
|
settings.schedule = linkedinSchedule;
|
|
30571
30620
|
}
|
|
30621
|
+
if (options.linkedinInviteSchedule !== undefined)
|
|
30622
|
+
settings.linkedin_invite_schedule = parseJsonValue(options.linkedinInviteSchedule, "--linkedin-invite-schedule");
|
|
30623
|
+
if (options.linkedinMessageSchedule !== undefined)
|
|
30624
|
+
settings.linkedin_message_schedule = parseJsonValue(options.linkedinMessageSchedule, "--linkedin-message-schedule");
|
|
30572
30625
|
// Tri-state: `--no-whatsapp-cold-initiate` (update) turns the opt-in off.
|
|
30573
30626
|
if (options.whatsappColdInitiate !== undefined) {
|
|
30574
30627
|
settings.whatsapp_cold_initiate = options.whatsappColdInitiate;
|
package/dist/skills.js
CHANGED
|
@@ -7,6 +7,7 @@ import { defaultApiUrl, loadCredentials, normalizeApiUrl } from "./credentials.j
|
|
|
7
7
|
import { requestOxygen } from "./http-client.js";
|
|
8
8
|
import { isRecord, readOption } from "./util.js";
|
|
9
9
|
import { resolveCmdShimInvocation } from "./windows-shim.js";
|
|
10
|
+
import { capabilityDiscoveryRecovery } from "@oxygen/shared/user-capability-routing";
|
|
10
11
|
const DEFAULT_SKILL_AGENTS = ["codex", "claude-code", "cursor"];
|
|
11
12
|
const SKILL_INDEX_PATH = "/.well-known/skills/index.json";
|
|
12
13
|
const SKILLS_ACCESS_API_PATH = "/api/cli/skills/access";
|
|
@@ -144,6 +145,7 @@ export async function searchAgentSkills(query, options, runtime = {}) {
|
|
|
144
145
|
return {
|
|
145
146
|
query: normalized,
|
|
146
147
|
route: serializeCapabilityRoute(route, { visibleSkills: index.skill_names }),
|
|
148
|
+
...(!route ? { hint: capabilityDiscoveryRecovery(normalized, "cli") } : {}),
|
|
147
149
|
total_matches: matches.length,
|
|
148
150
|
returned: Math.min(limit, matches.length),
|
|
149
151
|
skills: matches.slice(0, limit).map(({ skill, score, matching_documents }) => ({
|