@100xprompt/brain 0.0.0-stage → 0.1.1

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.
Files changed (260) hide show
  1. package/LICENSE +48 -0
  2. package/README.md +235 -2
  3. package/package.json +48 -4
  4. package/src/adapters/aider/index.ts +8 -0
  5. package/src/adapters/aider/reader.ts +129 -0
  6. package/src/adapters/aider/records.ts +220 -0
  7. package/src/adapters/aider/writer.ts +269 -0
  8. package/src/adapters/amazon-q-cli/index.ts +8 -0
  9. package/src/adapters/amazon-q-cli/reader.ts +84 -0
  10. package/src/adapters/amazon-q-cli/records.ts +191 -0
  11. package/src/adapters/amazon-q-cli/writer.ts +370 -0
  12. package/src/adapters/amazon-q-ide/index.ts +9 -0
  13. package/src/adapters/amazon-q-ide/reader.ts +131 -0
  14. package/src/adapters/amazon-q-ide/records.ts +141 -0
  15. package/src/adapters/amazon-q-ide/writer.ts +325 -0
  16. package/src/adapters/antigravity/index.ts +9 -0
  17. package/src/adapters/antigravity/proto.ts +194 -0
  18. package/src/adapters/antigravity/reader.ts +424 -0
  19. package/src/adapters/antigravity/records.ts +236 -0
  20. package/src/adapters/antigravity/writer.ts +463 -0
  21. package/src/adapters/auggie/index.ts +36 -0
  22. package/src/adapters/claude-code/index.ts +8 -0
  23. package/src/adapters/claude-code/reader.ts +639 -0
  24. package/src/adapters/claude-code/records.ts +88 -0
  25. package/src/adapters/claude-code/writer.ts +329 -0
  26. package/src/adapters/cline/index.ts +12 -0
  27. package/src/adapters/cline/reader.ts +432 -0
  28. package/src/adapters/cline/records.ts +151 -0
  29. package/src/adapters/cline/vscdb.ts +56 -0
  30. package/src/adapters/cline/writer.ts +656 -0
  31. package/src/adapters/cline/xml.ts +177 -0
  32. package/src/adapters/cline4/index.ts +9 -0
  33. package/src/adapters/cline4/reader.ts +179 -0
  34. package/src/adapters/cline4/records.ts +92 -0
  35. package/src/adapters/cline4/writer.ts +361 -0
  36. package/src/adapters/codebuff/index.ts +8 -0
  37. package/src/adapters/codebuff/project.ts +207 -0
  38. package/src/adapters/codebuff/reader.ts +215 -0
  39. package/src/adapters/codebuff/records.ts +173 -0
  40. package/src/adapters/codebuff/writer.ts +383 -0
  41. package/src/adapters/codex/index.ts +8 -0
  42. package/src/adapters/codex/native.ts +239 -0
  43. package/src/adapters/codex/reader.ts +747 -0
  44. package/src/adapters/codex/records.ts +114 -0
  45. package/src/adapters/codex/writer.ts +479 -0
  46. package/src/adapters/continue/index.ts +9 -0
  47. package/src/adapters/continue/reader.ts +68 -0
  48. package/src/adapters/continue/records.ts +418 -0
  49. package/src/adapters/continue/writer.ts +162 -0
  50. package/src/adapters/copilot-cli/index.ts +9 -0
  51. package/src/adapters/copilot-cli/reader.ts +186 -0
  52. package/src/adapters/copilot-cli/records.ts +241 -0
  53. package/src/adapters/copilot-cli/writer.ts +353 -0
  54. package/src/adapters/crush/index.ts +8 -0
  55. package/src/adapters/crush/reader.ts +180 -0
  56. package/src/adapters/crush/records.ts +169 -0
  57. package/src/adapters/crush/writer.ts +333 -0
  58. package/src/adapters/cursor-cli/index.ts +405 -0
  59. package/src/adapters/cursor-ide/agentkv.ts +95 -0
  60. package/src/adapters/cursor-ide/index.ts +501 -0
  61. package/src/adapters/cursor-ide/transcripts.ts +122 -0
  62. package/src/adapters/cursor-ide/workspaces.ts +119 -0
  63. package/src/adapters/cursor-ide/writer.ts +170 -0
  64. package/src/adapters/devin/index.ts +8 -0
  65. package/src/adapters/devin/reader.ts +305 -0
  66. package/src/adapters/devin/records.ts +277 -0
  67. package/src/adapters/devin/writer.ts +523 -0
  68. package/src/adapters/factory-droid/index.ts +12 -0
  69. package/src/adapters/factory-droid/reader.ts +184 -0
  70. package/src/adapters/factory-droid/records.ts +79 -0
  71. package/src/adapters/factory-droid/writer.ts +350 -0
  72. package/src/adapters/gemini-cli/exchanges.ts +228 -0
  73. package/src/adapters/gemini-cli/index.ts +396 -0
  74. package/src/adapters/gemini-cli/records.ts +521 -0
  75. package/src/adapters/gemini-cli/writer.ts +354 -0
  76. package/src/adapters/goose/index.ts +8 -0
  77. package/src/adapters/goose/reader.ts +138 -0
  78. package/src/adapters/goose/records.ts +199 -0
  79. package/src/adapters/goose/writer.ts +340 -0
  80. package/src/adapters/grok-cli/image.ts +44 -0
  81. package/src/adapters/grok-cli/index.ts +8 -0
  82. package/src/adapters/grok-cli/reader.ts +302 -0
  83. package/src/adapters/grok-cli/records.ts +172 -0
  84. package/src/adapters/grok-cli/writer.ts +329 -0
  85. package/src/adapters/hermes/index.ts +8 -0
  86. package/src/adapters/hermes/reader.ts +249 -0
  87. package/src/adapters/hermes/records.ts +285 -0
  88. package/src/adapters/hermes/writer.ts +452 -0
  89. package/src/adapters/iflow-cli/index.ts +72 -0
  90. package/src/adapters/iflow-cli/records.ts +215 -0
  91. package/src/adapters/iflow-cli/writer.ts +204 -0
  92. package/src/adapters/index.ts +112 -0
  93. package/src/adapters/kilo-cli/index.ts +21 -0
  94. package/src/adapters/kilo-code/index.ts +11 -0
  95. package/src/adapters/kilo-code/writer.ts +92 -0
  96. package/src/adapters/kimi/index.ts +9 -0
  97. package/src/adapters/kimi/reader.ts +92 -0
  98. package/src/adapters/kimi/records.ts +221 -0
  99. package/src/adapters/kimi/writer.ts +367 -0
  100. package/src/adapters/kiro-cli/index.ts +8 -0
  101. package/src/adapters/kiro-cli/reader.ts +87 -0
  102. package/src/adapters/kiro-cli/records.ts +260 -0
  103. package/src/adapters/kiro-cli/writer.ts +305 -0
  104. package/src/adapters/kiro-ide/index.ts +8 -0
  105. package/src/adapters/kiro-ide/writer.ts +52 -0
  106. package/src/adapters/letta/index.ts +8 -0
  107. package/src/adapters/letta/reader.ts +177 -0
  108. package/src/adapters/letta/records.ts +119 -0
  109. package/src/adapters/letta/writer.ts +413 -0
  110. package/src/adapters/mastracode/index.ts +8 -0
  111. package/src/adapters/mastracode/reader.ts +323 -0
  112. package/src/adapters/mastracode/records.ts +336 -0
  113. package/src/adapters/mastracode/writer.ts +385 -0
  114. package/src/adapters/mistral-vibe/index.ts +9 -0
  115. package/src/adapters/mistral-vibe/reader.ts +62 -0
  116. package/src/adapters/mistral-vibe/records.ts +104 -0
  117. package/src/adapters/mistral-vibe/writer.ts +359 -0
  118. package/src/adapters/muse/index.ts +10 -0
  119. package/src/adapters/muse/reader.ts +332 -0
  120. package/src/adapters/muse/records.ts +104 -0
  121. package/src/adapters/muse/writer.ts +387 -0
  122. package/src/adapters/omp/index.ts +13 -0
  123. package/src/adapters/omp/reader.ts +314 -0
  124. package/src/adapters/omp/records.ts +153 -0
  125. package/src/adapters/omp/writer.ts +216 -0
  126. package/src/adapters/opencode/ids.ts +52 -0
  127. package/src/adapters/opencode/index.ts +31 -0
  128. package/src/adapters/opencode/note.ts +8 -0
  129. package/src/adapters/opencode/reader.ts +452 -0
  130. package/src/adapters/opencode/tools.ts +174 -0
  131. package/src/adapters/opencode/v2.ts +367 -0
  132. package/src/adapters/opencode/writer.ts +379 -0
  133. package/src/adapters/openhands/index.ts +8 -0
  134. package/src/adapters/openhands/reader.ts +222 -0
  135. package/src/adapters/openhands/records.ts +136 -0
  136. package/src/adapters/openhands/writer.ts +404 -0
  137. package/src/adapters/pearai/index.ts +10 -0
  138. package/src/adapters/pi/index.ts +28 -0
  139. package/src/adapters/pi/reader.ts +261 -0
  140. package/src/adapters/pi/records.ts +99 -0
  141. package/src/adapters/pi/writer.ts +362 -0
  142. package/src/adapters/qoder-cli/index.ts +8 -0
  143. package/src/adapters/qoder-cli/reader.ts +83 -0
  144. package/src/adapters/qoder-cli/records.ts +45 -0
  145. package/src/adapters/qoder-cli/writer.ts +72 -0
  146. package/src/adapters/qwen-code/index.ts +84 -0
  147. package/src/adapters/qwen-code/records.ts +194 -0
  148. package/src/adapters/qwen-code/writer.ts +224 -0
  149. package/src/adapters/tabnine-cli/index.ts +8 -0
  150. package/src/adapters/tabnine-cli/reader.ts +106 -0
  151. package/src/adapters/tabnine-cli/records.ts +33 -0
  152. package/src/adapters/tabnine-cli/writer.ts +156 -0
  153. package/src/adapters/types.ts +73 -0
  154. package/src/adapters/void/index.ts +9 -0
  155. package/src/adapters/void/reader.ts +48 -0
  156. package/src/adapters/void/records.ts +285 -0
  157. package/src/adapters/void/writer.ts +93 -0
  158. package/src/adapters/vscode-copilot/index.ts +9 -0
  159. package/src/adapters/vscode-copilot/reader.ts +212 -0
  160. package/src/adapters/vscode-copilot/records.ts +388 -0
  161. package/src/adapters/vscode-copilot/shared.ts +68 -0
  162. package/src/adapters/vscode-copilot/writer.ts +546 -0
  163. package/src/adapters/zed/index.ts +8 -0
  164. package/src/adapters/zed/reader.ts +198 -0
  165. package/src/adapters/zed/records.ts +87 -0
  166. package/src/adapters/zed/writer.ts +341 -0
  167. package/src/brief-writers.ts +71 -0
  168. package/src/brief.ts +238 -0
  169. package/src/cli.ts +351 -0
  170. package/src/dashboard/cli.ts +148 -0
  171. package/src/dashboard/conversions.ts +170 -0
  172. package/src/dashboard/report.ts +261 -0
  173. package/src/dashboard/send.ts +213 -0
  174. package/src/dashboard/state.ts +95 -0
  175. package/src/fsutil.ts +133 -0
  176. package/src/gateway/common.ts +232 -0
  177. package/src/gateway/connect.ts +847 -0
  178. package/src/gateway/connectors/aider.ts +356 -0
  179. package/src/gateway/connectors/amazon-q-cli.ts +6 -0
  180. package/src/gateway/connectors/amazon-q-ide.ts +6 -0
  181. package/src/gateway/connectors/antigravity.ts +6 -0
  182. package/src/gateway/connectors/auggie.ts +6 -0
  183. package/src/gateway/connectors/cline-sdk.ts +85 -0
  184. package/src/gateway/connectors/cline.ts +34 -0
  185. package/src/gateway/connectors/codebuff.ts +6 -0
  186. package/src/gateway/connectors/continue.ts +579 -0
  187. package/src/gateway/connectors/copilot-cli.ts +138 -0
  188. package/src/gateway/connectors/crush.ts +313 -0
  189. package/src/gateway/connectors/cursor-cli.ts +6 -0
  190. package/src/gateway/connectors/cursor.ts +6 -0
  191. package/src/gateway/connectors/devin.ts +6 -0
  192. package/src/gateway/connectors/factory-droid.ts +308 -0
  193. package/src/gateway/connectors/goose.ts +79 -0
  194. package/src/gateway/connectors/grok-cli.ts +82 -0
  195. package/src/gateway/connectors/hermes.ts +230 -0
  196. package/src/gateway/connectors/iflow-cli.ts +50 -0
  197. package/src/gateway/connectors/index.ts +86 -0
  198. package/src/gateway/connectors/kilo-cli.ts +434 -0
  199. package/src/gateway/connectors/kilo-code.ts +32 -0
  200. package/src/gateway/connectors/kimi-code.ts +241 -0
  201. package/src/gateway/connectors/kiro-cli.ts +6 -0
  202. package/src/gateway/connectors/kiro-ide.ts +6 -0
  203. package/src/gateway/connectors/letta-code.ts +170 -0
  204. package/src/gateway/connectors/mastracode.ts +91 -0
  205. package/src/gateway/connectors/mistral-vibe.ts +495 -0
  206. package/src/gateway/connectors/muse.ts +27 -0
  207. package/src/gateway/connectors/omp.ts +279 -0
  208. package/src/gateway/connectors/openhands.ts +296 -0
  209. package/src/gateway/connectors/pearai.ts +52 -0
  210. package/src/gateway/connectors/pi.ts +357 -0
  211. package/src/gateway/connectors/qoder-cli.ts +6 -0
  212. package/src/gateway/connectors/qwen-code.ts +411 -0
  213. package/src/gateway/connectors/roo.ts +43 -0
  214. package/src/gateway/connectors/tabnine-cli.ts +6 -0
  215. package/src/gateway/connectors/void.ts +6 -0
  216. package/src/gateway/connectors/vscode-copilot.ts +286 -0
  217. package/src/gateway/connectors/windsurf.ts +6 -0
  218. package/src/gateway/connectors/zed.ts +29 -0
  219. package/src/gateway/signin.ts +323 -0
  220. package/src/handoff.ts +104 -0
  221. package/src/hooks.ts +455 -0
  222. package/src/memory/audit.ts +514 -0
  223. package/src/memory/blobs.ts +94 -0
  224. package/src/memory/cli.ts +117 -0
  225. package/src/memory/distill.ts +381 -0
  226. package/src/memory/externalize.ts +435 -0
  227. package/src/memory/lookups.ts +70 -0
  228. package/src/memory/mcp-install.ts +401 -0
  229. package/src/memory/mcp-server.ts +296 -0
  230. package/src/memory/prepare.ts +179 -0
  231. package/src/memory/preview.ts +154 -0
  232. package/src/memory/project.ts +35 -0
  233. package/src/memory/redact.ts +72 -0
  234. package/src/memory/relevance.ts +187 -0
  235. package/src/memory/repomap.ts +516 -0
  236. package/src/memory/store.ts +270 -0
  237. package/src/memory/sync.ts +166 -0
  238. package/src/memory/tidy.ts +193 -0
  239. package/src/memory/types.ts +118 -0
  240. package/src/org/admin-ops.ts +96 -0
  241. package/src/org/auth.ts +112 -0
  242. package/src/org/backup.ts +115 -0
  243. package/src/org/cli.ts +438 -0
  244. package/src/org/client.ts +404 -0
  245. package/src/org/config.ts +44 -0
  246. package/src/org/crypto.ts +537 -0
  247. package/src/org/db.ts +435 -0
  248. package/src/org/graph.ts +318 -0
  249. package/src/org/oidc.ts +435 -0
  250. package/src/org/policy.ts +398 -0
  251. package/src/org/server.ts +920 -0
  252. package/src/org/sso.ts +316 -0
  253. package/src/org/tls.ts +66 -0
  254. package/src/org/types.ts +193 -0
  255. package/src/self.ts +68 -0
  256. package/src/session/attachments.ts +53 -0
  257. package/src/session/peekcache.ts +50 -0
  258. package/src/session/types.ts +104 -0
  259. package/src/session/util.ts +130 -0
  260. package/src/sqlite.ts +48 -0
