@oxygen-agent/cli 1.692.5 → 1.696.1
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/index.js +101 -17
- package/node_modules/@oxygen/shared/dist/crm-reply-events.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/crm-reply-events.js +4 -0
- package/node_modules/@oxygen/shared/dist/index.d.ts +1 -0
- package/node_modules/@oxygen/shared/dist/index.js +1 -0
- package/node_modules/@oxygen/shared/dist/sequence-hubspot-sync.d.ts +62 -0
- package/node_modules/@oxygen/shared/dist/sequence-hubspot-sync.js +100 -0
- package/node_modules/@oxygen/shared/dist/version.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/version.js +1 -1
- package/node_modules/@oxygen/shared/dist/workspace-event-catalog.js +7 -0
- package/package.json +1 -1
package/README.md
CHANGED
package/dist/index.js
CHANGED
|
@@ -627,7 +627,7 @@ function writeWorkflowActionTail(record) {
|
|
|
627
627
|
// Bulk `workflows enable | disable | delete` return a per-item results[] plus
|
|
628
628
|
// summary counts. In human (non --json) output there is no JSON dump, so render
|
|
629
629
|
// one readable line per item and a summary-count line to stdout. `okVerb` is the
|
|
630
|
-
// past-tense success verb (enabled / disabled /
|
|
630
|
+
// past-tense success verb (enabled / disabled / deleted).
|
|
631
631
|
function writeBulkWorkflowResults(data, okVerb) {
|
|
632
632
|
if (!data || typeof data !== "object" || Array.isArray(data))
|
|
633
633
|
return;
|
|
@@ -656,7 +656,7 @@ function writeBulkWorkflowResults(data, okVerb) {
|
|
|
656
656
|
}
|
|
657
657
|
}
|
|
658
658
|
const counts = [];
|
|
659
|
-
for (const key of ["
|
|
659
|
+
for (const key of ["deleted", "enabled", "disabled", "skipped", "failed", "archived", "purged"]) {
|
|
660
660
|
const value = record[key];
|
|
661
661
|
if (typeof value === "number")
|
|
662
662
|
counts.push(`${value} ${key}`);
|
|
@@ -808,7 +808,7 @@ function writeMaxCreditsHint(error) {
|
|
|
808
808
|
// command does not take. The server message is the signal: it spells out
|
|
809
809
|
// "(CLI: --yes)" on those gates.
|
|
810
810
|
if (error.message.includes("--yes")) {
|
|
811
|
-
process.stderr.write("hint: inspect the preview, then re-run with --yes to approve the permanent
|
|
811
|
+
process.stderr.write("hint: inspect the preview, then re-run with --yes to approve the permanent deletion\n");
|
|
812
812
|
return;
|
|
813
813
|
}
|
|
814
814
|
const estimated = readDetailsNumber(error.details, "estimated_credits");
|
|
@@ -11499,15 +11499,16 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
11499
11499
|
});
|
|
11500
11500
|
})))
|
|
11501
11501
|
.addCommand(new Command("hubspot-sync")
|
|
11502
|
-
.description("
|
|
11502
|
+
.description("Project sequence activity, native email/LinkedIn timeline entries, and reply outcomes into the connected HubSpot portal. One workspace setting manages the event and classified-reply workflows across every sequence.")
|
|
11503
11503
|
.addCommand(new Command("show")
|
|
11504
|
-
.description("Show the connected portal
|
|
11504
|
+
.description("Show the connected portal, live contact schema, activity/status configuration, component health, and enabled state.")
|
|
11505
11505
|
.option("--json", "Print a JSON envelope.")
|
|
11506
11506
|
.action(async (options) => {
|
|
11507
11507
|
await handleAsyncAction("sequences hubspot-sync show", options, () => requestOxygen("/api/cli/sequencer/hubspot-sync"));
|
|
11508
11508
|
}))
|
|
11509
11509
|
.addCommand(new Command("configure")
|
|
11510
|
-
.description("Preview or save
|
|
11510
|
+
.description("Preview or save the workspace-wide HubSpot projection. Existing fields need no new grant; creating missing properties requires --approved and HubSpot property-creation access.")
|
|
11511
|
+
.option("--config-file <path>", "Full or partial JSON configuration: mappings, timeline_events, LinkedIn identity property, lead-status property, sequence/reply status mappings, and opt-out status.")
|
|
11511
11512
|
.option("--mappings-file <path>", "JSON object mapping every sequencer event key to a HubSpot contact datetime-property internal name; use an empty string to disable an event.")
|
|
11512
11513
|
.option("--armed", "Enable future event delivery.")
|
|
11513
11514
|
.option("--disarmed", "Disable future event delivery.")
|
|
@@ -11519,6 +11520,13 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
11519
11520
|
if (options.armed === options.disarmed) {
|
|
11520
11521
|
throw new Error("Pass exactly one of --armed or --disarmed.");
|
|
11521
11522
|
}
|
|
11523
|
+
const configPath = readOption(options.configFile);
|
|
11524
|
+
const configValue = configPath
|
|
11525
|
+
? readJsonFileValue(resolve(configPath), "--config-file")
|
|
11526
|
+
: undefined;
|
|
11527
|
+
if (configValue !== undefined && !isRecord(configValue)) {
|
|
11528
|
+
throw new Error("--config-file must contain a JSON object.");
|
|
11529
|
+
}
|
|
11522
11530
|
const mappingsPath = readOption(options.mappingsFile);
|
|
11523
11531
|
const mappings = mappingsPath
|
|
11524
11532
|
? readJsonFileValue(resolve(mappingsPath), "--mappings-file")
|
|
@@ -11526,13 +11534,90 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
11526
11534
|
if (mappings !== undefined && !isRecord(mappings)) {
|
|
11527
11535
|
throw new Error("--mappings-file must contain a JSON object.");
|
|
11528
11536
|
}
|
|
11537
|
+
const config = {
|
|
11538
|
+
...(configValue ?? {}),
|
|
11539
|
+
...(mappings
|
|
11540
|
+
? {
|
|
11541
|
+
mappings: {
|
|
11542
|
+
...(isRecord(configValue?.mappings)
|
|
11543
|
+
? configValue.mappings
|
|
11544
|
+
: {}),
|
|
11545
|
+
...mappings,
|
|
11546
|
+
},
|
|
11547
|
+
}
|
|
11548
|
+
: {}),
|
|
11549
|
+
};
|
|
11529
11550
|
return requestOxygen("/api/cli/sequencer/hubspot-sync", {
|
|
11530
11551
|
method: "POST",
|
|
11531
11552
|
body: {
|
|
11532
11553
|
armed: options.armed === true,
|
|
11533
11554
|
mode: options.live ? "live" : "dry_run",
|
|
11534
|
-
...(
|
|
11555
|
+
...(Object.keys(config).length > 0 ? { config } : {}),
|
|
11556
|
+
...(options.approved ? { approved: true } : {}),
|
|
11557
|
+
},
|
|
11558
|
+
});
|
|
11559
|
+
});
|
|
11560
|
+
})))
|
|
11561
|
+
.addCommand(new Command("hubspot-list-import")
|
|
11562
|
+
.description("Discover saved HubSpot contact lists and configure one bounded DNC-first automatic import into an Oxygen Sequence. Draft and paused recipients stay pending until launch or resume.")
|
|
11563
|
+
.addCommand(new Command("lists")
|
|
11564
|
+
.description("List saved HubSpot contact segments available for a Sequence import. This is one no-bill provider read and changes no members, DNC entries, rows, workflows, or enrollments.")
|
|
11565
|
+
.argument("<sequence>", "Sequence id or slug.")
|
|
11566
|
+
.option("--query <text>", "Optional case-insensitive words to match in the HubSpot list name.")
|
|
11567
|
+
.option("--json", "Print a JSON envelope.")
|
|
11568
|
+
.action(async (sequence, options) => {
|
|
11569
|
+
await handleAsyncAction("sequences hubspot-list-import lists", options, () => {
|
|
11570
|
+
const params = new URLSearchParams({ sequence });
|
|
11571
|
+
const query = readOption(options.query);
|
|
11572
|
+
if (query)
|
|
11573
|
+
params.set("query", query);
|
|
11574
|
+
return requestOxygen(`/api/cli/sequencer/hubspot-list-import?${params.toString()}`);
|
|
11575
|
+
});
|
|
11576
|
+
}))
|
|
11577
|
+
.addCommand(new Command("configure")
|
|
11578
|
+
.description("Preview or start a scheduled HubSpot-list import. Preview first; then pass its reviewed fingerprint with --live --approved. The complete DNC list is applied before leads are read or enrolled.")
|
|
11579
|
+
.argument("<sequence>", "Sequence id or slug.")
|
|
11580
|
+
.requiredOption("--lead-list <id>", "Exact HubSpot ILS contact-list id whose current members should be imported.")
|
|
11581
|
+
.requiredOption("--dnc-list <id>", "Different exact HubSpot ILS contact-list id synchronized additively into Oxygen DNC first.")
|
|
11582
|
+
.option("--cron <expression>", "Standing cron cadence. Defaults to every 15 minutes.")
|
|
11583
|
+
.option("--timezone <iana>", "IANA timezone. Defaults to UTC.")
|
|
11584
|
+
.option("--max-records-per-list <n>", "Hard complete-snapshot cap per list (1-5000).", "5000")
|
|
11585
|
+
.option("--max-credits <n>", "Hard credit ceiling per delivery.", "10")
|
|
11586
|
+
.option("--include-contacted", "Allow leads already contacted by another active Sequence (default excludes them).")
|
|
11587
|
+
.option("--reviewed-fingerprint <hash>", "Fingerprint returned by the exact dry-run preview; required for live activation.")
|
|
11588
|
+
.option("--idempotency-key <key>", "Optional idempotency key for the immediate first durable run.")
|
|
11589
|
+
.option("--live", "Create/bind the Table if needed, arm the Workflow, and enqueue the first cycle.")
|
|
11590
|
+
.option("--approved", "Approve the reviewed recurring HubSpot reads, additive DNC writes, Table upserts, and Sequence enrollment.")
|
|
11591
|
+
.option("--json", "Print a JSON envelope.")
|
|
11592
|
+
.action(async (sequence, options) => {
|
|
11593
|
+
await handleAsyncAction("sequences hubspot-list-import configure", options, () => {
|
|
11594
|
+
const maxRecords = readPositiveInt(options.maxRecordsPerList);
|
|
11595
|
+
const maxCredits = readPositiveNumber(options.maxCredits);
|
|
11596
|
+
if (maxRecords === undefined || maxRecords > 5000) {
|
|
11597
|
+
throw new Error("--max-records-per-list must be between 1 and 5000.");
|
|
11598
|
+
}
|
|
11599
|
+
if (maxCredits === undefined) {
|
|
11600
|
+
throw new Error("--max-credits must be a positive number.");
|
|
11601
|
+
}
|
|
11602
|
+
const reviewedFingerprint = readOption(options.reviewedFingerprint);
|
|
11603
|
+
if (options.live && !reviewedFingerprint) {
|
|
11604
|
+
throw new Error("Live activation requires --reviewed-fingerprint from the exact dry-run preview.");
|
|
11605
|
+
}
|
|
11606
|
+
return requestOxygen("/api/cli/sequencer/hubspot-list-import", {
|
|
11607
|
+
method: "POST",
|
|
11608
|
+
body: {
|
|
11609
|
+
sequence,
|
|
11610
|
+
lead_list_id: options.leadList,
|
|
11611
|
+
dnc_list_id: options.dncList,
|
|
11612
|
+
mode: options.live ? "live" : "dry_run",
|
|
11613
|
+
max_records_per_list: maxRecords,
|
|
11614
|
+
max_credits: maxCredits,
|
|
11615
|
+
exclude_contacted: options.includeContacted !== true,
|
|
11616
|
+
...(readOption(options.cron) ? { cron: readOption(options.cron) } : {}),
|
|
11617
|
+
...(readOption(options.timezone) ? { timezone: readOption(options.timezone) } : {}),
|
|
11535
11618
|
...(options.approved ? { approved: true } : {}),
|
|
11619
|
+
...(reviewedFingerprint ? { reviewed_fingerprint: reviewedFingerprint } : {}),
|
|
11620
|
+
...(readOption(options.idempotencyKey) ? { idempotency_key: readOption(options.idempotencyKey) } : {}),
|
|
11536
11621
|
},
|
|
11537
11622
|
});
|
|
11538
11623
|
});
|
|
@@ -13925,15 +14010,13 @@ Run completion:
|
|
|
13925
14010
|
});
|
|
13926
14011
|
}))
|
|
13927
14012
|
.addCommand(new Command("list")
|
|
13928
|
-
.description("List workflow automations.
|
|
13929
|
-
.
|
|
14013
|
+
.description("List workflow automations.")
|
|
14014
|
+
.addOption(new Option("--include-archived", "Deprecated no-op retained for compatibility.").hideHelp())
|
|
13930
14015
|
.option("--tag <tag>", "Only workflows carrying this workspace tag (see `oxygen tags list`).")
|
|
13931
14016
|
.option("--json", "Print a JSON envelope.")
|
|
13932
14017
|
.action(async (options) => {
|
|
13933
14018
|
await handleAsyncAction("workflows list", options, async () => {
|
|
13934
14019
|
const params = new URLSearchParams();
|
|
13935
|
-
if (options.includeArchived)
|
|
13936
|
-
params.set("include_archived", "true");
|
|
13937
14020
|
const tag = readOption(options.tag);
|
|
13938
14021
|
if (tag)
|
|
13939
14022
|
params.set("tag", tag);
|
|
@@ -14400,10 +14483,11 @@ Full trigger schema: oxygen workflows schema --subject trigger --json
|
|
|
14400
14483
|
}
|
|
14401
14484
|
}))
|
|
14402
14485
|
.addCommand(new Command("delete")
|
|
14403
|
-
.description("
|
|
14486
|
+
.description("Preview or permanently delete one or more workflow automations. Deletion disarms the workflow and removes its definition, revisions, triggers, and workflow-scoped budget policies; historical run and usage records remain inspectable. Run without --yes to preview, then repeat with --yes to approve.")
|
|
14404
14487
|
.argument("<workflows...>", "One or more workflow ids, slugs, or names.")
|
|
14405
|
-
.
|
|
14406
|
-
.option("--
|
|
14488
|
+
.addOption(new Option("--purge", "Deprecated no-op retained for compatibility.").hideHelp())
|
|
14489
|
+
.option("--yes", "Approve the previewed permanent deletion.")
|
|
14490
|
+
.option("--reason <text>", "Why you are deleting it. Attached to the deletion request.")
|
|
14407
14491
|
.option("--json", "Print a JSON envelope.")
|
|
14408
14492
|
.action(async (workflows, options) => {
|
|
14409
14493
|
try {
|
|
@@ -14412,7 +14496,7 @@ Full trigger schema: oxygen workflows schema --subject trigger --json
|
|
|
14412
14496
|
method: "POST",
|
|
14413
14497
|
body: {
|
|
14414
14498
|
...(single ? { workflow: workflows[0] } : { workflows }),
|
|
14415
|
-
...(options.
|
|
14499
|
+
...(options.yes ? { approved: true } : {}),
|
|
14416
14500
|
...(readOption(options.reason) ? { reason: readOption(options.reason) } : {}),
|
|
14417
14501
|
},
|
|
14418
14502
|
});
|
|
@@ -14423,11 +14507,11 @@ Full trigger schema: oxygen workflows schema --subject trigger --json
|
|
|
14423
14507
|
if (single) {
|
|
14424
14508
|
const workflow = isRecord(data.workflow) ? data.workflow : null;
|
|
14425
14509
|
const slug = workflow && typeof workflow.slug === "string" ? workflow.slug : workflows[0];
|
|
14426
|
-
process.stdout.write(
|
|
14510
|
+
process.stdout.write(`deleted ${slug}\n`);
|
|
14427
14511
|
writeWorkflowActionTail(data);
|
|
14428
14512
|
}
|
|
14429
14513
|
else {
|
|
14430
|
-
writeBulkWorkflowResults(data,
|
|
14514
|
+
writeBulkWorkflowResults(data, "deleted");
|
|
14431
14515
|
}
|
|
14432
14516
|
}
|
|
14433
14517
|
catch (error) {
|
|
@@ -29,7 +29,7 @@ export declare const CRM_SIGNAL_WORKFLOW_EVENT_DEFINITIONS: readonly [{
|
|
|
29
29
|
readonly label: "Reply classified";
|
|
30
30
|
readonly group: "Replies";
|
|
31
31
|
readonly description: "An inbound email, LinkedIn, or WhatsApp reply received a CRM classification.";
|
|
32
|
-
readonly payloadFields: readonly ["channel", "status", "email", "linkedin_url", "external_id", "conversation_id", "conversation_url", "counterpart_name", "message", "sequence_id", "enrollment_id", "primary_source"];
|
|
32
|
+
readonly payloadFields: readonly ["channel", "status", "email", "linkedin_url", "external_id", "conversation_id", "conversation_url", "counterpart_name", "message", "subject", "last_message_text", "occurred_at", "opt_out", "sequence_id", "enrollment_id", "primary_source"];
|
|
33
33
|
}, {
|
|
34
34
|
readonly event: "signup";
|
|
35
35
|
readonly label: "Signup received";
|
|
@@ -40,6 +40,7 @@ export * from "./networks.js";
|
|
|
40
40
|
export * from "./recipes.js";
|
|
41
41
|
export * from "./sequence-template.js";
|
|
42
42
|
export * from "./sequence-crm-events.js";
|
|
43
|
+
export * from "./sequence-hubspot-sync.js";
|
|
43
44
|
export * from "./sequence-terminal-events.js";
|
|
44
45
|
export * from "./call-outcomes.js";
|
|
45
46
|
export * from "./dial-guardrail-overrides.js";
|
|
@@ -40,6 +40,7 @@ export * from "./networks.js";
|
|
|
40
40
|
export * from "./recipes.js";
|
|
41
41
|
export * from "./sequence-template.js";
|
|
42
42
|
export * from "./sequence-crm-events.js";
|
|
43
|
+
export * from "./sequence-hubspot-sync.js";
|
|
43
44
|
export * from "./sequence-terminal-events.js";
|
|
44
45
|
export * from "./call-outcomes.js";
|
|
45
46
|
export * from "./dial-guardrail-overrides.js";
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { type SequenceCrmEventKey } from "./sequence-crm-events.js";
|
|
2
|
+
/**
|
|
3
|
+
* The HubSpot destination is one product setting backed by two ordinary
|
|
4
|
+
* Workflows. Sequence activity and classified replies have different durable
|
|
5
|
+
* event sources, so forcing both through one trigger would either lose reply
|
|
6
|
+
* content or create a second, provider-specific event bus.
|
|
7
|
+
*/
|
|
8
|
+
export declare const SEQUENCE_HUBSPOT_REPLY_SYNC_TEMPLATE_ID = "sequencer-hubspot-reply-sync";
|
|
9
|
+
/** Existing HubSpot's standard LinkedIn field wins; Oxygen creates its own only as a fallback. */
|
|
10
|
+
export declare const HUBSPOT_LINKEDIN_IDENTITY_PROPERTY_CANDIDATES: readonly ["hs_linkedin_url", "oxygen_linkedin_url"];
|
|
11
|
+
export declare const HUBSPOT_DEFAULT_LEAD_STATUS_PROPERTY = "hs_lead_status";
|
|
12
|
+
/**
|
|
13
|
+
* Native HubSpot timeline coverage. Reply activities are owned by the
|
|
14
|
+
* classified-reply Workflow so the activity bridge never creates a contentless
|
|
15
|
+
* duplicate before classification finishes.
|
|
16
|
+
*/
|
|
17
|
+
export declare const DEFAULT_HUBSPOT_SEQUENCE_TIMELINE_EVENTS: readonly ["email_sent", "email_reply_received", "email_bounced", "linkedin_message_sent", "linkedin_inmail_sent", "linkedin_reply_received"];
|
|
18
|
+
/** Events for which the HubSpot adapter has a first-class activity object. */
|
|
19
|
+
export declare const HUBSPOT_SEQUENCE_TIMELINE_EVENT_KEYS: readonly ["email_sent", "email_reply_received", "email_bounced", "linkedin_message_sent", "linkedin_inmail_sent", "linkedin_reply_received"];
|
|
20
|
+
export declare const HUBSPOT_CLASSIFIED_REPLY_TIMELINE_EVENTS: Set<"email_sent" | "meeting_booked" | "email_bounced" | "linkedin_profile_visited" | "linkedin_connection_sent" | "linkedin_connection_accepted" | "linkedin_message_sent" | "linkedin_inmail_sent" | "linkedin_reply_received" | "linkedin_followed" | "linkedin_post_liked" | "linkedin_post_commented" | "linkedin_connection_withdrawn" | "email_campaign_enrolled" | "email_campaign_moved" | "email_campaign_stopped" | "email_opened" | "email_clicked" | "email_reply_received" | "email_unsubscribed" | "email_spam_complaint" | "whatsapp_message_sent" | "whatsapp_reply_received" | "call_task_created" | "call_connected" | "call_no_answer" | "call_voicemail" | "crm_task_created">;
|
|
21
|
+
export type HubSpotSequenceSyncConfig = {
|
|
22
|
+
/** Optional latest-occurrence snapshots on HubSpot contact properties. */
|
|
23
|
+
mappings: Record<SequenceCrmEventKey, string>;
|
|
24
|
+
/** Writable string property used when a recipient has no email identity. */
|
|
25
|
+
linkedin_identity_property: string;
|
|
26
|
+
/** Canonical events projected as native HubSpot Email/Communication activities. */
|
|
27
|
+
timeline_events: SequenceCrmEventKey[];
|
|
28
|
+
/** Writable HubSpot enumeration property receiving mapped lead-status values. */
|
|
29
|
+
lead_status_property: string;
|
|
30
|
+
/** Deterministic activity outcome (for example a bounce) -> HubSpot option value. */
|
|
31
|
+
sequence_event_status_mappings: Partial<Record<SequenceCrmEventKey, string>>;
|
|
32
|
+
/** Classified Oxygen reply status -> HubSpot option value. */
|
|
33
|
+
reply_status_mappings: Record<string, string>;
|
|
34
|
+
/** HubSpot option value that wins when the reply carries opt_out=true. */
|
|
35
|
+
opt_out_status: string;
|
|
36
|
+
};
|
|
37
|
+
export declare const HUBSPOT_SEQUENCE_SYNC_CONFIG_KEYS: readonly ["mappings", "linkedin_identity_property", "timeline_events", "lead_status_property", "sequence_event_status_mappings", "reply_status_mappings", "opt_out_status"];
|
|
38
|
+
/**
|
|
39
|
+
* Portal labels are discovery hints only. Runtime revisions persist the actual
|
|
40
|
+
* portal option values returned by HubSpot, which may be localized or custom.
|
|
41
|
+
*/
|
|
42
|
+
export declare const HUBSPOT_SEQUENCE_STATUS_LABEL_RECOMMENDATIONS: {
|
|
43
|
+
readonly sequenceEvents: {
|
|
44
|
+
readonly email_bounced: readonly ["Bounced", "Bounce"];
|
|
45
|
+
};
|
|
46
|
+
readonly replyStatuses: {
|
|
47
|
+
readonly interested: readonly ["Interested"];
|
|
48
|
+
readonly positive_reply: readonly ["Interested"];
|
|
49
|
+
readonly bounced: readonly ["Bounced", "Bounce"];
|
|
50
|
+
readonly do_not_contact_again: readonly ["Do Not Contact Again", "Do not contact"];
|
|
51
|
+
readonly left_company: readonly ["Left Company"];
|
|
52
|
+
};
|
|
53
|
+
readonly optOut: readonly ["Do Not Contact Again", "Do not contact"];
|
|
54
|
+
};
|
|
55
|
+
/** Fresh mutable defaults for validation/building; callers may safely merge into them. */
|
|
56
|
+
export declare function defaultHubSpotSequenceSyncConfig(): HubSpotSequenceSyncConfig;
|
|
57
|
+
/**
|
|
58
|
+
* Apply a partial operator patch without erasing sibling event/status entries.
|
|
59
|
+
* Arrays and scalar selections replace; keyed maps merge. Validation remains
|
|
60
|
+
* the Workflow template's responsibility after the merge.
|
|
61
|
+
*/
|
|
62
|
+
export declare function mergeHubSpotSequenceSyncConfigInput(current: Record<string, unknown> | null | undefined, patch: Record<string, unknown> | null | undefined): Record<string, unknown>;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { DEFAULT_SEQUENCE_HUBSPOT_EVENT_MAPPINGS, SEQUENCE_HUBSPOT_LINKEDIN_IDENTITY_PROPERTY, } from "./sequence-crm-events.js";
|
|
2
|
+
/**
|
|
3
|
+
* The HubSpot destination is one product setting backed by two ordinary
|
|
4
|
+
* Workflows. Sequence activity and classified replies have different durable
|
|
5
|
+
* event sources, so forcing both through one trigger would either lose reply
|
|
6
|
+
* content or create a second, provider-specific event bus.
|
|
7
|
+
*/
|
|
8
|
+
export const SEQUENCE_HUBSPOT_REPLY_SYNC_TEMPLATE_ID = "sequencer-hubspot-reply-sync";
|
|
9
|
+
/** Existing HubSpot's standard LinkedIn field wins; Oxygen creates its own only as a fallback. */
|
|
10
|
+
export const HUBSPOT_LINKEDIN_IDENTITY_PROPERTY_CANDIDATES = [
|
|
11
|
+
"hs_linkedin_url",
|
|
12
|
+
SEQUENCE_HUBSPOT_LINKEDIN_IDENTITY_PROPERTY,
|
|
13
|
+
];
|
|
14
|
+
export const HUBSPOT_DEFAULT_LEAD_STATUS_PROPERTY = "hs_lead_status";
|
|
15
|
+
/**
|
|
16
|
+
* Native HubSpot timeline coverage. Reply activities are owned by the
|
|
17
|
+
* classified-reply Workflow so the activity bridge never creates a contentless
|
|
18
|
+
* duplicate before classification finishes.
|
|
19
|
+
*/
|
|
20
|
+
export const DEFAULT_HUBSPOT_SEQUENCE_TIMELINE_EVENTS = [
|
|
21
|
+
"email_sent",
|
|
22
|
+
"email_reply_received",
|
|
23
|
+
"email_bounced",
|
|
24
|
+
"linkedin_message_sent",
|
|
25
|
+
"linkedin_inmail_sent",
|
|
26
|
+
"linkedin_reply_received",
|
|
27
|
+
];
|
|
28
|
+
/** Events for which the HubSpot adapter has a first-class activity object. */
|
|
29
|
+
export const HUBSPOT_SEQUENCE_TIMELINE_EVENT_KEYS = DEFAULT_HUBSPOT_SEQUENCE_TIMELINE_EVENTS;
|
|
30
|
+
export const HUBSPOT_CLASSIFIED_REPLY_TIMELINE_EVENTS = new Set([
|
|
31
|
+
"email_reply_received",
|
|
32
|
+
"linkedin_reply_received",
|
|
33
|
+
]);
|
|
34
|
+
const HUBSPOT_SEQUENCE_SYNC_MAP_KEYS = [
|
|
35
|
+
"mappings",
|
|
36
|
+
"sequence_event_status_mappings",
|
|
37
|
+
"reply_status_mappings",
|
|
38
|
+
];
|
|
39
|
+
function recordValue(value) {
|
|
40
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
41
|
+
? value
|
|
42
|
+
: {};
|
|
43
|
+
}
|
|
44
|
+
export const HUBSPOT_SEQUENCE_SYNC_CONFIG_KEYS = [
|
|
45
|
+
"mappings",
|
|
46
|
+
"linkedin_identity_property",
|
|
47
|
+
"timeline_events",
|
|
48
|
+
"lead_status_property",
|
|
49
|
+
"sequence_event_status_mappings",
|
|
50
|
+
"reply_status_mappings",
|
|
51
|
+
"opt_out_status",
|
|
52
|
+
];
|
|
53
|
+
/**
|
|
54
|
+
* Portal labels are discovery hints only. Runtime revisions persist the actual
|
|
55
|
+
* portal option values returned by HubSpot, which may be localized or custom.
|
|
56
|
+
*/
|
|
57
|
+
export const HUBSPOT_SEQUENCE_STATUS_LABEL_RECOMMENDATIONS = {
|
|
58
|
+
sequenceEvents: {
|
|
59
|
+
email_bounced: ["Bounced", "Bounce"],
|
|
60
|
+
},
|
|
61
|
+
replyStatuses: {
|
|
62
|
+
interested: ["Interested"],
|
|
63
|
+
positive_reply: ["Interested"],
|
|
64
|
+
bounced: ["Bounced", "Bounce"],
|
|
65
|
+
do_not_contact_again: ["Do Not Contact Again", "Do not contact"],
|
|
66
|
+
left_company: ["Left Company"],
|
|
67
|
+
},
|
|
68
|
+
optOut: ["Do Not Contact Again", "Do not contact"],
|
|
69
|
+
};
|
|
70
|
+
/** Fresh mutable defaults for validation/building; callers may safely merge into them. */
|
|
71
|
+
export function defaultHubSpotSequenceSyncConfig() {
|
|
72
|
+
return {
|
|
73
|
+
mappings: { ...DEFAULT_SEQUENCE_HUBSPOT_EVENT_MAPPINGS },
|
|
74
|
+
linkedin_identity_property: SEQUENCE_HUBSPOT_LINKEDIN_IDENTITY_PROPERTY,
|
|
75
|
+
timeline_events: [...DEFAULT_HUBSPOT_SEQUENCE_TIMELINE_EVENTS],
|
|
76
|
+
lead_status_property: "",
|
|
77
|
+
sequence_event_status_mappings: {},
|
|
78
|
+
reply_status_mappings: {},
|
|
79
|
+
opt_out_status: "",
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Apply a partial operator patch without erasing sibling event/status entries.
|
|
84
|
+
* Arrays and scalar selections replace; keyed maps merge. Validation remains
|
|
85
|
+
* the Workflow template's responsibility after the merge.
|
|
86
|
+
*/
|
|
87
|
+
export function mergeHubSpotSequenceSyncConfigInput(current, patch) {
|
|
88
|
+
const base = current ?? {};
|
|
89
|
+
const next = patch ?? {};
|
|
90
|
+
const merged = { ...base, ...next };
|
|
91
|
+
for (const key of HUBSPOT_SEQUENCE_SYNC_MAP_KEYS) {
|
|
92
|
+
if (Object.prototype.hasOwnProperty.call(next, key)) {
|
|
93
|
+
merged[key] = {
|
|
94
|
+
...recordValue(base[key]),
|
|
95
|
+
...recordValue(next[key]),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return merged;
|
|
100
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export const OXYGEN_VERSION = "1.
|
|
1
|
+
export const OXYGEN_VERSION = "1.696.1";
|
|
2
2
|
// The GLOBAL CLI compatibility floor: the oldest CLI allowed to call any
|
|
3
3
|
// operational route. Raising it hard-rejects every older CLI from the entire
|
|
4
4
|
// product, so it obeys one law, enforced by scripts/ci/cli-min-version-gate.mjs:
|
|
@@ -11,6 +11,13 @@ const sequenceActivityFields = [
|
|
|
11
11
|
"email",
|
|
12
12
|
"linkedin_url",
|
|
13
13
|
"lead_name",
|
|
14
|
+
"channel",
|
|
15
|
+
"direction",
|
|
16
|
+
"subject",
|
|
17
|
+
"body",
|
|
18
|
+
"body_html",
|
|
19
|
+
"action_kind",
|
|
20
|
+
"provider_message_id",
|
|
14
21
|
];
|
|
15
22
|
const sequenceTerminalFields = [
|
|
16
23
|
"occurred_at",
|