@100xprompt/brain 0.0.0-stage → 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/LICENSE +48 -0
- package/README.md +235 -2
- package/package.json +48 -4
- package/src/adapters/aider/index.ts +8 -0
- package/src/adapters/aider/reader.ts +129 -0
- package/src/adapters/aider/records.ts +220 -0
- package/src/adapters/aider/writer.ts +269 -0
- package/src/adapters/amazon-q-cli/index.ts +8 -0
- package/src/adapters/amazon-q-cli/reader.ts +84 -0
- package/src/adapters/amazon-q-cli/records.ts +191 -0
- package/src/adapters/amazon-q-cli/writer.ts +370 -0
- package/src/adapters/amazon-q-ide/index.ts +9 -0
- package/src/adapters/amazon-q-ide/reader.ts +131 -0
- package/src/adapters/amazon-q-ide/records.ts +141 -0
- package/src/adapters/amazon-q-ide/writer.ts +325 -0
- package/src/adapters/antigravity/index.ts +9 -0
- package/src/adapters/antigravity/proto.ts +194 -0
- package/src/adapters/antigravity/reader.ts +424 -0
- package/src/adapters/antigravity/records.ts +236 -0
- package/src/adapters/antigravity/writer.ts +463 -0
- package/src/adapters/auggie/index.ts +36 -0
- package/src/adapters/claude-code/index.ts +8 -0
- package/src/adapters/claude-code/reader.ts +639 -0
- package/src/adapters/claude-code/records.ts +88 -0
- package/src/adapters/claude-code/writer.ts +329 -0
- package/src/adapters/cline/index.ts +12 -0
- package/src/adapters/cline/reader.ts +432 -0
- package/src/adapters/cline/records.ts +151 -0
- package/src/adapters/cline/vscdb.ts +56 -0
- package/src/adapters/cline/writer.ts +656 -0
- package/src/adapters/cline/xml.ts +177 -0
- package/src/adapters/cline4/index.ts +9 -0
- package/src/adapters/cline4/reader.ts +179 -0
- package/src/adapters/cline4/records.ts +92 -0
- package/src/adapters/cline4/writer.ts +361 -0
- package/src/adapters/codebuff/index.ts +8 -0
- package/src/adapters/codebuff/project.ts +207 -0
- package/src/adapters/codebuff/reader.ts +215 -0
- package/src/adapters/codebuff/records.ts +173 -0
- package/src/adapters/codebuff/writer.ts +383 -0
- package/src/adapters/codex/index.ts +8 -0
- package/src/adapters/codex/native.ts +239 -0
- package/src/adapters/codex/reader.ts +747 -0
- package/src/adapters/codex/records.ts +114 -0
- package/src/adapters/codex/writer.ts +479 -0
- package/src/adapters/continue/index.ts +9 -0
- package/src/adapters/continue/reader.ts +68 -0
- package/src/adapters/continue/records.ts +418 -0
- package/src/adapters/continue/writer.ts +162 -0
- package/src/adapters/copilot-cli/index.ts +9 -0
- package/src/adapters/copilot-cli/reader.ts +186 -0
- package/src/adapters/copilot-cli/records.ts +241 -0
- package/src/adapters/copilot-cli/writer.ts +353 -0
- package/src/adapters/crush/index.ts +8 -0
- package/src/adapters/crush/reader.ts +180 -0
- package/src/adapters/crush/records.ts +169 -0
- package/src/adapters/crush/writer.ts +333 -0
- package/src/adapters/cursor-cli/index.ts +405 -0
- package/src/adapters/cursor-ide/agentkv.ts +95 -0
- package/src/adapters/cursor-ide/index.ts +501 -0
- package/src/adapters/cursor-ide/transcripts.ts +122 -0
- package/src/adapters/cursor-ide/workspaces.ts +119 -0
- package/src/adapters/cursor-ide/writer.ts +170 -0
- package/src/adapters/devin/index.ts +8 -0
- package/src/adapters/devin/reader.ts +305 -0
- package/src/adapters/devin/records.ts +277 -0
- package/src/adapters/devin/writer.ts +523 -0
- package/src/adapters/factory-droid/index.ts +12 -0
- package/src/adapters/factory-droid/reader.ts +184 -0
- package/src/adapters/factory-droid/records.ts +79 -0
- package/src/adapters/factory-droid/writer.ts +350 -0
- package/src/adapters/gemini-cli/exchanges.ts +228 -0
- package/src/adapters/gemini-cli/index.ts +396 -0
- package/src/adapters/gemini-cli/records.ts +521 -0
- package/src/adapters/gemini-cli/writer.ts +354 -0
- package/src/adapters/goose/index.ts +8 -0
- package/src/adapters/goose/reader.ts +138 -0
- package/src/adapters/goose/records.ts +199 -0
- package/src/adapters/goose/writer.ts +340 -0
- package/src/adapters/grok-cli/image.ts +44 -0
- package/src/adapters/grok-cli/index.ts +8 -0
- package/src/adapters/grok-cli/reader.ts +302 -0
- package/src/adapters/grok-cli/records.ts +172 -0
- package/src/adapters/grok-cli/writer.ts +329 -0
- package/src/adapters/hermes/index.ts +8 -0
- package/src/adapters/hermes/reader.ts +249 -0
- package/src/adapters/hermes/records.ts +285 -0
- package/src/adapters/hermes/writer.ts +452 -0
- package/src/adapters/iflow-cli/index.ts +72 -0
- package/src/adapters/iflow-cli/records.ts +215 -0
- package/src/adapters/iflow-cli/writer.ts +204 -0
- package/src/adapters/index.ts +112 -0
- package/src/adapters/kilo-cli/index.ts +21 -0
- package/src/adapters/kilo-code/index.ts +11 -0
- package/src/adapters/kilo-code/writer.ts +92 -0
- package/src/adapters/kimi/index.ts +9 -0
- package/src/adapters/kimi/reader.ts +92 -0
- package/src/adapters/kimi/records.ts +221 -0
- package/src/adapters/kimi/writer.ts +367 -0
- package/src/adapters/kiro-cli/index.ts +8 -0
- package/src/adapters/kiro-cli/reader.ts +87 -0
- package/src/adapters/kiro-cli/records.ts +260 -0
- package/src/adapters/kiro-cli/writer.ts +305 -0
- package/src/adapters/kiro-ide/index.ts +8 -0
- package/src/adapters/kiro-ide/writer.ts +52 -0
- package/src/adapters/letta/index.ts +8 -0
- package/src/adapters/letta/reader.ts +177 -0
- package/src/adapters/letta/records.ts +119 -0
- package/src/adapters/letta/writer.ts +413 -0
- package/src/adapters/mastracode/index.ts +8 -0
- package/src/adapters/mastracode/reader.ts +323 -0
- package/src/adapters/mastracode/records.ts +336 -0
- package/src/adapters/mastracode/writer.ts +385 -0
- package/src/adapters/mistral-vibe/index.ts +9 -0
- package/src/adapters/mistral-vibe/reader.ts +62 -0
- package/src/adapters/mistral-vibe/records.ts +104 -0
- package/src/adapters/mistral-vibe/writer.ts +359 -0
- package/src/adapters/muse/index.ts +10 -0
- package/src/adapters/muse/reader.ts +332 -0
- package/src/adapters/muse/records.ts +104 -0
- package/src/adapters/muse/writer.ts +387 -0
- package/src/adapters/omp/index.ts +13 -0
- package/src/adapters/omp/reader.ts +314 -0
- package/src/adapters/omp/records.ts +153 -0
- package/src/adapters/omp/writer.ts +216 -0
- package/src/adapters/opencode/ids.ts +52 -0
- package/src/adapters/opencode/index.ts +31 -0
- package/src/adapters/opencode/note.ts +8 -0
- package/src/adapters/opencode/reader.ts +452 -0
- package/src/adapters/opencode/tools.ts +174 -0
- package/src/adapters/opencode/v2.ts +367 -0
- package/src/adapters/opencode/writer.ts +379 -0
- package/src/adapters/openhands/index.ts +8 -0
- package/src/adapters/openhands/reader.ts +222 -0
- package/src/adapters/openhands/records.ts +136 -0
- package/src/adapters/openhands/writer.ts +404 -0
- package/src/adapters/pearai/index.ts +10 -0
- package/src/adapters/pi/index.ts +28 -0
- package/src/adapters/pi/reader.ts +261 -0
- package/src/adapters/pi/records.ts +99 -0
- package/src/adapters/pi/writer.ts +362 -0
- package/src/adapters/qoder-cli/index.ts +8 -0
- package/src/adapters/qoder-cli/reader.ts +83 -0
- package/src/adapters/qoder-cli/records.ts +45 -0
- package/src/adapters/qoder-cli/writer.ts +72 -0
- package/src/adapters/qwen-code/index.ts +84 -0
- package/src/adapters/qwen-code/records.ts +194 -0
- package/src/adapters/qwen-code/writer.ts +224 -0
- package/src/adapters/tabnine-cli/index.ts +8 -0
- package/src/adapters/tabnine-cli/reader.ts +106 -0
- package/src/adapters/tabnine-cli/records.ts +33 -0
- package/src/adapters/tabnine-cli/writer.ts +156 -0
- package/src/adapters/types.ts +73 -0
- package/src/adapters/void/index.ts +9 -0
- package/src/adapters/void/reader.ts +48 -0
- package/src/adapters/void/records.ts +285 -0
- package/src/adapters/void/writer.ts +93 -0
- package/src/adapters/vscode-copilot/index.ts +9 -0
- package/src/adapters/vscode-copilot/reader.ts +212 -0
- package/src/adapters/vscode-copilot/records.ts +388 -0
- package/src/adapters/vscode-copilot/shared.ts +68 -0
- package/src/adapters/vscode-copilot/writer.ts +546 -0
- package/src/adapters/zed/index.ts +8 -0
- package/src/adapters/zed/reader.ts +198 -0
- package/src/adapters/zed/records.ts +87 -0
- package/src/adapters/zed/writer.ts +341 -0
- package/src/brief-writers.ts +71 -0
- package/src/brief.ts +238 -0
- package/src/cli.ts +351 -0
- 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/fsutil.ts +133 -0
- package/src/gateway/common.ts +232 -0
- package/src/gateway/connect.ts +847 -0
- 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
- package/src/gateway/signin.ts +323 -0
- package/src/handoff.ts +104 -0
- package/src/hooks.ts +455 -0
- package/src/memory/audit.ts +514 -0
- package/src/memory/blobs.ts +94 -0
- package/src/memory/cli.ts +117 -0
- package/src/memory/distill.ts +381 -0
- package/src/memory/externalize.ts +435 -0
- package/src/memory/lookups.ts +70 -0
- package/src/memory/mcp-install.ts +401 -0
- package/src/memory/mcp-server.ts +296 -0
- package/src/memory/prepare.ts +179 -0
- package/src/memory/preview.ts +154 -0
- package/src/memory/project.ts +35 -0
- package/src/memory/redact.ts +72 -0
- package/src/memory/relevance.ts +187 -0
- package/src/memory/repomap.ts +516 -0
- package/src/memory/store.ts +270 -0
- package/src/memory/sync.ts +166 -0
- package/src/memory/tidy.ts +193 -0
- package/src/memory/types.ts +118 -0
- package/src/org/admin-ops.ts +96 -0
- package/src/org/auth.ts +112 -0
- package/src/org/backup.ts +115 -0
- package/src/org/cli.ts +438 -0
- package/src/org/client.ts +404 -0
- package/src/org/config.ts +44 -0
- package/src/org/crypto.ts +537 -0
- package/src/org/db.ts +435 -0
- package/src/org/graph.ts +318 -0
- package/src/org/oidc.ts +435 -0
- package/src/org/policy.ts +398 -0
- package/src/org/server.ts +920 -0
- package/src/org/sso.ts +316 -0
- package/src/org/tls.ts +66 -0
- package/src/org/types.ts +193 -0
- package/src/self.ts +68 -0
- package/src/session/attachments.ts +53 -0
- package/src/session/peekcache.ts +50 -0
- package/src/session/types.ts +104 -0
- package/src/session/util.ts +130 -0
- package/src/sqlite.ts +48 -0
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
// Gateway connector for GitHub Copilot CLI. Copilot CLI's "bring your own key" providers live in $COPILOT_HOME/providers.json
|
|
2
|
+
// (default ~/.copilot; $COPILOT_PROVIDERS_CONFIG names another file), and settings.json's `model` picks one as the default
|
|
3
|
+
// as "<provider>/<model id>". Checked against @github/copilot 1.0.107 run against a local fake server (2026-11-10): an
|
|
4
|
+
// "openai" provider with baseUrl <route>/v1 sends POST <route>/v1/chat/completions with `Authorization: Bearer <apiKey>`
|
|
5
|
+
// and the provider's `headers`, with no GitHub sign-in; `apiKeyCommand` is NOT read from this file (no key is sent), so
|
|
6
|
+
// the key itself is kept there and the file is made private. A `modelProviderId` left in settings.json by an earlier /model
|
|
7
|
+
// pick makes the CLI refuse the gateway model ("The configured provider/model pair is not available"), so it is cleared
|
|
8
|
+
// while connected. Docs: https://docs.github.com/en/copilot/how-tos/copilot-cli/customize-copilot/use-byok-models
|
|
9
|
+
import { isAbsolute, join } from "node:path"
|
|
10
|
+
import { type Codec, type Connector, dirFromEnv, EDIT_HEADER, type Env, type Json, readKey, routeUrl } from "../common.ts"
|
|
11
|
+
|
|
12
|
+
const PROVIDER = "brain-gateway"
|
|
13
|
+
const copilotHome = (home: string, env: Env) => dirFromEnv(env, "COPILOT_HOME", join(home, ".copilot"))
|
|
14
|
+
/** The providers file the CLI reads: $COPILOT_PROVIDERS_CONFIG when it is set (the CLI then reads nothing else), else <home>/providers.json. */
|
|
15
|
+
const providersPath = (home: string, env: Env) => {
|
|
16
|
+
const named = env.COPILOT_PROVIDERS_CONFIG?.trim()
|
|
17
|
+
return named && isAbsolute(named) ? named : join(copilotHome(home, env), "providers.json")
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Parses a JSON object file ({} when empty); throws on anything else. */
|
|
21
|
+
function parseObject(text: string): Json {
|
|
22
|
+
if (!text.trim()) return {}
|
|
23
|
+
const v = JSON.parse(text)
|
|
24
|
+
if (!v || typeof v !== "object" || Array.isArray(v)) throw new Error("not an object")
|
|
25
|
+
return v as Json
|
|
26
|
+
}
|
|
27
|
+
const render = (text: string, o: Json) => `${JSON.stringify(o, null, text.match(/\n([ \t]+)"/)?.[1] ?? " ")}\n`
|
|
28
|
+
function checkObject(text: string): string | undefined {
|
|
29
|
+
try {
|
|
30
|
+
parseObject(text)
|
|
31
|
+
return undefined
|
|
32
|
+
} catch {
|
|
33
|
+
return "it is not a plain JSON object (comments?); left as it is"
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* providers.json: only brain's own entries are touched, so the person's other providers and models stay usable while
|
|
39
|
+
* connected. Keys:
|
|
40
|
+
* providers/brain-gateway the entry of `providers` whose name is brain-gateway
|
|
41
|
+
* models/brain-gateway the entry of `models` whose provider is brain-gateway
|
|
42
|
+
* An entry is replaced where it is, or added at the end; removing the last entry of a list removes the list.
|
|
43
|
+
*/
|
|
44
|
+
const ENTRY: Record<string, { list: "providers" | "models"; field: "name" | "provider" }> = {
|
|
45
|
+
[`providers/${PROVIDER}`]: { list: "providers", field: "name" },
|
|
46
|
+
[`models/${PROVIDER}`]: { list: "models", field: "provider" },
|
|
47
|
+
}
|
|
48
|
+
export const providersCodec: Codec = {
|
|
49
|
+
check(text) {
|
|
50
|
+
const why = checkObject(text)
|
|
51
|
+
if (why) return why
|
|
52
|
+
const o = parseObject(text)
|
|
53
|
+
for (const list of ["providers", "models"]) if (list in o && !Array.isArray(o[list])) return `its "${list}" is not a list; left as it is`
|
|
54
|
+
return undefined
|
|
55
|
+
},
|
|
56
|
+
get(text, key) {
|
|
57
|
+
const at = ENTRY[key]
|
|
58
|
+
if (!at) return undefined
|
|
59
|
+
const list = parseObject(text)[at.list]
|
|
60
|
+
return Array.isArray(list) ? list.find((x) => x && typeof x === "object" && x[at.field] === PROVIDER) : undefined
|
|
61
|
+
},
|
|
62
|
+
set(text, key, value) {
|
|
63
|
+
const at = ENTRY[key]
|
|
64
|
+
if (!at) return text
|
|
65
|
+
const o = parseObject(text)
|
|
66
|
+
const list: unknown[] = Array.isArray(o[at.list]) ? [...o[at.list]] : []
|
|
67
|
+
const i = list.findIndex((x) => !!x && typeof x === "object" && (x as Json)[at.field] === PROVIDER)
|
|
68
|
+
if (value === undefined) {
|
|
69
|
+
if (i < 0) return text
|
|
70
|
+
list.splice(i, 1)
|
|
71
|
+
} else if (i >= 0) list[i] = value
|
|
72
|
+
else list.push(value)
|
|
73
|
+
if (list.length) o[at.list] = list
|
|
74
|
+
else delete o[at.list]
|
|
75
|
+
return render(text, o)
|
|
76
|
+
},
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* settings.json: top-level keys. `modelProviderId` reads as null when it is absent, and setting it to null removes it:
|
|
81
|
+
* connect sets null (cleared), and disconnect puts back the person's own provider id, or nothing.
|
|
82
|
+
*/
|
|
83
|
+
const CLEARED = "modelProviderId"
|
|
84
|
+
export const settingsCodec: Codec = {
|
|
85
|
+
check: checkObject,
|
|
86
|
+
get(text, key) {
|
|
87
|
+
const o = parseObject(text)
|
|
88
|
+
return key in o ? o[key] : key === CLEARED ? null : undefined
|
|
89
|
+
},
|
|
90
|
+
set(text, key, value) {
|
|
91
|
+
const o = parseObject(text)
|
|
92
|
+
if (value === undefined || (key === CLEARED && value === null)) {
|
|
93
|
+
if (!(key in o)) return text
|
|
94
|
+
delete o[key]
|
|
95
|
+
} else o[key] = value
|
|
96
|
+
return render(text, o)
|
|
97
|
+
},
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export const connectors: Connector[] = [
|
|
101
|
+
{
|
|
102
|
+
id: "copilot-cli",
|
|
103
|
+
label: "GitHub Copilot CLI",
|
|
104
|
+
file: (home, env) => providersPath(home, env),
|
|
105
|
+
probe: [".copilot"],
|
|
106
|
+
authHeader: "authorization",
|
|
107
|
+
edits: (home, s, env) => [
|
|
108
|
+
{
|
|
109
|
+
path: providersPath(home, env),
|
|
110
|
+
format: "custom",
|
|
111
|
+
codec: providersCodec,
|
|
112
|
+
holdsKey: true,
|
|
113
|
+
secretKeys: [`providers/${PROVIDER}`],
|
|
114
|
+
values: {
|
|
115
|
+
[`providers/${PROVIDER}`]: {
|
|
116
|
+
name: PROVIDER,
|
|
117
|
+
type: "openai",
|
|
118
|
+
baseUrl: `${routeUrl(s)}/v1`,
|
|
119
|
+
apiKey: readKey(home),
|
|
120
|
+
...(s.contextEdit ? { headers: { [EDIT_HEADER]: "on" } } : {}),
|
|
121
|
+
},
|
|
122
|
+
// Gemini takes images; without this Copilot CLI treats a BYOK model as text-only and drops images tools return.
|
|
123
|
+
[`models/${PROVIDER}`]: { id: s.model, provider: PROVIDER, capabilities: { supports: { vision: true } } },
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
path: join(copilotHome(home, env), "settings.json"),
|
|
128
|
+
format: "custom",
|
|
129
|
+
codec: settingsCodec,
|
|
130
|
+
values: { model: `${PROVIDER}/${s.model}`, [CLEARED]: null },
|
|
131
|
+
},
|
|
132
|
+
],
|
|
133
|
+
notes: [
|
|
134
|
+
"Copilot CLI reads a provider's key only from providers.json or the environment, so the key is kept in providers.json, which is now readable by you only (0600).",
|
|
135
|
+
"The gateway model is now Copilot CLI's default and needs no GitHub sign-in; --model, COPILOT_MODEL or /model still pick another model for a session.",
|
|
136
|
+
],
|
|
137
|
+
},
|
|
138
|
+
]
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
// Gateway connector for Crush (charmbracelet/crush), checked against v0.100.0 (the copy in ~/.cache/brain-wire/crush,
|
|
2
|
+
// `crush --version`) and its source at tag v0.100.0.
|
|
3
|
+
//
|
|
4
|
+
// Where Crush reads its settings (internal/config/load.go, lookupConfigs and Load): these files are merged, a later one
|
|
5
|
+
// winning over an earlier one:
|
|
6
|
+
// ~/.config/crush/crush.json the person's JSON config (deprecated for crushrc; JSONC, may hold comments)
|
|
7
|
+
// ~/.config/crush/crushrc the person's Bash config (docs/config/README.md, "Where config lives")
|
|
8
|
+
// ~/.local/share/crush/crush.json Crush's data file ($CRUSH_GLOBAL_DATA/crush.json, else $XDG_DATA_HOME/crush/crush.json;
|
|
9
|
+
// load.go GlobalConfigData)
|
|
10
|
+
// ./crushrc, ./crush.json, ... project files, then <project>/.crush/crush.json (the workspace file)
|
|
11
|
+
// The model picked in Crush's UI, and an API key typed into it, go to the data file (store.go: configPath returns
|
|
12
|
+
// globalDataPath for the global scope; UpdatePreferredModel writes models.large / models.small there), and the data file
|
|
13
|
+
// outranks ~/.config/crush. A model selection written to the person's config would be overridden for anyone who has ever
|
|
14
|
+
// picked a model, so brain writes where Crush itself writes: the data file. It is plain JSON (Crush refuses anything else:
|
|
15
|
+
// json.Valid) that Crush edits in place with sjson; the codec below edits it the same way, changing only the members brain
|
|
16
|
+
// owns, so every other byte stays and disconnect gives back the exact bytes.
|
|
17
|
+
//
|
|
18
|
+
// What is written there:
|
|
19
|
+
// providers.brain-gateway a custom OpenAI-compatible provider: "type": "openai-compat" (Chat Completions at
|
|
20
|
+
// <route>/v1; v0.100.0 reads it as "completions" in memory, load.go NormalizeProviderType,
|
|
21
|
+
// without rewriting the file; older versions take it as it is), "api_key": "$(cat '<keyPath>')"
|
|
22
|
+
// (Crush shell-expands api_key, base_url and extra_headers when it loads, load.go
|
|
23
|
+
// configureProviders and the README's "any $(...) in crush.json runs at load time": the key is
|
|
24
|
+
// read from brain's key file at each start and never copied), "extra_headers" with
|
|
25
|
+
// x-100x-context-edit, and one model shown as "100xprompt Pro". The model uses only catwalk v3
|
|
26
|
+
// fields: a v2 field (supports_attachments, can_reason, cost_per_1m_in) would make Crush rewrite
|
|
27
|
+
// the whole file on load (load.go migrateCatwalkV2File).
|
|
28
|
+
// models.large, models.small that model. The small one titles sessions and summarises; left on another provider it would
|
|
29
|
+
// go around the gateway.
|
|
30
|
+
// A model list is given, so Crush does not ask the gateway for /v1/models (403 there).
|
|
31
|
+
import { existsSync } from "node:fs"
|
|
32
|
+
import { isAbsolute, join, resolve } from "node:path"
|
|
33
|
+
import { type Codec, type Connector, EDIT_HEADER, type Env, getAt, type Json, keyPath, modelName, routeUrl, shellQuote } from "../common.ts"
|
|
34
|
+
|
|
35
|
+
/** Crush's data file, resolved as Crush does; the env overrides describe the person's real home, so they count only there. */
|
|
36
|
+
export function crushDataFile(home: string, env: Env): string {
|
|
37
|
+
const own = !!env.HOME && resolve(env.HOME) === resolve(home)
|
|
38
|
+
const g = env.CRUSH_GLOBAL_DATA
|
|
39
|
+
if (own && g && isAbsolute(g) && existsSync(g)) return join(g, "crush.json")
|
|
40
|
+
const x = env.XDG_DATA_HOME
|
|
41
|
+
if (own && x && isAbsolute(x)) return join(x, "crush", "crush.json")
|
|
42
|
+
return join(home, ".local", "share", "crush", "crush.json")
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ---- JSON, edited in place ----------------------------------------------------------------------------------------------
|
|
46
|
+
|
|
47
|
+
interface Member {
|
|
48
|
+
key: string
|
|
49
|
+
keyStart: number
|
|
50
|
+
keyEnd: number
|
|
51
|
+
valueStart: number
|
|
52
|
+
valueEnd: number
|
|
53
|
+
value: Node
|
|
54
|
+
}
|
|
55
|
+
type Node = { kind: "object"; start: number; end: number; members: Member[] } | { kind: "value"; start: number; end: number }
|
|
56
|
+
type ObjectNode = Extract<Node, { kind: "object" }>
|
|
57
|
+
|
|
58
|
+
/** Where every value of a JSON text starts and ends (throws on anything that is not JSON). */
|
|
59
|
+
function scan(text: string): Node {
|
|
60
|
+
let i = 0
|
|
61
|
+
const ws = () => {
|
|
62
|
+
while (i < text.length && " \t\n\r".includes(text[i]!)) i++
|
|
63
|
+
}
|
|
64
|
+
const str = (): string => {
|
|
65
|
+
const s = i++
|
|
66
|
+
while (i < text.length && text[i] !== '"') i += text[i] === "\\" ? 2 : 1
|
|
67
|
+
if (i >= text.length) throw new Error("unterminated string")
|
|
68
|
+
i++
|
|
69
|
+
return JSON.parse(text.slice(s, i)) as string
|
|
70
|
+
}
|
|
71
|
+
const value = (): Node => {
|
|
72
|
+
ws()
|
|
73
|
+
const start = i
|
|
74
|
+
const c = text[i]
|
|
75
|
+
if (c === "{") {
|
|
76
|
+
i++
|
|
77
|
+
const members: Member[] = []
|
|
78
|
+
ws()
|
|
79
|
+
if (text[i] === "}") return { kind: "object", start, end: ++i, members }
|
|
80
|
+
for (;;) {
|
|
81
|
+
ws()
|
|
82
|
+
if (text[i] !== '"') throw new Error("expected a key")
|
|
83
|
+
const keyStart = i
|
|
84
|
+
const key = str()
|
|
85
|
+
const keyEnd = i
|
|
86
|
+
ws()
|
|
87
|
+
if (text[i++] !== ":") throw new Error("expected :")
|
|
88
|
+
const v = value()
|
|
89
|
+
members.push({ key, keyStart, keyEnd, valueStart: v.start, valueEnd: v.end, value: v })
|
|
90
|
+
ws()
|
|
91
|
+
const d = text[i++]
|
|
92
|
+
if (d === "}") return { kind: "object", start, end: i, members }
|
|
93
|
+
if (d !== ",") throw new Error("expected , or }")
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (c === "[") {
|
|
97
|
+
i++
|
|
98
|
+
ws()
|
|
99
|
+
if (text[i] === "]") return { kind: "value", start, end: ++i }
|
|
100
|
+
for (;;) {
|
|
101
|
+
value()
|
|
102
|
+
ws()
|
|
103
|
+
const d = text[i++]
|
|
104
|
+
if (d === "]") return { kind: "value", start, end: i }
|
|
105
|
+
if (d !== ",") throw new Error("expected , or ]")
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if (c === '"') {
|
|
109
|
+
str()
|
|
110
|
+
return { kind: "value", start, end: i }
|
|
111
|
+
}
|
|
112
|
+
while (i < text.length && !",}] \t\n\r".includes(text[i]!)) i++
|
|
113
|
+
if (i === start) throw new Error("expected a value")
|
|
114
|
+
return { kind: "value", start, end: i }
|
|
115
|
+
}
|
|
116
|
+
const root = value()
|
|
117
|
+
ws()
|
|
118
|
+
if (i !== text.length) throw new Error("text after the JSON value")
|
|
119
|
+
return root
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** How the file is laid out, so what is added looks like what is there (one line, as Crush writes it, or indented). */
|
|
123
|
+
interface Style {
|
|
124
|
+
pretty: boolean
|
|
125
|
+
indent: string
|
|
126
|
+
/** Between a key and its value: ":" or ": ". */
|
|
127
|
+
colon: string
|
|
128
|
+
}
|
|
129
|
+
function styleOf(text: string, root: ObjectNode): Style {
|
|
130
|
+
const pretty = text.slice(root.start, root.end).includes("\n")
|
|
131
|
+
const indent = text.match(/\n([ \t]+)"/)?.[1] ?? " "
|
|
132
|
+
const m = root.members[0]
|
|
133
|
+
return { pretty, indent, colon: m ? text.slice(m.keyEnd, m.valueStart) : pretty ? ": " : ":" }
|
|
134
|
+
}
|
|
135
|
+
const render = (v: unknown, depth: number, s: Style) => (s.pretty ? JSON.stringify(v, null, s.indent).split("\n").join(`\n${s.indent.repeat(depth)}`) : JSON.stringify(v))
|
|
136
|
+
const splice = (text: string, from: number, to: number, put: string) => text.slice(0, from) + put + text.slice(to)
|
|
137
|
+
const lastIndex = (o: ObjectNode, key: string) => o.members.map((m) => m.key).lastIndexOf(key)
|
|
138
|
+
|
|
139
|
+
/** Adds `"key": value` at the end of `o` (whose members sit at `depth`), separated the way its members already are. */
|
|
140
|
+
function insert(text: string, o: ObjectNode, depth: number, key: string, v: unknown, s: Style): string {
|
|
141
|
+
const member = `${JSON.stringify(key)}${s.colon}${render(v, depth, s)}`
|
|
142
|
+
const n = o.members.length
|
|
143
|
+
if (!n) return splice(text, o.start + 1, o.end - 1, s.pretty ? `\n${s.indent.repeat(depth)}${member}\n${s.indent.repeat(depth - 1)}` : member)
|
|
144
|
+
const last = o.members[n - 1]!
|
|
145
|
+
const gap = n >= 2 ? text.slice(o.members[n - 2]!.valueEnd, last.keyStart) : `,${text.slice(o.start + 1, o.members[0]!.keyStart)}`
|
|
146
|
+
return splice(text, last.valueEnd, last.valueEnd, `${gap}${member}`)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Removes member `i` of `o` with the separator that came with it (the exact reverse of insert). */
|
|
150
|
+
function remove(text: string, o: ObjectNode, i: number): string {
|
|
151
|
+
const n = o.members.length
|
|
152
|
+
if (n === 1) return splice(text, o.start + 1, o.end - 1, "")
|
|
153
|
+
if (i > 0) return splice(text, o.members[i - 1]!.valueEnd, o.members[i]!.valueEnd, "")
|
|
154
|
+
return splice(text, o.members[0]!.keyStart, o.members[1]!.keyStart, "")
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** The object at `path` (undefined when absent or not an object). */
|
|
158
|
+
function objectAt(root: ObjectNode, path: string[]): ObjectNode | undefined {
|
|
159
|
+
let cur: ObjectNode = root
|
|
160
|
+
for (const p of path) {
|
|
161
|
+
const i = lastIndex(cur, p)
|
|
162
|
+
const next = i >= 0 ? cur.members[i]!.value : undefined
|
|
163
|
+
if (!next || next.kind !== "object") return undefined
|
|
164
|
+
cur = next
|
|
165
|
+
}
|
|
166
|
+
return cur
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const rootOf = (text: string): ObjectNode => {
|
|
170
|
+
const r = scan(text)
|
|
171
|
+
if (r.kind !== "object") throw new Error("not a JSON object")
|
|
172
|
+
return r
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function jsonSet(text: string, key: string, value: unknown): string {
|
|
176
|
+
const blank = !text.trim()
|
|
177
|
+
if (blank && value === undefined) return text
|
|
178
|
+
const base = blank ? "{}\n" : text
|
|
179
|
+
const root = rootOf(base)
|
|
180
|
+
// A file brain starts is indented, for people to read; one that exists keeps its own layout.
|
|
181
|
+
const s: Style = blank ? { pretty: true, indent: " ", colon: ": " } : styleOf(base, root)
|
|
182
|
+
const parts = key.split("/")
|
|
183
|
+
// The deepest object on the path that exists, and the members still missing below it.
|
|
184
|
+
let depth = 0
|
|
185
|
+
let o = root
|
|
186
|
+
while (depth < parts.length - 1) {
|
|
187
|
+
const i = lastIndex(o, parts[depth]!)
|
|
188
|
+
const next = i >= 0 ? o.members[i]!.value : undefined
|
|
189
|
+
if (!next || next.kind !== "object") break
|
|
190
|
+
o = next
|
|
191
|
+
depth++
|
|
192
|
+
}
|
|
193
|
+
const name = parts[depth]!
|
|
194
|
+
const i = lastIndex(o, name)
|
|
195
|
+
if (value === undefined) {
|
|
196
|
+
if (depth < parts.length - 1 || i < 0) return text
|
|
197
|
+
let out = remove(base, o, i)
|
|
198
|
+
// Drop the objects this left empty (as the core's JSON handling does), never the root.
|
|
199
|
+
for (let d = parts.length - 1; d >= 1; d--) {
|
|
200
|
+
const r = rootOf(out)
|
|
201
|
+
const child = objectAt(r, parts.slice(0, d))
|
|
202
|
+
if (!child || child.members.length) break
|
|
203
|
+
const parent = objectAt(r, parts.slice(0, d - 1))!
|
|
204
|
+
out = remove(out, parent, lastIndex(parent, parts[d - 1]!))
|
|
205
|
+
}
|
|
206
|
+
return out
|
|
207
|
+
}
|
|
208
|
+
// What goes in at `name`: the value itself, or the missing objects around it.
|
|
209
|
+
const nested = parts.slice(depth + 1).reduceRight<unknown>((v, p) => ({ [p]: v }), value)
|
|
210
|
+
if (i >= 0) {
|
|
211
|
+
const m = o.members[i]!
|
|
212
|
+
return splice(base, m.valueStart, m.valueEnd, render(nested, depth + 1, s))
|
|
213
|
+
}
|
|
214
|
+
return insert(base, o, depth + 1, name, nested, s)
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const canon = (v: unknown): string =>
|
|
218
|
+
Array.isArray(v) ? `[${v.map(canon).join(",")}]` : v && typeof v === "object" ? `{${Object.keys(v).sort().map((k) => `${JSON.stringify(k)}:${canon((v as Json)[k])}`).join(",")}}` : JSON.stringify(v)
|
|
219
|
+
|
|
220
|
+
/** Drops `key` and the objects it leaves empty (to compare the rest of a file). */
|
|
221
|
+
function without(o: Json, key: string): Json {
|
|
222
|
+
const c = structuredClone(o)
|
|
223
|
+
const ps = key.split("/")
|
|
224
|
+
const chain: Json[] = [c]
|
|
225
|
+
for (const p of ps.slice(0, -1)) {
|
|
226
|
+
const next = chain[chain.length - 1]?.[p]
|
|
227
|
+
if (!next || typeof next !== "object" || Array.isArray(next)) return c
|
|
228
|
+
chain.push(next)
|
|
229
|
+
}
|
|
230
|
+
delete chain[chain.length - 1]![ps[ps.length - 1]!]
|
|
231
|
+
for (let d = ps.length - 1; d >= 1; d--) if (!Object.keys(chain[d]!).length) delete chain[d - 1]![ps[d - 1]!]
|
|
232
|
+
return c
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** The members brain sets, with stand-in values: a trial edit with them must land exactly there and touch nothing else. */
|
|
236
|
+
const TRIAL: Record<string, unknown> = {
|
|
237
|
+
"providers/brain-gateway": { name: "trial", type: "openai-compat", models: [{ id: "m" }] },
|
|
238
|
+
"models/large": { model: "m", provider: "brain-gateway" },
|
|
239
|
+
"models/small": { model: "m", provider: "brain-gateway" },
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** Crush's data file: plain JSON, edited in place (keys are paths with "/", as in the core's JSON handling). */
|
|
243
|
+
export const crushJson: Codec = {
|
|
244
|
+
check(text) {
|
|
245
|
+
if (!text.trim()) return undefined
|
|
246
|
+
let parsed: unknown
|
|
247
|
+
try {
|
|
248
|
+
parsed = JSON.parse(text)
|
|
249
|
+
} catch {
|
|
250
|
+
return "it is not valid JSON; left as it is"
|
|
251
|
+
}
|
|
252
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return "it is not a JSON object; left as it is"
|
|
253
|
+
try {
|
|
254
|
+
let t = text
|
|
255
|
+
for (const [k, v] of Object.entries(TRIAL)) t = jsonSet(t, k, v)
|
|
256
|
+
const after = JSON.parse(t) as Json
|
|
257
|
+
let rest = after
|
|
258
|
+
let was = parsed as Json
|
|
259
|
+
for (const [k, v] of Object.entries(TRIAL)) {
|
|
260
|
+
if (canon(getAt(after, k)) !== canon(v)) throw new Error(k)
|
|
261
|
+
rest = without(rest, k)
|
|
262
|
+
was = without(was, k)
|
|
263
|
+
}
|
|
264
|
+
if (canon(rest) !== canon(was)) throw new Error("other values moved")
|
|
265
|
+
} catch {
|
|
266
|
+
return "brain cannot change it without touching other settings; left as it is"
|
|
267
|
+
}
|
|
268
|
+
return undefined
|
|
269
|
+
},
|
|
270
|
+
get(text, key) {
|
|
271
|
+
if (!text.trim()) return undefined
|
|
272
|
+
return getAt(JSON.parse(text) as Json, key)
|
|
273
|
+
},
|
|
274
|
+
set: jsonSet,
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export const PROVIDER = "brain-gateway"
|
|
278
|
+
|
|
279
|
+
export const connectors: Connector[] = [
|
|
280
|
+
{
|
|
281
|
+
id: "crush",
|
|
282
|
+
label: "Crush",
|
|
283
|
+
file: crushDataFile,
|
|
284
|
+
probe: [".config/crush", ".local/share/crush"],
|
|
285
|
+
authHeader: "authorization",
|
|
286
|
+
edits: (home, s, env) => {
|
|
287
|
+
const pick = { model: s.model, provider: PROVIDER }
|
|
288
|
+
return [
|
|
289
|
+
{
|
|
290
|
+
path: crushDataFile(home, env),
|
|
291
|
+
format: "custom",
|
|
292
|
+
codec: crushJson,
|
|
293
|
+
values: {
|
|
294
|
+
[`providers/${PROVIDER}`]: {
|
|
295
|
+
name: "100xprompt gateway",
|
|
296
|
+
type: "openai-compat",
|
|
297
|
+
base_url: `${routeUrl(s)}/v1`,
|
|
298
|
+
api_key: `$(cat ${shellQuote(keyPath(home))})`,
|
|
299
|
+
...(s.contextEdit ? { extra_headers: { [EDIT_HEADER]: "on" } } : {}),
|
|
300
|
+
models: [{ id: s.model, name: modelName(s.model), context_window: 1048576, default_max_tokens: 65536, capabilities: { vision: true } }],
|
|
301
|
+
},
|
|
302
|
+
"models/large": pick,
|
|
303
|
+
"models/small": pick,
|
|
304
|
+
},
|
|
305
|
+
},
|
|
306
|
+
]
|
|
307
|
+
},
|
|
308
|
+
notes: [
|
|
309
|
+
"Crush reads the key from the brain's key file each time it starts (its api_key runs `cat` on that file); no key is written to Crush's files.",
|
|
310
|
+
"The gateway and the model choice are in Crush's own data file (~/.local/share/crush/crush.json), where Crush keeps the model you pick in its UI; a crushrc or crush.json inside a project still overrides it there.",
|
|
311
|
+
],
|
|
312
|
+
},
|
|
313
|
+
]
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// Gateway connector for Cursor CLI (cursor-agent): none. It talks only to Cursor's backend: its hidden --endpoint picks a
|
|
2
|
+
// Cursor API server (default https://api2.cursor.sh, index.js:33 of build 2025.09.12), and Cursor offers no own key or base
|
|
3
|
+
// URL in the CLI (https://help.openai.com/en/articles/20001506; https://docs.litellm.ai/docs/tutorials/cursor_integration).
|
|
4
|
+
import type { Connector } from "../common.ts"
|
|
5
|
+
|
|
6
|
+
export const connectors: Connector[] = []
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// Gateway connector for Cursor (IDE): none. Its "Override OpenAI Base URL" and key live only in the app's state database and
|
|
2
|
+
// the OS keychain (Cursor 3.29.9 workbench.desktop.main.js:2037 `openAIBaseUrl`, :20344 `cursorAuth/openAIKey`), and Cursor's
|
|
3
|
+
// servers make the calls, for chat models only (https://cursor.com/docs/settings/api-keys; env-var local agent off: :59).
|
|
4
|
+
import type { Connector } from "../common.ts"
|
|
5
|
+
|
|
6
|
+
export const connectors: Connector[] = []
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// Gateway connector for Devin CLI: none. The standard CLI uses Cognition's models only (~/.config/devin/config.json names one,
|
|
2
|
+
// with no endpoint or key; wire/drivers/f.ts:47); a custom endpoint exists only in federal air-gapped builds, through
|
|
3
|
+
// $DEVIN_MODELS_FILE with the key in an environment variable (https://docs.devin.ai/federal/air-gapped/models-file).
|
|
4
|
+
import type { Connector } from "../common.ts"
|
|
5
|
+
|
|
6
|
+
export const connectors: Connector[] = []
|