@100xprompt/brain 0.1.0 → 0.1.2

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 (51) hide show
  1. package/README.md +66 -3
  2. package/package.json +1 -1
  3. package/src/cli.ts +28 -3
  4. package/src/dashboard/cli.ts +148 -0
  5. package/src/dashboard/conversions.ts +170 -0
  6. package/src/dashboard/report.ts +275 -0
  7. package/src/dashboard/send.ts +213 -0
  8. package/src/dashboard/state.ts +95 -0
  9. package/src/gateway/common.ts +232 -0
  10. package/src/gateway/connect.ts +164 -162
  11. package/src/gateway/connectors/aider.ts +356 -0
  12. package/src/gateway/connectors/amazon-q-cli.ts +6 -0
  13. package/src/gateway/connectors/amazon-q-ide.ts +6 -0
  14. package/src/gateway/connectors/antigravity.ts +6 -0
  15. package/src/gateway/connectors/auggie.ts +6 -0
  16. package/src/gateway/connectors/cline-sdk.ts +85 -0
  17. package/src/gateway/connectors/cline.ts +34 -0
  18. package/src/gateway/connectors/codebuff.ts +6 -0
  19. package/src/gateway/connectors/continue.ts +579 -0
  20. package/src/gateway/connectors/copilot-cli.ts +138 -0
  21. package/src/gateway/connectors/crush.ts +313 -0
  22. package/src/gateway/connectors/cursor-cli.ts +6 -0
  23. package/src/gateway/connectors/cursor.ts +6 -0
  24. package/src/gateway/connectors/devin.ts +6 -0
  25. package/src/gateway/connectors/factory-droid.ts +308 -0
  26. package/src/gateway/connectors/goose.ts +79 -0
  27. package/src/gateway/connectors/grok-cli.ts +82 -0
  28. package/src/gateway/connectors/hermes.ts +230 -0
  29. package/src/gateway/connectors/iflow-cli.ts +50 -0
  30. package/src/gateway/connectors/index.ts +86 -0
  31. package/src/gateway/connectors/kilo-cli.ts +434 -0
  32. package/src/gateway/connectors/kilo-code.ts +32 -0
  33. package/src/gateway/connectors/kimi-code.ts +241 -0
  34. package/src/gateway/connectors/kiro-cli.ts +6 -0
  35. package/src/gateway/connectors/kiro-ide.ts +6 -0
  36. package/src/gateway/connectors/letta-code.ts +170 -0
  37. package/src/gateway/connectors/mastracode.ts +91 -0
  38. package/src/gateway/connectors/mistral-vibe.ts +495 -0
  39. package/src/gateway/connectors/muse.ts +27 -0
  40. package/src/gateway/connectors/omp.ts +279 -0
  41. package/src/gateway/connectors/openhands.ts +296 -0
  42. package/src/gateway/connectors/pearai.ts +52 -0
  43. package/src/gateway/connectors/pi.ts +357 -0
  44. package/src/gateway/connectors/qoder-cli.ts +6 -0
  45. package/src/gateway/connectors/qwen-code.ts +411 -0
  46. package/src/gateway/connectors/roo.ts +43 -0
  47. package/src/gateway/connectors/tabnine-cli.ts +6 -0
  48. package/src/gateway/connectors/void.ts +6 -0
  49. package/src/gateway/connectors/vscode-copilot.ts +286 -0
  50. package/src/gateway/connectors/windsurf.ts +6 -0
  51. package/src/gateway/connectors/zed.ts +29 -0
