@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 CHANGED
@@ -34,4 +34,4 @@ oxygen update
34
34
 
35
35
  For product documentation, visit https://oxygen-agent.com/docs. For support, visit https://oxygen-agent.com.
36
36
 
37
- Version: 1.692.5
37
+ Version: 1.696.1
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 / archived / purged).
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 ["archived", "purged", "enabled", "disabled", "skipped", "failed"]) {
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 purge\n");
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("Map actual sequencer events to contact datetime fields in the connected HubSpot portal. Reuses native or provider-managed authorization and only requests known-missing property access if a preview needs new fields.")
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 and fetch its writable contact datetime fields, supported sequencer events, current mappings, and enabled state.")
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 event → HubSpot property mappings. Existing fields need no new grant; creating missing properties requires --approved and HubSpot property-creation access.")
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
- ...(mappings ? { config: { mappings } } : {}),
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. Archived workflows are hidden unless you pass --include-archived.")
13929
- .option("--include-archived", "Also list archived workflows, which are hidden by default.")
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("Archive (or, with --purge, permanently delete) one or more workflow automations. Archiving hides the workflow from `workflows list`, disarms its trigger, and is reversible with `workflows enable`/`disable` — see archived workflows again with `workflows list --include-archived`. --purge cannot be undone.")
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
- .option("--purge", "Permanently remove the workflow instead of archiving it. Run history pages for this workflow become unreachable; this cannot be undone.")
14406
- .option("--reason <text>", "Why you are deleting it. Recorded on the workflow and shown to the workspace.")
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.purge ? { purge: true } : {}),
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(`${data.purged === true ? "purged" : "archived"} ${slug}\n`);
14510
+ process.stdout.write(`deleted ${slug}\n`);
14427
14511
  writeWorkflowActionTail(data);
14428
14512
  }
14429
14513
  else {
14430
- writeBulkWorkflowResults(data, options.purge ? "purged" : "archived");
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,10 @@ export const CRM_SIGNAL_WORKFLOW_EVENT_DEFINITIONS = [
40
40
  "conversation_url",
41
41
  "counterpart_name",
42
42
  "message",
43
+ "subject",
44
+ "last_message_text",
45
+ "occurred_at",
46
+ "opt_out",
43
47
  "sequence_id",
44
48
  "enrollment_id",
45
49
  "primary_source",
@@ -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,3 +1,3 @@
1
- export declare const OXYGEN_VERSION = "1.692.5";
1
+ export declare const OXYGEN_VERSION = "1.696.1";
2
2
  export declare const OXYGEN_MINIMUM_CLI_VERSION = "1.181.0";
3
3
  export declare const MANAGED_INBOX_MINIMUM_CLI_VERSION = "1.326.2";
@@ -1,4 +1,4 @@
1
- export const OXYGEN_VERSION = "1.692.5";
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",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxygen-agent/cli",
3
- "version": "1.692.5",
3
+ "version": "1.696.1",
4
4
  "private": false,
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",