@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,308 @@
1
+ // Gateway connector for Factory Droid (the `droid` CLI): a bring-your-own-key custom model pointed at the gateway's
2
+ // Anthropic Messages endpoint, made Droid's default model.
3
+ //
4
+ // Checked against droid 1.6.0 (npm `droid`, the darwin-arm64 build in ~/.cache/brain-wire/factory-droid; its bundled
5
+ // settings code reports itself as 1.309.0), read from the strings of the compiled binary, and against
6
+ // https://docs.factory.com/cli/byok/overview:
7
+ // - Custom models live in ~/.factory/settings.json under `customModels` (an array). The legacy ~/.factory/config.json
8
+ // `custom_models` list is only migrated into settings.json on start; 1.6.0 reads nothing else ("Note: Custom models
9
+ // are loaded from ~/.factory/settings.json"). The folder is $FACTORY_HOME_OVERRIDE/.factory when that is set.
10
+ // - An entry is {model, id?, displayName?, baseUrl, apiKey? | apiKeyHelper?, provider, extraHeaders?, maxOutputTokens?,
11
+ // ...}; provider is "anthropic" (Messages), "openai" (Responses), "generic-chat-completion-api" (Chat Completions) or
12
+ // "bedrock-converse" (the zod schema `Ro` in the binary).
13
+ // - `apiKeyHelper` is a shell command whose stdout is the key ("Runs locally when the model is used. Prints a token to
14
+ // stdout."), run with shell:true, cached for 5 minutes (apiKeyHelperTtlMs, FACTORY_API_KEY_HELPER_TTL_MS). So the key
15
+ // is read from the brain's key file at run time, never copied, and a new sign-in reaches Droid without a reconnect.
16
+ // - `extraHeaders` is sent with every request (case-insensitively unique names), so the context-edit header can go.
17
+ // - A model's id is `custom:<displayName with spaces as ->-<n>` unless the entry names its own `id`; this connector gives
18
+ // its entry the id `custom:brain-gateway`, so the default stays put whatever else is in the list.
19
+ // - The default model is `sessionDefaultSettings.model` (older top-level `model` is folded into it on read and only
20
+ // used when sessionDefaultSettings.model is unset). An id that already resolves to an entry's own id is not rewritten.
21
+ // - settings.json is JSON with comments: Droid reads // and /* */ comments, but a trailing comma or a duplicate key
22
+ // makes it drop the whole file (checked with the real CLI: with a trailing comma its custom models are gone and it
23
+ // asks for a Factory login), and it edits the file in place keeping comments. The core's plain-JSON format would
24
+ // refuse a file with comments, and a JSON path cannot address one entry of the customModels list, so this file has
25
+ // its own codec (below): comments and formatting are kept, only the connector's own entry and the default-model value
26
+ // are touched, and a file Droid itself could not read is left alone.
27
+ // - Checked for real (scripts/gateway-real/factory-droid.ts): `droid exec` with no -m, no Factory login, sends its
28
+ // request to <route>/v1/messages with x-api-key from the helper, the context-edit header and the gateway's model.
29
+ import { join } from "node:path"
30
+ import { type Codec, type Connector, dirFromEnv, EDIT_HEADER, type Env, type Json, keyPath, modelName, routeUrl, shellQuote } from "../common.ts"
31
+
32
+ /** The id of the connector's entry in customModels, and the default model value that selects it. */
33
+ export const DROID_MODEL_ID = "custom:brain-gateway"
34
+ const LIST = "customModels"
35
+
36
+ // ---- JSONC with positions ------------------------------------------------------------------------------------------------
37
+
38
+ type Node =
39
+ | { kind: "object"; start: number; end: number; members: Entry[] }
40
+ | { kind: "array"; start: number; end: number; members: Entry[] }
41
+ | { kind: "scalar"; start: number; end: number; value: unknown }
42
+ /** One member of an object (key set) or element of an array; `start` is where its key (or value) begins. */
43
+ interface Entry {
44
+ key?: string
45
+ start: number
46
+ value: Node
47
+ /** Offset of the comma that follows it, when one does. */
48
+ comma?: number
49
+ }
50
+
51
+ class JsoncError extends Error {}
52
+
53
+ /** Parses JSON with comments (as Droid reads it: no trailing commas), keeping where every value is. Throws JsoncError otherwise. */
54
+ function parse(text: string): Node {
55
+ let i = 0
56
+ const fail = (why: string): never => {
57
+ throw new JsoncError(`${why} at offset ${i}`)
58
+ }
59
+ const skip = () => {
60
+ for (;;) {
61
+ const c = text[i]
62
+ if (c === " " || c === "\t" || c === "\n" || c === "\r" || c === "") i++
63
+ else if (c === "/" && text[i + 1] === "/") {
64
+ while (i < text.length && text[i] !== "\n") i++
65
+ } else if (c === "/" && text[i + 1] === "*") {
66
+ const close = text.indexOf("*/", i + 2)
67
+ if (close < 0) fail("unterminated comment")
68
+ i = close + 2
69
+ } else return
70
+ }
71
+ }
72
+ const str = (): string => {
73
+ const start = i++
74
+ while (i < text.length && text[i] !== '"') i += text[i] === "\\" ? 2 : 1
75
+ if (i >= text.length) fail("unterminated string")
76
+ i++
77
+ try {
78
+ return JSON.parse(text.slice(start, i)) as string
79
+ } catch {
80
+ return fail("bad string")
81
+ }
82
+ }
83
+ const value = (): Node => {
84
+ skip()
85
+ const start = i
86
+ const c = text[i]
87
+ if (c === "{" || c === "[") {
88
+ const close = c === "{" ? "}" : "]"
89
+ i++
90
+ const members: Entry[] = []
91
+ const keys = new Set<string>()
92
+ skip()
93
+ while (text[i] !== close) {
94
+ const at = i
95
+ let key: string | undefined
96
+ if (c === "{") {
97
+ if (text[i] !== '"') fail("expected a quoted key")
98
+ key = str()
99
+ if (keys.has(key)) fail(`duplicate key "${key}"`)
100
+ keys.add(key)
101
+ skip()
102
+ if (text[i] !== ":") fail("expected ':'")
103
+ i++
104
+ }
105
+ const v = value()
106
+ const e: Entry = { start: at, value: v, ...(key !== undefined ? { key } : {}) }
107
+ members.push(e)
108
+ skip()
109
+ if (text[i] === ",") {
110
+ e.comma = i++
111
+ skip()
112
+ if (text[i] === close) fail("a trailing comma (Droid cannot read the file with one)")
113
+ } else if (text[i] !== close) fail(`expected ',' or '${close}'`)
114
+ }
115
+ i++
116
+ return { kind: c === "{" ? "object" : "array", start, end: i, members }
117
+ }
118
+ if (c === '"') {
119
+ const v = str()
120
+ return { kind: "scalar", start, end: i, value: v }
121
+ }
122
+ const m = /^(?:-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?|true|false|null)/.exec(text.slice(i, i + 400))
123
+ if (!m) return fail("unexpected character")
124
+ i += m[0].length
125
+ return { kind: "scalar", start, end: i, value: JSON.parse(m[0]) }
126
+ }
127
+ const root = value()
128
+ skip()
129
+ if (i < text.length) fail("unexpected text after the value")
130
+ return root
131
+ }
132
+
133
+ /** The plain value of a parsed node. */
134
+ function plain(n: Node): unknown {
135
+ if (n.kind === "scalar") return n.value
136
+ if (n.kind === "array") return n.members.map((m) => plain(m.value))
137
+ return Object.fromEntries(n.members.map((m) => [m.key!, plain(m.value)]))
138
+ }
139
+
140
+ const member = (o: Node, key: string) => (o.kind === "object" ? o.members.find((m) => m.key === key) : undefined)
141
+ const lineIndent = (text: string, pos: number) => /^[ \t]*/.exec(text.slice(text.lastIndexOf("\n", pos - 1) + 1))![0]
142
+ const indentUnit = (text: string) => /\n([ \t]+)"/.exec(text)?.[1] ?? " "
143
+ const multiline = (text: string, c: Node & { members: Entry[] }) => text.slice(c.start, c.members[0]?.start ?? c.end).includes("\n")
144
+
145
+ /** `v` as JSON at a nesting where the enclosing line starts with `indent`. */
146
+ function render(text: string, v: unknown, indent: string, pretty: boolean): string {
147
+ return pretty ? JSON.stringify(v, null, indentUnit(text)).replace(/\n/g, `\n${indent}`) : JSON.stringify(v)
148
+ }
149
+
150
+ /** Adds an entry (`"key": value` or an array element) at the end of container `c`, in the container's own layout. */
151
+ function append(text: string, c: Node & { members: Entry[] }, key: string | undefined, v: unknown): string {
152
+ const head = key === undefined ? "" : `${JSON.stringify(key)}: `
153
+ const last = c.members[c.members.length - 1]
154
+ if (!last) {
155
+ const outer = lineIndent(text, c.start)
156
+ const inner = outer + indentUnit(text)
157
+ const entry = `${head}${render(text, v, inner, true)}`
158
+ // An empty container: `{}` becomes a block holding the entry. One with only comments in it keeps them after it.
159
+ if (!text.slice(c.start + 1, c.end - 1).trim()) return `${text.slice(0, c.start + 1)}\n${inner}${entry}\n${outer}${text.slice(c.end - 1)}`
160
+ return `${text.slice(0, c.start + 1)}\n${inner}${entry}${text.slice(c.start + 1)}`
161
+ }
162
+ const pretty = multiline(text, c)
163
+ const indent = pretty ? lineIndent(text, last.start) : ""
164
+ const entry = `${head}${render(text, v, indent, pretty)}`
165
+ return `${text.slice(0, last.value.end)},${pretty ? `\n${indent}` : " "}${entry}${text.slice(last.value.end)}`
166
+ }
167
+
168
+ /** Removes entry `at` of container `c` with its separator: the exact inverse of append() for an entry append() added. */
169
+ function remove(text: string, c: Node & { members: Entry[] }, at: number): string {
170
+ const e = c.members[at]!
171
+ if (e.comma !== undefined) {
172
+ const from = at === 0 ? c.start + 1 : c.members[at - 1]!.comma! + 1
173
+ return text.slice(0, from) + text.slice(e.comma + 1)
174
+ }
175
+ if (at > 0) return text.slice(0, c.members[at - 1]!.comma!) + text.slice(e.value.end)
176
+ const rest = text.slice(c.start + 1, e.start) + text.slice(e.value.end, c.end - 1)
177
+ return rest.trim() ? text.slice(0, c.start + 1) + text.slice(e.value.end) : text.slice(0, c.start + 1) + text.slice(c.end - 1)
178
+ }
179
+
180
+ function replace(text: string, c: Node & { members: Entry[] }, e: Entry, v: unknown): string {
181
+ const pretty = multiline(text, c)
182
+ return text.slice(0, e.value.start) + render(text, v, pretty ? lineIndent(text, e.start) : "", pretty) + text.slice(e.value.end)
183
+ }
184
+
185
+ /** The root object of `text` (an empty file reads as an empty object). */
186
+ const rootOf = (text: string) => parse(text.trim() ? text : "{}")
187
+ const baseText = (text: string) => (text.trim() ? text : "{}\n")
188
+
189
+ /** Sets (or, with undefined, removes) the value at an object path. Objects the removal leaves empty go too (never the root). */
190
+ function setPath(text: string, path: string[], v: unknown): string {
191
+ text = baseText(text)
192
+ let node = rootOf(text)
193
+ for (let d = 0; d < path.length - 1; d++) {
194
+ const m = member(node, path[d]!)
195
+ if (!m) {
196
+ if (v === undefined) return text
197
+ const nested = path.slice(d + 1).reduceRight<unknown>((acc, k) => ({ [k]: acc }), v)
198
+ return append(text, node as Node & { members: Entry[] }, path[d]!, nested)
199
+ }
200
+ node = m.value
201
+ if (node.kind !== "object") throw new JsoncError(`${path.slice(0, d + 1).join(".")} is not an object`)
202
+ }
203
+ const last = path[path.length - 1]!
204
+ const obj = node as Node & { members: Entry[] }
205
+ const at = obj.members.findIndex((m) => m.key === last)
206
+ if (v !== undefined) return at >= 0 ? replace(text, obj, obj.members[at]!, v) : append(text, obj, last, v)
207
+ if (at < 0) return text
208
+ text = remove(text, obj, at)
209
+ // Drop the objects this left empty, innermost first.
210
+ for (let d = path.length - 1; d >= 1; d--) {
211
+ let parent = rootOf(text)
212
+ for (const k of path.slice(0, d - 1)) parent = member(parent, k)!.value
213
+ const i = (parent as Node & { members: Entry[] }).members.findIndex((m) => m.key === path[d - 1])
214
+ const child = (parent as Node & { members: Entry[] }).members[i]!.value
215
+ if (child.kind !== "object" || child.members.length) break
216
+ text = remove(text, parent as Node & { members: Entry[] }, i)
217
+ }
218
+ return text
219
+ }
220
+
221
+ /** Sets (or removes) the customModels entry whose `id` is `id`. The list goes when the removal leaves it empty. */
222
+ function setListEntry(text: string, id: string, v: unknown): string {
223
+ text = baseText(text)
224
+ const root = rootOf(text) as Node & { members: Entry[] }
225
+ const listAt = root.members.findIndex((m) => m.key === LIST)
226
+ if (listAt < 0) return v === undefined ? text : append(text, root, LIST, [v])
227
+ const list = root.members[listAt]!.value
228
+ if (list.kind !== "array") throw new JsoncError(`${LIST} is not a list`)
229
+ const at = list.members.findIndex((m) => (plain(m.value) as Json | null)?.id === id)
230
+ if (v !== undefined) return at >= 0 ? replace(text, list, list.members[at]!, v) : append(text, list, undefined, v)
231
+ if (at < 0) return text
232
+ text = remove(text, list, at)
233
+ const after = (rootOf(text) as Node & { members: Entry[] }).members[listAt]!.value
234
+ return after.kind === "array" && !after.members.length ? remove(text, rootOf(text) as Node & { members: Entry[] }, listAt) : text
235
+ }
236
+
237
+ /** `customModels/<id>` addresses one entry of the list (by its id); any other key is an object path ("a/b"). */
238
+ const listKey = (key: string) => (key.startsWith(`${LIST}/`) ? key.slice(LIST.length + 1) : undefined)
239
+
240
+ /**
241
+ * Droid's settings.json: JSON with comments, edited in place. Everything outside the connector's keys stays byte for byte,
242
+ * so a disconnect gives back the file as it was (an empty `customModels: []` or `sessionDefaultSettings: {}` that only held
243
+ * the connector's values goes with them, as the core does for JSON).
244
+ */
245
+ export const droidSettings: Codec = {
246
+ check(text) {
247
+ if (!text.trim()) return undefined
248
+ let root: Node
249
+ try {
250
+ root = parse(text)
251
+ } catch (e) {
252
+ return `it is not valid JSON with comments (${(e as Error).message}); left as it is`
253
+ }
254
+ if (root.kind !== "object") return "it is not a JSON object; left as it is"
255
+ const list = member(root, LIST)
256
+ if (list && list.value.kind !== "array") return `its ${LIST} is not a list; left as it is`
257
+ const defaults = member(root, "sessionDefaultSettings")
258
+ if (defaults && defaults.value.kind !== "object") return "its sessionDefaultSettings is not an object; left as it is"
259
+ return undefined
260
+ },
261
+ get(text, key) {
262
+ if (!text.trim()) return undefined
263
+ const root = plain(parse(text)) as Json
264
+ const id = listKey(key)
265
+ if (id === undefined) return key.split("/").reduce<unknown>((o, k) => (o && typeof o === "object" && !Array.isArray(o) ? (o as Json)[k] : undefined), root)
266
+ const list = root[LIST]
267
+ return Array.isArray(list) ? list.find((m) => m && typeof m === "object" && m.id === id) : undefined
268
+ },
269
+ set(text, key, value) {
270
+ const id = listKey(key)
271
+ return id === undefined ? setPath(text, key.split("/"), value) : setListEntry(text, id, value)
272
+ },
273
+ }
274
+
275
+ const factoryDir = (home: string, env: Env) => join(dirFromEnv(env, "FACTORY_HOME_OVERRIDE", home), ".factory")
276
+
277
+ export const connectors: Connector[] = [
278
+ {
279
+ id: "factory-droid",
280
+ label: "Factory Droid",
281
+ file: (home, env) => join(factoryDir(home, env), "settings.json"),
282
+ probe: [".factory"],
283
+ authHeader: "x-api-key",
284
+ edits: (home, s, env) => [
285
+ {
286
+ path: join(factoryDir(home, env), "settings.json"),
287
+ format: "custom",
288
+ codec: droidSettings,
289
+ values: {
290
+ [`${LIST}/${DROID_MODEL_ID}`]: {
291
+ model: s.model,
292
+ id: DROID_MODEL_ID,
293
+ displayName: modelName(s.model),
294
+ baseUrl: routeUrl(s),
295
+ provider: "anthropic",
296
+ apiKeyHelper: `cat ${shellQuote(keyPath(home))}`,
297
+ ...(s.contextEdit ? { extraHeaders: { [EDIT_HEADER]: "on" } } : {}),
298
+ },
299
+ "sessionDefaultSettings/model": DROID_MODEL_ID,
300
+ },
301
+ },
302
+ ],
303
+ notes: [
304
+ "Droid now starts on the gateway's model (a custom model in ~/.factory/settings.json, made the default); your own models stay in its /model picker.",
305
+ "Droid reads the key from the brain's key file when it needs one (apiKeyHelper, re-read every 5 minutes), so a new sign-in reaches it without reconnecting.",
306
+ ],
307
+ },
308
+ ]
@@ -0,0 +1,79 @@
1
+ // Gateway connector for Goose (block/goose; checked against goose 1.57.0, the binary in ~/.cache/brain-wire/goose).
2
+ //
3
+ // How Goose is pointed at the gateway: a custom ("declarative") provider plus Goose's default provider and model.
4
+ // <config>/custom_providers/brain-gateway.json a provider of its own, engine "openai" (OpenAI Chat Completions):
5
+ // base_url <route>/v1 (Goose appends chat/completions to a URL ending in a version segment), the gateway's model
6
+ // in `models`, `headers` { x-100x-context-edit: on }, and `auth.command` /bin/cat <keyPath>: Goose runs that
7
+ // command for the key (no shell, stdout trimmed, cached for refresh_interval seconds) and sends it as Bearer.
8
+ // So the key is never copied: a new sign-in reaches Goose by itself.
9
+ // <config>/config.yaml GOOSE_PROVIDER: brain-gateway, GOOSE_MODEL: <model> (top-level keys; the rest is left alone).
10
+ // <config> is ~/.config/goose (etcetera's XDG strategy, also on macOS), $XDG_CONFIG_HOME/goose, or $GOOSE_PATH_ROOT/config.
11
+ //
12
+ // Evidence (goose v1.57.0, https://github.com/block/goose/tree/v1.57.0):
13
+ // crates/goose-providers/src/declarative.rs DeclarativeProviderConfig { name, engine, display_name, description,
14
+ // api_key_env (default ""), base_url, models, headers, supports_streaming, requires_auth, base_path, auth, ... };
15
+ // AuthConfig { command, args, refresh_interval (default 3600), timeout_seconds, cwd }; validate_auth(): api_key_env
16
+ // and auth.command are mutually exclusive; load_custom_providers(dir) reads every *.json in the folder.
17
+ // crates/goose/src/config/declarative_providers.rs custom_providers_dir() = Paths::config_dir().join("custom_providers").
18
+ // crates/goose-providers/src/openai.rs from_declarative_config(): host from base_url, derive_base_path(): a path ending
19
+ // in a version segment gets "/chat/completions"; `headers` are added to every request.
20
+ // crates/goose/src/providers/command_auth.rs runs auth.command directly, `.trim()`s stdout, empty output is an error.
21
+ // crates/goose/src/config/paths.rs GOOSE_PATH_ROOT (absolute) -> <root>/config, else etcetera choose_app_strategy.
22
+ // The binary's strings list the same field names (`strings goose | grep api_key_env`), and the real-tool check
23
+ // (scripts/gateway-real/goose.ts) runs this exact setup.
24
+ import { isAbsolute, join, resolve } from "node:path"
25
+ import { type Connector, EDIT_HEADER, type Env, keyPath, modelName, routeUrl, yamlTopLevel } from "../common.ts"
26
+
27
+ /** The provider id (the custom provider's file name and `name`, and GOOSE_PROVIDER), as Codex and OpenCode name it. */
28
+ export const GOOSE_PROVIDER = "brain-gateway"
29
+ /** The context window Goose is told (it compacts the conversation near it): the Gemini Flash models' 1,048,576 tokens. */
30
+ const CONTEXT_LIMIT = 1_048_576
31
+
32
+ /** Goose's config folder, found the way Goose finds it. XDG_CONFIG_HOME describes the real home, so it counts only there. */
33
+ export function gooseDir(home: string, env: Env): string {
34
+ const root = env.GOOSE_PATH_ROOT
35
+ if (root && isAbsolute(root)) return join(root, "config")
36
+ const xdg = env.XDG_CONFIG_HOME
37
+ if (xdg && isAbsolute(xdg) && env.HOME && resolve(home) === resolve(env.HOME)) return join(xdg, "goose")
38
+ return join(home, ".config", "goose")
39
+ }
40
+
41
+ export const connectors: Connector[] = [
42
+ {
43
+ id: "goose",
44
+ label: "Goose",
45
+ file: (home, env) => join(gooseDir(home, env), "config.yaml"),
46
+ // ~/.local/state/goose holds its logs from the first run, ~/.local/share/goose its sessions.
47
+ probe: [".config/goose", ".local/share/goose", ".local/state/goose"],
48
+ authHeader: "authorization",
49
+ edits: (home, s, env) => [
50
+ {
51
+ path: join(gooseDir(home, env), "custom_providers", `${GOOSE_PROVIDER}.json`),
52
+ format: "json",
53
+ values: {
54
+ name: GOOSE_PROVIDER,
55
+ engine: "openai",
56
+ display_name: "100xprompt gateway",
57
+ description: `${modelName(s.model)} through the 100xprompt gateway (set up by brain gateway connect --tool goose)`,
58
+ base_url: `${routeUrl(s)}/v1`,
59
+ models: [{ name: s.model, context_limit: CONTEXT_LIMIT }],
60
+ ...(s.contextEdit ? { headers: { [EDIT_HEADER]: "on" } } : {}),
61
+ supports_streaming: true,
62
+ requires_auth: true,
63
+ // Re-read every 5 minutes, so a running Goose picks up a new sign-in too.
64
+ auth: { command: "/bin/cat", args: [keyPath(home)], refresh_interval: 300 },
65
+ },
66
+ },
67
+ {
68
+ path: join(gooseDir(home, env), "config.yaml"),
69
+ format: "custom",
70
+ codec: yamlTopLevel,
71
+ values: { GOOSE_PROVIDER, GOOSE_MODEL: s.model },
72
+ },
73
+ ],
74
+ notes: [
75
+ "Goose now uses its own provider brain-gateway (custom_providers/brain-gateway.json), which reads the key from the brain's key file when it needs it; nothing goes into the keychain.",
76
+ "GOOSE_PROVIDER or GOOSE_MODEL exported in your shell take precedence over config.yaml: unset them for Goose.",
77
+ ],
78
+ },
79
+ ]
@@ -0,0 +1,82 @@
1
+ // Gateway connector for the Grok CLI: xAI's `grok` ("Grok Build", the closed-source binary whose sessions brain reads as
2
+ // grok-cli), checked against grok 0.2.103 (`grok --version`: the copy installed on the author's machine) with the manual
3
+ // it ships (written to $GROK_HOME/docs/user-guide/ on first run) and https://docs.x.ai/build/settings.
4
+ //
5
+ // How Grok reaches a custom endpoint (docs/user-guide/11-custom-models.md):
6
+ // - <GROK_HOME or ~/.grok>/config.toml takes custom models as [model.<id>] tables (lines 74-90): model (the id sent),
7
+ // base_url, name (shown in the model picker), api_key, env_key, api_backend ("chat_completions", the default, posts to
8
+ // <base_url>/chat/completions; also "responses" and "messages", lines 56-66), context_window, and extra_headers, "sent
9
+ // verbatim with every request" (lines 68, 89). [models] default picks the model new sessions start with (lines 45-52).
10
+ // A project's .grok/config.toml cannot set models: it contributes only MCP servers, plugins and permission rules
11
+ // (05-configuration.md:308).
12
+ // - Session titles are written by a helper model, [models] session_summary (env GROK_SESSION_SUMMARY_MODEL; both names
13
+ // are in the binary's config schema, not in the manual). Left unset, Grok sent that request to the default model's
14
+ // endpoint, here the gateway, but for xAI's "grok-build" (seen with the real binary against the stand-in gateway), so
15
+ // it is pointed at the gateway's model too, as Claude Code's small-model setting is.
16
+ // - The key (lines 92-99): the model's api_key, else the variables named by env_key, else the person's Grok sign-in, else
17
+ // XAI_API_KEY. There is no key-file or key-command option for a model, and Grok reads no .env file, so the key is
18
+ // copied into the model's table (holdsKey: config.toml is made 0600). `[auth] auth_provider_command` does run a
19
+ // command for a token, but that token becomes the session token every model without a key of its own uses, xAI's
20
+ // included, in place of the person's Grok sign-in (02-authentication.md:246-256), so it is not used.
21
+ //
22
+ // Not this connector: the open-source superagent-ai/grok-cli (GitHub), a different tool that keeps
23
+ // ~/.grok/user-settings.json (apiKey, baseURL, defaultModel). Brain's grok-cli sessions are xAI's (src/adapters/grok-cli).
24
+ //
25
+ // config.toml is edited with the comment-keeping TOML codec from the Mistral Vibe connector: [models] default and
26
+ // session_summary and the [model."100xprompt"] table, nothing else; every other line stays as it was.
27
+ import { join } from "node:path"
28
+ import { dirFromEnv, EDIT_HEADER, type Connector, modelName, readKey, routeUrl } from "../common.ts"
29
+ import { type TomlTarget, tomlCodec } from "./mistral-vibe.ts"
30
+
31
+ /** The [model.<id>] table brain adds; `grok -m 100xprompt` and /model pick it, the picker shows its name. */
32
+ const ID = "100xprompt"
33
+
34
+ const grokHome = (home: string, env: Record<string, string | undefined>) => dirFromEnv(env, "GROK_HOME", join(home, ".grok"))
35
+
36
+ const grokTarget = (key: string): TomlTarget => {
37
+ const [head, ...rest] = key.split("/")
38
+ if (head === "models" && rest.length === 1) return { kind: "key", table: ["models"], key: rest[0]! }
39
+ if (head === "model" && rest.length) return { kind: "table", path: ["model", rest.join("/")] }
40
+ return { kind: "key", table: [], key }
41
+ }
42
+
43
+ export const grokToml = tomlCodec(grokTarget, [
44
+ ["models/default", ID],
45
+ ["models/session_summary", ID],
46
+ [`model/${ID}`, { model: "model-id", name: "100xprompt Pro", base_url: "https://gateway.example/flash/v1", api_backend: "chat_completions", api_key: "k", extra_headers: { [EDIT_HEADER]: "on" } }],
47
+ ])
48
+
49
+ export const connectors: Connector[] = [
50
+ {
51
+ id: "grok-cli",
52
+ label: "Grok CLI",
53
+ file: (home, env) => join(grokHome(home, env), "config.toml"),
54
+ probe: [".grok"],
55
+ authHeader: "authorization",
56
+ edits: (home, s, env) => [
57
+ {
58
+ path: join(grokHome(home, env), "config.toml"),
59
+ format: "custom",
60
+ codec: grokToml,
61
+ holdsKey: true,
62
+ secretKeys: [`model/${ID}`],
63
+ values: {
64
+ "models/default": ID,
65
+ "models/session_summary": ID,
66
+ [`model/${ID}`]: {
67
+ model: s.model,
68
+ name: modelName(s.model),
69
+ base_url: `${routeUrl(s)}/v1`,
70
+ api_backend: "chat_completions",
71
+ api_key: readKey(home),
72
+ ...(s.contextEdit ? { extra_headers: { [EDIT_HEADER]: "on" } } : {}),
73
+ },
74
+ },
75
+ },
76
+ ],
77
+ notes: [
78
+ "Grok CLI takes a custom model's key only in its config.toml or from an environment variable: the key is kept in config.toml, so that file is now readable by you only (0600).",
79
+ "New Grok sessions start on the gateway's model, which also writes their titles; /model or grok -m still picks Grok's own models, which keep using your Grok sign-in.",
80
+ ],
81
+ },
82
+ ]