@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.
- package/README.md +1 -1
- package/dist/command-manifest.js +7 -0
- package/dist/connectors-commands.d.ts +4 -1
- package/dist/connectors-commands.js +81 -10
- package/dist/contract-flags.d.ts +2 -0
- package/dist/contract-flags.js +4 -2
- package/dist/crm-support-flags.d.ts +11 -0
- package/dist/crm-support-flags.js +60 -0
- package/dist/help.js +10 -0
- package/dist/index.js +175 -65
- package/dist/settings-commands.d.ts +11 -0
- package/dist/settings-commands.js +72 -0
- package/node_modules/@oxygen/shared/dist/bounded-read-result.d.ts +4 -0
- package/node_modules/@oxygen/shared/dist/bounded-read-result.js +25 -0
- package/node_modules/@oxygen/shared/dist/capability-discovery.d.ts +30 -0
- package/node_modules/@oxygen/shared/dist/capability-discovery.js +277 -16
- package/node_modules/@oxygen/shared/dist/contracts/connectors.d.ts +32 -6
- package/node_modules/@oxygen/shared/dist/contracts/connectors.js +56 -4
- package/node_modules/@oxygen/shared/dist/contracts/crm-support.d.ts +117 -0
- package/node_modules/@oxygen/shared/dist/contracts/crm-support.js +43 -0
- package/node_modules/@oxygen/shared/dist/contracts/dashboard-tabs.d.ts +3 -0
- package/node_modules/@oxygen/shared/dist/contracts/dashboard-tabs.js +2 -0
- package/node_modules/@oxygen/shared/dist/contracts/integration-connect.d.ts +4 -0
- package/node_modules/@oxygen/shared/dist/contracts/integration-connect.js +7 -0
- package/node_modules/@oxygen/shared/dist/contracts/operations.js +8 -2
- package/node_modules/@oxygen/shared/dist/contracts/workspace-pages.d.ts +16 -0
- package/node_modules/@oxygen/shared/dist/contracts/workspace-pages.js +37 -0
- package/node_modules/@oxygen/shared/dist/decision-ledger-parser.d.ts +32 -0
- package/node_modules/@oxygen/shared/dist/decision-ledger-parser.js +303 -0
- package/node_modules/@oxygen/shared/dist/decision-ledger.d.ts +10 -0
- package/node_modules/@oxygen/shared/dist/decision-ledger.generated.d.ts +2 -0
- package/node_modules/@oxygen/shared/dist/decision-ledger.generated.js +717 -0
- package/node_modules/@oxygen/shared/dist/decision-ledger.js +20 -0
- package/node_modules/@oxygen/shared/dist/mcp-event-contract.d.ts +10 -7
- package/node_modules/@oxygen/shared/dist/mcp-event-contract.js +11 -7
- package/node_modules/@oxygen/shared/dist/mcp-table-widget.d.ts +50 -0
- package/node_modules/@oxygen/shared/dist/mcp-table-widget.js +24 -0
- package/node_modules/@oxygen/shared/dist/plan-band.d.ts +8 -0
- package/node_modules/@oxygen/shared/dist/plan-band.js +12 -0
- package/node_modules/@oxygen/shared/dist/plan-limits.d.ts +21 -0
- package/node_modules/@oxygen/shared/dist/plan-limits.js +41 -0
- package/node_modules/@oxygen/shared/dist/sequences.d.ts +33 -4
- package/node_modules/@oxygen/shared/dist/sequences.js +48 -13
- package/node_modules/@oxygen/shared/dist/user-capability-routing.js +17 -0
- package/node_modules/@oxygen/shared/dist/version.generated.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/version.generated.js +1 -1
- package/node_modules/@oxygen/shared/package.json +5 -0
- 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
|
-
/**
|
|
20
|
-
|
|
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
|
|
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" | "
|
|
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
|
-
/**
|
|
63
|
-
|
|
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
|
|
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`)
|
|
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
|
|
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
|
|
1016
|
-
* must open a new thread under its own subject.
|
|
1017
|
-
*
|
|
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
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
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
|
|
1039
|
-
* must open a new thread under its own subject.
|
|
1040
|
-
*
|
|
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
|
|
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
|
|
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
|
|
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
|
-
?
|
|
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.
|
|
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.
|
|
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",
|