@knolo/cli 6.2.0 → 6.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -12,8 +12,9 @@ the format is still draft and compressed object/event images require a VQF-aware
12
12
  reader.
13
13
 
14
14
  `knolo compose`, `knolo attest`, and `knolo verify-attestation` cite local
15
- Knowledge Images and write an attestation sidecar. `@knolo/cli` `6.2.0`
16
- depends on `@knolo/core` `^6.2.0`. They do not fetch Hub. Verification
15
+ Knowledge Images and write an attestation sidecar. This tree's `@knolo/cli`
16
+ package file is `6.2.2` and depends on `@knolo/core` `^6.2.0`. npm
17
+ `latest` is `6.2.1` until `6.2.2` is published. They do not fetch Hub. Verification
17
18
  recomputes `compositionRoot` and checks bundled excerpt bytes. A keyring key
18
19
  must fall inside `notBefore`, `notAfter`, and `revokedAt`. `used` means a
19
20
  query or evaluation ran. It does not mean `SATISFIED`, and it does not mean a
@@ -24,6 +25,18 @@ claim is true or false. Hub `attested: true` is a listing flag.
24
25
  model and does not take an API key. A verified trace matches the image. It
25
26
  does not mean the answer is true.
26
27
 
28
+ `knolo flight ingest` packs a `flight-events-1` document into a V5 Knowledge
29
+ Image and binds it to the SHA-256 of one log file. It does not decode the log.
30
+ `log.format` is `ulog`, `dataflash`, or `rosbag2`. Optional producers live in
31
+ `scripts/flight/` and are not part of `npm test`. `SATISFIED` on a later KAR
32
+ check means the evidence requirements were met. It does not mean a flight was
33
+ safe, legal, or compliant. See `docs/flight/README.md`.
34
+
35
+ `knolo skill` creates a skill pack for the host agent. The CLI does not call
36
+ a model. A receipt is an integrity check. It does not mean a claim is proven.
37
+ `SATISFIED` means the evidence requirements were met. See
38
+ [Skills](../../docs/skills/README.md).
39
+
27
40
  The official CLI for building `.knolo` knowledge packs.
28
41
 
29
42
  It indexes structured content and produces a deterministic, local-first knowledge bundle for use with `@knolo/core`.
@@ -120,6 +133,72 @@ Successful installs are cached by SHA-256 and recorded in
120
133
  `knolo.lock.json`. A yanked version or an existing conflicting pin requires
121
134
  `--force`; digest and artifact validation cannot be bypassed.
122
135
 
