@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,286 @@
1
+ // Gateway connector for GitHub Copilot Chat in VS Code (checked 2026-11-10 against VS Code 1.144.0 and the Copilot Chat
2
+ // extension 0.72.0 built into it: /Applications/Visual Studio Code.app/Contents/Resources/app/extensions/copilot).
3
+ //
4
+ // Bring-your-own-key models live in the VS Code user folder, in chatLanguageModels.json (out/vs/workbench/
5
+ // workbench.desktop.main.js: profile `languageModelsResource` = <User>/chatLanguageModels.json, a JSON array of provider
6
+ // groups `{ name, vendor, ...settings }` that VS Code watches and parses as JSON with comments). The Copilot extension's
7
+ // "Custom Endpoint" provider (vendor `customendpoint`, package.json contributes.languageModelChatProviders; in Stable since
8
+ // VS Code 1.122, https://code.visualstudio.com/updates/v1_122) takes per model an `id`, `name`, `url`, `apiType`
9
+ // (chat-completions | responses | messages) and `requestHeaders`.
10
+ //
11
+ // The key. A group's `apiKey` is a secret: VS Code keeps only `${input:chat.lm.secret.<n>}` in the file and the value in
12
+ // its secret storage (workbench: _resolveConfiguration / _resolveLanguageModelProviderGroup), which brain cannot write.
13
+ // But the Custom Endpoint provider lets a model's `requestHeaders` carry the authorization itself: dist/extension.js class
14
+ // for `CustomEndpoint` endpoints overrides _isReservedHeader so `authorization` and `api-key` are allowed
15
+ // (_overridableReservedAuthHeaders), and skips its own `Authorization: Bearer <apiKey>` when the model sets one
16
+ // (_userAuthHeaderSuppressionSet). So brain adds one group, "100xprompt gateway", with no apiKey and a model whose
17
+ // requestHeaders hold `Authorization: Bearer <key>` (and `x-100x-context-edit: on`): the key is copied into the file, which
18
+ // is made private (0600). chatLanguageModels.json is not part of Settings Sync (sync resources: settings, keybindings,
19
+ // snippets, prompts, tasks, extensions, globalState, profiles, mcp), so the copy stays on this machine.
20
+ //
21
+ // What brain does not set: the model picked in Copilot Chat. VS Code remembers that in its own storage; the
22
+ // `chat.defaultModel` setting matches models by their id alone (workbench: function resolving it compares metadata.id),
23
+ // and the gateway's model id (gemini-3.8-flash) could equally name a model Copilot serves itself, so the person picks
24
+ // "100xprompt Pro" in the model picker once (the notes say so). The model list is given in the file, so the gateway's
25
+ // /v1/models (403) is never asked (CustomEndpoint.getAllModels reads `models` unless the group has a `url`).
26
+ //
27
+ // Only the default VS Code profile and VS Code Stable ("Code") are handled; Insiders, VSCodium and other profiles keep
28
+ // their own User folders.
29
+ import { join, resolve } from "node:path"
30
+ import { type Codec, type Connector, EDIT_HEADER, type Env, type Json, modelName, readKey, routeUrl } from "../common.ts"
31
+
32
+ const GROUP = "100xprompt gateway"
33
+ const VENDOR = "customendpoint"
34
+ /** Gemini 3.x Flash, which the gateway's models are: a 1M-token window, 64K out. */
35
+ const MAX_OUTPUT = 65_536
36
+ const MAX_INPUT = 1_048_576 - MAX_OUTPUT
37
+
38
+ /** VS Code's user folder: ~/Library/Application Support/Code/User on macOS, $XDG_CONFIG_HOME/Code/User or ~/.config/Code/User elsewhere. */
39
+ export function vscodeUserDir(home: string, env: Env, platform: string = process.platform): string {
40
+ if (platform === "darwin") return join(home, "Library", "Application Support", "Code", "User")
41
+ if (platform === "win32") return join(env.APPDATA ?? join(home, "AppData", "Roaming"), "Code", "User")
42
+ // XDG_CONFIG_HOME describes the real home: followed only when brain works in that home.
43
+ const xdg = env.XDG_CONFIG_HOME?.startsWith("/") && env.HOME && resolve(home) === resolve(env.HOME) ? env.XDG_CONFIG_HOME : join(home, ".config")
44
+ return join(xdg, "Code", "User")
45
+ }
46
+
47
+ // ---- JSON with comments, with the position of every value -------------------------------------------------------------
48
+
49
+ type Node =
50
+ | { kind: "object"; start: number; end: number; members: Array<{ key: string; value: Node }> }
51
+ | { kind: "array"; start: number; end: number; items: Node[] }
52
+ | { kind: "scalar"; start: number; end: number; value: unknown }
53
+
54
+ /** Parses JSON with // and /* comments and trailing commas (what VS Code accepts in its JSON files); throws otherwise. */
55
+ export function parseJsonc(text: string): Node | undefined {
56
+ let i = 0
57
+ const fail = (what: string): never => {
58
+ throw new Error(`${what} at offset ${i}`)
59
+ }
60
+ const skip = () => {
61
+ for (;;) {
62
+ while (i < text.length && /\s/.test(text[i]!)) i++
63
+ if (text.startsWith("//", i)) {
64
+ const nl = text.indexOf("\n", i)
65
+ i = nl < 0 ? text.length : nl + 1
66
+ } else if (text.startsWith("/*", i)) {
67
+ const end = text.indexOf("*/", i + 2)
68
+ if (end < 0) fail("unclosed comment")
69
+ i = end + 2
70
+ } else return
71
+ }
72
+ }
73
+ const string = (): string => {
74
+ const start = i
75
+ i++
76
+ while (i < text.length && text[i] !== '"') i += text[i] === "\\" ? 2 : 1
77
+ if (i >= text.length) fail("unclosed string")
78
+ i++
79
+ return JSON.parse(text.slice(start, i)) as string
80
+ }
81
+ const value = (): Node => {
82
+ skip()
83
+ const start = i
84
+ const c = text[i]
85
+ if (c === "{") {
86
+ i++
87
+ const members: Array<{ key: string; value: Node }> = []
88
+ for (;;) {
89
+ skip()
90
+ if (text[i] === "}") break
91
+ if (text[i] !== '"') fail("expected a key")
92
+ const key = string()
93
+ skip()
94
+ if (text[i] !== ":") fail("expected :")
95
+ i++
96
+ members.push({ key, value: value() })
97
+ skip()
98
+ if (text[i] === ",") i++
99
+ else if (text[i] !== "}") fail("expected , or }")
100
+ }
101
+ i++
102
+ return { kind: "object", start, end: i, members }
103
+ }
104
+ if (c === "[") {
105
+ i++
106
+ const items: Node[] = []
107
+ for (;;) {
108
+ skip()
109
+ if (text[i] === "]") break
110
+ items.push(value())
111
+ skip()
112
+ if (text[i] === ",") i++
113
+ else if (text[i] !== "]") fail("expected , or ]")
114
+ }
115
+ i++
116
+ return { kind: "array", start, end: i, items }
117
+ }
118
+ if (c === '"') {
119
+ const v = string()
120
+ return { kind: "scalar", start, end: i, value: v }
121
+ }
122
+ const m = /^(?:true|false|null|-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?)/.exec(text.slice(i, i + 400))
123
+ if (!m) fail("unexpected character")
124
+ i += m![0].length
125
+ return { kind: "scalar", start, end: i, value: JSON.parse(m![0]) }
126
+ }
127
+ skip()
128
+ if (i >= text.length) return undefined
129
+ const root = value()
130
+ skip()
131
+ if (i < text.length) fail("text after the end")
132
+ return root
133
+ }
134
+
135
+ export function plain(n: Node): unknown {
136
+ if (n.kind === "scalar") return n.value
137
+ if (n.kind === "array") return n.items.map(plain)
138
+ const o: Json = {}
139
+ for (const m of n.members) o[m.key] = plain(m.value)
140
+ return o
141
+ }
142
+
143
+ /** The first `,` between from and to that is not inside a comment, or -1. */
144
+ function commaBetween(text: string, from: number, to: number): number {
145
+ for (let i = from; i < to; i++) {
146
+ if (text.startsWith("//", i)) {
147
+ const nl = text.indexOf("\n", i)
148
+ i = nl < 0 ? to : nl
149
+ } else if (text.startsWith("/*", i)) i = text.indexOf("*/", i + 2) + 1
150
+ else if (text[i] === ",") return i
151
+ }
152
+ return -1
153
+ }
154
+
155
+ /** The whitespace a value at `at` is indented by, when it starts its line; undefined when something precedes it there. */
156
+ function lineIndent(text: string, at: number): string | undefined {
157
+ const lineStart = text.lastIndexOf("\n", at - 1) + 1
158
+ const before = text.slice(lineStart, at)
159
+ return /^[ \t]*$/.test(before) ? before : undefined
160
+ }
161
+
162
+ const indentUnit = (text: string) => text.match(/\n([ \t]+)[^\s]/)?.[1]?.replace(/^(\t| {2,4}).*$/, "$1") ?? "\t"
163
+ const render = (v: unknown, indent: string, unit: string) => JSON.stringify(v, null, unit).replace(/\n/g, `\n${indent}`)
164
+
165
+ /**
166
+ * chatLanguageModels.json: a JSON-with-comments array of model groups. A key names one Custom Endpoint group (its `name`);
167
+ * set adds, replaces or removes that one array element and leaves every other byte as it was, so removing a group this
168
+ * codec added gives the file back exactly. A file that does not parse as such an array is refused.
169
+ */
170
+ export const languageModelGroups: Codec = {
171
+ check(text) {
172
+ try {
173
+ const root = parseJsonc(text)
174
+ if (root && root.kind !== "array") return "it is not a list of model groups; left as it is"
175
+ } catch {
176
+ return "it is not valid JSON (with comments); left as it is"
177
+ }
178
+ return undefined
179
+ },
180
+ get(text, key) {
181
+ const root = parseJsonc(text)
182
+ if (root?.kind !== "array") return undefined
183
+ const el = findGroup(root, key)
184
+ return el ? plain(el) : undefined
185
+ },
186
+ set(text, key, value) {
187
+ const root = parseJsonc(text)
188
+ const unit = indentUnit(text)
189
+ if (!root) return value === undefined ? text : `[\n${unit}${render(value, unit, unit)}\n]\n`
190
+ if (root.kind !== "array") throw new Error("not a list of model groups")
191
+ const el = findGroup(root, key)
192
+ if (el && value !== undefined) return text.slice(0, el.start) + render(value, lineIndent(text, el.start) ?? unit, unit) + text.slice(el.end)
193
+ if (!el && value === undefined) return text
194
+ if (!el) {
195
+ const last = root.items[root.items.length - 1]
196
+ if (!last) {
197
+ // An empty list: `[]` (VS Code's own empty file) becomes one element on its own lines, closing at the list's indent.
198
+ const close = lineIndent(text, root.end - 1) ?? ""
199
+ const inner = text.slice(root.start + 1, root.end - 1)
200
+ const body = inner.trim() ? `${inner.replace(/\s*$/, "")}\n${close}${unit}` : `\n${close}${unit}`
201
+ return `${text.slice(0, root.start + 1)}${body}${render(value, close + unit, unit)}\n${close}${text.slice(root.end - 1)}`
202
+ }
203
+ const indent = lineIndent(text, last.start) ?? unit
204
+ return `${text.slice(0, last.end)},\n${indent}${render(value, indent, unit)}${text.slice(last.end)}`
205
+ }
206
+ // Remove the element with the comma that joins it to its neighbour.
207
+ const at = root.items.indexOf(el)
208
+ const prev = root.items[at - 1]
209
+ const next = root.items[at + 1]
210
+ let out: string
211
+ if (prev) {
212
+ const gap = text.slice(prev.end, el.start)
213
+ if (/^\s*,\s*$/.test(gap)) out = text.slice(0, prev.end) + text.slice(el.end)
214
+ else {
215
+ // Comments between the two: keep them, drop the comma before this element and the element's own line.
216
+ const comma = commaBetween(text, prev.end, el.start)
217
+ const from = lineIndent(text, el.start) !== undefined ? text.lastIndexOf("\n", el.start - 1) : el.start
218
+ out = text.slice(0, comma) + text.slice(comma + 1, from) + text.slice(el.end)
219
+ }
220
+ } else if (next) {
221
+ const comma = commaBetween(text, el.end, next.start)
222
+ out = text.slice(0, el.start) + (/^\s*,\s*$/.test(text.slice(el.end, next.start)) ? text.slice(next.start) : text.slice(comma + 1).replace(/^[ \t]*\n?[ \t]*/, ""))
223
+ } else {
224
+ const inner = text.slice(root.start + 1, el.start) + text.slice(el.end, root.end - 1)
225
+ out = text.slice(0, root.start + 1) + (inner.trim() ? inner : "") + text.slice(root.end - 1)
226
+ }
227
+ // A file this codec created (one group, "[...]\n") ends empty, so disconnect removes it; VS Code's own "[]" stays.
228
+ return out === "[]\n" ? "" : out
229
+ },
230
+ }
231
+
232
+ function findGroup(root: Extract<Node, { kind: "array" }>, name: string) {
233
+ let found: Node | undefined
234
+ for (const it of root.items) {
235
+ if (it.kind !== "object") continue
236
+ const v = plain(it) as Json
237
+ if (v.name === name && v.vendor === VENDOR) found = it
238
+ }
239
+ return found
240
+ }
241
+
242
+ // ---- the connector ------------------------------------------------------------------------------------------------------
243
+
244
+ export const connectors: Connector[] = [
245
+ {
246
+ id: "vscode-copilot",
247
+ label: "Copilot Chat (VS Code)",
248
+ file: (home, env) => join(vscodeUserDir(home, env), "chatLanguageModels.json"),
249
+ probe: [],
250
+ authHeader: "authorization",
251
+ edits: (home, s, env) => [
252
+ {
253
+ path: join(vscodeUserDir(home, env), "chatLanguageModels.json"),
254
+ format: "custom",
255
+ codec: languageModelGroups,
256
+ holdsKey: true,
257
+ secretKeys: [GROUP],
258
+ values: {
259
+ [GROUP]: {
260
+ name: GROUP,
261
+ vendor: VENDOR,
262
+ apiType: "chat-completions",
263
+ models: [
264
+ {
265
+ id: s.model,
266
+ name: modelName(s.model),
267
+ url: `${routeUrl(s)}/v1/chat/completions`,
268
+ apiType: "chat-completions",
269
+ toolCalling: true,
270
+ vision: true,
271
+ maxInputTokens: MAX_INPUT,
272
+ maxOutputTokens: MAX_OUTPUT,
273
+ requestHeaders: { Authorization: `Bearer ${readKey(home)}`, ...(s.contextEdit ? { [EDIT_HEADER]: "on" } : {}) },
274
+ },
275
+ ],
276
+ },
277
+ },
278
+ },
279
+ ],
280
+ notes: [
281
+ 'In Copilot Chat, open the model picker and choose the model listed under "100xprompt gateway" once; VS Code remembers the choice (brain does not change your default model).',
282
+ "VS Code keeps a custom model's key in its secret storage, which brain cannot write, so the gateway key is in the model's request headers in chatLanguageModels.json instead; that file is now readable by you only (0600) and is not synced by Settings Sync.",
283
+ "Needs VS Code 1.122 or later (the Custom Endpoint provider). On Copilot Business or Enterprise, your organization must allow bring-your-own-key models.",
284
+ ],
285
+ },
286
+ ]
@@ -0,0 +1,6 @@
1
+ // Gateway connector for Windsurf (now Devin Desktop): none. Its models are served by Cognition's backend only; the one
2
+ // network setting is an HTTP proxy, and "You cannot point Devin Desktop at your own LLM" (checked 2026-11-10:
3
+ // https://agentgateway.dev/docs/standalone/main/integrations/llm-clients/devin/; docs.windsurf.com now redirects to docs.devin.ai).
4
+ import type { Connector } from "../common.ts"
5
+
6
+ export const connectors: Connector[] = []
@@ -0,0 +1,29 @@
1
+ // Gateway connector for Zed: NOT BUILT. Zed cannot be given the gateway key through a file brain can write and undo.
2
+ //
3
+ // Checked 2026-11-10 (Zed is not installed on this machine) against the Zed docs and the zed-industries/zed main source:
4
+ // - https://zed.dev/docs/ai/use-api-access ("OpenAI-Compatible Endpoints", "Anthropic-Compatible Endpoints"): providers
5
+ // go in settings.json (JSON with comments, ~/.config/zed/settings.json) under `language_models.openai_compatible.<id>`
6
+ // (`api_url`, `available_models: [{ name, display_name, max_tokens, max_output_tokens, capabilities }]`,
7
+ // `custom_headers`) or `language_models.anthropic_compatible.<name>`, and the agent's model under `agent.default_model`.
8
+ // All of that is writable. The key is not: "Enter the API key in the provider settings UI or set the generated
9
+ // environment variable" (<PROVIDER_ID>_API_KEY, upper snake case), keys saved in Zed "are stored in the system
10
+ // keychain, not in settings.json", and `custom_headers` cannot carry it ("Headers Zed manages, such as provider
11
+ // authentication headers, are ignored with a warning").
12
+ // - crates/language_model/src/api_key.rs (ApiKeyState::load_if_needed): the key comes from the environment variable if it
13
+ // is set and non-empty, otherwise from the credentials provider (the system keychain) keyed by the API URL
14
+ // (read_credentials(url)); there is no file or command source.
15
+ // - crates/language_models/src/provider/open_ai_compatible.rs: OpenAiCompatibleSettings { api_url, available_models,
16
+ // custom_headers }; requests fail with NoApiKey when neither source has a key.
17
+ //
18
+ // So the key could only reach Zed through (a) the keychain, which brain must not write (and could neither refresh on a new
19
+ // sign-in nor remove on disconnect), or (b) an environment variable in Zed's own launch environment, which means the
20
+ // person's shell profile, not Zed's config, and would expose the key to every process started from that shell.
21
+ //
22
+ // A semi-manual connector was considered and not built: brain could add the provider and default model to settings.json
23
+ // and ask the person to paste the key into Zed's Agent settings. It is not automatic, `connect` would report Zed as
24
+ // connected while it has no key, `disconnect` would leave the key in the keychain, and a new `brain gateway login` would
25
+ // leave Zed on the old key without anyone noticing. Not worth it until the core can mark a connector as needing a manual
26
+ // key step (and status can say whether that step was done).
27
+ import type { Connector } from "../common.ts"
28
+
29
+ export const connectors: Connector[] = []