@hasna/knowledge 0.2.22 → 0.2.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +48 -13
- package/bin/open-knowledge-mcp.js +1080 -430
- package/bin/open-knowledge.js +1 -1
- package/docs/architecture/ai-native-knowledge-base.md +14 -0
- package/docs/architecture/hybrid-semantic-search.md +11 -10
- package/package.json +1 -1
- package/src/mcp.js +789 -1
|
@@ -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.
|
|
13663
|
+
version: "0.2.24",
|
|
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/
|
|
13727
|
-
import {
|
|
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/
|
|
13859
|
-
|
|
13860
|
-
|
|
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
|
-
|
|
13934
|
-
|
|
13935
|
-
|
|
13936
|
-
|
|
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
|
-
|
|
13998
|
-
|
|
13999
|
-
|
|
14000
|
-
|
|
14001
|
-
|
|
14002
|
-
|
|
14003
|
-
|
|
14004
|
-
|
|
14005
|
-
|
|
14006
|
-
|
|
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
|
-
|
|
14019
|
-
|
|
14020
|
-
|
|
14021
|
-
|
|
14022
|
-
|
|
14023
|
-
|
|
14024
|
-
|
|
14025
|
-
|
|
14026
|
-
|
|
14027
|
-
|
|
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,
|
|
@@ -14536,28 +14158,406 @@ function count(db, table) {
|
|
|
14536
14158
|
function getKnowledgeDbStats(path) {
|
|
14537
14159
|
const db = openKnowledgeDb(path);
|
|
14538
14160
|
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();
|
|
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);
|
|
14502
|
+
}
|
|
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
|
+
}
|
|
14513
|
+
}
|
|
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) {
|
|
14527
|
+
try {
|
|
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";
|
|
@@ -19344,14 +19344,520 @@ function sortItems(items, sort = "created", desc = false) {
|
|
|
19344
19344
|
function activeItems(items, includeArchived) {
|
|
19345
19345
|
return includeArchived ? items : items.filter((item) => !item.archived);
|
|
19346
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
|
+
}
|
|
19347
19371
|
function registerTool(server, name, title, description, inputSchema, handler) {
|
|
19348
19372
|
server.registerTool(name, { title, description, inputSchema }, handler);
|
|
19349
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
|
+
}
|
|
19350
19855
|
function buildServer() {
|
|
19351
19856
|
const server = new McpServer({
|
|
19352
19857
|
name: "open-knowledge",
|
|
19353
19858
|
version: package_default.version
|
|
19354
19859
|
});
|
|
19860
|
+
registerKnowledgeResources(server);
|
|
19355
19861
|
registerTool(server, "ok_paths", "Knowledge workspace paths", "Show resolved workspace and store paths", {
|
|
19356
19862
|
scope: scopeField
|
|
19357
19863
|
}, async ({ scope }) => {
|
|
@@ -19532,6 +20038,150 @@ function buildServer() {
|
|
|
19532
20038
|
return errorText(error48 instanceof Error ? error48.message : String(error48));
|
|
19533
20039
|
}
|
|
19534
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
|
+
});
|
|
19535
20185
|
registerTool(server, "ok_web_search", "Provider web search", "Run safety-gated provider-native web search and return citations/sources", {
|
|
19536
20186
|
scope: scopeField,
|
|
19537
20187
|
query: exports_external.string().describe("Web search query"),
|