@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,411 @@
1
+ // Gateway connector for Qwen Code (`qwen`), and the comment-keeping JSON editor it (and iflow-cli.ts) writes settings with.
2
+ //
3
+ // Checked against Qwen Code 0.27.0 as installed (~/.cache/brain-wire/qwen-code, @qwen-code/qwen-code; the bundle is
4
+ // minified, so places are named by chunk file, line and function) and its docs
5
+ // (https://qwenlm.github.io/qwen-code-docs/en/users/configuration/model-providers/):
6
+ // - Config lives in $QWEN_HOME or ~/.qwen (chunks/chunk-5VB4YX3Z.js:3 Storage.getGlobalQwenDir); user settings are
7
+ // <dir>/settings.json, read as JSON with comments (strip-json-comments then JSON.parse: no trailing commas;
8
+ // chunks/chunk-E6R76DS6.js:39 loadAndMigrate). Qwen Code writes it back itself keeping comments and the file's mode
9
+ // (chunks/chunk-6O22NZFU.js:3 updateSettingsFilePreservingFormat, writeWithBackupSync), and adds "$version": 4 to a file
10
+ // that has none on its first start.
11
+ // - A custom OpenAI-compatible endpoint is a `modelProviders` entry: { id, name, description, baseUrl, envKey,
12
+ // generationConfig: { customHeaders } }. A provider id that is not built in is routed by `providerProtocol`
13
+ // ({ "<id>": "openai" }, Chat Completions unless wireApi says otherwise; chunks/chunk-KUBWE5YV.js:3
14
+ // resolveProviderProtocol, ModelRegistry). The selected model is `model.name` (+ `model.baseUrl` to tell entries with
15
+ // the same id apart) under `security.auth.selectedType` "openai". The entry's id, baseUrl and envKey win over
16
+ // OPENAI_MODEL / OPENAI_BASE_URL / OPENAI_API_KEY (chunks/chunk-576P2HBP.js:2151 resolveModelConfig).
17
+ // - The key: only ever from the environment variable the entry names (`envKey`); there is no key-file or command
18
+ // option. The docs recommend ~/.qwen/.env for it, which Qwen Code always loads, also in a project with its own .env
19
+ // (chunks/chunk-WGMYJOAD.js:3 findEnvFiles pushes the home candidates after a project's). So the key is copied there,
20
+ // under a name of its own (BRAIN_GATEWAY_KEY, so no OPENAI_API_KEY of the person's is touched), and that file is 0600.
21
+ // - Custom headers: generationConfig.customHeaders is sent with every request (chunks/chunk-ESPAP3UW.js buildHeaders),
22
+ // so `x-100x-context-edit: on` goes there.
23
+ // What connect writes: settings.json gets a "brain-gateway" provider ("100xprompt gateway", model shown as its 100xprompt
24
+ // name), its providerProtocol mapping, and the selection (auth type, model.name, model.baseUrl); ~/.qwen/.env gets the
25
+ // key. The person's own providers, models and comments are left as they are; disconnect puts every key back.
26
+ // Needs Qwen Code 0.23 or later (custom provider ids through providerProtocol); older ones skip the provider.
27
+ import { existsSync } from "node:fs"
28
+ import { join } from "node:path"
29
+ import { type Codec, type Connector, EDIT_HEADER, type Env, type GatewaySettings, type Json, modelName, readKey, routeUrl } from "../common.ts"
30
+
31
+ // ---- JSON with comments, edited in place ----------------------------------------------------------------------------
32
+ //
33
+ // The core's "json" format refuses a file with comments and rewrites the whole file. This codec changes only the bytes of
34
+ // the keys it is given: a new key is added as the last member of its object (in the file's own indentation), a value is
35
+ // replaced where it stands, and a removed key takes away exactly what adding it put there, so disconnect gives back the
36
+ // file byte for byte. Keys are paths with "/" (security/auth/selectedType), like the core's json format; an object that a
37
+ // removal leaves empty is removed too (as the core does), never the root.
38
+
39
+ interface JObject {
40
+ t: "object"
41
+ start: number
42
+ end: number
43
+ members: JMember[]
44
+ }
45
+ interface JArray {
46
+ t: "array"
47
+ start: number
48
+ end: number
49
+ items: JNode[]
50
+ }
51
+ interface JScalar {
52
+ t: "scalar"
53
+ start: number
54
+ end: number
55
+ value: unknown
56
+ }
57
+ type JNode = JObject | JArray | JScalar
58
+ interface JMember {
59
+ key: string
60
+ keyStart: number
61
+ value: JNode
62
+ /** Index of the comma after the value, -1 for the last member. */
63
+ comma: number
64
+ }
65
+
66
+ class JsoncError extends Error {}
67
+
68
+ /** Parses JSON with // and /* comments (no trailing commas: the tools' own JSON.parse refuses them). Empty text: no root. */
69
+ function parseJsonc(text: string): { root?: JNode; duplicate: boolean } {
70
+ let i = text.charCodeAt(0) === 0xfeff ? 1 : 0
71
+ let duplicate = false
72
+ const fail = (why: string): never => {
73
+ throw new JsoncError(`${why} at offset ${i}`)
74
+ }
75
+ const skip = () => {
76
+ for (;;) {
77
+ const c = text[i]
78
+ if (c === " " || c === "\t" || c === "\n" || c === "\r") i++
79
+ else if (c === "/" && text[i + 1] === "/") {
80
+ const n = text.indexOf("\n", i)
81
+ i = n < 0 ? text.length : n
82
+ } else if (c === "/" && text[i + 1] === "*") {
83
+ const n = text.indexOf("*/", i + 2)
84
+ if (n < 0) fail("unclosed comment")
85
+ i = n + 2
86
+ } else return
87
+ }
88
+ }
89
+ const str = (): string => {
90
+ const s = i++
91
+ while (i < text.length && text[i] !== '"') {
92
+ if (text[i] === "\\") i++
93
+ i++
94
+ }
95
+ if (i >= text.length) fail("unclosed string")
96
+ i++
97
+ try {
98
+ return JSON.parse(text.slice(s, i)) as string
99
+ } catch {
100
+ return fail("bad string")
101
+ }
102
+ }
103
+ const value = (): JNode => {
104
+ skip()
105
+ const s = i
106
+ const c = text[i]
107
+ if (c === "{") {
108
+ i++
109
+ const members: JMember[] = []
110
+ const seen = new Set<string>()
111
+ skip()
112
+ if (text[i] === "}") return { t: "object", start: s, end: ++i, members }
113
+ for (;;) {
114
+ skip()
115
+ if (text[i] !== '"') fail("expected a key")
116
+ const keyStart = i
117
+ const key = str()
118
+ if (seen.has(key)) duplicate = true
119
+ seen.add(key)
120
+ skip()
121
+ if (text[i] !== ":") fail("expected ':'")
122
+ i++
123
+ const v = value()
124
+ skip()
125
+ if (text[i] === ",") {
126
+ members.push({ key, keyStart, value: v, comma: i++ })
127
+ continue
128
+ }
129
+ if (text[i] === "}") {
130
+ members.push({ key, keyStart, value: v, comma: -1 })
131
+ return { t: "object", start: s, end: ++i, members }
132
+ }
133
+ fail("expected ',' or '}'")
134
+ }
135
+ }
136
+ if (c === "[") {
137
+ i++
138
+ const items: JNode[] = []
139
+ skip()
140
+ if (text[i] === "]") return { t: "array", start: s, end: ++i, items }
141
+ for (;;) {
142
+ items.push(value())
143
+ skip()
144
+ if (text[i] === ",") {
145
+ i++
146
+ continue
147
+ }
148
+ if (text[i] === "]") return { t: "array", start: s, end: ++i, items }
149
+ fail("expected ',' or ']'")
150
+ }
151
+ }
152
+ if (c === '"') return { t: "scalar", start: s, value: str(), end: i }
153
+ const lit = /true|false|null|-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?/y
154
+ lit.lastIndex = i
155
+ const m = lit.exec(text)
156
+ if (!m) return fail("unexpected character")
157
+ i += m[0].length
158
+ return { t: "scalar", start: s, end: i, value: JSON.parse(m[0]) }
159
+ }
160
+ skip()
161
+ if (i >= text.length) return { duplicate }
162
+ const root = value()
163
+ skip()
164
+ if (i < text.length) fail("unexpected text after the end")
165
+ return { root, duplicate }
166
+ }
167
+
168
+ const valueOf = (n: JNode): unknown =>
169
+ n.t === "scalar" ? n.value : n.t === "array" ? n.items.map(valueOf) : Object.fromEntries(n.members.map((m) => [m.key, valueOf(m.value)]))
170
+
171
+ /** The member called `key` (the last one, as JSON.parse would take it). */
172
+ const memberOf = (o: JObject, key: string) => [...o.members].reverse().find((m) => m.key === key)
173
+
174
+ /** Walks `path` from the root: the objects passed through, and the member at the end if it is there. */
175
+ function walk(root: JObject, path: string[]): { chain: Array<{ obj: JObject; member?: JMember }> } {
176
+ const chain: Array<{ obj: JObject; member?: JMember }> = []
177
+ let obj: JObject | undefined = root
178
+ for (const p of path) {
179
+ if (!obj) break
180
+ const member: JMember | undefined = memberOf(obj, p)
181
+ chain.push({ obj, ...(member ? { member } : {}) })
182
+ obj = member?.value.t === "object" ? member.value : undefined
183
+ }
184
+ return { chain }
185
+ }
186
+
187
+ function editor(text: string) {
188
+ const nl = text.includes("\r\n") ? "\r\n" : "\n"
189
+ const lineStart = (pos: number) => text.lastIndexOf("\n", pos - 1) + 1
190
+ const indentAt = (pos: number) => /^[ \t]*/.exec(text.slice(lineStart(pos)))![0]
191
+ /** Only spaces or tabs (or the BOM) between the start of its line and `pos`. */
192
+ const ownLine = (pos: number) => /^[ \t\uFEFF]*$/.test(text.slice(lineStart(pos), pos))
193
+ /** Where the line that `pos` is on ends (the \r of \r\n, or the \n), skipping comments; -1 when `limit` comes first. */
194
+ const lineEnd = (pos: number, limit: number) => {
195
+ for (let j = pos; j < limit; j++) {
196
+ if (text[j] === "\n") return text[j - 1] === "\r" ? j - 1 : j
197
+ if (text[j] === "/" && text[j + 1] === "/") {
198
+ const n = text.indexOf("\n", j)
199
+ return n < 0 ? -1 : text[n - 1] === "\r" ? n - 1 : n
200
+ }
201
+ if (text[j] === "/" && text[j + 1] === "*") {
202
+ const close = text.indexOf("*/", j + 2)
203
+ if (close < 0) return -1
204
+ j = close + 1
205
+ }
206
+ }
207
+ return -1
208
+ }
209
+ const lineBreakLen = (at: number) => (text[at] === "\r" ? 2 : 1)
210
+ const blank = (s: string) => /^[ \t\r]*$/.test(s)
211
+ return { nl, lineStart, indentAt, ownLine, lineEnd, lineBreakLen, blank }
212
+ }
213
+
214
+ /** One indentation step as the file uses it (the root's first member on a line of its own), else two spaces. */
215
+ function indentUnit(text: string, root: JObject): string {
216
+ const first = root.members[0]
217
+ if (!first) return " "
218
+ const e = editor(text)
219
+ return e.ownLine(first.keyStart) ? e.indentAt(first.keyStart) || " " : " "
220
+ }
221
+
222
+ const splice = (text: string, from: number, to: number, put: string) => text.slice(0, from) + put + text.slice(to)
223
+
224
+ /** `value` as JSON, indented to sit at `indent`. */
225
+ const render = (value: unknown, indent: string, unit: string, nl: string) => JSON.stringify(value, null, unit).split("\n").join(nl + indent)
226
+
227
+ /** Adds `key: value` as the last member of `obj`. */
228
+ function insertMember(text: string, obj: JObject, key: string, value: unknown, unit: string): string {
229
+ const e = editor(text)
230
+ const first = obj.members[0]
231
+ const indent = first && e.ownLine(first.keyStart) ? e.indentAt(first.keyStart) : e.indentAt(obj.start) + unit
232
+ const entry = `${JSON.stringify(key)}: ${render(value, indent, unit, e.nl)}`
233
+ const last = obj.members[obj.members.length - 1]
234
+ if (!last) {
235
+ const inner = text.slice(obj.start + 1, obj.end - 1)
236
+ if (!inner.trim()) return splice(text, obj.start + 1, obj.end - 1, `${e.nl}${indent}${entry}${e.nl}${e.indentAt(obj.start)}`)
237
+ return splice(text, obj.start + 1, obj.start + 1, `${e.nl}${indent}${entry}`)
238
+ }
239
+ const at = e.lineEnd(last.value.end, obj.end - 1)
240
+ // The closing brace on the same line as the last value: an inline object, so the member goes inline too.
241
+ if (at < 0) return splice(text, last.value.end, last.value.end, `, ${JSON.stringify(key)}: ${JSON.stringify(value)}`)
242
+ const withEntry = splice(text, at, at, `${e.nl}${indent}${entry}`)
243
+ return splice(withEntry, last.value.end, last.value.end, ",")
244
+ }
245
+
246
+ /** Removes `m` from `obj`, taking away what insertMember put there (and, for a member it did not add, as little as can be). */
247
+ function removeMember(text: string, obj: JObject, m: JMember): string {
248
+ const e = editor(text)
249
+ const ms = obj.members
250
+ const idx = ms.indexOf(m)
251
+ if (ms.length === 1) {
252
+ const before = text.slice(obj.start + 1, m.keyStart)
253
+ const after = text.slice(m.value.end, obj.end - 1)
254
+ if (!before.trim() && !after.trim()) return splice(text, obj.start + 1, obj.end - 1, "")
255
+ if (!before.trim()) return splice(text, obj.start + 1, m.value.end, "")
256
+ return splice(text, m.keyStart, m.value.end, "")
257
+ }
258
+ if (idx < ms.length - 1) {
259
+ const next = ms[idx + 1]!
260
+ const end = e.lineEnd(m.comma + 1, next.keyStart)
261
+ // On lines of its own, nothing after its comma: those lines go, with one line break.
262
+ if (e.ownLine(m.keyStart) && end >= 0 && e.blank(text.slice(m.comma + 1, end))) return splice(text, e.lineStart(m.keyStart), end + e.lineBreakLen(end), "")
263
+ // The next member on the same line: up to it.
264
+ if (end < 0) return splice(text, m.keyStart, next.keyStart, "")
265
+ return splice(text, m.keyStart, m.comma + 1, "")
266
+ }
267
+ // The last member: it goes from the line break before it (or the spaces before it), and the comma before it goes too.
268
+ const prev = ms[idx - 1]!
269
+ let from = m.keyStart
270
+ if (e.ownLine(m.keyStart) && e.lineStart(m.keyStart) > prev.comma + 1) {
271
+ from = e.lineStart(m.keyStart) - 1
272
+ if (text[from - 1] === "\r") from--
273
+ } else while (from > prev.comma + 1 && /[ \t]/.test(text[from - 1]!)) from--
274
+ return splice(splice(text, from, m.value.end, ""), prev.comma, prev.comma + 1, "")
275
+ }
276
+
277
+ /** `text` with the value at `key` (a "/" path) set to `value`, or removed when `value` is undefined. */
278
+ function setPath(text: string, key: string, value: unknown): string {
279
+ const path = key.split("/")
280
+ // An empty (or missing) file starts as an empty object.
281
+ const src = text.trim() ? text : "{}\n"
282
+ const { root } = parseJsonc(src)
283
+ if (!root || root.t !== "object") throw new JsoncError("not a JSON object")
284
+ const unit = indentUnit(src, root)
285
+ const { chain } = walk(root, path)
286
+ if (value === undefined) {
287
+ const last = chain[path.length - 1]
288
+ if (chain.length < path.length || !last?.member) return text
289
+ let out = removeMember(src, last.obj, last.member)
290
+ // Objects this removal left empty go too (never the root).
291
+ for (let depth = path.length - 1; depth >= 1; depth--) {
292
+ const r = parseJsonc(out).root as JObject
293
+ const c = walk(r, path.slice(0, depth)).chain[depth - 1]
294
+ const holder = c?.member
295
+ if (!holder || holder.value.t !== "object" || holder.value.members.length) break
296
+ out = removeMember(out, c.obj, holder)
297
+ }
298
+ return out
299
+ }
300
+ // Down the path: the first part that is missing (or not an object) gets the rest of the path as a new value.
301
+ for (let d = 0; d < path.length; d++) {
302
+ const step = chain[d]!
303
+ const rest = path.slice(d + 1).reduceRight<unknown>((v, p) => ({ [p]: v }), value)
304
+ if (!step.member) return insertMember(src, step.obj, path[d]!, rest, unit)
305
+ if (d === path.length - 1 || step.member.value.t !== "object") {
306
+ const e = editor(src)
307
+ return splice(src, step.member.value.start, step.member.value.end, render(rest, e.indentAt(step.member.keyStart), unit, e.nl))
308
+ }
309
+ }
310
+ return src
311
+ }
312
+
313
+ /**
314
+ * A codec for a JSON settings file that may have comments (Qwen Code, iFlow CLI and the other Gemini CLI forks read theirs
315
+ * through strip-json-comments). `objects`: paths that must be objects where the file has them (an older layout where one
316
+ * is a string is refused, rather than replaced).
317
+ */
318
+ export function jsonc(objects: string[] = []): Codec {
319
+ return {
320
+ check(text) {
321
+ if (!text.trim()) return undefined
322
+ let parsed: ReturnType<typeof parseJsonc>
323
+ try {
324
+ parsed = parseJsonc(text)
325
+ } catch {
326
+ return "it is not valid JSON (comments are fine, trailing commas are not); left as it is"
327
+ }
328
+ if (parsed.root?.t !== "object") return "it is not a JSON object; left as it is"
329
+ if (parsed.duplicate) return "it has the same key twice in one object; left as it is"
330
+ const v = valueOf(parsed.root) as Json
331
+ for (const path of objects) {
332
+ let cur: unknown = v
333
+ for (const p of path.split("/")) {
334
+ if (!cur || typeof cur !== "object" || Array.isArray(cur)) break
335
+ cur = (cur as Json)[p]
336
+ if (cur !== undefined && (typeof cur !== "object" || cur === null || Array.isArray(cur)))
337
+ return `its "${path.replace(/\//g, ".")}" setting is not an object (an older settings layout?); start the tool once so it updates the file, then connect`
338
+ }
339
+ }
340
+ return undefined
341
+ },
342
+ get(text, key) {
343
+ if (!text.trim()) return undefined
344
+ const { root } = parseJsonc(text)
345
+ if (root?.t !== "object") return undefined
346
+ let cur: unknown = valueOf(root)
347
+ for (const p of key.split("/")) {
348
+ if (!cur || typeof cur !== "object" || Array.isArray(cur) || !(p in (cur as Json))) return undefined
349
+ cur = (cur as Json)[p]
350
+ }
351
+ return cur
352
+ },
353
+ set: setPath,
354
+ }
355
+ }
356
+
357
+ // ---- Qwen Code --------------------------------------------------------------------------------------------------------
358
+
359
+ /** The variable the provider entry reads its key from, set in <dir>/.env. */
360
+ export const QWEN_KEY_VAR = "BRAIN_GATEWAY_KEY"
361
+ const PROVIDER = "brain-gateway"
362
+
363
+ /** $QWEN_HOME (as Qwen Code resolves it: ~ expanded, used when it exists here) or ~/.qwen. */
364
+ export function qwenDir(home: string, env: Env): string {
365
+ const raw = env.QWEN_HOME?.trim()
366
+ if (raw) {
367
+ const p = raw === "~" ? home : raw.startsWith("~/") ? join(home, raw.slice(2)) : raw
368
+ if (p.startsWith("/") && existsSync(p)) return p
369
+ }
370
+ return join(home, ".qwen")
371
+ }
372
+
373
+ const base = (s: GatewaySettings) => `${routeUrl(s)}/v1`
374
+
375
+ export const connectors: Connector[] = [
376
+ {
377
+ id: "qwen-code",
378
+ label: "Qwen Code",
379
+ file: (home, env) => join(qwenDir(home, env), "settings.json"),
380
+ probe: [".qwen"],
381
+ authHeader: "authorization",
382
+ edits: (home, s, env) => [
383
+ {
384
+ path: join(qwenDir(home, env), "settings.json"),
385
+ format: "custom",
386
+ codec: jsonc(["modelProviders", "providerProtocol", "security", "security/auth", "model"]),
387
+ values: {
388
+ [`modelProviders/${PROVIDER}`]: [
389
+ {
390
+ id: s.model,
391
+ name: modelName(s.model),
392
+ description: "100xprompt gateway",
393
+ baseUrl: base(s),
394
+ envKey: QWEN_KEY_VAR,
395
+ ...(s.contextEdit ? { generationConfig: { customHeaders: { [EDIT_HEADER]: "on" } } } : {}),
396
+ },
397
+ ],
398
+ [`providerProtocol/${PROVIDER}`]: "openai",
399
+ "security/auth/selectedType": "openai",
400
+ "model/name": s.model,
401
+ "model/baseUrl": base(s),
402
+ },
403
+ },
404
+ { path: join(qwenDir(home, env), ".env"), format: "dotenv", holdsKey: true, secretKeys: [QWEN_KEY_VAR], values: { [QWEN_KEY_VAR]: readKey(home) } },
405
+ ],
406
+ notes: [
407
+ `Qwen Code reads the key from ~/.qwen/.env (${QWEN_KEY_VAR}; it has no key-file option), so that file is now readable by you only (0600). settings.json gets a "100xprompt gateway" provider, made the default, with its sign-in set to OpenAI-compatible; your own providers stay listed in /model.`,
408
+ "Needs Qwen Code 0.23 or later. A project's own .qwen/settings.json that sets modelProviders or model takes precedence in that project.",
409
+ ],
410
+ },
411
+ ]
@@ -0,0 +1,43 @@
1
+ // Gateway connector for Roo Code (the VS Code extension rooveterinaryinc.roo-cline): NOT BUILT. Roo can be pointed at the
2
+ // gateway only through its own storage, which a disconnect cannot undo.
3
+ //
4
+ // Checked against Roo Code 3.38.1 (~/.vscode/extensions/rooveterinaryinc.roo-cline-3.38.1: package.json, package.nls.json,
5
+ // dist/extension.js) and 3.54.0 (the official bundle in ~/.cache/brain-wire/roo/roo-cli-darwin-arm64/extension, run by the
6
+ // wire harness), and shown with the real 3.54.0 bundle by scripts/gateway-real/roo.ts.
7
+ //
8
+ // Where Roo keeps a provider: as a "provider profile" (apiProvider "openai" takes an OpenAI-compatible openAiBaseUrl,
9
+ // openAiApiKey, openAiModelId and openAiHeaders, so the gateway and the x-100x-context-edit header would both work). The
10
+ // profile list, keys included, is one entry of VS Code's SecretStorage (roo_cline_config_api_config: the OS keychain in
11
+ // VS Code), and the active profile's settings are in VS Code's extension state (globalState) with its key in SecretStorage
12
+ // again (openAiApiKey). No file brain can write, and nothing reads a key from a file or a command.
13
+ //
14
+ // The one file-based way in: the VS Code setting roo-cline.autoImportSettingsPath (3.38.1 package.json:374; package.nls.json:39
15
+ // "Path to a RooCode configuration file to automatically import on extension startup"). On every activation Roo reads that
16
+ // file ({"providerProfiles": {"currentApiConfigName", "apiConfigs"}, "globalSettings"?}) and imports it (extension.js:
17
+ // the auto-import at activation, then importSettingsFromPath): the imported profiles are merged into the profile list,
18
+ // the imported current profile becomes Roo's current one, its settings become the active provider, and every key is
19
+ // stored in SecretStorage. So a connector could write such a file (0600, it holds the key) and the setting in VS Code's
20
+ // settings.json, and Roo would talk to the gateway from its next start.
21
+ //
22
+ // Why that is not safe: removing the setting and the file (all a disconnect can do) undoes nothing inside Roo. The import
23
+ // only ever adds; Roo keeps the "100xprompt gateway" profile, keeps it selected, and keeps the gateway key in the keychain.
24
+ // scripts/gateway-real/roo.ts shows it with the real 3.54.0 bundle: after the import the profiles are ["default",
25
+ // "100xprompt gateway"], current "100xprompt gateway", active provider openai at the gateway URL, the key in SecretStorage;
26
+ // after the setting and the file are removed and Roo starts again, all of that is still there. So:
27
+ // - after `brain gateway disconnect --tool roo` Roo would go on sending every request to the gateway, billed to the
28
+ // 100xprompt account, while brain reports it disconnected;
29
+ // - a key copy in the keychain cannot be refreshed by a new sign-in or removed by brain (logout's "disconnect removes the
30
+ // copy" would be false);
31
+ // - the person's own profile is not restored: they would have to open Roo's settings, pick their profile again and
32
+ // delete "100xprompt gateway" by hand, and the import cannot do that for them (it cannot delete a profile, and it
33
+ // can only select one it brings along, with its key).
34
+ // Writing the profile into SecretStorage or state.vscdb directly would be worse (the keychain, and VS Code's own database
35
+ // while it may be running). By hand, a person can add an "OpenAI Compatible" profile in Roo's settings with the gateway's
36
+ // <route>/v1 address and the key, and remove it there again.
37
+ //
38
+ // What would make a connector possible: a Roo setting or file that is read, not imported (a provider from a file each
39
+ // time, as Kilo Code 7 and OpenCode do), or an import that can remove what it brought. Kilo Code 7 is no longer built on
40
+ // Roo and is connected through the Kilo CLI's config (kilo-code.ts).
41
+ import type { Connector } from "../common.ts"
42
+
43
+ export const connectors: Connector[] = []
@@ -0,0 +1,6 @@
1
+ // Gateway connector for Tabnine CLI: none. It uses only models served by the Tabnine host (custom endpoints are an admin's
2
+ // self-managed models on that host, not a client setting), and the CLI only gets critical fixes until 2026-12-31
3
+ // (https://docs.tabnine.com/main/getting-started/tabnine-cli/features/model-selection).
4
+ import type { Connector } from "../common.ts"
5
+
6
+ export const connectors: Connector[] = []
@@ -0,0 +1,6 @@
1
+ // Gateway connector for Void: none. Void can use any OpenAI-compatible endpoint, but its provider settings are kept only
2
+ // encrypted (OS keychain) in the app's state database (voidSettingsService.ts:351-365, key 'void.settingsServiceStorageII');
3
+ // the project is archived and deprecated (https://github.com/voideditor/void).
4
+ import type { Connector } from "../common.ts"
5
+
6
+ export const connectors: Connector[] = []