@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/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
- export async function listAgentSkills(options) {
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 indexUrl = skillIndexUrl(apiUrl);
13
- const index = await inspectSkillIndex(indexUrl);
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: indexUrl,
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 indexUrl = skillIndexUrl(apiUrl);
31
- const index = await inspectSkillIndex(indexUrl);
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: indexUrl,
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 indexUrl = skillIndexUrl(apiUrl);
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(indexUrl, agents, skill);
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: indexUrl,
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: indexUrl,
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 fetch(indexUrl, {
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>;