@hasna/knowledge 0.2.9 → 0.2.10
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 +5 -0
- package/bin/open-knowledge-mcp.js +1447 -34
- package/bin/open-knowledge.js +63 -63
- package/docs/architecture/ai-native-knowledge-base.md +4 -1
- package/package.json +2 -2
- package/src/cli.ts +18 -74
- package/src/mcp.js +5 -27
- package/src/service.ts +157 -0
|
@@ -15,6 +15,7 @@ var __export = (target, all) => {
|
|
|
15
15
|
});
|
|
16
16
|
};
|
|
17
17
|
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
18
|
+
var __require = import.meta.require;
|
|
18
19
|
|
|
19
20
|
// src/mcp-http.js
|
|
20
21
|
var exports_mcp_http = {};
|
|
@@ -13655,11 +13656,11 @@ function date4(params) {
|
|
|
13655
13656
|
// node_modules/zod/v4/classic/external.js
|
|
13656
13657
|
config(en_default());
|
|
13657
13658
|
// src/mcp.js
|
|
13658
|
-
import { existsSync as
|
|
13659
|
+
import { existsSync as existsSync7, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "fs";
|
|
13659
13660
|
// package.json
|
|
13660
13661
|
var package_default = {
|
|
13661
13662
|
name: "@hasna/knowledge",
|
|
13662
|
-
version: "0.2.
|
|
13663
|
+
version: "0.2.10",
|
|
13663
13664
|
description: "Agent-friendly local knowledge CLI with JSON output, pagination, and safe destructive actions",
|
|
13664
13665
|
type: "module",
|
|
13665
13666
|
bin: {
|
|
@@ -13676,7 +13677,7 @@ var package_default = {
|
|
|
13676
13677
|
scripts: {
|
|
13677
13678
|
test: "bun test",
|
|
13678
13679
|
"test:cli": "bun test tests/cli.test.ts",
|
|
13679
|
-
build: "bun build --target=bun --outfile=bin/open-knowledge.js --minify --external @aws-sdk/client-s3 --external @aws-sdk/credential-providers src/cli.ts && bun build --target=bun --outfile=bin/open-knowledge-mcp.js --external @modelcontextprotocol/sdk src/mcp.js",
|
|
13680
|
+
build: "bun build --target=bun --outfile=bin/open-knowledge.js --minify --external @aws-sdk/client-s3 --external @aws-sdk/credential-providers src/cli.ts && bun build --target=bun --outfile=bin/open-knowledge-mcp.js --external @modelcontextprotocol/sdk --external @aws-sdk/client-s3 --external @aws-sdk/credential-providers src/mcp.js",
|
|
13680
13681
|
prepublishOnly: "bun run build",
|
|
13681
13682
|
postinstall: "bun run build"
|
|
13682
13683
|
},
|
|
@@ -13952,6 +13953,160 @@ function revisionIdForSourceRef(uri) {
|
|
|
13952
13953
|
return parsed.kind === "open-files" && parsed.entity === "file" ? parsed.revision_id ?? null : null;
|
|
13953
13954
|
}
|
|
13954
13955
|
|
|
13956
|
+
// src/artifact-store.ts
|
|
13957
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
13958
|
+
import { dirname as dirname2, join as join2, relative, sep } from "path";
|
|
13959
|
+
function normalizeArtifactKey(key) {
|
|
13960
|
+
const raw = key.replace(/\\/g, "/").trim();
|
|
13961
|
+
if (!raw || raw.startsWith("/")) {
|
|
13962
|
+
throw new Error(`Invalid artifact key: ${key}`);
|
|
13963
|
+
}
|
|
13964
|
+
const segments = raw.split("/").filter(Boolean);
|
|
13965
|
+
if (segments.length === 0 || segments.some((segment) => segment === "." || segment === "..")) {
|
|
13966
|
+
throw new Error(`Invalid artifact key: ${key}`);
|
|
13967
|
+
}
|
|
13968
|
+
return segments.join("/");
|
|
13969
|
+
}
|
|
13970
|
+
function assertInside(root, target) {
|
|
13971
|
+
const rel = relative(root, target);
|
|
13972
|
+
if (rel.startsWith("..") || rel === ".." || rel.startsWith(`..${sep}`)) {
|
|
13973
|
+
throw new Error(`Artifact path escapes root: ${target}`);
|
|
13974
|
+
}
|
|
13975
|
+
}
|
|
13976
|
+
|
|
13977
|
+
class LocalArtifactStore {
|
|
13978
|
+
root;
|
|
13979
|
+
type = "local";
|
|
13980
|
+
canRead = true;
|
|
13981
|
+
canWrite = true;
|
|
13982
|
+
constructor(root) {
|
|
13983
|
+
this.root = root;
|
|
13984
|
+
mkdirSync2(root, { recursive: true });
|
|
13985
|
+
}
|
|
13986
|
+
async put(entry) {
|
|
13987
|
+
const key = normalizeArtifactKey(entry.key);
|
|
13988
|
+
const path = join2(this.root, key);
|
|
13989
|
+
assertInside(this.root, path);
|
|
13990
|
+
mkdirSync2(dirname2(path), { recursive: true });
|
|
13991
|
+
writeFileSync3(path, entry.body);
|
|
13992
|
+
return { key, uri: `file://${path}` };
|
|
13993
|
+
}
|
|
13994
|
+
async getText(key) {
|
|
13995
|
+
const normalizedKey = normalizeArtifactKey(key);
|
|
13996
|
+
const path = join2(this.root, normalizedKey);
|
|
13997
|
+
assertInside(this.root, path);
|
|
13998
|
+
return readFileSync3(path, "utf8");
|
|
13999
|
+
}
|
|
14000
|
+
async exists(key) {
|
|
14001
|
+
const normalizedKey = normalizeArtifactKey(key);
|
|
14002
|
+
const path = join2(this.root, normalizedKey);
|
|
14003
|
+
assertInside(this.root, path);
|
|
14004
|
+
return existsSync3(path);
|
|
14005
|
+
}
|
|
14006
|
+
}
|
|
14007
|
+
|
|
14008
|
+
class S3ArtifactStore {
|
|
14009
|
+
options;
|
|
14010
|
+
type = "s3";
|
|
14011
|
+
canRead = true;
|
|
14012
|
+
canWrite = true;
|
|
14013
|
+
client;
|
|
14014
|
+
constructor(options) {
|
|
14015
|
+
this.options = options;
|
|
14016
|
+
this.client = options.client;
|
|
14017
|
+
}
|
|
14018
|
+
async getClient() {
|
|
14019
|
+
if (this.client)
|
|
14020
|
+
return this.client;
|
|
14021
|
+
const [{ S3Client }, { fromIni }] = await Promise.all([
|
|
14022
|
+
import("@aws-sdk/client-s3"),
|
|
14023
|
+
import("@aws-sdk/credential-providers")
|
|
14024
|
+
]);
|
|
14025
|
+
this.client = new S3Client({
|
|
14026
|
+
region: this.options.region,
|
|
14027
|
+
credentials: this.options.profile ? fromIni({ profile: this.options.profile }) : undefined,
|
|
14028
|
+
maxAttempts: this.options.max_attempts
|
|
14029
|
+
});
|
|
14030
|
+
return this.client;
|
|
14031
|
+
}
|
|
14032
|
+
objectKey(key) {
|
|
14033
|
+
const normalizedKey = normalizeArtifactKey(key);
|
|
14034
|
+
const prefix = this.options.prefix ? normalizeArtifactKey(this.options.prefix) : "";
|
|
14035
|
+
return prefix ? `${prefix}/${normalizedKey}` : normalizedKey;
|
|
14036
|
+
}
|
|
14037
|
+
async put(entry) {
|
|
14038
|
+
const [{ PutObjectCommand }, client] = await Promise.all([
|
|
14039
|
+
import("@aws-sdk/client-s3"),
|
|
14040
|
+
this.getClient()
|
|
14041
|
+
]);
|
|
14042
|
+
const key = this.objectKey(entry.key);
|
|
14043
|
+
await client.send(new PutObjectCommand({
|
|
14044
|
+
Bucket: this.options.bucket,
|
|
14045
|
+
Key: key,
|
|
14046
|
+
Body: entry.body,
|
|
14047
|
+
ContentType: entry.content_type,
|
|
14048
|
+
Metadata: entry.metadata,
|
|
14049
|
+
ServerSideEncryption: this.options.server_side_encryption,
|
|
14050
|
+
SSEKMSKeyId: this.options.kms_key_id
|
|
14051
|
+
}));
|
|
14052
|
+
return { key, uri: `s3://${this.options.bucket}/${key}` };
|
|
14053
|
+
}
|
|
14054
|
+
async getText(key) {
|
|
14055
|
+
const [{ GetObjectCommand }, client] = await Promise.all([
|
|
14056
|
+
import("@aws-sdk/client-s3"),
|
|
14057
|
+
this.getClient()
|
|
14058
|
+
]);
|
|
14059
|
+
const objectKey = this.objectKey(key);
|
|
14060
|
+
const response = await client.send(new GetObjectCommand({
|
|
14061
|
+
Bucket: this.options.bucket,
|
|
14062
|
+
Key: objectKey
|
|
14063
|
+
}));
|
|
14064
|
+
if (!response.Body)
|
|
14065
|
+
return "";
|
|
14066
|
+
return await response.Body.transformToString();
|
|
14067
|
+
}
|
|
14068
|
+
async exists(key) {
|
|
14069
|
+
const [{ HeadObjectCommand }, client] = await Promise.all([
|
|
14070
|
+
import("@aws-sdk/client-s3"),
|
|
14071
|
+
this.getClient()
|
|
14072
|
+
]);
|
|
14073
|
+
const objectKey = this.objectKey(key);
|
|
14074
|
+
try {
|
|
14075
|
+
await client.send(new HeadObjectCommand({
|
|
14076
|
+
Bucket: this.options.bucket,
|
|
14077
|
+
Key: objectKey
|
|
14078
|
+
}));
|
|
14079
|
+
return true;
|
|
14080
|
+
} catch (error48) {
|
|
14081
|
+
const name = error48 instanceof Error ? error48.name : "";
|
|
14082
|
+
if (name === "NotFound" || name === "NoSuchKey" || name === "NotFoundError")
|
|
14083
|
+
return false;
|
|
14084
|
+
throw error48;
|
|
14085
|
+
}
|
|
14086
|
+
}
|
|
14087
|
+
}
|
|
14088
|
+
function createArtifactStore(config2, workspace) {
|
|
14089
|
+
if (config2.storage.type === "s3") {
|
|
14090
|
+
if (!config2.storage.s3?.bucket)
|
|
14091
|
+
throw new Error("S3 artifact storage requires storage.s3.bucket");
|
|
14092
|
+
return new S3ArtifactStore({
|
|
14093
|
+
bucket: config2.storage.s3.bucket,
|
|
14094
|
+
prefix: config2.storage.s3.prefix,
|
|
14095
|
+
region: config2.storage.s3.region,
|
|
14096
|
+
profile: config2.storage.s3.profile,
|
|
14097
|
+
max_attempts: config2.storage.s3.max_attempts,
|
|
14098
|
+
server_side_encryption: config2.storage.s3.server_side_encryption,
|
|
14099
|
+
kms_key_id: config2.storage.s3.kms_key_id
|
|
14100
|
+
});
|
|
14101
|
+
}
|
|
14102
|
+
return new LocalArtifactStore(workspace.artifactsDir);
|
|
14103
|
+
}
|
|
14104
|
+
|
|
14105
|
+
// src/outbox-consume.ts
|
|
14106
|
+
import { createHash as createHash2, randomUUID as randomUUID3 } from "crypto";
|
|
14107
|
+
import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
|
|
14108
|
+
import { basename } from "path";
|
|
14109
|
+
|
|
13955
14110
|
// src/knowledge-db.ts
|
|
13956
14111
|
import { Database } from "bun:sqlite";
|
|
13957
14112
|
var MIGRATION_1 = `
|
|
@@ -14191,10 +14346,35 @@ function getSchemaVersion(db) {
|
|
|
14191
14346
|
const row = db.query("SELECT MAX(version) AS version FROM schema_versions").get();
|
|
14192
14347
|
return row?.version ?? 0;
|
|
14193
14348
|
}
|
|
14349
|
+
function count(db, table) {
|
|
14350
|
+
const row = db.query(`SELECT COUNT(*) AS n FROM ${table}`).get();
|
|
14351
|
+
return row?.n ?? 0;
|
|
14352
|
+
}
|
|
14353
|
+
function getKnowledgeDbStats(path) {
|
|
14354
|
+
const db = openKnowledgeDb(path);
|
|
14355
|
+
try {
|
|
14356
|
+
return {
|
|
14357
|
+
schema_version: getSchemaVersion(db),
|
|
14358
|
+
sources: count(db, "sources"),
|
|
14359
|
+
source_revisions: count(db, "source_revisions"),
|
|
14360
|
+
chunks: count(db, "chunks"),
|
|
14361
|
+
wiki_pages: count(db, "wiki_pages"),
|
|
14362
|
+
citations: count(db, "citations"),
|
|
14363
|
+
indexes: count(db, "knowledge_indexes"),
|
|
14364
|
+
runs: count(db, "runs"),
|
|
14365
|
+
run_events: count(db, "run_events"),
|
|
14366
|
+
redaction_findings: count(db, "redaction_findings"),
|
|
14367
|
+
audit_events: count(db, "audit_events"),
|
|
14368
|
+
approval_gates: count(db, "approval_gates")
|
|
14369
|
+
};
|
|
14370
|
+
} finally {
|
|
14371
|
+
db.close();
|
|
14372
|
+
}
|
|
14373
|
+
}
|
|
14194
14374
|
|
|
14195
14375
|
// src/safety.ts
|
|
14196
14376
|
import { createHash, randomUUID as randomUUID2 } from "crypto";
|
|
14197
|
-
import { relative, resolve as resolve2, sep } from "path";
|
|
14377
|
+
import { relative as relative2, resolve as resolve2, sep as sep2 } from "path";
|
|
14198
14378
|
function envEnabled(name) {
|
|
14199
14379
|
const value = process.env[name];
|
|
14200
14380
|
return value === "1" || value === "true" || value === "yes";
|
|
@@ -14237,8 +14417,8 @@ function resolveSafetyPolicy(config2, workspace) {
|
|
|
14237
14417
|
};
|
|
14238
14418
|
}
|
|
14239
14419
|
function isInside(root, target) {
|
|
14240
|
-
const rel =
|
|
14241
|
-
return rel === "" || !rel.startsWith("..") && rel !== ".." && !rel.startsWith(`..${
|
|
14420
|
+
const rel = relative2(root, target);
|
|
14421
|
+
return rel === "" || !rel.startsWith("..") && rel !== ".." && !rel.startsWith(`..${sep2}`);
|
|
14242
14422
|
}
|
|
14243
14423
|
function assertWriteAllowed(targetPath, policy) {
|
|
14244
14424
|
const resolved = resolve2(targetPath);
|
|
@@ -14246,6 +14426,47 @@ function assertWriteAllowed(targetPath, policy) {
|
|
|
14246
14426
|
throw new Error(`Safety policy denied write outside .hasna/apps/knowledge: ${targetPath}`);
|
|
14247
14427
|
}
|
|
14248
14428
|
}
|
|
14429
|
+
function assertS3ReadAllowed(uri, policy) {
|
|
14430
|
+
const parsed = new URL(uri);
|
|
14431
|
+
const bucket = parsed.hostname;
|
|
14432
|
+
if (!policy.network.s3ReadsEnabled) {
|
|
14433
|
+
throw new Error("Safety policy denied S3 read. Set safety.network.s3_reads_enabled=true or HASNA_KNOWLEDGE_ALLOW_S3_READS=1.");
|
|
14434
|
+
}
|
|
14435
|
+
if (!policy.network.allowedS3Buckets.includes(bucket)) {
|
|
14436
|
+
throw new Error(`Safety policy denied S3 bucket "${bucket}". Add it to safety.network.allowed_s3_buckets or HASNA_KNOWLEDGE_ALLOWED_S3_BUCKETS.`);
|
|
14437
|
+
}
|
|
14438
|
+
}
|
|
14439
|
+
function assertWebSearchAllowed(policy) {
|
|
14440
|
+
if (!policy.network.webSearchEnabled) {
|
|
14441
|
+
throw new Error("Safety policy denied web search. Set safety.network.web_search_enabled=true or HASNA_KNOWLEDGE_WEB_SEARCH=1.");
|
|
14442
|
+
}
|
|
14443
|
+
}
|
|
14444
|
+
var REDACTION_PATTERNS = [
|
|
14445
|
+
{ type: "private_key_block", severity: "high", regex: /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g, replacement: "[REDACTED:private_key_block]" },
|
|
14446
|
+
{ type: "secret_assignment", severity: "high", regex: /\b(?:api[_-]?key|secret|token|password)\s*[:=]\s*['"]?[^'"\s]{8,}/gi, replacement: "[REDACTED:secret_assignment]" },
|
|
14447
|
+
{ type: "openai_api_key", severity: "high", regex: /\bsk-[A-Za-z0-9_-]{20,}\b/g, replacement: "[REDACTED:openai_api_key]" },
|
|
14448
|
+
{ type: "anthropic_api_key", severity: "high", regex: /\bsk-ant-[A-Za-z0-9_-]{20,}\b/g, replacement: "[REDACTED:anthropic_api_key]" },
|
|
14449
|
+
{ type: "aws_access_key_id", severity: "high", regex: /\bA(?:KIA|SIA)[A-Z0-9]{16}\b/g, replacement: "[REDACTED:aws_access_key_id]" }
|
|
14450
|
+
];
|
|
14451
|
+
function redactSecrets(text, policy) {
|
|
14452
|
+
if (policy && !policy.redaction.enabled)
|
|
14453
|
+
return { text, findings: [] };
|
|
14454
|
+
let output = text;
|
|
14455
|
+
const findings = [];
|
|
14456
|
+
for (const pattern of REDACTION_PATTERNS) {
|
|
14457
|
+
output = output.replace(pattern.regex, (match, ...args) => {
|
|
14458
|
+
const offset = typeof args.at(-2) === "number" ? args.at(-2) : output.indexOf(match);
|
|
14459
|
+
findings.push({
|
|
14460
|
+
type: pattern.type,
|
|
14461
|
+
severity: pattern.severity,
|
|
14462
|
+
start: Math.max(0, offset),
|
|
14463
|
+
end: Math.max(0, offset + match.length)
|
|
14464
|
+
});
|
|
14465
|
+
return pattern.replacement;
|
|
14466
|
+
});
|
|
14467
|
+
}
|
|
14468
|
+
return { text: output, findings };
|
|
14469
|
+
}
|
|
14249
14470
|
function auditId(input) {
|
|
14250
14471
|
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
14472
|
}
|
|
@@ -14264,6 +14485,795 @@ function recordAuditEvent(db, input) {
|
|
|
14264
14485
|
]);
|
|
14265
14486
|
return id;
|
|
14266
14487
|
}
|
|
14488
|
+
function recordRedactionFindings(db, input) {
|
|
14489
|
+
const createdAt = input.created_at ?? new Date().toISOString();
|
|
14490
|
+
for (const finding of input.findings) {
|
|
14491
|
+
db.run(`INSERT INTO redaction_findings (id, source_uri, run_id, severity, finding_type, metadata_json, created_at)
|
|
14492
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`, [
|
|
14493
|
+
`redact_${randomUUID2()}`,
|
|
14494
|
+
input.source_uri ?? null,
|
|
14495
|
+
input.run_id ?? null,
|
|
14496
|
+
finding.severity,
|
|
14497
|
+
finding.type,
|
|
14498
|
+
JSON.stringify({ ...input.metadata ?? {}, start: finding.start, end: finding.end }),
|
|
14499
|
+
createdAt
|
|
14500
|
+
]);
|
|
14501
|
+
}
|
|
14502
|
+
return input.findings.length;
|
|
14503
|
+
}
|
|
14504
|
+
|
|
14505
|
+
// src/outbox-consume.ts
|
|
14506
|
+
function stableId(prefix, value) {
|
|
14507
|
+
return `${prefix}_${createHash2("sha256").update(value).digest("hex").slice(0, 20)}`;
|
|
14508
|
+
}
|
|
14509
|
+
function asObject(value) {
|
|
14510
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : undefined;
|
|
14511
|
+
}
|
|
14512
|
+
function asString(value) {
|
|
14513
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
14514
|
+
}
|
|
14515
|
+
function buildSourceRef(event) {
|
|
14516
|
+
const explicit = asString(event.source_ref) ?? asString(event.source_uri) ?? asString(event.uri);
|
|
14517
|
+
if (explicit)
|
|
14518
|
+
return explicit;
|
|
14519
|
+
const fileId = asString(event.file_id);
|
|
14520
|
+
if (fileId) {
|
|
14521
|
+
const revision = asString(event.revision_id) ?? asString(event.revision);
|
|
14522
|
+
const fileRef = `open-files://file/${encodeURIComponent(fileId)}`;
|
|
14523
|
+
return revision ? `${fileRef}/revision/${encodeURIComponent(revision)}` : fileRef;
|
|
14524
|
+
}
|
|
14525
|
+
const sourceId = asString(event.source_id);
|
|
14526
|
+
const path = asString(event.path);
|
|
14527
|
+
if (sourceId && path) {
|
|
14528
|
+
return `open-files://source/${encodeURIComponent(sourceId)}/path/${encodeURIComponent(path)}`;
|
|
14529
|
+
}
|
|
14530
|
+
throw new Error("Outbox event is missing source_ref, file_id, or source_id/path.");
|
|
14531
|
+
}
|
|
14532
|
+
function baseSourceUri(sourceRef, parsed) {
|
|
14533
|
+
if (parsed.kind === "open-files" && parsed.entity === "file" && parsed.revision_id) {
|
|
14534
|
+
return sourceRef.replace(/\/revision\/[^/]+$/, "");
|
|
14535
|
+
}
|
|
14536
|
+
return sourceRef;
|
|
14537
|
+
}
|
|
14538
|
+
function hashFromEvent(event) {
|
|
14539
|
+
return asString(event.hash) ?? asString(event.checksum) ?? asString(event.sha256) ?? null;
|
|
14540
|
+
}
|
|
14541
|
+
function revisionFromEvent(event, parsed, hash2) {
|
|
14542
|
+
return asString(event.revision_id) ?? asString(event.revision) ?? asString(event.version_id) ?? (parsed.kind === "open-files" ? parsed.revision_id : undefined) ?? hash2 ?? null;
|
|
14543
|
+
}
|
|
14544
|
+
function eventType(event) {
|
|
14545
|
+
return (asString(event.event) ?? asString(event.type) ?? asString(event.action) ?? asString(event.change_type) ?? "changed").toLowerCase();
|
|
14546
|
+
}
|
|
14547
|
+
function titleFromEvent(event) {
|
|
14548
|
+
const path = asString(event.path);
|
|
14549
|
+
return asString(event.title) ?? asString(event.name) ?? (path ? basename(path) : null);
|
|
14550
|
+
}
|
|
14551
|
+
function normalizeEvent(event, now) {
|
|
14552
|
+
const sourceRef = buildSourceRef(event);
|
|
14553
|
+
const parsed = parseSourceRef(sourceRef);
|
|
14554
|
+
const hash2 = hashFromEvent(event);
|
|
14555
|
+
return {
|
|
14556
|
+
raw: event,
|
|
14557
|
+
eventType: eventType(event),
|
|
14558
|
+
sourceRef,
|
|
14559
|
+
sourceUri: baseSourceUri(sourceRef, parsed),
|
|
14560
|
+
kind: parsed.kind,
|
|
14561
|
+
title: titleFromEvent(event),
|
|
14562
|
+
revision: revisionFromEvent(event, parsed, hash2),
|
|
14563
|
+
hash: hash2,
|
|
14564
|
+
status: asString(event.status)?.toLowerCase() ?? null,
|
|
14565
|
+
updatedAt: asString(event.updated_at) ?? now,
|
|
14566
|
+
acl: event.permissions ?? event.acl ?? undefined
|
|
14567
|
+
};
|
|
14568
|
+
}
|
|
14569
|
+
function parseOutboxText(text) {
|
|
14570
|
+
const trimmed = text.trim();
|
|
14571
|
+
if (!trimmed)
|
|
14572
|
+
return [];
|
|
14573
|
+
if (trimmed.startsWith("[")) {
|
|
14574
|
+
const parsed = JSON.parse(trimmed);
|
|
14575
|
+
if (!Array.isArray(parsed))
|
|
14576
|
+
throw new Error("Outbox array parse failed.");
|
|
14577
|
+
return parsed.map((entry) => {
|
|
14578
|
+
const event = asObject(entry);
|
|
14579
|
+
if (!event)
|
|
14580
|
+
throw new Error("Outbox array entries must be objects.");
|
|
14581
|
+
return event;
|
|
14582
|
+
});
|
|
14583
|
+
}
|
|
14584
|
+
if (trimmed.startsWith("{")) {
|
|
14585
|
+
try {
|
|
14586
|
+
const parsed = JSON.parse(trimmed);
|
|
14587
|
+
const object2 = asObject(parsed);
|
|
14588
|
+
if (!object2)
|
|
14589
|
+
throw new Error("Outbox object parse failed.");
|
|
14590
|
+
if (Array.isArray(object2.events)) {
|
|
14591
|
+
return object2.events.map((entry) => {
|
|
14592
|
+
const event = asObject(entry);
|
|
14593
|
+
if (!event)
|
|
14594
|
+
throw new Error("Outbox events entries must be objects.");
|
|
14595
|
+
return event;
|
|
14596
|
+
});
|
|
14597
|
+
}
|
|
14598
|
+
if ("source_ref" in object2 || "source_uri" in object2 || "file_id" in object2)
|
|
14599
|
+
return [object2];
|
|
14600
|
+
} catch (error48) {
|
|
14601
|
+
const lines = trimmed.split(/\r?\n/).filter((line) => line.trim().length > 0);
|
|
14602
|
+
if (lines.length <= 1)
|
|
14603
|
+
throw error48;
|
|
14604
|
+
return lines.map((line) => {
|
|
14605
|
+
const event = asObject(JSON.parse(line));
|
|
14606
|
+
if (!event)
|
|
14607
|
+
throw new Error("Outbox JSONL entries must be objects.");
|
|
14608
|
+
return event;
|
|
14609
|
+
});
|
|
14610
|
+
}
|
|
14611
|
+
}
|
|
14612
|
+
return trimmed.split(/\r?\n/).filter((line) => line.trim().length > 0).map((line) => {
|
|
14613
|
+
const event = asObject(JSON.parse(line));
|
|
14614
|
+
if (!event)
|
|
14615
|
+
throw new Error("Outbox JSONL entries must be objects.");
|
|
14616
|
+
return event;
|
|
14617
|
+
});
|
|
14618
|
+
}
|
|
14619
|
+
async function readS3Text(uri, config2, safetyPolicy) {
|
|
14620
|
+
const parsed = new URL(uri);
|
|
14621
|
+
const bucket = parsed.hostname;
|
|
14622
|
+
const key = decodeURIComponent(parsed.pathname.replace(/^\/+/, ""));
|
|
14623
|
+
if (!bucket || !key)
|
|
14624
|
+
throw new Error(`Invalid S3 outbox URI: ${uri}`);
|
|
14625
|
+
if (safetyPolicy)
|
|
14626
|
+
assertS3ReadAllowed(uri, safetyPolicy);
|
|
14627
|
+
const [{ S3Client, GetObjectCommand }, { fromIni }] = await Promise.all([
|
|
14628
|
+
import("@aws-sdk/client-s3"),
|
|
14629
|
+
import("@aws-sdk/credential-providers")
|
|
14630
|
+
]);
|
|
14631
|
+
const s3Config = config2?.storage.type === "s3" && config2.storage.s3?.bucket === bucket ? config2.storage.s3 : undefined;
|
|
14632
|
+
const client = new S3Client({
|
|
14633
|
+
region: s3Config?.region,
|
|
14634
|
+
credentials: s3Config?.profile ? fromIni({ profile: s3Config.profile }) : undefined,
|
|
14635
|
+
maxAttempts: s3Config?.max_attempts
|
|
14636
|
+
});
|
|
14637
|
+
const response = await client.send(new GetObjectCommand({ Bucket: bucket, Key: key }));
|
|
14638
|
+
if (!response.Body)
|
|
14639
|
+
return "";
|
|
14640
|
+
return await response.Body.transformToString();
|
|
14641
|
+
}
|
|
14642
|
+
async function readOutboxInput(input, config2, safetyPolicy) {
|
|
14643
|
+
if (input.startsWith("s3://"))
|
|
14644
|
+
return readS3Text(input, config2, safetyPolicy);
|
|
14645
|
+
if (!existsSync4(input))
|
|
14646
|
+
throw new Error(`Outbox not found: ${input}`);
|
|
14647
|
+
return readFileSync4(input, "utf8");
|
|
14648
|
+
}
|
|
14649
|
+
function mergeJson(existing, patch) {
|
|
14650
|
+
let base = {};
|
|
14651
|
+
if (existing) {
|
|
14652
|
+
try {
|
|
14653
|
+
base = asObject(JSON.parse(existing)) ?? {};
|
|
14654
|
+
} catch {
|
|
14655
|
+
base = {};
|
|
14656
|
+
}
|
|
14657
|
+
}
|
|
14658
|
+
return JSON.stringify({ ...base, ...patch });
|
|
14659
|
+
}
|
|
14660
|
+
function ensureSource(db, event, now) {
|
|
14661
|
+
const id = stableId("src", event.sourceUri);
|
|
14662
|
+
db.run(`INSERT INTO sources (id, uri, kind, title, metadata_json, acl_json, created_at, updated_at)
|
|
14663
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
14664
|
+
ON CONFLICT(uri) DO UPDATE SET
|
|
14665
|
+
kind = excluded.kind,
|
|
14666
|
+
title = COALESCE(excluded.title, sources.title),
|
|
14667
|
+
updated_at = excluded.updated_at`, [
|
|
14668
|
+
id,
|
|
14669
|
+
event.sourceUri,
|
|
14670
|
+
event.kind,
|
|
14671
|
+
event.title,
|
|
14672
|
+
JSON.stringify({ source_ref: event.sourceRef, source_uri: event.sourceUri, status: event.status, last_outbox_event: event.eventType }),
|
|
14673
|
+
JSON.stringify(event.acl ?? {}),
|
|
14674
|
+
now,
|
|
14675
|
+
event.updatedAt
|
|
14676
|
+
]);
|
|
14677
|
+
const row = db.query("SELECT id, metadata_json, acl_json FROM sources WHERE uri = ?").get(event.sourceUri);
|
|
14678
|
+
if (!row)
|
|
14679
|
+
throw new Error(`Failed to upsert source for outbox event: ${event.sourceUri}`);
|
|
14680
|
+
const patch = {
|
|
14681
|
+
source_ref: event.sourceRef,
|
|
14682
|
+
source_uri: event.sourceUri,
|
|
14683
|
+
last_outbox_event: event.eventType,
|
|
14684
|
+
last_outbox_at: event.updatedAt
|
|
14685
|
+
};
|
|
14686
|
+
if (event.status)
|
|
14687
|
+
patch.status = event.status;
|
|
14688
|
+
if (asString(event.raw.path))
|
|
14689
|
+
patch.path = event.raw.path;
|
|
14690
|
+
db.run("UPDATE sources SET metadata_json = ?, acl_json = CASE WHEN ? IS NULL THEN acl_json ELSE ? END, updated_at = ? WHERE id = ?", [
|
|
14691
|
+
mergeJson(row.metadata_json, patch),
|
|
14692
|
+
event.acl === undefined ? null : JSON.stringify(event.acl),
|
|
14693
|
+
event.acl === undefined ? null : JSON.stringify(event.acl),
|
|
14694
|
+
event.updatedAt,
|
|
14695
|
+
row.id
|
|
14696
|
+
]);
|
|
14697
|
+
return row.id;
|
|
14698
|
+
}
|
|
14699
|
+
function ensureRevision(db, sourceId, event, now) {
|
|
14700
|
+
if (!event.revision)
|
|
14701
|
+
return null;
|
|
14702
|
+
const id = stableId("rev", `${sourceId}\x00${event.revision}`);
|
|
14703
|
+
const metadata = {
|
|
14704
|
+
source_ref: event.sourceRef,
|
|
14705
|
+
source_uri: event.sourceUri,
|
|
14706
|
+
status: event.status,
|
|
14707
|
+
last_outbox_event: event.eventType,
|
|
14708
|
+
reindex_required: true
|
|
14709
|
+
};
|
|
14710
|
+
db.run(`INSERT INTO source_revisions (id, source_id, revision, hash, extracted_text_uri, metadata_json, created_at)
|
|
14711
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
14712
|
+
ON CONFLICT(source_id, revision) DO UPDATE SET
|
|
14713
|
+
hash = COALESCE(excluded.hash, source_revisions.hash),
|
|
14714
|
+
metadata_json = excluded.metadata_json`, [id, sourceId, event.revision, event.hash, asString(event.raw.extracted_text_ref) ?? null, JSON.stringify(metadata), now]);
|
|
14715
|
+
const row = db.query("SELECT id FROM source_revisions WHERE source_id = ? AND revision = ?").get(sourceId, event.revision);
|
|
14716
|
+
return row?.id ?? null;
|
|
14717
|
+
}
|
|
14718
|
+
function revisionIdsForEvent(db, sourceId, event) {
|
|
14719
|
+
if (event.revision) {
|
|
14720
|
+
return db.query("SELECT id FROM source_revisions WHERE source_id = ? AND revision = ?").all(sourceId, event.revision).map((row) => row.id);
|
|
14721
|
+
}
|
|
14722
|
+
if (event.hash) {
|
|
14723
|
+
return db.query("SELECT id FROM source_revisions WHERE source_id = ? AND hash = ?").all(sourceId, event.hash).map((row) => row.id);
|
|
14724
|
+
}
|
|
14725
|
+
return db.query("SELECT id FROM source_revisions WHERE source_id = ?").all(sourceId).map((row) => row.id);
|
|
14726
|
+
}
|
|
14727
|
+
function invalidateRevision(db, revisionId) {
|
|
14728
|
+
const chunks = db.query("SELECT id FROM chunks WHERE source_revision_id = ?").all(revisionId);
|
|
14729
|
+
let embeddingsDeleted = 0;
|
|
14730
|
+
for (const chunk of chunks) {
|
|
14731
|
+
const row = db.query("SELECT COUNT(*) AS n FROM chunk_embeddings WHERE chunk_id = ?").get(chunk.id);
|
|
14732
|
+
embeddingsDeleted += row?.n ?? 0;
|
|
14733
|
+
db.run("DELETE FROM chunk_embeddings WHERE chunk_id = ?", [chunk.id]);
|
|
14734
|
+
db.run("DELETE FROM chunks_fts WHERE chunk_id = ?", [chunk.id]);
|
|
14735
|
+
}
|
|
14736
|
+
db.run("DELETE FROM chunks WHERE source_revision_id = ?", [revisionId]);
|
|
14737
|
+
const revision = db.query("SELECT metadata_json FROM source_revisions WHERE id = ?").get(revisionId);
|
|
14738
|
+
db.run("UPDATE source_revisions SET metadata_json = ? WHERE id = ?", [mergeJson(revision?.metadata_json, { reindex_required: true, invalidated_at: new Date().toISOString() }), revisionId]);
|
|
14739
|
+
return { chunksDeleted: chunks.length, embeddingsDeleted };
|
|
14740
|
+
}
|
|
14741
|
+
function isDeleteEvent(eventType2, status) {
|
|
14742
|
+
return status === "deleted" || ["delete", "deleted", "remove", "removed"].includes(eventType2);
|
|
14743
|
+
}
|
|
14744
|
+
function isMoveEvent(eventType2) {
|
|
14745
|
+
return ["move", "moved", "rename", "renamed", "path_changed"].includes(eventType2);
|
|
14746
|
+
}
|
|
14747
|
+
function isPermissionEvent(eventType2) {
|
|
14748
|
+
return ["permission", "permissions", "permission_changed", "acl_changed"].includes(eventType2);
|
|
14749
|
+
}
|
|
14750
|
+
async function consumeOpenFilesOutbox(options) {
|
|
14751
|
+
const now = (options.now ?? new Date).toISOString();
|
|
14752
|
+
if (options.safetyPolicy)
|
|
14753
|
+
assertWriteAllowed(options.dbPath, options.safetyPolicy);
|
|
14754
|
+
migrateKnowledgeDb(options.dbPath);
|
|
14755
|
+
const text = await readOutboxInput(options.input, options.config, options.safetyPolicy);
|
|
14756
|
+
const events = parseOutboxText(text);
|
|
14757
|
+
const db = openKnowledgeDb(options.dbPath);
|
|
14758
|
+
const runId = `run_${randomUUID3()}`;
|
|
14759
|
+
try {
|
|
14760
|
+
return db.transaction(() => {
|
|
14761
|
+
db.run(`INSERT INTO runs (id, type, prompt, status, provider, model, metadata_json, created_at, updated_at)
|
|
14762
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
14763
|
+
runId,
|
|
14764
|
+
"open-files-outbox",
|
|
14765
|
+
options.input,
|
|
14766
|
+
"completed",
|
|
14767
|
+
"local",
|
|
14768
|
+
"open-files-outbox",
|
|
14769
|
+
JSON.stringify({ path: options.input, events: events.length }),
|
|
14770
|
+
now,
|
|
14771
|
+
now
|
|
14772
|
+
]);
|
|
14773
|
+
const sourcesTouched = new Set;
|
|
14774
|
+
const revisionsTouched = new Set;
|
|
14775
|
+
let chunksDeleted = 0;
|
|
14776
|
+
let embeddingsDeleted = 0;
|
|
14777
|
+
let staleRevisions = 0;
|
|
14778
|
+
let deletedSources = 0;
|
|
14779
|
+
let movedSources = 0;
|
|
14780
|
+
let permissionUpdates = 0;
|
|
14781
|
+
recordAuditEvent(db, {
|
|
14782
|
+
event_type: "source_read",
|
|
14783
|
+
action: options.input.startsWith("s3://") ? "s3_outbox_read" : "local_outbox_read",
|
|
14784
|
+
target_uri: options.input,
|
|
14785
|
+
decision: "allow",
|
|
14786
|
+
metadata: { events: events.length, read_only: true },
|
|
14787
|
+
created_at: now
|
|
14788
|
+
});
|
|
14789
|
+
events.forEach((raw, index) => {
|
|
14790
|
+
const event = normalizeEvent(raw, now);
|
|
14791
|
+
const sourceId = ensureSource(db, event, now);
|
|
14792
|
+
sourcesTouched.add(sourceId);
|
|
14793
|
+
const createdRevisionId = ensureRevision(db, sourceId, event, now);
|
|
14794
|
+
if (createdRevisionId)
|
|
14795
|
+
revisionsTouched.add(createdRevisionId);
|
|
14796
|
+
const affectedRevisionIds = revisionIdsForEvent(db, sourceId, event);
|
|
14797
|
+
for (const revisionId of affectedRevisionIds) {
|
|
14798
|
+
revisionsTouched.add(revisionId);
|
|
14799
|
+
const invalidation = invalidateRevision(db, revisionId);
|
|
14800
|
+
chunksDeleted += invalidation.chunksDeleted;
|
|
14801
|
+
embeddingsDeleted += invalidation.embeddingsDeleted;
|
|
14802
|
+
staleRevisions += 1;
|
|
14803
|
+
}
|
|
14804
|
+
if (isDeleteEvent(event.eventType, event.status))
|
|
14805
|
+
deletedSources += 1;
|
|
14806
|
+
if (isMoveEvent(event.eventType))
|
|
14807
|
+
movedSources += 1;
|
|
14808
|
+
if (isPermissionEvent(event.eventType) || event.acl !== undefined)
|
|
14809
|
+
permissionUpdates += 1;
|
|
14810
|
+
db.run(`INSERT INTO run_events (id, run_id, level, event, metadata_json, created_at)
|
|
14811
|
+
VALUES (?, ?, ?, ?, ?, ?)`, [
|
|
14812
|
+
stableId("evt", `${runId}\x00${index}\x00${event.sourceRef}\x00${event.eventType}`),
|
|
14813
|
+
runId,
|
|
14814
|
+
"info",
|
|
14815
|
+
event.eventType,
|
|
14816
|
+
JSON.stringify({
|
|
14817
|
+
source_ref: event.sourceRef,
|
|
14818
|
+
source_uri: event.sourceUri,
|
|
14819
|
+
revision: event.revision,
|
|
14820
|
+
hash: event.hash,
|
|
14821
|
+
status: event.status,
|
|
14822
|
+
affected_revisions: affectedRevisionIds.length
|
|
14823
|
+
}),
|
|
14824
|
+
event.updatedAt
|
|
14825
|
+
]);
|
|
14826
|
+
});
|
|
14827
|
+
db.run(`INSERT INTO provider_usage (id, run_id, provider, model, input_tokens, output_tokens, cost_usd, metadata_json, created_at)
|
|
14828
|
+
VALUES (?, ?, ?, ?, 0, 0, 0, ?, ?)`, [
|
|
14829
|
+
stableId("usage", runId),
|
|
14830
|
+
runId,
|
|
14831
|
+
"local",
|
|
14832
|
+
"open-files-outbox",
|
|
14833
|
+
JSON.stringify({ note: "No model provider used for outbox invalidation." }),
|
|
14834
|
+
now
|
|
14835
|
+
]);
|
|
14836
|
+
recordAuditEvent(db, {
|
|
14837
|
+
event_type: "write",
|
|
14838
|
+
action: "knowledge_outbox_invalidation",
|
|
14839
|
+
target_uri: options.dbPath,
|
|
14840
|
+
decision: "allow",
|
|
14841
|
+
metadata: {
|
|
14842
|
+
run_id: runId,
|
|
14843
|
+
events: events.length,
|
|
14844
|
+
sources: sourcesTouched.size,
|
|
14845
|
+
revisions: revisionsTouched.size,
|
|
14846
|
+
chunks_deleted: chunksDeleted,
|
|
14847
|
+
embeddings_deleted: embeddingsDeleted
|
|
14848
|
+
},
|
|
14849
|
+
created_at: now
|
|
14850
|
+
});
|
|
14851
|
+
return {
|
|
14852
|
+
path: options.input,
|
|
14853
|
+
db_path: options.dbPath,
|
|
14854
|
+
run_id: runId,
|
|
14855
|
+
events_seen: events.length,
|
|
14856
|
+
sources_touched: sourcesTouched.size,
|
|
14857
|
+
revisions_touched: revisionsTouched.size,
|
|
14858
|
+
chunks_deleted: chunksDeleted,
|
|
14859
|
+
embeddings_deleted: embeddingsDeleted,
|
|
14860
|
+
stale_revisions: staleRevisions,
|
|
14861
|
+
deleted_sources: deletedSources,
|
|
14862
|
+
moved_sources: movedSources,
|
|
14863
|
+
permission_updates: permissionUpdates
|
|
14864
|
+
};
|
|
14865
|
+
})();
|
|
14866
|
+
} finally {
|
|
14867
|
+
db.close();
|
|
14868
|
+
}
|
|
14869
|
+
}
|
|
14870
|
+
|
|
14871
|
+
// src/manifest-ingest.ts
|
|
14872
|
+
import { createHash as createHash3 } from "crypto";
|
|
14873
|
+
import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
|
|
14874
|
+
import { basename as basename2 } from "path";
|
|
14875
|
+
function stableId2(prefix, value) {
|
|
14876
|
+
return `${prefix}_${createHash3("sha256").update(value).digest("hex").slice(0, 20)}`;
|
|
14877
|
+
}
|
|
14878
|
+
function asObject2(value) {
|
|
14879
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : undefined;
|
|
14880
|
+
}
|
|
14881
|
+
function asString2(value) {
|
|
14882
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
14883
|
+
}
|
|
14884
|
+
function asNumber(value) {
|
|
14885
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
14886
|
+
}
|
|
14887
|
+
function buildSourceRefFromItem(item) {
|
|
14888
|
+
const explicit = asString2(item.source_ref) ?? asString2(item.source_uri) ?? asString2(item.uri);
|
|
14889
|
+
if (explicit)
|
|
14890
|
+
return explicit;
|
|
14891
|
+
const fileId = asString2(item.file_id);
|
|
14892
|
+
if (fileId) {
|
|
14893
|
+
const revision = asString2(item.revision_id) ?? asString2(item.revision);
|
|
14894
|
+
const fileRef = `open-files://file/${encodeURIComponent(fileId)}`;
|
|
14895
|
+
return revision ? `${fileRef}/revision/${encodeURIComponent(revision)}` : fileRef;
|
|
14896
|
+
}
|
|
14897
|
+
const sourceId = asString2(item.source_id);
|
|
14898
|
+
const path = asString2(item.path);
|
|
14899
|
+
if (sourceId && path) {
|
|
14900
|
+
return `open-files://source/${encodeURIComponent(sourceId)}/path/${encodeURIComponent(path)}`;
|
|
14901
|
+
}
|
|
14902
|
+
throw new Error("Manifest item is missing source_ref, file_id, or source_id/path.");
|
|
14903
|
+
}
|
|
14904
|
+
function baseSourceUri2(sourceRef, parsed) {
|
|
14905
|
+
if (parsed.kind === "open-files" && parsed.entity === "file" && parsed.revision_id) {
|
|
14906
|
+
return sourceRef.replace(/\/revision\/[^/]+$/, "");
|
|
14907
|
+
}
|
|
14908
|
+
return sourceRef;
|
|
14909
|
+
}
|
|
14910
|
+
function textFromItem(item) {
|
|
14911
|
+
const direct = asString2(item.extracted_text) ?? asString2(item.text) ?? asString2(item.content_text) ?? asString2(item.markdown);
|
|
14912
|
+
if (direct !== undefined)
|
|
14913
|
+
return direct;
|
|
14914
|
+
const content = item.content;
|
|
14915
|
+
return typeof content === "string" ? content : null;
|
|
14916
|
+
}
|
|
14917
|
+
function extractedTextUriFromItem(item) {
|
|
14918
|
+
const direct = asString2(item.extracted_text_ref) ?? asString2(item.extracted_text_uri) ?? asString2(item.text_ref);
|
|
14919
|
+
if (direct)
|
|
14920
|
+
return direct;
|
|
14921
|
+
const content = asObject2(item.content);
|
|
14922
|
+
return asString2(content?.extracted_text_ref) ?? asString2(content?.extracted_text_uri) ?? null;
|
|
14923
|
+
}
|
|
14924
|
+
function titleFromItem(item) {
|
|
14925
|
+
const path = asString2(item.path);
|
|
14926
|
+
return asString2(item.title) ?? asString2(item.name) ?? (path ? basename2(path) : null);
|
|
14927
|
+
}
|
|
14928
|
+
function hashFromItem(item) {
|
|
14929
|
+
return asString2(item.hash) ?? asString2(item.checksum) ?? asString2(item.sha256) ?? null;
|
|
14930
|
+
}
|
|
14931
|
+
function revisionFromItem(item, parsed, hash2) {
|
|
14932
|
+
const revision = asString2(item.revision_id) ?? asString2(item.revision) ?? asString2(item.version_id) ?? (parsed.kind === "open-files" ? parsed.revision_id : undefined) ?? hash2 ?? asString2(item.updated_at);
|
|
14933
|
+
return revision ?? "current";
|
|
14934
|
+
}
|
|
14935
|
+
function metadataFromItem(item, normalized) {
|
|
14936
|
+
const metadata = {};
|
|
14937
|
+
for (const [key, value] of Object.entries(item)) {
|
|
14938
|
+
if (["text", "content", "content_text", "extracted_text", "markdown"].includes(key))
|
|
14939
|
+
continue;
|
|
14940
|
+
metadata[key] = value;
|
|
14941
|
+
}
|
|
14942
|
+
metadata.source_ref = normalized.sourceRef;
|
|
14943
|
+
metadata.source_uri = normalized.sourceUri;
|
|
14944
|
+
metadata.status = normalized.status;
|
|
14945
|
+
return metadata;
|
|
14946
|
+
}
|
|
14947
|
+
function normalizeManifestItem(item, now) {
|
|
14948
|
+
const sourceRef = buildSourceRefFromItem(item);
|
|
14949
|
+
const parsed = parseSourceRef(sourceRef);
|
|
14950
|
+
const sourceUri = baseSourceUri2(sourceRef, parsed);
|
|
14951
|
+
const hash2 = hashFromItem(item);
|
|
14952
|
+
const status = asString2(item.status) ?? "active";
|
|
14953
|
+
return {
|
|
14954
|
+
raw: item,
|
|
14955
|
+
sourceRef,
|
|
14956
|
+
sourceUri,
|
|
14957
|
+
kind: parsed.kind,
|
|
14958
|
+
title: titleFromItem(item),
|
|
14959
|
+
revision: revisionFromItem(item, parsed, hash2),
|
|
14960
|
+
hash: hash2,
|
|
14961
|
+
extractedTextUri: extractedTextUriFromItem(item),
|
|
14962
|
+
text: textFromItem(item),
|
|
14963
|
+
metadata: metadataFromItem(item, { sourceRef, sourceUri, status }),
|
|
14964
|
+
acl: item.permissions ?? item.acl ?? {},
|
|
14965
|
+
status,
|
|
14966
|
+
updatedAt: asString2(item.updated_at) ?? now
|
|
14967
|
+
};
|
|
14968
|
+
}
|
|
14969
|
+
function parseManifestText(text) {
|
|
14970
|
+
const trimmed = text.trim();
|
|
14971
|
+
if (!trimmed)
|
|
14972
|
+
return [];
|
|
14973
|
+
if (trimmed.startsWith("[")) {
|
|
14974
|
+
const parsed = JSON.parse(trimmed);
|
|
14975
|
+
if (!Array.isArray(parsed))
|
|
14976
|
+
throw new Error("Manifest array parse failed.");
|
|
14977
|
+
return parsed.map((entry) => {
|
|
14978
|
+
const item = asObject2(entry);
|
|
14979
|
+
if (!item)
|
|
14980
|
+
throw new Error("Manifest array entries must be objects.");
|
|
14981
|
+
return item;
|
|
14982
|
+
});
|
|
14983
|
+
}
|
|
14984
|
+
if (trimmed.startsWith("{")) {
|
|
14985
|
+
try {
|
|
14986
|
+
const parsed = JSON.parse(trimmed);
|
|
14987
|
+
const object2 = asObject2(parsed);
|
|
14988
|
+
if (!object2)
|
|
14989
|
+
throw new Error("Manifest object parse failed.");
|
|
14990
|
+
if (Array.isArray(object2.items)) {
|
|
14991
|
+
return object2.items.map((entry) => {
|
|
14992
|
+
const item = asObject2(entry);
|
|
14993
|
+
if (!item)
|
|
14994
|
+
throw new Error("Manifest items entries must be objects.");
|
|
14995
|
+
return item;
|
|
14996
|
+
});
|
|
14997
|
+
}
|
|
14998
|
+
if ("source_ref" in object2 || "source_uri" in object2 || "file_id" in object2)
|
|
14999
|
+
return [object2];
|
|
15000
|
+
} catch (error48) {
|
|
15001
|
+
const lines = trimmed.split(/\r?\n/).filter((line) => line.trim().length > 0);
|
|
15002
|
+
if (lines.length <= 1)
|
|
15003
|
+
throw error48;
|
|
15004
|
+
return lines.map((line) => {
|
|
15005
|
+
const item = asObject2(JSON.parse(line));
|
|
15006
|
+
if (!item)
|
|
15007
|
+
throw new Error("Manifest JSONL entries must be objects.");
|
|
15008
|
+
return item;
|
|
15009
|
+
});
|
|
15010
|
+
}
|
|
15011
|
+
}
|
|
15012
|
+
return trimmed.split(/\r?\n/).filter((line) => line.trim().length > 0).map((line) => {
|
|
15013
|
+
const item = asObject2(JSON.parse(line));
|
|
15014
|
+
if (!item)
|
|
15015
|
+
throw new Error("Manifest JSONL entries must be objects.");
|
|
15016
|
+
return item;
|
|
15017
|
+
});
|
|
15018
|
+
}
|
|
15019
|
+
async function readS3Text2(uri, config2, safetyPolicy) {
|
|
15020
|
+
const parsed = new URL(uri);
|
|
15021
|
+
const bucket = parsed.hostname;
|
|
15022
|
+
const key = decodeURIComponent(parsed.pathname.replace(/^\/+/, ""));
|
|
15023
|
+
if (!bucket || !key)
|
|
15024
|
+
throw new Error(`Invalid S3 manifest URI: ${uri}`);
|
|
15025
|
+
if (safetyPolicy)
|
|
15026
|
+
assertS3ReadAllowed(uri, safetyPolicy);
|
|
15027
|
+
const [{ S3Client, GetObjectCommand }, { fromIni }] = await Promise.all([
|
|
15028
|
+
import("@aws-sdk/client-s3"),
|
|
15029
|
+
import("@aws-sdk/credential-providers")
|
|
15030
|
+
]);
|
|
15031
|
+
const s3Config = config2?.storage.type === "s3" && config2.storage.s3?.bucket === bucket ? config2.storage.s3 : undefined;
|
|
15032
|
+
const client = new S3Client({
|
|
15033
|
+
region: s3Config?.region,
|
|
15034
|
+
credentials: s3Config?.profile ? fromIni({ profile: s3Config.profile }) : undefined,
|
|
15035
|
+
maxAttempts: s3Config?.max_attempts
|
|
15036
|
+
});
|
|
15037
|
+
const response = await client.send(new GetObjectCommand({ Bucket: bucket, Key: key }));
|
|
15038
|
+
if (!response.Body)
|
|
15039
|
+
return "";
|
|
15040
|
+
return await response.Body.transformToString();
|
|
15041
|
+
}
|
|
15042
|
+
async function readManifestInput(input, config2, safetyPolicy) {
|
|
15043
|
+
if (input.startsWith("s3://"))
|
|
15044
|
+
return readS3Text2(input, config2, safetyPolicy);
|
|
15045
|
+
if (!existsSync5(input))
|
|
15046
|
+
throw new Error(`Manifest not found: ${input}`);
|
|
15047
|
+
return readFileSync5(input, "utf8");
|
|
15048
|
+
}
|
|
15049
|
+
function chunkText(text, maxChars, overlapChars) {
|
|
15050
|
+
const normalized = text.replace(/\r\n/g, `
|
|
15051
|
+
`);
|
|
15052
|
+
if (!normalized.trim())
|
|
15053
|
+
return [];
|
|
15054
|
+
const chunks = [];
|
|
15055
|
+
let start = 0;
|
|
15056
|
+
while (start < normalized.length) {
|
|
15057
|
+
const hardEnd = Math.min(normalized.length, start + maxChars);
|
|
15058
|
+
let end = hardEnd;
|
|
15059
|
+
if (hardEnd < normalized.length) {
|
|
15060
|
+
const paragraphBreak = normalized.lastIndexOf(`
|
|
15061
|
+
|
|
15062
|
+
`, hardEnd);
|
|
15063
|
+
const sentenceBreak = normalized.lastIndexOf(". ", hardEnd);
|
|
15064
|
+
const candidate = Math.max(paragraphBreak, sentenceBreak);
|
|
15065
|
+
if (candidate > start + Math.floor(maxChars * 0.5))
|
|
15066
|
+
end = candidate + (candidate === paragraphBreak ? 2 : 1);
|
|
15067
|
+
}
|
|
15068
|
+
const chunk = normalized.slice(start, end).trim();
|
|
15069
|
+
if (chunk) {
|
|
15070
|
+
chunks.push({
|
|
15071
|
+
ordinal: chunks.length,
|
|
15072
|
+
text: chunk,
|
|
15073
|
+
startOffset: start,
|
|
15074
|
+
endOffset: end
|
|
15075
|
+
});
|
|
15076
|
+
}
|
|
15077
|
+
if (end >= normalized.length)
|
|
15078
|
+
break;
|
|
15079
|
+
start = Math.max(0, end - overlapChars);
|
|
15080
|
+
}
|
|
15081
|
+
return chunks;
|
|
15082
|
+
}
|
|
15083
|
+
function estimateTokenCount(text) {
|
|
15084
|
+
const words = text.trim().split(/\s+/).filter(Boolean).length;
|
|
15085
|
+
return Math.max(1, Math.ceil(words * 1.25));
|
|
15086
|
+
}
|
|
15087
|
+
function deleteChunksForRevision(db, sourceRevisionId) {
|
|
15088
|
+
const rows = db.query("SELECT id FROM chunks WHERE source_revision_id = ?").all(sourceRevisionId);
|
|
15089
|
+
for (const row of rows) {
|
|
15090
|
+
db.run("DELETE FROM chunks_fts WHERE chunk_id = ?", [row.id]);
|
|
15091
|
+
}
|
|
15092
|
+
db.run("DELETE FROM chunks WHERE source_revision_id = ?", [sourceRevisionId]);
|
|
15093
|
+
return rows.length;
|
|
15094
|
+
}
|
|
15095
|
+
function upsertSource(db, item, now) {
|
|
15096
|
+
const sourceId = stableId2("src", item.sourceUri);
|
|
15097
|
+
db.run(`INSERT INTO sources (id, uri, kind, title, metadata_json, acl_json, created_at, updated_at)
|
|
15098
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
15099
|
+
ON CONFLICT(uri) DO UPDATE SET
|
|
15100
|
+
kind = excluded.kind,
|
|
15101
|
+
title = excluded.title,
|
|
15102
|
+
metadata_json = excluded.metadata_json,
|
|
15103
|
+
acl_json = excluded.acl_json,
|
|
15104
|
+
updated_at = excluded.updated_at`, [
|
|
15105
|
+
sourceId,
|
|
15106
|
+
item.sourceUri,
|
|
15107
|
+
item.kind,
|
|
15108
|
+
item.title,
|
|
15109
|
+
JSON.stringify(item.metadata),
|
|
15110
|
+
JSON.stringify(item.acl ?? {}),
|
|
15111
|
+
now,
|
|
15112
|
+
item.updatedAt
|
|
15113
|
+
]);
|
|
15114
|
+
const row = db.query("SELECT id FROM sources WHERE uri = ?").get(item.sourceUri);
|
|
15115
|
+
if (!row)
|
|
15116
|
+
throw new Error(`Failed to upsert source: ${item.sourceUri}`);
|
|
15117
|
+
return row.id;
|
|
15118
|
+
}
|
|
15119
|
+
function upsertRevision(db, sourceId, item, now) {
|
|
15120
|
+
const revisionId = stableId2("rev", `${sourceId}\x00${item.revision}`);
|
|
15121
|
+
db.run(`INSERT INTO source_revisions (id, source_id, revision, hash, extracted_text_uri, metadata_json, created_at)
|
|
15122
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
15123
|
+
ON CONFLICT(source_id, revision) DO UPDATE SET
|
|
15124
|
+
hash = excluded.hash,
|
|
15125
|
+
extracted_text_uri = excluded.extracted_text_uri,
|
|
15126
|
+
metadata_json = excluded.metadata_json`, [
|
|
15127
|
+
revisionId,
|
|
15128
|
+
sourceId,
|
|
15129
|
+
item.revision,
|
|
15130
|
+
item.hash,
|
|
15131
|
+
item.extractedTextUri,
|
|
15132
|
+
JSON.stringify(item.metadata),
|
|
15133
|
+
now
|
|
15134
|
+
]);
|
|
15135
|
+
const row = db.query("SELECT id FROM source_revisions WHERE source_id = ? AND revision = ?").get(sourceId, item.revision);
|
|
15136
|
+
if (!row)
|
|
15137
|
+
throw new Error(`Failed to upsert source revision: ${item.sourceRef}`);
|
|
15138
|
+
return row.id;
|
|
15139
|
+
}
|
|
15140
|
+
function insertChunks(db, sourceRevisionId, item, now, maxChars, overlapChars, safetyPolicy) {
|
|
15141
|
+
if (!item.text || item.status.toLowerCase() === "deleted")
|
|
15142
|
+
return { chunksInserted: 0, redactions: 0 };
|
|
15143
|
+
const redacted = redactSecrets(item.text, safetyPolicy);
|
|
15144
|
+
if (redacted.findings.length > 0) {
|
|
15145
|
+
recordRedactionFindings(db, {
|
|
15146
|
+
source_uri: item.sourceUri,
|
|
15147
|
+
findings: redacted.findings,
|
|
15148
|
+
metadata: { source_ref: item.sourceRef, revision: item.revision },
|
|
15149
|
+
created_at: now
|
|
15150
|
+
});
|
|
15151
|
+
recordAuditEvent(db, {
|
|
15152
|
+
event_type: "redaction",
|
|
15153
|
+
action: "source_text_redact",
|
|
15154
|
+
target_uri: item.sourceUri,
|
|
15155
|
+
decision: "redacted",
|
|
15156
|
+
metadata: { findings: redacted.findings.length, source_ref: item.sourceRef, revision: item.revision },
|
|
15157
|
+
created_at: now
|
|
15158
|
+
});
|
|
15159
|
+
}
|
|
15160
|
+
const chunks = chunkText(redacted.text, maxChars, overlapChars);
|
|
15161
|
+
for (const chunk of chunks) {
|
|
15162
|
+
const chunkId = stableId2("chk", `${sourceRevisionId}\x00${chunk.ordinal}\x00${chunk.text}`);
|
|
15163
|
+
const metadata = {
|
|
15164
|
+
source_ref: item.sourceRef,
|
|
15165
|
+
source_uri: item.sourceUri,
|
|
15166
|
+
hash: item.hash,
|
|
15167
|
+
status: item.status,
|
|
15168
|
+
path: asString2(item.raw.path) ?? null,
|
|
15169
|
+
mime: asString2(item.raw.mime) ?? asString2(item.raw.content_type) ?? null,
|
|
15170
|
+
size: asNumber(item.raw.size) ?? null
|
|
15171
|
+
};
|
|
15172
|
+
db.run(`INSERT INTO chunks (id, source_revision_id, kind, ordinal, text, token_count, start_offset, end_offset, metadata_json, created_at)
|
|
15173
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
15174
|
+
chunkId,
|
|
15175
|
+
sourceRevisionId,
|
|
15176
|
+
"source",
|
|
15177
|
+
chunk.ordinal,
|
|
15178
|
+
chunk.text,
|
|
15179
|
+
estimateTokenCount(chunk.text),
|
|
15180
|
+
chunk.startOffset,
|
|
15181
|
+
chunk.endOffset,
|
|
15182
|
+
JSON.stringify(metadata),
|
|
15183
|
+
now
|
|
15184
|
+
]);
|
|
15185
|
+
db.run("INSERT INTO chunks_fts (chunk_id, text, title, source_uri) VALUES (?, ?, ?, ?)", [chunkId, chunk.text, item.title ?? "", item.sourceUri]);
|
|
15186
|
+
}
|
|
15187
|
+
return { chunksInserted: chunks.length, redactions: redacted.findings.length };
|
|
15188
|
+
}
|
|
15189
|
+
async function ingestOpenFilesManifest(options) {
|
|
15190
|
+
const now = options.now ?? new Date;
|
|
15191
|
+
if (options.safetyPolicy)
|
|
15192
|
+
assertWriteAllowed(options.dbPath, options.safetyPolicy);
|
|
15193
|
+
migrateKnowledgeDb(options.dbPath);
|
|
15194
|
+
const text = await readManifestInput(options.input, options.config, options.safetyPolicy);
|
|
15195
|
+
const items = parseManifestText(text);
|
|
15196
|
+
return ingestOpenFilesManifestItems({
|
|
15197
|
+
dbPath: options.dbPath,
|
|
15198
|
+
items,
|
|
15199
|
+
sourceLabel: options.input,
|
|
15200
|
+
safetyPolicy: options.safetyPolicy,
|
|
15201
|
+
now,
|
|
15202
|
+
maxChunkChars: options.maxChunkChars,
|
|
15203
|
+
chunkOverlapChars: options.chunkOverlapChars
|
|
15204
|
+
});
|
|
15205
|
+
}
|
|
15206
|
+
async function ingestOpenFilesManifestItems(options) {
|
|
15207
|
+
const now = (options.now ?? new Date).toISOString();
|
|
15208
|
+
const maxChunkChars = options.maxChunkChars ?? 4000;
|
|
15209
|
+
const chunkOverlapChars = options.chunkOverlapChars ?? 200;
|
|
15210
|
+
if (maxChunkChars < 500)
|
|
15211
|
+
throw new Error("maxChunkChars must be at least 500.");
|
|
15212
|
+
if (chunkOverlapChars < 0 || chunkOverlapChars >= maxChunkChars)
|
|
15213
|
+
throw new Error("chunkOverlapChars must be less than maxChunkChars.");
|
|
15214
|
+
if (options.safetyPolicy)
|
|
15215
|
+
assertWriteAllowed(options.dbPath, options.safetyPolicy);
|
|
15216
|
+
migrateKnowledgeDb(options.dbPath);
|
|
15217
|
+
const db = openKnowledgeDb(options.dbPath);
|
|
15218
|
+
try {
|
|
15219
|
+
const result = db.transaction(() => {
|
|
15220
|
+
const seenSources = new Set;
|
|
15221
|
+
const seenRevisions = new Set;
|
|
15222
|
+
let chunksInserted = 0;
|
|
15223
|
+
let chunksDeleted = 0;
|
|
15224
|
+
let redactions = 0;
|
|
15225
|
+
let skipped = 0;
|
|
15226
|
+
recordAuditEvent(db, {
|
|
15227
|
+
event_type: "source_read",
|
|
15228
|
+
action: options.readAction ?? (options.sourceLabel.startsWith("s3://") ? "s3_manifest_read" : "local_manifest_read"),
|
|
15229
|
+
target_uri: options.sourceLabel,
|
|
15230
|
+
decision: "allow",
|
|
15231
|
+
metadata: { items: options.items.length, read_only: true },
|
|
15232
|
+
created_at: now
|
|
15233
|
+
});
|
|
15234
|
+
for (const raw of options.items) {
|
|
15235
|
+
const item = normalizeManifestItem(raw, now);
|
|
15236
|
+
const sourceId = upsertSource(db, item, now);
|
|
15237
|
+
const revisionId = upsertRevision(db, sourceId, item, now);
|
|
15238
|
+
seenSources.add(sourceId);
|
|
15239
|
+
seenRevisions.add(revisionId);
|
|
15240
|
+
if (item.text || item.status.toLowerCase() === "deleted") {
|
|
15241
|
+
chunksDeleted += deleteChunksForRevision(db, revisionId);
|
|
15242
|
+
}
|
|
15243
|
+
const inserted = insertChunks(db, revisionId, item, now, maxChunkChars, chunkOverlapChars, options.safetyPolicy);
|
|
15244
|
+
chunksInserted += inserted.chunksInserted;
|
|
15245
|
+
redactions += inserted.redactions;
|
|
15246
|
+
}
|
|
15247
|
+
recordAuditEvent(db, {
|
|
15248
|
+
event_type: "write",
|
|
15249
|
+
action: "knowledge_manifest_ingest",
|
|
15250
|
+
target_uri: options.dbPath,
|
|
15251
|
+
decision: "allow",
|
|
15252
|
+
metadata: { items: options.items.length, sources: seenSources.size, revisions: seenRevisions.size, chunks_inserted: chunksInserted, redactions },
|
|
15253
|
+
created_at: now
|
|
15254
|
+
});
|
|
15255
|
+
return {
|
|
15256
|
+
path: options.sourceLabel,
|
|
15257
|
+
db_path: options.dbPath,
|
|
15258
|
+
items_seen: options.items.length,
|
|
15259
|
+
sources_upserted: seenSources.size,
|
|
15260
|
+
revisions_upserted: seenRevisions.size,
|
|
15261
|
+
chunks_inserted: chunksInserted,
|
|
15262
|
+
chunks_deleted: chunksDeleted,
|
|
15263
|
+
redactions,
|
|
15264
|
+
skipped
|
|
15265
|
+
};
|
|
15266
|
+
})();
|
|
15267
|
+
return result;
|
|
15268
|
+
} finally {
|
|
15269
|
+
db.close();
|
|
15270
|
+
}
|
|
15271
|
+
}
|
|
15272
|
+
|
|
15273
|
+
// src/source-ingest.ts
|
|
15274
|
+
import { createHash as createHash4 } from "crypto";
|
|
15275
|
+
import { existsSync as existsSync6, readFileSync as readFileSync6 } from "fs";
|
|
15276
|
+
import { basename as basename3 } from "path";
|
|
14267
15277
|
|
|
14268
15278
|
// src/source-resolver.ts
|
|
14269
15279
|
function parseJsonObject(value) {
|
|
@@ -14539,6 +15549,429 @@ async function resolveOpenFilesSource(options) {
|
|
|
14539
15549
|
}
|
|
14540
15550
|
}
|
|
14541
15551
|
|
|
15552
|
+
// src/source-ingest.ts
|
|
15553
|
+
function sha256Text(text) {
|
|
15554
|
+
return `sha256:${createHash4("sha256").update(text).digest("hex")}`;
|
|
15555
|
+
}
|
|
15556
|
+
function stripHtml(html) {
|
|
15557
|
+
return html.replace(/<script[\s\S]*?<\/script>/gi, " ").replace(/<style[\s\S]*?<\/style>/gi, " ").replace(/<[^>]+>/g, " ").replace(/ /g, " ").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/\s+\n/g, `
|
|
15558
|
+
`).replace(/\n\s+/g, `
|
|
15559
|
+
`).replace(/[ \t]{2,}/g, " ").trim();
|
|
15560
|
+
}
|
|
15561
|
+
async function readS3Text3(uri, config2, safetyPolicy) {
|
|
15562
|
+
const parsed = new URL(uri);
|
|
15563
|
+
const bucket = parsed.hostname;
|
|
15564
|
+
const key = decodeURIComponent(parsed.pathname.replace(/^\/+/, ""));
|
|
15565
|
+
if (!bucket || !key)
|
|
15566
|
+
throw new Error(`Invalid S3 source URI: ${uri}`);
|
|
15567
|
+
if (safetyPolicy)
|
|
15568
|
+
assertS3ReadAllowed(uri, safetyPolicy);
|
|
15569
|
+
const [{ S3Client, GetObjectCommand }, { fromIni }] = await Promise.all([
|
|
15570
|
+
import("@aws-sdk/client-s3"),
|
|
15571
|
+
import("@aws-sdk/credential-providers")
|
|
15572
|
+
]);
|
|
15573
|
+
const s3Config = config2?.storage.type === "s3" && config2.storage.s3?.bucket === bucket ? config2.storage.s3 : undefined;
|
|
15574
|
+
const client = new S3Client({
|
|
15575
|
+
region: s3Config?.region,
|
|
15576
|
+
credentials: s3Config?.profile ? fromIni({ profile: s3Config.profile }) : undefined,
|
|
15577
|
+
maxAttempts: s3Config?.max_attempts
|
|
15578
|
+
});
|
|
15579
|
+
const response = await client.send(new GetObjectCommand({ Bucket: bucket, Key: key }));
|
|
15580
|
+
if (!response.Body)
|
|
15581
|
+
return "";
|
|
15582
|
+
return await response.Body.transformToString();
|
|
15583
|
+
}
|
|
15584
|
+
async function readWebText(uri, safetyPolicy) {
|
|
15585
|
+
if (safetyPolicy)
|
|
15586
|
+
assertWebSearchAllowed(safetyPolicy);
|
|
15587
|
+
const response = await fetch(uri, {
|
|
15588
|
+
headers: {
|
|
15589
|
+
accept: "text/markdown,text/plain,text/html,application/json;q=0.8,*/*;q=0.5",
|
|
15590
|
+
"user-agent": "@hasna/knowledge source-ingest"
|
|
15591
|
+
}
|
|
15592
|
+
});
|
|
15593
|
+
if (!response.ok)
|
|
15594
|
+
throw new Error(`Web source read failed ${response.status}: ${uri}`);
|
|
15595
|
+
const mime = response.headers.get("content-type");
|
|
15596
|
+
const body = await response.text();
|
|
15597
|
+
return { text: mime?.includes("html") ? stripHtml(body) : body, mime };
|
|
15598
|
+
}
|
|
15599
|
+
function titleForRef(parsed) {
|
|
15600
|
+
if (parsed.kind === "file")
|
|
15601
|
+
return basename3(parsed.path);
|
|
15602
|
+
if (parsed.kind === "s3")
|
|
15603
|
+
return basename3(parsed.key);
|
|
15604
|
+
if (parsed.kind === "web")
|
|
15605
|
+
return basename3(new URL(parsed.url).pathname) || parsed.url;
|
|
15606
|
+
return parsed.path ? basename3(parsed.path) : parsed.id;
|
|
15607
|
+
}
|
|
15608
|
+
async function readDirectSourceText(parsed, config2, safetyPolicy) {
|
|
15609
|
+
if (parsed.kind === "file") {
|
|
15610
|
+
if (!existsSync6(parsed.path))
|
|
15611
|
+
throw new Error(`Source file not found: ${parsed.path}`);
|
|
15612
|
+
const text = readFileSync6(parsed.path, "utf8");
|
|
15613
|
+
return {
|
|
15614
|
+
text,
|
|
15615
|
+
contentSource: "file",
|
|
15616
|
+
title: titleForRef(parsed),
|
|
15617
|
+
mime: "text/plain",
|
|
15618
|
+
size: text.length,
|
|
15619
|
+
hash: sha256Text(text),
|
|
15620
|
+
revision: null,
|
|
15621
|
+
extractedTextRef: null,
|
|
15622
|
+
metadata: { path: parsed.path },
|
|
15623
|
+
permissions: { mode: "read_only" }
|
|
15624
|
+
};
|
|
15625
|
+
}
|
|
15626
|
+
if (parsed.kind === "s3") {
|
|
15627
|
+
const text = await readS3Text3(parsed.uri, config2, safetyPolicy);
|
|
15628
|
+
return {
|
|
15629
|
+
text,
|
|
15630
|
+
contentSource: "s3",
|
|
15631
|
+
title: titleForRef(parsed),
|
|
15632
|
+
mime: "text/plain",
|
|
15633
|
+
size: text.length,
|
|
15634
|
+
hash: sha256Text(text),
|
|
15635
|
+
revision: null,
|
|
15636
|
+
extractedTextRef: null,
|
|
15637
|
+
metadata: { bucket: parsed.bucket, key: parsed.key },
|
|
15638
|
+
permissions: { mode: "read_only" }
|
|
15639
|
+
};
|
|
15640
|
+
}
|
|
15641
|
+
if (parsed.kind === "web") {
|
|
15642
|
+
const web = await readWebText(parsed.url, safetyPolicy);
|
|
15643
|
+
return {
|
|
15644
|
+
text: web.text,
|
|
15645
|
+
contentSource: "web",
|
|
15646
|
+
title: titleForRef(parsed),
|
|
15647
|
+
mime: web.mime,
|
|
15648
|
+
size: web.text.length,
|
|
15649
|
+
hash: sha256Text(web.text),
|
|
15650
|
+
revision: null,
|
|
15651
|
+
extractedTextRef: null,
|
|
15652
|
+
metadata: { url: parsed.url },
|
|
15653
|
+
permissions: { mode: "read_only" }
|
|
15654
|
+
};
|
|
15655
|
+
}
|
|
15656
|
+
throw new Error(`Direct source reading is not available for ${parsed.uri}`);
|
|
15657
|
+
}
|
|
15658
|
+
async function readTextRef(uri, config2, safetyPolicy) {
|
|
15659
|
+
if (uri.startsWith("open-files://")) {
|
|
15660
|
+
throw new Error("Open-files extracted text refs require an open-files resolver API. Ingest an open-files manifest with extracted_text or an extracted_text_ref using file://, s3://, or https://.");
|
|
15661
|
+
}
|
|
15662
|
+
const parsed = parseSourceRef(uri);
|
|
15663
|
+
const direct = await readDirectSourceText(parsed, config2, safetyPolicy);
|
|
15664
|
+
return { text: direct.text, contentSource: "extracted_text_ref" };
|
|
15665
|
+
}
|
|
15666
|
+
async function readOpenFilesSourceText(options) {
|
|
15667
|
+
const resolved = await resolveOpenFilesSource({
|
|
15668
|
+
dbPath: options.dbPath,
|
|
15669
|
+
sourceRef: options.sourceRef,
|
|
15670
|
+
purpose: options.purpose ?? "knowledge_index",
|
|
15671
|
+
limit: 100,
|
|
15672
|
+
safetyPolicy: options.safetyPolicy,
|
|
15673
|
+
now: options.now
|
|
15674
|
+
});
|
|
15675
|
+
if (!resolved.resolved) {
|
|
15676
|
+
throw new Error("Open-files source is not in the local knowledge catalog. Ingest an open-files manifest first or use the open-files resolver API.");
|
|
15677
|
+
}
|
|
15678
|
+
if (resolved.revision?.extracted_text_uri && !resolved.content.text_available) {
|
|
15679
|
+
const textRef = await readTextRef(resolved.revision.extracted_text_uri, options.config, options.safetyPolicy);
|
|
15680
|
+
return {
|
|
15681
|
+
text: textRef.text,
|
|
15682
|
+
contentSource: textRef.contentSource,
|
|
15683
|
+
title: resolved.source?.title ?? null,
|
|
15684
|
+
mime: resolved.content.mime,
|
|
15685
|
+
size: textRef.text.length,
|
|
15686
|
+
hash: resolved.revision.hash ?? sha256Text(textRef.text),
|
|
15687
|
+
revision: resolved.revision.revision,
|
|
15688
|
+
extractedTextRef: resolved.revision.extracted_text_uri,
|
|
15689
|
+
metadata: resolved.source?.metadata ?? {},
|
|
15690
|
+
permissions: resolved.source?.permissions ?? { mode: "read_only" }
|
|
15691
|
+
};
|
|
15692
|
+
}
|
|
15693
|
+
if (resolved.chunks.length === 0) {
|
|
15694
|
+
throw new Error("Open-files source has no extracted text chunks yet. Ingest an open-files manifest with extracted_text or extracted_text_ref first.");
|
|
15695
|
+
}
|
|
15696
|
+
const text = resolved.chunks.map((chunk) => chunk.text).join(`
|
|
15697
|
+
|
|
15698
|
+
`);
|
|
15699
|
+
return {
|
|
15700
|
+
text,
|
|
15701
|
+
contentSource: "catalog_chunks",
|
|
15702
|
+
title: resolved.source?.title ?? null,
|
|
15703
|
+
mime: resolved.content.mime,
|
|
15704
|
+
size: text.length,
|
|
15705
|
+
hash: resolved.revision?.hash ?? sha256Text(text),
|
|
15706
|
+
revision: resolved.revision?.revision ?? null,
|
|
15707
|
+
extractedTextRef: resolved.revision?.extracted_text_uri ?? null,
|
|
15708
|
+
metadata: resolved.source?.metadata ?? {},
|
|
15709
|
+
permissions: resolved.source?.permissions ?? { mode: "read_only" }
|
|
15710
|
+
};
|
|
15711
|
+
}
|
|
15712
|
+
function manifestItemForSource(sourceRef, parsed, resolved, purpose) {
|
|
15713
|
+
const hash2 = resolved.hash ?? sha256Text(resolved.text);
|
|
15714
|
+
const metadata = {
|
|
15715
|
+
...resolved.metadata,
|
|
15716
|
+
source_ref: sourceRef,
|
|
15717
|
+
content_source: resolved.contentSource,
|
|
15718
|
+
read_only: true
|
|
15719
|
+
};
|
|
15720
|
+
const item = {
|
|
15721
|
+
source_ref: sourceRef,
|
|
15722
|
+
name: resolved.title ?? titleForRef(parsed),
|
|
15723
|
+
mime: resolved.mime ?? "text/plain",
|
|
15724
|
+
size: resolved.size ?? resolved.text.length,
|
|
15725
|
+
hash: hash2,
|
|
15726
|
+
revision: resolved.revision ?? hash2,
|
|
15727
|
+
status: "active",
|
|
15728
|
+
updated_at: new Date().toISOString(),
|
|
15729
|
+
permissions: {
|
|
15730
|
+
mode: "read_only",
|
|
15731
|
+
allowed_purposes: [purpose],
|
|
15732
|
+
...resolved.permissions
|
|
15733
|
+
},
|
|
15734
|
+
metadata,
|
|
15735
|
+
extracted_text_ref: resolved.extractedTextRef,
|
|
15736
|
+
extracted_text: resolved.text
|
|
15737
|
+
};
|
|
15738
|
+
if (parsed.kind === "open-files") {
|
|
15739
|
+
if (parsed.entity === "file")
|
|
15740
|
+
item.file_id = parsed.id;
|
|
15741
|
+
if (parsed.entity === "source") {
|
|
15742
|
+
item.source_id = parsed.id;
|
|
15743
|
+
item.path = parsed.path;
|
|
15744
|
+
}
|
|
15745
|
+
}
|
|
15746
|
+
if (parsed.kind === "file")
|
|
15747
|
+
item.path = parsed.path;
|
|
15748
|
+
if (parsed.kind === "s3")
|
|
15749
|
+
item.path = parsed.key;
|
|
15750
|
+
if (parsed.kind === "web")
|
|
15751
|
+
item.url = parsed.url;
|
|
15752
|
+
return item;
|
|
15753
|
+
}
|
|
15754
|
+
async function ingestSourceRef(options) {
|
|
15755
|
+
const purpose = options.purpose ?? "knowledge_index";
|
|
15756
|
+
const parsed = parseSourceRef(options.sourceRef);
|
|
15757
|
+
const resolved = parsed.kind === "open-files" ? await readOpenFilesSourceText(options) : await readDirectSourceText(parsed, options.config, options.safetyPolicy);
|
|
15758
|
+
const item = manifestItemForSource(options.sourceRef, parsed, resolved, purpose);
|
|
15759
|
+
const result = await ingestOpenFilesManifestItems({
|
|
15760
|
+
dbPath: options.dbPath,
|
|
15761
|
+
items: [item],
|
|
15762
|
+
sourceLabel: options.sourceRef,
|
|
15763
|
+
readAction: "source_ref_ingest_read",
|
|
15764
|
+
safetyPolicy: options.safetyPolicy,
|
|
15765
|
+
now: options.now
|
|
15766
|
+
});
|
|
15767
|
+
return {
|
|
15768
|
+
...result,
|
|
15769
|
+
source_ref: options.sourceRef,
|
|
15770
|
+
content_source: resolved.contentSource,
|
|
15771
|
+
read_only: true,
|
|
15772
|
+
hash: String(item.hash)
|
|
15773
|
+
};
|
|
15774
|
+
}
|
|
15775
|
+
|
|
15776
|
+
// src/wiki-layout.ts
|
|
15777
|
+
function todayParts(now) {
|
|
15778
|
+
const year = String(now.getUTCFullYear());
|
|
15779
|
+
const month = String(now.getUTCMonth() + 1).padStart(2, "0");
|
|
15780
|
+
const day = String(now.getUTCDate()).padStart(2, "0");
|
|
15781
|
+
return { year, month, day };
|
|
15782
|
+
}
|
|
15783
|
+
function agentSchemaTemplate() {
|
|
15784
|
+
return `# Knowledge Agent Schema v1
|
|
15785
|
+
|
|
15786
|
+
## Source Rules
|
|
15787
|
+
|
|
15788
|
+
- Treat open-files source references as the preferred source of truth.
|
|
15789
|
+
- Do not copy raw source files into open-knowledge.
|
|
15790
|
+
- Cite every durable fact with a source URI, revision/hash when available, and optional span.
|
|
15791
|
+
- Mark uncertainty explicitly when sources disagree or are incomplete.
|
|
15792
|
+
|
|
15793
|
+
## Wiki Rules
|
|
15794
|
+
|
|
15795
|
+
- Write generated knowledge as Markdown pages under wiki/.
|
|
15796
|
+
- Keep root indexes small; use topic, team, project, and machine-readable shards for scale.
|
|
15797
|
+
- Preserve backlinks between related pages and decisions.
|
|
15798
|
+
- Prefer updating existing pages over creating near-duplicates.
|
|
15799
|
+
|
|
15800
|
+
## Query Rules
|
|
15801
|
+
|
|
15802
|
+
- Search wiki pages first, then source chunks, then deeper read-only source refs.
|
|
15803
|
+
- Use web search only when requested or when current external context is required.
|
|
15804
|
+
- File useful answers back into the wiki only after approval or approved auto-write mode.
|
|
15805
|
+
|
|
15806
|
+
## Lint Rules
|
|
15807
|
+
|
|
15808
|
+
- Flag stale pages, missing citations, contradictions, orphan pages, duplicate pages, and unresolved source refs.
|
|
15809
|
+
`;
|
|
15810
|
+
}
|
|
15811
|
+
function rootIndexTemplate() {
|
|
15812
|
+
return `# Knowledge Index
|
|
15813
|
+
|
|
15814
|
+
This is a compact orientation index for agents. It is not the full search index.
|
|
15815
|
+
|
|
15816
|
+
## Shards
|
|
15817
|
+
|
|
15818
|
+
- wiki/
|
|
15819
|
+
- indexes/
|
|
15820
|
+
- schemas/
|
|
15821
|
+
- logs/
|
|
15822
|
+
|
|
15823
|
+
## Source Ownership
|
|
15824
|
+
|
|
15825
|
+
Raw source files are resolved through open-files. This app stores source refs,
|
|
15826
|
+
citations, chunks, generated wiki artifacts, indexes, and run records.
|
|
15827
|
+
`;
|
|
15828
|
+
}
|
|
15829
|
+
function wikiReadmeTemplate() {
|
|
15830
|
+
return `# Wiki
|
|
15831
|
+
|
|
15832
|
+
Generated durable knowledge pages live here.
|
|
15833
|
+
|
|
15834
|
+
Pages should be concise, cited, and organized for both humans and agents.
|
|
15835
|
+
`;
|
|
15836
|
+
}
|
|
15837
|
+
async function initializeWikiLayout(store, now = new Date) {
|
|
15838
|
+
const { year, month, day } = todayParts(now);
|
|
15839
|
+
const schemaKey = "schemas/v1.md";
|
|
15840
|
+
const rootIndexKey = "indexes/root.md";
|
|
15841
|
+
const wikiReadmeKey = "wiki/README.md";
|
|
15842
|
+
const logKey = `logs/${year}/${month}/${day}.jsonl`;
|
|
15843
|
+
const event = {
|
|
15844
|
+
ts: now.toISOString(),
|
|
15845
|
+
event: "wiki_layout_initialized",
|
|
15846
|
+
schema_key: schemaKey,
|
|
15847
|
+
root_index_key: rootIndexKey,
|
|
15848
|
+
wiki_readme_key: wikiReadmeKey
|
|
15849
|
+
};
|
|
15850
|
+
const writes = [
|
|
15851
|
+
store.put({ key: schemaKey, body: agentSchemaTemplate(), content_type: "text/markdown" }),
|
|
15852
|
+
store.put({ key: rootIndexKey, body: rootIndexTemplate(), content_type: "text/markdown" }),
|
|
15853
|
+
store.put({ key: wikiReadmeKey, body: wikiReadmeTemplate(), content_type: "text/markdown" }),
|
|
15854
|
+
store.put({ key: logKey, body: `${JSON.stringify(event)}
|
|
15855
|
+
`, content_type: "application/x-ndjson" })
|
|
15856
|
+
];
|
|
15857
|
+
await Promise.all(writes);
|
|
15858
|
+
return {
|
|
15859
|
+
schema_key: schemaKey,
|
|
15860
|
+
root_index_key: rootIndexKey,
|
|
15861
|
+
wiki_readme_key: wikiReadmeKey,
|
|
15862
|
+
log_key: logKey,
|
|
15863
|
+
written: [schemaKey, rootIndexKey, wikiReadmeKey, logKey]
|
|
15864
|
+
};
|
|
15865
|
+
}
|
|
15866
|
+
|
|
15867
|
+
// src/service.ts
|
|
15868
|
+
class KnowledgeService {
|
|
15869
|
+
options;
|
|
15870
|
+
ensuredWorkspace;
|
|
15871
|
+
cachedConfig;
|
|
15872
|
+
constructor(options = {}) {
|
|
15873
|
+
this.options = options;
|
|
15874
|
+
}
|
|
15875
|
+
get scope() {
|
|
15876
|
+
return this.options.scope ?? "global";
|
|
15877
|
+
}
|
|
15878
|
+
get workspace() {
|
|
15879
|
+
return this.ensuredWorkspace ?? resolveScopedWorkspace(this.options.scope, this.options.cwd);
|
|
15880
|
+
}
|
|
15881
|
+
ensureWorkspace() {
|
|
15882
|
+
if (!this.ensuredWorkspace)
|
|
15883
|
+
this.ensuredWorkspace = ensureKnowledgeWorkspace(this.workspace.home);
|
|
15884
|
+
return this.ensuredWorkspace;
|
|
15885
|
+
}
|
|
15886
|
+
jsonStorePath() {
|
|
15887
|
+
return this.ensureWorkspace().jsonStorePath;
|
|
15888
|
+
}
|
|
15889
|
+
config() {
|
|
15890
|
+
if (!this.cachedConfig) {
|
|
15891
|
+
const workspace = this.ensureWorkspace();
|
|
15892
|
+
this.cachedConfig = readKnowledgeConfig(workspace.configPath);
|
|
15893
|
+
}
|
|
15894
|
+
return this.cachedConfig;
|
|
15895
|
+
}
|
|
15896
|
+
safetyPolicy() {
|
|
15897
|
+
return resolveSafetyPolicy(this.config(), this.ensureWorkspace());
|
|
15898
|
+
}
|
|
15899
|
+
artifactStore() {
|
|
15900
|
+
return createArtifactStore(this.config(), this.ensureWorkspace());
|
|
15901
|
+
}
|
|
15902
|
+
paths() {
|
|
15903
|
+
const workspace = this.ensureWorkspace();
|
|
15904
|
+
return {
|
|
15905
|
+
ok: true,
|
|
15906
|
+
scope: this.scope,
|
|
15907
|
+
home: workspace.home,
|
|
15908
|
+
config_path: workspace.configPath,
|
|
15909
|
+
json_store_path: workspace.jsonStorePath,
|
|
15910
|
+
knowledge_db_path: workspace.knowledgeDbPath,
|
|
15911
|
+
artifacts_dir: workspace.artifactsDir,
|
|
15912
|
+
indexes_dir: workspace.indexesDir,
|
|
15913
|
+
logs_dir: workspace.logsDir,
|
|
15914
|
+
runs_dir: workspace.runsDir,
|
|
15915
|
+
schemas_dir: workspace.schemasDir,
|
|
15916
|
+
wiki_dir: workspace.wikiDir,
|
|
15917
|
+
config: this.config(),
|
|
15918
|
+
message: workspace.home
|
|
15919
|
+
};
|
|
15920
|
+
}
|
|
15921
|
+
initDb() {
|
|
15922
|
+
return migrateKnowledgeDb(this.ensureWorkspace().knowledgeDbPath);
|
|
15923
|
+
}
|
|
15924
|
+
dbStats() {
|
|
15925
|
+
const workspace = this.ensureWorkspace();
|
|
15926
|
+
migrateKnowledgeDb(workspace.knowledgeDbPath);
|
|
15927
|
+
return getKnowledgeDbStats(workspace.knowledgeDbPath);
|
|
15928
|
+
}
|
|
15929
|
+
async initWiki() {
|
|
15930
|
+
return initializeWikiLayout(this.artifactStore());
|
|
15931
|
+
}
|
|
15932
|
+
async ingestManifest(input) {
|
|
15933
|
+
const workspace = this.ensureWorkspace();
|
|
15934
|
+
return ingestOpenFilesManifest({
|
|
15935
|
+
dbPath: workspace.knowledgeDbPath,
|
|
15936
|
+
input,
|
|
15937
|
+
config: this.config(),
|
|
15938
|
+
safetyPolicy: this.safetyPolicy()
|
|
15939
|
+
});
|
|
15940
|
+
}
|
|
15941
|
+
async ingestSource(sourceRef, purpose) {
|
|
15942
|
+
const workspace = this.ensureWorkspace();
|
|
15943
|
+
return ingestSourceRef({
|
|
15944
|
+
dbPath: workspace.knowledgeDbPath,
|
|
15945
|
+
sourceRef,
|
|
15946
|
+
purpose,
|
|
15947
|
+
config: this.config(),
|
|
15948
|
+
safetyPolicy: this.safetyPolicy()
|
|
15949
|
+
});
|
|
15950
|
+
}
|
|
15951
|
+
async resolveSource(sourceRef, options = {}) {
|
|
15952
|
+
const workspace = this.ensureWorkspace();
|
|
15953
|
+
return resolveOpenFilesSource({
|
|
15954
|
+
dbPath: workspace.knowledgeDbPath,
|
|
15955
|
+
sourceRef,
|
|
15956
|
+
purpose: options.purpose,
|
|
15957
|
+
limit: options.limit,
|
|
15958
|
+
safetyPolicy: this.safetyPolicy()
|
|
15959
|
+
});
|
|
15960
|
+
}
|
|
15961
|
+
async consumeOutbox(input) {
|
|
15962
|
+
const workspace = this.ensureWorkspace();
|
|
15963
|
+
return consumeOpenFilesOutbox({
|
|
15964
|
+
dbPath: workspace.knowledgeDbPath,
|
|
15965
|
+
input,
|
|
15966
|
+
config: this.config(),
|
|
15967
|
+
safetyPolicy: this.safetyPolicy()
|
|
15968
|
+
});
|
|
15969
|
+
}
|
|
15970
|
+
}
|
|
15971
|
+
function createKnowledgeService(options = {}) {
|
|
15972
|
+
return new KnowledgeService(options);
|
|
15973
|
+
}
|
|
15974
|
+
|
|
14542
15975
|
// src/mcp.js
|
|
14543
15976
|
var storePathField = exports_external.string().optional().describe("Path to the JSON store file");
|
|
14544
15977
|
var scopeField = exports_external.enum(["local", "global", "project"]).optional().describe("Workspace scope");
|
|
@@ -14555,7 +15988,7 @@ function resolveStorePath(storePath, scope) {
|
|
|
14555
15988
|
if (storePath)
|
|
14556
15989
|
return storePath;
|
|
14557
15990
|
if (scope === "project" || scope === "local") {
|
|
14558
|
-
return
|
|
15991
|
+
return createKnowledgeService({ scope }).jsonStorePath();
|
|
14559
15992
|
}
|
|
14560
15993
|
return defaultStorePath();
|
|
14561
15994
|
}
|
|
@@ -14597,22 +16030,7 @@ function buildServer() {
|
|
|
14597
16030
|
registerTool(server, "ok_paths", "Knowledge workspace paths", "Show resolved workspace and store paths", {
|
|
14598
16031
|
scope: scopeField
|
|
14599
16032
|
}, async ({ scope }) => {
|
|
14600
|
-
|
|
14601
|
-
return jsonText({
|
|
14602
|
-
ok: true,
|
|
14603
|
-
scope: scope ?? "global",
|
|
14604
|
-
home: workspace.home,
|
|
14605
|
-
config_path: workspace.configPath,
|
|
14606
|
-
json_store_path: workspace.jsonStorePath,
|
|
14607
|
-
knowledge_db_path: workspace.knowledgeDbPath,
|
|
14608
|
-
artifacts_dir: workspace.artifactsDir,
|
|
14609
|
-
indexes_dir: workspace.indexesDir,
|
|
14610
|
-
logs_dir: workspace.logsDir,
|
|
14611
|
-
runs_dir: workspace.runsDir,
|
|
14612
|
-
schemas_dir: workspace.schemasDir,
|
|
14613
|
-
wiki_dir: workspace.wikiDir,
|
|
14614
|
-
config: readKnowledgeConfig(workspace.configPath)
|
|
14615
|
-
});
|
|
16033
|
+
return jsonText(createKnowledgeService({ scope }).paths());
|
|
14616
16034
|
});
|
|
14617
16035
|
registerTool(server, "ok_parse_source_ref", "Parse source reference", "Parse and validate an open-files, S3, file, or web source ref", {
|
|
14618
16036
|
uri: exports_external.string().describe("Source reference URI")
|
|
@@ -14629,16 +16047,11 @@ function buildServer() {
|
|
|
14629
16047
|
limit: exports_external.number().optional().describe("Maximum chunks to return, default 10"),
|
|
14630
16048
|
scope: scopeField
|
|
14631
16049
|
}, async ({ source_ref, purpose, limit, scope }) => {
|
|
14632
|
-
const
|
|
14633
|
-
const config2 = readKnowledgeConfig(workspace.configPath);
|
|
14634
|
-
const safetyPolicy = resolveSafetyPolicy(config2, workspace);
|
|
16050
|
+
const service = createKnowledgeService({ scope });
|
|
14635
16051
|
try {
|
|
14636
|
-
const result = await
|
|
14637
|
-
dbPath: workspace.knowledgeDbPath,
|
|
14638
|
-
sourceRef: source_ref,
|
|
16052
|
+
const result = await service.resolveSource(source_ref, {
|
|
14639
16053
|
purpose,
|
|
14640
|
-
limit
|
|
14641
|
-
safetyPolicy
|
|
16054
|
+
limit
|
|
14642
16055
|
});
|
|
14643
16056
|
return jsonText({ ok: true, ...result });
|
|
14644
16057
|
} catch (error48) {
|
|
@@ -14969,7 +16382,7 @@ function buildServer() {
|
|
|
14969
16382
|
const storePath = resolveStorePath(store_path, scope);
|
|
14970
16383
|
return readStoreLocked(storePath, (db) => {
|
|
14971
16384
|
const filePath = file2 || "./knowledge-export.json";
|
|
14972
|
-
|
|
16385
|
+
writeFileSync4(filePath, JSON.stringify(db, null, 2));
|
|
14973
16386
|
return jsonText({ ok: true, file: filePath, count: db.items.length });
|
|
14974
16387
|
});
|
|
14975
16388
|
});
|
|
@@ -14978,9 +16391,9 @@ function buildServer() {
|
|
|
14978
16391
|
store_path: storePathField,
|
|
14979
16392
|
scope: scopeField
|
|
14980
16393
|
}, async ({ file: file2, store_path, scope }) => {
|
|
14981
|
-
if (!
|
|
16394
|
+
if (!existsSync7(file2))
|
|
14982
16395
|
return errorText(`File not found: ${file2}`);
|
|
14983
|
-
const imported = JSON.parse(
|
|
16396
|
+
const imported = JSON.parse(readFileSync7(file2, "utf8"));
|
|
14984
16397
|
if (!imported || !Array.isArray(imported.items))
|
|
14985
16398
|
return errorText('Invalid import file: expected {"items": [...]}');
|
|
14986
16399
|
const storePath = resolveStorePath(store_path, scope);
|