@oxygen-agent/cli 1.1011.322 → 1.1011.405

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 (48) hide show
  1. package/README.md +1 -1
  2. package/dist/command-manifest.js +7 -0
  3. package/dist/connectors-commands.d.ts +4 -1
  4. package/dist/connectors-commands.js +81 -10
  5. package/dist/contract-flags.d.ts +2 -0
  6. package/dist/contract-flags.js +4 -2
  7. package/dist/crm-support-flags.d.ts +11 -0
  8. package/dist/crm-support-flags.js +60 -0
  9. package/dist/help.js +10 -0
  10. package/dist/index.js +175 -65
  11. package/dist/settings-commands.d.ts +11 -0
  12. package/dist/settings-commands.js +72 -0
  13. package/node_modules/@oxygen/shared/dist/bounded-read-result.d.ts +4 -0
  14. package/node_modules/@oxygen/shared/dist/bounded-read-result.js +25 -0
  15. package/node_modules/@oxygen/shared/dist/capability-discovery.d.ts +30 -0
  16. package/node_modules/@oxygen/shared/dist/capability-discovery.js +277 -16
  17. package/node_modules/@oxygen/shared/dist/contracts/connectors.d.ts +32 -6
  18. package/node_modules/@oxygen/shared/dist/contracts/connectors.js +56 -4
  19. package/node_modules/@oxygen/shared/dist/contracts/crm-support.d.ts +117 -0
  20. package/node_modules/@oxygen/shared/dist/contracts/crm-support.js +43 -0
  21. package/node_modules/@oxygen/shared/dist/contracts/dashboard-tabs.d.ts +3 -0
  22. package/node_modules/@oxygen/shared/dist/contracts/dashboard-tabs.js +2 -0
  23. package/node_modules/@oxygen/shared/dist/contracts/integration-connect.d.ts +4 -0
  24. package/node_modules/@oxygen/shared/dist/contracts/integration-connect.js +7 -0
  25. package/node_modules/@oxygen/shared/dist/contracts/operations.js +8 -2
  26. package/node_modules/@oxygen/shared/dist/contracts/workspace-pages.d.ts +16 -0
  27. package/node_modules/@oxygen/shared/dist/contracts/workspace-pages.js +37 -0
  28. package/node_modules/@oxygen/shared/dist/decision-ledger-parser.d.ts +32 -0
  29. package/node_modules/@oxygen/shared/dist/decision-ledger-parser.js +303 -0
  30. package/node_modules/@oxygen/shared/dist/decision-ledger.d.ts +10 -0
  31. package/node_modules/@oxygen/shared/dist/decision-ledger.generated.d.ts +2 -0
  32. package/node_modules/@oxygen/shared/dist/decision-ledger.generated.js +717 -0
  33. package/node_modules/@oxygen/shared/dist/decision-ledger.js +20 -0
  34. package/node_modules/@oxygen/shared/dist/mcp-event-contract.d.ts +10 -7
  35. package/node_modules/@oxygen/shared/dist/mcp-event-contract.js +11 -7
  36. package/node_modules/@oxygen/shared/dist/mcp-table-widget.d.ts +50 -0
  37. package/node_modules/@oxygen/shared/dist/mcp-table-widget.js +24 -0
  38. package/node_modules/@oxygen/shared/dist/plan-band.d.ts +8 -0
  39. package/node_modules/@oxygen/shared/dist/plan-band.js +12 -0
  40. package/node_modules/@oxygen/shared/dist/plan-limits.d.ts +21 -0
  41. package/node_modules/@oxygen/shared/dist/plan-limits.js +41 -0
  42. package/node_modules/@oxygen/shared/dist/sequences.d.ts +33 -4
  43. package/node_modules/@oxygen/shared/dist/sequences.js +48 -13
  44. package/node_modules/@oxygen/shared/dist/user-capability-routing.js +17 -0
  45. package/node_modules/@oxygen/shared/dist/version.generated.d.ts +1 -1
  46. package/node_modules/@oxygen/shared/dist/version.generated.js +1 -1
  47. package/node_modules/@oxygen/shared/package.json +5 -0
  48. package/package.json +1 -1
