@knolo/cli 3.2.2 → 5.0.0

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.
Files changed (28) hide show
  1. package/README.md +68 -7
  2. package/bin/knolo.mjs +514 -7
  3. package/package.json +21 -3
  4. package/templates/icp-knowledge-canister/README.md +76 -0
  5. package/templates/icp-knowledge-canister/canisters/knolo-core-rust/Cargo.toml +13 -0
  6. package/templates/icp-knowledge-canister/canisters/knolo-core-rust/src/lib.rs +1603 -0
  7. package/templates/icp-knowledge-canister/canisters/knolo-icp-canister/Cargo.lock +541 -0
  8. package/templates/icp-knowledge-canister/canisters/knolo-icp-canister/Cargo.toml +20 -0
  9. package/templates/icp-knowledge-canister/canisters/knolo-icp-canister/knolo_icp.did +29 -0
  10. package/templates/icp-knowledge-canister/canisters/knolo-icp-canister/src/lib.rs +605 -0
  11. package/templates/icp-knowledge-canister/dfx.json +25 -0
  12. package/templates/icp-knowledge-canister/frontend/index.html +12 -0
  13. package/templates/icp-knowledge-canister/frontend/package.json +27 -0
  14. package/templates/icp-knowledge-canister/frontend/src/App.tsx +225 -0
  15. package/templates/icp-knowledge-canister/frontend/src/app.css +271 -0
  16. package/templates/icp-knowledge-canister/frontend/src/canister.ts +119 -0
  17. package/templates/icp-knowledge-canister/frontend/src/canisterConfig.test.ts +34 -0
  18. package/templates/icp-knowledge-canister/frontend/src/canisterConfig.ts +26 -0
  19. package/templates/icp-knowledge-canister/frontend/src/main.tsx +5 -0
  20. package/templates/icp-knowledge-canister/frontend/src/vite-env.d.ts +14 -0
  21. package/templates/icp-knowledge-canister/frontend/tsconfig.json +18 -0
  22. package/templates/icp-knowledge-canister/frontend/vite.config.ts +66 -0
  23. package/templates/icp-knowledge-canister/knolo.config.json +15 -0
  24. package/templates/icp-knowledge-canister/knowledge/alpha.md +5 -0
  25. package/templates/icp-knowledge-canister/knowledge/guides/beta.md +5 -0
  26. package/templates/icp-knowledge-canister/scripts/build-sample-pack.mjs +24 -0
  27. package/templates/icp-knowledge-canister/scripts/query.sh +11 -0
  28. package/templates/icp-knowledge-canister/scripts/upload-pack.sh +16 -0
package/bin/knolo.mjs CHANGED
@@ -1,15 +1,21 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ import { execFileSync } from 'node:child_process';
3
4
  import {
5
+ chmodSync,
6
+ copyFileSync,
4
7
  existsSync,
5
8
  mkdirSync,
9
+ mkdtempSync,
6
10
  readFileSync,
7
11
  readdirSync,
12
+ rmSync,
8
13
  statSync,
9
14
  watch,
10
15
  writeFileSync,
11
16
  } from 'node:fs';
12
17
  import { promises as fs } from 'node:fs';
18
+ import { tmpdir } from 'node:os';
13
19
  import path from 'node:path';
14
20
  import process from 'node:process';
15
21
  import { fileURLToPath, pathToFileURL } from 'node:url';
@@ -26,8 +32,13 @@ const DEFAULT_CONFIG = {
26
32
  query: { topK: 5 },
27
33
  };
28
34
  const SUPPORTED_EXTENSIONS = new Set(['.md', '.txt', '.json']);
29
- const SKIP_DIRS = new Set(['node_modules', 'dist', '.git']);
30
- const SUBCOMMANDS = new Set(['init', 'add', 'build', 'query', 'dev', 'semantic:index', 'semantic:inspect', 'semantic:validate']);
35
+ const SKIP_DIRS = new Set(['node_modules', 'dist', '.git', '.dfx', '.cargo-target']);
36
+ const SUBCOMMANDS = new Set(['init', 'add', 'build', 'query', 'inspect', 'migrate', 'verify', 'explain', 'diff', 'dev', 'v5', 'semantic:index', 'semantic:inspect', 'semantic:validate']);
37
+ const ICP_SUBCOMMANDS = new Set(['init', 'build-pack', 'upload', 'query', 'health', 'info', 'clear']);
38
+ const ICP_TEMPLATE_CANDIDATES = [
39
+ path.resolve(__dirname, '../templates/icp-knowledge-canister'),
40
+ path.resolve(__dirname, '../../../examples/icp-knowledge-canister'),
41
+ ];
31
42
 
