@hasna/knowledge 0.2.11 → 0.2.12

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 CHANGED
@@ -62,6 +62,9 @@ open-knowledge export --format jsonl
62
62
  # Show resolved workspace paths
63
63
  open-knowledge paths --scope project --json
64
64
 
65
+ # Inspect local/S3 artifact storage and source ownership
66
+ open-knowledge storage status --scope project --json
67
+
65
68
  # Initialize the project SQLite catalog
66
69
  open-knowledge db init --scope project
67
70
 
@@ -168,6 +171,18 @@ open-knowledge paths [--scope global|project|local] [--json]
168
171
  Show the resolved Hasna app workspace, JSON compatibility store, SQLite path,
169
172
  artifact directories, and config.
170
173
 
174
+ ### storage
175
+ ```bash
176
+ open-knowledge storage status [--scope project] [--json]
177
+ open-knowledge storage validate [--scope project] [--json]
178
+ ```
179
+ Show the storage contract for local or S3-backed generated artifacts. Local mode
180
+ uses `.hasna/apps/knowledge` for config, SQLite, indexes, wiki artifacts, logs,
181
+ runs, and exports. S3 mode stores generated artifacts under the configured
182
+ knowledge bucket/prefix while `open-files` remains the source of truth for raw
183
+ source bytes. The command also reports artifact classes, allowed source ref
184
+ schemes, and warnings for non-scalable or unsafe config.
185
+
171
186
  ### db
