@hasna/knowledge 0.2.8 → 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.
@@ -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 existsSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
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.8",
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 = relative(root, target);
14241
- return rel === "" || !rel.startsWith("..") && rel !== ".." && !rel.startsWith(`..${sep}`);
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(/&nbsp;/g, " ").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/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 ensureKnowledgeWorkspace(resolveScopedWorkspace(scope).home).jsonStorePath;
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
- const workspace = ensureKnowledgeWorkspace(resolveScopedWorkspace(scope).home);
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 workspace = ensureKnowledgeWorkspace(resolveScopedWorkspace(scope).home);
14633
- const config2 = readKnowledgeConfig(workspace.configPath);
14634
- const safetyPolicy = resolveSafetyPolicy(config2, workspace);
16050
+ const service = createKnowledgeService({ scope });
14635
16051
  try {
14636
- const result = await resolveOpenFilesSource({
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
- writeFileSync3(filePath, JSON.stringify(db, null, 2));
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 (!existsSync3(file2))
16394
+ if (!existsSync7(file2))
14982
16395
  return errorText(`File not found: ${file2}`);
14983
- const imported = JSON.parse(readFileSync3(file2, "utf8"));
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);