@oxygen-agent/cli 1.922.14 → 1.948.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/admin-primary-providers-render.d.ts +18 -0
- package/dist/admin-primary-providers-render.js +371 -0
- package/dist/command-manifest.js +30 -2
- package/dist/functions-commands.d.ts +6 -0
- package/dist/functions-commands.js +56 -0
- package/dist/help.js +1 -0
- package/dist/http-client.d.ts +4 -0
- package/dist/http-client.js +49 -2
- package/dist/index.js +515 -92
- package/dist/ugc-commands.d.ts +6 -0
- package/dist/ugc-commands.js +1089 -0
- package/dist/visual-commands.d.ts +6 -0
- package/dist/visual-commands.js +57 -0
- package/dist/visual-render-wait.d.ts +3 -0
- package/dist/visual-render-wait.js +56 -0
- package/node_modules/@oxygen/shared/dist/byok-connect.d.ts +48 -0
- package/node_modules/@oxygen/shared/dist/byok-connect.js +92 -0
- package/node_modules/@oxygen/shared/dist/capability-discovery.js +77 -13
- package/node_modules/@oxygen/shared/dist/email-dsn.d.ts +60 -0
- package/node_modules/@oxygen/shared/dist/email-dsn.js +120 -0
- package/node_modules/@oxygen/shared/dist/email-warmup-readiness.d.ts +64 -0
- package/node_modules/@oxygen/shared/dist/email-warmup-readiness.js +90 -0
- package/node_modules/@oxygen/shared/dist/feature-gates.d.ts +2 -0
- package/node_modules/@oxygen/shared/dist/feature-gates.js +3 -0
- package/node_modules/@oxygen/shared/dist/index.d.ts +10 -0
- package/node_modules/@oxygen/shared/dist/index.js +10 -0
- package/node_modules/@oxygen/shared/dist/knowledge-bootstrap.d.ts +50 -21
- package/node_modules/@oxygen/shared/dist/knowledge-bootstrap.js +47 -21
- package/node_modules/@oxygen/shared/dist/knowledge-constants.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/knowledge-constants.js +3 -2
- package/node_modules/@oxygen/shared/dist/knowledge-seed-content.js +1 -1
- package/node_modules/@oxygen/shared/dist/langfuse.d.ts +8 -3
- package/node_modules/@oxygen/shared/dist/langfuse.js +185 -121
- package/node_modules/@oxygen/shared/dist/llm-payload.d.ts +10 -0
- package/node_modules/@oxygen/shared/dist/llm-payload.js +54 -0
- package/node_modules/@oxygen/shared/dist/llm-usage.d.ts +11 -0
- package/node_modules/@oxygen/shared/dist/llm-usage.js +30 -0
- package/node_modules/@oxygen/shared/dist/object-storage.d.ts +6 -0
- package/node_modules/@oxygen/shared/dist/object-storage.js +5 -0
- package/node_modules/@oxygen/shared/dist/product-analytics-core.d.ts +98 -0
- package/node_modules/@oxygen/shared/dist/product-analytics-core.js +159 -0
- package/node_modules/@oxygen/shared/dist/product-analytics-environment.d.ts +18 -0
- package/node_modules/@oxygen/shared/dist/product-analytics-environment.js +46 -0
- package/node_modules/@oxygen/shared/dist/product-analytics-events.d.ts +92 -0
- package/node_modules/@oxygen/shared/dist/product-analytics-events.js +96 -0
- package/node_modules/@oxygen/shared/dist/provider-balance-signal.d.ts +113 -0
- package/node_modules/@oxygen/shared/dist/provider-balance-signal.js +158 -0
- package/node_modules/@oxygen/shared/dist/sequence-failures.d.ts +12 -0
- package/node_modules/@oxygen/shared/dist/sequence-failures.js +24 -0
- package/node_modules/@oxygen/shared/dist/sequences.d.ts +16 -0
- package/node_modules/@oxygen/shared/dist/sequences.js +54 -0
- package/node_modules/@oxygen/shared/dist/ugc.d.ts +133 -0
- package/node_modules/@oxygen/shared/dist/ugc.js +2 -0
- package/node_modules/@oxygen/shared/dist/vercel-sandbox-fetch.d.ts +9 -0
- package/node_modules/@oxygen/shared/dist/vercel-sandbox-fetch.js +33 -0
- 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/dist/visual-render.d.ts +30 -0
- package/node_modules/@oxygen/shared/dist/visual-render.js +55 -0
- package/node_modules/@oxygen/shared/dist/workspace-file-storage.d.ts +43 -0
- package/node_modules/@oxygen/shared/dist/workspace-file-storage.js +126 -0
- package/node_modules/@oxygen/shared/package.json +10 -0
- package/node_modules/@oxygen/workflows/dist/graph/lint.js +22 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -7,6 +7,10 @@ import { createInterface } from "node:readline/promises";
|
|
|
7
7
|
import { stdin as input, stdout as output } from "node:process";
|
|
8
8
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
9
9
|
import { Command, CommanderError, Option } from "commander";
|
|
10
|
+
import { registerUgcCommands } from "./ugc-commands.js";
|
|
11
|
+
import { registerVisualCommands } from "./visual-commands.js";
|
|
12
|
+
import { renderPrimaryProviderBoard } from "./admin-primary-providers-render.js";
|
|
13
|
+
import { registerFunctionsCommands } from "./functions-commands.js";
|
|
10
14
|
import { applyOxygenHelp } from "./help.js";
|
|
11
15
|
import { buildCommandManifest, getCommandManifestEntry, searchCommandManifest, suggestCommandNames, } from "./command-manifest.js";
|
|
12
16
|
import { AGENCY_DIRECTORY_REGIONS, AGENCY_DIRECTORY_SERVICES, COLLAB_GATE_KINDS, COLLAB_GATE_PROSE, COLLAB_SUBJECT_KINDS, COLLAB_SUBJECT_KINDS_PROSE, COLLAB_SUBJECT_LABELS, COLLAB_SUBJECT_PROSE, GATE_KIND_SUBJECTS, describeWorkflowStatusChange, formatCellForDisplay, formatCopilotPlanDuration, formatCopilotPlanSeconds, formatPublicBudgetScopes, SUBJECT_PATH_FORMS_PROSE, formatSubjectPath, exitCodeForOxygenError, parseSubjectPath, parseSubjectRef, parseWorkflowStatusChange, isVersionGreater, isVersionLess, KNOWLEDGE_BOOTSTRAP_MAX_CREDITS, MAX_MCP_TOOL_NAME_LENGTH, normalizeCopilotPlanStepStatus, OXYGEN_CAPABILITY_ROUTES, OXYGEN_VERSION, OxygenError, getCapabilityRouteMatch, inferUserCapabilityRoute, parseKnowledgePageMarkdown, PLAN_LIMITS, serializeCapabilityRoute, sleep, success, TABLE_IMPORT_ROW_LIMIT, TAG_KINDS_PROSE, toFailure, workflowMcpToolName, } from "@oxygen/shared";
|
|
@@ -196,8 +200,13 @@ function buildFindBody(capability, options) {
|
|
|
196
200
|
}
|
|
197
201
|
if (options.mode)
|
|
198
202
|
body.mode = options.mode;
|
|
199
|
-
|
|
200
|
-
|
|
203
|
+
// Company only: 0 is a real ceiling meaning "run only the zero-credit lanes".
|
|
204
|
+
// The person capabilities are paid on every lane, so 0 stays a usage error
|
|
205
|
+
// there. `!== undefined` because 0 is falsy and would otherwise be dropped.
|
|
206
|
+
const maxCredits = capability === "company"
|
|
207
|
+
? readCreditCeilingOrZero(options.maxCredits)
|
|
208
|
+
: readPositiveNumber(options.maxCredits);
|
|
209
|
+
if (maxCredits !== undefined)
|
|
201
210
|
body.max_credits = maxCredits;
|
|
202
211
|
// Phone-only opt-in; the route ignores it for other capabilities.
|
|
203
212
|
if (options.verify)
|
|
@@ -439,6 +448,33 @@ function emitCliFailure(command, error) {
|
|
|
439
448
|
writeMaxCreditsHint(error);
|
|
440
449
|
process.exitCode = error instanceof OxygenError ? exitCodeForOxygenError(error) : 1;
|
|
441
450
|
}
|
|
451
|
+
// A post's engagers list is the one payload in this tree that is routinely
|
|
452
|
+
// thousands of lines long, and the field that decides whether you may act on it
|
|
453
|
+
// — `truncated` — is one boolean inside it. Printed raw, a partial audience
|
|
454
|
+
// looks exactly like a whole one until you scroll past every person in it. So
|
|
455
|
+
// lead with the receipt on stderr (the same stdout/stderr split the dry-run and
|
|
456
|
+
// credits notices use, leaving stdout a clean envelope) and say plainly what to
|
|
457
|
+
// do about a short read. `--json` callers are untouched: they read the fields.
|
|
458
|
+
function writeEngagersReceipt(data) {
|
|
459
|
+
if (!data || typeof data !== "object" || Array.isArray(data))
|
|
460
|
+
return;
|
|
461
|
+
const record = data;
|
|
462
|
+
const pages = isRecord(record.pages_read) ? record.pages_read : {};
|
|
463
|
+
const line = (text) => process.stderr.write(`${text}\n`);
|
|
464
|
+
line(`Post ${String(record.post ?? "?")}`);
|
|
465
|
+
line(` ${String(record.total_engagers ?? 0)} engagers`
|
|
466
|
+
+ ` (${String(record.reactors_count ?? 0)} reactions, ${String(record.commenters_count ?? 0)} comments)`
|
|
467
|
+
+ ` from ${String(pages.reactions ?? 0)}+${String(pages.comments ?? 0)} pages`);
|
|
468
|
+
if (record.truncated === true) {
|
|
469
|
+
const reason = typeof record.partial_reason === "string" ? record.partial_reason : "unknown";
|
|
470
|
+
line(` PARTIAL (${reason}) — this post has more engagers than were read.`);
|
|
471
|
+
line(reason === "max_pages"
|
|
472
|
+
? " Raise --max-pages (max 20), or run `oxygen engagement harvest` to walk the whole post into a table."
|
|
473
|
+
: " Retry, or run `oxygen engagement harvest` to walk the whole post into a table.");
|
|
474
|
+
}
|
|
475
|
+
if (typeof record.web_url === "string")
|
|
476
|
+
line(` ${record.web_url}`);
|
|
477
|
+
}
|
|
442
478
|
// A dry run's stdout envelope looks like a successful result — same shape, same
|
|
443
479
|
// `ok: true` — so in a terminal the only tell that nothing was fetched was
|
|
444
480
|
// `meta.mode` buried inside the payload. That is how a working provider key gets
|
|
@@ -456,6 +492,29 @@ function writeDryRunNotice(data) {
|
|
|
456
492
|
process.stderr.write(`${block.message}\n`);
|
|
457
493
|
if (typeof block.next_step === "string")
|
|
458
494
|
process.stderr.write(`${block.next_step}\n`);
|
|
495
|
+
// The server's next_step quotes the MANAGED price, because the live gate's
|
|
496
|
+
// `billedTool` reads the descriptor and never the credential mode. On a
|
|
497
|
+
// customer's own key that number is not what anyone pays, so say so instead
|
|
498
|
+
// of leaving a managed credit estimate as the last word on a BYOK preview.
|
|
499
|
+
// The `--max-credits` ceiling in that line is still required by the gate, so
|
|
500
|
+
// the command stays copy-pasteable rather than being edited into a 400.
|
|
501
|
+
const byok = readByokCredentialMode(data);
|
|
502
|
+
if (byok) {
|
|
503
|
+
process.stderr.write(`That estimate is the managed-key price. This run uses credential_mode ${byok}, so Oxygen charges no credits for it — your provider bills you directly. The live gate still requires --max-credits as a ceiling.\n`);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
/**
|
|
507
|
+
* The tool run's credential mode when the call is NOT on Oxygen's managed key
|
|
508
|
+
* — the one fact that decides whether a quoted credit estimate means anything.
|
|
509
|
+
* Null for managed runs and for payloads with no billing block at all.
|
|
510
|
+
*/
|
|
511
|
+
function readByokCredentialMode(data) {
|
|
512
|
+
const result = asPayloadRecord(asPayloadRecord(data)?.result);
|
|
513
|
+
const billing = asPayloadRecord(asPayloadRecord(result?.meta)?.billing);
|
|
514
|
+
const mode = billing?.credential_mode;
|
|
515
|
+
if (typeof mode !== "string" || mode.length === 0 || mode === "managed")
|
|
516
|
+
return null;
|
|
517
|
+
return mode;
|
|
459
518
|
}
|
|
460
519
|
// An `oxy_live_` key can only ever answer for the one workspace it is bound to,
|
|
461
520
|
// so `orgs list` returns a single row and `orgs use` refuses — both truthfully,
|
|
@@ -554,7 +613,12 @@ function writeCreditsReceipt(data) {
|
|
|
554
613
|
return;
|
|
555
614
|
}
|
|
556
615
|
if (typeof block.estimated_credits === "number") {
|
|
557
|
-
|
|
616
|
+
// A BYOK preview spends zero Oxygen credits, so quoting the managed
|
|
617
|
+
// estimate as "for a live run" is simply wrong; name the mode instead.
|
|
618
|
+
const byok = readByokCredentialMode(data);
|
|
619
|
+
process.stderr.write(byok
|
|
620
|
+
? `estimated 0 Oxygen credits for a live run on your own key (${byok})${remaining !== null ? `, ${remaining} available` : ""}\n`
|
|
621
|
+
: `estimated ${block.estimated_credits.toLocaleString("en-US")} credits for a live run${remaining !== null ? `, ${remaining} available` : ""}\n`);
|
|
558
622
|
}
|
|
559
623
|
}
|
|
560
624
|
// A disabled workflow is a customer's automation at zero, and `status:
|
|
@@ -1636,6 +1700,8 @@ function buildPublishingPostsListPath(options) {
|
|
|
1636
1700
|
approval_status: options.approvalStatus,
|
|
1637
1701
|
tag: options.tag,
|
|
1638
1702
|
provider: options.provider,
|
|
1703
|
+
sender_account_id: options.sender,
|
|
1704
|
+
cursor: options.cursor,
|
|
1639
1705
|
limit: options.limit,
|
|
1640
1706
|
};
|
|
1641
1707
|
for (const [key, value] of Object.entries(filters)) {
|
|
@@ -3606,7 +3672,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
3606
3672
|
}));
|
|
3607
3673
|
program
|
|
3608
3674
|
.command("projects")
|
|
3609
|
-
.description("Manage table projects. Inspect contents with `oxygen tables list --project <project>`.")
|
|
3675
|
+
.description("Manage table folders (projects). Inspect contents with `oxygen tables list --project <project>`.")
|
|
3610
3676
|
.addCommand(new Command("list")
|
|
3611
3677
|
.description("List table projects in the current tenant database.")
|
|
3612
3678
|
.option("--json", "Print a JSON envelope.")
|
|
@@ -3643,11 +3709,12 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
3643
3709
|
});
|
|
3644
3710
|
}))
|
|
3645
3711
|
.addCommand(new Command("delete")
|
|
3646
|
-
.description("Delete
|
|
3712
|
+
.description("Delete a non-General folder and its tables. Restoring any deleted table with `oxygen tables restore` before the purge date also restores the folder. Defaults to dry-run; --live requires --confirm and the preview's table IDs.")
|
|
3647
3713
|
.argument("<project>", "Project slug or id.")
|
|
3648
3714
|
.option("--dry-run", "Preview whether the project can be deleted without writing. Default.")
|
|
3649
3715
|
.option("--live", "Delete the project after inspecting the dry-run preview. Requires --confirm.")
|
|
3650
3716
|
.option("--confirm", "Confirm the live delete.")
|
|
3717
|
+
.option("--expected-table-ids <ids>", "Comma-separated table IDs from the preview. Required for nonempty folders; refuses if contents changed.")
|
|
3651
3718
|
.option("--json", "Print a JSON envelope.")
|
|
3652
3719
|
.action(async (project, options) => {
|
|
3653
3720
|
await handleAsyncAction("projects delete", options, () => {
|
|
@@ -3660,6 +3727,9 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
3660
3727
|
body: {
|
|
3661
3728
|
mode,
|
|
3662
3729
|
...(mode === "live" ? { confirm: true } : {}),
|
|
3730
|
+
...(options.expectedTableIds !== undefined
|
|
3731
|
+
? { expected_table_ids: splitCommaList(options.expectedTableIds) }
|
|
3732
|
+
: {}),
|
|
3663
3733
|
},
|
|
3664
3734
|
});
|
|
3665
3735
|
});
|
|
@@ -3761,6 +3831,8 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
3761
3831
|
.option("--approval-status <status>", "Filter by draft, needs_approval, approved, or rejected.")
|
|
3762
3832
|
.option("--tag <tag>", "Only posts carrying this workspace tag.")
|
|
3763
3833
|
.option("--provider <provider>", "Filter by provider: linkedin, x, instagram, tiktok, facebook, or youtube.")
|
|
3834
|
+
.option("--sender <sender_account_id>", "Only posts belonging to this connected sender in the active workspace.")
|
|
3835
|
+
.option("--cursor <cursor>", "Continue from next_cursor with the same filters.")
|
|
3764
3836
|
.option("--limit <n>", "Maximum posts to return.")
|
|
3765
3837
|
.option("--json", "Print a JSON envelope.")
|
|
3766
3838
|
.action(async (options) => {
|
|
@@ -3855,7 +3927,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
3855
3927
|
}));
|
|
3856
3928
|
}))
|
|
3857
3929
|
.addCommand(new Command("delete")
|
|
3858
|
-
.description("
|
|
3930
|
+
.description("Permanently delete the OXYGEN copy and its settled history, keeping the live platform post intact. Supports never-attempted draft/scheduled/queued posts, settled canceled unpublished posts, and settled published copies. Published deletions stay excluded from background discovery. Cancel failed or re-armed posts first. In-flight publishing, replies, amplification, and active amplification spend accounting remain protected. Live LinkedIn deletion is a separate action using `oxygen posts delete`.")
|
|
3859
3931
|
.argument("<post_id>", "Scheduled post id.")
|
|
3860
3932
|
.option("--json", "Print a JSON envelope.")
|
|
3861
3933
|
.action(async (postId, options) => {
|
|
@@ -4973,7 +5045,8 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
4973
5045
|
.description("Source EXTERNAL market signals into a table: hiring, technology adoption, funding, acquisitions, and news. This is the sourcing half of Signals — `signals list` reads the events already captured for your workspace.")
|
|
4974
5046
|
.addCommand(new Command("plan")
|
|
4975
5047
|
.description("Compile a signal-sourcing request into ordered provider routes without provider calls: the chain, per-route applied/dropped filters, credit estimate, table blueprint, and whether the route can be kept LIVE on a cadence. Free.")
|
|
4976
|
-
.
|
|
5048
|
+
.argument("[prompt]", "Prompt text or @file (same as --prompt). The --prompt flag wins if both are given.")
|
|
5049
|
+
.option("--prompt <text-or-file>", "Signal-sourcing prompt, or a path to a prompt file.")
|
|
4977
5050
|
.requiredOption("--family <family>", "Signal family: hiring, tech, funding, acquisition, news, or job_change.")
|
|
4978
5051
|
.option("--scope <scope>", "market (discover new companies) or watch_list (track companies you name via --domains). Defaults per family.")
|
|
4979
5052
|
.option("--target-count <n>", "Desired row count for routing and estimates.")
|
|
@@ -4987,14 +5060,15 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
4987
5060
|
.option("--filters-json <json-or-file>", "Filters JSON inline or a path to a JSON file; wins over individual flags per top-level filter path.")
|
|
4988
5061
|
.option("--estimate", "Run the free server-side preflight pass: resolves provider enum values and, where a provider publishes one, a free match count (zero credits).")
|
|
4989
5062
|
.option("--json", "Print a JSON envelope.")
|
|
4990
|
-
.action(async (options) => {
|
|
5063
|
+
.action(async (promptArg, options) => {
|
|
4991
5064
|
await handleAsyncAction("signals search plan", options, () => requestOxygen("/api/cli/signals/search/plan", {
|
|
4992
5065
|
method: "POST",
|
|
4993
|
-
body: readSignalsSearchPlanBody(options),
|
|
5066
|
+
body: readSignalsSearchPlanBody(options, promptArg),
|
|
4994
5067
|
}));
|
|
4995
5068
|
}))
|
|
4996
5069
|
.addCommand(new Command("run")
|
|
4997
5070
|
.description("Return a dry-run request or queue a live signal-search ingestion run. Live requires --approved and --max-credits. Add --bind-feed --every to also bind a pull feed to the same table in the same call, so the table keeps refilling on a cadence.")
|
|
5071
|
+
.argument("[prompt]", "Prompt text or @file (same as --prompt); requires --family. The --prompt flag wins if both are given.")
|
|
4998
5072
|
.option("--prompt <text-or-file>", "Signal-sourcing prompt, or a path to a prompt file. Requires --family.")
|
|
4999
5073
|
.option("--plan-json <json-or-file>", "Plan JSON returned by signals search plan, or a path to a JSON file.")
|
|
5000
5074
|
.option("--family <family>", "Signal family when planning from --prompt: hiring, tech, funding, acquisition, news, or job_change.")
|
|
@@ -5022,10 +5096,10 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
5022
5096
|
.option("--max-credits-per-cycle <n>", "Credit ceiling PER sync cycle for the bound feed.")
|
|
5023
5097
|
.option("--max-rows-per-cycle <n>", "Advisory row ceiling per sync cycle for the bound feed.")
|
|
5024
5098
|
.option("--json", "Print a JSON envelope.")
|
|
5025
|
-
.action(async (options) => {
|
|
5099
|
+
.action(async (promptArg, options) => {
|
|
5026
5100
|
await handleAsyncAction("signals search run", options, () => requestOxygen("/api/cli/signals/search/run", {
|
|
5027
5101
|
method: "POST",
|
|
5028
|
-
body: readSignalsSearchRunBody(options),
|
|
5102
|
+
body: readSignalsSearchRunBody(options, promptArg),
|
|
5029
5103
|
}));
|
|
5030
5104
|
})));
|
|
5031
5105
|
const tablesCommand = program
|
|
@@ -5034,7 +5108,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
5034
5108
|
.addCommand(new Command("create")
|
|
5035
5109
|
.description("Create a real Postgres-backed workspace table. Free — 0 Oxygen credits. To create a table and import a file in one step, use `tables import --create <name>`.")
|
|
5036
5110
|
.argument("<name>", "Display name for the table.")
|
|
5037
|
-
.requiredOption("--columns-json <json>", 'JSON array of column definitions, e.g. [{"key":"name","label":"Name","dataType":"text"},{"key":"domain","label":"Domain","dataType":"text"}]. Inspect an existing shape with `oxygen tables describe <table>`.')
|
|
5111
|
+
.requiredOption("--columns-json <json>", 'Required nonempty JSON array of column definitions, e.g. [{"key":"name","label":"Name","dataType":"text"},{"key":"domain","label":"Domain","dataType":"text"}]. Inspect an existing shape with `oxygen tables describe <table>`.')
|
|
5038
5112
|
.option("--project <project>", "Project id or slug. Defaults to General.")
|
|
5039
5113
|
.option("--json", "Print a JSON envelope.")
|
|
5040
5114
|
.action(async (name, options) => {
|
|
@@ -5438,6 +5512,63 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
5438
5512
|
});
|
|
5439
5513
|
});
|
|
5440
5514
|
}));
|
|
5515
|
+
const watcherCommand = tablesCommand.command("watcher")
|
|
5516
|
+
.description("LinkedIn Profile Watcher: one editable table of daily engagers, source attribution, and current employers from posts in the past 7 days. Start with preview (free); create/resume activate paid daily monitoring after exact approval.");
|
|
5517
|
+
watcherCommand.command("get")
|
|
5518
|
+
.description("Read a table's LinkedIn Profile Watcher configuration and status. Free.")
|
|
5519
|
+
.argument("<table>", "Watcher table id or slug.")
|
|
5520
|
+
.option("--json", "Print a JSON envelope.")
|
|
5521
|
+
.action(async (table, options) => {
|
|
5522
|
+
await handleAsyncAction("tables watcher get", options, () => requestOxygen(`/api/cli/tables/linkedin-profile-watcher?table=${encodeURIComponent(table)}`, { method: "GET" }));
|
|
5523
|
+
});
|
|
5524
|
+
for (const action of ["preview", "create", "update", "pause", "resume"]) {
|
|
5525
|
+
const command = watcherCommand.command(action)
|
|
5526
|
+
.description({
|
|
5527
|
+
preview: "Free configuration and credit preview: no provider calls, writes, or scheduling. Omit --max-credits for a recommendation and preview hash. Ask for real profile URLs; never invent them.",
|
|
5528
|
+
create: "Create and activate a LinkedIn Profile Watcher after approval of its exact preview. Starts collection now and daily at 07:00 UTC under the approved per-cycle cap.",
|
|
5529
|
+
update: "Edit watched profiles or the per-cycle cap from the table. Preview the changes first; approval binds the exact new configuration.",
|
|
5530
|
+
pause: "Pause daily monitoring while retaining the table and collected rows.",
|
|
5531
|
+
resume: "Resume paid daily monitoring after reviewing a fresh preview and approving its per-cycle credit cap.",
|
|
5532
|
+
}[action])
|
|
5533
|
+
.option("--table <table>", "Existing watcher table id or slug; required for get/update/pause/resume.")
|
|
5534
|
+
.option("--json", "Print a JSON envelope.");
|
|
5535
|
+
if (action !== "pause") {
|
|
5536
|
+
command
|
|
5537
|
+
.option("--name <name>", "Watcher table display name.")
|
|
5538
|
+
.option("--project <project>", "Project id or slug; defaults to General.")
|
|
5539
|
+
.option("--profiles-json <json>", "JSON array of 1–10 real public LinkedIn profile URLs; replaces the watched list.")
|
|
5540
|
+
.option("--max-credits <credits>", "Hard credit ceiling for each daily cycle; never a monthly ceiling.");
|
|
5541
|
+
if (action !== "preview")
|
|
5542
|
+
command
|
|
5543
|
+
.option("--preview-hash <hash>", "Exact configuration hash returned by preview; re-preview after any change.")
|
|
5544
|
+
.option("--approved", "Approve this exact configuration and recurring per-cycle spending.");
|
|
5545
|
+
}
|
|
5546
|
+
if (action === "create") {
|
|
5547
|
+
command.requiredOption("--request-id <uuid>", "One stable UUID for this new watcher. Reuse it and the identical configuration after a timeout; never generate a new retry key.");
|
|
5548
|
+
}
|
|
5549
|
+
command.action(async (options) => {
|
|
5550
|
+
await handleAsyncAction(`tables watcher ${action}`, options, () => {
|
|
5551
|
+
const profiles = options.profilesJson === undefined ? undefined : parseJsonArray(options.profilesJson);
|
|
5552
|
+
if (profiles !== undefined && profiles.some((profile) => typeof profile !== "string")) {
|
|
5553
|
+
throw new OxygenError("invalid_input", "--profiles-json must be an array of LinkedIn profile URL strings.");
|
|
5554
|
+
}
|
|
5555
|
+
return requestOxygen("/api/cli/tables/linkedin-profile-watcher", {
|
|
5556
|
+
method: "POST",
|
|
5557
|
+
body: {
|
|
5558
|
+
action,
|
|
5559
|
+
...(readOption(options.table) ? { table: readOption(options.table) } : {}),
|
|
5560
|
+
...(readOption(options.name) ? { name: readOption(options.name) } : {}),
|
|
5561
|
+
...(readOption(options.project) ? { project: readOption(options.project) } : {}),
|
|
5562
|
+
...(profiles !== undefined ? { profiles } : {}),
|
|
5563
|
+
...(options.maxCredits !== undefined ? { max_credits_per_cycle: readPositiveNumber(options.maxCredits) } : {}),
|
|
5564
|
+
...(readOption(options.requestId) ? { request_id: readOption(options.requestId) } : {}),
|
|
5565
|
+
...(readOption(options.previewHash) ? { preview_hash: readOption(options.previewHash) } : {}),
|
|
5566
|
+
...(options.approved ? { approved: true } : {}),
|
|
5567
|
+
},
|
|
5568
|
+
});
|
|
5569
|
+
});
|
|
5570
|
+
});
|
|
5571
|
+
}
|
|
5441
5572
|
tablesCommand.addCommand(new Command("relate")
|
|
5442
5573
|
.description("Relate two tables: define empty Tables-owned relation columns on the source and target. Then use `oxygen tables link` to populate row-to-row edges. Works on any workspace table; plain tables stay plain and are never registered as CRM objects. Defaults to dry-run.")
|
|
5443
5574
|
.argument("<table>", "Source table id or slug.")
|
|
@@ -6280,11 +6411,46 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
6280
6411
|
program
|
|
6281
6412
|
.command("knowledge")
|
|
6282
6413
|
.description("Company knowledge wiki: slug-addressed pages, a [[wikilink]] graph, and a decision log.")
|
|
6414
|
+
.addCommand(new Command("folders")
|
|
6415
|
+
.description("Browse, create, and move nested folders for knowledge pages; slugs and wikilinks stay stable.")
|
|
6416
|
+
.addCommand(new Command("list")
|
|
6417
|
+
.description("List workspace folders and page locations, including empty folders. Read-only, 0 credits.")
|
|
6418
|
+
.option("--json", "Print a JSON envelope.")
|
|
6419
|
+
.action(async (options) => {
|
|
6420
|
+
await handleAsyncAction("knowledge folders list", options, () => requestOxygen("/api/cli/knowledge/folders"));
|
|
6421
|
+
}))
|
|
6422
|
+
.addCommand(new Command("create")
|
|
6423
|
+
.description("Create a persistent knowledge folder, optionally inside another folder.")
|
|
6424
|
+
.requiredOption("--name <name>", "Folder name.")
|
|
6425
|
+
.option("--parent <folder_id>", "Parent folder UUID from knowledge folders list. Omit for root.")
|
|
6426
|
+
.option("--json", "Print a JSON envelope.")
|
|
6427
|
+
.action(async (options) => {
|
|
6428
|
+
await handleAsyncAction("knowledge folders create", options, () => requestOxygen("/api/cli/knowledge/folders/create", {
|
|
6429
|
+
method: "POST",
|
|
6430
|
+
body: {
|
|
6431
|
+
name: options.name,
|
|
6432
|
+
...(readOption(options.parent) ? { parentId: readOption(options.parent) } : {}),
|
|
6433
|
+
},
|
|
6434
|
+
}));
|
|
6435
|
+
}))
|
|
6436
|
+
.addCommand(new Command("move")
|
|
6437
|
+
.description("Move a folder and its contents to another folder or the workspace root.")
|
|
6438
|
+
.argument("<id>", "Folder UUID from knowledge folders list.")
|
|
6439
|
+
.requiredOption("--parent <folder_id|root>", "Destination folder UUID, or root.")
|
|
6440
|
+
.option("--json", "Print a JSON envelope.")
|
|
6441
|
+
.action(async (id, options) => {
|
|
6442
|
+
await handleAsyncAction("knowledge folders move", options, () => requestOxygen("/api/cli/knowledge/folders/move", {
|
|
6443
|
+
method: "POST",
|
|
6444
|
+
body: { id, parentId: options.parent === "root" ? null : options.parent },
|
|
6445
|
+
}));
|
|
6446
|
+
})))
|
|
6283
6447
|
.addCommand(new Command("page")
|
|
6284
6448
|
.description("Knowledge wiki pages (slug-addressed, revision-guarded).")
|
|
6285
6449
|
.addCommand(new Command("upsert")
|
|
6286
6450
|
.description("Create or update a knowledge wiki page.")
|
|
6287
6451
|
.option("--slug <slug>", "Stable page slug to create or address. Omit to create by title.")
|
|
6452
|
+
.option("--folder <folder_id>", "Folder UUID, or root to remove folder placement. Omit to preserve the current folder.")
|
|
6453
|
+
.option("--create-only", "Fail if the page already exists instead of updating it.")
|
|
6288
6454
|
.option("--id <page_id>", "Existing page UUID to update. Omit to create.")
|
|
6289
6455
|
.option("--type <type>", "Page type (positioning, competitor, research_note, playbook, strategy, other). Defaults to other on create.")
|
|
6290
6456
|
.option("--title <title>", "Page title. Required on create.")
|
|
@@ -6307,7 +6473,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
6307
6473
|
});
|
|
6308
6474
|
}))
|
|
6309
6475
|
.addCommand(new Command("get")
|
|
6310
|
-
.description("Read one knowledge wiki page by slug or UUID.")
|
|
6476
|
+
.description("Read one knowledge wiki page by slug or UUID. Read-only, 0 credits.")
|
|
6311
6477
|
.argument("<slug_or_id>", "Page slug or UUID.")
|
|
6312
6478
|
.option("--json", "Print a JSON envelope.")
|
|
6313
6479
|
.action(async (slugOrId, options) => {
|
|
@@ -6317,7 +6483,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
6317
6483
|
}));
|
|
6318
6484
|
}))
|
|
6319
6485
|
.addCommand(new Command("list")
|
|
6320
|
-
.description("List knowledge wiki pages.")
|
|
6486
|
+
.description("List knowledge wiki pages. Read-only, 0 credits.")
|
|
6321
6487
|
.option("--type <type>", "Filter by page type.")
|
|
6322
6488
|
.option("--status <status>", "Filter by draft, active, or archived.")
|
|
6323
6489
|
.option("--tags <csv>", "Comma-separated tags that must be present.")
|
|
@@ -6365,7 +6531,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
6365
6531
|
});
|
|
6366
6532
|
}))
|
|
6367
6533
|
.addCommand(new Command("revisions")
|
|
6368
|
-
.description("List the revision history of a knowledge wiki page, newest first.")
|
|
6534
|
+
.description("List the revision history of a knowledge wiki page, newest first. Read-only, 0 credits.")
|
|
6369
6535
|
.argument("<slug_or_id>", "Page slug or UUID.")
|
|
6370
6536
|
.option("--limit <n>", "Maximum revisions to return.")
|
|
6371
6537
|
.option("--json", "Print a JSON envelope.")
|
|
@@ -6382,7 +6548,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
6382
6548
|
});
|
|
6383
6549
|
}))
|
|
6384
6550
|
.addCommand(new Command("revision")
|
|
6385
|
-
.description("Read one full revision snapshot of a knowledge wiki page.")
|
|
6551
|
+
.description("Read one full revision snapshot of a knowledge wiki page. Read-only, 0 credits.")
|
|
6386
6552
|
.argument("<slug_or_id>", "Page slug or UUID.")
|
|
6387
6553
|
.argument("<revision>", "Revision number.")
|
|
6388
6554
|
.option("--json", "Print a JSON envelope.")
|
|
@@ -6404,7 +6570,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
6404
6570
|
});
|
|
6405
6571
|
})))
|
|
6406
6572
|
.addCommand(new Command("search")
|
|
6407
|
-
.description("
|
|
6573
|
+
.description("Search wiki pages by meaning and keywords. Read-only, 0 credits. Plain language uses hybrid retrieval when available; check match_type and semantic_status. Quotes, OR, and exclusions stay lexical. If the platform-funded embedding allowance is unavailable, keyword search still works.")
|
|
6408
6574
|
.argument("<query>", "Search text. Supports quoted phrases, OR, and -exclude.")
|
|
6409
6575
|
.option("--type <type>", "Filter by page type.")
|
|
6410
6576
|
.option("--status <status>", "Filter by draft, active, or archived.")
|
|
@@ -6418,7 +6584,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
6418
6584
|
}));
|
|
6419
6585
|
}))
|
|
6420
6586
|
.addCommand(new Command("index")
|
|
6421
|
-
.description("Compact wiki index: each page's slug, one-liner, tags, link degree, plus type/status counts over the whole wiki. Lists the 200 most recently updated pages by default.")
|
|
6587
|
+
.description("Compact wiki index: each page's slug, one-liner, tags, link degree, plus type/status counts over the whole wiki. Lists the 200 most recently updated pages by default. Read-only, 0 credits. semantic.budget is Oxygen's platform-funded embedding safety allowance; its dollar and query counters are Oxygen's spend, never charges against your credits.")
|
|
6422
6588
|
.option("--limit <n>", "Maximum pages to list. Defaults to 200; hard cap is 1000. Counts always cover the whole wiki.")
|
|
6423
6589
|
.option("--all", "List every page instead of the most recently updated window.")
|
|
6424
6590
|
.option("--json", "Print a JSON envelope.")
|
|
@@ -6438,7 +6604,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
6438
6604
|
});
|
|
6439
6605
|
}))
|
|
6440
6606
|
.addCommand(new Command("graph")
|
|
6441
|
-
.description("Knowledge graph of pages and their [[wikilink]] edges. Pass --center to render one page's local neighborhood instead of the whole graph.")
|
|
6607
|
+
.description("Knowledge graph of pages and their [[wikilink]] edges. Pass --center to render one page's local neighborhood instead of the whole graph. Read-only, 0 credits.")
|
|
6442
6608
|
.option("--max-nodes <n>", "Maximum nodes to include in the graph.")
|
|
6443
6609
|
.option("--center <slug_or_id>", "Center the graph on one page and show only its neighborhood (local mode).")
|
|
6444
6610
|
.option("--depth <n>", "Local-graph hop depth (1-3, default 1). Only applies with --center.")
|
|
@@ -6450,7 +6616,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
6450
6616
|
}));
|
|
6451
6617
|
}))
|
|
6452
6618
|
.addCommand(new Command("lint")
|
|
6453
|
-
.description("
|
|
6619
|
+
.description("Read-only structural wiki health report, 0 credits: orphans (unfilled seed stubs excluded), dead-ends, unresolved links, contradictions (disputed pages, conflicting trust tags, competing live-copy pages), missing canonicals, untagged pages, duplicate titles, oversized hubs, unfilled seed stubs (total plus the most-linked-to), researchable stubs, decisions missing a reversal condition, stale pages (untouched or superseded by newer sources), oversized bodies, and aged proposals.")
|
|
6454
6620
|
.option("--json", "Print a JSON envelope.")
|
|
6455
6621
|
.action(async (options) => {
|
|
6456
6622
|
await handleAsyncAction("knowledge lint", options, () => requestOxygen("/api/cli/knowledge/lint"));
|
|
@@ -6471,7 +6637,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
6471
6637
|
.addCommand(new Command("log")
|
|
6472
6638
|
.description("Knowledge decision and activity log.")
|
|
6473
6639
|
.addCommand(new Command("list")
|
|
6474
|
-
.description("List knowledge log entries, newest first.")
|
|
6640
|
+
.description("List knowledge log entries, newest first. Read-only, 0 credits.")
|
|
6475
6641
|
.option("--cursor <cursor>", "Pagination cursor from a previous page.")
|
|
6476
6642
|
.option("--limit <n>", "Maximum log entries to return.")
|
|
6477
6643
|
.option("--json", "Print a JSON envelope.")
|
|
@@ -6495,7 +6661,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
6495
6661
|
}));
|
|
6496
6662
|
})))
|
|
6497
6663
|
.addCommand(new Command("proposals")
|
|
6498
|
-
.description("Draft change proposals against wiki pages, awaiting human review.")
|
|
6664
|
+
.description("Draft change proposals against wiki pages, awaiting human review. Read-only, 0 credits.")
|
|
6499
6665
|
.option("--status <status>", "open, decided, or all. Defaults to open.")
|
|
6500
6666
|
.option("--json", "Print a JSON envelope.")
|
|
6501
6667
|
.action(async (options) => {
|
|
@@ -7393,10 +7559,11 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
7393
7559
|
.argument("<table>", "Table id or slug.")
|
|
7394
7560
|
.option("--preset <preset>", "Add a pre-built enrichment bundle instead of one column: `person_enrich` (one LinkedIn profile lookup, then headline, bio, location and followers for free) or `company_enrich` (domain, LinkedIn page, headcount, industry and full profile, cascading across providers until one answers). Oxygen finds the identity column itself — override with --input. Creating the columns is free; run them afterwards, --dry-run first.")
|
|
7395
7561
|
.option("--input <slot=column...>", "Bind a preset input to an exact column, e.g. --input url=linkedin_url, or --input company_name=account --input domain=website. Repeatable. Only needed when the automatic match is wrong or missing.", collectRepeatable, [])
|
|
7396
|
-
.option("--
|
|
7562
|
+
.option("--capability <capability>", "Seed a ready-to-run enrichment column WITHOUT running it (0 credits): verify_email grades the address a row already holds \u2014 MillionVerifier first, catch-all domains escalate to BounceBan; work_email, mobile_phone and linkedin_url find a value the row is missing through the managed waterfall. Sets kind=enrichment and jsonb; label and key default from the capability. Preview cost with `enrich-column preview --capability <same>` and run later with `enrich-column run --approved --max-credits <n>`.")
|
|
7563
|
+
.option("--label <label>", "Display label for the new column. Required unless --prompt-key or --capability supplies a default title.")
|
|
7397
7564
|
.option("--key <key>", "Optional stable column key. Defaults to a normalized label.")
|
|
7398
7565
|
.option("--data-type <type>", "Column data type: text, numeric, boolean, jsonb, or timestamptz.")
|
|
7399
|
-
.option("--kind <kind>", "Column kind: manual, research, ai, formula, enrichment, tool, bind, or lookup. Defaults to manual. Use research for anything you would look up on the web. `lookup` reads a value out of another table; to LINK two tables row-to-row use `oxygen tables relate` instead \u2014 relation columns are two-sided and cannot be added here.")
|
|
7566
|
+
.option("--kind <kind>", "Column kind: manual, research, ai, formula, enrichment, tool, bind, or lookup. Defaults to manual. Use research for anything you would look up on the web. Enrichment columns always hold a jsonb cell, and --data-type is set for you \u2014 use --capability to seed a ready-to-run one. `lookup` reads a value out of another table; to LINK two tables row-to-row use `oxygen tables relate` instead \u2014 relation columns are two-sided and cannot be added here.")
|
|
7400
7567
|
.option("--semantic-type <type>", "Optional semantic type such as company_domain.")
|
|
7401
7568
|
.option("--definition-json <json>", "Optional JSON object with column definition metadata.")
|
|
7402
7569
|
.option("--prompt <text-or-file>", "AI or research column prompt, or a path to a prompt file — a value that resolves to a readable file is read as one, matching --prompt everywhere else in this CLI. On its own it sets kind=ai; pair it with --kind research to search the web per row instead. If the prompt names its output sections — a line reading 'Return the following sections:' followed by 'Score: ...', 'Reasoning: ...' — the column answers in exactly that shape and each section becomes a referenceable sub-column; otherwise it answers in plain text. Use --no-structured-output to keep it plain text either way. Reference other columns inline as {{column_key}} — no --input-mapping needed; unknown keys are rejected here instead of failing per row. Merges into --definition-json (the escape hatch for everything else); a `prompt` in both is an error.")
|
|
@@ -7430,6 +7597,36 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
7430
7597
|
// skipcq: JS-R1005 — intentional per-option branching to assemble the columns-add request body
|
|
7431
7598
|
.action(async (table, options) => {
|
|
7432
7599
|
await handleAsyncAction("columns add", options, async () => {
|
|
7600
|
+
// --capability seeds a COMPLETE enrichment column server-side (kind,
|
|
7601
|
+
// jsonb data type, intent, the default provider order, label and
|
|
7602
|
+
// key), so any flag that authors a different column is a conflict,
|
|
7603
|
+
// not a modifier. Caught here rather than server-side so the user
|
|
7604
|
+
// never gets back a column they did not ask for.
|
|
7605
|
+
const capability = readOption(options.capability);
|
|
7606
|
+
if (capability) {
|
|
7607
|
+
const conflicting = [
|
|
7608
|
+
readOption(options.preset) ? "--preset" : null,
|
|
7609
|
+
readOption(options.prompt) ? "--prompt" : null,
|
|
7610
|
+
readOption(options.promptKey) ? "--prompt-key" : null,
|
|
7611
|
+
readOption(options.bindObject) ? "--bind-object" : null,
|
|
7612
|
+
readOption(options.bindMap) ? "--bind-map" : null,
|
|
7613
|
+
options.bindCreate ? "--bind-create" : null,
|
|
7614
|
+
readOption(options.lookupTable) ? "--lookup-table" : null,
|
|
7615
|
+
readOption(options.lookupMatch) ? "--lookup-match" : null,
|
|
7616
|
+
readOption(options.lookupMode) ? "--lookup-mode" : null,
|
|
7617
|
+
readOption(options.lookupReturn) ? "--lookup-return" : null,
|
|
7618
|
+
readOption(options.lookupOrder) ? "--lookup-order" : null,
|
|
7619
|
+
readOption(options.lookupAggregate) ? "--lookup-aggregate" : null,
|
|
7620
|
+
readOption(options.lookupNormalize) ? "--lookup-normalize" : null,
|
|
7621
|
+
].filter((flag) => flag !== null);
|
|
7622
|
+
if (conflicting.length > 0) {
|
|
7623
|
+
throw new OxygenError("invalid_request", `--capability ${capability} seeds a complete enrichment column, so it cannot be combined with ${conflicting.join(", ")}. Drop one of the two.`, { exitCode: 1 });
|
|
7624
|
+
}
|
|
7625
|
+
const capabilityKind = readOption(options.kind)?.toLowerCase() ?? null;
|
|
7626
|
+
if (capabilityKind && capabilityKind !== "enrichment") {
|
|
7627
|
+
throw new OxygenError("invalid_request", `--capability authors an enrichment column, but --kind ${capabilityKind} was requested. Drop --kind, or drop --capability.`, { exitCode: 1 });
|
|
7628
|
+
}
|
|
7629
|
+
}
|
|
7433
7630
|
// A preset names its own columns, so --label does not apply to it.
|
|
7434
7631
|
const preset = readOption(options.preset);
|
|
7435
7632
|
if (preset) {
|
|
@@ -7447,7 +7644,9 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
7447
7644
|
body: presetBody,
|
|
7448
7645
|
});
|
|
7449
7646
|
}
|
|
7450
|
-
|
|
7647
|
+
// --capability supplies its own label ("Email Verification", ...)
|
|
7648
|
+
// server-side, the same one the web picker writes.
|
|
7649
|
+
if (!options.promptKey && !capability && !options.label) {
|
|
7451
7650
|
throw new OxygenError("invalid_request", "--label is required.", { exitCode: 1 });
|
|
7452
7651
|
}
|
|
7453
7652
|
if (options.promptKey && !options.inputMapping) {
|
|
@@ -7472,6 +7671,15 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
7472
7671
|
column.definition = parseJsonObject(options.definitionJson);
|
|
7473
7672
|
const requestedKind = readOption(options.kind)?.toLowerCase() ?? null;
|
|
7474
7673
|
const isResearch = requestedKind === "research";
|
|
7674
|
+
if (capability)
|
|
7675
|
+
column.capability = capability;
|
|
7676
|
+
// An enrichment cell is always the provider envelope, so the server
|
|
7677
|
+
// rejects any other data type ("Enrichment columns must use jsonb
|
|
7678
|
+
// data type."). Default it, the way --bind-object already does,
|
|
7679
|
+
// instead of making the caller bolt on --data-type jsonb.
|
|
7680
|
+
if (requestedKind === "enrichment" && !options.dataType) {
|
|
7681
|
+
column.data_type = "jsonb";
|
|
7682
|
+
}
|
|
7475
7683
|
if (prompt !== null) {
|
|
7476
7684
|
if (requestedKind && requestedKind !== "ai" && !isResearch) {
|
|
7477
7685
|
throw new OxygenError("invalid_request", `--prompt authors an AI or research column, but --kind ${requestedKind} was requested. Drop --kind, or drop --prompt.`, { exitCode: 1 });
|
|
@@ -7577,7 +7785,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
7577
7785
|
.argument("<column>", "Column id or key.")
|
|
7578
7786
|
.option("--row-id <row_id>", "Workspace row id to run. Get one from `oxygen tables query <table> --limit 1 --json` — the field is `_row_id`, not `id`.")
|
|
7579
7787
|
.option("--limit <n>", "Run the next N rows whose target cell is still empty (--force runs the first N regardless). Repeat until rowCount is 0 to page through a table. Defaults to 10; inline deterministic runs have a hard cap of 25.")
|
|
7580
|
-
.option("--all", "Run all rows. Requires --background.")
|
|
7788
|
+
.option("--all", "Run all rows. Requires --background, except with --dry-run, which previews the background run without queueing it.")
|
|
7581
7789
|
.option("--filter-json <json>", "Row selector filter object or array for background runs. Do not combine with --all, --limit, or --row-id.")
|
|
7582
7790
|
.option("--formula-values <mode>", "With --filter-json on a formula column, first refresh that selector with `columns run <table> <column> --force` (0 credits), then pass 'materialized'. The run filters the stored snapshot and persists this freshness acknowledgement.")
|
|
7583
7791
|
.option("--force", "Run even when the target cell already has a value.")
|
|
@@ -7610,18 +7818,27 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
7610
7818
|
Boolean(readOption(options.rowId)),
|
|
7611
7819
|
Boolean(effectiveFilterSelection),
|
|
7612
7820
|
].filter(Boolean).length;
|
|
7613
|
-
if (selectedModes > 1) {
|
|
7614
|
-
throw new OxygenError("invalid_selection", "Pass only one of --all, --limit, --row-id, or --filter-json.", {
|
|
7615
|
-
exitCode: 1,
|
|
7616
|
-
});
|
|
7617
|
-
}
|
|
7618
|
-
if (options.all && !options.background) {
|
|
7619
|
-
throw new OxygenError("invalid_column_run", "--all requires --background.", {
|
|
7620
|
-
exitCode: 1,
|
|
7621
|
-
});
|
|
7622
|
-
}
|
|
7623
7821
|
// skipcq: JS-R1005 — intentional branching for local/background/filter column-run modes
|
|
7624
7822
|
await handleAsyncAction("columns run", options, async () => {
|
|
7823
|
+
// Selection-shape errors are raised inside the action so --json
|
|
7824
|
+
// callers get the failure envelope, not a bare stack trace.
|
|
7825
|
+
if (selectedModes > 1) {
|
|
7826
|
+
throw new OxygenError("invalid_selection", "Pass only one of --all, --limit, --row-id, or --filter-json.", {
|
|
7827
|
+
exitCode: 1,
|
|
7828
|
+
});
|
|
7829
|
+
}
|
|
7830
|
+
// A dry run of an all-rows run previews the background run the live
|
|
7831
|
+
// command would create and queues nothing, so demanding --background
|
|
7832
|
+
// for it only teaches a flag with no behavioural basis (blind eval
|
|
7833
|
+
// 2026-09-10: the agent tripped the error, then re-ran with the flag).
|
|
7834
|
+
if (options.all && options.dryRun && !options.background && !options.local) {
|
|
7835
|
+
options.background = true;
|
|
7836
|
+
}
|
|
7837
|
+
if (options.all && !options.background) {
|
|
7838
|
+
throw new OxygenError("invalid_column_run", "--all requires --background.", {
|
|
7839
|
+
exitCode: 1,
|
|
7840
|
+
});
|
|
7841
|
+
}
|
|
7625
7842
|
if (options.local) {
|
|
7626
7843
|
if (options.background) {
|
|
7627
7844
|
throw new OxygenError("invalid_column_run", "Pass either --local or --background, not both.", {
|
|
@@ -8464,7 +8681,8 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
8464
8681
|
.description("Plan, dry-run, or queue provider-backed company search.")
|
|
8465
8682
|
.addCommand(new Command("plan")
|
|
8466
8683
|
.description("Compile a company-search prompt into ordered provider routes without provider calls.")
|
|
8467
|
-
.
|
|
8684
|
+
.argument("[prompt]", "Prompt text or @file (same as --prompt). The --prompt flag wins if both are given.")
|
|
8685
|
+
.option("--prompt <text-or-file>", "Company-search prompt, or a path to a prompt file.")
|
|
8468
8686
|
.option("--target-count <n>", "Desired company count. Single-plan ceiling 50,000; larger requests return an explicit clamp warning and segmentation guidance.")
|
|
8469
8687
|
.option("--source-intent <intent>", "Override detected intent: sizing, structured, lookalike, technology, hiring, local, known_source, concept, web, url, or fallback.")
|
|
8470
8688
|
.option("--filters-json <json-or-file>", "CompanySearchFilters JSON inline or a @file/path; wins over individual flags per top-level filter path.")
|
|
@@ -8482,14 +8700,15 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
8482
8700
|
.option("--estimate", "Run a free server-side preflight pass: resolves provider enums and a free count probe for an estimated match count (zero credits).")
|
|
8483
8701
|
.option("--materialize-preview", "Create a preview table with route rows.")
|
|
8484
8702
|
.option("--json", "Print a JSON envelope.")
|
|
8485
|
-
.action(async (options) => {
|
|
8703
|
+
.action(async (promptArg, options) => {
|
|
8486
8704
|
await handleAsyncAction("companies search plan", options, () => requestOxygen("/api/cli/companies/search/plan", {
|
|
8487
8705
|
method: "POST",
|
|
8488
|
-
body: readCompaniesSearchPlanBody(options),
|
|
8706
|
+
body: readCompaniesSearchPlanBody(options, promptArg),
|
|
8489
8707
|
}));
|
|
8490
8708
|
}))
|
|
8491
8709
|
.addCommand(new Command("run")
|
|
8492
8710
|
.description("Return a dry-run request or queue a live company-search ingestion run.")
|
|
8711
|
+
.argument("[prompt]", "Prompt text or @file (same as --prompt). The --prompt flag wins if both are given.")
|
|
8493
8712
|
.option("--prompt <text-or-file>", "Company-search prompt, or a path to a prompt file.")
|
|
8494
8713
|
.option("--plan-json <json-or-file>", "Plan JSON returned by companies search plan, or a path to a JSON file.")
|
|
8495
8714
|
.option("--route-id <id>", "Route id from the plan to execute.")
|
|
@@ -8517,10 +8736,10 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
8517
8736
|
.option("--estimate", "Run a free server-side preflight pass when planning from --prompt: resolves provider enums and a free count probe (zero credits).")
|
|
8518
8737
|
.option("--approved", "Required for live runs after inspecting dry-run output.")
|
|
8519
8738
|
.option("--json", "Print a JSON envelope.")
|
|
8520
|
-
.action(async (options) => {
|
|
8739
|
+
.action(async (promptArg, options) => {
|
|
8521
8740
|
await handleAsyncAction("companies search run", options, () => requestOxygen("/api/cli/companies/search/run", {
|
|
8522
8741
|
method: "POST",
|
|
8523
|
-
body: readCompaniesSearchRunBody(options),
|
|
8742
|
+
body: readCompaniesSearchRunBody(options, promptArg),
|
|
8524
8743
|
}));
|
|
8525
8744
|
})))
|
|
8526
8745
|
.addCommand(new Command("enrich")
|
|
@@ -8756,7 +8975,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
8756
8975
|
}));
|
|
8757
8976
|
}))
|
|
8758
8977
|
.addCommand(new Command("balance")
|
|
8759
|
-
.description("Show the current plan, subscription entitlement, and managed credit balance: available and reserved credits, FIXED credits committed to recurring per-resource charges, and the FLEXIBLE free-to-spend remainder. `renewals_past_due` lists infrastructure renewals (managed-inbox domains, warm-ups) OXYGEN could NOT charge this month, what they cost, and the exact top-up that clears them — nothing is cancelled and billing retries automatically after a top-up. `warnings` also flags a recurring_shortfall when your connected infrastructure costs more per month than the plan grants. After failed-payment grace expires, mutations stop while read/export and billing recovery remain available. Recovery: https://oxygen-agent.com/billing. Policy: https://oxygen-agent.com/docs/safety/billing.")
|
|
8978
|
+
.description("Show the current plan, subscription entitlement, and managed credit balance: available and reserved credits, FIXED credits committed to recurring per-resource charges, and the FLEXIBLE free-to-spend remainder. `next_renewal` answers \"will I be charged?\": read `will_charge` first — true only when this workspace's own Stripe plan is going to charge its card, in which case `charge_at` and the plan's monthly list price in money say when and roughly how much (the exact amount, with any promotion code or tax, is on the invoice); `period_ends_at` is the trial or period end either way, `cancellation_scheduled` says whether a cancellation is already set, and `stop_command` names the one command that changes it (`billing cancel`, undone by `billing resume`) or is null when nothing here can stop it (staff-invoiced, billed through another workspace, not active). `renewals_past_due` lists infrastructure renewals (managed-inbox domains, warm-ups) OXYGEN could NOT charge this month, what they cost, and the exact top-up that clears them — nothing is cancelled and billing retries automatically after a top-up. `warnings` also flags a recurring_shortfall when your connected infrastructure costs more per month than the plan grants. After failed-payment grace expires, mutations stop while read/export and billing recovery remain available. Recovery: https://oxygen-agent.com/billing. Policy: https://oxygen-agent.com/docs/safety/billing.")
|
|
8760
8979
|
.option("--json", "Print a JSON envelope.")
|
|
8761
8980
|
.action(async (options) => {
|
|
8762
8981
|
await handleAsyncAction("billing balance", options, () => requestOxygen("/api/cli/billing/balance"));
|
|
@@ -8768,7 +8987,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
8768
8987
|
await handleAsyncAction("billing commitments", options, () => requestOxygen("/api/cli/billing/commitments"));
|
|
8769
8988
|
}))
|
|
8770
8989
|
.addCommand(new Command("seats")
|
|
8771
|
-
.description("Show sending capacity per channel: how many seats are purchased, how many are grandfathered, how many senders are connected, and how many are left. Sending seats are what let you CONNECT a sender — a LinkedIn account, WhatsApp number, phone number, or email mailbox — and they are billed in dollars on their own subscription, separate from your plan and separate from credits. You do not need a plan to buy seats. A grandfathered value of null means unlimited for that channel. Email mailbox allocation is read from this workspace's own database; when it cannot be read, the email seat's allocated/available/can_connect read null and `email_sender_allocation` is `tenant_unavailable`. Read-only, 0 Oxygen credits.")
|
|
8990
|
+
.description("Show sending capacity per channel: how many seats are purchased, how many are grandfathered, how many senders are connected, and how many are left. Sending seats are what let you CONNECT a sender — a LinkedIn account, WhatsApp number, phone number, or email mailbox — and they are billed in dollars on their own subscription, separate from your plan and separate from credits. You do not need a plan to buy seats. Seats belong to the billing owner: a workspace linked to another organization's plan with `orgs billing-link` connects senders against that organization's seats and buys them there. A grandfathered value of null means unlimited for that channel. Email mailbox allocation is read from this workspace's own database; when it cannot be read, the email seat's allocated/available/can_connect read null and `email_sender_allocation` is `tenant_unavailable`. Read-only, 0 Oxygen credits.")
|
|
8772
8991
|
.option("--json", "Print a JSON envelope.")
|
|
8773
8992
|
.action(async (options) => {
|
|
8774
8993
|
await handleAsyncAction("billing seats", options, () => requestOxygen("/api/cli/billing/seats"));
|
|
@@ -9233,10 +9452,60 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
9233
9452
|
});
|
|
9234
9453
|
}))
|
|
9235
9454
|
.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.
|
|
9455
|
+
.description("Board of the PRIMARY managed external data providers (enrichment, people/company search, web search, scraping, signals, AI-column web grounding, LLM inference): per provider health probe, 7d traffic, balance, 30d/7d COGS, rate policy, spend ceilings, breaker, and posture, plus the platform runaway guards and how old each snapshot is. Reads the snapshots the crons write; --refresh re-runs those same zero-credit snapshots first. Staff only.")
|
|
9456
|
+
.option("--refresh", "Re-run the zero-credit snapshot work the three crons run (provider health probes, managed balances, cost snapshot) before reading the board. No paid provider call and no credit spend; takes up to a few minutes.")
|
|
9457
|
+
.option("--stages <csv>", "Limit --refresh to these stages: health, balances, costs. Defaults to all three.")
|
|
9458
|
+
.option("--force", "Refresh even inside the server's cooldown on re-probing every managed vendor. Only with --refresh.")
|
|
9237
9459
|
.option("--json", "Print a JSON envelope.")
|
|
9238
9460
|
.action(async (options) => {
|
|
9239
|
-
|
|
9461
|
+
const stages = readCsvOption(options.stages);
|
|
9462
|
+
if (stages.length > 0 && !options.refresh) {
|
|
9463
|
+
// Fail with the envelope rather than reading a board the caller
|
|
9464
|
+
// believes it just refreshed.
|
|
9465
|
+
emitCliFailure("admin primary-providers", new OxygenError("invalid_request", "--stages only applies to --refresh. Add --refresh to re-run those snapshots.", { exitCode: 2 }));
|
|
9466
|
+
return;
|
|
9467
|
+
}
|
|
9468
|
+
if (options.force && !options.refresh) {
|
|
9469
|
+
// Same reason as --stages: --force only relaxes the refresh
|
|
9470
|
+
// cooldown, so on its own it reads the very board the caller
|
|
9471
|
+
// believes it just forced a re-probe of.
|
|
9472
|
+
emitCliFailure("admin primary-providers", new OxygenError("invalid_request", "--force only applies to --refresh. Add --refresh to re-run those snapshots.", { exitCode: 2 }));
|
|
9473
|
+
return;
|
|
9474
|
+
}
|
|
9475
|
+
let failed = false;
|
|
9476
|
+
const board = await requestOxygen("/api/cli/admin/primary-providers", options.refresh
|
|
9477
|
+
? {
|
|
9478
|
+
method: "POST",
|
|
9479
|
+
// The route budgets maxDuration = 300 and the balance stage
|
|
9480
|
+
// walks every fetcher sequentially at 15s each, so the
|
|
9481
|
+
// default 120s client budget aborts a refresh the server
|
|
9482
|
+
// finishes — the operator reads a failure for work that
|
|
9483
|
+
// succeeded and re-runs the outbound probes.
|
|
9484
|
+
timeoutMs: 300_000,
|
|
9485
|
+
body: {
|
|
9486
|
+
refresh: true,
|
|
9487
|
+
...(stages.length > 0 ? { stages } : {}),
|
|
9488
|
+
...(options.force ? { force: true } : {}),
|
|
9489
|
+
},
|
|
9490
|
+
}
|
|
9491
|
+
: undefined).catch((error) => {
|
|
9492
|
+
emitCliFailure("admin primary-providers", error);
|
|
9493
|
+
failed = true;
|
|
9494
|
+
return null;
|
|
9495
|
+
});
|
|
9496
|
+
// Only a transport failure ends the command silently; an empty
|
|
9497
|
+
// payload still gets rendered (or enveloped) rather than exiting 0
|
|
9498
|
+
// with nothing printed.
|
|
9499
|
+
if (failed)
|
|
9500
|
+
return;
|
|
9501
|
+
if (options.json) {
|
|
9502
|
+
emitSuccess("admin primary-providers", board, options);
|
|
9503
|
+
return;
|
|
9504
|
+
}
|
|
9505
|
+
// Without this the default invocation printed ~1,500 lines of raw
|
|
9506
|
+
// JSON while the MCP tool printed a worst-first summary of the same
|
|
9507
|
+
// payload — same board, two different products.
|
|
9508
|
+
process.stdout.write(`${renderPrimaryProviderBoard(board, { binary: binaryName })}\n`);
|
|
9240
9509
|
}))
|
|
9241
9510
|
.addCommand(new Command("spend")
|
|
9242
9511
|
.description("Global managed-provider spend limiter: burn, ceilings, breakers. Staff only.")
|
|
@@ -10342,11 +10611,20 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
10342
10611
|
.option("--return <mode>", "Legacy response shape: raw, compact, or summary. Defaults to raw.")
|
|
10343
10612
|
.option("--return-mode <mode>", "Response shape: raw, compact, or summary. Prefer summary for large search responses.")
|
|
10344
10613
|
.option("--oxygen-cursor <cursor>", "Short Oxygen cursor returned as oxygen_next_cursor by a previous tool run.")
|
|
10345
|
-
.option("--max-credits <n>", "Credit ceiling for live paid tools; not needed for no-bill tools.")
|
|
10614
|
+
.option("--max-credits <n>", "Credit ceiling for live paid tools; not needed for no-bill tools, and 0 is accepted for no-bill operations.")
|
|
10346
10615
|
.option("--approved", "Required for live paid tools and external writes after inspecting dry-run output.")
|
|
10347
10616
|
.option("--json", "Print a JSON envelope.")
|
|
10348
10617
|
.action(async (toolId, options) => {
|
|
10349
|
-
|
|
10618
|
+
// Zero is a real ceiling here, not a typo: /api/cli/tools/run only
|
|
10619
|
+
// demands a positive max_credits for BILLED tools, so a documented
|
|
10620
|
+
// no-bill operation is legitimately run with --max-credits 0. The
|
|
10621
|
+
// positive-only reader rejected that locally, before the request.
|
|
10622
|
+
//
|
|
10623
|
+
// Non-negative NUMBER, not readPositiveNumberOrZero (a whole seat
|
|
10624
|
+
// count): this command's own dry-run hint prints "--max-credits
|
|
10625
|
+
// 24.94", so an integer reader would refuse the very command the CLI
|
|
10626
|
+
// just told the operator to run.
|
|
10627
|
+
const maxCredits = readNonNegativeNumber(options.maxCredits);
|
|
10350
10628
|
await handleAsyncAction("tools run", options, () => requestOxygen("/api/cli/tools/run", {
|
|
10351
10629
|
method: "POST",
|
|
10352
10630
|
body: {
|
|
@@ -10450,7 +10728,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
10450
10728
|
}));
|
|
10451
10729
|
program
|
|
10452
10730
|
.command("find")
|
|
10453
|
-
.description("One-shot contact and company lookups over enrichment waterfalls (no table). dry_run previews for free; live spends and needs --max-credits.")
|
|
10731
|
+
.description("One-shot contact and company lookups over enrichment waterfalls (no table). dry_run previews the plan for free (which lanes would run, in order, and what each costs — not the answer); live spends and needs --max-credits.")
|
|
10454
10732
|
.addCommand(new Command("email")
|
|
10455
10733
|
.description("Find a person's work email via an input-aware multi-provider waterfall: a LinkedIn URL, name+domain, or first/last+domain each select the best provider profile, with an email-pattern pre-step and verification. dry_run shows the resolved profile + chain for free.")
|
|
10456
10734
|
.option("--linkedin-url <url>", "Person LinkedIn profile URL (strongest signal).")
|
|
@@ -10459,7 +10737,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
10459
10737
|
.option("--last-name <name>", "Person last name.")
|
|
10460
10738
|
.option("--company-domain <domain>", "Company apex domain, e.g. acme.com.")
|
|
10461
10739
|
.option("--company-name <name>", "Company name.")
|
|
10462
|
-
.option("--mode <mode>", "dry_run (default)
|
|
10740
|
+
.option("--mode <mode>", "dry_run (default) previews the plan and spends nothing; live spends credits and returns the answer.")
|
|
10463
10741
|
.option("--max-credits <credits>", "Spend ceiling. Required for --mode live.")
|
|
10464
10742
|
.option("--json", "Print a JSON envelope.")
|
|
10465
10743
|
.action(async (options) => {
|
|
@@ -10473,7 +10751,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
10473
10751
|
.option("--company-domain <domain>", "Company apex domain.")
|
|
10474
10752
|
.option("--company-name <name>", "Company name.")
|
|
10475
10753
|
.option("--verify", "Verify the found number with ClearoutPhone — adds line_type/carrier and keeps the number on a non-verdict (only a genuine 'not valid' is discarded).")
|
|
10476
|
-
.option("--mode <mode>", "dry_run (default)
|
|
10754
|
+
.option("--mode <mode>", "dry_run (default) previews the plan and spends nothing; live spends credits and returns the answer.")
|
|
10477
10755
|
.option("--max-credits <credits>", "Spend ceiling. Required for --mode live.")
|
|
10478
10756
|
.option("--json", "Print a JSON envelope.")
|
|
10479
10757
|
.action(async (options) => {
|
|
@@ -10486,7 +10764,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
10486
10764
|
.option("--company-domain <domain>", "Company apex domain.")
|
|
10487
10765
|
.option("--company-name <name>", "Company name.")
|
|
10488
10766
|
.option("--company-linkedin-url <url>", "Company LinkedIn URL.")
|
|
10489
|
-
.option("--mode <mode>", "dry_run (default)
|
|
10767
|
+
.option("--mode <mode>", "dry_run (default) previews the plan and spends nothing; live spends credits and returns the answer.")
|
|
10490
10768
|
.option("--max-credits <credits>", "Spend ceiling. Required for --mode live.")
|
|
10491
10769
|
.option("--json", "Print a JSON envelope.")
|
|
10492
10770
|
.action(async (options) => {
|
|
@@ -10498,8 +10776,8 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
10498
10776
|
.option("--name <name>", "Company name.")
|
|
10499
10777
|
.option("--linkedin-url <url>", "Company LinkedIn URL.")
|
|
10500
10778
|
.option("--fields <fields>", "Comma-separated company fields. Defaults to domain,linkedin_url,headcount,industry.")
|
|
10501
|
-
.option("--mode <mode>", "dry_run (default)
|
|
10502
|
-
.option("--max-credits <credits>", "Spend ceiling. Required for --mode live.")
|
|
10779
|
+
.option("--mode <mode>", "dry_run (default) previews the plan and spends nothing; live spends credits and returns the answer.")
|
|
10780
|
+
.option("--max-credits <credits>", "Spend ceiling. Required for --mode live; 0 runs only the zero-credit lanes (the priced lanes are skipped as credit_ceiling_reached).")
|
|
10503
10781
|
.option("--json", "Print a JSON envelope.")
|
|
10504
10782
|
.action(async (options) => {
|
|
10505
10783
|
await handleAsyncAction("find company", options, () => requestOxygen("/api/cli/find/run", { method: "POST", body: buildFindBody("company", options) }));
|
|
@@ -10508,11 +10786,11 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
10508
10786
|
.command("verify")
|
|
10509
10787
|
.description("Check whether emails you already have are safe to send to. dry_run previews the provider chain for free; live spends and needs --max-credits.")
|
|
10510
10788
|
.addCommand(new Command("email")
|
|
10511
|
-
.description("Verify one or more email addresses. MillionVerifier answers first; addresses on catch-all (accept-all) domains, which it can only flag as risky, escalate automatically to BounceBan to get a real answer. Returns valid / invalid / catch_all / unknown per address.")
|
|
10789
|
+
.description("Verify one or more email addresses. MillionVerifier answers first; addresses on catch-all (accept-all) domains, which it can only flag as risky, escalate automatically to BounceBan to get a real answer. Returns valid / invalid / catch_all / unknown per address. An address whose escalation could not run keeps catch_all — record it as unconfirmed, never verified — and its escalation_unavailable_reason plus next_action say why and what to do: provider_account_dry on the managed lane is Oxygen's BounceBan balance, not your workspace credits, and you were not charged for it.")
|
|
10512
10790
|
.argument("<emails...>", "One or more email addresses to verify.")
|
|
10513
|
-
.option("--mode <mode>", "dry_run (default)
|
|
10791
|
+
.option("--mode <mode>", "dry_run (default) previews the plan and spends nothing; live spends credits and returns the answer.")
|
|
10514
10792
|
.option("--approved", "Same as --mode live. Accepted because the global help footer names --approved as the way to authorize a credit-spending command.")
|
|
10515
|
-
.option("--max-credits <credits>", "Spend ceiling for the whole run. Required for --mode live. Run the free dry_run first — it returns estimate.recommended_max_credits, the value that guarantees every catch-all address still gets escalated.")
|
|
10793
|
+
.option("--max-credits <credits>", "Spend ceiling for the whole run. Required for --mode live. Run the free dry_run first — it returns estimate.recommended_max_credits, the value that guarantees every catch-all address still gets escalated. Re-verifying the exact same address in this workspace replays MillionVerifier's first pass free for up to 30 days, so a repeat run can report credits_used 0 — the catch-all escalation is never cached, and its reservation is released in full when that call returns no verdict.")
|
|
10516
10794
|
.option("--json", "Print a JSON envelope.")
|
|
10517
10795
|
.action(async (emails, options) => {
|
|
10518
10796
|
await handleAsyncAction("verify email", options, () => requestOxygen("/api/cli/verify/run", {
|
|
@@ -11330,7 +11608,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
11330
11608
|
});
|
|
11331
11609
|
}))
|
|
11332
11610
|
.addCommand(new Command("delete")
|
|
11333
|
-
.description("Delete a LinkedIn post the connected account authored — a REAL, irreversible public write. Refuses without --approved (exit 7). --post is the composite social_id returned when the post was created (or by `oxygen posts get`), NOT the activity URN. To remove
|
|
11611
|
+
.description("Delete a LinkedIn post the connected account authored — a REAL, irreversible public write. Refuses without --approved (exit 7). --post is the composite social_id returned when the post was created (or by `oxygen posts get`), NOT the activity URN. To remove only the OXYGEN copy, including a settled published copy, while keeping LinkedIn intact, use `oxygen publishing posts delete`.")
|
|
11334
11612
|
.requiredOption("--post <social_id>", "Composite post social_id from `oxygen posts get` (the id returned when the post was created).")
|
|
11335
11613
|
.option("--account <ref>", "Sender account that authored the post (sender id, connection id, or Unipile account id). Omit for the org default.")
|
|
11336
11614
|
.option("--approved", "Actually delete the post. Without it the command refuses and deletes nothing.")
|
|
@@ -11360,7 +11638,46 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
11360
11638
|
});
|
|
11361
11639
|
})));
|
|
11362
11640
|
program.addCommand(new Command("engagement")
|
|
11363
|
-
.description("Capture LinkedIn intent from one known post (harvest needs its public URL or the composite social_id from `oxygen posts get`), and react or comment on it. Recurring capture — a post's engagers on a cadence, 'who viewed my profile', new followers, new connections — is a live table: arm it with `oxygen linkedin intent setup` and operate it with `oxygen feeds list|pause|resume|run`. For
|
|
11641
|
+
.description("Capture LinkedIn intent from one known post (harvest needs its public URL or the composite social_id from `oxygen posts get`), and react or comment on it. Recurring capture — a post's engagers on a cadence, 'who viewed my profile', new followers, new connections — is a live table: arm it with `oxygen linkedin intent setup` and operate it with `oxygen feeds list|pause|resume|run`. For daily monitoring across profiles and their recent post engagers, start with `oxygen tables watcher preview --help` (free credit review; one editable table). Harvests run as a slow, durable drip under a conservative read budget.")
|
|
11642
|
+
.addCommand(new Command("engagers")
|
|
11643
|
+
.description("Read one post's reactors and commenters right now as a de-duplicated people list ready to enroll. Pages both sources up to --max-pages (default 5, max 20) x 100 per page; `truncated: true` in the envelope means a cap stopped a source that still had more — raise --max-pages, or use `oxygen engagement harvest` for a post too big to read in one request. Nothing is sent and no credits are spent, but every page is one LinkedIn read against the sender account. --post is the composite social_id from `oxygen posts get` (NOT the activity URN).")
|
|
11644
|
+
.requiredOption("--post <social_id>", "Composite post social_id from `oxygen posts get` (NOT the activity URN).")
|
|
11645
|
+
.option("--account <ref>", "Sender account that reads (sender id, connection id, or Unipile account id). Omit for the org default.")
|
|
11646
|
+
.option("--limit <n>", "Engagers per PAGE (default 100, which is also the provider maximum). Per source you get --limit x --max-pages.")
|
|
11647
|
+
.option("--max-pages <n>", "Pages to walk per source, 1-20 (default 5, so 500 reactors + 500 commenters).")
|
|
11648
|
+
.option("--no-reactions", "Skip reactors.")
|
|
11649
|
+
.option("--no-comments", "Skip commenters.")
|
|
11650
|
+
.option("--json", "Print a JSON envelope.")
|
|
11651
|
+
.action(async (options) => {
|
|
11652
|
+
await handleAsyncAction("engagement engagers", options, async () => {
|
|
11653
|
+
const post = readOption(options.post);
|
|
11654
|
+
if (!post)
|
|
11655
|
+
throw new Error("--post is required (the composite social_id from `oxygen posts get`).");
|
|
11656
|
+
const account = readOption(options.account);
|
|
11657
|
+
const limit = readPositiveInteger(options.limit);
|
|
11658
|
+
const maxPages = readPositiveInteger(options.maxPages);
|
|
11659
|
+
const params = new URLSearchParams({ post });
|
|
11660
|
+
if (account)
|
|
11661
|
+
params.set("account", account);
|
|
11662
|
+
if (limit !== undefined)
|
|
11663
|
+
params.set("limit", String(limit));
|
|
11664
|
+
// Forwarded as-is: the route owns the 1-20 range so an
|
|
11665
|
+
// out-of-range value fails loudly instead of silently reading
|
|
11666
|
+
// fewer pages than the caller asked for.
|
|
11667
|
+
if (maxPages !== undefined)
|
|
11668
|
+
params.set("max_pages", String(maxPages));
|
|
11669
|
+
if (options.reactions === false)
|
|
11670
|
+
params.set("include_reactions", "false");
|
|
11671
|
+
if (options.comments === false)
|
|
11672
|
+
params.set("include_comments", "false");
|
|
11673
|
+
const data = await requestOxygen(`/api/cli/linkedin/engagement?${params.toString()}`);
|
|
11674
|
+
// Ahead of the payload, and only for a human: a machine caller
|
|
11675
|
+
// reads truncated/partial_reason off the envelope itself.
|
|
11676
|
+
if (!options.json)
|
|
11677
|
+
writeEngagersReceipt(data);
|
|
11678
|
+
return data;
|
|
11679
|
+
});
|
|
11680
|
+
}))
|
|
11364
11681
|
.addCommand(new Command("harvest")
|
|
11365
11682
|
.description("Start (or re-arm) a harvest of a post's engagers into a workspace table you can enroll into a sequence. Engagers drip into the table over many ticks; poll `engagement status` to watch it fill. No messages are sent. Cookieless harvests spend Oxygen credits per scraper page and require --max-credits.")
|
|
11366
11683
|
.requiredOption("--post <social_id_or_url>", "Composite post social_id from `oxygen posts get` (NOT the activity URN), or the public LinkedIn post URL for cookieless.")
|
|
@@ -12411,7 +12728,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
12411
12728
|
program.addCommand(new Command("messages")
|
|
12412
12729
|
.description("Cross-channel message corpus: every individual email + LinkedIn + WhatsApp message as one searchable stream (Postgres full-text search over bodies, keyset paginated by recency), plus reply-rate/campaign analytics. Message-level, unlike inbox (conversation-level triage).")
|
|
12413
12730
|
.addCommand(new Command("query")
|
|
12414
|
-
.description("Query messages across channels newest first, or relevance-ranked when -q is set. --channel all merges email + LinkedIn + WhatsApp (narrow with --channels); a campaign filter (--sequence-id) restricts to email. Filter by direction, account, contact, status, and date range.")
|
|
12731
|
+
.description("Query messages across channels newest first, or relevance-ranked when -q is set. --channel all merges email + LinkedIn + WhatsApp (narrow with --channels); a campaign filter (--sequence-id) restricts to email. Filter by direction, account, contact, status, and date range. Scope: the Messages store (Unibox conversations, every native email since v1.927.0 linked to its campaign; earlier sends without a provider thread id are not). The complete send ledger of a sequence is `sequences events --kind sent`; its counts are `sequences stats`.")
|
|
12415
12732
|
.option("-q, --query <text>", "Full-text search over message bodies (relevance-ranked).")
|
|
12416
12733
|
.option("--channel <channel>", "Channel: all (merged, default), email, linkedin, or whatsapp.")
|
|
12417
12734
|
.option("--channels <list>", "channel=all only: comma-separated channels to include (email,linkedin,whatsapp). Empty = all three.")
|
|
@@ -12763,14 +13080,14 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
12763
13080
|
.option("--email-definition-file <path>", "Path to a JSON file with the email content spec (subjects/bodies/delays/subsequences) compiled to an Instantly campaign on start.")
|
|
12764
13081
|
.option("--max-credits <n>", "Credit cap for the LinkedIn track (also set when starting).")
|
|
12765
13082
|
.option("--max-live-sends <n>", "External-action ceiling for 0-credit email/WhatsApp/CRM-task tracks (positive integer). Required to start any of them live.")
|
|
12766
|
-
.option("--max-emails-per-mailbox-per-day <n>", "Per-mailbox daily email cap (positive integer) applied across the sending pool.")
|
|
13083
|
+
.option("--max-emails-per-mailbox-per-day <n>", "Per-mailbox daily email cap (positive integer) applied across the sending pool; also sets each mailbox's send pace (send window ÷ cap, catching up when a slot is missed). Daily capacity = this cap × sendable mailboxes; `sequences stats` reports it under daily_budget.")
|
|
12767
13084
|
.option("--send-window-file <path>", "Path to a JSON file with the sequence-level email send window: { timezone, days?, start, end, timezone_mode?, recipient_timezone_column? }.")
|
|
12768
13085
|
.option("--max-emails-per-day <n>", "Sequence-wide daily live-send fleet cap (positive integer) across every sender/mailbox.")
|
|
12769
13086
|
.option("--max-new-enrollments-per-day <n>", "Daily drip cap on NEW first-touch leads the planner starts (positive integer).")
|
|
12770
13087
|
.option("--sequence-prioritization <mode>", "Under a tight daily budget, serve 'followups' or 'new_leads' first.")
|
|
12771
13088
|
.option("--esp-matching <mode>", "Native-email ESP matching: 'prefer' (DEFAULT) biases toward a mailbox on the recipient's own provider, falling back to any; 'off' rotates mailboxes freely; 'strict' requires a same-provider mailbox and defers the send when none exists.")
|
|
12772
13089
|
.option("--sender-failover <mode>", "What happens when an enrollment's LinkedIn/WhatsApp sender goes unavailable: 'wait' (default) resumes when the sender recovers; 'rebind' moves UNTOUCHED enrollments (no thread, no pending invite, no lead binding) to the least-loaded healthy sender in the pool after ~5h of confirmed outage.")
|
|
12773
|
-
.option("--email-min-gap-minutes <n>", "Minimum minutes between two live emails from the SAME mailbox for this sequence (Instantly's 'time gap between emails'; integer 0-720, 0 = none). A humanization
|
|
13090
|
+
.option("--email-min-gap-minutes <n>", "Minimum minutes between two live emails from the SAME mailbox for this sequence (Instantly's 'time gap between emails'; integer 0-720, 0 = none). A humanization FLOOR only: Oxygen already spaces each mailbox's sends evenly across its send window (window length ÷ per-mailbox daily cap, e.g. 9h ÷ 15 = 36 min, tightening through the day to catch up on any lost slot), so it only binds when it exceeds the derived spacing — it also floors the late-day catch-up, so 12 keeps every gap ≥12 min. It never bypasses the daily caps or the send window. To send MORE per day, raise --max-emails-per-mailbox-per-day, add mailboxes, or widen the send window.")
|
|
12774
13091
|
.option("--opportunity-value <usd>", "Estimated USD value of one positive-reply opportunity (>= 0). Analytics-only: sequence stats multiply it by the positive-reply count to report pipeline $ (stats.opportunities). Never gates a send or spends a credit.")
|
|
12775
13092
|
.option("--mailboxes <ids>", "Per-sequence sending-account allowlist for native email (Instantly's 'Accounts to use'): comma-separated mailbox ids. Native email sends rotate ONLY over these mailboxes instead of the whole pool. Omit for the whole pool. Note: a narrow allowlist plus --esp-matching strict can starve sends when no allowed same-provider mailbox exists (surfaced as blocked defer reasons in stats).")
|
|
12776
13093
|
.option("--sender-profiles <ids>", "Send from these sender profiles (unified LinkedIn/WhatsApp/inbox identities): comma-separated profile ids. The server resolves each into its senders + inboxes. Live start is blocked if a selected profile lacks an account for a channel the journey uses.")
|
|
@@ -12865,14 +13182,14 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
12865
13182
|
.option("--clear-email", "Remove the email binding from the sequence (draft only).")
|
|
12866
13183
|
.option("--max-credits <n>", "Credit cap for the LinkedIn track (draft only).")
|
|
12867
13184
|
.option("--max-live-sends <n>", "Draft-time external-action ceiling for 0-credit email/WhatsApp/CRM-task tracks (positive integer). After first start, change it through `sequences start`.")
|
|
12868
|
-
.option("--max-emails-per-mailbox-per-day <n>", "Per-mailbox daily email cap (positive integer) applied across the sending pool.")
|
|
13185
|
+
.option("--max-emails-per-mailbox-per-day <n>", "Per-mailbox daily email cap (positive integer) applied across the sending pool; also sets each mailbox's send pace (send window ÷ cap, catching up when a slot is missed). Daily capacity = this cap × sendable mailboxes; `sequences stats` reports it under daily_budget.")
|
|
12869
13186
|
.option("--send-window-file <path>", "Path to a JSON file with the sequence-level email send window: { timezone, days?, start, end, timezone_mode?, recipient_timezone_column? }.")
|
|
12870
13187
|
.option("--max-emails-per-day <n>", "Sequence-wide daily live-send fleet cap (positive integer) across every sender/mailbox.")
|
|
12871
13188
|
.option("--max-new-enrollments-per-day <n>", "Daily drip cap on NEW first-touch leads the planner starts (positive integer).")
|
|
12872
13189
|
.option("--sequence-prioritization <mode>", "Under a tight daily budget, serve 'followups' or 'new_leads' first.")
|
|
12873
13190
|
.option("--esp-matching <mode>", "Native-email ESP matching: 'prefer' (DEFAULT) biases toward a mailbox on the recipient's own provider, falling back to any; 'off' rotates mailboxes freely; 'strict' requires a same-provider mailbox and defers the send when none exists.")
|
|
12874
13191
|
.option("--sender-failover <mode>", "What happens when an enrollment's LinkedIn/WhatsApp sender goes unavailable: 'wait' (default) resumes when the sender recovers; 'rebind' moves UNTOUCHED enrollments (no thread, no pending invite, no lead binding) to the least-loaded healthy sender in the pool after ~5h of confirmed outage.")
|
|
12875
|
-
.option("--email-min-gap-minutes <n>", "Minimum minutes between two live emails from the SAME mailbox for this sequence (Instantly's 'time gap between emails'; integer 0-720, 0 = none). A humanization
|
|
13192
|
+
.option("--email-min-gap-minutes <n>", "Minimum minutes between two live emails from the SAME mailbox for this sequence (Instantly's 'time gap between emails'; integer 0-720, 0 = none). A humanization FLOOR only: Oxygen already spaces each mailbox's sends evenly across its send window (window length ÷ per-mailbox daily cap, e.g. 9h ÷ 15 = 36 min, tightening through the day to catch up on any lost slot), so it only binds when it exceeds the derived spacing — it also floors the late-day catch-up, so 12 keeps every gap ≥12 min. It never bypasses the daily caps or the send window. To send MORE per day, raise --max-emails-per-mailbox-per-day, add mailboxes, or widen the send window.")
|
|
12876
13193
|
.option("--opportunity-value <usd>", "Estimated USD value of one positive-reply opportunity (>= 0). Analytics-only: sequence stats multiply it by the positive-reply count to report pipeline $ (stats.opportunities). Never gates a send or spends a credit.")
|
|
12877
13194
|
.option("--mailboxes <ids>", "Per-sequence sending-account allowlist for native email (Instantly's 'Accounts to use'): comma-separated mailbox ids. Native email sends rotate ONLY over these mailboxes instead of the whole pool. Note: a narrow allowlist plus --esp-matching strict can starve sends when no allowed same-provider mailbox exists (surfaced as blocked defer reasons in stats).")
|
|
12878
13195
|
.option("--sender-profiles <ids>", "Send from these sender profiles (unified LinkedIn/WhatsApp/inbox identities): comma-separated profile ids. The server resolves each into its senders + inboxes. Live start is blocked if a selected profile lacks an account for a channel the journey uses.")
|
|
@@ -13090,7 +13407,9 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
13090
13407
|
.description("List a launched campaign's table-backed contacts with full source variables, durable engagement stage, and factual or assigned sender. Contacts removed from this campaign are hidden; source rows remain intact.")
|
|
13091
13408
|
.argument("<sequence>", "Sequence id or slug.")
|
|
13092
13409
|
.option("--contact-state <state>", "Filter: all, not_contacted, contacted, connected, replied, or positive_reply.", "all")
|
|
13093
|
-
.option("--limit <n>", "Maximum contacts
|
|
13410
|
+
.option("--limit <n>", "Maximum contacts per page (1-500). Page with --cursor (next_cursor from the previous page) or --offset.", "100")
|
|
13411
|
+
.option("--cursor <c>", "Pagination cursor from the previous page's next_cursor.")
|
|
13412
|
+
.option("--offset <n>", "Skip this many contacts before the page (0-based); an absolute alternative to --cursor.")
|
|
13094
13413
|
.option("--json", "Print a JSON envelope.")
|
|
13095
13414
|
.action(async (sequence, options) => {
|
|
13096
13415
|
await handleAsyncAction("sequences contacts", options, () => {
|
|
@@ -13106,6 +13425,20 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
13106
13425
|
contact_state: contactState,
|
|
13107
13426
|
limit: String(limit),
|
|
13108
13427
|
});
|
|
13428
|
+
const cursor = readOption(options.cursor);
|
|
13429
|
+
if (cursor)
|
|
13430
|
+
params.set("cursor", cursor);
|
|
13431
|
+
const offsetRaw = readOption(options.offset);
|
|
13432
|
+
if (offsetRaw !== null) {
|
|
13433
|
+
const offset = Number(offsetRaw);
|
|
13434
|
+
if (!Number.isInteger(offset) || offset < 0) {
|
|
13435
|
+
throw new OxygenError("invalid_offset", "--offset must be a non-negative integer.", {
|
|
13436
|
+
details: { offset: offsetRaw },
|
|
13437
|
+
exitCode: 2,
|
|
13438
|
+
});
|
|
13439
|
+
}
|
|
13440
|
+
params.set("offset", String(offset));
|
|
13441
|
+
}
|
|
13109
13442
|
return requestOxygen(`/api/cli/sequences/${encodeURIComponent(sequence)}/contacts?${params.toString()}`);
|
|
13110
13443
|
});
|
|
13111
13444
|
}))
|
|
@@ -13198,7 +13531,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
13198
13531
|
});
|
|
13199
13532
|
}))
|
|
13200
13533
|
.addCommand(new Command("stats")
|
|
13201
|
-
.description("Show one sequence's persisted launch status and current operational state separately, including reasons, open work, lifetime sent/terminal-failed/deferral counts, and the funnel.")
|
|
13534
|
+
.description("Show one sequence's persisted launch status and current operational state separately, including reasons, open work, lifetime sent/terminal-failed/deferral counts, and the funnel. daily_budget carries today's sends against the caps; operational.reasons carries why work is waiting (outside_send_window, mailbox_spacing, daily_send_cap_reached, ...). For a per-day send series use `sequences analytics --sequence <id> --range <window>`.")
|
|
13202
13535
|
.argument("<sequence>", "Sequence id or slug.")
|
|
13203
13536
|
.option("--json", "Print a JSON envelope.")
|
|
13204
13537
|
.action(async (sequence, options) => {
|
|
@@ -13540,18 +13873,63 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
13540
13873
|
});
|
|
13541
13874
|
}))
|
|
13542
13875
|
.addCommand(new Command("remove")
|
|
13543
|
-
.description("Remove
|
|
13544
|
-
.
|
|
13545
|
-
.option("--
|
|
13546
|
-
.
|
|
13547
|
-
|
|
13548
|
-
|
|
13549
|
-
|
|
13550
|
-
|
|
13551
|
-
|
|
13552
|
-
|
|
13876
|
+
.description("Remove one lead provider id (--lead), or every identity of one merged person (--subject-id), from the org do-not-contact list (re-enable contact). A subject id is stamped on each identifier of an imported contact: `hubspot:contacts:<id>` from `suppressions hubspot sync`, `manual:<uuid>` returned by `suppressions import-person`. Read it as metadata.dnc_subject_id in `suppressions list` (LinkedIn rows only) or in `suppressions addresses|phones|companies --json`, which is where an email- or phone-only contact appears.")
|
|
13877
|
+
.option("--lead <provider_id>", "The lead provider id to un-suppress. Mutually exclusive with --subject-id.")
|
|
13878
|
+
.option("--subject-id <id>", "DNC subject id (metadata.dnc_subject_id). Clears that person's email, phone, LinkedIn, and company suppressions in one transaction.")
|
|
13879
|
+
.option("--json", "Print a JSON envelope.")
|
|
13880
|
+
.action(async (options) => {
|
|
13881
|
+
const subjectId = readOption(options.subjectId);
|
|
13882
|
+
if (!subjectId) {
|
|
13883
|
+
await handleAsyncAction("suppressions remove", options, () => {
|
|
13884
|
+
const lead = readOption(options.lead);
|
|
13885
|
+
if (!lead)
|
|
13886
|
+
throw new Error("Pass --lead <provider_id> or --subject-id <id>.");
|
|
13887
|
+
return requestOxygen(`/api/cli/suppressions?lead_provider_id=${encodeURIComponent(lead)}`, {
|
|
13888
|
+
method: "DELETE",
|
|
13889
|
+
});
|
|
13553
13890
|
});
|
|
13554
|
-
|
|
13891
|
+
return;
|
|
13892
|
+
}
|
|
13893
|
+
try {
|
|
13894
|
+
if (readOption(options.lead)) {
|
|
13895
|
+
throw new Error("Pass either --lead or --subject-id, not both.");
|
|
13896
|
+
}
|
|
13897
|
+
const data = await requestOxygen(`/api/cli/suppressions?subject_id=${encodeURIComponent(subjectId)}`, { method: "DELETE" });
|
|
13898
|
+
if (options.json) {
|
|
13899
|
+
writeJson(success("suppressions remove", data));
|
|
13900
|
+
}
|
|
13901
|
+
else {
|
|
13902
|
+
// Same compact multi-ledger receipt style as `suppressions import`:
|
|
13903
|
+
// the per-channel counts plus up to 10 cleared identifiers (the full
|
|
13904
|
+
// list is always in the --json envelope).
|
|
13905
|
+
const parsed = (data ?? {});
|
|
13906
|
+
const identities = Array.isArray(parsed.removed_identities) ? parsed.removed_identities : [];
|
|
13907
|
+
const removedCount = parsed.removed_count ?? 0;
|
|
13908
|
+
writeJson({
|
|
13909
|
+
subject_id: parsed.subject_id ?? subjectId,
|
|
13910
|
+
removed_count: removedCount,
|
|
13911
|
+
removed_emails: parsed.removed?.email ?? 0,
|
|
13912
|
+
removed_phones: parsed.removed?.phone ?? 0,
|
|
13913
|
+
removed_linkedin: parsed.removed?.linkedin ?? 0,
|
|
13914
|
+
removed_companies: parsed.removed?.company ?? 0,
|
|
13915
|
+
removed_identities: identities.slice(0, 10),
|
|
13916
|
+
// Counted off removed_count, not the echoed array: the API caps
|
|
13917
|
+
// that list too, so --json is NOT a way to see the rest. The
|
|
13918
|
+
// counts above are the exact record of what was cleared.
|
|
13919
|
+
...(removedCount > Math.min(identities.length, 10)
|
|
13920
|
+
? {
|
|
13921
|
+
removed_identities_note: `Showing ${Math.min(identities.length, 10)} of ${removedCount} cleared identifiers; the counts above are exact.`,
|
|
13922
|
+
}
|
|
13923
|
+
: {}),
|
|
13924
|
+
...(parsed.note ? { note: parsed.note } : {}),
|
|
13925
|
+
deep_link: parsed.deepLink,
|
|
13926
|
+
});
|
|
13927
|
+
}
|
|
13928
|
+
writeCreditsReceipt(data);
|
|
13929
|
+
}
|
|
13930
|
+
catch (error) {
|
|
13931
|
+
emitCliFailure("suppressions remove", error);
|
|
13932
|
+
}
|
|
13555
13933
|
}))
|
|
13556
13934
|
.addCommand(new Command("import")
|
|
13557
13935
|
.description("Bulk-import a do-not-contact blocklist from a file (newline / comma / whitespace separated, max 5000). An entry with '@' goes on the per-address email list; a bare domain (e.g. acme.com) blocks email to that whole domain; a LinkedIn profile URL or member id (ACo...) lands on the people do-not-contact list; any other URL is rejected. A domain block is a sharp tool, so --reason is REQUIRED when the file contains any domains; otherwise manual is the default. Idempotent. Consumes 0 credits.")
|
|
@@ -13647,6 +14025,12 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
13647
14025
|
});
|
|
13648
14026
|
}))
|
|
13649
14027
|
.addCommand(new Command("import-person")
|
|
14028
|
+
// Deliberately NOT extended with the subjects[] pointer: this help text
|
|
14029
|
+
// is already 199 chars, and the generated CLI reference cell truncates a
|
|
14030
|
+
// description past 200 to its FIRST sentence — appending anything drops
|
|
14031
|
+
// the "domain is company-wide, never inferred from --email" warning from
|
|
14032
|
+
// the public table. The receipt itself carries `subjects[]`, and
|
|
14033
|
+
// `suppressions remove --help` names where subject ids come from.
|
|
13650
14034
|
.description("Import one person as one DNC subject with any combination of email, LinkedIn, E.164 phone, and an explicitly supplied company domain. The domain is company-wide and is never inferred from --email.")
|
|
13651
14035
|
.option("--name <name>", "Optional contact display name.")
|
|
13652
14036
|
.option("--email <address>", "Email address to suppress.")
|
|
@@ -14147,7 +14531,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
14147
14531
|
program.addCommand(new Command("mailboxes")
|
|
14148
14532
|
.description("Native email sending pool: register/refresh Google/Microsoft mailboxes (including secure local credential-file transfer), pause/disable inboxes, connect EmailGuard monitoring, and inspect/control OXYGEN Warm-up. Fresh managed InboxKit Google/Microsoft/Azure orders activate exact-scope warm-up automatically after provisioning under their approved default-on add-on. Google reuses the managed credential just in time; Microsoft/Azure uses native export. Standalone warm-up plans and credit caps are for BYOK/imported mailboxes, managed opt-outs, or later separate enrollment. OXYGEN Warm-up never owns campaign dispatch. Safe import preflight: run `oxygen mailboxes compatibility --catalog-only --json`, then `oxygen mailboxes import --file <path> --validate-only --json` (add the credential source flags shown by import help when the file contains credentials).")
|
|
14149
14533
|
.addCommand(new Command("list")
|
|
14150
|
-
.description("List the org's sending mailboxes with provider, status, warmup state, source (managed = bought through Oxygen, byok = bring-your-own), worker-owned connectionRepair and warmupRepair state, and a pool overview (including counts by source). mode=automatic means OXYGEN owns the next bounded retry — do not ask for browser consent or disable/re-enable an existing warm-up seat. To see only Google/Microsoft mailboxes that still need OAuth connection, including attempt budgets and manual remedies, use `oxygen mailboxes oauth-health --json`.")
|
|
14534
|
+
.description("List the org's sending mailboxes with provider, status, warmup state, source (managed = bought through Oxygen, byok = bring-your-own), worker-owned connectionRepair and warmupRepair state, and a pool overview (including counts by source). Read each mailbox's warmupTruth for warm-up (state, last_send, pause, dispatch, next_action with the exact command); warmupState is only the stored rail token and reads `error` for a provider-paused seat. mode=automatic means OXYGEN owns the next bounded retry — do not ask for browser consent or disable/re-enable an existing warm-up seat. To see only Google/Microsoft mailboxes that still need OAuth connection, including attempt budgets and manual remedies, use `oxygen mailboxes oauth-health --json`. Read the fleet from `summary` — `summary.by_warmup`, `by_status`, `by_provider`, `by_auth_mode`, `by_transport`, and `by_source` already aggregate every mailbox, so you never need to iterate the `mailboxes` array to count them.")
|
|
14151
14535
|
.option("--status <status>", "Filter by status: active, paused, disabled, or provisioning (ordered, still being set up).")
|
|
14152
14536
|
.option("--tag <tags>", "Comma-separated workspace tags — matches mailboxes carrying ANY of these tags (see `oxygen tags list`).")
|
|
14153
14537
|
.option("--json", "Print a JSON envelope.")
|
|
@@ -14201,7 +14585,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
14201
14585
|
});
|
|
14202
14586
|
}))
|
|
14203
14587
|
.addCommand(new Command("health")
|
|
14204
|
-
.description("Fleet email-health: the sending pool rolled up by external deliverability reputation (healthy/degraded/critical/unknown), per-mailbox scores, connected health providers, and DIRECTIONAL recommendations. Pure read — 0 credits; never pauses a mailbox.")
|
|
14588
|
+
.description("Fleet email-health: the sending pool rolled up by external deliverability reputation (healthy/degraded/critical/unknown), per-mailbox scores, connected health providers, and DIRECTIONAL recommendations — plus OXYGEN's OWN evidence for the last 7 days, which needs no provider connected: the bounce notifications each mailbox received classified as provider rejection / dead address / mailbox full / delay (a Gmail reputation block shows up as provider_rejected), distinct recipients that first hard-bounced, sequence sends OXYGEN logged (source=sequence only), warm-up day, health score and stop cause, the advisory daily cap that warm-up age supports, and the same rollup per sending domain ranked worst-first. Notifications whose body was never stored count as signals.dsn7d.unclassified, which means OXYGEN could not look — not that nothing was wrong. Read health_coverage to see how many mailboxes an external provider has actually scored. Start from the two fleet reads instead of scanning the mailbox array: warmup.byCause (why warm-up stopped, counted once for the whole pool) and signals.capOverRecommendedMailboxes (mailboxes whose configured cap is above the one warm-up supports — per mailbox, compare dailyCap with recommendedDailyCap; the gap is flagged as the cap_exceeds_readiness warning). Every count is scoped, and the scope is stamped into the payload as signals.window (7d) and signals.sendSource (sequence sends only), on the fleet object and on every mailbox: a 0 means nothing was recorded in that window for that send source, NOT that the fleet is un-blocklisted everywhere. Pure read — 0 credits; never pauses a mailbox or changes a cap.")
|
|
14205
14589
|
.option("--json", "Print a JSON envelope.")
|
|
14206
14590
|
.action(async (options) => {
|
|
14207
14591
|
await handleAsyncAction("mailboxes health", options, () => requestOxygen("/api/cli/mailboxes/health"));
|
|
@@ -14870,7 +15254,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
14870
15254
|
});
|
|
14871
15255
|
}))
|
|
14872
15256
|
.addCommand(new Command("status")
|
|
14873
|
-
.description("Read warmup analytics back from the rail each mailbox is enrolled on and
|
|
15257
|
+
.description("Read warmup analytics back from the rail each mailbox is enrolled on and refresh each mailbox's warmupTruth (state, last send, pause reason, dispatch counters, next_action). A read: 0 Oxygen credits, no campaign sends, no change to your configuration; the one write OXYGEN may make is its own breaker pausing a mailbox proven to be dispatching far above its ramp. Targets the whole pool unless --mailboxes is given. For a per-mailbox read without a rail sync, use `oxygen mailboxes get <address> --json`.")
|
|
14874
15258
|
.option("--mailboxes <list>", "Comma-separated mailbox ids or addresses to sync. Omit to sync the whole pool.")
|
|
14875
15259
|
.option("--provider <name>", "Optional machine routing filter. Omit it for OXYGEN Warm-up; pass trulyinbox only to read inboxes still warming on the retired rail.")
|
|
14876
15260
|
.option("--dry-run", "Skip the provider call (mailboxes marked pending).")
|
|
@@ -14912,9 +15296,9 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
14912
15296
|
}));
|
|
14913
15297
|
})));
|
|
14914
15298
|
program.addCommand(new Command("deliverability")
|
|
14915
|
-
.description("External email deliverability: fleet reputation health and DIRECTIONAL inbox-placement (spam) tests via EmailGuard, Zapmail, or an explicitly configured SendKit dev canary.
|
|
15299
|
+
.description("External email deliverability: fleet reputation health and DIRECTIONAL inbox-placement (spam) tests via EmailGuard, Zapmail, or an explicitly configured SendKit dev canary. Start with `placement-test list` and `placement-test get` to inspect existing results for 0 credits without creating or sending a test. For continuous EmailGuard account monitoring use `oxygen mailboxes emailguard connect`. Placement tests are approval-gated paid runs (managed bills Oxygen credits; BYOK = 0 Oxygen credits — Zapmail BYOK bills your Zapmail wallet ~$2/test). SendKit is never auto-selected or generally available: it is a 0-credit, one-test canary for the configured eligible connected sender only.")
|
|
14916
15300
|
.addCommand(new Command("placement-test")
|
|
14917
|
-
.description("
|
|
15301
|
+
.description("Inspect saved directional inbox-placement results with list/get (0 credits; no test email is sent). New tests use preview and approval through run/send.")
|
|
14918
15302
|
.addCommand(new Command("run")
|
|
14919
15303
|
.description("Create a placement test for one sending mailbox. Without --approved this returns a cost PREVIEW. EmailGuard creation returns exact seeds + phrase but sends nothing; next run `placement-test send <id>` to preview and approve that external email. Zapmail owns its probe delivery and completes async (2-24h). SendKit requires explicit --provider sendkit plus --subject and --body, and is available only for the configured dev canary with an eligible connected sender. Its approval creates AND sends one probe with no separate send step: preview first, then re-run the same mailbox, provider, subject, and body with --approved --max-credits 0 --plan <plan_hash>.")
|
|
14920
15304
|
.argument("<mailbox>", "Sending mailbox address to test (e.g. ada@send.acme.com).")
|
|
@@ -14969,7 +15353,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
14969
15353
|
}));
|
|
14970
15354
|
}))
|
|
14971
15355
|
.addCommand(new Command("list")
|
|
14972
|
-
.description("
|
|
15356
|
+
.description("Inspect recent directional inbox-placement tests, their status, results, and credits used. The returned web link opens Accounts; use get for an exact result link. Costs 0 credits and never creates or sends a test; active tests may refresh their observations.")
|
|
14973
15357
|
.option("--limit <n>", "Max rows (default 50, cap 200).")
|
|
14974
15358
|
.option("--json", "Print a JSON envelope.")
|
|
14975
15359
|
.action(async (options) => {
|
|
@@ -14979,7 +15363,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
14979
15363
|
});
|
|
14980
15364
|
}))
|
|
14981
15365
|
.addCommand(new Command("get")
|
|
14982
|
-
.description("
|
|
15366
|
+
.description("Inspect one saved directional inbox-placement test by id: status, seed counts, per-provider results, and a web result link. Costs 0 credits and never creates or sends a test; active tests may refresh their observations. Zapmail results land async (2-24h); keep polling while status is awaiting_results.")
|
|
14983
15367
|
.argument("<id>", "Placement test id.")
|
|
14984
15368
|
.option("--json", "Print a JSON envelope.")
|
|
14985
15369
|
.action(async (id, options) => {
|
|
@@ -16185,7 +16569,7 @@ Full trigger schema: oxygen workflows schema --subject trigger --json
|
|
|
16185
16569
|
}
|
|
16186
16570
|
}))
|
|
16187
16571
|
.addCommand(new Command("mcp")
|
|
16188
|
-
.description("
|
|
16572
|
+
.description("Expose hosted Workflows as dynamic MCP tools (oxygen_workflow_<slug>). For reusable table Functions, use oxygen functions.")
|
|
16189
16573
|
.addCommand(new Command("enable")
|
|
16190
16574
|
.description("Publish an active workflow as a callable MCP tool so any MCP client can run it. The published tool appears to MCP clients as oxygen_workflow_<slug>.")
|
|
16191
16575
|
.argument("<workflow>", "Workflow id, slug, or name.")
|
|
@@ -16286,6 +16670,9 @@ Full trigger schema: oxygen workflows schema --subject trigger --json
|
|
|
16286
16670
|
await handleAsyncAction("skills install", options, () => installAgentSkills(options));
|
|
16287
16671
|
}));
|
|
16288
16672
|
applyOxygenHelp(program, binaryName);
|
|
16673
|
+
registerVisualCommands(program, handleAsyncAction);
|
|
16674
|
+
registerFunctionsCommands(program, handleAsyncAction);
|
|
16675
|
+
registerUgcCommands(program, handleAsyncAction);
|
|
16289
16676
|
return program;
|
|
16290
16677
|
}
|
|
16291
16678
|
/**
|
|
@@ -18321,11 +18708,15 @@ function readFeedBindBody(table, options) {
|
|
|
18321
18708
|
...(options.approved ? { approved: true } : {}),
|
|
18322
18709
|
};
|
|
18323
18710
|
}
|
|
18324
|
-
function readSignalsSearchPlanBody(options) {
|
|
18711
|
+
function readSignalsSearchPlanBody(options, promptArg) {
|
|
18712
|
+
// The prompt may arrive as --prompt or as the positional argument; the flag wins.
|
|
18713
|
+
const promptSource = options.prompt ?? promptArg;
|
|
18325
18714
|
const targetCount = readPositiveInt(options.targetCount);
|
|
18326
18715
|
const filters = readSignalSearchFilters(options);
|
|
18327
18716
|
return {
|
|
18328
|
-
|
|
18717
|
+
// Omit when neither was given so the request still reaches the server, which
|
|
18718
|
+
// answers with a clean `prompt is required.` instead of crashing readFileIfPresent.
|
|
18719
|
+
...(promptSource !== undefined ? { prompt: readFileIfPresent(promptSource) } : {}),
|
|
18329
18720
|
family: options.family,
|
|
18330
18721
|
...(readOption(options.scope) ? { scope: readOption(options.scope) } : {}),
|
|
18331
18722
|
...(targetCount !== undefined ? { target_count: targetCount } : {}),
|
|
@@ -18333,8 +18724,10 @@ function readSignalsSearchPlanBody(options) {
|
|
|
18333
18724
|
...(options.estimate ? { estimate: true } : {}),
|
|
18334
18725
|
};
|
|
18335
18726
|
}
|
|
18336
|
-
function readSignalsSearchRunBody(options) {
|
|
18337
|
-
|
|
18727
|
+
function readSignalsSearchRunBody(options, promptArg) {
|
|
18728
|
+
// The prompt may arrive as --prompt or as the positional argument; the flag wins.
|
|
18729
|
+
const promptSource = options.prompt ?? promptArg;
|
|
18730
|
+
const prompt = promptSource ? readFileIfPresent(promptSource) : null;
|
|
18338
18731
|
const plan = options.planJson ? readSearchPlanJson(options.planJson) : null;
|
|
18339
18732
|
if (!prompt && !plan) {
|
|
18340
18733
|
throw new OxygenError("invalid_request", "Pass --prompt or --plan-json.", { exitCode: 1 });
|
|
@@ -18406,11 +18799,15 @@ function readSignalSearchFilters(options) {
|
|
|
18406
18799
|
}
|
|
18407
18800
|
return Object.keys(filters).length > 0 ? filters : null;
|
|
18408
18801
|
}
|
|
18409
|
-
function readCompaniesSearchPlanBody(options) {
|
|
18802
|
+
function readCompaniesSearchPlanBody(options, promptArg) {
|
|
18803
|
+
// The prompt may arrive as --prompt or as the positional argument; the flag wins.
|
|
18804
|
+
const promptSource = options.prompt ?? promptArg;
|
|
18410
18805
|
const targetCount = readPositiveInt(options.targetCount);
|
|
18411
18806
|
const filters = readCompanySearchFilters(options);
|
|
18412
18807
|
return {
|
|
18413
|
-
|
|
18808
|
+
// Omit when neither was given so the request still reaches the server, which
|
|
18809
|
+
// answers with a clean `prompt is required.` instead of crashing readFileIfPresent.
|
|
18810
|
+
...(promptSource !== undefined ? { prompt: readFileIfPresent(promptSource) } : {}),
|
|
18414
18811
|
...(targetCount !== undefined ? { target_count: targetCount } : {}),
|
|
18415
18812
|
...(options.sourceIntent ? { source_intent: options.sourceIntent } : {}),
|
|
18416
18813
|
...(filters ? { filters } : {}),
|
|
@@ -18418,8 +18815,10 @@ function readCompaniesSearchPlanBody(options) {
|
|
|
18418
18815
|
...(options.materializePreview ? { materialize_preview: true } : {}),
|
|
18419
18816
|
};
|
|
18420
18817
|
}
|
|
18421
|
-
function readCompaniesSearchRunBody(options) {
|
|
18422
|
-
|
|
18818
|
+
function readCompaniesSearchRunBody(options, promptArg) {
|
|
18819
|
+
// The prompt may arrive as --prompt or as the positional argument; the flag wins.
|
|
18820
|
+
const promptSource = options.prompt ?? promptArg;
|
|
18821
|
+
const prompt = promptSource ? readFileIfPresent(promptSource) : null;
|
|
18423
18822
|
const plan = options.planJson ? readSearchPlanJson(options.planJson) : null;
|
|
18424
18823
|
if (!prompt && !plan) {
|
|
18425
18824
|
throw new OxygenError("invalid_request", "Pass --prompt or --plan-json.", { exitCode: 1 });
|
|
@@ -23463,7 +23862,10 @@ function buildContextAssetUpsertBody(options) {
|
|
|
23463
23862
|
function buildKnowledgePageUpsertBody(options) {
|
|
23464
23863
|
const tags = readCsvOption(options.tags);
|
|
23465
23864
|
const expectedRevision = readPositiveInt(options.expectedRevision);
|
|
23865
|
+
const folder = readOption(options.folder);
|
|
23466
23866
|
return {
|
|
23867
|
+
...(folder ? { folderId: folder === "root" ? null : folder } : {}),
|
|
23868
|
+
...(options.createOnly ? { createOnly: true } : {}),
|
|
23467
23869
|
...(readOption(options.slug) ? { slug: readOption(options.slug) } : {}),
|
|
23468
23870
|
...(readOption(options.id) ? { id: readOption(options.id) } : {}),
|
|
23469
23871
|
...(readOption(options.type) ? { type: readOption(options.type) } : {}),
|
|
@@ -24206,6 +24608,27 @@ function readPositiveNumber(value) {
|
|
|
24206
24608
|
}
|
|
24207
24609
|
return parsed;
|
|
24208
24610
|
}
|
|
24611
|
+
/**
|
|
24612
|
+
* A spend ceiling where ZERO IS VALID and meaningful: `find company
|
|
24613
|
+
* --max-credits 0` asks for the lanes that cost nothing (crustdata's identify
|
|
24614
|
+
* lane resolves domain↔LinkedIn for 0 credits) and skips every priced one.
|
|
24615
|
+
* readPositiveNumber rejected it client-side, so the free lanes were
|
|
24616
|
+
* unreachable from the CLI at all (Plain T-111). Fractional ceilings stay legal
|
|
24617
|
+
* — lane estimates are fractional — so this is not the whole-number reader.
|
|
24618
|
+
*/
|
|
24619
|
+
function readCreditCeilingOrZero(value) {
|
|
24620
|
+
const trimmed = value?.trim();
|
|
24621
|
+
if (!trimmed)
|
|
24622
|
+
return undefined;
|
|
24623
|
+
const parsed = Number(trimmed);
|
|
24624
|
+
if (!Number.isFinite(parsed) || parsed < 0) {
|
|
24625
|
+
throw new OxygenError("invalid_number", "Expected a number of 0 or more.", {
|
|
24626
|
+
details: { value },
|
|
24627
|
+
exitCode: 1,
|
|
24628
|
+
});
|
|
24629
|
+
}
|
|
24630
|
+
return parsed;
|
|
24631
|
+
}
|
|
24209
24632
|
/**
|
|
24210
24633
|
* A whole count (days, rows, windows). Its positive-number sibling accepts 2.5,
|
|
24211
24634
|
* which for a scan bound is always a typo — rejected here so it costs no round trip,
|