@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.
- 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 +275 -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,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[] = []
|