@oxygen-agent/cli 1.893.0 → 1.906.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/command-manifest.js +8 -3
- package/dist/index.js +199 -36
- package/node_modules/@oxygen/shared/dist/capability-discovery.js +19 -1
- package/node_modules/@oxygen/shared/dist/copilot-plan.d.ts +137 -0
- package/node_modules/@oxygen/shared/dist/copilot-plan.js +435 -0
- package/node_modules/@oxygen/shared/dist/dnc-identities.d.ts +10 -0
- package/node_modules/@oxygen/shared/dist/dnc-identities.js +23 -0
- package/node_modules/@oxygen/shared/dist/egress-transport-readiness.d.ts +60 -0
- package/node_modules/@oxygen/shared/dist/egress-transport-readiness.js +67 -0
- package/node_modules/@oxygen/shared/dist/index.d.ts +3 -0
- package/node_modules/@oxygen/shared/dist/index.js +3 -0
- package/node_modules/@oxygen/shared/dist/langfuse.d.ts +57 -5
- package/node_modules/@oxygen/shared/dist/langfuse.js +243 -42
- package/node_modules/@oxygen/shared/dist/product-briefing-rules.d.ts +58 -0
- package/node_modules/@oxygen/shared/dist/product-briefing-rules.js +291 -0
- package/node_modules/@oxygen/shared/dist/product-doctrine.d.ts +11 -0
- package/node_modules/@oxygen/shared/dist/product-doctrine.js +70 -0
- package/node_modules/@oxygen/shared/dist/sending-seats.d.ts +31 -8
- package/node_modules/@oxygen/shared/dist/sending-seats.js +19 -15
- package/node_modules/@oxygen/shared/dist/sequences.d.ts +18 -11
- package/node_modules/@oxygen/shared/dist/sequences.js +47 -13
- package/node_modules/@oxygen/shared/dist/version.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/version.js +1 -1
- package/node_modules/@oxygen/shared/package.json +5 -0
- package/package.json +4 -2
package/README.md
CHANGED
package/dist/command-manifest.js
CHANGED
|
@@ -76,6 +76,10 @@ const MUTATING_VERBS = new Set([
|
|
|
76
76
|
// mode, but a small number of destructive local operations are explicitly
|
|
77
77
|
// zero-credit. Keep those exceptions exact so discovery never invents spend.
|
|
78
78
|
const ZERO_CREDIT_APPROVAL_COMMANDS = new Set([
|
|
79
|
+
// Starting an attended session itself is free. This legacy compatibility
|
|
80
|
+
// flag neither authorizes nor caps later inference; only `copilot send` can
|
|
81
|
+
// trigger the billed model loop.
|
|
82
|
+
"copilot start",
|
|
79
83
|
"mailboxes delete",
|
|
80
84
|
// This approval only reopens exact fingerprint-bound tenant rows. It never
|
|
81
85
|
// calls the provider, dispatches an action, or touches the credit ledger.
|
|
@@ -255,10 +259,11 @@ function toManifestEntry(command, path) {
|
|
|
255
259
|
spends_credits: !ZERO_CREDIT_APPROVAL_COMMANDS.has(commandName) &&
|
|
256
260
|
(flagStrings.some((flags) => flags.includes("--approved") ||
|
|
257
261
|
flags.includes("--max-credits") ||
|
|
258
|
-
// Copilot
|
|
262
|
+
// Legacy Copilot compatibility plus any future explicit budget flag.
|
|
263
|
+
// `copilot start` is excluded above because session creation is free.
|
|
259
264
|
flags.includes("--budget-credits")) ||
|
|
260
|
-
// `copilot send` bills managed inference (5x actual model cost)
|
|
261
|
-
//
|
|
265
|
+
// `copilot send` bills managed inference (5x actual model cost), and
|
|
266
|
+
// `copilot approve`
|
|
262
267
|
// dispatches the gated paid/external capability and requeues the paused
|
|
263
268
|
// turn (waking the worker for more 5x-billed inference) — no per-command
|
|
264
269
|
// cap flag exists for either, so the flag heuristic alone would misreport
|
package/dist/index.js
CHANGED
|
@@ -9,7 +9,7 @@ import { fileURLToPath, pathToFileURL } from "node:url";
|
|
|
9
9
|
import { Command, CommanderError, Option } from "commander";
|
|
10
10
|
import { applyOxygenHelp } from "./help.js";
|
|
11
11
|
import { buildCommandManifest, getCommandManifestEntry, searchCommandManifest, suggestCommandNames, } from "./command-manifest.js";
|
|
12
|
-
import { AGENCY_DIRECTORY_REGIONS, AGENCY_DIRECTORY_SERVICES, COLLAB_GATE_KINDS, COLLAB_GATE_PROSE, COLLAB_SUBJECT_KINDS, COLLAB_SUBJECT_KINDS_PROSE, COLLAB_SUBJECT_LABELS, COLLAB_SUBJECT_PROSE, GATE_KIND_SUBJECTS, describeWorkflowStatusChange, formatCellForDisplay, formatPublicBudgetScopes, SUBJECT_PATH_FORMS_PROSE, formatSubjectPath, exitCodeForOxygenError, parseSubjectPath, parseSubjectRef, parseWorkflowStatusChange, isVersionGreater, isVersionLess, KNOWLEDGE_BOOTSTRAP_MAX_CREDITS, MAX_MCP_TOOL_NAME_LENGTH, OXYGEN_CAPABILITY_ROUTES, OXYGEN_VERSION, OxygenError, getCapabilityRouteMatch, inferUserCapabilityRoute, parseKnowledgePageMarkdown, PLAN_LIMITS, serializeCapabilityRoute, sleep, success, TABLE_IMPORT_ROW_LIMIT, TAG_KINDS_PROSE, toFailure, workflowMcpToolName, } from "@oxygen/shared";
|
|
12
|
+
import { AGENCY_DIRECTORY_REGIONS, AGENCY_DIRECTORY_SERVICES, COLLAB_GATE_KINDS, COLLAB_GATE_PROSE, COLLAB_SUBJECT_KINDS, COLLAB_SUBJECT_KINDS_PROSE, COLLAB_SUBJECT_LABELS, COLLAB_SUBJECT_PROSE, GATE_KIND_SUBJECTS, describeWorkflowStatusChange, formatCellForDisplay, formatPublicBudgetScopes, SUBJECT_PATH_FORMS_PROSE, formatSubjectPath, exitCodeForOxygenError, parseSubjectPath, parseSubjectRef, parseWorkflowStatusChange, isVersionGreater, isVersionLess, KNOWLEDGE_BOOTSTRAP_MAX_CREDITS, MAX_MCP_TOOL_NAME_LENGTH, normalizeCopilotPlanStepStatus, OXYGEN_CAPABILITY_ROUTES, OXYGEN_VERSION, OxygenError, getCapabilityRouteMatch, inferUserCapabilityRoute, parseKnowledgePageMarkdown, PLAN_LIMITS, serializeCapabilityRoute, sleep, success, TABLE_IMPORT_ROW_LIMIT, TAG_KINDS_PROSE, toFailure, workflowMcpToolName, } from "@oxygen/shared";
|
|
13
13
|
import { TAG_COLORS } from "@oxygen/shared/select-options";
|
|
14
14
|
import { inferImportColumnLabels, inferRowsFileFormat, normalizeImportColumnKey, normalizeRowsForNewTable, normalizeRowsFormat, parseRowsFileBuffer, parseXlsxWorkbookBuffer, } from "@oxygen/shared/file-import";
|
|
15
15
|
import { MAILBOX_IMPORT_FILE_MAX_BYTES as SHARED_MAILBOX_IMPORT_FILE_MAX_BYTES, MAILBOX_IMPORT_ROW_LIMIT as SHARED_MAILBOX_IMPORT_ROW_LIMIT, normalizeMailboxImportFile as normalizeSharedMailboxImportFile, normalizeMailboxImportVendor as normalizeSharedMailboxImportVendor, normalizeMailboxWorkbookRows, parseMailboxImportText, summarizeMailboxImportValidation as summarizeSharedMailboxImportValidation, } from "@oxygen/shared/mailbox-import";
|
|
@@ -1466,6 +1466,7 @@ function buildCrmSearchBody(query, options) {
|
|
|
1466
1466
|
query,
|
|
1467
1467
|
...(objects.length > 0 ? { objects } : {}),
|
|
1468
1468
|
...(limit !== undefined ? { limit } : {}),
|
|
1469
|
+
...(options.suggest === true ? { mode: "suggest" } : {}),
|
|
1469
1470
|
};
|
|
1470
1471
|
}
|
|
1471
1472
|
function buildCrmMergeBody(object, survivorRowId, loserRowId, options) {
|
|
@@ -4332,6 +4333,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
4332
4333
|
.option("--objects <objects>", "Comma-separated CRM object slugs to search. Defaults to all configured objects.")
|
|
4333
4334
|
.option("--object <object>", "Alias for --objects; every sibling crm command spells it singular.")
|
|
4334
4335
|
.option("--limit <limit>", "Maximum records to return.")
|
|
4336
|
+
.option("--suggest", "Typeahead mode: ranked name/email/domain matches on a partial string, returning a compact suggestion per record instead of the full row. This is what the app's CRM search bar calls; use it when you have a fragment, and the default when you have an exact identity.")
|
|
4335
4337
|
.option("--json", "Print a JSON envelope.")
|
|
4336
4338
|
.action(async (query, options) => {
|
|
4337
4339
|
await handleAsyncAction("crm search", options, () => requestOxygen("/api/cli/crm/records/search", {
|
|
@@ -8789,23 +8791,28 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
8789
8791
|
}));
|
|
8790
8792
|
}));
|
|
8791
8793
|
program.addCommand(new Command("egress")
|
|
8792
|
-
.description("
|
|
8794
|
+
.description("Native email egress: status, shared/dedicated IP inventory, the dedicated-IP add-on, policy assignments, and a fail-closed retired rotation shim.")
|
|
8793
8795
|
.addCommand(new Command("status")
|
|
8794
|
-
.description("Show this workspace's actual
|
|
8796
|
+
.description("Show this workspace's actual native egress mode, active IP, health counts, dedicated add-on, and one bounded page of the inspectable mailbox policy ledger. Read-only, 0 credits.")
|
|
8797
|
+
.option("--assignment-offset <number>", "Continue the policy ledger from open_assignments_next_offset.")
|
|
8795
8798
|
.option("--json", "Print a JSON envelope.")
|
|
8796
8799
|
.action(async (options) => {
|
|
8797
|
-
|
|
8800
|
+
const assignmentOffset = readNonNegativeInt(options.assignmentOffset);
|
|
8801
|
+
const query = assignmentOffset === undefined
|
|
8802
|
+
? ""
|
|
8803
|
+
: `?assignment_offset=${encodeURIComponent(String(assignmentOffset))}`;
|
|
8804
|
+
await handleAsyncAction("egress status", options, () => requestOxygen(`/api/cli/egress${query}`));
|
|
8798
8805
|
}))
|
|
8799
8806
|
.addCommand(new Command("ips")
|
|
8800
|
-
.description("List the shared
|
|
8807
|
+
.description("List the shared native worker IP and this workspace's dedicated IP history. Read-only, 0 credits.")
|
|
8801
8808
|
.option("--json", "Print a JSON envelope.")
|
|
8802
8809
|
.action(async (options) => {
|
|
8803
8810
|
await handleAsyncAction("egress ips", options, () => requestOxygen("/api/cli/egress?view=ips"));
|
|
8804
8811
|
}))
|
|
8805
8812
|
.addCommand(new Command("dedicated")
|
|
8806
|
-
.description("Dedicated
|
|
8813
|
+
.description("Dedicated native egress add-on (25,000 credits per 30-day period): workspace tenant isolation and account safety, not a deliverability or inbox-placement lever. Preview the quote, order with `request --approved`, cancel with `cancel --approve`, or check status.")
|
|
8807
8814
|
.addCommand(new Command("request")
|
|
8808
|
-
.description("Preview the dedicated
|
|
8815
|
+
.description("Preview the dedicated native egress add-on (25,000 credits per 30-day period, $25 face value). With --approved, ORDER it: OXYGEN provisions one isolated send drain plus one static egress IP for the workspace, debits the first period immediately, and debits the next 30 days later — not on the 1st. Every current inbox is assigned to that IP, every future inbox joins it automatically, and after the guarded handoff every workspace send routes through the dedicated drain; the Google/Microsoft relay IP recipients see does not change. This is workspace tenant isolation and account safety, not a deliverability or inbox-placement lever. Without --approved nothing is ordered or charged. While native provisioning is unavailable, ordering fails closed and the preview says so.")
|
|
8809
8816
|
// --approved (not --approve): the credit-spending approval flag,
|
|
8810
8817
|
// which is also what derives spends_credits in the self-index.
|
|
8811
8818
|
.option("--approved", "Execute the order (a real recurring credit charge). Omit for a no-side-effect preview.")
|
|
@@ -8813,12 +8820,15 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
8813
8820
|
// derives spends_credits from --approved/--max-credits/--budget-credits
|
|
8814
8821
|
// appearing in a command's flag strings, so an approval-shaped name
|
|
8815
8822
|
// here would mislabel every command that carries it.
|
|
8816
|
-
.option("--move-existing", "Deprecated compatibility
|
|
8823
|
+
.option("--move-existing", "Deprecated compatibility flag. Existing inboxes now move to the dedicated IP automatically, so old scripts may keep passing this safely.")
|
|
8817
8824
|
.option("--country <code>", "ISO 3166-1 alpha-2 country the dedicated IP should sit in (e.g. US, DE). Match it to where your mailboxes' owners plausibly sign in from — the IP's job is making the sign-in look ordinary, and an account that suddenly authenticates from another country is what gets it challenged. Omitted uses the default region. An unserviceable or out-of-stock country fails before anything is ordered or charged.")
|
|
8825
|
+
.option("--idempotency-key <key>", "Stable key for safely retrying the same order after a timeout. Reuse the key shown in a timeout error; use a new key only for an intentionally new purchase.")
|
|
8818
8826
|
.option("--json", "Print a JSON envelope.")
|
|
8819
8827
|
.action(async (options) => {
|
|
8828
|
+
const idempotencyKey = readOption(options.idempotencyKey);
|
|
8820
8829
|
await handleAsyncAction("egress dedicated request", options, () => requestOxygen("/api/cli/egress/dedicated", {
|
|
8821
8830
|
method: "POST",
|
|
8831
|
+
...(idempotencyKey ? { idempotencyKey } : {}),
|
|
8822
8832
|
body: {
|
|
8823
8833
|
...(options.approved ? { approve: true } : {}),
|
|
8824
8834
|
...(options.moveExisting ? { move_existing: true } : {}),
|
|
@@ -8827,7 +8837,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
8827
8837
|
}));
|
|
8828
8838
|
}))
|
|
8829
8839
|
.addCommand(new Command("cancel")
|
|
8830
|
-
.description("Cancel the dedicated sending-IP add-on. Approval-gated: without --approve it previews. Cancellation takes effect at the END of the 30-day period already paid for: the IP keeps sending until then, and only at period end does it retire, the workspace returns to the shared
|
|
8840
|
+
.description("Cancel the dedicated sending-IP add-on. Approval-gated: without --approve it previews. Cancellation takes effect at the END of the 30-day period already paid for: the IP keeps sending until then, and only at period end does it retire, the workspace returns to the shared native worker, and credit billing stops. No refund, no early cutoff.")
|
|
8831
8841
|
.option("--approve", "Execute the cancellation. Omit for a no-side-effect preview.")
|
|
8832
8842
|
.option("--json", "Print a JSON envelope.")
|
|
8833
8843
|
.action(async (options) => {
|
|
@@ -8843,10 +8853,10 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
8843
8853
|
await handleAsyncAction("egress dedicated status", options, () => requestOxygen("/api/cli/egress/dedicated"));
|
|
8844
8854
|
})))
|
|
8845
8855
|
.addCommand(new Command("rotate")
|
|
8846
|
-
.description("
|
|
8856
|
+
.description("RETIRED compatibility command. Native egress is workspace-scoped, so OXYGEN cannot rotate one mailbox without violating the workspace's shared-or-one-dedicated routing contract. The API always fails closed with no provider call or change.")
|
|
8847
8857
|
.requiredOption("--mailbox <id>", "Mailbox id to rotate.")
|
|
8848
|
-
.option("--vendor <vendor>", "
|
|
8849
|
-
.option("--approve", "
|
|
8858
|
+
.option("--vendor <vendor>", "Retired compatibility input; never used for a provider call.")
|
|
8859
|
+
.option("--approve", "Retired compatibility flag; no rotation is executed.")
|
|
8850
8860
|
.option("--json", "Print a JSON envelope.")
|
|
8851
8861
|
.action(async (options) => {
|
|
8852
8862
|
await handleAsyncAction("egress rotate", options, () => requestOxygen("/api/cli/egress/rotate", {
|
|
@@ -8859,7 +8869,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
8859
8869
|
}));
|
|
8860
8870
|
}))
|
|
8861
8871
|
.addCommand(new Command("register")
|
|
8862
|
-
.description("STAFF: register
|
|
8872
|
+
.description("STAFF RECOVERY: register legacy/manual logical policy inventory for this environment. This does not configure native send transport; dedicated native drains are created only by `egress dedicated request`. Secrets never pass here — --credential-ref names the Doppler family EGRESS_CRED_<REF>_USERNAME/_PASSWORD, set separately in Doppler.")
|
|
8863
8873
|
.requiredOption("--host <host>", "Proxy hostname or IP — bare, no scheme/port/credentials.")
|
|
8864
8874
|
.requiredOption("--port <port>", "Proxy port (1-65535).")
|
|
8865
8875
|
.requiredOption("--credential-ref <ref>", "Doppler credential-family name (A-Z, 0-9, _).")
|
|
@@ -8885,9 +8895,9 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
8885
8895
|
}));
|
|
8886
8896
|
}))
|
|
8887
8897
|
.addCommand(new Command("assign")
|
|
8888
|
-
.description("
|
|
8898
|
+
.description("Converge mailbox policy assignments: managed/shared by default, or the workspace's dedicated IP for every current and future inbox once that add-on is active. Existing pins stay stable within their tier; --ip cannot override an active dedicated tier. Admin/owner role; no spend, 0 credits.")
|
|
8889
8899
|
.option("--mailboxes <list>", "Comma-separated mailbox ids or addresses to assign.")
|
|
8890
|
-
.option("--all", "
|
|
8900
|
+
.option("--all", "Converge every sending mailbox to the workspace's current shared or dedicated tier.")
|
|
8891
8901
|
.option("--ip <egressIpId>", "Pin the targeted mailboxes to this egress IP id (must be active and org-visible).")
|
|
8892
8902
|
.option("--json", "Print a JSON envelope.")
|
|
8893
8903
|
.action(async (options) => {
|
|
@@ -9571,13 +9581,13 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
9571
9581
|
}));
|
|
9572
9582
|
program
|
|
9573
9583
|
.command("copilot")
|
|
9574
|
-
.description("Workspace Copilot: an
|
|
9584
|
+
.description("Workspace Copilot: an attended workspace agent you drive turn by turn from the terminal. Start a session, send it messages and watch the reply stream, approve the actions it proposes, then cancel when done. Inference bills credits at 5x the actual model cost and is bounded by a 30,000-credit internal platform safety backstop; paid or external actions keep their own approval gates. Web: /copilot.")
|
|
9575
9585
|
.addCommand(new Command("start")
|
|
9576
|
-
.description("Start
|
|
9577
|
-
.
|
|
9586
|
+
.description("Start an attended Workspace Copilot session. Starting is free; the next send starts inference. No inference budget is required; inference bills at 5x actual model cost under a 30,000-credit internal platform safety backstop. Legacy budget flags remain accepted for older clients but do not authorize or cap spend. Paid or external actions require separate approval.")
|
|
9587
|
+
.option("--budget-credits <number>", "Legacy compatibility only: accepted and clamped, but does not authorize or cap attended Copilot inference.")
|
|
9578
9588
|
.option("--tier <low|medium|high>", "Reasoning effort tier for the session's model.")
|
|
9579
9589
|
.option("--model <id>", "Pin a specific model id instead of the journey/tier default.")
|
|
9580
|
-
.option("--per-turn-ceiling <number>", "
|
|
9590
|
+
.option("--per-turn-ceiling <number>", "Legacy compatibility only: accepted and clamped, but does not cap an attended turn.")
|
|
9581
9591
|
.option("--journey <slug>", "Optional journey slug to seed the session's goal and context.")
|
|
9582
9592
|
.option("--title <text>", "Optional human title for the session.")
|
|
9583
9593
|
.option("--auto-approve", "Start with auto-approve ON for paid, workspace-internal actions (cards are still created and decided automatically within each action's credit cap). External sends, enrollments, publishes, DNS, and external CRM pushes always stay human-gated.")
|
|
@@ -9586,15 +9596,20 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
9586
9596
|
.action(async (options) => {
|
|
9587
9597
|
await handleAsyncAction("copilot start", options, () => {
|
|
9588
9598
|
const budgetCredits = readPositiveNumber(options.budgetCredits);
|
|
9589
|
-
if (budgetCredits === undefined) {
|
|
9590
|
-
throw new OxygenError("invalid_budget", "
|
|
9599
|
+
if (options.budgetCredits !== undefined && budgetCredits === undefined) {
|
|
9600
|
+
throw new OxygenError("invalid_budget", "When supplied for legacy compatibility, --budget-credits must be a positive number.", { exitCode: 1 });
|
|
9591
9601
|
}
|
|
9592
9602
|
const perTurnCeiling = readPositiveNumber(options.perTurnCeiling);
|
|
9603
|
+
if (options.perTurnCeiling !== undefined && perTurnCeiling === undefined) {
|
|
9604
|
+
throw new OxygenError("invalid_budget", "When supplied for legacy compatibility, --per-turn-ceiling must be a positive number.", { exitCode: 1 });
|
|
9605
|
+
}
|
|
9593
9606
|
const tier = readOption(options.tier);
|
|
9594
9607
|
const model = readOption(options.model);
|
|
9595
9608
|
const journey = readOption(options.journey);
|
|
9596
9609
|
const title = readOption(options.title);
|
|
9597
|
-
const body = {
|
|
9610
|
+
const body = {};
|
|
9611
|
+
if (budgetCredits !== undefined)
|
|
9612
|
+
body.budget_credits = budgetCredits;
|
|
9598
9613
|
if (tier)
|
|
9599
9614
|
body.tier = tier;
|
|
9600
9615
|
if (model)
|
|
@@ -9615,7 +9630,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
9615
9630
|
});
|
|
9616
9631
|
}))
|
|
9617
9632
|
.addCommand(new Command("list")
|
|
9618
|
-
.description("List your Workspace Copilot sessions
|
|
9633
|
+
.description("List your Workspace Copilot sessions and their current status.")
|
|
9619
9634
|
.option("--json", "Print a JSON envelope.")
|
|
9620
9635
|
.action(async (options) => {
|
|
9621
9636
|
await handleAsyncAction("copilot list", options, () => requestOxygen("/api/cli/copilot/sessions"));
|
|
@@ -9638,6 +9653,24 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
9638
9653
|
const suffix = query.toString() ? `?${query.toString()}` : "";
|
|
9639
9654
|
return requestOxygen(`/api/cli/copilot/sessions/${encodeURIComponent(sessionId)}${suffix}`);
|
|
9640
9655
|
});
|
|
9656
|
+
}))
|
|
9657
|
+
.addCommand(new Command("plan")
|
|
9658
|
+
.description("Show how far along a Workspace Copilot session is: its plan's major steps, the substeps under each, what is done, and how much longer the rest is expected to take.")
|
|
9659
|
+
.argument("<sessionId>", "Copilot session id.")
|
|
9660
|
+
.option("--json", "Print a JSON envelope.")
|
|
9661
|
+
.action(async (sessionId, options) => {
|
|
9662
|
+
const result = await requestOxygen(`/api/cli/copilot/sessions/${encodeURIComponent(sessionId)}?event_limit=1`).catch((error) => {
|
|
9663
|
+
emitCliFailure("copilot plan", error);
|
|
9664
|
+
return null;
|
|
9665
|
+
});
|
|
9666
|
+
if (!result)
|
|
9667
|
+
return;
|
|
9668
|
+
const plan = isRecord(result) ? result.plan : null;
|
|
9669
|
+
if (options.json) {
|
|
9670
|
+
emitSuccess("copilot plan", { plan, web_url: isRecord(result) ? result.web_url : null }, options);
|
|
9671
|
+
return;
|
|
9672
|
+
}
|
|
9673
|
+
writeCopilotPlan(plan, isRecord(result) ? result.web_url : null);
|
|
9641
9674
|
}))
|
|
9642
9675
|
.addCommand(new Command("follow")
|
|
9643
9676
|
.description("Turn screen-follow ON (or, with --off, OFF) for an open Workspace Copilot session: while it is open in the browser, your screen opens onto whatever the copilot creates or changes, with the live session docked beside it. Same switch as the web composer's \"Show me\" toggle; changes nothing about what the session is allowed to do.")
|
|
@@ -9651,7 +9684,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
9651
9684
|
}));
|
|
9652
9685
|
}))
|
|
9653
9686
|
.addCommand(new Command("send")
|
|
9654
|
-
.description("Send a message to a Workspace Copilot session and stream the reply to stderr. Waits for the turn to finish (or pause for an approval) unless --no-wait. Inference bills credits at 5x the actual model cost
|
|
9687
|
+
.description("Send a message to a Workspace Copilot session and stream the reply to stderr. Waits for the turn to finish (or pause for an approval) unless --no-wait. Inference bills credits at 5x the actual model cost under the attended platform safety backstop.")
|
|
9655
9688
|
.argument("<sessionId>", "Copilot session id.")
|
|
9656
9689
|
.argument("<message...>", "Message to send (all words joined with spaces).")
|
|
9657
9690
|
.option("--no-wait", "Post the turn and return immediately without streaming the reply.")
|
|
@@ -11359,7 +11392,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
11359
11392
|
});
|
|
11360
11393
|
}))
|
|
11361
11394
|
.addCommand(new Command("get")
|
|
11362
|
-
.description("Get one WhatsApp account with limits, warm-up ramp state (account age + today's effective send
|
|
11395
|
+
.description("Get one WhatsApp account with limits, warm-up ramp state (account age + today's effective send ceiling), daily-reset timezone, and usage. Direct sends are warm-only; opted-in WhatsApp Sequences share these caps. <id> accepts an account id, connection id, or Unipile account id.")
|
|
11363
11396
|
.argument("<id>", "WhatsApp account id, connection id, or Unipile account id.")
|
|
11364
11397
|
.option("--json", "Print a JSON envelope.")
|
|
11365
11398
|
.action(async (id, options) => {
|
|
@@ -11386,18 +11419,18 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
11386
11419
|
await handleAsyncAction("whatsapp disconnect", options, () => requestOxygen(`/api/cli/whatsapp/accounts/${encodeURIComponent(id)}`, { method: "DELETE" }));
|
|
11387
11420
|
}))
|
|
11388
11421
|
.addCommand(new Command("limits")
|
|
11389
|
-
.description("View and adjust per-account WhatsApp daily limits and the daily-reset timezone. The warm-up ramp is a non-bypassable
|
|
11422
|
+
.description("View and adjust per-account WhatsApp daily limits and the daily-reset timezone. The warm-up ramp is a non-bypassable ceiling on direct sends and opted-in Sequence actions.")
|
|
11390
11423
|
.addCommand(new Command("get")
|
|
11391
|
-
.description("Show current WhatsApp limits, the warm-up ramp state, daily-reset timezone, defaults, and safe maximums. <id> accepts an account id, connection id, or Unipile account id.")
|
|
11424
|
+
.description("Show current WhatsApp limits, the warm-up ramp state, direct-versus-Sequence delivery policy, daily-reset timezone, defaults, and safe maximums. <id> accepts an account id, connection id, or Unipile account id.")
|
|
11392
11425
|
.argument("<id>", "WhatsApp account id, connection id, or Unipile account id.")
|
|
11393
11426
|
.option("--json", "Print a JSON envelope.")
|
|
11394
11427
|
.action(async (id, options) => {
|
|
11395
11428
|
await handleAsyncAction("whatsapp limits get", options, () => requestOxygen(`/api/cli/whatsapp/accounts/${encodeURIComponent(id)}/limits`));
|
|
11396
11429
|
}))
|
|
11397
11430
|
.addCommand(new Command("set")
|
|
11398
|
-
.description("Adjust per-account WhatsApp daily limits and the daily-reset timezone. Values are clamped to safe maximums; the warm-up
|
|
11431
|
+
.description("Adjust per-account WhatsApp daily limits and the daily-reset timezone. Values are clamped to safe maximums; the warm-up ceiling still applies to direct and Sequence traffic. <id> accepts an account id, connection id, or Unipile account id.")
|
|
11399
11432
|
.argument("<id>", "WhatsApp account id, connection id, or Unipile account id.")
|
|
11400
|
-
.option("--messages-per-day <n>", "Daily WhatsApp messages cap (subject to the warm-up
|
|
11433
|
+
.option("--messages-per-day <n>", "Daily WhatsApp messages cap across direct and Sequence traffic (subject to the warm-up ceiling).")
|
|
11401
11434
|
.option("--total-actions-per-day <n>", "Daily cap across all WhatsApp action types.")
|
|
11402
11435
|
.option("--messages-reads-per-day <n>", "Daily cap on chat and message-history reads.")
|
|
11403
11436
|
.option("--min-spacing-seconds <n>", "Minimum seconds between actions.")
|
|
@@ -12487,6 +12520,8 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
12487
12520
|
.option("--exclude-contacted", "Default every enroll into this sequence to cross-campaign exclusion — skip leads any OTHER active sequence is already contacting. A per-enroll `sequences enroll --exclude-contacted` / `--no-exclude-contacted` always overrides this default. Off by default.")
|
|
12488
12521
|
.option("--no-exclude-contacted", "Turn the sequence-level exclude-contacted default back OFF (enrollments then exclude cross-campaign only when a call opts in).")
|
|
12489
12522
|
.option("--stop-on-reply-scope <scope>", "Native-email reply stop: 'lead' (default) stops the replying enrollment; 'company_domain' also stops active enrollments at the same non-freemail company domain.")
|
|
12523
|
+
.option("--include-unsubscribe-link", "Append OXYGEN's visible recipient-bound unsubscribe footer to first-touch cold emails. Explicit opt-in; off by default. Plain-text messages show the URL, while HTML messages show a linked Unsubscribe label.")
|
|
12524
|
+
.option("--no-include-unsubscribe-link", "Do not append OXYGEN's visible unsubscribe footer (the default). Sends the authored body unchanged and never removes suppression from recipients who already opted out.")
|
|
12490
12525
|
.option("--tracking-opens", "Turn open-pixel tracking ON for this sequence's native email sends (the default; injection still needs a verified tracking domain — see `oxygen domains tracking`).")
|
|
12491
12526
|
.option("--no-tracking-opens", "Turn OFF the open pixel for this sequence's native email sends (a deliverability knob — a pixel is spam-filter surface).")
|
|
12492
12527
|
.option("--tracking-clicks", "Turn click-link tracking ON for this sequence's native email sends (the default; same verified-tracking-domain gate as opens).")
|
|
@@ -12555,7 +12590,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
12555
12590
|
}
|
|
12556
12591
|
}))
|
|
12557
12592
|
.addCommand(new Command("update")
|
|
12558
|
-
.description("Update a sequence. Journey structure/channels/email binding are draft-only so revised scope receives a fresh launch approval; a started sequence may only re-time waits it already has (clear a delay by zeroing it, never by deleting the step — enrollments track position by index); sender pools can change while draft or paused; launch caps change through `sequences start` after first start. Name, throttles, tags, and open/click tracking toggles remain editable as allowed by the server. Pass only the fields you want to change.")
|
|
12593
|
+
.description("Update a sequence. Journey structure/channels/email binding are draft-only so revised scope receives a fresh launch approval; a started sequence may only re-time waits it already has (clear a delay by zeroing it, never by deleting the step — enrollments track position by index); sender pools can change while draft or paused; launch caps change through `sequences start` after first start. Name, throttles, tags, the default-off visible unsubscribe footer, and open/click tracking toggles remain editable as allowed by the server. Pass only the fields you want to change.")
|
|
12559
12594
|
.argument("<sequence>", "Sequence id or slug.")
|
|
12560
12595
|
.option("--name <name>", "New human-readable sequence name.")
|
|
12561
12596
|
.option("--steps-file <path>", "Draft only: path to a JSON file: { \"steps\": [...] } replacing the journey. Copy supports {{column}} interpolation; native email steps also expose {{sender_name}} / {{sender_first_name}} / {{sender_email}} from the sending mailbox (a same-named row column wins). A `branch` can also route on the lead's data with a data leaf { has_column: \"email\" } (true when that row_values column is non-empty; present:false for \"missing\").")
|
|
@@ -12587,6 +12622,8 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
12587
12622
|
.option("--exclude-contacted", "Default every enroll into this sequence to cross-campaign exclusion — skip leads any OTHER active sequence is already contacting. A per-enroll `sequences enroll --exclude-contacted` / `--no-exclude-contacted` always overrides this default. Off by default.")
|
|
12588
12623
|
.option("--no-exclude-contacted", "Turn the sequence-level exclude-contacted default back OFF (enrollments then exclude cross-campaign only when a call opts in).")
|
|
12589
12624
|
.option("--stop-on-reply-scope <scope>", "Native-email reply stop: 'lead' (default) stops the replying enrollment; 'company_domain' also stops active enrollments at the same non-freemail company domain.")
|
|
12625
|
+
.option("--include-unsubscribe-link", "Append OXYGEN's visible recipient-bound unsubscribe footer to first-touch cold emails. Explicit opt-in; off by default. Plain-text messages show the URL, while HTML messages show a linked Unsubscribe label.")
|
|
12626
|
+
.option("--no-include-unsubscribe-link", "Turn the visible unsubscribe footer OFF. Future first-touch emails send the authored body unchanged; existing suppression records remain enforced.")
|
|
12590
12627
|
.option("--tracking-opens", "Turn open-pixel tracking ON for this sequence's native email sends (the default; injection still needs a verified tracking domain — see `oxygen domains tracking`).")
|
|
12591
12628
|
.option("--no-tracking-opens", "Turn OFF the open pixel for this sequence's native email sends (a deliverability knob — a pixel is spam-filter surface).")
|
|
12592
12629
|
.option("--tracking-clicks", "Turn click-link tracking ON for this sequence's native email sends (the default; same verified-tracking-domain gate as opens).")
|
|
@@ -12645,7 +12682,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
12645
12682
|
body.email = email;
|
|
12646
12683
|
}
|
|
12647
12684
|
if (Object.keys(body).length === 0) {
|
|
12648
|
-
throw new Error("Provide at least one field to update (--name, --steps-file, --source-table, --channels, --senders, --tags, --email-*, --clear-email, --max-credits, --max-live-sends, --max-emails-per-mailbox-per-day, --send-window-file, --stop-on-reply-scope, --no-stop-on-bounce, --[no-]exclude-contacted, or --[no-]tracking-opens / --[no-]tracking-clicks).");
|
|
12685
|
+
throw new Error("Provide at least one field to update (--name, --steps-file, --source-table, --channels, --senders, --tags, --email-*, --clear-email, --max-credits, --max-live-sends, --max-emails-per-mailbox-per-day, --send-window-file, --stop-on-reply-scope, --[no-]include-unsubscribe-link, --no-stop-on-bounce, --[no-]exclude-contacted, or --[no-]tracking-opens / --[no-]tracking-clicks).");
|
|
12649
12686
|
}
|
|
12650
12687
|
return requestOxygen(`/api/cli/sequences/${encodeURIComponent(sequence)}`, {
|
|
12651
12688
|
method: "PATCH",
|
|
@@ -12854,7 +12891,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
12854
12891
|
}));
|
|
12855
12892
|
}))
|
|
12856
12893
|
.addCommand(new Command("retry-deferred")
|
|
12857
|
-
.description("Preview or re-ready pending
|
|
12894
|
+
.description("Preview or re-ready pending actions deferred by LinkedIn working hours or an email send window on a PAUSED sequence. Email actions refresh their snapshotted window from the current campaign settings. Applying never sends, spends credits, resumes the sequence, or bypasses window/spacing/quota/cap checks.")
|
|
12858
12895
|
.argument("<sequence>", "Paused sequence id or slug.")
|
|
12859
12896
|
.option("--limit <n>", "Maximum actions to preview/retry (1-500, default 100).")
|
|
12860
12897
|
.option("--approved", "Apply the bounded local retry. The sequence remains paused.")
|
|
@@ -12871,9 +12908,9 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
12871
12908
|
}));
|
|
12872
12909
|
}))
|
|
12873
12910
|
.addCommand(new Command("recover-capacity")
|
|
12874
|
-
.description("Preview definitive no-effect terminal provider-capacity
|
|
12911
|
+
.description("Preview definitive no-effect terminal failures that Oxygen can replay safely: provider-capacity denials and exact allowlisted request-format defects. Recover the signed scope only on a PAUSED Sequence. Recovery restores local action/enrollment state; it never sends, calls a provider, spends credits, or resumes dispatch.")
|
|
12875
12912
|
.argument("<sequence>", "Sequence id or slug. Preview may run while active; apply requires paused.")
|
|
12876
|
-
.option("--limit <n>", "Maximum
|
|
12913
|
+
.option("--limit <n>", "Maximum candidate failure rows to inspect (1-5000, default 500). A truncated preview cannot be approved.")
|
|
12877
12914
|
.option("--approval-token <token>", "Exact unexpired token returned by the preview.")
|
|
12878
12915
|
.option("--approved", "Apply the exact signed scope. Requires --approval-token; the Sequence remains paused.")
|
|
12879
12916
|
.option("--json", "Print a JSON envelope.")
|
|
@@ -13194,7 +13231,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
13194
13231
|
});
|
|
13195
13232
|
}))));
|
|
13196
13233
|
program.addCommand(new Command("suppressions")
|
|
13197
|
-
.description("Unified do-not-contact controls for
|
|
13234
|
+
.description("Unified do-not-contact controls for people and typed identities. `import-person` bundles one contact's email, LinkedIn, phone, and explicit company domain; `import` preserves the legacy mixed-file contract; `import-identities` is the integration-level typed endpoint; `hubspot lists|sync` arms additive synchronization. Company DNC applies across Sequence channels and is never inferred from a person's email. Consumes 0 credits.")
|
|
13198
13235
|
.addCommand(new Command("list")
|
|
13199
13236
|
.description("List the org's do-not-contact suppressions, newest first. Filter by --reason or by --search (case-insensitive substring of the lead provider id).")
|
|
13200
13237
|
.option("--reason <reason>", "Filter by reason: manual, replied, unsubscribed, bounced, do_not_contact, friends.")
|
|
@@ -13350,6 +13387,47 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
13350
13387
|
},
|
|
13351
13388
|
});
|
|
13352
13389
|
});
|
|
13390
|
+
}))
|
|
13391
|
+
.addCommand(new Command("import-person")
|
|
13392
|
+
.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.")
|
|
13393
|
+
.option("--name <name>", "Optional contact display name.")
|
|
13394
|
+
.option("--email <address>", "Email address to suppress.")
|
|
13395
|
+
.option("--linkedin <url_or_id>", "Public LinkedIn person URL or member id to suppress.")
|
|
13396
|
+
.option("--phone <e164>", "Strict E.164 phone number to suppress.")
|
|
13397
|
+
.option("--domain <domain>", "Explicit company-wide domain block. This affects every Sequence channel.")
|
|
13398
|
+
.option("--reason <reason>", "Suppression reason; manual by default and valid for every bundled identifier.")
|
|
13399
|
+
.option("--source <text>", "Optional provenance stored on every identifier.")
|
|
13400
|
+
.option("--detail <text>", "Optional note stored on every identifier.")
|
|
13401
|
+
.option("--json", "Print a JSON envelope.")
|
|
13402
|
+
.action(async (options) => {
|
|
13403
|
+
await handleAsyncAction("suppressions import-person", options, () => {
|
|
13404
|
+
const name = readOption(options.name);
|
|
13405
|
+
const email = readOption(options.email);
|
|
13406
|
+
const linkedin = readOption(options.linkedin);
|
|
13407
|
+
const phone = readOption(options.phone);
|
|
13408
|
+
const domain = readOption(options.domain);
|
|
13409
|
+
if (!email && !linkedin && !phone && !domain) {
|
|
13410
|
+
throw new Error("Provide at least one of --email, --linkedin, --phone, or --domain.");
|
|
13411
|
+
}
|
|
13412
|
+
const reason = readOption(options.reason);
|
|
13413
|
+
const source = readOption(options.source);
|
|
13414
|
+
const detail = readOption(options.detail);
|
|
13415
|
+
return requestOxygen("/api/cli/suppressions/import", {
|
|
13416
|
+
method: "POST",
|
|
13417
|
+
body: {
|
|
13418
|
+
people: [{
|
|
13419
|
+
...(name ? { name } : {}),
|
|
13420
|
+
...(email ? { email } : {}),
|
|
13421
|
+
...(linkedin ? { linkedin_url: linkedin } : {}),
|
|
13422
|
+
...(phone ? { phone } : {}),
|
|
13423
|
+
...(domain ? { company_domain: domain } : {}),
|
|
13424
|
+
...(reason ? { reason } : {}),
|
|
13425
|
+
...(source ? { source } : {}),
|
|
13426
|
+
...(detail ? { detail } : {}),
|
|
13427
|
+
}],
|
|
13428
|
+
},
|
|
13429
|
+
});
|
|
13430
|
+
});
|
|
13353
13431
|
}))
|
|
13354
13432
|
.addCommand(new Command("phones")
|
|
13355
13433
|
.description("List phone suppressions (strict E.164) used by call, WhatsApp, and Sequence safety gates.")
|
|
@@ -16625,6 +16703,72 @@ function writeCopilotTurnCreditsSummary(data, turnId) {
|
|
|
16625
16703
|
// notice. billing_settled is intentionally silent (its spend lands in the
|
|
16626
16704
|
// end-of-turn credits summary); tool_call_* surface as brief progress so a turn
|
|
16627
16705
|
// that pauses on a slow capability does not look frozen.
|
|
16706
|
+
/**
|
|
16707
|
+
* The plan as a person reads it.
|
|
16708
|
+
*
|
|
16709
|
+
* `copilot get` has no human renderer at all -- both its branches call writeJson,
|
|
16710
|
+
* so --json only decides whether the payload is wrapped in an envelope. This leaf
|
|
16711
|
+
* exists because "how far along is it" is a question a JSON dump answers badly,
|
|
16712
|
+
* and because the same projection has to be legible on all three surfaces.
|
|
16713
|
+
*/
|
|
16714
|
+
function writeCopilotPlan(plan, webUrl) {
|
|
16715
|
+
if (!isRecord(plan)) {
|
|
16716
|
+
process.stdout.write("No plan yet. The copilot publishes one for multi-step work; a short answer finishes without planning.\n");
|
|
16717
|
+
return;
|
|
16718
|
+
}
|
|
16719
|
+
const progress = isRecord(plan.progress) ? plan.progress : {};
|
|
16720
|
+
const eta = isRecord(plan.eta) ? plan.eta : {};
|
|
16721
|
+
const steps = Array.isArray(plan.steps) ? plan.steps : [];
|
|
16722
|
+
const lines = [];
|
|
16723
|
+
if (typeof plan.summary === "string" && plan.summary)
|
|
16724
|
+
lines.push(plan.summary, "");
|
|
16725
|
+
lines.push(`${progress.done ?? 0}/${progress.total ?? 0} steps done${formatPlanEta(eta)}`, "");
|
|
16726
|
+
for (const step of steps) {
|
|
16727
|
+
if (!isRecord(step))
|
|
16728
|
+
continue;
|
|
16729
|
+
const status = String(step.status ?? "pending");
|
|
16730
|
+
const mark = status === "done" ? "[x]" : status === "in_progress" ? "[>]" : status === "blocked" ? "[!]" : "[ ]";
|
|
16731
|
+
const timing = status === "done"
|
|
16732
|
+
? formatPlanDuration(step.elapsedMs)
|
|
16733
|
+
: typeof step.etaSeconds === "number" ? `~${formatPlanSeconds(step.etaSeconds)} left` : "";
|
|
16734
|
+
lines.push(`${mark} ${String(step.title ?? "")}${timing ? ` (${timing})` : ""}`);
|
|
16735
|
+
for (const sub of Array.isArray(step.substeps) ? step.substeps : []) {
|
|
16736
|
+
if (!isRecord(sub))
|
|
16737
|
+
continue;
|
|
16738
|
+
const subStatus = String(sub.status ?? "pending");
|
|
16739
|
+
const subMark = subStatus === "done" ? "-" : subStatus === "blocked" ? "!" : ">";
|
|
16740
|
+
const subTiming = formatPlanDuration(sub.durationMs);
|
|
16741
|
+
lines.push(` ${subMark} ${String(sub.title ?? "")}${subTiming ? ` (${subTiming})` : ""}`);
|
|
16742
|
+
}
|
|
16743
|
+
}
|
|
16744
|
+
if (typeof webUrl === "string" && webUrl)
|
|
16745
|
+
lines.push("", webUrl);
|
|
16746
|
+
process.stdout.write(`${lines.join("\n")}\n`);
|
|
16747
|
+
}
|
|
16748
|
+
/** Every estimate names its basis, so nobody reads a guess as a measurement. */
|
|
16749
|
+
function formatPlanEta(eta) {
|
|
16750
|
+
const remaining = typeof eta.remainingSeconds === "number" ? eta.remainingSeconds : null;
|
|
16751
|
+
if (remaining === null)
|
|
16752
|
+
return " · time remaining: estimating";
|
|
16753
|
+
const measured = typeof eta.measuredSteps === "number" ? eta.measuredSteps : 0;
|
|
16754
|
+
const basis = eta.basis === "calibrated" || eta.basis === "measured"
|
|
16755
|
+
? `measured from ${measured} completed step${measured === 1 ? "" : "s"}`
|
|
16756
|
+
: eta.basis === "model" ? "estimated, nothing measured yet" : "no basis";
|
|
16757
|
+
const bound = eta.deadlineBound === true ? ", capped by the turn deadline" : "";
|
|
16758
|
+
return ` · ~${formatPlanSeconds(remaining)} left (${basis}${bound})`;
|
|
16759
|
+
}
|
|
16760
|
+
function formatPlanSeconds(seconds) {
|
|
16761
|
+
if (seconds < 60)
|
|
16762
|
+
return `${Math.round(seconds)}s`;
|
|
16763
|
+
const minutes = Math.floor(seconds / 60);
|
|
16764
|
+
const rest = Math.round(seconds % 60);
|
|
16765
|
+
return rest === 0 ? `${minutes}m` : `${minutes}m ${rest}s`;
|
|
16766
|
+
}
|
|
16767
|
+
function formatPlanDuration(ms) {
|
|
16768
|
+
if (typeof ms !== "number" || !Number.isFinite(ms) || ms <= 0)
|
|
16769
|
+
return "";
|
|
16770
|
+
return ms < 1000 ? `${Math.round(ms)}ms` : formatPlanSeconds(ms / 1000);
|
|
16771
|
+
}
|
|
16628
16772
|
function printCopilotEvent(event, sessionId) {
|
|
16629
16773
|
if (!isRecord(event))
|
|
16630
16774
|
return;
|
|
@@ -16658,6 +16802,19 @@ function printCopilotEvent(event, sessionId) {
|
|
|
16658
16802
|
+ `Approve with: ${resolveCliBinaryName()} copilot approve ${sessionId} ${approvalId}\n`);
|
|
16659
16803
|
return;
|
|
16660
16804
|
}
|
|
16805
|
+
case "plan_updated": {
|
|
16806
|
+
// The plan is the one event that says how far along the turn is, and it was
|
|
16807
|
+
// the only kind this printer dropped -- a `copilot send` streamed every tool
|
|
16808
|
+
// call it made and never once said what it was working towards.
|
|
16809
|
+
const steps = Array.isArray(payload.steps) ? payload.steps : [];
|
|
16810
|
+
const done = steps.filter((step) => isRecord(step) && normalizeCopilotPlanStepStatus(step.status) === "done").length;
|
|
16811
|
+
const current = steps.find((step) => isRecord(step) && normalizeCopilotPlanStepStatus(step.status) === "in_progress");
|
|
16812
|
+
const title = isRecord(current)
|
|
16813
|
+
? String(current.title ?? current.description ?? "").trim()
|
|
16814
|
+
: "";
|
|
16815
|
+
process.stderr.write(`\n[plan] ${done}/${steps.length} done${title ? ` — now: ${title}` : ""}\n`);
|
|
16816
|
+
return;
|
|
16817
|
+
}
|
|
16661
16818
|
case "fallback_engaged": {
|
|
16662
16819
|
const from = typeof payload.from === "string" ? payload.from : "?";
|
|
16663
16820
|
const to = typeof payload.to === "string" ? payload.to : "?";
|
|
@@ -23808,7 +23965,7 @@ function readNonNegativeInt(value) {
|
|
|
23808
23965
|
if (!trimmed)
|
|
23809
23966
|
return undefined;
|
|
23810
23967
|
const parsed = Number(trimmed);
|
|
23811
|
-
if (!Number.
|
|
23968
|
+
if (!Number.isSafeInteger(parsed) || parsed < 0) {
|
|
23812
23969
|
throw new OxygenError("invalid_number", "Expected a non-negative integer.", {
|
|
23813
23970
|
details: { value },
|
|
23814
23971
|
exitCode: 1,
|
|
@@ -23855,6 +24012,12 @@ function readSequenceSettings(options) {
|
|
|
23855
24012
|
if (options.excludeContacted !== undefined) {
|
|
23856
24013
|
settings.exclude_contacted = options.excludeContacted;
|
|
23857
24014
|
}
|
|
24015
|
+
// A visible footer mutates outbound copy, so it is opt-in and tri-state:
|
|
24016
|
+
// absent leaves the stored value alone; either explicit flag persists the
|
|
24017
|
+
// chosen boolean. Missing/false is interpreted as OFF by the dispatcher.
|
|
24018
|
+
if (options.includeUnsubscribeLink !== undefined) {
|
|
24019
|
+
settings.include_unsubscribe_link = options.includeUnsubscribeLink;
|
|
24020
|
+
}
|
|
23858
24021
|
const stopOnReplyScope = readOption(options.stopOnReplyScope);
|
|
23859
24022
|
if (stopOnReplyScope)
|
|
23860
24023
|
settings.stop_on_reply_scope = stopOnReplyScope;
|
|
@@ -537,6 +537,13 @@ function normalizeIntent(query) {
|
|
|
537
537
|
return query.toLowerCase().replace(/[_-]+/g, " ").replace(/\s+/g, " ").trim();
|
|
538
538
|
}
|
|
539
539
|
function explicitCapabilityIntent(query) {
|
|
540
|
+
// A unified sender profile is an owned Sequence identity, even when the ask
|
|
541
|
+
// names every attached channel (LinkedIn + WhatsApp + email). Resolve this
|
|
542
|
+
// before public LinkedIn research, whose generic "profile" wording would
|
|
543
|
+
// otherwise route an account-readiness question to scraper tools.
|
|
544
|
+
if (isSenderProfileIntent(query)) {
|
|
545
|
+
return ROUTE_BY_PRIMITIVE.get("sequences") ?? null;
|
|
546
|
+
}
|
|
540
547
|
if (isInboxAvatarIntent(query)) {
|
|
541
548
|
return ROUTE_BY_ID.get("sending-infrastructure") ?? null;
|
|
542
549
|
}
|
|
@@ -765,6 +772,12 @@ function recommendationsFor(card, query) {
|
|
|
765
772
|
}
|
|
766
773
|
}
|
|
767
774
|
if (card.primitive === "sequences") {
|
|
775
|
+
if (isSenderProfileIntent(query)) {
|
|
776
|
+
return {
|
|
777
|
+
tools: ["oxygen_senders_profiles_list", "oxygen_senders_profiles_get"],
|
|
778
|
+
commands: ["senders profiles list", "senders profiles get"],
|
|
779
|
+
};
|
|
780
|
+
}
|
|
768
781
|
if (isNetNewLinkedInInitiation(query)) {
|
|
769
782
|
const publicResearch = isPublicLinkedInRead(query);
|
|
770
783
|
const harvestEngagers = publicResearch && isPublicLinkedInEngagerHarvest(query);
|
|
@@ -861,7 +874,7 @@ function recommendationsFor(card, query) {
|
|
|
861
874
|
if (card.id === "connected-whatsapp" && /\b(account|connect|limits?|sync)\b/.test(query)) {
|
|
862
875
|
return {
|
|
863
876
|
tools: ["oxygen_whatsapp_accounts_list", "oxygen_whatsapp_get", "oxygen_whatsapp_limits_get", "oxygen_whatsapp_connect"],
|
|
864
|
-
commands: ["whatsapp accounts
|
|
877
|
+
commands: ["whatsapp accounts", "whatsapp get", "whatsapp limits get", "whatsapp connect"],
|
|
865
878
|
};
|
|
866
879
|
}
|
|
867
880
|
return { tools: [...card.gatewayTools], commands: [...card.gatewayCommands] };
|
|
@@ -869,6 +882,11 @@ function recommendationsFor(card, query) {
|
|
|
869
882
|
function isInboxAvatarIntent(query) {
|
|
870
883
|
return /\b(avatar|profile (?:picture|photo)|headshot|hosted (?:picture|image)|mailbox (?:picture|photo))\b/.test(query);
|
|
871
884
|
}
|
|
885
|
+
function isSenderProfileIntent(query) {
|
|
886
|
+
return /\b(?:sender|sending|unified)\s+(?:profiles?|identit(?:y|ies))\b/.test(query)
|
|
887
|
+
|| (/\bprofiles?\b.{0,64}\b(?:linkedin|whatsapp|mailboxes?|inboxes?|email)\b/.test(query)
|
|
888
|
+
&& /\b(?:sender|sending|unif(?:y|ied))\b/.test(query));
|
|
889
|
+
}
|
|
872
890
|
function isMutualLinkedInConnectionsIntent(query) {
|
|
873
891
|
const linkedInContext = /\blinkedin\b|\bprofiles?\b/.test(query);
|
|
874
892
|
const mutualContext = /\b(mutual|shared|in common)\b.{0,48}\b(connections?|relations?)\b/.test(query)
|