@@ -0,0 +1,20 @@
1
+ import { DECISION_LEDGER_ENTRIES } from "./decision-ledger.generated.js";
2
+ export { parseDecisionLedger } from "./decision-ledger-parser.js";
3
+ const DECISIONS_BY_ID = new Map(DECISION_LEDGER_ENTRIES.map((entry) => [entry.id, entry]));
4
+ /**
5
+ * The ledger entry for a decision id. An exact id wins ("D34a" is its own
6
+ * decision); otherwise an answer-option suffix resolves to its base decision
7
+ * ("D19a" and "D19(a)" are D19's option (a)).
8
+ */
9
+ export function getDecision(id) {
10
+ const key = id.trim();
11
+ const exact = DECISIONS_BY_ID.get(key);
12
+ if (exact)
13
+ return exact;
14
+ const base = /^(D\d+)(?:[a-z]|\([a-z]\))$/.exec(key)?.[1];
15
+ return base ? DECISIONS_BY_ID.get(base) ?? null : null;
16
+ }
17
+ /** Every ledger entry, sorted by decision id. */
18
+ export function listDecisions() {
19
+ return DECISION_LEDGER_ENTRIES;
20
+ }
@@ -16,12 +16,18 @@ export type McpReplyClassification = (typeof MCP_REPLY_CLASSIFICATIONS)[number];
16
16
  export declare const MCP_REPLY_CHANNELS: readonly ["email", "linkedin", "whatsapp"];
17
17
  /** How an enrollment ended: the Sequencer's own terminal statuses. */
18
18
  export declare const MCP_ENROLLMENT_OUTCOMES: readonly ["completed", "replied", "stopped", "failed", "excluded"];
19
- /** The run ledgers `run_outcome_analytics` reads (`SettledRunKind` in @oxygen/tenant-db). */
20
- export declare const MCP_RUN_KINDS: readonly ["workflow", "table_action", "table_ingestion", "agent", "copilot_turn"];
19
+ /**
20
+ * The durable-run ledgers `run_outcome_analytics` reads (`SettledRunKind` in
21
+ * @oxygen/tenant-db), without Workspace Copilot turns: a turn is attended and
22
+ * private to its member, the own-action guard already drops a subscriber's own
23
+ * turns, and delivering other members' turns would expose private activity
24
+ * with deep links the subscriber may not be able to open.
25
+ */
26
+ export declare const MCP_RUN_KINDS: readonly ["workflow", "table_action", "table_ingestion", "agent"];
21
27
  export type McpRunKind = (typeof MCP_RUN_KINDS)[number];
22
28
  /**
23
29
  * Every terminal status those ledgers write, with one spelling of cancel:
24
- * Workflow and Table runs write `canceled`, Agent runs and Copilot turns `cancelled`.
30
+ * Workflow and Table runs write `canceled`, Agent runs `cancelled`.
25
31
  */
26
32
  export declare const MCP_RUN_STATUSES: readonly ["completed", "completed_with_errors", "failed", "canceled", "budget_exhausted", "effect_unknown"];
27
33
  export type McpRunStatus = (typeof MCP_RUN_STATUSES)[number];
