@100xprompt/brain 0.1.0 → 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.
- package/README.md +66 -3
- package/package.json +1 -1
- package/src/cli.ts +28 -3
- package/src/dashboard/cli.ts +148 -0
- package/src/dashboard/conversions.ts +170 -0
- package/src/dashboard/report.ts +261 -0
- package/src/dashboard/send.ts +213 -0
- package/src/dashboard/state.ts +95 -0
- package/src/gateway/common.ts +232 -0
- package/src/gateway/connect.ts +164 -162
- package/src/gateway/connectors/aider.ts +356 -0
- package/src/gateway/connectors/amazon-q-cli.ts +6 -0
- package/src/gateway/connectors/amazon-q-ide.ts +6 -0
- package/src/gateway/connectors/antigravity.ts +6 -0
- package/src/gateway/connectors/auggie.ts +6 -0
- package/src/gateway/connectors/cline-sdk.ts +85 -0
- package/src/gateway/connectors/cline.ts +34 -0
- package/src/gateway/connectors/codebuff.ts +6 -0
- package/src/gateway/connectors/continue.ts +579 -0
- package/src/gateway/connectors/copilot-cli.ts +138 -0
- package/src/gateway/connectors/crush.ts +313 -0
- package/src/gateway/connectors/cursor-cli.ts +6 -0
- package/src/gateway/connectors/cursor.ts +6 -0
- package/src/gateway/connectors/devin.ts +6 -0
- package/src/gateway/connectors/factory-droid.ts +308 -0
- package/src/gateway/connectors/goose.ts +79 -0
- package/src/gateway/connectors/grok-cli.ts +82 -0
- package/src/gateway/connectors/hermes.ts +230 -0
- package/src/gateway/connectors/iflow-cli.ts +50 -0
- package/src/gateway/connectors/index.ts +86 -0
- package/src/gateway/connectors/kilo-cli.ts +434 -0
- package/src/gateway/connectors/kilo-code.ts +32 -0
- package/src/gateway/connectors/kimi-code.ts +241 -0
- package/src/gateway/connectors/kiro-cli.ts +6 -0
- package/src/gateway/connectors/kiro-ide.ts +6 -0
- package/src/gateway/connectors/letta-code.ts +170 -0
- package/src/gateway/connectors/mastracode.ts +91 -0
- package/src/gateway/connectors/mistral-vibe.ts +495 -0
- package/src/gateway/connectors/muse.ts +27 -0
- package/src/gateway/connectors/omp.ts +279 -0
- package/src/gateway/connectors/openhands.ts +296 -0
- package/src/gateway/connectors/pearai.ts +52 -0
- package/src/gateway/connectors/pi.ts +357 -0
- package/src/gateway/connectors/qoder-cli.ts +6 -0
- package/src/gateway/connectors/qwen-code.ts +411 -0
- package/src/gateway/connectors/roo.ts +43 -0
- package/src/gateway/connectors/tabnine-cli.ts +6 -0
- package/src/gateway/connectors/void.ts +6 -0
- package/src/gateway/connectors/vscode-copilot.ts +286 -0
- package/src/gateway/connectors/windsurf.ts +6 -0
- 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
|
+
}
|