32
43
  function createError(message) {
33
44
  return new Error(message);
@@ -71,7 +82,14 @@ Commands:
71
82
  add <name> <path> Add or update a source entry in config
72
83
  build Build a .knolo pack from configured sources
73
84
  query <question> Query a built pack and print top hits
85
+ inspect <pack> Inspect pack format, sections, and manifest
86
+ migrate <pack> Migrate a legacy pack to v4
87
+ verify <pack> Verify pack structure and cryptographic digests
88
+ explain <receipt> Verify and explain a query receipt
89
+ diff <pack-a> <pack-b> Compare pack identities and source/chunk counts
74
90
  dev Watch config/sources and rebuild on change
91
+ v5 Inspect, health-check, and manage a V5 Knowledge Image
92
+ icp Scaffold and operate an ICP-native knowledge canister
75
93
 
76
94
  Global options:
77
95
  --debug Print stack traces for errors
@@ -85,8 +103,15 @@ function printCommandHelp(command) {
85
103
  init: 'Usage: knolo init',
86
104
  add: 'Usage: knolo add <name> <path>',
87
105
  build: 'Usage: knolo build',
88
- query: 'Usage: knolo query <question> [--pack <path>] [--k <number>] [--json]',
106
+ query: 'Usage: knolo query <question> [--pack <path>] [--k <number>] [--receipt <file>] [--json]',
107
+ inspect: 'Usage: knolo inspect <pack.knolo>',
108
+ migrate: 'Usage: knolo migrate <pack.knolo> --out <new-pack.knolo> [--to 4]',
109
+ verify: 'Usage: knolo verify <pack.knolo>',
110
+ explain: 'Usage: knolo explain <receipt.json> --pack <pack.knolo>',
111
+ diff: 'Usage: knolo diff <pack-a.knolo> <pack-b.knolo>',
89
112
  dev: 'Usage: knolo dev',
113
+ v5: 'Usage: knolo v5 <info|health|studio> <image.v5> [--index <file>] [--history <file>] [--run <file>] [--replay <file>]',
114
+ icp: 'Usage: knolo icp <command> [options]',
90
115
  'semantic:index': 'Usage: knolo semantic:index --pack <path> [--out <path>] [--model <id>] [--endpoint <url>]',
91
116
  'semantic:inspect': 'Usage: knolo semantic:inspect --sidecar <path>',
92
117
  'semantic:validate': 'Usage: knolo semantic:validate --pack <path> --sidecar <path> --model <id>',
@@ -94,6 +119,87 @@ function printCommandHelp(command) {
94
119
  console.log(help[command] ?? 'Unknown command.');
95
120
  }
96
121
 
122
+ function printIcpHelp() {
123
+ console.log(`KnoLo CLI - ICP
124
+
125
+ Usage:
126
+ knolo icp <command> [options]
127
+
128
+ Commands:
129
+ init <dir> Copy the bundled ICP canister template
130
+ build-pack <docsDir> --out <file> Build a lexical-only .knolo pack
131
+ upload <packFile> --canister <name-or-id> Upload a pack with dfx canister call set_pack
132
+ query <question> --canister <name-or-id> Query the canister with search
133
+ health --canister <name-or-id> Call the canister health method
134
+ info --canister <name-or-id> Call the canister pack_info method
135
+ clear --canister <name-or-id> Clear the loaded pack (controller only)
136
+
137
+ Examples:
138
+ knolo icp init ./icp-knowledge-canister
139
+ knolo icp build-pack ./knowledge --out ./dist/knowledge.knolo
140
+ knolo icp upload ./dist/knowledge.knolo --canister knolo_knowledge
141
+ knolo icp query "alpha beta" --canister knolo_knowledge --k 5
142
+ knolo icp health --canister knolo_knowledge
143
+ knolo icp info --canister knolo_knowledge
144
+ knolo icp clear --canister knolo_knowledge
145
+
146
+ Environment:
147
+ DFX_BIN Override the dfx executable path
148
+
149
+ Run "knolo icp <command> --help" for subcommand details.`);
150
+ }
151
+
152
+ function printIcpCommandHelp(command) {
153
+ const help = {
154
+ init: `Usage: knolo icp init <dir>
155
+
156
+ Copy the bundled ICP knowledge canister template into <dir>.
157
+
158
+ Example:
159
+ knolo icp init ./icp-knowledge-canister`,
160
+ 'build-pack': `Usage: knolo icp build-pack <docsDir> --out <file>
161
+
162
+ Build a lexical-only .knolo pack from markdown, text, or JSON docs.
163
+
164
+ Examples:
165
+ knolo icp build-pack ./knowledge --out ./dist/knowledge.knolo
166
+ knolo icp build-pack ./docs --out ./packs/support.knolo`,
167
+ upload: `Usage: knolo icp upload <packFile> --canister <name-or-id> [--label <text>]
168
+
169
+ Upload a .knolo pack through dfx canister call set_pack.
170
+
171
+ Examples:
172
+ knolo icp upload ./dist/knowledge.knolo --canister knolo_knowledge
173
+ knolo icp upload ./dist/knowledge.knolo --canister ux6qi-fyaaa-aaaab-qaaaq-cai --label release-2026-04-28`,
174
+ query: `Usage: knolo icp query <question> --canister <name-or-id> [--k <number>]
175
+
176
+ Query the canister's search method directly over dfx.
177
+
178
+ Examples:
179
+ knolo icp query "alpha beta" --canister knolo_knowledge
180
+ knolo icp query "billing escalation" --canister ux6qi-fyaaa-aaaab-qaaaq-cai --k 10`,
181
+ health: `Usage: knolo icp health --canister <name-or-id>
182
+
183
+ Call the canister health query method.
184
+
185
+ Example:
186
+ knolo icp health --canister knolo_knowledge`,
187
+ info: `Usage: knolo icp info --canister <name-or-id>
188
+
189
+ Call the canister pack_info query method.
190
+
191
+ Example:
192
+ knolo icp info --canister knolo_knowledge`,
193
+ clear: `Usage: knolo icp clear --canister <name-or-id>
194
+
195
+ Clear the loaded pack. Requires a controller identity.
196
+
197
+ Example:
198
+ knolo icp clear --canister knolo_knowledge`,
199
+ };
200
+ console.log(help[command] ?? 'Unknown ICP command.');
201
+ }
202
+
97
203
  function parseArgv(argv) {
98
204
  const args = [...argv];
99
205
  const global = { debug: false };
@@ -186,6 +292,120 @@ async function cmdAdd(args) {
186
292
  console.log(`✔ wrote ${CONFIG_FILE}`);
187
293
  }
188
294
 
295
+ function resolveIcpTemplateDir() {
296
+ for (const candidate of ICP_TEMPLATE_CANDIDATES) {
297
+ if (existsSync(candidate) && statSync(candidate).isDirectory()) return candidate;
298
+ }
299
+ throw createError('Bundled ICP template not found. Reinstall @knolo/cli or run from the Knolo repo checkout.');
300
+ }
301
+
302
+ function ensureDirectoryMissingOrEmpty(targetDir) {
303
+ if (!existsSync(targetDir)) return;
304
+ const stats = statSync(targetDir);
305
+ if (!stats.isDirectory()) throw createError(`Target path already exists and is not a directory: ${targetDir}`);
306
+ const visibleEntries = readdirSync(targetDir);
307
+ if (visibleEntries.length > 0) {
308
+ throw createError(`Target directory is not empty: ${targetDir}`);
309
+ }
310
+ }
311
+
312
+ function copyDirectoryContents(sourceDir, targetDir) {
313
+ mkdirSync(targetDir, { recursive: true });
314
+ const entries = readdirSync(sourceDir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
315
+ for (const entry of entries) {
316
+ const sourcePath = path.join(sourceDir, entry.name);
317
+ const targetPath = path.join(targetDir, entry.name);
318
+ if (entry.isSymbolicLink()) continue;
319
+ if (entry.isDirectory()) {
320
+ copyDirectoryContents(sourcePath, targetPath);
321
+ continue;
322
+ }
323
+ if (!entry.isFile()) continue;
324
+ mkdirSync(path.dirname(targetPath), { recursive: true });
325
+ copyFileSync(sourcePath, targetPath);
326
+ chmodSync(targetPath, statSync(sourcePath).mode);
327
+ }
328
+ }
329
+
330
+ function parseFlagArgs(args, { booleanFlags = [] } = {}) {
331
+ const positional = [];
332
+ const flags = {};
333
+ const booleanSet = new Set(booleanFlags);
334
+
335
+ for (let i = 0; i < args.length; i++) {
336
+ const arg = args[i];
337
+ if (!arg.startsWith('--')) {
338
+ positional.push(arg);
339
+ continue;
340
+ }
341
+
342
+ const name = arg.slice(2);
343
+ if (!name) throw createError(`Unexpected flag: ${arg}`);
344
+ if (booleanSet.has(name)) {
345
+ flags[name] = true;
346
+ continue;
347
+ }
348
+
349
+ const value = args[i + 1];
350
+ if (value === undefined || value.startsWith('--')) {
351
+ throw createError(`Missing value for ${arg}`);
352
+ }
353
+ flags[name] = value;
354
+ i += 1;
355
+ }
356
+
357
+ return { positional, flags };
358
+ }
359
+
360
+ function parsePositiveInteger(value, flagName) {
361
+ const parsed = Number.parseInt(String(value), 10);
362
+ if (!Number.isInteger(parsed) || parsed <= 0) {
363
+ throw createError(`${flagName} must be a positive integer.`);
364
+ }
365
+ return parsed;
366
+ }
367
+
368
+ function renderDidNat8Vec(bytes) {
369
+ return bytes.length ? ` ${Array.from(bytes, (value) => `${value}`).join('; ')} ` : '';
370
+ }
371
+
372
+ function withArgumentFile(prefix, contents, run) {
373
+ const tempDir = mkdtempSync(path.join(tmpdir(), prefix));
374
+ const argumentFile = path.join(tempDir, 'args.did');
375
+ try {
376
+ writeFileSync(argumentFile, contents, 'utf8');
377
+ return run(argumentFile);
378
+ } finally {
379
+ rmSync(tempDir, { recursive: true, force: true });
380
+ }
381
+ }
382
+
383
+ function runDfx(args, { cwd = process.cwd() } = {}) {
384
+ const dfxBin = process.env.DFX_BIN || 'dfx';
385
+ const env = { ...process.env };
386
+ if (env.TERM === 'dumb') env.TERM = 'xterm-256color';
387
+
388
+ try {
389
+ const output = execFileSync(dfxBin, args, {
390
+ cwd,
391
+ encoding: 'utf8',
392
+ env,
393
+ });
394
+ if (output) process.stdout.write(output);
395
+ return output;
396
+ } catch (error) {
397
+ if (error?.status === 0) {
398
+ const recovered = typeof error?.stdout === 'string' ? error.stdout : '';
399
+ if (recovered) process.stdout.write(recovered);
400
+ return recovered;
401
+ }
402
+ const stderr = typeof error?.stderr === 'string' ? error.stderr.trim() : '';
403
+ const stdout = typeof error?.stdout === 'string' ? error.stdout.trim() : '';
404
+ const detail = stderr || stdout || error?.message || 'dfx command failed';
405
+ throw createError(detail);
406
+ }
407
+ }
408
+
189
409
  async function walkDir(dir) {
190
410
  const out = [];
191
411
  const entries = await fs.readdir(dir, { withFileTypes: true });
@@ -258,6 +478,168 @@ async function cmdBuild(core) {
258
478
  await buildFromConfig(core);
259
479
  }
260
480
 
481
+ async function collectNamedFiles(sourceName, inputPath) {
482
+ const resolved = path.resolve(process.cwd(), inputPath);
483
+ if (!existsSync(resolved)) throw createError(`Source path not found: ${inputPath}`);
484
+
485
+ const stats = await fs.lstat(resolved);
486
+ if (stats.isSymbolicLink()) return [];
487
+ if (stats.isFile()) {
488
+ if (!SUPPORTED_EXTENSIONS.has(path.extname(resolved).toLowerCase())) {
489
+ throw createError(`Unsupported file type for ${inputPath}. Expected .md, .txt, or .json.`);
490
+ }
491
+ return [{ sourceName, absolutePath: resolved }];
492
+ }
493
+ if (!stats.isDirectory()) throw createError(`Source path must be a file or directory: ${inputPath}`);
494
+
495
+ return (await walkDir(resolved))
496
+ .map((absolutePath) => ({ sourceName, absolutePath }))
497
+ .sort((a, b) => a.absolutePath.localeCompare(b.absolutePath));
498
+ }
499
+
500
+ function buildDocsFromFiles(files, rootDir) {
501
+ return files.map((file) => {
502
+ const rel = path.relative(rootDir, file.absolutePath).replace(/\\/g, '/');
503
+ return {
504
+ id: rel,
505
+ heading: path.basename(rel),
506
+ namespace: file.sourceName,
507
+ text: readFileSync(file.absolutePath, 'utf8'),
508
+ };
509
+ });
510
+ }
511
+
512
+ async function cmdIcpInit(args) {
513
+ const [targetDir] = args;
514
+ if (!targetDir) throw createError('Usage: knolo icp init <dir>');
515
+
516
+ const templateDir = resolveIcpTemplateDir();
517
+ const resolvedTarget = path.resolve(process.cwd(), targetDir);
518
+ ensureDirectoryMissingOrEmpty(resolvedTarget);
519
+ mkdirSync(resolvedTarget, { recursive: true });
520
+ copyDirectoryContents(templateDir, resolvedTarget);
521
+
522
+ const relativeTarget = path.relative(process.cwd(), resolvedTarget) || '.';
523
+ console.log(`✔ created ${relativeTarget}`);
524
+ console.log('Next steps:');
525
+ console.log(` cd ${relativeTarget}`);
526
+ console.log(' dfx start --background');
527
+ console.log(' dfx deploy');
528
+ console.log(' knolo icp build-pack ./knowledge --out ./dist/knowledge.knolo');
529
+ console.log(' knolo icp upload ./dist/knowledge.knolo --canister knolo_knowledge');
530
+ console.log(' knolo icp query "alpha beta" --canister knolo_knowledge');
531
+ }
532
+
533
+ async function cmdIcpBuildPack(core, args) {
534
+ const { positional, flags } = parseFlagArgs(args);
535
+ const docsDir = positional[0];
536
+ if (!docsDir || !flags.out) {
537
+ throw createError('Usage: knolo icp build-pack <docsDir> --out <file>');
538
+ }
539
+
540
+ const resolvedInput = path.resolve(process.cwd(), docsDir);
541
+ const files = await collectNamedFiles('docs', docsDir);
542
+ if (!files.length) throw createError('No supported files found (.md, .txt, .json).');
543
+
544
+ const rootDir = statSync(resolvedInput).isDirectory() ? resolvedInput : path.dirname(resolvedInput);
545
+ const docs = buildDocsFromFiles(files, rootDir);
546
+ const bytes = await core.buildPack(docs, { format: 3 });
547
+ const outPath = path.resolve(process.cwd(), flags.out);
548
+
549
+ mkdirSync(path.dirname(outPath), { recursive: true });
550
+ writeFileSync(outPath, Buffer.from(bytes));
551
+
552
+ console.log(`✔ indexed ${files.length} files`);
553
+ console.log(`✔ wrote ${path.relative(process.cwd(), outPath)}`);
554
+ console.log('✔ lexical-only pack build completed');
555
+ }
556
+
557
+ function defaultPackLabel(packPath) {
558
+ return path.basename(packPath, path.extname(packPath)) || 'knowledge-pack';
559
+ }
560
+
561
+ async function cmdIcpUpload(args) {
562
+ const { positional, flags } = parseFlagArgs(args);
563
+ const packFile = positional[0];
564
+ if (!packFile || !flags.canister) {
565
+ throw createError('Usage: knolo icp upload <packFile> --canister <name-or-id> [--label <text>]');
566
+ }
567
+
568
+ const packPath = path.resolve(process.cwd(), packFile);
569
+ if (!existsSync(packPath)) throw createError(`Pack file not found at ${path.relative(process.cwd(), packPath)}.`);
570
+
571
+ const label = flags.label || defaultPackLabel(packPath);
572
+ const bytes = readFileSync(packPath);
573
+ const didArgs = `(vec {${renderDidNat8Vec(bytes)}}, ${JSON.stringify(label)})\n`;
574
+
575
+ withArgumentFile('knolo-icp-upload-', didArgs, (argumentFile) =>
576
+ runDfx(
577
+ ['canister', 'call', flags.canister, 'set_pack', '--argument-file', argumentFile, '--output', 'json'],
578
+ { cwd: process.cwd() }
579
+ )
580
+ );
581
+ }
582
+
583
+ async function cmdIcpQuery(args) {
584
+ const { positional, flags } = parseFlagArgs(args);
585
+ const question = positional.join(' ').trim();
586
+ if (!question || !flags.canister) {
587
+ throw createError('Usage: knolo icp query <question> --canister <name-or-id> [--k <number>]');
588
+ }
589
+
590
+ const topK = flags.k === undefined ? 5 : parsePositiveInteger(flags.k, '--k');
591
+ const didArgs = `(${JSON.stringify(question)}, ${topK} : nat32)\n`;
592
+
593
+ withArgumentFile('knolo-icp-query-', didArgs, (argumentFile) =>
594
+ runDfx(
595
+ ['canister', 'call', flags.canister, 'search', '--query', '--argument-file', argumentFile, '--output', 'json'],
596
+ { cwd: process.cwd() }
597
+ )
598
+ );
599
+ }
600
+
601
+ async function cmdIcpHealth(args) {
602
+ const { flags } = parseFlagArgs(args);
603
+ if (!flags.canister) throw createError('Usage: knolo icp health --canister <name-or-id>');
604
+ runDfx(['canister', 'call', flags.canister, 'health', '--query', '--output', 'json'], {
605
+ cwd: process.cwd(),
606
+ });
607
+ }
608
+
609
+ async function cmdIcpInfo(args) {
610
+ const { flags } = parseFlagArgs(args);
611
+ if (!flags.canister) throw createError('Usage: knolo icp info --canister <name-or-id>');
612
+ runDfx(['canister', 'call', flags.canister, 'pack_info', '--query', '--output', 'json'], {
613
+ cwd: process.cwd(),
614
+ });
615
+ }
616
+
617
+ async function cmdIcpClear(args) {
618
+ const { flags } = parseFlagArgs(args);
619
+ if (!flags.canister) throw createError('Usage: knolo icp clear --canister <name-or-id>');
620
+ runDfx(['canister', 'call', flags.canister, 'clear_pack', '--output', 'json'], {
621
+ cwd: process.cwd(),
622
+ });
623
+ }
624
+
625
+ async function cmdIcp(args) {
626
+ const [subcommand, ...rest] = args;
627
+ if (!subcommand) return printIcpHelp();
628
+ if (!ICP_SUBCOMMANDS.has(subcommand)) throw createError(`Unknown ICP command: ${subcommand}`);
629
+ if (rest.includes('--help') || rest.includes('-h')) return printIcpCommandHelp(subcommand);
630
+
631
+ if (subcommand === 'init') return await cmdIcpInit(rest);
632
+ if (subcommand === 'upload') return await cmdIcpUpload(rest);
633
+ if (subcommand === 'query') return await cmdIcpQuery(rest);
634
+ if (subcommand === 'health') return await cmdIcpHealth(rest);
635
+ if (subcommand === 'info') return await cmdIcpInfo(rest);
636
+ if (subcommand === 'clear') return await cmdIcpClear(rest);
637
+ if (subcommand === 'build-pack') {
638
+ const core = await loadCore();
639
+ return await cmdIcpBuildPack(core, rest);
640
+ }
641
+ }
642
+
261
643
  function parseQueryArgs(args) {
262
644
  const opts = { json: false };
263
645
  const positional = [];
@@ -266,6 +648,7 @@ function parseQueryArgs(args) {
266
648
  if (arg === '--json') opts.json = true;
267
649
  else if (arg === '--pack') opts.pack = args[++i];
268
650
  else if (arg === '--k') opts.k = args[++i];
651
+ else if (arg === '--receipt') opts.receipt = args[++i];
269
652
  else if (arg.startsWith('--')) throw createError(`Unknown flag for query: ${arg}`);
270
653
  else positional.push(arg);
271
654
  }
@@ -287,7 +670,12 @@ async function cmdQuery(core, args) {
287
670
  const packBuffer = readFileSync(packPath);
288
671
  const bytes = Uint8Array.from(packBuffer);
289
672
  const kb = await mountPackFromBytes(core, bytes);
290
- const hits = core.query(kb, question, { topK }).map((hit) => ({
673
+ const receiptResult = opts.receipt && core.queryWithReceipt ? core.queryWithReceipt(kb, question, { topK }) : null;
674
+ if (receiptResult && opts.receipt) {
675
+ writeFileSync(path.resolve(process.cwd(), opts.receipt), `${JSON.stringify(receiptResult.receipt, null, 2)}\n`);
676
+ }
677
+ const rawHits = receiptResult?.hits ?? core.query(kb, question, { topK });
678
+ const hits = rawHits.map((hit) => ({
291
679
  title: kb.headings?.[hit.blockId] || hit.source || `Block ${hit.blockId}`,
292
680
  source: hit.source || kb.docIds?.[hit.blockId] || 'unknown',
293
681
  score: Number(hit.score.toFixed(4)),
@@ -299,7 +687,7 @@ async function cmdQuery(core, args) {
299
687
  hits.map((h, i) => ({ blockId: i, score: h.score, text: h.snippet, source: h.source })),
300
688
  { budget: 'small' }
301
689
  );
302
- console.log(JSON.stringify({ question, packPath, topK, hits, contextPatch: patch }, null, 2));
690
+ console.log(JSON.stringify({ question, packPath, topK, hits, receipt: receiptResult?.receipt, contextPatch: patch }, null, 2));
303
691
  return;
304
692
  }
305
693
 
@@ -316,6 +704,112 @@ async function cmdQuery(core, args) {
316
704
  });
317
705
  }
318
706
 
707
+ function parsePackCommandArgs(args, command) {
708
+ const positional = [];
709
+ const flags = {};
710
+ for (let i = 0; i < args.length; i++) {
711
+ const arg = args[i];
712
+ if (arg === '--out' || arg === '--to' || arg === '--pack') flags[arg.slice(2)] = args[++i];
713
+ else if (arg.startsWith('--')) throw createError(`Unknown flag for ${command}: ${arg}`);
714
+ else positional.push(arg);
715
+ }
716
+ return { positional, flags };
717
+ }
718
+
719
+ async function cmdInspect(core, args) {
720
+ const { positional } = parsePackCommandArgs(args, 'inspect');
721
+ const packPath = path.resolve(process.cwd(), positional[0] || 'dist/knowledge.knolo');
722
+ if (!existsSync(packPath)) throw createError(`Pack file not found at ${path.relative(process.cwd(), packPath)}.`);
723
+ const bytes = Uint8Array.from(readFileSync(packPath));
724
+ const pack = await mountPackFromBytes(core, bytes);
725
+ const result = { format: pack.meta?.format || (pack.meta?.version === 4 ? 'v4' : `v${pack.meta?.version ?? 'unknown'}`), version: pack.meta?.version, stats: pack.meta?.stats, blocks: pack.blocks.length, semantic: Boolean(pack.semantic), claims: Boolean(pack.claimGraph) };
726
+ if (core.isPackV4?.(bytes.buffer)) result.container = core.inspectPackV4(bytes.buffer);
727
+ console.log(JSON.stringify(result, null, 2));
728
+ }
729
+
730
+ async function cmdVerify(core, args) {
731
+ const { positional } = parsePackCommandArgs(args, 'verify');
732
+ const packPath = path.resolve(process.cwd(), positional[0] || 'dist/knowledge.knolo');
733
+ if (!existsSync(packPath)) throw createError(`Pack file not found at ${path.relative(process.cwd(), packPath)}.`);
734
+ const bytes = Uint8Array.from(readFileSync(packPath));
735
+ const pack = await mountPackFromBytes(core, bytes);
736
+ console.log(JSON.stringify({ verified: true, format: pack.meta?.format || (pack.meta?.version === 4 ? 'v4' : `v${pack.meta?.version}`), blocks: pack.blocks.length, terms: pack.meta?.stats?.terms ?? pack.lexicon.size }, null, 2));
737
+ }
738
+
739
+ async function cmdMigrate(core, args) {
740
+ const { positional, flags } = parsePackCommandArgs(args, 'migrate');
741
+ const input = positional[0];
742
+ if (!input || !flags.out) throw createError('Usage: knolo migrate <pack.knolo> --out <new-pack.knolo> --to 4');
743
+ if (String(flags.to || '4') !== '4') throw createError('Only migration to format 4 is currently supported.');
744
+ const inputPath = path.resolve(process.cwd(), input);
745
+ if (!existsSync(inputPath)) throw createError(`Pack file not found at ${path.relative(process.cwd(), inputPath)}.`);
746
+ const bytes = Uint8Array.from(readFileSync(inputPath));
747
+ const pack = await mountPackFromBytes(core, bytes);
748
+ if (core.isPackV4?.(bytes.buffer)) throw createError('Input pack is already v4.');
749
+ const docs = pack.blocks.map((text, i) => ({ id: pack.docIds?.[i] ?? undefined, heading: pack.headings?.[i] ?? undefined, namespace: pack.namespaces?.[i] ?? undefined, text }));
750
+ const migrated = core.serializePackV4(pack, docs);
751
+ const outPath = path.resolve(process.cwd(), flags.out);
752
+ mkdirSync(path.dirname(outPath), { recursive: true });
753
+ writeFileSync(outPath, Buffer.from(migrated));
754
+ console.log(`✔ migrated ${path.relative(process.cwd(), inputPath)} to ${path.relative(process.cwd(), outPath)}`);
755
+ }
756
+
757
+ async function cmdExplain(core, args) {
758
+ const { positional, flags } = parsePackCommandArgs(args, 'explain');
759
+ const receiptPath = positional[0];
760
+ if (!receiptPath || !flags.out && !flags.pack) throw createError('Usage: knolo explain <receipt.json> --pack <pack.knolo>');
761
+ const packPath = path.resolve(process.cwd(), flags.pack);
762
+ const receipt = JSON.parse(readFileSync(path.resolve(process.cwd(), receiptPath), 'utf8'));
763
+ const pack = await mountPackFromBytes(core, Uint8Array.from(readFileSync(packPath)));
764
+ core.verifyReceipt(receipt, pack);
765
+ console.log(JSON.stringify({ verified: true, decision: receipt.decision, answerability: receipt.answerability, reasons: receipt.reasons, query: receipt.normalizedQuery, hits: receipt.hits.map((hit) => ({ blockId: hit.blockId, score: hit.score, source: hit.source, spans: hit.spans })) }, null, 2));
766
+ }
767
+
768
+ async function cmdDiff(core, args) {
769
+ const { positional } = parsePackCommandArgs(args, 'diff');
770
+ if (positional.length < 2) throw createError('Usage: knolo diff <pack-a.knolo> <pack-b.knolo>');
771
+ const load = async (file) => { const bytes = Uint8Array.from(readFileSync(path.resolve(process.cwd(), file))); return { bytes, pack: await mountPackFromBytes(core, bytes) }; };
772
+ const a = await load(positional[0]); const b = await load(positional[1]);
773
+ const aDigest = core.packDigest(a.pack); const bDigest = core.packDigest(b.pack);
774
+ const aSources = new Set(a.pack.docIds?.filter(Boolean) ?? []); const bSources = new Set(b.pack.docIds?.filter(Boolean) ?? []);
775
+ console.log(JSON.stringify({ equal: aDigest === bDigest, a: { packDigest: aDigest, blocks: a.pack.blocks.length, terms: a.pack.lexicon.size }, b: { packDigest: bDigest, blocks: b.pack.blocks.length, terms: b.pack.lexicon.size }, addedSources: [...bSources].filter((id) => !aSources.has(id)).sort(), removedSources: [...aSources].filter((id) => !bSources.has(id)).sort() }, null, 2));
776
+ }
777
+
778
+ function parseV5Args(args) {
779
+ const positional = [];
780
+ const flags = {};
781
+ for (let i = 0; i < args.length; i++) {
782
+ const arg = args[i];
783
+ if (arg === '--image' || arg === '--index' || arg === '--history' || arg === '--run' || arg === '--replay') flags[arg.slice(2)] = args[++i];
784
+ else if (arg.startsWith('--')) throw createError(`Unknown flag for v5: ${arg}`);
785
+ else positional.push(arg);
786
+ }
787
+ return { positional, flags };
788
+ }
789
+
790
+ async function cmdV5(core, args) {
791
+ const subcommand = args[0] || 'info';
792
+ if (!['info', 'health', 'studio'].includes(subcommand)) throw createError(`Unknown V5 command: ${subcommand}`);
793
+ const { positional, flags } = parseV5Args(args.slice(['info', 'health', 'studio'].includes(args[0]) ? 1 : 0));
794
+ const imagePath = path.resolve(process.cwd(), flags.image || positional[0] || 'dist/knowledge.v5');
795
+ if (!existsSync(imagePath)) throw createError(`V5 image file not found at ${path.relative(process.cwd(), imagePath)}.`);
796
+ const readOptional = (file, decode) => file ? decode(Uint8Array.from(readFileSync(path.resolve(process.cwd(), file)))) : undefined;
797
+ const input = {
798
+ image: Uint8Array.from(readFileSync(imagePath)),
799
+ ...(flags.index ? { queryIndex: readOptional(flags.index, core.deserializeKnowledgeQueryIndexV1) } : {}),
800
+ ...(flags.history ? { queryHistory: readOptional(flags.history, core.deserializeKnowledgeQueryHistoryV1) } : {}),
801
+ ...(flags.run ? { run: readOptional(flags.run, core.deserializeKnowledgeRunV1) } : {}),
802
+ ...(flags.replay ? { replayState: readOptional(flags.replay, core.deserializeKnowledgeSyncReplayStateV1) } : {}),
803
+ };
804
+ if (subcommand === 'studio') {
805
+ console.log(JSON.stringify(core.inspectKnowledgeStudioManagementV5(input), null, 2));
806
+ return;
807
+ }
808
+ const diagnostics = core.inspectKnowledgeRuntimeV5(input);
809
+ if (subcommand === 'health') console.log(JSON.stringify({ healthy: true, ...diagnostics }, null, 2));
810
+ else console.log(JSON.stringify(diagnostics, null, 2));
811
+ }
812
+
319
813
  function parseKeyValueArgs(args) {
320
814
  const out = {};
321
815
  for (let i = 0; i < args.length; i++) {
@@ -664,22 +1158,35 @@ async function main() {
664
1158
  const { global, command, commandArgs } = parseArgv(process.argv.slice(2));
665
1159
  try {
666
1160
  if (command === 'help') return printRootHelp();
667
-
668
- const core = await loadCore();
1161
+ if (command === 'icp') {
1162
+ if (commandArgs.length === 0 || commandArgs[0] === '--help' || commandArgs[0] === '-h') {
1163
+ return printIcpHelp();
1164
+ }
1165
+ return await cmdIcp(commandArgs);
1166
+ }
669
1167
 
670
1168
  if (SUBCOMMANDS.has(command)) {
671
1169
  if (commandArgs.includes('--help') || commandArgs.includes('-h')) return printCommandHelp(command);
672
1170
  if (command === 'init') return await cmdInit();
673
1171
  if (command === 'add') return await cmdAdd(commandArgs);
1172
+
1173
+ const core = await loadCore();
674
1174
  if (command === 'build') return await cmdBuild(core);
675
1175
  if (command === 'query') return await cmdQuery(core, commandArgs);
1176
+ if (command === 'inspect') return await cmdInspect(core, commandArgs);
1177
+ if (command === 'migrate') return await cmdMigrate(core, commandArgs);
1178
+ if (command === 'verify') return await cmdVerify(core, commandArgs);
1179
+ if (command === 'explain') return await cmdExplain(core, commandArgs);
1180
+ if (command === 'diff') return await cmdDiff(core, commandArgs);
676
1181
  if (command === 'dev') return await cmdDev(core);
1182
+ if (command === 'v5') return await cmdV5(core, commandArgs);
677
1183
  if (command === 'semantic:index') return await cmdSemanticIndex(core, commandArgs);
678
1184
  if (command === 'semantic:inspect') return await cmdSemanticInspect(core, commandArgs);
679
1185
  if (command === 'semantic:validate') return await cmdSemanticValidate(core, commandArgs);
680
1186
  }
681
1187
 
682
1188
  if (command.startsWith('-')) throw createError(`Unknown option: ${command}`);
1189
+ const core = await loadCore();
683
1190
  return await runDirectMode(core, command, commandArgs);
684
1191
  } catch (error) {
685
1192
  console.error(`knolo: ${error instanceof Error ? error.message : String(error)}`);
package/package.json CHANGED
@@ -1,19 +1,37 @@
1
1
  {
2
2
  "name": "@knolo/cli",
3
- "version": "3.2.2",
3
+ "version": "5.0.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "knolo": "bin/knolo.mjs"
7
7
  },
8
8
  "files": [
9
- "bin/"
9
+ "bin/",
10
+ "templates/"
10
11
  ],
12
+ "description": "CLI for building, inspecting, verifying, and operating Knolo V4 packs and V5 Knowledge Images.",
13
+ "license": "Apache-2.0",
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/HiveForensics-AI/knolo-core.git",
17
+ "directory": "packages/cli"
18
+ },
19
+ "homepage": "https://github.com/HiveForensics-AI/knolo-core#readme",
20
+ "bugs": {
21
+ "url": "https://github.com/HiveForensics-AI/knolo-core/issues"
22
+ },
23
+ "publishConfig": {
24
+ "access": "public"
25
+ },
26
+ "engines": {
27
+ "node": ">=20"
28
+ },
11
29
  "scripts": {
12
30
  "build": "node -e \"console.log('cli is executable via bin/knolo.mjs')\"",
13
31
  "test": "npm run build --prefix ../core && node --test test/cli.test.mjs"
14
32
  },
15
33
  "dependencies": {
16
- "@knolo/core": "^3.2.0"
34
+ "@knolo/core": "^5.0.0"
17
35
  },
18
36
  "private": false
19
37
  }