@100xprompt/brain 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/README.md +66 -3
  2. package/package.json +1 -1
  3. package/src/cli.ts +28 -3
  4. package/src/dashboard/cli.ts +148 -0
  5. package/src/dashboard/conversions.ts +170 -0
  6. package/src/dashboard/report.ts +275 -0
  7. package/src/dashboard/send.ts +213 -0
  8. package/src/dashboard/state.ts +95 -0
  9. package/src/gateway/common.ts +232 -0
  10. package/src/gateway/connect.ts +164 -162
  11. package/src/gateway/connectors/aider.ts +356 -0
  12. package/src/gateway/connectors/amazon-q-cli.ts +6 -0
  13. package/src/gateway/connectors/amazon-q-ide.ts +6 -0
  14. package/src/gateway/connectors/antigravity.ts +6 -0
  15. package/src/gateway/connectors/auggie.ts +6 -0
  16. package/src/gateway/connectors/cline-sdk.ts +85 -0
  17. package/src/gateway/connectors/cline.ts +34 -0
  18. package/src/gateway/connectors/codebuff.ts +6 -0
  19. package/src/gateway/connectors/continue.ts +579 -0
  20. package/src/gateway/connectors/copilot-cli.ts +138 -0
  21. package/src/gateway/connectors/crush.ts +313 -0
  22. package/src/gateway/connectors/cursor-cli.ts +6 -0
  23. package/src/gateway/connectors/cursor.ts +6 -0
  24. package/src/gateway/connectors/devin.ts +6 -0
  25. package/src/gateway/connectors/factory-droid.ts +308 -0
  26. package/src/gateway/connectors/goose.ts +79 -0
  27. package/src/gateway/connectors/grok-cli.ts +82 -0
  28. package/src/gateway/connectors/hermes.ts +230 -0
  29. package/src/gateway/connectors/iflow-cli.ts +50 -0
  30. package/src/gateway/connectors/index.ts +86 -0
  31. package/src/gateway/connectors/kilo-cli.ts +434 -0
  32. package/src/gateway/connectors/kilo-code.ts +32 -0
  33. package/src/gateway/connectors/kimi-code.ts +241 -0
  34. package/src/gateway/connectors/kiro-cli.ts +6 -0
  35. package/src/gateway/connectors/kiro-ide.ts +6 -0
  36. package/src/gateway/connectors/letta-code.ts +170 -0
  37. package/src/gateway/connectors/mastracode.ts +91 -0
  38. package/src/gateway/connectors/mistral-vibe.ts +495 -0
  39. package/src/gateway/connectors/muse.ts +27 -0
  40. package/src/gateway/connectors/omp.ts +279 -0
  41. package/src/gateway/connectors/openhands.ts +296 -0
  42. package/src/gateway/connectors/pearai.ts +52 -0
  43. package/src/gateway/connectors/pi.ts +357 -0
  44. package/src/gateway/connectors/qoder-cli.ts +6 -0
  45. package/src/gateway/connectors/qwen-code.ts +411 -0
  46. package/src/gateway/connectors/roo.ts +43 -0
  47. package/src/gateway/connectors/tabnine-cli.ts +6 -0
  48. package/src/gateway/connectors/void.ts +6 -0
  49. package/src/gateway/connectors/vscode-copilot.ts +286 -0
  50. package/src/gateway/connectors/windsurf.ts +6 -0
  51. package/src/gateway/connectors/zed.ts +29 -0
