@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,232 @@
1
+ // What every gateway connector shares: the settings, the shape of a connector, where the key lives, and the helpers a
2
+ // connector needs to describe its edits. connect.ts does the reading, writing, recording and undoing; a connector only
3
+ // says which files it changes and to what (src/gateway/connectors/<tool>.ts).
4
+ import { existsSync, readFileSync } from "node:fs"
5
+ import { join } from "node:path"
6
+
7
+ export type Json = Record<string, any>
8
+ export type Env = Record<string, string | undefined>
9
+
10
+ export const DEFAULT_URL = "https://proxy.100xprompt.com"
11
+ export const DEFAULT_ROUTE = "flash"
12
+ export const DEFAULT_MODEL = "gemini-3.8-flash"
13
+ export const EDIT_HEADER = "x-100x-context-edit"
14
+
15
+ /**
16
+ * What 100xprompt calls the models brain may connect a tool to, as the 100xprompt CLI shows them: tools are given the
17
+ * gateway's model id (that is what the gateway serves), and everything brain prints uses the product name.
18
+ */
19
+ export const MODEL_NAMES: Record<string, string> = { "gemini-3.8-flash": "100xprompt Pro", "gemini-3.7-flash": "100xprompt Flash", "gemini-3.6-flash": "100xprompt Flash" }
20
+ /** `--model pro` / `--model flash`: the product names, turned into the gateway's model ids. */
21
+ export const MODEL_ALIASES: Record<string, string> = { pro: "gemini-3.8-flash", flash: "gemini-3.7-flash" }
22
+ export const modelName = (id: string) => MODEL_NAMES[id] ?? id
23
+ export const modelId = (given: string) => MODEL_ALIASES[given.trim().toLowerCase()] ?? given.trim()
24
+
25
+ export interface GatewaySettings {
26
+ url: string
27
+ route: string
28
+ /** The gateway's model id (gemini-3.8-flash for 100xprompt Pro). Show it to people with modelName(). */
29
+ model: string
30
+ /** Send `x-100x-context-edit: on` (the gateway clears old tool results when that saves money). */
31
+ contextEdit: boolean
32
+ }
33
+
34
+ export const brainDir = (home: string) => join(home, ".brain")
35
+ export const statePath = (home: string) => join(brainDir(home), "gateway.json")
36
+ /** The gateway key, 0600, one line. A tool that can read a key from a file, a command or an env file should read it here. */
37
+ export const keyPath = (home: string) => join(brainDir(home), "gateway.key")
38
+
39
+ /** The stored key ("" when there is none: a dry run shows what would change without one). Never print it. */
40
+ export function readKey(home: string): string {
41
+ try {
42
+ return readFileSync(keyPath(home), "utf8").trim()
43
+ } catch {
44
+ return ""
45
+ }
46
+ }
47
+
48
+ /**
49
+ * The gateway route's base address, e.g. https://proxy.100xprompt.com/flash. Under it the gateway serves, for every model:
50
+ * /v1/chat/completions OpenAI Chat Completions (Authorization: Bearer <key>)
51
+ * /v1/responses OpenAI Responses (Authorization: Bearer <key>)
52
+ * /v1/messages Anthropic Messages (x-api-key: <key>, or Authorization: Bearer)
53
+ * /v1beta/models/<id>:generateContent and :streamGenerateContent Gemini (x-goog-api-key: <key>)
54
+ * /v1/models answers 403 ("not served"), so a tool that insists on listing models first needs the model named in its config.
55
+ */
56
+ export const routeUrl = (s: GatewaySettings) => `${s.url.replace(/\/+$/, "")}/${s.route}`
57
+
58
+ /** Single-quoted for a POSIX shell. */
59
+ export const shellQuote = (s: string) => `'${s.replace(/'/g, `'\\''`)}'`
60
+
61
+ /** An env-overridable folder that only counts when it exists (CLAUDE_CONFIG_DIR, CODEX_HOME and the like). */
62
+ export const dirFromEnv = (env: Env, name: string, fallback: string) => (env[name] && existsSync(env[name]!) ? env[name]! : fallback)
63
+
64
+ /**
65
+ * A file format the core does not read itself (YAML, JSON with comments, a tool's own format): the connector reads and
66
+ * writes it. Keys are whatever the codec understands (for yamlTopLevel: a top-level key).
67
+ */
68
+ export interface Codec {
69
+ /** undefined when the file can be changed safely; otherwise why not (the file is then left alone and nothing changes). */
70
+ check?(text: string): string | undefined
71
+ /** The value at `key` (undefined when absent), compared with what was written to tell whether it is still brain's. */
72
+ get(text: string, key: string): unknown
73
+ /** The whole text with `key` set to `value`, or removed when `value` is undefined. Everything else stays as it was. */
74
+ set(text: string, key: string, value: unknown): string
75
+ }
76
+
77
+ /**
78
+ * json plain JSON object; keys are paths with "/" (env/ANTHROPIC_BASE_URL). A file with comments is left alone.
79
+ * toml Codex's config.toml handling (top-level keys and the [model_providers.brain-gateway] table only).
80
+ * dotenv KEY=value lines.
81
+ * custom the edit's `codec` reads and writes it.
82
+ */
83
+ export type Format = "json" | "toml" | "dotenv" | "custom"
84
+
85
+ /** One file a connector changes, and the values it sets there. */
86
+ export interface FileEdit {
87
+ path: string
88
+ format: Format
89
+ /** Required with format "custom". */
90
+ codec?: Codec
91
+ values: Record<string, unknown>
92
+ /** The file holds the key: it is made private (0600). */
93
+ holdsKey?: boolean
94
+ /** Keys among `values` whose value contains the key: never shown or recorded, and replaced when the person signs in again. */
95
+ secretKeys?: string[]
96
+ }
97
+
98
+ export interface Connector {
99
+ /** The id people pass to --tool, the same id brain uses for the tool's sessions where it has one. */
100
+ id: string
101
+ label: string
102
+ /** The main file it edits (shown in status), absolute. */
103
+ file(home: string, env: Env): string
104
+ /** Folders or files (relative to home) whose presence shows the tool has run here, besides the main file or its folder. */
105
+ probe: string[]
106
+ /** Every file it changes, with the values to set. Called often (status, dry runs, disconnect): no side effects. */
107
+ edits(home: string, s: GatewaySettings, env: Env): FileEdit[]
108
+ /** How this tool sends the key, for the free key check before connecting. */
109
+ authHeader: "x-api-key" | "authorization" | "x-goog-api-key"
110
+ /** What the person should know (shown on connect). */
111
+ notes: string[]
112
+ }
113
+
114
+ // ---- JSON paths --------------------------------------------------------------------------------------------------------
115
+
116
+ const split = (k: string) => k.split("/")
117
+ export function getAt(o: Json, key: string): unknown {
118
+ let cur: any = o
119
+ for (const p of split(key)) {
120
+ if (!cur || typeof cur !== "object" || !(p in cur)) return undefined
121
+ cur = cur[p]
122
+ }
123
+ return cur
124
+ }
125
+ export function setAt(o: Json, key: string, v: unknown) {
126
+ const ps = split(key)
127
+ let cur: any = o
128
+ for (const p of ps.slice(0, -1)) {
129
+ if (!cur[p] || typeof cur[p] !== "object" || Array.isArray(cur[p])) cur[p] = {}
130
+ cur = cur[p]
131
+ }
132
+ cur[ps[ps.length - 1]!] = v
133
+ }
134
+ export function deleteAt(o: Json, key: string) {
135
+ const ps = split(key)
136
+ const parents: any[] = [o]
137
+ let cur: any = o
138
+ for (const p of ps.slice(0, -1)) {
139
+ if (!cur || typeof cur !== "object") return
140
+ cur = cur[p]
141
+ parents.push(cur)
142
+ }
143
+ if (cur && typeof cur === "object") delete cur[ps[ps.length - 1]!]
144
+ // Drop objects this left empty (env: {} after the last key went), never the root.
145
+ for (let i = ps.length - 1; i >= 1; i--) {
146
+ const parent = parents[i - 1]
147
+ const child = parent?.[ps[i - 1]!]
148
+ if (child && typeof child === "object" && !Array.isArray(child) && !Object.keys(child).length) delete parent[ps[i - 1]!]
149
+ }
150
+ }
151
+
152
+ // ---- YAML, top-level keys only ------------------------------------------------------------------------------------------
153
+
154
+ /**
155
+ * A YAML scalar or flow collection. A plain string is written as it is when YAML reads it back as the same string
156
+ * (`local-model`, `https://host/v1`); anything else is JSON (valid YAML), which keeps "on", "no" and "1.0" strings.
157
+ */
158
+ // Words and numbers YAML 1.1 readers (PyYAML: Aider, Hermes) take for booleans, null or numbers, though Bun's YAML 1.2
159
+ // reads them as strings: they are always quoted.
160
+ const YAML11_SPECIAL = /^(?:y|n|yes|no|on|off|true|false|null|~|[-+]?\.?(?:inf|nan)|[-+]?(?:0x[0-9a-f_]+|0o?[0-7_]+|0b[01_]+|[0-9][0-9_:]*(?:\.[0-9_]*)?(?:e[-+]?[0-9]+)?))$/i
161
+
162
+ function yamlValue(v: unknown): string {
163
+ if (typeof v === "string" && /^[A-Za-z0-9_./@-][A-Za-z0-9_./:@+=-]*$/.test(v) && !YAML11_SPECIAL.test(v)) {
164
+ try {
165
+ if ((Bun.YAML.parse(`k: ${v}`) as Json).k === v) return v
166
+ } catch {}
167
+ }
168
+ return JSON.stringify(v)
169
+ }
170
+
171
+ /** The ` # comment` at the end of a one-line YAML entry (a # after a space, outside quotes), with the spaces before it. */
172
+ function yamlComment(line: string): string {
173
+ let quote: string | undefined
174
+ for (let i = line.indexOf(":") + 1; i > 0 && i < line.length; i++) {
175
+ const c = line[i]!
176
+ if (quote) {
177
+ if (c === "\\" && quote === '"') i++
178
+ else if (c === quote) quote = undefined
179
+ } else if (c === '"' || c === "'") quote = c
180
+ else if (c === "#" && /[ \t]/.test(line[i - 1]!)) {
181
+ let j = i
182
+ while (j > 0 && /[ \t]/.test(line[j - 1]!)) j--
183
+ return line.slice(j).trimEnd()
184
+ }
185
+ }
186
+ return ""
187
+ }
188
+
189
+ /**
190
+ * Edits top-level `key: value` entries of a YAML file and leaves every other line (comments included) exactly as it was.
191
+ * A value is written on one line (strings quoted; objects and lists in flow style, which YAML reads like block style). An
192
+ * existing entry is replaced together with its indented block, if it had one; an entry that is not there is added at the
193
+ * end. Keys are top-level names only (no paths).
194
+ */
195
+ export const yamlTopLevel: Codec = {
196
+ check(text) {
197
+ if (!text.trim()) return undefined
198
+ if (/^---\s*$/m.test(text.replace(/^---\s*\n/, ""))) return "it holds more than one YAML document; left as it is"
199
+ try {
200
+ const v = Bun.YAML.parse(text)
201
+ if (v !== null && (typeof v !== "object" || Array.isArray(v))) return "it is not a YAML mapping; left as it is"
202
+ } catch {
203
+ return "it is not valid YAML; left as it is"
204
+ }
205
+ return undefined
206
+ },
207
+ get(text, key) {
208
+ if (!text.trim()) return undefined
209
+ const v = Bun.YAML.parse(text) as Json | null
210
+ return v && typeof v === "object" ? v[key] : undefined
211
+ },
212
+ set(text, key, value) {
213
+ const lines = text ? text.replace(/\n$/, "").split("\n") : []
214
+ const esc = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
215
+ const head = new RegExp(`^(?:${esc}|"${esc}"|'${esc}')\\s*:(?:\\s|$)`)
216
+ const at = lines.findIndex((l) => head.test(l))
217
+ let end = at + 1
218
+ // The entry's block: the indented (or blank, or comment) lines that follow it, up to the next top-level line.
219
+ if (at >= 0) {
220
+ while (end < lines.length && (/^[ \t]/.test(lines[end]!) || lines[end]!.trim() === "" || /^[ \t]*-\s/.test(lines[end]!))) end++
221
+ // Blank lines and comments at the end of that run belong to whatever comes next, not to this entry.
222
+ while (end > at + 1 && (lines[end - 1]!.trim() === "" || /^\s*#/.test(lines[end - 1]!))) end--
223
+ }
224
+ // A one-line entry keeps its trailing comment, so a later restore gives the line back as it was.
225
+ const line = `${key}: ${yamlValue(value)}${at >= 0 && end === at + 1 ? yamlComment(lines[at]!) : ""}`
226
+ if (value === undefined) {
227
+ if (at >= 0) lines.splice(at, end - at)
228
+ } else if (at >= 0) lines.splice(at, end - at, line)
229
+ else lines.push(line)
230
+ return lines.length ? `${lines.join("\n")}\n` : ""
231
+ },
232
+ }