136
+ ## Skill packs
137
+
138
+ `knolo skill` is separate from the Hub commands above. `knolo publish` still
139
+ uploads a `.knolo` knowledge pack to `https://hub.knolo.dev`. `knolo skill
140
+ publish` sends a skill pack to that same hub. `--hub <dir>` remains a local
141
+ directory for an offline catalog.
142
+
143
+ ```bash
144
+ knolo skill init refund-policy
145
+ knolo skill check refund-policy
146
+ knolo skill pack refund-policy --out dist/refund-policy.knolo-skill
147
+ knolo skill publish dist/refund-policy.knolo-skill
148
+ knolo skill install acme/refund-policy
149
+ knolo skill publish dist/refund-policy.knolo-skill --hub ./catalog
150
+ knolo skill install refund-policy --hub ./catalog --dir ./skills
151
+ knolo skill search refund
152
+ knolo skill list
153
+ knolo skill info acme/refund-policy
154
+ knolo setup --root <dir> --agent <id> --auto-rebuild
155
+ ```
156
+
157
+ `init` writes `SKILL.md` and an empty `knowledge/` directory. `check` reads
158
+ that directory. When `knowledge/` has files, `check` builds a V5 image, runs
159
+ one search, and prints a receipt id. The receipt is an integrity check. It
160
+ does not mean a claim is proven.
161
+
162
+ `pack` writes `manifest.json`, `SKILL.md`, `policy.json`, and, when
163
+ `knowledge/` has files, `knowledge.v5`. The manifest stores SHA-256 pins and
164
+ the image state root. It does not store a home directory or a computer
165
+ layout. The same files produce the same pins.
166
+
167
+ `publish` without `--hub` uses the stored `kno_…` token. The same pin can be
168
+ published again. A different pin is refused, and the catalog copy stays as
169
+ it was. `--hub <dir>` copies the pack into `<dir>/<name>` instead.
170
+ `install <publisher>/<name>` downloads one pack from the hub. It does not
171
+ read a stored token, and the download does not send one. `install` with
172
+ `--hub` or `--pack` reads a local directory. Either install checks the pin
173
+ before it writes. A mismatch writes nothing. The catalog id must match the
174
+ manifest. `SKILL.md` must match that name and description, and `policy.json`
175
+ must match the image policy root. The files land in `--dir/<name>`, or in
176
+ `~/.knolo/skills/<name>` when `--dir` is omitted. When that folder is a
177
+ skills root in `~/.knolo/box/box.config.json` and `knolo-box` is on `PATH`,
178
+ install runs one `knolo-box build`. A root written as `~/...` or
179
+ `$GROK_HOME/...` counts, and a skills scope left out of the file uses the
180
+ default `~/.grok` skill folders. A folder outside those roots is still
181
+ written, and install prints that it was not indexed. A failed build leaves
182
+ the installed files in place and prints that the install was not indexed.
183
+ `search`, `list`, and `info` read the public skill catalog. `list` prints
184
+ every `publisher/name`. `check` reads a source directory that contains
185
+ `knowledge/`. An installed pack contains `knowledge.v5` instead.
186
+
187
+ `knolo setup` runs `knolo-box init --tolerant` and then `knolo-box build`
188
+ when `knolo-box` is on `PATH`. It scans `~/.grok` when that directory
189
+ exists, plus every `--root`. It does not require `~/.grok`. It always
190
+ includes `~/.knolo/skills`. An existing `~/.knolo/box/box.config.json` is
191
+ kept and rebuilt. `--reset` replaces it. `--agent` sets the caller name.
192
+ `--auto-rebuild` turns on a rebuild at the next search. Those flags require
193
+ `--reset` when a config file is already there. Setup does not prompt,
194
+ install a pack, or call the hub. A missing `knolo-box` is one line, and
195
+ setup still exits 0. It prints the config path, `knolo skill install <publisher>/<name>`,
196
+ and `knolo-box mcp --config <path>`. The install path, auth, trust, and
197
+ version rules are in [Community install](../../docs/hub/COMMUNITY.md). This
198
+ page does not restate that contract. The hub stores packs. It does not run
199
+ a model, and it does not answer questions. `--hub` and `--registry` cannot
200
+ be combined.
201
+
123
202
  ---
124
203
 
125
204
  ## 🚀 Commands
@@ -280,7 +359,7 @@ This ensures:
280
359
  ## 🗺 Roadmap
281
360
 
282
361
  The active roadmap is maintained in [`../../docs/ROADMAP.md`](../../docs/ROADMAP.md).
283
- The current release is `6.2.0`. It preserves the V4 CLI, keeps V5 verification,
362
+ The published npm release is `6.2.1`. This package file is `6.2.2`. It preserves the V4 CLI, keeps V5 verification,
284
363
  migration, diagnostics, and read-only Studio management, and adds offline
285
364
  composition, attestation, and `knolo cortex`. Cortex verification does not
286
365
  mean an answer is true.