@@ -0,0 +1,495 @@
1
+ // Gateway connector for Mistral Vibe (the `vibe` CLI), checked against vibe 3.1.0 (`vibe --version`; the install in
2
+ // ~/.cache/brain-wire/mistral-vibe, read only) and https://docs.mistral.ai/vibe/code/cli/configuration-reference.
3
+ //
4
+ // How Vibe reaches a custom endpoint (paths under site-packages/):
5
+ // - <VIBE_HOME or ~/.vibe>/config.toml (vibe/core/config/layers/user.py:26, vibe/core/paths/_vibe_home.py:13) holds a
6
+ // `[[providers]]` list (vibe/core/config/models.py:135-162: name, api_base, api_key_env_var, api_style "openai" by
7
+ // default, backend "generic" by default, extra_headers), a `[[models]]` list (models.py:520-524: name = the model id
8
+ // sent, provider, alias, display_name = what the model picker shows) and `active_model`, an alias
9
+ // (vibe/core/config/vibe_schema.py:559). Providers merge with Vibe's built-in ones by name and models by alias
10
+ // (vibe_schema.py:585-596), so adding one entry of each leaves the person's own and Vibe's defaults in place.
11
+ // - api_style "openai" posts to <api_base>/chat/completions
12
+ // (mistralai_vibe_local_harness/vibe/adapters/generic/_openai_chat.py:47) with `Authorization: Bearer <key>`
13
+ // (adapters/generic/_base.py:72-76) plus the provider's extra_headers verbatim (vibe/_runtime_config.py:551-552): so
14
+ // the gateway's OpenAI route, with the context-edit header.
15
+ // - The key: a provider names an environment variable (api_key_env_var); Vibe looks in its run's credentials, the
16
+ // environment, then the OS keyring (vibe/utils/api_keys.py:92-117). There is no key-file or key-command option. The
17
+ // app server loads <VIBE_HOME>/.env into the environment at start (vibe/app_server/stdio.py:199,
18
+ // vibe_schema.py:159-180; a variable already set in the shell wins), and Vibe's own setup stores keys there, owner-only
19
+ // (vibe/setup/auth/api_key_persistence.py:33-39). So the key is copied into that .env (holdsKey, 0600).
20
+ //
21
+ // config.toml is TOML that the core's "toml" format cannot edit (lists of tables), so this file brings its own
22
+ // comment-keeping TOML editor (`tomlCodec`, also used by the Grok CLI connector): it changes only the entries it is given
23
+ // and leaves every other line, comment and blank line as it was.
24
+ import { join } from "node:path"
25
+ import { type Codec, dirFromEnv, EDIT_HEADER, type Connector, type Json, modelName, readKey, routeUrl } from "../common.ts"
26
+
27
+ // ---- a comment-keeping TOML editor ------------------------------------------------------------------------------------
28
+
29
+ /**
30
+ * What a codec key names in the file:
31
+ * key one `key = value` line in a table ([] = the top of the file, before any [table])
32
+ * table a whole [a.b] table, with its sub-tables
33
+ * entry one [[array]] element, found by the first of `fields` it has (Vibe's models: alias, else name); with `mapForm`
34
+ * a file that writes the list as [array.<match>] tables is edited in that form instead
35
+ */
36
+ export type TomlTarget =
37
+ | { kind: "key"; table: string[]; key: string }
38
+ | { kind: "table"; path: string[] }
39
+ | { kind: "entry"; array: string; fields: string[]; match: string; mapForm?: boolean }
40
+
41
+ /** A key as written: bare where it can be, quoted otherwise (also when it starts with a digit, which Bun's parser rejects). */
42
+ const BARE = /^[A-Za-z_-][A-Za-z0-9_-]*$/
43
+ export const tomlKey = (k: string) => (BARE.test(k) ? k : JSON.stringify(k))
44
+
45
+ /** A value on one line: strings as basic strings (JSON's escapes are TOML's), tables as inline tables. */
46
+ export function tomlValue(v: unknown): string {
47
+ if (typeof v === "string") return JSON.stringify(v).replace(/\u007f/g, "\\u007f")
48
+ if (typeof v === "boolean" || (typeof v === "number" && Number.isFinite(v))) return String(v)
49
+ if (Array.isArray(v)) return `[${v.map(tomlValue).join(", ")}]`
50
+ if (v && typeof v === "object") {
51
+ const e = Object.entries(v).filter(([, x]) => x !== undefined && x !== null)
52
+ return e.length ? `{ ${e.map(([k, x]) => `${tomlKey(k)} = ${tomlValue(x)}`).join(", ")} }` : "{}"
53
+ }
54
+ throw new Error("a value TOML cannot hold")
55
+ }
56
+
57
+ interface Row {
58
+ kind: "blank" | "comment" | "header" | "kv" | "more"
59
+ path?: string[]
60
+ array?: boolean
61
+ /** kv: where the value starts (after `=` and the spaces after it). */
62
+ at?: number
63
+ }
64
+ interface Doc {
65
+ lines: string[]
66
+ eol: string
67
+ rows: Row[]
68
+ }
69
+
70
+ /** A dotted key (bare, "basic" or 'literal' parts) starting at `at`, and where it ends. */
71
+ function keyPath(s: string, at: number): { path: string[]; end: number } | undefined {
72
+ const path: string[] = []
73
+ let i = at
74
+ for (;;) {
75
+ while (s[i] === " " || s[i] === "\t") i++
76
+ if (s[i] === '"') {
77
+ let j = i + 1
78
+ while (j < s.length && s[j] !== '"') j += s[j] === "\\" ? 2 : 1
79
+ if (j >= s.length) return undefined
80
+ try {
81
+ path.push(JSON.parse(s.slice(i, j + 1)))
82
+ } catch {
83
+ return undefined
84
+ }
85
+ i = j + 1
86
+ } else if (s[i] === "'") {
87
+ const j = s.indexOf("'", i + 1)
88
+ if (j < 0) return undefined
89
+ path.push(s.slice(i + 1, j))
90
+ i = j + 1
91
+ } else {
92
+ const m = /^[A-Za-z0-9_-]+/.exec(s.slice(i))
93
+ if (!m) return undefined
94
+ path.push(m[0])
95
+ i += m[0].length
96
+ }
97
+ while (s[i] === " " || s[i] === "\t") i++
98
+ if (s[i] !== ".") return { path, end: i }
99
+ i++
100
+ }
101
+ }
102
+
103
+ /** Where a multi-line string that opened with `q` closes on this line (the index after it), or -1. */
104
+ function closeAt(line: string, from: number, q: string): number {
105
+ for (let j = from; j < line.length; j++) {
106
+ if (q === '"""' && line[j] === "\\") {
107
+ j++
108
+ continue
109
+ }
110
+ if (line.startsWith(q, j)) {
111
+ let k = j + 3
112
+ for (let n = 0; n < 2 && line[k] === q[0]; n++) k++
113
+ return k
114
+ }
115
+ }
116
+ return -1
117
+ }
118
+
119
+ /**
120
+ * The file as lines, each classified. A line counts (header, key = value, comment, blank) only when it starts outside any
121
+ * multi-line string or bracket; the lines of a value that spans several are "more". undefined: something it cannot
122
+ * follow (an unclosed string or bracket, a line that is not TOML).
123
+ */
124
+ function readDoc(text: string): Doc | undefined {
125
+ const eol = text.includes("\r\n") ? "\r\n" : "\n"
126
+ const lines = text === "" ? [] : text.replace(/\r?\n$/, "").split(/\r?\n/)
127
+ const rows: Row[] = []
128
+ let ml: string | undefined
129
+ let depth = 0
130
+ for (const line of lines) {
131
+ const top = !ml && depth === 0
132
+ let i = 0
133
+ while (i < line.length) {
134
+ if (ml) {
135
+ const j = closeAt(line, i, ml)
136
+ if (j < 0) break
137
+ ml = undefined
138
+ i = j
139
+ continue
140
+ }
141
+ const c = line[i]!
142
+ if (c === "#") break
143
+ if (line.startsWith('"""', i) || line.startsWith("'''", i)) {
144
+ ml = line.slice(i, i + 3)
145
+ i += 3
146
+ continue
147
+ }
148
+ if (c === '"' || c === "'") {
149
+ let j = i + 1
150
+ while (j < line.length && line[j] !== c) j += c === '"' && line[j] === "\\" ? 2 : 1
151
+ if (j >= line.length) return undefined
152
+ i = j + 1
153
+ continue
154
+ }
155
+ if (c === "[" || c === "{") depth++
156
+ else if (c === "]" || c === "}") depth--
157
+ if (depth < 0) return undefined
158
+ i++
159
+ }
160
+ const t = line.trim()
161
+ if (!top) rows.push({ kind: "more" })
162
+ else if (!t) rows.push({ kind: "blank" })
163
+ else if (t.startsWith("#")) rows.push({ kind: "comment" })
164
+ else if (t.startsWith("[")) {
165
+ const m = /^\s*(\[\[?)/.exec(line)!
166
+ const array = m[1] === "[["
167
+ const k = keyPath(line, m[0].length)
168
+ const close = array ? "]]" : "]"
169
+ if (!k || !line.startsWith(close, k.end)) return undefined
170
+ const rest = line.slice(k.end + close.length).trim()
171
+ if (rest && !rest.startsWith("#")) return undefined
172
+ rows.push({ kind: "header", path: k.path, array })
173
+ } else {
174
+ const k = keyPath(line, 0)
175
+ if (!k || line[k.end] !== "=") return undefined
176
+ let at = k.end + 1
177
+ while (line[at] === " " || line[at] === "\t") at++
178
+ rows.push({ kind: "kv", path: k.path, at })
179
+ }
180
+ }
181
+ if (ml || depth) return undefined
182
+ return { lines, eol, rows }
183
+ }
184
+
185
+ const writeDoc = (lines: string[], eol: string) => (lines.length ? `${lines.join(eol)}${eol}` : "")
186
+ const eq = (a: string[] | undefined, b: string[]) => !!a && a.length === b.length && a.every((x, i) => x === b[i])
187
+ const startsWith = (a: string[] | undefined, b: string[]) => !!a && a.length > b.length && b.every((x, i) => x === a[i])
188
+ const headersWhere = (d: Doc, ok: (r: Row) => boolean) => d.rows.flatMap((r, i) => (r.kind === "header" && ok(r) ? [i] : []))
189
+
190
+ /** The first header after line `i` (lines.length when none); with `within`, headers of its sub-tables do not end it. */
191
+ function sectionEnd(d: Doc, i: number, within?: string[]): number {
192
+ for (let j = i + 1; j < d.rows.length; j++) if (d.rows[j]!.kind === "header" && !(within && startsWith(d.rows[j]!.path, within))) return j
193
+ return d.rows.length
194
+ }
195
+ /** A section's own lines: blank lines and comments at its end belong to whatever follows. */
196
+ function ownEnd(d: Doc, start: number, end: number): number {
197
+ while (end > start + 1 && (d.rows[end - 1]!.kind === "blank" || d.rows[end - 1]!.kind === "comment")) end--
198
+ return end
199
+ }
200
+ /** The end of the key = value starting at line i (a value may span lines). */
201
+ function kvEnd(d: Doc, i: number, limit: number): number {
202
+ let j = i + 1
203
+ while (j < limit && d.rows[j]!.kind === "more") j++
204
+ return j
205
+ }
206
+ /** A removed block takes one blank line before it along (the one an added block was given). */
207
+ const withBlankBefore = (d: Doc, from: number) => (from > 0 && d.rows[from - 1]!.kind === "blank" ? from - 1 : from)
208
+
209
+ /** The ` # comment` after a value, outside any string, with the spaces before it. */
210
+ function splitComment(s: string): { value: string; comment: string } {
211
+ for (let i = 0; i < s.length; i++) {
212
+ const c = s[i]
213
+ if (c === '"' || c === "'") {
214
+ let j = i + 1
215
+ while (j < s.length && s[j] !== c) j += c === '"' && s[j] === "\\" ? 2 : 1
216
+ i = j
217
+ } else if (c === "#") {
218
+ let j = i
219
+ while (j > 0 && /[ \t]/.test(s[j - 1]!)) j--
220
+ return { value: s.slice(0, j), comment: s.slice(j).trimEnd() }
221
+ }
222
+ }
223
+ return { value: s.trimEnd(), comment: "" }
224
+ }
225
+
226
+ interface Splice {
227
+ from: number
228
+ to: number
229
+ lines: string[]
230
+ }
231
+ function apply(d: Doc, edits: Splice[]): string {
232
+ const lines = d.lines.slice()
233
+ for (const e of edits.sort((a, b) => b.from - a.from)) lines.splice(e.from, e.to - e.from, ...e.lines)
234
+ return writeDoc(lines, d.eol)
235
+ }
236
+ /** A new block at the end of the file, after a blank line. */
237
+ const appendBlock = (d: Doc, block: string[]): Splice => ({ from: d.lines.length, to: d.lines.length, lines: d.lines.length ? ["", ...block] : block })
238
+ const body = (v: Json) => Object.entries(v).flatMap(([k, x]) => (x === undefined || x === null ? [] : [`${tomlKey(k)} = ${tomlValue(x)}`]))
239
+
240
+ function setKeyIn(d: Doc, table: string[], key: string, v: unknown): string {
241
+ let start = 0
242
+ let end = d.rows.findIndex((r) => r.kind === "header")
243
+ if (end < 0) end = d.rows.length
244
+ const h = table.length ? headersWhere(d, (r) => !r.array && eq(r.path, table))[0] : undefined
245
+ if (table.length) {
246
+ if (h === undefined) return v === undefined ? writeDoc(d.lines, d.eol) : apply(d, [appendBlock(d, [`[${table.map(tomlKey).join(".")}]`, `${tomlKey(key)} = ${tomlValue(v)}`])])
247
+ start = h + 1
248
+ end = sectionEnd(d, h)
249
+ }
250
+ let lastKv = -1
251
+ for (let i = start; i < end; i++) {
252
+ const r = d.rows[i]!
253
+ if (r.kind !== "kv") continue
254
+ const x = kvEnd(d, i, end)
255
+ if (!eq(r.path, [key])) {
256
+ lastKv = x
257
+ continue
258
+ }
259
+ if (v === undefined) {
260
+ // A table this leaves with nothing in it goes too (with the blank line before it): it was added with the key.
261
+ const emptied = h !== undefined && d.rows.slice(start, end).every((row, j) => start + j >= i && start + j < x ? true : row.kind === "blank")
262
+ return emptied ? apply(d, [{ from: withBlankBefore(d, h!), to: x, lines: [] }]) : apply(d, [{ from: i, to: x, lines: [] }])
263
+ }
264
+ const line = d.lines[i]!
265
+ const prefix = line.slice(0, r.at)
266
+ if (x > i + 1) return apply(d, [{ from: i, to: x, lines: [`${prefix}${tomlValue(v)}`] }])
267
+ // One line: its spacing, trailing comment and 'literal' quoting stay, so a later restore gives the line back as it was.
268
+ const { value, comment } = splitComment(line.slice(r.at))
269
+ const literal = value.startsWith("'") && !value.startsWith("'''") && typeof v === "string" && !/['\u0000-\u001f\u007f]/.test(v)
270
+ return apply(d, [{ from: i, to: x, lines: [`${prefix}${literal ? `'${v}'` : tomlValue(v)}${comment}`] }])
271
+ }
272
+ if (v === undefined) return writeDoc(d.lines, d.eol)
273
+ const at = lastKv >= 0 ? lastKv : table.length ? start : 0
274
+ return apply(d, [{ from: at, to: at, lines: [`${tomlKey(key)} = ${tomlValue(v)}`] }])
275
+ }
276
+
277
+ function setTable(d: Doc, path: string[], v: unknown): string {
278
+ const main = headersWhere(d, (r) => !r.array && eq(r.path, path))[0]
279
+ const subs = headersWhere(d, (r) => startsWith(r.path, path))
280
+ const edits: Splice[] = subs.map((h) => ({ from: withBlankBefore(d, h), to: ownEnd(d, h, sectionEnd(d, h)), lines: [] }))
281
+ const block = v === undefined ? [] : [`[${path.map(tomlKey).join(".")}]`, ...body(v as Json)]
282
+ if (main !== undefined) {
283
+ const to = ownEnd(d, main, sectionEnd(d, main))
284
+ edits.push(v === undefined ? { from: withBlankBefore(d, main), to, lines: [] } : { from: main, to, lines: block })
285
+ } else if (v !== undefined) edits.push(appendBlock(d, block))
286
+ return apply(d, edits)
287
+ }
288
+
289
+ function setEntry(d: Doc, parsed: Json, t: Extract<TomlTarget, { kind: "entry" }>, v: unknown): string {
290
+ const list = parsed[t.array]
291
+ if (list && typeof list === "object" && !Array.isArray(list)) return setTable(d, [t.array, t.match], v)
292
+ const heads = headersWhere(d, (r) => !!r.array && eq(r.path, [t.array]))
293
+ const idx = (Array.isArray(list) ? list : []).findIndex((e) => entryName(e, t.fields) === t.match)
294
+ const range = (h: number) => ({ from: h, to: ownEnd(d, h, sectionEnd(d, h, [t.array])) })
295
+ const block = v === undefined ? [] : [`[[${tomlKey(t.array)}]]`, ...body(v as Json)]
296
+ if (idx >= 0) {
297
+ const r = range(heads[idx]!)
298
+ return apply(d, [v === undefined ? { from: withBlankBefore(d, r.from), to: r.to, lines: [] } : { ...r, lines: block }])
299
+ }
300
+ if (v === undefined) return writeDoc(d.lines, d.eol)
301
+ // A new entry goes after the list's last one, or at the end of the file.
302
+ if (heads.length) {
303
+ const end = range(heads[heads.length - 1]!).to
304
+ return apply(d, [{ from: end, to: end, lines: ["", ...block] }])
305
+ }
306
+ return apply(d, [appendBlock(d, block)])
307
+ }
308
+
309
+ function entryName(e: unknown, fields: string[]): string | undefined {
310
+ if (!e || typeof e !== "object") return undefined
311
+ for (const f of fields) if (typeof (e as Json)[f] === "string") return (e as Json)[f]
312
+ return undefined
313
+ }
314
+
315
+ const parse = (text: string): Json => (text.trim() ? (Bun.TOML.parse(text) as Json) : {})
316
+ const walk = (o: unknown, path: string[]): unknown => path.reduce<unknown>((cur, p) => (cur && typeof cur === "object" && !Array.isArray(cur) ? (cur as Json)[p] : undefined), o)
317
+
318
+ function getTarget(parsed: Json, t: TomlTarget): unknown {
319
+ if (t.kind === "key") {
320
+ const tab = walk(parsed, t.table)
321
+ return tab && typeof tab === "object" && !Array.isArray(tab) ? (tab as Json)[t.key] : undefined
322
+ }
323
+ if (t.kind === "table") return walk(parsed, t.path)
324
+ const list = parsed[t.array]
325
+ if (Array.isArray(list)) return list.find((e) => entryName(e, t.fields) === t.match)
326
+ return t.mapForm && list && typeof list === "object" ? (list as Json)[t.match] : undefined
327
+ }
328
+
329
+ /** The parsed file without the target (and without a table or list the target alone made), for comparing the rest. */
330
+ function without(parsed: Json, t: TomlTarget): Json {
331
+ const o = structuredClone(parsed)
332
+ const drop = (path: string[], last: string) => {
333
+ const parent = walk(o, path) as Json | undefined
334
+ if (!parent || typeof parent !== "object") return
335
+ delete parent[last]
336
+ for (let n = path.length; n > 0; n--) {
337
+ const here = walk(o, path.slice(0, n)) as Json
338
+ if (here && typeof here === "object" && !Array.isArray(here) && !Object.keys(here).length) delete (walk(o, path.slice(0, n - 1)) as Json)[path[n - 1]!]
339
+ }
340
+ }
341
+ if (t.kind === "key") drop(t.table, t.key)
342
+ else if (t.kind === "table") drop(t.path.slice(0, -1), t.path[t.path.length - 1]!)
343
+ else {
344
+ const list = o[t.array]
345
+ if (Array.isArray(list)) {
346
+ o[t.array] = list.filter((e) => entryName(e, t.fields) !== t.match)
347
+ if (!o[t.array].length) delete o[t.array]
348
+ } else drop([t.array], t.match)
349
+ }
350
+ return o
351
+ }
352
+
353
+ /** JSON with keys sorted, so two parses compare by content. */
354
+ const canon = (v: unknown): string =>
355
+ Array.isArray(v) ? `[${v.map(canon).join(",")}]` : v && typeof v === "object" ? `{${Object.keys(v).sort().map((k) => `${JSON.stringify(k)}:${canon((v as Json)[k])}`).join(",")}}` : JSON.stringify(v) ?? "null"
356
+
357
+ /** Why the file's way of writing the target's tables is not one these edits can keep valid (Bun's parser lets some through). */
358
+ function layoutProblem(d: Doc, parsed: Json, t: TomlTarget): string | undefined {
359
+ const firstHeader = d.rows.findIndex((r) => r.kind === "header")
360
+ const pre = d.rows.slice(0, firstHeader < 0 ? d.rows.length : firstHeader)
361
+ const root = t.kind === "key" ? (t.table[0] ?? t.key) : t.kind === "table" ? t.path[0]! : t.array
362
+ const dotted = t.kind === "key" && !t.table.length ? pre.filter((r) => r.kind === "kv" && r.path![0] === root && r.path!.length > 1) : pre.filter((r) => r.kind === "kv" && r.path![0] === root)
363
+ if (dotted.length) return `${root} is written inline or with dotted keys`
364
+ if (t.kind === "key") {
365
+ if (!t.table.length) return pre.filter((r) => r.kind === "kv" && eq(r.path, [t.key])).length > 1 ? `${t.key} is set twice` : undefined
366
+ if (headersWhere(d, (r) => !r.array && eq(r.path, t.table)).length > 1) return `[${t.table.join(".")}] appears twice`
367
+ if (headersWhere(d, (r) => !!r.array && eq(r.path, t.table)).length) return `${t.table.join(".")} is a list of tables`
368
+ return undefined
369
+ }
370
+ if (t.kind === "table") {
371
+ const own = headersWhere(d, (r) => !r.array && eq(r.path, t.path))
372
+ if (own.length > 1) return `[${t.path.join(".")}] appears twice`
373
+ if (headersWhere(d, (r) => !!r.array && (eq(r.path, t.path) || startsWith(t.path, r.path!))).length) return `${t.path.join(".")} sits in a list of tables`
374
+ if (!own.length && walk(parsed, t.path) !== undefined) return `${t.path.join(".")} is written inline or with dotted keys`
375
+ return undefined
376
+ }
377
+ const list = parsed[t.array]
378
+ if (list && typeof list === "object" && !Array.isArray(list)) {
379
+ if (!t.mapForm) return `${t.array} is not a list`
380
+ if (headersWhere(d, (r) => !!r.array && eq(r.path, [t.array])).length) return `${t.array} is written two ways`
381
+ for (const h of headersWhere(d, (r) => !r.array && eq(r.path, [t.array]))) if (d.rows.slice(h + 1, sectionEnd(d, h)).some((r) => r.kind === "kv")) return `${t.array} entries are written with dotted keys`
382
+ for (const k of Object.keys(list)) if (headersWhere(d, (r) => !r.array && eq(r.path, [t.array, k])).length !== 1) return `${t.array}.${k} is not one [${t.array}.${k}] table`
383
+ return undefined
384
+ }
385
+ if (headersWhere(d, (r) => !r.array && eq(r.path, [t.array])).length) return `[${t.array}] is a table here, not a list`
386
+ if (headersWhere(d, (r) => !!r.array && eq(r.path, [t.array])).length !== (Array.isArray(list) ? list.length : 0)) return `${t.array} is not written as [[${t.array}]] tables`
387
+ return undefined
388
+ }
389
+
390
+ /**
391
+ * A comment-keeping TOML codec. `target` says what each key names; `samples` are the keys the connector edits, with a
392
+ * value like the one it writes: before anything changes, each is tried on the file (set, then removed), and a file where
393
+ * that would not give back the same content, or that writes those tables in a way these edits cannot keep valid, is refused.
394
+ */
395
+ export function tomlCodec(target: (key: string) => TomlTarget, samples: Array<[string, unknown]>): Codec {
396
+ const codec: Codec = {
397
+ check(text) {
398
+ if (!text.trim()) return undefined
399
+ let parsed: Json
400
+ try {
401
+ parsed = parse(text)
402
+ } catch {
403
+ return "it is not valid TOML; left as it is"
404
+ }
405
+ const d = readDoc(text)
406
+ if (!d) return "brain cannot follow its layout (an unclosed string or bracket?); left as it is"
407
+ for (const [key, sample] of samples) {
408
+ const t = target(key)
409
+ const why = layoutProblem(d, parsed, t)
410
+ if (why) return `${why}, which brain does not edit; left as it is`
411
+ try {
412
+ const added = codec.set(text, key, sample)
413
+ const a = parse(added)
414
+ const removed = parse(codec.set(added, key, undefined))
415
+ const rest = canon(without(parsed, t))
416
+ if (canon(getTarget(a, t)) !== canon(sample) || canon(without(a, t)) !== rest || getTarget(removed, t) !== undefined || canon(without(removed, t)) !== rest) throw new Error("mismatch")
417
+ } catch {
418
+ return `brain could not edit ${key.replace(/\//g, ".")} there without touching the rest; left as it is`
419
+ }
420
+ }
421
+ return undefined
422
+ },
423
+ get(text, key) {
424
+ return getTarget(parse(text), target(key))
425
+ },
426
+ set(text, key, value) {
427
+ const d = readDoc(text)
428
+ if (!d) throw new Error("unreadable TOML layout")
429
+ const t = target(key)
430
+ if (t.kind === "key") return setKeyIn(d, t.table, t.key, value)
431
+ if (t.kind === "table") return setTable(d, t.path, value)
432
+ return setEntry(d, parse(text), t, value)
433
+ },
434
+ }
435
+ return codec
436
+ }
437
+
438
+ // ---- Mistral Vibe -------------------------------------------------------------------------------------------------------
439
+
440
+ /** The provider and model entries brain adds (the alias is what `active_model` names; the picker shows display_name). */
441
+ const PROVIDER = "100xprompt-gateway"
442
+ const ALIAS = "100xprompt"
443
+ /** The variable in <VIBE_HOME>/.env holding the key, named by the provider's api_key_env_var. */
444
+ const KEY_VAR = "BRAIN_GATEWAY_KEY"
445
+
446
+ const vibeHome = (home: string, env: Record<string, string | undefined>) => dirFromEnv(env, "VIBE_HOME", join(home, ".vibe"))
447
+
448
+ const vibeTarget = (key: string): TomlTarget => {
449
+ const [head, ...rest] = key.split("/")
450
+ if (head === "providers" && rest.length) return { kind: "entry", array: "providers", fields: ["name"], match: rest.join("/") }
451
+ if (head === "models" && rest.length) return { kind: "entry", array: "models", fields: ["alias", "name"], match: rest.join("/"), mapForm: true }
452
+ return { kind: "key", table: [], key }
453
+ }
454
+
455
+ export const vibeToml = tomlCodec(vibeTarget, [
456
+ ["active_model", ALIAS],
457
+ [`providers/${PROVIDER}`, { name: PROVIDER, api_base: "https://gateway.example/flash/v1", api_key_env_var: KEY_VAR, extra_headers: { [EDIT_HEADER]: "on" } }],
458
+ [`models/${ALIAS}`, { name: "model-id", provider: PROVIDER, alias: ALIAS, display_name: "100xprompt Pro" }],
459
+ ])
460
+
461
+ export const connectors: Connector[] = [
462
+ {
463
+ id: "mistral-vibe",
464
+ label: "Mistral Vibe",
465
+ file: (home, env) => join(vibeHome(home, env), "config.toml"),
466
+ probe: [".vibe"],
467
+ authHeader: "authorization",
468
+ edits: (home, s, env) => [
469
+ {
470
+ path: join(vibeHome(home, env), "config.toml"),
471
+ format: "custom",
472
+ codec: vibeToml,
473
+ values: {
474
+ active_model: ALIAS,
475
+ // Tables last: Vibe rewrites the file with tomli_w when it saves a setting, which puts them last, so the entry
476
+ // still reads back the same after that.
477
+ [`providers/${PROVIDER}`]: {
478
+ name: PROVIDER,
479
+ api_base: `${routeUrl(s)}/v1`,
480
+ api_key_env_var: KEY_VAR,
481
+ api_style: "openai",
482
+ backend: "generic",
483
+ ...(s.contextEdit ? { extra_headers: { [EDIT_HEADER]: "on" } } : {}),
484
+ },
485
+ [`models/${ALIAS}`]: { name: s.model, provider: PROVIDER, alias: ALIAS, display_name: modelName(s.model) },
486
+ },
487
+ },
488
+ { path: join(vibeHome(home, env), ".env"), format: "dotenv", holdsKey: true, secretKeys: [KEY_VAR], values: { [KEY_VAR]: readKey(home) } },
489
+ ],
490
+ notes: [
491
+ "Mistral Vibe can read a key only from an environment variable, which it loads from ~/.vibe/.env at start: the key is kept there, so that file is now readable by you only (0600).",
492
+ "A project's own .vibe/config.toml that sets active_model still picks its model in that project; /model switches back and forth.",
493
+ ],
494
+ },
495
+ ]
@@ -0,0 +1,27 @@
1
+ // Gateway connector for Muse Code (Meta's terminal coding agent, `muse`): NOT BUILT, deliberately. Checked against Muse Code
2
+ // 0.2.1 (0.2.1-R1215.1, build b3170a534f), the version brain's session adapter and test/fixtures/native-corpus/muse were
3
+ // captured from. Muse ships as a Linux x86_64 binary only and is not installed here (wire/drivers/f.ts), so nothing below
4
+ // could be run; the evidence is Meta's docs and the one known working custom-endpoint setup.
5
+ //
6
+ // Why brain cannot point Muse at the 100xprompt gateway by editing a file:
7
+ // 1. Muse only talks to Meta's own API shape. Its documented settings (~/.config/muse/settings.json, schema_version 1:
8
+ // https://dev.meta.ai/docs/muse-code/configuration) name no provider, endpoint or key option; sign-in is a Meta
9
+ // account or META_API_KEY (https://dev.meta.ai/docs/muse-code). The 0.2.1 binary enables only its `meta` and `echo`
10
+ // providers (muse-code-openrouter README, below).
11
+ // 2. Its undocumented `endpoint_transport: {base_url, auth}` setting (and the `--base-url` flag) does move the `meta`
12
+ // provider, but before any inference Muse fetches Meta's proprietary model catalog (`/muse-code/models`), then sends
13
+ // Responses requests whose stream events must each carry `sequence_number`, for `meta/muse*` model ids listed in its
14
+ // `model_catalog`. The gateway serves none of that catalog (it answers /v1/models with 403 and has no /muse-code
15
+ // route) and serves gemini-* models. Source: muse-code-openrouter 0.3.2, the adapter session-migrate used to capture
16
+ // the fixture through a loopback (README "Why an adapter is needed"; src/muse_code_openrouter/install.py:240-253
17
+ // writes provider/model/endpoint_transport {base_url: http://127.0.0.1:<port>/v1, auth: "none"}/model_catalog;
18
+ // proxy.py:218-231 serves /muse-code/models and /responses, :200-203 adds sequence_number), and
19
+ // xhluca/session-migrate scripts/native-corpus/capture-muse.py (`muse exec --provider meta --base-url ...`).
20
+ // 3. With `auth: "none"` (the only form that setup shows working) Muse sends no key at all; how Muse would send a key to a
21
+ // moved endpoint is undocumented, so the gateway key could not be delivered from a file either.
22
+ // Reaching the gateway would take a local adapter process translating Muse's catalog and stream to the gateway (as
23
+ // muse-code-openrouter does for OpenRouter), which is not a config edit; brain does not run one. Muse is still supported
24
+ // for sessions (src/adapters/muse).
25
+ import type { Connector } from "../common.ts"
26
+
27
+ export const connectors: Connector[] = []