@hasna/knowledge 0.2.21 → 0.2.23

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.
@@ -120,7 +120,7 @@ var MCP_HTTP_SERVICE_NAME = "knowledge", DEFAULT_MCP_HTTP_PORT = 8819;
120
120
  var init_mcp_http = () => {};
121
121
 
122
122
  // src/mcp.js
123
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
123
+ import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
124
124
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
125
125
 
126
126
  // node_modules/zod/v4/classic/external.js
@@ -13660,7 +13660,7 @@ import { existsSync as existsSync8, readFileSync as readFileSync8, writeFileSync
13660
13660
  // package.json
13661
13661
  var package_default = {
13662
13662
  name: "@hasna/knowledge",
13663
- version: "0.2.21",
13663
+ version: "0.2.23",
13664
13664
  description: "Agent-friendly local knowledge CLI with JSON output, pagination, and safe destructive actions",
13665
13665
  type: "module",
13666
13666
  bin: {
@@ -13723,9 +13723,8 @@ var package_default = {
13723
13723
  }
13724
13724
  };
13725
13725
 
13726
- // src/store.ts
13727
- import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync as existsSync2, renameSync, unlinkSync } from "fs";
13728
- import { randomUUID } from "crypto";
13726
+ // src/knowledge-db.ts
13727
+ import { Database } from "bun:sqlite";
13729
13728
 
13730
13729
  // src/workspace.ts
13731
13730
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
@@ -13855,414 +13854,37 @@ function writeKnowledgeConfig(path, config2) {
13855
13854
  `);
13856
13855
  }
13857
13856
 
13858
- // src/store.ts
13859
- function defaultStorePath() {
13860
- return workspaceForHome(globalKnowledgeHome()).jsonStorePath;
13861
- }
13862
- function ensureStore(path) {
13863
- if (!existsSync2(path)) {
13864
- ensureParentDir(path);
13865
- if (path === defaultStorePath() && existsSync2(legacyGlobalStorePath())) {
13866
- writeFileSync2(path, readFileSync2(legacyGlobalStorePath(), "utf8"));
13867
- } else {
13868
- writeFileSync2(path, JSON.stringify({ items: [] }, null, 2));
13869
- }
13870
- }
13871
- }
13872
- function lockPath(path) {
13873
- return `${path}.lock`;
13874
- }
13875
- function acquireLock(lockPath2, ownerId) {
13876
- const maxWait = 5000;
13877
- const interval = 50;
13878
- const start = Date.now();
13879
- while (Date.now() - start < maxWait) {
13880
- try {
13881
- if (!existsSync2(lockPath2)) {
13882
- writeFileSync2(lockPath2, JSON.stringify({ owner: ownerId, ts: Date.now() }));
13883
- return;
13884
- }
13885
- const lock = JSON.parse(readFileSync2(lockPath2, "utf8"));
13886
- if (Date.now() - lock.ts > 1e4) {
13887
- unlinkSync(lockPath2);
13888
- }
13889
- } catch {}
13890
- const start2 = Date.now();
13891
- while (Date.now() - start2 < interval) {}
13892
- }
13893
- throw new Error(`Could not acquire lock on ${lockPath2} after ${maxWait}ms`);
13894
- }
13895
- function releaseLock(lockPath2, ownerId) {
13896
- try {
13897
- if (existsSync2(lockPath2)) {
13898
- const lock = JSON.parse(readFileSync2(lockPath2, "utf8"));
13899
- if (lock.owner === ownerId) {
13900
- unlinkSync(lockPath2);
13901
- }
13902
- }
13903
- } catch {}
13904
- }
13905
- function loadStore(path) {
13906
- ensureStore(path);
13907
- const raw = readFileSync2(path, "utf8");
13908
- const parsed = JSON.parse(raw);
13909
- if (!parsed || !Array.isArray(parsed.items)) {
13910
- return { items: [] };
13911
- }
13912
- return parsed;
13913
- }
13914
- function saveStore(path, store) {
13915
- const tmp = `${path}.tmp.${randomUUID()}`;
13916
- writeFileSync2(tmp, JSON.stringify(store, null, 2));
13917
- renameSync(tmp, path);
13918
- }
13919
- function withLock(path, fn) {
13920
- const owner = randomUUID();
13921
- const lpath = lockPath(path);
13922
- acquireLock(lpath, owner);
13923
- try {
13924
- return fn();
13925
- } finally {
13926
- releaseLock(lpath, owner);
13927
- }
13928
- }
13929
- function makeId() {
13930
- return `k_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
13931
- }
13857
+ // src/knowledge-db.ts
13858
+ var MIGRATION_1 = `
13859
+ PRAGMA journal_mode = WAL;
13860
+ PRAGMA foreign_keys = ON;
13932
13861
 
13933
- // src/source-ref.ts
13934
- function assertNonEmpty(value, message) {
13935
- if (!value)
13936
- throw new Error(message);
13937
- return value;
13938
- }
13939
- function parseOpenFilesRef(uri) {
13940
- const withoutScheme = uri.slice("open-files://".length);
13941
- const parts = withoutScheme.split("/").filter(Boolean);
13942
- const entity = parts[0];
13943
- if (entity !== "file" && entity !== "source") {
13944
- throw new Error("Invalid open-files ref. Expected open-files://file/<id>, open-files://file/<id>/revision/<revision_id>, or open-files://source/<id>/path/<path>.");
13945
- }
13946
- const id = assertNonEmpty(parts[1], "Invalid open-files ref. Missing id.");
13947
- if (entity === "file") {
13948
- if (parts.length === 2)
13949
- return { kind: "open-files", uri, entity, id };
13950
- if (parts[2] === "revision" && parts[3] && parts.length === 4) {
13951
- return { kind: "open-files", uri, entity, id, revision_id: decodeURIComponent(parts[3]) };
13952
- }
13953
- throw new Error("Invalid open-files file ref. Expected open-files://file/<id>/revision/<revision_id>.");
13954
- }
13955
- const pathIndex = parts.indexOf("path");
13956
- const path = pathIndex >= 0 ? decodeURIComponent(parts.slice(pathIndex + 1).join("/")) : undefined;
13957
- return { kind: "open-files", uri, entity, id, path };
13958
- }
13959
- function parseS3Ref(uri) {
13960
- const parsed = new URL(uri);
13961
- const bucket = assertNonEmpty(parsed.hostname, "Invalid s3 ref. Missing bucket.");
13962
- const key = decodeURIComponent(parsed.pathname.replace(/^\/+/, ""));
13963
- if (!key)
13964
- throw new Error("Invalid s3 ref. Missing object key.");
13965
- return { kind: "s3", uri, bucket, key };
13966
- }
13967
- function parseFileRef(uri) {
13968
- const parsed = new URL(uri);
13969
- return { kind: "file", uri, path: decodeURIComponent(parsed.pathname) };
13970
- }
13971
- function parseWebRef(uri) {
13972
- const parsed = new URL(uri);
13973
- return { kind: "web", uri, url: parsed.toString() };
13974
- }
13975
- function parseSourceRef(uri) {
13976
- if (uri.startsWith("open-files://"))
13977
- return parseOpenFilesRef(uri);
13978
- if (uri.startsWith("s3://"))
13979
- return parseS3Ref(uri);
13980
- if (uri.startsWith("file://"))
13981
- return parseFileRef(uri);
13982
- if (uri.startsWith("https://") || uri.startsWith("http://"))
13983
- return parseWebRef(uri);
13984
- throw new Error(`Unsupported source ref scheme: ${uri}`);
13985
- }
13986
- function catalogSourceUriForRef(uri, parsed = parseSourceRef(uri)) {
13987
- if (parsed.kind === "open-files" && parsed.entity === "file" && parsed.revision_id) {
13988
- return uri.replace(/\/revision\/[^/]+$/, "");
13989
- }
13990
- return uri;
13991
- }
13992
- function revisionIdForSourceRef(uri) {
13993
- const parsed = parseSourceRef(uri);
13994
- return parsed.kind === "open-files" && parsed.entity === "file" ? parsed.revision_id ?? null : null;
13995
- }
13862
+ CREATE TABLE IF NOT EXISTS schema_versions (
13863
+ version INTEGER PRIMARY KEY,
13864
+ applied_at TEXT NOT NULL
13865
+ );
13996
13866
 
13997
- // src/artifact-store.ts
13998
- import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
13999
- import { dirname as dirname2, join as join2, relative, sep } from "path";
14000
- function normalizeArtifactKey(key) {
14001
- const raw = key.replace(/\\/g, "/").trim();
14002
- if (!raw || raw.startsWith("/")) {
14003
- throw new Error(`Invalid artifact key: ${key}`);
14004
- }
14005
- const segments = raw.split("/").filter(Boolean);
14006
- if (segments.length === 0 || segments.some((segment) => segment === "." || segment === "..")) {
14007
- throw new Error(`Invalid artifact key: ${key}`);
14008
- }
14009
- return segments.join("/");
14010
- }
14011
- function assertInside(root, target) {
14012
- const rel = relative(root, target);
14013
- if (rel.startsWith("..") || rel === ".." || rel.startsWith(`..${sep}`)) {
14014
- throw new Error(`Artifact path escapes root: ${target}`);
14015
- }
14016
- }
13867
+ CREATE TABLE IF NOT EXISTS sources (
13868
+ id TEXT PRIMARY KEY,
13869
+ uri TEXT NOT NULL UNIQUE,
13870
+ kind TEXT NOT NULL,
13871
+ title TEXT,
13872
+ metadata_json TEXT NOT NULL DEFAULT '{}',
13873
+ acl_json TEXT NOT NULL DEFAULT '{}',
13874
+ created_at TEXT NOT NULL,
13875
+ updated_at TEXT NOT NULL
13876
+ );
14017
13877
 
14018
- class LocalArtifactStore {
14019
- root;
14020
- type = "local";
14021
- canRead = true;
14022
- canWrite = true;
14023
- constructor(root) {
14024
- this.root = root;
14025
- mkdirSync2(root, { recursive: true });
14026
- }
14027
- async put(entry) {
14028
- const key = normalizeArtifactKey(entry.key);
14029
- const path = join2(this.root, key);
14030
- assertInside(this.root, path);
14031
- mkdirSync2(dirname2(path), { recursive: true });
14032
- writeFileSync3(path, entry.body);
14033
- return { key, uri: `file://${path}` };
14034
- }
14035
- async getText(key) {
14036
- const normalizedKey = normalizeArtifactKey(key);
14037
- const path = join2(this.root, normalizedKey);
14038
- assertInside(this.root, path);
14039
- return readFileSync3(path, "utf8");
14040
- }
14041
- async exists(key) {
14042
- const normalizedKey = normalizeArtifactKey(key);
14043
- const path = join2(this.root, normalizedKey);
14044
- assertInside(this.root, path);
14045
- return existsSync3(path);
14046
- }
14047
- }
14048
-
14049
- class S3ArtifactStore {
14050
- options;
14051
- type = "s3";
14052
- canRead = true;
14053
- canWrite = true;
14054
- client;
14055
- constructor(options) {
14056
- this.options = options;
14057
- this.client = options.client;
14058
- }
14059
- async getClient() {
14060
- if (this.client)
14061
- return this.client;
14062
- const [{ S3Client }, { fromIni }] = await Promise.all([
14063
- import("@aws-sdk/client-s3"),
14064
- import("@aws-sdk/credential-providers")
14065
- ]);
14066
- this.client = new S3Client({
14067
- region: this.options.region,
14068
- credentials: this.options.profile ? fromIni({ profile: this.options.profile }) : undefined,
14069
- maxAttempts: this.options.max_attempts
14070
- });
14071
- return this.client;
14072
- }
14073
- objectKey(key) {
14074
- const normalizedKey = normalizeArtifactKey(key);
14075
- const prefix = this.options.prefix ? normalizeArtifactKey(this.options.prefix) : "";
14076
- return prefix ? `${prefix}/${normalizedKey}` : normalizedKey;
14077
- }
14078
- async put(entry) {
14079
- const [{ PutObjectCommand }, client] = await Promise.all([
14080
- import("@aws-sdk/client-s3"),
14081
- this.getClient()
14082
- ]);
14083
- const key = this.objectKey(entry.key);
14084
- await client.send(new PutObjectCommand({
14085
- Bucket: this.options.bucket,
14086
- Key: key,
14087
- Body: entry.body,
14088
- ContentType: entry.content_type,
14089
- Metadata: entry.metadata,
14090
- ServerSideEncryption: this.options.server_side_encryption,
14091
- SSEKMSKeyId: this.options.kms_key_id
14092
- }));
14093
- return { key, uri: `s3://${this.options.bucket}/${key}` };
14094
- }
14095
- async getText(key) {
14096
- const [{ GetObjectCommand }, client] = await Promise.all([
14097
- import("@aws-sdk/client-s3"),
14098
- this.getClient()
14099
- ]);
14100
- const objectKey = this.objectKey(key);
14101
- const response = await client.send(new GetObjectCommand({
14102
- Bucket: this.options.bucket,
14103
- Key: objectKey
14104
- }));
14105
- if (!response.Body)
14106
- return "";
14107
- return await response.Body.transformToString();
14108
- }
14109
- async exists(key) {
14110
- const [{ HeadObjectCommand }, client] = await Promise.all([
14111
- import("@aws-sdk/client-s3"),
14112
- this.getClient()
14113
- ]);
14114
- const objectKey = this.objectKey(key);
14115
- try {
14116
- await client.send(new HeadObjectCommand({
14117
- Bucket: this.options.bucket,
14118
- Key: objectKey
14119
- }));
14120
- return true;
14121
- } catch (error48) {
14122
- const name = error48 instanceof Error ? error48.name : "";
14123
- if (name === "NotFound" || name === "NoSuchKey" || name === "NotFoundError")
14124
- return false;
14125
- throw error48;
14126
- }
14127
- }
14128
- }
14129
- function createArtifactStore(config2, workspace) {
14130
- if (config2.storage.type === "s3") {
14131
- if (!config2.storage.s3?.bucket)
14132
- throw new Error("S3 artifact storage requires storage.s3.bucket");
14133
- return new S3ArtifactStore({
14134
- bucket: config2.storage.s3.bucket,
14135
- prefix: config2.storage.s3.prefix,
14136
- region: config2.storage.s3.region,
14137
- profile: config2.storage.s3.profile,
14138
- max_attempts: config2.storage.s3.max_attempts,
14139
- server_side_encryption: config2.storage.s3.server_side_encryption,
14140
- kms_key_id: config2.storage.s3.kms_key_id
14141
- });
14142
- }
14143
- return new LocalArtifactStore(workspace.artifactsDir);
14144
- }
14145
-
14146
- // src/auth.ts
14147
- import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync4, unlinkSync as unlinkSync2, writeFileSync as writeFileSync4 } from "fs";
14148
- import { homedir as homedir2 } from "os";
14149
- import { dirname as dirname3, join as join3 } from "path";
14150
- var DEFAULT_KNOWLEDGE_API_URL = "https://knowledge.hasna.xyz";
14151
- function normalizeKnowledgeApiOrigin(apiUrl) {
14152
- const url2 = new URL(apiUrl);
14153
- if (url2.protocol !== "http:" && url2.protocol !== "https:") {
14154
- throw new Error("Knowledge API URL must use http or https.");
14155
- }
14156
- const pathname = url2.pathname.replace(/\/+$/, "");
14157
- if (pathname === "/api" || pathname === "/api/v1") {
14158
- url2.pathname = "/";
14159
- } else if (pathname.endsWith("/api/v1")) {
14160
- url2.pathname = pathname.slice(0, -"/api/v1".length) || "/";
14161
- } else if (pathname.endsWith("/api")) {
14162
- url2.pathname = pathname.slice(0, -"/api".length) || "/";
14163
- }
14164
- return url2.toString().replace(/\/+$/, "");
14165
- }
14166
- function knowledgeAuthPath(env = process.env) {
14167
- if (env.HASNA_KNOWLEDGE_AUTH_PATH)
14168
- return env.HASNA_KNOWLEDGE_AUTH_PATH;
14169
- const root = env.HASNA_KNOWLEDGE_AUTH_DIR ?? join3(homedir2(), ".hasna", "knowledge");
14170
- return join3(root, "auth.json");
14171
- }
14172
- function resolveKnowledgeApiUrl(config2, env = process.env) {
14173
- return normalizeKnowledgeApiOrigin(env.KNOWLEDGE_API_URL ?? config2?.hosted?.api_url ?? DEFAULT_KNOWLEDGE_API_URL);
14174
- }
14175
- function getKnowledgeAuth(env = process.env) {
14176
- try {
14177
- const path = knowledgeAuthPath(env);
14178
- if (!existsSync4(path))
14179
- return null;
14180
- const parsed = JSON.parse(readFileSync4(path, "utf8"));
14181
- return typeof parsed.api_key === "string" && parsed.api_key.length > 0 ? parsed : null;
14182
- } catch {
14183
- return null;
14184
- }
14185
- }
14186
- function saveKnowledgeAuth(auth, env = process.env) {
14187
- const path = knowledgeAuthPath(env);
14188
- const stored = {
14189
- ...auth,
14190
- api_url: auth.api_url ? normalizeKnowledgeApiOrigin(auth.api_url) : undefined,
14191
- created_at: auth.created_at ?? new Date().toISOString()
14192
- };
14193
- mkdirSync3(dirname3(path), { recursive: true, mode: 448 });
14194
- writeFileSync4(path, `${JSON.stringify(stored, null, 2)}
14195
- `, { mode: 384 });
14196
- return stored;
14197
- }
14198
- function clearKnowledgeAuth(env = process.env) {
14199
- try {
14200
- unlinkSync2(knowledgeAuthPath(env));
14201
- return true;
14202
- } catch {
14203
- return false;
14204
- }
14205
- }
14206
- function getKnowledgeApiKey(env = process.env) {
14207
- if (env.KNOWLEDGE_API_KEY)
14208
- return { apiKey: env.KNOWLEDGE_API_KEY, source: "env" };
14209
- if (env.HASNA_KNOWLEDGE_API_KEY)
14210
- return { apiKey: env.HASNA_KNOWLEDGE_API_KEY, source: "env" };
14211
- const auth = getKnowledgeAuth(env);
14212
- return auth?.api_key ? { apiKey: auth.api_key, source: "file" } : { apiKey: null, source: "none" };
14213
- }
14214
- function knowledgeAuthStatus(config2, env = process.env) {
14215
- const auth = getKnowledgeAuth(env);
14216
- const key = getKnowledgeApiKey(env);
14217
- const apiUrl = env.KNOWLEDGE_API_URL ? resolveKnowledgeApiUrl(config2, env) : auth?.api_url ? normalizeKnowledgeApiOrigin(auth.api_url) : resolveKnowledgeApiUrl(config2, env);
14218
- return {
14219
- authenticated: Boolean(key.apiKey),
14220
- source: key.source,
14221
- api_url: apiUrl,
14222
- auth_path: knowledgeAuthPath(env),
14223
- email: key.source === "file" ? auth?.email ?? null : null,
14224
- org_id: key.source === "file" ? auth?.org_id ?? null : null,
14225
- org_slug: key.source === "file" ? auth?.org_slug ?? null : null,
14226
- user_id: key.source === "file" ? auth?.user_id ?? null : null,
14227
- api_key_present: Boolean(key.apiKey)
14228
- };
14229
- }
14230
-
14231
- // src/agent.ts
14232
- import { randomUUID as randomUUID3 } from "crypto";
14233
-
14234
- // src/knowledge-db.ts
14235
- import { Database } from "bun:sqlite";
14236
- var MIGRATION_1 = `
14237
- PRAGMA journal_mode = WAL;
14238
- PRAGMA foreign_keys = ON;
14239
-
14240
- CREATE TABLE IF NOT EXISTS schema_versions (
14241
- version INTEGER PRIMARY KEY,
14242
- applied_at TEXT NOT NULL
14243
- );
14244
-
14245
- CREATE TABLE IF NOT EXISTS sources (
14246
- id TEXT PRIMARY KEY,
14247
- uri TEXT NOT NULL UNIQUE,
14248
- kind TEXT NOT NULL,
14249
- title TEXT,
14250
- metadata_json TEXT NOT NULL DEFAULT '{}',
14251
- acl_json TEXT NOT NULL DEFAULT '{}',
14252
- created_at TEXT NOT NULL,
14253
- updated_at TEXT NOT NULL
14254
- );
14255
-
14256
- CREATE TABLE IF NOT EXISTS source_revisions (
14257
- id TEXT PRIMARY KEY,
14258
- source_id TEXT NOT NULL REFERENCES sources(id) ON DELETE CASCADE,
14259
- revision TEXT NOT NULL,
14260
- hash TEXT,
14261
- extracted_text_uri TEXT,
14262
- metadata_json TEXT NOT NULL DEFAULT '{}',
14263
- created_at TEXT NOT NULL,
14264
- UNIQUE(source_id, revision)
14265
- );
13878
+ CREATE TABLE IF NOT EXISTS source_revisions (
13879
+ id TEXT PRIMARY KEY,
13880
+ source_id TEXT NOT NULL REFERENCES sources(id) ON DELETE CASCADE,
13881
+ revision TEXT NOT NULL,
13882
+ hash TEXT,
13883
+ extracted_text_uri TEXT,
13884
+ metadata_json TEXT NOT NULL DEFAULT '{}',
13885
+ created_at TEXT NOT NULL,
13886
+ UNIQUE(source_id, revision)
13887
+ );
14266
13888
 
14267
13889
  CREATE TABLE IF NOT EXISTS chunks (
14268
13890
  id TEXT PRIMARY KEY,
@@ -14525,39 +14147,417 @@ function migrateKnowledgeDb(path) {
14525
14147
  db.close();
14526
14148
  }
14527
14149
  }
14528
- function getSchemaVersion(db) {
14529
- const row = db.query("SELECT MAX(version) AS version FROM schema_versions").get();
14530
- return row?.version ?? 0;
14150
+ function getSchemaVersion(db) {
14151
+ const row = db.query("SELECT MAX(version) AS version FROM schema_versions").get();
14152
+ return row?.version ?? 0;
14153
+ }
14154
+ function count(db, table) {
14155
+ const row = db.query(`SELECT COUNT(*) AS n FROM ${table}`).get();
14156
+ return row?.n ?? 0;
14157
+ }
14158
+ function getKnowledgeDbStats(path) {
14159
+ const db = openKnowledgeDb(path);
14160
+ try {
14161
+ return {
14162
+ schema_version: getSchemaVersion(db),
14163
+ sources: count(db, "sources"),
14164
+ source_revisions: count(db, "source_revisions"),
14165
+ chunks: count(db, "chunks"),
14166
+ wiki_pages: count(db, "wiki_pages"),
14167
+ citations: count(db, "citations"),
14168
+ indexes: count(db, "knowledge_indexes"),
14169
+ runs: count(db, "runs"),
14170
+ run_events: count(db, "run_events"),
14171
+ redaction_findings: count(db, "redaction_findings"),
14172
+ audit_events: count(db, "audit_events"),
14173
+ approval_gates: count(db, "approval_gates"),
14174
+ storage_objects: count(db, "storage_objects"),
14175
+ embeddings: count(db, "chunk_embeddings"),
14176
+ vector_entries: count(db, "vector_index_entries"),
14177
+ reindex_queue: count(db, "reindex_queue")
14178
+ };
14179
+ } finally {
14180
+ db.close();
14181
+ }
14182
+ }
14183
+
14184
+ // src/store.ts
14185
+ import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync as existsSync2, renameSync, unlinkSync } from "fs";
14186
+ import { randomUUID } from "crypto";
14187
+ function defaultStorePath() {
14188
+ return workspaceForHome(globalKnowledgeHome()).jsonStorePath;
14189
+ }
14190
+ function ensureStore(path) {
14191
+ if (!existsSync2(path)) {
14192
+ ensureParentDir(path);
14193
+ if (path === defaultStorePath() && existsSync2(legacyGlobalStorePath())) {
14194
+ writeFileSync2(path, readFileSync2(legacyGlobalStorePath(), "utf8"));
14195
+ } else {
14196
+ writeFileSync2(path, JSON.stringify({ items: [] }, null, 2));
14197
+ }
14198
+ }
14199
+ }
14200
+ function lockPath(path) {
14201
+ return `${path}.lock`;
14202
+ }
14203
+ function acquireLock(lockPath2, ownerId) {
14204
+ const maxWait = 5000;
14205
+ const interval = 50;
14206
+ const start = Date.now();
14207
+ while (Date.now() - start < maxWait) {
14208
+ try {
14209
+ if (!existsSync2(lockPath2)) {
14210
+ writeFileSync2(lockPath2, JSON.stringify({ owner: ownerId, ts: Date.now() }));
14211
+ return;
14212
+ }
14213
+ const lock = JSON.parse(readFileSync2(lockPath2, "utf8"));
14214
+ if (Date.now() - lock.ts > 1e4) {
14215
+ unlinkSync(lockPath2);
14216
+ }
14217
+ } catch {}
14218
+ const start2 = Date.now();
14219
+ while (Date.now() - start2 < interval) {}
14220
+ }
14221
+ throw new Error(`Could not acquire lock on ${lockPath2} after ${maxWait}ms`);
14222
+ }
14223
+ function releaseLock(lockPath2, ownerId) {
14224
+ try {
14225
+ if (existsSync2(lockPath2)) {
14226
+ const lock = JSON.parse(readFileSync2(lockPath2, "utf8"));
14227
+ if (lock.owner === ownerId) {
14228
+ unlinkSync(lockPath2);
14229
+ }
14230
+ }
14231
+ } catch {}
14232
+ }
14233
+ function loadStore(path) {
14234
+ ensureStore(path);
14235
+ const raw = readFileSync2(path, "utf8");
14236
+ const parsed = JSON.parse(raw);
14237
+ if (!parsed || !Array.isArray(parsed.items)) {
14238
+ return { items: [] };
14239
+ }
14240
+ return parsed;
14241
+ }
14242
+ function saveStore(path, store) {
14243
+ const tmp = `${path}.tmp.${randomUUID()}`;
14244
+ writeFileSync2(tmp, JSON.stringify(store, null, 2));
14245
+ renameSync(tmp, path);
14246
+ }
14247
+ function withLock(path, fn) {
14248
+ const owner = randomUUID();
14249
+ const lpath = lockPath(path);
14250
+ acquireLock(lpath, owner);
14251
+ try {
14252
+ return fn();
14253
+ } finally {
14254
+ releaseLock(lpath, owner);
14255
+ }
14256
+ }
14257
+ function makeId() {
14258
+ return `k_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
14259
+ }
14260
+
14261
+ // src/source-ref.ts
14262
+ function assertNonEmpty(value, message) {
14263
+ if (!value)
14264
+ throw new Error(message);
14265
+ return value;
14266
+ }
14267
+ function parseOpenFilesRef(uri) {
14268
+ const withoutScheme = uri.slice("open-files://".length);
14269
+ const parts = withoutScheme.split("/").filter(Boolean);
14270
+ const entity = parts[0];
14271
+ if (entity !== "file" && entity !== "source") {
14272
+ throw new Error("Invalid open-files ref. Expected open-files://file/<id>, open-files://file/<id>/revision/<revision_id>, or open-files://source/<id>/path/<path>.");
14273
+ }
14274
+ const id = assertNonEmpty(parts[1], "Invalid open-files ref. Missing id.");
14275
+ if (entity === "file") {
14276
+ if (parts.length === 2)
14277
+ return { kind: "open-files", uri, entity, id };
14278
+ if (parts[2] === "revision" && parts[3] && parts.length === 4) {
14279
+ return { kind: "open-files", uri, entity, id, revision_id: decodeURIComponent(parts[3]) };
14280
+ }
14281
+ throw new Error("Invalid open-files file ref. Expected open-files://file/<id>/revision/<revision_id>.");
14282
+ }
14283
+ const pathIndex = parts.indexOf("path");
14284
+ const path = pathIndex >= 0 ? decodeURIComponent(parts.slice(pathIndex + 1).join("/")) : undefined;
14285
+ return { kind: "open-files", uri, entity, id, path };
14286
+ }
14287
+ function parseS3Ref(uri) {
14288
+ const parsed = new URL(uri);
14289
+ const bucket = assertNonEmpty(parsed.hostname, "Invalid s3 ref. Missing bucket.");
14290
+ const key = decodeURIComponent(parsed.pathname.replace(/^\/+/, ""));
14291
+ if (!key)
14292
+ throw new Error("Invalid s3 ref. Missing object key.");
14293
+ return { kind: "s3", uri, bucket, key };
14294
+ }
14295
+ function parseFileRef(uri) {
14296
+ const parsed = new URL(uri);
14297
+ return { kind: "file", uri, path: decodeURIComponent(parsed.pathname) };
14298
+ }
14299
+ function parseWebRef(uri) {
14300
+ const parsed = new URL(uri);
14301
+ return { kind: "web", uri, url: parsed.toString() };
14302
+ }
14303
+ function parseSourceRef(uri) {
14304
+ if (uri.startsWith("open-files://"))
14305
+ return parseOpenFilesRef(uri);
14306
+ if (uri.startsWith("s3://"))
14307
+ return parseS3Ref(uri);
14308
+ if (uri.startsWith("file://"))
14309
+ return parseFileRef(uri);
14310
+ if (uri.startsWith("https://") || uri.startsWith("http://"))
14311
+ return parseWebRef(uri);
14312
+ throw new Error(`Unsupported source ref scheme: ${uri}`);
14313
+ }
14314
+ function catalogSourceUriForRef(uri, parsed = parseSourceRef(uri)) {
14315
+ if (parsed.kind === "open-files" && parsed.entity === "file" && parsed.revision_id) {
14316
+ return uri.replace(/\/revision\/[^/]+$/, "");
14317
+ }
14318
+ return uri;
14319
+ }
14320
+ function revisionIdForSourceRef(uri) {
14321
+ const parsed = parseSourceRef(uri);
14322
+ return parsed.kind === "open-files" && parsed.entity === "file" ? parsed.revision_id ?? null : null;
14323
+ }
14324
+
14325
+ // src/artifact-store.ts
14326
+ import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
14327
+ import { dirname as dirname2, join as join2, relative, sep } from "path";
14328
+ function normalizeArtifactKey(key) {
14329
+ const raw = key.replace(/\\/g, "/").trim();
14330
+ if (!raw || raw.startsWith("/")) {
14331
+ throw new Error(`Invalid artifact key: ${key}`);
14332
+ }
14333
+ const segments = raw.split("/").filter(Boolean);
14334
+ if (segments.length === 0 || segments.some((segment) => segment === "." || segment === "..")) {
14335
+ throw new Error(`Invalid artifact key: ${key}`);
14336
+ }
14337
+ return segments.join("/");
14338
+ }
14339
+ function assertInside(root, target) {
14340
+ const rel = relative(root, target);
14341
+ if (rel.startsWith("..") || rel === ".." || rel.startsWith(`..${sep}`)) {
14342
+ throw new Error(`Artifact path escapes root: ${target}`);
14343
+ }
14344
+ }
14345
+
14346
+ class LocalArtifactStore {
14347
+ root;
14348
+ type = "local";
14349
+ canRead = true;
14350
+ canWrite = true;
14351
+ constructor(root) {
14352
+ this.root = root;
14353
+ mkdirSync2(root, { recursive: true });
14354
+ }
14355
+ async put(entry) {
14356
+ const key = normalizeArtifactKey(entry.key);
14357
+ const path = join2(this.root, key);
14358
+ assertInside(this.root, path);
14359
+ mkdirSync2(dirname2(path), { recursive: true });
14360
+ writeFileSync3(path, entry.body);
14361
+ return { key, uri: `file://${path}` };
14362
+ }
14363
+ async getText(key) {
14364
+ const normalizedKey = normalizeArtifactKey(key);
14365
+ const path = join2(this.root, normalizedKey);
14366
+ assertInside(this.root, path);
14367
+ return readFileSync3(path, "utf8");
14368
+ }
14369
+ async exists(key) {
14370
+ const normalizedKey = normalizeArtifactKey(key);
14371
+ const path = join2(this.root, normalizedKey);
14372
+ assertInside(this.root, path);
14373
+ return existsSync3(path);
14374
+ }
14375
+ }
14376
+
14377
+ class S3ArtifactStore {
14378
+ options;
14379
+ type = "s3";
14380
+ canRead = true;
14381
+ canWrite = true;
14382
+ client;
14383
+ constructor(options) {
14384
+ this.options = options;
14385
+ this.client = options.client;
14386
+ }
14387
+ async getClient() {
14388
+ if (this.client)
14389
+ return this.client;
14390
+ const [{ S3Client }, { fromIni }] = await Promise.all([
14391
+ import("@aws-sdk/client-s3"),
14392
+ import("@aws-sdk/credential-providers")
14393
+ ]);
14394
+ this.client = new S3Client({
14395
+ region: this.options.region,
14396
+ credentials: this.options.profile ? fromIni({ profile: this.options.profile }) : undefined,
14397
+ maxAttempts: this.options.max_attempts
14398
+ });
14399
+ return this.client;
14400
+ }
14401
+ objectKey(key) {
14402
+ const normalizedKey = normalizeArtifactKey(key);
14403
+ const prefix = this.options.prefix ? normalizeArtifactKey(this.options.prefix) : "";
14404
+ return prefix ? `${prefix}/${normalizedKey}` : normalizedKey;
14405
+ }
14406
+ async put(entry) {
14407
+ const [{ PutObjectCommand }, client] = await Promise.all([
14408
+ import("@aws-sdk/client-s3"),
14409
+ this.getClient()
14410
+ ]);
14411
+ const key = this.objectKey(entry.key);
14412
+ await client.send(new PutObjectCommand({
14413
+ Bucket: this.options.bucket,
14414
+ Key: key,
14415
+ Body: entry.body,
14416
+ ContentType: entry.content_type,
14417
+ Metadata: entry.metadata,
14418
+ ServerSideEncryption: this.options.server_side_encryption,
14419
+ SSEKMSKeyId: this.options.kms_key_id
14420
+ }));
14421
+ return { key, uri: `s3://${this.options.bucket}/${key}` };
14422
+ }
14423
+ async getText(key) {
14424
+ const [{ GetObjectCommand }, client] = await Promise.all([
14425
+ import("@aws-sdk/client-s3"),
14426
+ this.getClient()
14427
+ ]);
14428
+ const objectKey = this.objectKey(key);
14429
+ const response = await client.send(new GetObjectCommand({
14430
+ Bucket: this.options.bucket,
14431
+ Key: objectKey
14432
+ }));
14433
+ if (!response.Body)
14434
+ return "";
14435
+ return await response.Body.transformToString();
14436
+ }
14437
+ async exists(key) {
14438
+ const [{ HeadObjectCommand }, client] = await Promise.all([
14439
+ import("@aws-sdk/client-s3"),
14440
+ this.getClient()
14441
+ ]);
14442
+ const objectKey = this.objectKey(key);
14443
+ try {
14444
+ await client.send(new HeadObjectCommand({
14445
+ Bucket: this.options.bucket,
14446
+ Key: objectKey
14447
+ }));
14448
+ return true;
14449
+ } catch (error48) {
14450
+ const name = error48 instanceof Error ? error48.name : "";
14451
+ if (name === "NotFound" || name === "NoSuchKey" || name === "NotFoundError")
14452
+ return false;
14453
+ throw error48;
14454
+ }
14455
+ }
14456
+ }
14457
+ function createArtifactStore(config2, workspace) {
14458
+ if (config2.storage.type === "s3") {
14459
+ if (!config2.storage.s3?.bucket)
14460
+ throw new Error("S3 artifact storage requires storage.s3.bucket");
14461
+ return new S3ArtifactStore({
14462
+ bucket: config2.storage.s3.bucket,
14463
+ prefix: config2.storage.s3.prefix,
14464
+ region: config2.storage.s3.region,
14465
+ profile: config2.storage.s3.profile,
14466
+ max_attempts: config2.storage.s3.max_attempts,
14467
+ server_side_encryption: config2.storage.s3.server_side_encryption,
14468
+ kms_key_id: config2.storage.s3.kms_key_id
14469
+ });
14470
+ }
14471
+ return new LocalArtifactStore(workspace.artifactsDir);
14472
+ }
14473
+
14474
+ // src/auth.ts
14475
+ import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync4, unlinkSync as unlinkSync2, writeFileSync as writeFileSync4 } from "fs";
14476
+ import { homedir as homedir2 } from "os";
14477
+ import { dirname as dirname3, join as join3 } from "path";
14478
+ var DEFAULT_KNOWLEDGE_API_URL = "https://knowledge.hasna.xyz";
14479
+ function normalizeKnowledgeApiOrigin(apiUrl) {
14480
+ const url2 = new URL(apiUrl);
14481
+ if (url2.protocol !== "http:" && url2.protocol !== "https:") {
14482
+ throw new Error("Knowledge API URL must use http or https.");
14483
+ }
14484
+ const pathname = url2.pathname.replace(/\/+$/, "");
14485
+ if (pathname === "/api" || pathname === "/api/v1") {
14486
+ url2.pathname = "/";
14487
+ } else if (pathname.endsWith("/api/v1")) {
14488
+ url2.pathname = pathname.slice(0, -"/api/v1".length) || "/";
14489
+ } else if (pathname.endsWith("/api")) {
14490
+ url2.pathname = pathname.slice(0, -"/api".length) || "/";
14491
+ }
14492
+ return url2.toString().replace(/\/+$/, "");
14493
+ }
14494
+ function knowledgeAuthPath(env = process.env) {
14495
+ if (env.HASNA_KNOWLEDGE_AUTH_PATH)
14496
+ return env.HASNA_KNOWLEDGE_AUTH_PATH;
14497
+ const root = env.HASNA_KNOWLEDGE_AUTH_DIR ?? join3(homedir2(), ".hasna", "knowledge");
14498
+ return join3(root, "auth.json");
14499
+ }
14500
+ function resolveKnowledgeApiUrl(config2, env = process.env) {
14501
+ return normalizeKnowledgeApiOrigin(env.KNOWLEDGE_API_URL ?? config2?.hosted?.api_url ?? DEFAULT_KNOWLEDGE_API_URL);
14531
14502
  }
