@oxygen-agent/cli 1.948.1 → 1.982.3
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/admin-primary-providers-render.js +9 -1
- package/dist/cli-values.d.ts +14 -0
- package/dist/cli-values.js +26 -0
- package/dist/command-manifest.js +6 -0
- package/dist/functions-commands.js +13 -5
- package/dist/help.js +1 -0
- package/dist/index.js +1171 -240
- package/dist/knowledge-repository-commands.d.ts +6 -0
- package/dist/knowledge-repository-commands.js +198 -0
- package/dist/skills.js +20 -0
- package/dist/ugc-commands.js +122 -8
- package/node_modules/@oxygen/recipe-sdk/dist/index.d.ts +2 -0
- package/node_modules/@oxygen/shared/dist/capability-discovery.d.ts +8 -0
- package/node_modules/@oxygen/shared/dist/capability-discovery.js +99 -15
- package/node_modules/@oxygen/shared/dist/copilot-errors.js +3 -0
- package/node_modules/@oxygen/shared/dist/copilot-journeys.d.ts +19 -1
- package/node_modules/@oxygen/shared/dist/copilot-journeys.generated.d.ts +19 -0
- package/node_modules/@oxygen/shared/dist/copilot-journeys.generated.js +26 -0
- package/node_modules/@oxygen/shared/dist/copilot-journeys.js +8 -41
- package/node_modules/@oxygen/shared/dist/inbox-avatar-url.d.ts +28 -0
- package/node_modules/@oxygen/shared/dist/inbox-avatar-url.js +57 -0
- package/node_modules/@oxygen/shared/dist/index.d.ts +4 -0
- package/node_modules/@oxygen/shared/dist/index.js +4 -0
- package/node_modules/@oxygen/shared/dist/knowledge-bases.d.ts +74 -0
- package/node_modules/@oxygen/shared/dist/knowledge-bases.js +456 -0
- package/node_modules/@oxygen/shared/dist/knowledge-bootstrap.d.ts +17 -38
- package/node_modules/@oxygen/shared/dist/knowledge-bootstrap.js +14 -39
- package/node_modules/@oxygen/shared/dist/knowledge-repository.d.ts +22 -0
- package/node_modules/@oxygen/shared/dist/knowledge-repository.js +121 -0
- package/node_modules/@oxygen/shared/dist/knowledge-vault-markdown.d.ts +20 -0
- package/node_modules/@oxygen/shared/dist/knowledge-vault-markdown.js +155 -0
- package/node_modules/@oxygen/shared/dist/mailbox-import.d.ts +10 -0
- package/node_modules/@oxygen/shared/dist/mailbox-import.js +53 -0
- package/node_modules/@oxygen/shared/dist/plan-limits.d.ts +8 -0
- package/node_modules/@oxygen/shared/dist/plan-limits.js +8 -0
- package/node_modules/@oxygen/shared/dist/pricing-sheet.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/pricing-sheet.js +1 -1
- package/node_modules/@oxygen/shared/dist/product-analytics-events.d.ts +24 -0
- package/node_modules/@oxygen/shared/dist/product-analytics-events.js +24 -0
- package/node_modules/@oxygen/shared/dist/recipes.d.ts +6 -0
- package/node_modules/@oxygen/shared/dist/recipes.js +23 -0
- package/node_modules/@oxygen/shared/dist/sequences.d.ts +126 -2
- package/node_modules/@oxygen/shared/dist/sequences.js +280 -4
- package/node_modules/@oxygen/shared/dist/ugc-amplification-identity.d.ts +2 -0
- package/node_modules/@oxygen/shared/dist/ugc-amplification-identity.js +24 -0
- package/node_modules/@oxygen/shared/dist/ugc.d.ts +8 -0
- package/node_modules/@oxygen/shared/dist/user-capability-routing.js +8 -1
- package/node_modules/@oxygen/shared/dist/version.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/version.js +3 -1
- package/node_modules/@oxygen/shared/dist/workspace-file-storage.d.ts +6 -2
- package/node_modules/@oxygen/shared/dist/workspace-file-storage.js +15 -4
- package/node_modules/@oxygen/shared/package.json +15 -0
- package/package.json +2 -1
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { OxygenError } from "./cli-result.js";
|
|
2
|
+
export const KNOWLEDGE_REPOSITORY_FILE_LIMIT = 25_000;
|
|
3
|
+
export const KNOWLEDGE_REPOSITORY_POLL_MS = 5 * 60_000;
|
|
4
|
+
export const KNOWLEDGE_REPOSITORY_IDLE_MS = 15_000;
|
|
5
|
+
export const KNOWLEDGE_REPOSITORY_MAX_WAIT_MS = 60_000;
|
|
6
|
+
function isCredentialFile(name) {
|
|
7
|
+
if (name === ".env" ||
|
|
8
|
+
name.startsWith(".env.") ||
|
|
9
|
+
[
|
|
10
|
+
".git-credentials",
|
|
11
|
+
".git_credentials_input",
|
|
12
|
+
".netrc",
|
|
13
|
+
".npmrc",
|
|
14
|
+
".pypirc",
|
|
15
|
+
"credentials",
|
|
16
|
+
].includes(name) ||
|
|
17
|
+
/^(?:id_rsa|id_ed25519|id_ecdsa)(?:\.|$)/.test(name) ||
|
|
18
|
+
/\.(?:jks|kdbx|keystore|pem|p12|pfx|key)$/.test(name)) {
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
const extension = name.match(/\.([^.]+)$/)?.[1] ?? "";
|
|
22
|
+
return ([
|
|
23
|
+
"json",
|
|
24
|
+
"yaml",
|
|
25
|
+
"yml",
|
|
26
|
+
"toml",
|
|
27
|
+
"ini",
|
|
28
|
+
"cfg",
|
|
29
|
+
"conf",
|
|
30
|
+
"properties",
|
|
31
|
+
].includes(extension) &&
|
|
32
|
+
/(?:^|[._-])(?:credentials?|secrets?)(?:[._-]|$)/.test(name));
|
|
33
|
+
}
|
|
34
|
+
/** Files may be preserved without being executed. Credentials and machine state are never imported. */
|
|
35
|
+
export function knowledgeRepositoryExclusion(path, mode = "100644") {
|
|
36
|
+
if (mode === "120000")
|
|
37
|
+
return "Symbolic link";
|
|
38
|
+
if (mode === "160000")
|
|
39
|
+
return "Submodule";
|
|
40
|
+
if (!path ||
|
|
41
|
+
path.startsWith("/") ||
|
|
42
|
+
path.includes("\\") ||
|
|
43
|
+
/[\x00-\x1f\x7f]/.test(path) ||
|
|
44
|
+
path.split("/").some((p) => p === "." || p === ".." || !p))
|
|
45
|
+
return "Unsafe path";
|
|
46
|
+
const parts = path.toLowerCase().split("/");
|
|
47
|
+
if (parts.some((p) => [".git", "node_modules", ".venv", "venv", ".ssh"].includes(p)))
|
|
48
|
+
return "Machine or dependency files";
|
|
49
|
+
const name = parts.at(-1);
|
|
50
|
+
if (isCredentialFile(name))
|
|
51
|
+
return "Credential file";
|
|
52
|
+
if (path.startsWith(".github/workflows/"))
|
|
53
|
+
return "GitHub automation";
|
|
54
|
+
if (/^\.obsidian\/(?:workspace(?:-mobile)?\.json|cache\/)/.test(path))
|
|
55
|
+
return "Device layout or cache";
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
export function normalizeKnowledgeRepositoryRoot(root) {
|
|
59
|
+
if (root === "" || root === "/")
|
|
60
|
+
return "";
|
|
61
|
+
const value = root.replace(/\/$/, "");
|
|
62
|
+
if (knowledgeRepositoryExclusion(`${value}/file.md`)) {
|
|
63
|
+
throw new OxygenError("invalid_knowledge_repository_root", "Choose a relative repository folder without parent paths or excluded directories.");
|
|
64
|
+
}
|
|
65
|
+
return value;
|
|
66
|
+
}
|
|
67
|
+
export function knowledgeRepositoryRelativePath(path, root) {
|
|
68
|
+
if (!root)
|
|
69
|
+
return path;
|
|
70
|
+
return path.startsWith(`${root}/`) ? path.slice(root.length + 1) : null;
|
|
71
|
+
}
|
|
72
|
+
/** Conservative three-way merge: identical changes and disjoint line edits are safe.
|
|
73
|
+
* Overlap, edit/delete and binary changes retain both versions for an explicit decision. */
|
|
74
|
+
export function mergeKnowledgeText(base, local, remote) {
|
|
75
|
+
if (local === remote)
|
|
76
|
+
return { kind: "merged", text: local };
|
|
77
|
+
if (base === local)
|
|
78
|
+
return { kind: "merged", text: remote };
|
|
79
|
+
if (base === remote)
|
|
80
|
+
return { kind: "merged", text: local };
|
|
81
|
+
if (base === null || local === null || remote === null)
|
|
82
|
+
return { kind: "conflict" };
|
|
83
|
+
const lines = (s) => s.match(/[^\n]*\n|[^\n]+$/g) ?? [];
|
|
84
|
+
const original = lines(base);
|
|
85
|
+
const change = (s) => {
|
|
86
|
+
const updated = lines(s);
|
|
87
|
+
let start = 0, tail = 0;
|
|
88
|
+
while (start < original.length &&
|
|
89
|
+
start < updated.length &&
|
|
90
|
+
original[start] === updated[start])
|
|
91
|
+
start++;
|
|
92
|
+
while (tail < original.length - start &&
|
|
93
|
+
tail < updated.length - start &&
|
|
94
|
+
original[original.length - tail - 1] ===
|
|
95
|
+
updated[updated.length - tail - 1])
|
|
96
|
+
tail++;
|
|
97
|
+
return {
|
|
98
|
+
start,
|
|
99
|
+
end: original.length - tail,
|
|
100
|
+
insert: updated.slice(start, updated.length - tail),
|
|
101
|
+
};
|
|
102
|
+
};
|
|
103
|
+
const a = change(local), b = change(remote);
|
|
104
|
+
const safelyBefore = (first, second) => first.end < second.start ||
|
|
105
|
+
(first.end === second.start && first.start < first.end);
|
|
106
|
+
if (!(safelyBefore(a, b) || safelyBefore(b, a)))
|
|
107
|
+
return { kind: "conflict" };
|
|
108
|
+
const result = [...original];
|
|
109
|
+
for (const c of [a, b].sort((x, y) => y.start - x.start))
|
|
110
|
+
result.splice(c.start, c.end - c.start, ...c.insert);
|
|
111
|
+
return { kind: "merged", text: result.join("") };
|
|
112
|
+
}
|
|
113
|
+
export function parseKnowledgeLfsPointer(text) {
|
|
114
|
+
const match = /^version https:\/\/git-lfs.github.com\/spec\/v1\r?\noid sha256:([a-f0-9]{64})\r?\nsize (\d+)\r?\n?$/.exec(text);
|
|
115
|
+
if (!match)
|
|
116
|
+
return null;
|
|
117
|
+
const size = Number(match[2]);
|
|
118
|
+
if (!Number.isSafeInteger(size) || size < 0)
|
|
119
|
+
return null;
|
|
120
|
+
return { oid: match[1], size };
|
|
121
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export declare const KNOWLEDGE_VAULT_MARKDOWN_MAX_BYTES = 1500000;
|
|
2
|
+
export declare const KNOWLEDGE_VAULT_PATH_MAX_BYTES = 1024;
|
|
3
|
+
export type KnowledgeVaultMarkdown = {
|
|
4
|
+
prefix: string;
|
|
5
|
+
body: string;
|
|
6
|
+
};
|
|
7
|
+
export type ParsedKnowledgeVaultMarkdown = KnowledgeVaultMarkdown & {
|
|
8
|
+
properties: Record<string, unknown>;
|
|
9
|
+
hasFrontmatter: boolean;
|
|
10
|
+
frontmatterError: string | null;
|
|
11
|
+
};
|
|
12
|
+
/** Paths are identities. Reject unsafe paths; never trim, lowercase or normalize them. */
|
|
13
|
+
export declare function assertKnowledgeVaultPath(path: string): string;
|
|
14
|
+
/** Fatal UTF-8 decoding and a roundtrip check prevent silent replacement of invalid bytes. */
|
|
15
|
+
export declare function decodeKnowledgeVaultMarkdown(input: string | Uint8Array): string;
|
|
16
|
+
export declare function parseKnowledgeVaultMarkdown(input: string | Uint8Array): ParsedKnowledgeVaultMarkdown;
|
|
17
|
+
/** No renderer normalization, generated metadata, or implicit trailing newline. */
|
|
18
|
+
export declare function renderKnowledgeVaultMarkdown(input: KnowledgeVaultMarkdown): string;
|
|
19
|
+
/** Change one property without reserializing unrelated YAML or the Markdown body. */
|
|
20
|
+
export declare function setKnowledgeVaultProperty(input: KnowledgeVaultMarkdown, key: string, value: unknown): KnowledgeVaultMarkdown;
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { isMap, parseDocument, stringify } from "yaml";
|
|
2
|
+
import { OxygenError } from "./cli-result.js";
|
|
3
|
+
export const KNOWLEDGE_VAULT_MARKDOWN_MAX_BYTES = 1_500_000;
|
|
4
|
+
export const KNOWLEDGE_VAULT_PATH_MAX_BYTES = 1_024;
|
|
5
|
+
function invalid(message) {
|
|
6
|
+
return new OxygenError("invalid_knowledge_file", message, { exitCode: 1 });
|
|
7
|
+
}
|
|
8
|
+
/** Paths are identities. Reject unsafe paths; never trim, lowercase or normalize them. */
|
|
9
|
+
export function assertKnowledgeVaultPath(path) {
|
|
10
|
+
if (typeof path !== "string" ||
|
|
11
|
+
!path ||
|
|
12
|
+
new TextEncoder().encode(path).length > KNOWLEDGE_VAULT_PATH_MAX_BYTES ||
|
|
13
|
+
/[\x00-\x1f\x7f\\]/u.test(path) ||
|
|
14
|
+
path.startsWith("/") ||
|
|
15
|
+
/^[A-Za-z]:/.test(path) ||
|
|
16
|
+
path.split("/").some((part) => !part || part === "." || part === "..")) {
|
|
17
|
+
throw invalid("File path must be a relative path without empty, dot, parent or control-character segments.");
|
|
18
|
+
}
|
|
19
|
+
if (new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(new TextEncoder().encode(path)) !== path) {
|
|
20
|
+
throw invalid("File path must contain valid Unicode characters.");
|
|
21
|
+
}
|
|
22
|
+
return path;
|
|
23
|
+
}
|
|
24
|
+
/** Fatal UTF-8 decoding and a roundtrip check prevent silent replacement of invalid bytes. */
|
|
25
|
+
export function decodeKnowledgeVaultMarkdown(input) {
|
|
26
|
+
const bytes = typeof input === "string" ? new TextEncoder().encode(input) : input;
|
|
27
|
+
if (bytes.byteLength > KNOWLEDGE_VAULT_MARKDOWN_MAX_BYTES)
|
|
28
|
+
throw invalid("Markdown file exceeds 1.5 MB.");
|
|
29
|
+
let text;
|
|
30
|
+
try {
|
|
31
|
+
text = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(bytes);
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
throw invalid("Markdown must contain valid UTF-8. Preserve other encodings as an attachment.");
|
|
35
|
+
}
|
|
36
|
+
if (text.includes("\0") || (typeof input === "string" && text !== input)) {
|
|
37
|
+
throw invalid("Markdown cannot contain NUL or invalid Unicode characters.");
|
|
38
|
+
}
|
|
39
|
+
return text;
|
|
40
|
+
}
|
|
41
|
+
function splitPrefix(text) {
|
|
42
|
+
const bom = text.startsWith("\ufeff") ? "\ufeff" : "";
|
|
43
|
+
const rest = text.slice(bom.length);
|
|
44
|
+
const open = /^---[ \t]*(?:\r\n|\n)/.exec(rest);
|
|
45
|
+
if (!open)
|
|
46
|
+
return { prefix: bom, body: rest, yaml: null, yamlOffset: bom.length };
|
|
47
|
+
const close = /^(?:---|\.\.\.)[ \t]*(?:\r\n|\n|$)/gm;
|
|
48
|
+
close.lastIndex = open[0].length;
|
|
49
|
+
const match = close.exec(rest);
|
|
50
|
+
if (!match)
|
|
51
|
+
return { prefix: bom, body: rest, yaml: null, yamlOffset: bom.length };
|
|
52
|
+
const end = match.index + match[0].length;
|
|
53
|
+
return {
|
|
54
|
+
prefix: bom + rest.slice(0, end),
|
|
55
|
+
body: rest.slice(end),
|
|
56
|
+
yaml: rest.slice(open[0].length, match.index),
|
|
57
|
+
yamlOffset: bom.length + open[0].length,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
export function parseKnowledgeVaultMarkdown(input) {
|
|
61
|
+
const split = splitPrefix(decodeKnowledgeVaultMarkdown(input));
|
|
62
|
+
let properties = {};
|
|
63
|
+
let frontmatterError = null;
|
|
64
|
+
if (split.yaml !== null) {
|
|
65
|
+
try {
|
|
66
|
+
const doc = parseDocument(split.yaml, {
|
|
67
|
+
keepSourceTokens: true,
|
|
68
|
+
uniqueKeys: true,
|
|
69
|
+
});
|
|
70
|
+
if (doc.errors.length || doc.warnings.length)
|
|
71
|
+
throw new Error(doc.errors[0]?.message ?? doc.warnings[0]?.message);
|
|
72
|
+
const value = doc.toJS({ maxAliasCount: 50 });
|
|
73
|
+
if (value !== null && (typeof value !== "object" || Array.isArray(value)))
|
|
74
|
+
throw new Error("Frontmatter must be a YAML mapping.");
|
|
75
|
+
if (value)
|
|
76
|
+
properties = value;
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
frontmatterError =
|
|
80
|
+
error instanceof Error ? error.message : "Invalid YAML frontmatter.";
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
prefix: split.prefix,
|
|
85
|
+
body: split.body,
|
|
86
|
+
properties,
|
|
87
|
+
hasFrontmatter: split.yaml !== null,
|
|
88
|
+
frontmatterError,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
/** No renderer normalization, generated metadata, or implicit trailing newline. */
|
|
92
|
+
export function renderKnowledgeVaultMarkdown(input) {
|
|
93
|
+
return decodeKnowledgeVaultMarkdown(input.prefix + input.body);
|
|
94
|
+
}
|
|
95
|
+
/** Change one property without reserializing unrelated YAML or the Markdown body. */
|
|
96
|
+
export function setKnowledgeVaultProperty(input, key, value) {
|
|
97
|
+
if (!key ||
|
|
98
|
+
/[\x00-\x1f\x7f]/u.test(key) ||
|
|
99
|
+
["__proto__", "constructor", "prototype"].includes(key)) {
|
|
100
|
+
throw invalid("A valid frontmatter property name is required.");
|
|
101
|
+
}
|
|
102
|
+
let json;
|
|
103
|
+
try {
|
|
104
|
+
json = JSON.stringify(value);
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
throw invalid("Frontmatter value must be JSON-compatible.");
|
|
108
|
+
}
|
|
109
|
+
if (json === undefined)
|
|
110
|
+
throw invalid("Frontmatter value must be JSON-compatible.");
|
|
111
|
+
const full = renderKnowledgeVaultMarkdown(input);
|
|
112
|
+
const parsed = parseKnowledgeVaultMarkdown(full);
|
|
113
|
+
if (parsed.frontmatterError)
|
|
114
|
+
throw invalid("Fix invalid YAML frontmatter before editing its properties.");
|
|
115
|
+
const split = splitPrefix(full);
|
|
116
|
+
const newline = input.prefix.includes("\r\n") || input.body.includes("\r\n")
|
|
117
|
+
? "\r\n"
|
|
118
|
+
: "\n";
|
|
119
|
+
const line = stringify({ [key]: JSON.parse(json) }).replace(/\n/g, newline);
|
|
120
|
+
if (split.yaml === null) {
|
|
121
|
+
const prefix = `${split.prefix}---${newline}${line}---${newline}`;
|
|
122
|
+
return { prefix, body: split.body };
|
|
123
|
+
}
|
|
124
|
+
const doc = parseDocument(split.yaml, {
|
|
125
|
+
keepSourceTokens: true,
|
|
126
|
+
uniqueKeys: true,
|
|
127
|
+
});
|
|
128
|
+
if (!isMap(doc.contents) && doc.contents !== null)
|
|
129
|
+
throw invalid("Frontmatter must be a YAML mapping.");
|
|
130
|
+
const pair = isMap(doc.contents)
|
|
131
|
+
? doc.contents.items.find((entry) => String(entry.key) === key)
|
|
132
|
+
: undefined;
|
|
133
|
+
if (pair) {
|
|
134
|
+
const range = pair.value
|
|
135
|
+
?.range;
|
|
136
|
+
if (!range)
|
|
137
|
+
throw invalid("This empty YAML value must be edited in the Markdown source.");
|
|
138
|
+
const start = split.yamlOffset + range[0];
|
|
139
|
+
const end = split.yamlOffset + range[1];
|
|
140
|
+
const old = split.prefix.slice(start, end);
|
|
141
|
+
const replacement = (split.prefix[start - 1] === ":" ? " " : "") +
|
|
142
|
+
json +
|
|
143
|
+
(split.prefix[end] === "#" ? " " : "") +
|
|
144
|
+
(old.endsWith("\n") ? newline : "");
|
|
145
|
+
return {
|
|
146
|
+
prefix: split.prefix.slice(0, start) + replacement + split.prefix.slice(end),
|
|
147
|
+
body: split.body,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
const insertion = split.yamlOffset + split.yaml.length;
|
|
151
|
+
return {
|
|
152
|
+
prefix: split.prefix.slice(0, insertion) + line + split.prefix.slice(insertion),
|
|
153
|
+
body: split.body,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
@@ -8,6 +8,16 @@ export type NormalizedMailboxImportRow = {
|
|
|
8
8
|
infrastructure_platform?: "google_workspace" | "microsoft_365" | "microsoft_azure";
|
|
9
9
|
tenant_id?: string;
|
|
10
10
|
app_password?: string;
|
|
11
|
+
/**
|
|
12
|
+
* WHO the inbox sends as (v1.954.0 — an inbox always belongs to a sender).
|
|
13
|
+
* Non-secret identity, so it rides the same whitelist as the address: a vendor
|
|
14
|
+
* export that already knows the person spares the customer a guessed profile
|
|
15
|
+
* named from the address local part.
|
|
16
|
+
*/
|
|
17
|
+
first_name?: string;
|
|
18
|
+
last_name?: string;
|
|
19
|
+
display_name?: string;
|
|
20
|
+
sender_profile_id?: string;
|
|
11
21
|
};
|
|
12
22
|
export type MailboxImportValidationSummary = {
|
|
13
23
|
valid: true;
|
|
@@ -49,6 +49,27 @@ const MAILBOX_TENANT_HEADERS = new Set([
|
|
|
49
49
|
"microsofttenantid",
|
|
50
50
|
"tenantid",
|
|
51
51
|
]);
|
|
52
|
+
const MAILBOX_FIRST_NAME_HEADERS = new Set([
|
|
53
|
+
"first",
|
|
54
|
+
"firstname",
|
|
55
|
+
"givenname",
|
|
56
|
+
]);
|
|
57
|
+
const MAILBOX_LAST_NAME_HEADERS = new Set([
|
|
58
|
+
"familyname",
|
|
59
|
+
"last",
|
|
60
|
+
"lastname",
|
|
61
|
+
"surname",
|
|
62
|
+
]);
|
|
63
|
+
const MAILBOX_DISPLAY_NAME_HEADERS = new Set([
|
|
64
|
+
"displayname",
|
|
65
|
+
"fromname",
|
|
66
|
+
"name",
|
|
67
|
+
"sendername",
|
|
68
|
+
]);
|
|
69
|
+
const MAILBOX_SENDER_PROFILE_HEADERS = new Set([
|
|
70
|
+
"senderprofile",
|
|
71
|
+
"senderprofileid",
|
|
72
|
+
]);
|
|
52
73
|
const MAILBOX_NON_TRANSFERABLE_SECRET_HEADERS = new Set([
|
|
53
74
|
"accesstoken",
|
|
54
75
|
"applicationsecret",
|
|
@@ -328,6 +349,17 @@ function normalizeMailboxExportRow(row, index, mode) {
|
|
|
328
349
|
? "microsoft_azure"
|
|
329
350
|
: null);
|
|
330
351
|
const infrastructurePlatform = normalizeMailboxInfrastructurePlatform(infrastructurePlatformRaw, provider, index);
|
|
352
|
+
// WHO the inbox sends as. Carried through because an inbox always belongs to a
|
|
353
|
+
// sender (v1.954.0): a file that already names the person is the difference
|
|
354
|
+
// between a real sender profile and one OXYGEN guesses from the address.
|
|
355
|
+
const firstName = readMailboxIdentityName(byHeader, MAILBOX_FIRST_NAME_HEADERS, index, "first_name");
|
|
356
|
+
const lastName = readMailboxIdentityName(byHeader, MAILBOX_LAST_NAME_HEADERS, index, "last_name");
|
|
357
|
+
const displayName = readMailboxIdentityName(byHeader, MAILBOX_DISPLAY_NAME_HEADERS, index, "display_name");
|
|
358
|
+
const senderProfileId = readUniqueMailboxExportString(byHeader, MAILBOX_SENDER_PROFILE_HEADERS, index, "sender_profile_id", (value) => value.trim().toLowerCase());
|
|
359
|
+
if (senderProfileId &&
|
|
360
|
+
!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(senderProfileId)) {
|
|
361
|
+
throw new OxygenError("invalid_request", `mailboxes[${index}].sender_profile_id must be a sender profile id (UUID). List them with \`oxygen senders profiles list\`, or drop the column and let the row's first/last name name the sender.`, { exitCode: 1 });
|
|
362
|
+
}
|
|
331
363
|
return {
|
|
332
364
|
email_address: email.trim().toLowerCase(),
|
|
333
365
|
provider,
|
|
@@ -338,6 +370,10 @@ function normalizeMailboxExportRow(row, index, mode) {
|
|
|
338
370
|
? { infrastructure_platform: infrastructurePlatform }
|
|
339
371
|
: {}),
|
|
340
372
|
...(tenantId ? { tenant_id: tenantId.trim().toLowerCase() } : {}),
|
|
373
|
+
...(firstName ? { first_name: firstName } : {}),
|
|
374
|
+
...(lastName ? { last_name: lastName } : {}),
|
|
375
|
+
...(displayName ? { display_name: displayName } : {}),
|
|
376
|
+
...(senderProfileId ? { sender_profile_id: senderProfileId } : {}),
|
|
341
377
|
...(mode === "credential" &&
|
|
342
378
|
provider === "google" &&
|
|
343
379
|
distinctPasswords[0] !== undefined
|
|
@@ -345,6 +381,23 @@ function normalizeMailboxExportRow(row, index, mode) {
|
|
|
345
381
|
: {}),
|
|
346
382
|
};
|
|
347
383
|
}
|
|
384
|
+
/**
|
|
385
|
+
* One identity half off an export row. Header-bound (the From name rides an SMTP
|
|
386
|
+
* header), so a CR/LF is refused by field name rather than silently stripped — the
|
|
387
|
+
* caller is usually holding hundreds of rows and needs to know which one.
|
|
388
|
+
*/
|
|
389
|
+
function readMailboxIdentityName(byHeader, headers, index, field) {
|
|
390
|
+
const value = readUniqueMailboxExportString(byHeader, headers, index, field, (raw) => raw.trim());
|
|
391
|
+
if (!value)
|
|
392
|
+
return null;
|
|
393
|
+
if (value.length > 200) {
|
|
394
|
+
throw new OxygenError("invalid_request", `mailboxes[${index}].${field} must be at most 200 characters.`, { exitCode: 1 });
|
|
395
|
+
}
|
|
396
|
+
if (/[\r\n]/.test(value)) {
|
|
397
|
+
throw new OxygenError("invalid_request", `mailboxes[${index}].${field} must be a single line.`, { exitCode: 1 });
|
|
398
|
+
}
|
|
399
|
+
return value;
|
|
400
|
+
}
|
|
348
401
|
function normalizeMailboxExportHeader(value) {
|
|
349
402
|
return value
|
|
350
403
|
.replace(/^\uFEFF/, "")
|
|
@@ -78,6 +78,14 @@ export type PlanLimits = {
|
|
|
78
78
|
*/
|
|
79
79
|
export declare const TABLE_IMPORT_ROW_LIMIT: 3000000;
|
|
80
80
|
export { VERCEL_REQUEST_BODY_LIMIT_BYTES } from "./import-limits.js";
|
|
81
|
+
/**
|
|
82
|
+
* OXYGEN's own JSON-body ceiling for every `/api/cli/*` route (enforced by
|
|
83
|
+
* `assertCliJsonBodyWithinLimit` on the content-length header). Shared so the CLI
|
|
84
|
+
* pre-splits a row batch by measured bytes instead of learning the number from a
|
|
85
|
+
* 413. Must stay below VERCEL_REQUEST_BODY_LIMIT_BYTES, and must never be raised
|
|
86
|
+
* in the CLI alone — an older server would still 413 at the old number.
|
|
87
|
+
*/
|
|
88
|
+
export declare const MAX_CLI_JSON_BODY_BYTES = 2000000;
|
|
81
89
|
/**
|
|
82
90
|
* The per-rung limit matrix. `ai_live` deliberately equals `tool_live` at every
|
|
83
91
|
* rung — one "live actions" mental model; the per-call cost asymmetry between
|
|
@@ -21,6 +21,14 @@ export const TABLE_IMPORT_ROW_LIMIT = WORKSPACE_TABLE_CAPACITY.tableRowLimit;
|
|
|
21
21
|
// ceilings below ("how big can an import be"), but it must stay importable from a
|
|
22
22
|
// browser bundle, so it is defined in ./import-limits and re-exported here.
|
|
23
23
|
export { VERCEL_REQUEST_BODY_LIMIT_BYTES } from "./import-limits.js";
|
|
24
|
+
/**
|
|
25
|
+
* OXYGEN's own JSON-body ceiling for every `/api/cli/*` route (enforced by
|
|
26
|
+
* `assertCliJsonBodyWithinLimit` on the content-length header). Shared so the CLI
|
|
27
|
+
* pre-splits a row batch by measured bytes instead of learning the number from a
|
|
28
|
+
* 413. Must stay below VERCEL_REQUEST_BODY_LIMIT_BYTES, and must never be raised
|
|
29
|
+
* in the CLI alone — an older server would still 413 at the old number.
|
|
30
|
+
*/
|
|
31
|
+
export const MAX_CLI_JSON_BODY_BYTES = 2_000_000;
|
|
24
32
|
/**
|
|
25
33
|
* The per-rung limit matrix. `ai_live` deliberately equals `tool_live` at every
|
|
26
34
|
* rung — one "live actions" mental model; the per-call cost asymmetry between
|
|
@@ -176,7 +176,7 @@ export declare const ENRICHMENT_CREDITS: {
|
|
|
176
176
|
readonly company_enrich_typical: 99;
|
|
177
177
|
readonly web_search: 5;
|
|
178
178
|
readonly page_scrape: 40;
|
|
179
|
-
readonly linkedin_profile_scrape:
|
|
179
|
+
readonly linkedin_profile_scrape: 10;
|
|
180
180
|
};
|
|
181
181
|
/** Editable calculator default: blended credits per fully-enriched row. */
|
|
182
182
|
export declare const ENRICHMENT_CREDITS_PER_ROW_TYPICAL = 100;
|
|
@@ -163,7 +163,7 @@ export const ENRICHMENT_CREDITS = {
|
|
|
163
163
|
company_enrich_typical: 99,
|
|
164
164
|
web_search: 5, // Serper
|
|
165
165
|
page_scrape: 40, // Firecrawl (wedge price)
|
|
166
|
-
linkedin_profile_scrape:
|
|
166
|
+
linkedin_profile_scrape: 10, // Managed profile credit at $0.002 COGS, exactly 5x
|
|
167
167
|
};
|
|
168
168
|
/** Editable calculator default: blended credits per fully-enriched row. */
|
|
169
169
|
export const ENRICHMENT_CREDITS_PER_ROW_TYPICAL = SNAPSHOT_ENRICHMENT_CREDITS_PER_ROW_TYPICAL;
|
|
@@ -77,6 +77,30 @@ export declare const PRODUCT_EVENTS: {
|
|
|
77
77
|
readonly TRIAL_CANCEL_SCHEDULED: "trial_cancel_scheduled";
|
|
78
78
|
readonly TRIAL_CANCEL_RESUMED: "trial_cancel_resumed";
|
|
79
79
|
readonly TRIAL_ENDED_UNCONVERTED: "trial_ended_unconverted";
|
|
80
|
+
/**
|
|
81
|
+
* Marketing site. The public hero lookup on oxygen-agent.com: an ANONYMOUS
|
|
82
|
+
* visitor types their website and OXYGEN spends its own credits profiling it.
|
|
83
|
+
*
|
|
84
|
+
* Deliberately absent from the web's KPI_TYPE_TO_EVENT. These describe a
|
|
85
|
+
* pre-signup funnel step, not a lifecycle moment the company reports, and
|
|
86
|
+
* mapping one onto a `company_kpi_events` type would redefine a headline
|
|
87
|
+
* number without anyone editing that number's definition.
|
|
88
|
+
*
|
|
89
|
+
* Properties are enums, numbers and the domain HASH only — never the raw
|
|
90
|
+
* domain. The visitor has no account and never consented to their employer's
|
|
91
|
+
* name landing in an analytics warehouse (ADR 0021: PostHog stays sanitized).
|
|
92
|
+
* `hero_lookup_cached` is what says whether the lookup cache is paying for
|
|
93
|
+
* itself, which is the whole economics of the surface.
|
|
94
|
+
*/
|
|
95
|
+
readonly HERO_LOOKUP_STARTED: "hero_lookup_started";
|
|
96
|
+
readonly HERO_PROFILE_COMPLETED: "hero_profile_completed";
|
|
97
|
+
readonly HERO_LEADS_COMPLETED: "hero_leads_completed";
|
|
98
|
+
readonly HERO_LOOKUP_CACHED: "hero_lookup_cached";
|
|
99
|
+
readonly HERO_LOOKUP_DEGRADED: "hero_lookup_degraded";
|
|
100
|
+
/** Browser-side hero events; props are enums, booleans and counts only. */
|
|
101
|
+
readonly HERO_LOOKUP_SUBMITTED: "hero_lookup_submitted";
|
|
102
|
+
readonly HERO_LOOKUP_RENDERED: "hero_lookup_rendered";
|
|
103
|
+
readonly HERO_SIGNUP_CLICKED: "hero_signup_clicked";
|
|
80
104
|
/** Pipeline heartbeat: proves ingestion is alive when the product is quiet. */
|
|
81
105
|
readonly POSTHOG_CANARY: "posthog_canary";
|
|
82
106
|
};
|
|
@@ -77,6 +77,30 @@ export const PRODUCT_EVENTS = {
|
|
|
77
77
|
TRIAL_CANCEL_SCHEDULED: "trial_cancel_scheduled",
|
|
78
78
|
TRIAL_CANCEL_RESUMED: "trial_cancel_resumed",
|
|
79
79
|
TRIAL_ENDED_UNCONVERTED: "trial_ended_unconverted",
|
|
80
|
+
/**
|
|
81
|
+
* Marketing site. The public hero lookup on oxygen-agent.com: an ANONYMOUS
|
|
82
|
+
* visitor types their website and OXYGEN spends its own credits profiling it.
|
|
83
|
+
*
|
|
84
|
+
* Deliberately absent from the web's KPI_TYPE_TO_EVENT. These describe a
|
|
85
|
+
* pre-signup funnel step, not a lifecycle moment the company reports, and
|
|
86
|
+
* mapping one onto a `company_kpi_events` type would redefine a headline
|
|
87
|
+
* number without anyone editing that number's definition.
|
|
88
|
+
*
|
|
89
|
+
* Properties are enums, numbers and the domain HASH only — never the raw
|
|
90
|
+
* domain. The visitor has no account and never consented to their employer's
|
|
91
|
+
* name landing in an analytics warehouse (ADR 0021: PostHog stays sanitized).
|
|
92
|
+
* `hero_lookup_cached` is what says whether the lookup cache is paying for
|
|
93
|
+
* itself, which is the whole economics of the surface.
|
|
94
|
+
*/
|
|
95
|
+
HERO_LOOKUP_STARTED: "hero_lookup_started",
|
|
96
|
+
HERO_PROFILE_COMPLETED: "hero_profile_completed",
|
|
97
|
+
HERO_LEADS_COMPLETED: "hero_leads_completed",
|
|
98
|
+
HERO_LOOKUP_CACHED: "hero_lookup_cached",
|
|
99
|
+
HERO_LOOKUP_DEGRADED: "hero_lookup_degraded",
|
|
100
|
+
/** Browser-side hero events; props are enums, booleans and counts only. */
|
|
101
|
+
HERO_LOOKUP_SUBMITTED: "hero_lookup_submitted",
|
|
102
|
+
HERO_LOOKUP_RENDERED: "hero_lookup_rendered",
|
|
103
|
+
HERO_SIGNUP_CLICKED: "hero_signup_clicked",
|
|
80
104
|
/** Pipeline heartbeat: proves ingestion is alive when the product is quiet. */
|
|
81
105
|
POSTHOG_CANARY: "posthog_canary",
|
|
82
106
|
};
|
|
@@ -16,4 +16,10 @@ export type RecipePrerequisiteKind = typeof RECIPE_PREREQUISITE_KINDS[number];
|
|
|
16
16
|
export declare const RECIPE_BODY_SECTIONS: readonly ["The Play", "What You Get", "When To Run (And When Not To)", "Before You Start", "Steps", "Ready Assets", "Calibrate", "Troubleshooting", "Operator Notes", "Related"];
|
|
17
17
|
export declare const RECIPE_SLUG_PATTERN: RegExp;
|
|
18
18
|
export declare function recipeWikiSlug(recipeSlug: string): string;
|
|
19
|
+
export declare const RECIPE_KIT_MAX_STAGES = 6;
|
|
20
|
+
export declare const RECIPE_KIT_TABLE_REF_PATTERN: RegExp;
|
|
21
|
+
export declare const RECIPE_KIT_STAGE_STATUSES: readonly ["planned", "installed", "reused", "skipped_active", "blocked", "failed"];
|
|
22
|
+
export type RecipeKitStageStatus = typeof RECIPE_KIT_STAGE_STATUSES[number];
|
|
23
|
+
export type RecipeKitStatus = "not_applied" | "partial" | "applied";
|
|
24
|
+
export declare function recipeKitSourceRef(recipeSlug: string, version: number): string;
|
|
19
25
|
export declare const RECIPE_TRIAL_SAFE_PILOT_CREDIT_CAP = 100;
|
|
@@ -87,6 +87,29 @@ export const RECIPE_SLUG_PATTERN = /^[a-z0-9][a-z0-9-]{0,119}$/;
|
|
|
87
87
|
export function recipeWikiSlug(recipeSlug) {
|
|
88
88
|
return `playbook-${recipeSlug}`;
|
|
89
89
|
}
|
|
90
|
+
// ── Kits (ADR 0025) ─────────────────────────────────────────────────────────
|
|
91
|
+
// A recipe carries an ORDERED kit of Blueprint stages. `oxygen recipes apply`
|
|
92
|
+
// preflights every stage into one forecast and installs them all under one
|
|
93
|
+
// approval — 0 credits, no provider call, every installed Workflow disabled.
|
|
94
|
+
// A stage may bind a table created by an earlier stage through a
|
|
95
|
+
// `$stages.<index>.tables.<ref>` reference so later stages graft onto it
|
|
96
|
+
// instead of creating a second table.
|
|
97
|
+
export const RECIPE_KIT_MAX_STAGES = 6;
|
|
98
|
+
export const RECIPE_KIT_TABLE_REF_PATTERN = /^\$stages\.(\d)\.tables\.([a-z][a-z0-9_]*)$/;
|
|
99
|
+
export const RECIPE_KIT_STAGE_STATUSES = [
|
|
100
|
+
"planned",
|
|
101
|
+
"installed",
|
|
102
|
+
"reused",
|
|
103
|
+
"skipped_active",
|
|
104
|
+
"blocked",
|
|
105
|
+
"failed",
|
|
106
|
+
];
|
|
107
|
+
// page_sources ref an apply writes on the recipe's playbook page — one per
|
|
108
|
+
// recipe version, ON CONFLICT DO NOTHING, so first-apply provenance survives
|
|
109
|
+
// reapplies.
|
|
110
|
+
export function recipeKitSourceRef(recipeSlug, version) {
|
|
111
|
+
return `https://oxygen-agent.com/recipes/${recipeSlug}@v${version}#kit`;
|
|
112
|
+
}
|
|
90
113
|
// trial_safe recipes must keep their pilot path within 10% of the 7-day
|
|
91
114
|
// card-required trial's 1,000 managed credits.
|
|
92
115
|
export const RECIPE_TRIAL_SAFE_PILOT_CREDIT_CAP = 100;
|