@oxygen-agent/cli 1.1014.1035 → 1.1014.1087
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/command-manifest.js +1 -0
- package/dist/commands/deliverability.js +2 -0
- package/dist/commands/sequences.js +30 -2
- package/dist/credentials.js +1 -1
- package/dist/customer-operation-commands.js +62 -3
- package/dist/help.js +1 -1
- package/dist/index.d.ts +0 -97
- package/dist/index.js +0 -98
- package/dist/workspace-placement-commands.d.ts +3 -0
- package/dist/workspace-placement-commands.js +32 -0
- package/node_modules/@oxygen/shared/dist/agent-sessions.js +1 -1
- package/node_modules/@oxygen/shared/dist/agents-enabled.js +5 -2
- package/node_modules/@oxygen/shared/dist/capability-discovery.js +23 -5
- package/node_modules/@oxygen/shared/dist/contracts/agent-sessions.js +1 -1
- package/node_modules/@oxygen/shared/dist/contracts/customer-operations.js +1 -1
- package/node_modules/@oxygen/shared/dist/contracts/guided-activation.js +5 -5
- package/node_modules/@oxygen/shared/dist/contracts/guided-company-files.d.ts +30 -0
- package/node_modules/@oxygen/shared/dist/contracts/guided-company-files.js +53 -0
- package/node_modules/@oxygen/shared/dist/contracts/guided-first-sourcing.d.ts +120 -0
- package/node_modules/@oxygen/shared/dist/contracts/guided-first-sourcing.js +48 -0
- package/node_modules/@oxygen/shared/dist/contracts/guided-onboarding-journey.d.ts +267 -0
- package/node_modules/@oxygen/shared/dist/contracts/guided-onboarding-journey.js +63 -0
- package/node_modules/@oxygen/shared/dist/contracts/operations.d.ts +6 -0
- package/node_modules/@oxygen/shared/dist/contracts/operations.js +9 -4
- package/node_modules/@oxygen/shared/dist/contracts/table-query-raw.d.ts +2 -0
- package/node_modules/@oxygen/shared/dist/contracts/table-query-raw.js +3 -0
- package/node_modules/@oxygen/shared/dist/contracts/teammates.js +7 -7
- package/node_modules/@oxygen/shared/dist/email-tracking-token.d.ts +2 -0
- package/node_modules/@oxygen/shared/dist/email-tracking-token.js +1 -0
- package/node_modules/@oxygen/shared/dist/file-import.d.ts +46 -0
- package/node_modules/@oxygen/shared/dist/file-import.js +138 -0
- package/node_modules/@oxygen/shared/dist/guided-activation.d.ts +8 -0
- package/node_modules/@oxygen/shared/dist/guided-company-files.d.ts +13 -0
- package/node_modules/@oxygen/shared/dist/guided-company-files.js +14 -0
- package/node_modules/@oxygen/shared/dist/guided-first-sourcing.d.ts +116 -0
- package/node_modules/@oxygen/shared/dist/guided-first-sourcing.js +11 -0
- package/node_modules/@oxygen/shared/dist/guided-onboarding-answers.d.ts +4 -0
- package/node_modules/@oxygen/shared/dist/guided-onboarding-answers.js +42 -0
- package/node_modules/@oxygen/shared/dist/guided-onboarding-company-context.d.ts +22 -0
- package/node_modules/@oxygen/shared/dist/guided-onboarding-company-context.js +38 -0
- package/node_modules/@oxygen/shared/dist/guided-onboarding-journey.d.ts +147 -0
- package/node_modules/@oxygen/shared/dist/guided-onboarding-journey.fixture.d.ts +12 -0
- package/node_modules/@oxygen/shared/dist/guided-onboarding-journey.fixture.js +83 -0
- package/node_modules/@oxygen/shared/dist/guided-onboarding-journey.js +44 -0
- package/node_modules/@oxygen/shared/dist/guided-onboarding-prospects.d.ts +7 -0
- package/node_modules/@oxygen/shared/dist/guided-onboarding-prospects.js +13 -0
- package/node_modules/@oxygen/shared/dist/guided-onboarding-signals.d.ts +4 -0
- package/node_modules/@oxygen/shared/dist/guided-onboarding-signals.js +34 -0
- package/node_modules/@oxygen/shared/dist/guided-trial-billing.d.ts +26 -0
- package/node_modules/@oxygen/shared/dist/guided-trial-billing.js +19 -0
- package/node_modules/@oxygen/shared/dist/user-capability-routing.js +78 -0
- package/node_modules/@oxygen/shared/dist/version.generated.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/version.generated.js +1 -1
- package/node_modules/@oxygen/shared/dist/workspace-placement-view.d.ts +149 -0
- package/node_modules/@oxygen/shared/dist/workspace-placement-view.js +101 -0
- package/node_modules/@oxygen/shared/dist/workspace-placement.d.ts +77 -0
- package/node_modules/@oxygen/shared/dist/workspace-placement.js +94 -0
- package/package.json +1 -1
|
@@ -33,6 +33,12 @@ export type OperationSpec<C extends ContractObject = ContractObject> = {
|
|
|
33
33
|
nameField: string;
|
|
34
34
|
typeField: string;
|
|
35
35
|
fileField: string;
|
|
36
|
+
/** Opt-in metadata derived from the original --file bytes. Explicit caller
|
|
37
|
+
* assertions must match; required native fields remain required on the wire. */
|
|
38
|
+
fileMetadata?: {
|
|
39
|
+
sha256Field: string;
|
|
40
|
+
sizeBytesField: string;
|
|
41
|
+
};
|
|
36
42
|
};
|
|
37
43
|
/** Download bytes through the same authenticated route; never follow redirects. */
|
|
38
44
|
binary?: {
|
|
@@ -12,6 +12,9 @@ import { BROWSER_OPERATIONS } from "./browser-operations.js";
|
|
|
12
12
|
import { TABLE_FEED_UPDATE_OPERATION } from "./table-feed-update.js";
|
|
13
13
|
import { GEO_OPERATIONS } from "./geo.js";
|
|
14
14
|
import { CUSTOMER_OPERATIONS } from "./customer-operations.js";
|
|
15
|
+
import { GUIDED_ONBOARDING_JOURNEY_OPERATIONS } from "./guided-onboarding-journey.js";
|
|
16
|
+
import { GUIDED_FIRST_SOURCING_OPERATIONS } from "./guided-first-sourcing.js";
|
|
17
|
+
import { GUIDED_COMPANY_FILE_UPLOAD_OPERATION } from "./guided-company-files.js";
|
|
15
18
|
import { GUIDED_ACTIVATION_OPERATIONS } from "./guided-activation.js";
|
|
16
19
|
import { AGENT_SESSION_OPERATIONS } from "./agent-sessions.js";
|
|
17
20
|
import { TEAMMATE_OPERATIONS } from "./teammates.js";
|
|
@@ -79,7 +82,9 @@ export function operationFormData(spec, body) {
|
|
|
79
82
|
if (!fields)
|
|
80
83
|
throw new Error("This operation does not accept a file.");
|
|
81
84
|
const encoded = body[fields.contentField];
|
|
82
|
-
|
|
85
|
+
// A repeated capture over multi-megabyte uploads can overflow V8's regexp
|
|
86
|
+
// stack. Validate the same alphabet, length and terminal padding separately.
|
|
87
|
+
if (typeof encoded !== "string" || !encoded || encoded.length % 4 !== 0 || /[^A-Za-z0-9+/]/u.test(encoded.replace(/={1,2}$/u, ""))) {
|
|
83
88
|
throw new Error(`${fields.contentField} must contain valid non-empty base64 file bytes.`);
|
|
84
89
|
}
|
|
85
90
|
const bytes = Uint8Array.from(atob(encoded), (character) => character.charCodeAt(0));
|
|
@@ -92,6 +97,6 @@ export function operationFormData(spec, body) {
|
|
|
92
97
|
}
|
|
93
98
|
return form;
|
|
94
99
|
}
|
|
95
|
-
export const OPERATION_SPECS = [SENDER_LIMITS_SET_OPERATION, CONNECTOR_UPDATE_OPERATION, CONNECTOR_EVENTS_REVOKE_OPERATION, WORKSPACE_PAGES_GET_OPERATION, WORKSPACE_PAGES_UPDATE_OPERATION, TABLE_FEED_UPDATE_OPERATION, ...CUSTOMER_OPERATIONS, ...GEO_OPERATIONS, ...BROWSER_OPERATIONS, ...AGENT_SESSION_OPERATIONS, ...GUIDED_ACTIVATION_OPERATIONS, ...TEAMMATE_OPERATIONS, ...WORKSPACE_SKILL_OPERATIONS];
|
|
96
|
-
export const CLI_OPERATIONS = [...CUSTOMER_OPERATIONS, ...GEO_OPERATIONS, ...BROWSER_OPERATIONS, ...AGENT_SESSION_OPERATIONS, ...GUIDED_ACTIVATION_OPERATIONS, ...TEAMMATE_OPERATIONS, ...WORKSPACE_SKILL_OPERATIONS].filter((spec) => spec.generated?.cli !== false);
|
|
97
|
-
export const MCP_OPERATIONS = [...CUSTOMER_OPERATIONS, ...GEO_OPERATIONS, ...BROWSER_OPERATIONS, ...AGENT_SESSION_OPERATIONS, ...GUIDED_ACTIVATION_OPERATIONS, ...TEAMMATE_OPERATIONS, ...WORKSPACE_SKILL_OPERATIONS].filter((spec) => spec.generated?.mcp !== false);
|
|
100
|
+
export const OPERATION_SPECS = [SENDER_LIMITS_SET_OPERATION, CONNECTOR_UPDATE_OPERATION, CONNECTOR_EVENTS_REVOKE_OPERATION, WORKSPACE_PAGES_GET_OPERATION, WORKSPACE_PAGES_UPDATE_OPERATION, TABLE_FEED_UPDATE_OPERATION, ...CUSTOMER_OPERATIONS, ...GEO_OPERATIONS, ...BROWSER_OPERATIONS, ...AGENT_SESSION_OPERATIONS, ...GUIDED_ACTIVATION_OPERATIONS, ...GUIDED_ONBOARDING_JOURNEY_OPERATIONS, ...GUIDED_FIRST_SOURCING_OPERATIONS, GUIDED_COMPANY_FILE_UPLOAD_OPERATION, ...TEAMMATE_OPERATIONS, ...WORKSPACE_SKILL_OPERATIONS];
|
|
101
|
+
export const CLI_OPERATIONS = [...CUSTOMER_OPERATIONS, ...GEO_OPERATIONS, ...BROWSER_OPERATIONS, ...AGENT_SESSION_OPERATIONS, ...GUIDED_ACTIVATION_OPERATIONS, ...GUIDED_ONBOARDING_JOURNEY_OPERATIONS, ...GUIDED_FIRST_SOURCING_OPERATIONS, GUIDED_COMPANY_FILE_UPLOAD_OPERATION, ...TEAMMATE_OPERATIONS, ...WORKSPACE_SKILL_OPERATIONS].filter((spec) => spec.generated?.cli !== false);
|
|
102
|
+
export const MCP_OPERATIONS = [...CUSTOMER_OPERATIONS, ...GEO_OPERATIONS, ...BROWSER_OPERATIONS, ...AGENT_SESSION_OPERATIONS, ...GUIDED_ACTIVATION_OPERATIONS, ...GUIDED_ONBOARDING_JOURNEY_OPERATIONS, ...GUIDED_FIRST_SOURCING_OPERATIONS, GUIDED_COMPANY_FILE_UPLOAD_OPERATION, ...TEAMMATE_OPERATIONS, ...WORKSPACE_SKILL_OPERATIONS].filter((spec) => spec.generated?.mcp !== false);
|
|
@@ -11,4 +11,6 @@ export declare const TABLE_QUERY_RAW_CONTRACT: import("./schema.js").ContractObj
|
|
|
11
11
|
export declare const TABLE_QUERY_CLI_DEFAULT_LIMIT = 20;
|
|
12
12
|
export declare const TABLE_QUERY_CLI_DEFAULT_MAX_CHARS = 12000;
|
|
13
13
|
export declare function tableQueryRawResultFits(data: Record<string, unknown>, maxChars: number | undefined): boolean;
|
|
14
|
+
/** What a page shortened to fit its result window says to do next (CLI, MCP and API alike). */
|
|
15
|
+
export declare const TABLE_QUERY_RESULT_WINDOW_NEXT_STEP: string;
|
|
14
16
|
export declare function tableQueryRawRecovery(table: string, row?: string): Record<string, unknown>;
|
|
@@ -9,6 +9,9 @@ export const TABLE_QUERY_CLI_DEFAULT_MAX_CHARS = 12_000;
|
|
|
9
9
|
export function tableQueryRawResultFits(data, maxChars) {
|
|
10
10
|
return maxChars === undefined || JSON.stringify(success("tables query", data)).length <= maxChars;
|
|
11
11
|
}
|
|
12
|
+
/** What a page shortened to fit its result window says to do next (CLI, MCP and API alike). */
|
|
13
|
+
export const TABLE_QUERY_RESULT_WINDOW_NEXT_STEP = "This page holds fewer rows than the limit so it fits the result window; every value is complete. "
|
|
14
|
+
+ "Continue with nextCursor (same table, filters, sorts and fields), or select fewer fields for more rows per page.";
|
|
12
15
|
export function tableQueryRawRecovery(table, row) {
|
|
13
16
|
return {
|
|
14
17
|
table,
|
|
@@ -53,20 +53,20 @@ export const TEAMMATE_OPERATIONS = [
|
|
|
53
53
|
// Adding a teammate (phase 7): a custom Agent that can chat at once and run
|
|
54
54
|
// on its own, under a policy that asks before any write, send or spend. As in
|
|
55
55
|
// Grok Bot, it can start as just "New Bot" and learn its job in the chat.
|
|
56
|
-
teammateOperation("teammates.create", "create", "POST", "", `Add an Oxygent to the workspace, named "${NEW_TEAMMATE_NAME}" unless you name it; tell it its job in your chat with it. It can chat at once and run on its own (background tasks, routines), reading freely and asking before anything that writes, sends or spends. You own it. Workspace access and a shared chat are the default; choose personal for creator-only access. Free: adding one starts no inference.`, {
|
|
56
|
+
teammateOperation("teammates.create", "create", "POST", "", `Add an Oxygent to the workspace, named "${NEW_TEAMMATE_NAME}" unless you name it; tell it its job in your chat with it. It can chat at once and run on its own (background tasks, routines), reading freely and asking before anything that writes, sends or spends. You own it. Workspace access as collaborators and a shared chat are the default; choose personal for creator-only access. Free: adding one starts no inference.`, {
|
|
57
57
|
...TEAMMATE_PROFILE_FIELDS,
|
|
58
|
-
visibility: enumOf(["workspace", "personal"], { description: "Workspace (default) shares one chat history with everyone who has access. Personal is accessible only to its creator here, including against workspace admins. An API key acts as the person who issued it." }),
|
|
58
|
+
visibility: enumOf(["workspace", "personal"], { description: "Workspace (default) shares one chat history with everyone who has access. Personal is accessible only to its creator here, including against workspace admins. Explicitly saving to team memory publishes that entry to the workspace. An API key acts as the person who issued it." }),
|
|
59
59
|
slug: str({ minLength: 2, maxLength: 49, pattern: TEAMMATE_SLUG_PATTERN, description: "Its permanent handle in commands and links; made from the name when omitted." }),
|
|
60
60
|
}),
|
|
61
61
|
teammateOperation("teammates.get", "get", "GET", "/{slug}", "Read one Oxygent's profile and its ongoing chat and visibility."),
|
|
62
|
-
teammateOperation("teammates.update", "update", "PATCH", "/{slug}", "Edit an Oxygent's profile as its owner
|
|
62
|
+
teammateOperation("teammates.update", "update", "PATCH", "/{slug}", "Edit an Oxygent's profile, or change visibility as its owner with confirm_visibility. Owners, managers and workspace admins can edit profiles. History is kept in both directions: Workspace shares it all; Personal removes others' future access but cannot retract what they saw. Home (the first Oxygent) stays Workspace. Free.", { ...TEAMMATE_PROFILE_FIELDS, visibility: enumOf(["workspace", "personal"], { description: "Only the owner can change visibility. Workspace grants everyone collaborator access and shares the complete history; Personal retains all messages, removes additional grants and everyone else's future access. Explicitly saving to team memory publishes that entry to the workspace. Home (the first Oxygent) stays Workspace." }), confirm_visibility: bool({ description: "Required when changing visibility: publishing shares all history; making personal removes everyone else's future access but cannot retract what they saw." }) }),
|
|
63
63
|
// Sharing (Tim, 2026-10-14: "make agents accessible to your team mates"), the
|
|
64
64
|
// way Grok Bot shares a Bot: everyone in the workspace, or people by email,
|
|
65
65
|
// each as a collaborator or a manager, with one ongoing shared chat.
|
|
66
|
-
teammateOperation("teammates.access", "access", "GET", "/{slug}/access", "Read access (visibility, owner, everyone, named people with roles, fixed and your permissions), without chat history. A collaborator uses its shared chat; a manager also changes its profile and access. Personal is creator-only; Home is fixed. Free."),
|
|
66
|
+
teammateOperation("teammates.access", "access", "GET", "/{slug}/access", "Read access (visibility, owner, everyone, named people with roles, fixed and your permissions), without chat history. A collaborator uses its shared chat; a manager also changes its profile and access. Personal is creator-only; Home (the first Oxygent) is fixed. Free."),
|
|
67
67
|
teammateOperation("teammates.share", "share", "POST", "/{slug}/access", "Share an Oxygent with teammates, or stop sharing it: with everyone in the workspace (everyone), or with people by their workspace email (add, as role; remove). Everyone with access reads the same chat history. Personal Oxygents cannot be shared; change visibility explicitly first. Its owner, its managers or a workspace admin only; never the first Oxygent, which is everyone's. Free.", {
|
|
68
68
|
everyone: enumOf([...TEAMMATE_SHARE_ROLES, "off"], {
|
|
69
|
-
description: "Workspace-wide role: collaborator, manager, or off (owner and named people, with workspace admins retaining management access). This restricts a Workspace Oxygent's audience; it does not make it Personal or split its shared chat.",
|
|
69
|
+
description: "Workspace-wide role: collaborator (the default), manager, or off (owner and named people, with workspace admins retaining management access). This restricts a Workspace Oxygent's audience; it does not make it Personal or split its shared chat.",
|
|
70
70
|
}),
|
|
71
71
|
add: arr(str({ minLength: 3, maxLength: 320 }), { maxItems: 50, description: "People to share it with, by their workspace email." }),
|
|
72
72
|
role: enumOf([...TEAMMATE_SHARE_ROLES], {
|
|
@@ -80,7 +80,7 @@ export const TEAMMATE_OPERATIONS = [
|
|
|
80
80
|
// Copy and delete (Tim, 2026-10-13: "CRUD actions ... through the right
|
|
81
81
|
// click"; "'delete' should be 'full delete'"), as Grok Bot's Bot menu has.
|
|
82
82
|
teammateOperation("teammates.duplicate", "duplicate", "POST", "/{slug}/duplicate", `Copy an Oxygent as a new one you own, named "<name> copy" unless you name it: its job, look, instructions, skills and tool settings, but not its chats, memory, routines or who it is shared with. The copy defaults to Workspace; choose Personal for creator-only access. Free.`, {
|
|
83
|
-
visibility: enumOf(["workspace", "personal"], { description: "Visibility of the copy; Workspace by default. No history is copied." }),
|
|
83
|
+
visibility: enumOf(["workspace", "personal"], { description: "Visibility of the copy; Workspace by default. No history is copied. Explicitly saving to team memory publishes that entry to the workspace." }),
|
|
84
84
|
name: str({
|
|
85
85
|
minLength: 1, maxLength: TEAMMATE_NAME_MAX_CHARS,
|
|
86
86
|
description: "What everyone calls the copy. Its slug is made from the name, and a deleted Oxygent's slug stays taken, so a copy can get a numbered one (scout-2).",
|
|
@@ -115,7 +115,7 @@ export const TEAMMATE_OPERATIONS = [
|
|
|
115
115
|
teammateOperation("teammates.memory", "memory", "GET", "/{slug}/memory", "List what the team asked an Oxygent to remember. Every chat with it reads this; it is a Knowledge page."),
|
|
116
116
|
teammateOperation("teammates.remember", "remember", "POST", "/{slug}/memory", "Add one thing to an Oxygent's team memory: every chat with it reads it, and the whole workspace can see it. Free.", { text: str({ minLength: 1, maxLength: 500 }) }, ["text"]),
|
|
117
117
|
teammateOperation("teammates.forget", "forget", "DELETE", "/{slug}/memory/{entry_id}", "Remove one entry from an Oxygent's team memory."),
|
|
118
|
-
teammateOperation("teammates.notes", "notes", "GET", "/{slug}/notes", "Read what an Oxygent keeps about you
|
|
118
|
+
teammateOperation("teammates.notes", "notes", "GET", "/{slug}/notes", "Read what an Oxygent keeps about you. Private to you; never included in shared Workspace or multi-person group chats."),
|
|
119
119
|
teammateOperation("teammates.notes_set", "notes_set", "POST", "/{slug}/notes", "Replace what an Oxygent keeps about you; empty clears it. Private to you.", { notes: str({ maxLength: 4000 }) }, ["notes"]),
|
|
120
120
|
teammateOperation("teammates.activity", "activity", "GET", "/{slug}/activity", "List an Oxygent's recent work: its runs and the actions taken in its chats, as metadata only (no goals or messages)."),
|
|
121
121
|
teammateGroupOperation("teammate_groups.list", "groups", "GET", "", "List the group chats you are in: name, Oxygents, people (each an admin or member) and the chat (working or waiting for you, unread, the latest words and who said them). Candidates are the teammates a new group chat could bring in."),
|
|
@@ -37,6 +37,8 @@ export type EmailTrackingTokenPayload = {
|
|
|
37
37
|
enr?: string;
|
|
38
38
|
/** Issued-at (epoch ms), recorded as provenance. */
|
|
39
39
|
iat: number;
|
|
40
|
+
/** Diagnostic copy test: preserve transport rendering without campaign analytics. */
|
|
41
|
+
diagnostic?: true;
|
|
40
42
|
};
|
|
41
43
|
/** SHA-256 hex of a destination URL — the value bound into a click token's `uh`. */
|
|
42
44
|
export declare function hashTrackingUrl(url: string): string;
|
|
@@ -45,6 +45,7 @@ function parseTokenPayload(body) {
|
|
|
45
45
|
...(uh ? { uh } : {}),
|
|
46
46
|
...(typeof candidate.seq === "string" && candidate.seq ? { seq: candidate.seq } : {}),
|
|
47
47
|
...(typeof candidate.enr === "string" && candidate.enr ? { enr: candidate.enr } : {}),
|
|
48
|
+
...(candidate.diagnostic === true ? { diagnostic: true } : {}),
|
|
48
49
|
iat: typeof candidate.iat === "number" && Number.isFinite(candidate.iat) ? candidate.iat : 0,
|
|
49
50
|
};
|
|
50
51
|
}
|
|
@@ -76,6 +76,52 @@ export declare function normalizeRowsForNewTable(rows: Record<string, unknown>[]
|
|
|
76
76
|
*/
|
|
77
77
|
export declare function dropBlankImportCells(row: Record<string, unknown>, keepKey?: string | null): Record<string, unknown>;
|
|
78
78
|
export declare function normalizeImportColumnKey(value: string): string;
|
|
79
|
+
/**
|
|
80
|
+
* Read `ignore_source_keys`: the headers a mapping step set to "Don't import".
|
|
81
|
+
* A JSON array (staged body) or its JSON text (multipart form field); headers
|
|
82
|
+
* are matched verbatim, so they are not trimmed. Anything malformed reads as
|
|
83
|
+
* no choice at all, never as a reason to fail the import.
|
|
84
|
+
*/
|
|
85
|
+
export declare function readImportHeaderList(value: unknown): string[];
|
|
86
|
+
/** A column of the table an import writes into. */
|
|
87
|
+
export type ImportTargetColumn = {
|
|
88
|
+
key: string;
|
|
89
|
+
label?: string | null;
|
|
90
|
+
};
|
|
91
|
+
/** Why a file column was not written. */
|
|
92
|
+
export type IgnoredImportHeaderReason = "not_selected" | "no_matching_column" | "duplicate_target" | "system_column";
|
|
93
|
+
export type IgnoredImportHeader = {
|
|
94
|
+
header: string;
|
|
95
|
+
reason: IgnoredImportHeaderReason;
|
|
96
|
+
column?: string;
|
|
97
|
+
};
|
|
98
|
+
/**
|
|
99
|
+
* Decide, once per file header, which column of an EXISTING table it lands in.
|
|
100
|
+
*
|
|
101
|
+
* File imports used to hand every header to the row writer verbatim, and the
|
|
102
|
+
* writer refuses an unknown key for the whole chunk. A standard export carries
|
|
103
|
+
* columns the table does not have — Baserow puts an `id` first — so on
|
|
104
|
+
* 2026-10-12 and 2026-10-15 three workspaces' CRM imports wrote nothing at all
|
|
105
|
+
* and showed only `invalid_row_values`. Headers now resolve in a fixed order:
|
|
106
|
+
* an explicit choice (`explicitMap`, `ignore`) first, then the column key, then
|
|
107
|
+
* the column's label or the normalized key. A header that resolves to no
|
|
108
|
+
* column, or to a column another header already fills, is left out and
|
|
109
|
+
* reported, never guessed: two headers writing one column would overwrite each
|
|
110
|
+
* other. Rows keep their cells; only the keys are renamed or dropped.
|
|
111
|
+
*/
|
|
112
|
+
export declare function createImportHeaderProjector(input: {
|
|
113
|
+
columns: readonly ImportTargetColumn[];
|
|
114
|
+
explicitMap?: Readonly<Record<string, string>> | null;
|
|
115
|
+
ignore?: readonly string[] | null;
|
|
116
|
+
}): {
|
|
117
|
+
/** Resolve any headers first seen in these rows; earlier decisions stand. */
|
|
118
|
+
observe(rows: readonly Record<string, unknown>[]): void;
|
|
119
|
+
/** The row with every key renamed to its column, unwritten headers dropped. */
|
|
120
|
+
project(row: Record<string, unknown>): Record<string, unknown>;
|
|
121
|
+
/** header → column for every header that writes; for a preview or a receipt. */
|
|
122
|
+
mapping(): Record<string, string>;
|
|
123
|
+
ignored(): IgnoredImportHeader[];
|
|
124
|
+
};
|
|
79
125
|
/**
|
|
80
126
|
* Refuse a JSON or XLSX file over the buffered-parse ceiling before anything is
|
|
81
127
|
* uploaded or queued. CSV and JSONL stream, so they pass at any size and only
|
|
@@ -236,6 +236,144 @@ export function dropBlankImportCells(row, keepKey) {
|
|
|
236
236
|
export function normalizeImportColumnKey(value) {
|
|
237
237
|
return toSnakeIdentifier(value, "column");
|
|
238
238
|
}
|
|
239
|
+
/**
|
|
240
|
+
* Read `ignore_source_keys`: the headers a mapping step set to "Don't import".
|
|
241
|
+
* A JSON array (staged body) or its JSON text (multipart form field); headers
|
|
242
|
+
* are matched verbatim, so they are not trimmed. Anything malformed reads as
|
|
243
|
+
* no choice at all, never as a reason to fail the import.
|
|
244
|
+
*/
|
|
245
|
+
export function readImportHeaderList(value) {
|
|
246
|
+
let parsed = value;
|
|
247
|
+
if (typeof value === "string") {
|
|
248
|
+
try {
|
|
249
|
+
parsed = JSON.parse(value);
|
|
250
|
+
}
|
|
251
|
+
catch {
|
|
252
|
+
return [];
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
if (!Array.isArray(parsed))
|
|
256
|
+
return [];
|
|
257
|
+
return parsed.filter((entry) => typeof entry === "string" && entry.length > 0).slice(0, 1_000);
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Decide, once per file header, which column of an EXISTING table it lands in.
|
|
261
|
+
*
|
|
262
|
+
* File imports used to hand every header to the row writer verbatim, and the
|
|
263
|
+
* writer refuses an unknown key for the whole chunk. A standard export carries
|
|
264
|
+
* columns the table does not have — Baserow puts an `id` first — so on
|
|
265
|
+
* 2026-10-12 and 2026-10-15 three workspaces' CRM imports wrote nothing at all
|
|
266
|
+
* and showed only `invalid_row_values`. Headers now resolve in a fixed order:
|
|
267
|
+
* an explicit choice (`explicitMap`, `ignore`) first, then the column key, then
|
|
268
|
+
* the column's label or the normalized key. A header that resolves to no
|
|
269
|
+
* column, or to a column another header already fills, is left out and
|
|
270
|
+
* reported, never guessed: two headers writing one column would overwrite each
|
|
271
|
+
* other. Rows keep their cells; only the keys are renamed or dropped.
|
|
272
|
+
*/
|
|
273
|
+
export function createImportHeaderProjector(input) {
|
|
274
|
+
const exact = new Map();
|
|
275
|
+
const aliases = new Map();
|
|
276
|
+
for (const column of input.columns) {
|
|
277
|
+
if (!column.key || column.key.startsWith("_"))
|
|
278
|
+
continue;
|
|
279
|
+
exact.set(column.key, column.key);
|
|
280
|
+
for (const alias of [
|
|
281
|
+
column.label?.trim(),
|
|
282
|
+
normalizeImportColumnKey(column.key),
|
|
283
|
+
column.label ? normalizeImportColumnKey(column.label) : undefined,
|
|
284
|
+
]) {
|
|
285
|
+
if (!alias)
|
|
286
|
+
continue;
|
|
287
|
+
const existing = aliases.get(alias);
|
|
288
|
+
if (existing === undefined)
|
|
289
|
+
aliases.set(alias, column.key);
|
|
290
|
+
else if (existing !== column.key)
|
|
291
|
+
aliases.set(alias, null);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
const explicitMap = input.explicitMap ?? {};
|
|
295
|
+
const ignore = new Set(input.ignore ?? []);
|
|
296
|
+
const keyByHeader = new Map();
|
|
297
|
+
const ignored = new Map();
|
|
298
|
+
const claimed = new Set();
|
|
299
|
+
const decide = (headers) => {
|
|
300
|
+
const pending = headers.filter((header) => !keyByHeader.has(header) && !ignored.has(header));
|
|
301
|
+
const aliasCandidates = new Map();
|
|
302
|
+
const skip = (header, reason, column) => ignored.set(header, { header, reason, ...(column ? { column } : {}) });
|
|
303
|
+
const claim = (header, key) => {
|
|
304
|
+
keyByHeader.set(header, key);
|
|
305
|
+
claimed.add(key);
|
|
306
|
+
};
|
|
307
|
+
// Explicit choices and exact keys claim their columns before any alias can.
|
|
308
|
+
for (const header of pending) {
|
|
309
|
+
if (ignore.has(header)) {
|
|
310
|
+
skip(header, "not_selected");
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
const explicit = explicitMap[header];
|
|
314
|
+
if (explicit) {
|
|
315
|
+
claim(header, explicit);
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
const key = exact.get(header) ?? exact.get(header.trim());
|
|
319
|
+
if (key && !claimed.has(key))
|
|
320
|
+
claim(header, key);
|
|
321
|
+
}
|
|
322
|
+
for (const header of pending) {
|
|
323
|
+
if (keyByHeader.has(header) || ignored.has(header))
|
|
324
|
+
continue;
|
|
325
|
+
const exactKey = exact.get(header) ?? exact.get(header.trim());
|
|
326
|
+
if (exactKey) {
|
|
327
|
+
skip(header, "duplicate_target", exactKey);
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
// `_created_at` must not alias `created_at`: system keys are never written.
|
|
331
|
+
if (header.startsWith("_")) {
|
|
332
|
+
skip(header, "system_column");
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
const alias = aliases.get(header.trim()) ?? aliases.get(normalizeImportColumnKey(header)) ?? null;
|
|
336
|
+
if (!alias) {
|
|
337
|
+
skip(header, "no_matching_column");
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
aliasCandidates.set(alias, [...(aliasCandidates.get(alias) ?? []), header]);
|
|
341
|
+
}
|
|
342
|
+
for (const [key, candidates] of aliasCandidates) {
|
|
343
|
+
if (candidates.length === 1 && !claimed.has(key)) {
|
|
344
|
+
claim(candidates[0], key);
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
for (const header of candidates)
|
|
348
|
+
skip(header, "duplicate_target", key);
|
|
349
|
+
}
|
|
350
|
+
};
|
|
351
|
+
return {
|
|
352
|
+
/** Resolve any headers first seen in these rows; earlier decisions stand. */
|
|
353
|
+
observe(rows) {
|
|
354
|
+
decide(inferImportColumnLabels(rows));
|
|
355
|
+
},
|
|
356
|
+
/** The row with every key renamed to its column, unwritten headers dropped. */
|
|
357
|
+
project(row) {
|
|
358
|
+
const projected = {};
|
|
359
|
+
for (const [header, value] of Object.entries(row)) {
|
|
360
|
+
if (!keyByHeader.has(header) && !ignored.has(header))
|
|
361
|
+
decide([header]);
|
|
362
|
+
const key = keyByHeader.get(header);
|
|
363
|
+
if (key !== undefined)
|
|
364
|
+
projected[key] = value;
|
|
365
|
+
}
|
|
366
|
+
return projected;
|
|
367
|
+
},
|
|
368
|
+
/** header → column for every header that writes; for a preview or a receipt. */
|
|
369
|
+
mapping() {
|
|
370
|
+
return Object.fromEntries(keyByHeader);
|
|
371
|
+
},
|
|
372
|
+
ignored() {
|
|
373
|
+
return [...ignored.values()];
|
|
374
|
+
},
|
|
375
|
+
};
|
|
376
|
+
}
|
|
239
377
|
async function parseXlsxRows(buffer, options) {
|
|
240
378
|
const rows = await parseXlsxWorkbookBuffer(buffer, options);
|
|
241
379
|
const [header, ...body] = rows;
|
|
@@ -8,6 +8,14 @@ export type GuidedActivationRoutine = {
|
|
|
8
8
|
export declare const GUIDED_ACTIVATION_POINTERS: readonly ["knowledge", "first_leads_table", "sequence", "sender", "oxygent", "direct_chat", "scan_routine", "briefing_routine"];
|
|
9
9
|
export type GuidedActivationPointer = typeof GUIDED_ACTIVATION_POINTERS[number];
|
|
10
10
|
export type GuidedActivationResources = Partial<Record<GuidedActivationPointer, string>>;
|
|
11
|
+
/** Read projection only. Historical resource/Start identities stay unchanged. */
|
|
12
|
+
export type GuidedActivationConversation = {
|
|
13
|
+
source_chat_id: string;
|
|
14
|
+
canonical_chat_id: string;
|
|
15
|
+
oxygent_id: string;
|
|
16
|
+
visibility: "workspace" | "personal";
|
|
17
|
+
history_migration_pending: boolean;
|
|
18
|
+
};
|
|
11
19
|
export declare const GUIDED_ACTIVATION_REVISIONS: readonly ["configuration", "sequence", "audience", "recipients", "sender", "copy", "knowledge", "schedule", "authority"];
|
|
12
20
|
export type GuidedActivationRevisions = Record<typeof GUIDED_ACTIVATION_REVISIONS[number], string>;
|
|
13
21
|
export type GuidedActivationBounds = {
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** Browser-safe budgets for the native guided company-file owner and all transports. */
|
|
2
|
+
export declare const GUIDED_COMPANY_FILE_LIMITS: {
|
|
3
|
+
readonly textBytes: 1500000;
|
|
4
|
+
readonly pdfBytes: 5242880;
|
|
5
|
+
readonly retainedWorkspaceBytes: 1073741824;
|
|
6
|
+
readonly bodyTimeoutMs: 15000;
|
|
7
|
+
readonly storageTimeoutMs: 15000;
|
|
8
|
+
readonly bodyLimitBytes: 5308416;
|
|
9
|
+
readonly nameBytes: 255;
|
|
10
|
+
};
|
|
11
|
+
export declare const GUIDED_COMPANY_FILE_CONTENT_TYPES: readonly ["text/plain", "text/markdown", "application/pdf"];
|
|
12
|
+
export type GuidedCompanyFileContentType = typeof GUIDED_COMPANY_FILE_CONTENT_TYPES[number];
|
|
13
|
+
export declare function guidedCompanyFileByteLimit(contentType: GuidedCompanyFileContentType): number;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Browser-safe budgets for the native guided company-file owner and all transports. */
|
|
2
|
+
export const GUIDED_COMPANY_FILE_LIMITS = {
|
|
3
|
+
textBytes: 1_500_000,
|
|
4
|
+
pdfBytes: 5_242_880,
|
|
5
|
+
retainedWorkspaceBytes: 1_073_741_824,
|
|
6
|
+
bodyTimeoutMs: 15_000,
|
|
7
|
+
storageTimeoutMs: 15_000,
|
|
8
|
+
bodyLimitBytes: 5_308_416,
|
|
9
|
+
nameBytes: 255,
|
|
10
|
+
};
|
|
11
|
+
export const GUIDED_COMPANY_FILE_CONTENT_TYPES = ["text/plain", "text/markdown", "application/pdf"];
|
|
12
|
+
export function guidedCompanyFileByteLimit(contentType) {
|
|
13
|
+
return contentType === "application/pdf" ? GUIDED_COMPANY_FILE_LIMITS.pdfBytes : GUIDED_COMPANY_FILE_LIMITS.textBytes;
|
|
14
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import type { GuidedOnboardingAnswers, GuidedOnboardingProspectLimitation } from "./guided-onboarding-journey.js";
|
|
2
|
+
export type GuidedFirstSourcingOwner = {
|
|
3
|
+
organizationId: string;
|
|
4
|
+
actorId: string;
|
|
5
|
+
activationId: string;
|
|
6
|
+
};
|
|
7
|
+
export type GuidedFirstSourcingBounds = {
|
|
8
|
+
targetCount: number;
|
|
9
|
+
maxPages: number;
|
|
10
|
+
maxCredits: number;
|
|
11
|
+
};
|
|
12
|
+
export type GuidedFirstSourcingCriteria = {
|
|
13
|
+
sourceKey: string;
|
|
14
|
+
digest: string;
|
|
15
|
+
company: NonNullable<GuidedOnboardingAnswers["company_review"]>;
|
|
16
|
+
targeting: {
|
|
17
|
+
filters: Record<string, unknown>;
|
|
18
|
+
signals: GuidedOnboardingAnswers["signals"];
|
|
19
|
+
limitations: GuidedOnboardingProspectLimitation[];
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
export type GuidedFirstSourcingQuote = {
|
|
23
|
+
toolId: "blitzapi.people_search";
|
|
24
|
+
perPageCredits: number;
|
|
25
|
+
revision: string;
|
|
26
|
+
canRun: boolean;
|
|
27
|
+
};
|
|
28
|
+
export type GuidedFirstSourcingPreview = {
|
|
29
|
+
id: string;
|
|
30
|
+
activationId: string;
|
|
31
|
+
actorId: string;
|
|
32
|
+
activationRevision: number;
|
|
33
|
+
tableId: string;
|
|
34
|
+
criteria: GuidedFirstSourcingCriteria;
|
|
35
|
+
bounds: GuidedFirstSourcingBounds;
|
|
36
|
+
quote: GuidedFirstSourcingQuote & {
|
|
37
|
+
pages: number;
|
|
38
|
+
estimatedCredits: number;
|
|
39
|
+
};
|
|
40
|
+
expiresAt: string;
|
|
41
|
+
digest: string;
|
|
42
|
+
};
|
|
43
|
+
/** Current authority witness, distinct from the provider query's stable identity.
|
|
44
|
+
* Confirmation revisions, journey generation, navigation and preview/cap expiry
|
|
45
|
+
* cannot turn the same approved native source into another first pull. */
|
|
46
|
+
export declare function guidedFirstSourcingAudienceWitness(answers: GuidedOnboardingAnswers): {
|
|
47
|
+
company: string | null;
|
|
48
|
+
company_size: import("./guided-onboarding-journey.js").GuidedOnboardingChoice | null;
|
|
49
|
+
markets: import("./guided-onboarding-journey.js").GuidedOnboardingChoice | null;
|
|
50
|
+
industries: import("./guided-onboarding-journey.js").GuidedOnboardingChoice | null;
|
|
51
|
+
personas: import("./guided-onboarding-journey.js").GuidedOnboardingChoice | null;
|
|
52
|
+
signals: {
|
|
53
|
+
hiring: import("./guided-onboarding-journey.js").GuidedOnboardingSignal;
|
|
54
|
+
technology: import("./guided-onboarding-journey.js").GuidedOnboardingTechnologySignal;
|
|
55
|
+
} | null;
|
|
56
|
+
};
|
|
57
|
+
/** Optional GET scope for a current native NIGHTLY quote. The timezone is a
|
|
58
|
+
* customer/browser IANA suggestion; it never confirms or saves a schedule. */
|
|
59
|
+
export type GuidedFirstSourcingDefaultsRequest = {
|
|
60
|
+
revision: number;
|
|
61
|
+
generation: number;
|
|
62
|
+
timezoneHint: string;
|
|
63
|
+
};
|
|
64
|
+
/** Structural projection of E's actual readonly producer; no native import in shared. */
|
|
65
|
+
export type GuidedFirstSourcingRoutineDefaults = {
|
|
66
|
+
initial: {
|
|
67
|
+
sourceIds: string[];
|
|
68
|
+
limits: {
|
|
69
|
+
max_examined: number;
|
|
70
|
+
max_new_leads: number;
|
|
71
|
+
max_provider_pages: number;
|
|
72
|
+
max_provider_credits: number;
|
|
73
|
+
};
|
|
74
|
+
scanBudget: {
|
|
75
|
+
per_run_credits: number;
|
|
76
|
+
daily_credits: number;
|
|
77
|
+
};
|
|
78
|
+
briefingBudget: {
|
|
79
|
+
per_run_credits: number;
|
|
80
|
+
daily_credits: number;
|
|
81
|
+
};
|
|
82
|
+
};
|
|
83
|
+
sourceNames: Record<string, string>;
|
|
84
|
+
quote: {
|
|
85
|
+
provider_credits_per_run: number;
|
|
86
|
+
inference_credits_per_run: 0;
|
|
87
|
+
source_quotes: Array<{
|
|
88
|
+
source_id: string;
|
|
89
|
+
provider_credits_per_page: number;
|
|
90
|
+
incremental_mode: "cursor" | "watermark";
|
|
91
|
+
}>;
|
|
92
|
+
};
|
|
93
|
+
};
|
|
94
|
+
export type GuidedFirstSourcingRoutineDefaultsScope = GuidedFirstSourcingOwner & GuidedFirstSourcingDefaultsRequest & {
|
|
95
|
+
sourceKey: string;
|
|
96
|
+
};
|
|
97
|
+
/** Observation metadata stays outside form defaults. Quoted time alone must
|
|
98
|
+
* never reset edits; consumers reject responses outside their current scope. */
|
|
99
|
+
export type GuidedFirstSourcingRoutineDefaultsReadback = {
|
|
100
|
+
routineDefaults: null;
|
|
101
|
+
routineDefaultsScope: null;
|
|
102
|
+
} | {
|
|
103
|
+
routineDefaults: null;
|
|
104
|
+
routineDefaultsScope: GuidedFirstSourcingRoutineDefaultsScope & {
|
|
105
|
+
state: "not_ready";
|
|
106
|
+
};
|
|
107
|
+
} | {
|
|
108
|
+
routineDefaults: GuidedFirstSourcingRoutineDefaults;
|
|
109
|
+
routineDefaultsScope: GuidedFirstSourcingRoutineDefaultsScope & {
|
|
110
|
+
state: "ready";
|
|
111
|
+
quoteDigest: string;
|
|
112
|
+
quotedAt: string;
|
|
113
|
+
defaultMaxCredits: number;
|
|
114
|
+
defaultsDigest: string;
|
|
115
|
+
};
|
|
116
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Current authority witness, distinct from the provider query's stable identity.
|
|
2
|
+
* Confirmation revisions, journey generation, navigation and preview/cap expiry
|
|
3
|
+
* cannot turn the same approved native source into another first pull. */
|
|
4
|
+
export function guidedFirstSourcingAudienceWitness(answers) {
|
|
5
|
+
return {
|
|
6
|
+
company: answers.company_review?.statement ?? null,
|
|
7
|
+
company_size: answers.audience_company_size ?? null, markets: answers.audience_markets ?? null,
|
|
8
|
+
industries: answers.audience_industries ?? null, personas: answers.audience_personas ?? null,
|
|
9
|
+
signals: answers.signals ?? null,
|
|
10
|
+
};
|
|
11
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { GuidedOnboardingAnswers, GuidedOnboardingStep } from "./guided-onboarding-journey.js";
|
|
2
|
+
/** Only the current step can be edited, never native billing/resources/research.
|
|
3
|
+
* Validation does not claim a file is owned, a connection exists or results ran. */
|
|
4
|
+
export declare function normalizeGuidedOnboardingAnswers(step: GuidedOnboardingStep, input: unknown): GuidedOnboardingAnswers;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { OxygenError } from "./cli-result.js";
|
|
2
|
+
import { validateContract } from "./contracts/schema.js";
|
|
3
|
+
import { GUIDED_ONBOARDING_ANSWERS_CONTRACT } from "./contracts/guided-onboarding-journey.js";
|
|
4
|
+
import { normalizeGuidedOnboardingCompanyContext } from "./guided-onboarding-company-context.js";
|
|
5
|
+
import { normalizeGuidedOnboardingSignals } from "./guided-onboarding-signals.js";
|
|
6
|
+
const fail = (message) => { throw new OxygenError("invalid_onboarding_answer", message, { exitCode: 2 }); };
|
|
7
|
+
const clean = (value) => value.normalize("NFKC").trim().replace(/\s+/gu, " ");
|
|
8
|
+
const choices = (values) => {
|
|
9
|
+
const names = values.map(clean);
|
|
10
|
+
if (names.some(value => !value))
|
|
11
|
+
return fail("Choose a nonempty audience value.");
|
|
12
|
+
return [...new Map(names.map(value => [value.toLowerCase(), value])).values()];
|
|
13
|
+
};
|
|
14
|
+
/** Only the current step can be edited, never native billing/resources/research.
|
|
15
|
+
* Validation does not claim a file is owned, a connection exists or results ran. */
|
|
16
|
+
export function normalizeGuidedOnboardingAnswers(step, input) {
|
|
17
|
+
const checked = validateContract(GUIDED_ONBOARDING_ANSWERS_CONTRACT, input, { unknown: "reject", overMax: "reject" });
|
|
18
|
+
if (!checked.ok)
|
|
19
|
+
return fail("Use the reviewed answer fields for this onboarding step.");
|
|
20
|
+
const answers = checked.value;
|
|
21
|
+
if (Object.keys(answers).some(key => key !== step))
|
|
22
|
+
return fail("Save only the current step's answers.");
|
|
23
|
+
if (answers.company_context)
|
|
24
|
+
answers.company_context = normalizeGuidedOnboardingCompanyContext(answers.company_context);
|
|
25
|
+
if (answers.company_review) {
|
|
26
|
+
const statement = answers.company_review.statement.trim();
|
|
27
|
+
if (!statement || !/^(?:0|[1-9]\d*)$/.test(answers.company_review.profile_revision))
|
|
28
|
+
return fail("Review a company statement at the current native profile revision.");
|
|
29
|
+
answers.company_review = { ...answers.company_review, statement };
|
|
30
|
+
}
|
|
31
|
+
for (const key of ["audience_company_size", "audience_markets", "audience_industries", "audience_personas"]) {
|
|
32
|
+
if (answers[key])
|
|
33
|
+
answers[key] = { values: choices(answers[key].values) };
|
|
34
|
+
}
|
|
35
|
+
if (answers.signals)
|
|
36
|
+
answers.signals = normalizeGuidedOnboardingSignals(answers.signals);
|
|
37
|
+
if (answers.prospects)
|
|
38
|
+
answers.prospects = { ...answers.prospects, feedback: answers.prospects.feedback.trim() };
|
|
39
|
+
if (answers.tour)
|
|
40
|
+
answers.tour = { visited: [...new Set(answers.tour.visited)] };
|
|
41
|
+
return answers;
|
|
42
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { GuidedOnboardingAnswers } from "./guided-onboarding-journey.js";
|
|
2
|
+
/** One native-supported input budget for web/CLI/MCP and D research preparation. */
|
|
3
|
+
export declare const GUIDED_ONBOARDING_COMPANY_CONTEXT_LIMITS: {
|
|
4
|
+
readonly descriptionChars: 6000;
|
|
5
|
+
readonly contextLinks: 5;
|
|
6
|
+
readonly fileIds: 5;
|
|
7
|
+
readonly nameChars: 160;
|
|
8
|
+
readonly urlChars: 2000;
|
|
9
|
+
};
|
|
10
|
+
export declare const GUIDED_ONBOARDING_COMPANY_CONTEXT_CONTRACT: import("./contracts/schema.js").ContractObject<{
|
|
11
|
+
readonly website: {
|
|
12
|
+
oneOf: [import("./contracts/schema.js").StringProperty, import("./contracts/schema.js").NullProperty];
|
|
13
|
+
description?: string;
|
|
14
|
+
};
|
|
15
|
+
readonly name: import("./contracts/schema.js").StringProperty;
|
|
16
|
+
readonly description: import("./contracts/schema.js").StringProperty;
|
|
17
|
+
readonly context_links: import("./contracts/schema.js").ArrayProperty<import("./contracts/schema.js").StringProperty>;
|
|
18
|
+
readonly file_ids: import("./contracts/schema.js").ArrayProperty<import("./contracts/schema.js").StringProperty>;
|
|
19
|
+
}, "context_links" | "description" | "file_ids" | "name" | "website">;
|
|
20
|
+
/** Input-only rule; existing evidence links retain their independent navigation policy. */
|
|
21
|
+
export declare function normalizeGuidedOnboardingContextLink(value: string): string;
|
|
22
|
+
export declare function normalizeGuidedOnboardingCompanyContext(input: unknown): NonNullable<GuidedOnboardingAnswers["company_context"]>;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { OxygenError } from "./cli-result.js";
|
|
2
|
+
import { arr, nullable, obj, str, validateContract } from "./contracts/schema.js";
|
|
3
|
+
/** One native-supported input budget for web/CLI/MCP and D research preparation. */
|
|
4
|
+
export const GUIDED_ONBOARDING_COMPANY_CONTEXT_LIMITS = {
|
|
5
|
+
descriptionChars: 6000, contextLinks: 5, fileIds: 5, nameChars: 160, urlChars: 2000,
|
|
6
|
+
};
|
|
7
|
+
const limits = GUIDED_ONBOARDING_COMPANY_CONTEXT_LIMITS;
|
|
8
|
+
export const GUIDED_ONBOARDING_COMPANY_CONTEXT_CONTRACT = obj({
|
|
9
|
+
website: nullable(str({ maxLength: limits.urlChars })), name: str({ maxLength: limits.nameChars }),
|
|
10
|
+
description: str({ maxLength: limits.descriptionChars }),
|
|
11
|
+
context_links: arr(str({ minLength: 1, maxLength: limits.urlChars }), { maxItems: limits.contextLinks }),
|
|
12
|
+
file_ids: arr(str({ minLength: 1, maxLength: 160 }), { maxItems: limits.fileIds }),
|
|
13
|
+
}, { required: ["website", "name", "description", "context_links", "file_ids"] });
|
|
14
|
+
const fail = (message) => { throw new OxygenError("invalid_onboarding_answer", message, { exitCode: 2 }); };
|
|
15
|
+
/** Input-only rule; existing evidence links retain their independent navigation policy. */
|
|
16
|
+
export function normalizeGuidedOnboardingContextLink(value) {
|
|
17
|
+
try {
|
|
18
|
+
const url = new URL(value.includes("://") ? value : `https://${value}`);
|
|
19
|
+
if (url.protocol !== "https:" || url.username || url.password || !url.hostname.includes("."))
|
|
20
|
+
return fail("Use an HTTPS website or context link without credentials.");
|
|
21
|
+
return url.href;
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return fail("Use a valid HTTPS website or context link.");
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export function normalizeGuidedOnboardingCompanyContext(input) {
|
|
28
|
+
const checked = validateContract(GUIDED_ONBOARDING_COMPANY_CONTEXT_CONTRACT, input, { unknown: "reject", overMax: "reject" });
|
|
29
|
+
if (!checked.ok)
|
|
30
|
+
return fail("Use up to 6000 description characters, five context links and five Knowledge files.");
|
|
31
|
+
const value = checked.value;
|
|
32
|
+
const website = value.website?.trim() ? normalizeGuidedOnboardingContextLink(value.website.trim()) : null;
|
|
33
|
+
const description = value.description.trim();
|
|
34
|
+
if (!website && !description)
|
|
35
|
+
return fail("Provide a website or describe your company.");
|
|
36
|
+
return { website, name: value.name.normalize("NFKC").trim().replace(/\s+/gu, " "), description,
|
|
37
|
+
context_links: [...new Set(value.context_links.map(link => normalizeGuidedOnboardingContextLink(link.trim())))], file_ids: [...new Set(value.file_ids)] };
|
|
38
|
+
}
|