@oxygen-agent/cli 1.906.0 → 1.922.12
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 +13 -1
- package/dist/index.js +337 -40
- package/node_modules/@oxygen/formula/dist/expression.d.ts +21 -0
- package/node_modules/@oxygen/formula/dist/expression.js +42 -1
- package/node_modules/@oxygen/formula/dist/formula-functions.d.ts +1 -1
- package/node_modules/@oxygen/formula/dist/formula-functions.js +10 -1
- package/node_modules/@oxygen/shared/dist/billing.d.ts +17 -0
- package/node_modules/@oxygen/shared/dist/billing.js +20 -0
- package/node_modules/@oxygen/shared/dist/capability-discovery.js +12 -7
- package/node_modules/@oxygen/shared/dist/copilot-errors.d.ts +1 -0
- package/node_modules/@oxygen/shared/dist/copilot-errors.js +9 -0
- package/node_modules/@oxygen/shared/dist/copilot-plan.d.ts +39 -0
- package/node_modules/@oxygen/shared/dist/copilot-plan.js +76 -7
- package/node_modules/@oxygen/shared/dist/langfuse.d.ts +36 -0
- package/node_modules/@oxygen/shared/dist/langfuse.js +83 -0
- package/node_modules/@oxygen/shared/dist/linkedin-quota-denial.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/linkedin-quota-denial.js +16 -5
- package/node_modules/@oxygen/shared/dist/linkedin-url.d.ts +22 -0
- package/node_modules/@oxygen/shared/dist/linkedin-url.js +104 -0
- package/node_modules/@oxygen/shared/dist/provider-funding-errors.d.ts +16 -2
- package/node_modules/@oxygen/shared/dist/provider-funding-errors.js +19 -9
- package/node_modules/@oxygen/shared/dist/table-limits.d.ts +18 -0
- package/node_modules/@oxygen/shared/dist/table-limits.js +18 -0
- package/node_modules/@oxygen/shared/dist/user-capability-routing.js +23 -2
- package/node_modules/@oxygen/shared/dist/version.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/version.js +1 -1
- package/node_modules/@oxygen/shared/package.json +10 -0
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -9,7 +9,7 @@ import { fileURLToPath, pathToFileURL } from "node:url";
|
|
|
9
9
|
import { Command, CommanderError, Option } from "commander";
|
|
10
10
|
import { applyOxygenHelp } from "./help.js";
|
|
11
11
|
import { buildCommandManifest, getCommandManifestEntry, searchCommandManifest, suggestCommandNames, } from "./command-manifest.js";
|
|
12
|
-
import { AGENCY_DIRECTORY_REGIONS, AGENCY_DIRECTORY_SERVICES, COLLAB_GATE_KINDS, COLLAB_GATE_PROSE, COLLAB_SUBJECT_KINDS, COLLAB_SUBJECT_KINDS_PROSE, COLLAB_SUBJECT_LABELS, COLLAB_SUBJECT_PROSE, GATE_KIND_SUBJECTS, describeWorkflowStatusChange, formatCellForDisplay, formatPublicBudgetScopes, SUBJECT_PATH_FORMS_PROSE, formatSubjectPath, exitCodeForOxygenError, parseSubjectPath, parseSubjectRef, parseWorkflowStatusChange, isVersionGreater, isVersionLess, KNOWLEDGE_BOOTSTRAP_MAX_CREDITS, MAX_MCP_TOOL_NAME_LENGTH, normalizeCopilotPlanStepStatus, OXYGEN_CAPABILITY_ROUTES, OXYGEN_VERSION, OxygenError, getCapabilityRouteMatch, inferUserCapabilityRoute, parseKnowledgePageMarkdown, PLAN_LIMITS, serializeCapabilityRoute, sleep, success, TABLE_IMPORT_ROW_LIMIT, TAG_KINDS_PROSE, toFailure, workflowMcpToolName, } from "@oxygen/shared";
|
|
12
|
+
import { AGENCY_DIRECTORY_REGIONS, AGENCY_DIRECTORY_SERVICES, COLLAB_GATE_KINDS, COLLAB_GATE_PROSE, COLLAB_SUBJECT_KINDS, COLLAB_SUBJECT_KINDS_PROSE, COLLAB_SUBJECT_LABELS, COLLAB_SUBJECT_PROSE, GATE_KIND_SUBJECTS, describeWorkflowStatusChange, formatCellForDisplay, formatCopilotPlanDuration, formatCopilotPlanSeconds, formatPublicBudgetScopes, SUBJECT_PATH_FORMS_PROSE, formatSubjectPath, exitCodeForOxygenError, parseSubjectPath, parseSubjectRef, parseWorkflowStatusChange, isVersionGreater, isVersionLess, KNOWLEDGE_BOOTSTRAP_MAX_CREDITS, MAX_MCP_TOOL_NAME_LENGTH, normalizeCopilotPlanStepStatus, OXYGEN_CAPABILITY_ROUTES, OXYGEN_VERSION, OxygenError, getCapabilityRouteMatch, inferUserCapabilityRoute, parseKnowledgePageMarkdown, PLAN_LIMITS, serializeCapabilityRoute, sleep, success, TABLE_IMPORT_ROW_LIMIT, TAG_KINDS_PROSE, toFailure, workflowMcpToolName, } from "@oxygen/shared";
|
|
13
13
|
import { TAG_COLORS } from "@oxygen/shared/select-options";
|
|
14
14
|
import { inferImportColumnLabels, inferRowsFileFormat, normalizeImportColumnKey, normalizeRowsForNewTable, normalizeRowsFormat, parseRowsFileBuffer, parseXlsxWorkbookBuffer, } from "@oxygen/shared/file-import";
|
|
15
15
|
import { MAILBOX_IMPORT_FILE_MAX_BYTES as SHARED_MAILBOX_IMPORT_FILE_MAX_BYTES, MAILBOX_IMPORT_ROW_LIMIT as SHARED_MAILBOX_IMPORT_ROW_LIMIT, normalizeMailboxImportFile as normalizeSharedMailboxImportFile, normalizeMailboxImportVendor as normalizeSharedMailboxImportVendor, normalizeMailboxWorkbookRows, parseMailboxImportText, summarizeMailboxImportValidation as summarizeSharedMailboxImportValidation, } from "@oxygen/shared/mailbox-import";
|
|
@@ -416,6 +416,11 @@ function emitSuccess(command, data, options) {
|
|
|
416
416
|
async function handleAsyncAction(command, options, action) {
|
|
417
417
|
try {
|
|
418
418
|
const data = await action();
|
|
419
|
+
// BEFORE the envelope, deliberately: an unpaid renewal or a recurring shortfall is the
|
|
420
|
+
// reason the customer ran the command, and a warning printed after 150 lines of JSON
|
|
421
|
+
// has already scrolled away. stdout stays the clean machine-readable envelope — the
|
|
422
|
+
// same stdout/stderr split writeDryRunNotice and writeCreditsReceipt use.
|
|
423
|
+
writeBillingNotices(command, data);
|
|
419
424
|
emitSuccess(command, data, options);
|
|
420
425
|
writeDryRunNotice(data);
|
|
421
426
|
writeAvatarWarning(data);
|
|
@@ -479,6 +484,57 @@ function writeAvatarWarning(data) {
|
|
|
479
484
|
if (typeof warning === "string" && warning.trim())
|
|
480
485
|
process.stderr.write(`${warning}\n`);
|
|
481
486
|
}
|
|
487
|
+
// The human half of the unpaid-renewal surfaces: `billing balance` warnings, the
|
|
488
|
+
// `billing allowance` past_due segment, and the billing state of each managed domain in
|
|
489
|
+
// `domains list`. All three are already in the JSON envelope (and stay there for --json);
|
|
490
|
+
// these lines exist because none of them is legible at a glance in a payload, and the one
|
|
491
|
+
// customer this shipped for could read every number in the product and still not learn
|
|
492
|
+
// that 139,000 credits of renewals had failed.
|
|
493
|
+
function writeBillingNotices(command, data) {
|
|
494
|
+
const payload = asPayloadRecord(data);
|
|
495
|
+
if (!payload)
|
|
496
|
+
return;
|
|
497
|
+
if (command === "billing balance") {
|
|
498
|
+
for (const warning of readWarningMessages(payload))
|
|
499
|
+
process.stderr.write(`! ${warning}\n`);
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
if (command === "billing allowance") {
|
|
503
|
+
const unpaid = asPayloadRecord(payload.renewals_past_due);
|
|
504
|
+
if (!unpaid)
|
|
505
|
+
return;
|
|
506
|
+
const count = typeof unpaid.count === "number" ? unpaid.count : 0;
|
|
507
|
+
const credits = typeof unpaid.credits_required === "number" ? unpaid.credits_required : 0;
|
|
508
|
+
// Said explicitly because it is the one number in this command that is NOT part of the
|
|
509
|
+
// pool: it is money the biller could not take, so it is not in total_credits.
|
|
510
|
+
process.stderr.write(`! renewals unpaid: ${count} item${count === 1 ? "" : "s"}, ${credits.toLocaleString("en-US")} credits — not included in total_credits. Run \`oxygen billing balance\` for the top-up amount.\n`);
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
if (command === "domains list") {
|
|
514
|
+
const managed = Array.isArray(payload.managed_domains) ? payload.managed_domains : [];
|
|
515
|
+
const unpaid = managed
|
|
516
|
+
.map((row) => asPayloadRecord(row))
|
|
517
|
+
.filter((row) => row?.internal_billing_status === "past_due")
|
|
518
|
+
.map((row) => (typeof row.domain === "string" ? row.domain : "unknown"));
|
|
519
|
+
if (unpaid.length === 0)
|
|
520
|
+
return;
|
|
521
|
+
// The vendor keeps these live, so their `status` still reads `active` — which is
|
|
522
|
+
// exactly why naming them is worth a line.
|
|
523
|
+
process.stderr.write(`! renewal unpaid on ${unpaid.length} managed domain${unpaid.length === 1 ? "" : "s"}: ${unpaid.join(", ")} (see internal_billing_status)\n`);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
function readWarningMessages(payload) {
|
|
527
|
+
if (!Array.isArray(payload.warnings))
|
|
528
|
+
return [];
|
|
529
|
+
return payload.warnings
|
|
530
|
+
.map((warning) => asPayloadRecord(warning)?.message)
|
|
531
|
+
.filter((message) => typeof message === "string" && message.trim().length > 0);
|
|
532
|
+
}
|
|
533
|
+
function asPayloadRecord(value) {
|
|
534
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
535
|
+
? value
|
|
536
|
+
: null;
|
|
537
|
+
}
|
|
482
538
|
// Paid envelopes (push 3 legibility) carry a `credits` block: quote on
|
|
483
539
|
// dry_run, receipt on live, remaining balance on both. Mirror it as one
|
|
484
540
|
// stderr line so spend stays visible in a terminal without polluting the
|
|
@@ -567,6 +623,72 @@ function writeObservabilityCapsNotice(data) {
|
|
|
567
623
|
: Array.isArray(record.items) ? record.items.length : 0;
|
|
568
624
|
process.stderr.write(`note: showing ${returned} item(s); some sources hit the per-source cap (${cappedSources.join(", ")}) — raise --limit (max 100) or filter --source to see more.\n`);
|
|
569
625
|
}
|
|
626
|
+
// Discovery lists are windowed by default (200 rows, max 1000) so the cost of
|
|
627
|
+
// asking "what is in this workspace?" does not scale with how much the customer
|
|
628
|
+
// has built. A window nobody can see is indistinguishable from a complete
|
|
629
|
+
// answer, so say it on stderr — same treatment the observability console's
|
|
630
|
+
// per-source cap already gets. Machine-read stdout carries `capped`, `returned`
|
|
631
|
+
// and `limit` either way.
|
|
632
|
+
function writeListCapNotice(data, noun) {
|
|
633
|
+
if (!data || typeof data !== "object" || Array.isArray(data))
|
|
634
|
+
return;
|
|
635
|
+
const record = data;
|
|
636
|
+
if (record.capped !== true)
|
|
637
|
+
return;
|
|
638
|
+
const returned = typeof record.returned === "number" ? record.returned : null;
|
|
639
|
+
process.stderr.write(`note: showing ${returned === null ? "a window of" : returned} ${noun}; more exist — raise --limit (max 1000) or pass --all.\n`);
|
|
640
|
+
}
|
|
641
|
+
// The map is bounded twice over — a scan window per kind, then a character
|
|
642
|
+
// budget across all of them — and both bounds are invisible in the rendered
|
|
643
|
+
// output. Say them on stderr for the same reason every other cap here is said:
|
|
644
|
+
// a window presented as a complete answer is a confident lie, and this one is
|
|
645
|
+
// the first thing an agent or a person reads about a workspace. `degraded` gets
|
|
646
|
+
// the same treatment: an empty kind that failed to read is not an empty kind.
|
|
647
|
+
function writeWorkspaceMapNotices(data) {
|
|
648
|
+
if (!data || typeof data !== "object" || Array.isArray(data))
|
|
649
|
+
return;
|
|
650
|
+
const record = data;
|
|
651
|
+
const degraded = Array.isArray(record.degraded) ? record.degraded : [];
|
|
652
|
+
if (degraded.length > 0) {
|
|
653
|
+
process.stderr.write(`note: could not read ${degraded.join(", ")} — those kinds are unknown, not empty.\n`);
|
|
654
|
+
}
|
|
655
|
+
const kinds = Array.isArray(record.kinds) ? record.kinds : [];
|
|
656
|
+
const capped = kinds
|
|
657
|
+
.filter((kind) => Boolean(kind) && typeof kind === "object" && !Array.isArray(kind) && kind.capped === true)
|
|
658
|
+
.map((kind) => String(kind.kind));
|
|
659
|
+
if (capped.length > 0) {
|
|
660
|
+
process.stderr.write(`note: ${capped.join(", ")} hold more than this scan saw; their counts are a floor. Narrow with --query or --tag.\n`);
|
|
661
|
+
}
|
|
662
|
+
const budget = record.budget;
|
|
663
|
+
if (budget && typeof budget === "object" && !Array.isArray(budget)) {
|
|
664
|
+
const trimmed = budget.trimmed;
|
|
665
|
+
if (Array.isArray(trimmed) && trimmed.length > 0) {
|
|
666
|
+
process.stderr.write(`note: the character budget trimmed ${trimmed.length} item(s) from ${[...new Set(trimmed.map(String))].join(", ")}.\n`);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
const tagNote = record.tag_note;
|
|
670
|
+
if (typeof tagNote === "string" && tagNote)
|
|
671
|
+
process.stderr.write(`note: ${tagNote}\n`);
|
|
672
|
+
}
|
|
673
|
+
// The wiki index counts the whole wiki even when the page list is a window, so
|
|
674
|
+
// name both numbers: "200 of 512 pages" is honest, "200 pages" is not.
|
|
675
|
+
function writeKnowledgeIndexCapNotice(data) {
|
|
676
|
+
if (!data || typeof data !== "object" || Array.isArray(data))
|
|
677
|
+
return;
|
|
678
|
+
const index = data.index;
|
|
679
|
+
if (!index || typeof index !== "object" || Array.isArray(index))
|
|
680
|
+
return;
|
|
681
|
+
const block = index;
|
|
682
|
+
if (block.capped !== true)
|
|
683
|
+
return;
|
|
684
|
+
const counts = block.counts;
|
|
685
|
+
const returned = typeof counts?.returned === "number" ? counts.returned : null;
|
|
686
|
+
const total = typeof counts?.total === "number" ? counts.total : null;
|
|
687
|
+
const shown = returned === null
|
|
688
|
+
? "a window of this wiki's pages"
|
|
689
|
+
: `${returned}${total === null ? "" : ` of ${total}`} page(s)`;
|
|
690
|
+
process.stderr.write(`note: listing ${shown}, most recently updated first; counts cover the whole wiki — raise --limit (max 1000) or pass --all.\n`);
|
|
691
|
+
}
|
|
570
692
|
// Arming a cron commits recurring spend, so `workflows enable` mirrors its
|
|
571
693
|
// `automation` block as stderr lines: what the schedule burns per day, what
|
|
572
694
|
// share of the monthly allowance that is, and when it runs out. Observed burn
|
|
@@ -2648,6 +2770,38 @@ export function createProgram() {
|
|
|
2648
2770
|
.action(async (options) => {
|
|
2649
2771
|
await handleAsyncAction("home standup", options, readHomeStandup);
|
|
2650
2772
|
});
|
|
2773
|
+
// `oxygen workspace map` sits beside `oxygen home` on purpose: home answers
|
|
2774
|
+
// "what needs me", the map answers "what is in here". Both are Control-surface
|
|
2775
|
+
// compositions over reads the primitives already own — neither is a primitive.
|
|
2776
|
+
program
|
|
2777
|
+
.command("workspace")
|
|
2778
|
+
.description("Read what this workspace contains, across every primitive.")
|
|
2779
|
+
.addCommand(new Command("map")
|
|
2780
|
+
.description("What this workspace actually contains: the newest tables, CRM objects, sequences, workflows, agents, wiki pages, posts and projects, each with its id, last activity, tags and link. Read-only, 0 credits. Capability search tells you what OXYGEN can do; this tells you what is here.")
|
|
2781
|
+
.option("--kind <kinds>", "Comma-separated kinds to include: table, crm_object, sequence, workflow, agent, knowledge_page, post, project.")
|
|
2782
|
+
.option("--tag <tag>", "Only objects carrying this workspace tag. Answered by the Tags footprint read, so it also covers kinds this map does not model.")
|
|
2783
|
+
.option("--query <text>", "Match on name, slug, or tag.")
|
|
2784
|
+
.option("--json", "Print a JSON envelope.")
|
|
2785
|
+
.addHelpText("after", "\nEach kind returns its newest few objects under a character budget the payload echoes back; `capped` says when a kind holds more than the scan saw. Hydrate one object with its own primitive's read (`oxygen tables describe`, `oxygen workflows get`, `oxygen knowledge page get`).\n\nRead-only means read-only: the map creates nothing in your workspace in order to answer, so an empty wiki reports zero pages rather than a starter set this command seeded. Use `oxygen knowledge index` when you do want the starter wiki created.\n")
|
|
2786
|
+
.action(async (options) => {
|
|
2787
|
+
await handleAsyncAction("workspace map", options, async () => {
|
|
2788
|
+
const params = new URLSearchParams();
|
|
2789
|
+
const kind = readOption(options.kind);
|
|
2790
|
+
if (kind)
|
|
2791
|
+
params.set("kind", kind);
|
|
2792
|
+
const tag = readOption(options.tag);
|
|
2793
|
+
if (tag)
|
|
2794
|
+
params.set("tag", tag);
|
|
2795
|
+
const query = readOption(options.query);
|
|
2796
|
+
if (query)
|
|
2797
|
+
params.set("query", query);
|
|
2798
|
+
const qs = params.toString() ? `?${params.toString()}` : "";
|
|
2799
|
+
const data = await requestOxygen(`/api/cli/workspace/map${qs}`);
|
|
2800
|
+
if (!options.json)
|
|
2801
|
+
writeWorkspaceMapNotices(data);
|
|
2802
|
+
return data;
|
|
2803
|
+
});
|
|
2804
|
+
}));
|
|
2651
2805
|
const ACTIVATION_DESCRIPTION = "Where this workspace stands in the prescribed activation play (inbound-led outbound) and the ONE thing to do next: each step's state, what blocks a blocked one, the exact CLI / MCP tool / API call for the next step, and an honest pacing note — LinkedIn is read on a metered drip and a sender's warm-up ramp, not the size of your list, sets how fast anyone is contacted. Read-only, 0 credits. Read the play itself with `oxygen recipes list --stage day-1 --json`.";
|
|
2652
2806
|
// Bare `oxygen activation` is the spelling a founder guesses; `activation state`
|
|
2653
2807
|
// is the exact name discovery routes to (it is a gatewayCommand of the
|
|
@@ -3520,6 +3674,17 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
3520
3674
|
method: "POST",
|
|
3521
3675
|
body: { tags: splitCommaList(options.tags) },
|
|
3522
3676
|
}));
|
|
3677
|
+
}))
|
|
3678
|
+
.addCommand(new Command("color")
|
|
3679
|
+
.description("Set a project's colour in the Tables rail. `--color auto` restores the automatic colour.")
|
|
3680
|
+
.argument("<project>", "Project slug or id.")
|
|
3681
|
+
.requiredOption("--color <color>", "Colour name (gray, red, orange, amber, yellow, green, teal, blue, indigo, purple, pink) or `auto` to clear.")
|
|
3682
|
+
.option("--json", "Print a JSON envelope.")
|
|
3683
|
+
.action(async (project, options) => {
|
|
3684
|
+
await handleAsyncAction("projects color", options, () => requestOxygen(`/api/cli/projects/${encodeURIComponent(project)}/color`, {
|
|
3685
|
+
method: "POST",
|
|
3686
|
+
body: { color: options.color?.trim().toLowerCase() ?? null },
|
|
3687
|
+
}));
|
|
3523
3688
|
}));
|
|
3524
3689
|
program
|
|
3525
3690
|
.command("notetaker")
|
|
@@ -5038,9 +5203,12 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
5038
5203
|
.option("--project <project>", "Project id or slug to filter by.")
|
|
5039
5204
|
.option("--tag <tag>", "Only tables carrying this workspace tag (see `oxygen tags list`).")
|
|
5040
5205
|
.option("--include-archived", "Also list archived and delete-scheduled tables (the restorable trash; each carries a lifecycle with purge_scheduled_at).")
|
|
5206
|
+
.option("--limit <n>", "Maximum tables to list. Defaults to 200; hard cap is 1000.")
|
|
5207
|
+
.option("--all", "List every table instead of the newest window.")
|
|
5041
5208
|
.option("--json", "Print a JSON envelope.")
|
|
5209
|
+
.addHelpText("after", "\nLists the 200 newest tables by default; `capped` in the JSON envelope says when there are more.\n")
|
|
5042
5210
|
.action(async (options) => {
|
|
5043
|
-
await handleAsyncAction("tables list", options, () => {
|
|
5211
|
+
await handleAsyncAction("tables list", options, async () => {
|
|
5044
5212
|
const params = new URLSearchParams();
|
|
5045
5213
|
const project = readOption(options.project);
|
|
5046
5214
|
if (project)
|
|
@@ -5050,8 +5218,16 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
5050
5218
|
params.set("tag", tag);
|
|
5051
5219
|
if (options.includeArchived)
|
|
5052
5220
|
params.set("include_archived", "true");
|
|
5221
|
+
const limit = readPositiveInt(options.limit);
|
|
5222
|
+
if (limit !== undefined)
|
|
5223
|
+
params.set("limit", String(limit));
|
|
5224
|
+
if (options.all)
|
|
5225
|
+
params.set("all", "true");
|
|
5053
5226
|
const qs = params.toString() ? `?${params.toString()}` : "";
|
|
5054
|
-
|
|
5227
|
+
const data = await requestOxygen(`/api/cli/tables${qs}`);
|
|
5228
|
+
if (!options.json)
|
|
5229
|
+
writeListCapNotice(data, "table(s)");
|
|
5230
|
+
return data;
|
|
5055
5231
|
});
|
|
5056
5232
|
}))
|
|
5057
5233
|
.addCommand(new Command("query")
|
|
@@ -5375,7 +5551,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
5375
5551
|
.option("--on <column>", "Pin one table's column by name or key. Oxygen must still find a compatible column on the other table: generic text needs a strongly similar label, while domains, emails, LinkedIn URLs, and external IDs pair by semantic role.")
|
|
5376
5552
|
.option("--approved", "Queue the bulk link after inspecting the preview; then wait with `oxygen table-runs wait <run_id>`.")
|
|
5377
5553
|
.option("--create-missing", "Also create target rows for unmatched source keys (CRM records when filling an existing CRM relationship). Free.")
|
|
5378
|
-
.option("--max-concurrency <n>", "Maximum concurrent row items for the run. Defaults to
|
|
5554
|
+
.option("--max-concurrency <n>", "Maximum concurrent row items for the run (1-250). Defaults to 250; native --create-missing runs serialize target-row creation safely.")
|
|
5379
5555
|
.option("--undo <run_id>", "Undo a previous link run, archiving only the links that run created.")
|
|
5380
5556
|
.option("--relation <slug>", "Row form: relation slug defined on the source table, such as client.")
|
|
5381
5557
|
.option("--target-row-id <row_id>", "Row form: target row id in the related table.")
|
|
@@ -5462,7 +5638,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
5462
5638
|
.option("--row-id <id...>", "Promote only these row ids. Defaults to every bound row.")
|
|
5463
5639
|
.option("--dry-run", "Preview per-field would_fill/would_overwrite/conflicts without writing (the default when --approved is absent).")
|
|
5464
5640
|
.option("--approved", "Confirm the truth write onto CRM records after inspecting the preview.")
|
|
5465
|
-
.option("--max-concurrency <n>", "Maximum concurrent row items for the run. Defaults to 50.")
|
|
5641
|
+
.option("--max-concurrency <n>", "Maximum concurrent row items for the run (1-250). Defaults to 50.")
|
|
5466
5642
|
.option("--json", "Print a JSON envelope.")
|
|
5467
5643
|
.action(async (table, options) => {
|
|
5468
5644
|
if (options.dryRun && options.approved) {
|
|
@@ -6242,10 +6418,24 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
6242
6418
|
}));
|
|
6243
6419
|
}))
|
|
6244
6420
|
.addCommand(new Command("index")
|
|
6245
|
-
.description("Compact wiki index:
|
|
6421
|
+
.description("Compact wiki index: each page's slug, one-liner, tags, link degree, plus type/status counts over the whole wiki. Lists the 200 most recently updated pages by default.")
|
|
6422
|
+
.option("--limit <n>", "Maximum pages to list. Defaults to 200; hard cap is 1000. Counts always cover the whole wiki.")
|
|
6423
|
+
.option("--all", "List every page instead of the most recently updated window.")
|
|
6246
6424
|
.option("--json", "Print a JSON envelope.")
|
|
6247
6425
|
.action(async (options) => {
|
|
6248
|
-
await handleAsyncAction("knowledge index", options, () =>
|
|
6426
|
+
await handleAsyncAction("knowledge index", options, async () => {
|
|
6427
|
+
const params = new URLSearchParams();
|
|
6428
|
+
const limit = readPositiveInt(options.limit);
|
|
6429
|
+
if (limit !== undefined)
|
|
6430
|
+
params.set("limit", String(limit));
|
|
6431
|
+
if (options.all)
|
|
6432
|
+
params.set("all", "true");
|
|
6433
|
+
const qs = params.toString() ? `?${params.toString()}` : "";
|
|
6434
|
+
const data = await requestOxygen(`/api/cli/knowledge/index${qs}`);
|
|
6435
|
+
if (!options.json)
|
|
6436
|
+
writeKnowledgeIndexCapNotice(data);
|
|
6437
|
+
return data;
|
|
6438
|
+
});
|
|
6249
6439
|
}))
|
|
6250
6440
|
.addCommand(new Command("graph")
|
|
6251
6441
|
.description("Knowledge graph of pages and their [[wikilink]] edges. Pass --center to render one page's local neighborhood instead of the whole graph.")
|
|
@@ -7394,8 +7584,9 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
7394
7584
|
.option("--connection-id <connection_id>", "Optional provider integration connection id.")
|
|
7395
7585
|
.option("--background", "Create a durable background run for a free deterministic column. Paid AI/tool/enrichment/custom-HTTP server runs are always backgrounded.")
|
|
7396
7586
|
.option("--approved", "Confirm a paid durable run, or a bind create-mode run (onNoMatch=create), after inspecting a dry run or preview.")
|
|
7587
|
+
.option("--approved-effect-unknown", "With --row-id, allow rerunning a cell whose previous external write was dispatched but never confirmed. Verify the destination first: this can apply the same external effect twice.")
|
|
7397
7588
|
.option("--max-credits <n>", "Maximum managed model/provider/web-grounding credits to reserve. BYOK covers the model call only; managed grounding still needs this ceiling.")
|
|
7398
|
-
.option("--max-concurrency <n>", "Maximum concurrent row items for a background run. Defaults to 250 for AI columns and 50 otherwise.")
|
|
7589
|
+
.option("--max-concurrency <n>", "Maximum concurrent row items for a background run (1-250). Defaults to 250 for AI columns and 50 otherwise (160 for Firecrawl scrape columns).")
|
|
7399
7590
|
.option("--local", "Run a custom HTTP column in this CLI process so env-var secrets stay local.")
|
|
7400
7591
|
.option("--local-concurrency <n>", "Maximum concurrent custom HTTP requests for --local. Defaults to 3.")
|
|
7401
7592
|
.option("--dry-run", "Preview resolved model, credit estimate, run-condition posture, and — for an AI column — the prompt rendered with one real row's values, without spending any credits.")
|
|
@@ -7460,6 +7651,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
7460
7651
|
...(readOption(options.connectionId) ? { connection_id: readOption(options.connectionId) } : {}),
|
|
7461
7652
|
...(options.background ? { background: true } : {}),
|
|
7462
7653
|
...(options.approved ? { approved: true } : {}),
|
|
7654
|
+
...(options.approvedEffectUnknown ? { approved_effect_unknown: true } : {}),
|
|
7463
7655
|
...(maxCredits !== undefined ? { max_credits: maxCredits } : {}),
|
|
7464
7656
|
...(maxConcurrency ? { max_concurrency: maxConcurrency } : {}),
|
|
7465
7657
|
...(options.dryRun ? { dry_run: true } : {}),
|
|
@@ -7828,7 +8020,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
7828
8020
|
.command("table-runs")
|
|
7829
8021
|
.description("Durable background table action run commands.")
|
|
7830
8022
|
.addCommand(new Command("create")
|
|
7831
|
-
.description("Create a durable background run for table tool-column actions.")
|
|
8023
|
+
.description("Create a durable background run for table tool-column actions. Large runs are accepted immediately and their rows are prepared in the background (execution_status 'planning'); follow with `table-runs wait <run_id>`.")
|
|
7832
8024
|
.argument("<table>", "Table id or slug.")
|
|
7833
8025
|
.option("--column <column>", "Column id or key for a single tool-column action.")
|
|
7834
8026
|
.option("--actions-json <json>", "JSON array of actions, each with type tool_column and column.")
|
|
@@ -7841,7 +8033,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
7841
8033
|
.option("--connection-id <connection_id>", "Optional provider integration connection id.")
|
|
7842
8034
|
.option("--approved", "Confirm the paid table action run after inspecting a dry run or preview.")
|
|
7843
8035
|
.option("--max-credits <n>", "Maximum managed/provider credits to reserve for this run.")
|
|
7844
|
-
.option("--max-concurrency <n>", "Maximum concurrent row items for this run.")
|
|
8036
|
+
.option("--max-concurrency <n>", "Maximum concurrent row items for this run (1-250). Defaults to 50 (160 for Firecrawl scrape columns).")
|
|
7845
8037
|
.option("--metadata-json <json>", "Optional metadata object to attach to the run.")
|
|
7846
8038
|
.option("--then-json <json>", "JSON array of sequential follow-up steps. Each step runs after the previous terminates with completed or completed_with_errors. Paid steps require selection and max_credits. Shape: [{actions:[{type:'tool_column',column}],selection,force,max_concurrency,max_credits,metadata,run_on_failure}].")
|
|
7847
8039
|
.option("--json", "Print a JSON envelope.")
|
|
@@ -7946,6 +8138,24 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
7946
8138
|
method: "POST",
|
|
7947
8139
|
body: {},
|
|
7948
8140
|
}));
|
|
8141
|
+
}))
|
|
8142
|
+
.addCommand(new Command("effect-unknown")
|
|
8143
|
+
.description("List the items of a run whose external write was dispatched but never confirmed. Oxygen will not retry them on its own: verify each destination, then record what you found with `oxygen cells resolve`.")
|
|
8144
|
+
.argument("<run_id>", "Table action run UUID.")
|
|
8145
|
+
.option("--include-resolved", "Also list items whose outcome a human has already recorded.")
|
|
8146
|
+
.option("--limit <n>", "Maximum items to return. Defaults to 200.")
|
|
8147
|
+
.option("--json", "Print a JSON envelope.")
|
|
8148
|
+
.action(async (runId, options) => {
|
|
8149
|
+
await handleAsyncAction("table-runs effect-unknown", options, () => {
|
|
8150
|
+
const query = new URLSearchParams();
|
|
8151
|
+
const limit = readPositiveInt(options.limit);
|
|
8152
|
+
if (limit)
|
|
8153
|
+
query.set("limit", String(limit));
|
|
8154
|
+
if (options.includeResolved)
|
|
8155
|
+
query.set("include_resolved", "true");
|
|
8156
|
+
const suffix = query.toString() ? `?${query.toString()}` : "";
|
|
8157
|
+
return requestOxygen(`/api/cli/table-action-runs/${encodeURIComponent(runId)}/effect-unknown${suffix}`);
|
|
8158
|
+
});
|
|
7949
8159
|
}))
|
|
7950
8160
|
.addCommand(new Command("retry-failed")
|
|
7951
8161
|
.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.")
|
|
@@ -8546,7 +8756,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
8546
8756
|
}));
|
|
8547
8757
|
}))
|
|
8548
8758
|
.addCommand(new Command("balance")
|
|
8549
|
-
.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.")
|
|
8759
|
+
.description("Show the current plan, subscription entitlement, and managed credit balance: available and reserved credits, FIXED credits committed to recurring per-resource charges, and the FLEXIBLE free-to-spend remainder. `renewals_past_due` lists infrastructure renewals (managed-inbox domains, warm-ups) OXYGEN could NOT charge this month, what they cost, and the exact top-up that clears them — nothing is cancelled and billing retries automatically after a top-up. `warnings` also flags a recurring_shortfall when your connected infrastructure costs more per month than the plan grants. After failed-payment grace expires, mutations stop while read/export and billing recovery remain available. Recovery: https://oxygen-agent.com/billing. Policy: https://oxygen-agent.com/docs/safety/billing.")
|
|
8550
8760
|
.option("--json", "Print a JSON envelope.")
|
|
8551
8761
|
.action(async (options) => {
|
|
8552
8762
|
await handleAsyncAction("billing balance", options, () => requestOxygen("/api/cli/billing/balance"));
|
|
@@ -8558,13 +8768,13 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
8558
8768
|
await handleAsyncAction("billing commitments", options, () => requestOxygen("/api/cli/billing/commitments"));
|
|
8559
8769
|
}))
|
|
8560
8770
|
.addCommand(new Command("seats")
|
|
8561
|
-
.description("Show sending capacity per channel: how many seats are purchased, how many are grandfathered, how many senders are connected, and how many are left. Sending seats are what let you CONNECT a sender — a LinkedIn account, WhatsApp number, phone number, or email mailbox — and they are billed in dollars on their own subscription, separate from your plan and separate from credits. You do not need a plan to buy seats. A grandfathered value of null means unlimited for that channel. Email mailbox allocation is
|
|
8771
|
+
.description("Show sending capacity per channel: how many seats are purchased, how many are grandfathered, how many senders are connected, and how many are left. Sending seats are what let you CONNECT a sender — a LinkedIn account, WhatsApp number, phone number, or email mailbox — and they are billed in dollars on their own subscription, separate from your plan and separate from credits. You do not need a plan to buy seats. A grandfathered value of null means unlimited for that channel. Email mailbox allocation is read from this workspace's own database; when it cannot be read, the email seat's allocated/available/can_connect read null and `email_sender_allocation` is `tenant_unavailable`. Read-only, 0 Oxygen credits.")
|
|
8562
8772
|
.option("--json", "Print a JSON envelope.")
|
|
8563
8773
|
.action(async (options) => {
|
|
8564
8774
|
await handleAsyncAction("billing seats", options, () => requestOxygen("/api/cli/billing/seats"));
|
|
8565
8775
|
})
|
|
8566
8776
|
.addCommand(new Command("set")
|
|
8567
|
-
.description("Set how many sending seats of one channel this workspace holds. The quantity is ABSOLUTE, not a delta: `--quantity 3` means you end up with three, so a retried command cannot buy extra. Increasing charges the card pro rata immediately; decreasing credits you pro rata. Reducing below the number of senders you have connected is REFUSED and names how many to disconnect first, because an orphaned sender is one you keep paying a provider for while Oxygen has stopped honouring it. Charges real money.")
|
|
8777
|
+
.description("Set how many sending seats of one channel this workspace holds. The quantity is ABSOLUTE, not a delta: `--quantity 3` means you end up with three, so a retried command cannot buy extra. Increasing charges the card pro rata immediately; decreasing credits you pro rata. Reducing below the number of senders you have connected is REFUSED and names how many to disconnect first, because an orphaned sender is one you keep paying a provider for while Oxygen has stopped honouring it. An email_sender reduction is refused with `seat_allocation_unavailable` (503) while the workspace's mailbox count cannot be read; retry it in a moment. Charges real money.")
|
|
8568
8778
|
.requiredOption("--seat-key <kind>", "linkedin, whatsapp, phone_number, or email_sender.")
|
|
8569
8779
|
.requiredOption("--quantity <n>", "Total seats to hold for this channel, 0 or more.")
|
|
8570
8780
|
.option("--json", "Print a JSON envelope.")
|
|
@@ -8584,7 +8794,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
8584
8794
|
await handleAsyncAction("billing invoices", options, () => requestOxygen("/api/cli/billing/invoices"));
|
|
8585
8795
|
}))
|
|
8586
8796
|
.addCommand(new Command("allowance")
|
|
8587
|
-
.description("Show this billing cycle's credit pool as one breakdown: fixed spend, flexible spend, credits committed to the next renewal, and what is free to spend — the numbers behind the bar on the credit-usage page. The parts always sum to the total. fixed_spent_credits is what has ALREADY been charged this cycle, NOT your monthly run-rate — for the forward figure billed at the next renewal see `billing commitments`, which is normally much larger. reserved_credits is in-flight spend already carved out of free_to_spend_credits, so treat free_to_spend as the ceiling and free_to_spend minus reserved as what is genuinely uncommitted. Every segment carries region=fixed|flexible. Read-only, 0 Oxygen credits.")
|
|
8797
|
+
.description("Show this billing cycle's credit pool as one breakdown: fixed spend, flexible spend, credits committed to the next renewal, and what is free to spend — the numbers behind the bar on the credit-usage page. The parts always sum to the total, with ONE deliberate exception: `renewals_past_due` (and its region=past_due segment) reports renewals that FAILED to charge, which is money not taken and therefore outside total_credits. fixed_spent_credits is what has ALREADY been charged this cycle, NOT your monthly run-rate — for the forward figure billed at the next renewal see `billing commitments`, which is normally much larger. reserved_credits is in-flight spend already carved out of free_to_spend_credits, so treat free_to_spend as the ceiling and free_to_spend minus reserved as what is genuinely uncommitted. Every segment carries region=fixed|flexible. Read-only, 0 Oxygen credits.")
|
|
8588
8798
|
.option("--json", "Print a JSON envelope.")
|
|
8589
8799
|
.action(async (options) => {
|
|
8590
8800
|
await handleAsyncAction("billing allowance", options, () => requestOxygen("/api/cli/billing/allowance"));
|
|
@@ -9021,6 +9231,12 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
9021
9231
|
const suffix = params.toString() ? `?${params.toString()}` : "";
|
|
9022
9232
|
return requestOxygen(`/api/cli/admin/costs${suffix}`);
|
|
9023
9233
|
});
|
|
9234
|
+
}))
|
|
9235
|
+
.addCommand(new Command("primary-providers")
|
|
9236
|
+
.description("Board of the PRIMARY managed external data providers (enrichment, people/company search, web search, scraping, signals, AI-column web grounding, LLM inference): per provider health probe, 7d traffic, balance, 30d/7d COGS, rate policy, spend ceilings, breaker, and posture, plus the platform runaway guards. Read-only — never calls a provider. Staff only.")
|
|
9237
|
+
.option("--json", "Print a JSON envelope.")
|
|
9238
|
+
.action(async (options) => {
|
|
9239
|
+
await handleAsyncAction("admin primary-providers", options, () => requestOxygen("/api/cli/admin/primary-providers"));
|
|
9024
9240
|
}))
|
|
9025
9241
|
.addCommand(new Command("spend")
|
|
9026
9242
|
.description("Global managed-provider spend limiter: burn, ceilings, breakers. Staff only.")
|
|
@@ -9239,7 +9455,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
9239
9455
|
// the MCP tool enums. The API rejects any other value with
|
|
9240
9456
|
// invalid_request so a typo fails loudly.
|
|
9241
9457
|
.option("--type <type>", "Filter by operation or provider_request.")
|
|
9242
|
-
.option("--source <source>", "Filter by cli, web, mcp, or provider.")
|
|
9458
|
+
.option("--source <source>", "Filter by cli, web, mcp, workflow, or provider.")
|
|
9243
9459
|
.option("--trace-id <trace_id>", "Filter by trace id.")
|
|
9244
9460
|
.option("--run-id <run_id>", "Filter by workspace run id.")
|
|
9245
9461
|
.option("--limit <n>", "Maximum events to return. Defaults to 50.")
|
|
@@ -9837,6 +10053,48 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
9837
10053
|
},
|
|
9838
10054
|
});
|
|
9839
10055
|
});
|
|
10056
|
+
}))
|
|
10057
|
+
.addCommand(new Command("resolve")
|
|
10058
|
+
.description("Record what a human verified about a cell whose external write was dispatched but never confirmed. It records the outcome only; it never calls the provider again. Use not_applied when the write provably did not land (the cell becomes rerunnable) or applied when it did.")
|
|
10059
|
+
.argument("<table>", "Table id or slug.")
|
|
10060
|
+
.argument("<row_id>", "Workspace row UUID.")
|
|
10061
|
+
.argument("<column>", "Column id or key.")
|
|
10062
|
+
.requiredOption("--outcome <outcome>", "applied (the external write landed) or not_applied (it never did).")
|
|
10063
|
+
.option("--note <text>", "Short note about how the destination was verified.")
|
|
10064
|
+
.option("--value <json>", "JSON value read off the destination, recorded into the cell. Only valid with --outcome applied.")
|
|
10065
|
+
.option("--json", "Print a JSON envelope.")
|
|
10066
|
+
.action(async (table, rowId, column, options) => {
|
|
10067
|
+
const outcome = readOption(options.outcome);
|
|
10068
|
+
if (outcome !== "applied" && outcome !== "not_applied") {
|
|
10069
|
+
throw new OxygenError("invalid_effect_resolution_outcome", "--outcome must be applied or not_applied.", { exitCode: 1 });
|
|
10070
|
+
}
|
|
10071
|
+
const rawValue = readOption(options.value);
|
|
10072
|
+
if (rawValue !== null && outcome !== "applied") {
|
|
10073
|
+
throw new OxygenError("invalid_effect_resolution_value", "--value is only valid with --outcome applied; a not-applied effect wrote nothing.", { exitCode: 1 });
|
|
10074
|
+
}
|
|
10075
|
+
// Parsed locally so a malformed --value fails with a clear message
|
|
10076
|
+
// instead of being posted as a JSON string the server would then
|
|
10077
|
+
// faithfully write into the cell.
|
|
10078
|
+
let value;
|
|
10079
|
+
if (rawValue !== null) {
|
|
10080
|
+
try {
|
|
10081
|
+
value = JSON.parse(rawValue);
|
|
10082
|
+
}
|
|
10083
|
+
catch {
|
|
10084
|
+
throw new OxygenError("invalid_json", `--value must be valid JSON. Quote a string as '"text"'.`, { exitCode: 1 });
|
|
10085
|
+
}
|
|
10086
|
+
}
|
|
10087
|
+
await handleAsyncAction("cells resolve", options, () => requestOxygen("/api/cli/tables/cells/resolve", {
|
|
10088
|
+
method: "POST",
|
|
10089
|
+
body: {
|
|
10090
|
+
table,
|
|
10091
|
+
row_id: rowId,
|
|
10092
|
+
column,
|
|
10093
|
+
outcome,
|
|
10094
|
+
...(readOption(options.note) ? { note: readOption(options.note) } : {}),
|
|
10095
|
+
...(rawValue !== null ? { value } : {}),
|
|
10096
|
+
},
|
|
10097
|
+
}));
|
|
9840
10098
|
}))
|
|
9841
10099
|
.addCommand(new Command("history")
|
|
9842
10100
|
.description("Show cell change history.")
|
|
@@ -10174,7 +10432,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
10174
10432
|
.option("--selection-json <json>", "Full row selection object for server-side row selection.")
|
|
10175
10433
|
.option("--only-missing", "Queue rows missing the capability's normalized output when no explicit selection is passed.")
|
|
10176
10434
|
.option("--force", "Re-run rows with an existing target enrichment value.")
|
|
10177
|
-
.option("--max-concurrency <n>", "Maximum concurrent row items for this enrichment run. Defaults to 20.")
|
|
10435
|
+
.option("--max-concurrency <n>", "Maximum concurrent row items for this enrichment run (1-250). Defaults to 20.")
|
|
10178
10436
|
.option("--json", "Print a JSON envelope.")
|
|
10179
10437
|
.option("--approved", "Approve THIS run after inspecting `enrich-column preview` (free). Paid enrichment runs fail with approval_required (exit 7) without it.")
|
|
10180
10438
|
.action(async (table, options) => {
|
|
@@ -12698,7 +12956,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
12698
12956
|
await handleAsyncAction("sequences get", options, () => requestOxygen(`/api/cli/sequences/${encodeURIComponent(sequence)}`));
|
|
12699
12957
|
}))
|
|
12700
12958
|
.addCommand(new Command("enroll")
|
|
12701
|
-
.description("Enroll leads into a sequence. Enrolling spends no credits and sends nothing — dispatch only happens at `sequences start`. --from-table enrolls every not-yet-enrolled row of the sequence's bound source table (up to 500 per run; the response's from_table.has_more says whether to run again) — for a LinkedIn sequence the profile
|
|
12959
|
+
.description("Enroll leads into a sequence. Enrolling spends no credits and sends nothing — dispatch only happens at `sequences start`. --from-table enrolls every not-yet-enrolled row of the sequence's bound source table (up to 500 per run; the response's from_table.has_more says whether to run again) — for a LinkedIn sequence the profile is read from the bound table's URL column, where a full /in/ URL and a bare public handle carrying a hyphen or digit (ada-lovelace) both work, so no pre-resolved provider ids are needed; each handle Oxygen canonicalized is echoed in linkedin_handles_canonicalized (value + url) to check before start, and every row that still cannot be resolved is named in unresolved_linkedin_lead_details (row, value, reason) rather than only counted in unresolved_linkedin_leads. --leads-file enrolls an explicit JSON list { leads: [...] }: for an email/WhatsApp sequence a lead identified only by an email (row_values.email) or phone (row_values.phone) is auto-filed as a row in the sequence's leads table (auto-creating and binding one if there is none), deduped by email/phone, so no table has to exist first (the table is returned as leads_table with a web_url); a LinkedIn lead needs a lead_provider_id, a lead_profile_url (a /in/ URL or a bare public handle with a hyphen or digit), or a table_row_id whose row carries one of those. A CRM-only lead needs a stable table_row_id or lead_provider_id in addition to mapped row_values; a HubSpot contact id used in crm_task associations identifies the destination record, not the Oxygen enrollment. When the sequence is bound to a source table, a lead's table_row_id auto-snapshots that row's columns (incl. AI/tool outputs) into row_values for {{column}} copy — explicit row_values win. Idempotent per table row (and per email/phone for auto-filed leads). The org do-not-contact list is always enforced; --exclude-contacted and --suppress-list add further opt-in skips (reported under skipped_by_reason). Leads already owned by a sender account (from an earlier real send) are routed back to that same account; a lead owned by a sender NOT on this sequence is skipped (bound_to_other_sender) unless --ignore-sender-bindings.")
|
|
12702
12960
|
.argument("<sequence>", "Sequence id or slug.")
|
|
12703
12961
|
.option("--leads-file <path>", "Path to a JSON file: { \"leads\": [{ row_values: { email }, lead_name }] } for cold email; { lead_provider_id, lead_name, table_row_id, row_values } for LinkedIn/existing rows; CRM-only leads require table_row_id or a stable lead_provider_id alongside mapped row_values. A CRM association object id is not the enrollment identity. Exactly one of --leads-file or --from-table.")
|
|
12704
12962
|
.option("--from-table", "Enroll every not-yet-enrolled row of the sequence's bound source table (up to 500 per run; re-run to continue). Exactly one of --leads-file or --from-table.")
|
|
@@ -13838,6 +14096,37 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
13838
14096
|
},
|
|
13839
14097
|
});
|
|
13840
14098
|
});
|
|
14099
|
+
}))
|
|
14100
|
+
.addCommand(new Command("exit")
|
|
14101
|
+
.description("Leave OXYGEN with a managed domain INTACT: stop every OXYGEN renewal for it (inboxes, warm-up, inbox placement) at the end of the period already paid for, keep the domain, mailboxes, DNS, warm-up state and data exactly as they are, and record a handoff request. Nothing is cancelled, disconnected, or released — for that use `managed-inboxes cancel`. WITHOUT --approved prints the plan: each renewal line with the date it stops, what stays, and the vendor-carry note (OXYGEN keeps paying the inbox vendor for these mailboxes after that date until a human completes the handoff or you cancel). To record it, re-run with --approved AND --confirm <domain> echoing the domain exactly. The Google Workspace / registrar handoff itself is coordinated by a human with the vendor — this command records the request, it does not perform the transfer. --revoke resumes renewals on a domain whose exit was requested. Free, 0 Oxygen credits; re-running on an already-exited domain changes nothing (effect_outcome no_change).")
|
|
14102
|
+
.argument("[domain]", "The managed domain to stop renewing. May also be passed as --domain.")
|
|
14103
|
+
.option("--domain <domain>", "The managed inbox domain (alternative to the positional argument).")
|
|
14104
|
+
.option("--contact <email>", "Who the human handoff should be coordinated with (recorded on the request).")
|
|
14105
|
+
.option("--note <text>", "Free-text context for the handoff, e.g. where the mailboxes are moving.")
|
|
14106
|
+
.option("--revoke", "Undo a recorded exit: renewals resume on the next cycle. Still needs --approved --confirm.")
|
|
14107
|
+
.option("--approved", "Actually record the exit (or the revoke); otherwise the plan is printed.")
|
|
14108
|
+
.option("--confirm <domain>", "Type the domain again to confirm. Required with --approved.")
|
|
14109
|
+
.option("--json", "Print a JSON envelope.")
|
|
14110
|
+
.action(async (domainArg, options) => {
|
|
14111
|
+
await handleAsyncAction("managed-inboxes exit", options, () => {
|
|
14112
|
+
const domain = requireDomainArg(domainArg, options.domain);
|
|
14113
|
+
const confirm = readOption(options.confirm);
|
|
14114
|
+
const contact = readOption(options.contact);
|
|
14115
|
+
const note = readOption(options.note);
|
|
14116
|
+
return requestOxygen("/api/cli/managed-inboxes/exit", {
|
|
14117
|
+
method: "POST",
|
|
14118
|
+
body: {
|
|
14119
|
+
domain,
|
|
14120
|
+
...(contact ? { contact } : {}),
|
|
14121
|
+
...(note ? { note } : {}),
|
|
14122
|
+
...(options.revoke ? { revoke: true } : {}),
|
|
14123
|
+
...(options.approved ? { approved: true } : {}),
|
|
14124
|
+
// Sent verbatim, never defaulted to `domain` — same rule as cancel: the
|
|
14125
|
+
// echo only means something if a human (or an agent) typed it a second time.
|
|
14126
|
+
...(confirm ? { confirm_domain: confirm } : {}),
|
|
14127
|
+
},
|
|
14128
|
+
});
|
|
14129
|
+
});
|
|
13841
14130
|
}))
|
|
13842
14131
|
.addCommand(new Command("upload-avatar")
|
|
13843
14132
|
.description("Host a one-off mailbox profile picture and print the URL to pass as profile_picture_url in --mailboxes JSON. For a reusable sender identity used by FUTURE --sender <id> orders, prefer `oxygen senders profiles set-photo <id> --file <path>` instead. The inbox vendor FETCHES the hosted URL when it provisions the mailbox, often long after the order, so it must be public and permanent. Uploads a PNG, JPEG, or WebP (max 8MB), then normalizes it to a metadata-free 400x400 PNG at a matching .png URL. This only hosts the image: it does not update a sender, place an order, call the inbox provider, or charge credits.")
|
|
@@ -14623,15 +14912,15 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
14623
14912
|
}));
|
|
14624
14913
|
})));
|
|
14625
14914
|
program.addCommand(new Command("deliverability")
|
|
14626
|
-
.description("External email deliverability: fleet reputation health and DIRECTIONAL inbox-placement (spam) tests via EmailGuard or
|
|
14915
|
+
.description("External email deliverability: fleet reputation health and DIRECTIONAL inbox-placement (spam) tests via EmailGuard, Zapmail, or an explicitly configured SendKit dev canary. This group creates one-off placement probes; for continuous EmailGuard account monitoring use `oxygen mailboxes emailguard connect`. Placement tests are approval-gated paid runs (managed bills Oxygen credits; BYOK = 0 Oxygen credits — Zapmail BYOK bills your Zapmail wallet ~$2/test). SendKit is never auto-selected or generally available: it is a 0-credit, one-test canary for the configured eligible connected sender only.")
|
|
14627
14916
|
.addCommand(new Command("placement-test")
|
|
14628
14917
|
.description("Directional inbox-placement (spam) tests: create, separately approve EmailGuard's exact seed send, then poll results.")
|
|
14629
14918
|
.addCommand(new Command("run")
|
|
14630
|
-
.description("Create a placement test for one sending mailbox. Without --approved this returns a cost PREVIEW. EmailGuard creation returns exact seeds + phrase but sends nothing; next run `placement-test send <id>` to preview and approve that external email. Zapmail owns its probe delivery and completes async (2-24h).")
|
|
14919
|
+
.description("Create a placement test for one sending mailbox. Without --approved this returns a cost PREVIEW. EmailGuard creation returns exact seeds + phrase but sends nothing; next run `placement-test send <id>` to preview and approve that external email. Zapmail owns its probe delivery and completes async (2-24h). SendKit requires explicit --provider sendkit plus --subject and --body, and is available only for the configured dev canary with an eligible connected sender. Its approval creates AND sends one probe with no separate send step: preview first, then re-run the same mailbox, provider, subject, and body with --approved --max-credits 0 --plan <plan_hash>.")
|
|
14631
14920
|
.argument("<mailbox>", "Sending mailbox address to test (e.g. ada@send.acme.com).")
|
|
14632
|
-
.
|
|
14633
|
-
.option("--subject <subject>", "
|
|
14634
|
-
.option("--body <text>", "
|
|
14921
|
+
.addOption(new Option("--provider <provider>", "Health provider: emailguard, zapmail, or sendkit. Omit to auto-resolve EmailGuard/Zapmail. SendKit is never auto-selected and requires the configured dev canary sender.").choices(["emailguard", "zapmail", "sendkit"]))
|
|
14922
|
+
.option("--subject <subject>", "Seed-message subject. Required with --provider sendkit; Zapmail ignores it beyond labeling the test.")
|
|
14923
|
+
.option("--body <text>", "Plain-text seed-message body. Required with --provider sendkit; ignored by Zapmail.")
|
|
14635
14924
|
.option("--approved", "Actually run the test (otherwise a preview is returned).")
|
|
14636
14925
|
.option("--max-credits <n>", "Credit cap the caller accepts (must be >= credits_required for managed runs).")
|
|
14637
14926
|
.option("--plan <hash>", "plan_hash from a fresh create preview (required with --approved).")
|
|
@@ -14699,7 +14988,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
14699
14988
|
program.addCommand(new Command("domains")
|
|
14700
14989
|
.description("Cold-email domain management on the org's own Cloudflare account (BYOK): sync zones, inspect age/warmup/DNS health, check availability and pricing, and buy domains. Purchases bill your Cloudflare payment method, never Oxygen credits.")
|
|
14701
14990
|
.addCommand(new Command("list")
|
|
14702
|
-
.description("List the org's cached Cloudflare domains with cold-email metadata (age, mailboxes, warmup, sending volume, DNS health). Each row's `dns` object explains the status (ok, issues, not checked, check failed, partial check, no zone) with a reason and fix path; `managed_domains` lists InboxKit-managed bundle domains (vendor-registered + vendor-DNS'd — manage via `managed-inboxes`). Reads the cache only — run `domains sync` to refresh.")
|
|
14991
|
+
.description("List the org's cached Cloudflare domains with cold-email metadata (age, mailboxes, warmup, sending volume, DNS health). Each row's `dns` object explains the status (ok, issues, not checked, check failed, partial check, no zone) with a reason and fix path; `managed_domains` lists InboxKit-managed bundle domains (vendor-registered + vendor-DNS'd — manage via `managed-inboxes`), each carrying `internal_billing_status` (ok|past_due) and `last_billed_cycle_key` — a domain the vendor keeps live can still be one OXYGEN could not renew. Reads the cache only — run `domains sync` to refresh.")
|
|
14703
14992
|
.option("--status <status>", "Filter by zone status: unknown, initializing, pending, active, moved, or deleted.")
|
|
14704
14993
|
.option("--registrar <registrar>", "Filter by registrar name.")
|
|
14705
14994
|
.option("--q <text>", "Filter by domain substring.")
|
|
@@ -15292,8 +15581,10 @@ Run completion:
|
|
|
15292
15581
|
.option("--tag <tag>", "Only workflows carrying this workspace tag (see `oxygen tags list`).")
|
|
15293
15582
|
.option("--node-testable", "Only canonical graph workflows, the ones `workflows call --node` can scope a test to. Legacy recipes and v1 step lists own their own execution order and are excluded.")
|
|
15294
15583
|
.option("--search <text>", "Match on name, slug, trigger event, or an integration the workflow uses — so \"meeting\" or \"hubspot\" finds it without knowing what someone named it.")
|
|
15584
|
+
.option("--limit <n>", "Maximum workflows to list. Defaults to 200; hard cap is 1000.")
|
|
15585
|
+
.option("--all", "List every workflow instead of the most-recently-updated window.")
|
|
15295
15586
|
.option("--json", "Print a JSON envelope.")
|
|
15296
|
-
.addHelpText("after", "\nEach row carries `format` (graph, recipe or steps) and `nodeTestable`, so you can tell which workflows support single-step testing without opening them.\n")
|
|
15587
|
+
.addHelpText("after", "\nEach row carries `format` (graph, recipe or steps) and `nodeTestable`, so you can tell which workflows support single-step testing without opening them.\nLists the 200 most recently updated workflows by default; `capped` in the JSON envelope says when there are more.\n")
|
|
15297
15588
|
.action(async (options) => {
|
|
15298
15589
|
await handleAsyncAction("workflows list", options, async () => {
|
|
15299
15590
|
const params = new URLSearchParams();
|
|
@@ -15305,10 +15596,17 @@ Run completion:
|
|
|
15305
15596
|
const search = readOption(options.search);
|
|
15306
15597
|
if (search)
|
|
15307
15598
|
params.set("search", search);
|
|
15599
|
+
const limit = readPositiveInt(options.limit);
|
|
15600
|
+
if (limit !== undefined)
|
|
15601
|
+
params.set("limit", String(limit));
|
|
15602
|
+
if (options.all)
|
|
15603
|
+
params.set("all", "true");
|
|
15308
15604
|
const qs = params.toString() ? `?${params.toString()}` : "";
|
|
15309
15605
|
const data = await requestOxygen(`/api/cli/workflows${qs}`);
|
|
15310
|
-
if (!options.json)
|
|
15606
|
+
if (!options.json) {
|
|
15311
15607
|
writeDisabledWorkflowNotices(data);
|
|
15608
|
+
writeListCapNotice(data, "workflow(s)");
|
|
15609
|
+
}
|
|
15312
15610
|
return data;
|
|
15313
15611
|
});
|
|
15314
15612
|
}))
|
|
@@ -16722,7 +17020,7 @@ function writeCopilotPlan(plan, webUrl) {
|
|
|
16722
17020
|
const lines = [];
|
|
16723
17021
|
if (typeof plan.summary === "string" && plan.summary)
|
|
16724
17022
|
lines.push(plan.summary, "");
|
|
16725
|
-
lines.push(`${progress.done ?? 0}/${progress.total ?? 0} steps done${formatPlanEta(eta)}`, "");
|
|
17023
|
+
lines.push(`${progress.done ?? 0}/${progress.total ?? 0} steps done${formatPlanEta(eta, plan.turnActive !== false)}`, "");
|
|
16726
17024
|
for (const step of steps) {
|
|
16727
17025
|
if (!isRecord(step))
|
|
16728
17026
|
continue;
|
|
@@ -16730,7 +17028,7 @@ function writeCopilotPlan(plan, webUrl) {
|
|
|
16730
17028
|
const mark = status === "done" ? "[x]" : status === "in_progress" ? "[>]" : status === "blocked" ? "[!]" : "[ ]";
|
|
16731
17029
|
const timing = status === "done"
|
|
16732
17030
|
? formatPlanDuration(step.elapsedMs)
|
|
16733
|
-
: typeof step.etaSeconds === "number" ? `~${
|
|
17031
|
+
: typeof step.etaSeconds === "number" ? `~${formatCopilotPlanSeconds(step.etaSeconds)} left` : "";
|
|
16734
17032
|
lines.push(`${mark} ${String(step.title ?? "")}${timing ? ` (${timing})` : ""}`);
|
|
16735
17033
|
for (const sub of Array.isArray(step.substeps) ? step.substeps : []) {
|
|
16736
17034
|
if (!isRecord(sub))
|
|
@@ -16746,7 +17044,12 @@ function writeCopilotPlan(plan, webUrl) {
|
|
|
16746
17044
|
process.stdout.write(`${lines.join("\n")}\n`);
|
|
16747
17045
|
}
|
|
16748
17046
|
/** Every estimate names its basis, so nobody reads a guess as a measurement. */
|
|
16749
|
-
function formatPlanEta(eta) {
|
|
17047
|
+
function formatPlanEta(eta, turnActive) {
|
|
17048
|
+
// Nothing is running, so nothing REMAINS. "estimating" on a session that stopped
|
|
17049
|
+
// weeks ago describes work in flight that does not exist; the step marks still
|
|
17050
|
+
// say where the copilot got to, which is the honest half of the answer.
|
|
17051
|
+
if (!turnActive)
|
|
17052
|
+
return "";
|
|
16750
17053
|
const remaining = typeof eta.remainingSeconds === "number" ? eta.remainingSeconds : null;
|
|
16751
17054
|
if (remaining === null)
|
|
16752
17055
|
return " · time remaining: estimating";
|
|
@@ -16755,19 +17058,11 @@ function formatPlanEta(eta) {
|
|
|
16755
17058
|
? `measured from ${measured} completed step${measured === 1 ? "" : "s"}`
|
|
16756
17059
|
: eta.basis === "model" ? "estimated, nothing measured yet" : "no basis";
|
|
16757
17060
|
const bound = eta.deadlineBound === true ? ", capped by the turn deadline" : "";
|
|
16758
|
-
return ` · ~${
|
|
16759
|
-
}
|
|
16760
|
-
function formatPlanSeconds(seconds) {
|
|
16761
|
-
if (seconds < 60)
|
|
16762
|
-
return `${Math.round(seconds)}s`;
|
|
16763
|
-
const minutes = Math.floor(seconds / 60);
|
|
16764
|
-
const rest = Math.round(seconds % 60);
|
|
16765
|
-
return rest === 0 ? `${minutes}m` : `${minutes}m ${rest}s`;
|
|
17061
|
+
return ` · ~${formatCopilotPlanSeconds(remaining)} left (${basis}${bound})`;
|
|
16766
17062
|
}
|
|
17063
|
+
/** Narrows the untyped ledger value, then defers to the one shared formatter. */
|
|
16767
17064
|
function formatPlanDuration(ms) {
|
|
16768
|
-
|
|
16769
|
-
return "";
|
|
16770
|
-
return ms < 1000 ? `${Math.round(ms)}ms` : formatPlanSeconds(ms / 1000);
|
|
17065
|
+
return (typeof ms === "number" ? formatCopilotPlanDuration(ms) : null) ?? "";
|
|
16771
17066
|
}
|
|
16772
17067
|
function printCopilotEvent(event, sessionId) {
|
|
16773
17068
|
if (!isRecord(event))
|
|
@@ -23458,7 +23753,9 @@ options, orgOverride) {
|
|
|
23458
23753
|
}
|
|
23459
23754
|
// Generated summaries come from the server's own projections, not a client-side
|
|
23460
23755
|
// recomputation. A real wiki page may own the `index`/`log` slug — page bytes win.
|
|
23461
|
-
|
|
23756
|
+
// `all=true` on purpose: the generated index.md must list every page this
|
|
23757
|
+
// mirror just cloned, and the route's default is a 200-page window.
|
|
23758
|
+
const index = await requestOxygen("/api/cli/knowledge/index?all=true", requestOrg);
|
|
23462
23759
|
const logPage = await requestOxygen("/api/cli/knowledge/log", {
|
|
23463
23760
|
method: "POST",
|
|
23464
23761
|
body: {},
|