claude-second-brain 0.5.1 → 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/bin/create.js +249 -28
- package/package.json +1 -1
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")) {
|
|
@@ -186,6 +386,11 @@ async function main() {
|
|
|
186
386
|
if (pnpmOk) spin.stop("dependencies installed")
|
|
187
387
|
else spin.stop("pnpm install failed — run it manually inside your vault", 1)
|
|
188
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
|
+
|
|
189
394
|
// Git init
|
|
190
395
|
spin.start("Initializing git repo")
|
|
191
396
|
const gitOk = run(["git", "init"], targetDir)
|
|
@@ -198,9 +403,9 @@ async function main() {
|
|
|
198
403
|
}
|
|
199
404
|
|
|
200
405
|
// GitHub repo (optional)
|
|
201
|
-
if (
|
|
406
|
+
if (remoteProvider === "github") {
|
|
202
407
|
if (!commandExists("gh")) {
|
|
203
|
-
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`)
|
|
204
409
|
} else {
|
|
205
410
|
const authCheck = spawnSync("gh", ["auth", "status"], { stdio: "pipe" })
|
|
206
411
|
let loggedIn = authCheck.status === 0
|
|
@@ -211,33 +416,49 @@ async function main() {
|
|
|
211
416
|
}
|
|
212
417
|
|
|
213
418
|
if (loggedIn) {
|
|
214
|
-
spin.start(`Creating GitHub repo ${pc.dim(
|
|
419
|
+
spin.start(`Creating GitHub repo ${pc.dim(repoName)}`)
|
|
215
420
|
const ghOk = run(
|
|
216
|
-
["gh", "repo", "create",
|
|
421
|
+
["gh", "repo", "create", repoName, "--private", "--source=.", "--remote=origin", "--push"],
|
|
217
422
|
targetDir
|
|
218
423
|
)
|
|
219
424
|
if (ghOk) {
|
|
220
|
-
spin.stop(`GitHub repo created (private): ${pc.cyan(
|
|
425
|
+
spin.stop(`GitHub repo created (private): ${pc.cyan(repoName)}`)
|
|
221
426
|
} else {
|
|
222
|
-
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)
|
|
223
428
|
}
|
|
224
429
|
}
|
|
225
430
|
}
|
|
226
431
|
}
|
|
227
432
|
|
|
228
|
-
//
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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
|
+
}
|
|
232
440
|
|
|
233
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
|
+
|
|
234
458
|
const nextSteps = [
|
|
235
459
|
`${pc.cyan(`cd ${targetName}`)}`,
|
|
236
460
|
`${pc.cyan("claude")} open Claude Code, then run ${pc.bold("/setup")}`,
|
|
237
|
-
...
|
|
238
|
-
`${pc.cyan("git remote add origin <url>")} connect to GitHub for sync`,
|
|
239
|
-
`${pc.cyan("git push -u origin main")}`,
|
|
240
|
-
] : []),
|
|
461
|
+
...remoteSteps,
|
|
241
462
|
].join("\n")
|
|
242
463
|
|
|
243
464
|
p.note(nextSteps, "Next steps")
|