@oxygen-agent/cli 1.377.3 → 1.591.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/column-run-notices.d.ts +11 -0
- package/dist/column-run-notices.js +37 -0
- package/dist/command-manifest.js +13 -8
- package/dist/help.js +79 -16
- package/dist/index.js +3812 -460
- package/dist/skills.js +106 -1
- package/node_modules/@oxygen/formula/dist/coerce.d.ts +8 -0
- package/node_modules/@oxygen/formula/dist/coerce.js +10 -0
- package/node_modules/@oxygen/formula/dist/evaluate.d.ts +31 -0
- package/node_modules/@oxygen/formula/dist/evaluate.js +248 -0
- package/node_modules/@oxygen/formula/dist/expression.d.ts +64 -0
- package/node_modules/@oxygen/formula/dist/expression.js +428 -0
- package/node_modules/@oxygen/formula/dist/formula-functions.d.ts +71 -0
- package/node_modules/@oxygen/formula/dist/formula-functions.js +1100 -0
- package/node_modules/@oxygen/formula/dist/index.d.ts +17 -0
- package/node_modules/@oxygen/formula/dist/index.js +17 -0
- package/node_modules/@oxygen/formula/dist/value-normalizers.d.ts +30 -0
- package/node_modules/@oxygen/formula/dist/value-normalizers.js +80 -0
- package/node_modules/@oxygen/formula/package.json +26 -0
- package/node_modules/@oxygen/recipe-sdk/dist/index.d.ts +30 -0
- package/node_modules/@oxygen/recipe-sdk/dist/index.js +2 -2
- package/node_modules/@oxygen/shared/dist/billing-anchors.d.ts +60 -0
- package/node_modules/@oxygen/shared/dist/billing-anchors.js +135 -0
- package/node_modules/@oxygen/shared/dist/billing.d.ts +99 -5
- package/node_modules/@oxygen/shared/dist/billing.js +185 -8
- package/node_modules/@oxygen/shared/dist/call-outcomes.d.ts +59 -0
- package/node_modules/@oxygen/shared/dist/call-outcomes.js +73 -0
- package/node_modules/@oxygen/shared/dist/cli-result.js +1 -0
- package/node_modules/@oxygen/shared/dist/credit-guidance.js +3 -1
- package/node_modules/@oxygen/shared/dist/crm-reply-events.d.ts +35 -0
- package/node_modules/@oxygen/shared/dist/crm-reply-events.js +31 -0
- package/node_modules/@oxygen/shared/dist/dial-guardrail-overrides.d.ts +50 -0
- package/node_modules/@oxygen/shared/dist/dial-guardrail-overrides.js +65 -0
- package/node_modules/@oxygen/shared/dist/directory.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/directory.js +1 -0
- package/node_modules/@oxygen/shared/dist/file-import.js +58 -11
- package/node_modules/@oxygen/shared/dist/hosted-ai.d.ts +15 -0
- package/node_modules/@oxygen/shared/dist/hosted-ai.js +19 -0
- package/node_modules/@oxygen/shared/dist/index.d.ts +9 -0
- package/node_modules/@oxygen/shared/dist/index.js +9 -0
- package/node_modules/@oxygen/shared/dist/linkedin-quota-denial.d.ts +31 -0
- package/node_modules/@oxygen/shared/dist/linkedin-quota-denial.js +56 -0
- package/node_modules/@oxygen/shared/dist/linkedin-sequences.d.ts +5 -4
- package/node_modules/@oxygen/shared/dist/linkedin-sequences.js +5 -4
- package/node_modules/@oxygen/shared/dist/linkedin-url.d.ts +22 -0
- package/node_modules/@oxygen/shared/dist/linkedin-url.js +7 -4
- package/node_modules/@oxygen/shared/dist/log.js +41 -2
- package/node_modules/@oxygen/shared/dist/microsoft-consent-url.d.ts +7 -0
- package/node_modules/@oxygen/shared/dist/microsoft-consent-url.js +29 -0
- package/node_modules/@oxygen/shared/dist/object-storage.d.ts +31 -0
- package/node_modules/@oxygen/shared/dist/object-storage.js +61 -0
- package/node_modules/@oxygen/shared/dist/plan-limits.d.ts +636 -0
- package/node_modules/@oxygen/shared/dist/plan-limits.js +199 -0
- package/node_modules/@oxygen/shared/dist/pricing-sheet.d.ts +89 -23
- package/node_modules/@oxygen/shared/dist/pricing-sheet.js +88 -24
- package/node_modules/@oxygen/shared/dist/sequence-crm-events.d.ts +291 -0
- package/node_modules/@oxygen/shared/dist/sequence-crm-events.js +224 -0
- package/node_modules/@oxygen/shared/dist/sequence-template.d.ts +42 -1
- package/node_modules/@oxygen/shared/dist/sequence-template.js +0 -0
- package/node_modules/@oxygen/shared/dist/sequences.d.ts +287 -24
- package/node_modules/@oxygen/shared/dist/sequences.js +940 -60
- package/node_modules/@oxygen/shared/dist/spend-safety.d.ts +70 -0
- package/node_modules/@oxygen/shared/dist/spend-safety.js +106 -0
- package/node_modules/@oxygen/shared/dist/tags.d.ts +90 -1
- package/node_modules/@oxygen/shared/dist/tags.js +122 -6
- 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 +1 -1
- package/node_modules/@oxygen/shared/dist/workflow-trigger-metadata.js +4 -0
- package/node_modules/@oxygen/shared/package.json +95 -0
- package/node_modules/@oxygen/workflows/dist/event-dispatch.d.ts +126 -0
- package/node_modules/@oxygen/workflows/dist/event-dispatch.js +173 -0
- package/node_modules/@oxygen/workflows/dist/graph/expression.d.ts +78 -0
- package/node_modules/@oxygen/workflows/dist/graph/expression.js +700 -0
- package/node_modules/@oxygen/workflows/dist/graph/index.d.ts +20 -0
- package/node_modules/@oxygen/workflows/dist/graph/index.js +20 -0
- package/node_modules/@oxygen/workflows/dist/graph/lint.d.ts +4 -0
- package/node_modules/@oxygen/workflows/dist/graph/lint.js +812 -0
- package/node_modules/@oxygen/workflows/dist/graph/manifest-schema.d.ts +501 -0
- package/node_modules/@oxygen/workflows/dist/graph/manifest-schema.js +200 -0
- package/node_modules/@oxygen/workflows/dist/graph/params.d.ts +86 -0
- package/node_modules/@oxygen/workflows/dist/graph/params.js +173 -0
- package/node_modules/@oxygen/workflows/dist/graph/remap.d.ts +48 -0
- package/node_modules/@oxygen/workflows/dist/graph/remap.js +213 -0
- package/node_modules/@oxygen/workflows/dist/graph/topology.d.ts +46 -0
- package/node_modules/@oxygen/workflows/dist/graph/topology.js +280 -0
- package/node_modules/@oxygen/workflows/dist/graph/types.d.ts +270 -0
- package/node_modules/@oxygen/workflows/dist/graph/types.js +93 -0
- package/node_modules/@oxygen/workflows/dist/index.d.ts +113 -1
- package/node_modules/@oxygen/workflows/dist/index.js +179 -13
- package/node_modules/@oxygen/workflows/dist/tool-effects.d.ts +1 -0
- package/node_modules/@oxygen/workflows/dist/tool-effects.js +19 -0
- package/node_modules/@oxygen/workflows/dist/usage-estimate.js +135 -4
- package/node_modules/@oxygen/workflows/package.json +4 -0
- package/package.json +7 -5
|
@@ -66,8 +66,9 @@ export function parseRowsText(text, format) {
|
|
|
66
66
|
if (format === "jsonl") {
|
|
67
67
|
return normalizeRowObjects(text
|
|
68
68
|
.split(/\r?\n/)
|
|
69
|
-
.
|
|
70
|
-
.
|
|
69
|
+
.map((line, index) => ({ line, lineNumber: index + 1 }))
|
|
70
|
+
.filter(({ line }) => line.trim())
|
|
71
|
+
.map(({ line, lineNumber }) => parseJsonImportLine(line, lineNumber)));
|
|
71
72
|
}
|
|
72
73
|
return parseCsvRows(text);
|
|
73
74
|
}
|
|
@@ -162,12 +163,26 @@ function normalizeXlsxImportCell(value) {
|
|
|
162
163
|
return String(value);
|
|
163
164
|
}
|
|
164
165
|
function parseJsonArray(text) {
|
|
165
|
-
|
|
166
|
+
let parsed;
|
|
167
|
+
try {
|
|
168
|
+
parsed = JSON.parse(text);
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
throw new OxygenError("invalid_json", "Import file must contain valid JSON. Input contents are omitted from this error.", { exitCode: 1 });
|
|
172
|
+
}
|
|
166
173
|
if (!Array.isArray(parsed)) {
|
|
167
174
|
throw new OxygenError("invalid_json", "Expected a JSON array.", { exitCode: 1 });
|
|
168
175
|
}
|
|
169
176
|
return parsed;
|
|
170
177
|
}
|
|
178
|
+
function parseJsonImportLine(line, lineNumber) {
|
|
179
|
+
try {
|
|
180
|
+
return JSON.parse(line);
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
throw new OxygenError("invalid_jsonl", `Import file contains invalid JSON on line ${lineNumber}. Input contents are omitted from this error.`, { exitCode: 1 });
|
|
184
|
+
}
|
|
185
|
+
}
|
|
171
186
|
function normalizeRowObjects(rows) {
|
|
172
187
|
return rows.map((row) => normalizeRowObject(row));
|
|
173
188
|
}
|
|
@@ -182,6 +197,7 @@ function parseCsvRows(text) {
|
|
|
182
197
|
const [header, ...rows] = records;
|
|
183
198
|
if (!header || header.length === 0)
|
|
184
199
|
return [];
|
|
200
|
+
assertUniqueCsvHeaders(header);
|
|
185
201
|
return rows
|
|
186
202
|
.filter((row) => row.some((cell) => cell.trim()))
|
|
187
203
|
.map((row) => Object.fromEntries(header.map((key, index) => [key, normalizeCsvImportCell(row[index])])));
|
|
@@ -209,6 +225,8 @@ function* iterateCsvRowBatches(text, batchSize) {
|
|
|
209
225
|
if (ready)
|
|
210
226
|
yield ready;
|
|
211
227
|
}
|
|
228
|
+
if (state.inQuotes)
|
|
229
|
+
throw unterminatedCsvError();
|
|
212
230
|
if (state.field || state.record.length > 0) {
|
|
213
231
|
const ready = appendCsvRecordToBatch(state, finishCsvRecord(state), batchSize);
|
|
214
232
|
if (ready)
|
|
@@ -249,6 +267,8 @@ async function* iterateCsvRowStreamBatches(chunks, batchSize) {
|
|
|
249
267
|
if (ready.batch)
|
|
250
268
|
yield ready.batch;
|
|
251
269
|
}
|
|
270
|
+
if (state.inQuotes)
|
|
271
|
+
throw unterminatedCsvError();
|
|
252
272
|
if (state.field || state.record.length > 0) {
|
|
253
273
|
const ready = appendCsvRecordToBatch(state, finishCsvRecord(state), batchSize);
|
|
254
274
|
if (ready)
|
|
@@ -313,8 +333,10 @@ function finishCsvRecord(state) {
|
|
|
313
333
|
}
|
|
314
334
|
function appendCsvRecordToBatch(state, record, batchSize) {
|
|
315
335
|
const row = csvRecordToRow(record, state.header);
|
|
316
|
-
if (state.header === null)
|
|
336
|
+
if (state.header === null) {
|
|
337
|
+
assertUniqueCsvHeaders(record);
|
|
317
338
|
state.header = record;
|
|
339
|
+
}
|
|
318
340
|
if (row)
|
|
319
341
|
state.batch.push(row);
|
|
320
342
|
if (state.batch.length < batchSize)
|
|
@@ -333,22 +355,27 @@ function csvRecordToRow(record, header) {
|
|
|
333
355
|
function* iterateJsonlRowBatches(text, batchSize) {
|
|
334
356
|
let batch = [];
|
|
335
357
|
let lineStart = 0;
|
|
358
|
+
let lineNumber = 1;
|
|
336
359
|
for (let index = 0; index < text.length; index += 1) {
|
|
337
360
|
if (text.charAt(index) !== "\n")
|
|
338
361
|
continue;
|
|
339
362
|
const line = text.slice(lineStart, index).replace(/\r$/, "").trim();
|
|
340
363
|
lineStart = index + 1;
|
|
364
|
+
if (line) {
|
|
365
|
+
batch.push(normalizeRowObject(parseJsonImportLine(line, lineNumber)));
|
|
366
|
+
}
|
|
367
|
+
lineNumber += 1;
|
|
341
368
|
if (!line)
|
|
342
369
|
continue;
|
|
343
|
-
batch.push(normalizeRowObject(JSON.parse(line)));
|
|
344
370
|
if (batch.length >= batchSize) {
|
|
345
371
|
yield batch;
|
|
346
372
|
batch = [];
|
|
347
373
|
}
|
|
348
374
|
}
|
|
349
375
|
const trailingLine = text.slice(lineStart).replace(/\r$/, "").trim();
|
|
350
|
-
if (trailingLine)
|
|
351
|
-
batch.push(normalizeRowObject(
|
|
376
|
+
if (trailingLine) {
|
|
377
|
+
batch.push(normalizeRowObject(parseJsonImportLine(trailingLine, lineNumber)));
|
|
378
|
+
}
|
|
352
379
|
if (batch.length > 0)
|
|
353
380
|
yield batch;
|
|
354
381
|
}
|
|
@@ -356,14 +383,17 @@ async function* iterateJsonlRowStreamBatches(chunks, batchSize) {
|
|
|
356
383
|
const decoder = new TextDecoder();
|
|
357
384
|
let pending = "";
|
|
358
385
|
let batch = [];
|
|
386
|
+
let lineNumber = 1;
|
|
359
387
|
for await (const chunk of chunks) {
|
|
360
388
|
pending += decoder.decode(toUint8Array(chunk), { stream: true });
|
|
361
389
|
let newline = pending.indexOf("\n");
|
|
362
390
|
while (newline >= 0) {
|
|
363
391
|
const line = pending.slice(0, newline).replace(/\r$/, "").trim();
|
|
364
392
|
pending = pending.slice(newline + 1);
|
|
365
|
-
if (line)
|
|
366
|
-
batch.push(normalizeRowObject(
|
|
393
|
+
if (line) {
|
|
394
|
+
batch.push(normalizeRowObject(parseJsonImportLine(line, lineNumber)));
|
|
395
|
+
}
|
|
396
|
+
lineNumber += 1;
|
|
367
397
|
if (batch.length >= batchSize) {
|
|
368
398
|
yield batch;
|
|
369
399
|
batch = [];
|
|
@@ -372,8 +402,9 @@ async function* iterateJsonlRowStreamBatches(chunks, batchSize) {
|
|
|
372
402
|
}
|
|
373
403
|
}
|
|
374
404
|
const tail = (pending + decoder.decode()).replace(/\r$/, "").trim();
|
|
375
|
-
if (tail)
|
|
376
|
-
batch.push(normalizeRowObject(
|
|
405
|
+
if (tail) {
|
|
406
|
+
batch.push(normalizeRowObject(parseJsonImportLine(tail, lineNumber)));
|
|
407
|
+
}
|
|
377
408
|
if (batch.length > 0)
|
|
378
409
|
yield batch;
|
|
379
410
|
}
|
|
@@ -459,12 +490,28 @@ function parseCsvRecords(text) {
|
|
|
459
490
|
field += char;
|
|
460
491
|
}
|
|
461
492
|
}
|
|
493
|
+
if (inQuotes) {
|
|
494
|
+
throw unterminatedCsvError();
|
|
495
|
+
}
|
|
462
496
|
if (field || record.length > 0) {
|
|
463
497
|
record.push(field);
|
|
464
498
|
records.push(record);
|
|
465
499
|
}
|
|
466
500
|
return records;
|
|
467
501
|
}
|
|
502
|
+
function unterminatedCsvError() {
|
|
503
|
+
return new OxygenError("invalid_csv", "CSV contains an unterminated quoted field.", { exitCode: 1 });
|
|
504
|
+
}
|
|
505
|
+
function assertUniqueCsvHeaders(header) {
|
|
506
|
+
const seen = new Set();
|
|
507
|
+
for (const value of header) {
|
|
508
|
+
const normalized = value.normalize("NFKC").trim().toLowerCase();
|
|
509
|
+
if (!normalized || seen.has(normalized)) {
|
|
510
|
+
throw new OxygenError("invalid_csv", "CSV headers must be non-empty and unique (case-insensitive).", { exitCode: 1 });
|
|
511
|
+
}
|
|
512
|
+
seen.add(normalized);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
468
515
|
function makeUniqueImportKey(label, taken) {
|
|
469
516
|
return makeUniqueIdentifier(label, taken, {
|
|
470
517
|
exhaustedMessage: "Unable to create a unique column key.",
|
|
@@ -45,6 +45,21 @@ export type HostedAiModelSpec = {
|
|
|
45
45
|
maxOutputTokens: number;
|
|
46
46
|
};
|
|
47
47
|
export declare const HOSTED_AI_MODEL_REGISTRY: Record<HostedAiUseCase, Record<HostedAiLevel, HostedAiModelSpec>>;
|
|
48
|
+
/**
|
|
49
|
+
* What a MANAGED AI column's reasoning tier is called in front of a customer.
|
|
50
|
+
*
|
|
51
|
+
* A managed customer buys an Oxygen tier at a fixed credit price, not a specific
|
|
52
|
+
* third-party model — which is why the web editor shows these names and never the
|
|
53
|
+
* vendor model id. The names live here, beside the registry that decides which
|
|
54
|
+
* model each tier resolves to, so every surface says the same word: a blind user
|
|
55
|
+
* eval found the web UI offering "Oxygen Balanced" while the CLI's dry-run
|
|
56
|
+
* reported only `deepseek/deepseek-v4-pro`, leaving a customer to guess whether
|
|
57
|
+
* they were the same thing.
|
|
58
|
+
*
|
|
59
|
+
* BYOK is deliberately excluded: there the user picks real OpenRouter models on
|
|
60
|
+
* their own key, so the real model id is the correct thing to show.
|
|
61
|
+
*/
|
|
62
|
+
export declare const MANAGED_AI_TIER_LABELS: Record<HostedAiLevel, string>;
|
|
48
63
|
/** The reasoning tier the hosted copilot runs at by default. */
|
|
49
64
|
export declare const COPILOT_DEFAULT_LEVEL: HostedAiLevel;
|
|
50
65
|
export declare const AGENT_DEFAULT_LEVEL: HostedAiLevel;
|
|
@@ -106,6 +106,25 @@ export const HOSTED_AI_MODEL_REGISTRY = {
|
|
|
106
106
|
},
|
|
107
107
|
},
|
|
108
108
|
};
|
|
109
|
+
/**
|
|
110
|
+
* What a MANAGED AI column's reasoning tier is called in front of a customer.
|
|
111
|
+
*
|
|
112
|
+
* A managed customer buys an Oxygen tier at a fixed credit price, not a specific
|
|
113
|
+
* third-party model — which is why the web editor shows these names and never the
|
|
114
|
+
* vendor model id. The names live here, beside the registry that decides which
|
|
115
|
+
* model each tier resolves to, so every surface says the same word: a blind user
|
|
116
|
+
* eval found the web UI offering "Oxygen Balanced" while the CLI's dry-run
|
|
117
|
+
* reported only `deepseek/deepseek-v4-pro`, leaving a customer to guess whether
|
|
118
|
+
* they were the same thing.
|
|
119
|
+
*
|
|
120
|
+
* BYOK is deliberately excluded: there the user picks real OpenRouter models on
|
|
121
|
+
* their own key, so the real model id is the correct thing to show.
|
|
122
|
+
*/
|
|
123
|
+
export const MANAGED_AI_TIER_LABELS = {
|
|
124
|
+
low: "Oxygen Fast",
|
|
125
|
+
medium: "Oxygen Balanced",
|
|
126
|
+
high: "Oxygen Max",
|
|
127
|
+
};
|
|
109
128
|
/** The reasoning tier the hosted copilot runs at by default. */
|
|
110
129
|
export const COPILOT_DEFAULT_LEVEL = "high";
|
|
111
130
|
export const AGENT_DEFAULT_LEVEL = "medium";
|
|
@@ -2,11 +2,15 @@ export { MANAGED_INBOX_MINIMUM_CLI_VERSION, OXYGEN_MINIMUM_CLI_VERSION, OXYGEN_V
|
|
|
2
2
|
export { WORKFLOW_TRIGGER_AUTO_PAUSE_METADATA_KEYS, clearWorkflowTriggerAutoPauseMetadata, } from "./workflow-trigger-metadata.js";
|
|
3
3
|
export { WORKFLOW_STATUS_CHANGE_METADATA_KEY, type WorkflowStatusChange, type WorkflowStatusChangeActor, type WorkflowStatusChangeSource, describeWorkflowStatusChange, formatWorkflowStatusChangeTimestamp, parseWorkflowStatusChange, readWorkflowStatusChange, } from "./workflow-status-change.js";
|
|
4
4
|
export * from "./billing.js";
|
|
5
|
+
export * from "./billing-anchors.js";
|
|
5
6
|
export * from "./budget-scopes.js";
|
|
7
|
+
export * from "./plan-limits.js";
|
|
8
|
+
export * from "./spend-safety.js";
|
|
6
9
|
export * from "./cell-format.js";
|
|
7
10
|
export * from "./cli-envelope.js";
|
|
8
11
|
export * from "./cli-login-code.js";
|
|
9
12
|
export * from "./cli-result.js";
|
|
13
|
+
export * from "./crm-reply-events.js";
|
|
10
14
|
export * from "./column-types.js";
|
|
11
15
|
export * from "./copilot-journeys.js";
|
|
12
16
|
export * from "./credit-guidance.js";
|
|
@@ -23,11 +27,16 @@ export * from "./knowledge-seed-content.js";
|
|
|
23
27
|
export * from "./langfuse.js";
|
|
24
28
|
export * from "./linkedin-mentions.js";
|
|
25
29
|
export * from "./linkedin-post-url.js";
|
|
30
|
+
export * from "./linkedin-quota-denial.js";
|
|
26
31
|
export * from "./linkedin-url.js";
|
|
27
32
|
export * from "./linkedin-sequences.js";
|
|
33
|
+
export * from "./microsoft-consent-url.js";
|
|
28
34
|
export * from "./networks.js";
|
|
29
35
|
export * from "./recipes.js";
|
|
30
36
|
export * from "./sequence-template.js";
|
|
37
|
+
export * from "./sequence-crm-events.js";
|
|
38
|
+
export * from "./call-outcomes.js";
|
|
39
|
+
export * from "./dial-guardrail-overrides.js";
|
|
31
40
|
export * from "./sequences.js";
|
|
32
41
|
export * from "./suppression-entries.js";
|
|
33
42
|
export * from "./log.js";
|
|
@@ -2,11 +2,15 @@ export { MANAGED_INBOX_MINIMUM_CLI_VERSION, OXYGEN_MINIMUM_CLI_VERSION, OXYGEN_V
|
|
|
2
2
|
export { WORKFLOW_TRIGGER_AUTO_PAUSE_METADATA_KEYS, clearWorkflowTriggerAutoPauseMetadata, } from "./workflow-trigger-metadata.js";
|
|
3
3
|
export { WORKFLOW_STATUS_CHANGE_METADATA_KEY, describeWorkflowStatusChange, formatWorkflowStatusChangeTimestamp, parseWorkflowStatusChange, readWorkflowStatusChange, } from "./workflow-status-change.js";
|
|
4
4
|
export * from "./billing.js";
|
|
5
|
+
export * from "./billing-anchors.js";
|
|
5
6
|
export * from "./budget-scopes.js";
|
|
7
|
+
export * from "./plan-limits.js";
|
|
8
|
+
export * from "./spend-safety.js";
|
|
6
9
|
export * from "./cell-format.js";
|
|
7
10
|
export * from "./cli-envelope.js";
|
|
8
11
|
export * from "./cli-login-code.js";
|
|
9
12
|
export * from "./cli-result.js";
|
|
13
|
+
export * from "./crm-reply-events.js";
|
|
10
14
|
export * from "./column-types.js";
|
|
11
15
|
export * from "./copilot-journeys.js";
|
|
12
16
|
export * from "./credit-guidance.js";
|
|
@@ -23,11 +27,16 @@ export * from "./knowledge-seed-content.js";
|
|
|
23
27
|
export * from "./langfuse.js";
|
|
24
28
|
export * from "./linkedin-mentions.js";
|
|
25
29
|
export * from "./linkedin-post-url.js";
|
|
30
|
+
export * from "./linkedin-quota-denial.js";
|
|
26
31
|
export * from "./linkedin-url.js";
|
|
27
32
|
export * from "./linkedin-sequences.js";
|
|
33
|
+
export * from "./microsoft-consent-url.js";
|
|
28
34
|
export * from "./networks.js";
|
|
29
35
|
export * from "./recipes.js";
|
|
30
36
|
export * from "./sequence-template.js";
|
|
37
|
+
export * from "./sequence-crm-events.js";
|
|
38
|
+
export * from "./call-outcomes.js";
|
|
39
|
+
export * from "./dial-guardrail-overrides.js";
|
|
31
40
|
export * from "./sequences.js";
|
|
32
41
|
export * from "./suppression-entries.js";
|
|
33
42
|
export * from "./log.js";
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How a LinkedIn quota denial is read by the background jobs that hit it.
|
|
3
|
+
*
|
|
4
|
+
* The denial itself is raised by the chokepoint in
|
|
5
|
+
* packages/integrations/src/linkedin-quota.ts; this module is the consumer half,
|
|
6
|
+
* and it lives here because the consumers are spread across the worker, the web
|
|
7
|
+
* app, and the integrations package — three copies of "is this a denial, and
|
|
8
|
+
* until when" is exactly how the inbox sync and the publishing metrics sync ended
|
|
9
|
+
* up with different behavior for the same condition.
|
|
10
|
+
*
|
|
11
|
+
* The pattern (established in v1.423.0 for `linkedin.inbox_sync_failed`, which
|
|
12
|
+
* emitted 158,882 warn lines in 31h for one account): a denial is logged at
|
|
13
|
+
* `info`, not `warn` — a rate budget doing its job is normal operation, and at
|
|
14
|
+
* background cadence it buries the real warnings — and the account is parked
|
|
15
|
+
* until its budget resets rather than re-denied on every tenant tick.
|
|
16
|
+
*/
|
|
17
|
+
import { OxygenError } from "./cli-result.js";
|
|
18
|
+
/** Was this thrown error the quota chokepoint refusing the call? */
|
|
19
|
+
export declare function isLinkedInQuotaDenial(error: unknown): error is OxygenError;
|
|
20
|
+
/**
|
|
21
|
+
* When a quota-denied caller may try again — the denial's own `resets_at` when it
|
|
22
|
+
* carries a usable one, else a flat hour. Null for anything that is NOT a quota
|
|
23
|
+
* denial: an ordinary failure (provider timeout, malformed payload) has no reset
|
|
24
|
+
* instant and must keep retrying on its caller's normal cadence.
|
|
25
|
+
*/
|
|
26
|
+
export declare function linkedInQuotaBackoffUntil(error: unknown, now: Date): Date | null;
|
|
27
|
+
/**
|
|
28
|
+
* Epoch-ms a sender's park — written under `key` in the freeform
|
|
29
|
+
* `sender_accounts.metadata` jsonb — expires; 0 when it is not parked.
|
|
30
|
+
*/
|
|
31
|
+
export declare function linkedInQuotaParkedUntil(metadata: unknown, key: string): number;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How a LinkedIn quota denial is read by the background jobs that hit it.
|
|
3
|
+
*
|
|
4
|
+
* The denial itself is raised by the chokepoint in
|
|
5
|
+
* packages/integrations/src/linkedin-quota.ts; this module is the consumer half,
|
|
6
|
+
* and it lives here because the consumers are spread across the worker, the web
|
|
7
|
+
* app, and the integrations package — three copies of "is this a denial, and
|
|
8
|
+
* until when" is exactly how the inbox sync and the publishing metrics sync ended
|
|
9
|
+
* up with different behavior for the same condition.
|
|
10
|
+
*
|
|
11
|
+
* The pattern (established in v1.423.0 for `linkedin.inbox_sync_failed`, which
|
|
12
|
+
* emitted 158,882 warn lines in 31h for one account): a denial is logged at
|
|
13
|
+
* `info`, not `warn` — a rate budget doing its job is normal operation, and at
|
|
14
|
+
* background cadence it buries the real warnings — and the account is parked
|
|
15
|
+
* until its budget resets rather than re-denied on every tenant tick.
|
|
16
|
+
*/
|
|
17
|
+
import { OxygenError } from "./cli-result.js";
|
|
18
|
+
import { isRecord } from "./type-guards.js";
|
|
19
|
+
// The two codes a denial is raised with. Either is a "come back later" signal,
|
|
20
|
+
// not a broken caller: a daily cap / closed active window that reopens on its own
|
|
21
|
+
// clock, or an account the status webhook will reactivate.
|
|
22
|
+
const QUOTA_DENIED_CODES = new Set(["linkedin_rate_limited", "linkedin_account_unavailable"]);
|
|
23
|
+
/** Park length when a denial carries no usable `resets_at` hint. */
|
|
24
|
+
const QUOTA_FALLBACK_BACKOFF_MS = 60 * 60 * 1000;
|
|
25
|
+
/** Was this thrown error the quota chokepoint refusing the call? */
|
|
26
|
+
export function isLinkedInQuotaDenial(error) {
|
|
27
|
+
return error instanceof OxygenError && QUOTA_DENIED_CODES.has(error.code);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* When a quota-denied caller may try again — the denial's own `resets_at` when it
|
|
31
|
+
* carries a usable one, else a flat hour. Null for anything that is NOT a quota
|
|
32
|
+
* denial: an ordinary failure (provider timeout, malformed payload) has no reset
|
|
33
|
+
* instant and must keep retrying on its caller's normal cadence.
|
|
34
|
+
*/
|
|
35
|
+
export function linkedInQuotaBackoffUntil(error, now) {
|
|
36
|
+
if (!isLinkedInQuotaDenial(error))
|
|
37
|
+
return null;
|
|
38
|
+
const raw = isRecord(error.details) ? error.details.resets_at : undefined;
|
|
39
|
+
if (typeof raw === "string") {
|
|
40
|
+
const ms = Date.parse(raw);
|
|
41
|
+
if (!Number.isNaN(ms) && ms > now.getTime())
|
|
42
|
+
return new Date(ms);
|
|
43
|
+
}
|
|
44
|
+
return new Date(now.getTime() + QUOTA_FALLBACK_BACKOFF_MS);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Epoch-ms a sender's park — written under `key` in the freeform
|
|
48
|
+
* `sender_accounts.metadata` jsonb — expires; 0 when it is not parked.
|
|
49
|
+
*/
|
|
50
|
+
export function linkedInQuotaParkedUntil(metadata, key) {
|
|
51
|
+
const raw = isRecord(metadata) ? metadata[key] : undefined;
|
|
52
|
+
if (typeof raw !== "string")
|
|
53
|
+
return 0;
|
|
54
|
+
const ms = Date.parse(raw);
|
|
55
|
+
return Number.isNaN(ms) ? 0 : ms;
|
|
56
|
+
}
|
|
@@ -144,10 +144,11 @@ export declare function waitStepDelayMs(step: LinkedInWaitStep): number;
|
|
|
144
144
|
/**
|
|
145
145
|
* Render a sequence-copy template against a row's values. Delegates to the shared
|
|
146
146
|
* deterministic engine, so beyond `{{column}}` substitution it also handles
|
|
147
|
-
* `{{column|fallback}}`, `{{RANDOM|…}}`
|
|
148
|
-
* Unknown `{{column}}` placeholders render empty. Used
|
|
149
|
-
* produce the final message text. Pass `{ seed }` to make
|
|
150
|
-
* replayable (crash-safe); with no seed a stable template+values
|
|
147
|
+
* `{{column|fallback}}`, `{{RANDOM|…}}` and bare `{a|b|c}` spintax (nestable), and
|
|
148
|
+
* `{% if … %}` conditionals. Unknown `{{column}}` placeholders render empty. Used
|
|
149
|
+
* by the dispatch engine to produce the final message text. Pass `{ seed }` to make
|
|
150
|
+
* spintax choices replayable (crash-safe); with no seed a stable template+values
|
|
151
|
+
* seed is derived.
|
|
151
152
|
*/
|
|
152
153
|
export declare function renderLinkedInTemplate(template: string, values: Record<string, unknown>, options?: RenderTemplateOptions): string;
|
|
153
154
|
export {};
|
|
@@ -64,10 +64,11 @@ export function waitStepDelayMs(step) {
|
|
|
64
64
|
/**
|
|
65
65
|
* Render a sequence-copy template against a row's values. Delegates to the shared
|
|
66
66
|
* deterministic engine, so beyond `{{column}}` substitution it also handles
|
|
67
|
-
* `{{column|fallback}}`, `{{RANDOM|…}}`
|
|
68
|
-
* Unknown `{{column}}` placeholders render empty. Used
|
|
69
|
-
* produce the final message text. Pass `{ seed }` to make
|
|
70
|
-
* replayable (crash-safe); with no seed a stable template+values
|
|
67
|
+
* `{{column|fallback}}`, `{{RANDOM|…}}` and bare `{a|b|c}` spintax (nestable), and
|
|
68
|
+
* `{% if … %}` conditionals. Unknown `{{column}}` placeholders render empty. Used
|
|
69
|
+
* by the dispatch engine to produce the final message text. Pass `{ seed }` to make
|
|
70
|
+
* spintax choices replayable (crash-safe); with no seed a stable template+values
|
|
71
|
+
* seed is derived.
|
|
71
72
|
*/
|
|
72
73
|
export function renderLinkedInTemplate(template, values, options) {
|
|
73
74
|
return renderTemplate(template, values, options);
|
|
@@ -1,6 +1,28 @@
|
|
|
1
1
|
export type LinkedinUrlNormalization = {
|
|
2
|
+
/**
|
|
3
|
+
* Lowercase canonical URL — the stable MATCH key (CRM `linkedin_url_v1`
|
|
4
|
+
* identities, suppression entries). Lowercasing is what makes two spellings of
|
|
5
|
+
* the same profile compare equal, so this must stay case-folded forever: a
|
|
6
|
+
* suppression key that changed case would stop matching every row already
|
|
7
|
+
* stored under the old spelling, and someone who opted out would be contacted.
|
|
8
|
+
*/
|
|
2
9
|
normalized: string;
|
|
10
|
+
/** Lowercase handle — the stable match key. Same contract as `normalized`. */
|
|
3
11
|
handle: string;
|
|
12
|
+
/**
|
|
13
|
+
* The handle with its ORIGINAL case, for anything that hands the identifier
|
|
14
|
+
* back to a provider or stores it on a record.
|
|
15
|
+
*
|
|
16
|
+
* A public slug is lowercase by construction, so this equals `handle` for it.
|
|
17
|
+
* An `ACoAA…` provider id is base64url of a member URN — its case is
|
|
18
|
+
* significant, and folding it is irreversible data loss: the id then resolves
|
|
19
|
+
* at neither Unipile nor HarvestAPI (which answers HTTP 400 "You need to
|
|
20
|
+
* provide at least one valid argument"). Reach for this whenever the value
|
|
21
|
+
* leaves Oxygen; reach for `handle`/`normalized` whenever it is compared.
|
|
22
|
+
*/
|
|
23
|
+
dispatchHandle: string;
|
|
24
|
+
/** Canonical URL built from `dispatchHandle` — what we store on a record. */
|
|
25
|
+
dispatchUrl: string;
|
|
4
26
|
} | null;
|
|
5
27
|
export declare function normalizeLinkedinProfileUrl(raw: string | null | undefined): LinkedinUrlNormalization;
|
|
6
28
|
/**
|
|
@@ -48,18 +48,21 @@ export function normalizeLinkedinProfileUrl(raw) {
|
|
|
48
48
|
const rawHandle = parts[markerIndex + 1];
|
|
49
49
|
if (!rawHandle)
|
|
50
50
|
return null;
|
|
51
|
-
let
|
|
51
|
+
let decoded;
|
|
52
52
|
try {
|
|
53
|
-
|
|
53
|
+
decoded = decodeURIComponent(rawHandle).trim();
|
|
54
54
|
}
|
|
55
55
|
catch {
|
|
56
|
-
|
|
56
|
+
decoded = rawHandle.trim();
|
|
57
57
|
}
|
|
58
|
-
if (!
|
|
58
|
+
if (!decoded)
|
|
59
59
|
return null;
|
|
60
|
+
const handle = decoded.toLowerCase();
|
|
60
61
|
return {
|
|
61
62
|
normalized: `https://www.linkedin.com/in/${handle}`,
|
|
62
63
|
handle,
|
|
64
|
+
dispatchHandle: decoded,
|
|
65
|
+
dispatchUrl: `https://www.linkedin.com/in/${decoded}`,
|
|
63
66
|
};
|
|
64
67
|
}
|
|
65
68
|
// A LinkedIn post URL carries the numeric activity id we address the post by:
|
|
@@ -73,6 +73,35 @@ export function setLogSink(sink) {
|
|
|
73
73
|
function getLogSink() {
|
|
74
74
|
return globalThis[LOG_SINK_SLOT] ?? null;
|
|
75
75
|
}
|
|
76
|
+
const LEVEL_RANK = { debug: 10, info: 20, warn: 30, error: 40 };
|
|
77
|
+
/**
|
|
78
|
+
* Minimum level that reaches the Axiom SINK. stdout is deliberately untouched —
|
|
79
|
+
* `flyctl logs`, `vercel logs`, and local runs keep every line, so nothing becomes
|
|
80
|
+
* un-debuggable; this only decides what we pay to store and query.
|
|
81
|
+
*
|
|
82
|
+
* Default `info`, i.e. debug never ships. Until 2026-07-31 `log()` had no level gate
|
|
83
|
+
* at all, so debug lines were shipped exactly like info — which contradicted the code
|
|
84
|
+
* that chose debug specifically to stay quiet (see the `work_queue.wake.failed` call
|
|
85
|
+
* in packages/work-queue/src/wake-client.ts: "Log at debug level so this doesn't
|
|
86
|
+
* generate noise during deploys" — it generated 10k rows/week regardless). Measured
|
|
87
|
+
* the day this landed: debug was 97,704 rows/day of `oxygen-logs`, 24% of all
|
|
88
|
+
* in-process rows, and 98% of that was a single `crm_person_photo_cycle_skipped`
|
|
89
|
+
* line reporting that nothing happened.
|
|
90
|
+
*
|
|
91
|
+
* Set `OXYGEN_LOG_SHIP_MIN_LEVEL=debug` to restore the old behaviour (e.g. while
|
|
92
|
+
* debugging a specific incident), or raise it to `warn` to ship only actionable lines.
|
|
93
|
+
*/
|
|
94
|
+
function shipMinRank() {
|
|
95
|
+
const configured = process.env.OXYGEN_LOG_SHIP_MIN_LEVEL?.trim().toLowerCase();
|
|
96
|
+
if (configured
|
|
97
|
+
&& Object.prototype.hasOwnProperty.call(LEVEL_RANK, configured)) {
|
|
98
|
+
return LEVEL_RANK[configured];
|
|
99
|
+
}
|
|
100
|
+
return LEVEL_RANK.info;
|
|
101
|
+
}
|
|
102
|
+
function shouldShip(level) {
|
|
103
|
+
return LEVEL_RANK[level] >= shipMinRank();
|
|
104
|
+
}
|
|
76
105
|
export function log(level, msg, fields) {
|
|
77
106
|
const context = getLogContext();
|
|
78
107
|
const record = {
|
|
@@ -101,6 +130,8 @@ export function log(level, msg, fields) {
|
|
|
101
130
|
else {
|
|
102
131
|
console.log(line);
|
|
103
132
|
}
|
|
133
|
+
if (!shouldShip(level))
|
|
134
|
+
return;
|
|
104
135
|
const logSink = getLogSink();
|
|
105
136
|
if (!logSink)
|
|
106
137
|
return;
|
|
@@ -131,8 +162,16 @@ function hashErrorKey(input) {
|
|
|
131
162
|
export function errorId(err) {
|
|
132
163
|
if (!(err instanceof Error))
|
|
133
164
|
return "non_error";
|
|
134
|
-
|
|
135
|
-
|
|
165
|
+
// A drizzle DrizzleQueryError carries the bound parameter values inside its
|
|
166
|
+
// own message (`Failed query: <sql>\nparams: <values>`), which also makes
|
|
167
|
+
// stack line 1 the `params:` continuation instead of a frame. Hashing either
|
|
168
|
+
// raw gave every repetition of one DB failure its own id — 101 identical
|
|
169
|
+
// oxygen_lead_db_cache.write_failed events grouped as 101 buckets of one, and
|
|
170
|
+
// the hash input was derived from customer row values (OXY-46). Redact the
|
|
171
|
+
// params tail and hash the first real frame so DB errors group again.
|
|
172
|
+
const message = redactSqlParameters(err.message);
|
|
173
|
+
const firstFrame = (err.stack ?? "").split("\n").find((line) => /^\s+at\s/.test(line)) ?? "";
|
|
174
|
+
return hashErrorKey(`${err.name}:${message}:${firstFrame}`);
|
|
136
175
|
}
|
|
137
176
|
export function errorFields(err) {
|
|
138
177
|
if (err instanceof Error) {
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalize a Microsoft Entra admin-consent URL for an operator handoff.
|
|
3
|
+
*
|
|
4
|
+
* Provider responses are external input. Only Microsoft's documented HTTPS
|
|
5
|
+
* login host and either provider-supported admin-consent path may become clickable.
|
|
6
|
+
*/
|
|
7
|
+
export declare function normalizeMicrosoftAdminConsentUrl(value: unknown): string | null;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
const MICROSOFT_LOGIN_HOST = "login.microsoftonline.com";
|
|
2
|
+
const ADMIN_CONSENT_PATH = /^\/(?:[^/]+\/)?adminconsent\/?$/i;
|
|
3
|
+
/**
|
|
4
|
+
* Normalize a Microsoft Entra admin-consent URL for an operator handoff.
|
|
5
|
+
*
|
|
6
|
+
* Provider responses are external input. Only Microsoft's documented HTTPS
|
|
7
|
+
* login host and either provider-supported admin-consent path may become clickable.
|
|
8
|
+
*/
|
|
9
|
+
export function normalizeMicrosoftAdminConsentUrl(value) {
|
|
10
|
+
if (typeof value !== "string" || value.trim() === "")
|
|
11
|
+
return null;
|
|
12
|
+
let url;
|
|
13
|
+
try {
|
|
14
|
+
url = new URL(value.trim());
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
if (url.protocol !== "https:" ||
|
|
20
|
+
url.hostname.toLowerCase() !== MICROSOFT_LOGIN_HOST ||
|
|
21
|
+
url.username !== "" ||
|
|
22
|
+
url.password !== "" ||
|
|
23
|
+
url.port !== "" ||
|
|
24
|
+
url.hash !== "" ||
|
|
25
|
+
!ADMIN_CONSENT_PATH.test(url.pathname)) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
return url.toString();
|
|
29
|
+
}
|
|
@@ -7,7 +7,12 @@ export declare function buildPublishingMediaObjectKey(input: {
|
|
|
7
7
|
organizationId: string;
|
|
8
8
|
fileName?: string | null;
|
|
9
9
|
}): string;
|
|
10
|
+
export declare function buildCopilotAttachmentObjectKey(input: {
|
|
11
|
+
organizationId: string;
|
|
12
|
+
fileName?: string | null;
|
|
13
|
+
}): string;
|
|
10
14
|
export declare function isImportObjectKeyForOrganization(key: string, organizationId: string): boolean;
|
|
15
|
+
export declare function isCopilotAttachmentObjectKeyForOrganization(key: string, organizationId: string): boolean;
|
|
11
16
|
export declare function isPublishingMediaObjectKeyForOrganization(key: string, organizationId: string): boolean;
|
|
12
17
|
export type PresignedImportUpload = {
|
|
13
18
|
uploadUrl: string;
|
|
@@ -30,6 +35,32 @@ export declare function presignPublishingMediaUpload(input: {
|
|
|
30
35
|
contentType?: string | null;
|
|
31
36
|
contentLength: number;
|
|
32
37
|
}): Promise<PresignedPublishingMediaUpload>;
|
|
38
|
+
export declare function presignCopilotAttachmentUpload(input: {
|
|
39
|
+
organizationId: string;
|
|
40
|
+
fileName?: string | null;
|
|
41
|
+
contentType?: string | null;
|
|
42
|
+
contentLength: number;
|
|
43
|
+
}): Promise<PresignedImportUpload>;
|
|
44
|
+
/**
|
|
45
|
+
* Read an uploaded attachment back into memory. Bounded by `maxBytes` because
|
|
46
|
+
* the callers are a CSV header sniff and a text extraction, neither of which
|
|
47
|
+
* needs the whole object — and a 400MB upload must not become a 400MB buffer in
|
|
48
|
+
* a Vercel handler.
|
|
49
|
+
*/
|
|
50
|
+
export declare function readCopilotAttachmentObject(input: {
|
|
51
|
+
storageKey: string;
|
|
52
|
+
maxBytes: number;
|
|
53
|
+
}): Promise<{
|
|
54
|
+
body: Uint8Array;
|
|
55
|
+
contentType: string | null;
|
|
56
|
+
truncated: boolean;
|
|
57
|
+
}>;
|
|
58
|
+
export declare function getCopilotAttachmentObjectMetadata(input: {
|
|
59
|
+
storageKey: string;
|
|
60
|
+
}): Promise<{
|
|
61
|
+
contentLength: number | null;
|
|
62
|
+
contentType: string | null;
|
|
63
|
+
}>;
|
|
33
64
|
export declare function getImportObjectMetadata(input: {
|
|
34
65
|
storageKey: string;
|
|
35
66
|
}): Promise<{
|