@msareen/knowledge-hub-builder 0.2.2 → 0.2.3

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/scripts/hubs.ts CHANGED
@@ -31,10 +31,14 @@ import {
31
31
  } from "./lib/registry";
32
32
  import { takeFlag, takeOpt, rejectUnknownFlags } from "./lib/args";
33
33
  import { detail, section, totalElapsed } from "./lib/log";
34
+ import { paint, paintErr } from "./lib/color";
34
35
 
35
36
  const cmd = process.env.KHB_SUBCOMMAND ?? "go";
36
37
  const argv = process.argv.slice(2);
37
38
 
39
+ /** Plural on purpose: dropping several dead shortcuts after a reorg is the common case. */
40
+ const FORGET_USAGE = "khb forget <name|path> [more…]";
41
+
38
42
 
39
43
  const ago = (iso?: string): string => {
40
44
  if (!iso) return "";
@@ -44,13 +48,16 @@ const ago = (iso?: string): string => {
44
48
  };
45
49
 
46
50
  function printList(hubs: HubEntry[]): void {
47
- const w = Math.max(4, ...hubs.map((h) => h.name.length));
48
- hubs.forEach((h, i) => {
49
- const live = isAlive(h);
50
- const tag = live ? String(i + 1).padStart(2) : " x";
51
- const note = live ? ago(h.lastUsed) : "MISSING";
52
- console.log(` ${tag} ${h.name.padEnd(w)} ${h.description}`);
53
- console.log(` ${" ".repeat(w)} ${h.path}${note ? ` (${note})` : ""}`);
51
+ const nameWidth = Math.max(4, ...hubs.map((hub) => hub.name.length));
52
+ hubs.forEach((hub, index) => {
53
+ const live = isAlive(hub);
54
+ // Pad first, paint second, always: an escape sequence has width in the string and none
55
+ // on the screen, so padding an already-coloured value misaligns the column.
56
+ const tag = live ? paint.cmd(String(index + 1).padStart(2)) : paint.bad(" x");
57
+ const used = ago(hub.lastUsed);
58
+ const note = live ? (used ? paint.dim(` (${used})`) : "") : ` ${paint.bad("(MISSING)")}`;
59
+ console.log(` ${tag} ${paint.name(hub.name.padEnd(nameWidth))} ${hub.description}`);
60
+ console.log(` ${" ".repeat(nameWidth)} ${paint.path(hub.path)}${note}`);
54
61
  });
55
62
  }
56
63
 
@@ -59,10 +66,10 @@ function printList(hubs: HubEntry[]): void {
59
66
  * The interactive path is `wizard()`; this is its non-TTY twin.
60
67
  */
61
68
  function noHubs(): never {
62
- console.log(`No hubs registered on this machine (${CONFIG}).\n`);
63
- console.log(`Create one: khb init <dir>`);
64
- console.log(`Or register an existing: khb go <dir> (any path holding a khb.json)`);
65
- console.log(`\nRun 'khb' on a terminal and it walks you through the first one.`);
69
+ console.log(`No hubs registered on this machine ${paint.path(`(${CONFIG})`)}.\n`);
70
+ console.log(`Create one: ${paint.cmd("khb init <dir>")}`);
71
+ console.log(`Or register an existing: ${paint.cmd("khb go <dir>")} ${paint.dim("(any path holding a khb.json)")}`);
72
+ console.log(`\nRun '${paint.cmd("khb")}' on a terminal and it walks you through the first one.`);
66
73
  process.exit(0);
67
74
  }
68
75
 
@@ -188,17 +195,18 @@ function finish(stdin: typeof process.stdin, line: string): string {
188
195
  */
189
196
  function launch(hub: HubEntry, agentName?: string, noAgent = false): never {
190
197
  touchHub(hub.path);
191
- console.log(`\n${hub.name} — ${hub.path}`);
192
- console.log(` cd ${/\s/.test(hub.path) ? JSON.stringify(hub.path) : hub.path}`);
198
+ console.log(`\n${paint.name(hub.name)} — ${paint.path(hub.path)}`);
199
+ console.log(` ${paint.cmd(`cd ${/\s/.test(hub.path) ? JSON.stringify(hub.path) : hub.path}`)}`);
193
200
 
194
201
  const cfg = loadConfig();
195
202
  const agent = noAgent ? undefined : agentFor(cfg, agentName);
196
203
  if (!agent) {
197
- if (!noAgent) console.log(`\nNo default agent set — khb agent <claude|codex> to set one.`);
204
+ if (!noAgent)
205
+ console.log(`\nNo default agent set — ${paint.cmd("khb agent <claude|codex>")} to set one.`);
198
206
  process.exit(0);
199
207
  }
200
208
 
201
- console.log(` launching ${agent.name}…\n`);
209
+ console.log(` ${paint.dim(`launching ${agent.name}…`)}\n`);
202
210
  // shell:true on Windows so PATH shims (claude.cmd, codex.cmd) resolve; stdio inherited
203
211
  // so the agent owns the terminal from here.
204
212
  const r = spawnSync(agent.spec.command, agent.spec.args ?? [], {
@@ -207,8 +215,10 @@ function launch(hub: HubEntry, agentName?: string, noAgent = false): never {
207
215
  shell: process.platform === "win32",
208
216
  });
209
217
  if (r.error) {
210
- console.error(`\nCould not launch ${agent.name} (${agent.spec.command}): ${r.error.message}`);
211
- console.error(`Fix the command: khb agent ${agent.name} --command <exe>`);
218
+ console.error(
219
+ `\n${paintErr.bad(`Could not launch ${agent.name}`)} (${agent.spec.command}): ${r.error.message}`,
220
+ );
221
+ console.error(`Fix the command: ${paintErr.cmd(`khb agent ${agent.name} --command <exe>`)}`);
212
222
  process.exit(1);
213
223
  }
214
224
  process.exit(r.status ?? 0);
@@ -248,7 +258,7 @@ function askWith(question: string, fallback: string): string {
248
258
 
249
259
  async function wizard(): Promise<never> {
250
260
  const cfg = loadConfig();
251
- console.log(`khb ${version()} — Knowledge Hub Builder\n`);
261
+ console.log(`${paint.head(`khb ${version()}`)} — Knowledge Hub Builder\n`);
252
262
  console.log(`No hubs on this machine yet. Let's set one up.\n`);
253
263
  console.log(`A hub is a folder of plain markdown that holds your knowledge. khb does the`);
254
264
  console.log(`mechanical half — pulling documents in, extracting text, checking structure —`);
@@ -257,34 +267,38 @@ async function wizard(): Promise<never> {
257
267
  // 1. Where. Default under the home directory rather than cwd: a hub is long-lived, and
258
268
  // the folder someone happens to be standing in when they first run khb rarely is.
259
269
  const suggested = join(homedir(), "knowledge");
260
- const dir = resolve(askWith(`1/5 Where should the hub live?`, suggested));
270
+ const dir = resolve(askWith(`${paint.head("1/5")} Where should the hub live?`, suggested));
261
271
 
262
272
  if (markerIn(dir)) {
263
273
  // Already a hub — the machine simply had not heard of it. Adopt, do not re-create.
264
274
  const entry = registerHub(dir);
265
- console.log(`\n${dir} is already a hub — added it to the list as "${entry.name}".`);
266
- console.log(`Open it: khb`);
275
+ console.log(
276
+ `\n${paint.path(dir)} is already a hub — added it to the list as ${paint.name(`"${entry.name}"`)}.`,
277
+ );
278
+ console.log(`Open it: ${paint.cmd("khb")}`);
267
279
  process.exit(0);
268
280
  }
269
281
  if (existsSync(dir) && readdirSync(dir).length) {
270
- const ok = askWith(` ${dir} is not empty. Put the hub there anyway? [y/N]`, "n");
282
+ const ok = askWith(` ${paint.warn(`${dir} is not empty.`)} Put the hub there anyway? [y/N]`, "n");
271
283
  if (!/^y/i.test(ok)) {
272
- console.log(`\nStopped. Nothing was created. Re-run 'khb' to start again.`);
284
+ console.log(`\nStopped. Nothing was created. Re-run '${paint.cmd("khb")}' to start again.`);
273
285
  process.exit(0);
274
286
  }
275
287
  }
276
288
 
277
289
  // 2-3. Identity. Both land in the hub's own khb.json, so they travel with the folder.
278
- const name = askWith(`2/5 What should it be called?`, basename(dir) || "knowledge");
279
- const description = askWith(`3/5 One line describing it (optional)?`, "");
290
+ const name = askWith(`${paint.head("2/5")} What should it be called?`, basename(dir) || "knowledge");
291
+ const description = askWith(`${paint.head("3/5")} One line describing it (optional)?`, "");
280
292
 
281
293
  // 4. The agent. Detected rather than asked blind — being offered a tool you do not have
282
294
  // installed is a worse first question than being told which one was found.
283
295
  const found = ["claude", "codex"].filter(onPath);
284
- console.log(`\n4/5 Which agent should 'khb' open the hub with?`);
285
- for (const [i, k] of Object.keys(cfg.agents).entries())
286
- console.log(` ${i + 1}) ${k}${found.includes(k) ? " (found on PATH)" : ""}`);
287
- console.log(` or type any command, or 'none' to just print the path`);
296
+ console.log(`\n${paint.head("4/5")} Which agent should '${paint.cmd("khb")}' open the hub with?`);
297
+ for (const [index, agentKey] of Object.keys(cfg.agents).entries())
298
+ console.log(
299
+ ` ${paint.cmd(String(index + 1))}) ${agentKey}${found.includes(agentKey) ? paint.ok(" (found on PATH)") : ""}`,
300
+ );
301
+ console.log(paint.dim(` or type any command, or 'none' to just print the path`));
288
302
  const agentAnswer = askWith(` Choice?`, found[0] ?? "none");
289
303
  const keys = Object.keys(cfg.agents);
290
304
  const picked =
@@ -292,7 +306,9 @@ async function wizard(): Promise<never> {
292
306
 
293
307
  // 5. A first bundle, because an empty hub has nowhere to put anything and the next
294
308
  // question a new user hits is where material goes. Skippable with '-'.
295
- console.log(`\n5/5 A bundle is a unit of ownership — you, a team, a client, a project.`);
309
+ console.log(
310
+ `\n${paint.head("5/5")} A bundle is a unit of ownership — you, a team, a client, a project.`,
311
+ );
296
312
  const bundle = askWith(` Name your first one ('-' to skip)?`, "personal");
297
313
  const scope =
298
314
  bundle === "-" ? "" : askWith(` One line on what it covers?`, "TODO scope");
@@ -300,7 +316,7 @@ async function wizard(): Promise<never> {
300
316
  // ---- act ----
301
317
  const { createHub } = await import("./lib/create");
302
318
  const { hub, entry } = createHub(dir, { name, description });
303
- console.log(`\nCreated ${hub}`);
319
+ console.log(`\n${paint.ok("Created")} ${paint.path(hub)}`);
304
320
 
305
321
  // scaffold.ts resolves the hub through util.ts at import time, so the hub must be
306
322
  // announced before it is loaded — hence the import inside the branch, not at the top.
@@ -310,11 +326,13 @@ async function wizard(): Promise<never> {
310
326
  const { createBundle, VALID_NAME } = await import("./lib/scaffold");
311
327
  if (VALID_NAME.test(bundle)) {
312
328
  createBundle(bundle, scope);
313
- console.log(` bundles/${bundle}/ — registered in outer.index.md`);
329
+ console.log(` ${paint.name(`bundles/${bundle}/`)} — registered in outer.index.md`);
314
330
  madeBundle = true;
315
331
  } else {
316
- console.log(` skipped the bundle: '${bundle}' is not a valid name (lowercase, digits, hyphens)`);
317
- console.log(` add one later: khb new-bundle <name> "<scope>"`);
332
+ console.log(
333
+ ` ${paint.warn("skipped the bundle:")} '${bundle}' is not a valid name (lowercase, digits, hyphens)`,
334
+ );
335
+ console.log(` add one later: ${paint.cmd('khb new-bundle <name> "<scope>"')}`);
318
336
  }
319
337
  }
320
338
 
@@ -325,11 +343,15 @@ async function wizard(): Promise<never> {
325
343
  cfg.defaultAgent = picked;
326
344
  }
327
345
  saveConfig(cfg);
328
- console.log(` registered as "${entry.name}", agent: ${cfg.defaultAgent || "none"}`);
346
+ console.log(` registered as ${paint.name(`"${entry.name}"`)}, agent: ${cfg.defaultAgent || "none"}`);
329
347
 
330
- console.log(`\nFrom any terminal, 'khb' comes back here${cfg.defaultAgent ? ` and starts ${cfg.defaultAgent}` : ""}.`);
348
+ console.log(
349
+ `\nFrom any terminal, '${paint.cmd("khb")}' comes back here${cfg.defaultAgent ? ` and starts ${cfg.defaultAgent}` : ""}.`,
350
+ );
331
351
  if (madeBundle)
332
- console.log(`Next: add sources to bundles/${bundle}/sources.yaml, then 'khb ingest ${bundle}'.`);
352
+ console.log(
353
+ `Next: add sources to ${paint.path(`bundles/${bundle}/sources.yaml`)}, then ${paint.cmd(`khb ingest ${bundle}`)}.`,
354
+ );
333
355
 
334
356
  const go = askWith(`\nOpen it now?`, "y");
335
357
  if (!/^y/i.test(go)) process.exit(0);
@@ -348,12 +370,19 @@ if (cmd === "list") {
348
370
  }
349
371
  if (!hubs.length) noHubs();
350
372
 
351
- console.log(`Hubs on this machine (${CONFIG})\n`);
373
+ console.log(`${paint.head("Hubs on this machine")} ${paint.path(`(${CONFIG})`)}\n`);
352
374
  printList(hubs);
353
375
  const cfg = loadConfig();
354
376
  const agent = agentFor(cfg);
355
- console.log(`\nDefault agent: ${agent ? `${agent.name} (${agent.spec.command})` : "none"}`);
356
- console.log(`Open one: khb go ${hubs[0].name} | khb go 1 | khb`);
377
+ console.log(
378
+ `\nDefault agent: ${agent ? `${paint.name(agent.name)} ${paint.dim(`(${agent.spec.command})`)}` : "none"}`,
379
+ );
380
+ console.log(
381
+ `Open one: ${paint.cmd(`khb go ${hubs[0].name}`)} | ${paint.cmd("khb go 1")} | ${paint.cmd("khb")}`,
382
+ );
383
+ console.log(
384
+ `Drop one: ${paint.cmd("khb forget <name>")} ${paint.dim("(or several; the folders are left untouched)")}`,
385
+ );
357
386
  process.exit(0);
358
387
  }
359
388
 
@@ -402,8 +431,11 @@ function repairPaths(newPath: string, fromOpt: string | undefined, dryRun: boole
402
431
  oldPath = resolve(certain[0].path);
403
432
  oldSpellings.push(certain[0].path);
404
433
  } else if (certain.length > 1) {
405
- console.error(`Several registered hubs share this hub's identity — name the old path:`);
406
- for (const h of certain) console.error(` khb update ${newPath} --path --from ${h.path}`);
434
+ console.error(
435
+ paintErr.warn(`Several registered hubs share this hub's identity name the old path:`),
436
+ );
437
+ for (const candidate of certain)
438
+ console.error(` ${paintErr.cmd(`khb update ${newPath} --path --from ${candidate.path}`)}`);
407
439
  process.exit(1);
408
440
  } else if (likely.length === 1) {
409
441
  // A name match is circumstantial — hubs registered before khb recorded an identity
@@ -411,18 +443,21 @@ function repairPaths(newPath: string, fromOpt: string | undefined, dryRun: boole
411
443
  const guess = likely[0];
412
444
  const yes = ask(`Was this hub at ${guess.path} (registered as "${guess.name}")? [y/N] `);
413
445
  if (yes === undefined) {
414
- console.error(`This hub carries no identity stamp, so the old path cannot be proven.`);
415
- console.error(`Likely: ${guess.path}`);
416
- console.error(`Confirm it: khb update --path --from ${guess.path}`);
446
+ console.error(paintErr.warn(`This hub carries no identity stamp, so the old path cannot be proven.`));
447
+ console.error(`Likely: ${paintErr.path(guess.path)}`);
448
+ console.error(`Confirm it: ${paintErr.cmd(`khb update --path --from ${guess.path}`)}`);
417
449
  process.exit(1);
418
450
  }
419
451
  if (!/^y/i.test(yes.trim())) process.exit(0);
420
452
  oldPath = resolve(guess.path);
421
453
  oldSpellings.push(guess.path);
422
454
  } else if (likely.length > 1) {
423
- console.error(`No registered hub matches this one's identity, but these went missing:`);
424
- for (const h of likely) console.error(` ${h.name} ${h.path}`);
425
- console.error(`\nIf one of those is this hub, say so: khb update --path --from <old-path>`);
455
+ console.error(paintErr.warn(`No registered hub matches this one's identity, but these went missing:`));
456
+ for (const candidate of likely)
457
+ console.error(` ${paintErr.name(candidate.name)} ${paintErr.path(candidate.path)}`);
458
+ console.error(
459
+ `\nIf one of those is this hub, say so: ${paintErr.cmd("khb update --path --from <old-path>")}`,
460
+ );
426
461
  process.exit(1);
427
462
  }
428
463
  }
@@ -439,9 +474,11 @@ function repairPaths(newPath: string, fromOpt: string | undefined, dryRun: boole
439
474
  }
440
475
 
441
476
  if (sameLocation(oldPath, newPath)) {
442
- console.error(`Old and new paths are the same directory:`);
443
- console.error(` ${newPath}`);
444
- console.error(`There is no move to repair. Name the real old path: khb update --path --from <old-path>`);
477
+ console.error(paintErr.bad(`Old and new paths are the same directory:`));
478
+ console.error(` ${paintErr.path(newPath)}`);
479
+ console.error(
480
+ `There is no move to repair. Name the real old path: ${paintErr.cmd("khb update --path --from <old-path>")}`,
481
+ );
445
482
  process.exit(1);
446
483
  }
447
484
 
@@ -469,9 +506,10 @@ function repairPaths(newPath: string, fromOpt: string | undefined, dryRun: boole
469
506
  detail(`no old-path references in ${scanned} text file(s) — nothing inside the hub to fix`);
470
507
  } else {
471
508
  detail(`${total} reference(s) in ${hits.length} of ${scanned} text file(s):`);
472
- for (const h of hits) console.log(` ${String(h.count).padStart(3)} ${h.file}`);
509
+ for (const hit of hits) console.log(` ${paint.warn(String(hit.count).padStart(3))} ${hit.file}`);
473
510
  }
474
- for (const f of failed) console.error(` could not write ${f.file}: ${f.reason}`);
511
+ for (const failure of failed)
512
+ console.error(` ${paintErr.bad("could not write")} ${failure.file}: ${failure.reason}`);
475
513
 
476
514
  if (dryRun) {
477
515
  detail(`nothing was written`);
@@ -497,17 +535,17 @@ function repairSchema(hub: string, dryRun: boolean): void {
497
535
  detail(`sources.yaml already current in every bundle`);
498
536
  return;
499
537
  }
500
- const totalFields = diffs.reduce((n, d) => n + d.changes.length, 0);
538
+ const totalFields = diffs.reduce((total, diff) => total + diff.changes.length, 0);
501
539
  detail(`${totalFields} field(s) across ${diffs.length} bundle(s):`);
502
- for (const d of diffs) {
503
- console.log(` ${relative(hub, d.path)}`);
504
- for (const c of d.changes) console.log(` ${c}`);
540
+ for (const diff of diffs) {
541
+ console.log(` ${paint.name(relative(hub, diff.path))}`);
542
+ for (const change of diff.changes) console.log(` ${paint.ok(change)}`);
505
543
  }
506
544
  if (dryRun) {
507
545
  detail(`nothing was written`);
508
546
  return;
509
547
  }
510
- for (const d of diffs) applySourcesDiff(d);
548
+ for (const diff of diffs) applySourcesDiff(diff);
511
549
  detail(`${diffs.length} file(s) updated`);
512
550
  }
513
551
 
@@ -523,14 +561,16 @@ if (cmd === "update") {
523
561
  // The hub is wherever it is now: the path given, else the hub containing cwd.
524
562
  const newPath = canonical(dest ?? findHub() ?? process.cwd());
525
563
  if (!markerIn(newPath)) {
526
- console.error(`Not a KHB hub: ${newPath}`);
527
- console.error(`Run 'khb update' from inside the moved hub, or name it: khb update <new-path>`);
564
+ console.error(`${paintErr.bad("Not a KHB hub:")} ${paintErr.path(newPath)}`);
565
+ console.error(
566
+ `Run '${paintErr.cmd("khb update")}' from inside the moved hub, or name it: ${paintErr.cmd("khb update <new-path>")}`,
567
+ );
528
568
  process.exit(1);
529
569
  }
530
570
 
531
571
  // State the whole plan before doing any of it — same contract as ingest: a command that
532
572
  // rewrites files says what it is about to rewrite, and where, before the first write.
533
- console.log(`khb update${dryRun ? " (dry run)" : ""}`);
573
+ console.log(`${paint.head("khb update")}${dryRun ? paint.dim(" (dry run)") : ""}`);
534
574
 
535
575
  let failed = false;
536
576
  if (both || doPath) {
@@ -542,28 +582,69 @@ if (cmd === "update") {
542
582
  repairSchema(newPath, dryRun);
543
583
  }
544
584
 
545
- console.log(`\ndone in ${totalElapsed()}`);
546
- if (dryRun) console.log(`Re-run without --dry-run to apply.`);
547
- else console.log(`Next: khb lint`);
585
+ console.log(`\n${failed ? paint.warn("done") : paint.ok("done")} in ${totalElapsed()}`);
586
+ if (dryRun) console.log(`Re-run without ${paint.cmd("--dry-run")} to apply.`);
587
+ else console.log(`Next: ${paint.cmd("khb lint")}`);
548
588
  process.exit(failed ? 1 : 0);
549
589
  }
550
590
 
551
591
  // --------------------------------------------------------------------------- forget
552
592
 
553
593
  if (cmd === "forget") {
554
- rejectUnknownFlags(argv, "khb forget <name|path>");
555
- const [what] = argv;
556
- if (!what) {
557
- console.error(`Usage: khb forget <name|path>`);
558
- console.error(`Removes the shortcut only — the hub folder is left untouched.`);
594
+ rejectUnknownFlags(argv, FORGET_USAGE);
595
+ // Naming a hub means knowing what it is called here, and the name in the shortcut list
596
+ // is not always the folder's — so the answer to a bare `khb forget` is where to look it
597
+ // up, not just the usage line it already failed to satisfy.
598
+ if (!argv.length) {
599
+ console.error(`Usage: ${paintErr.cmd(FORGET_USAGE)}`);
600
+ console.error(`See the names of every hub on this machine: ${paintErr.cmd("khb list")}`);
601
+ console.error(paintErr.dim(`Removes shortcuts only — the hub folders are left untouched.`));
559
602
  process.exit(1);
560
603
  }
561
- const gone = forgetHub(findHubEntry(what)?.path ?? what);
562
- if (!gone) {
563
- console.error(`Not registered: ${what}`);
604
+
605
+ // Resolve every target BEFORE removing any of them. A target may be a list position
606
+ // (`khb forget 2`), and positions renumber the moment the first entry is gone — resolving
607
+ // as we went would drop the wrong hub from the second name onward. Two spellings of one
608
+ // hub collapse here as well, so `khb forget work D:\work` is one removal rather than one
609
+ // removal and a mystery "not registered".
610
+ const seen = new Set<string>();
611
+ const targets: { asked: string; entry?: HubEntry }[] = [];
612
+ for (const asked of argv) {
613
+ const entry = findHubEntry(asked);
614
+ const key = entry?.path ?? asked;
615
+ if (seen.has(key)) continue;
616
+ seen.add(key);
617
+ targets.push({ asked, entry });
618
+ }
619
+
620
+ const gone: HubEntry[] = [];
621
+ const unknown: string[] = [];
622
+ for (const target of targets) {
623
+ // Best effort, like `rm`: a typo among five names should not cost you the other four,
624
+ // and forgetting is reversible — any khb command run inside a hub re-registers it.
625
+ const dropped = target.entry && forgetHub(target.entry.path);
626
+ if (dropped) gone.push(dropped);
627
+ else unknown.push(target.asked);
628
+ }
629
+
630
+ if (gone.length === 1) {
631
+ const [only] = gone;
632
+ console.log(
633
+ `${paint.ok("Forgot")} ${paint.name(only.name)} ${paint.path(`(${only.path})`)}. The folder itself is untouched.`,
634
+ );
635
+ } else if (gone.length) {
636
+ console.log(`${paint.ok(`Forgot ${gone.length} shortcuts`)}:`);
637
+ const nameWidth = Math.max(...gone.map((entry) => entry.name.length));
638
+ for (const entry of gone)
639
+ console.log(` ${paint.name(entry.name.padEnd(nameWidth))} ${paint.path(entry.path)}`);
640
+ console.log(paint.dim(`The folders themselves are untouched.`));
641
+ }
642
+
643
+ for (const name of unknown) console.error(`${paintErr.bad("Not registered:")} ${name}`);
644
+ if (unknown.length) {
645
+ console.error(`Registered hubs: ${paintErr.cmd("khb list")}`);
564
646
  process.exit(1);
565
647
  }
566
- console.log(`Forgot ${gone.name} (${gone.path}). The folder itself is untouched.`);
567
648
  process.exit(0);
568
649
  }
569
650
 
@@ -578,20 +659,27 @@ if (cmd === "agent") {
578
659
 
579
660
  if (!name && !command) {
580
661
  const cur = agentFor(cfg);
581
- console.log(`Default agent: ${cur ? `${cur.name} (${cur.spec.command})` : "none"}`);
582
- console.log(`\nKnown:`);
583
- for (const [k, v] of Object.entries(cfg.agents))
584
- console.log(` ${k === cfg.defaultAgent ? "*" : " "} ${k.padEnd(8)} ${[v.command, ...(v.args ?? [])].join(" ")}`);
585
- console.log(`\nSet: khb agent codex`);
586
- console.log(` khb agent claude --command claude --args "--continue"`);
587
- console.log(`Off: khb agent none (khb go just prints the path)`);
662
+ console.log(
663
+ `Default agent: ${cur ? `${paint.name(cur.name)} ${paint.dim(`(${cur.spec.command})`)}` : "none"}`,
664
+ );
665
+ console.log(`\n${paint.head("Known")}:`);
666
+ for (const [agentKey, agentSpec] of Object.entries(cfg.agents))
667
+ console.log(
668
+ ` ${agentKey === cfg.defaultAgent ? paint.ok("*") : " "} ${paint.name(agentKey.padEnd(8))} ` +
669
+ paint.dim([agentSpec.command, ...(agentSpec.args ?? [])].join(" ")),
670
+ );
671
+ console.log(`\nSet: ${paint.cmd("khb agent codex")}`);
672
+ console.log(` ${paint.cmd('khb agent claude --command claude --args "--continue"')}`);
673
+ console.log(
674
+ `Off: ${paint.cmd("khb agent none")} ${paint.dim("(khb go just prints the path)")}`,
675
+ );
588
676
  process.exit(0);
589
677
  }
590
678
 
591
679
  if (name === "none") {
592
680
  cfg.defaultAgent = "";
593
681
  saveConfig(cfg);
594
- console.log(`Default agent cleared — khb go will print the hub path and stop.`);
682
+ console.log(`Default agent cleared — ${paint.cmd("khb go")} will print the hub path and stop.`);
595
683
  process.exit(0);
596
684
  }
597
685
 
@@ -604,8 +692,10 @@ if (cmd === "agent") {
604
692
  cfg.defaultAgent = key;
605
693
  saveConfig(cfg);
606
694
  const spec = cfg.agents[key];
607
- console.log(`Default agent: ${key} — ${[spec.command, ...(spec.args ?? [])].join(" ")}`);
608
- console.log(`Saved to ${CONFIG}`);
695
+ console.log(
696
+ `Default agent: ${paint.name(key)} ${paint.dim([spec.command, ...(spec.args ?? [])].join(" "))}`,
697
+ );
698
+ console.log(paint.dim(`Saved to ${CONFIG}`));
609
699
  process.exit(0);
610
700
  }
611
701
 
@@ -623,13 +713,15 @@ if (what) {
623
713
  let entry = findHubEntry(what);
624
714
  if (!entry && existsSync(resolve(what)) && markerIn(resolve(what))) entry = registerHub(resolve(what));
625
715
  if (!entry) {
626
- console.error(`No such hub: ${what}`);
627
- console.error(`Registered hubs: khb list`);
716
+ console.error(`${paintErr.bad("No such hub:")} ${what}`);
717
+ console.error(`Registered hubs: ${paintErr.cmd("khb list")}`);
628
718
  process.exit(1);
629
719
  }
630
720
  if (!isAlive(entry)) {
631
- console.error(`${entry.name} is registered at ${entry.path}, but that is no longer a hub.`);
632
- console.error(`Drop the shortcut: khb forget ${entry.name}`);
721
+ console.error(
722
+ `${paintErr.name(entry.name)} is registered at ${paintErr.path(entry.path)}, but ${paintErr.bad("that is no longer a hub")}.`,
723
+ );
724
+ console.error(`Drop the shortcut: ${paintErr.cmd(`khb forget ${entry.name}`)}`);
633
725
  process.exit(1);
634
726
  }
635
727
  if (wantPath) {
@@ -661,9 +753,9 @@ if (hubs.length === 1) {
661
753
  );
662
754
  if (answer === undefined) {
663
755
  // No terminal to ask on — say where it is and how to go there, then stop.
664
- console.log(`One hub registered:\n`);
756
+ console.log(`${paint.head("One hub registered")}:\n`);
665
757
  printList([only]);
666
- console.log(`\nOpen it: khb go ${only.name}`);
758
+ console.log(`\nOpen it: ${paint.cmd(`khb go ${only.name}`)}`);
667
759
  process.exit(0);
668
760
  }
669
761
  if (/^n/i.test(answer.trim())) process.exit(0);
@@ -671,21 +763,21 @@ if (hubs.length === 1) {
671
763
  }
672
764
 
673
765
  // More than one: show the list and take a pick.
674
- console.log(`Hubs on this machine (${CONFIG})\n`);
766
+ console.log(`${paint.head("Hubs on this machine")} ${paint.path(`(${CONFIG})`)}\n`);
675
767
  printList(hubs);
676
768
 
677
769
  if (wantPath) process.exit(0); // ambiguous: a script must name the hub it means
678
770
 
679
771
  const answer = ask(`\nOpen which? [1-${hubs.length}, Esc to cancel] `);
680
772
  if (answer === undefined) {
681
- console.log(`\nOpen one: khb go ${hubs[0].name} | khb go 1`);
773
+ console.log(`\nOpen one: ${paint.cmd(`khb go ${hubs[0].name}`)} | ${paint.cmd("khb go 1")}`);
682
774
  process.exit(0);
683
775
  }
684
776
  const picked = answer.trim();
685
777
  if (!picked) process.exit(0);
686
778
  const chosen = findHubEntry(picked);
687
779
  if (!chosen || !isAlive(chosen)) {
688
- console.error(`Not a listed hub: ${picked}`);
780
+ console.error(`${paintErr.bad("Not a listed hub:")} ${picked}`);
689
781
  process.exit(1);
690
782
  }
691
783
  launch(chosen, agentName, noAgent);
@@ -6,6 +6,7 @@ import { join, normPath } from "../lib/util";
6
6
  import type { Entry } from "../lib/ledger";
7
7
  import { acquireFile, newCounters, report, type Options } from "./acquire";
8
8
  import { detail, pos } from "../lib/log";
9
+ import { paintErr } from "../lib/color";
9
10
  import { makeExcluder } from "./exclude";
10
11
  import type { Source } from "./index";
11
12
 
@@ -17,7 +18,7 @@ export async function ingestFolder(
17
18
  opts: Options,
18
19
  ) {
19
20
  if (!existsSync(s.path)) {
20
- console.warn(` missing folder, skipped: ${s.path}`);
21
+ console.warn(` ${paintErr.warn("missing folder, skipped:")} ${s.path}`);
21
22
  return;
22
23
  }
23
24
 
@@ -15,6 +15,7 @@ import { detail, section, totalElapsed } from "../lib/log";
15
15
  import { bundleForIngest, listBundles, DEFAULT_BUNDLE } from "../lib/scaffold";
16
16
  import { readLedger, writeLedger } from "../lib/ledger";
17
17
  import { takeFlag, rejectUnknownFlags } from "../lib/args";
18
+ import { paint, paintErr } from "../lib/color";
18
19
  import { ingestFolder } from "./folder";
19
20
  import { ingestFiles } from "./files";
20
21
  import { ingestWeb } from "./web";
@@ -37,7 +38,7 @@ const opts: Options = {
37
38
  rejectUnknownFlags(argv, USAGE);
38
39
  const positional = argv;
39
40
  if (positional.length > 1) {
40
- console.error(`Usage: ${USAGE}`);
41
+ console.error(`Usage: ${paintErr.cmd(USAGE)}`);
41
42
  process.exit(1);
42
43
  }
43
44
  // No bundle named: which one owns the material is a human decision (AGENTS.md) and a CLI
@@ -49,10 +50,10 @@ if (!bundle) {
49
50
  const have = listBundles();
50
51
  const onlyLanding = have.length === 1 && have[0] === DEFAULT_BUNDLE;
51
52
  if (have.length && !onlyLanding) {
52
- console.error(`Usage: ${USAGE}`);
53
- console.error(`\nBundles in this hub: ${have.join(", ")}`);
53
+ console.error(`Usage: ${paintErr.cmd(USAGE)}`);
54
+ console.error(`\nBundles in this hub: ${have.map((name) => paintErr.name(name)).join(", ")}`);
54
55
  console.error(`Name the one that owns this material, or start a new one:`);
55
- console.error(` khb new-bundle <name> "<scope>"`);
56
+ console.error(` ${paintErr.cmd('khb new-bundle <name> "<scope>"')}`);
56
57
  process.exit(1);
57
58
  }
58
59
  bundle = DEFAULT_BUNDLE;
@@ -101,8 +102,10 @@ for (const [i, s] of sources.entries()) {
101
102
  }
102
103
  }
103
104
  if (!sources.length) {
104
- console.log(`${bundle}: no sources configured.`);
105
- console.log(`Declare them in bundles/${bundle}/sources.yaml, then re-run: khb ingest ${bundle}`);
105
+ console.log(`${paint.name(bundle)}: ${paint.warn("no sources configured")}.`);
106
+ console.log(
107
+ `Declare them in ${paint.path(`bundles/${bundle}/sources.yaml`)}, then re-run: ${paint.cmd(`khb ingest ${bundle}`)}`,
108
+ );
106
109
  process.exit(0);
107
110
  }
108
111
 
@@ -110,7 +113,7 @@ const entries = readLedger(dir);
110
113
 
111
114
  // State the whole plan before doing any of it: which hub (a --hub/$KHB_HUB run can target a
112
115
  // folder you did not expect), which bundle, and which of the expensive extractors are armed.
113
- console.log(`khb ingest → bundle '${bundle}'`);
116
+ console.log(`${paint.head("khb ingest")} → bundle ${paint.name(`'${bundle}'`)}`);
114
117
  detail(`hub: ${HUB}`);
115
118
  detail(`bundle: ${dir}`);
116
119
  detail(`sources: ${sources.length} declared in bundles/${bundle}/sources.yaml`);
@@ -128,7 +131,11 @@ for (const [i, s] of sources.entries()) {
128
131
  if (s.type === "folder") await ingestFolder(s, rawDir, dir, entries, opts);
129
132
  else if (s.type === "files") await ingestFiles(s, rawDir, dir, entries, opts);
130
133
  else if (s.type === "web") await ingestWeb(s, rawDir, dir, entries, opts);
131
- else console.warn(` '${(s as any).type}' has no scripted ingester — the agent pulls it via MCP/CLI (see skills/ingest/SKILL.md)`);
134
+ else console.warn(
135
+ paintErr.warn(
136
+ ` '${(s as any).type}' has no scripted ingester — the agent pulls it via MCP/CLI (see skills/ingest/SKILL.md)`,
137
+ ),
138
+ );
132
139
  }
133
140
 
134
141
  writeLedger(dir, entries, bundle);
@@ -136,8 +143,9 @@ writeLedger(dir, entries, bundle);
136
143
  const all = [...entries.values()];
137
144
  const pending = all.filter((e) => !e.raw).length;
138
145
  const uncurated = all.filter((e) => e.raw && !e.curated).length;
139
- console.log(`\ndone in ${totalElapsed()}`);
140
- console.log(`ledger: ${all.length} source(s) in ${join(dir, "log.md")}`);
141
- if (pending) console.log(` ${pending} with an empty 'raw' — not extracted; see the reasons above`);
142
- console.log(` ${uncurated} in raw/ but not yet cataloged (empty 'curated')`);
143
- console.log(`Next: catalog ${bundle} turn raw/ into concept docs (skills/catalog/SKILL.md).`);
146
+ console.log(`\n${paint.ok("done")} in ${totalElapsed()}`);
147
+ console.log(`ledger: ${all.length} source(s) in ${paint.path(join(dir, "log.md"))}`);
148
+ if (pending)
149
+ console.log(` ${paint.warn(String(pending))} with an empty 'raw' not extracted; see the reasons above`);
150
+ console.log(` ${uncurated ? paint.warn(String(uncurated)) : "0"} in raw/ but not yet cataloged (empty 'curated')`);
151
+ console.log(`Next: catalog ${paint.name(bundle)} — turn raw/ into concept docs (skills/catalog/SKILL.md).`);