172
187
  ```bash
173
188
  open-knowledge db init [--scope project]
@@ -277,8 +292,9 @@ open-knowledge-mcp
277
292
  The MCP server exposes item tools (`ok_add`, `ok_list`, `ok_get`, `ok_update`,
278
293
  `ok_delete`, `ok_archive`, `ok_restore`, `ok_upsert`, `ok_untag`,
279
294
  `ok_bulk_delete`, `ok_prune`, `ok_dedupe`, `ok_stats`, `ok_export`,
280
- `ok_import`, `ok_batch`), workspace inspection (`ok_paths`), and source-ref
281
- parsing/resolution (`ok_parse_source_ref`, `ok_resolve_source`).
295
+ `ok_import`, `ok_batch`), workspace/storage inspection (`ok_paths`,
296
+ `ok_storage_status`), and source-ref parsing/resolution
297
+ (`ok_parse_source_ref`, `ok_resolve_source`).
282
298
 
283
299
  ## Source And Artifact Boundary
284
300
 
@@ -13660,7 +13660,7 @@ import { existsSync as existsSync7, readFileSync as readFileSync7, writeFileSync
13660
13660
  // package.json
13661
13661
  var package_default = {
13662
13662
  name: "@hasna/knowledge",
13663
- version: "0.2.11",
13663
+ version: "0.2.12",
13664
13664
  description: "Agent-friendly local knowledge CLI with JSON output, pagination, and safe destructive actions",
13665
13665
  type: "module",
13666
13666
  bin: {
@@ -14391,7 +14391,8 @@ function getKnowledgeDbStats(path) {
14391
14391
  run_events: count(db, "run_events"),
14392
14392
  redaction_findings: count(db, "redaction_findings"),
14393
14393
  audit_events: count(db, "audit_events"),
14394
- approval_gates: count(db, "approval_gates")
14394
+ approval_gates: count(db, "approval_gates"),
14395
+ storage_objects: count(db, "storage_objects")
14395
14396
  };
14396
14397
  } finally {
14397
14398
  db.close();
@@ -15922,6 +15923,177 @@ function providerStatus(config2, env = process.env) {
15922
15923
  };
15923
15924
  }
15924
15925
 
15926
+ // src/storage-contract.ts
15927
+ import { createHash as createHash5, randomUUID as randomUUID4 } from "crypto";
15928
+ var GENERATED_ARTIFACTS = [
15929
+ {
15930
+ kind: "schema",
15931
+ prefix: "schemas/",
15932
+ description: "Machine-readable agent schemas and source rules."
15933
+ },
15934
+ {
15935
+ kind: "index",
15936
+ prefix: "indexes/",
15937
+ description: "Small orientation indexes and future shard manifests."
15938
+ },
15939
+ {
15940
+ kind: "log",
15941
+ prefix: "logs/",
15942
+ description: "Append-only JSONL run and wiki-maintenance log partitions."
15943
+ },
15944
+ {
15945
+ kind: "run",
15946
+ prefix: "runs/",
15947
+ description: "Prompt/tool/cost ledgers and generated output records."
15948
+ },
15949
+ {
15950
+ kind: "wiki_page",
15951
+ prefix: "wiki/",
15952
+ description: "Generated cited Markdown pages, not raw source files."
15953
+ },
15954
+ {
15955
+ kind: "export",
15956
+ prefix: "exports/",
15957
+ description: "Portable exports and snapshots of derived knowledge state."
15958
+ }
15959
+ ];
15960
+ function hashArtifactBody(body) {
15961
+ const bytes = typeof body === "string" ? Buffer.from(body) : Buffer.from(body);
15962
+ return {
15963
+ hash: `sha256:${createHash5("sha256").update(bytes).digest("hex")}`,
15964
+ size_bytes: bytes.byteLength
15965
+ };
15966
+ }
15967
+ function artifactKindForKey(key) {
15968
+ const match = GENERATED_ARTIFACTS.find((entry) => key.startsWith(entry.prefix));
15969
+ return match?.kind ?? "artifact";
15970
+ }
15971
+ function resolveStorageContract(config2, workspace, scope = "global") {
15972
+ const validation = validateStorageConfig(config2, workspace);
15973
+ const s3 = config2.storage.s3 ?? null;
15974
+ const prefix = s3?.prefix?.replace(/^\/+|\/+$/g, "") ?? "";
15975
+ const s3UriPrefix = s3 ? `s3://${s3.bucket}/${prefix ? `${prefix}/` : ""}` : "";
15976
+ return {
15977
+ scope,
15978
+ mode: config2.mode,
15979
+ storage_type: config2.storage.type,
15980
+ workspace_home: workspace.home,
15981
+ local_layout: {
15982
+ app_path: HASNA_KNOWLEDGE_APP_PATH,
15983
+ config_path: workspace.configPath,
15984
+ json_store_path: workspace.jsonStorePath,
15985
+ knowledge_db_path: workspace.knowledgeDbPath,
15986
+ directories: {
15987
+ artifacts: workspace.artifactsDir,
15988
+ cache: workspace.cacheDir,
15989
+ exports: workspace.exportsDir,
15990
+ indexes: workspace.indexesDir,
15991
+ logs: workspace.logsDir,
15992
+ runs: workspace.runsDir,
15993
+ schemas: workspace.schemasDir,
15994
+ wiki: workspace.wikiDir
15995
+ }
15996
+ },
15997
+ artifact_store: {
15998
+ type: config2.storage.type,
15999
+ artifacts_root: config2.storage.artifacts_root,
16000
+ uri_prefix: config2.storage.type === "s3" ? s3UriPrefix : `file://${workspace.artifactsDir}/`,
16001
+ s3: s3 ? {
16002
+ bucket: s3.bucket,
16003
+ prefix,
16004
+ region: s3.region ?? null,
16005
+ profile: s3.profile ?? null,
16006
+ server_side_encryption: s3.server_side_encryption ?? null,
16007
+ kms_key_configured: Boolean(s3.kms_key_id)
16008
+ } : null
16009
+ },
16010
+ source_ownership: {
16011
+ owner: "open-files",
16012
+ preferred_ref: config2.sources.preferred_ref,
16013
+ allowed_schemes: config2.sources.allowed_schemes,
16014
+ raw_source_bytes_stored_in_open_knowledge: false,
16015
+ stores: [
16016
+ "source refs",
16017
+ "source revisions and hashes",
16018
+ "citation spans",
16019
+ "redacted extracted chunks",
16020
+ "embeddings",
16021
+ "generated wiki artifacts",
16022
+ "indexes",
16023
+ "run ledgers"
16024
+ ],
16025
+ does_not_store: [
16026
+ "raw open-files bytes",
16027
+ "S3 object credentials",
16028
+ "connector secrets",
16029
+ "hosted tenant ownership state"
16030
+ ]
16031
+ },
16032
+ generated_artifacts: GENERATED_ARTIFACTS,
16033
+ scalability: {
16034
+ catalog: "knowledge.db tracks sources, revisions, chunks, citations, indexes, runs, and storage_objects.",
16035
+ indexes: "Indexes are cataloged DB rows plus sharded artifacts, not one giant index.md.",
16036
+ logs: "Logs use dated JSONL partitions under logs/yyyy/mm/dd.jsonl.",
16037
+ markdown: "Markdown pages are the readable wiki layer over DB/object-store state."
16038
+ },
16039
+ warnings: validation.warnings
16040
+ };
16041
+ }
16042
+ function validateStorageConfig(config2, workspace) {
16043
+ const errors3 = [];
16044
+ const warnings = [];
16045
+ if (!workspace.home.endsWith(HASNA_KNOWLEDGE_APP_PATH)) {
16046
+ warnings.push(`Workspace home does not end with ${HASNA_KNOWLEDGE_APP_PATH}: ${workspace.home}`);
16047
+ }
16048
+ if (config2.storage.type === "s3") {
16049
+ if (!config2.storage.s3?.bucket)
16050
+ errors3.push("storage.s3.bucket is required when storage.type is s3.");
16051
+ if (!config2.storage.s3?.prefix)
16052
+ warnings.push("storage.s3.prefix is empty; generated knowledge artifacts will be written at the bucket root.");
16053
+ if (config2.mode === "local")
16054
+ warnings.push("storage.type is s3 while mode is local; this is valid for BYO S3, but hosted wrappers should set mode to hosted.");
16055
+ }
16056
+ if (config2.storage.type === "local" && config2.storage.s3) {
16057
+ warnings.push("storage.s3 is configured but ignored while storage.type is local.");
16058
+ }
16059
+ if (config2.sources.preferred_ref !== "open-files") {
16060
+ warnings.push("sources.preferred_ref should stay open-files for durable company knowledge.");
16061
+ }
16062
+ if (!config2.sources.allowed_schemes.includes("open-files")) {
16063
+ errors3.push("sources.allowed_schemes must include open-files.");
16064
+ }
16065
+ return {
16066
+ ok: errors3.length === 0,
16067
+ errors: errors3,
16068
+ warnings
16069
+ };
16070
+ }
16071
+ function recordStorageObjects(db, objects, now = new Date) {
16072
+ const timestamp = now.toISOString();
16073
+ const statement = db.prepare(`
16074
+ INSERT INTO storage_objects (
16075
+ id, artifact_uri, kind, content_type, hash, size_bytes, metadata_json, created_at, updated_at
16076
+ )
16077
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
16078
+ ON CONFLICT(artifact_uri) DO UPDATE SET
16079
+ kind = excluded.kind,
16080
+ content_type = excluded.content_type,
16081
+ hash = excluded.hash,
16082
+ size_bytes = excluded.size_bytes,
16083
+ metadata_json = excluded.metadata_json,
16084
+ updated_at = excluded.updated_at
16085
+ `);
16086
+ const insert = db.transaction((entries) => {
16087
+ for (const entry of entries) {
16088
+ statement.run(randomUUID4(), entry.uri, entry.kind, entry.content_type ?? null, entry.hash ?? null, entry.size_bytes ?? null, JSON.stringify({
16089
+ key: entry.key,
16090
+ ...entry.metadata ?? {}
16091
+ }), timestamp, timestamp);
16092
+ }
16093
+ });
16094
+ insert(objects);
16095
+ }
16096
+
15925
16097
  // src/wiki-layout.ts
