@oxygen-agent/cli 1.1014.1225 → 1.1014.1343
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/browser-login.js +4 -1
- package/dist/cli-output.js +14 -0
- package/dist/cli-values.js +1 -1
- package/dist/command-manifest.js +4 -1
- package/dist/commands/billing.js +2 -2
- package/dist/commands/crm.js +12 -4
- package/dist/commands/db.js +30 -0
- package/dist/commands/domains.js +1 -1
- package/dist/commands/feeds.js +29 -1
- package/dist/commands/knowledge.js +24 -1
- package/dist/commands/sequences.js +44 -8
- package/dist/deliverability-evidence-commands.js +6 -6
- package/dist/functions-commands.js +1 -1
- package/dist/help.js +1 -1
- package/dist/local-custom-http-column.js +1 -1
- package/dist/runtime.js +1 -1
- package/dist/session.js +1 -1
- package/dist/transcript.js +1 -1
- package/dist/util.d.ts +1 -1
- package/dist/util.js +1 -1
- package/dist/visual-commands.js +1 -1
- package/dist/visual-render-wait.js +1 -1
- package/node_modules/@oxygen/shared/dist/agent-sessions.d.ts +34 -173
- package/node_modules/@oxygen/shared/dist/agent-sessions.js +134 -18
- package/node_modules/@oxygen/shared/dist/capability-discovery.js +1 -1
- package/node_modules/@oxygen/shared/dist/contracts/customer-operations.js +1 -0
- package/node_modules/@oxygen/shared/dist/conversation-lineage.d.ts +131 -0
- package/node_modules/@oxygen/shared/dist/conversation-lineage.js +72 -0
- package/node_modules/@oxygen/shared/dist/copilot-errors.d.ts +3 -3
- package/node_modules/@oxygen/shared/dist/copilot-errors.js +6 -3
- package/node_modules/@oxygen/shared/dist/crm-record-brief.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/crm-record-brief.js +3 -1
- package/node_modules/@oxygen/shared/dist/deliverability-surface.d.ts +4 -2
- package/node_modules/@oxygen/shared/dist/deliverability-surface.js +4 -2
- package/node_modules/@oxygen/shared/dist/dev-eval-context.d.ts +20 -0
- package/node_modules/@oxygen/shared/dist/enrollment-origin.d.ts +65 -0
- package/node_modules/@oxygen/shared/dist/enrollment-origin.js +92 -0
- package/node_modules/@oxygen/shared/dist/first-month-offer.d.ts +18 -0
- package/node_modules/@oxygen/shared/dist/first-month-offer.js +13 -0
- package/node_modules/@oxygen/shared/dist/guided-onboarding-recommendations.d.ts +54 -0
- package/node_modules/@oxygen/shared/dist/guided-onboarding-recommendations.js +100 -0
- package/node_modules/@oxygen/shared/dist/hosted-ai.d.ts +6 -0
- package/node_modules/@oxygen/shared/dist/hosted-ai.js +9 -0
- package/node_modules/@oxygen/shared/dist/hubspot-engagements.d.ts +26 -0
- package/node_modules/@oxygen/shared/dist/hubspot-engagements.js +107 -0
- package/node_modules/@oxygen/shared/dist/integration-secrets.d.ts +19 -0
- package/node_modules/@oxygen/shared/dist/integration-secrets.js +77 -0
- package/node_modules/@oxygen/shared/dist/knowledge-bootstrap.d.ts +15 -4
- package/node_modules/@oxygen/shared/dist/knowledge-bootstrap.js +15 -4
- package/node_modules/@oxygen/shared/dist/knowledge-brain-draft.d.ts +2 -0
- package/node_modules/@oxygen/shared/dist/knowledge-brain-draft.js +2 -0
- package/node_modules/@oxygen/shared/dist/linkedin-sequences.d.ts +4 -0
- package/node_modules/@oxygen/shared/dist/network-mining-approval.d.ts +90 -0
- package/node_modules/@oxygen/shared/dist/network-mining-approval.js +225 -0
- package/node_modules/@oxygen/shared/dist/network-mining-signals.d.ts +25 -0
- package/node_modules/@oxygen/shared/dist/network-mining-signals.js +34 -0
- package/node_modules/@oxygen/shared/dist/reply-oxygent.d.ts +2 -0
- package/node_modules/@oxygen/shared/dist/reply-oxygent.js +6 -0
- package/node_modules/@oxygen/shared/dist/research-findings.d.ts +11 -2
- package/node_modules/@oxygen/shared/dist/research-findings.js +16 -3
- package/node_modules/@oxygen/shared/dist/sequence-failures.js +3 -1
- package/node_modules/@oxygen/shared/dist/sequences.d.ts +25 -4
- package/node_modules/@oxygen/shared/dist/sequences.js +40 -10
- package/node_modules/@oxygen/shared/dist/user-capability-routing.js +18 -0
- package/node_modules/@oxygen/shared/dist/version.d.ts +1 -1
- 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/dist/version.js +1 -1
- package/node_modules/@oxygen/shared/dist/workspace-event-catalog.js +21 -1
- package/package.json +1 -1
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// Conversation lineage (Network Mining LN1, DEV-only): where a reply came from,
|
|
2
|
+
// as one bounded, free read on the conversation (`inbox get`'s `lineage`). The
|
|
3
|
+
// tenant-db assembler (conversation-lineage.ts) owns no fact; this module is the
|
|
4
|
+
// shape every surface and agent shares, and the one digest agents read.
|
|
5
|
+
//
|
|
6
|
+
// Nothing is guessed: an attribution names the rule that made it, an unknown
|
|
7
|
+
// origin says `unknown`, and a name-only match never fills a record id.
|
|
8
|
+
export const CONVERSATION_LINEAGE_SCHEMA_VERSION = 1;
|
|
9
|
+
export const CONVERSATION_LINEAGE_MAX_CHARS = 2_500;
|
|
10
|
+
export const LINEAGE_DIGEST_MAX_CHARS = 600;
|
|
11
|
+
function duration(seconds) {
|
|
12
|
+
if (seconds === null || !Number.isFinite(seconds) || seconds < 0)
|
|
13
|
+
return null;
|
|
14
|
+
if (seconds < 3_600)
|
|
15
|
+
return `${Math.max(1, Math.round(seconds / 60))} min`;
|
|
16
|
+
if (seconds < 172_800)
|
|
17
|
+
return `${Math.round(seconds / 3_600)} h`;
|
|
18
|
+
return `${Math.round(seconds / 86_400)} d`;
|
|
19
|
+
}
|
|
20
|
+
const ORIGIN_KINDS = {
|
|
21
|
+
table_row: "a table row", lead_list: "a lead list", workflow: "a Workflow", agent: "an Agent",
|
|
22
|
+
engagement_watch: "an engagement watch", one_off_send: "a one-off send", import: "an import",
|
|
23
|
+
};
|
|
24
|
+
/** "a one-off send via cli, focus decile 2"; `unknown` when the enrollment recorded none. */
|
|
25
|
+
export function originText(origin) {
|
|
26
|
+
if (!("gateway" in origin))
|
|
27
|
+
return "unknown";
|
|
28
|
+
const decile = origin.focus?.decile ? `, focus decile ${origin.focus.decile}` : "";
|
|
29
|
+
return `${ORIGIN_KINDS[origin.kind] ?? origin.kind.replace(/_/g, " ")} via ${origin.gateway}${decile}`;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* One short, plain-text line set an agent reads beside the thread (≤600 chars):
|
|
33
|
+
* which sequence, step and variant the reply answered, how fast, from which
|
|
34
|
+
* sender, where the lead came from, and what is unknown.
|
|
35
|
+
*/
|
|
36
|
+
export function renderLineageDigest(lineage) {
|
|
37
|
+
if (!lineage)
|
|
38
|
+
return null;
|
|
39
|
+
const lines = [];
|
|
40
|
+
const sender = lineage.conversation.sender.label;
|
|
41
|
+
lines.push(`Thread started by ${lineage.conversation.started_by ?? "unknown"}${sender ? `, our side ${sender}` : ""}.`);
|
|
42
|
+
const enrollment = lineage.enrollments[0];
|
|
43
|
+
if (!enrollment) {
|
|
44
|
+
lines.push("Not from a Sequence this workspace can link.");
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
const reply = enrollment.reply;
|
|
48
|
+
const answered = reply?.answered;
|
|
49
|
+
const step = answered?.step_index !== null && answered?.step_index !== undefined ? `step ${answered.step_index + 1}` : null;
|
|
50
|
+
const variant = answered?.variant_id ? `variant ${answered.variant_id}` : null;
|
|
51
|
+
const linked = enrollment.link === "stored" ? "" : `, linked by ${enrollment.link === "chat_id" ? "chat id" : "lead id"}`;
|
|
52
|
+
lines.push(`Sequence "${enrollment.sequence.name ?? enrollment.sequence.id}" (${enrollment.status ?? "unknown status"}${linked}).`);
|
|
53
|
+
if (reply) {
|
|
54
|
+
const answeredText = [step, variant].filter(Boolean).join(", ");
|
|
55
|
+
const latency = duration(reply.latency_seconds);
|
|
56
|
+
lines.push(`Replied ${reply.occurred_at.slice(0, 16).replace("T", " ")}Z${answeredText ? ` to ${answeredText}` : ""}${latency ? `, ${latency} after that send` : ""} (${reply.attribution.replace(/_/g, " ")}).`);
|
|
57
|
+
}
|
|
58
|
+
if (enrollment.touches_before_reply)
|
|
59
|
+
lines.push(`${enrollment.touches_before_reply.total} touches before the reply.`);
|
|
60
|
+
// An enrollment without an origin predates origin recording; saying so spares a reader the search for one.
|
|
61
|
+
lines.push(`Lead source: ${"gateway" in enrollment.origin ? originText(enrollment.origin) : "unknown (enrolled before origins were recorded)"}.`);
|
|
62
|
+
}
|
|
63
|
+
if ("row_id" in lineage.record)
|
|
64
|
+
lines.push(`Record: ${lineage.record.object} ${lineage.record.row_id} (by ${lineage.record.matched_on}).`);
|
|
65
|
+
else
|
|
66
|
+
lines.push("No CRM record is linked.");
|
|
67
|
+
const [status, previous] = lineage.classification_history;
|
|
68
|
+
if (status)
|
|
69
|
+
lines.push(`Status ${status.status} (${status.source})${previous ? `, before that ${previous.status} (${previous.source})` : ""}.`);
|
|
70
|
+
const text = lines.join(" ");
|
|
71
|
+
return text.length <= LINEAGE_DIGEST_MAX_CHARS ? text : `${text.slice(0, LINEAGE_DIGEST_MAX_CHARS - 1)}…`;
|
|
72
|
+
}
|
|
@@ -6,9 +6,9 @@ export type CustomerFacingCopilotError = {
|
|
|
6
6
|
message: string | null;
|
|
7
7
|
};
|
|
8
8
|
/**
|
|
9
|
-
*
|
|
10
|
-
* The message-pattern fallback heals rows written before the worker
|
|
11
|
-
* was persisted consistently.
|
|
9
|
+
* Project known internal failures to stable customer-facing copy.
|
|
10
|
+
* The timeout message-pattern fallback heals rows written before the worker
|
|
11
|
+
* error code was persisted consistently. Unrecognized errors pass through.
|
|
12
12
|
*/
|
|
13
13
|
export declare function customerFacingCopilotError(input: {
|
|
14
14
|
code?: string | null;
|
|
@@ -14,13 +14,16 @@ export const COPILOT_TURN_TIMEOUT_MESSAGE = "This Copilot request timed out and
|
|
|
14
14
|
export const COPILOT_TURN_DEADLINE_EXCEEDED_CODE = "copilot_turn_deadline_exceeded";
|
|
15
15
|
const WORKER_STEP_TIMEOUT_MESSAGE = /\bWorker step '[^']+' exceeded \d+ms deadline\.?/i;
|
|
16
16
|
/**
|
|
17
|
-
*
|
|
18
|
-
* The message-pattern fallback heals rows written before the worker
|
|
19
|
-
* was persisted consistently.
|
|
17
|
+
* Project known internal failures to stable customer-facing copy.
|
|
18
|
+
* The timeout message-pattern fallback heals rows written before the worker
|
|
19
|
+
* error code was persisted consistently. Unrecognized errors pass through.
|
|
20
20
|
*/
|
|
21
21
|
export function customerFacingCopilotError(input) {
|
|
22
22
|
const code = input.code ?? null;
|
|
23
23
|
const message = input.message ?? null;
|
|
24
|
+
if (code === "dev_eval_policy_refused") {
|
|
25
|
+
return { code, message: "The run's safety policy refused this request, so this turn stopped. Review the run's authorization and limits before continuing." };
|
|
26
|
+
}
|
|
24
27
|
if (code === "agent_response_incomplete") {
|
|
25
28
|
return { code, message: "I stopped before completing the requested work. Completed actions are saved; ask me to continue." };
|
|
26
29
|
}
|
|
@@ -5,6 +5,6 @@
|
|
|
5
5
|
*/
|
|
6
6
|
export declare const CRM_RECORD_BRIEF_SECTIONS: readonly ["record", "company", "intent", "warm_paths", "meetings", "timeline", "conversations", "sequences", "notes", "tasks", "related", "knowledge"];
|
|
7
7
|
export type CrmRecordBriefSection = typeof CRM_RECORD_BRIEF_SECTIONS[number];
|
|
8
|
-
export declare const CRM_RECORD_BRIEF_DEFAULT_MAX_CHARS =
|
|
8
|
+
export declare const CRM_RECORD_BRIEF_DEFAULT_MAX_CHARS = 16000;
|
|
9
9
|
export declare const CRM_RECORD_BRIEF_MIN_MAX_CHARS = 3000;
|
|
10
10
|
export declare const CRM_RECORD_BRIEF_MAX_MAX_CHARS = 20000;
|
|
@@ -17,6 +17,8 @@ export const CRM_RECORD_BRIEF_SECTIONS = [
|
|
|
17
17
|
"related",
|
|
18
18
|
"knowledge",
|
|
19
19
|
];
|
|
20
|
-
|
|
20
|
+
// A typical person brief (record, company, warm paths, threads, timeline) fits uncut at the default
|
|
21
|
+
// (S1 acceptance round 6 used ~12.2k); agents offload any result above their inline size either way.
|
|
22
|
+
export const CRM_RECORD_BRIEF_DEFAULT_MAX_CHARS = 16_000;
|
|
21
23
|
export const CRM_RECORD_BRIEF_MIN_MAX_CHARS = 3_000;
|
|
22
24
|
export const CRM_RECORD_BRIEF_MAX_MAX_CHARS = 20_000;
|
|
@@ -11,8 +11,10 @@ export declare const DELIVERABILITY_SURFACE_MAX_LIMIT = 100;
|
|
|
11
11
|
/** A check's stored raw evidence is capped at 64 KB; reads return it whole up to this size. */
|
|
12
12
|
export declare const DELIVERABILITY_SURFACE_RAW_MAX_CHARS = 16000;
|
|
13
13
|
export declare const DELIVERABILITY_SURFACE_EVIDENCE_DEFAULT_DAYS = 28;
|
|
14
|
-
|
|
15
|
-
export declare const
|
|
14
|
+
/** Placement results are observations, never check runs, so `placement` is not a readable kind. */
|
|
15
|
+
export declare const DELIVERABILITY_SURFACE_CHECK_KINDS: ("bimi" | "blacklist" | "content_score" | "dns_auth" | "header_proof" | "tracking_cname")[];
|
|
16
|
+
/** Placement runs are read through observations and compare, so no check run has that subject type. */
|
|
17
|
+
export declare const DELIVERABILITY_SURFACE_SUBJECT_TYPES: ("copy" | "domain" | "mailbox")[];
|
|
16
18
|
export declare const DELIVERABILITY_SURFACE_RUN_STATUSES: readonly ["succeeded", "partial", "failed", "unknown"];
|
|
17
19
|
export declare const DELIVERABILITY_SURFACE_FINDING_STATUSES: readonly ["pass", "warn", "fail", "unknown"];
|
|
18
20
|
export declare const DELIVERABILITY_SURFACE_FOLDERS: readonly ["inbox", "spam", "other", "not_observed", "unknown"];
|
|
@@ -12,8 +12,10 @@ export const DELIVERABILITY_SURFACE_MAX_LIMIT = 100;
|
|
|
12
12
|
/** A check's stored raw evidence is capped at 64 KB; reads return it whole up to this size. */
|
|
13
13
|
export const DELIVERABILITY_SURFACE_RAW_MAX_CHARS = 16_000;
|
|
14
14
|
export const DELIVERABILITY_SURFACE_EVIDENCE_DEFAULT_DAYS = 28;
|
|
15
|
-
|
|
16
|
-
export const
|
|
15
|
+
/** Placement results are observations, never check runs, so `placement` is not a readable kind. */
|
|
16
|
+
export const DELIVERABILITY_SURFACE_CHECK_KINDS = DELIVERABILITY_CHECK_KINDS.filter((kind) => kind !== "placement");
|
|
17
|
+
/** Placement runs are read through observations and compare, so no check run has that subject type. */
|
|
18
|
+
export const DELIVERABILITY_SURFACE_SUBJECT_TYPES = DELIVERABILITY_SUBJECT_TYPES.filter((type) => type !== "placement_run");
|
|
17
19
|
export const DELIVERABILITY_SURFACE_RUN_STATUSES = DELIVERABILITY_RUN_STATUSES;
|
|
18
20
|
export const DELIVERABILITY_SURFACE_FINDING_STATUSES = DELIVERABILITY_FINDING_STATUSES;
|
|
19
21
|
export const DELIVERABILITY_SURFACE_FOLDERS = PLACEMENT_FOLDERS;
|
|
@@ -1,8 +1,25 @@
|
|
|
1
|
+
export type DevEvalNoDispatchEffectReceipt = Readonly<{
|
|
2
|
+
kind: "dev_eval_verified_no_dispatch/1";
|
|
3
|
+
campaignId: string;
|
|
4
|
+
organizationId: string;
|
|
5
|
+
sessionId: string;
|
|
6
|
+
actionRunId: string;
|
|
7
|
+
requests: readonly Readonly<{
|
|
8
|
+
provider: string;
|
|
9
|
+
operation: string;
|
|
10
|
+
nativeAttemptId: string;
|
|
11
|
+
physicalRequestSha256: string;
|
|
12
|
+
attestationSha256: string;
|
|
13
|
+
}>[];
|
|
14
|
+
}>;
|
|
1
15
|
export type DevEvalContext = Readonly<{
|
|
2
16
|
campaignId: string;
|
|
3
17
|
organizationId: string;
|
|
4
18
|
sessionId: string;
|
|
5
19
|
actionRunId?: string;
|
|
20
|
+
/** Installed only by the authenticated native host; never deserialized.
|
|
21
|
+
* The callback proves the complete failed operation, not just its last error. */
|
|
22
|
+
observeNoDispatch?: <T>(operation: () => Promise<T>, onVerified: (receipt: DevEvalNoDispatchEffectReceipt) => Promise<void>) => Promise<T>;
|
|
6
23
|
/** Installed by the authenticated host after control-DB lookup, never deserialized. */
|
|
7
24
|
fetch: (provider: string, operation: string, input: string | Request | URL, init: RequestInit | undefined, transport: typeof fetch) => Promise<Response>;
|
|
8
25
|
}>;
|
|
@@ -11,6 +28,9 @@ export declare const currentDevEvalContext: () => Readonly<{
|
|
|
11
28
|
organizationId: string;
|
|
12
29
|
sessionId: string;
|
|
13
30
|
actionRunId?: string;
|
|
31
|
+
/** Installed only by the authenticated native host; never deserialized.
|
|
32
|
+
* The callback proves the complete failed operation, not just its last error. */
|
|
33
|
+
observeNoDispatch?: <T>(operation: () => Promise<T>, onVerified: (receipt: DevEvalNoDispatchEffectReceipt) => Promise<void>) => Promise<T>;
|
|
14
34
|
/** Installed by the authenticated host after control-DB lookup, never deserialized. */
|
|
15
35
|
fetch: (provider: string, operation: string, input: string | Request | URL, init: RequestInit | undefined, transport: typeof fetch) => Promise<Response>;
|
|
16
36
|
}> | null;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where a Sequence enrollment came from, stamped once at its single insert
|
|
3
|
+
* (`ox_sequencer.enrollments.origin`, migration 0541) because the actor, run and
|
|
4
|
+
* gateway exist only at enroll time and `sequences.source_table_id` can be
|
|
5
|
+
* rebound later. Read by the conversation lineage and the day-1 funnel.
|
|
6
|
+
*
|
|
7
|
+
* One contract for every writer: `parseEnrollmentOrigin` refuses an unknown
|
|
8
|
+
* kind, actor, gateway or ref type, and the pg CHECK on `origin->>'kind'` keeps
|
|
9
|
+
* the column from rotting like `metadata`. `{}` is a pre-0541 row and reads as
|
|
10
|
+
* `unknown`; nothing is ever inferred into it.
|
|
11
|
+
*/
|
|
12
|
+
export declare const ENROLLMENT_ORIGIN_SCHEMA_VERSION = 1;
|
|
13
|
+
/** What chose these leads. Mirrored by the CHECK constraint in 0541; add to both. */
|
|
14
|
+
export declare const ENROLLMENT_ORIGIN_KINDS: readonly ["table_row", "lead_list", "workflow", "agent", "engagement_watch", "one_off_send", "import", "unknown"];
|
|
15
|
+
export type EnrollmentOriginKind = typeof ENROLLMENT_ORIGIN_KINDS[number];
|
|
16
|
+
export declare const ENROLLMENT_ACTOR_TYPES: readonly ["user", "agent", "workflow", "api_key", "system"];
|
|
17
|
+
export type EnrollmentActorType = typeof ENROLLMENT_ACTOR_TYPES[number];
|
|
18
|
+
/** The surface the enroll call came through. */
|
|
19
|
+
export declare const ENROLLMENT_GATEWAYS: readonly ["cli", "mcp", "web", "copilot", "agent", "workflow", "engagement_watch"];
|
|
20
|
+
export type EnrollmentGateway = typeof ENROLLMENT_GATEWAYS[number];
|
|
21
|
+
export declare const ENROLLMENT_REF_TYPES: readonly ["workflow_run", "agent_run", "copilot_session", "engagement_watch", "guided_activation"];
|
|
22
|
+
export type EnrollmentRefType = typeof ENROLLMENT_REF_TYPES[number];
|
|
23
|
+
/** Network Mining (gated): the lead's place in the focus rank when it was enrolled. Focus is read-time and drifts. */
|
|
24
|
+
export type EnrollmentFocusSnapshot = {
|
|
25
|
+
/** The People record the enrolled row resolved to, or null when it resolved to none. */
|
|
26
|
+
record_row_id: string | null;
|
|
27
|
+
in_focus: boolean;
|
|
28
|
+
/** 1-based position in the whole candidate set; null when not in focus. */
|
|
29
|
+
rank: number | null;
|
|
30
|
+
/** 1 (warmest tenth) to 10; null when not in focus. */
|
|
31
|
+
decile: number | null;
|
|
32
|
+
/** How many people the rank held; null when none of the batch was in it. */
|
|
33
|
+
candidates: number | null;
|
|
34
|
+
reasons: Array<{
|
|
35
|
+
code: string;
|
|
36
|
+
evidence_ids: string[];
|
|
37
|
+
}>;
|
|
38
|
+
rule_version: string;
|
|
39
|
+
computed_at: string;
|
|
40
|
+
};
|
|
41
|
+
export type EnrollmentOrigin = {
|
|
42
|
+
schema_version: typeof ENROLLMENT_ORIGIN_SCHEMA_VERSION;
|
|
43
|
+
kind: EnrollmentOriginKind;
|
|
44
|
+
gateway: EnrollmentGateway;
|
|
45
|
+
enrolled_by: {
|
|
46
|
+
type: EnrollmentActorType;
|
|
47
|
+
id: string | null;
|
|
48
|
+
};
|
|
49
|
+
/** The table the enrolled rows belong to (also stamped as the typed `source_table_id`). */
|
|
50
|
+
table_id: string | null;
|
|
51
|
+
ref: {
|
|
52
|
+
type: EnrollmentRefType;
|
|
53
|
+
id: string;
|
|
54
|
+
} | null;
|
|
55
|
+
/** Network Mining only; absent while the gate is off, null when the read failed. */
|
|
56
|
+
focus_at_enroll?: EnrollmentFocusSnapshot | null;
|
|
57
|
+
};
|
|
58
|
+
/** Everything a writer states; the enroller adds the per-lead focus snapshot. */
|
|
59
|
+
export type EnrollmentOriginInput = Omit<EnrollmentOrigin, "schema_version" | "focus_at_enroll">;
|
|
60
|
+
/** Validates a writer's origin; throws on anything outside the contract. */
|
|
61
|
+
export declare function parseEnrollmentOrigin(value: unknown): EnrollmentOrigin;
|
|
62
|
+
/** A stored origin as readers see it: `{}` and anything malformed read as `unknown`, never guessed. */
|
|
63
|
+
export declare function readEnrollmentOrigin(value: unknown): EnrollmentOrigin | {
|
|
64
|
+
kind: "unknown";
|
|
65
|
+
};
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where a Sequence enrollment came from, stamped once at its single insert
|
|
3
|
+
* (`ox_sequencer.enrollments.origin`, migration 0541) because the actor, run and
|
|
4
|
+
* gateway exist only at enroll time and `sequences.source_table_id` can be
|
|
5
|
+
* rebound later. Read by the conversation lineage and the day-1 funnel.
|
|
6
|
+
*
|
|
7
|
+
* One contract for every writer: `parseEnrollmentOrigin` refuses an unknown
|
|
8
|
+
* kind, actor, gateway or ref type, and the pg CHECK on `origin->>'kind'` keeps
|
|
9
|
+
* the column from rotting like `metadata`. `{}` is a pre-0541 row and reads as
|
|
10
|
+
* `unknown`; nothing is ever inferred into it.
|
|
11
|
+
*/
|
|
12
|
+
export const ENROLLMENT_ORIGIN_SCHEMA_VERSION = 1;
|
|
13
|
+
/** What chose these leads. Mirrored by the CHECK constraint in 0541; add to both. */
|
|
14
|
+
export const ENROLLMENT_ORIGIN_KINDS = [
|
|
15
|
+
"table_row",
|
|
16
|
+
"lead_list",
|
|
17
|
+
"workflow",
|
|
18
|
+
"agent",
|
|
19
|
+
"engagement_watch",
|
|
20
|
+
"one_off_send",
|
|
21
|
+
"import",
|
|
22
|
+
"unknown",
|
|
23
|
+
];
|
|
24
|
+
export const ENROLLMENT_ACTOR_TYPES = ["user", "agent", "workflow", "api_key", "system"];
|
|
25
|
+
/** The surface the enroll call came through. */
|
|
26
|
+
export const ENROLLMENT_GATEWAYS = [
|
|
27
|
+
"cli",
|
|
28
|
+
"mcp",
|
|
29
|
+
"web",
|
|
30
|
+
"copilot",
|
|
31
|
+
"agent",
|
|
32
|
+
"workflow",
|
|
33
|
+
"engagement_watch",
|
|
34
|
+
];
|
|
35
|
+
export const ENROLLMENT_REF_TYPES = [
|
|
36
|
+
"workflow_run",
|
|
37
|
+
"agent_run",
|
|
38
|
+
"copilot_session",
|
|
39
|
+
"engagement_watch",
|
|
40
|
+
"guided_activation",
|
|
41
|
+
];
|
|
42
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
43
|
+
function oneOf(values, value, field) {
|
|
44
|
+
if (typeof value === "string" && values.includes(value))
|
|
45
|
+
return value;
|
|
46
|
+
throw new Error(`Enrollment origin ${field} must be one of ${values.join(", ")}.`);
|
|
47
|
+
}
|
|
48
|
+
function optionalId(value, field) {
|
|
49
|
+
if (value === null || value === undefined)
|
|
50
|
+
return null;
|
|
51
|
+
if (typeof value === "string" && value.trim() && value.length <= 200)
|
|
52
|
+
return value.trim();
|
|
53
|
+
throw new Error(`Enrollment origin ${field} must be a non-empty id or null.`);
|
|
54
|
+
}
|
|
55
|
+
/** Validates a writer's origin; throws on anything outside the contract. */
|
|
56
|
+
export function parseEnrollmentOrigin(value) {
|
|
57
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
58
|
+
throw new Error("Enrollment origin must be an object.");
|
|
59
|
+
const input = value;
|
|
60
|
+
const enrolledBy = input.enrolled_by;
|
|
61
|
+
if (!enrolledBy || typeof enrolledBy !== "object")
|
|
62
|
+
throw new Error("Enrollment origin enrolled_by is required.");
|
|
63
|
+
const tableId = optionalId(input.table_id, "table_id");
|
|
64
|
+
if (tableId !== null && !UUID_RE.test(tableId))
|
|
65
|
+
throw new Error("Enrollment origin table_id must be a uuid.");
|
|
66
|
+
const ref = input.ref;
|
|
67
|
+
const refId = ref ? optionalId(ref.id, "ref.id") : null;
|
|
68
|
+
if (ref && refId === null)
|
|
69
|
+
throw new Error("Enrollment origin ref.id is required with a ref.");
|
|
70
|
+
return {
|
|
71
|
+
schema_version: ENROLLMENT_ORIGIN_SCHEMA_VERSION,
|
|
72
|
+
kind: oneOf(ENROLLMENT_ORIGIN_KINDS, input.kind, "kind"),
|
|
73
|
+
gateway: oneOf(ENROLLMENT_GATEWAYS, input.gateway, "gateway"),
|
|
74
|
+
enrolled_by: {
|
|
75
|
+
type: oneOf(ENROLLMENT_ACTOR_TYPES, enrolledBy.type, "enrolled_by.type"),
|
|
76
|
+
id: optionalId(enrolledBy.id, "enrolled_by.id"),
|
|
77
|
+
},
|
|
78
|
+
table_id: tableId,
|
|
79
|
+
ref: ref && refId ? { type: oneOf(ENROLLMENT_REF_TYPES, ref.type, "ref.type"), id: refId } : null,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
/** A stored origin as readers see it: `{}` and anything malformed read as `unknown`, never guessed. */
|
|
83
|
+
export function readEnrollmentOrigin(value) {
|
|
84
|
+
try {
|
|
85
|
+
const origin = parseEnrollmentOrigin(value);
|
|
86
|
+
const focus = value.focus_at_enroll;
|
|
87
|
+
return focus === undefined ? origin : { ...origin, focus_at_enroll: focus };
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return { kind: "unknown" };
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -22,6 +22,12 @@ export declare const FIRST_MONTH_OFFER: Readonly<{
|
|
|
22
22
|
key: "first_month_half";
|
|
23
23
|
/** The cheapest size it applies to; every dearer size qualifies too. */
|
|
24
24
|
minimumPlanKey: "oxygen_99";
|
|
25
|
+
/**
|
|
26
|
+
* The size every surface proposes while the offer runs ("By default it's the
|
|
27
|
+
* $99/month plan, which is 50% off for the first month"): `/pricing` and the
|
|
28
|
+
* upgrade page preselect it, and the CLI and MCP name it to an agent.
|
|
29
|
+
*/
|
|
30
|
+
defaultPlanKey: "oxygen_99";
|
|
25
31
|
/** How long a person has to claim it after first seeing it. */
|
|
26
32
|
windowMs: number;
|
|
27
33
|
}>;
|
|
@@ -29,6 +35,18 @@ export declare const FIRST_MONTH_OFFER: Readonly<{
|
|
|
29
35
|
export declare const FIRST_MONTH_OFFER_PLAN_KEYS: readonly OxygenPlanKey[];
|
|
30
36
|
/** Whether the offer applies to this size (billed monthly; checkout checks the rest). */
|
|
31
37
|
export declare function firstMonthOfferAppliesTo(planKey: string): planKey is OxygenPlanKey;
|
|
38
|
+
/**
|
|
39
|
+
* Whether the offer covers one first subscription: a size it applies to, billed
|
|
40
|
+
* monthly (never a yearly commitment). Who may claim it, and when, is the
|
|
41
|
+
* server's `readFirstMonthOfferState`.
|
|
42
|
+
*/
|
|
43
|
+
export type FirstMonthOfferCoverage = {
|
|
44
|
+
applies: true;
|
|
45
|
+
} | {
|
|
46
|
+
applies: false;
|
|
47
|
+
reason: "plan_not_covered" | "yearly_not_covered";
|
|
48
|
+
};
|
|
49
|
+
export declare function firstMonthOfferCoverage(planKey: string, interval: "month" | "year"): FirstMonthOfferCoverage;
|
|
32
50
|
export type FirstMonthOfferPrice = {
|
|
33
51
|
planKey: OxygenPlanKey;
|
|
34
52
|
regularCents: number;
|
|
@@ -22,6 +22,12 @@ export const FIRST_MONTH_OFFER = Object.freeze({
|
|
|
22
22
|
key: "first_month_half",
|
|
23
23
|
/** The cheapest size it applies to; every dearer size qualifies too. */
|
|
24
24
|
minimumPlanKey: "oxygen_99",
|
|
25
|
+
/**
|
|
26
|
+
* The size every surface proposes while the offer runs ("By default it's the
|
|
27
|
+
* $99/month plan, which is 50% off for the first month"): `/pricing` and the
|
|
28
|
+
* upgrade page preselect it, and the CLI and MCP name it to an agent.
|
|
29
|
+
*/
|
|
30
|
+
defaultPlanKey: "oxygen_99",
|
|
25
31
|
/** How long a person has to claim it after first seeing it. */
|
|
26
32
|
windowMs: FIRST_MONTH_OFFER_WINDOW_MS,
|
|
27
33
|
});
|
|
@@ -36,6 +42,13 @@ export const FIRST_MONTH_OFFER_PLAN_KEYS = (() => {
|
|
|
36
42
|
export function firstMonthOfferAppliesTo(planKey) {
|
|
37
43
|
return FIRST_MONTH_OFFER_PLAN_KEYS.includes(planKey);
|
|
38
44
|
}
|
|
45
|
+
export function firstMonthOfferCoverage(planKey, interval) {
|
|
46
|
+
if (!firstMonthOfferAppliesTo(planKey))
|
|
47
|
+
return { applies: false, reason: "plan_not_covered" };
|
|
48
|
+
if (interval !== "month")
|
|
49
|
+
return { applies: false, reason: "yearly_not_covered" };
|
|
50
|
+
return { applies: true };
|
|
51
|
+
}
|
|
39
52
|
/** A size's first month under the offer: half its price, rounded down to the dollar ($99 → $49, 50% off). */
|
|
40
53
|
export function firstMonthOfferPrice(planKey = FIRST_MONTH_OFFER.minimumPlanKey) {
|
|
41
54
|
if (!firstMonthOfferAppliesTo(planKey))
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { GuidedOnboardingAnswers } from "./guided-onboarding-journey.js";
|
|
2
|
+
export declare const ONBOARDING_AUDIENCE_QUESTIONS: {
|
|
3
|
+
readonly audience_company_size: "What size companies do you sell to?";
|
|
4
|
+
readonly audience_markets: "Which markets do you serve?";
|
|
5
|
+
readonly audience_industries: "Which industries fit your customers?";
|
|
6
|
+
readonly audience_personas: "Who buys from you?";
|
|
7
|
+
};
|
|
8
|
+
export type OnboardingAudienceStep = keyof typeof ONBOARDING_AUDIENCE_QUESTIONS;
|
|
9
|
+
export type OnboardingRecommendation = {
|
|
10
|
+
values: string[];
|
|
11
|
+
rationale: string;
|
|
12
|
+
source_revision: number;
|
|
13
|
+
source_page?: {
|
|
14
|
+
id: string;
|
|
15
|
+
slug: string;
|
|
16
|
+
};
|
|
17
|
+
sources: Array<{
|
|
18
|
+
id: string;
|
|
19
|
+
url: string;
|
|
20
|
+
title: string | null;
|
|
21
|
+
}>;
|
|
22
|
+
};
|
|
23
|
+
export declare const ONBOARDING_DRAFT_FILTER_PAGES: {
|
|
24
|
+
readonly audience_company_size: "filter-company-size";
|
|
25
|
+
readonly audience_markets: "filter-regions";
|
|
26
|
+
readonly audience_industries: "filter-industries";
|
|
27
|
+
readonly audience_personas: "filter-job-titles";
|
|
28
|
+
};
|
|
29
|
+
export type OnboardingDraftFilterPage = {
|
|
30
|
+
id: string;
|
|
31
|
+
slug: string;
|
|
32
|
+
revision: number;
|
|
33
|
+
body: string;
|
|
34
|
+
data: unknown;
|
|
35
|
+
};
|
|
36
|
+
export type OnboardingRecommendations = Partial<Record<OnboardingAudienceStep, OnboardingRecommendation>> & {
|
|
37
|
+
signals?: {
|
|
38
|
+
answer: NonNullable<GuidedOnboardingAnswers["signals"]>;
|
|
39
|
+
source_revision: number;
|
|
40
|
+
};
|
|
41
|
+
};
|
|
42
|
+
/** The background research's target-customer projection, never the company's
|
|
43
|
+
* own headcount/industry or a generic catalog. Recommendations are unsaved drafts. */
|
|
44
|
+
export declare function guidedOnboardingRecommendations(profile: {
|
|
45
|
+
revision: number;
|
|
46
|
+
data: {
|
|
47
|
+
icp?: Record<string, unknown>;
|
|
48
|
+
};
|
|
49
|
+
} | null | undefined, evidence?: OnboardingRecommendation["sources"], draft?: {
|
|
50
|
+
domain: string | null;
|
|
51
|
+
pages: OnboardingDraftFilterPage[];
|
|
52
|
+
}): OnboardingRecommendations;
|
|
53
|
+
/** Only deliberate, saved answers enter shared chat history; no sealed research. */
|
|
54
|
+
export declare function onboardingAnswerPost(answers: GuidedOnboardingAnswers): string | null;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { asRecordOrEmpty } from "./type-guards.js";
|
|
2
|
+
export const ONBOARDING_AUDIENCE_QUESTIONS = {
|
|
3
|
+
audience_company_size: "What size companies do you sell to?",
|
|
4
|
+
audience_markets: "Which markets do you serve?",
|
|
5
|
+
audience_industries: "Which industries fit your customers?",
|
|
6
|
+
audience_personas: "Who buys from you?",
|
|
7
|
+
};
|
|
8
|
+
export const ONBOARDING_DRAFT_FILTER_PAGES = {
|
|
9
|
+
audience_company_size: "filter-company-size", audience_markets: "filter-regions",
|
|
10
|
+
audience_industries: "filter-industries", audience_personas: "filter-job-titles",
|
|
11
|
+
};
|
|
12
|
+
function values(value) {
|
|
13
|
+
const list = typeof value === "string" ? [value] : Array.isArray(value) ? value : [];
|
|
14
|
+
const unique = new Map();
|
|
15
|
+
for (const item of list) {
|
|
16
|
+
if (typeof item !== "string")
|
|
17
|
+
continue;
|
|
18
|
+
const text = item.normalize("NFKC").trim().replace(/\s+/gu, " ");
|
|
19
|
+
if (text && text.length <= 160)
|
|
20
|
+
unique.set(text.toLowerCase(), text);
|
|
21
|
+
}
|
|
22
|
+
return [...unique.values()];
|
|
23
|
+
}
|
|
24
|
+
/** The background research's target-customer projection, never the company's
|
|
25
|
+
* own headcount/industry or a generic catalog. Recommendations are unsaved drafts. */
|
|
26
|
+
export function guidedOnboardingRecommendations(profile, evidence = [], draft) {
|
|
27
|
+
const icp = profile?.data.icp ?? {};
|
|
28
|
+
const candidates = {
|
|
29
|
+
audience_company_size: icp.company_sizes ?? icp.company_size,
|
|
30
|
+
audience_markets: icp.markets ?? icp.geographies,
|
|
31
|
+
audience_industries: icp.industries,
|
|
32
|
+
audience_personas: icp.buyer_personas ?? icp.buyer_titles,
|
|
33
|
+
};
|
|
34
|
+
const sources = evidence.filter(source => {
|
|
35
|
+
try {
|
|
36
|
+
const url = new URL(source.url);
|
|
37
|
+
return url.protocol === "https:" && !url.username && !url.password;
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
}).slice(0, 8);
|
|
43
|
+
const result = {};
|
|
44
|
+
for (const [step, candidate] of Object.entries(candidates)) {
|
|
45
|
+
const key = step;
|
|
46
|
+
const page = candidate === undefined && draft?.domain ? draft.pages.find(page => page.slug === ONBOARDING_DRAFT_FILTER_PAGES[key]
|
|
47
|
+
&& asRecordOrEmpty(asRecordOrEmpty(page.data).brain_draft).domain === draft.domain) : undefined;
|
|
48
|
+
const provenance = asRecordOrEmpty(asRecordOrEmpty(page?.data).brain_draft);
|
|
49
|
+
const selected = values(page ? [...page.body.matchAll(/^- ([^\r\n]+)$/gm)].map(match => match[1]) : candidate).slice(0, 3);
|
|
50
|
+
if (!selected.length)
|
|
51
|
+
continue;
|
|
52
|
+
const references = page ? values(provenance.source_urls).flatMap((url, index) => {
|
|
53
|
+
try {
|
|
54
|
+
const parsed = new URL(url);
|
|
55
|
+
return parsed.protocol === "https:" && !parsed.username && !parsed.password ? [{ id: `${page.id}:${index}`, url, title: null }] : [];
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return [];
|
|
59
|
+
}
|
|
60
|
+
}) : sources;
|
|
61
|
+
result[key] = { values: selected,
|
|
62
|
+
rationale: `I'd start with ${selected.join(", ")} from your ${page ? "draft" : "saved"} target-customer research. Confirm this starting audience or adjust it.`,
|
|
63
|
+
source_revision: page?.revision ?? profile.revision, sources: references,
|
|
64
|
+
...(page ? { source_page: { id: page.id, slug: page.slug } } : {}) };
|
|
65
|
+
}
|
|
66
|
+
const hiring = values(icp.hiring_roles).slice(0, 3);
|
|
67
|
+
const relationships = new Set(["uses", "integrates_with", "replaces"]);
|
|
68
|
+
const technology = (Array.isArray(icp.technology_relationships) ? icp.technology_relationships : []).flatMap(item => {
|
|
69
|
+
if (!item || typeof item !== "object" || !relationships.has(item.relationship))
|
|
70
|
+
return [];
|
|
71
|
+
const name = values(item.name)[0];
|
|
72
|
+
return name ? [{ name, relationship: item.relationship }] : [];
|
|
73
|
+
}).slice(0, 3);
|
|
74
|
+
if (hiring.length || technology.length)
|
|
75
|
+
result.signals = { source_revision: profile.revision,
|
|
76
|
+
answer: { hiring: { mode: hiring.length ? "selected" : "none", values: hiring },
|
|
77
|
+
technology: { mode: technology.length ? "selected" : "none", values: technology } } };
|
|
78
|
+
return result;
|
|
79
|
+
}
|
|
80
|
+
/** Only deliberate, saved answers enter shared chat history; no sealed research. */
|
|
81
|
+
export function onboardingAnswerPost(answers) {
|
|
82
|
+
for (const [step, question] of Object.entries(ONBOARDING_AUDIENCE_QUESTIONS)) {
|
|
83
|
+
const answer = answers[step];
|
|
84
|
+
if (answer)
|
|
85
|
+
return `${question}\n\nConfirmed: ${answer.values.join(", ")}`;
|
|
86
|
+
}
|
|
87
|
+
if (answers.company_context) {
|
|
88
|
+
const answer = answers.company_context;
|
|
89
|
+
return `Your company\n\n${[answer.name, answer.website, answer.description].filter(Boolean).join("\n")}`;
|
|
90
|
+
}
|
|
91
|
+
if (answers.company_review)
|
|
92
|
+
return `Your company\n\nConfirmed: ${answers.company_review.statement}`;
|
|
93
|
+
if (answers.signals) {
|
|
94
|
+
const signals = answers.signals;
|
|
95
|
+
const selected = [...signals.hiring.values.map(role => `Hiring: ${role}`),
|
|
96
|
+
...signals.technology.values.map(item => `${item.name} (${item.relationship})`)];
|
|
97
|
+
return `Which signals matter to you?\n\nConfirmed: ${selected.length ? selected.join(", ") : "No signals for now"}`;
|
|
98
|
+
}
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
@@ -90,6 +90,12 @@ export type ManagedModelPin = {
|
|
|
90
90
|
fallbackModels: readonly string[];
|
|
91
91
|
/** Send `reasoning: {enabled: false}`, as the pass was measured. */
|
|
92
92
|
disableReasoning: boolean;
|
|
93
|
+
/** The pass's own completion ceiling, replacing the column tier's. Credits stay
|
|
94
|
+
* the tier's flat price, so it only decides whether a long answer is cut off. */
|
|
95
|
+
maxOutputTokens?: number;
|
|
96
|
+
/** OpenRouter provider routing for the pinned model: `throughput` sends each call
|
|
97
|
+
* to the provider serving it fastest right now, at the same model and price tier. */
|
|
98
|
+
providerSort?: "throughput" | "latency" | "price";
|
|
93
99
|
};
|
|
94
100
|
/**
|
|
95
101
|
* Signup research synthesis (knowledge bootstrap): the company profile, cited
|
|
@@ -278,6 +278,15 @@ export const KNOWLEDGE_BOOTSTRAP_SYNTHESIS_MODEL = {
|
|
|
278
278
|
model: "deepseek/deepseek-v4.1-flash",
|
|
279
279
|
fallbackModels: ["openai/gpt-6-luna"],
|
|
280
280
|
disableReasoning: true,
|
|
281
|
+
// The profile plus the cited note of a well-documented company runs past the
|
|
282
|
+
// Balanced tier's 4,096: on dev (2026-10-18) attio.com and lemlist.com stopped at
|
|
283
|
+
// exactly 4,096 tokens (`finish_reason: length`), mid-note, as invalid JSON, so the
|
|
284
|
+
// run ended `partial` with no research page. A shorter answer is unaffected.
|
|
285
|
+
maxOutputTokens: 8192,
|
|
286
|
+
// The same model ran 3x apart by provider on dev (2026-10-18): DigitalOcean and
|
|
287
|
+
// Together wrote 300 to 370 tokens a second, Relace and Parasail 105 to 120, so one
|
|
288
|
+
// 7,600-token research note took 66 s instead of about 20 s.
|
|
289
|
+
providerSort: "throughput",
|
|
281
290
|
};
|
|
282
291
|
/**
|
|
283
292
|
* Managed model ids OXYGEN ran that never sat in the registry above, mapped to
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export declare const HUBSPOT_ENGAGEMENT_TYPES: readonly ["calls", "meetings", "notes", "emails", "tasks"];
|
|
2
|
+
export type HubspotEngagementType = (typeof HUBSPOT_ENGAGEMENT_TYPES)[number];
|
|
3
|
+
/** Properties each list page asks for. Email bodies are read only as an excerpt. */
|
|
4
|
+
export declare const HUBSPOT_ENGAGEMENT_PROPERTIES: Readonly<Record<HubspotEngagementType, readonly string[]>>;
|
|
5
|
+
/** The records an engagement may link to: HubSpot association type → registered external object. */
|
|
6
|
+
export declare const HUBSPOT_ENGAGEMENT_ASSOCIATIONS: readonly ["contacts", "companies"];
|
|
7
|
+
export type HubspotEngagementActivity = {
|
|
8
|
+
hubspotType: HubspotEngagementType;
|
|
9
|
+
hubspotId: string;
|
|
10
|
+
activityType: "call" | "meeting_booked" | "meeting_completed" | "no_show" | "note" | "email_sent" | "email_received" | "task_created" | "task_completed";
|
|
11
|
+
direction: "inbound" | "outbound" | "internal" | null;
|
|
12
|
+
channel: "phone" | "meeting" | "email" | null;
|
|
13
|
+
occurredAt: string;
|
|
14
|
+
summary: string;
|
|
15
|
+
body: string | null;
|
|
16
|
+
/** HubSpot ids of the associated contacts and companies, in that order of preference. */
|
|
17
|
+
contactIds: string[];
|
|
18
|
+
companyIds: string[];
|
|
19
|
+
/** The source properties kept for provenance (never the full email body). */
|
|
20
|
+
properties: Record<string, string>;
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* One HubSpot engagement object (`crm/v3/objects/<type>` with associations)
|
|
24
|
+
* as a timeline activity, or null when it has no id or no timestamp.
|
|
25
|
+
*/
|
|
26
|
+
export declare function normalizeHubspotEngagement(type: HubspotEngagementType, raw: unknown): HubspotEngagementActivity | null;
|