package/bin/flight.mjs ADDED
@@ -0,0 +1,367 @@
1
+ /**
2
+ * knolo flight ingest
3
+ *
4
+ * Packs a flight-events-1 document into a V5 Knowledge Image.
5
+ * It does not decode the log. log.format is ulog, dataflash, or rosbag2.
6
+ * SATISFIED is not a statement that a flight was safe or compliant.
7
+ */
8
+ import { createHash } from 'node:crypto';
9
+ import { createReadStream, existsSync, statSync, writeFileSync } from 'node:fs';
10
+ import path from 'node:path';
11
+ import { fileURLToPath, pathToFileURL } from 'node:url';
12
+ import { readBounded } from './kar-limits.mjs';
13
+
14
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
15
+
16
+ export const FLIGHT_EVENTS_FORMAT = 'flight-events-1';
17
+ export const MAX_FLIGHT_EVENTS = 10_000;
18
+ export const MAX_FLIGHT_EVENT_BYTES = 2 * 1024 * 1024;
19
+ export const FLIGHT_ACTOR = 'knolo-flight';
20
+
21
+ const TOKEN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
22
+ const PHRASE = /^[a-z0-9][a-z0-9 -]{0,160}$/;
23
+ const DIGEST = /^sha256-[0-9a-f]{64}$/;
24
+ const TOP_KEYS = ['format', 'parser', 'log', 'events'];
25
+ const EVENT_KEYS = ['id', 'kind', 'timestampUs', 'byteStart', 'byteEnd', 'fields', 'text'];
26
+ const KIND_FIELDS = {
27
+ arming: ['state'],
28
+ 'nav-mode': ['mode'],
29
+ 'failsafe-assert': ['name'],
30
+ 'failsafe-clear': ['name'],
31
+ 'geofence-breach': ['name'],
32
+ 'geofence-clear': [],
33
+ 'battery-threshold': ['level'],
34
+ 'link-loss': [],
35
+ 'link-restored': [],
36
+ 'scan-summary': ['phrase'],
37
+ };
38
+
39
+ const USAGE = `Usage: knolo flight ingest --events <events.json> --log <file> --out <image.knolo> [--json]
40
+
41
+ Packs a flight-events-1 document into a V5 Knowledge Image.
42
+ The command hashes --log and refuses the image when the hash or byte length differs.
43
+ It does not decode a flight log. Parsing stays in an external producer.
44
+ A later KAR status of SATISFIED means evidence requirements were met.
45
+ It does not mean a flight was safe, legal, or compliant.`;
46
+
47
+ export async function runFlightCli(argv) {
48
+ const { positionals, flags } = parseArgs(argv);
49
+ const sub = positionals[0] ?? '';
50
+ if (!sub || sub === 'help' || flags.help) {
51
+ console.log(USAGE);
52
+ return;
53
+ }
54
+ if (sub !== 'ingest') throw new Error(`Unknown flight command "${sub}".\n${USAGE}`);
55
+ const eventsPath = required(flags, 'events');
56
+ const logPath = required(flags, 'log');
57
+ const outPath = required(flags, 'out');
58
+ const resolvedOut = path.resolve(process.cwd(), outPath);
59
+ if (existsSync(resolvedOut)) throw new Error(`Refusing to overwrite ${outPath}. Choose a new --out path.`);
60
+ const document = readEvents(eventsPath);
61
+ const log = await inspectLog(logPath);
62
+ if (log.byteLength !== document.log.byteLength || log.sha256 !== document.log.sha256) {
63
+ throw new Error('FLIGHT_LOG_MISMATCH: log bytes do not match events.log.sha256 or events.log.byteLength.');
64
+ }
65
+ const image = await flightImageFromDocument(document);
66
+ writeFileSync(resolvedOut, image.bytes);
67
+ if (flags.json) {
68
+ console.log(JSON.stringify({
69
+ ok: true,
70
+ stateRoot: image.stateRoot,
71
+ events: document.events.length,
72
+ wrote: outPath,
73
+ }, null, 2));
74
+ return;
75
+ }
76
+ console.log(`FLIGHT IMAGE\nstate root ${image.stateRoot}\nevents ${document.events.length}\nwrote ${outPath}\n\nEvidence requirements are not a safety or compliance determination.`);
77
+ }
78
+
79
+ export function readEvents(file) {
80
+ const resolved = path.resolve(process.cwd(), file);
81
+ const stat = statSync(resolved);
82
+ if (!stat.isFile()) throw new Error(`FLIGHT_EVENTS_INVALID: ${file} is not a file.`);
83
+ if (stat.size > MAX_FLIGHT_EVENT_BYTES) {
84
+ throw new Error(`FLIGHT_EVENTS_TOO_LARGE: ${file} is ${stat.size} bytes; limit is ${MAX_FLIGHT_EVENT_BYTES}.`);
85
+ }
86
+ let parsed;
87
+ try {
88
+ parsed = JSON.parse(readBounded(file, MAX_FLIGHT_EVENT_BYTES).toString('utf8'));
89
+ } catch (error) {
90
+ const message = error instanceof Error ? error.message : String(error);
91
+ throw new Error(`FLIGHT_EVENTS_INVALID: ${message}`);
92
+ }
93
+ const document = parseFlightEvents(parsed);
94
+ if (!document.ok) throw new Error(`${document.code}: ${document.message}`);
95
+ return document.document;
96
+ }
97
+
98
+ export function parseFlightEvents(value) {
99
+ if (!isPlain(value) || !sameKeys(value, TOP_KEYS)) {
100
+ return fail('FLIGHT_EVENTS_INVALID', 'Document must be flight-events-1 with parser, log, and events.');
101
+ }
102
+ if (value.format !== FLIGHT_EVENTS_FORMAT) {
103
+ return fail('FLIGHT_EVENTS_INVALID', 'format must be flight-events-1.');
104
+ }
105
+ const parser = parseParser(value.parser);
106
+ if (!parser.ok) return parser;
107
+ const log = parseLog(value.log);
108
+ if (!log.ok) return log;
109
+ if (!Array.isArray(value.events)) return fail('FLIGHT_EVENTS_INVALID', 'events must be an array.');
110
+ if (value.events.length > MAX_FLIGHT_EVENTS) {
111
+ return fail('FLIGHT_TOO_MANY_EVENTS', `event count ${value.events.length} exceeds ${MAX_FLIGHT_EVENTS}.`);
112
+ }
113
+ const events = [];
114
+ const ids = new Set();
115
+ for (const [index, raw] of value.events.entries()) {
116
+ const parsed = parseEvent(raw, index, log.log.byteLength);
117
+ if (!parsed.ok) return parsed;
118
+ if (ids.has(parsed.event.id)) return fail('FLIGHT_EVENTS_INVALID', `Duplicate event id ${parsed.event.id}.`);
119
+ ids.add(parsed.event.id);
120
+ events.push(parsed.event);
121
+ }
122
+ for (let index = 1; index < events.length; index += 1) {
123
+ if (compareEvents(events[index - 1], events[index]) > 0) {
124
+ return fail('FLIGHT_EVENTS_INVALID', 'events must be sorted by timestampUs, kind, byteStart, and id.');
125
+ }
126
+ }
127
+ return { ok: true, document: { format: FLIGHT_EVENTS_FORMAT, parser: parser.parser, log: log.log, events } };
128
+ }
129
+
130
+ export function renderEventText(event) {
131
+ const time = event.timestampUs;
132
+ const fields = event.fields;
133
+ switch (event.kind) {
134
+ case 'arming':
135
+ return `ARMING_STATE ${fields.state} at ${time} us.`;
136
+ case 'nav-mode':
137
+ return `NAV_STATE ${fields.mode} at ${time} us.`;
138
+ case 'failsafe-assert':
139
+ return `FAILSAFE ${fields.name} asserted at ${time} us.`;
140
+ case 'failsafe-clear':
141
+ return `FAILSAFE ${fields.name} cleared at ${time} us.`;
142
+ case 'geofence-breach':
143
+ return `GEOFENCE_BREACH ${fields.name} at ${time} us.`;
144
+ case 'geofence-clear':
145
+ return `GEOFENCE clear at ${time} us.`;
146
+ case 'battery-threshold':
147
+ return `battery threshold crossed at ${time} us. level ${fields.level}.`;
148
+ case 'link-loss':
149
+ return `link loss recorded at ${time} us.`;
150
+ case 'link-restored':
151
+ return `LINK_RESTORED at ${time} us.`;
152
+ case 'scan-summary':
153
+ return `${fields.phrase} at ${time} us.`;
154
+ default:
155
+ return '';
156
+ }
157
+ }
158
+
159
+ export async function flightImageFromDocument(document) {
160
+ const api = await loadImageApi();
161
+ const encoder = new TextEncoder();
162
+ const objects = [
163
+ {
164
+ kind: 'metadata',
165
+ bytes: encoder.encode(metadataText(document)),
166
+ meta: {
167
+ source: 'flight-log.txt',
168
+ role: 'flight-log',
169
+ logSha256: document.log.sha256,
170
+ byteLength: String(document.log.byteLength),
171
+ parserId: document.parser.id,
172
+ parserVersion: document.parser.version,
173
+ eventCount: String(document.events.length),
174
+ logFormat: document.log.format,
175
+ },
176
+ },
177
+ ...document.events.map((event) => ({
178
+ kind: 'chunk',
179
+ bytes: encoder.encode(event.text),
180
+ meta: {
181
+ source: `${event.id}.txt`,
182
+ eventKind: event.kind,
183
+ timestampUs: String(event.timestampUs),
184
+ byteStart: String(event.byteStart),
185
+ byteEnd: String(event.byteEnd),
186
+ logSha256: document.log.sha256,
187
+ },
188
+ })),
189
+ ];
190
+ return api.createKnowledgeImageV5({ actor: FLIGHT_ACTOR, sequence: 1, objects });
191
+ }
192
+
193
+ export function hashFile(file) {
194
+ const resolved = path.resolve(process.cwd(), file);
195
+ return new Promise((resolve, reject) => {
196
+ const hash = createHash('sha256');
197
+ const stream = createReadStream(resolved);
198
+ stream.on('data', (chunk) => hash.update(chunk));
199
+ stream.on('error', reject);
200
+ stream.on('end', () => resolve(`sha256-${hash.digest('hex')}`));
201
+ });
202
+ }
203
+
204
+ function metadataText(document) {
205
+ return [
206
+ 'format flight-events-1',
207
+ `log ${document.log.sha256}`,
208
+ `bytes ${document.log.byteLength}`,
209
+ `parser ${document.parser.id} ${document.parser.version}`,
210
+ `events ${document.events.length}`,
211
+ '',
212
+ ].join('\n');
213
+ }
214
+
215
+ function parseParser(value) {
216
+ if (!isPlain(value) || !sameKeys(value, ['id', 'version'])) {
217
+ return fail('FLIGHT_EVENTS_INVALID', 'parser must contain id and version.');
218
+ }
219
+ if (typeof value.id !== 'string' || !TOKEN.test(value.id)) {
220
+ return fail('FLIGHT_EVENTS_INVALID', 'parser.id must be a short token.');
221
+ }
222
+ if (typeof value.version !== 'string' || !TOKEN.test(value.version)) {
223
+ return fail('FLIGHT_EVENTS_INVALID', 'parser.version must be a short token.');
224
+ }
225
+ return { ok: true, parser: { id: value.id, version: value.version } };
226
+ }
227
+
228
+ function parseLog(value) {
229
+ if (!isPlain(value) || !sameKeys(value, ['sha256', 'byteLength', 'format'])) {
230
+ return fail('FLIGHT_EVENTS_INVALID', 'log must contain sha256, byteLength, and format.');
231
+ }
232
+ if (typeof value.sha256 !== 'string' || !DIGEST.test(value.sha256)) {
233
+ return fail('FLIGHT_EVENTS_INVALID', 'log.sha256 must be sha256- and 64 lowercase hex digits.');
234
+ }
235
+ if (!nonNegativeInteger(value.byteLength)) {
236
+ return fail('FLIGHT_EVENTS_INVALID', 'log.byteLength must be a non-negative integer.');
237
+ }
238
+ if (value.format !== 'ulog' && value.format !== 'dataflash' && value.format !== 'rosbag2') {
239
+ return fail('FLIGHT_EVENTS_INVALID', 'log.format must be ulog, dataflash, or rosbag2.');
240
+ }
241
+ return { ok: true, log: { sha256: value.sha256, byteLength: value.byteLength, format: value.format } };
242
+ }
243
+
244
+ function parseEvent(value, index, byteLength) {
245
+ const pathName = `events[${index}]`;
246
+ if (!isPlain(value) || !sameKeys(value, EVENT_KEYS)) {
247
+ return fail('FLIGHT_EVENTS_INVALID', `${pathName} has missing or unknown fields.`);
248
+ }
249
+ if (typeof value.id !== 'string' || !TOKEN.test(value.id)) {
250
+ return fail('FLIGHT_EVENTS_INVALID', `${pathName}.id must be a short token.`);
251
+ }
252
+ const fields = KIND_FIELDS[value.kind];
253
+ if (!fields) return fail('FLIGHT_EVENTS_INVALID', `${pathName}.kind is not a phase 1 event kind.`);
254
+ if (!nonNegativeInteger(value.timestampUs) || !nonNegativeInteger(value.byteStart) || !nonNegativeInteger(value.byteEnd)) {
255
+ return fail('FLIGHT_EVENTS_INVALID', `${pathName} timestamps and byte offsets must be non-negative integers.`);
256
+ }
257
+ if (value.byteEnd < value.byteStart || value.byteEnd > byteLength) {
258
+ return fail('FLIGHT_EVENTS_INVALID', `${pathName} byte range is outside the log.`);
259
+ }
260
+ const parsedFields = parseFields(value.fields, fields, pathName);
261
+ if (!parsedFields.ok) return parsedFields;
262
+ const event = {
263
+ id: value.id,
264
+ kind: value.kind,
265
+ timestampUs: value.timestampUs,
266
+ byteStart: value.byteStart,
267
+ byteEnd: value.byteEnd,
268
+ fields: parsedFields.fields,
269
+ text: '',
270
+ };
271
+ event.text = renderEventText(event);
272
+ if (value.text !== event.text) {
273
+ return fail('FLIGHT_EVENTS_INVALID', `${pathName}.text must be the canonical sentence for its kind and fields.`);
274
+ }
275
+ return { ok: true, event };
276
+ }
277
+
278
+ function parseFields(value, names, pathName) {
279
+ if (!isPlain(value) || !sameKeys(value, names)) {
280
+ return fail('FLIGHT_EVENTS_INVALID', `${pathName}.fields must contain exactly ${names.join(', ') || 'no keys'}.`);
281
+ }
282
+ const fields = {};
283
+ for (const name of names) {
284
+ const text = value[name];
285
+ const pattern = name === 'phrase' ? PHRASE : TOKEN;
286
+ if (typeof text !== 'string' || !pattern.test(text)) {
287
+ return fail('FLIGHT_EVENTS_INVALID', `${pathName}.fields.${name} is not a canonical token.`);
288
+ }
289
+ fields[name] = text;
290
+ }
291
+ return { ok: true, fields };
292
+ }
293
+
294
+ function compareEvents(left, right) {
295
+ if (left.timestampUs !== right.timestampUs) return left.timestampUs - right.timestampUs;
296
+ if (left.kind !== right.kind) return left.kind < right.kind ? -1 : 1;
297
+ if (left.byteStart !== right.byteStart) return left.byteStart - right.byteStart;
298
+ if (left.id !== right.id) return left.id < right.id ? -1 : 1;
299
+ return 0;
300
+ }
301
+
302
+ function fail(code, message) {
303
+ return { ok: false, code, message };
304
+ }
305
+
306
+ function nonNegativeInteger(value) {
307
+ return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0;
308
+ }
309
+
310
+ function isPlain(value) {
311
+ return !!value && typeof value === 'object' && !Array.isArray(value) && Object.getPrototypeOf(value) === Object.prototype;
312
+ }
313
+
314
+ function sameKeys(value, keys) {
315
+ const actual = Object.keys(value);
316
+ if (actual.length !== keys.length) return false;
317
+ const allowed = new Set(keys);
318
+ return actual.every((key) => allowed.has(key));
319
+ }
320
+
321
+ async function inspectLog(file) {
322
+ const resolved = path.resolve(process.cwd(), file);
323
+ const stat = statSync(resolved);
324
+ if (!stat.isFile()) throw new Error(`FLIGHT_LOG_MISMATCH: ${file} is not a file.`);
325
+ const sha256 = await hashFile(file);
326
+ return { byteLength: stat.size, sha256 };
327
+ }
328
+
329
+ async function loadImageApi() {
330
+ const candidates = [
331
+ path.resolve(__dirname, '../../core/dist/index.js'),
332
+ '@knolo/core',
333
+ ];
334
+ for (const candidate of candidates) {
335
+ try {
336
+ const href = candidate.startsWith('@') ? candidate : pathToFileURL(candidate).href;
337
+ const loaded = await import(href);
338
+ if (loaded.createKnowledgeImageV5) return loaded;
339
+ } catch {
340
+ // Try the next candidate.
341
+ }
342
+ }
343
+ throw new Error('Could not load @knolo/core. Build packages/core first.');
344
+ }
345
+
346
+ function parseArgs(argv) {
347
+ const flags = {};
348
+ const positionals = [];
349
+ for (let index = 0; index < argv.length; index += 1) {
350
+ const arg = argv[index];
351
+ if (arg === '--json') flags.json = true;
352
+ else if (arg === '--help' || arg === '-h') flags.help = true;
353
+ else if (arg.startsWith('--')) {
354
+ const key = arg.slice(2);
355
+ const value = argv[index + 1];
356
+ if (value === undefined || value.startsWith('--')) throw new Error(`Missing value for --${key}`);
357
+ flags[key] = value;
358
+ index += 1;
359
+ } else positionals.push(arg);
360
+ }
361
+ return { positionals, flags };
362
+ }
363
+
364
+ function required(flags, name) {
365
+ if (!flags[name]) throw new Error(`Missing --${name}`);
366
+ return flags[name];
367
+ }
package/bin/knolo.mjs CHANGED
@@ -77,6 +77,9 @@ const SUBCOMMANDS = new Set([
77
77
  'attest',
78
78
  'verify-attestation',
79
79
  'cortex',
80
+ 'flight',
81
+ 'skill',
82
+ 'setup',
80
83
  ]);