@@ -285,7 +291,7 @@ declare const RECORD_STAGE_CHANGED_PAYLOAD: ContractObject<{
285
291
  declare const RUN_SETTLED_PAYLOAD: ContractObject<{
286
292
  readonly kind: {
287
293
  type: "string";
288
- enum: readonly ("agent" | "copilot_turn" | "table_action" | "table_ingestion" | "workflow")[];
294
+ enum: readonly ("agent" | "table_action" | "table_ingestion" | "workflow")[];
289
295
  description?: string;
290
296
  };
291
297
  readonly status: {
@@ -574,9 +580,6 @@ export type McpRunSettledSource = McpRunSettledCommon & ({
574
580
  } | {
575
581
  kind: "agent";
576
582
  agentSlug: string;
577
- } | {
578
- kind: "copilot_turn";
579
- copilotSessionId: string;
580
583
  });
581
584
  /** A settled run as `run.settled`. Its producer is OXP-11.5.21. */
582
585
  export declare function projectRunSettledEvent(source: McpRunSettledSource, context?: McpEventLinkOptions): McpRunSettledPayload;
@@ -59,11 +59,17 @@ const REPLY_LABELS_DELIVERED_AS_IS = new Set([
59
59
  export const MCP_REPLY_CHANNELS = ["email", "linkedin", "whatsapp"];
60
60
  /** How an enrollment ended: the Sequencer's own terminal statuses. */
61
61
  export const MCP_ENROLLMENT_OUTCOMES = SEQUENCE_TERMINAL_STATUSES;
62
- /** The run ledgers `run_outcome_analytics` reads (`SettledRunKind` in @oxygen/tenant-db). */
63
- export const MCP_RUN_KINDS = ["workflow", "table_action", "table_ingestion", "agent", "copilot_turn"];
62
+ /**
63
+ * The durable-run ledgers `run_outcome_analytics` reads (`SettledRunKind` in
64
+ * @oxygen/tenant-db), without Workspace Copilot turns: a turn is attended and
65
+ * private to its member, the own-action guard already drops a subscriber's own
66
+ * turns, and delivering other members' turns would expose private activity
67
+ * with deep links the subscriber may not be able to open.
68
+ */
69
+ export const MCP_RUN_KINDS = ["workflow", "table_action", "table_ingestion", "agent"];
64
70
  /**
65
71
  * Every terminal status those ledgers write, with one spelling of cancel:
66
- * Workflow and Table runs write `canceled`, Agent runs and Copilot turns `cancelled`.
72
+ * Workflow and Table runs write `canceled`, Agent runs `cancelled`.
67
73
  */
68
74
  export const MCP_RUN_STATUSES = [
69
75
  "completed",
@@ -160,7 +166,7 @@ const RUN_SETTLED_INPUT = obj({
160
166
  });
161
167
  const RUN_SETTLED_PAYLOAD = payloadObject({
162
168
  kind: enumOf(MCP_RUN_KINDS, {
163
- description: "What ran: a Workflow run, a Table action run, a Table import (`table_ingestion`), an Agent run or a Workspace Copilot turn.",
169
+ description: "What ran: a Workflow run, a Table action run, a Table import (`table_ingestion`) or an Agent run.",
164
170
  }),
165
171
  status: enumOf(MCP_RUN_STATUSES, {
166
172
  description: "How it ended. An Agent run can also end `budget_exhausted` or `effect_unknown`.",
@@ -189,7 +195,7 @@ export const MCP_EVENT_DEFINITIONS = [
189
195
  eventDefinition("enrollment.ended", "A lead's enrollment in an OXYGEN Sequence ended: the lead replied, every step was sent, the enrollment was stopped or excluded, or sending failed.", ENROLLMENT_ENDED_INPUT, ENROLLMENT_ENDED_PAYLOAD),
190
196
  eventDefinition("meeting.completed", "OXYGEN's Meeting Notetaker finished processing a recorded meeting; its transcript and meeting note are ready in OXYGEN.", MEETING_COMPLETED_INPUT, MEETING_COMPLETED_PAYLOAD),
191
197
  eventDefinition("record.stage_changed", "A CRM record's stage changed in OXYGEN, such as a person's lead stage, a company's account stage or a deal's pipeline stage.", RECORD_STAGE_CHANGED_INPUT, RECORD_STAGE_CHANGED_PAYLOAD),
192
- eventDefinition("run.settled", "A run in OXYGEN reached its final status: a Workflow run, a Table action run or import, an Agent run, or a Workspace Copilot turn.", RUN_SETTLED_INPUT, RUN_SETTLED_PAYLOAD),
198
+ eventDefinition("run.settled", "A run in OXYGEN reached its final status: a Workflow run, a Table action run or import, or an Agent run.", RUN_SETTLED_INPUT, RUN_SETTLED_PAYLOAD),
193
199
  eventDefinition("approval.requested", "A Workflow run or an Agent run in OXYGEN paused and is waiting for a person to approve its next step.", APPROVAL_REQUESTED_INPUT, APPROVAL_REQUESTED_PAYLOAD),
194
200
  ];
195
201
  const DEFINITIONS_BY_NAME = new Map(MCP_EVENT_DEFINITIONS.map((entry) => [entry.name, entry]));
@@ -574,8 +580,6 @@ function runPath(source) {
574
580
  return `/tables/${encodeURIComponent(source.tableId)}/ingestions/${encodeURIComponent(source.runId)}`;
575
581
  case "agent":
576
582
  return agentRunPath(source.agentSlug, source.runId);
577
- case "copilot_turn":
578
- return `/copilot/${encodeURIComponent(source.copilotSessionId)}`;
579
583
  }
580
584
  }
581
585
  // Every catalogue projection first proves the row is that event, so a reader
@@ -0,0 +1,50 @@
1
+ export declare const TABLE_WIDGET_NAME = "oxygen.table";
2
+ /** A run filling the Table, shown as progress until it reaches a terminal state. */
3
+ export type TableWidgetRun = {
4
+ kind: "ingestion" | "column_run";
5
+ id: string;
6
+ status: string;
7
+ terminal: boolean;
8
+ /** Settled / total work items in this run (chunks for an ingestion), not Table rows. */
9
+ rowsDone?: number | null;
10
+ rowsTarget?: number | null;
11
+ creditsUsed?: number | null;
12
+ creditsCap?: number | null;
13
+ };
14
+ export type TableWidgetGridPage = {
15
+ /** SerializedWorkspaceColumn[] from the web grid serializer. */
16
+ columns: unknown[];
17
+ /** SerializedWorkspaceTableRow[] from the web grid serializer. */
18
+ rows: unknown[];
19
+ cellStates?: Record<string, Record<string, unknown>> | null;
20
+ rowCount?: number | null;
21
+ hasMore?: boolean;
22
+ nextCursor?: string | null;
23
+ /**
24
+ * SerializedWorkspaceColumnStats[], the grid's column stats: what it
25
+ * collapses as empty, its header fill lines and run bars. Only a first page
26
+ * carries them, and only when they answered in time.
27
+ */
28
+ columnStats?: unknown[] | null;
29
+ /**
30
+ * An empty first page's fill state, the body of GET
31
+ * /api/tables/<id>/fill-progress: rows still being written, a failed fill,
32
+ * or neither.
33
+ */
34
+ fillProgress?: Record<string, unknown> | null;
35
+ };
36
+ export type TableWidgetPayload = {
37
+ version: 1;
38
+ /** "table": rows of a saved Table. "preview": sample rows of a search that saved nothing yet. */
39
+ source: "table" | "preview";
40
+ title: string;
41
+ /** The counts line under the title, e.g. "1,840 matches · 0 credits". */
42
+ summary?: string | null;
43
+ /** The Table these rows belong to; absent for a preview. Paging and live progress read it. */
44
+ tableId?: string | null;
45
+ webUrl?: string | null;
46
+ grid: TableWidgetGridPage;
47
+ run?: TableWidgetRun | null;
48
+ };
49
+ /** The payload inside a tool result, or null when the result carries none (a refusal, a filter list). */
50
+ export declare function readTableWidgetPayload(result: unknown): TableWidgetPayload | null;
@@ -0,0 +1,24 @@
1
+ // What an MCP tool hands the OXYGEN Table widget. The tool puts it in its
2
+ // result's `_meta["oxygen/widget"].data`; MCP Apps hosts pass result `_meta` to
3
+ // the widget and not to the model, so the grid page the widget draws costs the
4
+ // model no context. The widget renders the page with the web grid's own
5
+ // components, so `grid` carries the web grid's serialization unchanged
6
+ // (columns, rows, cell states as GET /api/tables/<id>/rows returns them).
7
+ export const TABLE_WIDGET_NAME = "oxygen.table";
8
+ function isRecord(value) {
9
+ return typeof value === "object" && value !== null && !Array.isArray(value);
10
+ }
11
+ /** The payload inside a tool result, or null when the result carries none (a refusal, a filter list). */
12
+ export function readTableWidgetPayload(result) {
13
+ if (!isRecord(result) || !isRecord(result._meta))
14
+ return null;
15
+ const widget = result._meta["oxygen/widget"];
16
+ if (!isRecord(widget) || widget.name !== TABLE_WIDGET_NAME || !isRecord(widget.data))
17
+ return null;
18
+ const data = widget.data;
19
+ if (data.version !== 1 || typeof data.title !== "string" || !isRecord(data.grid))
20
+ return null;
21
+ if (!Array.isArray(data.grid.columns) || !Array.isArray(data.grid.rows))
22
+ return null;
23
+ return data;
24
+ }
@@ -84,6 +84,14 @@ export type PlanSpendResolution = {
84
84
  * is never clamped by this.
85
85
  */
86
86
  export declare function resolveDefaultTriggerRunCreditCeilingForPlan(resolution: PlanSpendResolution, options?: RepricingOptions): number | null;
87
+ /**
88
+ * The default rolling 24-hour credit ceiling of one connected app (an MCP OAuth
89
+ * connection such as ChatGPT) while it holds MCP event subscriptions (ADR 0030
90
+ * §11, decision D3): the trigger default above, so the two cannot drift.
91
+ * Enterprise has no trigger default and uses the largest rung's, the way Agent
92
+ * run defaults do; a connection is never uncapped by default.
93
+ */
94
+ export declare function resolveDefaultConnectionCreditCeilingForPlan(resolution: PlanSpendResolution, options?: RepricingOptions): number;
87
95
  /** The same default for one standing or webhook table auto-run batch. */
88
96
  export declare function resolveDefaultAutoRunBatchCreditCeilingForPlan(resolution: PlanSpendResolution, options?: RepricingOptions): number | null;
89
97
  /**
@@ -183,6 +183,18 @@ function resolveDeliveryCreditCeiling(resolution, preRepricingTable, options) {
183
183
  export function resolveDefaultTriggerRunCreditCeilingForPlan(resolution, options = {}) {
184
184
  return resolveDeliveryCreditCeiling(resolution, DEFAULT_TRIGGER_RUN_CREDIT_CEILING, options);
185
185
  }
186
+ /**
187
+ * The default rolling 24-hour credit ceiling of one connected app (an MCP OAuth
188
+ * connection such as ChatGPT) while it holds MCP event subscriptions (ADR 0030
189
+ * §11, decision D3): the trigger default above, so the two cannot drift.
190
+ * Enterprise has no trigger default and uses the largest rung's, the way Agent
191
+ * run defaults do; a connection is never uncapped by default.
192
+ */
193
+ export function resolveDefaultConnectionCreditCeilingForPlan(resolution, options = {}) {
194
+ return resolveDefaultTriggerRunCreditCeilingForPlan(resolution, options)
195
+ ?? resolveDefaultTriggerRunCreditCeilingForPlan(bandSpendResolution("1999"), options)
196
+ ?? 0;
197
+ }
186
198
  /** The same default for one standing or webhook table auto-run batch. */
187
199
  export function resolveDefaultAutoRunBatchCreditCeilingForPlan(resolution, options = {}) {
188
200
  return resolveDeliveryCreditCeiling(resolution, DEFAULT_AUTO_RUN_BATCH_CREDIT_CEILING, options);
@@ -844,6 +844,27 @@ export declare function assertAgentRunCeilingApproval(input: {
844
844
  }): {
845
845
  aboveDefault: boolean;
846
846
  };
847
+ /** `details.approval_kind` when a connected app's ceiling above its default needs approval. */
848
+ export declare const CONNECTION_CEILING_ABOVE_DEFAULT_APPROVAL_KIND = "connection_ceiling_above_default";
849
+ /**
850
+ * The same approval posture for a connected app's rolling 24-hour credit
851
+ * ceiling (ADR 0030 §11): at or under the default (the plan-sized trigger
852
+ * default) nothing more to approve; above it only with the explicit
853
+ * above-default approval, whose flag and refusal shape match the Agent run and
854
+ * trigger ceilings; above the platform maximum never. `defaultCredits` is the
855
+ * lowest default among the workspaces the app can spend in, so the approval
856
+ * covers every one of them.
857
+ */
858
+ export declare function assertConnectionCreditCeilingApproval(input: {
859
+ creditCeiling: number;
860
+ defaultCredits: number;
861
+ approveAboveDefault: boolean;
862
+ appName: string;
863
+ /** The workspace whose default is the one compared, named in the refusal. */
864
+ workspaceName?: string;
865
+ }): {
866
+ aboveDefault: boolean;
867
+ };
847
868
  /**
848
869
  * The ceilings one run is held to, for the worker and every budget check that
849
870
  * reads a run's authority. A run with a recorded above-default approval is
@@ -419,6 +419,47 @@ export function assertAgentRunCeilingApproval(input) {
419
419
  },
420
420
  });
421
421
  }
422
+ /** `details.approval_kind` when a connected app's ceiling above its default needs approval. */
423
+ export const CONNECTION_CEILING_ABOVE_DEFAULT_APPROVAL_KIND = "connection_ceiling_above_default";
424
+ /**
425
+ * The same approval posture for a connected app's rolling 24-hour credit
426
+ * ceiling (ADR 0030 §11): at or under the default (the plan-sized trigger
427
+ * default) nothing more to approve; above it only with the explicit
428
+ * above-default approval, whose flag and refusal shape match the Agent run and
429
+ * trigger ceilings; above the platform maximum never. `defaultCredits` is the
430
+ * lowest default among the workspaces the app can spend in, so the approval
431
+ * covers every one of them.
432
+ */
433
+ export function assertConnectionCreditCeilingApproval(input) {
434
+ const subject = `${input.appName}'s rolling 24-hour credit ceiling`;
435
+ const where = input.workspaceName ? ` in ${input.workspaceName}` : "";
436
+ if (input.creditCeiling > AGENT_RUN_APPROVED_CEILING_MAX_CREDITS) {
437
+ throw new OxygenError("invalid_request", `credit_ceiling ${input.creditCeiling} is above the platform maximum of ${AGENT_RUN_APPROVED_CEILING_MAX_CREDITS} credits for ${subject}.`, {
438
+ exitCode: 2,
439
+ details: {
440
+ requested_credit_ceiling: input.creditCeiling,
441
+ default_credit_ceiling: input.defaultCredits,
442
+ platform_max_credits: AGENT_RUN_APPROVED_CEILING_MAX_CREDITS,
443
+ },
444
+ });
445
+ }
446
+ if (input.creditCeiling <= input.defaultCredits)
447
+ return { aboveDefault: false };
448
+ if (input.approveAboveDefault)
449
+ return { aboveDefault: true };
450
+ throw new OxygenError("approval_required", `credit_ceiling ${input.creditCeiling} is above the plan default of ${input.defaultCredits} credits${where} for ${subject}. Approve the higher ceiling with approve_above_default=true, or set ${input.defaultCredits} or less.`, {
451
+ exitCode: 7,
452
+ details: {
453
+ approval_kind: CONNECTION_CEILING_ABOVE_DEFAULT_APPROVAL_KIND,
454
+ scope: "connection",
455
+ requested_credit_ceiling: input.creditCeiling,
456
+ default_credit_ceiling: input.defaultCredits,
457
+ ...(input.workspaceName ? { default_workspace_name: input.workspaceName } : {}),
458
+ platform_max_credits: AGENT_RUN_APPROVED_CEILING_MAX_CREDITS,
459
+ next_step: `Re-send with approve_above_default=true (CLI: --approve-above-default) to approve up to ${input.creditCeiling} credits a day for ${input.appName}.`,
460
+ },
461
+ });
462
+ }
422
463
  /**
423
464
  * The ceilings one run is held to, for the worker and every budget check that
424
465
  * reads a run's authority. A run with a recorded above-default approval is
@@ -570,6 +570,13 @@ export type SequenceEmailSendStep = {
570
570
  kind: "email_send";
571
571
  /** Advanced: send this step from this exact inbox instead of the lead's normal assignment. */
572
572
  sender_mailbox_id?: string;
573
+ /**
574
+ * Advanced: send this step from any inbox of this sender profile (person)
575
+ * instead of the lead's normal assignment. Each lead gets one of that
576
+ * person's inboxes and keeps it for every step naming the same person. Set
577
+ * this or sender_mailbox_id, never both.
578
+ */
579
+ sender_profile_id?: string;
573
580
  /**
574
581
  * Subject line. Supports {{column}} interpolation.
575
582
  *
@@ -945,10 +952,13 @@ export type SequenceLintIssue = {
945
952
  * code as its launch blocker and the preview's `blocked` marker). A blind MCP run
946
953
  * pinned the founder on the connection request alone and learned why the
947
954
  * draft could not launch only from the start preview (2026-09-29).
955
+ * - `sequence_step_sender_ignored`: a sender field on a step kind that does not
956
+ * take it is not stored. A blind CLI run put `sender_profile_id` on an email
957
+ * step before email took one; it was dropped without a word (2026-09-30).
948
958
  */
949
959
  export type SequenceDefinitionNotice = {
950
960
  path: string;
951
- code: "email_reply_rewritten" | "sequence_linkedin_owner_conflict";
961
+ code: "email_reply_rewritten" | "sequence_linkedin_owner_conflict" | "sequence_step_sender_ignored";
952
962
  message: string;
953
963
  };
954
964
  export type ValidateSequenceOptions = {
@@ -1009,12 +1019,31 @@ export type SequenceMissingCopy = {
1009
1019
  * stored under `draft`. Empty means nothing to send, so starting would
1010
1020
  * either dispatch a blank message or fail per lead at send time.
1011
1021
  */
1022
+ /**
1023
+ * Who an email step names as its From: one exact inbox, any inbox of one
1024
+ * person, or null for the lead's own assignment.
1025
+ */
1026
+ export type SequenceEmailStepSender = {
1027
+ kind: "mailbox";
1028
+ mailboxId: string;
1029
+ } | {
1030
+ kind: "person";
1031
+ senderProfileId: string;
1032
+ } | null;
1033
+ export declare function sequenceEmailStepSender(step: SequenceStep): SequenceEmailStepSender;
1034
+ /** One comparable value per From rule, so a change of rule is a change of thread. */
1035
+ export declare function sequenceEmailStepSenderKey(step: SequenceStep): string | undefined;
1036
+ /** Whether any email step follows the lead's own assigned person and inbox. */
1037
+ export declare function sequenceHasDefaultEmailStep(steps: readonly {
1038
+ kind: string;
1039
+ }[]): boolean;
1012
1040
  /**
1013
1041
  * Threaded email steps that would continue another inbox's thread. A provider
1014
1042
  * thread belongs to the mailbox that opened it, so a step that changes From —
1015
- * an exact-inbox step, or the return to the lead's assigned inbox after one —
1016
- * must open a new thread under its own subject. Checked on save and again at
1017
- * launch, because a draft may be saved half-written.
1043
+ * an exact-inbox or person step, or the return to the lead's assigned inbox
1044
+ * after one — must open a new thread under its own subject. Consecutive steps
1045
+ * naming the same person keep one inbox per lead, so they may thread. Checked
1046
+ * on save and again at launch, because a draft may be saved half-written.
1018
1047
  */
1019
1048
  export declare function sequenceSenderThreadConflicts(steps: readonly SequenceStep[]): {
1020
1049
  index: number;
@@ -1027,17 +1027,33 @@ export function sequenceChannels(definition) {
1027
1027
  }
1028
1028
  return [...channels];
1029
1029
  }
1030
- /**
1031
- * Steps that still have empty copy — the launch gate for definitions stored
1032
- * stored under `draft`. Empty means nothing to send, so starting would
1033
- * either dispatch a blank message or fail per lead at send time.
1034
- */
1030
+ export function sequenceEmailStepSender(step) {
1031
+ if (step.kind !== "email_send")
1032
+ return null;
1033
+ if (step.sender_mailbox_id)
1034
+ return { kind: "mailbox", mailboxId: step.sender_mailbox_id };
1035
+ if (step.sender_profile_id)
1036
+ return { kind: "person", senderProfileId: step.sender_profile_id };
1037
+ return null;
1038
+ }
1039
+ /** One comparable value per From rule, so a change of rule is a change of thread. */
1040
+ export function sequenceEmailStepSenderKey(step) {
1041
+ const sender = sequenceEmailStepSender(step);
1042
+ if (!sender)
1043
+ return undefined;
1044
+ return sender.kind === "mailbox" ? `mailbox:${sender.mailboxId}` : `person:${sender.senderProfileId}`;
1045
+ }
1046
+ /** Whether any email step follows the lead's own assigned person and inbox. */
1047
+ export function sequenceHasDefaultEmailStep(steps) {
1048
+ return steps.some((step) => (step.kind === "email_send" || step.kind === "email_reply") && sequenceEmailStepSender(step) === null);
1049
+ }
1035
1050
  /**
1036
1051
  * Threaded email steps that would continue another inbox's thread. A provider
1037
1052
  * thread belongs to the mailbox that opened it, so a step that changes From —
1038
- * an exact-inbox step, or the return to the lead's assigned inbox after one —
1039
- * must open a new thread under its own subject. Checked on save and again at
1040
- * launch, because a draft may be saved half-written.
1053
+ * an exact-inbox or person step, or the return to the lead's assigned inbox
1054
+ * after one — must open a new thread under its own subject. Consecutive steps
1055
+ * naming the same person keep one inbox per lead, so they may thread. Checked
1056
+ * on save and again at launch, because a draft may be saved half-written.
1041
1057
  */
1042
1058
  export function sequenceSenderThreadConflicts(steps) {
1043
1059
  const conflicts = [];
@@ -1046,12 +1062,12 @@ export function sequenceSenderThreadConflicts(steps) {
1046
1062
  steps.forEach((step, index) => {
1047
1063
  if (step.kind !== "email_send" && step.kind !== "email_reply")
1048
1064
  return;
1049
- const sender = step.kind === "email_send" ? step.sender_mailbox_id : undefined;
1065
+ const sender = sequenceEmailStepSenderKey(step);
1050
1066
  if (seenEmail && sequenceEmailStepThreads(step) && sender !== precedingSender) {
1051
1067
  conflicts.push({
1052
1068
  index,
1053
1069
  stepId: step.id,
1054
- message: `step '${step.id}' changes the sending inbox. Give it a new subject to start a new thread.`,
1070
+ message: `step '${step.id}' changes who sends it. Give it a new subject to start a new thread.`,
1055
1071
  });
1056
1072
  }
1057
1073
  seenEmail = true;
@@ -1064,7 +1080,7 @@ export const LINKEDIN_RELATIONSHIP_STEP_KINDS = new Set(["invite", "message", "i
1064
1080
  /** LinkedIn light touches: they bind nothing, so each may come from any selected person. */
1065
1081
  export const LINKEDIN_LIGHT_TOUCH_STEP_KINDS = new Set(["visit_profile", "follow", "like_post", "comment_post"]);
1066
1082
  const LINKEDIN_SENDER_STEP_KINDS = new Set([...LINKEDIN_RELATIONSHIP_STEP_KINDS, ...LINKEDIN_LIGHT_TOUCH_STEP_KINDS]);
1067
- function normalizeLinkedInStepSender(value, path, issues) {
1083
+ function normalizeStepSenderProfile(value, path, issues) {
1068
1084
  if (value === undefined || value === null || value === "")
1069
1085
  return {};
1070
1086
  if (typeof value !== "string" || !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value.trim())) {
@@ -1409,8 +1425,20 @@ raw, index, options, issues) {
1409
1425
  issues.push({ path: `${path}.channel`, message: `channel '${channel}' is not enabled for this sequence.` });
1410
1426
  }
1411
1427
  const linkedInSender = LINKEDIN_SENDER_STEP_KINDS.has(stepKind)
1412
- ? normalizeLinkedInStepSender(raw.sender_profile_id, `${path}.sender_profile_id`, issues)
1428
+ ? normalizeStepSenderProfile(raw.sender_profile_id, `${path}.sender_profile_id`, issues)
1413
1429
  : {};
1430
+ // A sender field this kind does not take is not stored: say so rather than
1431
+ // let the definition read back without it.
1432
+ for (const field of ["sender_profile_id", "sender_mailbox_id"]) {
1433
+ const takes = stepKind === "email_send" || (field === "sender_profile_id" && LINKEDIN_SENDER_STEP_KINDS.has(stepKind));
1434
+ if (!takes && raw[field] !== undefined && raw[field] !== null && raw[field] !== "") {
1435
+ options.notices?.push({
1436
+ path: `${path}.${field}`,
1437
+ code: "sequence_step_sender_ignored",
1438
+ message: `step '${id}': ${stepKind} steps do not take ${field}, so it was not saved. Email steps take sender_profile_id (any inbox of that person) or sender_mailbox_id (one exact inbox); LinkedIn steps take sender_profile_id.`,
1439
+ });
1440
+ }
1441
+ }
1414
1442
  switch (stepKind) {
1415
1443
  case "visit_profile":
1416
1444
  return { id, channel: "linkedin", kind: "visit_profile", ...linkedInSender };
@@ -1489,9 +1517,16 @@ raw, index, options, issues) {
1489
1517
  if (senderMailboxId && !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(senderMailboxId)) {
1490
1518
  issues.push({ path: `${path}.sender_mailbox_id`, message: "must be an inbox id." });
1491
1519
  }
1520
+ const senderPerson = normalizeStepSenderProfile(raw.sender_profile_id, `${path}.sender_profile_id`, issues);
1521
+ if (senderMailboxId && senderPerson.sender_profile_id) {
1522
+ issues.push({
1523
+ path: `${path}.sender_profile_id`,
1524
+ message: "set sender_profile_id (any inbox of that person) or sender_mailbox_id (one exact inbox), not both.",
1525
+ });
1526
+ }
1492
1527
  return {
1493
1528
  id, channel: "email", kind: "email_send",
1494
- ...(senderMailboxId ? { sender_mailbox_id: senderMailboxId } : {}),
1529
+ ...(senderMailboxId ? { sender_mailbox_id: senderMailboxId } : senderPerson),
1495
1530
  ...(threaded ? {} : { subject_template: subject }),
1496
1531
  body_template: body ?? "",
1497
1532
  ...(bodyHtml !== undefined ? { body_html_template: bodyHtml } : {}),
@@ -5,6 +5,15 @@ import { getCapabilityRoute, getCapabilityRouteMatch, inferCapabilityRoute, } fr
5
5
  // the user-facing boundary while preserving table joins and channel-specific
6
6
  // connection routes.
7
7
  export function inferUserCapabilityRoute(query) {
8
+ if (isProviderCredentialSetupIntent(query.toLowerCase())) {
9
+ const setup = getCapabilityRouteMatch("connected-integrations");
10
+ if (setup)
11
+ return {
12
+ ...setup,
13
+ recommendedTools: ["oxygen_integrations_connect", ...setup.recommendedTools.filter((tool) => tool !== "oxygen_integrations_connect")],
14
+ recommendedCommands: ["integrations connect", ...setup.recommendedCommands.filter((command) => command !== "integrations connect")],
15
+ };
16
+ }
8
17
  const route = inferCapabilityRoute(query);
9
18
  const providerConnectionIntent = isSimpleProviderConnectionIntent(query);
10
19
  if (!providerConnectionIntent
@@ -55,3 +64,11 @@ function isSimpleProviderConnectionIntent(query) {
55
64
  return !new Set(["this", "that", "them", "us", "me", "account", "workspace"])
56
65
  .has(match[1] ?? "");
57
66
  }
67
+ /** Provider credential setup, rather than workspace keys or provider data actions. */
68
+ function isProviderCredentialSetupIntent(query) {
69
+ const provider = /\b(providers?|integrations?|cloudflare|bounceban)\b/.test(query);
70
+ const credential = /\b(keys?|tokens?|credentials?|byok|bring your own key)\b/.test(query);
71
+ const setup = /\b(connect\w*|reconnect\w*|set up|setup|configure|replace|rotate|check|validate|test)\b/.test(query);
72
+ const cloudflareAccountChoice = /\bcloudflare\b/.test(query) && /\baccount\b/.test(query) && /\b(select|choose|pick)\b/.test(query);
73
+ return (provider && credential && setup) || cloudflareAccountChoice;
74
+ }
@@ -1 +1 @@
1
- export declare const OXYGEN_BUILD_VERSION = "1.1011.322";
1
+ export declare const OXYGEN_BUILD_VERSION = "1.1011.405";
@@ -1,2 +1,2 @@
1
1
  // GENERATED by scripts/ci/version-stamp.mjs from VERSION + git history. Do not edit; do not commit.
2
- export const OXYGEN_BUILD_VERSION = "1.1011.322";
2
+ export const OXYGEN_BUILD_VERSION = "1.1011.405";
@@ -276,6 +276,11 @@
276
276
  "import": "./dist/enrichment-intents.js",
277
277
  "default": "./dist/enrichment-intents.js"
278
278
  },
279
+ "./decision-ledger": {
280
+ "types": "./dist/decision-ledger.d.ts",
281
+ "import": "./dist/decision-ledger.js",
282
+ "default": "./dist/decision-ledger.js"
283
+ },
279
284
  "./log": {
280
285
  "types": "./dist/log.d.ts",
281
286
  "import": "./dist/log.js",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxygen-agent/cli",
3
- "version": "1.1011.322",
3
+ "version": "1.1011.405",
4
4
  "private": false,
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",