15926
16098
  function todayParts(now) {
15927
16099
  const year = String(now.getUTCFullYear());
@@ -15996,19 +16168,29 @@ async function initializeWikiLayout(store, now = new Date) {
15996
16168
  root_index_key: rootIndexKey,
15997
16169
  wiki_readme_key: wikiReadmeKey
15998
16170
  };
15999
- const writes = [
16000
- store.put({ key: schemaKey, body: agentSchemaTemplate(), content_type: "text/markdown" }),
16001
- store.put({ key: rootIndexKey, body: rootIndexTemplate(), content_type: "text/markdown" }),
16002
- store.put({ key: wikiReadmeKey, body: wikiReadmeTemplate(), content_type: "text/markdown" }),
16003
- store.put({ key: logKey, body: `${JSON.stringify(event)}
16004
- `, content_type: "application/x-ndjson" })
16171
+ const entries = [
16172
+ { key: schemaKey, body: agentSchemaTemplate(), content_type: "text/markdown" },
16173
+ { key: rootIndexKey, body: rootIndexTemplate(), content_type: "text/markdown" },
16174
+ { key: wikiReadmeKey, body: wikiReadmeTemplate(), content_type: "text/markdown" },
16175
+ { key: logKey, body: `${JSON.stringify(event)}
16176
+ `, content_type: "application/x-ndjson" }
16005
16177
  ];
16006
- await Promise.all(writes);
16178
+ const artifacts = await Promise.all(entries.map(async (entry) => {
16179
+ const result = await store.put(entry);
16180
+ return {
16181
+ key: result.key,
16182
+ uri: result.uri,
16183
+ kind: artifactKindForKey(entry.key),
16184
+ content_type: entry.content_type,
16185
+ ...hashArtifactBody(entry.body)
16186
+ };
16187
+ }));
16007
16188
  return {
16008
16189
  schema_key: schemaKey,
16009
16190
  root_index_key: rootIndexKey,
16010
16191
  wiki_readme_key: wikiReadmeKey,
16011
16192
  log_key: logKey,
16193
+ artifacts,
16012
16194
  written: [schemaKey, rootIndexKey, wikiReadmeKey, logKey]
16013
16195
  };
16014
16196
  }
@@ -16048,6 +16230,12 @@ class KnowledgeService {
16048
16230
  artifactStore() {
16049
16231
  return createArtifactStore(this.config(), this.ensureWorkspace());
16050
16232
  }
16233
+ storageContract() {
16234
+ return resolveStorageContract(this.config(), this.ensureWorkspace(), this.scope);
16235
+ }
16236
+ validateStorage() {
16237
+ return validateStorageConfig(this.config(), this.ensureWorkspace());
16238
+ }
16051
16239
  paths() {
16052
16240
  const workspace = this.ensureWorkspace();
16053
16241
  return {
@@ -16076,7 +16264,16 @@ class KnowledgeService {
16076
16264
  return getKnowledgeDbStats(workspace.knowledgeDbPath);
16077
16265
  }
16078
16266
  async initWiki() {
16079
- return initializeWikiLayout(this.artifactStore());
16267
+ const workspace = this.ensureWorkspace();
16268
+ migrateKnowledgeDb(workspace.knowledgeDbPath);
16269
+ const result = await initializeWikiLayout(this.artifactStore());
16270
+ const db = openKnowledgeDb(workspace.knowledgeDbPath);
16271
+ try {
16272
+ recordStorageObjects(db, result.artifacts);
16273
+ } finally {
16274
+ db.close();
16275
+ }
16276
+ return result;
16080
16277
  }
16081
16278
  async ingestManifest(input) {
16082
16279
  const workspace = this.ensureWorkspace();
@@ -16187,6 +16384,17 @@ function buildServer() {
16187
16384
  }, async ({ scope }) => {
16188
16385
  return jsonText(createKnowledgeService({ scope }).paths());
16189
16386
  });
16387
+ registerTool(server, "ok_storage_status", "Knowledge storage status", "Inspect local/S3 artifact storage, source ownership, and scalability contract", {
16388
+ scope: scopeField
16389
+ }, async ({ scope }) => {
16390
+ const service = createKnowledgeService({ scope });
16391
+ const validation = service.validateStorage();
16392
+ return jsonText({
16393
+ ok: validation.ok,
16394
+ ...service.storageContract(),
16395
+ validation
16396
+ });
16397
+ });
16190
16398
  registerTool(server, "ok_parse_source_ref", "Parse source reference", "Parse and validate an open-files, S3, file, or web source ref", {
16191
16399
  uri: exports_external.string().describe("Source reference URI")
16192
16400
  }, async ({ uri }) => {