@100xprompt/brain 0.0.0-stage → 0.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/LICENSE +48 -0
- package/README.md +172 -2
- package/package.json +48 -4
- package/src/adapters/aider/index.ts +8 -0
- package/src/adapters/aider/reader.ts +129 -0
- package/src/adapters/aider/records.ts +220 -0
- package/src/adapters/aider/writer.ts +269 -0
- package/src/adapters/amazon-q-cli/index.ts +8 -0
- package/src/adapters/amazon-q-cli/reader.ts +84 -0
- package/src/adapters/amazon-q-cli/records.ts +191 -0
- package/src/adapters/amazon-q-cli/writer.ts +370 -0
- package/src/adapters/amazon-q-ide/index.ts +9 -0
- package/src/adapters/amazon-q-ide/reader.ts +131 -0
- package/src/adapters/amazon-q-ide/records.ts +141 -0
- package/src/adapters/amazon-q-ide/writer.ts +325 -0
- package/src/adapters/antigravity/index.ts +9 -0
- package/src/adapters/antigravity/proto.ts +194 -0
- package/src/adapters/antigravity/reader.ts +424 -0
- package/src/adapters/antigravity/records.ts +236 -0
- package/src/adapters/antigravity/writer.ts +463 -0
- package/src/adapters/auggie/index.ts +36 -0
- package/src/adapters/claude-code/index.ts +8 -0
- package/src/adapters/claude-code/reader.ts +639 -0
- package/src/adapters/claude-code/records.ts +88 -0
- package/src/adapters/claude-code/writer.ts +329 -0
- package/src/adapters/cline/index.ts +12 -0
- package/src/adapters/cline/reader.ts +432 -0
- package/src/adapters/cline/records.ts +151 -0
- package/src/adapters/cline/vscdb.ts +56 -0
- package/src/adapters/cline/writer.ts +656 -0
- package/src/adapters/cline/xml.ts +177 -0
- package/src/adapters/cline4/index.ts +9 -0
- package/src/adapters/cline4/reader.ts +179 -0
- package/src/adapters/cline4/records.ts +92 -0
- package/src/adapters/cline4/writer.ts +361 -0
- package/src/adapters/codebuff/index.ts +8 -0
- package/src/adapters/codebuff/project.ts +207 -0
- package/src/adapters/codebuff/reader.ts +215 -0
- package/src/adapters/codebuff/records.ts +173 -0
- package/src/adapters/codebuff/writer.ts +383 -0
- package/src/adapters/codex/index.ts +8 -0
- package/src/adapters/codex/native.ts +239 -0
- package/src/adapters/codex/reader.ts +747 -0
- package/src/adapters/codex/records.ts +114 -0
- package/src/adapters/codex/writer.ts +479 -0
- package/src/adapters/continue/index.ts +9 -0
- package/src/adapters/continue/reader.ts +68 -0
- package/src/adapters/continue/records.ts +418 -0
- package/src/adapters/continue/writer.ts +162 -0
- package/src/adapters/copilot-cli/index.ts +9 -0
- package/src/adapters/copilot-cli/reader.ts +186 -0
- package/src/adapters/copilot-cli/records.ts +241 -0
- package/src/adapters/copilot-cli/writer.ts +353 -0
- package/src/adapters/crush/index.ts +8 -0
- package/src/adapters/crush/reader.ts +180 -0
- package/src/adapters/crush/records.ts +169 -0
- package/src/adapters/crush/writer.ts +333 -0
- package/src/adapters/cursor-cli/index.ts +405 -0
- package/src/adapters/cursor-ide/agentkv.ts +95 -0
- package/src/adapters/cursor-ide/index.ts +501 -0
- package/src/adapters/cursor-ide/transcripts.ts +122 -0
- package/src/adapters/cursor-ide/workspaces.ts +119 -0
- package/src/adapters/cursor-ide/writer.ts +170 -0
- package/src/adapters/devin/index.ts +8 -0
- package/src/adapters/devin/reader.ts +305 -0
- package/src/adapters/devin/records.ts +277 -0
- package/src/adapters/devin/writer.ts +523 -0
- package/src/adapters/factory-droid/index.ts +12 -0
- package/src/adapters/factory-droid/reader.ts +184 -0
- package/src/adapters/factory-droid/records.ts +79 -0
- package/src/adapters/factory-droid/writer.ts +350 -0
- package/src/adapters/gemini-cli/exchanges.ts +228 -0
- package/src/adapters/gemini-cli/index.ts +396 -0
- package/src/adapters/gemini-cli/records.ts +521 -0
- package/src/adapters/gemini-cli/writer.ts +354 -0
- package/src/adapters/goose/index.ts +8 -0
- package/src/adapters/goose/reader.ts +138 -0
- package/src/adapters/goose/records.ts +199 -0
- package/src/adapters/goose/writer.ts +340 -0
- package/src/adapters/grok-cli/image.ts +44 -0
- package/src/adapters/grok-cli/index.ts +8 -0
- package/src/adapters/grok-cli/reader.ts +302 -0
- package/src/adapters/grok-cli/records.ts +172 -0
- package/src/adapters/grok-cli/writer.ts +329 -0
- package/src/adapters/hermes/index.ts +8 -0
- package/src/adapters/hermes/reader.ts +249 -0
- package/src/adapters/hermes/records.ts +285 -0
- package/src/adapters/hermes/writer.ts +452 -0
- package/src/adapters/iflow-cli/index.ts +72 -0
- package/src/adapters/iflow-cli/records.ts +215 -0
- package/src/adapters/iflow-cli/writer.ts +204 -0
- package/src/adapters/index.ts +112 -0
- package/src/adapters/kilo-cli/index.ts +21 -0
- package/src/adapters/kilo-code/index.ts +11 -0
- package/src/adapters/kilo-code/writer.ts +92 -0
- package/src/adapters/kimi/index.ts +9 -0
- package/src/adapters/kimi/reader.ts +92 -0
- package/src/adapters/kimi/records.ts +221 -0
- package/src/adapters/kimi/writer.ts +367 -0
- package/src/adapters/kiro-cli/index.ts +8 -0
- package/src/adapters/kiro-cli/reader.ts +87 -0
- package/src/adapters/kiro-cli/records.ts +260 -0
- package/src/adapters/kiro-cli/writer.ts +305 -0
- package/src/adapters/kiro-ide/index.ts +8 -0
- package/src/adapters/kiro-ide/writer.ts +52 -0
- package/src/adapters/letta/index.ts +8 -0
- package/src/adapters/letta/reader.ts +177 -0
- package/src/adapters/letta/records.ts +119 -0
- package/src/adapters/letta/writer.ts +413 -0
- package/src/adapters/mastracode/index.ts +8 -0
- package/src/adapters/mastracode/reader.ts +323 -0
- package/src/adapters/mastracode/records.ts +336 -0
- package/src/adapters/mastracode/writer.ts +385 -0
- package/src/adapters/mistral-vibe/index.ts +9 -0
- package/src/adapters/mistral-vibe/reader.ts +62 -0
- package/src/adapters/mistral-vibe/records.ts +104 -0
- package/src/adapters/mistral-vibe/writer.ts +359 -0
- package/src/adapters/muse/index.ts +10 -0
- package/src/adapters/muse/reader.ts +332 -0
- package/src/adapters/muse/records.ts +104 -0
- package/src/adapters/muse/writer.ts +387 -0
- package/src/adapters/omp/index.ts +13 -0
- package/src/adapters/omp/reader.ts +314 -0
- package/src/adapters/omp/records.ts +153 -0
- package/src/adapters/omp/writer.ts +216 -0
- package/src/adapters/opencode/ids.ts +52 -0
- package/src/adapters/opencode/index.ts +31 -0
- package/src/adapters/opencode/note.ts +8 -0
- package/src/adapters/opencode/reader.ts +452 -0
- package/src/adapters/opencode/tools.ts +174 -0
- package/src/adapters/opencode/v2.ts +367 -0
- package/src/adapters/opencode/writer.ts +379 -0
- package/src/adapters/openhands/index.ts +8 -0
- package/src/adapters/openhands/reader.ts +222 -0
- package/src/adapters/openhands/records.ts +136 -0
- package/src/adapters/openhands/writer.ts +404 -0
- package/src/adapters/pearai/index.ts +10 -0
- package/src/adapters/pi/index.ts +28 -0
- package/src/adapters/pi/reader.ts +261 -0
- package/src/adapters/pi/records.ts +99 -0
- package/src/adapters/pi/writer.ts +362 -0
- package/src/adapters/qoder-cli/index.ts +8 -0
- package/src/adapters/qoder-cli/reader.ts +83 -0
- package/src/adapters/qoder-cli/records.ts +45 -0
- package/src/adapters/qoder-cli/writer.ts +72 -0
- package/src/adapters/qwen-code/index.ts +84 -0
- package/src/adapters/qwen-code/records.ts +194 -0
- package/src/adapters/qwen-code/writer.ts +224 -0
- package/src/adapters/tabnine-cli/index.ts +8 -0
- package/src/adapters/tabnine-cli/reader.ts +106 -0
- package/src/adapters/tabnine-cli/records.ts +33 -0
- package/src/adapters/tabnine-cli/writer.ts +156 -0
- package/src/adapters/types.ts +73 -0
- package/src/adapters/void/index.ts +9 -0
- package/src/adapters/void/reader.ts +48 -0
- package/src/adapters/void/records.ts +285 -0
- package/src/adapters/void/writer.ts +93 -0
- package/src/adapters/vscode-copilot/index.ts +9 -0
- package/src/adapters/vscode-copilot/reader.ts +212 -0
- package/src/adapters/vscode-copilot/records.ts +388 -0
- package/src/adapters/vscode-copilot/shared.ts +68 -0
- package/src/adapters/vscode-copilot/writer.ts +546 -0
- package/src/adapters/zed/index.ts +8 -0
- package/src/adapters/zed/reader.ts +198 -0
- package/src/adapters/zed/records.ts +87 -0
- package/src/adapters/zed/writer.ts +341 -0
- package/src/brief-writers.ts +71 -0
- package/src/brief.ts +238 -0
- package/src/cli.ts +326 -0
- package/src/fsutil.ts +133 -0
- package/src/gateway/connect.ts +845 -0
- package/src/gateway/signin.ts +323 -0
- package/src/handoff.ts +104 -0
- package/src/hooks.ts +455 -0
- package/src/memory/audit.ts +514 -0
- package/src/memory/blobs.ts +94 -0
- package/src/memory/cli.ts +117 -0
- package/src/memory/distill.ts +381 -0
- package/src/memory/externalize.ts +435 -0
- package/src/memory/lookups.ts +70 -0
- package/src/memory/mcp-install.ts +401 -0
- package/src/memory/mcp-server.ts +296 -0
- package/src/memory/prepare.ts +179 -0
- package/src/memory/preview.ts +154 -0
- package/src/memory/project.ts +35 -0
- package/src/memory/redact.ts +72 -0
- package/src/memory/relevance.ts +187 -0
- package/src/memory/repomap.ts +516 -0
- package/src/memory/store.ts +270 -0
- package/src/memory/sync.ts +166 -0
- package/src/memory/tidy.ts +193 -0
- package/src/memory/types.ts +118 -0
- package/src/org/admin-ops.ts +96 -0
- package/src/org/auth.ts +112 -0
- package/src/org/backup.ts +115 -0
- package/src/org/cli.ts +438 -0
- package/src/org/client.ts +404 -0
- package/src/org/config.ts +44 -0
- package/src/org/crypto.ts +537 -0
- package/src/org/db.ts +435 -0
- package/src/org/graph.ts +318 -0
- package/src/org/oidc.ts +435 -0
- package/src/org/policy.ts +398 -0
- package/src/org/server.ts +920 -0
- package/src/org/sso.ts +316 -0
- package/src/org/tls.ts +66 -0
- package/src/org/types.ts +193 -0
- package/src/self.ts +68 -0
- package/src/session/attachments.ts +53 -0
- package/src/session/peekcache.ts +50 -0
- package/src/session/types.ts +104 -0
- package/src/session/util.ts +130 -0
- 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
|
+
}
|