claude-second-brain 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -6
- package/bin/create.js +367 -32
- package/package.json +1 -1
- package/template/README.md +10 -3
package/README.md
CHANGED
|
@@ -245,16 +245,22 @@ All brains live under `~/.claude-second-brain/`. A central `config.toml` tracks
|
|
|
245
245
|
|
|
246
246
|
Run `npx claude-second-brain` again to create additional brains. Each gets its own folder, its own qmd index, and its own git remote — fully isolated. The global skills (`/brain-ingest`, `/brain-search`, `/brain-refresh`) always operate on the **default brain** declared in `config.toml`. The first brain you create is set as the default automatically.
|
|
247
247
|
|
|
248
|
-
Manage brains from the CLI:
|
|
248
|
+
Manage brains from the CLI (`csb` is the short alias — swap for `npx claude-second-brain` if not installed globally):
|
|
249
249
|
|
|
250
250
|
```bash
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
251
|
+
csb ls # list all brains (default marked with *)
|
|
252
|
+
csb use <name> # switch the default brain
|
|
253
|
+
csb rm <name> # remove a brain (deletes its folder + config entry)
|
|
254
|
+
csb path # print the default brain's directory
|
|
255
|
+
csb path qmd # print the default brain's qmd index path
|
|
256
|
+
csb path config # print ~/.claude-second-brain/config.toml
|
|
257
|
+
csb qmd query -c wiki "<terms>" # run qmd against the default brain
|
|
258
|
+
csb exec -- pnpm qmd:reindex # run any command inside the default brain
|
|
259
|
+
csb doctor # verify tools, config, and each brain
|
|
256
260
|
```
|
|
257
261
|
|
|
262
|
+
Every subcommand accepts `--help` for details (`csb rm --help`, `csb path --help`, …).
|
|
263
|
+
|
|
258
264
|
---
|
|
259
265
|
|
|
260
266
|
## Installing and updating skills
|
package/bin/create.js
CHANGED
|
@@ -425,20 +425,131 @@ function parseConfig(content) {
|
|
|
425
425
|
return { defaultBrain: defaultMatch ? defaultMatch[1] : null, brains, header }
|
|
426
426
|
}
|
|
427
427
|
|
|
428
|
-
function
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
428
|
+
function hasHelpFlag(args) {
|
|
429
|
+
return args.some(a => a === "--help" || a === "-h")
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
async function readConfigStatus() {
|
|
433
|
+
try {
|
|
434
|
+
const content = await readFile(CSB_CONFIG, "utf8")
|
|
435
|
+
const { defaultBrain, brains } = parseConfig(content)
|
|
436
|
+
return { defaultBrain, brains }
|
|
437
|
+
} catch {
|
|
438
|
+
return { defaultBrain: null, brains: [] }
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function printHelp(topic = "top") {
|
|
443
|
+
const c = pc.cyan
|
|
444
|
+
const topics = {
|
|
445
|
+
top: async () => {
|
|
446
|
+
const { defaultBrain, brains } = await readConfigStatus()
|
|
447
|
+
const lines = [
|
|
448
|
+
`${pc.bold("claude-second-brain")} v${version} ${pc.dim("(alias: csb)")}`,
|
|
449
|
+
"",
|
|
450
|
+
"Usage:",
|
|
451
|
+
` ${c("csb")} create a new brain (interactive)`,
|
|
452
|
+
` ${c("csb <name>")} create a new brain named <name>`,
|
|
453
|
+
` ${c("csb ls")} list all brains (default marked with *)`,
|
|
454
|
+
` ${c("csb use <name>")} set the default brain`,
|
|
455
|
+
` ${c("csb rm [<name>…]")} remove brains + their folders`,
|
|
456
|
+
` ${c("csb path [qmd|config|root]")} print a path (default: root)`,
|
|
457
|
+
` ${c("csb qmd [--brain N] <qmd args…>")} run qmd against the resolved brain`,
|
|
458
|
+
` ${c("csb exec [--brain N] -- <cmd…>")} run a command inside the resolved brain`,
|
|
459
|
+
` ${c("csb doctor")} verify your setup and suggest fixes`,
|
|
460
|
+
` ${c("csb help [<command>]")} show help for a command`,
|
|
461
|
+
"",
|
|
462
|
+
"Examples:",
|
|
463
|
+
` ${c('csb qmd query -c wiki "distributed systems"')}`,
|
|
464
|
+
` ${c("csb path qmd")}`,
|
|
465
|
+
` ${c("csb use work")}`,
|
|
466
|
+
` ${c("csb exec -- pnpm qmd:reindex")}`,
|
|
467
|
+
"",
|
|
468
|
+
`Config: ${pc.dim(CSB_CONFIG)}`,
|
|
469
|
+
brains.length === 0
|
|
470
|
+
? pc.dim("No brains registered yet.")
|
|
471
|
+
: `Default brain: ${defaultBrain ? pc.cyan(defaultBrain) : pc.yellow("(none set — run `csb use <name>`)")} ${pc.dim(`(${brains.length} registered)`)}`,
|
|
472
|
+
]
|
|
473
|
+
console.log(lines.join("\n"))
|
|
474
|
+
},
|
|
475
|
+
path: () => console.log([
|
|
476
|
+
`${pc.bold("csb path")} — print a path for the default (or named) brain`,
|
|
477
|
+
"",
|
|
478
|
+
"Usage:",
|
|
479
|
+
` ${c("csb path [qmd|config|root] [--brain <name>]")}`,
|
|
480
|
+
"",
|
|
481
|
+
"Positional (canonical):",
|
|
482
|
+
` ${c("root")} the brain's directory (default)`,
|
|
483
|
+
` ${c("qmd")} the brain's qmd SQLite index`,
|
|
484
|
+
` ${c("config")} the central config.toml`,
|
|
485
|
+
"",
|
|
486
|
+
"Flags:",
|
|
487
|
+
` ${c("--brain <name>")} target a specific brain instead of the default`,
|
|
488
|
+
` ${c("--root | --qmd | --config")} ${pc.dim("(deprecated — use positional form)")}`,
|
|
489
|
+
].join("\n")),
|
|
490
|
+
qmd: () => console.log([
|
|
491
|
+
`${pc.bold("csb qmd")} — run qmd against the default (or named) brain`,
|
|
492
|
+
"",
|
|
493
|
+
"Usage:",
|
|
494
|
+
` ${c("csb qmd [--brain <name>] [--] <qmd args…>")}`,
|
|
495
|
+
"",
|
|
496
|
+
`${pc.dim("`--` is optional; use it only to pass a literal `--brain` through to qmd.")}`,
|
|
497
|
+
"",
|
|
498
|
+
"Examples:",
|
|
499
|
+
` ${c('csb qmd query -c wiki "kafka"')}`,
|
|
500
|
+
` ${c("csb qmd --brain work search -c wiki kafka")}`,
|
|
501
|
+
].join("\n")),
|
|
502
|
+
exec: () => console.log([
|
|
503
|
+
`${pc.bold("csb exec")} — run a command inside the resolved brain's directory`,
|
|
504
|
+
"",
|
|
505
|
+
"Usage:",
|
|
506
|
+
` ${c("csb exec [--brain <name>] -- <cmd…>")}`,
|
|
507
|
+
"",
|
|
508
|
+
`Sets ${c("INDEX_PATH")} to the brain's qmd index and ${c("cwd")} to the brain's root.`,
|
|
509
|
+
"",
|
|
510
|
+
"Examples:",
|
|
511
|
+
` ${c("csb exec -- pnpm qmd:reindex")}`,
|
|
512
|
+
` ${c("csb exec --brain work -- git status")}`,
|
|
513
|
+
].join("\n")),
|
|
514
|
+
use: () => console.log([
|
|
515
|
+
`${pc.bold("csb use")} — set the default brain`,
|
|
516
|
+
"",
|
|
517
|
+
"Usage:",
|
|
518
|
+
` ${c("csb use <name>")} ${pc.dim("(aliases: default, switch)")}`,
|
|
519
|
+
"",
|
|
520
|
+
"Rewrites the `default = …` line in config.toml. Affects every command that resolves the default brain (path, qmd, exec, global skills).",
|
|
521
|
+
].join("\n")),
|
|
522
|
+
rm: () => console.log([
|
|
523
|
+
`${pc.bold("csb rm")} — remove one or more brains`,
|
|
524
|
+
"",
|
|
525
|
+
"Usage:",
|
|
526
|
+
` ${c("csb rm [<name>…] [-y|--yes] [--delete-remote|--keep-remote]")} ${pc.dim("(alias: remove)")}`,
|
|
527
|
+
"",
|
|
528
|
+
"Flags:",
|
|
529
|
+
` ${c("-y, --yes")} skip the confirmation prompt`,
|
|
530
|
+
` ${c("--delete-remote")} also delete GitHub/Cloudflare remotes (no prompt)`,
|
|
531
|
+
` ${c("--keep-remote")} keep remotes (no prompt)`,
|
|
532
|
+
"",
|
|
533
|
+
`${pc.dim("With -y and no remote flag, remotes are preserved (safe default).")}`,
|
|
534
|
+
`${pc.dim("Without -y and no remote flag, you'll be prompted per brain.")}`,
|
|
535
|
+
].join("\n")),
|
|
536
|
+
ls: () => console.log([
|
|
537
|
+
`${pc.bold("csb ls")} — list all brains ${pc.dim("(alias: list)")}`,
|
|
538
|
+
"",
|
|
539
|
+
"Prints each registered brain with its path, creation date, and remote.",
|
|
540
|
+
"The default brain is shown in bold with a `*` marker.",
|
|
541
|
+
].join("\n")),
|
|
542
|
+
doctor: () => console.log([
|
|
543
|
+
`${pc.bold("csb doctor")} — verify your setup and suggest fixes`,
|
|
544
|
+
"",
|
|
545
|
+
"Checks required and optional tools (gh, mise, pnpm, wrangler),",
|
|
546
|
+
"the central config, each registered brain's path, and its qmd index.",
|
|
547
|
+
"Prints a `Fix:` hint under each failing check.",
|
|
548
|
+
].join("\n")),
|
|
549
|
+
}
|
|
550
|
+
const renderer = topics[topic] || topics.top
|
|
551
|
+
const out = renderer()
|
|
552
|
+
if (out && typeof out.then === "function") return out
|
|
442
553
|
}
|
|
443
554
|
|
|
444
555
|
// Resolve a brain entry from config. Name defaults to the `default` field.
|
|
@@ -454,8 +565,8 @@ async function resolveBrain(name) {
|
|
|
454
565
|
if (!target) {
|
|
455
566
|
const available = brains.map(b => b.name).join(", ")
|
|
456
567
|
const hint = available
|
|
457
|
-
? `
|
|
458
|
-
: `Run \`
|
|
568
|
+
? `Quick fix: \`csb use <name>\` (available: ${available}), or pass --brain <name>.`
|
|
569
|
+
: `Run \`csb <name>\` to create your first brain.`
|
|
459
570
|
throw new Error(`No default brain set in config.toml. ${hint}`)
|
|
460
571
|
}
|
|
461
572
|
const entry = brains.find(b => b.name === target)
|
|
@@ -466,17 +577,70 @@ async function resolveBrain(name) {
|
|
|
466
577
|
return entry
|
|
467
578
|
}
|
|
468
579
|
|
|
580
|
+
// Rewrites config.toml so `name` is the new default. Returns {previous, current}.
|
|
581
|
+
// Strips any existing `active = …` / `default = …` line from the header first.
|
|
582
|
+
async function setDefaultBrain(name) {
|
|
583
|
+
let content
|
|
584
|
+
try {
|
|
585
|
+
content = await readFile(CSB_CONFIG, "utf8")
|
|
586
|
+
} catch {
|
|
587
|
+
throw new Error(`No config at ${CSB_CONFIG}. Run \`csb <name>\` to create your first brain.`)
|
|
588
|
+
}
|
|
589
|
+
const { defaultBrain, brains, header } = parseConfig(content)
|
|
590
|
+
if (!brains.find(b => b.name === name)) {
|
|
591
|
+
const available = brains.map(b => b.name).join(", ") || "(none)"
|
|
592
|
+
throw new Error(`No brain named "${name}". Available: ${available}.`)
|
|
593
|
+
}
|
|
594
|
+
if (defaultBrain === name) {
|
|
595
|
+
return { previous: name, current: name, changed: false }
|
|
596
|
+
}
|
|
597
|
+
const blocks = content.split(/\n(?=\[\[brains\]\])/)
|
|
598
|
+
let newHeader = (blocks[0] || "")
|
|
599
|
+
.replace(/^active\s*=\s*"[^"]*"\s*\n?/m, "")
|
|
600
|
+
.replace(/^default\s*=\s*"[^"]*"\s*\n?/m, "")
|
|
601
|
+
.replace(/^\s+|\s+$/g, "")
|
|
602
|
+
newHeader = `default = "${name}"${newHeader ? "\n" + newHeader : ""}`
|
|
603
|
+
const entries = blocks.slice(1).map(b => b.replace(/^\s+|\s+$/g, ""))
|
|
604
|
+
const out = [newHeader, ...entries].filter(Boolean).join("\n\n").trimEnd() + "\n"
|
|
605
|
+
await writeFile(CSB_CONFIG, out, "utf8")
|
|
606
|
+
return { previous: defaultBrain, current: name, changed: true }
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
async function cmdUse(args) {
|
|
610
|
+
const name = args.find(a => !a.startsWith("-"))
|
|
611
|
+
if (!name) {
|
|
612
|
+
throw new Error("Usage: csb use <name>")
|
|
613
|
+
}
|
|
614
|
+
const { previous, current, changed } = await setDefaultBrain(name)
|
|
615
|
+
if (!changed) {
|
|
616
|
+
console.log(`default is already ${pc.cyan(current)}`)
|
|
617
|
+
return
|
|
618
|
+
}
|
|
619
|
+
if (previous) {
|
|
620
|
+
console.log(`default: ${pc.dim(previous)} → ${pc.cyan(current)}`)
|
|
621
|
+
} else {
|
|
622
|
+
console.log(`default set to ${pc.cyan(current)}`)
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
469
626
|
async function cmdPath(args) {
|
|
470
|
-
//
|
|
627
|
+
// Canonical: csb path [qmd|config|root] [--brain <name>]
|
|
628
|
+
// Deprecated: csb path --qmd | --config | --root
|
|
471
629
|
let name = null
|
|
472
|
-
let mode =
|
|
630
|
+
let mode = null
|
|
631
|
+
let sawLegacyFlag = false
|
|
473
632
|
for (let i = 0; i < args.length; i++) {
|
|
474
633
|
const a = args[i]
|
|
475
634
|
if (a === "--brain") { name = args[++i]; continue }
|
|
476
|
-
if (a === "--root") { mode = "root"; continue }
|
|
477
|
-
if (a === "--qmd") { mode = "qmd"; continue }
|
|
478
|
-
if (a === "--config") { mode = "config"; continue }
|
|
479
|
-
|
|
635
|
+
if (a === "--root") { mode = "root"; sawLegacyFlag = true; continue }
|
|
636
|
+
if (a === "--qmd") { mode = "qmd"; sawLegacyFlag = true; continue }
|
|
637
|
+
if (a === "--config") { mode = "config"; sawLegacyFlag = true; continue }
|
|
638
|
+
if (a === "root" || a === "qmd" || a === "config") { mode = a; continue }
|
|
639
|
+
throw new Error(`Unknown path arg: ${a}. Run \`csb help path\` for usage.`)
|
|
640
|
+
}
|
|
641
|
+
if (!mode) mode = "root"
|
|
642
|
+
if (sawLegacyFlag) {
|
|
643
|
+
process.stderr.write(`${pc.yellow("warning:")} \`--${mode}\` is deprecated — use \`csb path ${mode}\` instead.\n`)
|
|
480
644
|
}
|
|
481
645
|
if (mode === "config") {
|
|
482
646
|
process.stdout.write(CSB_CONFIG + "\n")
|
|
@@ -487,8 +651,9 @@ async function cmdPath(args) {
|
|
|
487
651
|
process.stdout.write(out + "\n")
|
|
488
652
|
}
|
|
489
653
|
|
|
490
|
-
|
|
491
|
-
|
|
654
|
+
// Parse [--brain <name>] [--] <rest…>. `--` is optional; use it only to pass a
|
|
655
|
+
// literal `--brain` through to the inner command.
|
|
656
|
+
function parseBrainAndRest(args) {
|
|
492
657
|
let name = null
|
|
493
658
|
const rest = []
|
|
494
659
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -497,14 +662,132 @@ async function cmdQmd(args) {
|
|
|
497
662
|
if (a === "--") { rest.push(...args.slice(i + 1)); break }
|
|
498
663
|
rest.push(a)
|
|
499
664
|
}
|
|
500
|
-
|
|
501
|
-
|
|
665
|
+
return { name, rest }
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
async function runInBrain(brain, cmd) {
|
|
669
|
+
if (cmd.length === 0) {
|
|
670
|
+
throw new Error("No command to run. Usage: csb exec [--brain <name>] -- <cmd…>")
|
|
671
|
+
}
|
|
672
|
+
const result = spawnSync(cmd[0], cmd.slice(1), {
|
|
673
|
+
cwd: brain.path,
|
|
502
674
|
stdio: "inherit",
|
|
503
675
|
env: { ...process.env, INDEX_PATH: brain.qmd_index },
|
|
504
676
|
})
|
|
505
677
|
process.exit(result.status ?? 1)
|
|
506
678
|
}
|
|
507
679
|
|
|
680
|
+
async function cmdQmd(args) {
|
|
681
|
+
const { name, rest } = parseBrainAndRest(args)
|
|
682
|
+
const brain = await resolveBrain(name)
|
|
683
|
+
await runInBrain(brain, ["npx", "-y", "@tobilu/qmd", ...rest])
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
async function cmdExec(args) {
|
|
687
|
+
const { name, rest } = parseBrainAndRest(args)
|
|
688
|
+
const brain = await resolveBrain(name)
|
|
689
|
+
await runInBrain(brain, rest)
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
async function cmdDoctor() {
|
|
693
|
+
const toDisplayPath = p => p && p.startsWith(homedir()) ? "~" + p.slice(homedir().length) : p
|
|
694
|
+
const ok = s => ` ${pc.green("✓")} ${s}`
|
|
695
|
+
const warn = (s, fix) => ` ${pc.yellow("⚠")} ${s}${fix ? `\n ${pc.dim("Fix:")} ${fix}` : ""}`
|
|
696
|
+
const fail = (s, fix) => ` ${pc.red("✗")} ${s}${fix ? `\n ${pc.dim("Fix:")} ${fix}` : ""}`
|
|
697
|
+
const lines = []
|
|
698
|
+
let failures = 0
|
|
699
|
+
let warnings = 0
|
|
700
|
+
|
|
701
|
+
p.intro(`${pc.bgCyan(pc.black(" csb doctor "))} v${version}`)
|
|
702
|
+
|
|
703
|
+
// Tools
|
|
704
|
+
lines.push(pc.bold("Tools"))
|
|
705
|
+
lines.push(commandExists("gh")
|
|
706
|
+
? ok("gh (GitHub CLI) installed")
|
|
707
|
+
: (warnings++, warn("gh (GitHub CLI) not installed — required for GitHub remotes", "brew install gh | https://cli.github.com")))
|
|
708
|
+
if (commandExists("gh")) {
|
|
709
|
+
const auth = spawnSync("gh", ["auth", "status"], { stdio: "pipe" })
|
|
710
|
+
lines.push(auth.status === 0
|
|
711
|
+
? ok("gh authenticated")
|
|
712
|
+
: (warnings++, warn("gh is installed but not authenticated", "gh auth login")))
|
|
713
|
+
}
|
|
714
|
+
lines.push(commandExists("mise")
|
|
715
|
+
? ok("mise installed")
|
|
716
|
+
: (failures++, fail("mise not installed — required for scaffolded vaults", "npm install -g @jdxcode/mise")))
|
|
717
|
+
lines.push(commandExists("wrangler") || commandExists("npx")
|
|
718
|
+
? ok(commandExists("wrangler") ? "wrangler installed" : "wrangler available via npx (optional — for Cloudflare Artifacts)")
|
|
719
|
+
: (warnings++, warn("wrangler not installed — required only for Cloudflare Artifacts remotes", "npm install -g wrangler")))
|
|
720
|
+
|
|
721
|
+
// Config
|
|
722
|
+
lines.push("")
|
|
723
|
+
lines.push(pc.bold("Config"))
|
|
724
|
+
let parsed = null
|
|
725
|
+
try {
|
|
726
|
+
const content = await readFile(CSB_CONFIG, "utf8")
|
|
727
|
+
parsed = parseConfig(content)
|
|
728
|
+
lines.push(ok(`config readable at ${pc.dim(toDisplayPath(CSB_CONFIG))}`))
|
|
729
|
+
} catch {
|
|
730
|
+
failures++
|
|
731
|
+
lines.push(fail(`no config at ${toDisplayPath(CSB_CONFIG)}`, "csb <name> # create your first brain"))
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
if (parsed) {
|
|
735
|
+
if (parsed.brains.length === 0) {
|
|
736
|
+
warnings++
|
|
737
|
+
lines.push(warn("no brains registered", "csb <name> # create your first brain"))
|
|
738
|
+
} else {
|
|
739
|
+
lines.push(ok(`${parsed.brains.length} brain${parsed.brains.length === 1 ? "" : "s"} registered`))
|
|
740
|
+
if (parsed.defaultBrain) {
|
|
741
|
+
const entry = parsed.brains.find(b => b.name === parsed.defaultBrain)
|
|
742
|
+
if (entry) {
|
|
743
|
+
lines.push(ok(`default brain: ${pc.cyan(parsed.defaultBrain)}`))
|
|
744
|
+
} else {
|
|
745
|
+
failures++
|
|
746
|
+
lines.push(fail(`default brain "${parsed.defaultBrain}" is not registered`, `csb use <name> # pick one of: ${parsed.brains.map(b => b.name).join(", ")}`))
|
|
747
|
+
}
|
|
748
|
+
} else {
|
|
749
|
+
warnings++
|
|
750
|
+
lines.push(warn("no default brain set", `csb use <name> # pick one of: ${parsed.brains.map(b => b.name).join(", ")}`))
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
// Per-brain checks
|
|
755
|
+
if (parsed.brains.length > 0) {
|
|
756
|
+
lines.push("")
|
|
757
|
+
lines.push(pc.bold("Brains"))
|
|
758
|
+
for (const b of parsed.brains) {
|
|
759
|
+
lines.push(pc.cyan(b.name))
|
|
760
|
+
try {
|
|
761
|
+
await access(b.path)
|
|
762
|
+
lines.push(ok(`path exists: ${pc.dim(toDisplayPath(b.path))}`))
|
|
763
|
+
} catch {
|
|
764
|
+
failures++
|
|
765
|
+
lines.push(fail(`path missing: ${toDisplayPath(b.path)}`, `csb rm ${b.name} # drop the dead entry, then recreate`))
|
|
766
|
+
}
|
|
767
|
+
try {
|
|
768
|
+
await access(b.qmd_index)
|
|
769
|
+
lines.push(ok(`qmd index exists: ${pc.dim(toDisplayPath(b.qmd_index))}`))
|
|
770
|
+
} catch {
|
|
771
|
+
warnings++
|
|
772
|
+
lines.push(warn(`qmd index missing: ${toDisplayPath(b.qmd_index)}`, `csb exec --brain ${b.name} -- pnpm qmd:reindex`))
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
p.note(lines.join("\n"), "Checks")
|
|
779
|
+
|
|
780
|
+
if (failures > 0) {
|
|
781
|
+
p.outro(`${pc.red(`${failures} failure${failures === 1 ? "" : "s"}`)}${warnings > 0 ? `, ${pc.yellow(`${warnings} warning${warnings === 1 ? "" : "s"}`)}` : ""}`)
|
|
782
|
+
process.exit(1)
|
|
783
|
+
}
|
|
784
|
+
if (warnings > 0) {
|
|
785
|
+
p.outro(`${pc.yellow(`${warnings} warning${warnings === 1 ? "" : "s"}`)}`)
|
|
786
|
+
return
|
|
787
|
+
}
|
|
788
|
+
p.outro(pc.green("all checks passed"))
|
|
789
|
+
}
|
|
790
|
+
|
|
508
791
|
async function listBrains() {
|
|
509
792
|
const toDisplayPath = p => p && p.startsWith(homedir()) ? "~" + p.slice(homedir().length) : p
|
|
510
793
|
let content
|
|
@@ -537,7 +820,7 @@ async function listBrains() {
|
|
|
537
820
|
p.outro(defaultBrain ? `default: ${pc.cyan(defaultBrain)}` : "no default brain")
|
|
538
821
|
}
|
|
539
822
|
|
|
540
|
-
async function removeBrain(names, { yes = false } = {}) {
|
|
823
|
+
async function removeBrain(names, { yes = false, remoteMode = "ask" } = {}) {
|
|
541
824
|
p.intro(`${pc.bgCyan(pc.black(" claude-second-brain "))} v${version}`)
|
|
542
825
|
|
|
543
826
|
let content
|
|
@@ -592,14 +875,27 @@ async function removeBrain(names, { yes = false } = {}) {
|
|
|
592
875
|
}
|
|
593
876
|
}
|
|
594
877
|
|
|
595
|
-
//
|
|
596
|
-
//
|
|
878
|
+
// Decide per-target whether to also delete the remote.
|
|
879
|
+
// remoteMode === "keep" → never delete remotes
|
|
880
|
+
// remoteMode === "delete" → always delete remotes (no prompt)
|
|
881
|
+
// remoteMode === "ask" → default: prompt unless -y (then keep)
|
|
597
882
|
const remoteDecisions = new Map()
|
|
598
|
-
|
|
883
|
+
const resolvedRemoteMode =
|
|
884
|
+
remoteMode === "keep" ? "keep"
|
|
885
|
+
: remoteMode === "delete" ? "delete"
|
|
886
|
+
: yes ? "keep" : "ask"
|
|
887
|
+
|
|
888
|
+
if (resolvedRemoteMode !== "keep") {
|
|
599
889
|
for (const target of targets) {
|
|
600
890
|
const gh = parseGithubRepo(target.git_remote)
|
|
601
891
|
const cf = parseCloudflareArtifact(target.git_remote)
|
|
602
892
|
if (!gh && !cf) continue
|
|
893
|
+
if (resolvedRemoteMode === "delete") {
|
|
894
|
+
remoteDecisions.set(target.name, gh
|
|
895
|
+
? { kind: "github", slug: gh }
|
|
896
|
+
: { kind: "cloudflare", namespace: cf.namespace, repo: cf.repo })
|
|
897
|
+
continue
|
|
898
|
+
}
|
|
603
899
|
const label = gh ? `GitHub repo ${pc.cyan(gh)}` : `Cloudflare Artifact ${pc.cyan(`${cf.namespace}/${cf.repo}`)}`
|
|
604
900
|
const ok = await p.confirm({
|
|
605
901
|
message: `Also delete ${label} for "${target.name}"?`,
|
|
@@ -958,13 +1254,34 @@ async function createBrain(initialName) {
|
|
|
958
1254
|
p.outro("Happy knowledge building!")
|
|
959
1255
|
}
|
|
960
1256
|
|
|
1257
|
+
const SUBCOMMANDS = new Set([
|
|
1258
|
+
"help", "--help", "-h",
|
|
1259
|
+
"ls", "list",
|
|
1260
|
+
"rm", "remove",
|
|
1261
|
+
"path",
|
|
1262
|
+
"qmd",
|
|
1263
|
+
"exec",
|
|
1264
|
+
"use", "default", "switch",
|
|
1265
|
+
"doctor",
|
|
1266
|
+
])
|
|
1267
|
+
|
|
961
1268
|
async function main() {
|
|
962
1269
|
const [, , cmd, ...rest] = process.argv
|
|
963
1270
|
|
|
964
1271
|
if (cmd === "help" || cmd === "--help" || cmd === "-h") {
|
|
965
|
-
|
|
1272
|
+
// `csb help <topic>` or `csb help`
|
|
1273
|
+
const topic = rest[0]
|
|
1274
|
+
await printHelp(topic || "top")
|
|
966
1275
|
return
|
|
967
1276
|
}
|
|
1277
|
+
|
|
1278
|
+
// Per-subcommand --help / -h — intercept before any prompts run.
|
|
1279
|
+
if (SUBCOMMANDS.has(cmd) && hasHelpFlag(rest)) {
|
|
1280
|
+
const topicMap = { list: "ls", remove: "rm", default: "use", switch: "use" }
|
|
1281
|
+
await printHelp(topicMap[cmd] || cmd)
|
|
1282
|
+
return
|
|
1283
|
+
}
|
|
1284
|
+
|
|
968
1285
|
if (cmd === "ls" || cmd === "list") {
|
|
969
1286
|
await listBrains()
|
|
970
1287
|
return
|
|
@@ -972,7 +1289,13 @@ async function main() {
|
|
|
972
1289
|
if (cmd === "rm" || cmd === "remove") {
|
|
973
1290
|
const names = rest.filter(a => !a.startsWith("-"))
|
|
974
1291
|
const yes = rest.some(a => a === "-y" || a === "--yes")
|
|
975
|
-
|
|
1292
|
+
const deleteRemote = rest.some(a => a === "--delete-remote")
|
|
1293
|
+
const keepRemote = rest.some(a => a === "--keep-remote")
|
|
1294
|
+
if (deleteRemote && keepRemote) {
|
|
1295
|
+
throw new Error("Cannot use --delete-remote and --keep-remote together.")
|
|
1296
|
+
}
|
|
1297
|
+
const remoteMode = deleteRemote ? "delete" : keepRemote ? "keep" : "ask"
|
|
1298
|
+
await removeBrain(names, { yes, remoteMode })
|
|
976
1299
|
return
|
|
977
1300
|
}
|
|
978
1301
|
if (cmd === "path") {
|
|
@@ -983,6 +1306,18 @@ async function main() {
|
|
|
983
1306
|
await cmdQmd(rest)
|
|
984
1307
|
return
|
|
985
1308
|
}
|
|
1309
|
+
if (cmd === "exec") {
|
|
1310
|
+
await cmdExec(rest)
|
|
1311
|
+
return
|
|
1312
|
+
}
|
|
1313
|
+
if (cmd === "use" || cmd === "default" || cmd === "switch") {
|
|
1314
|
+
await cmdUse(rest)
|
|
1315
|
+
return
|
|
1316
|
+
}
|
|
1317
|
+
if (cmd === "doctor") {
|
|
1318
|
+
await cmdDoctor()
|
|
1319
|
+
return
|
|
1320
|
+
}
|
|
986
1321
|
await createBrain(cmd)
|
|
987
1322
|
}
|
|
988
1323
|
|
package/package.json
CHANGED
package/template/README.md
CHANGED
|
@@ -181,13 +181,20 @@ This wraps `pnpm qmd:reindex` — you can also run that command directly if you'
|
|
|
181
181
|
|
|
182
182
|
## Managing brains
|
|
183
183
|
|
|
184
|
-
From anywhere:
|
|
184
|
+
From anywhere (`csb` is the short alias — use `npx claude-second-brain` if not installed globally):
|
|
185
185
|
|
|
186
186
|
```bash
|
|
187
|
-
|
|
188
|
-
|
|
187
|
+
csb ls # list all brains (default marked with *)
|
|
188
|
+
csb use <name> # switch the default brain
|
|
189
|
+
csb rm <name> # remove a brain
|
|
190
|
+
csb path qmd # print the default brain's qmd index
|
|
191
|
+
csb qmd query -c wiki "<terms>" # run qmd against the default brain
|
|
192
|
+
csb exec -- pnpm qmd:reindex # run any command inside the default brain
|
|
193
|
+
csb doctor # verify tools, config, and each brain
|
|
189
194
|
```
|
|
190
195
|
|
|
196
|
+
Run `csb help <command>` (or `csb <command> --help`) for per-command details.
|
|
197
|
+
|
|
191
198
|
---
|
|
192
199
|
|
|
193
200
|
## License
|