@oxygen-agent/cli 1.1013.94 → 1.1014.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/README.md +1 -1
  2. package/dist/index.js +190 -70
  3. package/node_modules/@oxygen/shared/dist/agent-sessions.d.ts +115 -0
  4. package/node_modules/@oxygen/shared/dist/agent-sessions.js +133 -0
  5. package/node_modules/@oxygen/shared/dist/app-storage-maintenance.d.ts +3 -0
  6. package/node_modules/@oxygen/shared/dist/app-storage-maintenance.js +16 -0
  7. package/node_modules/@oxygen/shared/dist/capability-discovery.js +77 -4
  8. package/node_modules/@oxygen/shared/dist/contracts/agent-sessions.d.ts +72 -0
  9. package/node_modules/@oxygen/shared/dist/contracts/agent-sessions.js +35 -0
  10. package/node_modules/@oxygen/shared/dist/contracts/browser-operations.js +1 -0
  11. package/node_modules/@oxygen/shared/dist/contracts/operations.js +4 -3
  12. package/node_modules/@oxygen/shared/dist/contracts/table-query-grid.d.ts +3 -0
  13. package/node_modules/@oxygen/shared/dist/contracts/table-query-grid.js +16 -4
  14. package/node_modules/@oxygen/shared/dist/copilot-access.d.ts +33 -0
  15. package/node_modules/@oxygen/shared/dist/copilot-access.js +33 -0
  16. package/node_modules/@oxygen/shared/dist/copilot-questions.d.ts +91 -0
  17. package/node_modules/@oxygen/shared/dist/copilot-questions.js +246 -0
  18. package/node_modules/@oxygen/shared/dist/hosted-ai.d.ts +28 -18
  19. package/node_modules/@oxygen/shared/dist/hosted-ai.js +96 -45
  20. package/node_modules/@oxygen/shared/dist/langfuse.d.ts +1 -1
  21. package/node_modules/@oxygen/shared/dist/langfuse.js +37 -9
  22. package/node_modules/@oxygen/shared/dist/object-storage.js +9 -0
  23. package/node_modules/@oxygen/shared/dist/sequences.d.ts +5 -1
  24. package/node_modules/@oxygen/shared/dist/sequences.js +91 -0
  25. package/node_modules/@oxygen/shared/dist/tool-call-markup.d.ts +23 -0
  26. package/node_modules/@oxygen/shared/dist/tool-call-markup.js +213 -0
  27. package/node_modules/@oxygen/shared/dist/user-capability-routing.js +5 -0
  28. package/node_modules/@oxygen/shared/dist/version.d.ts +1 -1
  29. package/node_modules/@oxygen/shared/dist/version.generated.d.ts +1 -1
  30. package/node_modules/@oxygen/shared/dist/version.generated.js +1 -1
  31. package/node_modules/@oxygen/shared/dist/version.js +1 -1
  32. package/node_modules/@oxygen/shared/dist/workspace-file-storage.js +4 -0
  33. package/package.json +1 -1
package/README.md CHANGED
@@ -34,4 +34,4 @@ oxygen update
34
34
 
35
35
  For product documentation, visit https://oxygen-agent.com/docs. For support, visit https://oxygen-agent.com.
36
36
 
37
- Version: 1.1013.94
37
+ Version: 1.1014.18
package/dist/index.js CHANGED
@@ -67,6 +67,7 @@ import { TABLE_LIST_QUERY_CONTRACT, tableListQueryParams } from "@oxygen/shared/
67
67
  import { TABLE_QUERY_GRID_CONTRACT } from "@oxygen/shared/contracts/table-query-grid";
68
68
  import { WHATSAPP_ACCOUNTS_READ_CONTRACT } from "@oxygen/shared/contracts/whatsapp-reads";
69
69
  import { WORKFLOW_RUN_LIST_CONTRACT, workflowRunListQueryParams } from "@oxygen/shared/contracts/workflow-run-list";
70
+ import { COPILOT_ASK_USER_TOOL_NAME, normalizeCopilotQuestions } from "@oxygen/shared/copilot-questions";
70
71
  import { MANAGED_DATA_SUPPLIERS } from "@oxygen/shared/data-suppliers";
71
72
  import { MAX_BUFFERED_IMPORT_PARSE_BYTES, assertImportFormatWithinBufferedLimit, inferImportColumnLabels, inferRowsFileFormat, normalizeImportColumnKey, normalizeRowsForNewTable, normalizeRowsFormat, parseRowsFileBuffer, parseXlsxWorkbookBuffer, } from "@oxygen/shared/file-import";
72
73
  import { KNOWLEDGE_PAGE_TYPES } from "@oxygen/shared/knowledge-constants";
@@ -1275,7 +1276,8 @@ function writeListCapNotice(data, noun, hint) {
1275
1276
  if (record.capped !== true)
1276
1277
  return;
1277
1278
  const returned = typeof record.returned === "number" ? record.returned : null;
1278
- process.stderr.write(`note: showing ${returned === null ? "a window of" : returned} ${noun}; more exist — raise --limit (max 1000) or pass --all${hint ? `; ${hint}` : ""}.\n`);
1279
+ const total = typeof record.total === "number" ? ` of ${record.total}` : "";
1280
+ process.stderr.write(`note: showing ${returned === null ? "a window of" : returned}${total} ${noun}; more exist — raise --limit (max 1000) or pass --all${hint ? `; ${hint}` : ""}.\n`);
1279
1281
  }
1280
1282
  // The map is bounded twice over — a scan window per kind, then a character
1281
1283
  // budget across all of them — and both bounds are invisible in the rendered
@@ -12706,12 +12708,12 @@ Examples:
12706
12708
  }));
12707
12709
  program
12708
12710
  .command("copilot")