@@ -0,0 +1,356 @@
1
+ // Gateway connector for Aider (aider-chat; checked against aider 0.86.2 with litellm 1.81.10, the copy in
2
+ // ~/.cache/brain-wire/aider/venv).
3
+ //
4
+ // How Aider is pointed at the gateway: a model entry of its own, made Aider's default, without touching the person's
5
+ // OpenAI settings (openai-api-base / openai-api-key / OPENAI_API_KEY keep serving their own OpenAI models).
6
+ // ~/.aider.model.settings.yml an entry `name: openai/<model>` (marked with a comment as brain's) whose extra_params
7
+ // carry api_base <route>/v1, api_key and extra_headers { x-100x-context-edit: on }. Aider passes extra_params
8
+ // straight to litellm.completion, so the gateway is called as OpenAI Chat Completions (<route>/v1/chat/completions,
9
+ // Authorization: Bearer <key>). The entry also gives Aider the edit settings it uses for Gemini Flash.
10
+ // ~/.aider.model.metadata.json the same model's context window (else Aider assumes an unknown model: a 1k-token chat
11
+ // history budget, a small repo map and a warning on every start). JSON5, read by Aider with json5.loads.
12
+ // ~/.aider.conf.yml `model: openai/<model>` (the default model) and `show-model-warnings: false` (Aider would warn
13
+ // at every start that OPENAI_API_KEY is not set, which the gateway model does not use). Both put back on disconnect.
14
+ // The key is copied (holdsKey, 0600): Aider has no key-file or key-command option. Its conf file (configargparse YAML)
15
+ // and settings file (yaml.safe_load) do no interpolation, and its .env files (python-dotenv) only expand ${VAR}. litellm
16
+ // can read a key file (api_key "os.environ/oidc/file/<path>", litellm/secret_managers/main.py get_secret), but it does
17
+ // not strip the trailing newline the brain's key file ends with, and the request then fails (tried: "Connection error").
18
+ //
19
+ // Evidence (site-packages of the copy above):
20
+ // aider/main.py:464-497 config files: ./.aider.conf.yml, <git root>/.aider.conf.yml, ~/.aider.conf.yml (the nearest wins).
21
+ // aider/main.py:305-358 .aider.model.settings.yml searched in ~, the git root and the cwd (register_models).
22
+ // aider/main.py:390-412 .aider.model.metadata.json likewise (register_litellm_models); aider/models.py:1097-1118 json5.
23
+ // aider/main.py:500-504 .env files (~/.env, git root, cwd) are loaded, then the arguments parsed again (AIDER_* win).
24
+ // aider/models.py:1070-1094 each settings entry replaces any earlier entry of the same name (later files/entries win).
25
+ // aider/models.py:116-138 ModelSettings fields (extra_params among them); unknown fields are an error.
26
+ // aider/models.py:970-1022 send_completion(): kwargs.update(self.extra_params) (996), then litellm.completion(**kwargs) (1021).
27
+ // aider/models.py:697-765, 1149-1185 the start-up check that warns when OPENAI_API_KEY is missing for openai/ models.
28
+ // aider/main.py:890-902 that check runs only with --show-model-warnings (the default).
29
+ // Docs: https://aider.chat/docs/config/adv-model-settings.html (extra_params, extra_headers, metadata file),
30
+ // https://aider.chat/docs/llms/openai-compat.html (openai/<model> against an OpenAI-compatible API).
31
+ import { join } from "node:path"
32
+ import { type Codec, type Connector, EDIT_HEADER, type Json, readKey, routeUrl, yamlTopLevel } from "../common.ts"
33
+
34
+ /** The one key of brain's own entry in the settings list and in the metadata object. */
35
+ export const AIDER_ENTRY = "brain-gateway"
36
+ /** Marks brain's entry in each file (a YAML comment, a JSON5 comment), so it is told apart from the person's own. */
37
+ export const AIDER_MARK = "100xprompt gateway, added by brain gateway connect --tool aider"
38
+ /** What Aider is told about the gateway's model: the Gemini Flash models' window and output limit. */
39
+ const CONTEXT = 1_048_576
40
+ const OUTPUT = 65_536
41
+
42
+ const esc = (s: string) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
43
+
44
+ /** A value in YAML flow style, written as JSON (which PyYAML and Bun both read): strings are always quoted. */
45
+ const flow = (v: unknown): string =>
46
+ Array.isArray(v)
47
+ ? `[${v.map(flow).join(", ")}]`
48
+ : v && typeof v === "object"
49
+ ? `{${Object.entries(v)
50
+ .filter(([, x]) => x !== undefined)
51
+ .map(([k, x]) => `${JSON.stringify(k)}: ${flow(x)}`)
52
+ .join(", ")}}`
53
+ : JSON.stringify(v)
54
+
55
+ // ---- ~/.aider.model.settings.yml: a YAML list; brain's entry is one key ---------------------------------------------
56
+
57
+ /** Where brain's entry is: [start, end) is its text without the last newline; `next` is past that newline. */
58
+ function findEntry(text: string): { start: number; end: number; next: number } | undefined {
59
+ const m = new RegExp(`^- .*# ${esc(AIDER_MARK)}[ \\t]*$`, "m").exec(text)
60
+ if (!m) return undefined
61
+ const start = m.index
62
+ let end = start + m[0].length
63
+ // Its fields: the indented lines that follow (brain writes no blank or comment lines inside it).
64
+ while (text[end] === "\n") {
65
+ const nl = text.indexOf("\n", end + 1)
66
+ const lineEnd = nl < 0 ? text.length : nl
67
+ const line = text.slice(end + 1, lineEnd)
68
+ if (!/^[ \t]+[^\s#]/.test(line)) break
69
+ end = lineEnd
70
+ }
71
+ return { start, end, next: end < text.length ? end + 1 : end }
72
+ }
73
+
74
+ function renderEntry(v: Json): string {
75
+ const { name, ...rest } = v
76
+ const fields = Object.entries(rest)
77
+ .filter(([, x]) => x !== undefined)
78
+ .map(([k, x]) => ` ${k}: ${flow(x)}`)
79
+ return [`- name: ${flow(name)} # ${AIDER_MARK}`, ...fields].join("\n")
80
+ }
81
+
82
+ const SAMPLE_ENTRY = { name: "openai/brain-check", extra_params: { api_base: "https://example.invalid/v1" } }
83
+
84
+ /**
85
+ * Aider's model settings file: a top-level YAML list of entries. Brain's entry (the key AIDER_ENTRY) is found by its
86
+ * marker comment, replaced or removed as a whole, and added at the end (a later entry of the same name wins in Aider).
87
+ * Every other byte stays as it was; adding and then removing the entry gives the file back exactly.
88
+ */
89
+ export const modelSettingsList: Codec = {
90
+ check(text) {
91
+ if (!text.trim()) return undefined
92
+ if (/^---\s*$/m.test(text.replace(/^---\s*\n/, "")) || /^\.\.\.\s*$/m.test(text)) return "it holds more than one YAML document; left as it is"
93
+ let v: unknown
94
+ try {
95
+ v = Bun.YAML.parse(text)
96
+ } catch {
97
+ return "it is not valid YAML; left as it is"
98
+ }
99
+ if (v !== null && !Array.isArray(v)) return "it is not a YAML list of model settings; left as it is"
100
+ // Brain adds `- name: ...` at the left margin, so the list must be written that way (not [...] or indented).
101
+ const first = text.split("\n").find((l) => l.trim() && !/^\s*#/.test(l) && !/^---\s*$/.test(l))
102
+ if (first !== undefined && !/^-(\s|$)/.test(first)) return "its list is not a block list at the left margin; left as it is"
103
+ // Proof that brain's entry can be added: the result reads as the same list plus that entry.
104
+ try {
105
+ const had = Array.isArray(v) ? v.length : 0
106
+ const ours = findEntry(text) !== undefined
107
+ const probe = Bun.YAML.parse(modelSettingsList.set(text, AIDER_ENTRY, SAMPLE_ENTRY)) as unknown
108
+ // Brain's entry already there is replaced in place; otherwise the sample must be a new last entry.
109
+ if (!Array.isArray(probe) || probe.length !== had + (ours ? 0 : 1) || (!ours && (probe.at(-1) as Json | undefined)?.name !== SAMPLE_ENTRY.name))
110
+ return "brain cannot add its entry to it safely; left as it is"
111
+ } catch {
112
+ return "brain cannot add its entry to it safely; left as it is"
113
+ }
114
+ return undefined
115
+ },
116
+ get(text, key) {
117
+ if (key !== AIDER_ENTRY) return undefined
118
+ const at = findEntry(text)
119
+ if (!at) return undefined
120
+ try {
121
+ const v = Bun.YAML.parse(text.slice(at.start, at.end)) as unknown
122
+ return Array.isArray(v) ? v[0] : v
123
+ } catch {
124
+ return text.slice(at.start, at.end)
125
+ }
126
+ },
127
+ set(text, key, value) {
128
+ if (key !== AIDER_ENTRY) return text
129
+ const at = findEntry(text)
130
+ if (value === undefined) {
131
+ if (!at) return text
132
+ // The last line of a file with no final newline: the newline brain put before it goes too.
133
+ if (at.next === at.end && at.start > 0) return text.slice(0, at.start - 1)
134
+ return text.slice(0, at.start) + text.slice(at.next)
135
+ }
136
+ const entry = renderEntry(value as Json)
137
+ if (at) return text.slice(0, at.start) + entry + text.slice(at.end)
138
+ if (!text) return `${entry}\n`
139
+ return text.endsWith("\n") ? `${text}${entry}\n` : `${text}\n${entry}`
140
+ },
141
+ }
142
+
143
+ // ---- ~/.aider.model.metadata.json: a JSON5 object; brain's member is one key -----------------------------------------
144
+
145
+ /** The index just past the JSON string that starts at i (a double quote). */
146
+ function stringEnd(t: string, i: number): number {
147
+ let j = i + 1
148
+ while (j < t.length) {
149
+ if (t[j] === "\\") j += 2
150
+ else if (t[j] === '"') return j + 1
151
+ else j++
152
+ }
153
+ return t.length
154
+ }
155
+
156
+ /** The index of the next character that is not whitespace or inside a comment. */
157
+ function skipBlank(t: string, i: number): number {
158
+ while (i < t.length) {
159
+ if (/\s/.test(t[i]!)) i++
160
+ else if (t.startsWith("//", i)) {
161
+ const n = t.indexOf("\n", i)
162
+ i = n < 0 ? t.length : n + 1
163
+ } else if (t.startsWith("/*", i)) {
164
+ const n = t.indexOf("*/", i + 2)
165
+ if (n < 0) throw new Error("unclosed comment")
166
+ i = n + 2
167
+ } else break
168
+ }
169
+ return i
170
+ }
171
+
172
+ /** The text with comments and trailing commas taken out (outside strings): what JSON.parse can read. */
173
+ function plainJson(t: string): string {
174
+ let out = ""
175
+ let i = 0
176
+ while (i < t.length) {
177
+ const c = t[i]!
178
+ if (c === '"') {
179
+ const j = stringEnd(t, i)
180
+ out += t.slice(i, j)
181
+ i = j
182
+ } else if (t.startsWith("//", i) || t.startsWith("/*", i)) {
183
+ const j = skipBlank(t, i)
184
+ out += " "
185
+ i = j
186
+ } else if (c === ",") {
187
+ const j = skipBlank(t, i + 1)
188
+ if (t[j] !== "}" && t[j] !== "]") out += c
189
+ i++
190
+ } else {
191
+ out += c
192
+ i++
193
+ }
194
+ }
195
+ return out
196
+ }
197
+
198
+ /** The index just past the JSON value that starts at i. */
199
+ function valueEnd(t: string, i: number): number {
200
+ if (t[i] === '"') return stringEnd(t, i)
201
+ if (t[i] === "{" || t[i] === "[") {
202
+ let depth = 0
203
+ let j = i
204
+ while (j < t.length) {
205
+ const c = t[j]!
206
+ if (c === '"') {
207
+ j = stringEnd(t, j)
208
+ continue
209
+ }
210
+ if (t.startsWith("//", j) || t.startsWith("/*", j)) {
211
+ j = skipBlank(t, j)
212
+ continue
213
+ }
214
+ if (c === "{" || c === "[") depth++
215
+ else if (c === "}" || c === "]") {
216
+ depth--
217
+ if (depth === 0) return j + 1
218
+ }
219
+ j++
220
+ }
221
+ return t.length
222
+ }
223
+ let j = i
224
+ while (j < t.length && !/[\s,}\]]/.test(t[j]!) && !t.startsWith("//", j) && !t.startsWith("/*", j)) j++
225
+ return j
226
+ }
227
+
228
+ const TAG = `/* ${AIDER_MARK} */`
229
+
230
+ /** Brain's member: `\n /* mark *\/ "<name>": <value>,` from start to end, as brain writes it right after the opening brace. */
231
+ function findMember(t: string): { start: number; end: number; name: string; value: string } | undefined {
232
+ const at = t.indexOf(TAG)
233
+ if (at < 0) return undefined
234
+ let i = at + TAG.length
235
+ while (t[i] === " ") i++
236
+ if (t[i] !== '"') return undefined
237
+ const nameEnd = stringEnd(t, i)
238
+ let name: string
239
+ try {
240
+ name = JSON.parse(t.slice(i, nameEnd))
241
+ } catch {
242
+ return undefined
243
+ }
244
+ i = nameEnd
245
+ while (t[i] === " ") i++
246
+ if (t[i] !== ":") return undefined
247
+ i++
248
+ while (t[i] === " ") i++
249
+ const vEnd = valueEnd(t, i)
250
+ if (t[vEnd] !== ",") return undefined
251
+ return { start: t.slice(at - 3, at) === "\n " ? at - 3 : at, end: vEnd + 1, name, value: t.slice(i, vEnd) }
252
+ }
253
+
254
+ const memberText = (name: string, info: unknown) => `\n ${TAG} ${JSON.stringify(name)}: ${JSON.stringify(info)},`
255
+
256
+ /**
257
+ * Aider's model metadata file: a JSON5 object (Aider reads it with json5.loads). Brain's member (the key AIDER_ENTRY,
258
+ * value { "<model>": {...} }) is written on one line right after the opening brace, marked with a comment, and taken
259
+ * out again exactly; comments and the person's own members stay as they were. A file that is JSON5 beyond comments and
260
+ * trailing commas (unquoted keys, single quotes) is left alone.
261
+ */
262
+ export const modelMetadataJson: Codec = {
263
+ check(text) {
264
+ if (!text.trim()) return undefined
265
+ try {
266
+ const v = JSON.parse(plainJson(text))
267
+ if (!v || typeof v !== "object" || Array.isArray(v)) return "it is not a JSON object; left as it is"
268
+ const probe = JSON.parse(plainJson(modelMetadataJson.set(text, AIDER_ENTRY, { "openai/brain-check": { mode: "chat" } })))
269
+ if (!probe?.["openai/brain-check"]) return "brain cannot add its entry to it safely; left as it is"
270
+ } catch {
271
+ return "it is not JSON that brain can edit safely (JSON5 beyond comments and trailing commas?); left as it is"
272
+ }
273
+ return undefined
274
+ },
275
+ get(text, key) {
276
+ if (key !== AIDER_ENTRY) return undefined
277
+ const m = findMember(text)
278
+ if (!m) return undefined
279
+ try {
280
+ return { [m.name]: JSON.parse(plainJson(m.value)) }
281
+ } catch {
282
+ return m.value
283
+ }
284
+ },
285
+ set(text, key, value) {
286
+ if (key !== AIDER_ENTRY) return text
287
+ const m = findMember(text)
288
+ if (value === undefined) {
289
+ if (!m) return text
290
+ // Added to an empty object, the member took the newline before the closing brace with it.
291
+ const extra = text[m.start - 1] === "{" && text.startsWith("\n}", m.end) ? 1 : 0
292
+ return text.slice(0, m.start) + text.slice(m.end + extra)
293
+ }
294
+ const [name, info] = Object.entries(value as Json)[0]!
295
+ const seg = memberText(name, info)
296
+ if (m) return text.slice(0, m.start) + seg + text.slice(m.end)
297
+ const base = text.trim() ? text : "{}\n"
298
+ const open = skipBlank(base, 0)
299
+ const rest = base.slice(open + 1)
300
+ if (/^\s*\}/.test(rest)) return `${base.slice(0, open + 1)}${seg}\n${rest.replace(/^\s*/, "")}`
301
+ return base.slice(0, open + 1) + seg + rest
302
+ },
303
+ }
304
+
305
+ // ---- the connector ------------------------------------------------------------------------------------------------------
306
+
307
+ export const connectors: Connector[] = [
308
+ {
309
+ id: "aider",
310
+ label: "Aider",
311
+ file: (home) => join(home, ".aider.conf.yml"),
312
+ // ~/.aider holds Aider's caches and analytics from its first run.
313
+ probe: [".aider"],
314
+ authHeader: "authorization",
315
+ edits: (home, s) => {
316
+ const model = `openai/${s.model}`
317
+ return [
318
+ {
319
+ path: join(home, ".aider.model.settings.yml"),
320
+ format: "custom",
321
+ codec: modelSettingsList,
322
+ holdsKey: true,
323
+ secretKeys: [AIDER_ENTRY],
324
+ values: {
325
+ [AIDER_ENTRY]: {
326
+ name: model,
327
+ // As Aider's own settings for Gemini Flash (aider/resources/model-settings.yml, gemini/gemini-3-flash-preview).
328
+ edit_format: "diff-fenced",
329
+ use_repo_map: true,
330
+ overeager: true,
331
+ use_temperature: false,
332
+ extra_params: { api_base: `${routeUrl(s)}/v1`, api_key: readKey(home), ...(s.contextEdit ? { extra_headers: { [EDIT_HEADER]: "on" } } : {}) },
333
+ },
334
+ },
335
+ },
336
+ {
337
+ path: join(home, ".aider.model.metadata.json"),
338
+ format: "custom",
339
+ codec: modelMetadataJson,
340
+ values: { [AIDER_ENTRY]: { [model]: { max_input_tokens: CONTEXT, max_output_tokens: OUTPUT, max_tokens: OUTPUT, litellm_provider: "openai", mode: "chat" } } },
341
+ },
342
+ {
343
+ path: join(home, ".aider.conf.yml"),
344
+ format: "custom",
345
+ codec: yamlTopLevel,
346
+ values: { model, "show-model-warnings": false },
347
+ },
348
+ ]
349
+ },
350
+ notes: [
351
+ "Aider cannot read its key from a file, so the key is kept in ~/.aider.model.settings.yml (the gateway model's own entry), which is now readable by you only (0600); a new sign-in replaces it there.",
352
+ "The gateway's model is now Aider's default (model: in ~/.aider.conf.yml); --model, AIDER_MODEL (in your shell or a .env file) or a project's own .aider.conf.yml still take precedence. Your OpenAI settings are not touched.",
353
+ "Aider's start-up model warnings are off while connected (it would warn that OPENAI_API_KEY is not set, which the gateway does not use); disconnect puts that setting back.",
354
+ ],
355
+ },
356
+ ]
@@ -0,0 +1,6 @@
1
+ // Gateway connector for Amazon Q Developer CLI: none. Its endpoint settings (api.codewhisperer.service, api.q.service) only
2
+ // move it between AWS Q servers, which speak AWS's own streaming API (generateAssistantResponse), not OpenAI or Anthropic; it is
3
+ // replaced by Kiro CLI (aws/amazon-q-developer-cli crates/chat-cli/src/api_client/endpoints.rs; wire/drivers/b.ts:26).
4
+ import type { Connector } from "../common.ts"
5
+
6
+ export const connectors: Connector[] = []
@@ -0,0 +1,6 @@
1
+ // Gateway connector for Amazon Q Developer (IDE plugin): none. Chat runs in AWS's language server against Amazon Q (AWS
2
+ // sign-in, AWS event-stream API); the model menu offers only Amazon Q's own models and there is no endpoint or key setting
3
+ // (https://docs.aws.amazon.com/amazonq/latest/qdeveloper-ug/q-in-ides-chat-models.html; wire/drivers/e.ts:306).
4
+ import type { Connector } from "../common.ts"
5
+
6
+ export const connectors: Connector[] = []
@@ -0,0 +1,6 @@
1
+ // Gateway connector for Antigravity: none. The IDE's agent runs in Google's `agy` language server against Google's backend
2
+ // with a fixed model list; neither Antigravity IDE 1.107.0 (extensions/agy/package.json settings) nor its docs offer an
3
+ // endpoint or key, and a settings.json "openai-compatible" attempt went unanswered (https://discuss.ai.google.dev/t/168654, 2026-06-01).
4
+ import type { Connector } from "../common.ts"
5
+
6
+ export const connectors: Connector[] = []
@@ -0,0 +1,6 @@
1
+ // Gateway connector for Auggie: none. It speaks only Augment's own chat-stream API (its model registry keeps provider
2
+ // "augment" models only, wire/drivers/b.ts:12), and ~/.augment/settings.json has `defaultModel` but no provider, endpoint or
3
+ // key (https://docs.augmentcode.com/cli/config, checked 2026-11-10).
4
+ import type { Connector } from "../common.ts"
5
+
6
+ export const connectors: Connector[] = []
@@ -0,0 +1,85 @@
1
+ // Gateway connector for Cline's CLI / SDK (`cline`, npm package cline 3.0.74 on @cline/core 0.2.0, checked 2026-11-10 in
2
+ // ~/.cache/brain-wire/cline-sdk). Cline keeps its providers in two plain JSON files under ~/.cline/data/settings/:
3
+ //
4
+ // providers.json { version: 1, lastUsedProvider, modes, providers: { <id>: { settings, updatedAt, tokenSource } } }
5
+ // settings = { provider, apiKey, model, baseUrl, headers, ... } (@cline/core dist/services/llms/
6
+ // provider-settings.d.ts ProviderSettingsSchema; StoredProviderSettings in dist/types/provider-settings.d.ts).
7
+ // Cline itself keeps this file 0600 (ProviderSettingsManager: write() with mode 384, chmod 384 on load),
8
+ // reads it fresh before every change (saveProviderSettings is read-modify-write) and starts a run on
9
+ // `lastUsedProvider` when no -P is given.
10
+ // models.json { version: 1, providers: { <id>: { provider: { name, baseUrl, defaultModelId, protocol, client,
11
+ // capabilities }, models: { <model>: { id, name, contextWindow, maxTokens, capabilities } } } } }
12
+ // (dist/services/providers/local-provider-registry.d.ts StoredModelsFileSchema): the custom providers
13
+ // Cline knows about; `cline auth -p <id>` refuses an id that is in neither the built-ins nor this file.
14
+ //
15
+ // So brain adds its own named provider, "brain-gateway" (shown as "100xprompt gateway", its model as 100xprompt Pro), an
16
+ // OpenAI Chat Completions client on <route>/v1 with the x-100x-context-edit header (settings.headers), and makes it the
17
+ // last-used provider. The person's own providers are not touched. Cline has no key-file or key-command option for a
18
+ // custom provider (built-ins read a fixed env var at most: `apiKeyEnv` in @cline/llms), so the key is copied into
19
+ // providers.json, which stays 0600.
20
+ //
21
+ // Checked for real (scripts/gateway-real/cline-sdk.ts): with these two files, `cline -y "<prompt>"` sends POST
22
+ // /flash/v1/chat/completions with `Authorization: Bearer <key>`, `x-100x-context-edit: on` and model gemini-3.8-flash, and
23
+ // never asks for /v1/models.
24
+ //
25
+ // Not covered here: CLINE_PROVIDER_SETTINGS_PATH (a providers.json somewhere else; models.json then sits next to it) is
26
+ // not followed; CLINE_DIR and CLINE_DATA_DIR are, when they exist. The VS Code extension (id `cline`, see cline.ts) shares
27
+ // ~/.cline/data but chooses its provider from its own globalState.json, so this connector does not change it.
28
+ import { join } from "node:path"
29
+ import { type Connector, dirFromEnv, EDIT_HEADER, type Env, modelName, readKey, routeUrl } from "../common.ts"
30
+
31
+ const ID = "brain-gateway"
32
+ /** Gemini 3.x Flash, which the gateway's models are: a 1M-token window, 64K out. */
33
+ const CONTEXT_WINDOW = 1_048_576
34
+ const MAX_OUTPUT = 65_536
35
+ /** Cline requires an ISO date here; a fixed one keeps edits() free of side effects (Cline sets its own when it saves). */
36
+ const UPDATED_AT = "2026-01-01T00:00:00.000Z"
37
+
38
+ /** ~/.cline/data, or $CLINE_DATA_DIR, or $CLINE_DIR/data (each only when it exists), as @cline/shared resolves it. */
39
+ export const clineDataDir = (home: string, env: Env) => dirFromEnv(env, "CLINE_DATA_DIR", join(dirFromEnv(env, "CLINE_DIR", join(home, ".cline")), "data"))
40
+ const settingsDir = (home: string, env: Env) => join(clineDataDir(home, env), "settings")
41
+
42
+ export const connectors: Connector[] = [
43
+ {
44
+ id: "cline-sdk",
45
+ label: "Cline CLI",
46
+ file: (home, env) => join(settingsDir(home, env), "providers.json"),
47
+ probe: [".cline"],
48
+ authHeader: "authorization",
49
+ edits: (home, s, env) => {
50
+ const baseUrl = `${routeUrl(s)}/v1`
51
+ return [
52
+ {
53
+ path: join(settingsDir(home, env), "models.json"),
54
+ format: "json",
55
+ values: {
56
+ version: 1,
57
+ [`providers/${ID}`]: {
58
+ provider: { name: "100xprompt gateway", baseUrl, defaultModelId: s.model, protocol: "openai-chat", client: "openai-compatible", capabilities: ["tools", "streaming"] },
59
+ models: { [s.model]: { id: s.model, name: modelName(s.model), contextWindow: CONTEXT_WINDOW, maxTokens: MAX_OUTPUT, capabilities: ["tools", "streaming", "images"] } },
60
+ },
61
+ },
62
+ },
63
+ {
64
+ path: join(settingsDir(home, env), "providers.json"),
65
+ format: "json",
66
+ holdsKey: true,
67
+ secretKeys: [`providers/${ID}`],
68
+ values: {
69
+ version: 1,
70
+ lastUsedProvider: ID,
71
+ [`providers/${ID}`]: {
72
+ settings: { provider: ID, apiKey: readKey(home), model: s.model, baseUrl, ...(s.contextEdit ? { headers: { [EDIT_HEADER]: "on" } } : {}) },
73
+ updatedAt: UPDATED_AT,
74
+ tokenSource: "manual",
75
+ },
76
+ },
77
+ },
78
+ ]
79
+ },
80
+ notes: [
81
+ "Cline CLI now starts on the 100xprompt gateway provider (its last-used provider); `cline -P <provider>` or /model still picks another for a run.",
82
+ "Cline keeps provider keys in ~/.cline/data/settings/providers.json (it has no key-file option), so the gateway key is copied there; the file stays readable by you only (0600).",
83
+ ],
84
+ },
85
+ ]
@@ -0,0 +1,34 @@
1
+ // Gateway connector for Cline, the VS Code extension: NOT BUILT. Its settings are files, but not files brain can safely
2
+ // write and cleanly undo. (Cline's CLI / SDK is connected by cline-sdk.ts.)
3
+ //
4
+ // Checked 2026-11-10 against Cline 4.1.26 (saoudrizwan.claude-dev-4.1.26 in ~/.vscode/extensions, dist/extension.js) and
5
+ // the SDK it shares with the CLI (@cline/core 0.2.0 in ~/.cache/brain-wire/cline-sdk):
6
+ // - Where the settings are: since 4.x the extension's global state and secrets are JSON files, ~/.cline/data/
7
+ // globalState.json and secrets.json (0600) ($CLINE_DATA_DIR overrides): p3s() builds them as TDe file stores, and
8
+ // activation (bg0 -> RWu) migrates VS Code's own extension state into them. The provider a task uses comes from that
9
+ // state (SessionFactory: getApiConfiguration() -> actModeApiProvider / planModeApiProvider), with per-provider keys:
10
+ // for an OpenAI-compatible endpoint `openAiBaseUrl`, `actModeOpenAiModelId` / `planModeOpenAiModelId`,
11
+ // `actModeOpenAiModelInfo`, `openAiHeaders` in globalState.json and `openAiApiKey` in secrets.json.
12
+ // - Why brain cannot edit them safely:
13
+ // 1. The extension keeps both files in memory and writes them back whole. TDe reads its file once, in the constructor
14
+ // (`this.data = this.readFromDisk()`), and every change rewrites the file from that copy (setBatch -> writeToDisk);
15
+ // nothing reloads it. While VS Code (or Cursor or Windsurf, which share ~/.cline/data) runs Cline, a connect is
16
+ // overwritten by the next state write, and a disconnect is undone: the gateway settings, and on the next secrets
17
+ // write the key copy, come back from memory after brain recorded them as removed.
18
+ // 2. There is one OpenAI-compatible slot, not a named provider: connect would have to rewrite the person's own
19
+ // openAiBaseUrl / openAiApiKey / model entries and the act and plan providers.
20
+ // 3. A second key copy brain would not know about: the SDK's legacy migration (@cline/core migrateLegacyProviderSettings,
21
+ // run by the ProviderSettingsManager constructor, which both the extension and the CLI create to read
22
+ // providers.json) copies every provider that has a key in
23
+ // globalState.json / secrets.json (`openAiApiKey` -> "openai" -> "openai-compatible") into
24
+ // ~/.cline/data/settings/providers.json, key included, when that provider is not there yet. Disconnect would leave it.
25
+ // - What does reach the extension: when no provider was ever chosen in it (actModeApiProvider unset), the SessionFactory
26
+ // falls back to providers.json's last-used provider ("Using SDK provider fallback"), which `--tool cline-sdk` sets. Not
27
+ // verified with a custom provider id in the extension, so not claimed.
28
+ //
29
+ // What would make it buildable: a connector hook in the core that refuses to connect or disconnect while the app holding
30
+ // the files is running (VS Code, Cursor, Windsurf), and a way to also own (and remove) the providers.json copy the
31
+ // migration makes, e.g. by writing providers.json's "openai-compatible" entry as well so the migration skips it.
32
+ import type { Connector } from "../common.ts"
33
+
34
+ export const connectors: Connector[] = []
@@ -0,0 +1,6 @@
1
+ // Gateway connector for Codebuff (now the Freebuff CLI, npm `freebuff`): not built. Its /byok takes an OpenAI-compatible
2
+ // base URL (kept in ~/.config/freebuff/byok/connections.json), but a CLI connection's key can only be an `env:NAME` reference
3
+ // or an OS-keychain entry, never a file (CodebuffAI/codebuff sdk/src/byok.ts:27,1196-1300 and cli/src/utils/byok.ts:57-63, 2026-10-14).
4
+ import type { Connector } from "../common.ts"
5
+
6
+ export const connectors: Connector[] = []