@knolo/cli 5.0.0 → 5.2.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 +35 -0
- package/bin/knolo.mjs +30 -3
- package/bin/registry/artifacts.mjs +143 -0
- package/bin/registry/commands.mjs +392 -0
- package/bin/registry/config.mjs +46 -0
- package/bin/registry/credentials.mjs +93 -0
- package/bin/registry/errors.mjs +28 -0
- package/bin/registry/http.mjs +58 -0
- package/bin/registry/lockfile.mjs +78 -0
- package/bin/registry/manifest.mjs +41 -0
- package/bin/registry/specs.mjs +20 -0
- package/package.json +4 -3
- package/templates/icp-knowledge-canister/canisters/knolo-core-rust/Cargo.toml +1 -1
- package/templates/icp-knowledge-canister/canisters/knolo-core-rust/src/lib.rs +136 -0
- package/templates/icp-knowledge-canister/canisters/knolo-icp-canister/Cargo.lock +2 -2
- package/templates/icp-knowledge-canister/canisters/knolo-icp-canister/Cargo.toml +2 -2
package/README.md
CHANGED
|
@@ -25,6 +25,41 @@ Or use via npx:
|
|
|
25
25
|
npx knolo build
|
|
26
26
|
```
|
|
27
27
|
|
|
28
|
+
## Hub Registry Discovery (V5.2)
|
|
29
|
+
|
|
30
|
+
The CLI can search the public Knolo Hub and inspect a published pack without
|
|
31
|
+
changing the local pack workflow:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
knolo search "refund policy"
|
|
35
|
+
knolo info acme/refund-policy
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
The production registry is `https://hub.knolo.dev`. Use `--registry` for a
|
|
39
|
+
one-off override, or set `KNOLO_HUB_URL` for the current environment:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
knolo search docs --registry https://hub.knolo.dev
|
|
43
|
+
KNOLO_HUB_URL=http://localhost:3000 knolo search docs
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
An explicit `--registry` value takes precedence over `KNOLO_HUB_URL`. In
|
|
47
|
+
development mode (`NODE_ENV=development`), the default is
|
|
48
|
+
`http://localhost:3000`; otherwise the default is `https://hub.knolo.dev`.
|
|
49
|
+
|
|
50
|
+
The current Hub surface is read-only discovery and verified installation. Local
|
|
51
|
+
`knolo add <name> <path>` continues to update a project’s local source
|
|
52
|
+
configuration. A Hub pack is installed only after its manifest, Blob bytes,
|
|
53
|
+
digest, size, and local Knowledge Image structure have all been verified:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
knolo add acme/refund-policy@1.2.0
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Successful installs are cached by SHA-256 and recorded in
|
|
60
|
+
`knolo.lock.json`. A yanked version or an existing conflicting pin requires
|
|
61
|
+
`--force`; digest and artifact validation cannot be bypassed.
|
|
62
|
+
|
|
28
63
|
---
|
|
29
64
|
|
|
30
65
|
## 🚀 Commands
|
package/bin/knolo.mjs
CHANGED
|
@@ -20,6 +20,7 @@ import path from 'node:path';
|
|
|
20
20
|
import process from 'node:process';
|
|
21
21
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
22
22
|
import { createRequire } from 'node:module';
|
|
23
|
+
import { isHubAddInvocation, runHubAdd, runHubInfo, runHubLogin, runHubLogout, runHubPublishStub, runHubSearch, runHubWhoami } from './registry/commands.mjs';
|
|
23
24
|
|
|
24
25
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
25
26
|
const require = createRequire(import.meta.url);
|
|
@@ -33,7 +34,7 @@ const DEFAULT_CONFIG = {
|
|
|
33
34
|
};
|
|
34
35
|
const SUPPORTED_EXTENSIONS = new Set(['.md', '.txt', '.json']);
|
|
35
36
|
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 SUBCOMMANDS = new Set(['init', 'add', 'build', 'query', 'inspect', 'migrate', 'verify', 'explain', 'diff', 'dev', 'v5', 'search', 'info', 'login', 'whoami', 'logout', 'publish', 'semantic:index', 'semantic:inspect', 'semantic:validate']);
|
|
37
38
|
const ICP_SUBCOMMANDS = new Set(['init', 'build-pack', 'upload', 'query', 'health', 'info', 'clear']);
|
|
38
39
|
const ICP_TEMPLATE_CANDIDATES = [
|
|
39
40
|
path.resolve(__dirname, '../templates/icp-knowledge-canister'),
|
|
@@ -79,7 +80,13 @@ Usage:
|
|
|
79
80
|
|
|
80
81
|
Commands:
|
|
81
82
|
init Initialize knolo.config.json and starter docs
|
|
82
|
-
add <name> <path> Add or update a source entry
|
|
83
|
+
add <name> <path> Add or update a local source entry, or install a Hub pack
|
|
84
|
+
search <query> Search the Knolo Hub registry
|
|
85
|
+
info <publisher>/<slug> Show a Hub registry pack
|
|
86
|
+
login Store a Hub token locally
|
|
87
|
+
whoami Show the stored Hub token prefix
|
|
88
|
+
logout Remove the stored Hub token
|
|
89
|
+
publish Publish support is waiting for Hub write APIs
|
|
83
90
|
build Build a .knolo pack from configured sources
|
|
84
91
|
query <question> Query a built pack and print top hits
|
|
85
92
|
inspect <pack> Inspect pack format, sections, and manifest
|
|
@@ -101,7 +108,13 @@ Run "knolo <command> --help" for command details.`);
|
|
|
101
108
|
function printCommandHelp(command) {
|
|
102
109
|
const help = {
|
|
103
110
|
init: 'Usage: knolo init',
|
|
104
|
-
add: 'Usage: knolo add <name> <path>',
|
|
111
|
+
add: 'Usage: knolo add <name> <path> | <publisher>/<slug>[@<version>] [--out <path>] [--force] [--json] [--registry <url>]',
|
|
112
|
+
search: 'Usage: knolo search <query> [--format V4|V5] [--license <id>] [--official] [--agents] [--json] [--registry <url>]',
|
|
113
|
+
info: 'Usage: knolo info <publisher>/<slug> [--json] [--registry <url>]',
|
|
114
|
+
login: 'Usage: knolo login [--token kno_…] [--stdin] [--registry <url>]',
|
|
115
|
+
whoami: 'Usage: knolo whoami',
|
|
116
|
+
logout: 'Usage: knolo logout',
|
|
117
|
+
publish: 'Usage: knolo publish <pack.knolo> (not available until Hub write APIs support CLI tokens)',
|
|
105
118
|
build: 'Usage: knolo build',
|
|
106
119
|
query: 'Usage: knolo query <question> [--pack <path>] [--k <number>] [--receipt <file>] [--json]',
|
|
107
120
|
inspect: 'Usage: knolo inspect <pack.knolo>',
|
|
@@ -272,6 +285,14 @@ async function cmdInit() {
|
|
|
272
285
|
}
|
|
273
286
|
|
|
274
287
|
async function cmdAdd(args) {
|
|
288
|
+
if (isHubAddInvocation(args)) {
|
|
289
|
+
const core = await loadCore();
|
|
290
|
+
return await runHubAdd(args, { core });
|
|
291
|
+
}
|
|
292
|
+
return await cmdAddLocal(args);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
async function cmdAddLocal(args) {
|
|
275
296
|
const [name, sourcePath] = args;
|
|
276
297
|
if (!name || !sourcePath) throw createError('Usage: knolo add <name> <path>');
|
|
277
298
|
|
|
@@ -1169,6 +1190,12 @@ async function main() {
|
|
|
1169
1190
|
if (commandArgs.includes('--help') || commandArgs.includes('-h')) return printCommandHelp(command);
|
|
1170
1191
|
if (command === 'init') return await cmdInit();
|
|
1171
1192
|
if (command === 'add') return await cmdAdd(commandArgs);
|
|
1193
|
+
if (command === 'search') return await runHubSearch(commandArgs);
|
|
1194
|
+
if (command === 'info') return await runHubInfo(commandArgs);
|
|
1195
|
+
if (command === 'login') return await runHubLogin(commandArgs);
|
|
1196
|
+
if (command === 'whoami') return await runHubWhoami(commandArgs);
|
|
1197
|
+
if (command === 'logout') return await runHubLogout(commandArgs);
|
|
1198
|
+
if (command === 'publish') return runHubPublishStub(commandArgs);
|
|
1172
1199
|
|
|
1173
1200
|
const core = await loadCore();
|
|
1174
1201
|
if (command === 'build') return await cmdBuild(core);
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { createWriteStream } from 'node:fs';
|
|
3
|
+
import { chmod, mkdir, readFile, rename, unlink } from 'node:fs/promises';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { Readable } from 'node:stream';
|
|
6
|
+
|
|
7
|
+
export const MAX_ARTIFACT_BYTES = 250 * 1024 * 1024;
|
|
8
|
+
|
|
9
|
+
export async function downloadBlobToTemp(url, {
|
|
10
|
+
tempDir,
|
|
11
|
+
expectedSize = 0,
|
|
12
|
+
fetchImpl = globalThis.fetch,
|
|
13
|
+
} = {}) {
|
|
14
|
+
const blobUrl = new URL(url);
|
|
15
|
+
if (blobUrl.protocol !== 'https:') throw new Error('Blob URL must use HTTPS.');
|
|
16
|
+
if (typeof fetchImpl !== 'function') throw new Error('This Node runtime does not provide fetch.');
|
|
17
|
+
if (!Number.isSafeInteger(expectedSize) || expectedSize < 0) throw new Error('Manifest sizeBytes is invalid.');
|
|
18
|
+
if (expectedSize > MAX_ARTIFACT_BYTES) throw new Error('Artifact exceeds Hub’s 250 MB limit.');
|
|
19
|
+
|
|
20
|
+
let response;
|
|
21
|
+
try {
|
|
22
|
+
response = await fetchImpl(blobUrl, { method: 'GET', redirect: 'follow' });
|
|
23
|
+
} catch (error) {
|
|
24
|
+
throw new Error(`Could not download artifact from ${blobUrl.origin}.`, { cause: error });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (!response.ok) {
|
|
28
|
+
const detail = await safeResponseText(response);
|
|
29
|
+
throw new Error(`Artifact download failed with HTTP ${response.status}${detail ? `: ${detail}` : '.'}`);
|
|
30
|
+
}
|
|
31
|
+
if (response.url) {
|
|
32
|
+
const finalUrl = new URL(response.url);
|
|
33
|
+
if (finalUrl.protocol !== 'https:') throw new Error('Blob redirect must remain HTTPS.');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const contentLength = readContentLength(response);
|
|
37
|
+
if (contentLength !== undefined && contentLength > MAX_ARTIFACT_BYTES) {
|
|
38
|
+
throw new Error('Artifact exceeds Hub’s 250 MB limit.');
|
|
39
|
+
}
|
|
40
|
+
if (expectedSize > 0 && contentLength !== undefined && contentLength !== expectedSize) {
|
|
41
|
+
throw new Error(`Artifact Content-Length ${contentLength} does not match manifest sizeBytes ${expectedSize}.`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
await mkdir(tempDir, { recursive: true });
|
|
45
|
+
const tempPath = path.join(tempDir, `.download-${randomUUID()}.tmp`);
|
|
46
|
+
const output = createWriteStream(tempPath, { flags: 'wx', mode: 0o600 });
|
|
47
|
+
const hash = createHash('sha256');
|
|
48
|
+
let sizeBytes = 0;
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
if (response.body && typeof response.body.getReader === 'function') {
|
|
52
|
+
for await (const chunk of Readable.fromWeb(response.body)) {
|
|
53
|
+
const bytes = Buffer.from(chunk);
|
|
54
|
+
sizeBytes += bytes.length;
|
|
55
|
+
if (sizeBytes > MAX_ARTIFACT_BYTES) throw new Error('Artifact exceeds Hub’s 250 MB limit.');
|
|
56
|
+
hash.update(bytes);
|
|
57
|
+
await writeChunk(output, bytes);
|
|
58
|
+
}
|
|
59
|
+
} else if (typeof response.arrayBuffer === 'function') {
|
|
60
|
+
const bytes = Buffer.from(await response.arrayBuffer());
|
|
61
|
+
sizeBytes = bytes.length;
|
|
62
|
+
if (sizeBytes > MAX_ARTIFACT_BYTES) throw new Error('Artifact exceeds Hub’s 250 MB limit.');
|
|
63
|
+
hash.update(bytes);
|
|
64
|
+
await writeChunk(output, bytes);
|
|
65
|
+
} else {
|
|
66
|
+
throw new Error('Artifact response did not provide a readable body.');
|
|
67
|
+
}
|
|
68
|
+
await endStream(output);
|
|
69
|
+
} catch (error) {
|
|
70
|
+
output.destroy();
|
|
71
|
+
await unlink(tempPath).catch(() => {});
|
|
72
|
+
throw error;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (expectedSize > 0 && sizeBytes !== expectedSize) {
|
|
76
|
+
await unlink(tempPath).catch(() => {});
|
|
77
|
+
throw new Error(`Artifact body length ${sizeBytes} does not match manifest sizeBytes ${expectedSize}.`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
await chmod(tempPath, 0o600);
|
|
81
|
+
return { tempPath, sizeBytes, sha256: hash.digest('hex'), contentLength };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export async function readArtifact(tempPath) {
|
|
85
|
+
return new Uint8Array(await readFile(tempPath));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export async function installCachedArtifact(tempPath, cachePath) {
|
|
89
|
+
await mkdir(path.dirname(cachePath), { recursive: true });
|
|
90
|
+
await chmod(tempPath, 0o644);
|
|
91
|
+
await rename(tempPath, cachePath);
|
|
92
|
+
await chmod(cachePath, 0o644);
|
|
93
|
+
return cachePath;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function writeChunk(stream, chunk) {
|
|
97
|
+
if (stream.write(chunk)) return;
|
|
98
|
+
await new Promise((resolve, reject) => {
|
|
99
|
+
const onDrain = () => {
|
|
100
|
+
stream.off('error', onError);
|
|
101
|
+
resolve();
|
|
102
|
+
};
|
|
103
|
+
const onError = (error) => {
|
|
104
|
+
stream.off('drain', onDrain);
|
|
105
|
+
reject(error);
|
|
106
|
+
};
|
|
107
|
+
stream.once('drain', onDrain);
|
|
108
|
+
stream.once('error', onError);
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function endStream(stream) {
|
|
113
|
+
await new Promise((resolve, reject) => {
|
|
114
|
+
const onClose = () => {
|
|
115
|
+
stream.off('error', onError);
|
|
116
|
+
resolve();
|
|
117
|
+
};
|
|
118
|
+
const onError = (error) => {
|
|
119
|
+
stream.off('close', onClose);
|
|
120
|
+
reject(error);
|
|
121
|
+
};
|
|
122
|
+
stream.once('close', onClose);
|
|
123
|
+
stream.once('error', onError);
|
|
124
|
+
stream.end();
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function readContentLength(response) {
|
|
129
|
+
const raw = response.headers?.get?.('content-length') ?? response.headers?.['content-length'] ?? response.headers?.['Content-Length'];
|
|
130
|
+
if (raw === undefined || raw === null || raw === '') return undefined;
|
|
131
|
+
const value = Number(raw);
|
|
132
|
+
if (!Number.isSafeInteger(value) || value < 0) throw new Error('Artifact Content-Length is invalid.');
|
|
133
|
+
return value;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async function safeResponseText(response) {
|
|
137
|
+
try {
|
|
138
|
+
const text = await response.text();
|
|
139
|
+
return String(text || '').replace(/\s+/g, ' ').slice(0, 240);
|
|
140
|
+
} catch {
|
|
141
|
+
return '';
|
|
142
|
+
}
|
|
143
|
+
}
|
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
import { copyFile, mkdir, rename, unlink } from 'node:fs/promises';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { registryApiUrl, resolveRegistryUrl } from './config.mjs';
|
|
5
|
+
import { downloadBlobToTemp, installCachedArtifact, readArtifact } from './artifacts.mjs';
|
|
6
|
+
import { RegistryError } from './errors.mjs';
|
|
7
|
+
import { getJson } from './http.mjs';
|
|
8
|
+
import { cachePath, upsertLockfile, validateLockfileUpdate } from './lockfile.mjs';
|
|
9
|
+
import { validatePackManifest } from './manifest.mjs';
|
|
10
|
+
import { parsePackName, parsePackSpec } from './specs.mjs';
|
|
11
|
+
import { loadCredentials, promptForToken, removeCredentials, saveCredentials, validateToken } from './credentials.mjs';
|
|
12
|
+
|
|
13
|
+
const PACK_SHA256_PATTERN = /^[0-9a-f]{64}$/;
|
|
14
|
+
|
|
15
|
+
export async function runHubSearch(args, { env = process.env, fetchImpl = globalThis.fetch, print = console.log } = {}) {
|
|
16
|
+
const { query, flags } = parseSearchArgs(args);
|
|
17
|
+
const registry = resolveRegistryUrl({ value: flags.registry, env });
|
|
18
|
+
const url = registryApiUrl(registry, ['packs'], {
|
|
19
|
+
q: query,
|
|
20
|
+
format: flags.format,
|
|
21
|
+
license: flags.license,
|
|
22
|
+
official: flags.official ? 'true' : undefined,
|
|
23
|
+
agents: flags.agents ? 'true' : undefined,
|
|
24
|
+
});
|
|
25
|
+
const body = await getJson(url, { fetchImpl });
|
|
26
|
+
validatePacksResponse(body);
|
|
27
|
+
|
|
28
|
+
if (flags.json) print(JSON.stringify(body, null, 2));
|
|
29
|
+
else print(renderSearchTable(body.packs));
|
|
30
|
+
|
|
31
|
+
return { registry, url: String(url), body };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function runHubInfo(args, { env = process.env, fetchImpl = globalThis.fetch, print = console.log } = {}) {
|
|
35
|
+
const { positional, flags } = parseInfoArgs(args);
|
|
36
|
+
const pack = parsePackName(positional[0]);
|
|
37
|
+
const registry = resolveRegistryUrl({ value: flags.registry, env });
|
|
38
|
+
const url = registryApiUrl(registry, ['packs', pack.publisher, pack.slug]);
|
|
39
|
+
const body = await getJson(url, { fetchImpl });
|
|
40
|
+
validatePackInfoResponse(body);
|
|
41
|
+
|
|
42
|
+
if (flags.json) print(JSON.stringify(body, null, 2));
|
|
43
|
+
else print(renderPackInfo(body));
|
|
44
|
+
|
|
45
|
+
return { registry, url: String(url), body };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function runHubAdd(args, {
|
|
49
|
+
env = process.env,
|
|
50
|
+
fetchImpl = globalThis.fetch,
|
|
51
|
+
core,
|
|
52
|
+
cwd = process.cwd(),
|
|
53
|
+
homeDir,
|
|
54
|
+
print = console.log,
|
|
55
|
+
warn = console.error,
|
|
56
|
+
} = {}) {
|
|
57
|
+
const { spec, flags } = parseAddArgs(args);
|
|
58
|
+
if (!core) throw new Error('Could not load @knolo/core for Hub artifact validation.');
|
|
59
|
+
const registry = resolveRegistryUrl({ value: flags.registry, env });
|
|
60
|
+
const manifestUrl = registryApiUrl(registry, ['packs', spec.publisher, spec.slug, spec.version]);
|
|
61
|
+
|
|
62
|
+
let manifest;
|
|
63
|
+
let yanked = false;
|
|
64
|
+
try {
|
|
65
|
+
const body = await getJson(manifestUrl, { fetchImpl });
|
|
66
|
+
manifest = validatePackManifest(body, { expectedName: spec.name, requestedVersion: spec.version });
|
|
67
|
+
} catch (error) {
|
|
68
|
+
if (error instanceof RegistryError && (error.status === 404 || error.code === 'not_found')) {
|
|
69
|
+
throw new Error('pack version not found');
|
|
70
|
+
}
|
|
71
|
+
if (error instanceof RegistryError && (error.status === 410 || error.code === 'yanked')) {
|
|
72
|
+
manifest = validatePackManifest(error.body, { expectedName: spec.name, requestedVersion: spec.version });
|
|
73
|
+
yanked = true;
|
|
74
|
+
} else {
|
|
75
|
+
throw error;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (manifest.yanked) yanked = true;
|
|
80
|
+
if (yanked && !flags.force) throw new Error('version yanked; use --force to download yanked bytes');
|
|
81
|
+
if (yanked) warn(`warning: downloading yanked version ${manifest.name}@${manifest.version}`);
|
|
82
|
+
if (!manifest.url) throw new Error('artifact bytes are not stored yet');
|
|
83
|
+
if (!PACK_SHA256_PATTERN.test(manifest.sha256)) throw new Error('manifest sha256 is invalid');
|
|
84
|
+
|
|
85
|
+
const artifactHome = homeDir || homedir();
|
|
86
|
+
await validateLockfileUpdate({ cwd, registry, manifest, force: flags.force });
|
|
87
|
+
const finalCachePath = cachePath(artifactHome, manifest.sha256);
|
|
88
|
+
const tempDir = path.dirname(finalCachePath);
|
|
89
|
+
const download = await downloadBlobToTemp(manifest.url, {
|
|
90
|
+
tempDir,
|
|
91
|
+
expectedSize: manifest.sizeBytes,
|
|
92
|
+
fetchImpl,
|
|
93
|
+
});
|
|
94
|
+
let installed = false;
|
|
95
|
+
try {
|
|
96
|
+
if (download.sha256 !== manifest.sha256) {
|
|
97
|
+
throw new Error(`artifact sha256 mismatch: expected ${manifest.sha256}, got ${download.sha256}`);
|
|
98
|
+
}
|
|
99
|
+
const bytes = await readArtifact(download.tempPath);
|
|
100
|
+
const validation = await validateKnowledgeImage(core, bytes);
|
|
101
|
+
if (manifest.format && String(manifest.format).toUpperCase() !== validation.format) {
|
|
102
|
+
throw new Error(`artifact format mismatch: manifest=${manifest.format}, bytes=${validation.format}`);
|
|
103
|
+
}
|
|
104
|
+
if (manifest.stateRoot !== undefined && manifest.stateRoot !== validation.stateRoot) {
|
|
105
|
+
throw new Error(`artifact stateRoot mismatch: expected ${manifest.stateRoot}, got ${validation.stateRoot || 'none'}`);
|
|
106
|
+
}
|
|
107
|
+
const cache = await installCachedArtifact(download.tempPath, finalCachePath);
|
|
108
|
+
installed = true;
|
|
109
|
+
const lock = await upsertLockfile({ cwd, registry, manifest, force: flags.force });
|
|
110
|
+
let outputPath = cache;
|
|
111
|
+
if (flags.out) {
|
|
112
|
+
outputPath = await copyArtifact(cache, flags.out, cwd);
|
|
113
|
+
}
|
|
114
|
+
const result = { ...manifest, path: outputPath, lockfile: lock.path };
|
|
115
|
+
if (flags.json) print(JSON.stringify(result, null, 2));
|
|
116
|
+
else {
|
|
117
|
+
print(`added ${manifest.name}@${manifest.version}`);
|
|
118
|
+
print(`sha256 ${manifest.sha256}`);
|
|
119
|
+
print(`url ${manifest.url}`);
|
|
120
|
+
print(`path ${outputPath}`);
|
|
121
|
+
}
|
|
122
|
+
return result;
|
|
123
|
+
} finally {
|
|
124
|
+
if (!installed) await unlinkIfPresent(download.tempPath);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export async function runHubLogin(args, {
|
|
129
|
+
env = process.env,
|
|
130
|
+
homeDir,
|
|
131
|
+
input = process.stdin,
|
|
132
|
+
output = process.stderr,
|
|
133
|
+
print = console.log,
|
|
134
|
+
} = {}) {
|
|
135
|
+
const { flags } = parseLoginArgs(args);
|
|
136
|
+
const registry = resolveRegistryUrl({ value: flags.registry, env });
|
|
137
|
+
print(`Create a token at ${registry}/dashboard/tokens (Sign in with GitHub).`);
|
|
138
|
+
const token = (flags.token ?? await promptForToken({ input, output, forceStdin: flags.stdin })).trim();
|
|
139
|
+
validateToken(token);
|
|
140
|
+
const saved = await saveCredentials({ registry, token, env, homeDir });
|
|
141
|
+
print(`logged in to ${saved.registry}`);
|
|
142
|
+
print(`token ${saved.prefix}`);
|
|
143
|
+
return saved;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export async function runHubWhoami(args, { env = process.env, homeDir, print = console.log } = {}) {
|
|
147
|
+
if (args.length) throw new Error('Usage: knolo whoami');
|
|
148
|
+
const credentials = await loadCredentials({ env, homeDir });
|
|
149
|
+
print(`registry ${credentials.registry}`);
|
|
150
|
+
print(`token ${credentials.prefix}`);
|
|
151
|
+
return { registry: credentials.registry, prefix: credentials.prefix };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export async function runHubLogout(args, { env = process.env, homeDir, print = console.log } = {}) {
|
|
155
|
+
if (args.length) throw new Error('Usage: knolo logout');
|
|
156
|
+
const result = await removeCredentials({ env, homeDir });
|
|
157
|
+
print(result.removed ? 'logged out' : 'not logged in');
|
|
158
|
+
return result;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function runHubPublishStub() {
|
|
162
|
+
throw new Error('Hub write APIs do not accept CLI tokens yet');
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function parseAddArgs(args) {
|
|
166
|
+
const positional = [];
|
|
167
|
+
const flags = {};
|
|
168
|
+
for (let i = 0; i < args.length; i++) {
|
|
169
|
+
const arg = args[i];
|
|
170
|
+
if (arg === '--force' || arg === '--json') {
|
|
171
|
+
flags[arg.slice(2)] = true;
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
if (['--out', '--registry'].includes(arg)) {
|
|
175
|
+
const value = args[++i];
|
|
176
|
+
if (value === undefined || value.startsWith('--')) throw new Error(`Missing value for ${arg}`);
|
|
177
|
+
flags[arg.slice(2)] = value;
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
if (arg.startsWith('--')) throw new Error(`Unknown flag for add: ${arg}`);
|
|
181
|
+
positional.push(arg);
|
|
182
|
+
}
|
|
183
|
+
if (positional.length !== 1) throw new Error('Usage: knolo add <publisher>/<slug>[@<version>] [--out <path>] [--force] [--json] [--registry <url>]');
|
|
184
|
+
return { spec: parsePackSpec(positional[0]), flags };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function parseLoginArgs(args) {
|
|
188
|
+
const flags = {};
|
|
189
|
+
for (let i = 0; i < args.length; i++) {
|
|
190
|
+
const arg = args[i];
|
|
191
|
+
if (arg === '--stdin') {
|
|
192
|
+
flags.stdin = true;
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
if (arg === '--token' || arg === '--registry') {
|
|
196
|
+
const value = args[++i];
|
|
197
|
+
if (value === undefined || value.startsWith('--')) throw new Error(`Missing value for ${arg}`);
|
|
198
|
+
flags[arg.slice(2)] = value;
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
if (arg.startsWith('--')) throw new Error(`Unknown flag for login: ${arg}`);
|
|
202
|
+
throw new Error('Usage: knolo login [--token kno_…] [--stdin] [--registry <url>]');
|
|
203
|
+
}
|
|
204
|
+
return { flags };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function isHubAddInvocation(args) {
|
|
208
|
+
if (!args[0] || args[0].startsWith('--')) return false;
|
|
209
|
+
const positional = [];
|
|
210
|
+
for (let i = 0; i < args.length; i++) {
|
|
211
|
+
const arg = args[i];
|
|
212
|
+
if (['--out', '--registry'].includes(arg)) {
|
|
213
|
+
i += 1;
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
if (arg === '--force' || arg === '--json') continue;
|
|
217
|
+
if (arg.startsWith('--')) return false;
|
|
218
|
+
positional.push(arg);
|
|
219
|
+
}
|
|
220
|
+
if (positional.length !== 1) return false;
|
|
221
|
+
try {
|
|
222
|
+
parsePackSpec(positional[0]);
|
|
223
|
+
return true;
|
|
224
|
+
} catch {
|
|
225
|
+
return false;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export function parseSearchArgs(args) {
|
|
230
|
+
const positional = [];
|
|
231
|
+
const flags = {};
|
|
232
|
+
for (let i = 0; i < args.length; i++) {
|
|
233
|
+
const arg = args[i];
|
|
234
|
+
if (arg === '--json' || arg === '--official' || arg === '--agents') {
|
|
235
|
+
flags[arg.slice(2)] = true;
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
if (['--format', '--license', '--registry'].includes(arg)) {
|
|
239
|
+
const value = args[++i];
|
|
240
|
+
if (value === undefined || value.startsWith('--')) throw new Error(`Missing value for ${arg}`);
|
|
241
|
+
flags[arg.slice(2)] = value;
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
if (arg.startsWith('--')) throw new Error(`Unknown flag for search: ${arg}`);
|
|
245
|
+
positional.push(arg);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const query = positional.join(' ').trim();
|
|
249
|
+
if (!query) throw new Error('Usage: knolo search <query> [--format V4|V5] [--license <id>] [--official] [--agents] [--json] [--registry <url>]');
|
|
250
|
+
if (flags.format && !['V4', 'V5'].includes(String(flags.format).toUpperCase())) {
|
|
251
|
+
throw new Error('--format must be V4 or V5.');
|
|
252
|
+
}
|
|
253
|
+
if (flags.format) flags.format = String(flags.format).toUpperCase();
|
|
254
|
+
return { query, flags };
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export function parseInfoArgs(args) {
|
|
258
|
+
const positional = [];
|
|
259
|
+
const flags = {};
|
|
260
|
+
for (let i = 0; i < args.length; i++) {
|
|
261
|
+
const arg = args[i];
|
|
262
|
+
if (arg === '--json') {
|
|
263
|
+
flags.json = true;
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
if (arg === '--registry') {
|
|
267
|
+
const value = args[++i];
|
|
268
|
+
if (value === undefined || value.startsWith('--')) throw new Error('Missing value for --registry');
|
|
269
|
+
flags.registry = value;
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
if (arg.startsWith('--')) throw new Error(`Unknown flag for info: ${arg}`);
|
|
273
|
+
positional.push(arg);
|
|
274
|
+
}
|
|
275
|
+
if (positional.length !== 1) throw new Error('Usage: knolo info <publisher>/<slug> [--json] [--registry <url>]');
|
|
276
|
+
return { positional, flags };
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function validatePacksResponse(body) {
|
|
280
|
+
if (!body || typeof body !== 'object' || !Array.isArray(body.packs)) {
|
|
281
|
+
throw new Error('Registry returned an invalid packs response.');
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function validatePackInfoResponse(body) {
|
|
286
|
+
if (!body || typeof body !== 'object' || typeof body.name !== 'string' || !body.pack || typeof body.pack !== 'object') {
|
|
287
|
+
throw new Error('Registry returned an invalid pack response.');
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
async function validateKnowledgeImage(core, bytes) {
|
|
292
|
+
if (core.isKnowledgeImageV5?.(bytes)) {
|
|
293
|
+
try {
|
|
294
|
+
const verification = core.verifyKnowledgeImageV5(bytes);
|
|
295
|
+
return { format: 'V5', stateRoot: verification.stateRoot };
|
|
296
|
+
} catch (error) {
|
|
297
|
+
throw new Error(`invalid V5 Knowledge Image: ${error instanceof Error ? error.message : String(error)}`);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const buffer = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
|
|
302
|
+
if (core.isPackV4?.(buffer)) {
|
|
303
|
+
try {
|
|
304
|
+
await core.mountPack({ src: bytes });
|
|
305
|
+
return { format: 'V4' };
|
|
306
|
+
} catch (error) {
|
|
307
|
+
throw new Error(`invalid V4 Knowledge Image: ${error instanceof Error ? error.message : String(error)}`);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
throw new Error('not a Knowledge Image');
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
async function copyArtifact(sourcePath, outputPath, cwd) {
|
|
314
|
+
const targetPath = path.resolve(cwd, outputPath);
|
|
315
|
+
const tempPath = `${targetPath}.tmp-${process.pid}-${Date.now()}`;
|
|
316
|
+
await mkdir(path.dirname(targetPath), { recursive: true });
|
|
317
|
+
try {
|
|
318
|
+
await copyFile(sourcePath, tempPath);
|
|
319
|
+
await rename(tempPath, targetPath);
|
|
320
|
+
} finally {
|
|
321
|
+
await unlinkIfPresent(tempPath);
|
|
322
|
+
}
|
|
323
|
+
return targetPath;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
async function unlinkIfPresent(filePath) {
|
|
327
|
+
try {
|
|
328
|
+
await unlink(filePath);
|
|
329
|
+
} catch (error) {
|
|
330
|
+
if (error?.code !== 'ENOENT') throw error;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
export function renderSearchTable(packs) {
|
|
335
|
+
if (!packs.length) return 'No packs found.';
|
|
336
|
+
|
|
337
|
+
const rows = packs.map((pack) => [
|
|
338
|
+
pack.id || pack.name || `${pack.publisher || ''}/${pack.slug || ''}`,
|
|
339
|
+
pack.version ?? '',
|
|
340
|
+
pack.format ?? '',
|
|
341
|
+
pack.license ?? '',
|
|
342
|
+
pack.pulls ?? 0,
|
|
343
|
+
pack.description ?? '',
|
|
344
|
+
].map((value) => String(value)));
|
|
345
|
+
const widths = [24, 9, 6, 11, 8];
|
|
346
|
+
const header = ['NAME', 'VERSION', 'FORMAT', 'LICENSE', 'PULLS', 'DESCRIPTION'];
|
|
347
|
+
const formatRow = (row) => [
|
|
348
|
+
pad(truncate(row[0], widths[0]), widths[0]),
|
|
349
|
+
pad(truncate(row[1], widths[1]), widths[1]),
|
|
350
|
+
pad(truncate(row[2], widths[2]), widths[2]),
|
|
351
|
+
pad(truncate(row[3], widths[3]), widths[3]),
|
|
352
|
+
pad(truncate(row[4], widths[4]), widths[4]),
|
|
353
|
+
truncate(row[5], 72),
|
|
354
|
+
].join(' ').trimEnd();
|
|
355
|
+
|
|
356
|
+
return [formatRow(header), ...rows.map(formatRow)].join('\n');
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
export function renderPackInfo(body) {
|
|
360
|
+
const pack = body.pack || {};
|
|
361
|
+
const latest = body.latest || {};
|
|
362
|
+
const lines = [
|
|
363
|
+
`name ${body.name}`,
|
|
364
|
+
`description ${body.description || pack.description || ''}`,
|
|
365
|
+
`publisher ${body.publisher || pack.publisher || ''}`,
|
|
366
|
+
`slug ${body.slug || pack.slug || ''}`,
|
|
367
|
+
`format ${pack.format || ''}`,
|
|
368
|
+
`version ${pack.version || latest.version || ''}`,
|
|
369
|
+
`license ${pack.license || latest.license || ''}`,
|
|
370
|
+
`sizeBytes ${pack.sizeBytes ?? latest.sizeBytes ?? 0}`,
|
|
371
|
+
`docs ${pack.docs ?? 0}`,
|
|
372
|
+
`blocks ${pack.blocks ?? 0}`,
|
|
373
|
+
`namespaces ${Array.isArray(pack.namespaces) ? pack.namespaces.join(', ') : ''}`,
|
|
374
|
+
`pulls ${pack.pulls ?? 0}`,
|
|
375
|
+
`stars ${pack.stars ?? 0}`,
|
|
376
|
+
`topics ${Array.isArray(pack.topics) ? pack.topics.join(', ') : ''}`,
|
|
377
|
+
`latest ${latest.version || ''}`,
|
|
378
|
+
`sha256 ${latest.sha256 || pack.sha256 || ''}`,
|
|
379
|
+
`url ${latest.url || pack.blobUrl || ''}`,
|
|
380
|
+
];
|
|
381
|
+
return lines.join('\n');
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function truncate(value, width) {
|
|
385
|
+
if (value.length <= width) return value;
|
|
386
|
+
if (width <= 1) return '…'.slice(0, width);
|
|
387
|
+
return `${value.slice(0, width - 1)}…`;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function pad(value, width) {
|
|
391
|
+
return value.padEnd(width, ' ');
|
|
392
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export const DEFAULT_PRODUCTION_REGISTRY = 'https://hub.knolo.dev';
|
|
2
|
+
export const DEFAULT_DEVELOPMENT_REGISTRY = 'http://localhost:3000';
|
|
3
|
+
|
|
4
|
+
export function resolveRegistryUrl({ value, env = process.env } = {}) {
|
|
5
|
+
const configured = value || env.KNOLO_HUB_URL;
|
|
6
|
+
if (configured) return normalizeRegistryUrl(configured);
|
|
7
|
+
|
|
8
|
+
const development = env.NODE_ENV === 'development' || env.KNOLO_ENV === 'development';
|
|
9
|
+
return normalizeRegistryUrl(development ? DEFAULT_DEVELOPMENT_REGISTRY : DEFAULT_PRODUCTION_REGISTRY);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function normalizeRegistryUrl(value) {
|
|
13
|
+
if (typeof value !== 'string' || !value.trim()) {
|
|
14
|
+
throw new Error('Registry URL must be a non-empty HTTP(S) URL.');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
let url;
|
|
18
|
+
try {
|
|
19
|
+
url = new URL(value.trim());
|
|
20
|
+
} catch {
|
|
21
|
+
throw new Error(`Invalid registry URL: ${value}`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (!['http:', 'https:'].includes(url.protocol)) {
|
|
25
|
+
throw new Error(`Registry URL must use http or https: ${value}`);
|
|
26
|
+
}
|
|
27
|
+
if (url.username || url.password || url.search || url.hash) {
|
|
28
|
+
throw new Error('Registry URL must be an origin without credentials, query parameters, or fragments.');
|
|
29
|
+
}
|
|
30
|
+
if (url.pathname !== '/' && url.pathname !== '') {
|
|
31
|
+
throw new Error('Registry URL must be an origin without a path.');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return url.origin;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function registryApiUrl(registry, segments, searchParams) {
|
|
38
|
+
const url = new URL(normalizeRegistryUrl(registry));
|
|
39
|
+
url.pathname = `/api/v1/${segments.map((segment) => encodeURIComponent(String(segment))).join('/')}`;
|
|
40
|
+
if (searchParams) {
|
|
41
|
+
for (const [key, value] of Object.entries(searchParams)) {
|
|
42
|
+
if (value !== undefined && value !== null && value !== '') url.searchParams.set(key, String(value));
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return url;
|
|
46
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { chmod, mkdir, readFile, rename, unlink, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { createInterface } from 'node:readline/promises';
|
|
6
|
+
|
|
7
|
+
const TOKEN_PATTERN = /^kno_[A-Za-z0-9_-]+$/;
|
|
8
|
+
|
|
9
|
+
export function credentialsPath({ env = process.env, homeDir = homedir() } = {}) {
|
|
10
|
+
const configHome = env.XDG_CONFIG_HOME ? path.resolve(env.XDG_CONFIG_HOME) : path.join(homeDir, '.config');
|
|
11
|
+
return path.join(configHome, 'knolo', 'credentials.json');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function saveCredentials({ registry, token, env = process.env, homeDir = homedir() } = {}) {
|
|
15
|
+
const normalizedToken = typeof token === 'string' ? token.trim() : token;
|
|
16
|
+
validateToken(normalizedToken);
|
|
17
|
+
if (typeof registry !== 'string' || !registry) throw new Error('Credential registry is required.');
|
|
18
|
+
const targetPath = credentialsPath({ env, homeDir });
|
|
19
|
+
const directory = path.dirname(targetPath);
|
|
20
|
+
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
21
|
+
await chmod(directory, 0o700);
|
|
22
|
+
|
|
23
|
+
const value = {
|
|
24
|
+
registry,
|
|
25
|
+
token: normalizedToken,
|
|
26
|
+
prefix: normalizedToken.slice(0, 12),
|
|
27
|
+
};
|
|
28
|
+
const tempPath = `${targetPath}.tmp-${process.pid}-${randomUUID()}`;
|
|
29
|
+
try {
|
|
30
|
+
await writeFile(tempPath, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600 });
|
|
31
|
+
await chmod(tempPath, 0o600);
|
|
32
|
+
await rename(tempPath, targetPath);
|
|
33
|
+
await chmod(targetPath, 0o600);
|
|
34
|
+
} finally {
|
|
35
|
+
await unlink(tempPath).catch(() => {});
|
|
36
|
+
}
|
|
37
|
+
return { path: targetPath, registry, prefix: value.prefix };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function loadCredentials({ env = process.env, homeDir = homedir() } = {}) {
|
|
41
|
+
const filePath = credentialsPath({ env, homeDir });
|
|
42
|
+
let text;
|
|
43
|
+
try {
|
|
44
|
+
text = await readFile(filePath, 'utf8');
|
|
45
|
+
} catch (error) {
|
|
46
|
+
if (error?.code === 'ENOENT') throw new Error('Not logged in. Run "knolo login" first.');
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
let value;
|
|
51
|
+
try {
|
|
52
|
+
value = JSON.parse(text);
|
|
53
|
+
} catch {
|
|
54
|
+
throw new Error('Invalid JSON in knolo credentials.');
|
|
55
|
+
}
|
|
56
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('Knolo credentials must be a JSON object.');
|
|
57
|
+
if (typeof value.registry !== 'string' || !value.registry) throw new Error('Knolo credentials are missing registry.');
|
|
58
|
+
validateToken(value.token);
|
|
59
|
+
if (typeof value.prefix !== 'string' || value.prefix !== value.token.slice(0, 12)) throw new Error('Knolo credentials have an invalid token prefix.');
|
|
60
|
+
return { path: filePath, registry: value.registry, token: value.token, prefix: value.prefix };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export async function removeCredentials({ env = process.env, homeDir = homedir() } = {}) {
|
|
64
|
+
const filePath = credentialsPath({ env, homeDir });
|
|
65
|
+
try {
|
|
66
|
+
await unlink(filePath);
|
|
67
|
+
return { path: filePath, removed: true };
|
|
68
|
+
} catch (error) {
|
|
69
|
+
if (error?.code === 'ENOENT') return { path: filePath, removed: false };
|
|
70
|
+
throw error;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export async function promptForToken({ input = process.stdin, output = process.stderr, forceStdin = false } = {}) {
|
|
75
|
+
if (forceStdin || !input.isTTY) {
|
|
76
|
+
const chunks = [];
|
|
77
|
+
for await (const chunk of input) chunks.push(Buffer.from(chunk));
|
|
78
|
+
return Buffer.concat(chunks).toString('utf8').trim();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const readline = createInterface({ input, output });
|
|
82
|
+
try {
|
|
83
|
+
return (await readline.question('Hub token (kno_…): ')).trim();
|
|
84
|
+
} finally {
|
|
85
|
+
readline.close();
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function validateToken(token) {
|
|
90
|
+
if (typeof token !== 'string' || !TOKEN_PATTERN.test(token.trim())) {
|
|
91
|
+
throw new Error('Token must start with kno_ and contain only base64url characters.');
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export class RegistryError extends Error {
|
|
2
|
+
constructor(message, { status, code, url, body, cause } = {}) {
|
|
3
|
+
super(message, cause ? { cause } : undefined);
|
|
4
|
+
this.name = 'RegistryError';
|
|
5
|
+
this.status = status;
|
|
6
|
+
this.code = code;
|
|
7
|
+
this.url = url;
|
|
8
|
+
this.body = body;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export class RegistryNetworkError extends Error {
|
|
13
|
+
constructor(message, { url, cause } = {}) {
|
|
14
|
+
super(message, cause ? { cause } : undefined);
|
|
15
|
+
this.name = 'RegistryNetworkError';
|
|
16
|
+
this.url = url;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function registryErrorMessage({ status, code, body, url }) {
|
|
21
|
+
if (typeof body?.error === 'string' && body.error.trim()) return body.error.trim();
|
|
22
|
+
if (status === 401 || code === 'unauthenticated') return 'Registry authentication is required.';
|
|
23
|
+
if (status === 404 || code === 'not_found') return 'Registry resource not found.';
|
|
24
|
+
if (status === 410 || code === 'yanked') return 'Registry version is yanked.';
|
|
25
|
+
if (status === 413 || code === 'too_large') return 'Registry artifact is too large.';
|
|
26
|
+
if (status === 503 || code === 'unconfigured') return 'Registry is not configured.';
|
|
27
|
+
return `Registry request failed${status ? ` with HTTP ${status}` : ''}${url ? `: ${url}` : '.'}`;
|
|
28
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { RegistryError, RegistryNetworkError, registryErrorMessage } from './errors.mjs';
|
|
2
|
+
|
|
3
|
+
export async function getJson(url, { fetchImpl = globalThis.fetch, headers = {} } = {}) {
|
|
4
|
+
if (typeof fetchImpl !== 'function') throw new Error('This Node runtime does not provide fetch.');
|
|
5
|
+
|
|
6
|
+
let response;
|
|
7
|
+
try {
|
|
8
|
+
response = await fetchImpl(url, {
|
|
9
|
+
method: 'GET',
|
|
10
|
+
redirect: 'follow',
|
|
11
|
+
headers: { accept: 'application/json', ...headers },
|
|
12
|
+
});
|
|
13
|
+
} catch (error) {
|
|
14
|
+
throw new RegistryNetworkError(`Could not reach registry at ${new URL(url).origin}.`, { url: String(url), cause: error });
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const text = await readResponseText(response, url);
|
|
18
|
+
let body;
|
|
19
|
+
if (text) {
|
|
20
|
+
try {
|
|
21
|
+
body = JSON.parse(text);
|
|
22
|
+
} catch {
|
|
23
|
+
if (!response.ok) {
|
|
24
|
+
throw new RegistryError(`Registry returned HTTP ${response.status} with a non-JSON response.`, {
|
|
25
|
+
status: response.status,
|
|
26
|
+
url: String(url),
|
|
27
|
+
body: text.slice(0, 240),
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
throw new RegistryError('Registry returned invalid JSON.', {
|
|
31
|
+
status: response.status,
|
|
32
|
+
code: 'invalid_response',
|
|
33
|
+
url: String(url),
|
|
34
|
+
body: text.slice(0, 240),
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (!response.ok) {
|
|
40
|
+
const code = typeof body?.code === 'string' ? body.code : undefined;
|
|
41
|
+
throw new RegistryError(registryErrorMessage({ status: response.status, code, body, url: String(url) }), {
|
|
42
|
+
status: response.status,
|
|
43
|
+
code,
|
|
44
|
+
url: String(url),
|
|
45
|
+
body,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return body;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function readResponseText(response, url) {
|
|
53
|
+
try {
|
|
54
|
+
return await response.text();
|
|
55
|
+
} catch (error) {
|
|
56
|
+
throw new RegistryNetworkError(`Could not read registry response from ${new URL(url).origin}.`, { url: String(url), cause: error });
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { mkdir, readFile, rename, unlink, writeFile } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
export const LOCKFILE_NAME = 'knolo.lock.json';
|
|
6
|
+
|
|
7
|
+
export function lockfilePath(cwd = process.cwd()) {
|
|
8
|
+
return path.resolve(cwd, LOCKFILE_NAME);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function cachePath(homeDir, sha256) {
|
|
12
|
+
return path.join(homeDir, '.knolo', 'cache', 'sha256', `${sha256}.knolo`);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function validateLockfileUpdate({ cwd = process.cwd(), registry, manifest, force = false } = {}) {
|
|
16
|
+
const targetPath = lockfilePath(cwd);
|
|
17
|
+
const current = await readLockfile(targetPath);
|
|
18
|
+
if (current.registry && current.registry !== registry && !force) {
|
|
19
|
+
throw new Error(`Lockfile registry is ${current.registry}; refusing to mix registries without --force.`);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const packs = current.packs && typeof current.packs === 'object' && !Array.isArray(current.packs)
|
|
23
|
+
? current.packs
|
|
24
|
+
: {};
|
|
25
|
+
const existing = packs[manifest.name];
|
|
26
|
+
if (existing?.sha256 && existing.sha256 !== manifest.sha256 && !force) {
|
|
27
|
+
throw new Error(`Lockfile already pins ${manifest.name} to a different digest; use --force to replace it.`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const entry = {
|
|
31
|
+
version: manifest.version,
|
|
32
|
+
sha256: manifest.sha256,
|
|
33
|
+
...(manifest.stateRoot !== undefined ? { stateRoot: manifest.stateRoot } : {}),
|
|
34
|
+
...(manifest.license !== undefined ? { license: manifest.license } : {}),
|
|
35
|
+
};
|
|
36
|
+
return { targetPath, current, entry, packs };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function upsertLockfile({ cwd = process.cwd(), registry, manifest, force = false } = {}) {
|
|
40
|
+
const { targetPath, entry, packs } = await validateLockfileUpdate({ cwd, registry, manifest, force });
|
|
41
|
+
const next = {
|
|
42
|
+
registry,
|
|
43
|
+
packs: { ...packs, [manifest.name]: entry },
|
|
44
|
+
};
|
|
45
|
+
await atomicWriteJson(targetPath, next);
|
|
46
|
+
return { path: targetPath, entry };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function readLockfile(filePath) {
|
|
50
|
+
let text;
|
|
51
|
+
try {
|
|
52
|
+
text = await readFile(filePath, 'utf8');
|
|
53
|
+
} catch (error) {
|
|
54
|
+
if (error?.code === 'ENOENT') return { packs: {} };
|
|
55
|
+
throw error;
|
|
56
|
+
}
|
|
57
|
+
let value;
|
|
58
|
+
try {
|
|
59
|
+
value = JSON.parse(text);
|
|
60
|
+
} catch {
|
|
61
|
+
throw new Error(`Invalid JSON in ${LOCKFILE_NAME}.`);
|
|
62
|
+
}
|
|
63
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error(`${LOCKFILE_NAME} must be a JSON object.`);
|
|
64
|
+
if (value.registry !== undefined && typeof value.registry !== 'string') throw new Error(`${LOCKFILE_NAME}.registry must be a string.`);
|
|
65
|
+
if (value.packs !== undefined && (typeof value.packs !== 'object' || Array.isArray(value.packs))) throw new Error(`${LOCKFILE_NAME}.packs must be an object.`);
|
|
66
|
+
return value;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function atomicWriteJson(filePath, value) {
|
|
70
|
+
await mkdir(path.dirname(filePath), { recursive: true });
|
|
71
|
+
const tempPath = `${filePath}.tmp-${process.pid}-${randomUUID()}`;
|
|
72
|
+
try {
|
|
73
|
+
await writeFile(tempPath, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o644 });
|
|
74
|
+
await rename(tempPath, filePath);
|
|
75
|
+
} finally {
|
|
76
|
+
await unlink(tempPath).catch(() => {});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
const SHA256_PATTERN = /^[0-9a-f]{64}$/;
|
|
2
|
+
|
|
3
|
+
export function validatePackManifest(value, { expectedName, requestedVersion } = {}) {
|
|
4
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
5
|
+
throw new Error('Registry returned an invalid pack manifest.');
|
|
6
|
+
}
|
|
7
|
+
if (typeof value.name !== 'string' || !value.name) throw new Error('Registry manifest is missing name.');
|
|
8
|
+
if (expectedName && value.name !== expectedName) throw new Error(`Registry manifest name mismatch: expected ${expectedName}, got ${value.name}.`);
|
|
9
|
+
if (typeof value.version !== 'string' || !value.version) throw new Error('Registry manifest is missing version.');
|
|
10
|
+
if (requestedVersion && requestedVersion !== 'latest' && value.version !== requestedVersion) {
|
|
11
|
+
throw new Error(`Registry manifest version mismatch: expected ${requestedVersion}, got ${value.version}.`);
|
|
12
|
+
}
|
|
13
|
+
if (typeof value.sha256 !== 'string' || !SHA256_PATTERN.test(value.sha256)) {
|
|
14
|
+
throw new Error('Registry manifest sha256 must be 64 lowercase hexadecimal characters.');
|
|
15
|
+
}
|
|
16
|
+
if (typeof value.url !== 'string') throw new Error('Registry manifest is missing url.');
|
|
17
|
+
if (!Number.isSafeInteger(value.sizeBytes) || value.sizeBytes < 0) {
|
|
18
|
+
throw new Error('Registry manifest sizeBytes must be a non-negative integer.');
|
|
19
|
+
}
|
|
20
|
+
if (value.stateRoot !== undefined && typeof value.stateRoot !== 'string') {
|
|
21
|
+
throw new Error('Registry manifest stateRoot must be a string when present.');
|
|
22
|
+
}
|
|
23
|
+
if (value.license !== undefined && typeof value.license !== 'string') {
|
|
24
|
+
throw new Error('Registry manifest license must be a string when present.');
|
|
25
|
+
}
|
|
26
|
+
if (value.yanked !== undefined && typeof value.yanked !== 'boolean') {
|
|
27
|
+
throw new Error('Registry manifest yanked must be a boolean when present.');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
name: value.name,
|
|
32
|
+
version: value.version,
|
|
33
|
+
sha256: value.sha256,
|
|
34
|
+
...(value.stateRoot !== undefined ? { stateRoot: value.stateRoot } : {}),
|
|
35
|
+
url: value.url,
|
|
36
|
+
...(value.license !== undefined ? { license: value.license } : {}),
|
|
37
|
+
sizeBytes: value.sizeBytes,
|
|
38
|
+
yanked: value.yanked === true,
|
|
39
|
+
...(value.format !== undefined ? { format: value.format } : {}),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
const PACK_SPEC_PATTERN = /^(?<publisher>[a-z0-9-]+)\/(?<slug>[a-z0-9-]+)(?:@(?<version>[^@]+))?$/;
|
|
2
|
+
|
|
3
|
+
export function parsePackSpec(value) {
|
|
4
|
+
if (typeof value !== 'string' || !value.trim()) throw new Error('Pack spec must be publisher/slug[@version].');
|
|
5
|
+
const match = PACK_SPEC_PATTERN.exec(value.trim());
|
|
6
|
+
if (!match) throw new Error(`Invalid pack spec: ${value}. Expected publisher/slug[@version].`);
|
|
7
|
+
|
|
8
|
+
return {
|
|
9
|
+
publisher: match.groups.publisher,
|
|
10
|
+
slug: match.groups.slug,
|
|
11
|
+
version: match.groups.version || 'latest',
|
|
12
|
+
name: `${match.groups.publisher}/${match.groups.slug}`,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function parsePackName(value) {
|
|
17
|
+
const parsed = parsePackSpec(value);
|
|
18
|
+
if (value.includes('@')) throw new Error(`Pack name must not include a version: ${value}`);
|
|
19
|
+
return parsed;
|
|
20
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@knolo/cli",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.2.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"knolo": "bin/knolo.mjs"
|
|
@@ -28,10 +28,11 @@
|
|
|
28
28
|
},
|
|
29
29
|
"scripts": {
|
|
30
30
|
"build": "node -e \"console.log('cli is executable via bin/knolo.mjs')\"",
|
|
31
|
-
"test": "npm run build --prefix ../core && node --test test/cli.test.mjs"
|
|
31
|
+
"test": "npm run build --prefix ../core && node --test test/cli.test.mjs test/hub.integration.test.mjs",
|
|
32
|
+
"test:hub": "node --test test/hub.integration.test.mjs"
|
|
32
33
|
},
|
|
33
34
|
"dependencies": {
|
|
34
|
-
"@knolo/core": "^5.
|
|
35
|
+
"@knolo/core": "^5.1.0"
|
|
35
36
|
},
|
|
36
37
|
"private": false
|
|
37
38
|
}
|
|
@@ -578,6 +578,47 @@ pub struct KnowledgeImageVerification {
|
|
|
578
578
|
pub segments: Vec<KnowledgeImageSegment>,
|
|
579
579
|
}
|
|
580
580
|
|
|
581
|
+
#[derive(Debug, Clone, PartialEq)]
|
|
582
|
+
pub struct KnowledgeRuntimeDiagnosticsImageV1 {
|
|
583
|
+
pub state_root: String,
|
|
584
|
+
pub commit_digest: String,
|
|
585
|
+
pub sequence: u64,
|
|
586
|
+
pub object_count: usize,
|
|
587
|
+
pub event_count: usize,
|
|
588
|
+
pub segment_count: usize,
|
|
589
|
+
pub active_superblock: char,
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
#[derive(Debug, Clone, PartialEq)]
|
|
593
|
+
pub struct KnowledgeRuntimeDiagnosticsV1 {
|
|
594
|
+
pub version: u64,
|
|
595
|
+
pub valid: bool,
|
|
596
|
+
pub image: KnowledgeRuntimeDiagnosticsImageV1,
|
|
597
|
+
pub diagnostics_root: String,
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
#[derive(Debug, Clone, PartialEq)]
|
|
601
|
+
pub struct KnowledgeStudioCapabilitiesV1 {
|
|
602
|
+
pub inspect_image: bool,
|
|
603
|
+
pub verify_image: bool,
|
|
604
|
+
pub inspect_query_index: bool,
|
|
605
|
+
pub inspect_query_history: bool,
|
|
606
|
+
pub inspect_run: bool,
|
|
607
|
+
pub inspect_replay: bool,
|
|
608
|
+
pub mutate_image: bool,
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
#[derive(Debug, Clone, PartialEq)]
|
|
612
|
+
pub struct KnowledgeStudioManagementV1 {
|
|
613
|
+
pub version: u64,
|
|
614
|
+
pub surface: String,
|
|
615
|
+
pub valid: bool,
|
|
616
|
+
pub read_only: bool,
|
|
617
|
+
pub diagnostics: KnowledgeRuntimeDiagnosticsV1,
|
|
618
|
+
pub capabilities: KnowledgeStudioCapabilitiesV1,
|
|
619
|
+
pub management_root: String,
|
|
620
|
+
}
|
|
621
|
+
|
|
581
622
|
#[derive(Debug, Clone, PartialEq)]
|
|
582
623
|
pub struct MigrationMapping {
|
|
583
624
|
pub legacy_block_id: usize,
|
|
@@ -625,6 +666,101 @@ pub fn verify_knowledge_image(bytes: &[u8]) -> Result<KnowledgeImageVerification
|
|
|
625
666
|
inspect_knowledge_image(bytes)
|
|
626
667
|
}
|
|
627
668
|
|
|
669
|
+
pub fn inspect_knowledge_runtime_v5(bytes: &[u8]) -> Result<KnowledgeRuntimeDiagnosticsV1, KnoloError> {
|
|
670
|
+
let image = mount_knowledge_image(bytes)?;
|
|
671
|
+
let diagnostics = KnowledgeRuntimeDiagnosticsV1 {
|
|
672
|
+
version: 1,
|
|
673
|
+
valid: true,
|
|
674
|
+
image: KnowledgeRuntimeDiagnosticsImageV1 {
|
|
675
|
+
state_root: image.state_root,
|
|
676
|
+
commit_digest: image.commit_digest,
|
|
677
|
+
sequence: image.commit.sequence,
|
|
678
|
+
object_count: image.objects.len(),
|
|
679
|
+
event_count: image.events.len(),
|
|
680
|
+
segment_count: image.segments.len(),
|
|
681
|
+
active_superblock: image.active_superblock,
|
|
682
|
+
},
|
|
683
|
+
diagnostics_root: String::new(),
|
|
684
|
+
};
|
|
685
|
+
let diagnostics_root = runtime_diagnostics_root_v5(&diagnostics);
|
|
686
|
+
Ok(KnowledgeRuntimeDiagnosticsV1 { diagnostics_root, ..diagnostics })
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
pub fn runtime_diagnostics_root_v5(diagnostics: &KnowledgeRuntimeDiagnosticsV1) -> String {
|
|
690
|
+
digest_domain("runtime-diagnostics", &encode_cbor(&runtime_diagnostics_body(diagnostics)))
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
pub fn inspect_knowledge_studio_management_v5(bytes: &[u8]) -> Result<KnowledgeStudioManagementV1, KnoloError> {
|
|
694
|
+
let diagnostics = inspect_knowledge_runtime_v5(bytes)?;
|
|
695
|
+
let management = KnowledgeStudioManagementV1 {
|
|
696
|
+
version: 1,
|
|
697
|
+
surface: "studio-management".into(),
|
|
698
|
+
valid: true,
|
|
699
|
+
read_only: true,
|
|
700
|
+
diagnostics,
|
|
701
|
+
capabilities: KnowledgeStudioCapabilitiesV1 {
|
|
702
|
+
inspect_image: true,
|
|
703
|
+
verify_image: true,
|
|
704
|
+
inspect_query_index: false,
|
|
705
|
+
inspect_query_history: false,
|
|
706
|
+
inspect_run: false,
|
|
707
|
+
inspect_replay: false,
|
|
708
|
+
mutate_image: false,
|
|
709
|
+
},
|
|
710
|
+
management_root: String::new(),
|
|
711
|
+
};
|
|
712
|
+
let management_root = studio_management_root_v5(&management);
|
|
713
|
+
Ok(KnowledgeStudioManagementV1 { management_root, ..management })
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
pub fn studio_management_root_v5(management: &KnowledgeStudioManagementV1) -> String {
|
|
717
|
+
digest_domain("studio-management", &encode_cbor(&CborValue::Map(vec![
|
|
718
|
+
("capabilities".into(), studio_capabilities_value(&management.capabilities)),
|
|
719
|
+
("diagnostics".into(), runtime_diagnostics_value(&management.diagnostics)),
|
|
720
|
+
("readOnly".into(), CborValue::Bool(management.read_only)),
|
|
721
|
+
("surface".into(), CborValue::Text(management.surface.clone())),
|
|
722
|
+
("valid".into(), CborValue::Bool(management.valid)),
|
|
723
|
+
("version".into(), CborValue::UInt(management.version)),
|
|
724
|
+
])))
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
fn runtime_diagnostics_body(diagnostics: &KnowledgeRuntimeDiagnosticsV1) -> CborValue {
|
|
728
|
+
CborValue::Map(vec![
|
|
729
|
+
("image".into(), CborValue::Map(vec![
|
|
730
|
+
("activeSuperblock".into(), CborValue::Text(diagnostics.image.active_superblock.to_string())),
|
|
731
|
+
("commitDigest".into(), CborValue::Text(diagnostics.image.commit_digest.clone())),
|
|
732
|
+
("eventCount".into(), CborValue::UInt(diagnostics.image.event_count as u64)),
|
|
733
|
+
("objectCount".into(), CborValue::UInt(diagnostics.image.object_count as u64)),
|
|
734
|
+
("segmentCount".into(), CborValue::UInt(diagnostics.image.segment_count as u64)),
|
|
735
|
+
("sequence".into(), CborValue::UInt(diagnostics.image.sequence)),
|
|
736
|
+
("stateRoot".into(), CborValue::Text(diagnostics.image.state_root.clone())),
|
|
737
|
+
])),
|
|
738
|
+
("valid".into(), CborValue::Bool(diagnostics.valid)),
|
|
739
|
+
("version".into(), CborValue::UInt(diagnostics.version)),
|
|
740
|
+
])
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
fn runtime_diagnostics_value(diagnostics: &KnowledgeRuntimeDiagnosticsV1) -> CborValue {
|
|
744
|
+
let mut entries = match runtime_diagnostics_body(diagnostics) {
|
|
745
|
+
CborValue::Map(entries) => entries,
|
|
746
|
+
_ => Vec::new(),
|
|
747
|
+
};
|
|
748
|
+
entries.push(("diagnosticsRoot".into(), CborValue::Text(diagnostics.diagnostics_root.clone())));
|
|
749
|
+
CborValue::Map(entries)
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
fn studio_capabilities_value(capabilities: &KnowledgeStudioCapabilitiesV1) -> CborValue {
|
|
753
|
+
CborValue::Map(vec![
|
|
754
|
+
("inspectImage".into(), CborValue::Bool(capabilities.inspect_image)),
|
|
755
|
+
("inspectQueryHistory".into(), CborValue::Bool(capabilities.inspect_query_history)),
|
|
756
|
+
("inspectQueryIndex".into(), CborValue::Bool(capabilities.inspect_query_index)),
|
|
757
|
+
("inspectReplay".into(), CborValue::Bool(capabilities.inspect_replay)),
|
|
758
|
+
("inspectRun".into(), CborValue::Bool(capabilities.inspect_run)),
|
|
759
|
+
("mutateImage".into(), CborValue::Bool(capabilities.mutate_image)),
|
|
760
|
+
("verifyImage".into(), CborValue::Bool(capabilities.verify_image)),
|
|
761
|
+
])
|
|
762
|
+
}
|
|
763
|
+
|
|
628
764
|
pub fn state_root(image: &KnowledgeImage) -> &str {
|
|
629
765
|
&image.state_root
|
|
630
766
|
}
|
|
@@ -245,11 +245,11 @@ dependencies = [
|
|
|
245
245
|
|
|
246
246
|
[[package]]
|
|
247
247
|
name = "knolo-core-rust"
|
|
248
|
-
version = "5.
|
|
248
|
+
version = "5.1.0"
|
|
249
249
|
|
|
250
250
|
[[package]]
|
|
251
251
|
name = "knolo-icp-canister"
|
|
252
|
-
version = "5.
|
|
252
|
+
version = "5.1.0"
|
|
253
253
|
dependencies = [
|
|
254
254
|
"candid",
|
|
255
255
|
"ic-cdk",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "knolo-icp-canister"
|
|
3
|
-
version = "5.
|
|
3
|
+
version = "5.1.0"
|
|
4
4
|
edition = "2021"
|
|
5
5
|
license = "Apache-2.0"
|
|
6
6
|
description = "ICP canister adapter for Knolo retrieval over Candid"
|
|
@@ -16,5 +16,5 @@ path = "src/lib.rs"
|
|
|
16
16
|
candid = "0.10"
|
|
17
17
|
ic-cdk = "0.17"
|
|
18
18
|
ic-cdk-macros = "0.17"
|
|
19
|
-
knolo-core-rust = { path = "../knolo-core-rust", version = "5.
|
|
19
|
+
knolo-core-rust = { path = "../knolo-core-rust", version = "5.1.0" }
|
|
20
20
|
serde = { version = "1", features = ["derive"] }
|