12709
- .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 3,000-credit internal platform safety backstop; paid or external actions keep their own approval gates. Web: /copilot.")
12711
+ .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. Managed inference uses workspace credits; paid or external actions keep their own approval gates. Web: /copilot.")
12710
12712
  .addCommand(new Command("start")
12711
- .description("Start an attended Workspace Copilot session on Oxygen Auto — Oxygen picks the model, keeps one conversation on one model, and runs its own errands on a cheaper one. Starting is free; the next send starts inference. No inference budget is required; inference bills at 5x actual model cost under a 3,000-credit internal platform safety backstop. Legacy budget and tier flags remain accepted for older clients but do not authorize or cap spend. Paid or external actions require separate approval.")
12713
+ .description("Start an attended Workspace Copilot session on Oxygen Balanced by default. Starting is free; the next send starts inference using workspace credits. No inference budget is required. Legacy budget flags remain accepted but do not authorize or cap spend. Paid or external actions require separate approval.")
12712
12714
  .option("--budget-credits <number>", "Legacy compatibility only: accepted and clamped, but does not authorize or cap attended Copilot inference.")
12713
- .option("--tier <auto|low|medium|high>", "Legacy. Sessions run on Oxygen Auto; Oxygen picks the model. The three older tiers are still accepted so existing scripts keep working.")
12714
- .option("--model <id>", "Pin a specific priced model id instead of letting Oxygen choose. An id Oxygen does not price is ignored and the session runs on Oxygen Auto.")
12715
+ .option("--tier <auto|low|medium|high>", "Model tier: auto=Oxygen Auto, low=Oxygen Fast, medium=Oxygen Balanced (default), high=Oxygen Max.")
12716
+ .option("--model <id>", "Pin a specific priced model id. Unknown ids fall back to the selected tier.")
12715
12717
  .option("--per-turn-ceiling <number>", "Legacy compatibility only: accepted and clamped, but does not cap an attended turn.")
12716
12718
  .option("--journey <slug>", "Optional journey slug to seed the session's goal and context.")
12717
12719
  .option("--title <text>", "Optional human title for the session.")
@@ -12813,7 +12815,7 @@ Examples:
12813
12815
  }));
12814
12816
  }))
12815
12817
  .addCommand(new Command("send")
12816
- .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.")
12818
+ .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. Managed inference uses workspace credits.")
12817
12819
  .argument("<sessionId>", "Copilot session id.")
12818
12820
  .argument("<message...>", "Message to send (all words joined with spaces).")
12819
12821
  .option("--no-wait", "Post the turn and return immediately without streaming the reply.")
@@ -16374,14 +16376,15 @@ Examples:
16374
16376
  .addHelpText("after", "\nSenders: email_send.sender_profile_id sends one email from any inbox of a selected person (a lead already assigned to that person keeps its own inbox), and email_send.sender_mailbox_id from one exact inbox of a selected sender; a LinkedIn step's sender_profile_id sends it from a selected person (invite, message, inmail and withdraw_invite share one or none). Omit both to use the lead's assigned person. An email whose From changes needs its own subject_template. `sequences start <sequence>` without --approved shows who sends each step; nothing changes.\n")
16375
16377
  .argument("<sequence>", "Sequence id or slug.")
16376
16378
  .option("--name <name>", "New human-readable sequence name.")
16377
- .option("--steps-file <path>", "Draft only: path to a JSON file: { \"steps\": [...] } replacing the journey. Copy supports {{column}} interpolation; every step also takes {{sender_first_name}} / {{sender_last_name}} / {{sender_full_name}} for the person who sends it, and email adds {{sender_email}} (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\").")
16379
+ .option("--steps-file <path>", "Path to a JSON file: { \"steps\": [...] } replacing the journey. Once started, only wait timing may change: same steps, same order (zero a delay, never delete its step). Copy supports {{column}} interpolation; every step also takes {{sender_first_name}} / {{sender_last_name}} / {{sender_full_name}} for the person who sends it, and email adds {{sender_email}} (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\").")
16378
16380
  .option("--channels <list>", "Draft only: comma-separated channels: linkedin,email,whatsapp,call,crm.")
16379
16381
  .option("--whatsapp-cold-initiate", "WhatsApp: allow cold-initiating new chats (no prior conversation). Required to start a WhatsApp sequence live (draft-editable).")
16382
+ .option("--no-whatsapp-cold-initiate", "WhatsApp: stop allowing cold chats; a live WhatsApp start then refuses.")
16380
16383
  .addOption(new Option("--whatsapp-duplicate-scope <scope>", "Prevent duplicate WhatsApp copy in this campaign (default) or all campaigns.").choices(["campaign", "workspace"]))
16381
- .option("--phone-column-key <key>", "WhatsApp: row_values key holding each lead's phone number (else falls back to phone/phone_number/mobile).")
16382
- .option("--email-column-key <key>", "Native email: row_values key holding each lead's email (else falls back to email/email_address/work_email). Maps a non-English header (e.g. E-Mail).")
16384
+ .option("--phone-column-key <key>", "WhatsApp: row_values key holding each lead's phone number (else falls back to phone/phone_number/mobile). \"\" clears it back to auto-detect.")
16385
+ .option("--email-column-key <key>", "Native email: row_values key holding each lead's email (else falls back to email/email_address/work_email). Maps a non-English header (e.g. E-Mail). \"\" clears it back to auto-detect.")
16383
16386
  .option("--linkedin-url-column-key <key>", "LinkedIn: row_values key holding each lead's profile URL (else falls back to linkedin_url/linkedinUrl/linkedin/profile_url). \"\" clears it back to auto-detect.")
16384
- .option("--senders <ids>", "Comma-separated LinkedIn or WhatsApp sender account ids (or connection / Unipile ids; `senders list` / `whatsapp accounts` list them).")
16387
+ .option("--senders <ids>", "LinkedIn or WhatsApp accounts attached directly, besides the --sender-profiles people's own accounts: comma-separated sender account ids (or connection / Unipile ids; `senders list` / `whatsapp accounts` list them). Replaces the directly attached set; \"\" removes them all and keeps the selected senders. `sequences get` lists both under sender_pool. Draft/paused only.")
16385
16388
  .option("--source-table <idOrSlug>", "Bind the table this sequence enrolls leads from (id or slug). Only lands while the sequence has no source table yet (first-writer-wins); its rows become enrollable leads. Draft/paused only.")
16386
16389
  .option("--email-provider <provider>", "Optional linked-campaign provider: 'instantly'. Omit to preserve the existing binding; --clear-email removes it on a draft. Native email needs no provider binding and sends through connected Google/Microsoft mailboxes, including Microsoft SMTP transport.")
16387
16390
  .option("--email-connection <id>", "Instantly connection id for the email track.")
@@ -16389,7 +16392,7 @@ Examples:
16389
16392
  .option("--clear-email", "Remove the email binding from the sequence (draft only).")
16390
16393
  .option("--max-credits <n>", "Stored draft credit ceiling (0 or more); 0 is valid. Changing a started sequence's cap requires its normal live-start approval.")
16391
16394
  .option("--max-live-sends <n>", "Draft-time total live-send and CRM-write ceiling across channels, including LinkedIn invites/messages (positive integer). After first start, change it through `sequences start`.")
16392
- .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.")
16395
+ .option("--max-emails-per-mailbox-per-day <n>", "Per-mailbox daily email cap (0 or more) 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.")
16393
16396
  .option("--schedule-timezone <tz>", "One sending schedule for every channel, the same as Launch's Sending schedule: IANA zone, e.g. Europe/Berlin. Give it with --schedule-days, --schedule-hours-start and --schedule-hours-end; it sets the email window and the LinkedIn/WhatsApp window together. Use it instead of --send-window-file and the --linkedin-* flags. Email set to send in each recipient's own timezone keeps doing so, with this zone for leads that have none.")
16394
16397
  .option("--schedule-days <days>", "Sending schedule days: mon-fri, mon,wed,fri, or ISO numbers 1-7 (1 = Monday).")
16395
16398
  .option("--schedule-hours-start <HH:MM>", "Sending schedule start, local to --schedule-timezone, e.g. 09:00.")
@@ -16399,15 +16402,15 @@ Examples:
16399
16402
  .option("--linkedin-days <days>", "LinkedIn sending schedule days: mon-fri, mon,wed,fri, or ISO numbers 1-7 (1 = Monday).")
16400
16403
  .option("--linkedin-hours-start <HH:MM>", "LinkedIn sending schedule start, local to --linkedin-timezone, e.g. 06:45.")
16401
16404
  .option("--linkedin-hours-end <HH:MM>", "LinkedIn sending schedule end (exclusive), e.g. 18:30. Must be after the start. For LinkedIn the window only narrows each account's working hours (default 07:00-22:00, see senders limits get), never widens them.")
16402
- .option("--max-emails-per-day <n>", "Sequence-wide daily live-send cap across all senders/mailboxes. Unset means no additional sequence-wide cap; mailbox caps still apply. Omit on update to keep the current setting.")
16403
- .option("--max-new-enrollments-per-day <n>", "Daily cap on NEW first-touch leads. Unset means no additional intake cap; sending limits still apply. Omit on update to keep the current setting.")
16405
+ .option("--max-emails-per-day <n|none>", "Sequence-wide daily cap on live sends across every channel (email, LinkedIn, WhatsApp) and sender; 0 sends nothing. `none` removes the cap; mailbox and account caps still apply. Omit to keep the current setting.")
16406
+ .option("--max-new-enrollments-per-day <n|none>", "Daily cap on NEW first-touch leads (0 or more). `none` removes it; sending limits still apply. Omit to keep the current setting.")
16404
16407
  .option("--sequence-prioritization <mode>", "Under a tight daily budget, serve 'followups' or 'new_leads' first.")
16405
- .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.")
16408
+ .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. Changes only the mode: routes saved by --esp-routing-file or the web stay.")
16406
16409
  .option("--esp-routing-file <path>", "Path to a JSON file holding the WHOLE native-email routing policy: { mode?, routes? }. `routes` is keyed by the RECIPIENT's provider and its values are relative send shares per sending provider, so { \"google\": { \"microsoft\": 100 } } sends Google-hosted recipients from Microsoft inboxes. Replaces --esp-matching and the file is the complete policy, not a patch. To stop sending from a domain, leave its inboxes out with --mailboxes.")
16407
16410
  .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.")
16408
16411
  .option("--email-min-gap-minutes <n>", "Minimum same-mailbox gap, default 12 minutes (integer 0-720; 0 disables the extra gap). Sends are spread across the window and may be farther apart. Daily caps and windows still apply.")
16409
16412
  .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.")
16410
- .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).")
16413
+ .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). \"\" removes the allowlist.")
16411
16414
  .option("--sender-profiles <ids>", "Send from these sender profiles (unified LinkedIn/WhatsApp/inbox identities): comma-separated profile ids (`oxygen senders profiles list` lists them). 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.")
