claude-second-brain 0.5.0 → 0.6.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 +1 -1
- package/bin/create.js +258 -31
- package/package.json +15 -2
- package/template/.claude/skills/brain-ingest/SKILL.md +1 -1
- package/template/.claude/skills/brain-rebuild/SKILL.md +10 -10
- package/template/.claude/skills/brain-refresh/SKILL.md +5 -5
- package/template/.claude/skills/brain-search/SKILL.md +1 -1
- package/template/.claude/skills/qmd-cli/SKILL.md +34 -34
- package/template/.claude/skills/setup/SKILL.md +8 -8
- package/template/CLAUDE.md +8 -8
- package/template/README.md +6 -3
- package/template/mise.toml +2 -1
- package/template/package.json +33 -0
- package/template/scripts/qmd/reindex.ts +2 -2
- package/template/scripts/qmd/setup.ts +6 -5
- package/template/scripts/qmd/tsconfig.json +3 -13
- package/template/scripts/qmd/package.json +0 -14
package/README.md
CHANGED
|
@@ -63,7 +63,7 @@ The CLI will ask:
|
|
|
63
63
|
- **qmd index path** — where to store the local search index (default: `~/.cache/qmd/index.sqlite`)
|
|
64
64
|
- **GitHub repo** — optionally create a private repo and push automatically (requires `gh` CLI)
|
|
65
65
|
|
|
66
|
-
Then scaffolds the vault, installs `mise` + `
|
|
66
|
+
Then scaffolds the vault, installs `mise` + `node` + `pnpm`, runs `pnpm install`, and `git init`.
|
|
67
67
|
|
|
68
68
|
**Step 2 — Initialize inside Claude Code**
|
|
69
69
|
|
package/bin/create.js
CHANGED
|
@@ -65,6 +65,187 @@ async function patchVault(targetDir, qmdPath, brainName) {
|
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
// Resolve the `wrangler` CLI as either an installed binary or `npx wrangler` fallback.
|
|
69
|
+
function resolveWranglerCli() {
|
|
70
|
+
if (commandExists("wrangler")) return { cmd: "wrangler", prefix: [] }
|
|
71
|
+
return { cmd: "npx", prefix: ["wrangler"] }
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Strip ANSI escape sequences that wrangler injects into stdout.
|
|
75
|
+
function stripAnsi(s) {
|
|
76
|
+
// eslint-disable-next-line no-control-regex
|
|
77
|
+
return s.replace(/\x1B\[[0-9;]*[A-Za-z]/g, "")
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Wrangler prints a decorative banner (⛅, ❅, etc.) alongside the token.
|
|
81
|
+
// Find the actual token line — a long run of Bearer-safe ASCII chars — and ignore the rest.
|
|
82
|
+
function extractBearerToken(stdout) {
|
|
83
|
+
const lines = stripAnsi(stdout).split(/\r?\n/)
|
|
84
|
+
const candidates = lines
|
|
85
|
+
.map(l => l.trim())
|
|
86
|
+
.filter(l => /^[A-Za-z0-9._~+/=-]{20,}$/.test(l))
|
|
87
|
+
// Prefer the longest candidate (real tokens are longer than any accidental match).
|
|
88
|
+
candidates.sort((a, b) => b.length - a.length)
|
|
89
|
+
return candidates[0] || null
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function truncate(s, n = 80) {
|
|
93
|
+
if (!s) return ""
|
|
94
|
+
return s.length > n ? s.slice(0, n) + "…" : s
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// End-to-end Cloudflare Artifacts setup: auth, create repo, set remote, push.
|
|
98
|
+
// Every failure logs the exact step, exit codes, stderr, and HTTP bodies so the
|
|
99
|
+
// user can see where in the flow the break happened.
|
|
100
|
+
async function setupCloudflareRemote({ targetDir, repoName, namespace, spin }) {
|
|
101
|
+
// Honor an existing API token as a shortcut — skip wrangler entirely.
|
|
102
|
+
let cfApiToken = process.env.CLOUDFLARE_API_TOKEN
|
|
103
|
+
let tokenSource = "CLOUDFLARE_API_TOKEN env"
|
|
104
|
+
|
|
105
|
+
if (!cfApiToken) {
|
|
106
|
+
// wrangler login requests artifacts:write by default — use it for auth.
|
|
107
|
+
const wrangler = resolveWranglerCli()
|
|
108
|
+
const wranglerLabel = [wrangler.cmd, ...wrangler.prefix].join(" ")
|
|
109
|
+
|
|
110
|
+
p.log.info(`Checking Cloudflare auth via \`${wranglerLabel} whoami\`...`)
|
|
111
|
+
const authCheck = spawnSync(wrangler.cmd, [...wrangler.prefix, "whoami"], { stdio: "pipe" })
|
|
112
|
+
const whoamiOut = stripAnsi(authCheck.stdout?.toString() || "") + "\n" + stripAnsi(authCheck.stderr?.toString() || "")
|
|
113
|
+
let loggedIn = authCheck.status === 0
|
|
114
|
+
// Wrangler's existing session may predate Artifacts — it warns "missing some expected Oauth scopes"
|
|
115
|
+
// and lists "artifacts:write". Detect that and force a re-login.
|
|
116
|
+
const hasArtifactsScope = /^\s*-\s*artifacts\s*\(write\)/im.test(whoamiOut)
|
|
117
|
+
const missingArtifactsScope = /missing some expected Oauth scopes/i.test(whoamiOut)
|
|
118
|
+
&& /artifacts:write/i.test(whoamiOut)
|
|
119
|
+
const needsRelogin = loggedIn && (!hasArtifactsScope || missingArtifactsScope)
|
|
120
|
+
|
|
121
|
+
if (!loggedIn) {
|
|
122
|
+
const stderr = authCheck.stderr?.toString().trim()
|
|
123
|
+
p.log.info(`wrangler whoami exited ${authCheck.status}${stderr ? ` (${truncate(stderr, 160)})` : ""}`)
|
|
124
|
+
} else if (needsRelogin) {
|
|
125
|
+
p.log.info("Your wrangler session is missing the artifacts:write scope — re-login required.")
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (!loggedIn || needsRelogin) {
|
|
129
|
+
p.log.info("Starting wrangler login (grants artifacts:write scope)...")
|
|
130
|
+
const loginResult = spawnSync(wrangler.cmd, [...wrangler.prefix, "login"], { stdio: "inherit" })
|
|
131
|
+
loggedIn = loginResult.status === 0
|
|
132
|
+
if (!loggedIn) {
|
|
133
|
+
p.log.warn(`wrangler login exited ${loginResult.status} — set CLOUDFLARE_API_TOKEN and re-run.`)
|
|
134
|
+
return null
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Verify the new session actually has artifacts:write before proceeding.
|
|
138
|
+
const verify = spawnSync(wrangler.cmd, [...wrangler.prefix, "whoami"], { stdio: "pipe" })
|
|
139
|
+
const verifyOut = stripAnsi(verify.stdout?.toString() || "") + "\n" + stripAnsi(verify.stderr?.toString() || "")
|
|
140
|
+
if (!/^\s*-\s*artifacts\s*\(write\)/im.test(verifyOut)) {
|
|
141
|
+
p.log.warn("wrangler login completed but artifacts:write scope is still missing.")
|
|
142
|
+
p.log.warn("Upgrade wrangler (npm i -g wrangler@latest) or set CLOUDFLARE_API_TOKEN manually.")
|
|
143
|
+
return null
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Retrieve the OAuth token wrangler stored; it refreshes automatically if expired.
|
|
148
|
+
p.log.info(`Fetching OAuth token via \`${wranglerLabel} auth token\`...`)
|
|
149
|
+
const tokenResult = spawnSync(wrangler.cmd, [...wrangler.prefix, "auth", "token"], { stdio: "pipe" })
|
|
150
|
+
const rawStdout = tokenResult.stdout?.toString() || ""
|
|
151
|
+
const rawStderr = tokenResult.stderr?.toString() || ""
|
|
152
|
+
|
|
153
|
+
if (tokenResult.status !== 0) {
|
|
154
|
+
p.log.warn(`wrangler auth token exited ${tokenResult.status}`)
|
|
155
|
+
if (rawStderr.trim()) p.log.warn(`stderr: ${truncate(rawStderr.trim(), 400)}`)
|
|
156
|
+
if (rawStdout.trim()) p.log.warn(`stdout: ${truncate(rawStdout.trim(), 400)}`)
|
|
157
|
+
return null
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
cfApiToken = extractBearerToken(rawStdout)
|
|
161
|
+
if (!cfApiToken) {
|
|
162
|
+
p.log.warn("Could not parse a Bearer-safe token from wrangler output.")
|
|
163
|
+
p.log.warn(`raw stdout: ${truncate(rawStdout.trim(), 400)}`)
|
|
164
|
+
if (rawStderr.trim()) p.log.warn(`raw stderr: ${truncate(rawStderr.trim(), 400)}`)
|
|
165
|
+
return null
|
|
166
|
+
}
|
|
167
|
+
tokenSource = "wrangler auth token"
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Guard against tokens that contain non-ASCII (which would crash fetch's header encoder).
|
|
171
|
+
if (!/^[\x20-\x7E]+$/.test(cfApiToken)) {
|
|
172
|
+
p.log.warn(`Token from ${tokenSource} contains non-ASCII characters — refusing to use it.`)
|
|
173
|
+
p.log.warn(`token preview: ${truncate(cfApiToken, 60)}`)
|
|
174
|
+
return null
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
p.log.info(`Using token from ${tokenSource} (length ${cfApiToken.length}, prefix ${cfApiToken.slice(0, 8)}…)`)
|
|
178
|
+
|
|
179
|
+
const baseUrl = `https://artifacts.cloudflare.net/v1/api/namespaces/${namespace}`
|
|
180
|
+
spin.start(`Creating Cloudflare Artifact ${pc.dim(repoName)} at ${pc.dim(baseUrl)}`)
|
|
181
|
+
|
|
182
|
+
let res
|
|
183
|
+
let rawBody = ""
|
|
184
|
+
try {
|
|
185
|
+
res = await fetch(`${baseUrl}/repos`, {
|
|
186
|
+
method: "POST",
|
|
187
|
+
headers: {
|
|
188
|
+
"Authorization": `Bearer ${cfApiToken}`,
|
|
189
|
+
"Content-Type": "application/json",
|
|
190
|
+
},
|
|
191
|
+
body: JSON.stringify({ name: repoName, default_branch: "main" }),
|
|
192
|
+
})
|
|
193
|
+
rawBody = await res.text()
|
|
194
|
+
} catch (err) {
|
|
195
|
+
spin.stop(`Network error calling Artifacts API: ${err.message}`, 1)
|
|
196
|
+
return null
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
let createData
|
|
200
|
+
try {
|
|
201
|
+
createData = rawBody ? JSON.parse(rawBody) : {}
|
|
202
|
+
} catch {
|
|
203
|
+
spin.stop(`Artifacts API returned non-JSON (HTTP ${res.status})`, 1)
|
|
204
|
+
p.log.warn(`body: ${truncate(rawBody, 400)}`)
|
|
205
|
+
return null
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (!res.ok || !createData?.success) {
|
|
209
|
+
const errMsg = createData?.errors?.[0]?.message || createData?.message || "unknown error"
|
|
210
|
+
spin.stop(`Artifacts API failed (HTTP ${res.status}): ${errMsg}`, 1)
|
|
211
|
+
p.log.warn(`full response: ${truncate(rawBody, 400)}`)
|
|
212
|
+
return null
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const { remote, token: repoToken } = createData.result
|
|
216
|
+
if (!remote || !repoToken) {
|
|
217
|
+
spin.stop("Artifacts API succeeded but response is missing remote/token", 1)
|
|
218
|
+
p.log.warn(`response: ${truncate(rawBody, 400)}`)
|
|
219
|
+
return null
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const remoteAddResult = spawnSync("git", ["remote", "add", "origin", remote], { cwd: targetDir, stdio: "pipe" })
|
|
223
|
+
if (remoteAddResult.status !== 0) {
|
|
224
|
+
spin.stop(`git remote add failed (exit ${remoteAddResult.status})`, 1)
|
|
225
|
+
const stderr = remoteAddResult.stderr?.toString().trim()
|
|
226
|
+
if (stderr) p.log.warn(`git stderr: ${truncate(stderr, 400)}`)
|
|
227
|
+
return null
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const pushResult = spawnSync(
|
|
231
|
+
"git",
|
|
232
|
+
["-c", `http.extraHeader=Authorization: Bearer ${repoToken}`, "push", "-u", "origin", "main"],
|
|
233
|
+
{ cwd: targetDir, stdio: "pipe" }
|
|
234
|
+
)
|
|
235
|
+
if (pushResult.status !== 0) {
|
|
236
|
+
spin.stop(`git push to Cloudflare Artifact failed (exit ${pushResult.status})`, 1)
|
|
237
|
+
const stderr = pushResult.stderr?.toString().trim()
|
|
238
|
+
const stdout = pushResult.stdout?.toString().trim()
|
|
239
|
+
if (stderr) p.log.warn(`git stderr: ${truncate(stderr, 400)}`)
|
|
240
|
+
if (stdout) p.log.warn(`git stdout: ${truncate(stdout, 400)}`)
|
|
241
|
+
return { repoToken, remote }
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
spin.stop(`Cloudflare Artifact created: ${pc.cyan(repoName)}`)
|
|
245
|
+
p.log.info(`Remote: ${pc.dim(remote)}`)
|
|
246
|
+
return { repoToken, remote }
|
|
247
|
+
}
|
|
248
|
+
|
|
68
249
|
async function installGlobalSkills(qmdPath) {
|
|
69
250
|
const globalSkillsDir = join(homedir(), ".claude", "skills")
|
|
70
251
|
|
|
@@ -100,37 +281,56 @@ async function main() {
|
|
|
100
281
|
process.env.XDG_CACHE_HOME || join(homedir(), ".cache"),
|
|
101
282
|
"qmd", "index.sqlite"
|
|
102
283
|
)
|
|
284
|
+
const toDisplayPath = p => p.startsWith(homedir()) ? "~" + p.slice(homedir().length) : p
|
|
285
|
+
const expandHome = p => p.startsWith("~/") || p === "~" ? join(homedir(), p.slice(1)) : p
|
|
286
|
+
const displayQmdPath = toDisplayPath(defaultQmdPath)
|
|
103
287
|
let qmdPath
|
|
104
288
|
if (isInteractive) {
|
|
105
289
|
const answer = await p.text({
|
|
106
290
|
message: "Where to store the qmd index?",
|
|
107
|
-
placeholder:
|
|
108
|
-
defaultValue:
|
|
291
|
+
placeholder: displayQmdPath,
|
|
292
|
+
defaultValue: displayQmdPath,
|
|
109
293
|
})
|
|
110
294
|
if (p.isCancel(answer)) { p.cancel("Setup cancelled."); process.exit(0) }
|
|
111
|
-
qmdPath = answer
|
|
295
|
+
qmdPath = expandHome(answer)
|
|
112
296
|
} else {
|
|
113
297
|
qmdPath = defaultQmdPath
|
|
114
298
|
}
|
|
115
299
|
|
|
116
|
-
let
|
|
117
|
-
let
|
|
300
|
+
let remoteProvider = "none"
|
|
301
|
+
let repoName = null
|
|
302
|
+
let cfNamespace = "default"
|
|
118
303
|
if (isInteractive) {
|
|
119
|
-
const
|
|
120
|
-
message: "
|
|
121
|
-
|
|
304
|
+
const provider = await p.select({
|
|
305
|
+
message: "Where to host the Git remote?",
|
|
306
|
+
options: [
|
|
307
|
+
{ value: "github", label: "GitHub", hint: "default" },
|
|
308
|
+
{ value: "cloudflare", label: "Cloudflare Artifacts" },
|
|
309
|
+
{ value: "none", label: "Skip — I'll add a remote later" },
|
|
310
|
+
],
|
|
311
|
+
initialValue: "github",
|
|
122
312
|
})
|
|
123
|
-
if (p.isCancel(
|
|
124
|
-
|
|
313
|
+
if (p.isCancel(provider)) { p.cancel("Setup cancelled."); process.exit(0) }
|
|
314
|
+
remoteProvider = provider
|
|
125
315
|
|
|
126
|
-
if (
|
|
316
|
+
if (remoteProvider !== "none") {
|
|
127
317
|
const answer = await p.text({
|
|
128
|
-
message: "
|
|
318
|
+
message: "Repo name?",
|
|
129
319
|
placeholder: targetName,
|
|
130
320
|
defaultValue: targetName,
|
|
131
321
|
})
|
|
132
322
|
if (p.isCancel(answer)) { p.cancel("Setup cancelled."); process.exit(0) }
|
|
133
|
-
|
|
323
|
+
repoName = answer
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (remoteProvider === "cloudflare") {
|
|
327
|
+
const ns = await p.text({
|
|
328
|
+
message: "Artifacts namespace?",
|
|
329
|
+
placeholder: "default",
|
|
330
|
+
defaultValue: "default",
|
|
331
|
+
})
|
|
332
|
+
if (p.isCancel(ns)) { p.cancel("Setup cancelled."); process.exit(0) }
|
|
333
|
+
cfNamespace = ns
|
|
134
334
|
}
|
|
135
335
|
}
|
|
136
336
|
|
|
@@ -163,7 +363,7 @@ async function main() {
|
|
|
163
363
|
// Patch vault files with chosen qmd path
|
|
164
364
|
spin.start("Configuring qmd index path")
|
|
165
365
|
await patchVault(targetDir, qmdPath, targetName)
|
|
166
|
-
spin.stop(`qmd index → ${pc.dim(qmdPath)}`)
|
|
366
|
+
spin.stop(`qmd index → ${pc.dim(toDisplayPath(qmdPath))}`)
|
|
167
367
|
|
|
168
368
|
// Install mise if not present
|
|
169
369
|
if (!commandExists("mise")) {
|
|
@@ -173,13 +373,24 @@ async function main() {
|
|
|
173
373
|
else spin.stop("Failed to install mise — run: npm install -g @jdxcode/mise", 1)
|
|
174
374
|
}
|
|
175
375
|
|
|
176
|
-
// Run mise install inside the new vault to install
|
|
177
|
-
spin.start("Installing
|
|
376
|
+
// Run mise install inside the new vault to install node + pnpm
|
|
377
|
+
spin.start("Installing node + pnpm via mise")
|
|
178
378
|
run(["mise", "trust"], targetDir)
|
|
179
379
|
const miseOk = run(["mise", "install"], targetDir)
|
|
180
|
-
if (miseOk) spin.stop("
|
|
380
|
+
if (miseOk) spin.stop("node + pnpm installed")
|
|
181
381
|
else spin.stop("mise install failed — run it manually inside your vault", 1)
|
|
182
382
|
|
|
383
|
+
// Install vault dependencies via pnpm
|
|
384
|
+
spin.start("Installing vault dependencies")
|
|
385
|
+
const pnpmOk = run(["mise", "exec", "--", "pnpm", "install"], targetDir)
|
|
386
|
+
if (pnpmOk) spin.stop("dependencies installed")
|
|
387
|
+
else spin.stop("pnpm install failed — run it manually inside your vault", 1)
|
|
388
|
+
|
|
389
|
+
// Install global skills
|
|
390
|
+
spin.start("Installing global Claude skills")
|
|
391
|
+
await installGlobalSkills(qmdPath)
|
|
392
|
+
spin.stop(`Global skills installed → ${pc.dim(toDisplayPath(join(homedir(), ".claude", "skills")))}`)
|
|
393
|
+
|
|
183
394
|
// Git init
|
|
184
395
|
spin.start("Initializing git repo")
|
|
185
396
|
const gitOk = run(["git", "init"], targetDir)
|
|
@@ -192,9 +403,9 @@ async function main() {
|
|
|
192
403
|
}
|
|
193
404
|
|
|
194
405
|
// GitHub repo (optional)
|
|
195
|
-
if (
|
|
406
|
+
if (remoteProvider === "github") {
|
|
196
407
|
if (!commandExists("gh")) {
|
|
197
|
-
p.log.warn(`gh CLI not found — install from https://cli.github.com, then run:\n gh repo create ${
|
|
408
|
+
p.log.warn(`gh CLI not found — install from https://cli.github.com, then run:\n gh repo create ${repoName} --private --source=. --remote=origin --push`)
|
|
198
409
|
} else {
|
|
199
410
|
const authCheck = spawnSync("gh", ["auth", "status"], { stdio: "pipe" })
|
|
200
411
|
let loggedIn = authCheck.status === 0
|
|
@@ -205,33 +416,49 @@ async function main() {
|
|
|
205
416
|
}
|
|
206
417
|
|
|
207
418
|
if (loggedIn) {
|
|
208
|
-
spin.start(`Creating GitHub repo ${pc.dim(
|
|
419
|
+
spin.start(`Creating GitHub repo ${pc.dim(repoName)}`)
|
|
209
420
|
const ghOk = run(
|
|
210
|
-
["gh", "repo", "create",
|
|
421
|
+
["gh", "repo", "create", repoName, "--private", "--source=.", "--remote=origin", "--push"],
|
|
211
422
|
targetDir
|
|
212
423
|
)
|
|
213
424
|
if (ghOk) {
|
|
214
|
-
spin.stop(`GitHub repo created (private): ${pc.cyan(
|
|
425
|
+
spin.stop(`GitHub repo created (private): ${pc.cyan(repoName)}`)
|
|
215
426
|
} else {
|
|
216
|
-
spin.stop(`gh repo create failed — run: gh repo create ${
|
|
427
|
+
spin.stop(`gh repo create failed — run: gh repo create ${repoName} --private --source=. --remote=origin --push`, 1)
|
|
217
428
|
}
|
|
218
429
|
}
|
|
219
430
|
}
|
|
220
431
|
}
|
|
221
432
|
|
|
222
|
-
//
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
433
|
+
// Cloudflare Artifacts repo (optional)
|
|
434
|
+
if (remoteProvider === "cloudflare") {
|
|
435
|
+
const result = await setupCloudflareRemote({ targetDir, repoName, namespace: cfNamespace, spin })
|
|
436
|
+
if (result?.repoToken) {
|
|
437
|
+
p.log.warn(`Save your Artifacts repo token — it expires and you'll need to mint a new one:\n ${result.repoToken}`)
|
|
438
|
+
}
|
|
439
|
+
}
|
|
226
440
|
|
|
227
441
|
// Next steps
|
|
442
|
+
const remoteSteps = remoteProvider === "cloudflare"
|
|
443
|
+
? [
|
|
444
|
+
`${pc.dim("# Mint a fresh git push/pull token when the current one expires:")}`,
|
|
445
|
+
`${pc.cyan(`TOKEN=$(wrangler auth token)`)}`,
|
|
446
|
+
`${pc.cyan(`curl -X POST https://artifacts.cloudflare.net/v1/api/namespaces/${cfNamespace}/tokens \\`)}`,
|
|
447
|
+
`${pc.cyan(` -H "Authorization: Bearer $TOKEN" \\`)}`,
|
|
448
|
+
`${pc.cyan(` -H "Content-Type: application/json" \\`)}`,
|
|
449
|
+
`${pc.cyan(` -d '{"repo":"${repoName}","scope":"write","ttl":86400}'`)}`,
|
|
450
|
+
]
|
|
451
|
+
: remoteProvider === "none"
|
|
452
|
+
? [
|
|
453
|
+
`${pc.cyan("git remote add origin <url>")} connect to a remote for sync`,
|
|
454
|
+
`${pc.cyan("git push -u origin main")}`,
|
|
455
|
+
]
|
|
456
|
+
: []
|
|
457
|
+
|
|
228
458
|
const nextSteps = [
|
|
229
459
|
`${pc.cyan(`cd ${targetName}`)}`,
|
|
230
460
|
`${pc.cyan("claude")} open Claude Code, then run ${pc.bold("/setup")}`,
|
|
231
|
-
...
|
|
232
|
-
`${pc.cyan("git remote add origin <url>")} connect to GitHub for sync`,
|
|
233
|
-
`${pc.cyan("git push -u origin main")}`,
|
|
234
|
-
] : []),
|
|
461
|
+
...remoteSteps,
|
|
235
462
|
].join("\n")
|
|
236
463
|
|
|
237
464
|
p.note(nextSteps, "Next steps")
|
package/package.json
CHANGED
|
@@ -1,19 +1,25 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-second-brain",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.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": {
|
|
7
7
|
"claude-second-brain": "./bin/create.js"
|
|
8
8
|
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"docs:dev": "vocs dev",
|
|
11
|
+
"docs:build": "vocs build",
|
|
12
|
+
"docs:preview": "vocs preview"
|
|
13
|
+
},
|
|
9
14
|
"files": [
|
|
10
15
|
"bin/",
|
|
11
16
|
"template/",
|
|
12
17
|
"image.png"
|
|
13
18
|
],
|
|
14
19
|
"engines": {
|
|
15
|
-
"node": ">=
|
|
20
|
+
"node": ">=22.0.0"
|
|
16
21
|
},
|
|
22
|
+
"packageManager": "pnpm@10.8.0",
|
|
17
23
|
"dependencies": {
|
|
18
24
|
"@clack/prompts": "^1.2.0",
|
|
19
25
|
"picocolors": "^1.1.1"
|
|
@@ -22,5 +28,12 @@
|
|
|
22
28
|
"repository": {
|
|
23
29
|
"type": "git",
|
|
24
30
|
"url": "https://github.com/jessepinkman9900/claude-second-brain"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@types/react": "^19.2.14",
|
|
34
|
+
"react": "^19.2.5",
|
|
35
|
+
"react-dom": "^19.2.5",
|
|
36
|
+
"typescript": "^6.0.2",
|
|
37
|
+
"vocs": "1.4.1"
|
|
25
38
|
}
|
|
26
39
|
}
|
|
@@ -35,7 +35,7 @@ Runs the full 9-step ingest workflow defined in CLAUDE.md. Do not skip steps.
|
|
|
35
35
|
- Add the new source to the Sources Ingested section: one-line description + `[[wiki/sources/slug]]` link
|
|
36
36
|
|
|
37
37
|
**Step 5 — Identify affected wiki pages**
|
|
38
|
-
- Run: `INDEX_PATH=__QMD_PATH__
|
|
38
|
+
- Run: `INDEX_PATH=__QMD_PATH__ pnpm dlx @tobilu/qmd query -c wiki "<source topic and key claims>"`
|
|
39
39
|
- Also Glob `wiki/*.md` and `wiki/sources/*.md` to catch anything qmd missed
|
|
40
40
|
- List all pages to create or update before proceeding
|
|
41
41
|
|
|
@@ -25,9 +25,9 @@ All commands run from the vault root.
|
|
|
25
25
|
- Read `CLAUDE.md` to understand the documented schema and any references to collection names
|
|
26
26
|
- List current state from qmd:
|
|
27
27
|
```bash
|
|
28
|
-
INDEX_PATH=__QMD_PATH__
|
|
29
|
-
INDEX_PATH=__QMD_PATH__
|
|
30
|
-
INDEX_PATH=__QMD_PATH__
|
|
28
|
+
INDEX_PATH=__QMD_PATH__ pnpm dlx @tobilu/qmd collection list
|
|
29
|
+
INDEX_PATH=__QMD_PATH__ pnpm dlx @tobilu/qmd context list
|
|
30
|
+
INDEX_PATH=__QMD_PATH__ pnpm dlx @tobilu/qmd status
|
|
31
31
|
```
|
|
32
32
|
|
|
33
33
|
### Step 2 — Analyze the wiki
|
|
@@ -74,20 +74,20 @@ If collection names changed:
|
|
|
74
74
|
For each existing collection that no longer fits the new schema:
|
|
75
75
|
|
|
76
76
|
```bash
|
|
77
|
-
INDEX_PATH=__QMD_PATH__
|
|
77
|
+
INDEX_PATH=__QMD_PATH__ pnpm dlx @tobilu/qmd collection remove <old-name>
|
|
78
78
|
```
|
|
79
79
|
|
|
80
80
|
For each obsolete context:
|
|
81
81
|
|
|
82
82
|
```bash
|
|
83
|
-
INDEX_PATH=__QMD_PATH__
|
|
83
|
+
INDEX_PATH=__QMD_PATH__ pnpm dlx @tobilu/qmd context rm <path>
|
|
84
84
|
```
|
|
85
85
|
|
|
86
86
|
### Step 8 — Register the new schema
|
|
87
87
|
|
|
88
88
|
Run:
|
|
89
89
|
```bash
|
|
90
|
-
|
|
90
|
+
pnpm qmd:setup
|
|
91
91
|
```
|
|
92
92
|
|
|
93
93
|
This is idempotent — collections that already exist (e.g. ones you kept) are skipped; new ones are added; contexts are upserted.
|
|
@@ -96,7 +96,7 @@ This is idempotent — collections that already exist (e.g. ones you kept) are s
|
|
|
96
96
|
|
|
97
97
|
Run:
|
|
98
98
|
```bash
|
|
99
|
-
|
|
99
|
+
pnpm qmd:reindex
|
|
100
100
|
```
|
|
101
101
|
|
|
102
102
|
This indexes all files under the new collections and generates fresh embeddings.
|
|
@@ -104,9 +104,9 @@ This indexes all files under the new collections and generates fresh embeddings.
|
|
|
104
104
|
### Step 10 — Verify and report
|
|
105
105
|
|
|
106
106
|
```bash
|
|
107
|
-
INDEX_PATH=__QMD_PATH__
|
|
108
|
-
INDEX_PATH=__QMD_PATH__
|
|
109
|
-
INDEX_PATH=__QMD_PATH__
|
|
107
|
+
INDEX_PATH=__QMD_PATH__ pnpm dlx @tobilu/qmd collection list
|
|
108
|
+
INDEX_PATH=__QMD_PATH__ pnpm dlx @tobilu/qmd context list
|
|
109
|
+
INDEX_PATH=__QMD_PATH__ pnpm dlx @tobilu/qmd status
|
|
110
110
|
```
|
|
111
111
|
|
|
112
112
|
Confirm the new collections appear, contexts match the plan, and document/embedding counts are non-zero. Report a summary of what changed (collections added/removed/renamed, contexts updated, files re-indexed, references updated in CLAUDE.md and skills).
|
|
@@ -6,7 +6,7 @@ argument-hint: "Optional: 'force' to force re-embedding of every chunk (slow, us
|
|
|
6
6
|
|
|
7
7
|
# Brain Refresh
|
|
8
8
|
|
|
9
|
-
Refreshes the qmd index so search reflects the current state of the vault. Wraps `
|
|
9
|
+
Refreshes the qmd index so search reflects the current state of the vault. Wraps `pnpm qmd:reindex` (incremental) and the qmd CLI's force-embed flag.
|
|
10
10
|
|
|
11
11
|
## When to Use
|
|
12
12
|
|
|
@@ -23,7 +23,7 @@ All commands run from the vault root.
|
|
|
23
23
|
|
|
24
24
|
Run:
|
|
25
25
|
```bash
|
|
26
|
-
|
|
26
|
+
pnpm qmd:reindex
|
|
27
27
|
```
|
|
28
28
|
|
|
29
29
|
This script does two things:
|
|
@@ -38,10 +38,10 @@ When the user passes `force`, re-embed **every** chunk — not just the changed
|
|
|
38
38
|
|
|
39
39
|
```bash
|
|
40
40
|
# Step 1 — update the file index first
|
|
41
|
-
|
|
41
|
+
pnpm qmd:reindex
|
|
42
42
|
|
|
43
43
|
# Step 2 — force re-embed everything
|
|
44
|
-
INDEX_PATH=__QMD_PATH__
|
|
44
|
+
INDEX_PATH=__QMD_PATH__ pnpm dlx @tobilu/qmd embed -f
|
|
45
45
|
```
|
|
46
46
|
|
|
47
47
|
Force mode is slow — confirm with the user before proceeding if the wiki is large.
|
|
@@ -51,7 +51,7 @@ Force mode is slow — confirm with the user before proceeding if the wiki is la
|
|
|
51
51
|
After refresh, confirm with:
|
|
52
52
|
|
|
53
53
|
```bash
|
|
54
|
-
INDEX_PATH=__QMD_PATH__
|
|
54
|
+
INDEX_PATH=__QMD_PATH__ pnpm dlx @tobilu/qmd status
|
|
55
55
|
```
|
|
56
56
|
|
|
57
57
|
Document and embedding counts should be non-zero and reflect recent activity. If embeddings show as `0` or far below document count, re-run the refresh.
|
|
@@ -11,7 +11,7 @@ Runs the 4-step query workflow defined in CLAUDE.md. Answers come with inline `[
|
|
|
11
11
|
## Workflow
|
|
12
12
|
|
|
13
13
|
**Step 1 — Search the wiki**
|
|
14
|
-
- Run hybrid search: `INDEX_PATH=__QMD_PATH__
|
|
14
|
+
- Run hybrid search: `INDEX_PATH=__QMD_PATH__ pnpm dlx @tobilu/qmd query -c wiki "<question>"`
|
|
15
15
|
- Read `wiki/index.md` to confirm coverage and catch any pages qmd didn't surface
|
|
16
16
|
- Read the 2–5 most relevant pages in full before synthesizing
|
|
17
17
|
|
|
@@ -13,10 +13,10 @@ Local semantic document index with hybrid search (BM25 + vector), collection man
|
|
|
13
13
|
This vault keeps its index at `__QMD_PATH__`. All CLI commands **must** prefix with `INDEX_PATH=__QMD_PATH__`:
|
|
14
14
|
|
|
15
15
|
```bash
|
|
16
|
-
INDEX_PATH=__QMD_PATH__
|
|
16
|
+
INDEX_PATH=__QMD_PATH__ pnpm dlx @tobilu/qmd query -c wiki "..."
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
-
The scripts `
|
|
19
|
+
The scripts `pnpm qmd:setup` and `pnpm qmd:reindex` write to this same file. The CLI must match.
|
|
20
20
|
|
|
21
21
|
---
|
|
22
22
|
|
|
@@ -24,40 +24,40 @@ The scripts `bun scripts/qmd/setup.ts` and `bun scripts/qmd/reindex.ts` write to
|
|
|
24
24
|
|
|
25
25
|
```bash
|
|
26
26
|
# Create/index a collection
|
|
27
|
-
INDEX_PATH=__QMD_PATH__
|
|
27
|
+
INDEX_PATH=__QMD_PATH__ pnpm dlx @tobilu/qmd collection add <path> --name <name> --mask <glob-pattern>
|
|
28
28
|
# e.g.:
|
|
29
|
-
INDEX_PATH=__QMD_PATH__
|
|
29
|
+
INDEX_PATH=__QMD_PATH__ pnpm dlx @tobilu/qmd collection add wiki --name wiki --mask "**/*.md"
|
|
30
30
|
|
|
31
31
|
# List all collections with details
|
|
32
|
-
INDEX_PATH=__QMD_PATH__
|
|
32
|
+
INDEX_PATH=__QMD_PATH__ pnpm dlx @tobilu/qmd collection list
|
|
33
33
|
|
|
34
34
|
# Remove a collection
|
|
35
|
-
INDEX_PATH=__QMD_PATH__
|
|
35
|
+
INDEX_PATH=__QMD_PATH__ pnpm dlx @tobilu/qmd collection remove <name>
|
|
36
36
|
|
|
37
37
|
# Rename a collection
|
|
38
|
-
INDEX_PATH=__QMD_PATH__
|
|
38
|
+
INDEX_PATH=__QMD_PATH__ pnpm dlx @tobilu/qmd collection rename <old-name> <new-name>
|
|
39
39
|
```
|
|
40
40
|
|
|
41
41
|
## Listing Files
|
|
42
42
|
|
|
43
43
|
```bash
|
|
44
|
-
INDEX_PATH=__QMD_PATH__
|
|
45
|
-
INDEX_PATH=__QMD_PATH__
|
|
44
|
+
INDEX_PATH=__QMD_PATH__ pnpm dlx @tobilu/qmd ls
|
|
45
|
+
INDEX_PATH=__QMD_PATH__ pnpm dlx @tobilu/qmd ls [collection[/path]]
|
|
46
46
|
```
|
|
47
47
|
|
|
48
48
|
## Context
|
|
49
49
|
|
|
50
50
|
```bash
|
|
51
|
-
INDEX_PATH=__QMD_PATH__
|
|
52
|
-
INDEX_PATH=__QMD_PATH__
|
|
53
|
-
INDEX_PATH=__QMD_PATH__
|
|
51
|
+
INDEX_PATH=__QMD_PATH__ pnpm dlx @tobilu/qmd context add [path] "description text"
|
|
52
|
+
INDEX_PATH=__QMD_PATH__ pnpm dlx @tobilu/qmd context list
|
|
53
|
+
INDEX_PATH=__QMD_PATH__ pnpm dlx @tobilu/qmd context rm <path>
|
|
54
54
|
```
|
|
55
55
|
|
|
56
56
|
## Retrieving Documents
|
|
57
57
|
|
|
58
58
|
```bash
|
|
59
|
-
INDEX_PATH=__QMD_PATH__
|
|
60
|
-
INDEX_PATH=__QMD_PATH__
|
|
59
|
+
INDEX_PATH=__QMD_PATH__ pnpm dlx @tobilu/qmd get <file>[:line] [-l N] [--from N]
|
|
60
|
+
INDEX_PATH=__QMD_PATH__ pnpm dlx @tobilu/qmd multi-get <pattern> [-l N] [--max-bytes N]
|
|
61
61
|
```
|
|
62
62
|
|
|
63
63
|
**Multi-get format flags:** `--json`, `--csv`, `--md`, `--xml`, `--files`
|
|
@@ -66,16 +66,16 @@ INDEX_PATH=__QMD_PATH__ bunx @tobilu/qmd multi-get <pattern> [-l N] [--max-bytes
|
|
|
66
66
|
|
|
67
67
|
```bash
|
|
68
68
|
# Show index status and collections
|
|
69
|
-
INDEX_PATH=__QMD_PATH__
|
|
69
|
+
INDEX_PATH=__QMD_PATH__ pnpm dlx @tobilu/qmd status
|
|
70
70
|
|
|
71
71
|
# Re-index all collections
|
|
72
|
-
INDEX_PATH=__QMD_PATH__
|
|
72
|
+
INDEX_PATH=__QMD_PATH__ pnpm dlx @tobilu/qmd update [--pull]
|
|
73
73
|
|
|
74
74
|
# Create vector embeddings (900 tokens/chunk, 15% overlap)
|
|
75
|
-
INDEX_PATH=__QMD_PATH__
|
|
75
|
+
INDEX_PATH=__QMD_PATH__ pnpm dlx @tobilu/qmd embed [-f]
|
|
76
76
|
|
|
77
77
|
# Remove cache and orphaned data, vacuum DB
|
|
78
|
-
INDEX_PATH=__QMD_PATH__
|
|
78
|
+
INDEX_PATH=__QMD_PATH__ pnpm dlx @tobilu/qmd cleanup
|
|
79
79
|
```
|
|
80
80
|
|
|
81
81
|
> After bulk ingest sessions, run `update` then `embed` to keep the index fresh.
|
|
@@ -108,28 +108,28 @@ INDEX_PATH=__QMD_PATH__ bunx @tobilu/qmd cleanup
|
|
|
108
108
|
|
|
109
109
|
```bash
|
|
110
110
|
# Hybrid search across all collections
|
|
111
|
-
INDEX_PATH=__QMD_PATH__
|
|
111
|
+
INDEX_PATH=__QMD_PATH__ pnpm dlx @tobilu/qmd query "distributed systems consensus"
|
|
112
112
|
|
|
113
113
|
# Search only the wiki collection, JSON output
|
|
114
|
-
INDEX_PATH=__QMD_PATH__
|
|
114
|
+
INDEX_PATH=__QMD_PATH__ pnpm dlx @tobilu/qmd query -c wiki "transformer architecture" --json
|
|
115
115
|
|
|
116
116
|
# Keyword-only search, top 10, markdown output
|
|
117
|
-
INDEX_PATH=__QMD_PATH__
|
|
117
|
+
INDEX_PATH=__QMD_PATH__ pnpm dlx @tobilu/qmd search -c wiki "kafka" -n 10 --md
|
|
118
118
|
|
|
119
119
|
# Vector search with full documents
|
|
120
|
-
INDEX_PATH=__QMD_PATH__
|
|
120
|
+
INDEX_PATH=__QMD_PATH__ pnpm dlx @tobilu/qmd vsearch "attention mechanism" --full
|
|
121
121
|
|
|
122
122
|
# Filter by score threshold
|
|
123
|
-
INDEX_PATH=__QMD_PATH__
|
|
123
|
+
INDEX_PATH=__QMD_PATH__ pnpm dlx @tobilu/qmd query "machine learning" --all --min-score 0.7
|
|
124
124
|
```
|
|
125
125
|
|
|
126
126
|
## MCP Server
|
|
127
127
|
|
|
128
128
|
```bash
|
|
129
|
-
INDEX_PATH=__QMD_PATH__
|
|
130
|
-
INDEX_PATH=__QMD_PATH__
|
|
131
|
-
INDEX_PATH=__QMD_PATH__
|
|
132
|
-
INDEX_PATH=__QMD_PATH__
|
|
129
|
+
INDEX_PATH=__QMD_PATH__ pnpm dlx @tobilu/qmd mcp
|
|
130
|
+
INDEX_PATH=__QMD_PATH__ pnpm dlx @tobilu/qmd mcp --http [--port N]
|
|
131
|
+
INDEX_PATH=__QMD_PATH__ pnpm dlx @tobilu/qmd mcp --http --daemon
|
|
132
|
+
INDEX_PATH=__QMD_PATH__ pnpm dlx @tobilu/qmd mcp stop
|
|
133
133
|
```
|
|
134
134
|
|
|
135
135
|
## Global Options
|
|
@@ -145,24 +145,24 @@ Note: use `INDEX_PATH` env var rather than `--index` — `INDEX_PATH` accepts a
|
|
|
145
145
|
### First-time setup for this vault
|
|
146
146
|
```bash
|
|
147
147
|
# Handled by the /setup skill:
|
|
148
|
-
|
|
149
|
-
|
|
148
|
+
pnpm qmd:setup
|
|
149
|
+
pnpm qmd:reindex
|
|
150
150
|
```
|
|
151
151
|
|
|
152
152
|
### After a bulk ingest session
|
|
153
153
|
```bash
|
|
154
|
-
INDEX_PATH=__QMD_PATH__
|
|
155
|
-
INDEX_PATH=__QMD_PATH__
|
|
154
|
+
INDEX_PATH=__QMD_PATH__ pnpm dlx @tobilu/qmd update
|
|
155
|
+
INDEX_PATH=__QMD_PATH__ pnpm dlx @tobilu/qmd embed
|
|
156
156
|
```
|
|
157
157
|
|
|
158
158
|
### Research workflow
|
|
159
159
|
```bash
|
|
160
160
|
# Discover relevant pages
|
|
161
|
-
INDEX_PATH=__QMD_PATH__
|
|
161
|
+
INDEX_PATH=__QMD_PATH__ pnpm dlx @tobilu/qmd query -c wiki "<topic>"
|
|
162
162
|
|
|
163
163
|
# Get a specific file
|
|
164
|
-
INDEX_PATH=__QMD_PATH__
|
|
164
|
+
INDEX_PATH=__QMD_PATH__ pnpm dlx @tobilu/qmd get wiki/distributed-systems.md
|
|
165
165
|
|
|
166
166
|
# Get multiple related files
|
|
167
|
-
INDEX_PATH=__QMD_PATH__
|
|
167
|
+
INDEX_PATH=__QMD_PATH__ pnpm dlx @tobilu/qmd multi-get "wiki/kafka.md,wiki/distributed-systems.md" --md
|
|
168
168
|
```
|
|
@@ -21,7 +21,7 @@ All commands run from the vault root.
|
|
|
21
21
|
|
|
22
22
|
Run:
|
|
23
23
|
```bash
|
|
24
|
-
|
|
24
|
+
pnpm qmd:setup
|
|
25
25
|
```
|
|
26
26
|
|
|
27
27
|
Registers the two core qmd collections (`wiki`, `raw-sources`) and their path-level context descriptions. Idempotent — safe to re-run.
|
|
@@ -30,7 +30,7 @@ Registers the two core qmd collections (`wiki`, `raw-sources`) and their path-le
|
|
|
30
30
|
|
|
31
31
|
Run:
|
|
32
32
|
```bash
|
|
33
|
-
|
|
33
|
+
pnpm qmd:reindex
|
|
34
34
|
```
|
|
35
35
|
|
|
36
36
|
Scans all collections for new/changed files and generates vector embeddings. The first run downloads ~2GB of local GGUF models — this will take a while.
|
|
@@ -39,7 +39,7 @@ Scans all collections for new/changed files and generates vector embeddings. The
|
|
|
39
39
|
|
|
40
40
|
Skip step 1 and run only:
|
|
41
41
|
```bash
|
|
42
|
-
|
|
42
|
+
pnpm qmd:reindex
|
|
43
43
|
```
|
|
44
44
|
|
|
45
45
|
Do **not** re-run after every single file edit — batch it after a session.
|
|
@@ -50,17 +50,17 @@ Run these three commands to confirm everything is working:
|
|
|
50
50
|
|
|
51
51
|
```bash
|
|
52
52
|
# List registered collections (expect: wiki, raw-sources)
|
|
53
|
-
INDEX_PATH=__QMD_PATH__
|
|
53
|
+
INDEX_PATH=__QMD_PATH__ pnpm dlx @tobilu/qmd collection list
|
|
54
54
|
|
|
55
55
|
# List registered contexts
|
|
56
|
-
INDEX_PATH=__QMD_PATH__
|
|
56
|
+
INDEX_PATH=__QMD_PATH__ pnpm dlx @tobilu/qmd context list
|
|
57
57
|
|
|
58
58
|
# Show index status and embedding counts
|
|
59
|
-
INDEX_PATH=__QMD_PATH__
|
|
59
|
+
INDEX_PATH=__QMD_PATH__ pnpm dlx @tobilu/qmd status
|
|
60
60
|
```
|
|
61
61
|
|
|
62
62
|
Both collections should appear in `collection list`. `status` should show non-zero document and embedding counts — if embeddings are 0, re-run step 2.
|
|
63
63
|
|
|
64
64
|
## Notes
|
|
65
|
-
- `
|
|
66
|
-
- If `
|
|
65
|
+
- `node` and `pnpm` are managed via `mise` — ensure `mise install` and `pnpm install` have been run before setup
|
|
66
|
+
- If `pnpm dlx @tobilu/qmd` commands fail after setup, re-run step 1 then step 2
|
package/template/CLAUDE.md
CHANGED
|
@@ -111,7 +111,7 @@ Run this workflow whenever the user adds a new source. Do not skip steps.
|
|
|
111
111
|
- Add the new source to the Sources section with a one-line description and link.
|
|
112
112
|
|
|
113
113
|
**Step 5 — Identify affected wiki pages**
|
|
114
|
-
- Run `INDEX_PATH=__QMD_PATH__
|
|
114
|
+
- Run `INDEX_PATH=__QMD_PATH__ pnpm dlx @tobilu/qmd query -c wiki "<source topic and key claims>"` to surface related existing wiki pages.
|
|
115
115
|
- Also Glob `wiki/*.md` and `wiki/sources/*.md` to ensure completeness.
|
|
116
116
|
- List all pages to create or update.
|
|
117
117
|
|
|
@@ -142,7 +142,7 @@ Run this workflow whenever the user adds a new source. Do not skip steps.
|
|
|
142
142
|
Run this workflow when the user asks a question against the wiki.
|
|
143
143
|
|
|
144
144
|
**Step 1 — Search the wiki**
|
|
145
|
-
- Run `INDEX_PATH=__QMD_PATH__
|
|
145
|
+
- Run `INDEX_PATH=__QMD_PATH__ pnpm dlx @tobilu/qmd query -c wiki "<question>"` to surface semantically relevant pages.
|
|
146
146
|
- Read `wiki/index.md` to confirm coverage and catch any pages qmd didn't surface.
|
|
147
147
|
- Read the 2-5 most relevant pages fully.
|
|
148
148
|
|
|
@@ -211,21 +211,21 @@ N issues found, N fixed. [Brief summary of notable findings.]
|
|
|
211
211
|
|
|
212
212
|
## Search Tool
|
|
213
213
|
|
|
214
|
-
This vault uses **qmd** (`
|
|
215
|
-
Collections and contexts are registered via `
|
|
214
|
+
This vault uses **qmd** (`pnpm dlx @tobilu/qmd`) for local semantic and full-text search.
|
|
215
|
+
Collections and contexts are registered via `pnpm qmd:setup` and stored at `__QMD_PATH__` (gitignored).
|
|
216
216
|
|
|
217
217
|
Key commands:
|
|
218
218
|
|
|
219
219
|
| Command | Use |
|
|
220
220
|
|---------|-----|
|
|
221
|
-
| `INDEX_PATH=__QMD_PATH__
|
|
222
|
-
| `INDEX_PATH=__QMD_PATH__
|
|
223
|
-
| `INDEX_PATH=__QMD_PATH__
|
|
221
|
+
| `INDEX_PATH=__QMD_PATH__ pnpm dlx @tobilu/qmd query -c wiki "<question>"` | Hybrid search — best for topic discovery |
|
|
222
|
+
| `INDEX_PATH=__QMD_PATH__ pnpm dlx @tobilu/qmd search -c wiki "<terms>"` | Fast keyword search (BM25, no LLM) |
|
|
223
|
+
| `INDEX_PATH=__QMD_PATH__ pnpm dlx @tobilu/qmd vsearch -c wiki "<question>"` | Pure vector/semantic search |
|
|
224
224
|
|
|
225
225
|
Add `--json` for structured output. Omit `-c wiki` to search all collections (wiki, raw-sources, human, daily-notes).
|
|
226
226
|
|
|
227
227
|
**Re-indexing:** After a bulk ingest session run:
|
|
228
228
|
```bash
|
|
229
|
-
|
|
229
|
+
pnpm qmd:reindex
|
|
230
230
|
```
|
|
231
231
|
Do NOT re-index after every single file edit.
|
package/template/README.md
CHANGED
|
@@ -13,10 +13,13 @@ Inspired by [Andrej Karpathy's approach to LLM-powered knowledge management](htt
|
|
|
13
13
|
## Quick start
|
|
14
14
|
|
|
15
15
|
```bash
|
|
16
|
-
# 1. Install tools
|
|
16
|
+
# 1. Install tools (node + pnpm via mise)
|
|
17
17
|
mise install
|
|
18
18
|
|
|
19
|
-
# 2.
|
|
19
|
+
# 2. Install dependencies
|
|
20
|
+
pnpm install
|
|
21
|
+
|
|
22
|
+
# 3. Open Claude Code
|
|
20
23
|
claude
|
|
21
24
|
```
|
|
22
25
|
|
|
@@ -185,7 +188,7 @@ After a bulk ingest session, re-index to keep search current:
|
|
|
185
188
|
/brain-refresh
|
|
186
189
|
```
|
|
187
190
|
|
|
188
|
-
This wraps `
|
|
191
|
+
This wraps `pnpm qmd:reindex` — you can also run that command directly if you're not inside Claude Code. Pass `force` to `/brain-refresh` to re-embed every chunk (e.g. after changing the embedding model).
|
|
189
192
|
|
|
190
193
|
---
|
|
191
194
|
|
package/template/mise.toml
CHANGED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "claude-second-brain-vault",
|
|
3
|
+
"private": true,
|
|
4
|
+
"type": "module",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"qmd:setup": "tsx scripts/qmd/setup.ts",
|
|
7
|
+
"qmd:reindex": "tsx scripts/qmd/reindex.ts"
|
|
8
|
+
},
|
|
9
|
+
"dependencies": {
|
|
10
|
+
"@tobilu/qmd": "^2.1.0"
|
|
11
|
+
},
|
|
12
|
+
"devDependencies": {
|
|
13
|
+
"@types/node": "^22.10.2",
|
|
14
|
+
"tsx": "^4.19.2",
|
|
15
|
+
"typescript": "^5.7.2"
|
|
16
|
+
},
|
|
17
|
+
"packageManager": "pnpm@10.8.0",
|
|
18
|
+
"engines": {
|
|
19
|
+
"node": ">=22.0.0"
|
|
20
|
+
},
|
|
21
|
+
"pnpm": {
|
|
22
|
+
"onlyBuiltDependencies": [
|
|
23
|
+
"better-sqlite3",
|
|
24
|
+
"esbuild",
|
|
25
|
+
"node-llama-cpp",
|
|
26
|
+
"tree-sitter-go",
|
|
27
|
+
"tree-sitter-javascript",
|
|
28
|
+
"tree-sitter-python",
|
|
29
|
+
"tree-sitter-rust",
|
|
30
|
+
"tree-sitter-typescript"
|
|
31
|
+
]
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -4,10 +4,10 @@
|
|
|
4
4
|
* First run downloads ~2GB of GGUF models — expected, one-time.
|
|
5
5
|
*
|
|
6
6
|
* Run from vault root:
|
|
7
|
-
*
|
|
7
|
+
* pnpm qmd:reindex
|
|
8
8
|
*
|
|
9
9
|
* Suitable as a cronjob:
|
|
10
|
-
* 0 * * * * cd /home/user/my-brain &&
|
|
10
|
+
* 0 * * * * cd /home/user/my-brain && pnpm qmd:reindex
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import { createStore } from "@tobilu/qmd"
|
|
@@ -3,16 +3,17 @@
|
|
|
3
3
|
* Safe to re-run: skips collections that already exist, upserts contexts.
|
|
4
4
|
*
|
|
5
5
|
* Run once from vault root:
|
|
6
|
-
*
|
|
6
|
+
* pnpm qmd:setup
|
|
7
7
|
*
|
|
8
8
|
* After setup, index the vault:
|
|
9
|
-
*
|
|
9
|
+
* pnpm qmd:reindex
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { createStore } from "@tobilu/qmd"
|
|
13
|
-
import { join } from "path"
|
|
13
|
+
import { dirname, join } from "node:path"
|
|
14
|
+
import { fileURLToPath } from "node:url"
|
|
14
15
|
|
|
15
|
-
const VAULT = join(import.meta.
|
|
16
|
+
const VAULT = join(dirname(fileURLToPath(import.meta.url)), "../..")
|
|
16
17
|
const DB = "__QMD_PATH__"
|
|
17
18
|
|
|
18
19
|
const store = await createStore({ dbPath: DB })
|
|
@@ -53,4 +54,4 @@ await store.addContext("raw-sources", "/articles", "Web articles saved as markdo
|
|
|
53
54
|
await store.addContext("raw-sources", "/pdfs", "PDF files or their extracted text")
|
|
54
55
|
await store.addContext("raw-sources", "/personal", "Personal notes flagged for wiki ingestion")
|
|
55
56
|
|
|
56
|
-
console.log("\nSetup complete. Run:
|
|
57
|
+
console.log("\nSetup complete. Run: pnpm qmd:reindex")
|
|
@@ -1,29 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"compilerOptions": {
|
|
3
|
-
// Environment setup & latest features
|
|
4
3
|
"lib": ["ESNext"],
|
|
5
4
|
"target": "ESNext",
|
|
6
|
-
"module": "
|
|
5
|
+
"module": "NodeNext",
|
|
6
|
+
"moduleResolution": "NodeNext",
|
|
7
7
|
"moduleDetection": "force",
|
|
8
|
-
"jsx": "react-jsx",
|
|
9
8
|
"allowJs": true,
|
|
10
9
|
|
|
11
|
-
// Bundler mode
|
|
12
|
-
"moduleResolution": "bundler",
|
|
13
|
-
"allowImportingTsExtensions": true,
|
|
14
10
|
"verbatimModuleSyntax": true,
|
|
15
11
|
"noEmit": true,
|
|
16
12
|
|
|
17
|
-
// Best practices
|
|
18
13
|
"strict": true,
|
|
19
14
|
"skipLibCheck": true,
|
|
20
15
|
"noFallthroughCasesInSwitch": true,
|
|
21
16
|
"noUncheckedIndexedAccess": true,
|
|
22
|
-
"noImplicitOverride": true
|
|
23
|
-
|
|
24
|
-
// Some stricter flags (disabled by default)
|
|
25
|
-
"noUnusedLocals": false,
|
|
26
|
-
"noUnusedParameters": false,
|
|
27
|
-
"noPropertyAccessFromIndexSignature": false
|
|
17
|
+
"noImplicitOverride": true
|
|
28
18
|
}
|
|
29
19
|
}
|