14532
- function count(db, table) {
14533
- const row = db.query(`SELECT COUNT(*) AS n FROM ${table}`).get();
14534
- return row?.n ?? 0;
14503
+ function getKnowledgeAuth(env = process.env) {
14504
+ try {
14505
+ const path = knowledgeAuthPath(env);
14506
+ if (!existsSync4(path))
14507
+ return null;
14508
+ const parsed = JSON.parse(readFileSync4(path, "utf8"));
14509
+ return typeof parsed.api_key === "string" && parsed.api_key.length > 0 ? parsed : null;
14510
+ } catch {
14511
+ return null;
14512
+ }
14535
14513
  }
14536
- function getKnowledgeDbStats(path) {
14537
- const db = openKnowledgeDb(path);
14514
+ function saveKnowledgeAuth(auth, env = process.env) {
14515
+ const path = knowledgeAuthPath(env);
14516
+ const stored = {
14517
+ ...auth,
14518
+ api_url: auth.api_url ? normalizeKnowledgeApiOrigin(auth.api_url) : undefined,
14519
+ created_at: auth.created_at ?? new Date().toISOString()
14520
+ };
14521
+ mkdirSync3(dirname3(path), { recursive: true, mode: 448 });
14522
+ writeFileSync4(path, `${JSON.stringify(stored, null, 2)}
14523
+ `, { mode: 384 });
14524
+ return stored;
14525
+ }
14526
+ function clearKnowledgeAuth(env = process.env) {
14538
14527
  try {
14539
- return {
14540
- schema_version: getSchemaVersion(db),
14541
- sources: count(db, "sources"),
14542
- source_revisions: count(db, "source_revisions"),
14543
- chunks: count(db, "chunks"),
14544
- wiki_pages: count(db, "wiki_pages"),
14545
- citations: count(db, "citations"),
14546
- indexes: count(db, "knowledge_indexes"),
14547
- runs: count(db, "runs"),
14548
- run_events: count(db, "run_events"),
14549
- redaction_findings: count(db, "redaction_findings"),
14550
- audit_events: count(db, "audit_events"),
14551
- approval_gates: count(db, "approval_gates"),
14552
- storage_objects: count(db, "storage_objects"),
14553
- embeddings: count(db, "chunk_embeddings"),
14554
- vector_entries: count(db, "vector_index_entries"),
14555
- reindex_queue: count(db, "reindex_queue")
14556
- };
14557
- } finally {
14558
- db.close();
14528
+ unlinkSync2(knowledgeAuthPath(env));
14529
+ return true;
14530
+ } catch {
14531
+ return false;
14559
14532
  }
14560
14533
  }
14534
+ function getKnowledgeApiKey(env = process.env) {
14535
+ if (env.KNOWLEDGE_API_KEY)
14536
+ return { apiKey: env.KNOWLEDGE_API_KEY, source: "env" };
14537
+ if (env.HASNA_KNOWLEDGE_API_KEY)
14538
+ return { apiKey: env.HASNA_KNOWLEDGE_API_KEY, source: "env" };
14539
+ const auth = getKnowledgeAuth(env);
14540
+ return auth?.api_key ? { apiKey: auth.api_key, source: "file" } : { apiKey: null, source: "none" };
14541
+ }
14542
+ function knowledgeAuthStatus(config2, env = process.env) {
14543
+ const auth = getKnowledgeAuth(env);
14544
+ const key = getKnowledgeApiKey(env);
14545
+ const apiUrl = env.KNOWLEDGE_API_URL ? resolveKnowledgeApiUrl(config2, env) : auth?.api_url ? normalizeKnowledgeApiOrigin(auth.api_url) : resolveKnowledgeApiUrl(config2, env);
14546
+ return {
14547
+ authenticated: Boolean(key.apiKey),
14548
+ source: key.source,
14549
+ api_url: apiUrl,
14550
+ auth_path: knowledgeAuthPath(env),
14551
+ email: key.source === "file" ? auth?.email ?? null : null,
14552
+ org_id: key.source === "file" ? auth?.org_id ?? null : null,
14553
+ org_slug: key.source === "file" ? auth?.org_slug ?? null : null,
14554
+ user_id: key.source === "file" ? auth?.user_id ?? null : null,
14555
+ api_key_present: Boolean(key.apiKey)
14556
+ };
14557
+ }
14558
+
14559
+ // src/agent.ts
14560
+ import { randomUUID as randomUUID3 } from "crypto";
14561
14561
 
14562
14562
  // src/providers.ts
14563
14563
  import { randomUUID as randomUUID2 } from "crypto";
@@ -18062,6 +18062,9 @@ async function runProviderWebSearch(options) {
18062
18062
  };
18063
18063
  }
18064
18064
 
18065
+ // src/wiki-compiler.ts
18066
+ import { createHash as createHash10, randomUUID as randomUUID9 } from "crypto";
18067
+
18065
18068
  // src/storage-contract.ts
18066
18069
  import { createHash as createHash9, randomUUID as randomUUID8 } from "crypto";
18067
18070
  var GENERATED_ARTIFACTS = [
@@ -18223,44 +18226,582 @@ function validateStorageConfig(config2, workspace) {
18223
18226
  warnings
18224
18227
  };
18225
18228
  }
18226
- function recordStorageObjects(db, objects, now = new Date) {
18227
- const timestamp = now.toISOString();
18228
- const statement = db.prepare(`
18229
- INSERT INTO storage_objects (
18230
- id, artifact_uri, kind, content_type, hash, size_bytes, metadata_json, created_at, updated_at
18231
- )
18232
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
18233
- ON CONFLICT(artifact_uri) DO UPDATE SET
18234
- kind = excluded.kind,
18235
- content_type = excluded.content_type,
18236
- hash = excluded.hash,
18237
- size_bytes = excluded.size_bytes,
18238
- metadata_json = excluded.metadata_json,
18239
- updated_at = excluded.updated_at
18240
- `);
18241
- const insert = db.transaction((entries) => {
18242
- for (const entry of entries) {
18243
- statement.run(randomUUID8(), entry.uri, entry.kind, entry.content_type ?? null, entry.hash ?? null, entry.size_bytes ?? null, JSON.stringify({
18244
- key: entry.key,
18245
- ...entry.metadata ?? {}
18246
- }), timestamp, timestamp);
18229
+ function recordStorageObjects(db, objects, now = new Date) {
18230
+ const timestamp = now.toISOString();
18231
+ const statement = db.prepare(`
18232
+ INSERT INTO storage_objects (
18233
+ id, artifact_uri, kind, content_type, hash, size_bytes, metadata_json, created_at, updated_at
18234
+ )
18235
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
18236
+ ON CONFLICT(artifact_uri) DO UPDATE SET
18237
+ kind = excluded.kind,
18238
+ content_type = excluded.content_type,
18239
+ hash = excluded.hash,
18240
+ size_bytes = excluded.size_bytes,
18241
+ metadata_json = excluded.metadata_json,
18242
+ updated_at = excluded.updated_at
18243
+ `);
18244
+ const insert = db.transaction((entries) => {
18245
+ for (const entry of entries) {
18246
+ statement.run(randomUUID8(), entry.uri, entry.kind, entry.content_type ?? null, entry.hash ?? null, entry.size_bytes ?? null, JSON.stringify({
18247
+ key: entry.key,
18248
+ ...entry.metadata ?? {}
18249
+ }), timestamp, timestamp);
18250
+ }
18251
+ });
18252
+ insert(objects);
18253
+ }
18254
+
18255
+ // src/wiki-compiler.ts
18256
+ function stableId6(prefix, value) {
18257
+ return `${prefix}_${createHash10("sha256").update(value).digest("hex").slice(0, 20)}`;
18258
+ }
18259
+ function slugify2(value) {
18260
+ const slug = value.normalize("NFKC").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80);
18261
+ return slug || "knowledge-page";
18262
+ }
18263
+ function todayParts(now) {
18264
+ return {
18265
+ year: String(now.getUTCFullYear()),
18266
+ month: String(now.getUTCMonth() + 1).padStart(2, "0"),
18267
+ day: String(now.getUTCDate()).padStart(2, "0")
18268
+ };
18269
+ }
18270
+ function estimateTokenCount2(text) {
18271
+ const words = text.trim().split(/\s+/).filter(Boolean).length;
18272
+ return Math.max(1, Math.ceil(words * 1.25));
18273
+ }
18274
+ function parseJsonObject4(value) {
18275
+ if (!value)
18276
+ return {};
18277
+ try {
18278
+ const parsed = JSON.parse(value);
18279
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
18280
+ } catch {
18281
+ return {};
18282
+ }
18283
+ }
18284
+ function queryTerms3(query) {
18285
+ return Array.from(new Set((query ?? "").toLowerCase().match(/[\p{L}\p{N}_]+/gu) ?? [])).slice(0, 12);
18286
+ }
18287
+ function escapeLike(value) {
18288
+ return value.replace(/[\\%_]/g, (char) => `\\${char}`);
18289
+ }
18290
+ function selectSourceChunks(db, options) {
18291
+ const limit = Math.max(1, Math.min(options.limit ?? 10, 50));
18292
+ const sourceRefs = options.sourceRefs ?? [];
18293
+ const terms = queryTerms3(options.query);
18294
+ const where = ["c.kind = 'source'"];
18295
+ const params = [];
18296
+ if (sourceRefs.length > 0) {
18297
+ where.push(`(${sourceRefs.map(() => "(s.uri = ? OR c.metadata_json LIKE ?)").join(" OR ")})`);
18298
+ for (const ref of sourceRefs) {
18299
+ params.push(ref, `%${escapeLike(ref)}%`);
18300
+ }
18301
+ }
18302
+ if (terms.length > 0) {
18303
+ where.push(`(${terms.map(() => "lower(c.text) LIKE ? ESCAPE '\\'").join(" OR ")})`);
18304
+ for (const term of terms)
18305
+ params.push(`%${escapeLike(term)}%`);
18306
+ }
18307
+ params.push(limit);
18308
+ return db.query(`SELECT
18309
+ c.id AS chunk_id,
18310
+ c.text,
18311
+ c.start_offset,
18312
+ c.end_offset,
18313
+ c.metadata_json,
18314
+ c.source_revision_id,
18315
+ sr.revision,
18316
+ sr.hash,
18317
+ s.uri AS source_uri,
18318
+ s.title AS source_title
18319
+ FROM chunks c
18320
+ JOIN source_revisions sr ON sr.id = c.source_revision_id
18321
+ JOIN sources s ON s.id = sr.source_id
18322
+ WHERE ${where.join(" AND ")}
18323
+ ORDER BY c.created_at ASC, c.ordinal ASC
18324
+ LIMIT ?`).all(...params);
18325
+ }
18326
+ function excerpt(text, max = 420) {
18327
+ const normalized = text.replace(/\s+/g, " ").trim();
18328
+ return normalized.length <= max ? normalized : `${normalized.slice(0, max - 1).trim()}...`;
18329
+ }
18330
+ function titleFor(options, rows) {
18331
+ if (options.title?.trim())
18332
+ return options.title.trim();
18333
+ if (options.query?.trim())
18334
+ return options.query.trim();
18335
+ return rows[0]?.source_title ?? "Compiled Knowledge";
18336
+ }
18337
+ function compileBody(title, rows, now) {
18338
+ const sourceLines = rows.map((row, index) => {
18339
+ const label = `S${index + 1}`;
18340
+ return `- [${label}] ${row.source_title ?? row.source_uri ?? "Source"} (${row.source_uri ?? "unknown"}, revision ${row.revision ?? "unknown"}, hash ${row.hash ?? "unknown"})`;
18341
+ });
18342
+ const noteLines = rows.map((row, index) => {
18343
+ const label = `S${index + 1}`;
18344
+ return [
18345
+ `## ${row.source_title ?? `Source ${index + 1}`}`,
18346
+ "",
18347
+ excerpt(row.text),
18348
+ "",
18349
+ `Citation: [${label}]`
18350
+ ].join(`
18351
+ `);
18352
+ });
18353
+ return [
18354
+ `# ${title}`,
18355
+ "",
18356
+ `Generated at: ${now}`,
18357
+ "",
18358
+ "## Sources",
18359
+ "",
18360
+ ...sourceLines,
18361
+ "",
18362
+ ...noteLines,
18363
+ ""
18364
+ ].join(`
18365
+ `);
18366
+ }
18367
+ async function writeArtifact(store, entry) {
18368
+ const written = await store.put(entry);
18369
+ return {
18370
+ key: written.key,
18371
+ uri: written.uri,
18372
+ kind: entry.key.startsWith("logs/") ? "log" : "wiki_page",
18373
+ content_type: entry.content_type,
18374
+ ...hashArtifactBody(entry.body),
18375
+ metadata: {
18376
+ ...entry.metadata ?? {}
18377
+ }
18378
+ };
18379
+ }
18380
+ async function appendLog(store, event, now) {
18381
+ const { year, month, day } = todayParts(now);
18382
+ const key = `logs/${year}/${month}/${day}.jsonl`;
18383
+ let existing = "";
18384
+ try {
18385
+ existing = await store.getText(key);
18386
+ } catch {
18387
+ existing = "";
18388
+ }
18389
+ return writeArtifact(store, {
18390
+ key,
18391
+ body: `${existing}${JSON.stringify(event)}
18392
+ `,
18393
+ content_type: "application/x-ndjson"
18394
+ });
18395
+ }
18396
+ function upsertWikiPage(db, input) {
18397
+ db.run(`INSERT INTO wiki_pages (id, path, title, artifact_uri, content_hash, status, metadata_json, created_at, updated_at)
18398
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
18399
+ ON CONFLICT(path) DO UPDATE SET
18400
+ title = excluded.title,
18401
+ artifact_uri = excluded.artifact_uri,
18402
+ content_hash = excluded.content_hash,
18403
+ status = excluded.status,
18404
+ metadata_json = excluded.metadata_json,
18405
+ updated_at = excluded.updated_at`, [
18406
+ input.pageId,
18407
+ input.path,
18408
+ input.title,
18409
+ input.artifactUri,
18410
+ input.contentHash,
18411
+ "active",
18412
+ JSON.stringify({
18413
+ artifact_key: input.path,
18414
+ provenance: input.provenance
18415
+ }),
18416
+ input.now,
18417
+ input.now
18418
+ ]);
18419
+ const existing = db.query("SELECT id FROM chunks WHERE wiki_page_id = ?").all(input.pageId);
18420
+ for (const row of existing)
18421
+ db.run("DELETE FROM chunks_fts WHERE chunk_id = ?", [row.id]);
18422
+ db.run("DELETE FROM chunks WHERE wiki_page_id = ?", [input.pageId]);
18423
+ const chunkId = stableId6("chk", `${input.pageId}\x00${input.contentHash}`);
18424
+ db.run(`INSERT INTO chunks (id, wiki_page_id, kind, ordinal, text, token_count, start_offset, end_offset, metadata_json, created_at)
18425
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
18426
+ chunkId,
18427
+ input.pageId,
18428
+ "wiki",
18429
+ 0,
18430
+ input.body,
18431
+ estimateTokenCount2(input.body),
18432
+ 0,
18433
+ input.body.length,
18434
+ JSON.stringify({
18435
+ artifact_key: input.path,
18436
+ artifact_uri: input.artifactUri,
18437
+ content_hash: input.contentHash,
18438
+ provenance: input.provenance
18439
+ }),
18440
+ input.now
18441
+ ]);
18442
+ db.run("INSERT INTO chunks_fts (chunk_id, text, title, source_uri) VALUES (?, ?, ?, ?)", [
18443
+ chunkId,
18444
+ input.body,
18445
+ input.title,
18446
+ input.artifactUri
18447
+ ]);
18448
+ }
18449
+ function replacePageCitations(db, pageId, citations, now) {
18450
+ db.run("DELETE FROM citations WHERE wiki_page_id = ?", [pageId]);
18451
+ for (const citation of citations) {
18452
+ db.run(`INSERT INTO citations (id, wiki_page_id, chunk_id, source_uri, quote, start_offset, end_offset, metadata_json, created_at)
18453
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
18454
+ stableId6("cit", `${pageId}\x00${citation.source_uri}\x00${citation.chunk_id ?? randomUUID9()}`),
18455
+ pageId,
18456
+ citation.chunk_id,
18457
+ citation.source_uri,
18458
+ citation.quote,
18459
+ citation.start_offset,
18460
+ citation.end_offset,
18461
+ JSON.stringify(citation.metadata),
18462
+ now
18463
+ ]);
18464
+ }
18465
+ return citations.length;
18466
+ }
18467
+ function upsertIndex(db, input) {
18468
+ db.run(`INSERT INTO knowledge_indexes (id, kind, name, artifact_uri, shard_key, metadata_json, created_at, updated_at)
18469
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
18470
+ ON CONFLICT(kind, name, shard_key) DO UPDATE SET
18471
+ artifact_uri = excluded.artifact_uri,
18472
+ metadata_json = excluded.metadata_json,
18473
+ updated_at = excluded.updated_at`, [
18474
+ stableId6("idx", `wiki-topic\x00${input.path}`),
18475
+ "wiki_topic",
18476
+ input.title,
18477
+ input.artifactUri,
18478
+ input.path,
18479
+ JSON.stringify({
18480
+ artifact_key: input.path,
18481
+ content_hash: input.contentHash
18482
+ }),
18483
+ input.now,
18484
+ input.now
18485
+ ]);
18486
+ return 1;
18487
+ }
18488
+ function firstConcept(title) {
18489
+ return title.toLowerCase().match(/[a-z0-9][a-z0-9-]{2,}/)?.[0] ?? "knowledge";
18490
+ }
18491
+ async function compileWikiPage(options) {
18492
+ const nowDate = options.now ?? new Date;
18493
+ const now = nowDate.toISOString();
18494
+ migrateKnowledgeDb(options.dbPath);
18495
+ const readDb = openKnowledgeDb(options.dbPath);
18496
+ let rows;
18497
+ try {
18498
+ rows = selectSourceChunks(readDb, options);
18499
+ } finally {
18500
+ readDb.close();
18501
+ }
18502
+ if (rows.length === 0)
18503
+ throw new Error("No source chunks matched wiki compile input.");
18504
+ const title = titleFor(options, rows);
18505
+ const slug = slugify2(title);
18506
+ const path = `wiki/generated/${slug}.md`;
18507
+ const body = compileBody(title, rows, now);
18508
+ const sourceRefs = rows.map((row) => {
18509
+ const metadata = parseJsonObject4(row.metadata_json);
18510
+ return typeof metadata.source_ref === "string" ? metadata.source_ref : row.source_uri;
18511
+ }).filter((ref) => Boolean(ref));
18512
+ const provenance = generatedArtifactProvenance({
18513
+ generated_from: "wiki_compile",
18514
+ artifact_key: path,
18515
+ source_refs: sourceRefs
18516
+ });
18517
+ const pageArtifact = await writeArtifact(options.store, {
18518
+ key: path,
18519
+ body,
18520
+ content_type: "text/markdown",
18521
+ metadata: { generated_from: "wiki_compile" }
18522
+ });
18523
+ const pageId = stableId6("wiki", path);
18524
+ const citations = rows.map((row) => ({
18525
+ chunk_id: row.chunk_id,
18526
+ source_uri: row.source_uri ?? "unknown",
18527
+ quote: excerpt(row.text, 240),
18528
+ start_offset: row.start_offset,
18529
+ end_offset: row.end_offset,
18530
+ metadata: {
18531
+ source_revision_id: row.source_revision_id,
18532
+ revision: row.revision,
18533
+ hash: row.hash,
18534
+ source_ref: parseJsonObject4(row.metadata_json).source_ref ?? row.source_uri
18535
+ }
18536
+ }));
18537
+ const concept = firstConcept(title);
18538
+ const conceptPath = `wiki/concepts/${slugify2(concept)}.md`;
18539
+ const conceptBody = [`# ${concept}`, "", `Related page: [[${path}]]`, ""].join(`
18540
+ `);
18541
+ const conceptProvenance = generatedArtifactProvenance({
18542
+ generated_from: "wiki_compile_concept",
18543
+ artifact_key: conceptPath,
18544
+ source_refs: sourceRefs
18545
+ });
18546
+ const conceptArtifact = await writeArtifact(options.store, {
18547
+ key: conceptPath,
18548
+ body: conceptBody,
18549
+ content_type: "text/markdown",
18550
+ metadata: { generated_from: "wiki_compile_concept" }
18551
+ });
18552
+ const conceptPageId = stableId6("wiki", conceptPath);
18553
+ const log = await appendLog(options.store, {
18554
+ ts: now,
18555
+ event: "wiki_compile_completed",
18556
+ page_key: path,
18557
+ source_refs: sourceRefs,
18558
+ chunks_seen: rows.length
18559
+ }, nowDate);
18560
+ const db = openKnowledgeDb(options.dbPath);
18561
+ try {
18562
+ recordStorageObjects(db, [pageArtifact, conceptArtifact, log], nowDate);
18563
+ upsertWikiPage(db, {
18564
+ pageId,
18565
+ path,
18566
+ title,
18567
+ artifactUri: pageArtifact.uri,
18568
+ contentHash: pageArtifact.hash ?? "",
18569
+ body,
18570
+ provenance,
18571
+ now
18572
+ });
18573
+ upsertWikiPage(db, {
18574
+ pageId: conceptPageId,
18575
+ path: conceptPath,
18576
+ title: concept,
18577
+ artifactUri: conceptArtifact.uri,
18578
+ contentHash: conceptArtifact.hash ?? "",
18579
+ body: conceptBody,
18580
+ provenance: conceptProvenance,
18581
+ now
18582
+ });
18583
+ db.run(`INSERT OR REPLACE INTO wiki_backlinks (from_page_id, to_page_id, label, created_at)
18584
+ VALUES (?, ?, ?, ?)`, [pageId, conceptPageId, "concept", now]);
18585
+ const citationsWritten = replacePageCitations(db, pageId, citations, now);
18586
+ const indexesUpdated = upsertIndex(db, {
18587
+ title,
18588
+ path,
18589
+ artifactUri: pageArtifact.uri,
18590
+ contentHash: pageArtifact.hash ?? "",
18591
+ now
18592
+ });
18593
+ return {
18594
+ page_id: pageId,
18595
+ path,
18596
+ artifact_uri: pageArtifact.uri,
18597
+ content_hash: pageArtifact.hash ?? "",
18598
+ chunks_seen: rows.length,
18599
+ citations_written: citationsWritten,
18600
+ concept_page_id: conceptPageId,
18601
+ indexes_updated: indexesUpdated,
18602
+ log_key: log.key,
18603
+ warnings: []
18604
+ };
18605
+ } finally {
18606
+ db.close();
18607
+ }
18608
+ }
18609
+ async function fileAnswerToWiki(options) {
18610
+ if (!options.approveWrite) {
18611
+ return {
18612
+ approved: false,
18613
+ durable_writes_performed: false,
18614
+ page_id: null,
18615
+ path: null,
18616
+ artifact_uri: null,
18617
+ citations_written: 0,
18618
+ log_key: null,
18619
+ message: "Dry-run: answer filing requires --approve-write."
18620
+ };
18621
+ }
18622
+ const nowDate = options.now ?? new Date;
18623
+ const now = nowDate.toISOString();
18624
+ const title = options.prompt.length > 80 ? `${options.prompt.slice(0, 77)}...` : options.prompt;
18625
+ const slug = slugify2(title);
18626
+ const path = `wiki/answers/${slug}.md`;
18627
+ const citations = options.context.citations;
18628
+ const body = [
18629
+ `# ${title}`,
18630
+ "",
18631
+ options.answer,
18632
+ "",
18633
+ "## Citations",
18634
+ "",
18635
+ ...citations.map((citation, index) => `- [C${index + 1}] ${citation.source_ref ?? citation.source_uri ?? citation.artifact_path ?? citation.artifact_uri ?? "unknown"} ${citation.hash ? `(hash ${citation.hash})` : ""}`),
18636
+ ""
18637
+ ].join(`
18638
+ `);
18639
+ const sourceRefs = citations.map((citation) => citation.source_ref ?? citation.source_uri).filter((ref) => Boolean(ref));
18640
+ const provenance = generatedArtifactProvenance({
18641
+ generated_from: "knowledge_answer",
18642
+ artifact_key: path,
18643
+ source_refs: sourceRefs
18644
+ });
18645
+ const artifact = await writeArtifact(options.store, {
18646
+ key: path,
18647
+ body,
18648
+ content_type: "text/markdown",
18649
+ metadata: { generated_from: "knowledge_answer" }
18650
+ });
18651
+ const log = await appendLog(options.store, {
18652
+ ts: now,
18653
+ event: "wiki_answer_filed",
18654
+ page_key: path,
18655
+ prompt: options.prompt,
18656
+ citations: citations.length
18657
+ }, nowDate);
18658
+ const pageId = stableId6("wiki", path);
18659
+ const db = openKnowledgeDb(options.dbPath);
18660
+ try {
18661
+ recordStorageObjects(db, [artifact, log], nowDate);
18662
+ upsertWikiPage(db, {
18663
+ pageId,
18664
+ path,
18665
+ title,
18666
+ artifactUri: artifact.uri,
18667
+ contentHash: artifact.hash ?? "",
18668
+ body,
18669
+ provenance,
18670
+ now
18671
+ });
18672
+ const written = replacePageCitations(db, pageId, citations.map((citation) => ({
18673
+ chunk_id: citation.chunk_id,
18674
+ source_uri: citation.source_uri ?? citation.artifact_uri ?? "unknown",
18675
+ quote: citation.quote,
18676
+ start_offset: citation.start_offset,
18677
+ end_offset: citation.end_offset,
18678
+ metadata: {
18679
+ source_ref: citation.source_ref,
18680
+ artifact_path: citation.artifact_path,
18681
+ revision: citation.revision,
18682
+ hash: citation.hash
18683
+ }
18684
+ })), now);
18685
+ upsertIndex(db, {
18686
+ title,
18687
+ path,
18688
+ artifactUri: artifact.uri,
18689
+ contentHash: artifact.hash ?? "",
18690
+ now
18691
+ });
18692
+ return {
18693
+ approved: true,
18694
+ durable_writes_performed: true,
18695
+ page_id: pageId,
18696
+ path,
18697
+ artifact_uri: artifact.uri,
18698
+ citations_written: written,
18699
+ log_key: log.key,
18700
+ message: `Filed answer to ${path}`
18701
+ };
18702
+ } finally {
18703
+ db.close();
18704
+ }
18705
+ }
18706
+ function addIssue(issues, issue2) {
18707
+ issues.push(issue2);
18708
+ }
18709
+ function lintWiki(options) {
18710
+ migrateKnowledgeDb(options.dbPath);
18711
+ const db = openKnowledgeDb(options.dbPath);
18712
+ const issues = [];
18713
+ try {
18714
+ const activePages = db.query("SELECT COUNT(*) AS n FROM wiki_pages WHERE status = 'active'").get()?.n ?? 0;
18715
+ const citationCount = db.query("SELECT COUNT(*) AS n FROM citations").get()?.n ?? 0;
18716
+ const backlinkCount = db.query("SELECT COUNT(*) AS n FROM wiki_backlinks").get()?.n ?? 0;
18717
+ const missingCitations = db.query(`SELECT wp.id, wp.path
18718
+ FROM wiki_pages wp
18719
+ LEFT JOIN citations c ON c.wiki_page_id = wp.id
18720
+ WHERE wp.status = 'active' AND wp.path LIKE 'wiki/generated/%'
18721
+ GROUP BY wp.id
18722
+ HAVING COUNT(c.id) = 0`).all();
18723
+ for (const page of missingCitations) {
18724
+ addIssue(issues, { type: "missing_citation", severity: "error", page_id: page.id, path: page.path, message: "Generated wiki page has no citations." });
18725
+ }
18726
+ const stale = db.query(`SELECT wp.id AS page_id, wp.path, c.source_uri, c.chunk_id
18727
+ FROM citations c
18728
+ JOIN wiki_pages wp ON wp.id = c.wiki_page_id
18729
+ LEFT JOIN chunks ch ON ch.id = c.chunk_id
18730
+ WHERE ch.metadata_json LIKE '%"stale":true%' OR ch.metadata_json LIKE '%"status":"stale"%' OR ch.metadata_json LIKE '%"status":"deleted"%'`).all();
18731
+ for (const row of stale) {
18732
+ addIssue(issues, { type: "stale_citation", severity: "warn", page_id: row.page_id, path: row.path, source_uri: row.source_uri, chunk_id: row.chunk_id ?? undefined, message: "Page cites a stale or deleted source chunk." });
18733
+ }
18734
+ const duplicates = db.query(`SELECT lower(title) AS title, COUNT(*) AS n
18735
+ FROM wiki_pages
18736
+ WHERE status = 'active'
18737
+ GROUP BY lower(title)
18738
+ HAVING COUNT(*) > 1`).all();
18739
+ for (const row of duplicates) {
18740
+ addIssue(issues, { type: "duplicate_page", severity: "warn", message: `Duplicate active wiki title: ${row.title} (${row.n} pages).` });
18741
+ }
18742
+ const orphans = db.query(`SELECT wp.id, wp.path
18743
+ FROM wiki_pages wp
18744
+ LEFT JOIN wiki_backlinks wb1 ON wb1.from_page_id = wp.id
18745
+ LEFT JOIN wiki_backlinks wb2 ON wb2.to_page_id = wp.id
18746
+ WHERE wp.status = 'active'
18747
+ AND wp.path NOT IN ('wiki/README.md')
18748
+ GROUP BY wp.id
18749
+ HAVING COUNT(wb1.to_page_id) = 0 AND COUNT(wb2.from_page_id) = 0`).all();
18750
+ for (const page of orphans) {
18751
+ addIssue(issues, { type: "orphan_page", severity: "info", page_id: page.id, path: page.path, message: "Wiki page has no backlinks." });
18752
+ }
18753
+ const unresolved = db.query(`SELECT wp.id AS page_id, wp.path, c.source_uri
18754
+ FROM citations c
18755
+ JOIN wiki_pages wp ON wp.id = c.wiki_page_id
18756
+ LEFT JOIN sources s ON s.uri = c.source_uri
18757
+ WHERE s.id IS NULL AND c.source_uri NOT LIKE 'file://%' AND c.source_uri NOT LIKE 's3://%' AND c.source_uri NOT LIKE 'https://%' AND c.source_uri NOT LIKE 'open-files://%'`).all();
18758
+ for (const row of unresolved) {
18759
+ addIssue(issues, { type: "unresolved_source_ref", severity: "error", page_id: row.page_id, path: row.path, source_uri: row.source_uri, message: "Citation source URI cannot be resolved to a known or allowed source ref." });
18760
+ }
18761
+ const contradictions = db.query(`SELECT id, path FROM wiki_pages WHERE lower(metadata_json) LIKE '%contradiction%'`).all();
18762
+ for (const page of contradictions) {
18763
+ addIssue(issues, { type: "contradiction_marker", severity: "warn", page_id: page.id, path: page.path, message: "Page metadata contains a contradiction marker." });
18764
+ }
18765
+ const newArticleCandidates = db.query(`SELECT c.id AS chunk_id, s.uri AS source_uri
18766
+ FROM chunks c
18767
+ JOIN source_revisions sr ON sr.id = c.source_revision_id
18768
+ JOIN sources s ON s.id = sr.source_id
18769
+ LEFT JOIN citations cit ON cit.chunk_id = c.id
18770
+ WHERE c.kind = 'source'
18771
+ GROUP BY c.id
18772
+ HAVING COUNT(cit.id) = 0
18773
+ LIMIT 25`).all();
18774
+ for (const row of newArticleCandidates) {
18775
+ addIssue(issues, { type: "new_article_candidate", severity: "info", chunk_id: row.chunk_id, source_uri: row.source_uri ?? undefined, message: "Source chunk is indexed but not cited by any wiki page yet." });
18247
18776
  }
18248
- });
18249
- insert(objects);
18777
+ return {
18778
+ ok: issues.every((issue2) => issue2.severity !== "error"),
18779
+ issue_count: issues.length,
18780
+ issues,
18781
+ counts: {
18782
+ active_pages: activePages,
18783
+ citations: citationCount,
18784
+ backlinks: backlinkCount,
18785
+ new_article_candidates: newArticleCandidates.length
18786
+ }
18787
+ };
18788
+ } finally {
18789
+ db.close();
18790
+ }
18250
18791
  }