16412
16415
  .option("--no-stop-on-bounce", "Keep a lead's enrollment running after a hard bounce (default: stop it). The bounce is still recorded as an email_bounced signal either way.")
16413
16416
  .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.")
@@ -16415,7 +16418,7 @@ Examples:
16415
16418
  .option("--company-reply-pause <off|default|statuses>", "Pause the replier's company in this campaign when a reply has one of these statuses. Oxygen detects the company from work-email domains or explicit company fields; with no company identity, only the replier stops. New sequences start with this setting off. 'default' = interested,neutral,not_now,not_interested; or comma-separated Inbox status keys (`inbox labels list`); 'off' disables it. Read the saved setting with `sequences get`.")
16416
16419
  .option("--stop-on-reply-scope <scope>", "(deprecated: use --company-reply-pause) 'company_domain' = --company-reply-pause interested,neutral,not_now,not_interested,wrong_person; 'lead' = off.")
16417
16420
  .option("--bounce-protection <json>", "Sequence bounce policy JSON (partial updates merge): enabled=true, warning_rate=.02, pause_rate=.03, min_sends=20, window_days=7. Hard bounces / accepted sends in the rolling window; pauses this sequence, not the fleet. Rates are fractions; mailbox protection is separate.")
16418
- .option("--auto-optimize", "Enable the sequence-wide A/B auto-winner (default on).")
16421
+ .option("--auto-optimize", "Turn the A/B auto-winner on (the default): every step with variants is judged on replies, pausing losers once each variant has 100 sends and 5 replies, unless the step sets its own auto_optimize.")
16419
16422
  .option("--no-auto-optimize", "Disable the sequence-wide A/B auto-winner; manual variant decisions stay available.")
16420
16423
  .option("--include-unsubscribe", "Add an unsubscribe option to emails, the same as Launch's switch: a visible recipient-bound link (plain text shows the URL, HTML a linked Unsubscribe label) plus one-click headers that mail apps show as an Unsubscribe button, both on first-touch cold emails; in-thread follow-ups carry neither. Zapbox sends (mailboxes imported from Zapmail, `transport: \"zapbox\"` in `mailboxes list --json`) carry the link only. Off by default. There is no separate opt-out text: write any reply-to-stop line into the email copy.")