81
84
  const ICP_SUBCOMMANDS = new Set([
82
85
  'init',
@@ -175,6 +178,9 @@ Commands:
175
178
  compose Compose Knowledge Images, packs, and attestations offline
176
179
  attest Create or verify a composition attestation
177
180
  cortex Bounded evidence retrieval with a verifiable trace
181
+ flight Pack a flight-events-1 document into a V5 Knowledge Image
182
+ skill Create and share a Knolo skill pack
183
+ setup Prepare a local index and print the hub next steps
178
184
  icp Scaffold and operate an ICP-native knowledge canister
179
185
 
180
186
  Global options:
@@ -214,6 +220,40 @@ function printCommandHelp(command) {
214
220
  compose: 'Usage: knolo compose <create|verify|excerpt|query> [options]\n\nOffline composition of Knowledge Images, packs, KAR sidecars, Domain Packs, and bounded excerpts.\n\n knolo compose create --members members.json --out composition/ [--host knowledge.knolo] [--availability material|digest] [--policy policy.json]\n knolo compose verify composition/ [--require-material] [--member alias=path]\n knolo compose excerpt --image knowledge.knolo --object <id> --out excerpt.json\n knolo compose query --composition composition/ --eql "FROM chunk SEARCH \\"valve\\"" [--question "..."] [--json]\n\ncompositionRoot does not change a V5 state root or a KAR root. statement used means a query or evaluation ran. It does not mean SATISFIED, and it does not mean a claim is true or false. Hub attested: true is a listing flag and is not an attestation root. The CLI does not fetch Hub.',
215
221
  attest: 'Usage: knolo attest <create|verify> [options]\n\n knolo attest create --composition composition/composition.json --subject composition --statement mount|used|cited --out attestation.json [--sign-key key.json --principal <id> --key-id <id> --issued-at <ms> --expires-at <ms>]\n knolo attest verify attestation.json [--composition composition.json] [--public-key key.json] [--now <ms>] [--content-only]\n\nstatement used means a query or KAR evaluation ran. It does not mean the claim is true or false. Hub attested: true is unrelated. Signing uses the supplied times. The CLI does not read the clock.',
216
222
  cortex: 'Usage: knolo cortex <find|ask|explain|verify> [options]\n\nBounded evidence retrieval. The CLI does not call a model.\n\n knolo cortex find <image> --question "..." [--top-k 100] --out candidates.json\n knolo cortex ask <image> --question "..." [--graph file] [--plan plan.json] [--mode find_relevant] --out answer.cortex.json\n knolo cortex explain answer.cortex.json\n knolo cortex verify --image <image> --answer answer.cortex.json [--graph file]\n\nVerification recomputes the trace roots. It does not mean the answer is true. A KAR status of SATISFIED means evidence requirements were met. It does not mean a claim is proven.',
223
+ flight: 'Usage: knolo flight ingest --events <events.json> --log <file> --out <image.knolo> [--json]\n\nPacks a flight-events-1 document into a V5 Knowledge Image. The command hashes --log and writes nothing when the hash or byte length differs. It does not decode the log. log.format is ulog, dataflash, or rosbag2. A later KAR status of SATISFIED means evidence requirements were met. It does not mean a flight was safe, legal, or compliant.',
224
+ setup: `Usage: knolo setup [--registry <url>] [--root <dir>] [--agent <id>] [--auto-rebuild] [--reset] [--skills <path>] [--managed-skills <path>] [--memory <path>] [--transcripts <path>]
225
+
226
+ Resolves the hub the same way as the other hub commands.
227
+ Scans ~/.grok when that directory exists, plus every --root.
228
+ Runs knolo-box init --tolerant, then knolo-box build --config.
229
+ Keeps an existing config and rebuilds it. Pass --reset to replace it.
230
+ Always includes ~/.knolo/skills as a skills root.
231
+ Pass --agent to set the caller name. Pass --auto-rebuild to rebuild on the next search.
232
+ Does not prompt. Does not install a pack or call the hub.
233
+ A missing knolo-box is printed, and setup still exits 0.
234
+ When more than one skill folder matches, pass --skills or --managed-skills.`,
235
+ skill: `Usage: knolo skill <init|check|pack|publish|install|search|info|list>
236
+
237
+ knolo skill init <name> [--dir <parent>]
238
+ knolo skill check [<dir>] [--query <text>] [--json]
239
+ knolo skill pack [<dir>] [--out <dir>] [--json]
240
+ knolo skill publish <pack> [--registry <url>] [--json]
241
+ knolo skill publish <pack> --hub <dir> [--json]
242
+ knolo skill install <publisher>/<name> [--dir <path>] [--registry <url>] [--json]
243
+ knolo skill install <name> --hub <dir> [--dir <path>] [--json]
244
+ knolo skill install --pack <pack> [--dir <path>] [--json]
245
+ knolo skill search <query> [--registry <url>] [--json]
246
+ knolo skill info <publisher>/<name> [--registry <url>] [--json]
247
+ knolo skill list [--registry <url>] [--json]
248
+
249
+ Creates a skill pack for the host agent. Knolo does not call a model.
250
+ A receipt is an integrity check. It does not mean a claim is proven.
251
+ SATISFIED means the evidence requirements were met.
252
+ publish sends the pack to https://hub.knolo.dev unless --hub names a
253
+ local directory. install checks the pin before it writes. It writes into
254
+ --dir, or into ~/.knolo/skills when --dir is omitted. A pin mismatch
255
+ writes nothing. list prints every publisher/name from the public catalog.
256
+ The hub stores packs and does not answer questions.`,
217
257
  'verify-attestation': 'Usage: knolo verify-attestation <attestation.json> [--composition composition.json] [--public-key key.json] [--keyring keyring.json] [--now <ms>] [--content-only]\n\nSame check as knolo attest verify. A bad signature exits 1 even when the content root matches. --content-only checks the content root and ignores the envelope. statement used is not a truth verdict. Hub attested: true is unrelated.',
218
258
  icp: 'Usage: knolo icp <command> [options]',
219
259
  'semantic:index':
@@ -1872,6 +1912,18 @@ async function main() {
1872
1912
  const { runCortexCli } = await import('./cortex.mjs');
1873
1913
  return runCortexCli(commandArgs);
1874
1914
  }
1915
+ if (command === 'flight') {
1916
+ const { runFlightCli } = await import('./flight.mjs');
1917
+ return runFlightCli(commandArgs);
1918
+ }
1919
+ if (command === 'skill') {
1920
+ const { runSkillCli } = await import('./skill.mjs');
1921
+ return runSkillCli(commandArgs);
1922
+ }
1923
+ if (command === 'setup') {
1924
+ const { runKnoloSetup } = await import('./setup.mjs');
1925
+ return runKnoloSetup(commandArgs);
1926
+ }
1875
1927
  if (command === 'init') return await cmdInit();
1876
1928
  if (command === 'add') return await cmdAdd(commandArgs);
1877
1929
  if (command === 'search') return await runHubSearch(commandArgs);