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 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
- npx claude-second-brain ls # list all brains (default marked with *)
252
- npx claude-second-brain rm <name> # remove a brain (deletes its folder + config entry)
253
- npx claude-second-brain path # print the default brain's directory
254
- npx claude-second-brain path --qmd # print the default brain's qmd index path
255
- npx claude-second-brain qmd -- query -c wiki "<terms>" # run qmd against the default brain
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 printHelp() {
429
- const lines = [
430
- `${pc.bold("claude-second-brain")} v${version}`,
431
- "",
432
- "Usage:",
433
- ` ${pc.cyan("claude-second-brain")} create a new brain (interactive)`,
434
- ` ${pc.cyan("claude-second-brain <name>")} create a new brain named <name>`,
435
- ` ${pc.cyan("claude-second-brain ls")} list all brains`,
436
- ` ${pc.cyan("claude-second-brain rm [<name>…]")} remove one or more brains (space to multi-select)`,
437
- ` ${pc.cyan("claude-second-brain path [--brain N] [flag]")} print a path (flags: --root, --qmd, --config)`,
438
- ` ${pc.cyan("claude-second-brain qmd [--brain N] -- …")} run qmd against the resolved brain`,
439
- ` ${pc.cyan("claude-second-brain help")} show this message`,
440
- ]
441
- console.log(lines.join("\n"))
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
- ? `Pass --brain <name> (available: ${available}), or edit ${CSB_CONFIG} and set \`default = "<name>"\`.`
458
- : `Run \`claude-second-brain <name>\` to create your first brain.`
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
- // Parse flags: --brain <name>, --root | --qmd | --config (default: --root)
627
+ // Canonical: csb path [qmd|config|root] [--brain <name>]
628
+ // Deprecated: csb path --qmd | --config | --root
471
629
  let name = null
472
- let mode = "root"
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
- throw new Error(`Unknown path arg: ${a}`)
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
- async function cmdQmd(args) {
491
- // `claude-second-brain qmd [--brain N] [--] <qmd args…>`
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
- const brain = await resolveBrain(name)
501
- const result = spawnSync("npx", ["-y", "@tobilu/qmd", ...rest], {
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
- // Ask per-target whether to also delete the remote. Skip in -y mode — remote
596
- // deletion is destructive beyond the local vault and requires explicit opt-in.
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
- if (!yes) {
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
- printHelp()
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
- await removeBrain(names, { yes })
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-second-brain",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "The fastest way to start your personal knowledge base powered by Obsidian, Claude Code, qmd, and GitHub.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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
- npx claude-second-brain ls # list all brains
188
- npx claude-second-brain rm <name> # remove a brain
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