@oxygen-agent/cli 1.142.4 → 1.152.15
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/http-client.js +44 -28
- package/dist/index.js +690 -16
- package/node_modules/@oxygen/shared/dist/index.d.ts +24 -0
- package/node_modules/@oxygen/shared/dist/index.js +49 -0
- package/node_modules/@oxygen/shared/dist/linkedin-sequences.d.ts +83 -0
- package/node_modules/@oxygen/shared/dist/linkedin-sequences.js +268 -0
- package/node_modules/@oxygen/shared/dist/telemetry.d.ts +4 -1
- package/node_modules/@oxygen/shared/dist/telemetry.js +17 -5
- 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/workflow-trigger-metadata.d.ts +2 -0
- package/node_modules/@oxygen/shared/dist/workflow-trigger-metadata.js +14 -0
- package/package.json +1 -1
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
export { OXYGEN_MINIMUM_CLI_VERSION, OXYGEN_VERSION } from "./version.js";
|
|
2
|
+
export { WORKFLOW_TRIGGER_AUTO_PAUSE_METADATA_KEYS, clearWorkflowTriggerAutoPauseMetadata, } from "./workflow-trigger-metadata.js";
|
|
2
3
|
export * from "./billing.js";
|
|
3
4
|
export * from "./cell-format.js";
|
|
4
5
|
export * from "./column-types.js";
|
|
5
6
|
export * from "./credit-guidance.js";
|
|
7
|
+
export * from "./linkedin-sequences.js";
|
|
6
8
|
export * from "./log.js";
|
|
7
9
|
export * from "./provider-request-outcomes.js";
|
|
8
10
|
export * from "./signup-lead-deliveries.js";
|
|
9
11
|
export * from "./telemetry.js";
|
|
12
|
+
export declare const MAX_ROW_LOOP_WRITE_ROWS = 500;
|
|
10
13
|
export type JsonValue = string | number | boolean | null | JsonValue[] | {
|
|
11
14
|
[key: string]: JsonValue;
|
|
12
15
|
};
|
|
@@ -46,3 +49,24 @@ export declare function failure(command: string, error: {
|
|
|
46
49
|
details?: unknown;
|
|
47
50
|
}, version?: string, minimumCliVersion?: string): CliFailure;
|
|
48
51
|
export declare function toFailure(command: string, error: unknown, version?: string): CliFailure;
|
|
52
|
+
export type SemanticVersion = {
|
|
53
|
+
major: number;
|
|
54
|
+
minor: number;
|
|
55
|
+
patch: number;
|
|
56
|
+
};
|
|
57
|
+
/**
|
|
58
|
+
* Parse a three-segment semantic version (e.g. `1.142.17`). Pre-release and
|
|
59
|
+
* build metadata suffixes (`-rc.1`, `+build`) are tolerated but ignored.
|
|
60
|
+
* Returns `null` when the input is not a parseable `major.minor.patch` string.
|
|
61
|
+
*/
|
|
62
|
+
export declare function parseSemver(version: string): SemanticVersion | null;
|
|
63
|
+
/**
|
|
64
|
+
* Compare two semantic versions. Returns -1 when `a < b`, 1 when `a > b`, and
|
|
65
|
+
* 0 when they are equal. Unparseable inputs compare as equal (0) so callers
|
|
66
|
+
* fail open rather than misordering garbage.
|
|
67
|
+
*/
|
|
68
|
+
export declare function compareSemver(a: string, b: string): -1 | 0 | 1;
|
|
69
|
+
/** True when `a` is a strictly greater semantic version than `b`. */
|
|
70
|
+
export declare function isVersionGreater(a: string, b: string): boolean;
|
|
71
|
+
/** True when `a` is a strictly lesser semantic version than `b`. */
|
|
72
|
+
export declare function isVersionLess(a: string, b: string): boolean;
|
|
@@ -1,13 +1,21 @@
|
|
|
1
1
|
import { OXYGEN_MINIMUM_CLI_VERSION, OXYGEN_VERSION } from "./version.js";
|
|
2
2
|
export { OXYGEN_MINIMUM_CLI_VERSION, OXYGEN_VERSION } from "./version.js";
|
|
3
|
+
export { WORKFLOW_TRIGGER_AUTO_PAUSE_METADATA_KEYS, clearWorkflowTriggerAutoPauseMetadata, } from "./workflow-trigger-metadata.js";
|
|
3
4
|
export * from "./billing.js";
|
|
4
5
|
export * from "./cell-format.js";
|
|
5
6
|
export * from "./column-types.js";
|
|
6
7
|
export * from "./credit-guidance.js";
|
|
8
|
+
export * from "./linkedin-sequences.js";
|
|
7
9
|
export * from "./log.js";
|
|
8
10
|
export * from "./provider-request-outcomes.js";
|
|
9
11
|
export * from "./signup-lead-deliveries.js";
|
|
10
12
|
export * from "./telemetry.js";
|
|
13
|
+
// Maximum rows a single row-loop write (insert/upsert/preview) may process. The
|
|
14
|
+
// row-loop engine issues one DB round-trip per row, so a 500-row write already
|
|
15
|
+
// approaches request timeouts (~50s observed in prod); larger batches must use
|
|
16
|
+
// the COPY-based bulk engine. Tenant-db enforces this and the CLI/API row caps
|
|
17
|
+
// reference it so they never advertise a batch the row-loop will reject.
|
|
18
|
+
export const MAX_ROW_LOOP_WRITE_ROWS = 500;
|
|
11
19
|
export class OxygenError extends Error {
|
|
12
20
|
code;
|
|
13
21
|
details;
|
|
@@ -51,3 +59,44 @@ export function toFailure(command, error, version = OXYGEN_VERSION) {
|
|
|
51
59
|
}
|
|
52
60
|
return failure(command, { code: "unexpected_error", message: "An unexpected error occurred." }, version);
|
|
53
61
|
}
|
|
62
|
+
/**
|
|
63
|
+
* Parse a three-segment semantic version (e.g. `1.142.17`). Pre-release and
|
|
64
|
+
* build metadata suffixes (`-rc.1`, `+build`) are tolerated but ignored.
|
|
65
|
+
* Returns `null` when the input is not a parseable `major.minor.patch` string.
|
|
66
|
+
*/
|
|
67
|
+
export function parseSemver(version) {
|
|
68
|
+
const match = /^(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/.exec(version);
|
|
69
|
+
if (!match)
|
|
70
|
+
return null;
|
|
71
|
+
return {
|
|
72
|
+
major: Number(match[1]),
|
|
73
|
+
minor: Number(match[2]),
|
|
74
|
+
patch: Number(match[3]),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Compare two semantic versions. Returns -1 when `a < b`, 1 when `a > b`, and
|
|
79
|
+
* 0 when they are equal. Unparseable inputs compare as equal (0) so callers
|
|
80
|
+
* fail open rather than misordering garbage.
|
|
81
|
+
*/
|
|
82
|
+
export function compareSemver(a, b) {
|
|
83
|
+
const left = parseSemver(a);
|
|
84
|
+
const right = parseSemver(b);
|
|
85
|
+
if (!left || !right)
|
|
86
|
+
return 0;
|
|
87
|
+
for (const key of ["major", "minor", "patch"]) {
|
|
88
|
+
if (left[key] > right[key])
|
|
89
|
+
return 1;
|
|
90
|
+
if (left[key] < right[key])
|
|
91
|
+
return -1;
|
|
92
|
+
}
|
|
93
|
+
return 0;
|
|
94
|
+
}
|
|
95
|
+
/** True when `a` is a strictly greater semantic version than `b`. */
|
|
96
|
+
export function isVersionGreater(a, b) {
|
|
97
|
+
return compareSemver(a, b) > 0;
|
|
98
|
+
}
|
|
99
|
+
/** True when `a` is a strictly lesser semantic version than `b`. */
|
|
100
|
+
export function isVersionLess(a, b) {
|
|
101
|
+
return compareSemver(a, b) < 0;
|
|
102
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LinkedIn sequence step schema — the shared contract validated identically by
|
|
3
|
+
* CLI, MCP, API, and web. A sequence is an ordered list of steps applied to
|
|
4
|
+
* each enrolled lead; the worker dispatch engine materializes one
|
|
5
|
+
* sequence_action per step as it comes due.
|
|
6
|
+
*
|
|
7
|
+
* Step kinds:
|
|
8
|
+
* - visit_profile — view the lead's profile (warms up before an invite)
|
|
9
|
+
* - invite — send a connection request, optional note_template
|
|
10
|
+
* - wait_for_connection — gate: wait until the invite is accepted (new_relation
|
|
11
|
+
* webhook) or timeout_days elapses; on timeout stop or
|
|
12
|
+
* continue per on_timeout
|
|
13
|
+
* - wait — fixed delay (days/hours) before the next step
|
|
14
|
+
* - message — send a message (opens a chat if none exists);
|
|
15
|
+
* template supports {{column}} interpolation
|
|
16
|
+
* - inmail — send an InMail (works on non-connections); subject +
|
|
17
|
+
* template
|
|
18
|
+
*/
|
|
19
|
+
export declare const LINKEDIN_SEQUENCE_STEP_KINDS: readonly ["visit_profile", "invite", "wait_for_connection", "wait", "message", "inmail"];
|
|
20
|
+
export type LinkedInSequenceStepKind = typeof LINKEDIN_SEQUENCE_STEP_KINDS[number];
|
|
21
|
+
export type LinkedInVisitProfileStep = {
|
|
22
|
+
kind: "visit_profile";
|
|
23
|
+
};
|
|
24
|
+
export type LinkedInInviteStep = {
|
|
25
|
+
kind: "invite";
|
|
26
|
+
/** Optional connection-request note. Supports {{column}} interpolation. ~300 char max on LinkedIn. */
|
|
27
|
+
note_template?: string;
|
|
28
|
+
};
|
|
29
|
+
export type LinkedInWaitForConnectionStep = {
|
|
30
|
+
kind: "wait_for_connection";
|
|
31
|
+
/** Days to wait for the invite to be accepted before acting on on_timeout. */
|
|
32
|
+
timeout_days: number;
|
|
33
|
+
/** What to do if the invite is never accepted. Defaults to "stop". */
|
|
34
|
+
on_timeout?: "stop" | "continue";
|
|
35
|
+
};
|
|
36
|
+
export type LinkedInWaitStep = {
|
|
37
|
+
kind: "wait";
|
|
38
|
+
/** Fixed delay before the next step. Provide days and/or hours (>= 1 total). */
|
|
39
|
+
days?: number;
|
|
40
|
+
hours?: number;
|
|
41
|
+
};
|
|
42
|
+
export type LinkedInMessageStep = {
|
|
43
|
+
kind: "message";
|
|
44
|
+
/** Message body. Supports {{column}} interpolation from the source-table row. */
|
|
45
|
+
template: string;
|
|
46
|
+
};
|
|
47
|
+
export type LinkedInInMailStep = {
|
|
48
|
+
kind: "inmail";
|
|
49
|
+
subject_template: string;
|
|
50
|
+
template: string;
|
|
51
|
+
};
|
|
52
|
+
export type LinkedInSequenceStep = LinkedInVisitProfileStep | LinkedInInviteStep | LinkedInWaitForConnectionStep | LinkedInWaitStep | LinkedInMessageStep | LinkedInInMailStep;
|
|
53
|
+
export type LinkedInSequenceDefinition = {
|
|
54
|
+
steps: LinkedInSequenceStep[];
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* The dispatch-queue action kind a step produces (matches the
|
|
58
|
+
* ox_linkedin.sequence_actions.action_kind enum), or null for gate/wait steps
|
|
59
|
+
* that dispatch nothing. NOTE: this is the *action* kind, not the *quota* kind
|
|
60
|
+
* — visit_profile maps to the quota kind profile_view downstream.
|
|
61
|
+
*/
|
|
62
|
+
export declare const LINKEDIN_STEP_ACTION_KIND: Record<LinkedInSequenceStepKind, "visit_profile" | "invite" | "message" | "inmail" | null>;
|
|
63
|
+
export type LinkedInSequenceLintIssue = {
|
|
64
|
+
path: string;
|
|
65
|
+
message: string;
|
|
66
|
+
};
|
|
67
|
+
/**
|
|
68
|
+
* Validate a raw sequence definition. Returns the normalized definition or
|
|
69
|
+
* throws OxygenError("invalid_linkedin_sequence") with per-step issues. Pure —
|
|
70
|
+
* safe to call from any surface.
|
|
71
|
+
*/
|
|
72
|
+
export declare function validateLinkedInSequenceDefinition(input: unknown): LinkedInSequenceDefinition;
|
|
73
|
+
/** Non-throwing variant for lint surfaces. */
|
|
74
|
+
export declare function lintLinkedInSequenceDefinition(input: unknown): LinkedInSequenceLintIssue[];
|
|
75
|
+
/** Total delay in milliseconds a `wait` step introduces. */
|
|
76
|
+
export declare function waitStepDelayMs(step: LinkedInWaitStep): number;
|
|
77
|
+
/**
|
|
78
|
+
* Render a {{column}} template against a row's values. Unknown placeholders
|
|
79
|
+
* render empty. Used by the dispatch engine to produce the final message text.
|
|
80
|
+
*/
|
|
81
|
+
export declare function renderLinkedInTemplate(template: string, values: Record<string, unknown>): string;
|
|
82
|
+
/** Column keys referenced by {{...}} placeholders across all steps. */
|
|
83
|
+
export declare function linkedInTemplateVariables(definition: LinkedInSequenceDefinition): string[];
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import { OxygenError } from "./index.js";
|
|
2
|
+
/**
|
|
3
|
+
* LinkedIn sequence step schema — the shared contract validated identically by
|
|
4
|
+
* CLI, MCP, API, and web. A sequence is an ordered list of steps applied to
|
|
5
|
+
* each enrolled lead; the worker dispatch engine materializes one
|
|
6
|
+
* sequence_action per step as it comes due.
|
|
7
|
+
*
|
|
8
|
+
* Step kinds:
|
|
9
|
+
* - visit_profile — view the lead's profile (warms up before an invite)
|
|
10
|
+
* - invite — send a connection request, optional note_template
|
|
11
|
+
* - wait_for_connection — gate: wait until the invite is accepted (new_relation
|
|
12
|
+
* webhook) or timeout_days elapses; on timeout stop or
|
|
13
|
+
* continue per on_timeout
|
|
14
|
+
* - wait — fixed delay (days/hours) before the next step
|
|
15
|
+
* - message — send a message (opens a chat if none exists);
|
|
16
|
+
* template supports {{column}} interpolation
|
|
17
|
+
* - inmail — send an InMail (works on non-connections); subject +
|
|
18
|
+
* template
|
|
19
|
+
*/
|
|
20
|
+
export const LINKEDIN_SEQUENCE_STEP_KINDS = [
|
|
21
|
+
"visit_profile",
|
|
22
|
+
"invite",
|
|
23
|
+
"wait_for_connection",
|
|
24
|
+
"wait",
|
|
25
|
+
"message",
|
|
26
|
+
"inmail",
|
|
27
|
+
];
|
|
28
|
+
/**
|
|
29
|
+
* The dispatch-queue action kind a step produces (matches the
|
|
30
|
+
* ox_linkedin.sequence_actions.action_kind enum), or null for gate/wait steps
|
|
31
|
+
* that dispatch nothing. NOTE: this is the *action* kind, not the *quota* kind
|
|
32
|
+
* — visit_profile maps to the quota kind profile_view downstream.
|
|
33
|
+
*/
|
|
34
|
+
export const LINKEDIN_STEP_ACTION_KIND = {
|
|
35
|
+
visit_profile: "visit_profile",
|
|
36
|
+
invite: "invite",
|
|
37
|
+
wait_for_connection: null,
|
|
38
|
+
wait: null,
|
|
39
|
+
message: "message",
|
|
40
|
+
inmail: "inmail",
|
|
41
|
+
};
|
|
42
|
+
const MAX_STEPS = 25;
|
|
43
|
+
const MAX_TEMPLATE_LENGTH = 8_000;
|
|
44
|
+
const MAX_NOTE_LENGTH = 300;
|
|
45
|
+
/**
|
|
46
|
+
* Validate a raw sequence definition. Returns the normalized definition or
|
|
47
|
+
* throws OxygenError("invalid_linkedin_sequence") with per-step issues. Pure —
|
|
48
|
+
* safe to call from any surface.
|
|
49
|
+
*/
|
|
50
|
+
export function validateLinkedInSequenceDefinition(input) {
|
|
51
|
+
const issues = [];
|
|
52
|
+
const steps = collectSteps(input, issues);
|
|
53
|
+
const normalized = [];
|
|
54
|
+
steps.forEach((rawStep, index) => {
|
|
55
|
+
const step = normalizeStep(rawStep, index, issues);
|
|
56
|
+
if (step)
|
|
57
|
+
normalized.push(step);
|
|
58
|
+
});
|
|
59
|
+
validateStructure(normalized, issues);
|
|
60
|
+
if (issues.length > 0) {
|
|
61
|
+
throw new OxygenError("invalid_linkedin_sequence", `Sequence definition is invalid: ${issues.map((i) => `${i.path}: ${i.message}`).join("; ")}`, { details: { issues }, exitCode: 1 });
|
|
62
|
+
}
|
|
63
|
+
return { steps: normalized };
|
|
64
|
+
}
|
|
65
|
+
/** Non-throwing variant for lint surfaces. */
|
|
66
|
+
export function lintLinkedInSequenceDefinition(input) {
|
|
67
|
+
try {
|
|
68
|
+
validateLinkedInSequenceDefinition(input);
|
|
69
|
+
return [];
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
if (error instanceof OxygenError && error.details && typeof error.details === "object") {
|
|
73
|
+
const issues = error.details.issues;
|
|
74
|
+
if (Array.isArray(issues))
|
|
75
|
+
return issues;
|
|
76
|
+
}
|
|
77
|
+
return [{ path: "steps", message: error instanceof Error ? error.message : "Invalid sequence." }];
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function collectSteps(input, issues) {
|
|
81
|
+
const record = isRecord(input) ? input : null;
|
|
82
|
+
const steps = record?.steps;
|
|
83
|
+
if (!Array.isArray(steps)) {
|
|
84
|
+
issues.push({ path: "steps", message: "steps must be an array." });
|
|
85
|
+
return [];
|
|
86
|
+
}
|
|
87
|
+
if (steps.length === 0) {
|
|
88
|
+
issues.push({ path: "steps", message: "A sequence needs at least one step." });
|
|
89
|
+
}
|
|
90
|
+
if (steps.length > MAX_STEPS) {
|
|
91
|
+
issues.push({ path: "steps", message: `A sequence may have at most ${MAX_STEPS} steps.` });
|
|
92
|
+
}
|
|
93
|
+
return steps;
|
|
94
|
+
}
|
|
95
|
+
function normalizeStep(// skipcq: JS-R1005 -- step normalization validates a discriminated sequence DSL with per-step fields.
|
|
96
|
+
raw, index, issues) {
|
|
97
|
+
const path = `steps[${index}]`;
|
|
98
|
+
if (!isRecord(raw)) {
|
|
99
|
+
issues.push({ path, message: "Each step must be an object." });
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
const kind = raw.kind;
|
|
103
|
+
if (typeof kind !== "string" || !LINKEDIN_SEQUENCE_STEP_KINDS.includes(kind)) {
|
|
104
|
+
issues.push({
|
|
105
|
+
path: `${path}.kind`,
|
|
106
|
+
message: `kind must be one of: ${LINKEDIN_SEQUENCE_STEP_KINDS.join(", ")}.`,
|
|
107
|
+
});
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
switch (kind) {
|
|
111
|
+
case "visit_profile":
|
|
112
|
+
return { kind: "visit_profile" };
|
|
113
|
+
case "invite": {
|
|
114
|
+
const note = optionalTemplate(raw.note_template, `${path}.note_template`, MAX_NOTE_LENGTH, issues);
|
|
115
|
+
return note !== undefined ? { kind: "invite", note_template: note } : { kind: "invite" };
|
|
116
|
+
}
|
|
117
|
+
case "wait_for_connection": {
|
|
118
|
+
// timeout_days is optional (defaults to 14); only validate when provided.
|
|
119
|
+
const timeoutDays = raw.timeout_days === undefined || raw.timeout_days === null
|
|
120
|
+
? undefined
|
|
121
|
+
: positiveInt(raw.timeout_days, `${path}.timeout_days`, issues);
|
|
122
|
+
const onTimeout = raw.on_timeout;
|
|
123
|
+
if (onTimeout !== undefined && onTimeout !== "stop" && onTimeout !== "continue") {
|
|
124
|
+
issues.push({ path: `${path}.on_timeout`, message: "on_timeout must be 'stop' or 'continue'." });
|
|
125
|
+
}
|
|
126
|
+
return {
|
|
127
|
+
kind: "wait_for_connection",
|
|
128
|
+
timeout_days: timeoutDays ?? 14,
|
|
129
|
+
on_timeout: onTimeout === "continue" ? "continue" : "stop",
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
case "wait": {
|
|
133
|
+
const days = optionalNonNegativeInt(raw.days, `${path}.days`, issues);
|
|
134
|
+
const hours = optionalNonNegativeInt(raw.hours, `${path}.hours`, issues);
|
|
135
|
+
if ((days ?? 0) + (hours ?? 0) <= 0) {
|
|
136
|
+
issues.push({ path, message: "A wait step needs days and/or hours totaling at least 1 hour." });
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
kind: "wait",
|
|
140
|
+
...(days !== undefined ? { days } : {}),
|
|
141
|
+
...(hours !== undefined ? { hours } : {}),
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
case "message": {
|
|
145
|
+
const template = requiredTemplate(raw.template, `${path}.template`, issues);
|
|
146
|
+
return { kind: "message", template: template ?? "" };
|
|
147
|
+
}
|
|
148
|
+
case "inmail": {
|
|
149
|
+
const subject = requiredTemplate(raw.subject_template, `${path}.subject_template`, issues);
|
|
150
|
+
const template = requiredTemplate(raw.template, `${path}.template`, issues);
|
|
151
|
+
return { kind: "inmail", subject_template: subject ?? "", template: template ?? "" };
|
|
152
|
+
}
|
|
153
|
+
default:
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Structural rules across steps:
|
|
159
|
+
* - The first send step must be invite or visit_profile (you can't message a
|
|
160
|
+
* stranger without connecting); a message before any invite/wait_for_connection
|
|
161
|
+
* is valid for warm lists whose leads are already 1st-degree connections.
|
|
162
|
+
* - wait_for_connection must be preceded by an invite.
|
|
163
|
+
* - Two consecutive wait/wait_for_connection steps are pointless.
|
|
164
|
+
*/
|
|
165
|
+
function validateStructure(steps, issues) {
|
|
166
|
+
let sawInvite = false;
|
|
167
|
+
steps.forEach((step, index) => {
|
|
168
|
+
if (step.kind === "invite")
|
|
169
|
+
sawInvite = true;
|
|
170
|
+
if (step.kind === "wait_for_connection" && !sawInvite) {
|
|
171
|
+
issues.push({
|
|
172
|
+
path: `steps[${index}]`,
|
|
173
|
+
message: "wait_for_connection must come after an invite step.",
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
if (step.kind === "message" && !sawInvite && !steps.slice(0, index).some((s) => s.kind === "wait_for_connection")) {
|
|
177
|
+
// A message before connecting only works for existing 1st-degree
|
|
178
|
+
// connections. This is valid for warm lists; do not add a fatal issue
|
|
179
|
+
// until the API has a separate warning channel.
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
function requiredTemplate(value, path, issues) {
|
|
184
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
185
|
+
issues.push({ path, message: "is required and must be a non-empty string." });
|
|
186
|
+
return undefined;
|
|
187
|
+
}
|
|
188
|
+
if (value.length > MAX_TEMPLATE_LENGTH) {
|
|
189
|
+
issues.push({ path, message: `must be at most ${MAX_TEMPLATE_LENGTH} characters.` });
|
|
190
|
+
return undefined;
|
|
191
|
+
}
|
|
192
|
+
return value;
|
|
193
|
+
}
|
|
194
|
+
function optionalTemplate(value, path, maxLength, issues) {
|
|
195
|
+
if (value === undefined || value === null)
|
|
196
|
+
return undefined;
|
|
197
|
+
if (typeof value !== "string") {
|
|
198
|
+
issues.push({ path, message: "must be a string." });
|
|
199
|
+
return undefined;
|
|
200
|
+
}
|
|
201
|
+
if (value.length > maxLength) {
|
|
202
|
+
issues.push({ path, message: `must be at most ${maxLength} characters.` });
|
|
203
|
+
return undefined;
|
|
204
|
+
}
|
|
205
|
+
return value;
|
|
206
|
+
}
|
|
207
|
+
function positiveInt(value, path, issues) {
|
|
208
|
+
const num = Number(value);
|
|
209
|
+
if (!Number.isInteger(num) || num <= 0) {
|
|
210
|
+
issues.push({ path, message: "must be a positive integer." });
|
|
211
|
+
return undefined;
|
|
212
|
+
}
|
|
213
|
+
return num;
|
|
214
|
+
}
|
|
215
|
+
function optionalNonNegativeInt(value, path, issues) {
|
|
216
|
+
if (value === undefined || value === null)
|
|
217
|
+
return undefined;
|
|
218
|
+
const num = Number(value);
|
|
219
|
+
if (!Number.isInteger(num) || num < 0) {
|
|
220
|
+
issues.push({ path, message: "must be a non-negative integer." });
|
|
221
|
+
return undefined;
|
|
222
|
+
}
|
|
223
|
+
return num;
|
|
224
|
+
}
|
|
225
|
+
/** Total delay in milliseconds a `wait` step introduces. */
|
|
226
|
+
export function waitStepDelayMs(step) {
|
|
227
|
+
const days = step.days ?? 0;
|
|
228
|
+
const hours = step.hours ?? 0;
|
|
229
|
+
return (days * 24 + hours) * 60 * 60 * 1000;
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Render a {{column}} template against a row's values. Unknown placeholders
|
|
233
|
+
* render empty. Used by the dispatch engine to produce the final message text.
|
|
234
|
+
*/
|
|
235
|
+
export function renderLinkedInTemplate(template, values) {
|
|
236
|
+
return template.replace(/\{\{\s*([\w.]+)\s*\}\}/g, (_match, key) => {
|
|
237
|
+
const value = values[key];
|
|
238
|
+
if (value === null || value === undefined)
|
|
239
|
+
return "";
|
|
240
|
+
return typeof value === "string" ? value : String(value);
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
/** Column keys referenced by {{...}} placeholders across all steps. */
|
|
244
|
+
export function linkedInTemplateVariables(definition) {
|
|
245
|
+
const vars = new Set();
|
|
246
|
+
const scan = (template) => {
|
|
247
|
+
if (!template)
|
|
248
|
+
return;
|
|
249
|
+
for (const match of template.matchAll(/\{\{\s*([\w.]+)\s*\}\}/g)) {
|
|
250
|
+
if (match[1])
|
|
251
|
+
vars.add(match[1]);
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
for (const step of definition.steps) {
|
|
255
|
+
if (step.kind === "invite")
|
|
256
|
+
scan(step.note_template);
|
|
257
|
+
if (step.kind === "message")
|
|
258
|
+
scan(step.template);
|
|
259
|
+
if (step.kind === "inmail") {
|
|
260
|
+
scan(step.subject_template);
|
|
261
|
+
scan(step.template);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return [...vars];
|
|
265
|
+
}
|
|
266
|
+
function isRecord(value) {
|
|
267
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
268
|
+
}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
export type TelemetryAttributes = Record<string, unknown>;
|
|
2
|
-
export
|
|
2
|
+
export type WithTelemetrySpanOptions = {
|
|
3
|
+
isTransient?: (error: unknown) => boolean;
|
|
4
|
+
};
|
|
5
|
+
export declare function withTelemetrySpan<T>(tracerName: string, name: string, attributes: TelemetryAttributes | undefined, fn: () => Promise<T>, options?: WithTelemetrySpanOptions): Promise<T>;
|
|
3
6
|
export declare function setActiveTelemetryAttributes(attributes: TelemetryAttributes): void;
|
|
4
7
|
export declare function markActiveTelemetryError(message: string, attributes?: TelemetryAttributes): void;
|
|
5
8
|
export declare function addTelemetryEvent(name: string, attributes?: TelemetryAttributes): void;
|
|
@@ -4,7 +4,7 @@ import { normalizeTelemetryAttributes } from "./redaction.js";
|
|
|
4
4
|
import { OXYGEN_VERSION } from "./version.js";
|
|
5
5
|
const counterCache = new Map();
|
|
6
6
|
const histogramCache = new Map();
|
|
7
|
-
export async function withTelemetrySpan(tracerName, name, attributes, fn) {
|
|
7
|
+
export async function withTelemetrySpan(tracerName, name, attributes, fn, options) {
|
|
8
8
|
const tracer = trace.getTracer(tracerName, OXYGEN_VERSION);
|
|
9
9
|
return tracer.startActiveSpan(name, { attributes: normalizeTelemetryAttributes(commonTelemetryAttributes(attributes)) }, async (span) => {
|
|
10
10
|
try {
|
|
@@ -12,8 +12,16 @@ export async function withTelemetrySpan(tracerName, name, attributes, fn) {
|
|
|
12
12
|
}
|
|
13
13
|
catch (error) {
|
|
14
14
|
span.recordException(error instanceof Error ? error : new Error(String(error)));
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
if (options?.isTransient?.(error) === true) {
|
|
16
|
+
span.setAttributes(normalizeTelemetryAttributes({
|
|
17
|
+
...errorTelemetryAttributes(error),
|
|
18
|
+
outcome: "transient_error",
|
|
19
|
+
}));
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
span.setStatus({ code: SpanStatusCode.ERROR });
|
|
23
|
+
span.setAttributes(normalizeTelemetryAttributes(errorTelemetryAttributes(error)));
|
|
24
|
+
}
|
|
17
25
|
throw error;
|
|
18
26
|
}
|
|
19
27
|
finally {
|
|
@@ -97,14 +105,18 @@ function getHistogram(name) {
|
|
|
97
105
|
}
|
|
98
106
|
function errorTelemetryAttributes(error) {
|
|
99
107
|
if (error instanceof Error) {
|
|
108
|
+
// pg connect timeouts (and some driver errors) surface with an empty
|
|
109
|
+
// message; fall back to the error name so spans are never message-less.
|
|
110
|
+
const message = error.message.trim() ? error.message : error.name || "unknown_error";
|
|
100
111
|
return {
|
|
101
112
|
"error.id": errorId(error),
|
|
102
113
|
"error.name": error.name,
|
|
103
|
-
"error.message":
|
|
114
|
+
"error.message": message,
|
|
104
115
|
};
|
|
105
116
|
}
|
|
117
|
+
const text = String(error).trim();
|
|
106
118
|
return {
|
|
107
119
|
"error.id": "non_error",
|
|
108
|
-
"error.message":
|
|
120
|
+
"error.message": text || "unknown_error",
|
|
109
121
|
};
|
|
110
122
|
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const OXYGEN_VERSION = "1.
|
|
1
|
+
export declare const OXYGEN_VERSION = "1.152.15";
|
|
2
2
|
export declare const OXYGEN_MINIMUM_CLI_VERSION = "1.135.0";
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export declare const WORKFLOW_TRIGGER_AUTO_PAUSE_METADATA_KEYS: readonly ["consecutive_failure_count", "last_failure_code", "last_failure_at", "auto_paused_at", "auto_pause_reason"];
|
|
2
|
+
export declare function clearWorkflowTriggerAutoPauseMetadata(metadata: Record<string, unknown>): Record<string, unknown>;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export const WORKFLOW_TRIGGER_AUTO_PAUSE_METADATA_KEYS = [
|
|
2
|
+
"consecutive_failure_count",
|
|
3
|
+
"last_failure_code",
|
|
4
|
+
"last_failure_at",
|
|
5
|
+
"auto_paused_at",
|
|
6
|
+
"auto_pause_reason",
|
|
7
|
+
];
|
|
8
|
+
export function clearWorkflowTriggerAutoPauseMetadata(metadata) {
|
|
9
|
+
const next = { ...metadata };
|
|
10
|
+
for (const key of WORKFLOW_TRIGGER_AUTO_PAUSE_METADATA_KEYS) {
|
|
11
|
+
delete next[key];
|
|
12
|
+
}
|
|
13
|
+
return next;
|
|
14
|
+
}
|