@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,357 @@
1
+ // Gateway connector for Pi (badlogic's pi coding agent, npm @earendil-works/pi-coding-agent; checked against 1.5.0, the copy
2
+ // installed in ~/.cache/brain-wire/pi, whose docs and dist/ are cited below).
3
+ //
4
+ // Pi takes an OpenAI-compatible endpoint as a custom provider in <agent-dir>/models.json, and its startup model from
5
+ // <agent-dir>/settings.json; the agent dir is ~/.pi/agent, or $PI_CODING_AGENT_DIR (dist/config.js:494-516,
6
+ // docs/configuration.md "Agent directory").
7
+ // - models.json `providers.<id>`: name, baseUrl, api ("openai-completions", ...), apiKey, headers, compat, models[{id, name,
8
+ // input}] (docs/models-configuration.md "Providers", "Models"). The file may hold `//` comments and trailing commas
9
+ // (dist/core/model-config.js:139 parses it with stripJsonComments, dist/utils/json.js).
10
+ // - apiKey and header values are a literal, "$VAR", or "!command" whose stdout is used, run at request time
11
+ // (dist/core/resolve-config-value.js:66-68, 123-129; docs/models.md "Configure a compatible endpoint"). So the key is
12
+ // read at run time from brain's key file with `!cat '<key file>'`; it is never copied into Pi's files.
13
+ // - headers carry `x-100x-context-edit: on` (provider `headers`, "Values resolve like apiKey").
14
+ // - settings.json `defaultProvider` + `defaultModel` pick the startup model (docs/settings.md "Model and thinking";
15
+ // dist/core/sdk.js:107-108 findInitialModel). settings.json is plain JSON (dist/core/settings-manager.js:269) that Pi itself
16
+ // writes with JSON.stringify(_, null, 2) and no final newline (:471), so both files go through the text-keeping editor
17
+ // below, which changes only the keys brain owns and gives the exact bytes back on disconnect.
18
+ // The request is OpenAI Chat Completions on <route>/v1 with Authorization: Bearer (pi-ai dist/api/openai-completions.js:516-547,
19
+ // the OpenAI client's apiKey). compat pins the request to the shape OpenCode already sends the gateway (system role, no
20
+ // `store`, max_tokens), instead of Pi guessing from an unknown URL (openai-completions.js:1217-1300 would send the developer
21
+ // role and `store`).
22
+ import { join } from "node:path"
23
+ import { type Codec, type Connector, dirFromEnv, EDIT_HEADER, type Env, type GatewaySettings, type Json, keyPath, modelName, routeUrl, shellQuote } from "../common.ts"
24
+
25
+ // ---- a JSON / JSON-with-comments editor that keeps every byte it does not change --------------------------------------
26
+ //
27
+ // Keys are paths with "/" (providers/brain-gateway). A segment `list[field=value]` is the element of the array `list` whose
28
+ // `field` is `value`, so a whole list entry can be one key (customProviders[name=100xprompt gateway]). A value that is set
29
+ // replaces only the old value's text; a key that is added goes last in its object (or list), in the file's own indentation;
30
+ // a key that is removed takes back exactly what adding it put in. Objects a removal leaves empty are dropped, as the core's
31
+ // JSON handling does (never the root, never a list).
32
+
33
+ interface Member {
34
+ key?: string
35
+ /** Where the member starts (its key, or the element's value) and where its value ends. */
36
+ start: number
37
+ end: number
38
+ /** The comma after it, or -1. */
39
+ comma: number
40
+ node: Node
41
+ }
42
+ type Node = { kind: "object" | "array"; start: number; end: number; members: Member[] } | { kind: "scalar"; start: number; end: number; value: unknown }
43
+ type Hop = { key: string } | { field: string; equals: string }
44
+
45
+ class JsonSyntax extends Error {}
46
+
47
+ /** The syntax tree of a JSON text (with `//` and `/* *\/` comments and trailing commas when `jsonc`). Throws on bad input. */
48
+ function parseTree(text: string, jsonc: boolean): Node {
49
+ let i = text.charCodeAt(0) === 0xfeff ? 1 : 0
50
+ const fail = (): never => {
51
+ throw new JsonSyntax(`unexpected input at ${i}`)
52
+ }
53
+ const skip = () => {
54
+ for (;;) {
55
+ while (i < text.length && " \t\r\n".includes(text[i]!)) i++
56
+ if (jsonc && text.startsWith("//", i)) {
57
+ const n = text.indexOf("\n", i)
58
+ i = n < 0 ? text.length : n
59
+ } else if (jsonc && text.startsWith("/*", i)) {
60
+ const n = text.indexOf("*/", i + 2)
61
+ if (n < 0) fail()
62
+ i = n + 2
63
+ } else return
64
+ }
65
+ }
66
+ const STR = /"(?:[^"\\\u0000-\u001f]|\\(?:["\\/bfnrt]|u[0-9a-fA-F]{4}))*"/y
67
+ const LIT = /-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?|true|false|null/y
68
+ const token = (re: RegExp) => {
69
+ re.lastIndex = i
70
+ const m = re.exec(text)
71
+ if (!m) fail()
72
+ i += m![0].length
73
+ return JSON.parse(m![0]) as unknown
74
+ }
75
+ const value = (): Node => {
76
+ skip()
77
+ const start = i
78
+ const c = text[i]
79
+ if (c === "{" || c === "[") {
80
+ const close = c === "{" ? "}" : "]"
81
+ const members: Member[] = []
82
+ i++
83
+ for (;;) {
84
+ skip()
85
+ const last = members[members.length - 1]
86
+ if (text[i] === close) {
87
+ if (last && last.comma >= 0 && !jsonc) fail()
88
+ i++
89
+ break
90
+ }
91
+ if (last && last.comma < 0) fail()
92
+ const mStart = i
93
+ let key: string | undefined
94
+ if (c === "{") {
95
+ if (text[i] !== '"') fail()
96
+ key = token(STR) as string
97
+ skip()
98
+ if (text[i] !== ":") fail()
99
+ i++
100
+ }
101
+ const node = value()
102
+ const m: Member = { start: mStart, end: node.end, comma: -1, node, ...(key !== undefined ? { key } : {}) }
103
+ skip()
104
+ if (text[i] === ",") m.comma = i++
105
+ members.push(m)
106
+ }
107
+ return { kind: c === "{" ? "object" : "array", start, end: i, members }
108
+ }
109
+ return { kind: "scalar", start, value: token(c === '"' ? STR : LIT), end: i }
110
+ }
111
+ const root = value()
112
+ skip()
113
+ if (i !== text.length) fail()
114
+ return root
115
+ }
116
+
117
+ function valueOf(n: Node): unknown {
118
+ if (n.kind === "scalar") return n.value
119
+ if (n.kind === "array") return n.members.map((m) => valueOf(m.node))
120
+ return Object.fromEntries(n.members.map((m) => [m.key!, valueOf(m.node)]))
121
+ }
122
+
123
+ const ENTRY = /^(.*)\[([^=\]]+)=(.*)\]$/
124
+ function hops(key: string): Hop[] {
125
+ return key.split("/").flatMap((s): Hop[] => {
126
+ const m = ENTRY.exec(s)
127
+ return m ? [{ key: m[1]! }, { field: m[2]!, equals: m[3]! }] : [{ key: s }]
128
+ })
129
+ }
130
+
131
+ const isObj = (v: unknown): v is Json => !!v && typeof v === "object" && !Array.isArray(v)
132
+
133
+ /** The member of `n` that `h` names (the last one, as JSON.parse keeps the last of a repeated key), or -1. */
134
+ function memberIndex(n: Node, h: Hop): number {
135
+ if ("key" in h) {
136
+ if (n.kind !== "object") return -1
137
+ for (let k = n.members.length - 1; k >= 0; k--) if (n.members[k]!.key === h.key) return k
138
+ return -1
139
+ }
140
+ if (n.kind !== "array") return -1
141
+ return n.members.findIndex((m) => m.node.kind === "object" && (valueOf(m.node) as Json)[h.field] === h.equals)
142
+ }
143
+
144
+ /** The value a missing hop's member gets so that the remaining hops lead to `value`. */
145
+ function memberValue(hop: Hop, after: Hop[], value: unknown): unknown {
146
+ let v = value
147
+ if (after.length) {
148
+ const [next, ...more] = after
149
+ const inner = memberValue(next!, more, value)
150
+ v = "key" in next! ? { [next.key]: inner } : [inner]
151
+ }
152
+ return "key" in hop || !after.length ? v : { [hop.field]: hop.equals, ...(v as Json) }
153
+ }
154
+
155
+ const lineStart = (t: string, at: number) => t.lastIndexOf("\n", at - 1) + 1
156
+ const lineIndent = (t: string, at: number) => /^[ \t]*/.exec(t.slice(lineStart(t, at)))![0]
157
+ const startsLine = (t: string, at: number) => !t.slice(lineStart(t, at), at).trim()
158
+ const blank = (t: string, a: number, b: number) => !t.slice(a, b).trim()
159
+ /** The file's indentation step: the first indented member's, else two spaces (as the core's JSON handling). */
160
+ const unitOf = (t: string) => /\n([ \t]+)"/.exec(t)?.[1] ?? " "
161
+ const render = (v: unknown, indent: string, unit: string) => JSON.stringify(v, null, unit).replace(/\n/g, `\n${indent}`)
162
+ const oneLine = (t: string, n: Node) => !t.slice(n.start, n.end).includes("\n")
163
+
164
+ function insertMember(t: string, c: Node & { members: Member[] }, hop: Hop, v: unknown): string {
165
+ const label = "key" in hop ? `${JSON.stringify(hop.key)}: ` : ""
166
+ const open = c.start + 1
167
+ const close = c.end - 1
168
+ const last = c.members[c.members.length - 1]
169
+ if (last && oneLine(t, c)) {
170
+ const body = `${label}${JSON.stringify(v)}`
171
+ return last.comma >= 0 ? `${t.slice(0, last.comma + 1)} ${body},${t.slice(last.comma + 1)}` : `${t.slice(0, last.end)}, ${body}${t.slice(last.end)}`
172
+ }
173
+ const unit = unitOf(t)
174
+ const outer = lineIndent(t, c.start)
175
+ if (!last) {
176
+ const inner = `${outer}${unit}`
177
+ const body = `${label}${render(v, inner, unit)}`
178
+ if (blank(t, open, close)) return `${t.slice(0, open)}\n${inner}${body}\n${outer}${t.slice(close)}`
179
+ // Only comments inside: the new member goes on its own line before the closing bracket's line.
180
+ const ls = lineStart(t, close)
181
+ if (ls > open && startsLine(t, close)) return `${t.slice(0, ls)}${inner}${body}\n${t.slice(ls)}`
182
+ return `${t.slice(0, close)}\n${inner}${body}\n${outer}${t.slice(close)}`
183
+ }
184
+ const inner = startsLine(t, last.start) ? lineIndent(t, last.start) : `${outer}${unit}`
185
+ const body = `${label}${render(v, inner, unit)}`
186
+ // A trailing comma stays a trailing comma (JSONC); otherwise the comma goes right after the last value. A `// comment` at
187
+ // the end of the last member's line stays on that line: the new member goes on the next one.
188
+ const after = last.comma >= 0 ? last.comma + 1 : last.end
189
+ const nl = t.indexOf("\n", after)
190
+ const eol = nl < 0 ? t.length : nl
191
+ const tail = t.slice(after, eol).trim()
192
+ const at = tail.startsWith("//") || (tail.startsWith("/*") && tail.endsWith("*/")) ? eol : after
193
+ if (last.comma >= 0) return `${t.slice(0, at)}\n${inner}${body},${t.slice(at)}`
194
+ return `${t.slice(0, last.end)},${t.slice(last.end, at)}\n${inner}${body}${t.slice(at)}`
195
+ }
196
+
197
+ /** Removes [a, b), and the whole line when nothing else is left on it. */
198
+ function dropSpan(t: string, a: number, b: number): string {
199
+ const ls = lineStart(t, a)
200
+ const nl = t.indexOf("\n", b)
201
+ const le = nl < 0 ? t.length : nl
202
+ if (blank(t, ls, a) && blank(t, b, le)) return t.slice(0, ls) + t.slice(Math.min(le + 1, t.length))
203
+ return t.slice(0, a) + t.slice(b)
204
+ }
205
+
206
+ /** Removes member `idx`: exactly what insertMember added when it was added there, otherwise its text and one comma. */
207
+ function removeMember(t: string, c: Node & { members: Member[] }, idx: number): string {
208
+ const m = c.members[idx]!
209
+ const prev = c.members[idx - 1]
210
+ const next = c.members[idx + 1]
211
+ const open = c.start + 1
212
+ const close = c.end - 1
213
+ const own = m.comma >= 0 ? m.comma + 1 : m.end
214
+ if (c.members.length === 1) return blank(t, open, m.start) && blank(t, own, close) ? t.slice(0, open) + t.slice(close) : dropSpan(t, m.start, own)
215
+ if (next || m.comma >= 0) {
216
+ const s = prev ? prev.comma + 1 : open
217
+ return blank(t, s, m.start) && blank(t, m.end, m.comma) ? t.slice(0, s) + t.slice(m.comma + 1) : dropSpan(t, m.start, m.comma + 1)
218
+ }
219
+ // The last member, after `prev` (whose comma separates them).
220
+ const p = prev!
221
+ if (blank(t, p.end, p.comma) && blank(t, p.comma + 1, m.start)) return t.slice(0, p.comma) + t.slice(m.end)
222
+ const dropped = dropSpan(t, m.start, m.end)
223
+ return dropped.slice(0, p.comma) + dropped.slice(p.comma + 1)
224
+ }
225
+
226
+ /** The containers along `path` from the root: [container, index of the member in it] for each hop, while they exist. */
227
+ function walk(root: Node, path: Hop[]): Array<{ c: Node & { members: Member[] }; idx: number }> {
228
+ const out: Array<{ c: Node & { members: Member[] }; idx: number }> = []
229
+ let n = root
230
+ for (const h of path) {
231
+ if (n.kind === "scalar") break
232
+ const idx = memberIndex(n, h)
233
+ if (idx < 0) break
234
+ out.push({ c: n, idx })
235
+ n = n.members[idx]!.node
236
+ }
237
+ return out
238
+ }
239
+
240
+ /**
241
+ * A Codec for a JSON file that keeps comments, formatting and key order. `jsonc`: the file may hold comments and trailing
242
+ * commas (refused otherwise, as the tool would refuse them). `mustExist`: the reason to give when the file is missing or
243
+ * empty, for a tool whose own first run writes it (brain then waits for that rather than writing it first). `shape`: the
244
+ * containers the keys go through ("providers": "object"), so a file where one is something else is refused up front.
245
+ */
246
+ export function jsonText(o: { jsonc: boolean; mustExist?: string; shape?: Record<string, "object" | "array"> }): Codec {
247
+ const tree = (text: string) => parseTree(text, o.jsonc)
248
+ return {
249
+ check(text) {
250
+ if (!text.trim()) return o.mustExist
251
+ let root: Node
252
+ try {
253
+ root = tree(text)
254
+ } catch {
255
+ return o.jsonc ? "it is not valid JSON (comments and trailing commas allowed); left as it is" : "it is not plain JSON (comments?); left as it is"
256
+ }
257
+ if (root.kind !== "object") return "it is not a JSON object; left as it is"
258
+ const v = valueOf(root) as Json
259
+ for (const [path, kind] of Object.entries(o.shape ?? {})) {
260
+ let x: unknown = v
261
+ for (const p of path.split("/")) x = isObj(x) ? x[p] : undefined
262
+ if (x !== undefined && (kind === "array" ? !Array.isArray(x) || x.some((e) => !isObj(e)) : !isObj(x))) return `its ${path.replace(/\//g, ".")} is not ${kind === "array" ? "a list of objects" : "an object"}; left as it is`
263
+ }
264
+ return undefined
265
+ },
266
+ get(text, key) {
267
+ if (!text.trim()) return undefined
268
+ let v: unknown = valueOf(tree(text))
269
+ for (const h of hops(key)) {
270
+ if ("key" in h) v = isObj(v) && Object.hasOwn(v, h.key) ? v[h.key] : undefined
271
+ else v = Array.isArray(v) ? v.find((x) => isObj(x) && x[h.field] === h.equals) : undefined
272
+ if (v === undefined) return undefined
273
+ }
274
+ return v
275
+ },
276
+ set(text, key, value) {
277
+ const fresh = !text.trim()
278
+ let t = fresh ? "{}" : text
279
+ const path = hops(key)
280
+ const chain = walk(tree(t), path)
281
+ if (chain.length < path.length) {
282
+ if (value === undefined) return text
283
+ // Missing from hop k on: add one member there that holds the rest of the path.
284
+ const k = chain.length
285
+ const holder = (k ? chain[k - 1]!.c.members[chain[k - 1]!.idx]!.node : tree(t)) as Node & { members: Member[] }
286
+ if (holder.kind === "scalar" || ("key" in path[k]! ? holder.kind !== "object" : holder.kind !== "array")) throw new Error(`${key}: the file has a different shape there`)
287
+ t = insertMember(t, holder, path[k]!, memberValue(path[k]!, path.slice(k + 1), value))
288
+ return fresh ? `${t}\n` : t
289
+ }
290
+ const { c, idx } = chain[chain.length - 1]!
291
+ if (value !== undefined) {
292
+ const n = c.members[idx]!.node
293
+ t = t.slice(0, n.start) + (oneLine(t, c) ? JSON.stringify(value) : render(value, lineIndent(t, c.members[idx]!.start), unitOf(t))) + t.slice(n.end)
294
+ return fresh ? `${t}\n` : t
295
+ }
296
+ t = removeMember(t, c, idx)
297
+ // Objects the removal left empty go too (a key hop's object only; never the root or a list entry).
298
+ for (let d = path.length - 1; d >= 1; d--) {
299
+ if (!("key" in path[d - 1]!)) break
300
+ const up = walk(tree(t), path.slice(0, d))
301
+ if (up.length < d) break
302
+ const { c: pc, idx: pi } = up[d - 1]!
303
+ const n = pc.members[pi]!.node
304
+ if (n.kind !== "object" || n.members.length || !blank(t, n.start + 1, n.end - 1)) break
305
+ t = removeMember(t, pc, pi)
306
+ }
307
+ return t
308
+ },
309
+ }
310
+ }
311
+
312
+ // ---- Pi -----------------------------------------------------------------------------------------------------------------
313
+
314
+ export const PROVIDER_ID = "brain-gateway"
315
+ export const PROVIDER_NAME = "100xprompt gateway"
316
+
317
+ /**
318
+ * The OpenAI Chat Completions request shape the gateway is known to take from OpenCode: system role (not developer), no
319
+ * `store`, max_tokens. Pi and Oh My Pi share these compat names (Pi docs/models-configuration.md "openai-completions").
320
+ */
321
+ export const CHAT_COMPAT = { supportsDeveloperRole: false, supportsStore: false, maxTokensField: "max_tokens" }
322
+
323
+ /** The command Pi and Oh My Pi run to read the key at request time ("!command" values). */
324
+ export const keyCommand = (home: string) => `!cat ${shellQuote(keyPath(home))}`
325
+
326
+ const agentDir = (home: string, env: Env) => dirFromEnv(env, "PI_CODING_AGENT_DIR", join(home, ".pi", "agent"))
327
+
328
+ export function piProvider(home: string, s: GatewaySettings): Json {
329
+ return {
330
+ name: PROVIDER_NAME,
331
+ baseUrl: `${routeUrl(s)}/v1`,
332
+ api: "openai-completions",
333
+ apiKey: keyCommand(home),
334
+ ...(s.contextEdit ? { headers: { [EDIT_HEADER]: "on" } } : {}),
335
+ compat: CHAT_COMPAT,
336
+ models: [{ id: s.model, name: modelName(s.model), input: ["text", "image"] }],
337
+ }
338
+ }
339
+
340
+ const pi: Connector = {
341
+ id: "pi",
342
+ label: "Pi",
343
+ file: (home, env) => join(agentDir(home, env), "models.json"),
344
+ // The agent dir (the main file's folder) is what Pi's first run creates; nothing else to look for.
345
+ probe: [],
346
+ authHeader: "authorization",
347
+ edits: (home, s, env) => [
348
+ { path: join(agentDir(home, env), "models.json"), format: "custom", codec: jsonText({ jsonc: true, shape: { providers: "object" } }), values: { [`providers/${PROVIDER_ID}`]: piProvider(home, s) } },
349
+ { path: join(agentDir(home, env), "settings.json"), format: "custom", codec: jsonText({ jsonc: false }), values: { defaultProvider: PROVIDER_ID, defaultModel: s.model } },
350
+ ],
351
+ notes: [
352
+ `Pi now starts on the "${PROVIDER_NAME}" provider in models.json and reads the key from the brain's key file at each request (apiKey "!cat <key file>").`,
353
+ "A --provider/--model flag, a resumed session's own model, or an enabledModels list in settings.json that leaves this model out still take precedence.",
354
+ ],
355
+ }
356
+
357
+ export const connectors: Connector[] = [pi]
@@ -0,0 +1,6 @@
1
+ // Gateway connector for Qoder CLI: not built. It has OpenAI-compatible custom models, but only through the /model wizard of a
2
+ // signed-in account on an eligible plan, and Qoder says not to write them into settings.json by hand; it makes no model
3
+ // request before a Qoder sign-in (https://docs.qoder.com/cli/custom-models; wire/drivers/b.ts:20).
4
+ import type { Connector } from "../common.ts"
5
+
6
+ export const connectors: Connector[] = []