@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,138 @@
1
+ // Gateway connector for GitHub Copilot CLI. Copilot CLI's "bring your own key" providers live in $COPILOT_HOME/providers.json
2
+ // (default ~/.copilot; $COPILOT_PROVIDERS_CONFIG names another file), and settings.json's `model` picks one as the default
3
+ // as "<provider>/<model id>". Checked against @github/copilot 1.0.107 run against a local fake server (2026-11-10): an
4
+ // "openai" provider with baseUrl <route>/v1 sends POST <route>/v1/chat/completions with `Authorization: Bearer <apiKey>`
5
+ // and the provider's `headers`, with no GitHub sign-in; `apiKeyCommand` is NOT read from this file (no key is sent), so
6
+ // the key itself is kept there and the file is made private. A `modelProviderId` left in settings.json by an earlier /model
7
+ // pick makes the CLI refuse the gateway model ("The configured provider/model pair is not available"), so it is cleared
8
+ // while connected. Docs: https://docs.github.com/en/copilot/how-tos/copilot-cli/customize-copilot/use-byok-models
9
+ import { isAbsolute, join } from "node:path"
10
+ import { type Codec, type Connector, dirFromEnv, EDIT_HEADER, type Env, type Json, readKey, routeUrl } from "../common.ts"
11
+
12
+ const PROVIDER = "brain-gateway"
13
+ const copilotHome = (home: string, env: Env) => dirFromEnv(env, "COPILOT_HOME", join(home, ".copilot"))
14
+ /** The providers file the CLI reads: $COPILOT_PROVIDERS_CONFIG when it is set (the CLI then reads nothing else), else <home>/providers.json. */
15
+ const providersPath = (home: string, env: Env) => {
16
+ const named = env.COPILOT_PROVIDERS_CONFIG?.trim()
17
+ return named && isAbsolute(named) ? named : join(copilotHome(home, env), "providers.json")
18
+ }
19
+
20
+ /** Parses a JSON object file ({} when empty); throws on anything else. */
21
+ function parseObject(text: string): Json {
22
+ if (!text.trim()) return {}
23
+ const v = JSON.parse(text)
24
+ if (!v || typeof v !== "object" || Array.isArray(v)) throw new Error("not an object")
25
+ return v as Json
26
+ }
27
+ const render = (text: string, o: Json) => `${JSON.stringify(o, null, text.match(/\n([ \t]+)"/)?.[1] ?? " ")}\n`
28
+ function checkObject(text: string): string | undefined {
29
+ try {
30
+ parseObject(text)
31
+ return undefined
32
+ } catch {
33
+ return "it is not a plain JSON object (comments?); left as it is"
34
+ }
35
+ }
36
+
37
+ /**
38
+ * providers.json: only brain's own entries are touched, so the person's other providers and models stay usable while
39
+ * connected. Keys:
40
+ * providers/brain-gateway the entry of `providers` whose name is brain-gateway
41
+ * models/brain-gateway the entry of `models` whose provider is brain-gateway
42
+ * An entry is replaced where it is, or added at the end; removing the last entry of a list removes the list.
43
+ */
44
+ const ENTRY: Record<string, { list: "providers" | "models"; field: "name" | "provider" }> = {
45
+ [`providers/${PROVIDER}`]: { list: "providers", field: "name" },
46
+ [`models/${PROVIDER}`]: { list: "models", field: "provider" },
47
+ }
48
+ export const providersCodec: Codec = {
49
+ check(text) {
50
+ const why = checkObject(text)
51
+ if (why) return why
52
+ const o = parseObject(text)
53
+ for (const list of ["providers", "models"]) if (list in o && !Array.isArray(o[list])) return `its "${list}" is not a list; left as it is`
54
+ return undefined
55
+ },
56
+ get(text, key) {
57
+ const at = ENTRY[key]
58
+ if (!at) return undefined
59
+ const list = parseObject(text)[at.list]
60
+ return Array.isArray(list) ? list.find((x) => x && typeof x === "object" && x[at.field] === PROVIDER) : undefined
61
+ },
62
+ set(text, key, value) {
63
+ const at = ENTRY[key]
64
+ if (!at) return text
65
+ const o = parseObject(text)
66
+ const list: unknown[] = Array.isArray(o[at.list]) ? [...o[at.list]] : []
67
+ const i = list.findIndex((x) => !!x && typeof x === "object" && (x as Json)[at.field] === PROVIDER)
68
+ if (value === undefined) {
69
+ if (i < 0) return text
70
+ list.splice(i, 1)
71
+ } else if (i >= 0) list[i] = value
72
+ else list.push(value)
73
+ if (list.length) o[at.list] = list
74
+ else delete o[at.list]
75
+ return render(text, o)
76
+ },
77
+ }
78
+
79
+ /**
80
+ * settings.json: top-level keys. `modelProviderId` reads as null when it is absent, and setting it to null removes it:
81
+ * connect sets null (cleared), and disconnect puts back the person's own provider id, or nothing.
82
+ */
83
+ const CLEARED = "modelProviderId"
84
+ export const settingsCodec: Codec = {
85
+ check: checkObject,
86
+ get(text, key) {
87
+ const o = parseObject(text)
88
+ return key in o ? o[key] : key === CLEARED ? null : undefined
89
+ },
90
+ set(text, key, value) {
91
+ const o = parseObject(text)
92
+ if (value === undefined || (key === CLEARED && value === null)) {
93
+ if (!(key in o)) return text
94
+ delete o[key]
95
+ } else o[key] = value
96
+ return render(text, o)
97
+ },
98
+ }
99
+
100
+ export const connectors: Connector[] = [
101
+ {
102
+ id: "copilot-cli",
103
+ label: "GitHub Copilot CLI",
104
+ file: (home, env) => providersPath(home, env),
105
+ probe: [".copilot"],
106
+ authHeader: "authorization",
107
+ edits: (home, s, env) => [
108
+ {
109
+ path: providersPath(home, env),
110
+ format: "custom",
111
+ codec: providersCodec,
112
+ holdsKey: true,
113
+ secretKeys: [`providers/${PROVIDER}`],
114
+ values: {
115
+ [`providers/${PROVIDER}`]: {
116
+ name: PROVIDER,
117
+ type: "openai",
118
+ baseUrl: `${routeUrl(s)}/v1`,
119
+ apiKey: readKey(home),
120
+ ...(s.contextEdit ? { headers: { [EDIT_HEADER]: "on" } } : {}),
121
+ },
122
+ // Gemini takes images; without this Copilot CLI treats a BYOK model as text-only and drops images tools return.
123
+ [`models/${PROVIDER}`]: { id: s.model, provider: PROVIDER, capabilities: { supports: { vision: true } } },
124
+ },
125
+ },
126
+ {
127
+ path: join(copilotHome(home, env), "settings.json"),
128
+ format: "custom",
129
+ codec: settingsCodec,
130
+ values: { model: `${PROVIDER}/${s.model}`, [CLEARED]: null },
131
+ },
132
+ ],
133
+ notes: [
134
+ "Copilot CLI reads a provider's key only from providers.json or the environment, so the key is kept in providers.json, which is now readable by you only (0600).",
135
+ "The gateway model is now Copilot CLI's default and needs no GitHub sign-in; --model, COPILOT_MODEL or /model still pick another model for a session.",
136
+ ],
137
+ },
138
+ ]
@@ -0,0 +1,313 @@
1
+ // Gateway connector for Crush (charmbracelet/crush), checked against v0.100.0 (the copy in ~/.cache/brain-wire/crush,
2
+ // `crush --version`) and its source at tag v0.100.0.
3
+ //
4
+ // Where Crush reads its settings (internal/config/load.go, lookupConfigs and Load): these files are merged, a later one
5
+ // winning over an earlier one:
6
+ // ~/.config/crush/crush.json the person's JSON config (deprecated for crushrc; JSONC, may hold comments)
7
+ // ~/.config/crush/crushrc the person's Bash config (docs/config/README.md, "Where config lives")
8
+ // ~/.local/share/crush/crush.json Crush's data file ($CRUSH_GLOBAL_DATA/crush.json, else $XDG_DATA_HOME/crush/crush.json;
9
+ // load.go GlobalConfigData)
10
+ // ./crushrc, ./crush.json, ... project files, then <project>/.crush/crush.json (the workspace file)
11
+ // The model picked in Crush's UI, and an API key typed into it, go to the data file (store.go: configPath returns
12
+ // globalDataPath for the global scope; UpdatePreferredModel writes models.large / models.small there), and the data file
13
+ // outranks ~/.config/crush. A model selection written to the person's config would be overridden for anyone who has ever
14
+ // picked a model, so brain writes where Crush itself writes: the data file. It is plain JSON (Crush refuses anything else:
15
+ // json.Valid) that Crush edits in place with sjson; the codec below edits it the same way, changing only the members brain
16
+ // owns, so every other byte stays and disconnect gives back the exact bytes.
17
+ //
18
+ // What is written there:
19
+ // providers.brain-gateway a custom OpenAI-compatible provider: "type": "openai-compat" (Chat Completions at
20
+ // <route>/v1; v0.100.0 reads it as "completions" in memory, load.go NormalizeProviderType,
21
+ // without rewriting the file; older versions take it as it is), "api_key": "$(cat '<keyPath>')"
22
+ // (Crush shell-expands api_key, base_url and extra_headers when it loads, load.go
23
+ // configureProviders and the README's "any $(...) in crush.json runs at load time": the key is
24
+ // read from brain's key file at each start and never copied), "extra_headers" with
25
+ // x-100x-context-edit, and one model shown as "100xprompt Pro". The model uses only catwalk v3
26
+ // fields: a v2 field (supports_attachments, can_reason, cost_per_1m_in) would make Crush rewrite
27
+ // the whole file on load (load.go migrateCatwalkV2File).
28
+ // models.large, models.small that model. The small one titles sessions and summarises; left on another provider it would
29
+ // go around the gateway.
30
+ // A model list is given, so Crush does not ask the gateway for /v1/models (403 there).
31
+ import { existsSync } from "node:fs"
32
+ import { isAbsolute, join, resolve } from "node:path"
33
+ import { type Codec, type Connector, EDIT_HEADER, type Env, getAt, type Json, keyPath, modelName, routeUrl, shellQuote } from "../common.ts"
34
+
35
+ /** Crush's data file, resolved as Crush does; the env overrides describe the person's real home, so they count only there. */
36
+ export function crushDataFile(home: string, env: Env): string {
37
+ const own = !!env.HOME && resolve(env.HOME) === resolve(home)
38
+ const g = env.CRUSH_GLOBAL_DATA
39
+ if (own && g && isAbsolute(g) && existsSync(g)) return join(g, "crush.json")
40
+ const x = env.XDG_DATA_HOME
41
+ if (own && x && isAbsolute(x)) return join(x, "crush", "crush.json")
42
+ return join(home, ".local", "share", "crush", "crush.json")
43
+ }
44
+
45
+ // ---- JSON, edited in place ----------------------------------------------------------------------------------------------
46
+
47
+ interface Member {
48
+ key: string
49
+ keyStart: number
50
+ keyEnd: number
51
+ valueStart: number
52
+ valueEnd: number
53
+ value: Node
54
+ }
55
+ type Node = { kind: "object"; start: number; end: number; members: Member[] } | { kind: "value"; start: number; end: number }
56
+ type ObjectNode = Extract<Node, { kind: "object" }>
57
+
58
+ /** Where every value of a JSON text starts and ends (throws on anything that is not JSON). */
59
+ function scan(text: string): Node {
60
+ let i = 0
61
+ const ws = () => {
62
+ while (i < text.length && " \t\n\r".includes(text[i]!)) i++
63
+ }
64
+ const str = (): string => {
65
+ const s = i++
66
+ while (i < text.length && text[i] !== '"') i += text[i] === "\\" ? 2 : 1
67
+ if (i >= text.length) throw new Error("unterminated string")
68
+ i++
69
+ return JSON.parse(text.slice(s, i)) as string
70
+ }
71
+ const value = (): Node => {
72
+ ws()
73
+ const start = i
74
+ const c = text[i]
75
+ if (c === "{") {
76
+ i++
77
+ const members: Member[] = []
78
+ ws()
79
+ if (text[i] === "}") return { kind: "object", start, end: ++i, members }
80
+ for (;;) {
81
+ ws()
82
+ if (text[i] !== '"') throw new Error("expected a key")
83
+ const keyStart = i
84
+ const key = str()
85
+ const keyEnd = i
86
+ ws()
87
+ if (text[i++] !== ":") throw new Error("expected :")
88
+ const v = value()
89
+ members.push({ key, keyStart, keyEnd, valueStart: v.start, valueEnd: v.end, value: v })
90
+ ws()
91
+ const d = text[i++]
92
+ if (d === "}") return { kind: "object", start, end: i, members }
93
+ if (d !== ",") throw new Error("expected , or }")
94
+ }
95
+ }
96
+ if (c === "[") {
97
+ i++
98
+ ws()
99
+ if (text[i] === "]") return { kind: "value", start, end: ++i }
100
+ for (;;) {
101
+ value()
102
+ ws()
103
+ const d = text[i++]
104
+ if (d === "]") return { kind: "value", start, end: i }
105
+ if (d !== ",") throw new Error("expected , or ]")
106
+ }
107
+ }
108
+ if (c === '"') {
109
+ str()
110
+ return { kind: "value", start, end: i }
111
+ }
112
+ while (i < text.length && !",}] \t\n\r".includes(text[i]!)) i++
113
+ if (i === start) throw new Error("expected a value")
114
+ return { kind: "value", start, end: i }
115
+ }
116
+ const root = value()
117
+ ws()
118
+ if (i !== text.length) throw new Error("text after the JSON value")
119
+ return root
120
+ }
121
+
122
+ /** How the file is laid out, so what is added looks like what is there (one line, as Crush writes it, or indented). */
123
+ interface Style {
124
+ pretty: boolean
125
+ indent: string
126
+ /** Between a key and its value: ":" or ": ". */
127
+ colon: string
128
+ }
129
+ function styleOf(text: string, root: ObjectNode): Style {
130
+ const pretty = text.slice(root.start, root.end).includes("\n")
131
+ const indent = text.match(/\n([ \t]+)"/)?.[1] ?? " "
132
+ const m = root.members[0]
133
+ return { pretty, indent, colon: m ? text.slice(m.keyEnd, m.valueStart) : pretty ? ": " : ":" }
134
+ }
135
+ const render = (v: unknown, depth: number, s: Style) => (s.pretty ? JSON.stringify(v, null, s.indent).split("\n").join(`\n${s.indent.repeat(depth)}`) : JSON.stringify(v))
136
+ const splice = (text: string, from: number, to: number, put: string) => text.slice(0, from) + put + text.slice(to)
137
+ const lastIndex = (o: ObjectNode, key: string) => o.members.map((m) => m.key).lastIndexOf(key)
138
+
139
+ /** Adds `"key": value` at the end of `o` (whose members sit at `depth`), separated the way its members already are. */
140
+ function insert(text: string, o: ObjectNode, depth: number, key: string, v: unknown, s: Style): string {
141
+ const member = `${JSON.stringify(key)}${s.colon}${render(v, depth, s)}`
142
+ const n = o.members.length
143
+ if (!n) return splice(text, o.start + 1, o.end - 1, s.pretty ? `\n${s.indent.repeat(depth)}${member}\n${s.indent.repeat(depth - 1)}` : member)
144
+ const last = o.members[n - 1]!
145
+ const gap = n >= 2 ? text.slice(o.members[n - 2]!.valueEnd, last.keyStart) : `,${text.slice(o.start + 1, o.members[0]!.keyStart)}`
146
+ return splice(text, last.valueEnd, last.valueEnd, `${gap}${member}`)
147
+ }
148
+
149
+ /** Removes member `i` of `o` with the separator that came with it (the exact reverse of insert). */
150
+ function remove(text: string, o: ObjectNode, i: number): string {
151
+ const n = o.members.length
152
+ if (n === 1) return splice(text, o.start + 1, o.end - 1, "")
153
+ if (i > 0) return splice(text, o.members[i - 1]!.valueEnd, o.members[i]!.valueEnd, "")
154
+ return splice(text, o.members[0]!.keyStart, o.members[1]!.keyStart, "")
155
+ }
156
+
157
+ /** The object at `path` (undefined when absent or not an object). */
158
+ function objectAt(root: ObjectNode, path: string[]): ObjectNode | undefined {
159
+ let cur: ObjectNode = root
160
+ for (const p of path) {
161
+ const i = lastIndex(cur, p)
162
+ const next = i >= 0 ? cur.members[i]!.value : undefined
163
+ if (!next || next.kind !== "object") return undefined
164
+ cur = next
165
+ }
166
+ return cur
167
+ }
168
+
169
+ const rootOf = (text: string): ObjectNode => {
170
+ const r = scan(text)
171
+ if (r.kind !== "object") throw new Error("not a JSON object")
172
+ return r
173
+ }
174
+
175
+ function jsonSet(text: string, key: string, value: unknown): string {
176
+ const blank = !text.trim()
177
+ if (blank && value === undefined) return text
178
+ const base = blank ? "{}\n" : text
179
+ const root = rootOf(base)
180
+ // A file brain starts is indented, for people to read; one that exists keeps its own layout.
181
+ const s: Style = blank ? { pretty: true, indent: " ", colon: ": " } : styleOf(base, root)
182
+ const parts = key.split("/")
183
+ // The deepest object on the path that exists, and the members still missing below it.
184
+ let depth = 0
185
+ let o = root
186
+ while (depth < parts.length - 1) {
187
+ const i = lastIndex(o, parts[depth]!)
188
+ const next = i >= 0 ? o.members[i]!.value : undefined
189
+ if (!next || next.kind !== "object") break
190
+ o = next
191
+ depth++
192
+ }
193
+ const name = parts[depth]!
194
+ const i = lastIndex(o, name)
195
+ if (value === undefined) {
196
+ if (depth < parts.length - 1 || i < 0) return text
197
+ let out = remove(base, o, i)
198
+ // Drop the objects this left empty (as the core's JSON handling does), never the root.
199
+ for (let d = parts.length - 1; d >= 1; d--) {
200
+ const r = rootOf(out)
201
+ const child = objectAt(r, parts.slice(0, d))
202
+ if (!child || child.members.length) break
203
+ const parent = objectAt(r, parts.slice(0, d - 1))!
204
+ out = remove(out, parent, lastIndex(parent, parts[d - 1]!))
205
+ }
206
+ return out
207
+ }
208
+ // What goes in at `name`: the value itself, or the missing objects around it.
209
+ const nested = parts.slice(depth + 1).reduceRight<unknown>((v, p) => ({ [p]: v }), value)
210
+ if (i >= 0) {
211
+ const m = o.members[i]!
212
+ return splice(base, m.valueStart, m.valueEnd, render(nested, depth + 1, s))
213
+ }
214
+ return insert(base, o, depth + 1, name, nested, s)
215
+ }
216
+
217
+ const canon = (v: unknown): string =>
218
+ 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)
219
+
220
+ /** Drops `key` and the objects it leaves empty (to compare the rest of a file). */
221
+ function without(o: Json, key: string): Json {
222
+ const c = structuredClone(o)
223
+ const ps = key.split("/")
224
+ const chain: Json[] = [c]
225
+ for (const p of ps.slice(0, -1)) {
226
+ const next = chain[chain.length - 1]?.[p]
227
+ if (!next || typeof next !== "object" || Array.isArray(next)) return c
228
+ chain.push(next)
229
+ }
230
+ delete chain[chain.length - 1]![ps[ps.length - 1]!]
231
+ for (let d = ps.length - 1; d >= 1; d--) if (!Object.keys(chain[d]!).length) delete chain[d - 1]![ps[d - 1]!]
232
+ return c
233
+ }
234
+
235
+ /** The members brain sets, with stand-in values: a trial edit with them must land exactly there and touch nothing else. */
236
+ const TRIAL: Record<string, unknown> = {
237
+ "providers/brain-gateway": { name: "trial", type: "openai-compat", models: [{ id: "m" }] },
238
+ "models/large": { model: "m", provider: "brain-gateway" },
239
+ "models/small": { model: "m", provider: "brain-gateway" },
240
+ }
241
+
242
+ /** Crush's data file: plain JSON, edited in place (keys are paths with "/", as in the core's JSON handling). */
243
+ export const crushJson: Codec = {
244
+ check(text) {
245
+ if (!text.trim()) return undefined
246
+ let parsed: unknown
247
+ try {
248
+ parsed = JSON.parse(text)
249
+ } catch {
250
+ return "it is not valid JSON; left as it is"
251
+ }
252
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return "it is not a JSON object; left as it is"
253
+ try {
254
+ let t = text
255
+ for (const [k, v] of Object.entries(TRIAL)) t = jsonSet(t, k, v)
256
+ const after = JSON.parse(t) as Json
257
+ let rest = after
258
+ let was = parsed as Json
259
+ for (const [k, v] of Object.entries(TRIAL)) {
260
+ if (canon(getAt(after, k)) !== canon(v)) throw new Error(k)
261
+ rest = without(rest, k)
262
+ was = without(was, k)
263
+ }
264
+ if (canon(rest) !== canon(was)) throw new Error("other values moved")
265
+ } catch {
266
+ return "brain cannot change it without touching other settings; left as it is"
267
+ }
268
+ return undefined
269
+ },
270
+ get(text, key) {
271
+ if (!text.trim()) return undefined
272
+ return getAt(JSON.parse(text) as Json, key)
273
+ },
274
+ set: jsonSet,
275
+ }
276
+
277
+ export const PROVIDER = "brain-gateway"
278
+
279
+ export const connectors: Connector[] = [
280
+ {
281
+ id: "crush",
282
+ label: "Crush",
283
+ file: crushDataFile,
284
+ probe: [".config/crush", ".local/share/crush"],
285
+ authHeader: "authorization",
286
+ edits: (home, s, env) => {
287
+ const pick = { model: s.model, provider: PROVIDER }
288
+ return [
289
+ {
290
+ path: crushDataFile(home, env),
291
+ format: "custom",
292
+ codec: crushJson,
293
+ values: {
294
+ [`providers/${PROVIDER}`]: {
295
+ name: "100xprompt gateway",
296
+ type: "openai-compat",
297
+ base_url: `${routeUrl(s)}/v1`,
298
+ api_key: `$(cat ${shellQuote(keyPath(home))})`,
299
+ ...(s.contextEdit ? { extra_headers: { [EDIT_HEADER]: "on" } } : {}),
300
+ models: [{ id: s.model, name: modelName(s.model), context_window: 1048576, default_max_tokens: 65536, capabilities: { vision: true } }],
301
+ },
302
+ "models/large": pick,
303
+ "models/small": pick,
304
+ },
305
+ },
306
+ ]
307
+ },
308
+ notes: [
309
+ "Crush reads the key from the brain's key file each time it starts (its api_key runs `cat` on that file); no key is written to Crush's files.",
310
+ "The gateway and the model choice are in Crush's own data file (~/.local/share/crush/crush.json), where Crush keeps the model you pick in its UI; a crushrc or crush.json inside a project still overrides it there.",
311
+ ],
312
+ },
313
+ ]
@@ -0,0 +1,6 @@
1
+ // Gateway connector for Cursor CLI (cursor-agent): none. It talks only to Cursor's backend: its hidden --endpoint picks a
2
+ // Cursor API server (default https://api2.cursor.sh, index.js:33 of build 2025.09.12), and Cursor offers no own key or base
3
+ // URL in the CLI (https://help.openai.com/en/articles/20001506; https://docs.litellm.ai/docs/tutorials/cursor_integration).
4
+ import type { Connector } from "../common.ts"
5
+
6
+ export const connectors: Connector[] = []
@@ -0,0 +1,6 @@
1
+ // Gateway connector for Cursor (IDE): none. Its "Override OpenAI Base URL" and key live only in the app's state database and
2
+ // the OS keychain (Cursor 3.29.9 workbench.desktop.main.js:2037 `openAIBaseUrl`, :20344 `cursorAuth/openAIKey`), and Cursor's
3
+ // servers make the calls, for chat models only (https://cursor.com/docs/settings/api-keys; env-var local agent off: :59).
4
+ import type { Connector } from "../common.ts"
5
+
6
+ export const connectors: Connector[] = []
@@ -0,0 +1,6 @@
1
+ // Gateway connector for Devin CLI: none. The standard CLI uses Cognition's models only (~/.config/devin/config.json names one,
2
+ // with no endpoint or key; wire/drivers/f.ts:47); a custom endpoint exists only in federal air-gapped builds, through
3
+ // $DEVIN_MODELS_FILE with the key in an environment variable (https://docs.devin.ai/federal/air-gapped/models-file).
4
+ import type { Connector } from "../common.ts"
5
+
6
+ export const connectors: Connector[] = []