@knolo/cli 3.2.1 → 4.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.
- package/README.md +49 -3
- package/bin/knolo.mjs +555 -7
- package/package.json +4 -3
- package/templates/icp-knowledge-canister/README.md +71 -0
- package/templates/icp-knowledge-canister/canisters/knolo-core-rust/Cargo.toml +13 -0
- package/templates/icp-knowledge-canister/canisters/knolo-core-rust/src/lib.rs +499 -0
- package/templates/icp-knowledge-canister/canisters/knolo-icp-canister/Cargo.lock +541 -0
- package/templates/icp-knowledge-canister/canisters/knolo-icp-canister/Cargo.toml +20 -0
- package/templates/icp-knowledge-canister/canisters/knolo-icp-canister/knolo_icp.did +29 -0
- package/templates/icp-knowledge-canister/canisters/knolo-icp-canister/src/lib.rs +605 -0
- package/templates/icp-knowledge-canister/dfx.json +25 -0
- package/templates/icp-knowledge-canister/frontend/index.html +12 -0
- package/templates/icp-knowledge-canister/frontend/package.json +27 -0
- package/templates/icp-knowledge-canister/frontend/src/App.tsx +225 -0
- package/templates/icp-knowledge-canister/frontend/src/app.css +271 -0
- package/templates/icp-knowledge-canister/frontend/src/canister.ts +119 -0
- package/templates/icp-knowledge-canister/frontend/src/canisterConfig.test.ts +34 -0
- package/templates/icp-knowledge-canister/frontend/src/canisterConfig.ts +26 -0
- package/templates/icp-knowledge-canister/frontend/src/main.tsx +5 -0
- package/templates/icp-knowledge-canister/frontend/src/vite-env.d.ts +14 -0
- package/templates/icp-knowledge-canister/frontend/tsconfig.json +18 -0
- package/templates/icp-knowledge-canister/frontend/vite.config.ts +66 -0
- package/templates/icp-knowledge-canister/knolo.config.json +15 -0
- package/templates/icp-knowledge-canister/knowledge/alpha.md +5 -0
- package/templates/icp-knowledge-canister/knowledge/guides/beta.md +5 -0
- package/templates/icp-knowledge-canister/scripts/build-sample-pack.mjs +24 -0
- package/templates/icp-knowledge-canister/scripts/query.sh +11 -0
- 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']);
|
|
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', '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,13 @@ 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
|
+
icp Scaffold and operate an ICP-native knowledge canister
|
|
75
92
|
|
|
76
93
|
Global options:
|
|
77
94
|
--debug Print stack traces for errors
|
|
@@ -85,12 +102,102 @@ function printCommandHelp(command) {
|
|
|
85
102
|
init: 'Usage: knolo init',
|
|
86
103
|
add: 'Usage: knolo add <name> <path>',
|
|
87
104
|
build: 'Usage: knolo build',
|
|
88
|
-
query: 'Usage: knolo query <question> [--pack <path>] [--k <number>] [--json]',
|
|
105
|
+
query: 'Usage: knolo query <question> [--pack <path>] [--k <number>] [--receipt <file>] [--json]',
|
|
106
|
+
inspect: 'Usage: knolo inspect <pack.knolo>',
|
|
107
|
+
migrate: 'Usage: knolo migrate <pack.knolo> --out <new-pack.knolo> [--to 4]',
|
|
108
|
+
verify: 'Usage: knolo verify <pack.knolo>',
|
|
109
|
+
explain: 'Usage: knolo explain <receipt.json> --pack <pack.knolo>',
|
|
110
|
+
diff: 'Usage: knolo diff <pack-a.knolo> <pack-b.knolo>',
|
|
89
111
|
dev: 'Usage: knolo dev',
|
|
112
|
+
icp: 'Usage: knolo icp <command> [options]',
|
|
113
|
+
'semantic:index': 'Usage: knolo semantic:index --pack <path> [--out <path>] [--model <id>] [--endpoint <url>]',
|
|
114
|
+
'semantic:inspect': 'Usage: knolo semantic:inspect --sidecar <path>',
|
|
115
|
+
'semantic:validate': 'Usage: knolo semantic:validate --pack <path> --sidecar <path> --model <id>',
|
|
90
116
|
};
|
|
91
117
|
console.log(help[command] ?? 'Unknown command.');
|
|
92
118
|
}
|
|
93
119
|
|
|
120
|
+
function printIcpHelp() {
|
|
121
|
+
console.log(`KnoLo CLI - ICP
|
|
122
|
+
|
|
123
|
+
Usage:
|
|
124
|
+
knolo icp <command> [options]
|
|
125
|
+
|
|
126
|
+
Commands:
|
|
127
|
+
init <dir> Copy the bundled ICP canister template
|
|
128
|
+
build-pack <docsDir> --out <file> Build a lexical-only .knolo pack
|
|
129
|
+
upload <packFile> --canister <name-or-id> Upload a pack with dfx canister call set_pack
|
|
130
|
+
query <question> --canister <name-or-id> Query the canister with search
|
|
131
|
+
health --canister <name-or-id> Call the canister health method
|
|
132
|
+
info --canister <name-or-id> Call the canister pack_info method
|
|
133
|
+
clear --canister <name-or-id> Clear the loaded pack (controller only)
|
|
134
|
+
|
|
135
|
+
Examples:
|
|
136
|
+
knolo icp init ./icp-knowledge-canister
|
|
137
|
+
knolo icp build-pack ./knowledge --out ./dist/knowledge.knolo
|
|
138
|
+
knolo icp upload ./dist/knowledge.knolo --canister knolo_knowledge
|
|
139
|
+
knolo icp query "alpha beta" --canister knolo_knowledge --k 5
|
|
140
|
+
knolo icp health --canister knolo_knowledge
|
|
141
|
+
knolo icp info --canister knolo_knowledge
|
|
142
|
+
knolo icp clear --canister knolo_knowledge
|
|
143
|
+
|
|
144
|
+
Environment:
|
|
145
|
+
DFX_BIN Override the dfx executable path
|
|
146
|
+
|
|
147
|
+
Run "knolo icp <command> --help" for subcommand details.`);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function printIcpCommandHelp(command) {
|
|
151
|
+
const help = {
|
|
152
|
+
init: `Usage: knolo icp init <dir>
|
|
153
|
+
|
|
154
|
+
Copy the bundled ICP knowledge canister template into <dir>.
|
|
155
|
+
|
|
156
|
+
Example:
|
|
157
|
+
knolo icp init ./icp-knowledge-canister`,
|
|
158
|
+
'build-pack': `Usage: knolo icp build-pack <docsDir> --out <file>
|
|
159
|
+
|
|
160
|
+
Build a lexical-only .knolo pack from markdown, text, or JSON docs.
|
|
161
|
+
|
|
162
|
+
Examples:
|
|
163
|
+
knolo icp build-pack ./knowledge --out ./dist/knowledge.knolo
|
|
164
|
+
knolo icp build-pack ./docs --out ./packs/support.knolo`,
|
|
165
|
+
upload: `Usage: knolo icp upload <packFile> --canister <name-or-id> [--label <text>]
|
|
166
|
+
|
|
167
|
+
Upload a .knolo pack through dfx canister call set_pack.
|
|
168
|
+
|
|
169
|
+
Examples:
|
|
170
|
+
knolo icp upload ./dist/knowledge.knolo --canister knolo_knowledge
|
|
171
|
+
knolo icp upload ./dist/knowledge.knolo --canister ux6qi-fyaaa-aaaab-qaaaq-cai --label release-2026-04-28`,
|
|
172
|
+
query: `Usage: knolo icp query <question> --canister <name-or-id> [--k <number>]
|
|
173
|
+
|
|
174
|
+
Query the canister's search method directly over dfx.
|
|
175
|
+
|
|
176
|
+
Examples:
|
|
177
|
+
knolo icp query "alpha beta" --canister knolo_knowledge
|
|
178
|
+
knolo icp query "billing escalation" --canister ux6qi-fyaaa-aaaab-qaaaq-cai --k 10`,
|
|
179
|
+
health: `Usage: knolo icp health --canister <name-or-id>
|
|
180
|
+
|
|
181
|
+
Call the canister health query method.
|
|
182
|
+
|
|
183
|
+
Example:
|
|
184
|
+
knolo icp health --canister knolo_knowledge`,
|
|
185
|
+
info: `Usage: knolo icp info --canister <name-or-id>
|
|
186
|
+
|
|
187
|
+
Call the canister pack_info query method.
|
|
188
|
+
|
|
189
|
+
Example:
|
|
190
|
+
knolo icp info --canister knolo_knowledge`,
|
|
191
|
+
clear: `Usage: knolo icp clear --canister <name-or-id>
|
|
192
|
+
|
|
193
|
+
Clear the loaded pack. Requires a controller identity.
|
|
194
|
+
|
|
195
|
+
Example:
|
|
196
|
+
knolo icp clear --canister knolo_knowledge`,
|
|
197
|
+
};
|
|
198
|
+
console.log(help[command] ?? 'Unknown ICP command.');
|
|
199
|
+
}
|
|
200
|
+
|
|
94
201
|
function parseArgv(argv) {
|
|
95
202
|
const args = [...argv];
|
|
96
203
|
const global = { debug: false };
|
|
@@ -183,6 +290,120 @@ async function cmdAdd(args) {
|
|
|
183
290
|
console.log(`✔ wrote ${CONFIG_FILE}`);
|
|
184
291
|
}
|
|
185
292
|
|
|
293
|
+
function resolveIcpTemplateDir() {
|
|
294
|
+
for (const candidate of ICP_TEMPLATE_CANDIDATES) {
|
|
295
|
+
if (existsSync(candidate) && statSync(candidate).isDirectory()) return candidate;
|
|
296
|
+
}
|
|
297
|
+
throw createError('Bundled ICP template not found. Reinstall @knolo/cli or run from the Knolo repo checkout.');
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function ensureDirectoryMissingOrEmpty(targetDir) {
|
|
301
|
+
if (!existsSync(targetDir)) return;
|
|
302
|
+
const stats = statSync(targetDir);
|
|
303
|
+
if (!stats.isDirectory()) throw createError(`Target path already exists and is not a directory: ${targetDir}`);
|
|
304
|
+
const visibleEntries = readdirSync(targetDir);
|
|
305
|
+
if (visibleEntries.length > 0) {
|
|
306
|
+
throw createError(`Target directory is not empty: ${targetDir}`);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function copyDirectoryContents(sourceDir, targetDir) {
|
|
311
|
+
mkdirSync(targetDir, { recursive: true });
|
|
312
|
+
const entries = readdirSync(sourceDir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
|
|
313
|
+
for (const entry of entries) {
|
|
314
|
+
const sourcePath = path.join(sourceDir, entry.name);
|
|
315
|
+
const targetPath = path.join(targetDir, entry.name);
|
|
316
|
+
if (entry.isSymbolicLink()) continue;
|
|
317
|
+
if (entry.isDirectory()) {
|
|
318
|
+
copyDirectoryContents(sourcePath, targetPath);
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
if (!entry.isFile()) continue;
|
|
322
|
+
mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
323
|
+
copyFileSync(sourcePath, targetPath);
|
|
324
|
+
chmodSync(targetPath, statSync(sourcePath).mode);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function parseFlagArgs(args, { booleanFlags = [] } = {}) {
|
|
329
|
+
const positional = [];
|
|
330
|
+
const flags = {};
|
|
331
|
+
const booleanSet = new Set(booleanFlags);
|
|
332
|
+
|
|
333
|
+
for (let i = 0; i < args.length; i++) {
|
|
334
|
+
const arg = args[i];
|
|
335
|
+
if (!arg.startsWith('--')) {
|
|
336
|
+
positional.push(arg);
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const name = arg.slice(2);
|
|
341
|
+
if (!name) throw createError(`Unexpected flag: ${arg}`);
|
|
342
|
+
if (booleanSet.has(name)) {
|
|
343
|
+
flags[name] = true;
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const value = args[i + 1];
|
|
348
|
+
if (value === undefined || value.startsWith('--')) {
|
|
349
|
+
throw createError(`Missing value for ${arg}`);
|
|
350
|
+
}
|
|
351
|
+
flags[name] = value;
|
|
352
|
+
i += 1;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
return { positional, flags };
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function parsePositiveInteger(value, flagName) {
|
|
359
|
+
const parsed = Number.parseInt(String(value), 10);
|
|
360
|
+
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
361
|
+
throw createError(`${flagName} must be a positive integer.`);
|
|
362
|
+
}
|
|
363
|
+
return parsed;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function renderDidNat8Vec(bytes) {
|
|
367
|
+
return bytes.length ? ` ${Array.from(bytes, (value) => `${value}`).join('; ')} ` : '';
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function withArgumentFile(prefix, contents, run) {
|
|
371
|
+
const tempDir = mkdtempSync(path.join(tmpdir(), prefix));
|
|
372
|
+
const argumentFile = path.join(tempDir, 'args.did');
|
|
373
|
+
try {
|
|
374
|
+
writeFileSync(argumentFile, contents, 'utf8');
|
|
375
|
+
return run(argumentFile);
|
|
376
|
+
} finally {
|
|
377
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function runDfx(args, { cwd = process.cwd() } = {}) {
|
|
382
|
+
const dfxBin = process.env.DFX_BIN || 'dfx';
|
|
383
|
+
const env = { ...process.env };
|
|
384
|
+
if (env.TERM === 'dumb') env.TERM = 'xterm-256color';
|
|
385
|
+
|
|
386
|
+
try {
|
|
387
|
+
const output = execFileSync(dfxBin, args, {
|
|
388
|
+
cwd,
|
|
389
|
+
encoding: 'utf8',
|
|
390
|
+
env,
|
|
391
|
+
});
|
|
392
|
+
if (output) process.stdout.write(output);
|
|
393
|
+
return output;
|
|
394
|
+
} catch (error) {
|
|
395
|
+
if (error?.status === 0) {
|
|
396
|
+
const recovered = typeof error?.stdout === 'string' ? error.stdout : '';
|
|
397
|
+
if (recovered) process.stdout.write(recovered);
|
|
398
|
+
return recovered;
|
|
399
|
+
}
|
|
400
|
+
const stderr = typeof error?.stderr === 'string' ? error.stderr.trim() : '';
|
|
401
|
+
const stdout = typeof error?.stdout === 'string' ? error.stdout.trim() : '';
|
|
402
|
+
const detail = stderr || stdout || error?.message || 'dfx command failed';
|
|
403
|
+
throw createError(detail);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
186
407
|
async function walkDir(dir) {
|
|
187
408
|
const out = [];
|
|
188
409
|
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
@@ -255,6 +476,168 @@ async function cmdBuild(core) {
|
|
|
255
476
|
await buildFromConfig(core);
|
|
256
477
|
}
|
|
257
478
|
|
|
479
|
+
async function collectNamedFiles(sourceName, inputPath) {
|
|
480
|
+
const resolved = path.resolve(process.cwd(), inputPath);
|
|
481
|
+
if (!existsSync(resolved)) throw createError(`Source path not found: ${inputPath}`);
|
|
482
|
+
|
|
483
|
+
const stats = await fs.lstat(resolved);
|
|
484
|
+
if (stats.isSymbolicLink()) return [];
|
|
485
|
+
if (stats.isFile()) {
|
|
486
|
+
if (!SUPPORTED_EXTENSIONS.has(path.extname(resolved).toLowerCase())) {
|
|
487
|
+
throw createError(`Unsupported file type for ${inputPath}. Expected .md, .txt, or .json.`);
|
|
488
|
+
}
|
|
489
|
+
return [{ sourceName, absolutePath: resolved }];
|
|
490
|
+
}
|
|
491
|
+
if (!stats.isDirectory()) throw createError(`Source path must be a file or directory: ${inputPath}`);
|
|
492
|
+
|
|
493
|
+
return (await walkDir(resolved))
|
|
494
|
+
.map((absolutePath) => ({ sourceName, absolutePath }))
|
|
495
|
+
.sort((a, b) => a.absolutePath.localeCompare(b.absolutePath));
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function buildDocsFromFiles(files, rootDir) {
|
|
499
|
+
return files.map((file) => {
|
|
500
|
+
const rel = path.relative(rootDir, file.absolutePath).replace(/\\/g, '/');
|
|
501
|
+
return {
|
|
502
|
+
id: rel,
|
|
503
|
+
heading: path.basename(rel),
|
|
504
|
+
namespace: file.sourceName,
|
|
505
|
+
text: readFileSync(file.absolutePath, 'utf8'),
|
|
506
|
+
};
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
async function cmdIcpInit(args) {
|
|
511
|
+
const [targetDir] = args;
|
|
512
|
+
if (!targetDir) throw createError('Usage: knolo icp init <dir>');
|
|
513
|
+
|
|
514
|
+
const templateDir = resolveIcpTemplateDir();
|
|
515
|
+
const resolvedTarget = path.resolve(process.cwd(), targetDir);
|
|
516
|
+
ensureDirectoryMissingOrEmpty(resolvedTarget);
|
|
517
|
+
mkdirSync(resolvedTarget, { recursive: true });
|
|
518
|
+
copyDirectoryContents(templateDir, resolvedTarget);
|
|
519
|
+
|
|
520
|
+
const relativeTarget = path.relative(process.cwd(), resolvedTarget) || '.';
|
|
521
|
+
console.log(`✔ created ${relativeTarget}`);
|
|
522
|
+
console.log('Next steps:');
|
|
523
|
+
console.log(` cd ${relativeTarget}`);
|
|
524
|
+
console.log(' dfx start --background');
|
|
525
|
+
console.log(' dfx deploy');
|
|
526
|
+
console.log(' knolo icp build-pack ./knowledge --out ./dist/knowledge.knolo');
|
|
527
|
+
console.log(' knolo icp upload ./dist/knowledge.knolo --canister knolo_knowledge');
|
|
528
|
+
console.log(' knolo icp query "alpha beta" --canister knolo_knowledge');
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
async function cmdIcpBuildPack(core, args) {
|
|
532
|
+
const { positional, flags } = parseFlagArgs(args);
|
|
533
|
+
const docsDir = positional[0];
|
|
534
|
+
if (!docsDir || !flags.out) {
|
|
535
|
+
throw createError('Usage: knolo icp build-pack <docsDir> --out <file>');
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
const resolvedInput = path.resolve(process.cwd(), docsDir);
|
|
539
|
+
const files = await collectNamedFiles('docs', docsDir);
|
|
540
|
+
if (!files.length) throw createError('No supported files found (.md, .txt, .json).');
|
|
541
|
+
|
|
542
|
+
const rootDir = statSync(resolvedInput).isDirectory() ? resolvedInput : path.dirname(resolvedInput);
|
|
543
|
+
const docs = buildDocsFromFiles(files, rootDir);
|
|
544
|
+
const bytes = await core.buildPack(docs, { format: 3 });
|
|
545
|
+
const outPath = path.resolve(process.cwd(), flags.out);
|
|
546
|
+
|
|
547
|
+
mkdirSync(path.dirname(outPath), { recursive: true });
|
|
548
|
+
writeFileSync(outPath, Buffer.from(bytes));
|
|
549
|
+
|
|
550
|
+
console.log(`✔ indexed ${files.length} files`);
|
|
551
|
+
console.log(`✔ wrote ${path.relative(process.cwd(), outPath)}`);
|
|
552
|
+
console.log('✔ lexical-only pack build completed');
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function defaultPackLabel(packPath) {
|
|
556
|
+
return path.basename(packPath, path.extname(packPath)) || 'knowledge-pack';
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
async function cmdIcpUpload(args) {
|
|
560
|
+
const { positional, flags } = parseFlagArgs(args);
|
|
561
|
+
const packFile = positional[0];
|
|
562
|
+
if (!packFile || !flags.canister) {
|
|
563
|
+
throw createError('Usage: knolo icp upload <packFile> --canister <name-or-id> [--label <text>]');
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
const packPath = path.resolve(process.cwd(), packFile);
|
|
567
|
+
if (!existsSync(packPath)) throw createError(`Pack file not found at ${path.relative(process.cwd(), packPath)}.`);
|
|
568
|
+
|
|
569
|
+
const label = flags.label || defaultPackLabel(packPath);
|
|
570
|
+
const bytes = readFileSync(packPath);
|
|
571
|
+
const didArgs = `(vec {${renderDidNat8Vec(bytes)}}, ${JSON.stringify(label)})\n`;
|
|
572
|
+
|
|
573
|
+
withArgumentFile('knolo-icp-upload-', didArgs, (argumentFile) =>
|
|
574
|
+
runDfx(
|
|
575
|
+
['canister', 'call', flags.canister, 'set_pack', '--argument-file', argumentFile, '--output', 'json'],
|
|
576
|
+
{ cwd: process.cwd() }
|
|
577
|
+
)
|
|
578
|
+
);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
async function cmdIcpQuery(args) {
|
|
582
|
+
const { positional, flags } = parseFlagArgs(args);
|
|
583
|
+
const question = positional.join(' ').trim();
|
|
584
|
+
if (!question || !flags.canister) {
|
|
585
|
+
throw createError('Usage: knolo icp query <question> --canister <name-or-id> [--k <number>]');
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
const topK = flags.k === undefined ? 5 : parsePositiveInteger(flags.k, '--k');
|
|
589
|
+
const didArgs = `(${JSON.stringify(question)}, ${topK} : nat32)\n`;
|
|
590
|
+
|
|
591
|
+
withArgumentFile('knolo-icp-query-', didArgs, (argumentFile) =>
|
|
592
|
+
runDfx(
|
|
593
|
+
['canister', 'call', flags.canister, 'search', '--query', '--argument-file', argumentFile, '--output', 'json'],
|
|
594
|
+
{ cwd: process.cwd() }
|
|
595
|
+
)
|
|
596
|
+
);
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
async function cmdIcpHealth(args) {
|
|
600
|
+
const { flags } = parseFlagArgs(args);
|
|
601
|
+
if (!flags.canister) throw createError('Usage: knolo icp health --canister <name-or-id>');
|
|
602
|
+
runDfx(['canister', 'call', flags.canister, 'health', '--query', '--output', 'json'], {
|
|
603
|
+
cwd: process.cwd(),
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
async function cmdIcpInfo(args) {
|
|
608
|
+
const { flags } = parseFlagArgs(args);
|
|
609
|
+
if (!flags.canister) throw createError('Usage: knolo icp info --canister <name-or-id>');
|
|
610
|
+
runDfx(['canister', 'call', flags.canister, 'pack_info', '--query', '--output', 'json'], {
|
|
611
|
+
cwd: process.cwd(),
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
async function cmdIcpClear(args) {
|
|
616
|
+
const { flags } = parseFlagArgs(args);
|
|
617
|
+
if (!flags.canister) throw createError('Usage: knolo icp clear --canister <name-or-id>');
|
|
618
|
+
runDfx(['canister', 'call', flags.canister, 'clear_pack', '--output', 'json'], {
|
|
619
|
+
cwd: process.cwd(),
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
async function cmdIcp(args) {
|
|
624
|
+
const [subcommand, ...rest] = args;
|
|
625
|
+
if (!subcommand) return printIcpHelp();
|
|
626
|
+
if (!ICP_SUBCOMMANDS.has(subcommand)) throw createError(`Unknown ICP command: ${subcommand}`);
|
|
627
|
+
if (rest.includes('--help') || rest.includes('-h')) return printIcpCommandHelp(subcommand);
|
|
628
|
+
|
|
629
|
+
if (subcommand === 'init') return await cmdIcpInit(rest);
|
|
630
|
+
if (subcommand === 'upload') return await cmdIcpUpload(rest);
|
|
631
|
+
if (subcommand === 'query') return await cmdIcpQuery(rest);
|
|
632
|
+
if (subcommand === 'health') return await cmdIcpHealth(rest);
|
|
633
|
+
if (subcommand === 'info') return await cmdIcpInfo(rest);
|
|
634
|
+
if (subcommand === 'clear') return await cmdIcpClear(rest);
|
|
635
|
+
if (subcommand === 'build-pack') {
|
|
636
|
+
const core = await loadCore();
|
|
637
|
+
return await cmdIcpBuildPack(core, rest);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
258
641
|
function parseQueryArgs(args) {
|
|
259
642
|
const opts = { json: false };
|
|
260
643
|
const positional = [];
|
|
@@ -263,6 +646,7 @@ function parseQueryArgs(args) {
|
|
|
263
646
|
if (arg === '--json') opts.json = true;
|
|
264
647
|
else if (arg === '--pack') opts.pack = args[++i];
|
|
265
648
|
else if (arg === '--k') opts.k = args[++i];
|
|
649
|
+
else if (arg === '--receipt') opts.receipt = args[++i];
|
|
266
650
|
else if (arg.startsWith('--')) throw createError(`Unknown flag for query: ${arg}`);
|
|
267
651
|
else positional.push(arg);
|
|
268
652
|
}
|
|
@@ -284,7 +668,12 @@ async function cmdQuery(core, args) {
|
|
|
284
668
|
const packBuffer = readFileSync(packPath);
|
|
285
669
|
const bytes = Uint8Array.from(packBuffer);
|
|
286
670
|
const kb = await mountPackFromBytes(core, bytes);
|
|
287
|
-
const
|
|
671
|
+
const receiptResult = opts.receipt && core.queryWithReceipt ? core.queryWithReceipt(kb, question, { topK }) : null;
|
|
672
|
+
if (receiptResult && opts.receipt) {
|
|
673
|
+
writeFileSync(path.resolve(process.cwd(), opts.receipt), `${JSON.stringify(receiptResult.receipt, null, 2)}\n`);
|
|
674
|
+
}
|
|
675
|
+
const rawHits = receiptResult?.hits ?? core.query(kb, question, { topK });
|
|
676
|
+
const hits = rawHits.map((hit) => ({
|
|
288
677
|
title: kb.headings?.[hit.blockId] || hit.source || `Block ${hit.blockId}`,
|
|
289
678
|
source: hit.source || kb.docIds?.[hit.blockId] || 'unknown',
|
|
290
679
|
score: Number(hit.score.toFixed(4)),
|
|
@@ -296,7 +685,7 @@ async function cmdQuery(core, args) {
|
|
|
296
685
|
hits.map((h, i) => ({ blockId: i, score: h.score, text: h.snippet, source: h.source })),
|
|
297
686
|
{ budget: 'small' }
|
|
298
687
|
);
|
|
299
|
-
console.log(JSON.stringify({ question, packPath, topK, hits, contextPatch: patch }, null, 2));
|
|
688
|
+
console.log(JSON.stringify({ question, packPath, topK, hits, receipt: receiptResult?.receipt, contextPatch: patch }, null, 2));
|
|
300
689
|
return;
|
|
301
690
|
}
|
|
302
691
|
|
|
@@ -313,6 +702,150 @@ async function cmdQuery(core, args) {
|
|
|
313
702
|
});
|
|
314
703
|
}
|
|
315
704
|
|
|
705
|
+
function parsePackCommandArgs(args, command) {
|
|
706
|
+
const positional = [];
|
|
707
|
+
const flags = {};
|
|
708
|
+
for (let i = 0; i < args.length; i++) {
|
|
709
|
+
const arg = args[i];
|
|
710
|
+
if (arg === '--out' || arg === '--to' || arg === '--pack') flags[arg.slice(2)] = args[++i];
|
|
711
|
+
else if (arg.startsWith('--')) throw createError(`Unknown flag for ${command}: ${arg}`);
|
|
712
|
+
else positional.push(arg);
|
|
713
|
+
}
|
|
714
|
+
return { positional, flags };
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
async function cmdInspect(core, args) {
|
|
718
|
+
const { positional } = parsePackCommandArgs(args, 'inspect');
|
|
719
|
+
const packPath = path.resolve(process.cwd(), positional[0] || 'dist/knowledge.knolo');
|
|
720
|
+
if (!existsSync(packPath)) throw createError(`Pack file not found at ${path.relative(process.cwd(), packPath)}.`);
|
|
721
|
+
const bytes = Uint8Array.from(readFileSync(packPath));
|
|
722
|
+
const pack = await mountPackFromBytes(core, bytes);
|
|
723
|
+
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) };
|
|
724
|
+
if (core.isPackV4?.(bytes.buffer)) result.container = core.inspectPackV4(bytes.buffer);
|
|
725
|
+
console.log(JSON.stringify(result, null, 2));
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
async function cmdVerify(core, args) {
|
|
729
|
+
const { positional } = parsePackCommandArgs(args, 'verify');
|
|
730
|
+
const packPath = path.resolve(process.cwd(), positional[0] || 'dist/knowledge.knolo');
|
|
731
|
+
if (!existsSync(packPath)) throw createError(`Pack file not found at ${path.relative(process.cwd(), packPath)}.`);
|
|
732
|
+
const bytes = Uint8Array.from(readFileSync(packPath));
|
|
733
|
+
const pack = await mountPackFromBytes(core, bytes);
|
|
734
|
+
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));
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
async function cmdMigrate(core, args) {
|
|
738
|
+
const { positional, flags } = parsePackCommandArgs(args, 'migrate');
|
|
739
|
+
const input = positional[0];
|
|
740
|
+
if (!input || !flags.out) throw createError('Usage: knolo migrate <pack.knolo> --out <new-pack.knolo> --to 4');
|
|
741
|
+
if (String(flags.to || '4') !== '4') throw createError('Only migration to format 4 is currently supported.');
|
|
742
|
+
const inputPath = path.resolve(process.cwd(), input);
|
|
743
|
+
if (!existsSync(inputPath)) throw createError(`Pack file not found at ${path.relative(process.cwd(), inputPath)}.`);
|
|
744
|
+
const bytes = Uint8Array.from(readFileSync(inputPath));
|
|
745
|
+
const pack = await mountPackFromBytes(core, bytes);
|
|
746
|
+
if (core.isPackV4?.(bytes.buffer)) throw createError('Input pack is already v4.');
|
|
747
|
+
const docs = pack.blocks.map((text, i) => ({ id: pack.docIds?.[i] ?? undefined, heading: pack.headings?.[i] ?? undefined, namespace: pack.namespaces?.[i] ?? undefined, text }));
|
|
748
|
+
const migrated = core.serializePackV4(pack, docs);
|
|
749
|
+
const outPath = path.resolve(process.cwd(), flags.out);
|
|
750
|
+
mkdirSync(path.dirname(outPath), { recursive: true });
|
|
751
|
+
writeFileSync(outPath, Buffer.from(migrated));
|
|
752
|
+
console.log(`✔ migrated ${path.relative(process.cwd(), inputPath)} to ${path.relative(process.cwd(), outPath)}`);
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
async function cmdExplain(core, args) {
|
|
756
|
+
const { positional, flags } = parsePackCommandArgs(args, 'explain');
|
|
757
|
+
const receiptPath = positional[0];
|
|
758
|
+
if (!receiptPath || !flags.out && !flags.pack) throw createError('Usage: knolo explain <receipt.json> --pack <pack.knolo>');
|
|
759
|
+
const packPath = path.resolve(process.cwd(), flags.pack);
|
|
760
|
+
const receipt = JSON.parse(readFileSync(path.resolve(process.cwd(), receiptPath), 'utf8'));
|
|
761
|
+
const pack = await mountPackFromBytes(core, Uint8Array.from(readFileSync(packPath)));
|
|
762
|
+
core.verifyReceipt(receipt, pack);
|
|
763
|
+
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));
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
async function cmdDiff(core, args) {
|
|
767
|
+
const { positional } = parsePackCommandArgs(args, 'diff');
|
|
768
|
+
if (positional.length < 2) throw createError('Usage: knolo diff <pack-a.knolo> <pack-b.knolo>');
|
|
769
|
+
const load = async (file) => { const bytes = Uint8Array.from(readFileSync(path.resolve(process.cwd(), file))); return { bytes, pack: await mountPackFromBytes(core, bytes) }; };
|
|
770
|
+
const a = await load(positional[0]); const b = await load(positional[1]);
|
|
771
|
+
const aDigest = core.packDigest(a.pack); const bDigest = core.packDigest(b.pack);
|
|
772
|
+
const aSources = new Set(a.pack.docIds?.filter(Boolean) ?? []); const bSources = new Set(b.pack.docIds?.filter(Boolean) ?? []);
|
|
773
|
+
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));
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
function parseKeyValueArgs(args) {
|
|
777
|
+
const out = {};
|
|
778
|
+
for (let i = 0; i < args.length; i++) {
|
|
779
|
+
const key = args[i];
|
|
780
|
+
if (!key.startsWith('--')) throw createError(`Unexpected argument: ${key}`);
|
|
781
|
+
out[key.slice(2)] = args[++i];
|
|
782
|
+
}
|
|
783
|
+
return out;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
async function loadOllamaProvider() {
|
|
787
|
+
const mod = await tryImport(path.resolve(__dirname, '../../semantic-ollama/dist/index.js'));
|
|
788
|
+
if (mod?.OllamaEmbeddingProvider) return mod.OllamaEmbeddingProvider;
|
|
789
|
+
const pkg = await tryImport('@knolo/semantic-ollama');
|
|
790
|
+
if (pkg?.OllamaEmbeddingProvider) return pkg.OllamaEmbeddingProvider;
|
|
791
|
+
throw createError('Could not load @knolo/semantic-ollama. Build packages/semantic-ollama first.');
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
async function cmdSemanticIndex(core, args) {
|
|
795
|
+
const flags = parseKeyValueArgs(args);
|
|
796
|
+
const packPath = path.resolve(process.cwd(), flags.pack || 'dist/knowledge.knolo');
|
|
797
|
+
const outPath = path.resolve(process.cwd(), flags.out || `${packPath}.semantic.json`);
|
|
798
|
+
const modelId = flags.model || 'qwen3-embedding:4b';
|
|
799
|
+
const endpoint = flags.endpoint || 'http://localhost:11434';
|
|
800
|
+
if (!existsSync(packPath)) throw createError(`Pack file not found at ${path.relative(process.cwd(), packPath)}.`);
|
|
801
|
+
|
|
802
|
+
const bytes = Uint8Array.from(readFileSync(packPath));
|
|
803
|
+
const pack = await mountPackFromBytes(core, bytes);
|
|
804
|
+
const OllamaEmbeddingProvider = await loadOllamaProvider();
|
|
805
|
+
const provider = new OllamaEmbeddingProvider({ modelId, endpoint });
|
|
806
|
+
const vectors = await provider.embedTexts(pack.blocks);
|
|
807
|
+
const sidecar = {
|
|
808
|
+
version: 1,
|
|
809
|
+
packFingerprint: core.createPackFingerprint(pack),
|
|
810
|
+
modelId: provider.modelId,
|
|
811
|
+
dimension: vectors[0]?.length ?? 0,
|
|
812
|
+
metric: 'cosine',
|
|
813
|
+
createdAt: new Date().toISOString(),
|
|
814
|
+
blocks: vectors.map((vector, blockId) => ({ blockId, vector: Array.from(core.normalizeVector(vector)) })),
|
|
815
|
+
};
|
|
816
|
+
writeFileSync(outPath, core.serializeSidecar(sidecar));
|
|
817
|
+
console.log(`✔ wrote ${path.relative(process.cwd(), outPath)}`);
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
async function cmdSemanticInspect(core, args) {
|
|
821
|
+
const flags = parseKeyValueArgs(args);
|
|
822
|
+
const sidecarPath = path.resolve(process.cwd(), flags.sidecar);
|
|
823
|
+
const sidecar = core.parseSidecar(readFileSync(sidecarPath, 'utf8'));
|
|
824
|
+
console.log(JSON.stringify({
|
|
825
|
+
version: sidecar.version,
|
|
826
|
+
packFingerprint: sidecar.packFingerprint,
|
|
827
|
+
modelId: sidecar.modelId,
|
|
828
|
+
dimension: sidecar.dimension,
|
|
829
|
+
metric: sidecar.metric,
|
|
830
|
+
createdAt: sidecar.createdAt,
|
|
831
|
+
blocks: sidecar.blocks.length,
|
|
832
|
+
}, null, 2));
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
async function cmdSemanticValidate(core, args) {
|
|
836
|
+
const flags = parseKeyValueArgs(args);
|
|
837
|
+
const packPath = path.resolve(process.cwd(), flags.pack || 'dist/knowledge.knolo');
|
|
838
|
+
const sidecarPath = path.resolve(process.cwd(), flags.sidecar);
|
|
839
|
+
const modelId = flags.model;
|
|
840
|
+
if (!modelId) throw createError('semantic:validate requires --model <id>.');
|
|
841
|
+
const pack = await mountPackFromBytes(core, Uint8Array.from(readFileSync(packPath)));
|
|
842
|
+
const sidecar = core.parseSidecar(readFileSync(sidecarPath, 'utf8'));
|
|
843
|
+
core.validateSidecarForPack({ sidecar, pack, modelId });
|
|
844
|
+
if (sidecar.blocks.length !== pack.blocks.length) throw createError(`Semantic block count mismatch: sidecar=${sidecar.blocks.length}, pack=${pack.blocks.length}`);
|
|
845
|
+
if (sidecar.dimension <= 0) throw createError('Semantic sidecar dimension must be > 0.');
|
|
846
|
+
console.log('✔ semantic sidecar validation passed');
|
|
847
|
+
}
|
|
848
|
+
|
|
316
849
|
async function mountPackFromBytes(core, bytes) {
|
|
317
850
|
try {
|
|
318
851
|
return await core.mountPack({ bytes });
|
|
@@ -588,19 +1121,34 @@ async function main() {
|
|
|
588
1121
|
const { global, command, commandArgs } = parseArgv(process.argv.slice(2));
|
|
589
1122
|
try {
|
|
590
1123
|
if (command === 'help') return printRootHelp();
|
|
591
|
-
|
|
592
|
-
|
|
1124
|
+
if (command === 'icp') {
|
|
1125
|
+
if (commandArgs.length === 0 || commandArgs[0] === '--help' || commandArgs[0] === '-h') {
|
|
1126
|
+
return printIcpHelp();
|
|
1127
|
+
}
|
|
1128
|
+
return await cmdIcp(commandArgs);
|
|
1129
|
+
}
|
|
593
1130
|
|
|
594
1131
|
if (SUBCOMMANDS.has(command)) {
|
|
595
1132
|
if (commandArgs.includes('--help') || commandArgs.includes('-h')) return printCommandHelp(command);
|
|
596
1133
|
if (command === 'init') return await cmdInit();
|
|
597
1134
|
if (command === 'add') return await cmdAdd(commandArgs);
|
|
1135
|
+
|
|
1136
|
+
const core = await loadCore();
|
|
598
1137
|
if (command === 'build') return await cmdBuild(core);
|
|
599
1138
|
if (command === 'query') return await cmdQuery(core, commandArgs);
|
|
1139
|
+
if (command === 'inspect') return await cmdInspect(core, commandArgs);
|
|
1140
|
+
if (command === 'migrate') return await cmdMigrate(core, commandArgs);
|
|
1141
|
+
if (command === 'verify') return await cmdVerify(core, commandArgs);
|
|
1142
|
+
if (command === 'explain') return await cmdExplain(core, commandArgs);
|
|
1143
|
+
if (command === 'diff') return await cmdDiff(core, commandArgs);
|
|
600
1144
|
if (command === 'dev') return await cmdDev(core);
|
|
1145
|
+
if (command === 'semantic:index') return await cmdSemanticIndex(core, commandArgs);
|
|
1146
|
+
if (command === 'semantic:inspect') return await cmdSemanticInspect(core, commandArgs);
|
|
1147
|
+
if (command === 'semantic:validate') return await cmdSemanticValidate(core, commandArgs);
|
|
601
1148
|
}
|
|
602
1149
|
|
|
603
1150
|
if (command.startsWith('-')) throw createError(`Unknown option: ${command}`);
|
|
1151
|
+
const core = await loadCore();
|
|
604
1152
|
return await runDirectMode(core, command, commandArgs);
|
|
605
1153
|
} catch (error) {
|
|
606
1154
|
console.error(`knolo: ${error instanceof Error ? error.message : String(error)}`);
|
package/package.json
CHANGED
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@knolo/cli",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.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
|
],
|
|
11
12
|
"scripts": {
|
|
12
13
|
"build": "node -e \"console.log('cli is executable via bin/knolo.mjs')\"",
|
|
13
14
|
"test": "npm run build --prefix ../core && node --test test/cli.test.mjs"
|
|
14
15
|
},
|
|
15
16
|
"dependencies": {
|
|
16
|
-
"@knolo/core": "^
|
|
17
|
+
"@knolo/core": "^4.0.0"
|
|
17
18
|
},
|
|
18
19
|
"private": false
|
|
19
20
|
}
|