@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/AGENTS.md +4 -3
- package/README.md +41 -1
- package/SPEC.md +21 -5
- package/package.json +1 -1
- package/scripts/cli.ts +45 -25
- package/scripts/config.ts +229 -0
- package/scripts/doctor.ts +37 -8
- package/scripts/export.ts +8 -4
- package/scripts/hubs.ts +184 -92
- package/scripts/ingest/folder.ts +2 -1
- package/scripts/ingest/index.ts +21 -13
- package/scripts/init.ts +21 -12
- package/scripts/lib/color.ts +75 -0
- package/scripts/lib/config-check.ts +364 -0
- package/scripts/lib/log.ts +4 -2
- package/scripts/lib/registry.ts +20 -1
- package/scripts/lint.ts +9 -4
- package/scripts/new-bundle.ts +9 -4
- package/scripts/visualize.ts +7 -6
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
|
|
48
|
-
hubs.forEach((
|
|
49
|
-
const live = isAlive(
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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)
|
|
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}
|
|
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(
|
|
211
|
-
|
|
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(
|
|
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(
|
|
266
|
-
|
|
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
|
|
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(
|
|
279
|
-
const description = askWith(
|
|
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(`\
|
|
285
|
-
for (const [
|
|
286
|
-
console.log(
|
|
287
|
-
|
|
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(
|
|
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(`\
|
|
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}
|
|
329
|
+
console.log(` ${paint.name(`bundles/${bundle}/`)} — registered in outer.index.md`);
|
|
314
330
|
madeBundle = true;
|
|
315
331
|
} else {
|
|
316
|
-
console.log(
|
|
317
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
356
|
-
|
|
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(
|
|
406
|
-
|
|
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
|
|
425
|
-
|
|
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(
|
|
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
|
|
509
|
+
for (const hit of hits) console.log(` ${paint.warn(String(hit.count).padStart(3))} ${hit.file}`);
|
|
473
510
|
}
|
|
474
|
-
for (const
|
|
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((
|
|
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
|
|
503
|
-
console.log(` ${relative(hub,
|
|
504
|
-
for (const
|
|
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
|
|
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(
|
|
527
|
-
console.error(
|
|
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(
|
|
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(`\
|
|
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,
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
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
|
-
|
|
562
|
-
|
|
563
|
-
|
|
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(
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
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(
|
|
608
|
-
|
|
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(
|
|
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(
|
|
632
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
780
|
+
console.error(`${paintErr.bad("Not a listed hub:")} ${picked}`);
|
|
689
781
|
process.exit(1);
|
|
690
782
|
}
|
|
691
783
|
launch(chosen, agentName, noAgent);
|
package/scripts/ingest/folder.ts
CHANGED
|
@@ -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
|
|
package/scripts/ingest/index.ts
CHANGED
|
@@ -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(
|
|
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(
|
|
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(
|
|
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(`\
|
|
140
|
-
console.log(`ledger: ${all.length} source(s) in ${join(dir, "log.md")}`);
|
|
141
|
-
if (pending)
|
|
142
|
-
console.log(` ${
|
|
143
|
-
console.log(`
|
|
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).`);
|