18251
18792
 
18252
18793
  // src/wiki-layout.ts
18253
- import { createHash as createHash10 } from "crypto";
18254
- function todayParts(now) {
18794
+ import { createHash as createHash11 } from "crypto";
18795
+ function todayParts2(now) {
18255
18796
  const year = String(now.getUTCFullYear());
18256
18797
  const month = String(now.getUTCMonth() + 1).padStart(2, "0");
18257
18798
  const day = String(now.getUTCDate()).padStart(2, "0");
18258
18799
  return { year, month, day };
18259
18800
  }
18260
- function stableId6(prefix, value) {
18261
- return `${prefix}_${createHash10("sha256").update(value).digest("hex").slice(0, 20)}`;
18801
+ function stableId7(prefix, value) {
18802
+ return `${prefix}_${createHash11("sha256").update(value).digest("hex").slice(0, 20)}`;
18262
18803
  }
18263
- function estimateTokenCount2(text) {
18804
+ function estimateTokenCount3(text) {
18264
18805
  const words = text.trim().split(/\s+/).filter(Boolean).length;
18265
18806
  return Math.max(1, Math.ceil(words * 1.25));
18266
18807
  }
@@ -18319,7 +18860,7 @@ Pages should be concise, cited, and organized for both humans and agents.
18319
18860
  `;
18320
18861
  }
18321
18862
  async function initializeWikiLayout(store, now = new Date) {
18322
- const { year, month, day } = todayParts(now);
18863
+ const { year, month, day } = todayParts2(now);
18323
18864
  const schemaKey = "schemas/v1.md";
18324
18865
  const rootIndexKey = "indexes/root.md";
18325
18866
  const wikiReadmeKey = "wiki/README.md";
@@ -18376,7 +18917,7 @@ function provenanceFor(artifact) {
18376
18917
  }
18377
18918
  function recordWikiChunk(db, pageId, title, artifact, body, now) {
18378
18919
  const provenance = provenanceFor(artifact);
18379
- const chunkId = stableId6("chk", `${pageId}\x00${artifact.hash ?? artifact.uri}`);
18920
+ const chunkId = stableId7("chk", `${pageId}\x00${artifact.hash ?? artifact.uri}`);
18380
18921
  const existing = db.query("SELECT id FROM chunks WHERE wiki_page_id = ?").all(pageId);
18381
18922
  for (const row of existing)
18382
18923
  db.run("DELETE FROM chunks_fts WHERE chunk_id = ?", [row.id]);
@@ -18388,7 +18929,7 @@ function recordWikiChunk(db, pageId, title, artifact, body, now) {
18388
18929
  "wiki",
18389
18930
  0,
18390
18931
  body,
18391
- estimateTokenCount2(body),
18932
+ estimateTokenCount3(body),
18392
18933
  0,
18393
18934
  body.length,
18394
18935
  JSON.stringify({
@@ -18412,7 +18953,7 @@ function recordWikiLayoutCatalog(db, artifacts, now = new Date) {
18412
18953
  artifact_uri = excluded.artifact_uri,
18413
18954
  metadata_json = excluded.metadata_json,
18414
18955
  updated_at = excluded.updated_at`, [
18415
- stableId6("idx", "root:indexes/root.md"),
18956
+ stableId7("idx", "root:indexes/root.md"),
18416
18957
  "root",
18417
18958
  "root",
18418
18959
  rootIndex.uri,
@@ -18427,7 +18968,7 @@ function recordWikiLayoutCatalog(db, artifacts, now = new Date) {
18427
18968
  ]);
18428
18969
  }
18429
18970
  if (wikiReadme) {
18430
- const wikiPageId = stableId6("wiki", "wiki/README.md");
18971
+ const wikiPageId = stableId7("wiki", "wiki/README.md");
18431
18972
  db.run(`INSERT INTO wiki_pages (id, path, title, artifact_uri, content_hash, status, metadata_json, created_at, updated_at)
18432
18973
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
18433
18974
  ON CONFLICT(path) DO UPDATE SET
@@ -18599,6 +19140,37 @@ class KnowledgeService {
18599
19140
  }
18600
19141
  return result;
18601
19142
  }
19143
+ async compileWiki(options = {}) {
19144
+ const workspace = this.ensureWorkspace();
19145
+ return compileWikiPage({
19146
+ ...options,
19147
+ dbPath: workspace.knowledgeDbPath,
19148
+ store: this.artifactStore()
19149
+ });
19150
+ }
19151
+ async fileAnswer(options) {
19152
+ const workspace = this.ensureWorkspace();
19153
+ const context = await this.retrieveContext({
19154
+ query: options.prompt,
19155
+ limit: options.limit,
19156
+ semantic: options.semantic,
19157
+ modelRef: options.modelRef,
19158
+ dimensions: options.dimensions,
19159
+ fake: options.fake
19160
+ });
19161
+ return fileAnswerToWiki({
19162
+ dbPath: workspace.knowledgeDbPath,
19163
+ store: this.artifactStore(),
19164
+ prompt: options.prompt,
19165
+ answer: options.answer,
19166
+ context,
19167
+ approveWrite: options.approveWrite
19168
+ });
19169
+ }
19170
+ lintWiki() {
19171
+ const workspace = this.ensureWorkspace();
19172
+ return lintWiki({ dbPath: workspace.knowledgeDbPath });
19173
+ }
18602
19174
  async ingestManifest(input) {
18603
19175
  const workspace = this.ensureWorkspace();
18604
19176
  return ingestOpenFilesManifest({
@@ -18772,14 +19344,520 @@ function sortItems(items, sort = "created", desc = false) {
18772
19344
  function activeItems(items, includeArchived) {
18773
19345
  return includeArchived ? items : items.filter((item) => !item.archived);
18774
19346
  }
19347
+ function limitNumber(value, fallback = 20, max = 100) {
19348
+ if (!Number.isFinite(value) || value <= 0)
19349
+ return fallback;
19350
+ return Math.min(Math.floor(value), max);
19351
+ }
19352
+ function parseJsonObject5(value) {
19353
+ if (!value)
19354
+ return {};
19355
+ try {
19356
+ const parsed = JSON.parse(value);
19357
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
19358
+ } catch {
19359
+ return {};
19360
+ }
19361
+ }
19362
+ function jsonResource(uri, data) {
19363
+ return {
19364
+ contents: [{
19365
+ uri: uri.toString(),
19366
+ mimeType: "application/json",
19367
+ text: JSON.stringify(data, null, 2)
19368
+ }]
19369
+ };
19370
+ }
18775
19371
  function registerTool(server, name, title, description, inputSchema, handler) {
18776
19372
  server.registerTool(name, { title, description, inputSchema }, handler);
18777
19373
  }
19374
+ function registerJsonResource(server, name, uri, title, description, read) {
19375
+ server.registerResource(name, uri, {
19376
+ title,
19377
+ description,
19378
+ mimeType: "application/json"
19379
+ }, async (resourceUri) => jsonResource(resourceUri, await read(resourceUri)));
19380
+ }
19381
+ function registerJsonTemplate(server, name, template, title, description, list, read) {
19382
+ server.registerResource(name, new ResourceTemplate(template, { list }), {
19383
+ title,
19384
+ description,
19385
+ mimeType: "application/json"
19386
+ }, async (resourceUri, variables) => jsonResource(resourceUri, await read(resourceUri, variables)));
19387
+ }
19388
+ function projectService() {
19389
+ return createKnowledgeService({ scope: "project" });
19390
+ }
19391
+ function openProjectDb(service = projectService()) {
19392
+ const workspace = service.ensureWorkspace();
19393
+ migrateKnowledgeDb(workspace.knowledgeDbPath);
19394
+ return openKnowledgeDb(workspace.knowledgeDbPath);
19395
+ }
19396
+ function itemResources(storePath = createKnowledgeService({ scope: "project" }).jsonStorePath()) {
19397
+ return readStoreLocked(storePath, (db) => activeItems(db.items, false).slice(0, 100).map((item) => ({
19398
+ uri: `knowledge://project/items/${encodeURIComponent(item.id)}`,
19399
+ name: item.title,
19400
+ description: `Knowledge item ${item.id}`,
19401
+ mimeType: "application/json"
19402
+ })));
19403
+ }
19404
+ function listRows(db, sql, params = []) {
19405
+ return db.query(sql).all(...params);
19406
+ }
19407
+ function rowWithJson(row, fields = ["metadata_json", "acl_json"]) {
19408
+ if (!row)
19409
+ return null;
19410
+ const next = { ...row };
19411
+ for (const field of fields) {
19412
+ if (field in next) {
19413
+ const name = field.endsWith("_json") ? field.slice(0, -5) : field;
19414
+ next[name] = parseJsonObject5(next[field]);
19415
+ delete next[field];
19416
+ }
19417
+ }
19418
+ return next;
19419
+ }
19420
+ function dbStatsSnapshot(service = projectService()) {
19421
+ const stats = service.dbStats();
19422
+ const db = openProjectDb(service);
19423
+ try {
19424
+ return {
19425
+ ok: true,
19426
+ scope: "project",
19427
+ path: service.workspace.knowledgeDbPath,
19428
+ stats,
19429
+ schema_versions: listRows(db, "SELECT version, applied_at FROM schema_versions ORDER BY version ASC")
19430
+ };
19431
+ } finally {
19432
+ db.close();
19433
+ }
19434
+ }
19435
+ function storageSnapshot(service = projectService()) {
19436
+ const validation = service.validateStorage();
19437
+ return {
19438
+ ok: validation.ok,
19439
+ scope: "project",
19440
+ paths: service.paths(),
19441
+ storage: service.storageContract(),
19442
+ validation
19443
+ };
19444
+ }
19445
+ function configSnapshot(service = projectService()) {
19446
+ return {
19447
+ ok: true,
19448
+ scope: "project",
19449
+ package: {
19450
+ name: package_default.name,
19451
+ version: package_default.version
19452
+ },
19453
+ paths: service.paths(),
19454
+ storage: service.storageContract(),
19455
+ provider_status: service.providerStatus(),
19456
+ model_registry: service.modelRegistry()
19457
+ };
19458
+ }
19459
+ function sourceRows(limit = 50, service = projectService()) {
19460
+ const db = openProjectDb(service);
19461
+ try {
19462
+ return listRows(db, `
19463
+ SELECT
19464
+ s.id,
19465
+ s.uri,
19466
+ s.kind,
19467
+ s.title,
19468
+ s.metadata_json,
19469
+ s.acl_json,
19470
+ s.created_at,
19471
+ s.updated_at,
19472
+ COUNT(DISTINCT sr.id) AS revisions,
19473
+ COUNT(DISTINCT c.id) AS chunks
19474
+ FROM sources s
19475
+ LEFT JOIN source_revisions sr ON sr.source_id = s.id
19476
+ LEFT JOIN chunks c ON c.source_revision_id = sr.id
19477
+ GROUP BY s.id
19478
+ ORDER BY s.updated_at DESC
19479
+ LIMIT ?
19480
+ `, [limitNumber(limit, 50, 200)]).map((row) => rowWithJson(row));
19481
+ } finally {
19482
+ db.close();
19483
+ }
19484
+ }
19485
+ function sourceSnapshot(id, { limit = 10, service = projectService() } = {}) {
19486
+ const db = openProjectDb(service);
19487
+ try {
19488
+ const source = rowWithJson(db.query(`
19489
+ SELECT id, uri, kind, title, metadata_json, acl_json, created_at, updated_at
19490
+ FROM sources
19491
+ WHERE id = ? OR uri = ?
19492
+ `).get(id, id));
19493
+ if (!source)
19494
+ return null;
19495
+ const revisions = listRows(db, `
19496
+ SELECT id, revision, hash, extracted_text_uri, metadata_json, created_at
19497
+ FROM source_revisions
19498
+ WHERE source_id = ?
19499
+ ORDER BY created_at DESC
19500
+ LIMIT ?
19501
+ `, [source.id, limitNumber(limit, 10, 100)]).map((row) => rowWithJson(row, ["metadata_json"]));
19502
+ const chunks = listRows(db, `
19503
+ SELECT c.id, c.kind, c.ordinal, c.text, c.token_count, c.start_offset, c.end_offset, c.metadata_json, c.created_at,
19504
+ sr.revision, sr.hash
19505
+ FROM chunks c
19506
+ JOIN source_revisions sr ON sr.id = c.source_revision_id
19507
+ WHERE sr.source_id = ?
19508
+ ORDER BY sr.created_at DESC, c.ordinal ASC
19509
+ LIMIT ?
19510
+ `, [source.id, limitNumber(limit, 10, 50)]).map((row) => rowWithJson(row, ["metadata_json"]));
19511
+ return { source, revisions, chunks };
19512
+ } finally {
19513
+ db.close();
19514
+ }
19515
+ }
19516
+ function openFilesSnapshot(service = projectService()) {
19517
+ const db = openProjectDb(service);
19518
+ try {
19519
+ const rows = listRows(db, `
19520
+ SELECT
19521
+ s.id,
19522
+ s.uri,
19523
+ s.title,
19524
+ sr.revision,
19525
+ sr.hash,
19526
+ c.metadata_json,
19527
+ COUNT(c.id) AS chunks
19528
+ FROM sources s
19529
+ JOIN source_revisions sr ON sr.source_id = s.id
19530
+ LEFT JOIN chunks c ON c.source_revision_id = sr.id
19531
+ WHERE s.uri LIKE 'open-files://%'
19532
+ GROUP BY s.id, sr.id
19533
+ ORDER BY s.updated_at DESC
19534
+ LIMIT 100
19535
+ `);
19536
+ return {
19537
+ ok: true,
19538
+ scope: "project",
19539
+ source_ownership: "open-files",
19540
+ raw_source_bytes_exposed: false,
19541
+ refs: rows.map((row) => {
19542
+ const metadata = parseJsonObject5(row.metadata_json);
19543
+ return {
19544
+ id: row.id,
19545
+ uri: row.uri,
19546
+ source_ref: typeof metadata.source_ref === "string" ? metadata.source_ref : row.uri,
19547
+ title: row.title,
19548
+ revision: row.revision,
19549
+ hash: row.hash,
19550
+ chunks: row.chunks
19551
+ };
19552
+ })
19553
+ };
19554
+ } finally {
19555
+ db.close();
19556
+ }
19557
+ }
19558
+ function wikiRows(limit = 50, service = projectService()) {
19559
+ const db = openProjectDb(service);
19560
+ try {
19561
+ return listRows(db, `
19562
+ SELECT id, path, title, artifact_uri, content_hash, status, metadata_json, created_at, updated_at
19563
+ FROM wiki_pages
19564
+ ORDER BY updated_at DESC
19565
+ LIMIT ?
19566
+ `, [limitNumber(limit, 50, 200)]).map((row) => rowWithJson(row, ["metadata_json"]));
19567
+ } finally {
19568
+ db.close();
19569
+ }
19570
+ }
19571
+ async function wikiSnapshot(id, { includeContent = true, service = projectService() } = {}) {
19572
+ const db = openProjectDb(service);
19573
+ try {
19574
+ const page = rowWithJson(db.query(`
19575
+ SELECT id, path, title, artifact_uri, content_hash, status, metadata_json, created_at, updated_at
19576
+ FROM wiki_pages
19577
+ WHERE id = ? OR path = ?
19578
+ `).get(id, id), ["metadata_json"]);
19579
+ if (!page)
19580
+ return null;
19581
+ const citations = listRows(db, `
19582
+ SELECT id, chunk_id, source_uri, quote, start_offset, end_offset, metadata_json, created_at
19583
+ FROM citations
19584
+ WHERE wiki_page_id = ?
19585
+ ORDER BY created_at ASC
19586
+ LIMIT 100
19587
+ `, [page.id]).map((row) => rowWithJson(row, ["metadata_json"]));
19588
+ let content = null;
19589
+ if (includeContent) {
19590
+ const artifactKey = page.metadata?.artifact_key ?? page.path;
19591
+ if (typeof artifactKey === "string") {
19592
+ try {
19593
+ content = await service.artifactStore().getText(artifactKey);
19594
+ } catch {
19595
+ content = null;
19596
+ }
19597
+ }
19598
+ }
19599
+ return { page, citations, content };
19600
+ } finally {
19601
+ db.close();
19602
+ }
19603
+ }
19604
+ function indexRows(limit = 50, service = projectService()) {
19605
+ const db = openProjectDb(service);
19606
+ try {
19607
+ return listRows(db, `
19608
+ SELECT id, kind, name, artifact_uri, shard_key, metadata_json, created_at, updated_at
19609
+ FROM knowledge_indexes
19610
+ ORDER BY updated_at DESC
19611
+ LIMIT ?
19612
+ `, [limitNumber(limit, 50, 200)]).map((row) => rowWithJson(row, ["metadata_json"]));
19613
+ } finally {
19614
+ db.close();
19615
+ }
19616
+ }
19617
+ function indexSnapshot(id, service = projectService()) {
19618
+ const db = openProjectDb(service);
19619
+ try {
19620
+ const index = rowWithJson(db.query(`
19621
+ SELECT id, kind, name, artifact_uri, shard_key, metadata_json, created_at, updated_at
19622
+ FROM knowledge_indexes
19623
+ WHERE id = ? OR name = ? OR shard_key = ?
19624
+ `).get(id, id, id), ["metadata_json"]);
19625
+ if (!index)
19626
+ return null;
19627
+ const vector_counts = listRows(db, `
19628
+ SELECT provider, model, dimensions, status, COUNT(*) AS entries
19629
+ FROM vector_index_entries
19630
+ GROUP BY provider, model, dimensions, status
19631
+ ORDER BY entries DESC
19632
+ LIMIT 50
19633
+ `);
19634
+ return { index, vector_counts };
19635
+ } finally {
19636
+ db.close();
19637
+ }
19638
+ }
19639
+ function runRows(limit = 50, service = projectService()) {
19640
+ const db = openProjectDb(service);
19641
+ try {
19642
+ return listRows(db, `
19643
+ SELECT id, type, prompt, status, provider, model, cost_tokens, cost_usd, metadata_json, created_at, updated_at
19644
+ FROM runs
19645
+ ORDER BY updated_at DESC
19646
+ LIMIT ?
19647
+ `, [limitNumber(limit, 50, 200)]).map((row) => rowWithJson(row, ["metadata_json"]));
19648
+ } finally {
19649
+ db.close();
19650
+ }
19651
+ }
19652
+ function runSnapshot(id, { limit = 50, service = projectService() } = {}) {
19653
+ const db = openProjectDb(service);
19654
+ try {
19655
+ const run = rowWithJson(db.query(`
19656
+ SELECT id, type, prompt, status, provider, model, cost_tokens, cost_usd, metadata_json, created_at, updated_at
19657
+ FROM runs
19658
+ WHERE id = ?
19659
+ `).get(id), ["metadata_json"]);
19660
+ if (!run)
19661
+ return null;
19662
+ const events = listRows(db, `
19663
+ SELECT id, level, event, metadata_json, created_at
19664
+ FROM run_events
19665
+ WHERE run_id = ?
19666
+ ORDER BY created_at ASC
19667
+ LIMIT ?
19668
+ `, [id, limitNumber(limit, 50, 200)]).map((row) => rowWithJson(row, ["metadata_json"]));
19669
+ const usage = listRows(db, `
19670
+ SELECT id, provider, model, input_tokens, output_tokens, cost_usd, metadata_json, created_at
19671
+ FROM provider_usage
19672
+ WHERE run_id = ?
19673
+ ORDER BY created_at ASC
19674
+ LIMIT 100
19675
+ `, [id]).map((row) => rowWithJson(row, ["metadata_json"]));
19676
+ return { run, events, usage };
19677
+ } finally {
19678
+ db.close();
19679
+ }
19680
+ }
19681
+ function decisionsSnapshot(limit = 50, service = projectService()) {
19682
+ const db = openProjectDb(service);
19683
+ try {
19684
+ return {
19685
+ ok: true,
19686
+ scope: "project",
19687
+ approval_gates: listRows(db, `
19688
+ SELECT id, action, target_uri, status, reason, approved_by, metadata_json, created_at, updated_at
19689
+ FROM approval_gates
19690
+ ORDER BY updated_at DESC
19691
+ LIMIT ?
19692
+ `, [limitNumber(limit, 50, 200)]).map((row) => rowWithJson(row, ["metadata_json"])),
19693
+ audit_events: listRows(db, `
19694
+ SELECT id, event_type, action, target_uri, decision, metadata_json, created_at
19695
+ FROM audit_events
19696
+ ORDER BY created_at DESC
19697
+ LIMIT ?
19698
+ `, [limitNumber(limit, 50, 200)]).map((row) => rowWithJson(row, ["metadata_json"]))
19699
+ };
19700
+ } finally {
19701
+ db.close();
19702
+ }
19703
+ }
19704
+ function decisionSnapshot(id, service = projectService()) {
19705
+ const db = openProjectDb(service);
19706
+ try {
19707
+ const approval = rowWithJson(db.query(`
19708
+ SELECT id, action, target_uri, status, reason, approved_by, metadata_json, created_at, updated_at
19709
+ FROM approval_gates
19710
+ WHERE id = ? OR target_uri = ?
19711
+ `).get(id, id), ["metadata_json"]);
19712
+ if (approval)
19713
+ return { kind: "approval_gate", decision: approval };
19714
+ const audit = rowWithJson(db.query(`
19715
+ SELECT id, event_type, action, target_uri, decision, metadata_json, created_at
19716
+ FROM audit_events
19717
+ WHERE id = ? OR target_uri = ?
19718
+ `).get(id, id), ["metadata_json"]);
19719
+ return audit ? { kind: "audit_event", decision: audit } : null;
19720
+ } finally {
19721
+ db.close();
19722
+ }
19723
+ }
19724
+ async function getKnowledgeRecord(kind, id, options = {}) {
19725
+ const normalized = kind ?? "auto";
19726
+ const service = createKnowledgeService({ scope: options.scope });
19727
+ const attempts = normalized === "auto" ? ["item", "source", "wiki_page", "run", "index", "decision"] : [normalized];
19728
+ for (const entry of attempts) {
19729
+ if (entry === "item") {
19730
+ const storePath = resolveStorePath(options.store_path, options.scope);
19731
+ const item = readStoreLocked(storePath, (db) => findItem(db, id));
19732
+ if (item)
19733
+ return { kind: "item", item, store_path: storePath };
19734
+ }
19735
+ if (entry === "source") {
19736
+ const source = sourceSnapshot(id, { limit: options.limit, service });
19737
+ if (source)
19738
+ return { kind: "source", ...source };
19739
+ }
19740
+ if (entry === "wiki_page") {
19741
+ const page = await wikiSnapshot(id, { includeContent: options.include_content !== false, service });
19742
+ if (page)
19743
+ return { kind: "wiki_page", ...page };
19744
+ }
19745
+ if (entry === "run") {
19746
+ const run = runSnapshot(id, { limit: options.limit, service });
19747
+ if (run)
19748
+ return { kind: "run", ...run };
19749
+ }
19750
+ if (entry === "index") {
19751
+ const index = indexSnapshot(id, service);
19752
+ if (index)
19753
+ return { kind: "index", ...index };
19754
+ }
19755
+ if (entry === "decision") {
19756
+ const decision = decisionSnapshot(id, service);
19757
+ if (decision)
19758
+ return { kind: "decision", ...decision };
19759
+ }
19760
+ }
19761
+ return null;
19762
+ }
19763
+ function registerKnowledgeResources(server) {
19764
+ registerJsonResource(server, "knowledge-project-config", "knowledge://project/config", "Project knowledge config", "Resolved project workspace config, provider registry, and storage contract", async () => configSnapshot());
19765
+ registerJsonResource(server, "knowledge-project-storage", "knowledge://project/storage", "Project knowledge storage", "Artifact storage contract and validation for project knowledge", async () => storageSnapshot());
19766
+ registerJsonResource(server, "knowledge-project-schema", "knowledge://project/schema", "Project knowledge schema", "SQLite schema version and table counts for project knowledge", async () => dbStatsSnapshot());
19767
+ registerJsonResource(server, "knowledge-project-sources", "knowledge://project/sources", "Project knowledge sources", "Indexed source refs and revision/chunk counts without raw source bytes", async () => ({ ok: true, scope: "project", sources: sourceRows() }));
19768
+ registerJsonResource(server, "knowledge-project-open-files", "knowledge://project/open-files", "Project open-files refs", "Open-files source refs known to the project knowledge catalog", async () => openFilesSnapshot());
19769
+ registerJsonResource(server, "knowledge-project-wiki-pages", "knowledge://project/wiki/pages", "Project wiki pages", "Generated wiki pages and citation artifact metadata", async () => ({ ok: true, scope: "project", pages: wikiRows() }));
19770
+ registerJsonResource(server, "knowledge-project-indexes", "knowledge://project/indexes", "Project knowledge indexes", "Sharded knowledge indexes and vector-index status", async () => ({
19771
+ ok: true,
19772
+ scope: "project",
19773
+ indexes: indexRows(),
19774
+ embeddings: projectService().embeddingStatus()
19775
+ }));
19776
+ registerJsonResource(server, "knowledge-project-runs", "knowledge://project/runs", "Project knowledge runs", "Recent prompt, ingestion, web search, and reindex run ledger entries", async () => ({ ok: true, scope: "project", runs: runRows() }));
19777
+ registerJsonResource(server, "knowledge-project-decisions", "knowledge://project/decisions", "Project knowledge decisions", "Approval gates and audit decisions for generated knowledge operations", async () => decisionsSnapshot());
19778
+ registerJsonTemplate(server, "knowledge-project-items", "knowledge://project/items/{id}", "Project knowledge item", "Read a compatibility JSON-store item by id", async () => ({ resources: itemResources() }), async (_uri, variables) => {
19779
+ const id = decodeURIComponent(String(variables.id));
19780
+ const record2 = await getKnowledgeRecord("item", id, { scope: "project" });
19781
+ return record2 ? { ok: true, ...record2 } : { ok: false, error: `Item not found: ${id}` };
19782
+ });
19783
+ registerJsonTemplate(server, "knowledge-project-source", "knowledge://project/sources/{id}", "Project source", "Read indexed source metadata, revisions, and derived chunks", async () => ({
19784
+ resources: sourceRows().map((source) => ({
19785
+ uri: `knowledge://project/sources/${encodeURIComponent(source.id)}`,
19786
+ name: source.title ?? source.uri,
19787
+ description: `${source.kind} source with ${source.chunks} chunk(s)`,
19788
+ mimeType: "application/json"
19789
+ }))
19790
+ }), async (_uri, variables) => {
19791
+ const id = decodeURIComponent(String(variables.id));
19792
+ const record2 = sourceSnapshot(id);
19793
+ return record2 ? { ok: true, kind: "source", ...record2 } : { ok: false, error: `Source not found: ${id}` };
19794
+ });
19795
+ registerJsonTemplate(server, "knowledge-project-wiki-page", "knowledge://project/wiki/pages/{id}", "Project wiki page", "Read generated wiki page metadata, citations, and artifact text", async () => ({
19796
+ resources: wikiRows().map((page) => ({
19797
+ uri: `knowledge://project/wiki/pages/${encodeURIComponent(page.id)}`,
19798
+ name: page.title,
19799
+ description: page.path,
19800
+ mimeType: "application/json"
19801
+ }))
19802
+ }), async (_uri, variables) => {
19803
+ const id = decodeURIComponent(String(variables.id));
19804
+ const record2 = await wikiSnapshot(id);
19805
+ return record2 ? { ok: true, kind: "wiki_page", ...record2 } : { ok: false, error: `Wiki page not found: ${id}` };
19806
+ });
19807
+ registerJsonTemplate(server, "knowledge-project-index", "knowledge://project/indexes/{id}", "Project knowledge index", "Read a knowledge index row and vector-count snapshot", async () => ({
19808
+ resources: indexRows().map((index) => ({
19809
+ uri: `knowledge://project/indexes/${encodeURIComponent(index.id)}`,
19810
+ name: index.name,
19811
+ description: `${index.kind} index${index.shard_key ? ` shard ${index.shard_key}` : ""}`,
19812
+ mimeType: "application/json"
19813
+ }))
19814
+ }), async (_uri, variables) => {
19815
+ const id = decodeURIComponent(String(variables.id));
19816
+ const record2 = indexSnapshot(id);
19817
+ return record2 ? { ok: true, kind: "index", ...record2 } : { ok: false, error: `Index not found: ${id}` };
19818
+ });
19819
+ registerJsonTemplate(server, "knowledge-project-run", "knowledge://project/runs/{id}", "Project run", "Read a knowledge run ledger entry with events and usage", async () => ({
19820
+ resources: runRows().map((run) => ({
19821
+ uri: `knowledge://project/runs/${encodeURIComponent(run.id)}`,
19822
+ name: `${run.type}: ${run.status}`,
19823
+ description: run.prompt ?? run.id,
19824
+ mimeType: "application/json"
19825
+ }))
19826
+ }), async (_uri, variables) => {
19827
+ const id = decodeURIComponent(String(variables.id));
19828
+ const record2 = runSnapshot(id);
19829
+ return record2 ? { ok: true, kind: "run", ...record2 } : { ok: false, error: `Run not found: ${id}` };
19830
+ });
19831
+ registerJsonTemplate(server, "knowledge-project-decision", "knowledge://project/decisions/{id}", "Project decision", "Read an approval gate or audit decision", async () => {
19832
+ const decisions = decisionsSnapshot();
19833
+ return {
19834
+ resources: [
19835
+ ...decisions.approval_gates.map((entry) => ({
19836
+ uri: `knowledge://project/decisions/${encodeURIComponent(entry.id)}`,
19837
+ name: `${entry.action}: ${entry.status}`,
19838
+ description: entry.target_uri ?? entry.id,
19839
+ mimeType: "application/json"
19840
+ })),
19841
+ ...decisions.audit_events.map((entry) => ({
19842
+ uri: `knowledge://project/decisions/${encodeURIComponent(entry.id)}`,
19843
+ name: `${entry.action}: ${entry.decision}`,
19844
+ description: entry.target_uri ?? entry.id,
19845
+ mimeType: "application/json"
19846
+ }))
19847
+ ]
19848
+ };
19849
+ }, async (_uri, variables) => {
19850
+ const id = decodeURIComponent(String(variables.id));
19851
+ const record2 = decisionSnapshot(id);
19852
+ return record2 ? { ok: true, ...record2 } : { ok: false, error: `Decision not found: ${id}` };
19853
+ });
19854
+ }
18778
19855
  function buildServer() {
18779
19856
  const server = new McpServer({
18780
19857
  name: "open-knowledge",
18781
19858
  version: package_default.version
18782
19859
  });
19860
+ registerKnowledgeResources(server);
18783
19861
  registerTool(server, "ok_paths", "Knowledge workspace paths", "Show resolved workspace and store paths", {
18784
19862
  scope: scopeField
18785
19863
  }, async ({ scope }) => {
@@ -18960,6 +20038,150 @@ function buildServer() {
18960
20038
  return errorText(error48 instanceof Error ? error48.message : String(error48));
18961
20039
  }
18962
20040
  });
20041
+ registerTool(server, "knowledge_get", "Get knowledge record", "Read a knowledge item, indexed source, wiki page, run, index, or decision by id without raw source-byte access", {
20042
+ scope: scopeField,
20043
+ kind: exports_external.enum(["auto", "item", "source", "wiki_page", "run", "index", "decision"]).optional().describe("Record kind; auto tries all supported kinds"),
20044
+ id: exports_external.string().describe("Record id, short id, source URI, wiki path, index shard/name, or decision target URI"),
20045
+ include_content: exports_external.boolean().optional().describe("Include generated wiki artifact text when reading wiki pages"),
20046
+ limit: exports_external.number().optional().describe("Maximum related chunks/events to return"),
20047
+ store_path: storePathField
20048
+ }, async ({ scope, kind, id, include_content, limit, store_path }) => {
20049
+ try {
20050
+ const record2 = await getKnowledgeRecord(kind ?? "auto", id, {
20051
+ scope,
20052
+ include_content,
20053
+ limit,
20054
+ store_path
20055
+ });
20056
+ return record2 ? jsonText({ ok: true, ...record2 }) : errorText(`Knowledge record not found: ${id}`);
20057
+ } catch (error48) {
20058
+ return errorText(error48 instanceof Error ? error48.message : String(error48));
20059
+ }
20060
+ });
20061
+ registerTool(server, "knowledge_ingest", "Ingest knowledge source", "Ingest an open-files/S3/file/web source ref or open-files manifest into the derived knowledge catalog", {
20062
+ scope: scopeField,
20063
+ source_ref: exports_external.string().optional().describe("Source reference URI to ingest, e.g. open-files://file/<id>/revision/<rev>"),
20064
+ manifest: exports_external.string().optional().describe("Manifest file path or s3:// URI to ingest"),
20065
+ purpose: exports_external.string().optional().describe("Read-only purpose label, default knowledge_answer")
20066
+ }, async ({ scope, source_ref, manifest, purpose }) => {
20067
+ if (!source_ref && !manifest)
20068
+ return errorText("Missing input. Provide source_ref or manifest.");
20069
+ if (source_ref && manifest)
20070
+ return errorText("Use either source_ref or manifest, not both.");
20071
+ const service = createKnowledgeService({ scope });
20072
+ try {
20073
+ const result = source_ref ? await service.ingestSource(source_ref, purpose) : await service.ingestManifest(manifest);
20074
+ return jsonText({ ok: true, mode: source_ref ? "source" : "manifest", ...result });
20075
+ } catch (error48) {
20076
+ return errorText(error48 instanceof Error ? error48.message : String(error48));
20077
+ }
20078
+ });
20079
+ registerTool(server, "knowledge_build", "Build knowledge answer", "Run the knowledge prompt flow and optionally file the cited answer into generated wiki artifacts after approval", {
20080
+ scope: scopeField,
20081
+ prompt: exports_external.string().describe("Prompt to answer and build durable knowledge from"),
20082
+ limit: exports_external.number().optional().describe("Maximum context results"),
20083
+ semantic: exports_external.boolean().optional().describe("Include vector semantic results"),
20084
+ generate: exports_external.boolean().optional().describe("Call AI SDK text generation; omitted returns a local citation draft"),
20085
+ approve_write: exports_external.boolean().optional().describe("Approve durable wiki filing for this call"),
20086
+ file_answer: exports_external.boolean().optional().describe("Attempt wiki answer filing; writes only with approve_write=true"),
20087
+ model: exports_external.string().optional().describe("Model alias/ref, default configured provider default"),
20088
+ dimensions: exports_external.number().optional().describe("Embedding dimensions for deterministic fake mode"),
20089
+ fake: exports_external.boolean().optional().describe("Use deterministic fake embeddings/generation for local tests")
20090
+ }, async ({ scope, prompt, limit, semantic, generate, approve_write, file_answer, model, dimensions, fake }) => {
20091
+ const service = createKnowledgeService({ scope });
20092
+ try {
20093
+ const result = await service.runPrompt({ prompt, limit, semantic, generate, approveWrite: approve_write, modelRef: model, dimensions, fake });
20094
+ let wiki_file = null;
20095
+ if (file_answer === true || approve_write === true) {
20096
+ wiki_file = await service.fileAnswer({
20097
+ prompt,
20098
+ answer: result.answer,
20099
+ approveWrite: approve_write,
20100
+ limit,
20101
+ semantic,
20102
+ modelRef: model,
20103
+ dimensions,
20104
+ fake
20105
+ });
20106
+ }
20107
+ return jsonText({ ok: true, ...result, wiki_file });
20108
+ } catch (error48) {
20109
+ return errorText(error48 instanceof Error ? error48.message : String(error48));
20110
+ }
20111
+ });
20112
+ registerTool(server, "knowledge_web_search", "Knowledge web search", "Run safety-gated provider-native web search and optionally file snippets as web source refs", {
20113
+ scope: scopeField,
20114
+ query: exports_external.string().describe("Web search query"),
20115
+ limit: exports_external.number().optional().describe("Maximum sources"),
20116
+ provider: exports_external.enum(["openai", "anthropic", "deepseek"]).optional().describe("Provider override"),
20117
+ model: exports_external.string().optional().describe("Model alias/ref"),
20118
+ domains: exports_external.array(exports_external.string()).optional().describe("Allowed domains"),
20119
+ fake: exports_external.boolean().optional().describe("Use deterministic fake web results"),
20120
+ file_results: exports_external.boolean().optional().describe("File web snippets as web source refs")
20121
+ }, async ({ scope, query, limit, provider, model, domains, fake, file_results }) => {
20122
+ const service = createKnowledgeService({ scope });
20123
+ try {
20124
+ return jsonText({ ok: true, ...await service.webSearch({ query, limit, provider, modelRef: model, domains, fake, fileResults: file_results }) });
20125
+ } catch (error48) {
20126
+ return errorText(error48 instanceof Error ? error48.message : String(error48));
20127
+ }
20128
+ });
20129
+ registerTool(server, "knowledge_lint", "Lint knowledge wiki", "Check generated wiki pages for missing citations, stale citations, duplicates, or source issues", {
20130
+ scope: scopeField
20131
+ }, async ({ scope }) => {
20132
+ const service = createKnowledgeService({ scope });
20133
+ try {
20134
+ return jsonText({ ok: true, ...service.lintWiki() });
20135
+ } catch (error48) {
20136
+ return errorText(error48 instanceof Error ? error48.message : String(error48));
20137
+ }
20138
+ });
20139
+ registerTool(server, "knowledge_run_status", "Knowledge run status", "List recent runs or inspect one run ledger with events and provider usage", {
20140
+ scope: scopeField,
20141
+ run_id: exports_external.string().optional().describe("Run id to inspect; omitted lists recent runs"),
20142
+ limit: exports_external.number().optional().describe("Maximum runs or events to return")
20143
+ }, async ({ scope, run_id, limit }) => {
20144
+ const service = createKnowledgeService({ scope });
20145
+ try {
20146
+ if (run_id) {
20147
+ const run = runSnapshot(run_id, { limit, service });
20148
+ return run ? jsonText({ ok: true, kind: "run", ...run }) : errorText(`Run not found: ${run_id}`);
20149
+ }
20150
+ return jsonText({ ok: true, runs: runRows(limit, service) });
20151
+ } catch (error48) {
20152
+ return errorText(error48 instanceof Error ? error48.message : String(error48));
20153
+ }
20154
+ });
20155
+ registerTool(server, "knowledge_storage", "Knowledge storage contract", "Inspect local/S3 artifact storage, source ownership, and hosted/SaaS boundary metadata", {
20156
+ scope: scopeField
20157
+ }, async ({ scope }) => {
20158
+ const service = createKnowledgeService({ scope });
20159
+ try {
20160
+ const validation = service.validateStorage();
20161
+ return jsonText({
20162
+ ok: validation.ok,
20163
+ ...service.storageContract(),
20164
+ validation,
20165
+ remote_contract: service.remoteContract()
20166
+ });
20167
+ } catch (error48) {
20168
+ return errorText(error48 instanceof Error ? error48.message : String(error48));
20169
+ }
20170
+ });
20171
+ registerTool(server, "knowledge_resolve_source", "Resolve knowledge source", "Resolve indexed source chunks through the read-only open-files/source boundary with citation evidence", {
20172
+ source_ref: exports_external.string().describe("Source reference URI, preferably open-files://..."),
20173
+ purpose: exports_external.string().optional().describe("Read-only purpose label, default knowledge_answer"),
20174
+ limit: exports_external.number().optional().describe("Maximum chunks to return, default 10"),
20175
+ scope: scopeField
20176
+ }, async ({ source_ref, purpose, limit, scope }) => {
20177
+ const service = createKnowledgeService({ scope });
20178
+ try {
20179
+ const result = await service.resolveSource(source_ref, { purpose, limit });
20180
+ return jsonText({ ok: true, ...result });
20181
+ } catch (error48) {
20182
+ return errorText(error48 instanceof Error ? error48.message : String(error48));
20183
+ }
20184
+ });
18963
20185
  registerTool(server, "ok_web_search", "Provider web search", "Run safety-gated provider-native web search and return citations/sources", {
18964
20186
  scope: scopeField,
18965
20187
  query: exports_external.string().describe("Web search query"),