16421
16424
  .option("--no-include-unsubscribe", "Turn off both the visible unsubscribe link and the one-click headers.")
@@ -16446,9 +16449,10 @@ Examples:
16446
16449
  const channels = readCsvOption(options.channels);
16447
16450
  if (channels.length > 0)
16448
16451
  body.channels = channels;
16449
- const senders = readCsvOption(options.senders);
16450
- if (senders.length > 0)
16451
- body.senders = senders;
16452
+ // The directly attached accounts, replaced as a set; `--senders ""`
16453
+ // sends [] to remove them (the selected senders keep theirs).
16454
+ if (options.senders !== undefined)
16455
+ body.senders = readCsvOption(options.senders);
16452
16456
  const sourceTable = readOption(options.sourceTable);
16453
16457
  if (sourceTable)
16454
16458
  body.source_table_id = sourceTable;
@@ -16613,13 +16617,25 @@ Examples:
16613
16617
  });
16614
16618
  }))
16615
16619
  .addCommand(new Command("preview")
16616
- .description("The free launch check, as a read: the same preview sequences start returns without --approved. It returns launch_readiness (blockers and warnings), sender_journey (who sends each step and how an inbox is picked), copy_preview (rendered pending-lead LinkedIn/WhatsApp copy, character counts and scan), caps and estimated credits. Email copy is not rendered; inspect its saved templates and unsubscribe settings with sequences get. Changes nothing and costs 0 credits; it never starts, resumes or dry-runs the sequence. Start with sequences start <sequence> --approved after review. Planned counts include only leads each step's channel can reach; skipped_actions_by_reason, channel_coverage and copy_preview.skipped_leads show the rest.")
16620
+ .description("The free launch check, as a read: the same preview sequences start returns without --approved. It returns launch_readiness (blockers and warnings), sender_journey (who sends each step and how an inbox is picked), copy_preview (rendered pending-lead LinkedIn/WhatsApp copy, character counts and scan), caps and estimated credits. Email copy is not rendered here: `sequences render` shows it, and every A/B variant, for leads of the bound table. Changes nothing and costs 0 credits; it never starts, resumes or dry-runs the sequence. Start with sequences start <sequence> --approved after review. Planned counts include only leads each step's channel can reach; skipped_actions_by_reason, channel_coverage and copy_preview.skipped_leads show the rest.")
16617
16621
  .argument("<sequence>", "Sequence id or slug.")
16618
16622
  .option("--json", "Print a JSON envelope.")
16619
16623
  .action(async (sequence, options) => {
16620
16624
  await handleAsyncAction("sequences preview", options, () =>
16621
16625
  // The start route's preview branch: no approved, no dry_run, no ceilings.
16622
16626
  requestOxygen(`/api/cli/sequences/${encodeURIComponent(sequence)}/start`, { method: "POST", body: {} }));
16627
+ }))
16628
+ .addCommand(new Command("render")
16629
+ .description("Render a sequence's copy as the web step editor's Preview does: every A/B variant of each step (email subject/body, LinkedIn note/message/InMail, WhatsApp, comments) for leads of the bound table, or sample values when it has none, signed by one eligible sender. Works on a draft with nobody enrolled. Read-only, 0 credits. Each lead gets one variant at send; this shows all of them side by side, and spintax shows one pick per lead.")
16630
+ .argument("<sequence>", "Sequence id or slug.")
16631
+ .option("--step <stepId>", "Render only this step.")
16632
+ .option("--variant <id>", "Render only this variant (a is the base step, then b, c, ...).")
16633
+ .option("--row <ids>", "Comma-separated table_row_ids of the bound table to render for (at most 10). Default: its first rows.")
16634
+ .option("--sender <profileId>", "Sign as this sender profile (one the step may send from; the response lists eligible_senders). Default: the first eligible.")
16635
+ .option("--limit <n>", "How many leads to render when --row is not given (1-10, default 3).")
16636
+ .option("--json", "Print a JSON envelope.")
16637
+ .action(async (sequence, options) => {
16638
+ await handleSequenceRenderAction(sequence, options);
16623
16639
  }))
16624
16640
  .addCommand(new Command("signal")
16625
16641
  .description("Record an external GTM signal (e.g. linkedin_connected, email_replied, company_raised_funds, job_change, web_visit, intent) onto a running sequence's enrollment(s) to drive signal-triggered branch/wait control. Target an enrollment by --enrollment or --lead (at least one is required); the server validates the signal name.")
@@ -16632,10 +16648,10 @@ Examples:
16632
16648
  await handleSequenceSignalAction(sequence, options);
16633
16649
  }))
