@hasna/knowledge 0.2.7 → 0.2.9
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 +35 -1
- package/bin/open-knowledge-mcp.js +624 -5
- package/bin/open-knowledge.js +47 -25
- package/docs/architecture/ai-native-knowledge-base.md +24 -0
- package/package.json +1 -1
- package/src/cli.ts +61 -13
- package/src/manifest-ingest.ts +36 -10
- package/src/mcp.js +25 -0
- package/src/source-ingest.ts +268 -0
- package/src/source-ref.ts +12 -0
- package/src/source-resolver.ts +418 -0
|
@@ -100,9 +100,9 @@ async function startMcpHttpServer(buildServer, options = {}) {
|
|
|
100
100
|
}
|
|
101
101
|
}
|
|
102
102
|
});
|
|
103
|
-
await new Promise((
|
|
103
|
+
await new Promise((resolve3, reject) => {
|
|
104
104
|
httpServer.once("error", reject);
|
|
105
|
-
httpServer.listen(requestedPort, host, () =>
|
|
105
|
+
httpServer.listen(requestedPort, host, () => resolve3());
|
|
106
106
|
});
|
|
107
107
|
const addr = httpServer.address();
|
|
108
108
|
const port = typeof addr === "object" && addr ? addr.port : requestedPort;
|
|
@@ -110,8 +110,8 @@ async function startMcpHttpServer(buildServer, options = {}) {
|
|
|
110
110
|
return {
|
|
111
111
|
port,
|
|
112
112
|
host,
|
|
113
|
-
close: () => new Promise((
|
|
114
|
-
httpServer.close((err) => err ? reject(err) :
|
|
113
|
+
close: () => new Promise((resolve3, reject) => {
|
|
114
|
+
httpServer.close((err) => err ? reject(err) : resolve3());
|
|
115
115
|
})
|
|
116
116
|
};
|
|
117
117
|
}
|
|
@@ -13659,7 +13659,7 @@ import { existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync
|
|
|
13659
13659
|
// package.json
|
|
13660
13660
|
var package_default = {
|
|
13661
13661
|
name: "@hasna/knowledge",
|
|
13662
|
-
version: "0.2.
|
|
13662
|
+
version: "0.2.9",
|
|
13663
13663
|
description: "Agent-friendly local knowledge CLI with JSON output, pagination, and safe destructive actions",
|
|
13664
13664
|
type: "module",
|
|
13665
13665
|
bin: {
|
|
@@ -13941,6 +13941,603 @@ function parseSourceRef(uri) {
|
|
|
13941
13941
|
return parseWebRef(uri);
|
|
13942
13942
|
throw new Error(`Unsupported source ref scheme: ${uri}`);
|
|
13943
13943
|
}
|
|
13944
|
+
function catalogSourceUriForRef(uri, parsed = parseSourceRef(uri)) {
|
|
13945
|
+
if (parsed.kind === "open-files" && parsed.entity === "file" && parsed.revision_id) {
|
|
13946
|
+
return uri.replace(/\/revision\/[^/]+$/, "");
|
|
13947
|
+
}
|
|
13948
|
+
return uri;
|
|
13949
|
+
}
|
|
13950
|
+
function revisionIdForSourceRef(uri) {
|
|
13951
|
+
const parsed = parseSourceRef(uri);
|
|
13952
|
+
return parsed.kind === "open-files" && parsed.entity === "file" ? parsed.revision_id ?? null : null;
|
|
13953
|
+
}
|
|
13954
|
+
|
|
13955
|
+
// src/knowledge-db.ts
|
|
13956
|
+
import { Database } from "bun:sqlite";
|
|
13957
|
+
var MIGRATION_1 = `
|
|
13958
|
+
PRAGMA journal_mode = WAL;
|
|
13959
|
+
PRAGMA foreign_keys = ON;
|
|
13960
|
+
|
|
13961
|
+
CREATE TABLE IF NOT EXISTS schema_versions (
|
|
13962
|
+
version INTEGER PRIMARY KEY,
|
|
13963
|
+
applied_at TEXT NOT NULL
|
|
13964
|
+
);
|
|
13965
|
+
|
|
13966
|
+
CREATE TABLE IF NOT EXISTS sources (
|
|
13967
|
+
id TEXT PRIMARY KEY,
|
|
13968
|
+
uri TEXT NOT NULL UNIQUE,
|
|
13969
|
+
kind TEXT NOT NULL,
|
|
13970
|
+
title TEXT,
|
|
13971
|
+
metadata_json TEXT NOT NULL DEFAULT '{}',
|
|
13972
|
+
acl_json TEXT NOT NULL DEFAULT '{}',
|
|
13973
|
+
created_at TEXT NOT NULL,
|
|
13974
|
+
updated_at TEXT NOT NULL
|
|
13975
|
+
);
|
|
13976
|
+
|
|
13977
|
+
CREATE TABLE IF NOT EXISTS source_revisions (
|
|
13978
|
+
id TEXT PRIMARY KEY,
|
|
13979
|
+
source_id TEXT NOT NULL REFERENCES sources(id) ON DELETE CASCADE,
|
|
13980
|
+
revision TEXT NOT NULL,
|
|
13981
|
+
hash TEXT,
|
|
13982
|
+
extracted_text_uri TEXT,
|
|
13983
|
+
metadata_json TEXT NOT NULL DEFAULT '{}',
|
|
13984
|
+
created_at TEXT NOT NULL,
|
|
13985
|
+
UNIQUE(source_id, revision)
|
|
13986
|
+
);
|
|
13987
|
+
|
|
13988
|
+
CREATE TABLE IF NOT EXISTS chunks (
|
|
13989
|
+
id TEXT PRIMARY KEY,
|
|
13990
|
+
source_revision_id TEXT REFERENCES source_revisions(id) ON DELETE CASCADE,
|
|
13991
|
+
wiki_page_id TEXT,
|
|
13992
|
+
kind TEXT NOT NULL,
|
|
13993
|
+
ordinal INTEGER NOT NULL,
|
|
13994
|
+
text TEXT NOT NULL,
|
|
13995
|
+
token_count INTEGER,
|
|
13996
|
+
start_offset INTEGER,
|
|
13997
|
+
end_offset INTEGER,
|
|
13998
|
+
metadata_json TEXT NOT NULL DEFAULT '{}',
|
|
13999
|
+
created_at TEXT NOT NULL
|
|
14000
|
+
);
|
|
14001
|
+
|
|
14002
|
+
CREATE TABLE IF NOT EXISTS chunk_embeddings (
|
|
14003
|
+
id TEXT PRIMARY KEY,
|
|
14004
|
+
chunk_id TEXT NOT NULL REFERENCES chunks(id) ON DELETE CASCADE,
|
|
14005
|
+
provider TEXT NOT NULL,
|
|
14006
|
+
model TEXT NOT NULL,
|
|
14007
|
+
dimensions INTEGER NOT NULL,
|
|
14008
|
+
vector_json TEXT NOT NULL,
|
|
14009
|
+
created_at TEXT NOT NULL,
|
|
14010
|
+
UNIQUE(chunk_id, provider, model)
|
|
14011
|
+
);
|
|
14012
|
+
|
|
14013
|
+
CREATE TABLE IF NOT EXISTS wiki_pages (
|
|
14014
|
+
id TEXT PRIMARY KEY,
|
|
14015
|
+
path TEXT NOT NULL UNIQUE,
|
|
14016
|
+
title TEXT NOT NULL,
|
|
14017
|
+
artifact_uri TEXT,
|
|
14018
|
+
content_hash TEXT,
|
|
14019
|
+
status TEXT NOT NULL DEFAULT 'active',
|
|
14020
|
+
metadata_json TEXT NOT NULL DEFAULT '{}',
|
|
14021
|
+
created_at TEXT NOT NULL,
|
|
14022
|
+
updated_at TEXT NOT NULL
|
|
14023
|
+
);
|
|
14024
|
+
|
|
14025
|
+
CREATE TABLE IF NOT EXISTS wiki_backlinks (
|
|
14026
|
+
from_page_id TEXT NOT NULL REFERENCES wiki_pages(id) ON DELETE CASCADE,
|
|
14027
|
+
to_page_id TEXT NOT NULL REFERENCES wiki_pages(id) ON DELETE CASCADE,
|
|
14028
|
+
label TEXT,
|
|
14029
|
+
created_at TEXT NOT NULL,
|
|
14030
|
+
PRIMARY KEY(from_page_id, to_page_id)
|
|
14031
|
+
);
|
|
14032
|
+
|
|
14033
|
+
CREATE TABLE IF NOT EXISTS citations (
|
|
14034
|
+
id TEXT PRIMARY KEY,
|
|
14035
|
+
wiki_page_id TEXT REFERENCES wiki_pages(id) ON DELETE CASCADE,
|
|
14036
|
+
chunk_id TEXT REFERENCES chunks(id) ON DELETE SET NULL,
|
|
14037
|
+
source_uri TEXT NOT NULL,
|
|
14038
|
+
quote TEXT,
|
|
14039
|
+
start_offset INTEGER,
|
|
14040
|
+
end_offset INTEGER,
|
|
14041
|
+
metadata_json TEXT NOT NULL DEFAULT '{}',
|
|
14042
|
+
created_at TEXT NOT NULL
|
|
14043
|
+
);
|
|
14044
|
+
|
|
14045
|
+
CREATE TABLE IF NOT EXISTS knowledge_indexes (
|
|
14046
|
+
id TEXT PRIMARY KEY,
|
|
14047
|
+
kind TEXT NOT NULL,
|
|
14048
|
+
name TEXT NOT NULL,
|
|
14049
|
+
artifact_uri TEXT,
|
|
14050
|
+
shard_key TEXT,
|
|
14051
|
+
metadata_json TEXT NOT NULL DEFAULT '{}',
|
|
14052
|
+
created_at TEXT NOT NULL,
|
|
14053
|
+
updated_at TEXT NOT NULL,
|
|
14054
|
+
UNIQUE(kind, name, shard_key)
|
|
14055
|
+
);
|
|
14056
|
+
|
|
14057
|
+
CREATE TABLE IF NOT EXISTS runs (
|
|
14058
|
+
id TEXT PRIMARY KEY,
|
|
14059
|
+
type TEXT NOT NULL,
|
|
14060
|
+
prompt TEXT,
|
|
14061
|
+
status TEXT NOT NULL,
|
|
14062
|
+
provider TEXT,
|
|
14063
|
+
model TEXT,
|
|
14064
|
+
cost_tokens INTEGER NOT NULL DEFAULT 0,
|
|
14065
|
+
cost_usd REAL NOT NULL DEFAULT 0,
|
|
14066
|
+
metadata_json TEXT NOT NULL DEFAULT '{}',
|
|
14067
|
+
created_at TEXT NOT NULL,
|
|
14068
|
+
updated_at TEXT NOT NULL
|
|
14069
|
+
);
|
|
14070
|
+
|
|
14071
|
+
CREATE TABLE IF NOT EXISTS run_events (
|
|
14072
|
+
id TEXT PRIMARY KEY,
|
|
14073
|
+
run_id TEXT NOT NULL REFERENCES runs(id) ON DELETE CASCADE,
|
|
14074
|
+
level TEXT NOT NULL,
|
|
14075
|
+
event TEXT NOT NULL,
|
|
14076
|
+
metadata_json TEXT NOT NULL DEFAULT '{}',
|
|
14077
|
+
created_at TEXT NOT NULL
|
|
14078
|
+
);
|
|
14079
|
+
|
|
14080
|
+
CREATE TABLE IF NOT EXISTS provider_usage (
|
|
14081
|
+
id TEXT PRIMARY KEY,
|
|
14082
|
+
run_id TEXT REFERENCES runs(id) ON DELETE SET NULL,
|
|
14083
|
+
provider TEXT NOT NULL,
|
|
14084
|
+
model TEXT NOT NULL,
|
|
14085
|
+
input_tokens INTEGER NOT NULL DEFAULT 0,
|
|
14086
|
+
output_tokens INTEGER NOT NULL DEFAULT 0,
|
|
14087
|
+
cost_usd REAL NOT NULL DEFAULT 0,
|
|
14088
|
+
metadata_json TEXT NOT NULL DEFAULT '{}',
|
|
14089
|
+
created_at TEXT NOT NULL
|
|
14090
|
+
);
|
|
14091
|
+
|
|
14092
|
+
CREATE TABLE IF NOT EXISTS redaction_findings (
|
|
14093
|
+
id TEXT PRIMARY KEY,
|
|
14094
|
+
source_uri TEXT,
|
|
14095
|
+
run_id TEXT REFERENCES runs(id) ON DELETE SET NULL,
|
|
14096
|
+
severity TEXT NOT NULL,
|
|
14097
|
+
finding_type TEXT NOT NULL,
|
|
14098
|
+
metadata_json TEXT NOT NULL DEFAULT '{}',
|
|
14099
|
+
created_at TEXT NOT NULL
|
|
14100
|
+
);
|
|
14101
|
+
|
|
14102
|
+
CREATE TABLE IF NOT EXISTS storage_objects (
|
|
14103
|
+
id TEXT PRIMARY KEY,
|
|
14104
|
+
artifact_uri TEXT NOT NULL UNIQUE,
|
|
14105
|
+
kind TEXT NOT NULL,
|
|
14106
|
+
content_type TEXT,
|
|
14107
|
+
hash TEXT,
|
|
14108
|
+
size_bytes INTEGER,
|
|
14109
|
+
metadata_json TEXT NOT NULL DEFAULT '{}',
|
|
14110
|
+
created_at TEXT NOT NULL,
|
|
14111
|
+
updated_at TEXT NOT NULL
|
|
14112
|
+
);
|
|
14113
|
+
|
|
14114
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS chunks_fts USING fts5(
|
|
14115
|
+
text,
|
|
14116
|
+
title,
|
|
14117
|
+
source_uri,
|
|
14118
|
+
content='',
|
|
14119
|
+
tokenize='porter unicode61'
|
|
14120
|
+
);
|
|
14121
|
+
|
|
14122
|
+
INSERT OR IGNORE INTO schema_versions(version, applied_at)
|
|
14123
|
+
VALUES (1, datetime('now'));
|
|
14124
|
+
`;
|
|
14125
|
+
var MIGRATION_2 = `
|
|
14126
|
+
DROP TABLE IF EXISTS chunks_fts;
|
|
14127
|
+
|
|
14128
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS chunks_fts USING fts5(
|
|
14129
|
+
chunk_id UNINDEXED,
|
|
14130
|
+
text,
|
|
14131
|
+
title,
|
|
14132
|
+
source_uri,
|
|
14133
|
+
tokenize='porter unicode61'
|
|
14134
|
+
);
|
|
14135
|
+
|
|
14136
|
+
INSERT OR IGNORE INTO schema_versions(version, applied_at)
|
|
14137
|
+
VALUES (2, datetime('now'));
|
|
14138
|
+
`;
|
|
14139
|
+
var MIGRATION_3 = `
|
|
14140
|
+
CREATE TABLE IF NOT EXISTS audit_events (
|
|
14141
|
+
id TEXT PRIMARY KEY,
|
|
14142
|
+
event_type TEXT NOT NULL,
|
|
14143
|
+
action TEXT NOT NULL,
|
|
14144
|
+
target_uri TEXT,
|
|
14145
|
+
decision TEXT NOT NULL,
|
|
14146
|
+
metadata_json TEXT NOT NULL DEFAULT '{}',
|
|
14147
|
+
created_at TEXT NOT NULL
|
|
14148
|
+
);
|
|
14149
|
+
|
|
14150
|
+
CREATE TABLE IF NOT EXISTS approval_gates (
|
|
14151
|
+
id TEXT PRIMARY KEY,
|
|
14152
|
+
action TEXT NOT NULL,
|
|
14153
|
+
target_uri TEXT,
|
|
14154
|
+
status TEXT NOT NULL,
|
|
14155
|
+
reason TEXT,
|
|
14156
|
+
approved_by TEXT,
|
|
14157
|
+
metadata_json TEXT NOT NULL DEFAULT '{}',
|
|
14158
|
+
created_at TEXT NOT NULL,
|
|
14159
|
+
updated_at TEXT NOT NULL
|
|
14160
|
+
);
|
|
14161
|
+
|
|
14162
|
+
CREATE INDEX IF NOT EXISTS idx_audit_events_action ON audit_events(action);
|
|
14163
|
+
CREATE INDEX IF NOT EXISTS idx_audit_events_target ON audit_events(target_uri);
|
|
14164
|
+
CREATE INDEX IF NOT EXISTS idx_audit_events_created ON audit_events(created_at);
|
|
14165
|
+
CREATE INDEX IF NOT EXISTS idx_approval_gates_action ON approval_gates(action);
|
|
14166
|
+
CREATE INDEX IF NOT EXISTS idx_approval_gates_status ON approval_gates(status);
|
|
14167
|
+
|
|
14168
|
+
INSERT OR IGNORE INTO schema_versions(version, applied_at)
|
|
14169
|
+
VALUES (3, datetime('now'));
|
|
14170
|
+
`;
|
|
14171
|
+
function openKnowledgeDb(path) {
|
|
14172
|
+
ensureParentDir(path);
|
|
14173
|
+
const db = new Database(path);
|
|
14174
|
+
db.exec("PRAGMA foreign_keys = ON;");
|
|
14175
|
+
return db;
|
|
14176
|
+
}
|
|
14177
|
+
function migrateKnowledgeDb(path) {
|
|
14178
|
+
const db = openKnowledgeDb(path);
|
|
14179
|
+
try {
|
|
14180
|
+
db.exec(MIGRATION_1);
|
|
14181
|
+
if (getSchemaVersion(db) < 2)
|
|
14182
|
+
db.exec(MIGRATION_2);
|
|
14183
|
+
if (getSchemaVersion(db) < 3)
|
|
14184
|
+
db.exec(MIGRATION_3);
|
|
14185
|
+
return { path, schema_version: getSchemaVersion(db) };
|
|
14186
|
+
} finally {
|
|
14187
|
+
db.close();
|
|
14188
|
+
}
|
|
14189
|
+
}
|
|
14190
|
+
function getSchemaVersion(db) {
|
|
14191
|
+
const row = db.query("SELECT MAX(version) AS version FROM schema_versions").get();
|
|
14192
|
+
return row?.version ?? 0;
|
|
14193
|
+
}
|
|
14194
|
+
|
|
14195
|
+
// src/safety.ts
|
|
14196
|
+
import { createHash, randomUUID as randomUUID2 } from "crypto";
|
|
14197
|
+
import { relative, resolve as resolve2, sep } from "path";
|
|
14198
|
+
function envEnabled(name) {
|
|
14199
|
+
const value = process.env[name];
|
|
14200
|
+
return value === "1" || value === "true" || value === "yes";
|
|
14201
|
+
}
|
|
14202
|
+
function resolveSafetyPolicy(config2, workspace) {
|
|
14203
|
+
const extended = config2;
|
|
14204
|
+
const configuredBuckets = new Set(extended.safety?.network?.allowed_s3_buckets ?? []);
|
|
14205
|
+
if (config2.storage.type === "s3" && config2.storage.s3?.bucket)
|
|
14206
|
+
configuredBuckets.add(config2.storage.s3.bucket);
|
|
14207
|
+
if (process.env.HASNA_KNOWLEDGE_ALLOWED_S3_BUCKETS) {
|
|
14208
|
+
for (const bucket of process.env.HASNA_KNOWLEDGE_ALLOWED_S3_BUCKETS.split(",").map((entry) => entry.trim()).filter(Boolean)) {
|
|
14209
|
+
configuredBuckets.add(bucket);
|
|
14210
|
+
}
|
|
14211
|
+
}
|
|
14212
|
+
return {
|
|
14213
|
+
mode: config2.mode,
|
|
14214
|
+
allowWriteRoots: [
|
|
14215
|
+
workspace.home,
|
|
14216
|
+
workspace.artifactsDir,
|
|
14217
|
+
workspace.cacheDir,
|
|
14218
|
+
workspace.exportsDir,
|
|
14219
|
+
workspace.indexesDir,
|
|
14220
|
+
workspace.logsDir,
|
|
14221
|
+
workspace.runsDir,
|
|
14222
|
+
workspace.schemasDir,
|
|
14223
|
+
workspace.wikiDir
|
|
14224
|
+
].map((entry) => resolve2(entry)),
|
|
14225
|
+
readOnlySourceAccess: true,
|
|
14226
|
+
network: {
|
|
14227
|
+
webSearchEnabled: extended.safety?.network?.web_search_enabled ?? envEnabled("HASNA_KNOWLEDGE_WEB_SEARCH"),
|
|
14228
|
+
s3ReadsEnabled: extended.safety?.network?.s3_reads_enabled ?? envEnabled("HASNA_KNOWLEDGE_ALLOW_S3_READS"),
|
|
14229
|
+
allowedS3Buckets: [...configuredBuckets].sort()
|
|
14230
|
+
},
|
|
14231
|
+
redaction: {
|
|
14232
|
+
enabled: extended.safety?.redaction?.enabled ?? true
|
|
14233
|
+
},
|
|
14234
|
+
approvals: {
|
|
14235
|
+
generatedWritesRequireApproval: extended.safety?.approvals?.generated_writes_require_approval ?? true
|
|
14236
|
+
}
|
|
14237
|
+
};
|
|
14238
|
+
}
|
|
14239
|
+
function isInside(root, target) {
|
|
14240
|
+
const rel = relative(root, target);
|
|
14241
|
+
return rel === "" || !rel.startsWith("..") && rel !== ".." && !rel.startsWith(`..${sep}`);
|
|
14242
|
+
}
|
|
14243
|
+
function assertWriteAllowed(targetPath, policy) {
|
|
14244
|
+
const resolved = resolve2(targetPath);
|
|
14245
|
+
if (!policy.allowWriteRoots.some((root) => isInside(root, resolved))) {
|
|
14246
|
+
throw new Error(`Safety policy denied write outside .hasna/apps/knowledge: ${targetPath}`);
|
|
14247
|
+
}
|
|
14248
|
+
}
|
|
14249
|
+
function auditId(input) {
|
|
14250
|
+
return `audit_${createHash("sha256").update(`${input.event_type}\x00${input.action}\x00${input.target_uri ?? ""}\x00${input.created_at ?? ""}\x00${JSON.stringify(input.metadata ?? {})}\x00${randomUUID2()}`).digest("hex").slice(0, 24)}`;
|
|
14251
|
+
}
|
|
14252
|
+
function recordAuditEvent(db, input) {
|
|
14253
|
+
const createdAt = input.created_at ?? new Date().toISOString();
|
|
14254
|
+
const id = auditId({ ...input, created_at: createdAt });
|
|
14255
|
+
db.run(`INSERT INTO audit_events (id, event_type, action, target_uri, decision, metadata_json, created_at)
|
|
14256
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`, [
|
|
14257
|
+
id,
|
|
14258
|
+
input.event_type,
|
|
14259
|
+
input.action,
|
|
14260
|
+
input.target_uri ?? null,
|
|
14261
|
+
input.decision,
|
|
14262
|
+
JSON.stringify(input.metadata ?? {}),
|
|
14263
|
+
createdAt
|
|
14264
|
+
]);
|
|
14265
|
+
return id;
|
|
14266
|
+
}
|
|
14267
|
+
|
|
14268
|
+
// src/source-resolver.ts
|
|
14269
|
+
function parseJsonObject(value) {
|
|
14270
|
+
if (!value)
|
|
14271
|
+
return {};
|
|
14272
|
+
try {
|
|
14273
|
+
const parsed = JSON.parse(value);
|
|
14274
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
14275
|
+
} catch {
|
|
14276
|
+
return {};
|
|
14277
|
+
}
|
|
14278
|
+
}
|
|
14279
|
+
function metadataString(metadata, keys) {
|
|
14280
|
+
for (const key of keys) {
|
|
14281
|
+
const value = metadata[key];
|
|
14282
|
+
if (typeof value === "string" && value.length > 0)
|
|
14283
|
+
return value;
|
|
14284
|
+
}
|
|
14285
|
+
return null;
|
|
14286
|
+
}
|
|
14287
|
+
function metadataNumber(metadata, keys) {
|
|
14288
|
+
for (const key of keys) {
|
|
14289
|
+
const value = metadata[key];
|
|
14290
|
+
if (typeof value === "number" && Number.isFinite(value))
|
|
14291
|
+
return value;
|
|
14292
|
+
}
|
|
14293
|
+
return null;
|
|
14294
|
+
}
|
|
14295
|
+
function assertPurposeAllowed(permissions, purpose) {
|
|
14296
|
+
const mode = permissions.mode;
|
|
14297
|
+
if (typeof mode === "string" && mode !== "read_only") {
|
|
14298
|
+
throw new Error(`Source resolver denied ${purpose}. Permission mode is ${mode}, expected read_only.`);
|
|
14299
|
+
}
|
|
14300
|
+
const denied = permissions.denied_purposes;
|
|
14301
|
+
if (Array.isArray(denied) && denied.includes(purpose)) {
|
|
14302
|
+
throw new Error(`Source resolver denied ${purpose}. Purpose is explicitly denied.`);
|
|
14303
|
+
}
|
|
14304
|
+
const allowed = permissions.allowed_purposes;
|
|
14305
|
+
if (Array.isArray(allowed) && allowed.length > 0 && !allowed.includes(purpose)) {
|
|
14306
|
+
throw new Error(`Source resolver denied ${purpose}. Allowed purposes: ${allowed.join(", ")}`);
|
|
14307
|
+
}
|
|
14308
|
+
}
|
|
14309
|
+
function sourceRevisionRef(sourceUri, revision, fallback) {
|
|
14310
|
+
if (!revision)
|
|
14311
|
+
return fallback;
|
|
14312
|
+
try {
|
|
14313
|
+
const parsed = parseSourceRef(sourceUri);
|
|
14314
|
+
if (parsed.kind === "open-files" && parsed.entity === "file") {
|
|
14315
|
+
return `${sourceUri}/revision/${encodeURIComponent(revision.revision)}`;
|
|
14316
|
+
}
|
|
14317
|
+
} catch {
|
|
14318
|
+
return fallback;
|
|
14319
|
+
}
|
|
14320
|
+
return fallback;
|
|
14321
|
+
}
|
|
14322
|
+
function selectSource(db, sourceUri, requestedRef) {
|
|
14323
|
+
return db.query(`SELECT id, uri, kind, title, metadata_json, acl_json, updated_at
|
|
14324
|
+
FROM sources
|
|
14325
|
+
WHERE uri = ? OR uri = ?
|
|
14326
|
+
ORDER BY CASE WHEN uri = ? THEN 0 ELSE 1 END
|
|
14327
|
+
LIMIT 1`).get(sourceUri, requestedRef, sourceUri) ?? null;
|
|
14328
|
+
}
|
|
14329
|
+
function selectRevision(db, sourceId, revisionId) {
|
|
14330
|
+
if (revisionId) {
|
|
14331
|
+
return db.query(`SELECT id, revision, hash, extracted_text_uri, metadata_json, created_at
|
|
14332
|
+
FROM source_revisions
|
|
14333
|
+
WHERE source_id = ? AND revision = ?
|
|
14334
|
+
LIMIT 1`).get(sourceId, revisionId) ?? null;
|
|
14335
|
+
}
|
|
14336
|
+
return db.query(`SELECT id, revision, hash, extracted_text_uri, metadata_json, created_at
|
|
14337
|
+
FROM source_revisions
|
|
14338
|
+
WHERE source_id = ?
|
|
14339
|
+
ORDER BY created_at DESC, revision DESC
|
|
14340
|
+
LIMIT 1`).get(sourceId) ?? null;
|
|
14341
|
+
}
|
|
14342
|
+
function countChunks(db, revisionId) {
|
|
14343
|
+
if (!revisionId)
|
|
14344
|
+
return 0;
|
|
14345
|
+
const row = db.query("SELECT COUNT(*) AS n FROM chunks WHERE source_revision_id = ?").get(revisionId);
|
|
14346
|
+
return row?.n ?? 0;
|
|
14347
|
+
}
|
|
14348
|
+
function selectChunks(db, revisionId, limit) {
|
|
14349
|
+
if (!revisionId || limit <= 0)
|
|
14350
|
+
return [];
|
|
14351
|
+
return db.query(`SELECT id, kind, ordinal, text, token_count, start_offset, end_offset, metadata_json
|
|
14352
|
+
FROM chunks
|
|
14353
|
+
WHERE source_revision_id = ?
|
|
14354
|
+
ORDER BY ordinal ASC
|
|
14355
|
+
LIMIT ?`).all(revisionId, limit);
|
|
14356
|
+
}
|
|
14357
|
+
async function resolveOpenFilesSource(options) {
|
|
14358
|
+
const purpose = options.purpose ?? "knowledge_answer";
|
|
14359
|
+
const limit = Math.max(0, Math.min(options.limit ?? 10, 100));
|
|
14360
|
+
const resolvedAt = (options.now ?? new Date).toISOString();
|
|
14361
|
+
const parsed = parseSourceRef(options.sourceRef);
|
|
14362
|
+
const sourceUri = catalogSourceUriForRef(options.sourceRef, parsed);
|
|
14363
|
+
const requestedRevision = revisionIdForSourceRef(options.sourceRef);
|
|
14364
|
+
if (options.safetyPolicy) {
|
|
14365
|
+
if (!options.safetyPolicy.readOnlySourceAccess)
|
|
14366
|
+
throw new Error("Safety policy denied source resolution.");
|
|
14367
|
+
assertWriteAllowed(options.dbPath, options.safetyPolicy);
|
|
14368
|
+
}
|
|
14369
|
+
migrateKnowledgeDb(options.dbPath);
|
|
14370
|
+
const db = openKnowledgeDb(options.dbPath);
|
|
14371
|
+
try {
|
|
14372
|
+
return db.transaction(() => {
|
|
14373
|
+
const source = selectSource(db, sourceUri, options.sourceRef);
|
|
14374
|
+
if (!source) {
|
|
14375
|
+
recordAuditEvent(db, {
|
|
14376
|
+
event_type: "source_read",
|
|
14377
|
+
action: "open_files_resolve_missing",
|
|
14378
|
+
target_uri: options.sourceRef,
|
|
14379
|
+
decision: "allow",
|
|
14380
|
+
metadata: { purpose, read_only: true, source_uri: sourceUri },
|
|
14381
|
+
created_at: resolvedAt
|
|
14382
|
+
});
|
|
14383
|
+
return {
|
|
14384
|
+
source_ref: options.sourceRef,
|
|
14385
|
+
source_uri: sourceUri,
|
|
14386
|
+
purpose,
|
|
14387
|
+
read_only: true,
|
|
14388
|
+
resolved: false,
|
|
14389
|
+
resolver: {
|
|
14390
|
+
name: "open-files-read-only",
|
|
14391
|
+
mode: "local_catalog",
|
|
14392
|
+
contract: "open-files-knowledge-source-v1"
|
|
14393
|
+
},
|
|
14394
|
+
source: null,
|
|
14395
|
+
revision: null,
|
|
14396
|
+
content: {
|
|
14397
|
+
mime: null,
|
|
14398
|
+
size: null,
|
|
14399
|
+
hash: null,
|
|
14400
|
+
text_available: false,
|
|
14401
|
+
chunks_total: 0,
|
|
14402
|
+
chunks_returned: 0,
|
|
14403
|
+
char_count_returned: 0,
|
|
14404
|
+
extracted_text_ref: null,
|
|
14405
|
+
bytes_available: false,
|
|
14406
|
+
bytes_exposed: false
|
|
14407
|
+
},
|
|
14408
|
+
chunks: [],
|
|
14409
|
+
citations: []
|
|
14410
|
+
};
|
|
14411
|
+
}
|
|
14412
|
+
const sourceMetadata = parseJsonObject(source.metadata_json);
|
|
14413
|
+
const permissions = parseJsonObject(source.acl_json);
|
|
14414
|
+
try {
|
|
14415
|
+
assertPurposeAllowed(permissions, purpose);
|
|
14416
|
+
} catch (error48) {
|
|
14417
|
+
recordAuditEvent(db, {
|
|
14418
|
+
event_type: "source_read",
|
|
14419
|
+
action: "open_files_resolve",
|
|
14420
|
+
target_uri: options.sourceRef,
|
|
14421
|
+
decision: "deny",
|
|
14422
|
+
metadata: {
|
|
14423
|
+
purpose,
|
|
14424
|
+
read_only: true,
|
|
14425
|
+
source_uri: source.uri,
|
|
14426
|
+
error: error48 instanceof Error ? error48.message : String(error48)
|
|
14427
|
+
},
|
|
14428
|
+
created_at: resolvedAt
|
|
14429
|
+
});
|
|
14430
|
+
throw error48;
|
|
14431
|
+
}
|
|
14432
|
+
const revision = selectRevision(db, source.id, requestedRevision);
|
|
14433
|
+
const revisionMetadata = parseJsonObject(revision?.metadata_json);
|
|
14434
|
+
const totalChunks = countChunks(db, revision?.id ?? null);
|
|
14435
|
+
const rows = selectChunks(db, revision?.id ?? null, limit);
|
|
14436
|
+
const effectiveSourceRef = sourceRevisionRef(source.uri, revision, options.sourceRef);
|
|
14437
|
+
const chunks = rows.map((row) => {
|
|
14438
|
+
const metadata = parseJsonObject(row.metadata_json);
|
|
14439
|
+
const evidence = {
|
|
14440
|
+
resolver: "open-files-read-only",
|
|
14441
|
+
mode: "local_catalog",
|
|
14442
|
+
purpose,
|
|
14443
|
+
read_only: true,
|
|
14444
|
+
source_ref: metadataString(metadata, ["source_ref"]) ?? effectiveSourceRef,
|
|
14445
|
+
source_uri: source.uri,
|
|
14446
|
+
source_revision_id: revision?.id ?? null,
|
|
14447
|
+
revision: revision?.revision ?? null,
|
|
14448
|
+
hash: revision?.hash ?? metadataString(metadata, ["hash"]),
|
|
14449
|
+
chunk_id: row.id,
|
|
14450
|
+
start_offset: row.start_offset,
|
|
14451
|
+
end_offset: row.end_offset,
|
|
14452
|
+
resolved_at: resolvedAt
|
|
14453
|
+
};
|
|
14454
|
+
return {
|
|
14455
|
+
id: row.id,
|
|
14456
|
+
kind: row.kind,
|
|
14457
|
+
ordinal: row.ordinal,
|
|
14458
|
+
text: row.text,
|
|
14459
|
+
token_count: row.token_count,
|
|
14460
|
+
start_offset: row.start_offset,
|
|
14461
|
+
end_offset: row.end_offset,
|
|
14462
|
+
metadata,
|
|
14463
|
+
evidence
|
|
14464
|
+
};
|
|
14465
|
+
});
|
|
14466
|
+
const citations = chunks.map((chunk) => ({
|
|
14467
|
+
source_ref: chunk.evidence.source_ref,
|
|
14468
|
+
source_uri: source.uri,
|
|
14469
|
+
chunk_id: chunk.id,
|
|
14470
|
+
quote: chunk.text.slice(0, 500),
|
|
14471
|
+
start_offset: chunk.start_offset,
|
|
14472
|
+
end_offset: chunk.end_offset,
|
|
14473
|
+
evidence: chunk.evidence
|
|
14474
|
+
}));
|
|
14475
|
+
recordAuditEvent(db, {
|
|
14476
|
+
event_type: "source_read",
|
|
14477
|
+
action: "open_files_resolve",
|
|
14478
|
+
target_uri: options.sourceRef,
|
|
14479
|
+
decision: "allow",
|
|
14480
|
+
metadata: {
|
|
14481
|
+
purpose,
|
|
14482
|
+
read_only: true,
|
|
14483
|
+
source_uri: source.uri,
|
|
14484
|
+
revision: revision?.revision ?? null,
|
|
14485
|
+
chunks_returned: chunks.length,
|
|
14486
|
+
chunks_total: totalChunks
|
|
14487
|
+
},
|
|
14488
|
+
created_at: resolvedAt
|
|
14489
|
+
});
|
|
14490
|
+
const mime = metadataString(sourceMetadata, ["mime", "content_type"]) ?? metadataString(revisionMetadata, ["mime", "content_type"]);
|
|
14491
|
+
const size = metadataNumber(sourceMetadata, ["size", "size_bytes"]) ?? metadataNumber(revisionMetadata, ["size", "size_bytes"]);
|
|
14492
|
+
return {
|
|
14493
|
+
source_ref: effectiveSourceRef,
|
|
14494
|
+
source_uri: source.uri,
|
|
14495
|
+
purpose,
|
|
14496
|
+
read_only: true,
|
|
14497
|
+
resolved: true,
|
|
14498
|
+
resolver: {
|
|
14499
|
+
name: "open-files-read-only",
|
|
14500
|
+
mode: "local_catalog",
|
|
14501
|
+
contract: "open-files-knowledge-source-v1"
|
|
14502
|
+
},
|
|
14503
|
+
source: {
|
|
14504
|
+
id: source.id,
|
|
14505
|
+
uri: source.uri,
|
|
14506
|
+
kind: source.kind,
|
|
14507
|
+
title: source.title,
|
|
14508
|
+
metadata: sourceMetadata,
|
|
14509
|
+
permissions,
|
|
14510
|
+
updated_at: source.updated_at
|
|
14511
|
+
},
|
|
14512
|
+
revision: revision ? {
|
|
14513
|
+
id: revision.id,
|
|
14514
|
+
revision: revision.revision,
|
|
14515
|
+
hash: revision.hash,
|
|
14516
|
+
extracted_text_uri: revision.extracted_text_uri,
|
|
14517
|
+
metadata: revisionMetadata,
|
|
14518
|
+
created_at: revision.created_at,
|
|
14519
|
+
reindex_required: revisionMetadata.reindex_required === true
|
|
14520
|
+
} : null,
|
|
14521
|
+
content: {
|
|
14522
|
+
mime,
|
|
14523
|
+
size,
|
|
14524
|
+
hash: revision?.hash ?? metadataString(sourceMetadata, ["hash", "checksum", "sha256"]),
|
|
14525
|
+
text_available: totalChunks > 0,
|
|
14526
|
+
chunks_total: totalChunks,
|
|
14527
|
+
chunks_returned: chunks.length,
|
|
14528
|
+
char_count_returned: chunks.reduce((sum, chunk) => sum + chunk.text.length, 0),
|
|
14529
|
+
extracted_text_ref: revision?.extracted_text_uri ?? metadataString(revisionMetadata, ["extracted_text_ref", "extracted_text_uri"]),
|
|
14530
|
+
bytes_available: false,
|
|
14531
|
+
bytes_exposed: false
|
|
14532
|
+
},
|
|
14533
|
+
chunks,
|
|
14534
|
+
citations
|
|
14535
|
+
};
|
|
14536
|
+
})();
|
|
14537
|
+
} finally {
|
|
14538
|
+
db.close();
|
|
14539
|
+
}
|
|
14540
|
+
}
|
|
13944
14541
|
|
|
13945
14542
|
// src/mcp.js
|
|
13946
14543
|
var storePathField = exports_external.string().optional().describe("Path to the JSON store file");
|
|
@@ -14026,6 +14623,28 @@ function buildServer() {
|
|
|
14026
14623
|
return errorText(error48 instanceof Error ? error48.message : String(error48));
|
|
14027
14624
|
}
|
|
14028
14625
|
});
|
|
14626
|
+
registerTool(server, "ok_resolve_source", "Resolve source content", "Resolve an indexed source ref through the read-only open-files boundary and return chunk citation evidence", {
|
|
14627
|
+
source_ref: exports_external.string().describe("Source reference URI, preferably open-files://..."),
|
|
14628
|
+
purpose: exports_external.string().optional().describe("Read-only purpose label, default knowledge_answer"),
|
|
14629
|
+
limit: exports_external.number().optional().describe("Maximum chunks to return, default 10"),
|
|
14630
|
+
scope: scopeField
|
|
14631
|
+
}, async ({ source_ref, purpose, limit, scope }) => {
|
|
14632
|
+
const workspace = ensureKnowledgeWorkspace(resolveScopedWorkspace(scope).home);
|
|
14633
|
+
const config2 = readKnowledgeConfig(workspace.configPath);
|
|
14634
|
+
const safetyPolicy = resolveSafetyPolicy(config2, workspace);
|
|
14635
|
+
try {
|
|
14636
|
+
const result = await resolveOpenFilesSource({
|
|
14637
|
+
dbPath: workspace.knowledgeDbPath,
|
|
14638
|
+
sourceRef: source_ref,
|
|
14639
|
+
purpose,
|
|
14640
|
+
limit,
|
|
14641
|
+
safetyPolicy
|
|
14642
|
+
});
|
|
14643
|
+
return jsonText({ ok: true, ...result });
|
|
14644
|
+
} catch (error48) {
|
|
14645
|
+
return errorText(error48 instanceof Error ? error48.message : String(error48));
|
|
14646
|
+
}
|
|
14647
|
+
});
|
|
14029
14648
|
registerTool(server, "ok_add", "Add a knowledge item", "Add a new item to the knowledge store", {
|
|
14030
14649
|
title: exports_external.string().describe("Item title"),
|
|
14031
14650
|
content: exports_external.string().describe("Item content/body"),
|