@oxygen-agent/cli 1.936.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 +30 -2
- package/dist/functions-commands.js +13 -5
- package/dist/help.js +2 -0
- package/dist/index.js +1509 -290
- 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 +470 -15
- package/node_modules/@oxygen/recipe-sdk/dist/index.d.ts +2 -0
- package/node_modules/@oxygen/shared/dist/byok-connect.d.ts +11 -6
- package/node_modules/@oxygen/shared/dist/byok-connect.js +14 -6
- package/node_modules/@oxygen/shared/dist/capability-discovery.d.ts +8 -0
- package/node_modules/@oxygen/shared/dist/capability-discovery.js +152 -20
- 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/email-dsn.d.ts +60 -0
- package/node_modules/@oxygen/shared/dist/email-dsn.js +120 -0
- package/node_modules/@oxygen/shared/dist/email-warmup-readiness.d.ts +64 -0
- package/node_modules/@oxygen/shared/dist/email-warmup-readiness.js +90 -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 +10 -0
- package/node_modules/@oxygen/shared/dist/index.js +10 -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 +56 -48
- package/node_modules/@oxygen/shared/dist/knowledge-bootstrap.js +50 -49
- 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 +8 -3
- package/node_modules/@oxygen/shared/dist/langfuse.js +177 -130
- package/node_modules/@oxygen/shared/dist/llm-payload.d.ts +10 -0
- package/node_modules/@oxygen/shared/dist/llm-payload.js +54 -0
- package/node_modules/@oxygen/shared/dist/llm-usage.d.ts +11 -0
- package/node_modules/@oxygen/shared/dist/llm-usage.js +30 -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-core.d.ts +98 -0
- package/node_modules/@oxygen/shared/dist/product-analytics-core.js +159 -0
- package/node_modules/@oxygen/shared/dist/product-analytics-environment.d.ts +18 -0
- package/node_modules/@oxygen/shared/dist/product-analytics-environment.js +46 -0
- package/node_modules/@oxygen/shared/dist/product-analytics-events.d.ts +116 -0
- package/node_modules/@oxygen/shared/dist/product-analytics-events.js +120 -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 +29 -1
- 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/node_modules/@oxygen/workflows/dist/graph/lint.js +22 -0
- package/package.json +2 -1
|
@@ -2,28 +2,35 @@
|
|
|
2
2
|
* Knowledge bootstrap — the once-per-workspace, automatic company research pass.
|
|
3
3
|
*
|
|
4
4
|
* WHAT IT IS. Exactly once in a workspace's life, OXYGEN researches the customer's
|
|
5
|
-
* OWN company from the domain
|
|
6
|
-
* (control-DB `organizations.iconDomain
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
5
|
+
* OWN company from the domain the creator typed when the workspace was created
|
|
6
|
+
* (control-DB `organizations.iconDomain`), then fills the typed company profile
|
|
7
|
+
* (`ox_context.company_profile`) and writes one cited wiki page. A founder who
|
|
8
|
+
* signs up should not face an empty Knowledge Graph and have to type their own
|
|
9
|
+
* positioning back at us; every grounded AI action downstream (message drafts, AI
|
|
10
|
+
* columns, agent runs) reads that profile, so an empty one degrades the whole
|
|
11
|
+
* product's first hour.
|
|
12
12
|
*
|
|
13
13
|
* WHY THIS IS NOT A VIOLATION OF THE PAID-ACTIONS RULE. `CLAUDE.md` says never run
|
|
14
14
|
* paid provider actions unless the user explicitly asks. This slice is a deliberate,
|
|
15
|
-
* founder-approved exception:
|
|
16
|
-
* armed table-webhook auto-run
|
|
17
|
-
*
|
|
18
|
-
*
|
|
15
|
+
* founder-approved exception: naming the company at workspace creation IS the
|
|
16
|
+
* standing authorization, the same way an armed table-webhook auto-run
|
|
17
|
+
* configuration is scoped standing permission for the columns it queues — and
|
|
18
|
+
* since 2026-09-11 the pass is OXYGEN-funded, so it never draws down the
|
|
19
|
+
* customer's balance at all. The exception is defensible ONLY because every one of
|
|
20
|
+
* the following properties holds. They are load-bearing — do not drop one for
|
|
19
21
|
* convenience, and if you remove one, the exception no longer stands:
|
|
20
22
|
*
|
|
21
23
|
* 1. CAPPED — `KNOWLEDGE_BOOTSTRAP_MAX_CREDITS` is a hard ceiling on the whole
|
|
22
24
|
* pass, with `KNOWLEDGE_BOOTSTRAP_ENRICHMENT_MAX_CREDITS` a tighter
|
|
23
|
-
* sub-ceiling on the provider (external-money) half.
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
25
|
+
* sub-ceiling on the provider (external-money) half. The cap bounds
|
|
26
|
+
* OXYGEN's own money now, and it is the same number.
|
|
27
|
+
* 2. AUTHORIZED — it runs only for a workspace whose creator typed the company
|
|
28
|
+
* website into the REQUIRED field at workspace creation; that
|
|
29
|
+
* entry is recorded as `knowledge_bootstrap_consent` (source
|
|
30
|
+
* `workspace_creation`) and read FAIL-CLOSED, so a workspace that
|
|
31
|
+
* predates the field is never researched and a personal-email
|
|
32
|
+
* signup with no website never reaches the worker. We never
|
|
33
|
+
* research a stranger.
|
|
27
34
|
* 3. JOURNALLED — the marker below records status, timing, run id, credits and the
|
|
28
35
|
* domain in `knowledge_state.watermarks`; the URLs read land as
|
|
29
36
|
* immutable `page_sources` rows plus an `ingest` knowledge_log
|
|
@@ -36,33 +43,14 @@
|
|
|
36
43
|
* ordinary revisioned wiki writes a human can revert.
|
|
37
44
|
* 6. EXACTLY-ONCE — the marker is claimed with a conditional UPDATE, so N worker
|
|
38
45
|
* replicas polling one tenant produce one run, never N runs.
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
* Happy path = 1 serper.search (5) + 1 exa.contents over <= 5 URLs (5)
|
|
48
|
-
* + 1 medium synthesis pass (75) = 85 cr
|
|
49
|
-
* Enrichment sub-cap 60 cr — ~6x the 10 cr the happy path needs, so a waterfall
|
|
50
|
-
* retry or a second search fits, and a runaway
|
|
51
|
-
* per-URL fetch loop is stopped long before the
|
|
52
|
-
* overall ceiling.
|
|
53
|
-
* Hard cap 300 cr — 85 cr nominal plus room for two retried synthesis
|
|
54
|
-
* passes. 3% of the 10,000-credit signup grant
|
|
55
|
-
* (FREE_SIGNUP_GRANT_CREDITS, ./billing.ts), so the
|
|
56
|
-
* pass can never eat a founder's trial.
|
|
57
|
-
*
|
|
58
|
-
* The obvious implementation — firecrawl.map + 3x firecrawl.scrape — costs
|
|
59
|
-
* 4 x 40 = 160 cr for the SAME job, because Firecrawl bills per page. exa.contents
|
|
60
|
-
* is priced per call and is built for fetching text at known URLs; that is why the
|
|
61
|
-
* read budget is a page COUNT (`KNOWLEDGE_BOOTSTRAP_MAX_PAGES_READ`) inside one
|
|
62
|
-
* call rather than a call count.
|
|
63
|
-
*
|
|
64
|
-
* This module is a leaf: constants and types only, no DB and no provider imports, so
|
|
65
|
-
* the worker, tenant-db, CLI, and API surfaces all bind to one contract.
|
|
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.
|
|
50
|
+
*
|
|
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.
|
|
66
54
|
*/
|
|
67
55
|
/** Hard ceiling on managed credits ONE workspace bootstrap may spend, all phases. */
|
|
68
56
|
export const KNOWLEDGE_BOOTSTRAP_MAX_CREDITS = 300;
|
|
@@ -132,12 +120,14 @@ export function isKnowledgeBootstrapStatus(value) {
|
|
|
132
120
|
KNOWLEDGE_BOOTSTRAP_STATUSES.includes(value));
|
|
133
121
|
}
|
|
134
122
|
// ---------------------------------------------------------------------------
|
|
135
|
-
//
|
|
123
|
+
// Authorization (property 2 of the exception above, made explicit)
|
|
136
124
|
// ---------------------------------------------------------------------------
|
|
137
125
|
/**
|
|
138
|
-
* Control-DB `organizations.metadata` key carrying the workspace's recorded
|
|
139
|
-
*
|
|
140
|
-
*
|
|
126
|
+
* Control-DB `organizations.metadata` key carrying the workspace's recorded
|
|
127
|
+
* authorization for the automatic research pass. Written once, when the creator
|
|
128
|
+
* names the company website at workspace creation (`source: "workspace_creation"`;
|
|
129
|
+
* rows written by the retired `/setup` checkbox carry `source: "signup"` and stay
|
|
130
|
+
* valid); read by the worker cycle before anything is spent.
|
|
141
131
|
*
|
|
142
132
|
* It lives in shared, not in either caller, so the writer and the reader cannot
|
|
143
133
|
* drift onto two different key names — a silent drift there would either deny every
|
|
@@ -170,6 +160,8 @@ export function readKnowledgeBootstrapConsent(metadata) {
|
|
|
170
160
|
return null;
|
|
171
161
|
return {
|
|
172
162
|
granted_at: grantedAt,
|
|
163
|
+
...(record.scope === "company_and_creator" ? { scope: "company_and_creator" } : {}),
|
|
164
|
+
...(typeof record.domain === "string" ? { domain: record.domain } : {}),
|
|
173
165
|
source: typeof record.source === "string" && record.source.length > 0 ? record.source : null,
|
|
174
166
|
granted_by_clerk_user_id: typeof record.granted_by_clerk_user_id === "string" && record.granted_by_clerk_user_id.length > 0
|
|
175
167
|
? record.granted_by_clerk_user_id
|
|
@@ -208,6 +200,14 @@ export const KNOWLEDGE_BOOTSTRAP_LINKEDIN_METADATA_KEY = "company_linkedin_url";
|
|
|
208
200
|
* dead and the workspace is recoverable through the product.
|
|
209
201
|
*/
|
|
210
202
|
export const KNOWLEDGE_BOOTSTRAP_STUCK_AFTER_MS = 60 * 60 * 1000;
|
|
203
|
+
/**
|
|
204
|
+
* Idempotency-key prefix for the ledger row that hands an automatic pass's
|
|
205
|
+
* credits back to the workspace (property 7 above). The key is
|
|
206
|
+
* `${prefix}:${run_id}`, so one pass is covered exactly once no matter how many
|
|
207
|
+
* times the worker revisits the outcome, and a `--force` re-run — a different
|
|
208
|
+
* run — is covered only if it was itself automatic.
|
|
209
|
+
*/
|
|
210
|
+
export const KNOWLEDGE_BOOTSTRAP_COVERAGE_IDEMPOTENCY_PREFIX = "knowledge_bootstrap_coverage";
|
|
211
211
|
// ---------------------------------------------------------------------------
|
|
212
212
|
// Write targets (what a bootstrap actually changes)
|
|
213
213
|
// ---------------------------------------------------------------------------
|
|
@@ -222,13 +222,14 @@ export const KNOWLEDGE_BOOTSTRAP_STUCK_AFTER_MS = 60 * 60 * 1000;
|
|
|
222
222
|
*/
|
|
223
223
|
export const KNOWLEDGE_BOOTSTRAP_RESEARCH_PAGE_SLUG = "company-research";
|
|
224
224
|
/**
|
|
225
|
-
* The typed `company_profile` sections
|
|
226
|
-
* "never clobber a profile a human already filled" gate
|
|
225
|
+
* The typed `company_profile` sections inspected by the automatic bootstrap's
|
|
226
|
+
* "never clobber a profile a human already filled" gate.
|
|
227
227
|
*
|
|
228
228
|
* `gtm_stack` and `custom` are structurally excluded: research of a company's public
|
|
229
229
|
* web presence says nothing about which tools it has connected here, and a workspace
|
|
230
230
|
* that filled only those has told us nothing about its company and must still be
|
|
231
|
-
* bootstrapped.
|
|
231
|
+
* bootstrapped. This is the existing-context guard's list, not a list of writes:
|
|
232
|
+
* current research writes company facts and stores inferred GTM hypotheses separately.
|
|
232
233
|
*/
|
|
233
234
|
export const KNOWLEDGE_BOOTSTRAP_PROFILE_SECTIONS = [
|
|
234
235
|
"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
|
+
}
|
|
@@ -39,6 +39,7 @@ export type LlmSpanBody = {
|
|
|
39
39
|
};
|
|
40
40
|
export type LlmGenerationBody = LlmSpanBody & {
|
|
41
41
|
model?: string | null;
|
|
42
|
+
modelParameters?: Record<string, unknown>;
|
|
42
43
|
completionStartTime?: Date | null;
|
|
43
44
|
usageDetails?: Record<string, number>;
|
|
44
45
|
costDetails?: Record<string, number>;
|
|
@@ -74,6 +75,7 @@ export type LlmTracingClient = {
|
|
|
74
75
|
trace(body: LlmTraceBody): void;
|
|
75
76
|
span(body: LlmSpanBody): void;
|
|
76
77
|
generation(body: LlmGenerationBody): void;
|
|
78
|
+
embedding(body: LlmGenerationBody): void;
|
|
77
79
|
event(body: LlmEventBody): void;
|
|
78
80
|
/**
|
|
79
81
|
* Attach a score to an existing trace.
|
|
@@ -90,9 +92,9 @@ export type LlmTracingClient = {
|
|
|
90
92
|
* describes.
|
|
91
93
|
*/
|
|
92
94
|
score(body: LlmScoreBody): Promise<boolean>;
|
|
93
|
-
/** Never rejects; bounded at
|
|
95
|
+
/** Never rejects; bounded at 5s on workers, 15s in serverless after(). */
|
|
94
96
|
flush(): Promise<void>;
|
|
95
|
-
/** Flush + stop
|
|
97
|
+
/** Flush + stop timers. Same worker/serverless bound as flush(). */
|
|
96
98
|
shutdown(): Promise<void>;
|
|
97
99
|
};
|
|
98
100
|
/**
|
|
@@ -114,7 +116,7 @@ export declare function resolveLlmTracingEnvironment(env?: EnvMap): string;
|
|
|
114
116
|
* the SDK's own implementation so the two can never drift.
|
|
115
117
|
*/
|
|
116
118
|
export declare function llmTraceIdForSeed(seed: string): string;
|
|
117
|
-
export type LlmEmissionKind = "span" | "generation" | "event";
|
|
119
|
+
export type LlmEmissionKind = "span" | "generation" | "embedding" | "event";
|
|
118
120
|
/** v5 correlating attributes, propagated onto the emitted observation. */
|
|
119
121
|
export type LlmCorrelation = {
|
|
120
122
|
traceName?: string;
|
|
@@ -124,6 +126,9 @@ export type LlmCorrelation = {
|
|
|
124
126
|
};
|
|
125
127
|
export type LlmEmission = {
|
|
126
128
|
kind: LlmEmissionKind;
|
|
129
|
+
observationId?: string;
|
|
130
|
+
parentObservationId?: string;
|
|
131
|
+
isRoot?: boolean;
|
|
127
132
|
/** External seed (turn/run id) — hashed into the W3C trace id. */
|
|
128
133
|
traceSeed: string;
|
|
129
134
|
name: string;
|