@oxygen-agent/cli 1.948.1 → 1.987.20
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 +33 -9
- package/dist/help.d.ts +21 -0
- package/dist/help.js +94 -0
- package/dist/index.js +1493 -297
- 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.d.ts +3 -6
- package/dist/ugc-commands.js +2 -1086
- package/node_modules/@oxygen/cli-ugc/dist/commands.d.ts +3 -0
- package/node_modules/@oxygen/cli-ugc/dist/commands.js +1178 -0
- package/node_modules/@oxygen/cli-ugc/dist/field-parser.d.ts +7 -0
- package/node_modules/@oxygen/cli-ugc/dist/field-parser.js +25 -0
- package/node_modules/@oxygen/cli-ugc/dist/index.d.ts +14 -0
- package/node_modules/@oxygen/cli-ugc/dist/index.js +5 -0
- package/node_modules/@oxygen/cli-ugc/package.json +15 -0
- package/node_modules/@oxygen/formula/dist/expression.js +14 -1
- package/node_modules/@oxygen/formula/dist/formula-functions.js +71 -1
- package/node_modules/@oxygen/formula/dist/index.d.ts +1 -0
- package/node_modules/@oxygen/formula/dist/index.js +1 -0
- package/node_modules/@oxygen/formula/dist/value-cleaners.d.ts +69 -0
- package/node_modules/@oxygen/formula/dist/value-cleaners.js +374 -0
- package/node_modules/@oxygen/recipe-sdk/dist/index.d.ts +2 -0
- package/node_modules/@oxygen/shared/dist/billing.d.ts +27 -27
- package/node_modules/@oxygen/shared/dist/capability-discovery.d.ts +8 -0
- package/node_modules/@oxygen/shared/dist/capability-discovery.js +127 -18
- package/node_modules/@oxygen/shared/dist/column-output-fields.js +12 -4
- 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/copilot-playbooks.d.ts +18 -0
- package/node_modules/@oxygen/shared/dist/copilot-playbooks.js +43 -0
- package/node_modules/@oxygen/shared/dist/copilot-skills.d.ts +15 -0
- package/node_modules/@oxygen/shared/dist/copilot-skills.generated.d.ts +31 -0
- package/node_modules/@oxygen/shared/dist/copilot-skills.generated.js +41 -0
- package/node_modules/@oxygen/shared/dist/copilot-skills.js +6 -0
- 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 +5 -0
- package/node_modules/@oxygen/shared/dist/index.js +5 -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 +32 -38
- package/node_modules/@oxygen/shared/dist/knowledge-bootstrap.js +38 -41
- 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/langfuse.d.ts +12 -1
- package/node_modules/@oxygen/shared/dist/langfuse.js +48 -8
- 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/research-output-contract.d.ts +33 -1
- package/node_modules/@oxygen/shared/dist/research-output-contract.js +64 -2
- package/node_modules/@oxygen/shared/dist/sequence-crm-events.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/sequence-hubspot-sync.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/sequences.d.ts +152 -2
- package/node_modules/@oxygen/shared/dist/sequences.js +304 -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 +25 -0
- package/node_modules/@oxygen/workflows/dist/graph/manifest-schema.d.ts +15 -15
- package/package.json +6 -2
|
@@ -43,43 +43,16 @@
|
|
|
43
43
|
* ordinary revisioned wiki writes a human can revert.
|
|
44
44
|
* 6. EXACTLY-ONCE — the marker is claimed with a conditional UPDATE, so N worker
|
|
45
45
|
* replicas polling one tenant produce one run, never N runs.
|
|
46
|
-
* 7. OXYGEN-FUNDED — the
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
* row keyed by the run (`KNOWLEDGE_BOOTSTRAP_COVERAGE_IDEMPOTENCY_PREFIX`).
|
|
51
|
-
* The customer's balance nets to zero and both rows are visible in
|
|
52
|
-
* their ledger. Only the AUTOMATIC pass is covered: a requested
|
|
53
|
-
* `--live --max-credits` re-run is the customer's own paid action.
|
|
46
|
+
* 7. OXYGEN-FUNDED — the internal worker scope records nominal managed-credit
|
|
47
|
+
* holds and settlements with zero customer-balance deltas.
|
|
48
|
+
* Manual reruns remain customer-funded. Durable call checkpoints
|
|
49
|
+
* replay completed results and never repeat uncertain dispatch.
|
|
54
50
|
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
* serper.search 5 cr per call (cogs-rates: serper.customerOxygen…search)
|
|
59
|
-
* exa.contents 5 cr per CALL (not per URL — this is the whole trick)
|
|
60
|
-
* AI column, medium tier 75 cr per run (pricing-sheet: AI_COLUMN_CREDITS.medium)
|
|
61
|
-
*
|
|
62
|
-
* Happy path = 1 serper.search (5) + 1 exa.contents over <= 5 URLs (5)
|
|
63
|
-
* + 1 medium synthesis pass (75) = 85 cr
|
|
64
|
-
* Enrichment sub-cap 60 cr — ~6x the 10 cr the happy path needs, so a waterfall
|
|
65
|
-
* retry or a second search fits, and a runaway
|
|
66
|
-
* per-URL fetch loop is stopped long before the
|
|
67
|
-
* overall ceiling.
|
|
68
|
-
* Hard cap 300 cr — 85 cr nominal plus room for two retried synthesis
|
|
69
|
-
* passes. 3% of the 10,000-credit signup grant
|
|
70
|
-
* (FREE_SIGNUP_GRANT_CREDITS, ./billing.ts), which is
|
|
71
|
-
* the headroom the balance gate needs even though the
|
|
72
|
-
* spend is granted back.
|
|
73
|
-
*
|
|
74
|
-
* The obvious implementation — firecrawl.map + 3x firecrawl.scrape — costs
|
|
75
|
-
* 4 x 40 = 160 cr for the SAME job, because Firecrawl bills per page. exa.contents
|
|
76
|
-
* is priced per call and is built for fetching text at known URLs; that is why the
|
|
77
|
-
* read budget is a page COUNT (`KNOWLEDGE_BOOTSTRAP_MAX_PAGES_READ`) inside one
|
|
78
|
-
* call rather than a call count.
|
|
79
|
-
*
|
|
80
|
-
* This module is a leaf: constants and types only, no DB and no provider imports, so
|
|
81
|
-
* the worker, tenant-db, CLI, and API surfaces all bind to one contract.
|
|
51
|
+
* New workspace consent scopes the submitted domain and actual signup creator.
|
|
52
|
+
* Professional person evidence remains actor-scoped; ICP/offers are hypotheses.
|
|
53
|
+
* The provider sub-cap covers every external research leg, including fallbacks.
|
|
82
54
|
*/
|
|
55
|
+
import { normalizeLinkedinProfileUrl } from "./linkedin-url.js";
|
|
83
56
|
/** Hard ceiling on managed credits ONE workspace bootstrap may spend, all phases. */
|
|
84
57
|
export const KNOWLEDGE_BOOTSTRAP_MAX_CREDITS = 300;
|
|
85
58
|
/**
|
|
@@ -100,6 +73,19 @@ export const KNOWLEDGE_BOOTSTRAP_SYNTHESIS_TIER = "medium";
|
|
|
100
73
|
* it is a quality/latency knob, and the credit ceilings above remain the money guard.
|
|
101
74
|
*/
|
|
102
75
|
export const KNOWLEDGE_BOOTSTRAP_MAX_PAGES_READ = 5;
|
|
76
|
+
/**
|
|
77
|
+
* Per-call provider abort budget for every paid leg of the pass (LinkedIn company,
|
|
78
|
+
* person search/profile, page search, page read), in milliseconds.
|
|
79
|
+
*
|
|
80
|
+
* The platform default is 120 s, sized for a durable column run. This pass runs
|
|
81
|
+
* inside the worker's tenant-maintenance tick under a 300 s step deadline, so two
|
|
82
|
+
* hung LinkedIn calls at the default (measured 2026-09-15/16: the managed vendor
|
|
83
|
+
* accepted connections and never answered) consumed the whole budget before the
|
|
84
|
+
* page reads and the synthesis got a turn. 45 s keeps the worst case — five legs,
|
|
85
|
+
* every one hanging — inside the step, and a healthy vendor answers in well under
|
|
86
|
+
* it. This bounds latency only; the credit ceilings above remain the money guard.
|
|
87
|
+
*/
|
|
88
|
+
export const KNOWLEDGE_BOOTSTRAP_PROVIDER_TIMEOUT_MS = 45_000;
|
|
103
89
|
/** Fleet-wide kill switch, matching OXYGEN_KNOWLEDGE_RETRIEVAL/AUTO_SOURCES_DISABLED. */
|
|
104
90
|
export const KNOWLEDGE_BOOTSTRAP_DISABLED_ENV_VAR = "OXYGEN_KNOWLEDGE_BOOTSTRAP_DISABLED";
|
|
105
91
|
/**
|
|
@@ -186,12 +172,22 @@ export function readKnowledgeBootstrapConsent(metadata) {
|
|
|
186
172
|
return null;
|
|
187
173
|
if (!Number.isFinite(Date.parse(grantedAt)))
|
|
188
174
|
return null;
|
|
175
|
+
const scope = record.scope === "company_and_creator" ? "company_and_creator" : undefined;
|
|
176
|
+
const grantedBy = typeof record.granted_by_clerk_user_id === "string" && record.granted_by_clerk_user_id.length > 0
|
|
177
|
+
? record.granted_by_clerk_user_id
|
|
178
|
+
: null;
|
|
179
|
+
// A person hint is valid only as part of the creator-scoped grant. Otherwise a
|
|
180
|
+
// stale metadata value could be mistaken for an organization-wide profile hint.
|
|
181
|
+
const creatorLinkedinUrl = scope && grantedBy && typeof record.creator_linkedin_url === "string"
|
|
182
|
+
? normalizeLinkedinProfileUrl(record.creator_linkedin_url)?.dispatchUrl ?? null
|
|
183
|
+
: null;
|
|
189
184
|
return {
|
|
190
185
|
granted_at: grantedAt,
|
|
186
|
+
...(scope ? { scope } : {}),
|
|
187
|
+
...(typeof record.domain === "string" ? { domain: record.domain } : {}),
|
|
188
|
+
...(creatorLinkedinUrl ? { creator_linkedin_url: creatorLinkedinUrl } : {}),
|
|
191
189
|
source: typeof record.source === "string" && record.source.length > 0 ? record.source : null,
|
|
192
|
-
granted_by_clerk_user_id:
|
|
193
|
-
? record.granted_by_clerk_user_id
|
|
194
|
-
: null,
|
|
190
|
+
granted_by_clerk_user_id: grantedBy,
|
|
195
191
|
};
|
|
196
192
|
}
|
|
197
193
|
/**
|
|
@@ -248,13 +244,14 @@ export const KNOWLEDGE_BOOTSTRAP_COVERAGE_IDEMPOTENCY_PREFIX = "knowledge_bootst
|
|
|
248
244
|
*/
|
|
249
245
|
export const KNOWLEDGE_BOOTSTRAP_RESEARCH_PAGE_SLUG = "company-research";
|
|
250
246
|
/**
|
|
251
|
-
* The typed `company_profile` sections
|
|
252
|
-
* "never clobber a profile a human already filled" gate
|
|
247
|
+
* The typed `company_profile` sections inspected by the automatic bootstrap's
|
|
248
|
+
* "never clobber a profile a human already filled" gate.
|
|
253
249
|
*
|
|
254
250
|
* `gtm_stack` and `custom` are structurally excluded: research of a company's public
|
|
255
251
|
* web presence says nothing about which tools it has connected here, and a workspace
|
|
256
252
|
* that filled only those has told us nothing about its company and must still be
|
|
257
|
-
* bootstrapped.
|
|
253
|
+
* bootstrapped. This is the existing-context guard's list, not a list of writes:
|
|
254
|
+
* current research writes company facts and stores inferred GTM hypotheses separately.
|
|
258
255
|
*/
|
|
259
256
|
export const KNOWLEDGE_BOOTSTRAP_PROFILE_SECTIONS = [
|
|
260
257
|
"company",
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export declare const KNOWLEDGE_REPOSITORY_FILE_LIMIT = 25000;
|
|
2
|
+
export declare const KNOWLEDGE_REPOSITORY_POLL_MS: number;
|
|
3
|
+
export declare const KNOWLEDGE_REPOSITORY_IDLE_MS = 15000;
|
|
4
|
+
export declare const KNOWLEDGE_REPOSITORY_MAX_WAIT_MS = 60000;
|
|
5
|
+
export type KnowledgeRepositoryMode = "import" | "two_way";
|
|
6
|
+
/** Files may be preserved without being executed. Credentials and machine state are never imported. */
|
|
7
|
+
export declare function knowledgeRepositoryExclusion(path: string, mode?: string): string | null;
|
|
8
|
+
export declare function normalizeKnowledgeRepositoryRoot(root: string): string;
|
|
9
|
+
export declare function knowledgeRepositoryRelativePath(path: string, root: string): string | null;
|
|
10
|
+
export type KnowledgeMergeResult = {
|
|
11
|
+
kind: "merged";
|
|
12
|
+
text: string | null;
|
|
13
|
+
} | {
|
|
14
|
+
kind: "conflict";
|
|
15
|
+
};
|
|
16
|
+
/** Conservative three-way merge: identical changes and disjoint line edits are safe.
|
|
17
|
+
* Overlap, edit/delete and binary changes retain both versions for an explicit decision. */
|
|
18
|
+
export declare function mergeKnowledgeText(base: string | null, local: string | null, remote: string | null): KnowledgeMergeResult;
|
|
19
|
+
export declare function parseKnowledgeLfsPointer(text: string): {
|
|
20
|
+
oid: string;
|
|
21
|
+
size: number;
|
|
22
|
+
} | null;
|
|
@@ -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
|
+
}
|
|
@@ -38,6 +38,8 @@ export type LlmSpanBody = {
|
|
|
38
38
|
userId?: string | null;
|
|
39
39
|
};
|
|
40
40
|
export type LlmGenerationBody = LlmSpanBody & {
|
|
41
|
+
/** Persist this physical id when a later annotation must target this attempt. */
|
|
42
|
+
observationId?: string;
|
|
41
43
|
model?: string | null;
|
|
42
44
|
modelParameters?: Record<string, unknown>;
|
|
43
45
|
completionStartTime?: Date | null;
|
|
@@ -63,9 +65,11 @@ export type LlmEventBody = {
|
|
|
63
65
|
* because that variant requires a *string* value. Widening later stays additive.
|
|
64
66
|
*/
|
|
65
67
|
export type LlmScoreBody = {
|
|
66
|
-
/**
|
|
68
|
+
/** Retry-safe scores need the same id, name AND timestamp on every attempt. */
|
|
67
69
|
id?: string;
|
|
68
70
|
traceId: string;
|
|
71
|
+
observationId?: string;
|
|
72
|
+
timestamp?: Date;
|
|
69
73
|
name: string;
|
|
70
74
|
value: number;
|
|
71
75
|
dataType?: "BOOLEAN" | "NUMERIC";
|
|
@@ -103,6 +107,13 @@ export type LlmTracingClient = {
|
|
|
103
107
|
*/
|
|
104
108
|
export declare function isLlmTracingEnabled(env?: EnvMap): boolean;
|
|
105
109
|
export declare function resolveLlmTracingEnvironment(env?: EnvMap): string;
|
|
110
|
+
/**
|
|
111
|
+
* The private Langfuse provider must identify its own runtime rather than
|
|
112
|
+
* inherit the process-wide OTel resource. Fly identity wins because it is
|
|
113
|
+
* injected per machine and cannot drift; a local CLI/test process is neither
|
|
114
|
+
* the web app nor the worker.
|
|
115
|
+
*/
|
|
116
|
+
export declare function resolveLlmTracingServiceName(env?: EnvMap): string;
|
|
106
117
|
/**
|
|
107
118
|
* Deterministic Langfuse trace id for an external seed (copilot turn id, agent
|
|
108
119
|
* run id, AI-column run id).
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
// * Trace-level input/output is deprecated in v5. Overall IO goes on the ROOT
|
|
33
33
|
// observation instead; setTraceIO()/setActiveTraceIO() are deliberately not
|
|
34
34
|
// used here.
|
|
35
|
-
import { createHash, randomBytes } from "node:crypto";
|
|
35
|
+
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
36
36
|
import { OXYGEN_VERSION } from "./version.js";
|
|
37
37
|
import { prepareLlmPayload } from "./llm-payload.js";
|
|
38
38
|
import { log } from "./log.js";
|
|
@@ -83,6 +83,23 @@ export function resolveLlmTracingEnvironment(env = process.env) {
|
|
|
83
83
|
return flyEnv === "production" ? "production" : "development";
|
|
84
84
|
return "development";
|
|
85
85
|
}
|
|
86
|
+
/**
|
|
87
|
+
* The private Langfuse provider must identify its own runtime rather than
|
|
88
|
+
* inherit the process-wide OTel resource. Fly identity wins because it is
|
|
89
|
+
* injected per machine and cannot drift; a local CLI/test process is neither
|
|
90
|
+
* the web app nor the worker.
|
|
91
|
+
*/
|
|
92
|
+
export function resolveLlmTracingServiceName(env = process.env) {
|
|
93
|
+
const flyApp = env.FLY_APP_NAME?.trim() || env.FLY_APP?.trim();
|
|
94
|
+
if (flyApp)
|
|
95
|
+
return flyApp.includes("-dev") ? "oxygen-worker-dev" : "oxygen-worker";
|
|
96
|
+
const flyEnvironment = env.FLY_ENVIRONMENT?.trim().toLowerCase();
|
|
97
|
+
if (flyEnvironment)
|
|
98
|
+
return flyEnvironment === "production" ? "oxygen-worker" : "oxygen-worker-dev";
|
|
99
|
+
if (env.VERCEL?.trim() || env.VERCEL_ENV?.trim())
|
|
100
|
+
return "oxygen-web";
|
|
101
|
+
return "oxygen-local";
|
|
102
|
+
}
|
|
86
103
|
/**
|
|
87
104
|
* Deterministic Langfuse trace id for an external seed (copilot turn id, agent
|
|
88
105
|
* run id, AI-column run id).
|
|
@@ -140,8 +157,9 @@ function createOtelEmitter(env, warn) {
|
|
|
140
157
|
const init = () => {
|
|
141
158
|
handle ??= (async () => {
|
|
142
159
|
try {
|
|
143
|
-
const [{ LangfuseSpanProcessor }, { BasicTracerProvider, AlwaysOnSampler }, tracing, otel, core] = await Promise.all([
|
|
160
|
+
const [{ LangfuseSpanProcessor }, { BasicTracerProvider, AlwaysOnSampler }, { resourceFromAttributes }, tracing, otel, core] = await Promise.all([
|
|
144
161
|
import("@langfuse/otel"), import("@opentelemetry/sdk-trace-base"),
|
|
162
|
+
import("@opentelemetry/resources"),
|
|
145
163
|
import("@langfuse/tracing"), import("@opentelemetry/api"), import("@langfuse/core"),
|
|
146
164
|
]);
|
|
147
165
|
const processor = new LangfuseSpanProcessor({
|
|
@@ -160,6 +178,11 @@ function createOtelEmitter(env, warn) {
|
|
|
160
178
|
});
|
|
161
179
|
let identity;
|
|
162
180
|
const provider = new BasicTracerProvider({
|
|
181
|
+
// This provider is deliberately isolated from the application's
|
|
182
|
+
// global OTel pipeline, so it also needs its own explicit resource.
|
|
183
|
+
// Never trust OTEL_SERVICE_NAME here: a cross-environment Doppler
|
|
184
|
+
// value must not relabel prompt-bearing Langfuse observations.
|
|
185
|
+
resource: resourceFromAttributes({ "service.name": resolveLlmTracingServiceName(env) }),
|
|
163
186
|
spanProcessors: [{
|
|
164
187
|
onStart: (span) => {
|
|
165
188
|
// Directly pass the private parent claim to the processor. A
|
|
@@ -266,7 +289,7 @@ function createApiScorer(env, warn) {
|
|
|
266
289
|
username: env.LANGFUSE_PUBLIC_KEY,
|
|
267
290
|
password: env.LANGFUSE_SECRET_KEY,
|
|
268
291
|
});
|
|
269
|
-
return client
|
|
292
|
+
return client;
|
|
270
293
|
}
|
|
271
294
|
catch (error) {
|
|
272
295
|
warn(error, { stage: "score_init" });
|
|
@@ -281,15 +304,31 @@ function createApiScorer(env, warn) {
|
|
|
281
304
|
const api = await init();
|
|
282
305
|
if (!api)
|
|
283
306
|
return false;
|
|
284
|
-
|
|
285
|
-
id: body.id,
|
|
307
|
+
const payload = {
|
|
308
|
+
...(body.id ? { id: body.id } : {}),
|
|
286
309
|
traceId: body.traceId,
|
|
310
|
+
...(body.observationId ? { observationId: body.observationId } : {}),
|
|
287
311
|
name: body.name,
|
|
288
312
|
value: body.value,
|
|
289
|
-
dataType: body.dataType,
|
|
290
|
-
|
|
313
|
+
...(body.dataType ? { dataType: body.dataType } : {}),
|
|
314
|
+
...(typeof body.comment === "string" ? { comment: boundComment(body.comment) } : {}),
|
|
291
315
|
environment: resolveLlmTracingEnvironment(env),
|
|
292
|
-
}
|
|
316
|
+
};
|
|
317
|
+
const requestOptions = { timeoutInSeconds: 5, maxRetries: 0 };
|
|
318
|
+
if (body.timestamp) {
|
|
319
|
+
// POST /scores cannot carry a timestamp. Score ingestion remains
|
|
320
|
+
// supported in v4 (unlike trace/observation ingestion) and preserves
|
|
321
|
+
// the date component of the score's deduplication identity.
|
|
322
|
+
if (!body.id || !Number.isFinite(body.timestamp.getTime()))
|
|
323
|
+
return false;
|
|
324
|
+
const eventId = randomUUID();
|
|
325
|
+
const result = await api.ingestion.batch({ batch: [{
|
|
326
|
+
type: "score-create", id: eventId, timestamp: body.timestamp.toISOString(), body: payload,
|
|
327
|
+
}] }, requestOptions);
|
|
328
|
+
// A 207 is transport success even if the event was rejected.
|
|
329
|
+
return result.errors.length === 0 && result.successes.some((event) => event.id === eventId && event.status >= 200 && event.status < 300);
|
|
330
|
+
}
|
|
331
|
+
await api.scores.create(payload, requestOptions);
|
|
293
332
|
return true;
|
|
294
333
|
}
|
|
295
334
|
catch (error) {
|
|
@@ -398,6 +437,7 @@ export function createLlmTracingClient(env = process.env, options) {
|
|
|
398
437
|
emitter.emit({
|
|
399
438
|
kind,
|
|
400
439
|
traceSeed: body.traceId,
|
|
440
|
+
...(body.observationId ? { observationId: body.observationId } : {}),
|
|
401
441
|
name: body.name,
|
|
402
442
|
attributes: compact({
|
|
403
443
|
input: body.input,
|
|
@@ -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;
|