@oxygen-agent/cli 1.263.0 → 1.272.83
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/index.js +943 -39
- package/dist/knowledge-mirror.d.ts +106 -0
- package/dist/knowledge-mirror.js +375 -0
- package/dist/skills.d.ts +33 -4
- package/dist/skills.js +96 -19
- package/dist/update.d.ts +2 -2
- package/dist/update.js +4 -2
- package/node_modules/@oxygen/shared/dist/index.d.ts +3 -0
- package/node_modules/@oxygen/shared/dist/index.js +3 -0
- package/node_modules/@oxygen/shared/dist/knowledge-constants.d.ts +38 -0
- package/node_modules/@oxygen/shared/dist/knowledge-constants.js +137 -0
- package/node_modules/@oxygen/shared/dist/knowledge-links.d.ts +16 -0
- package/node_modules/@oxygen/shared/dist/knowledge-links.js +38 -0
- package/node_modules/@oxygen/shared/dist/knowledge-markdown.d.ts +23 -0
- package/node_modules/@oxygen/shared/dist/knowledge-markdown.js +57 -0
- package/node_modules/@oxygen/shared/dist/object-storage.d.ts +25 -0
- package/node_modules/@oxygen/shared/dist/object-storage.js +56 -0
- package/node_modules/@oxygen/shared/dist/version.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/skills.js
CHANGED
|
@@ -3,17 +3,67 @@ import { existsSync, readdirSync } from "node:fs";
|
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { join, resolve } from "node:path";
|
|
5
5
|
import { OxygenError, toFailure } from "@oxygen/shared";
|
|
6
|
-
import { defaultApiUrl, normalizeApiUrl } from "./credentials.js";
|
|
6
|
+
import { defaultApiUrl, loadCredentials, normalizeApiUrl } from "./credentials.js";
|
|
7
|
+
import { requestOxygen } from "./http-client.js";
|
|
7
8
|
import { resolveCmdShimInvocation } from "./windows-shim.js";
|
|
8
9
|
const DEFAULT_SKILL_AGENTS = ["codex", "claude-code", "cursor"];
|
|
9
10
|
const SKILL_INDEX_PATH = "/.well-known/skills/index.json";
|
|
10
|
-
|
|
11
|
+
const SKILLS_ACCESS_API_PATH = "/api/cli/skills/access";
|
|
12
|
+
/**
|
|
13
|
+
* Resolves where to install skills from. Logged-in CLIs mint a short-lived
|
|
14
|
+
* org-bound access URL (the skills surface is gated; anonymous serving is
|
|
15
|
+
* being retired). Any failure falls back to the plain well-known URL so
|
|
16
|
+
* installs never regress while older deployments are still in circulation.
|
|
17
|
+
*/
|
|
18
|
+
export async function resolveSkillsInstallSource(options) {
|
|
19
|
+
const publicSource = (reason) => ({
|
|
20
|
+
authenticated: false,
|
|
21
|
+
install_url: skillIndexUrl(options.apiUrl),
|
|
22
|
+
index_url: skillIndexUrl(options.apiUrl),
|
|
23
|
+
fallback_reason: reason,
|
|
24
|
+
});
|
|
25
|
+
let credentials;
|
|
26
|
+
try {
|
|
27
|
+
credentials = options.credentials !== undefined ? options.credentials : await loadCredentials();
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
const code = error instanceof OxygenError ? error.code : "unknown_error";
|
|
31
|
+
return publicSource(`credentials_unavailable:${code}`);
|
|
32
|
+
}
|
|
33
|
+
if (!credentials)
|
|
34
|
+
return publicSource("not_logged_in");
|
|
35
|
+
if (normalizeApiUrl(credentials.apiUrl) !== options.apiUrl)
|
|
36
|
+
return publicSource("api_url_mismatch");
|
|
37
|
+
try {
|
|
38
|
+
const access = await requestOxygen(SKILLS_ACCESS_API_PATH, {
|
|
39
|
+
method: "POST",
|
|
40
|
+
credentials,
|
|
41
|
+
...(options.fetch ? { fetch: options.fetch } : {}),
|
|
42
|
+
});
|
|
43
|
+
if (!access?.base_url?.trim() || !access?.index_url?.trim()) {
|
|
44
|
+
return publicSource("invalid_access_response");
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
authenticated: true,
|
|
48
|
+
install_url: access.base_url,
|
|
49
|
+
index_url: access.index_url,
|
|
50
|
+
...(access.expires_at ? { expires_at: access.expires_at } : {}),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
const code = error instanceof OxygenError ? error.code : "unknown_error";
|
|
55
|
+
return publicSource(`access_unavailable:${code}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
export async function listAgentSkills(options, runtime = {}) {
|
|
11
59
|
const apiUrl = normalizeApiUrl(readOption(options.apiUrl) ?? defaultApiUrl());
|
|
12
|
-
const
|
|
13
|
-
const index = await inspectSkillIndex(
|
|
60
|
+
const source = await resolveSkillsInstallSource({ apiUrl, ...runtime });
|
|
61
|
+
const index = await inspectSkillIndex(source.index_url, runtime.fetch);
|
|
14
62
|
const installed = detectInstalledAgentSkills();
|
|
15
63
|
return {
|
|
16
|
-
index_url:
|
|
64
|
+
index_url: source.index_url,
|
|
65
|
+
authenticated: source.authenticated,
|
|
66
|
+
...(source.fallback_reason ? { access_fallback: source.fallback_reason } : {}),
|
|
17
67
|
available: {
|
|
18
68
|
detected: index.reachable,
|
|
19
69
|
names: index.skill_names,
|
|
@@ -25,14 +75,16 @@ export async function listAgentSkills(options) {
|
|
|
25
75
|
install_command_args: recommendedSkillsInstallArgs(apiUrl),
|
|
26
76
|
};
|
|
27
77
|
}
|
|
28
|
-
export async function doctorAgentSkills(options) {
|
|
78
|
+
export async function doctorAgentSkills(options, runtime = {}) {
|
|
29
79
|
const apiUrl = normalizeApiUrl(readOption(options.apiUrl) ?? defaultApiUrl());
|
|
30
|
-
const
|
|
31
|
-
const index = await inspectSkillIndex(
|
|
80
|
+
const source = await resolveSkillsInstallSource({ apiUrl, ...runtime });
|
|
81
|
+
const index = await inspectSkillIndex(source.index_url, runtime.fetch);
|
|
32
82
|
const installer = inspectSkillsInstaller();
|
|
33
83
|
const installed = detectInstalledAgentSkills();
|
|
34
84
|
const data = {
|
|
35
|
-
index_url:
|
|
85
|
+
index_url: source.index_url,
|
|
86
|
+
authenticated: source.authenticated,
|
|
87
|
+
...(source.fallback_reason ? { access_fallback: source.fallback_reason } : {}),
|
|
36
88
|
index,
|
|
37
89
|
installer,
|
|
38
90
|
installed,
|
|
@@ -53,15 +105,19 @@ export async function doctorAgentSkills(options) {
|
|
|
53
105
|
}
|
|
54
106
|
return data;
|
|
55
107
|
}
|
|
56
|
-
export function installAgentSkills(// skipcq: JS-R1005
|
|
108
|
+
export async function installAgentSkills(// skipcq: JS-R1005
|
|
57
109
|
options, runtime = {}) {
|
|
58
110
|
const apiUrl = normalizeApiUrl(readOption(options.apiUrl) ?? defaultApiUrl());
|
|
59
|
-
const
|
|
111
|
+
const source = await resolveSkillsInstallSource({
|
|
112
|
+
apiUrl,
|
|
113
|
+
...(runtime.credentials !== undefined ? { credentials: runtime.credentials } : {}),
|
|
114
|
+
...(runtime.fetch ? { fetch: runtime.fetch } : {}),
|
|
115
|
+
});
|
|
60
116
|
const agents = readWords(options.agents ?? DEFAULT_SKILL_AGENTS);
|
|
61
117
|
const skill = readOption(options.skill) ?? "*";
|
|
62
118
|
const env = runtime.env ?? process.env;
|
|
63
119
|
const platform = runtime.platform ?? process.platform;
|
|
64
|
-
const args = installerAddArgs(
|
|
120
|
+
const args = installerAddArgs(source.install_url, agents, skill);
|
|
65
121
|
if (!options.project)
|
|
66
122
|
args.push("--global");
|
|
67
123
|
if (options.copy)
|
|
@@ -81,7 +137,8 @@ options, runtime = {}) {
|
|
|
81
137
|
const message = result.error instanceof Error ? result.error.message : null;
|
|
82
138
|
throw new OxygenError("skills_install_failed", "Unable to install Oxygen agent skills.", {
|
|
83
139
|
details: {
|
|
84
|
-
index_url:
|
|
140
|
+
index_url: source.index_url,
|
|
141
|
+
authenticated: source.authenticated,
|
|
85
142
|
executable: invocation.label,
|
|
86
143
|
exit_code: typeof result.status === "number" ? result.status : null,
|
|
87
144
|
stdout: stdout.slice(0, 2000),
|
|
@@ -91,8 +148,11 @@ options, runtime = {}) {
|
|
|
91
148
|
});
|
|
92
149
|
}
|
|
93
150
|
return {
|
|
94
|
-
indexUrl,
|
|
95
|
-
index_url:
|
|
151
|
+
indexUrl: source.index_url,
|
|
152
|
+
index_url: source.index_url,
|
|
153
|
+
install_url: source.install_url,
|
|
154
|
+
authenticated: source.authenticated,
|
|
155
|
+
...(source.fallback_reason ? { access_fallback: source.fallback_reason } : {}),
|
|
96
156
|
agents,
|
|
97
157
|
skill,
|
|
98
158
|
scope: options.project ? "project" : "global",
|
|
@@ -131,7 +191,7 @@ function wrapExecFileSync(exec) {
|
|
|
131
191
|
}
|
|
132
192
|
});
|
|
133
193
|
}
|
|
134
|
-
export function runAutomaticSkillsInstall(options = {}) {
|
|
194
|
+
export async function runAutomaticSkillsInstall(options = {}) {
|
|
135
195
|
const apiUrl = normalizeApiUrl(readOption(options.apiUrl) ?? defaultApiUrl());
|
|
136
196
|
const env = options.env ?? process.env;
|
|
137
197
|
const platform = options.platform ?? process.platform;
|
|
@@ -139,8 +199,23 @@ export function runAutomaticSkillsInstall(options = {}) {
|
|
|
139
199
|
return skippedAutomaticSkillsInstall("OXYGEN_SKIP_SKILLS=1", apiUrl);
|
|
140
200
|
}
|
|
141
201
|
const command = recommendedSkillsInstallCommand(apiUrl);
|
|
202
|
+
// The skills surface requires a logged-in identity; without credentials the
|
|
203
|
+
// unattended install would only produce a confusing installer error. A stale
|
|
204
|
+
// OXYGEN_PROFILE or unreadable credential store must not fail the parent
|
|
205
|
+
// command (e.g. `oxygen update`) — auto-install is strictly best-effort.
|
|
206
|
+
let credentials;
|
|
207
|
+
try {
|
|
208
|
+
credentials = options.credentials !== undefined ? options.credentials : await loadCredentials(env);
|
|
209
|
+
}
|
|
210
|
+
catch (error) {
|
|
211
|
+
const failure = toFailure("skills install", error);
|
|
212
|
+
return { attempted: false, ok: false, command, error: failure.error };
|
|
213
|
+
}
|
|
214
|
+
if (!credentials) {
|
|
215
|
+
return skippedAutomaticSkillsInstall("not_logged_in", apiUrl);
|
|
216
|
+
}
|
|
142
217
|
try {
|
|
143
|
-
installAgentSkills({
|
|
218
|
+
await installAgentSkills({
|
|
144
219
|
apiUrl,
|
|
145
220
|
agents: DEFAULT_SKILL_AGENTS,
|
|
146
221
|
skill: "*",
|
|
@@ -151,6 +226,8 @@ export function runAutomaticSkillsInstall(options = {}) {
|
|
|
151
226
|
}, {
|
|
152
227
|
env,
|
|
153
228
|
platform,
|
|
229
|
+
credentials,
|
|
230
|
+
...(options.fetch ? { fetch: options.fetch } : {}),
|
|
154
231
|
...(options.execFileSync ? { execFileSync: options.execFileSync } : {}),
|
|
155
232
|
...(options.spawnSync ? { spawnSync: options.spawnSync } : {}),
|
|
156
233
|
});
|
|
@@ -178,12 +255,12 @@ export function skippedAutomaticSkillsInstall(reason, apiUrl = normalizeApiUrl(d
|
|
|
178
255
|
reason,
|
|
179
256
|
};
|
|
180
257
|
}
|
|
181
|
-
async function inspectSkillIndex(indexUrl) {
|
|
258
|
+
async function inspectSkillIndex(indexUrl, fetchImpl = fetch) {
|
|
182
259
|
const controller = new AbortController();
|
|
183
260
|
const timeout = setTimeout(() => controller.abort(), 10_000);
|
|
184
261
|
timeout.unref?.();
|
|
185
262
|
try {
|
|
186
|
-
const response = await
|
|
263
|
+
const response = await fetchImpl(indexUrl, {
|
|
187
264
|
headers: { Accept: "application/json" },
|
|
188
265
|
signal: controller.signal,
|
|
189
266
|
});
|
package/dist/update.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
|
-
import { type AutomaticSkillsInstallResult, type ExecFileSyncLike } from "./skills.js";
|
|
2
|
+
import { type AutomaticSkillsInstallResult, type ExecFileSyncLike, type SkillsSourceRuntime } from "./skills.js";
|
|
3
3
|
type SpawnResult = ReturnType<typeof spawnSync>;
|
|
4
4
|
type SpawnSyncLike = (command: string, args: string[], options: {
|
|
5
5
|
encoding: BufferEncoding;
|
|
@@ -27,7 +27,7 @@ export declare function updateCli(options: UpdateOptions, runtime?: {
|
|
|
27
27
|
modulePath?: string;
|
|
28
28
|
spawnSync?: SpawnSyncLike;
|
|
29
29
|
execFileSync?: ExecFileSyncLike;
|
|
30
|
-
}): UpdateResult
|
|
30
|
+
} & SkillsSourceRuntime): Promise<UpdateResult>;
|
|
31
31
|
export declare function detectCliInstallPrefix(modulePath?: string): string | null;
|
|
32
32
|
export declare function detectCliInstallPrefixFromPath(modulePath: string): string | null;
|
|
33
33
|
export declare function resolveNpmExecutables(platform?: NodeJS.Platform): string[];
|
package/dist/update.js
CHANGED
|
@@ -5,7 +5,7 @@ import { runAutomaticSkillsInstall, skippedAutomaticSkillsInstall, } from "./ski
|
|
|
5
5
|
import { quoteWindowsCmdArg } from "./windows-shim.js";
|
|
6
6
|
const DEFAULT_CLI_PACKAGE_SPEC = "@oxygen-agent/cli@latest";
|
|
7
7
|
// skipcq: JS-R1005 — handles platform/install-mode/dry-run combinations end-to-end
|
|
8
|
-
export function updateCli(options, runtime = {}) {
|
|
8
|
+
export async function updateCli(options, runtime = {}) {
|
|
9
9
|
const env = runtime.env ?? process.env;
|
|
10
10
|
const platform = runtime.platform ?? process.platform;
|
|
11
11
|
const spawn = runtime.spawnSync ?? spawnSync;
|
|
@@ -52,9 +52,11 @@ export function updateCli(options, runtime = {}) {
|
|
|
52
52
|
command,
|
|
53
53
|
dry_run: false,
|
|
54
54
|
updated: true,
|
|
55
|
-
skills_install: runAutomaticSkillsInstall({
|
|
55
|
+
skills_install: await runAutomaticSkillsInstall({
|
|
56
56
|
env,
|
|
57
57
|
platform,
|
|
58
|
+
...(runtime.credentials !== undefined ? { credentials: runtime.credentials } : {}),
|
|
59
|
+
...(runtime.fetch ? { fetch: runtime.fetch } : {}),
|
|
58
60
|
...(runtime.execFileSync ? { execFileSync: runtime.execFileSync } : {}),
|
|
59
61
|
}),
|
|
60
62
|
};
|
|
@@ -10,6 +10,9 @@ export * from "./credit-guidance.js";
|
|
|
10
10
|
export * from "./directory.js";
|
|
11
11
|
export * from "./email-tracking-token.js";
|
|
12
12
|
export * from "./email-unsubscribe-token.js";
|
|
13
|
+
export * from "./knowledge-constants.js";
|
|
14
|
+
export * from "./knowledge-links.js";
|
|
15
|
+
export * from "./knowledge-markdown.js";
|
|
13
16
|
export * from "./linkedin-post-url.js";
|
|
14
17
|
export * from "./linkedin-sequences.js";
|
|
15
18
|
export * from "./networks.js";
|
|
@@ -10,6 +10,9 @@ export * from "./credit-guidance.js";
|
|
|
10
10
|
export * from "./directory.js";
|
|
11
11
|
export * from "./email-tracking-token.js";
|
|
12
12
|
export * from "./email-unsubscribe-token.js";
|
|
13
|
+
export * from "./knowledge-constants.js";
|
|
14
|
+
export * from "./knowledge-links.js";
|
|
15
|
+
export * from "./knowledge-markdown.js";
|
|
13
16
|
export * from "./linkedin-post-url.js";
|
|
14
17
|
export * from "./linkedin-sequences.js";
|
|
15
18
|
export * from "./networks.js";
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export declare const KNOWLEDGE_PAGE_TYPES: readonly ["playbook", "strategy", "campaign", "positioning", "persona", "competitor", "research_note", "message_playbook", "proposed_playbook_bullet", "voice", "brand", "other", "entity", "topic", "source_summary", "report", "schema"];
|
|
2
|
+
export type KnowledgePageType = typeof KNOWLEDGE_PAGE_TYPES[number];
|
|
3
|
+
export declare const KNOWLEDGE_META_PAGE_TYPES: readonly ["schema", "source_summary", "report"];
|
|
4
|
+
export declare const KNOWLEDGE_PAGE_STATUSES: readonly ["draft", "active", "archived"];
|
|
5
|
+
export type KnowledgePageStatus = typeof KNOWLEDGE_PAGE_STATUSES[number];
|
|
6
|
+
export declare const RESERVED_KNOWLEDGE_SLUGS: readonly ["index", "log", "schema", "company-profile", "readme"];
|
|
7
|
+
export declare const KNOWLEDGE_SLUG_MAX_LENGTH = 120;
|
|
8
|
+
export declare const KNOWLEDGE_SLUG_PATTERN: RegExp;
|
|
9
|
+
export declare function isValidKnowledgeSlug(value: string): boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Coerce arbitrary text (a title, a [[wikilink]] target) into slug form:
|
|
12
|
+
* lowercase, diacritics stripped, every non-alphanumeric run collapsed to one
|
|
13
|
+
* dash, trimmed, capped at KNOWLEDGE_SLUG_MAX_LENGTH. Returns "" when nothing
|
|
14
|
+
* survives — callers must validate with isValidKnowledgeSlug before writing.
|
|
15
|
+
*/
|
|
16
|
+
export declare function normalizeKnowledgeSlug(input: string): string;
|
|
17
|
+
/**
|
|
18
|
+
* The ONE resolution key for [[wikilink]] targets, shared by the tenant-db link
|
|
19
|
+
* recompute, the graph API, gap-fill, and the web click path. Strips an
|
|
20
|
+
* Obsidian-style |alias and #heading, then normalizes to slug form, so
|
|
21
|
+
* [[Founder Voice|our voice]] and [[founder-voice#tone]] both key to
|
|
22
|
+
* "founder-voice".
|
|
23
|
+
*/
|
|
24
|
+
export declare function knowledgeLinkKey(target: string): string;
|
|
25
|
+
export declare const KNOWLEDGE_LOG_EVENTS: readonly ["seed", "create", "update", "archive", "pin", "ingest", "synth", "lint", "relink", "approve", "reject", "query_filed", "note", "decision"];
|
|
26
|
+
export type KnowledgeLogEvent = typeof KNOWLEDGE_LOG_EVENTS[number];
|
|
27
|
+
export declare const KNOWLEDGE_EXTERNAL_LOG_EVENTS: readonly ["note", "query_filed", "decision"];
|
|
28
|
+
export type KnowledgeExternalLogEvent = typeof KNOWLEDGE_EXTERNAL_LOG_EVENTS[number];
|
|
29
|
+
export declare const KNOWLEDGE_LOG_ACTOR_KINDS: readonly ["user", "agent", "run"];
|
|
30
|
+
export type KnowledgeLogActorKind = typeof KNOWLEDGE_LOG_ACTOR_KINDS[number];
|
|
31
|
+
export declare const KNOWLEDGE_PAGE_SOURCE_KINDS: readonly ["message", "email_message", "meeting", "run", "workflow_run", "table", "table_row", "signal", "url", "manual"];
|
|
32
|
+
export type KnowledgePageSourceKind = typeof KNOWLEDGE_PAGE_SOURCE_KINDS[number];
|
|
33
|
+
export declare const KNOWLEDGE_PAGE_BODY_MAX_CHARS = 120000;
|
|
34
|
+
export declare const KNOWLEDGE_PAGE_SUMMARY_MAX_CHARS = 1000;
|
|
35
|
+
export declare const KNOWLEDGE_GRAPH_DEFAULT_MAX_NODES = 500;
|
|
36
|
+
export declare const KNOWLEDGE_GRAPH_MAX_NODES = 2000;
|
|
37
|
+
export declare const KNOWLEDGE_SYNC_MAX_PAGE_BATCH = 200;
|
|
38
|
+
export declare const RESERVED_KNOWLEDGE_FRONTMATTER_KEYS: readonly ["oxygen_page", "id", "slug", "type", "title", "status", "tags", "canonical", "summary", "revision", "updated_at", "web_url"];
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// Knowledge Graph (workspace wiki) shared constants — the single source of truth
|
|
2
|
+
// for page types, slug grammar, reserved names, log events, and caps across every
|
|
3
|
+
// surface (tenant-db write core, /api/cli/knowledge routes, CLI, MCP tools, web,
|
|
4
|
+
// sync mirror). The Knowledge Graph primitive evolves the legacy "context" store
|
|
5
|
+
// (ox_context.context_assets) into slug-addressed, wikilinked pages; these values
|
|
6
|
+
// must stay in lockstep with the 0103_knowledge_pages.sql CHECK constraints, so
|
|
7
|
+
// change them only together with a migration.
|
|
8
|
+
// All 17 page types: the 12 legacy context-asset types (unchanged wire values —
|
|
9
|
+
// existing rows, canonical pins, and playbookAssetId consumers keep working) plus
|
|
10
|
+
// the 5 wiki page types added by migration 0103.
|
|
11
|
+
export const KNOWLEDGE_PAGE_TYPES = [
|
|
12
|
+
"playbook",
|
|
13
|
+
"strategy",
|
|
14
|
+
"campaign",
|
|
15
|
+
"positioning",
|
|
16
|
+
"persona",
|
|
17
|
+
"competitor",
|
|
18
|
+
"research_note",
|
|
19
|
+
"message_playbook",
|
|
20
|
+
"proposed_playbook_bullet",
|
|
21
|
+
"voice",
|
|
22
|
+
"brand",
|
|
23
|
+
"other",
|
|
24
|
+
"entity",
|
|
25
|
+
"topic",
|
|
26
|
+
"source_summary",
|
|
27
|
+
"report",
|
|
28
|
+
"schema",
|
|
29
|
+
];
|
|
30
|
+
// Meta page types that describe the wiki itself (or summarize raw sources) rather
|
|
31
|
+
// than carrying company knowledge — excluded from AI retrieval presets (Phase 3).
|
|
32
|
+
export const KNOWLEDGE_META_PAGE_TYPES = ["schema", "source_summary", "report"];
|
|
33
|
+
export const KNOWLEDGE_PAGE_STATUSES = ["draft", "active", "archived"];
|
|
34
|
+
// Slugs the write path refuses (the scaffold seeder may create `schema`): they are
|
|
35
|
+
// computed projections or virtual pages, never ordinary rows.
|
|
36
|
+
export const RESERVED_KNOWLEDGE_SLUGS = [
|
|
37
|
+
"index",
|
|
38
|
+
"log",
|
|
39
|
+
"schema",
|
|
40
|
+
"company-profile",
|
|
41
|
+
"readme",
|
|
42
|
+
];
|
|
43
|
+
export const KNOWLEDGE_SLUG_MAX_LENGTH = 120;
|
|
44
|
+
// Flat kebab grammar: lowercase alphanumeric + dashes, no leading/trailing dash,
|
|
45
|
+
// no slashes (pages are a flat namespace; the graph, tags, and types organize).
|
|
46
|
+
export const KNOWLEDGE_SLUG_PATTERN = /^[a-z0-9][a-z0-9-]{0,119}$/;
|
|
47
|
+
export function isValidKnowledgeSlug(value) {
|
|
48
|
+
return KNOWLEDGE_SLUG_PATTERN.test(value);
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Coerce arbitrary text (a title, a [[wikilink]] target) into slug form:
|
|
52
|
+
* lowercase, diacritics stripped, every non-alphanumeric run collapsed to one
|
|
53
|
+
* dash, trimmed, capped at KNOWLEDGE_SLUG_MAX_LENGTH. Returns "" when nothing
|
|
54
|
+
* survives — callers must validate with isValidKnowledgeSlug before writing.
|
|
55
|
+
*/
|
|
56
|
+
export function normalizeKnowledgeSlug(input) {
|
|
57
|
+
const collapsed = input
|
|
58
|
+
.trim()
|
|
59
|
+
.toLowerCase()
|
|
60
|
+
.normalize("NFKD")
|
|
61
|
+
.replace(/[\u0300-\u036f]/gu, "")
|
|
62
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
63
|
+
.replace(/-{2,}/g, "-")
|
|
64
|
+
.replace(/^-+/, "")
|
|
65
|
+
.replace(/-+$/, "");
|
|
66
|
+
return collapsed.slice(0, KNOWLEDGE_SLUG_MAX_LENGTH).replace(/-+$/, "");
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* The ONE resolution key for [[wikilink]] targets, shared by the tenant-db link
|
|
70
|
+
* recompute, the graph API, gap-fill, and the web click path. Strips an
|
|
71
|
+
* Obsidian-style |alias and #heading, then normalizes to slug form, so
|
|
72
|
+
* [[Founder Voice|our voice]] and [[founder-voice#tone]] both key to
|
|
73
|
+
* "founder-voice".
|
|
74
|
+
*/
|
|
75
|
+
export function knowledgeLinkKey(target) {
|
|
76
|
+
const bare = (target.split("|")[0] ?? "").split("#")[0] ?? "";
|
|
77
|
+
return normalizeKnowledgeSlug(bare);
|
|
78
|
+
}
|
|
79
|
+
// Append-only knowledge_log event vocabulary. Must match the CHECK constraint in
|
|
80
|
+
// migration 0103 exactly; render/validation/descriptors derive from this list.
|
|
81
|
+
export const KNOWLEDGE_LOG_EVENTS = [
|
|
82
|
+
"seed",
|
|
83
|
+
"create",
|
|
84
|
+
"update",
|
|
85
|
+
"archive",
|
|
86
|
+
"pin",
|
|
87
|
+
"ingest",
|
|
88
|
+
"synth",
|
|
89
|
+
"lint",
|
|
90
|
+
"relink",
|
|
91
|
+
"approve",
|
|
92
|
+
"reject",
|
|
93
|
+
"query_filed",
|
|
94
|
+
"note",
|
|
95
|
+
"decision",
|
|
96
|
+
];
|
|
97
|
+
// The subset external agents may append directly (POST /knowledge/log): filings
|
|
98
|
+
// and free-form notes. Structural events are emitted only by the write core.
|
|
99
|
+
export const KNOWLEDGE_EXTERNAL_LOG_EVENTS = ["note", "query_filed", "decision"];
|
|
100
|
+
export const KNOWLEDGE_LOG_ACTOR_KINDS = ["user", "agent", "run"];
|
|
101
|
+
// Provenance source kinds for page_sources (CHECK in 0103): where a page's
|
|
102
|
+
// knowledge came from. Raw sources are cited by reference, never copied.
|
|
103
|
+
export const KNOWLEDGE_PAGE_SOURCE_KINDS = [
|
|
104
|
+
"message",
|
|
105
|
+
"email_message",
|
|
106
|
+
"meeting",
|
|
107
|
+
"run",
|
|
108
|
+
"workflow_run",
|
|
109
|
+
"table",
|
|
110
|
+
"table_row",
|
|
111
|
+
"signal",
|
|
112
|
+
"url",
|
|
113
|
+
"manual",
|
|
114
|
+
];
|
|
115
|
+
// Caps. Body cap is TS-enforced (the write core), not a DB CHECK, so it can move
|
|
116
|
+
// without a migration; graph/sync caps bound response payloads.
|
|
117
|
+
export const KNOWLEDGE_PAGE_BODY_MAX_CHARS = 120_000;
|
|
118
|
+
export const KNOWLEDGE_PAGE_SUMMARY_MAX_CHARS = 1_000;
|
|
119
|
+
export const KNOWLEDGE_GRAPH_DEFAULT_MAX_NODES = 500;
|
|
120
|
+
export const KNOWLEDGE_GRAPH_MAX_NODES = 2_000;
|
|
121
|
+
export const KNOWLEDGE_SYNC_MAX_PAGE_BATCH = 200;
|
|
122
|
+
// Frontmatter keys the deterministic renderer owns; user `data` keys colliding
|
|
123
|
+
// with these are skipped at render time (the stored jsonb keeps them).
|
|
124
|
+
export const RESERVED_KNOWLEDGE_FRONTMATTER_KEYS = [
|
|
125
|
+
"oxygen_page",
|
|
126
|
+
"id",
|
|
127
|
+
"slug",
|
|
128
|
+
"type",
|
|
129
|
+
"title",
|
|
130
|
+
"status",
|
|
131
|
+
"tags",
|
|
132
|
+
"canonical",
|
|
133
|
+
"summary",
|
|
134
|
+
"revision",
|
|
135
|
+
"updated_at",
|
|
136
|
+
"web_url",
|
|
137
|
+
];
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export type KnowledgeWikilink = {
|
|
2
|
+
/** Normalized resolution key (slug form) — matches pages by slug. */
|
|
3
|
+
key: string;
|
|
4
|
+
/** The bare target as written, alias/heading stripped, untrimmed case kept. */
|
|
5
|
+
target: string;
|
|
6
|
+
/** How many times this key occurs in the body. */
|
|
7
|
+
occurrences: number;
|
|
8
|
+
/** Character offset of the first occurrence's `[[`. */
|
|
9
|
+
firstPosition: number;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Extract every [[wikilink]] from a markdown body, deduplicated by resolution
|
|
13
|
+
* key with occurrence counts and first positions. Targets that normalize to an
|
|
14
|
+
* empty key (e.g. [[---]]) are dropped.
|
|
15
|
+
*/
|
|
16
|
+
export declare function extractKnowledgeLinks(body: string | null | undefined): KnowledgeWikilink[];
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// The ONE [[wikilink]] parser for the Knowledge Graph. The tenant-db write core
|
|
2
|
+
// recomputes page_links from it on every body write, the graph/lint APIs and the
|
|
3
|
+
// web vault renderer resolve through the same keys, so extraction semantics live
|
|
4
|
+
// here and nowhere else. The regex + alias/heading stripping mirror the vault
|
|
5
|
+
// component's original parser (apps/web/src/components/vault/vault.ts), which
|
|
6
|
+
// now delegates here — [[Target|alias]] and [[Target#heading]] both link to
|
|
7
|
+
// Target.
|
|
8
|
+
import { knowledgeLinkKey } from "./knowledge-constants.js";
|
|
9
|
+
const WIKILINK_RE = /\[\[([^\]]+)\]\]/g;
|
|
10
|
+
/**
|
|
11
|
+
* Extract every [[wikilink]] from a markdown body, deduplicated by resolution
|
|
12
|
+
* key with occurrence counts and first positions. Targets that normalize to an
|
|
13
|
+
* empty key (e.g. [[---]]) are dropped.
|
|
14
|
+
*/
|
|
15
|
+
export function extractKnowledgeLinks(body) {
|
|
16
|
+
if (!body)
|
|
17
|
+
return [];
|
|
18
|
+
const byKey = new Map();
|
|
19
|
+
WIKILINK_RE.lastIndex = 0;
|
|
20
|
+
let match;
|
|
21
|
+
while ((match = WIKILINK_RE.exec(body))) {
|
|
22
|
+
const inner = match[1] ?? "";
|
|
23
|
+
const target = (((inner.split("|")[0] ?? "").split("#")[0] ?? "")).trim();
|
|
24
|
+
if (!target)
|
|
25
|
+
continue;
|
|
26
|
+
const key = knowledgeLinkKey(target);
|
|
27
|
+
if (!key)
|
|
28
|
+
continue;
|
|
29
|
+
const existing = byKey.get(key);
|
|
30
|
+
if (existing) {
|
|
31
|
+
existing.occurrences += 1;
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
byKey.set(key, { key, target, occurrences: 1, firstPosition: match.index });
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return [...byKey.values()];
|
|
38
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export type KnowledgePageRenderInput = {
|
|
2
|
+
id: string;
|
|
3
|
+
slug: string;
|
|
4
|
+
type: string;
|
|
5
|
+
title: string;
|
|
6
|
+
status: string;
|
|
7
|
+
tags: string[];
|
|
8
|
+
canonical: boolean;
|
|
9
|
+
summary: string | null;
|
|
10
|
+
revision: number;
|
|
11
|
+
/** ISO timestamp string — pass a pre-formatted value for determinism. */
|
|
12
|
+
updatedAt: string;
|
|
13
|
+
webUrl: string;
|
|
14
|
+
/** Page frontmatter (the row's `data` jsonb). Only scalar values render. */
|
|
15
|
+
data: Record<string, unknown>;
|
|
16
|
+
body: string | null;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Render a page to its canonical markdown bytes: fixed-order reserved
|
|
20
|
+
* frontmatter, then the page's own scalar `data` keys sorted, then the body
|
|
21
|
+
* verbatim with exactly one trailing newline.
|
|
22
|
+
*/
|
|
23
|
+
export declare function renderKnowledgePageMarkdown(page: KnowledgePageRenderInput): string;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// Deterministic markdown rendering for Knowledge Graph pages. The SERVER renders
|
|
2
|
+
// every page to bytes with this function and hashes the result (sync/manifest
|
|
3
|
+
// content_sha256); sync clients write those bytes verbatim — the local mirror
|
|
4
|
+
// never re-renders, so renderer drift can't mass-quarantine mirrors. Pure string
|
|
5
|
+
// building (no node imports): safe for web bundles; hashing happens in the API
|
|
6
|
+
// layer. The frontmatter is intentionally parseable by the simple line-based
|
|
7
|
+
// parser in the vault component (key: value lines, tags as [a, b]).
|
|
8
|
+
import { RESERVED_KNOWLEDGE_FRONTMATTER_KEYS } from "./knowledge-constants.js";
|
|
9
|
+
const RESERVED_KEYS = new Set(RESERVED_KNOWLEDGE_FRONTMATTER_KEYS);
|
|
10
|
+
// Frontmatter values are single-line by contract: collapse whitespace runs
|
|
11
|
+
// (including newlines) so the line-based parser can never be broken by content.
|
|
12
|
+
function scalar(value) {
|
|
13
|
+
return value.replace(/\s+/g, " ").trim();
|
|
14
|
+
}
|
|
15
|
+
function renderDataValue(value) {
|
|
16
|
+
if (typeof value === "string")
|
|
17
|
+
return scalar(value);
|
|
18
|
+
if (typeof value === "number" && Number.isFinite(value))
|
|
19
|
+
return String(value);
|
|
20
|
+
if (typeof value === "boolean")
|
|
21
|
+
return value ? "true" : "false";
|
|
22
|
+
return null; // objects/arrays/null stay in the stored jsonb only
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Render a page to its canonical markdown bytes: fixed-order reserved
|
|
26
|
+
* frontmatter, then the page's own scalar `data` keys sorted, then the body
|
|
27
|
+
* verbatim with exactly one trailing newline.
|
|
28
|
+
*/
|
|
29
|
+
export function renderKnowledgePageMarkdown(page) {
|
|
30
|
+
const lines = [
|
|
31
|
+
"---",
|
|
32
|
+
"oxygen_page: true",
|
|
33
|
+
`id: ${scalar(page.id)}`,
|
|
34
|
+
`slug: ${scalar(page.slug)}`,
|
|
35
|
+
`type: ${scalar(page.type)}`,
|
|
36
|
+
`title: ${scalar(page.title)}`,
|
|
37
|
+
`status: ${scalar(page.status)}`,
|
|
38
|
+
`tags: [${page.tags.map((tag) => scalar(tag)).filter(Boolean).join(", ")}]`,
|
|
39
|
+
`canonical: ${page.canonical ? "true" : "false"}`,
|
|
40
|
+
`summary: ${page.summary ? scalar(page.summary) : ""}`,
|
|
41
|
+
`revision: ${Math.trunc(page.revision)}`,
|
|
42
|
+
`updated_at: ${scalar(page.updatedAt)}`,
|
|
43
|
+
`web_url: ${scalar(page.webUrl)}`,
|
|
44
|
+
];
|
|
45
|
+
const extraKeys = Object.keys(page.data)
|
|
46
|
+
.filter((key) => !RESERVED_KEYS.has(key) && /^[A-Za-z0-9_-]+$/.test(key))
|
|
47
|
+
.sort();
|
|
48
|
+
for (const key of extraKeys) {
|
|
49
|
+
const rendered = renderDataValue(page.data[key]);
|
|
50
|
+
if (rendered === null)
|
|
51
|
+
continue;
|
|
52
|
+
lines.push(`${key}: ${rendered}`);
|
|
53
|
+
}
|
|
54
|
+
lines.push("---", "");
|
|
55
|
+
const body = (page.body ?? "").replace(/\r\n/g, "\n").replace(/\n+$/, "");
|
|
56
|
+
return `${lines.join("\n")}\n${body}\n`;
|
|
57
|
+
}
|
|
@@ -3,7 +3,12 @@ export declare function buildImportObjectKey(input: {
|
|
|
3
3
|
organizationId: string;
|
|
4
4
|
fileName?: string | null;
|
|
5
5
|
}): string;
|
|
6
|
+
export declare function buildPublishingMediaObjectKey(input: {
|
|
7
|
+
organizationId: string;
|
|
8
|
+
fileName?: string | null;
|
|
9
|
+
}): string;
|
|
6
10
|
export declare function isImportObjectKeyForOrganization(key: string, organizationId: string): boolean;
|
|
11
|
+
export declare function isPublishingMediaObjectKeyForOrganization(key: string, organizationId: string): boolean;
|
|
7
12
|
export type PresignedImportUpload = {
|
|
8
13
|
uploadUrl: string;
|
|
9
14
|
bucket: string;
|
|
@@ -12,20 +17,40 @@ export type PresignedImportUpload = {
|
|
|
12
17
|
provider: "s3";
|
|
13
18
|
expiresInSeconds: number;
|
|
14
19
|
};
|
|
20
|
+
export type PresignedPublishingMediaUpload = PresignedImportUpload;
|
|
15
21
|
export declare function presignImportUpload(input: {
|
|
16
22
|
organizationId: string;
|
|
17
23
|
fileName?: string | null;
|
|
18
24
|
contentType?: string | null;
|
|
19
25
|
contentLength: number;
|
|
20
26
|
}): Promise<PresignedImportUpload>;
|
|
27
|
+
export declare function presignPublishingMediaUpload(input: {
|
|
28
|
+
organizationId: string;
|
|
29
|
+
fileName?: string | null;
|
|
30
|
+
contentType?: string | null;
|
|
31
|
+
contentLength: number;
|
|
32
|
+
}): Promise<PresignedPublishingMediaUpload>;
|
|
21
33
|
export declare function getImportObjectMetadata(input: {
|
|
22
34
|
storageKey: string;
|
|
23
35
|
}): Promise<{
|
|
24
36
|
contentLength: number | null;
|
|
25
37
|
}>;
|
|
38
|
+
export declare function getPublishingMediaObjectMetadata(input: {
|
|
39
|
+
storageKey: string;
|
|
40
|
+
}): Promise<{
|
|
41
|
+
contentLength: number | null;
|
|
42
|
+
contentType: string | null;
|
|
43
|
+
}>;
|
|
26
44
|
export declare function openImportObjectStream(input: {
|
|
27
45
|
storageKey: string;
|
|
28
46
|
}): Promise<NodeJS.ReadableStream>;
|
|
47
|
+
export declare function openPublishingMediaObjectStream(input: {
|
|
48
|
+
storageKey: string;
|
|
49
|
+
}): Promise<NodeJS.ReadableStream>;
|
|
50
|
+
export declare function presignPublishingMediaDownload(input: {
|
|
51
|
+
storageKey: string;
|
|
52
|
+
expiresInSeconds?: number;
|
|
53
|
+
}): Promise<string>;
|
|
29
54
|
export declare function deleteImportObject(input: {
|
|
30
55
|
storageKey: string;
|
|
31
56
|
}): Promise<void>;
|