@@ -0,0 +1,401 @@
1
+ import { PACKAGE, selfArgs, stableBun } from "../self.ts"
2
+ import { chmodSync, copyFileSync, existsSync, mkdirSync, readFileSync, realpathSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync } from "node:fs"
3
+ import { dirname, join } from "node:path"
4
+ import { runMcpServer } from "./mcp-server.ts"
5
+ import type { ProjectKey } from "./types.ts"
6
+
7
+ // `brain mcp serve | install | uninstall | status`. The installer writes one entry named "brain" into each tool's
8
+ // user-level MCP config, the way hooks.ts does for hooks: merged with the person's own file, the original backed up
9
+ // first under <home>/.brain/backups/, idempotent, removed exactly by uninstall, other entries never touched, and a
10
+ // file that cannot be parsed is left alone and reported. Formats and paths follow Chitta's installer registry
11
+ // (src/install/platforms.ts), reimplemented here: brain does not import Chitta.
12
+
13
+ type Json = Record<string, any>
14
+
15
+ const NAME = "brain"
16
+
17
+ export interface McpTool {
18
+ id: string
19
+ label: string
20
+ /** Config file, relative to home (macOS/Linux/Windows differences are resolved when the table is built). */
21
+ file: string
22
+ /** Folder or file that exists once the tool has run on this machine, relative to home. */
23
+ probe: string[]
24
+ format: "json" | "toml"
25
+ /** Where the entries live in the JSON file. */
26
+ key?: string
27
+ entry?: (command: string, args: string[]) => Json
28
+ }
29
+
30
+ const plain = (command: string, args: string[]): Json => ({ command, args })
31
+
32
+ function vscodeUser(): string {
33
+ return process.platform === "darwin" ? "Library/Application Support/Code/User" : process.platform === "win32" ? "AppData/Roaming/Code/User" : ".config/Code/User"
34
+ }
35
+
36
+ export const MCP_TOOLS: McpTool[] = [
37
+ { id: "claude-code", label: "Claude Code", file: ".claude.json", probe: [".claude.json", ".claude"], format: "json", key: "mcpServers", entry: plain },
38
+ { id: "codex", label: "Codex", file: ".codex/config.toml", probe: [".codex"], format: "toml" },
39
+ { id: "cursor", label: "Cursor", file: ".cursor/mcp.json", probe: [".cursor"], format: "json", key: "mcpServers", entry: plain },
40
+ { id: "gemini", label: "Gemini CLI", file: ".gemini/settings.json", probe: [".gemini"], format: "json", key: "mcpServers", entry: plain },
41
+ {
42
+ id: "opencode",
43
+ label: "OpenCode",
44
+ file: ".config/opencode/opencode.json",
45
+ probe: [".config/opencode"],
46
+ format: "json",
47
+ key: "mcp",
48
+ entry: (command, args) => ({ type: "local", command: [command, ...args], enabled: true }),
49
+ },
50
+ { id: "vscode", label: "VS Code", file: `${vscodeUser()}/mcp.json`, probe: [vscodeUser()], format: "json", key: "servers", entry: (command, args) => ({ type: "stdio", command, args }) },
51
+ ]
52
+
53
+ export interface McpChange {
54
+ tool: string
55
+ path: string
56
+ action: "installed" | "already-installed" | "removed" | "not-installed" | "skipped"
57
+ why?: string
58
+ }
59
+
60
+ interface Opts {
61
+ /** The cli.ts the entry runs (default: this brain's own, absolute). */
62
+ cli?: string
63
+ dryRun?: boolean
64
+ }
65
+
66
+ /** The entry's arguments after bun: this brain's cli.ts (or, run through bunx, the pinned package), then `mcp serve`. */
67
+ const argsFor = (cli?: string) => [...(cli ? selfArgs(cli) : selfArgs()), "mcp", "serve"]
68
+
69
+ // ---- files ----
70
+
71
+ type Loaded = { config: Json; indent: string } | { error: string }
72
+
73
+ function loadJson(path: string): Loaded {
74
+ if (!existsSync(path)) return { config: {}, indent: " " }
75
+ let text: string
76
+ try {
77
+ text = readFileSync(path, "utf8")
78
+ } catch (e) {
79
+ return { error: `cannot read it (${(e as Error).message})` }
80
+ }
81
+ if (!text.trim()) return { config: {}, indent: " " }
82
+ try {
83
+ const v = JSON.parse(text)
84
+ if (!v || typeof v !== "object" || Array.isArray(v)) return { error: "it is not a JSON object; left as it is" }
85
+ return { config: v, indent: text.match(/\n([ \t]+)"/)?.[1] ?? " " }
86
+ } catch {
87
+ return { error: "it is not plain JSON (comments?); left as it is" }
88
+ }
89
+ }
90
+
91
+ const backupPath = (home: string, tool: string, path: string) => join(home, ".brain", "backups", `mcp-${tool}-${path.split("/").pop()}`)
92
+
93
+ /** Atomic write through a symlink, keeping the file's mode; the original is copied once to <home>/.brain/backups/. */
94
+ function writeFile(home: string, tool: string, path: string, text: string) {
95
+ const real = existsSync(path) ? realpathSync(path) : path
96
+ mkdirSync(dirname(real), { recursive: true })
97
+ const backup = backupPath(home, tool, path)
98
+ if (existsSync(real) && !existsSync(backup)) {
99
+ mkdirSync(dirname(backup), { recursive: true, mode: 0o700 })
100
+ copyFileSync(real, backup)
101
+ chmodSync(backup, 0o600)
102
+ }
103
+ let mode: number | undefined
104
+ try {
105
+ mode = statSync(real).mode & 0o777
106
+ } catch {
107
+ // New file: default mode.
108
+ }
109
+ const tmp = `${real}.brain-tmp`
110
+ rmSync(tmp, { force: true })
111
+ writeFileSync(tmp, text, mode === undefined ? {} : { mode })
112
+ if (mode !== undefined) chmodSync(tmp, mode)
113
+ renameSync(tmp, real)
114
+ }
115
+
116
+ /** An entry is ours when it runs `... mcp serve` through a cli.ts, or through the brain package (`bun x @100xprompt/brain@...`). */
117
+ function isOurs(entry: unknown): boolean {
118
+ const e = entry as Json
119
+ if (!e || typeof e !== "object") return false
120
+ const argv: unknown[] = Array.isArray(e.command) ? e.command : [e.command, ...(Array.isArray(e.args) ? e.args : [])]
121
+ const s = argv.filter((x): x is string => typeof x === "string")
122
+ const runs = s[s.length - 3] ?? ""
123
+ return s.length >= 3 && s.slice(-2).join(" ") === "mcp serve" && (/cli\.ts$/.test(runs) || runs === PACKAGE || runs.startsWith(`${PACKAGE}@`))
124
+ }
125
+
126
+ const tomlString = (s: string) => JSON.stringify(s)
127
+
128
+ /**
129
+ * The bun to start the server with: its absolute path, because a tool started from a desktop launcher often has no
130
+ * ~/.bun/bin on its PATH and a bare "bun" would not start. BRAIN_BUN overrides it (tests pin "bun").
131
+ */
132
+ export function bunCommand(): string {
133
+ return stableBun()
134
+ }
135
+
136
+ function tomlBlock(command: string, args: string[]): string {
137
+ return `[mcp_servers.${NAME}]\ncommand = ${tomlString(command)}\nargs = [${args.map(tomlString).join(", ")}]\n`
138
+ }
139
+
140
+ /** The brain's table in a TOML file: its [mcp_servers.brain] header through the line before the next other header. */
141
+ function findTomlBlock(text: string): { start: number; end: number } | undefined {
142
+ const re = /^[ \t]*\[mcp_servers\.brain\][ \t]*(#.*)?$/m
143
+ const m = re.exec(text)
144
+ if (!m) return undefined
145
+ const start = m.index
146
+ const after = start + m[0].length
147
+ const next = /^[ \t]*\[(?!mcp_servers\.brain[.\]])/m
148
+ const rest = text.slice(after)
149
+ const n = next.exec(rest)
150
+ return { start, end: n ? after + n.index : text.length }
151
+ }
152
+
153
+ function parseToml(text: string): Json | undefined {
154
+ try {
155
+ return Bun.TOML.parse(text) as Json
156
+ } catch {
157
+ return undefined
158
+ }
159
+ }
160
+
161
+ // ---- install / uninstall / status ----
162
+
163
+ /**
164
+ * Where a tool keeps this config. Claude Code reads its global config from $CLAUDE_CONFIG_DIR/.claude.json when that is set
165
+ * (a custom config folder), not from ~/.claude.json: writing to the home copy would silently do nothing for those people.
166
+ */
167
+ function configPath(t: McpTool, home: string): string {
168
+ const custom = t.id === "claude-code" ? process.env.CLAUDE_CONFIG_DIR : undefined
169
+ return custom ? join(custom, ".claude.json") : join(home, t.file)
170
+ }
171
+
172
+ /** Whether the tool has run here (its config or folder exists). */
173
+ function hasRun(t: McpTool, home: string): boolean {
174
+ if (t.id === "claude-code" && process.env.CLAUDE_CONFIG_DIR && existsSync(process.env.CLAUDE_CONFIG_DIR)) return true
175
+ return t.probe.some((p) => existsSync(join(home, p)))
176
+ }
177
+
178
+ function prepare(tool: string, home: string): { t: McpTool; path: string } | McpChange {
179
+ const t = MCP_TOOLS.find((x) => x.id === tool)
180
+ if (!t) return { tool, path: "", action: "skipped", why: `no MCP installer for "${tool}"; available: ${MCP_TOOLS.map((x) => x.id).join(", ")}` }
181
+ const path = configPath(t, home)
182
+ if (!hasRun(t, home)) return { tool: t.id, path, action: "skipped", why: `${t.label} has not run on this machine (no ~/${t.probe[0]})` }
183
+ return { t, path }
184
+ }
185
+
186
+ function currentJson(t: McpTool, config: Json): { entry: unknown } | { error: string } {
187
+ const c = config[t.key!]
188
+ if (c === undefined) return { entry: undefined }
189
+ if (!c || typeof c !== "object" || Array.isArray(c)) return { error: `"${t.key}" is not an object; left as it is` }
190
+ return { entry: c[NAME] }
191
+ }
192
+
193
+ export function installMcp(tools: string[], home: string, opts: Opts = {}): McpChange[] {
194
+ const out: McpChange[] = []
195
+ const args = argsFor(opts.cli)
196
+ for (const tool of [...new Set(tools)]) {
197
+ const p = prepare(tool, home)
198
+ if (!("t" in p)) {
199
+ out.push(p)
200
+ continue
201
+ }
202
+ const { t, path } = p
203
+ const done = (action: McpChange["action"], why?: string): McpChange => ({ tool: t.id, path, action, ...(why ? { why } : {}) })
204
+ try {
205
+ if (t.format === "toml") {
206
+ const text = existsSync(path) ? readFileSync(path, "utf8") : ""
207
+ const parsed = text.trim() ? parseToml(text) : {}
208
+ if (!parsed) {
209
+ out.push(done("skipped", "it is not valid TOML; left as it is"))
210
+ continue
211
+ }
212
+ const existing = parsed.mcp_servers?.[NAME]
213
+ if (existing && !isOurs(existing)) {
214
+ out.push(done("skipped", `it already has an mcp_servers.${NAME} that is not this brain's; left as it is`))
215
+ continue
216
+ }
217
+ // Dotted keys or an inline table for mcp_servers cannot take a [mcp_servers.brain] header (a strict TOML parser, like
218
+ // Codex's, rejects the mix; Bun's is lenient, so the check is on the text).
219
+ if (/^[ \t]*mcp_servers[ \t]*[.=]/m.test(text)) {
220
+ out.push(done("skipped", "the file defines mcp_servers with dotted keys or an inline table, which this cannot extend safely; left as it is"))
221
+ continue
222
+ }
223
+ const bun = bunCommand()
224
+ const block = tomlBlock(bun, args)
225
+ const at = findTomlBlock(text)
226
+ if (at && text.slice(at.start, at.end).trimEnd() === block.trimEnd()) {
227
+ out.push(done("already-installed"))
228
+ continue
229
+ }
230
+ if (opts.dryRun) {
231
+ out.push(done(at ? "installed" : "not-installed", at ? "would update it" : "would add it"))
232
+ continue
233
+ }
234
+ const tail = at ? text.slice(at.end) : ""
235
+ const next = at ? `${text.slice(0, at.start)}${block}${tail ? "\n" : ""}${tail}` : `${text}${text && !text.endsWith("\n") ? "\n" : ""}${text.trim() ? "\n" : ""}${block}`
236
+ // The result must parse and say what we meant; otherwise (dotted keys, inline tables elsewhere) leave the file alone.
237
+ const check = parseToml(next)
238
+ if (!check || check.mcp_servers?.[NAME]?.command !== bun) {
239
+ out.push(done("skipped", "the file defines mcp_servers in a way this cannot extend safely; left as it is"))
240
+ continue
241
+ }
242
+ writeFile(home, t.id, path, next)
243
+ out.push(done("installed", at ? "updated it in place" : undefined))
244
+ continue
245
+ }
246
+ const loaded = loadJson(path)
247
+ if ("error" in loaded) {
248
+ out.push(done("skipped", loaded.error))
249
+ continue
250
+ }
251
+ const cur = currentJson(t, loaded.config)
252
+ if ("error" in cur) {
253
+ out.push(done("skipped", cur.error))
254
+ continue
255
+ }
256
+ if (cur.entry !== undefined && !isOurs(cur.entry)) {
257
+ out.push(done("skipped", `it already has an entry named "${NAME}" that is not this brain's; left as it is`))
258
+ continue
259
+ }
260
+ const fresh = t.entry!(bunCommand(), args)
261
+ if (cur.entry !== undefined && JSON.stringify(cur.entry) === JSON.stringify(fresh)) {
262
+ out.push(done("already-installed"))
263
+ continue
264
+ }
265
+ if (opts.dryRun) {
266
+ out.push(done(cur.entry !== undefined ? "installed" : "not-installed", cur.entry !== undefined ? "would update it" : "would add it"))
267
+ continue
268
+ }
269
+ loaded.config[t.key!] ??= {}
270
+ loaded.config[t.key!][NAME] = fresh
271
+ writeFile(home, t.id, path, `${JSON.stringify(loaded.config, null, loaded.indent)}\n`)
272
+ out.push(done("installed", cur.entry !== undefined ? "updated it in place" : undefined))
273
+ } catch (e) {
274
+ out.push(done("skipped", (e as Error).message))
275
+ }
276
+ }
277
+ return out
278
+ }
279
+
280
+ export function uninstallMcp(tools: string[], home: string, opts: Opts = {}): McpChange[] {
281
+ const out: McpChange[] = []
282
+ for (const tool of [...new Set(tools)]) {
283
+ const p = prepare(tool, home)
284
+ if (!("t" in p)) {
285
+ out.push(p)
286
+ continue
287
+ }
288
+ const { t, path } = p
289
+ const done = (action: McpChange["action"], why?: string): McpChange => ({ tool: t.id, path, action, ...(why ? { why } : {}) })
290
+ const noBackup = !existsSync(backupPath(home, t.id, path))
291
+ try {
292
+ if (t.format === "toml") {
293
+ if (!existsSync(path)) {
294
+ out.push(done("not-installed"))
295
+ continue
296
+ }
297
+ const text = readFileSync(path, "utf8")
298
+ if (!parseToml(text)) {
299
+ out.push(done("skipped", "it is not valid TOML; left as it is"))
300
+ continue
301
+ }
302
+ const at = findTomlBlock(text)
303
+ if (!at || !isOurs(parseToml(text)?.mcp_servers?.[NAME])) {
304
+ out.push(done("not-installed"))
305
+ continue
306
+ }
307
+ if (opts.dryRun) {
308
+ out.push(done("installed", "would remove it"))
309
+ continue
310
+ }
311
+ // Take the blank line that separated the block from what came before, so repeated cycles do not grow the file.
312
+ const before = text.slice(0, at.start).replace(/\n\n$/, "\n")
313
+ const next = `${before}${text.slice(at.end)}`
314
+ if (!next.trim() && noBackup) unlinkSync(path)
315
+ else writeFile(home, t.id, path, next)
316
+ out.push(done("removed"))
317
+ continue
318
+ }
319
+ const loaded = loadJson(path)
320
+ if ("error" in loaded) {
321
+ out.push(done("skipped", loaded.error))
322
+ continue
323
+ }
324
+ const cur = currentJson(t, loaded.config)
325
+ if ("error" in cur) {
326
+ out.push(done("skipped", cur.error))
327
+ continue
328
+ }
329
+ if (cur.entry === undefined || !isOurs(cur.entry)) {
330
+ out.push(done("not-installed", cur.entry === undefined ? undefined : `an entry named "${NAME}" exists but is not this brain's; left as it is`))
331
+ continue
332
+ }
333
+ if (opts.dryRun) {
334
+ out.push(done("installed", "would remove it"))
335
+ continue
336
+ }
337
+ const config = loaded.config
338
+ delete config[t.key!][NAME]
339
+ if (!Object.keys(config[t.key!]).length) delete config[t.key!]
340
+ if (!Object.keys(config).length && noBackup) unlinkSync(path)
341
+ else writeFile(home, t.id, path, `${JSON.stringify(config, null, loaded.indent)}\n`)
342
+ out.push(done("removed"))
343
+ } catch (e) {
344
+ out.push(done("skipped", (e as Error).message))
345
+ }
346
+ }
347
+ return out
348
+ }
349
+
350
+ /** Read-only: for each tool, whether the entry is in its config. */
351
+ export function statusMcp(home: string): McpChange[] {
352
+ return MCP_TOOLS.map((t) => {
353
+ const path = configPath(t, home)
354
+ if (!hasRun(t, home)) return { tool: t.id, path, action: "skipped" as const, why: `${t.label} has not run on this machine` }
355
+ if (t.format === "toml") {
356
+ if (!existsSync(path)) return { tool: t.id, path, action: "not-installed" as const }
357
+ const parsed = parseToml(readFileSync(path, "utf8"))
358
+ if (!parsed) return { tool: t.id, path, action: "skipped" as const, why: "it is not valid TOML" }
359
+ return { tool: t.id, path, action: isOurs(parsed.mcp_servers?.[NAME]) ? ("installed" as const) : ("not-installed" as const) }
360
+ }
361
+ const loaded = loadJson(path)
362
+ if ("error" in loaded) return { tool: t.id, path, action: "skipped" as const, why: loaded.error }
363
+ const cur = currentJson(t, loaded.config)
364
+ if ("error" in cur) return { tool: t.id, path, action: "skipped" as const, why: cur.error }
365
+ return { tool: t.id, path, action: isOurs(cur.entry) ? ("installed" as const) : ("not-installed" as const) }
366
+ })
367
+ }
368
+
369
+ // ---- the command ----
370
+
371
+ const flag = (args: string[], name: string) => {
372
+ const i = args.indexOf(name)
373
+ return i >= 0 ? args[i + 1] : undefined
374
+ }
375
+
376
+ export async function mcpCommand(args: string[], home: string): Promise<number> {
377
+ const [action, ...rest] = args
378
+ const say = (s: string) => console.log(s)
379
+ if (action === "serve") {
380
+ // stdout belongs to the protocol: nothing else may be printed here.
381
+ const p = flag(rest, "--project")
382
+ const project: ProjectKey | undefined = p ? { id: p, label: p.split("/").pop() || p } : undefined
383
+ await runMcpServer({ home, ...(project ? { project } : {}) })
384
+ return 0
385
+ }
386
+ if (action === "install" || action === "uninstall" || action === "status") {
387
+ const tool = flag(rest, "--tool")
388
+ const ids = tool ? [tool] : MCP_TOOLS.map((t) => t.id)
389
+ const dryRun = rest.includes("--dry-run")
390
+ const changes = action === "install" ? installMcp(ids, home, { dryRun }) : action === "uninstall" ? uninstallMcp(ids, home, { dryRun }) : tool ? statusMcp(home).filter((c) => c.tool === tool) : statusMcp(home)
391
+ if (!changes.length) {
392
+ say(`no tool "${tool}"; available: ${MCP_TOOLS.map((t) => t.id).join(", ")}`)
393
+ return 1
394
+ }
395
+ for (const c of changes) say(`${c.tool}: ${c.action}${c.path ? ` (${c.path})` : ""}${c.why ? ` - ${c.why}` : ""}`)
396
+ if (action === "install" && !dryRun && changes.some((c) => c.action === "installed")) say("restart the tool for it to pick up the brain MCP server")
397
+ return tool && changes.every((c) => c.action === "skipped") ? 1 : 0
398
+ }
399
+ say("usage: brain mcp serve [--project <id>] | install|uninstall|status [--tool <id>] [--dry-run]")
400
+ return action ? 1 : 0
401
+ }