16634
16650
  .addCommand(new Command("duplicate")
16635
- .description("Duplicate a sequence as a new DRAFT: copies the step definition, settings, channels, source table, and sender pool. Never copies enrollments, stats, or an Instantly email binding (re-bind with `sequences update` if the source used one; native email steps need no binding). Client-side composition of get + create — the copy starts fully inert until you `sequences start` it.")
16651
+ .description("Duplicate a sequence as a new DRAFT: copies the steps, settings, channels, source table, caps, tags and sender pool. Never copies enrollments, stats, A/B decisions or an Instantly email binding (re-bind with `sequences update` if the source used one; native email steps need no binding). The copy is inert until you `sequences start` it; this is how a started journey gets new steps.")
16636
16652
  .argument("<sequence>", "Source sequence id or slug.")
16637
- .requiredOption("--name <name>", "Name for the copy.")
16638
- .option("--slug <slug>", "Slug for the copy. Defaults to the source slug + '-copy'.")
16653
+ .option("--name <name>", "Name for the copy. Defaults to '<source name> (copy)'.")
16654
+ .option("--slug <slug>", "Slug for the copy. Defaults to the source slug + '-copy' (then -copy-2, ...).")
16639
16655
  .option("--no-senders", "Do not copy the source's sender pool onto the copy.")
16640
16656
  .option("--json", "Print a JSON envelope.")
16641
16657
  .action(async (sequence, options) => {
@@ -16973,7 +16989,7 @@ Examples:
16973
16989
  .addCommand(new Command("variants")
16974
16990
  .description("Generic per-step action/base-variant scoreboard for one sequence, plus native email by_mailbox, by_domain, reply types, rates, and explicit unattributed facts. This rollup is not the copy-only analytics.messagePerformance view; base rows can represent non-message actions, and campaign replies may lack step/variant provenance. Provider-owned campaigns appear only after normalization into Oxygen's native Sequence ledgers. Also returns reversible auto-winner state (paused variants + evidence); flags run or override/reset that state.")
16975
16991
  .argument("<sequence>", "Sequence id or slug.")
16976
- .option("--auto-optimize", "Run the auto-winner now: pause the statistically-losing variant(s) of any step carrying an auto_optimize config, once every variant clears its thresholds, on that step's configured metric — reply (the recommended default), click, or open (click/open need native open/click tracking; opens are soft signals under Apple Mail Privacy Protection). Reversible.")
16992
+ .option("--auto-optimize", "Run the auto-winner now on every step with variants (the whole sequence): once each active variant clears its thresholds (default 100 sends and 5 replies), pause the variants converting below the leader, on the step's metric — reply by default, or the click/open its own auto_optimize names (click/open need native open/click tracking; opens are soft signals under Apple Mail Privacy Protection). Nothing is evaluated while the sequence's auto-winner is off. Reversible.")
16977
16993
  .option("--pause <variant>", "Manually pause one variant (requires --step). Overrides the auto-winner.")
16978
16994
  .option("--activate <variant>", "Manually reactivate one paused variant (requires --step).")
16979
16995
  .option("--reset [variant]", "Clear an auto/manual decision: one variant (with --step), a whole step (--step only), or the entire sequence (no value). Reactivates the affected variants.")
@@ -21303,11 +21319,18 @@ function printCopilotEvent(event, sessionId) {
21303
21319
  return;
21304
21320
  }
21305
21321
  case "tool_call_started": {
21322
+ // A question prints whole once it is asked; "[tool] ask_user…" says nothing.
21323
+ if (payload.tool === COPILOT_ASK_USER_TOOL_NAME)
21324
+ return;
21306
21325
  const capability = typeof payload.capability === "string" ? payload.capability : "tool";
21307
21326
  process.stderr.write(`\n[tool] ${capability}…\n`);
21308
21327
  return;
21309
21328
  }
21310
21329
  case "tool_call_finished": {
21330
+ if (payload.tool === COPILOT_ASK_USER_TOOL_NAME) {
21331
+ printCopilotQuestion(payload, sessionId);
21332
+ return;
21333
+ }
21311
21334
  const capability = typeof payload.capability === "string" ? payload.capability : "tool";
21312
21335
  const summary = typeof payload.summary === "string" ? payload.summary : "";
21313
21336
  process.stderr.write(`[tool] ${capability} ${payload.ok === true ? "ok" : "failed"}${summary ? ` — ${summary}` : ""}\n`);
@@ -21353,6 +21376,29 @@ function printCopilotEvent(event, sessionId) {
21353
21376
  return;
21354
21377
  }
21355
21378
  }
21379
+ /**
21380
+ * The Copilot ended its turn on a question (`ask_user`). The web answers it on a
21381
+ * card; here the options print numbered and the answer is the next message, so
21382
+ * nothing about it needs a command of its own.
21383
+ */
21384
+ function printCopilotQuestion(payload, sessionId) {
21385
+ const result = isRecord(payload.result) ? payload.result : {};
21386
+ if (result.ok === false)
21387
+ return;
21388
+ const asked = normalizeCopilotQuestions(result.questions);
21389
+ if (!asked.ok)
21390
+ return;
21391
+ const lines = [];
21392
+ for (const question of asked.questions) {
21393
+ lines.push(`\n[question] ${question.question}${question.multiSelect ? " (choose any)" : ""}`);
21394
+ question.options.forEach((option, index) => {
21395
+ lines.push(` ${index + 1}. ${option.label}${option.recommended ? " (recommended)" : ""}`
21396
+ + `${option.description ? ` — ${option.description}` : ""}`);
21397
+ });
21398
+ }
21399
+ lines.push(`Answer with: ${resolveCliBinaryName()} copilot send ${sessionId} <your answer>\n`);
21400
+ process.stderr.write(lines.join("\n"));
21401
+ }
21356
21402
  // `copilot send` is the CLI's one streaming wait surface. It reuses the shared
21357
21403
  // waitForCliRun poll loop (timeout/interval parsing, deadline, bounded sleep,
21358
21404
  // typed timeout throw) but injects a fetch that GETs the session detail after a
@@ -26793,34 +26839,13 @@ function formatProfileUseSuccess(profile, options) {
26793
26839
  async function handleSequenceDuplicateAction(sequence, options) {
26794
26840
  try {
26795
26841
  const name = readOption(options.name);
26796
- if (!name)
26797
- throw new Error("--name is required.");
26798
- const source = await requestOxygen(`/api/cli/sequences/${encodeURIComponent(sequence)}`, { method: "GET" });
26799
- const src = source.sequence;
26800
- if (!src || src.definition === undefined) {
26801
- throw new Error(`Sequence '${sequence}' was found but returned no definition to copy.`);
26802
- }
26803
- const slug = readOption(options.slug) ?? `${src.slug ?? sequence}-copy`;
26804
- const copySenders = options.senders !== false;
26805
- const created = await requestOxygen("/api/cli/sequences", {
26842
+ const slug = readOption(options.slug);
26843
+ const created = await requestOxygen(`/api/cli/sequences/${encodeURIComponent(sequence)}/duplicate`, {
26806
26844
  method: "POST",
26807
- // The create route reads snake_case keys (source_table_id, max_credits,
26808
- // senders, …). This handler used to POST camelCase, so the copy silently
26809
- // dropped the sender pool, table binding, URL column, and caps.
26810
26845
  body: {
26811
- slug,
26812
- name,
26813
- definition: src.definition,
26814
- ...(src.channels ? { channels: src.channels } : {}),
26815
- ...(src.sourceTableId ? { source_table_id: src.sourceTableId } : {}),
26816
- ...(src.linkedinUrlColumnKey ? { linkedin_url_column_key: src.linkedinUrlColumnKey } : {}),
26817
- ...(src.settings ? { settings: src.settings } : {}),
26818
- ...(src.maxCredits != null ? { max_credits: src.maxCredits } : {}),
26819
- ...(src.maxLiveSends != null ? { max_live_sends: src.maxLiveSends } : {}),
26820
- ...(Array.isArray(src.tags) && src.tags.length > 0 ? { tags: src.tags } : {}),
26821
- ...(copySenders && src.senderAccountIds && src.senderAccountIds.length > 0
26822
- ? { senders: src.senderAccountIds }
26823
- : {}),
26846
+ ...(name ? { name } : {}),
26847
+ ...(slug ? { slug } : {}),
26848
+ ...(options.senders === false ? { include_senders: false } : {}),
26824
26849
  },
26825
26850
  });
26826
26851
  if (options.json) {
@@ -26828,15 +26853,76 @@ async function handleSequenceDuplicateAction(sequence, options) {
26828
26853
  return;
26829
26854
  }
26830
26855
  const link = created.deepLink ?? created.web_url;
26831
- const note = src.emailProvider
26832
- ? " Note: the source's email binding was NOT copied — re-bind with `sequences update` if needed."
26833
- : "";
26834
- process.stdout.write(`Duplicated '${sequence}' as draft '${created.sequence?.slug ?? slug}'.${note}${link ? ` ${link}` : ""}\n`);
26856
+ const notCopied = created.not_copied?.length ? ` Not copied: ${created.not_copied.join("; ")}.` : "";
26857
+ process.stdout.write(`Duplicated '${sequence}' as draft '${created.sequence?.slug ?? slug ?? "copy"}'.${notCopied}${link ? ` ${link}` : ""}\n`);
26835
26858
  }
26836
26859
  catch (error) {
26837
26860
  emitCliFailure("sequences duplicate", error);
26838
26861
  }
26839
26862
  }
26863
+ async function handleSequenceRenderAction(sequence, options) {
26864
+ try {
26865
+ const params = new URLSearchParams();
26866
+ const step = readOption(options.step);
26867
+ if (step)
26868
+ params.set("step", step);
26869
+ const variant = readOption(options.variant);
26870
+ if (variant)
26871
+ params.set("variant", variant);
26872
+ const rows = readCsvOption(options.row);
26873
+ if (rows.length > 0)
26874
+ params.set("row", rows.join(","));
26875
+ const sender = readOption(options.sender);
26876
+ if (sender)
26877
+ params.set("sender", sender);
26878
+ const limit = readPositiveInt(options.limit);
26879
+ if (limit !== undefined)
26880
+ params.set("limit", String(limit));
26881
+ const query = params.toString();
26882
+ const data = await requestOxygen(`/api/cli/sequences/${encodeURIComponent(sequence)}/render${query ? `?${query}` : ""}`);
26883
+ if (options.json) {
26884
+ writeJson(success("sequences render", data));
26885
+ return;
26886
+ }
26887
+ process.stdout.write(formatSequenceRender(data));
26888
+ }
26889
+ catch (error) {
26890
+ emitCliFailure("sequences render", error);
26891
+ }
26892
+ }
26893
+ function formatSequenceRender(data) {
26894
+ const lines = [];
26895
+ for (const step of data.steps ?? []) {
26896
+ const signer = step.sender ? ` · from ${step.sender.name}` : "";
26897
+ lines.push(`Step ${step.step_index + 1} ${step.step_id} (${step.kind}${step.threaded ? ", continues the thread" : ""})${signer}`);
26898
+ if (step.note)
26899
+ lines.push(` ${step.note}`);
26900
+ for (const variant of step.variants) {
26901
+ lines.push(` Variant ${variant.variant_id}${variant.status === "paused" ? " (paused)" : ""}`);
26902
+ for (const render of variant.renders) {
26903
+ lines.push(` ${render.lead}`);
26904
+ for (const field of render.fields) {
26905
+ const limit = field.character_limit !== null && field.character_count > field.character_limit
26906
+ ? ` [${field.character_count}/${field.character_limit} characters]`
26907
+ : "";
26908
+ const missing = field.missing_variables.length > 0
26909
+ ? ` [missing ${field.missing_variables.map((key) => `{{${key}}}`).join(", ")}]`
26910
+ : "";
26911
+ const text = field.text.replace(/\n/g, "\n ") || "(empty)";
26912
+ lines.push(` ${field.key}: ${text}${limit}${missing}`);
26913
+ }
26914
+ }
26915
+ }
26916
+ }
26917
+ if (lines.length === 0)
26918
+ lines.push("No step has copy to render.");
26919
+ if (data.note)
26920
+ lines.push("", data.note);
26921
+ const link = data.deepLink ?? data.web_url;
26922
+ if (link)
26923
+ lines.push(link);
26924
+ return `${lines.join("\n")}\n`;
26925
+ }
26840
26926
  async function handleSequenceSignalAction(sequence, options) {
26841
26927
  try {
26842
26928
  const signal = readOption(options.signal);
@@ -26943,6 +27029,22 @@ function formatSequenceEvents(data) {
26943
27029
  async function handleSequenceVariantsAction(sequence, options) {
26944
27030
  try {
26945
27031
  const path = `/api/cli/sequences/${encodeURIComponent(sequence)}/variants`;
27032
+ // One change per call: extra flags used to be dropped without a word, and a
27033
+ // lone --step read the whole scoreboard.
27034
+ const actions = [
27035
+ options.autoOptimize ? "--auto-optimize" : null,
27036
+ options.pause !== undefined ? "--pause" : null,
27037
+ options.activate !== undefined ? "--activate" : null,
27038
+ options.reset !== undefined ? "--reset" : null,
27039
+ ].filter((flag) => flag !== null);
27040
+ if (actions.length > 1) {
27041
+ throw new OxygenError("invalid_request", `Pass one of ${actions.join(", ")} per call.`, { details: { flags: actions }, exitCode: 2 });
27042
+ }
27043
+ if (options.step && (actions.length === 0 || options.autoOptimize)) {
27044
+ throw new OxygenError("invalid_request", options.autoOptimize
27045
+ ? "--auto-optimize runs on the whole sequence; drop --step."
27046
+ : "--step scopes --pause, --activate or --reset; add one of them.", { details: { step: options.step }, exitCode: 2 });
27047
+ }
26946
27048
  let body = null;
26947
27049
  if (options.autoOptimize)
26948
27050
  body = { action: "auto_optimize" };
@@ -30360,9 +30462,29 @@ function readCompanyReplyPauseOption(value, legacyScope) {
30360
30462
  // The server shallow-merges this fragment over the sequence's current settings
30361
30463
  // (apps/web .../sequences/[sequenceId]/route.ts), so a throttle-only PATCH
30362
30464
  // preserves other settings the server already holds, e.g. dispatch_mode/schedule.
30465
+ /**
30466
+ * A sequence daily cap flag: a whole number (0 is a real cap) or `none`, which
30467
+ * sends null to remove the cap, as clearing the web's Sends per day field does.
30468
+ */
30469
+ function readDailyCapOption(value, flag) {
30470
+ const trimmed = value?.trim();
30471
+ if (trimmed === undefined || trimmed === "")
30472
+ return undefined;
30473
+ if (trimmed.toLowerCase() === "none")
30474
+ return null;
30475
+ const parsed = Number(trimmed);
30476
+ if (!Number.isSafeInteger(parsed) || parsed < 0) {
30477
+ throw new OxygenError("invalid_number", `${flag} takes a whole number (0 or more) or none.`, {
30478
+ details: { flag, value },
30479
+ exitCode: 1,
30480
+ });
30481
+ }
30482
+ return parsed;
30483
+ }
30363
30484
  function readSequenceSettings(options) {
30364
30485
  const settings = {};
30365
- const maxPerMailbox = readPositiveInt(options.maxEmailsPerMailboxPerDay);
30486
+ // 0 is a real cap, as on the web's Sending limits (0-50).
30487
+ const maxPerMailbox = readNonNegativeInt(options.maxEmailsPerMailboxPerDay);
30366
30488
  if (maxPerMailbox !== undefined) {
30367
30489
  settings.max_emails_per_mailbox_per_day = maxPerMailbox;
30368
30490
  }
@@ -30374,20 +30496,18 @@ function readSequenceSettings(options) {
30374
30496
  if (linkedinSchedule) {
30375
30497
  settings.schedule = linkedinSchedule;
30376
30498
  }
30377
- if (options.whatsappColdInitiate === true) {
30378
- settings.whatsapp_cold_initiate = true;
30499
+ // Tri-state: `--no-whatsapp-cold-initiate` (update) turns the opt-in off.
30500
+ if (options.whatsappColdInitiate !== undefined) {
30501
+ settings.whatsapp_cold_initiate = options.whatsappColdInitiate;
30379
30502
  }
30380
30503
  const whatsappDuplicateScope = readOption(options.whatsappDuplicateScope);
30381
30504
  if (whatsappDuplicateScope)
30382
30505
  settings.whatsapp_duplicate_scope = whatsappDuplicateScope;
30383
- const phoneColumnKey = readOption(options.phoneColumnKey);
30384
- if (phoneColumnKey) {
30385
- settings.phone_column_key = phoneColumnKey;
30386
- }
30387
- const emailColumnKey = readOption(options.emailColumnKey);
30388
- if (emailColumnKey) {
30389
- settings.email_column_key = emailColumnKey;
30390
- }
30506
+ // "" is sent as "": back to auto-detect, as the web's Lead identifiers save it.
30507
+ if (options.phoneColumnKey !== undefined)
30508
+ settings.phone_column_key = options.phoneColumnKey.trim();
30509
+ if (options.emailColumnKey !== undefined)
30510
+ settings.email_column_key = options.emailColumnKey.trim();
30391
30511
  // stop_on_bounce defaults ON server-side; commander's `--no-stop-on-bounce`
30392
30512
  // sets this to false, so only send the key when the user explicitly opts OUT.
30393
30513
  // Leaving it absent keeps the safe default and a settings PATCH clean.
@@ -30415,10 +30535,10 @@ function readSequenceSettings(options) {
30415
30535
  const companyReplyPause = readCompanyReplyPauseOption(options.companyReplyPause, options.stopOnReplyScope);
30416
30536
  if (companyReplyPause)
30417
30537
  settings.company_reply_pause = companyReplyPause;
30418
- const maxEmailsPerDay = readPositiveInt(options.maxEmailsPerDay);
30538
+ const maxEmailsPerDay = readDailyCapOption(options.maxEmailsPerDay, "--max-emails-per-day");
30419
30539
  if (maxEmailsPerDay !== undefined)
30420
30540
  settings.max_emails_per_day = maxEmailsPerDay;
30421
- const maxNewEnrollmentsPerDay = readPositiveInt(options.maxNewEnrollmentsPerDay);
30541
+ const maxNewEnrollmentsPerDay = readDailyCapOption(options.maxNewEnrollmentsPerDay, "--max-new-enrollments-per-day");
30422
30542
  if (maxNewEnrollmentsPerDay !== undefined)
30423
30543
  settings.max_new_enrollments_per_day = maxNewEnrollmentsPerDay;
30424
30544
  const prioritization = readOption(options.sequencePrioritization);
@@ -30459,9 +30579,9 @@ function readSequenceSettings(options) {
30459
30579
  // Per-sequence sending-account allowlist (--mailboxes id1,id2). Split + trim +
30460
30580
  // drop empties client-side (light); the server validates each is a UUID and caps
30461
30581
  // the count. An all-empty list omits the key so a stray "--mailboxes ," is a no-op.
30462
- const mailboxIds = readCsvOption(options.mailboxes);
30463
- if (mailboxIds.length > 0)
30464
- settings.mailbox_ids = mailboxIds;
30582
+ // `--mailboxes ""` sends [] (no allowlist: the whole pool rotates again).
30583
+ if (options.mailboxes !== undefined)
30584
+ settings.mailbox_ids = readCsvOption(options.mailboxes);
30465
30585
  // Sender profiles (--sender-profiles id1,id2): the sequence sends from these
30466
30586
  // unified identities. The server resolves them into senderAccountIds + mailbox_ids.
30467
30587
  const senderProfileIds = readCsvOption(options.senderProfiles);
@@ -0,0 +1,115 @@
1
+ export type AgentSessionStatus = "draft" | "active" | "paused" | "budget_wait" | "completed" | "archived";
2
+ export type AgentSessionResources = {
3
+ sequences: string[];
4
+ tables: string[];
5
+ conversations: string[];
6
+ mailboxes: string[];
7
+ records: string[];
8
+ objects: string[];
9
+ knowledge: string[];
10
+ domains: string[];
11
+ };
12
+ export type AgentSessionConfig = {
13
+ title: string;
14
+ goal: string;
15
+ template: string;
16
+ resources: AgentSessionResources;
17
+ actions: string[];
18
+ schedule: {
19
+ interval_minutes: number;
20
+ min_interval_minutes: number;
21
+ max_interval_minutes: number;
22
+ };
23
+ budget: {
24
+ per_run_credits: number;
25
+ daily_credits: number;
26
+ lifetime_credits?: number;
27
+ };
28
+ ends_at?: string | undefined;
29
+ events: string[];
30
+ };
31
+ export declare function agentSessionsEnabled(env?: Record<string, string | undefined>): boolean;
32
+ export declare function agentSessionsAvailableTo(env: Record<string, string | undefined>, viewer: {
33
+ email: string | null | undefined;
34
+ member: boolean;
35
+ oxygenRole?: string | null | undefined;
36
+ }): boolean;
37
+ /** Reviewed native adapters. Unknown tools (including generic dispatch and agent management) fail closed. */
38
+ export declare const AGENT_SESSION_ADAPTERS: Readonly<Record<string, {
39
+ resource: keyof AgentSessionResources;
40
+ field: string;
41
+ read: boolean;
42
+ fixed?: string;
43
+ array?: boolean;
44
+ }>>;
45
+ export declare function isAgentSessionCapability(name: string): boolean;
46
+ export declare function agentSessionCapabilityDecision(config: AgentSessionConfig, name: string, args: Record<string, unknown>): "allow" | "ask" | "deny";
47
+ export declare const EMPTY_AGENT_SESSION_RESOURCES: AgentSessionResources;
48
+ export declare const AGENT_SESSION_EVENT_RESOURCES: {
49
+ readonly sequence_id: "sequences";
50
+ readonly table_id: "tables";
51
+ readonly conversation_id: "conversations";
52
+ readonly mailbox_id: "mailboxes";
53
+ readonly row_id: "records";
54
+ readonly record_id: "records";
55
+ };
56
+ export declare const AGENT_SESSION_EVENTS: import("./workspace-event-catalog.js").WorkspaceEventCatalogEntry[];
57
+ export declare function agentSessionEventIsScoped(config: AgentSessionConfig, payload: Record<string, unknown>): boolean;
58
+ export declare const AGENT_SESSION_TEMPLATES: readonly [{
59
+ readonly id: "campaign-monitor";
60
+ readonly title: "Campaign monitor";
61
+ readonly goal: "Watch this native campaign. Compare sends, replies and meetings with earlier reviews; explain changes, propose improvements and schedule the next review.";
62
+ readonly resources: readonly ["sequences"];
63
+ readonly actions: readonly ["oxygen_sequences_set_status"];
64
+ readonly events: readonly ["sequencer.contact_activity.any"];
65
+ }, {
66
+ readonly id: "lead-replenishment";
67
+ readonly title: "Lead replenishment";
68
+ readonly goal: "Maintain a qualified lead pool in the selected Table for this native campaign. Research gaps, deduplicate and enroll only qualified leads within the approved scope.";
69
+ readonly resources: readonly ["tables", "sequences"];
70
+ readonly actions: readonly ["oxygen_rows_upsert", "oxygen_sequences_enroll"];
71
+ readonly events: readonly [];
72
+ }, {
73
+ readonly id: "reply-handling";
74
+ readonly title: "Reply handling";
75
+ readonly goal: "Review the selected conversations, remember prior responses, qualify intent and draft replies. Send only when explicitly authorized; hand off uncertain or sensitive replies.";
76
+ readonly resources: readonly ["conversations"];
77
+ readonly actions: readonly ["oxygen_inbox_set_status"];
78
+ readonly events: readonly ["crm.email_reply_classified"];
79
+ }, {
80
+ readonly id: "deliverability";
81
+ readonly title: "Deliverability";
82
+ readonly goal: "Watch the selected mailboxes for sending health changes. Explain evidence and recommend or apply only approved cap and status adjustments.";
83
+ readonly resources: readonly ["mailboxes"];
84
+ readonly actions: readonly ["oxygen_mailboxes_set_cap", "oxygen_mailboxes_set_status"];
85
+ readonly events: readonly [];
86
+ }, {
87
+ readonly id: "account-research";
88
+ readonly title: "Account research";
89
+ readonly goal: "Maintain cited account research for the selected records and knowledge pages. Track changes and recommend next actions; keep facts separate from hypotheses.";
90
+ readonly resources: readonly ["records", "knowledge"];
91
+ readonly actions: readonly ["oxygen_crm_notes_add", "oxygen_knowledge_page_upsert"];
92
+ readonly events: readonly [];
93
+ }, {
94
+ readonly id: "inbound-qualification";
95
+ readonly title: "Inbound qualification";
96
+ readonly goal: "Qualify the selected inbound conversations and records against workspace ICP and product knowledge. Record evidence and recommendations; hand off high intent to a person.";
97
+ readonly resources: readonly ["conversations", "records"];
98
+ readonly actions: readonly ["oxygen_inbox_set_status", "oxygen_crm_notes_add"];
99
+ readonly events: readonly ["crm.email_reply_classified"];
100
+ }, {
101
+ readonly id: "crm-hygiene";
102
+ readonly title: "CRM hygiene";
103
+ readonly goal: "Review the selected CRM objects for duplicates and missing or inconsistent fields. Propose repairs with evidence, and apply only explicitly authorized changes.";
104
+ readonly resources: readonly ["objects"];
105
+ readonly actions: readonly ["oxygen_crm_merge"];
106
+ readonly events: readonly [];
107
+ }, {
108
+ readonly id: "pipeline-review";
109
+ readonly title: "Pipeline review";
110
+ readonly goal: "Review the selected pipeline and lifecycle stages. Remember earlier reviews, identify stalled opportunities and recommend the next action with evidence.";
111
+ readonly resources: readonly ["objects", "records"];
112
+ readonly actions: readonly ["oxygen_crm_notes_add"];
113
+ readonly events: readonly [];
114
+ }];
115
+ export declare function validateAgentSessionConfig(config: AgentSessionConfig, requireResources?: boolean): void;