@oxygen-agent/cli 1.861.0 → 1.879.0
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 +10 -0
- package/dist/column-run-notices.js +22 -0
- package/dist/command-manifest.js +4 -0
- package/dist/index.js +187 -73
- package/dist/local-custom-http-column.js +12 -37
- package/node_modules/@oxygen/formula/dist/formula-functions.js +10 -24
- package/node_modules/@oxygen/shared/dist/cell-format.js +23 -2
- package/node_modules/@oxygen/shared/dist/column-output-fields.d.ts +122 -0
- package/node_modules/@oxygen/shared/dist/column-output-fields.js +459 -0
- package/node_modules/@oxygen/shared/dist/index.d.ts +1 -0
- package/node_modules/@oxygen/shared/dist/index.js +1 -0
- package/node_modules/@oxygen/shared/dist/json-path.d.ts +109 -0
- package/node_modules/@oxygen/shared/dist/json-path.js +177 -0
- package/node_modules/@oxygen/shared/dist/log-collapse.d.ts +6 -3
- package/node_modules/@oxygen/shared/dist/log-collapse.js +6 -3
- package/node_modules/@oxygen/shared/dist/pricing-snapshot.generated.d.ts +2 -0
- package/node_modules/@oxygen/shared/dist/pricing-snapshot.generated.js +13 -0
- package/node_modules/@oxygen/shared/dist/research-output-contract.d.ts +53 -0
- package/node_modules/@oxygen/shared/dist/research-output-contract.js +196 -0
- package/node_modules/@oxygen/shared/dist/sending-seats.d.ts +17 -1
- package/node_modules/@oxygen/shared/dist/sending-seats.js +17 -1
- package/node_modules/@oxygen/shared/dist/sequence-failures.d.ts +47 -0
- package/node_modules/@oxygen/shared/dist/sequence-failures.js +301 -0
- package/node_modules/@oxygen/shared/dist/telemetry.d.ts +1 -0
- package/node_modules/@oxygen/shared/dist/telemetry.js +119 -2
- 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/package.json +15 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -9,3 +9,13 @@
|
|
|
9
9
|
* live run, or a preview the server could not produce).
|
|
10
10
|
*/
|
|
11
11
|
export declare function formatAiPromptPreviewNotice(data: unknown): string | null;
|
|
12
|
+
/**
|
|
13
|
+
* Advisory reference findings from `columns add` / `update` / `rename`.
|
|
14
|
+
*
|
|
15
|
+
* An unresolvable reference is a hard rejection, so anything here is something
|
|
16
|
+
* the author should see but does not have to act on: an input the prompt never
|
|
17
|
+
* writes down (the value still reaches the model, but nothing links the two),
|
|
18
|
+
* a column named in prose and never read, or a rename that just orphaned a
|
|
19
|
+
* sibling's reference. Printed to stderr so `--json` piping stays clean.
|
|
20
|
+
*/
|
|
21
|
+
export declare function formatColumnReferenceNotices(data: unknown): string[];
|
|
@@ -35,3 +35,25 @@ export function formatAiPromptPreviewNotice(data) {
|
|
|
35
35
|
shown,
|
|
36
36
|
].join("\n");
|
|
37
37
|
}
|
|
38
|
+
/**
|
|
39
|
+
* Advisory reference findings from `columns add` / `update` / `rename`.
|
|
40
|
+
*
|
|
41
|
+
* An unresolvable reference is a hard rejection, so anything here is something
|
|
42
|
+
* the author should see but does not have to act on: an input the prompt never
|
|
43
|
+
* writes down (the value still reaches the model, but nothing links the two),
|
|
44
|
+
* a column named in prose and never read, or a rename that just orphaned a
|
|
45
|
+
* sibling's reference. Printed to stderr so `--json` piping stays clean.
|
|
46
|
+
*/
|
|
47
|
+
export function formatColumnReferenceNotices(data) {
|
|
48
|
+
if (!isRecord(data) || !Array.isArray(data.reference_warnings))
|
|
49
|
+
return [];
|
|
50
|
+
const notices = [];
|
|
51
|
+
for (const entry of data.reference_warnings) {
|
|
52
|
+
if (!isRecord(entry))
|
|
53
|
+
continue;
|
|
54
|
+
const message = readRecordString(entry, "message");
|
|
55
|
+
if (message)
|
|
56
|
+
notices.push(`warning: ${message}`);
|
|
57
|
+
}
|
|
58
|
+
return notices;
|
|
59
|
+
}
|
package/dist/command-manifest.js
CHANGED
|
@@ -32,6 +32,9 @@ const MUTATING_VERBS = new Set([
|
|
|
32
32
|
// default (mutates:false) would advertise the most consequential write in the
|
|
33
33
|
// activation play as a read.
|
|
34
34
|
"autoenroll",
|
|
35
|
+
// Exact hyphenated leaf: EmailGuard auto-enrolment arms or revokes a standing
|
|
36
|
+
// recurring write, and --run can connect newly eligible mailboxes immediately.
|
|
37
|
+
"auto-enroll",
|
|
35
38
|
"backfill",
|
|
36
39
|
// Exact leaves, not the `billing` prefix — `orgs billing-owners` is a read.
|
|
37
40
|
// Both move a workspace's billing owner, so an agent must treat them as writes.
|
|
@@ -84,6 +87,7 @@ const PREVIEW_BY_DEFAULT_COMMANDS = new Set([
|
|
|
84
87
|
// audience and caps and writes nothing. Without this entry discovery would tell
|
|
85
88
|
// an agent that previewing the play arms it, and the preview would go unrun.
|
|
86
89
|
"linkedin intent autoenroll",
|
|
90
|
+
"mailboxes emailguard auto-enroll",
|
|
87
91
|
"mailboxes delete",
|
|
88
92
|
"support admin done",
|
|
89
93
|
"support admin reply",
|
package/dist/index.js
CHANGED
|
@@ -21,7 +21,7 @@ import { ensureFreshCliForApiUrl, requestOxygen } from "./http-client.js";
|
|
|
21
21
|
import { acquireMirrorLock, clearConflictFiles, deletePageFile, emptyMirrorState, findMirrorSlugByPageId, isFileDirty, listConflictFiles, listLocalMirrors, localPageSha256, markMirrorStale, mirrorExists, pageFilePath, planMirrorPush, purgeMirror, resetMirrorForFullResync, quarantineDirtyFile, readMirrorState, releaseMirrorLock, resolveDefaultConfigDir, resolveMirrorDir, writeGeneratedIndexFile, writeGeneratedLogFile, writeMirrorState, writePageFile, } from "./knowledge-mirror.js";
|
|
22
22
|
import { waitForCliRun } from "./run-wait.js";
|
|
23
23
|
import { assertModeFlagsExclusive, parseKeyValuePairs, parseJsonObject, readJsonObjectOption, readPositiveInt, readRecordString, resolveLiveDryRunMode, } from "./cli-values.js";
|
|
24
|
-
import { formatAiPromptPreviewNotice } from "./column-run-notices.js";
|
|
24
|
+
import { formatAiPromptPreviewNotice, formatColumnReferenceNotices, } from "./column-run-notices.js";
|
|
25
25
|
import { runLocalCustomHttpColumn } from "./local-custom-http-column.js";
|
|
26
26
|
import { captureCurrentTranscript, collectFeedbackEnvironment, TranscriptCaptureError, } from "./transcript.js";
|
|
27
27
|
import { addSessionOutput, addSessionStatus, getSessionUsage, startSession, updateSessionStep, } from "./session.js";
|
|
@@ -536,6 +536,11 @@ function writeAiPromptPreviewNotice(data) {
|
|
|
536
536
|
if (notice)
|
|
537
537
|
process.stderr.write(`${notice}\n`);
|
|
538
538
|
}
|
|
539
|
+
function writeColumnReferenceNotices(data) {
|
|
540
|
+
for (const notice of formatColumnReferenceNotices(data)) {
|
|
541
|
+
process.stderr.write(`${notice}\n`);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
539
544
|
function writeDisabledWorkflowNotices(data) {
|
|
540
545
|
for (const notice of formatDisabledWorkflowNotices(data)) {
|
|
541
546
|
process.stderr.write(`${notice}\n`);
|
|
@@ -7116,7 +7121,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
7116
7121
|
.option("--kind <kind>", "Column kind: manual, research, ai, formula, enrichment, tool, bind, or lookup. Defaults to manual. Use research for anything you would look up on the web. `lookup` reads a value out of another table; to LINK two tables row-to-row use `oxygen tables relate` instead \u2014 relation columns are two-sided and cannot be added here.")
|
|
7117
7122
|
.option("--semantic-type <type>", "Optional semantic type such as company_domain.")
|
|
7118
7123
|
.option("--definition-json <json>", "Optional JSON object with column definition metadata.")
|
|
7119
|
-
.option("--prompt <text-or-file>", "AI or research column prompt, or a path to a prompt file — a value that resolves to a readable file is read as one, matching --prompt everywhere else in this CLI. On its own it sets kind=ai
|
|
7124
|
+
.option("--prompt <text-or-file>", "AI or research column prompt, or a path to a prompt file — a value that resolves to a readable file is read as one, matching --prompt everywhere else in this CLI. On its own it sets kind=ai; pair it with --kind research to search the web per row instead. If the prompt names its output sections — a line reading 'Return the following sections:' followed by 'Score: ...', 'Reasoning: ...' — the column answers in exactly that shape and each section becomes a referenceable sub-column; otherwise it answers in plain text. Use --no-structured-output to keep it plain text either way. Reference other columns inline as {{column_key}} — no --input-mapping needed; unknown keys are rejected here instead of failing per row. Merges into --definition-json (the escape hatch for everything else); a `prompt` in both is an error.")
|
|
7120
7125
|
.option("--research-query <template>", "Research columns: the per-row web search query, templated like the prompt (e.g. \"{{company_name}} pricing page\"). Omit to derive the query from the row's values and the prompt.")
|
|
7121
7126
|
.option("--research-domains <csv>", "Research columns: comma-separated domains to search within, e.g. techcrunch.com,sec.gov. Omit to search the whole web.")
|
|
7122
7127
|
.option("--research-exclude-domains <csv>", "Research columns: comma-separated domains to exclude from results.")
|
|
@@ -7127,8 +7132,10 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
7127
7132
|
.option("--input-mapping <json>", "Required with --prompt-key. JSON object mapping prompt input names to column or literal refs. Copy templates (email_draft_v1, subject_line_variants_v1, ...) reject mappings built only from manual name/title/company columns — ground them on a research or enrichment column, or write a freeform prompt with --prompt instead.")
|
|
7128
7133
|
.option("--model <id>", "AI column model id (e.g. claude-sonnet-4-5). Explicit models require credentialMode byok unless allow-listed managed.")
|
|
7129
7134
|
.option("--reasoning-level <level>", "AI column reasoning level: low, medium, or high (shown in the web UI as Oxygen Fast, Oxygen Balanced, and Oxygen Max).")
|
|
7130
|
-
.option("--run-condition <formula>", "Formula expression gating whether the AI column runs per row.")
|
|
7135
|
+
.option("--run-condition <formula>", "Formula expression gating whether the AI column runs per row. A formula over bare column keys (eu_israel = true), not {{tokens}}.")
|
|
7131
7136
|
.option("--run-condition-columns <csv>", "Comma-separated column keys referenced by --run-condition.")
|
|
7137
|
+
.option("--no-structured-output", "Keep an AI column answering in plain text even when its prompt names output sections. Use this for copy — an email body is one answer, not a set of fields.")
|
|
7138
|
+
.option("--structured-output", "Undo --no-structured-output: let the prompt's named sections shape the answer again.")
|
|
7132
7139
|
.option("--output-schema-json <json>", "AI column output JSON schema as inline JSON.")
|
|
7133
7140
|
.option("--output-schema-file <path>", "AI column output JSON schema read from a file path.")
|
|
7134
7141
|
.option("--bind-object <slug>", "Bind column CRM object slug (companies, people, deals, ...). Sets kind=bind and resolves rows to CRM records by identity.")
|
|
@@ -7274,12 +7281,16 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
7274
7281
|
body.prompt_key = options.promptKey;
|
|
7275
7282
|
if (options.inputMapping)
|
|
7276
7283
|
body.input_mapping = parseJsonObject(options.inputMapping);
|
|
7277
|
-
|
|
7278
|
-
|
|
7279
|
-
|
|
7284
|
+
// Every authoring path, not just --prompt: a definition pasted into
|
|
7285
|
+
// --definition-json used to skip this entirely and fail once per row.
|
|
7286
|
+
await assertColumnDefinitionReferences(table, column.definition);
|
|
7287
|
+
const created = await requestOxygen("/api/cli/tables/columns", {
|
|
7280
7288
|
method: "POST",
|
|
7281
7289
|
body,
|
|
7282
7290
|
});
|
|
7291
|
+
if (!options.json)
|
|
7292
|
+
writeColumnReferenceNotices(created);
|
|
7293
|
+
return created;
|
|
7283
7294
|
});
|
|
7284
7295
|
}))
|
|
7285
7296
|
.addCommand(new Command("run")
|
|
@@ -7421,25 +7432,30 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
7421
7432
|
}));
|
|
7422
7433
|
}))
|
|
7423
7434
|
.addCommand(new Command("rename")
|
|
7424
|
-
.description("Rename a workspace column key and optionally label.")
|
|
7435
|
+
.description("Rename a workspace column key and optionally label. Warns when another column reads the old key — a rename does not rewrite the references pointing at it.")
|
|
7425
7436
|
.argument("<table>", "Table id or slug.")
|
|
7426
7437
|
.argument("<column>", "Column id or key.")
|
|
7427
7438
|
.requiredOption("--key <key>", "New stable column key.")
|
|
7428
7439
|
.option("--label <label>", "Optional new display label.")
|
|
7429
7440
|
.option("--json", "Print a JSON envelope.")
|
|
7430
7441
|
.action(async (table, column, options) => {
|
|
7431
|
-
await handleAsyncAction("columns rename", options, () =>
|
|
7432
|
-
|
|
7433
|
-
|
|
7434
|
-
|
|
7435
|
-
|
|
7436
|
-
|
|
7437
|
-
|
|
7438
|
-
|
|
7439
|
-
|
|
7442
|
+
await handleAsyncAction("columns rename", options, async () => {
|
|
7443
|
+
const renamed = await requestOxygen("/api/cli/tables/columns/rename", {
|
|
7444
|
+
method: "POST",
|
|
7445
|
+
body: {
|
|
7446
|
+
table,
|
|
7447
|
+
column,
|
|
7448
|
+
key: options.key,
|
|
7449
|
+
...(readOption(options.label) ? { label: readOption(options.label) } : {}),
|
|
7450
|
+
},
|
|
7451
|
+
});
|
|
7452
|
+
if (!options.json)
|
|
7453
|
+
writeColumnReferenceNotices(renamed);
|
|
7454
|
+
return renamed;
|
|
7455
|
+
});
|
|
7440
7456
|
}))
|
|
7441
7457
|
.addCommand(new Command("update")
|
|
7442
|
-
.description("Update workspace column
|
|
7458
|
+
.description("Update a workspace column: label and visibility, and for AI/research columns the prompt, named inputs, model, reasoning level, run condition, output schema, and web-research settings.")
|
|
7443
7459
|
.argument("<table>", "Table id or slug.")
|
|
7444
7460
|
.argument("<column>", "Column id or key.")
|
|
7445
7461
|
.option("--label <label>", "New display label.")
|
|
@@ -7448,10 +7464,13 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
7448
7464
|
.option("--definition-unset <keys>", "Comma-separated definition keys to REMOVE after the merge (e.g. outputSchema,model) — a merge can only add or overwrite. Lifecycle and provenance keys are rejected.")
|
|
7449
7465
|
.option("--data-type <type>", "New output data type for a lookup column (text, numeric, boolean, jsonb, timestamptz). Discards the cached cells; other column kinds use `oxygen columns retype`.")
|
|
7450
7466
|
.option("--prompt <text-or-file>", "Replacement AI column prompt, or a path to a prompt file. Reference other columns inline as {{column_key}}; unknown keys are rejected here instead of failing per row. Merges into --definition-json; a `prompt` in both is an error.")
|
|
7467
|
+
.option("--input-mapping <json>", "Replace the column's named inputs wholesale. Row columns belong in the prompt as {{column_key}}; use this for the inputs a prompt cannot name on its own — literals and workspace context ({\"icp\":{\"type\":\"context_profile\",\"path\":\"icp\"}}), which the prompt then reads as {{icp}}. Pass {} to clear it; --definition-json can only add mapping keys, never remove one.")
|
|
7451
7468
|
.option("--model <id>", "AI column model id (e.g. claude-sonnet-4-5). Explicit models require credentialMode byok unless allow-listed managed.")
|
|
7452
7469
|
.option("--reasoning-level <level>", "AI column reasoning level: low, medium, or high (shown in the web UI as Oxygen Fast, Oxygen Balanced, and Oxygen Max).")
|
|
7453
|
-
.option("--run-condition <formula>", "Formula expression gating whether the AI column runs per row.")
|
|
7470
|
+
.option("--run-condition <formula>", "Formula expression gating whether the AI column runs per row. A formula over bare column keys (eu_israel = true), not {{tokens}}.")
|
|
7454
7471
|
.option("--run-condition-columns <csv>", "Comma-separated column keys referenced by --run-condition.")
|
|
7472
|
+
.option("--no-structured-output", "Keep an AI column answering in plain text even when its prompt names output sections. Use this for copy — an email body is one answer, not a set of fields.")
|
|
7473
|
+
.option("--structured-output", "Undo --no-structured-output: let the prompt's named sections shape the answer again.")
|
|
7455
7474
|
.option("--output-schema-json <json>", "AI column output JSON schema as inline JSON.")
|
|
7456
7475
|
.option("--output-schema-file <path>", "AI column output JSON schema read from a file path.")
|
|
7457
7476
|
.option("--research-query <template>", "Research columns: replace the per-row web search query template, e.g. \"{{company_name}} pricing page\".")
|
|
@@ -7499,9 +7518,16 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
7499
7518
|
definition = applyResearchColumnConfig(definition, options);
|
|
7500
7519
|
}
|
|
7501
7520
|
const definitionUnset = readCsvOption(options.definitionUnset);
|
|
7502
|
-
|
|
7503
|
-
|
|
7504
|
-
|
|
7521
|
+
// --input-mapping is the only way to repair a bad mapping in place:
|
|
7522
|
+
// the server SHALLOW-merges `definition`, so a nested inputMapping
|
|
7523
|
+
// sent through --definition-json adds keys and can never remove one.
|
|
7524
|
+
// This replaces the object wholesale.
|
|
7525
|
+
const inputMapping = readOption(options.inputMapping);
|
|
7526
|
+
if (inputMapping) {
|
|
7527
|
+
definition = { ...(definition ?? {}), inputMapping: parseJsonObject(inputMapping) };
|
|
7528
|
+
}
|
|
7529
|
+
await assertColumnDefinitionReferences(table, definition);
|
|
7530
|
+
const updated = await requestOxygen("/api/cli/tables/columns/update", {
|
|
7505
7531
|
method: "POST",
|
|
7506
7532
|
body: {
|
|
7507
7533
|
table,
|
|
@@ -7518,6 +7544,9 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
7518
7544
|
...(options.dryRun ? { dry_run: true } : {}),
|
|
7519
7545
|
},
|
|
7520
7546
|
});
|
|
7547
|
+
if (!options.json)
|
|
7548
|
+
writeColumnReferenceNotices(updated);
|
|
7549
|
+
return updated;
|
|
7521
7550
|
});
|
|
7522
7551
|
}))
|
|
7523
7552
|
.addCommand(new Command("retype")
|
|
@@ -7627,6 +7656,17 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
7627
7656
|
params.set("column", column);
|
|
7628
7657
|
return requestOxygen(`/api/cli/tables/columns/deps?${params.toString()}`, { method: "GET" });
|
|
7629
7658
|
});
|
|
7659
|
+
}))
|
|
7660
|
+
.addCommand(new Command("fields")
|
|
7661
|
+
.description("List what a column's output exposes: every field you can reference as {{column.field}}, filter on, sort by, or promote to its own column with `columns materialize`. Works before the column has ever run. Read-only and free.")
|
|
7662
|
+
.argument("<table>", "Table id or slug.")
|
|
7663
|
+
.argument("<column>", "Column key or id.")
|
|
7664
|
+
.option("--json", "Print a JSON envelope.")
|
|
7665
|
+
.action(async (table, column, options) => {
|
|
7666
|
+
await handleAsyncAction("columns fields", options, () => {
|
|
7667
|
+
const params = new URLSearchParams({ table, column });
|
|
7668
|
+
return requestOxygen(`/api/cli/tables/columns/fields?${params.toString()}`, { method: "GET" });
|
|
7669
|
+
});
|
|
7630
7670
|
}));
|
|
7631
7671
|
program
|
|
7632
7672
|
.command("action-column")
|
|
@@ -9948,7 +9988,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
9948
9988
|
.option("--company-domain-column <column>", "Column key or id containing the company domain for work_email. Pair with full-name/first+last for name+company providers.")
|
|
9949
9989
|
.option("--company-name-column <column>", "Column key or id containing the company name for work_email when no domain is available.")
|
|
9950
9990
|
.option("--company-linkedin-url-column <column>", "Column key or id containing the company's LinkedIn URL for company identity fallback.")
|
|
9951
|
-
.option("--capability <capability>", "
|
|
9991
|
+
.option("--capability <capability>", "What to run: mobile_phone, work_email, linkedin_url (find a missing value), or verify_email (grade an email the row already has). Defaults to mobile_phone.")
|
|
9952
9992
|
.option("--target-column <column>", "Target enrichment column key. Defaults to the capability payload column.")
|
|
9953
9993
|
.option("--on-existing-manual-column <mode>", "How to handle an existing manual target: error, write_if_empty, or create_enrichment_column.")
|
|
9954
9994
|
.option("--provider-order <providers>", "Comma-separated provider order. Overrides the default cost-aware waterfall profile.")
|
|
@@ -9983,7 +10023,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
9983
10023
|
.option("--company-name-column <column>", "Column key or id containing the company name for work_email when no domain is available.")
|
|
9984
10024
|
.option("--company-linkedin-url-column <column>", "Column key or id containing the company's LinkedIn URL for company identity fallback.")
|
|
9985
10025
|
.requiredOption("--max-credits <credits>", "Required credit ceiling for the queued run.")
|
|
9986
|
-
.option("--capability <capability>", "
|
|
10026
|
+
.option("--capability <capability>", "What to run: mobile_phone, work_email, linkedin_url (find a missing value), or verify_email (grade an email the row already has). Defaults to mobile_phone.")
|
|
9987
10027
|
.option("--target-column <column>", "Target enrichment column key. Defaults to the capability payload column.")
|
|
9988
10028
|
.option("--on-existing-manual-column <mode>", "How to handle an existing manual target: error, write_if_empty, or create_enrichment_column.")
|
|
9989
10029
|
.option("--provider-order <providers>", "Comma-separated provider order. Overrides the default cost-aware waterfall profile.")
|
|
@@ -12285,7 +12325,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
12285
12325
|
});
|
|
12286
12326
|
})))
|
|
12287
12327
|
.addCommand(new Command("analytics")
|
|
12288
|
-
.description("Show organization-level sequencer analytics, per-sequence funnels, and native email attribution under analytics.emailAttribution: byMailbox, byDomain, and explicit unattributed facts. With --sequence, analytics.senderPerformance groups channel-conditional counts and rates by sending person
|
|
12328
|
+
.description("Show organization-level sequencer analytics, per-sequence funnels, and native email attribution under analytics.emailAttribution: byMailbox, byDomain, and explicit unattributed facts. With --sequence, analytics.senderPerformance groups channel-conditional counts and rates by sending person; analytics.messagePerformance reports each copy-bearing deterministic step variant, target/actual distribution, contacted recipients, and type-specific accepted/reply/positive outcomes (absent for note-less connect-only campaigns). Message-level reply outcomes require preserved step/variant provenance, so older unattributed replies remain in campaign totals without being guessed into a message row. Provider-owned campaigns appear only after their telemetry is normalized into Oxygen's native Sequence ledgers. Also reports whether the reply → CRM automation is armed, with a link to the workflow.")
|
|
12289
12329
|
.option("--range <range>", "Preset range: 7d, 14d, 28d, 30d, 90d, 180d, 365d, or all. Defaults to 14d. `all` starts at the oldest campaign in scope.")
|
|
12290
12330
|
.option("--from <date>", "Custom start date (YYYY-MM-DD). Windows up to five years are accepted.")
|
|
12291
12331
|
.option("--to <date>", "Custom end date (YYYY-MM-DD).")
|
|
@@ -12655,16 +12695,16 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
12655
12695
|
});
|
|
12656
12696
|
}))
|
|
12657
12697
|
.addCommand(new Command("contacts")
|
|
12658
|
-
.description("List a launched campaign's table-backed contacts with full source variables and
|
|
12698
|
+
.description("List a launched campaign's table-backed contacts with full source variables, durable engagement stage, and factual or assigned sender. Contacts removed from this campaign are hidden; source rows remain intact.")
|
|
12659
12699
|
.argument("<sequence>", "Sequence id or slug.")
|
|
12660
|
-
.option("--contact-state <state>", "Filter: all, contacted, or
|
|
12700
|
+
.option("--contact-state <state>", "Filter: all, not_contacted, contacted, connected, replied, or positive_reply.", "all")
|
|
12661
12701
|
.option("--limit <n>", "Maximum contacts to return (1-500).", "100")
|
|
12662
12702
|
.option("--json", "Print a JSON envelope.")
|
|
12663
12703
|
.action(async (sequence, options) => {
|
|
12664
12704
|
await handleAsyncAction("sequences contacts", options, () => {
|
|
12665
12705
|
const contactState = readOption(options.contactState) ?? "all";
|
|
12666
|
-
if (!["all", "contacted", "
|
|
12667
|
-
throw new Error("--contact-state must be all, contacted, or
|
|
12706
|
+
if (!["all", "not_contacted", "contacted", "connected", "replied", "positive_reply"].includes(contactState)) {
|
|
12707
|
+
throw new Error("--contact-state must be all, not_contacted, contacted, connected, replied, or positive_reply.");
|
|
12668
12708
|
}
|
|
12669
12709
|
const limit = readPositiveInt(options.limit);
|
|
12670
12710
|
if (limit === undefined || limit > 500) {
|
|
@@ -12764,7 +12804,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
12764
12804
|
await handleSequenceEventsAction(sequence, options);
|
|
12765
12805
|
}))
|
|
12766
12806
|
.addCommand(new Command("variants")
|
|
12767
|
-
.description("
|
|
12807
|
+
.description("Generic per-step action/base-variant scoreboard for one sequence, plus native email by_mailbox, by_domain, reply types, rates, and explicit unattributed facts. This rollup is not the copy-only analytics.messagePerformance view; base rows can represent non-message actions, and campaign replies may lack step/variant provenance. Provider-owned campaigns appear only after normalization into Oxygen's native Sequence ledgers. Also returns reversible auto-winner state (paused variants + evidence); flags run or override/reset that state.")
|
|
12768
12808
|
.argument("<sequence>", "Sequence id or slug.")
|
|
12769
12809
|
.option("--auto-optimize", "Run the auto-winner now: pause the statistically-losing variant(s) of any step carrying an auto_optimize config, once every variant clears its thresholds, on that step's configured metric — reply (the recommended default), click, or open (click/open need native open/click tracking; opens are soft signals under Apple Mail Privacy Protection). Reversible.")
|
|
12770
12810
|
.option("--pause <variant>", "Manually pause one variant (requires --step). Overrides the auto-winner.")
|
|
@@ -14630,26 +14670,6 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
14630
14670
|
.option("--json", "Print a JSON envelope.")
|
|
14631
14671
|
.action(async (domain, options) => {
|
|
14632
14672
|
await handleAsyncAction("domains tracking status", options, () => requestOxygen(`/api/cli/domains/${encodeURIComponent(domain)}/tracking`));
|
|
14633
|
-
})))
|
|
14634
|
-
.addCommand(new Command("postmaster")
|
|
14635
|
-
.description("Google Postmaster Tools onboarding for a sending domain: register it with Postmaster, publish the DNS verification TXT via its Cloudflare zone, and check verification status. FREE — 0 Oxygen credits.")
|
|
14636
|
-
.addCommand(new Command("onboard")
|
|
14637
|
-
.description("Register the domain with Google Postmaster Tools and publish the DNS verification TXT automatically via the domain's Cloudflare zone (or return the record for manual DNS); verification then completes automatically in the background. Without --approve, returns a preview. FREE — 0 Oxygen credits.")
|
|
14638
|
-
.argument("<domain>", "Sending domain, such as acme.com.")
|
|
14639
|
-
.option("--approve", "Perform the onboarding. Without this flag, returns a preview only.")
|
|
14640
|
-
.option("--json", "Print a JSON envelope.")
|
|
14641
|
-
.action(async (domain, options) => {
|
|
14642
|
-
await handleAsyncAction("domains postmaster onboard", options, () => requestOxygen(`/api/cli/domains/${encodeURIComponent(domain)}/postmaster`, {
|
|
14643
|
-
method: "POST",
|
|
14644
|
-
body: { ...(options.approve ? { approved: true } : {}) },
|
|
14645
|
-
}));
|
|
14646
|
-
}))
|
|
14647
|
-
.addCommand(new Command("status")
|
|
14648
|
-
.description("Show the Google Postmaster Tools onboarding status for a domain: registration, DNS verification state, and whether reputation data is available. Read-only — 0 Oxygen credits.")
|
|
14649
|
-
.argument("<domain>", "Sending domain, such as acme.com.")
|
|
14650
|
-
.option("--json", "Print a JSON envelope.")
|
|
14651
|
-
.action(async (domain, options) => {
|
|
14652
|
-
await handleAsyncAction("domains postmaster status", options, () => requestOxygen(`/api/cli/domains/${encodeURIComponent(domain)}/postmaster`));
|
|
14653
14673
|
})))
|
|
14654
14674
|
.addCommand(new Command("add")
|
|
14655
14675
|
.description("BYOK CLOUDFLARE: onboard a domain registered elsewhere by creating a zone in your connected Cloudflare account. For managed domain + inbox provisioning use `managed-inboxes subscribe`.")
|
|
@@ -16813,19 +16833,62 @@ const AI_PROMPT_TEMPLATE_TOKEN = /{{\s*([^{}]+?)\s*}}/g;
|
|
|
16813
16833
|
// in packages/tenant-db), so a prompt may legitimately reference them.
|
|
16814
16834
|
const AI_PROMPT_SYSTEM_ROW_KEYS = ["_row_id", "_created_at", "_updated_at"];
|
|
16815
16835
|
const AI_PROMPT_AVAILABLE_COLUMNS_SHOWN = 20;
|
|
16816
|
-
|
|
16817
|
-
|
|
16818
|
-
|
|
16819
|
-
// The first dot-segment is the column key; the rest is a JSON path into that cell,
|
|
16820
|
-
// so `{{research.summary}}` only requires a `research` column to exist.
|
|
16821
|
-
function readAiPromptColumnRefs(prompt) {
|
|
16822
|
-
const refs = [];
|
|
16836
|
+
function readAiPromptTemplateTokens(prompt) {
|
|
16837
|
+
const tokens = [];
|
|
16838
|
+
const seen = new Set();
|
|
16823
16839
|
for (const match of prompt.matchAll(AI_PROMPT_TEMPLATE_TOKEN)) {
|
|
16824
|
-
const
|
|
16825
|
-
if (
|
|
16826
|
-
|
|
16840
|
+
const raw = (match[1] ?? "").trim();
|
|
16841
|
+
if (!raw || seen.has(raw))
|
|
16842
|
+
continue;
|
|
16843
|
+
seen.add(raw);
|
|
16844
|
+
const segments = raw.split(".").map((segment) => segment.trim()).filter(Boolean);
|
|
16845
|
+
const root = segments.shift();
|
|
16846
|
+
if (!root)
|
|
16847
|
+
continue;
|
|
16848
|
+
tokens.push({ raw, root, path: segments.length > 0 ? segments.join(".") : null });
|
|
16849
|
+
}
|
|
16850
|
+
return tokens;
|
|
16851
|
+
}
|
|
16852
|
+
/**
|
|
16853
|
+
* The first `{{column.path}}` whose column declares fields but not that one.
|
|
16854
|
+
*
|
|
16855
|
+
* Author-time rather than per-row, and deliberately narrow: only a column that
|
|
16856
|
+
* actually publishes `output_fields` is checked. An undeclared jsonb payload can
|
|
16857
|
+
* hold anything, so rejecting a path into one would refuse references that work.
|
|
16858
|
+
*/
|
|
16859
|
+
function findUnknownColumnFieldReference(tokens, columns) {
|
|
16860
|
+
for (const token of tokens) {
|
|
16861
|
+
if (!token.path)
|
|
16862
|
+
continue;
|
|
16863
|
+
const column = (columns ?? []).find((entry) => entry.key === token.root);
|
|
16864
|
+
const fields = column?.output_fields ?? null;
|
|
16865
|
+
if (!fields || fields.length === 0)
|
|
16866
|
+
continue;
|
|
16867
|
+
const paths = fields.map((field) => field.path);
|
|
16868
|
+
if (paths.includes(token.path))
|
|
16869
|
+
continue;
|
|
16870
|
+
// A path INTO a declared object field is fine — the declaration stops at the
|
|
16871
|
+
// depth the schema described, not at the depth the payload goes.
|
|
16872
|
+
if (paths.some((path) => token.path?.startsWith(`${path}.`)))
|
|
16873
|
+
continue;
|
|
16874
|
+
const nearMisses = nearMissColumnKeys(token.path, paths);
|
|
16875
|
+
return new OxygenError("invalid_prompt_column", [
|
|
16876
|
+
`Prompt references {{${token.raw}}}, but "${token.root}" has no field "${token.path}".`,
|
|
16877
|
+
nearMisses.length > 0
|
|
16878
|
+
? ` Did you mean {{${token.root}.${nearMisses[0]}}}?`
|
|
16879
|
+
: "",
|
|
16880
|
+
` Fields on "${token.root}": ${paths.slice(0, AI_PROMPT_AVAILABLE_COLUMNS_SHOWN).join(", ")}.`,
|
|
16881
|
+
].join(""), {
|
|
16882
|
+
details: {
|
|
16883
|
+
column: token.root,
|
|
16884
|
+
unknown_field: token.path,
|
|
16885
|
+
near_misses: nearMisses,
|
|
16886
|
+
available_fields: paths,
|
|
16887
|
+
},
|
|
16888
|
+
exitCode: 1,
|
|
16889
|
+
});
|
|
16827
16890
|
}
|
|
16828
|
-
return
|
|
16891
|
+
return null;
|
|
16829
16892
|
}
|
|
16830
16893
|
// Plain Levenshtein distance, capped at the short keys this is used on — enough to
|
|
16831
16894
|
// catch the single-character and transposition typos ({{frist_name}}) the near-miss
|
|
@@ -16853,21 +16916,64 @@ function nearMissColumnKeys(reference, columnKeys) {
|
|
|
16853
16916
|
.slice(0, 3)
|
|
16854
16917
|
.map((candidate) => candidate.key);
|
|
16855
16918
|
}
|
|
16856
|
-
|
|
16857
|
-
|
|
16858
|
-
|
|
16859
|
-
|
|
16860
|
-
|
|
16861
|
-
|
|
16862
|
-
|
|
16863
|
-
|
|
16919
|
+
/**
|
|
16920
|
+
* Resolve a column definition's `{{tokens}}` against the table's live columns
|
|
16921
|
+
* BEFORE the write, so a typo is one typed error instead of one failed row per
|
|
16922
|
+
* row. `analyzeColumnReferences` (packages/tenant-db) is the authority and the
|
|
16923
|
+
* server now rejects the same thing on every surface; this is the local
|
|
16924
|
+
* pre-flight that saves a round trip and prints the near-miss hint.
|
|
16925
|
+
*
|
|
16926
|
+
* Scans the whole definition, not just `--prompt`: the prompt, the research
|
|
16927
|
+
* query template, and every `type: "template"` input all share one grammar, and
|
|
16928
|
+
* a definition pasted into `--definition-json` used to skip the check entirely.
|
|
16929
|
+
* A token naming one of the column's OWN inputs resolves at run time, so those
|
|
16930
|
+
* names count as known.
|
|
16931
|
+
*/
|
|
16932
|
+
async function assertColumnDefinitionReferences(table, definition) {
|
|
16933
|
+
if (!isRecord(definition))
|
|
16864
16934
|
return;
|
|
16865
|
-
const
|
|
16866
|
-
const
|
|
16867
|
-
|
|
16935
|
+
const inputMapping = isRecord(definition.inputMapping) ? definition.inputMapping : {};
|
|
16936
|
+
const templates = [];
|
|
16937
|
+
if (typeof definition.prompt === "string")
|
|
16938
|
+
templates.push(definition.prompt);
|
|
16939
|
+
const webSearch = isRecord(definition.webSearch) ? definition.webSearch : {};
|
|
16940
|
+
if (typeof webSearch.queryTemplate === "string")
|
|
16941
|
+
templates.push(webSearch.queryTemplate);
|
|
16942
|
+
for (const ref of Object.values(inputMapping)) {
|
|
16943
|
+
if (isRecord(ref) && ref.type === "template" && typeof ref.value === "string") {
|
|
16944
|
+
templates.push(ref.value);
|
|
16945
|
+
}
|
|
16946
|
+
}
|
|
16947
|
+
if (templates.length === 0)
|
|
16948
|
+
return;
|
|
16949
|
+
await assertPromptColumnReferences(table, templates.join("\n"), Object.keys(inputMapping));
|
|
16950
|
+
}
|
|
16951
|
+
// The core of the check above. The server is authoritative — this is the local
|
|
16952
|
+
// pre-flight that fails before the round trip and names the near miss.
|
|
16953
|
+
async function assertPromptColumnReferences(table, prompt, inputNames = []) {
|
|
16954
|
+
const tokens = readAiPromptTemplateTokens(prompt);
|
|
16955
|
+
if (tokens.length === 0)
|
|
16956
|
+
return;
|
|
16957
|
+
// include_archived: an archived column is a metadata flag over a physical
|
|
16958
|
+
// column that still exists, so `{{archived_key}}` still resolves at run time.
|
|
16959
|
+
// Leaving them out would reject here what the server accepts — the one
|
|
16960
|
+
// divergence a local pre-flight must never have.
|
|
16961
|
+
const describe = await requestOxygen("/api/cli/tables/describe", { method: "POST", body: { table, include_archived: true } });
|
|
16962
|
+
const columns = describe.columns ?? [];
|
|
16963
|
+
const columnKeys = columns.map((column) => column.key).filter(Boolean);
|
|
16964
|
+
const known = new Set([...columnKeys, ...AI_PROMPT_SYSTEM_ROW_KEYS, ...inputNames]);
|
|
16965
|
+
const refs = [...new Set(tokens.map((token) => token.root))];
|
|
16868
16966
|
const unknown = refs.filter((ref) => !known.has(ref));
|
|
16869
|
-
|
|
16967
|
+
// A path into a column that DOES exist is checked against what that column
|
|
16968
|
+
// declares it exposes, so `{{icp_fit.scoer}}` fails here rather than resolving
|
|
16969
|
+
// to null on every row of a paid run. Only checked when the column actually
|
|
16970
|
+
// declares fields — an undeclared jsonb payload can hold anything.
|
|
16971
|
+
if (unknown.length === 0) {
|
|
16972
|
+
const badPath = findUnknownColumnFieldReference(tokens, columns);
|
|
16973
|
+
if (badPath)
|
|
16974
|
+
throw badPath;
|
|
16870
16975
|
return;
|
|
16976
|
+
}
|
|
16871
16977
|
const nearMisses = nearMissColumnKeys(unknown[0], columnKeys);
|
|
16872
16978
|
const shown = columnKeys.slice(0, AI_PROMPT_AVAILABLE_COLUMNS_SHOWN);
|
|
16873
16979
|
const overflow = columnKeys.length - shown.length;
|
|
@@ -16875,7 +16981,7 @@ async function assertPromptColumnReferences(table, prompt) {
|
|
|
16875
16981
|
// invalid_* so exitCodeForOxygenError classifies this as a validation
|
|
16876
16982
|
// failure (exit 2) alongside the server's invalid_column_definition.
|
|
16877
16983
|
"invalid_prompt_column", [
|
|
16878
|
-
`Prompt references {{${unknown[0]}}}, which is not a column on table "${table}".`,
|
|
16984
|
+
`Prompt references {{${unknown[0]}}}, which is not a column on table "${table}" and not one of this column's inputs.`,
|
|
16879
16985
|
nearMisses.length > 0
|
|
16880
16986
|
? ` Did you mean ${nearMisses.map((key) => `{{${key}}}`).join(", ")}?`
|
|
16881
16987
|
: "",
|
|
@@ -16916,6 +17022,14 @@ function applyAiColumnConfig(definition, options) {
|
|
|
16916
17022
|
else if (outputSchemaFile) {
|
|
16917
17023
|
definition.outputSchema = readJsonFileValue(resolve(outputSchemaFile), "--output-schema-file");
|
|
16918
17024
|
}
|
|
17025
|
+
// Only written when the flag was actually passed. Absent must stay absent: on
|
|
17026
|
+
// `columns update` the definition is merged, so writing `true` by default
|
|
17027
|
+
// would re-enable structure on a column the author had deliberately set to
|
|
17028
|
+
// prose without ever mentioning it.
|
|
17029
|
+
if (options.structuredOutput === false)
|
|
17030
|
+
definition.structuredOutput = false;
|
|
17031
|
+
else if (options.structuredOutput === true)
|
|
17032
|
+
definition.structuredOutput = true;
|
|
16919
17033
|
return definition;
|
|
16920
17034
|
}
|
|
16921
17035
|
const RESEARCH_ENGINES = new Set(["exa", "parallel", "firecrawl"]);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Buffer } from "node:buffer";
|
|
2
2
|
import { OxygenError } from "@oxygen/shared";
|
|
3
|
+
import { readJsonPath, walkJsonPath } from "@oxygen/shared/json-path";
|
|
3
4
|
import { CustomHttpUrlSafetyError, assertCustomHttpPublicUrlSyntax, assertCustomHttpResolvedHostAllowed, } from "@oxygen/shared/custom-http-safety";
|
|
4
5
|
import { requestOxygen } from "./http-client.js";
|
|
5
6
|
import { isRecord } from "./util.js";
|
|
@@ -668,49 +669,23 @@ function resolveLocalTemplateRowRoot(root, segments, row, inputName, rawPath) {
|
|
|
668
669
|
return { current: row[root], segments };
|
|
669
670
|
}
|
|
670
671
|
function resolveLocalTemplateSegments(current, segments) {
|
|
671
|
-
|
|
672
|
-
for (const segment of segments) {
|
|
673
|
-
value = resolveLocalTemplateSegment(value, segment);
|
|
674
|
-
if (value === null)
|
|
675
|
-
return null;
|
|
676
|
-
}
|
|
677
|
-
return value ?? null;
|
|
678
|
-
}
|
|
679
|
-
function resolveLocalTemplateSegment(current, segment) {
|
|
680
|
-
if (current === null || current === undefined)
|
|
681
|
-
return null;
|
|
682
|
-
if (Array.isArray(current) && /^\d+$/.test(segment))
|
|
683
|
-
return current[Number(segment)] ?? null;
|
|
684
|
-
if (isRecord(current) && Object.hasOwn(current, segment))
|
|
685
|
-
return current[segment];
|
|
686
|
-
return null;
|
|
672
|
+
return readJsonPath(current, segments);
|
|
687
673
|
}
|
|
688
674
|
function applyLocalOutputPath(value, outputPath) {
|
|
689
675
|
const path = typeof outputPath === "string" ? outputPath.trim() : "";
|
|
690
676
|
if (!path)
|
|
691
677
|
return value;
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
}
|
|
701
|
-
if (current === null || current === undefined)
|
|
702
|
-
return null;
|
|
703
|
-
if (Array.isArray(current) && /^\d+$/.test(segment)) {
|
|
704
|
-
current = current[Number(segment)] ?? null;
|
|
705
|
-
continue;
|
|
706
|
-
}
|
|
707
|
-
if (isRecord(current) && Object.hasOwn(current, segment)) {
|
|
708
|
-
current = current[segment];
|
|
709
|
-
continue;
|
|
710
|
-
}
|
|
711
|
-
return null;
|
|
678
|
+
// Must stay byte-identical to `applyOutputPath` in @oxygen/integrations —
|
|
679
|
+
// `--local` runs the same column config in-process, and a divergence here is a
|
|
680
|
+
// cell that changes value depending on where it ran.
|
|
681
|
+
const walked = walkJsonPath(value, path);
|
|
682
|
+
if (!walked.ok) {
|
|
683
|
+
throw new OxygenError("invalid_output_path", "Custom HTTP outputPath is invalid.", {
|
|
684
|
+
details: { output_path: outputPath },
|
|
685
|
+
exitCode: 1,
|
|
686
|
+
});
|
|
712
687
|
}
|
|
713
|
-
return
|
|
688
|
+
return walked.value;
|
|
714
689
|
}
|
|
715
690
|
function redactLocalSecrets(value, secretValues) {
|
|
716
691
|
if (typeof value === "string") {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { OxygenError } from "@oxygen/shared/cli-result";
|
|
2
|
-
import {
|
|
2
|
+
import { walkJsonPath } from "@oxygen/shared/json-path";
|
|
3
3
|
import { normalizeDomain, normalizeEmail, normalizeLinkedinUrl, } from "./value-normalizers.js";
|
|
4
4
|
export const MAX_FORMULA_REGEX_PATTERN_LENGTH = 256;
|
|
5
5
|
export const MAX_FORMULA_REGEX_INPUT_LENGTH = 20_000;
|
|
@@ -70,30 +70,16 @@ export function formulaValuesEqual(left, right) {
|
|
|
70
70
|
return left === right;
|
|
71
71
|
}
|
|
72
72
|
export function readFormulaJsonPath(value, rawPath) {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
path: rawPath,
|
|
82
|
-
});
|
|
83
|
-
}
|
|
84
|
-
if (current === null || current === undefined)
|
|
85
|
-
return null;
|
|
86
|
-
if (Array.isArray(current) && /^\d+$/.test(segment)) {
|
|
87
|
-
current = current[Number(segment)] ?? null;
|
|
88
|
-
continue;
|
|
89
|
-
}
|
|
90
|
-
if (isRecord(current) && Object.hasOwn(current, segment)) {
|
|
91
|
-
current = current[segment];
|
|
92
|
-
continue;
|
|
93
|
-
}
|
|
94
|
-
return null;
|
|
73
|
+
// Lazy validation, deliberately: a formula is evaluated per row, and an empty
|
|
74
|
+
// segment only surfaces on a row whose value actually reaches it. Switching to
|
|
75
|
+
// up-front validation would start failing rows that used to return blank.
|
|
76
|
+
const walked = walkJsonPath(value, rawPath);
|
|
77
|
+
if (!walked.ok) {
|
|
78
|
+
throw formulaExpressionError("JSON path contains an empty segment.", {
|
|
79
|
+
path: rawPath,
|
|
80
|
+
});
|
|
95
81
|
}
|
|
96
|
-
return
|
|
82
|
+
return walked.value;
|
|
97
83
|
}
|
|
98
84
|
export function checkFormulaFunctionArity(spec, received) {
|
|
99
85
|
const withinMin = received >= spec.minArgs;
|