@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.
Files changed (260) hide show
  1. package/LICENSE +48 -0
  2. package/README.md +235 -2
  3. package/package.json +48 -4
  4. package/src/adapters/aider/index.ts +8 -0
  5. package/src/adapters/aider/reader.ts +129 -0
  6. package/src/adapters/aider/records.ts +220 -0
  7. package/src/adapters/aider/writer.ts +269 -0
  8. package/src/adapters/amazon-q-cli/index.ts +8 -0
  9. package/src/adapters/amazon-q-cli/reader.ts +84 -0
  10. package/src/adapters/amazon-q-cli/records.ts +191 -0
  11. package/src/adapters/amazon-q-cli/writer.ts +370 -0
  12. package/src/adapters/amazon-q-ide/index.ts +9 -0
  13. package/src/adapters/amazon-q-ide/reader.ts +131 -0
  14. package/src/adapters/amazon-q-ide/records.ts +141 -0
  15. package/src/adapters/amazon-q-ide/writer.ts +325 -0
  16. package/src/adapters/antigravity/index.ts +9 -0
  17. package/src/adapters/antigravity/proto.ts +194 -0
  18. package/src/adapters/antigravity/reader.ts +424 -0
  19. package/src/adapters/antigravity/records.ts +236 -0
  20. package/src/adapters/antigravity/writer.ts +463 -0
  21. package/src/adapters/auggie/index.ts +36 -0
  22. package/src/adapters/claude-code/index.ts +8 -0
  23. package/src/adapters/claude-code/reader.ts +639 -0
  24. package/src/adapters/claude-code/records.ts +88 -0
  25. package/src/adapters/claude-code/writer.ts +329 -0
  26. package/src/adapters/cline/index.ts +12 -0
  27. package/src/adapters/cline/reader.ts +432 -0
  28. package/src/adapters/cline/records.ts +151 -0
  29. package/src/adapters/cline/vscdb.ts +56 -0
  30. package/src/adapters/cline/writer.ts +656 -0
  31. package/src/adapters/cline/xml.ts +177 -0
  32. package/src/adapters/cline4/index.ts +9 -0
  33. package/src/adapters/cline4/reader.ts +179 -0
  34. package/src/adapters/cline4/records.ts +92 -0
  35. package/src/adapters/cline4/writer.ts +361 -0
  36. package/src/adapters/codebuff/index.ts +8 -0
  37. package/src/adapters/codebuff/project.ts +207 -0
  38. package/src/adapters/codebuff/reader.ts +215 -0
  39. package/src/adapters/codebuff/records.ts +173 -0
  40. package/src/adapters/codebuff/writer.ts +383 -0
  41. package/src/adapters/codex/index.ts +8 -0
  42. package/src/adapters/codex/native.ts +239 -0
  43. package/src/adapters/codex/reader.ts +747 -0
  44. package/src/adapters/codex/records.ts +114 -0
  45. package/src/adapters/codex/writer.ts +479 -0
  46. package/src/adapters/continue/index.ts +9 -0
  47. package/src/adapters/continue/reader.ts +68 -0
  48. package/src/adapters/continue/records.ts +418 -0
  49. package/src/adapters/continue/writer.ts +162 -0
  50. package/src/adapters/copilot-cli/index.ts +9 -0
  51. package/src/adapters/copilot-cli/reader.ts +186 -0
  52. package/src/adapters/copilot-cli/records.ts +241 -0
  53. package/src/adapters/copilot-cli/writer.ts +353 -0
  54. package/src/adapters/crush/index.ts +8 -0
  55. package/src/adapters/crush/reader.ts +180 -0
  56. package/src/adapters/crush/records.ts +169 -0
  57. package/src/adapters/crush/writer.ts +333 -0
  58. package/src/adapters/cursor-cli/index.ts +405 -0
  59. package/src/adapters/cursor-ide/agentkv.ts +95 -0
  60. package/src/adapters/cursor-ide/index.ts +501 -0
  61. package/src/adapters/cursor-ide/transcripts.ts +122 -0
  62. package/src/adapters/cursor-ide/workspaces.ts +119 -0
  63. package/src/adapters/cursor-ide/writer.ts +170 -0
  64. package/src/adapters/devin/index.ts +8 -0
  65. package/src/adapters/devin/reader.ts +305 -0
  66. package/src/adapters/devin/records.ts +277 -0
  67. package/src/adapters/devin/writer.ts +523 -0
  68. package/src/adapters/factory-droid/index.ts +12 -0
  69. package/src/adapters/factory-droid/reader.ts +184 -0
  70. package/src/adapters/factory-droid/records.ts +79 -0
  71. package/src/adapters/factory-droid/writer.ts +350 -0
  72. package/src/adapters/gemini-cli/exchanges.ts +228 -0
  73. package/src/adapters/gemini-cli/index.ts +396 -0
  74. package/src/adapters/gemini-cli/records.ts +521 -0
  75. package/src/adapters/gemini-cli/writer.ts +354 -0
  76. package/src/adapters/goose/index.ts +8 -0
  77. package/src/adapters/goose/reader.ts +138 -0
  78. package/src/adapters/goose/records.ts +199 -0
  79. package/src/adapters/goose/writer.ts +340 -0
  80. package/src/adapters/grok-cli/image.ts +44 -0
  81. package/src/adapters/grok-cli/index.ts +8 -0
  82. package/src/adapters/grok-cli/reader.ts +302 -0
  83. package/src/adapters/grok-cli/records.ts +172 -0
  84. package/src/adapters/grok-cli/writer.ts +329 -0
  85. package/src/adapters/hermes/index.ts +8 -0
  86. package/src/adapters/hermes/reader.ts +249 -0
  87. package/src/adapters/hermes/records.ts +285 -0
  88. package/src/adapters/hermes/writer.ts +452 -0
  89. package/src/adapters/iflow-cli/index.ts +72 -0
  90. package/src/adapters/iflow-cli/records.ts +215 -0
  91. package/src/adapters/iflow-cli/writer.ts +204 -0
  92. package/src/adapters/index.ts +112 -0
  93. package/src/adapters/kilo-cli/index.ts +21 -0
  94. package/src/adapters/kilo-code/index.ts +11 -0
  95. package/src/adapters/kilo-code/writer.ts +92 -0
  96. package/src/adapters/kimi/index.ts +9 -0
  97. package/src/adapters/kimi/reader.ts +92 -0
  98. package/src/adapters/kimi/records.ts +221 -0
  99. package/src/adapters/kimi/writer.ts +367 -0
  100. package/src/adapters/kiro-cli/index.ts +8 -0
  101. package/src/adapters/kiro-cli/reader.ts +87 -0
  102. package/src/adapters/kiro-cli/records.ts +260 -0
  103. package/src/adapters/kiro-cli/writer.ts +305 -0
  104. package/src/adapters/kiro-ide/index.ts +8 -0
  105. package/src/adapters/kiro-ide/writer.ts +52 -0
  106. package/src/adapters/letta/index.ts +8 -0
  107. package/src/adapters/letta/reader.ts +177 -0
  108. package/src/adapters/letta/records.ts +119 -0
  109. package/src/adapters/letta/writer.ts +413 -0
  110. package/src/adapters/mastracode/index.ts +8 -0
  111. package/src/adapters/mastracode/reader.ts +323 -0
  112. package/src/adapters/mastracode/records.ts +336 -0
  113. package/src/adapters/mastracode/writer.ts +385 -0
  114. package/src/adapters/mistral-vibe/index.ts +9 -0
  115. package/src/adapters/mistral-vibe/reader.ts +62 -0
  116. package/src/adapters/mistral-vibe/records.ts +104 -0
  117. package/src/adapters/mistral-vibe/writer.ts +359 -0
  118. package/src/adapters/muse/index.ts +10 -0
  119. package/src/adapters/muse/reader.ts +332 -0
  120. package/src/adapters/muse/records.ts +104 -0
  121. package/src/adapters/muse/writer.ts +387 -0
  122. package/src/adapters/omp/index.ts +13 -0
  123. package/src/adapters/omp/reader.ts +314 -0
  124. package/src/adapters/omp/records.ts +153 -0
  125. package/src/adapters/omp/writer.ts +216 -0
  126. package/src/adapters/opencode/ids.ts +52 -0
  127. package/src/adapters/opencode/index.ts +31 -0
  128. package/src/adapters/opencode/note.ts +8 -0
  129. package/src/adapters/opencode/reader.ts +452 -0
  130. package/src/adapters/opencode/tools.ts +174 -0
  131. package/src/adapters/opencode/v2.ts +367 -0
  132. package/src/adapters/opencode/writer.ts +379 -0
  133. package/src/adapters/openhands/index.ts +8 -0
  134. package/src/adapters/openhands/reader.ts +222 -0
  135. package/src/adapters/openhands/records.ts +136 -0
  136. package/src/adapters/openhands/writer.ts +404 -0
  137. package/src/adapters/pearai/index.ts +10 -0
  138. package/src/adapters/pi/index.ts +28 -0
  139. package/src/adapters/pi/reader.ts +261 -0
  140. package/src/adapters/pi/records.ts +99 -0
  141. package/src/adapters/pi/writer.ts +362 -0
  142. package/src/adapters/qoder-cli/index.ts +8 -0
  143. package/src/adapters/qoder-cli/reader.ts +83 -0
  144. package/src/adapters/qoder-cli/records.ts +45 -0
  145. package/src/adapters/qoder-cli/writer.ts +72 -0
  146. package/src/adapters/qwen-code/index.ts +84 -0
  147. package/src/adapters/qwen-code/records.ts +194 -0
  148. package/src/adapters/qwen-code/writer.ts +224 -0
  149. package/src/adapters/tabnine-cli/index.ts +8 -0
  150. package/src/adapters/tabnine-cli/reader.ts +106 -0
  151. package/src/adapters/tabnine-cli/records.ts +33 -0
  152. package/src/adapters/tabnine-cli/writer.ts +156 -0
  153. package/src/adapters/types.ts +73 -0
  154. package/src/adapters/void/index.ts +9 -0
  155. package/src/adapters/void/reader.ts +48 -0
  156. package/src/adapters/void/records.ts +285 -0
  157. package/src/adapters/void/writer.ts +93 -0
  158. package/src/adapters/vscode-copilot/index.ts +9 -0
  159. package/src/adapters/vscode-copilot/reader.ts +212 -0
  160. package/src/adapters/vscode-copilot/records.ts +388 -0
  161. package/src/adapters/vscode-copilot/shared.ts +68 -0
  162. package/src/adapters/vscode-copilot/writer.ts +546 -0
  163. package/src/adapters/zed/index.ts +8 -0
  164. package/src/adapters/zed/reader.ts +198 -0
  165. package/src/adapters/zed/records.ts +87 -0
  166. package/src/adapters/zed/writer.ts +341 -0
  167. package/src/brief-writers.ts +71 -0
  168. package/src/brief.ts +238 -0
  169. package/src/cli.ts +351 -0
  170. package/src/dashboard/cli.ts +148 -0
  171. package/src/dashboard/conversions.ts +170 -0
  172. package/src/dashboard/report.ts +261 -0
  173. package/src/dashboard/send.ts +213 -0
  174. package/src/dashboard/state.ts +95 -0
  175. package/src/fsutil.ts +133 -0
  176. package/src/gateway/common.ts +232 -0
  177. package/src/gateway/connect.ts +847 -0
  178. package/src/gateway/connectors/aider.ts +356 -0
  179. package/src/gateway/connectors/amazon-q-cli.ts +6 -0
  180. package/src/gateway/connectors/amazon-q-ide.ts +6 -0
  181. package/src/gateway/connectors/antigravity.ts +6 -0
  182. package/src/gateway/connectors/auggie.ts +6 -0
  183. package/src/gateway/connectors/cline-sdk.ts +85 -0
  184. package/src/gateway/connectors/cline.ts +34 -0
  185. package/src/gateway/connectors/codebuff.ts +6 -0
  186. package/src/gateway/connectors/continue.ts +579 -0
  187. package/src/gateway/connectors/copilot-cli.ts +138 -0
  188. package/src/gateway/connectors/crush.ts +313 -0
  189. package/src/gateway/connectors/cursor-cli.ts +6 -0
  190. package/src/gateway/connectors/cursor.ts +6 -0
  191. package/src/gateway/connectors/devin.ts +6 -0
  192. package/src/gateway/connectors/factory-droid.ts +308 -0
  193. package/src/gateway/connectors/goose.ts +79 -0
  194. package/src/gateway/connectors/grok-cli.ts +82 -0
  195. package/src/gateway/connectors/hermes.ts +230 -0
  196. package/src/gateway/connectors/iflow-cli.ts +50 -0
  197. package/src/gateway/connectors/index.ts +86 -0
  198. package/src/gateway/connectors/kilo-cli.ts +434 -0
  199. package/src/gateway/connectors/kilo-code.ts +32 -0
  200. package/src/gateway/connectors/kimi-code.ts +241 -0
  201. package/src/gateway/connectors/kiro-cli.ts +6 -0
  202. package/src/gateway/connectors/kiro-ide.ts +6 -0
  203. package/src/gateway/connectors/letta-code.ts +170 -0
  204. package/src/gateway/connectors/mastracode.ts +91 -0
  205. package/src/gateway/connectors/mistral-vibe.ts +495 -0
  206. package/src/gateway/connectors/muse.ts +27 -0
  207. package/src/gateway/connectors/omp.ts +279 -0
  208. package/src/gateway/connectors/openhands.ts +296 -0
  209. package/src/gateway/connectors/pearai.ts +52 -0
  210. package/src/gateway/connectors/pi.ts +357 -0
  211. package/src/gateway/connectors/qoder-cli.ts +6 -0
  212. package/src/gateway/connectors/qwen-code.ts +411 -0
  213. package/src/gateway/connectors/roo.ts +43 -0
  214. package/src/gateway/connectors/tabnine-cli.ts +6 -0
  215. package/src/gateway/connectors/void.ts +6 -0
  216. package/src/gateway/connectors/vscode-copilot.ts +286 -0
  217. package/src/gateway/connectors/windsurf.ts +6 -0
  218. package/src/gateway/connectors/zed.ts +29 -0
  219. package/src/gateway/signin.ts +323 -0
  220. package/src/handoff.ts +104 -0
  221. package/src/hooks.ts +455 -0
  222. package/src/memory/audit.ts +514 -0
  223. package/src/memory/blobs.ts +94 -0
  224. package/src/memory/cli.ts +117 -0
  225. package/src/memory/distill.ts +381 -0
  226. package/src/memory/externalize.ts +435 -0
  227. package/src/memory/lookups.ts +70 -0
  228. package/src/memory/mcp-install.ts +401 -0
  229. package/src/memory/mcp-server.ts +296 -0
  230. package/src/memory/prepare.ts +179 -0
  231. package/src/memory/preview.ts +154 -0
  232. package/src/memory/project.ts +35 -0
  233. package/src/memory/redact.ts +72 -0
  234. package/src/memory/relevance.ts +187 -0
  235. package/src/memory/repomap.ts +516 -0
  236. package/src/memory/store.ts +270 -0
  237. package/src/memory/sync.ts +166 -0
  238. package/src/memory/tidy.ts +193 -0
  239. package/src/memory/types.ts +118 -0
  240. package/src/org/admin-ops.ts +96 -0
  241. package/src/org/auth.ts +112 -0
  242. package/src/org/backup.ts +115 -0
  243. package/src/org/cli.ts +438 -0
  244. package/src/org/client.ts +404 -0
  245. package/src/org/config.ts +44 -0
  246. package/src/org/crypto.ts +537 -0
  247. package/src/org/db.ts +435 -0
  248. package/src/org/graph.ts +318 -0
  249. package/src/org/oidc.ts +435 -0
  250. package/src/org/policy.ts +398 -0
  251. package/src/org/server.ts +920 -0
  252. package/src/org/sso.ts +316 -0
  253. package/src/org/tls.ts +66 -0
  254. package/src/org/types.ts +193 -0
  255. package/src/self.ts +68 -0
  256. package/src/session/attachments.ts +53 -0
  257. package/src/session/peekcache.ts +50 -0
  258. package/src/session/types.ts +104 -0
  259. package/src/session/util.ts +130 -0
  260. package/src/sqlite.ts +48 -0
@@ -0,0 +1,434 @@
1
+ // Gateway connector for Kilo CLI (`kilo`, Kilo-Org/kilocode, an OpenCode fork). Checked against Kilo CLI 7.8.28 (the copy
2
+ // in ~/.cache/brain-wire/kilo-code, `kilo --version`), its source at Kilo-Org/kilocode main (package version 7.8.28) and
3
+ // https://kilo.ai/docs/code-with-ai/platforms/cli. The Kilo Code VS Code extension (7.x) runs this same CLI and reads the
4
+ // same file: its connector (kilo-code.ts) is built from the factory below.
5
+ //
6
+ // Where Kilo reads its settings:
7
+ // ~/.config/kilo/ (`kilo debug paths`: config = $XDG_CONFIG_HOME/kilo, else ~/.config/kilo; macOS too). Every one of
8
+ // config.json, kilo.json, kilo.jsonc, opencode.json, opencode.jsonc found there is read and merged, a later one winning
9
+ // (the binary's GLOBAL_CONFIG_FILES list; checked by running `kilo debug config` with a different "model" in each:
10
+ // opencode.jsonc wins, then opencode.json, kilo.jsonc, kilo.json). Each is JSON with comments and trailing commas
11
+ // (jsonc-parser, allowTrailingComma). Kilo writes ~/.config/kilo/kilo.jsonc on its first run ({"$schema": ...}), and its
12
+ // own settings writer (Config.updateGlobal) edits the first of kilo.jsonc, kilo.json, opencode.jsonc, opencode.json,
13
+ // config.json that exists. A project's kilo.json(c), opencode.json(c) or .kilo/ folder outranks all of these.
14
+ // Docs: "Global: ~/.config/kilo/kilo.json[c] or legacy opencode.json[c]"; "Project-level configuration takes precedence".
15
+ //
16
+ // What is written, in the global file with the highest rank that exists (so the model set there is the one Kilo uses; a
17
+ // new kilo.jsonc when there is none), or in the one that already holds brain's provider (so disconnect finds it again):
18
+ // provider.brain-gateway {"npm": "@ai-sdk/openai-compatible", "name": "100xprompt gateway",
19
+ // "options": {"baseURL": "<route>/v1", "apiKey": "{file:<keyPath>}", "headers": {...}},
20
+ // "models": {"<model id>": {"name": "100xprompt Pro"}}}
21
+ // the shape the Kilo Code extension itself saves for a custom provider (kilo-vscode
22
+ // src/provider-actions.ts saveCustomProvider writes provider.<id> to the global config).
23
+ // OpenAI Chat Completions with Authorization: Bearer; the openai-compatible SDK is bundled in
24
+ // the binary (no download). A model list is given, so /v1/models (403 at the gateway) is not
25
+ // needed. options.headers is sent with every model request (x-100x-context-edit: on).
26
+ // model "brain-gateway/<model id>": the default model of `kilo run`, the TUI and new Kilo Code
27
+ // sessions (kilo-vscode provider-actions.ts computeDefaultSelection: config.model first, the
28
+ // VS Code setting kilo-code.new.model.* only when the config names none).
29
+ // The key: {file:<path>} in a global (trusted) config is replaced with that file's text, trimmed, when Kilo loads it
30
+ // (docs: "{env:VAR} (and {file:...}) references are resolved only in trusted config"; the binary's config substitution). So
31
+ // the key is read from brain's key file at each start and never copied. If that file is missing (after `brain gateway
32
+ // logout`), Kilo refuses the whole config ("Configuration is invalid ... bad file reference") until the next sign-in or a
33
+ // disconnect: checked with the binary.
34
+ //
35
+ // Checked end to end with the 7.8.28 binary against the local stand-in gateway (scripts/gateway-real/kilo-cli.ts): the
36
+ // request came in on /flash/v1/chat/completions with "Authorization: Bearer <key>", x-100x-context-edit: on and the
37
+ // gateway's model id.
38
+ //
39
+ // The core's JSON handling refuses comments, and Kilo's files have them, so this file has its own codec (jsoncInPlace):
40
+ // it reads JSON with comments and trailing commas, and changes only the members brain owns, in place; every other byte
41
+ // stays, and a member brain added is removed with exactly the text it brought, so disconnect gives back the exact bytes.
42
+ // Before any change it makes a trial edit and refuses a file where that would not land exactly on brain's members.
43
+ import { existsSync, readFileSync } from "node:fs"
44
+ import { isAbsolute, join, resolve } from "node:path"
45
+ import { type Codec, type Connector, EDIT_HEADER, type Env, getAt, type Json, keyPath, modelName, routeUrl } from "../common.ts"
46
+
47
+ // ---- JSON with comments, edited in place --------------------------------------------------------------------------------
48
+
49
+ interface Member {
50
+ key: string
51
+ keyStart: number
52
+ valueStart: number
53
+ valueEnd: number
54
+ /** The comma after the value (a separator or a trailing comma), or -1. */
55
+ comma: number
56
+ value: Node
57
+ }
58
+ type ObjectNode = { kind: "object"; start: number; end: number; members: Member[] }
59
+ type Node = ObjectNode | { kind: "array" | "value"; start: number; end: number }
60
+
61
+ const WS = /[ \t\n\r\v\f\u00a0\ufeff\u2028\u2029]/
62
+
63
+ /** Positions and value of a JSON-with-comments text (comments, trailing commas; throws on anything else). */
64
+ function scan(text: string): { node: Node; value: unknown } {
65
+ let i = 0
66
+ const fail = (what: string): never => {
67
+ throw new Error(`${what} at offset ${i}`)
68
+ }
69
+ const skip = () => {
70
+ for (;;) {
71
+ if (i < text.length && WS.test(text[i]!)) i++
72
+ else if (text.startsWith("//", i)) {
73
+ const n = text.indexOf("\n", i)
74
+ i = n < 0 ? text.length : n
75
+ } else if (text.startsWith("/*", i)) {
76
+ const n = text.indexOf("*/", i + 2)
77
+ if (n < 0) fail("unclosed comment")
78
+ i = n + 2
79
+ } else return
80
+ }
81
+ }
82
+ const string = (): string => {
83
+ const start = i
84
+ i++
85
+ while (i < text.length && text[i] !== '"') i += text[i] === "\\" ? 2 : 1
86
+ if (i >= text.length) fail("unclosed string")
87
+ i++
88
+ try {
89
+ return JSON.parse(text.slice(start, i)) as string
90
+ } catch {
91
+ return fail("bad string")
92
+ }
93
+ }
94
+ const value = (): { node: Node; value: unknown } => {
95
+ skip()
96
+ const start = i
97
+ const c = text[i]
98
+ if (c === "{") {
99
+ i++
100
+ const members: Member[] = []
101
+ const obj: Json = {}
102
+ for (;;) {
103
+ skip()
104
+ if (text[i] === "}") break
105
+ if (text[i] !== '"') fail("expected a key")
106
+ const keyStart = i
107
+ const key = string()
108
+ skip()
109
+ if (text[i] !== ":") fail("expected ':'")
110
+ i++
111
+ skip()
112
+ const valueStart = i
113
+ const v = value()
114
+ const m: Member = { key, keyStart, valueStart, valueEnd: i, comma: -1, value: v.node }
115
+ members.push(m)
116
+ obj[key] = v.value
117
+ skip()
118
+ if (text[i] === ",") {
119
+ m.comma = i
120
+ i++
121
+ continue
122
+ }
123
+ if (text[i] !== "}") fail("expected ',' or '}'")
124
+ }
125
+ i++
126
+ return { node: { kind: "object", start, end: i, members }, value: obj }
127
+ }
128
+ if (c === "[") {
129
+ i++
130
+ const arr: unknown[] = []
131
+ for (;;) {
132
+ skip()
133
+ if (text[i] === "]") break
134
+ arr.push(value().value)
135
+ skip()
136
+ if (text[i] === ",") {
137
+ i++
138
+ continue
139
+ }
140
+ if (text[i] !== "]") fail("expected ',' or ']'")
141
+ }
142
+ i++
143
+ return { node: { kind: "array", start, end: i }, value: arr }
144
+ }
145
+ if (c === '"') {
146
+ const s = string()
147
+ return { node: { kind: "value", start, end: i }, value: s }
148
+ }
149
+ const lit = /^(?:true|false|null|-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?)/.exec(text.slice(i, i + 400))
150
+ if (!lit) fail("unexpected character")
151
+ i += lit![0].length
152
+ return { node: { kind: "value", start, end: i }, value: JSON.parse(lit![0]) }
153
+ }
154
+ const out = value()
155
+ skip()
156
+ if (i < text.length) fail("text after the value")
157
+ return out
158
+ }
159
+
160
+ const lineStart = (text: string, pos: number) => text.lastIndexOf("\n", pos - 1) + 1
161
+ const indentAt = (text: string, pos: number) => /^[ \t]*/.exec(text.slice(lineStart(text, pos)))![0]
162
+ const startsLine = (text: string, pos: number) => /^[ \t]*$/.test(text.slice(lineStart(text, pos), pos))
163
+ const unitOf = (text: string) => /\n([ \t]+)["/]/.exec(text)?.[1] ?? " "
164
+ const eolOf = (text: string) => (text.includes("\r\n") ? "\r\n" : "\n")
165
+ /** `value` as JSON: on one line inside a one-line object, else spread over lines under `indent`. */
166
+ const render = (text: string, v: unknown, indent: string, oneLine: boolean) =>
167
+ oneLine ? JSON.stringify(v) : JSON.stringify(v, null, unitOf(text)).replace(/\n/g, eolOf(text) + indent)
168
+ /** Where a key appears twice, JSON readers keep the last: that is the one brain reads and changes. */
169
+ const lastIndex = (o: ObjectNode, key: string) => {
170
+ for (let j = o.members.length - 1; j >= 0; j--) if (o.members[j]!.key === key) return j
171
+ return -1
172
+ }
173
+ const lastMember = (o: ObjectNode, key: string) => o.members[lastIndex(o, key)]
174
+ const oneLineObject = (text: string, o: ObjectNode) => !text.slice(o.start, o.end).includes("\n")
175
+
176
+ function insert(text: string, o: ObjectNode, key: string, v: unknown): string {
177
+ const eol = eolOf(text)
178
+ const last = o.members[o.members.length - 1]
179
+ if (!last) {
180
+ const outer = indentAt(text, o.start)
181
+ const inner = outer + unitOf(text)
182
+ const interior = text.slice(o.start + 1, o.end - 1)
183
+ // A file written on one line stays on one line.
184
+ if (!text.trim().includes("\n")) return `${text.slice(0, o.start + 1)}${JSON.stringify(key)}: ${JSON.stringify(v)}${text.slice(o.start + 1)}`
185
+ const member = `${JSON.stringify(key)}: ${render(text, v, inner, false)}`
186
+ // An empty object: `{}` becomes `{\n "key": value\n}`; one holding only comments keeps them after the new member.
187
+ if (/^\s*$/.test(interior)) return `${text.slice(0, o.start + 1)}${eol}${inner}${member}${eol}${outer}${text.slice(o.end - 1)}`
188
+ return `${text.slice(0, o.start + 1)}${eol}${inner}${member}${text.slice(o.start + 1)}`
189
+ }
190
+ const flat = !startsLine(text, last.keyStart) || oneLineObject(text, o)
191
+ const indent = indentAt(text, last.keyStart)
192
+ const member = `${JSON.stringify(key)}: ${render(text, v, indent, flat)}`
193
+ // After a trailing comma the new member keeps that style (`"b": 2,`); otherwise it gets the comma before it. Either
194
+ // way the text added is exactly what remove() takes out again.
195
+ if (last.comma >= 0) return `${text.slice(0, last.comma + 1)}${flat ? " " : eol + indent}${member},${text.slice(last.comma + 1)}`
196
+ return `${text.slice(0, last.valueEnd)},${flat ? " " : eol + indent}${member}${text.slice(last.valueEnd)}`
197
+ }
198
+
199
+ function remove(text: string, o: ObjectNode, at: number): string {
200
+ const m = o.members[at]!
201
+ const prev = o.members[at - 1]
202
+ const next = o.members[at + 1]
203
+ const end = m.comma >= 0 ? m.comma + 1 : m.valueEnd
204
+ if (prev) {
205
+ // The last member with a trailing comma: the comma before it stays as the trailing comma of the one before.
206
+ if (!next && m.comma >= 0) return text.slice(0, prev.comma + 1) + text.slice(end)
207
+ return text.slice(0, prev.comma) + text.slice(m.valueEnd)
208
+ }
209
+ if (next) {
210
+ let j = end
211
+ while (j < next.keyStart && WS.test(text[j]!)) j++
212
+ return text.slice(0, m.keyStart) + text.slice(j)
213
+ }
214
+ const before = text.slice(o.start + 1, m.keyStart)
215
+ const after = text.slice(end, o.end - 1)
216
+ if (/^\s*$/.test(before) && /^\s*$/.test(after)) return text.slice(0, o.start + 1) + text.slice(o.end - 1)
217
+ if (/^\s*$/.test(before)) return text.slice(0, o.start + 1) + text.slice(end)
218
+ return text.slice(0, m.keyStart) + text.slice(end)
219
+ }
220
+
221
+ /** The object at `path` (each step the last member of that name), or undefined. */
222
+ function objectAt(root: ObjectNode, path: string[]): ObjectNode | undefined {
223
+ let o: ObjectNode = root
224
+ for (const p of path) {
225
+ const m = lastMember(o, p)
226
+ if (!m || m.value.kind !== "object") return undefined
227
+ o = m.value
228
+ }
229
+ return o
230
+ }
231
+
232
+ function setPath(text: string, path: string[], v: unknown): string {
233
+ const root = scan(text).node as ObjectNode
234
+ if (v === undefined) {
235
+ const parent = objectAt(root, path.slice(0, -1))
236
+ const at = parent ? lastIndex(parent, path[path.length - 1]!) : -1
237
+ if (!parent || at < 0) return text
238
+ let out = remove(text, parent, at)
239
+ // Objects this left empty (nothing in them, not even a comment) go too, as JSON connectors drop them; never the root.
240
+ for (let d = path.length - 1; d >= 1; d--) {
241
+ const r = scan(out).node as ObjectNode
242
+ const o = objectAt(r, path.slice(0, d))
243
+ const holder = objectAt(r, path.slice(0, d - 1))
244
+ if (!o || !holder || o.members.length || !/^\s*$/.test(out.slice(o.start + 1, o.end - 1))) break
245
+ out = remove(out, holder, lastIndex(holder, path[d - 1]!))
246
+ }
247
+ return out
248
+ }
249
+ let o = root
250
+ for (let d = 0; d < path.length; d++) {
251
+ const m = lastMember(o, path[d]!)
252
+ // What is still missing below this point is added as one nested value.
253
+ const rest = path.slice(d + 1).reduceRight<unknown>((acc, k) => ({ [k]: acc }), v)
254
+ if (d === path.length - 1 || !m || m.value.kind !== "object") {
255
+ if (!m) return insert(text, o, path[d]!, rest)
256
+ const flat = oneLineObject(text, o) || !startsLine(text, m.keyStart)
257
+ return text.slice(0, m.valueStart) + render(text, rest, indentAt(text, m.keyStart), flat) + text.slice(m.valueEnd)
258
+ }
259
+ o = m.value
260
+ }
261
+ return text
262
+ }
263
+
264
+ const parse = (text: string): unknown => (text.trim() ? scan(text).value : {})
265
+ const same = (a: unknown, b: unknown) => JSON.stringify(a) === JSON.stringify(b)
266
+ /**
267
+ * What `parse` gives after `key` is set to `v`; undefined removes it, and with `dropEmpty` also the objects that leaves
268
+ * empty (setPath keeps one that still holds a comment).
269
+ */
270
+ function expected(obj: Json, path: string[], v: unknown, dropEmpty = true): Json {
271
+ const o = structuredClone(obj)
272
+ let cur: any = o
273
+ if (v === undefined) {
274
+ const chain: any[] = [o]
275
+ for (const p of path.slice(0, -1)) {
276
+ cur = cur?.[p]
277
+ chain.push(cur)
278
+ }
279
+ if (cur && typeof cur === "object") delete cur[path[path.length - 1]!]
280
+ for (let d = path.length - 1; dropEmpty && d >= 1; d--) {
281
+ const c = chain[d]
282
+ if (c && typeof c === "object" && !Array.isArray(c) && !Object.keys(c).length) delete chain[d - 1][path[d - 1]!]
283
+ else break
284
+ }
285
+ return o
286
+ }
287
+ for (const p of path.slice(0, -1)) {
288
+ if (!cur[p] || typeof cur[p] !== "object" || Array.isArray(cur[p])) cur[p] = {}
289
+ cur = cur[p]
290
+ }
291
+ cur[path[path.length - 1]!] = v
292
+ return o
293
+ }
294
+
295
+ /**
296
+ * A codec for a JSON file that may hold comments and trailing commas, edited in place: keys are paths with "/"
297
+ * (provider/brain-gateway). `trial` lists keys with a value like the one that will be written: check() refuses a file
298
+ * where setting or removing them would not give exactly the expected result.
299
+ */
300
+ export function jsoncInPlace(trial: Record<string, unknown> = {}, extraCheck?: (obj: Json) => string | undefined): Codec {
301
+ const codec: Codec = {
302
+ check(text) {
303
+ if (!text.trim()) return undefined
304
+ let obj: unknown
305
+ try {
306
+ obj = scan(text).value
307
+ } catch (e) {
308
+ return `it is not valid JSON with comments (${(e as Error).message}); left as it is`
309
+ }
310
+ if (!obj || typeof obj !== "object" || Array.isArray(obj)) return "it is not a JSON object; left as it is"
311
+ const more = extraCheck?.(obj as Json)
312
+ if (more) return more
313
+ // A trial edit of every key brain owns, and its undoing, must land exactly where expected.
314
+ for (const [key, v] of Object.entries(trial)) {
315
+ const path = key.split("/")
316
+ const shown = key.replace(/\//g, ".")
317
+ try {
318
+ const set = setPath(text, path, v)
319
+ const after = parse(set) as Json
320
+ if (!same(after, expected(obj as Json, path, v))) return `brain could not place ${shown} in it safely; left as it is`
321
+ const was = getAt(obj as Json, key)
322
+ const back = parse(setPath(set, path, was))
323
+ const ok = was === undefined ? same(back, expected(after, path, undefined)) || same(back, expected(after, path, undefined, false)) : same(back, obj)
324
+ if (!ok) return `brain could not undo a change to ${shown} in it safely; left as it is`
325
+ } catch {
326
+ return `brain could not edit ${shown} in it safely; left as it is`
327
+ }
328
+ }
329
+ return undefined
330
+ },
331
+ get(text, key) {
332
+ return getAt(parse(text) as Json, key)
333
+ },
334
+ set(text, key, value) {
335
+ const bom = text.startsWith("\ufeff") ? "\ufeff" : ""
336
+ const body = bom ? text.slice(1) : text
337
+ if (!body.trim()) {
338
+ if (value === undefined) return text
339
+ return `${bom}${setPath("{\n}", key.split("/"), value)}\n`
340
+ }
341
+ return bom + setPath(body, key.split("/"), value)
342
+ },
343
+ }
344
+ return codec
345
+ }
346
+
347
+ // ---- Kilo -------------------------------------------------------------------------------------------------------------
348
+
349
+ /** The global config files, lowest rank first: a later one wins where two set the same key. */
350
+ export const KILO_CONFIG_FILES = ["config.json", "kilo.json", "kilo.jsonc", "opencode.json", "opencode.jsonc"]
351
+ const PROVIDER = "brain-gateway"
352
+
353
+ /** Kilo's global config folder, as Kilo resolves it; $XDG_CONFIG_HOME describes the person's real home, so it counts only there. */
354
+ export function kiloConfigDir(home: string, env: Env): string {
355
+ const own = !!env.HOME && resolve(env.HOME) === resolve(home)
356
+ const x = env.XDG_CONFIG_HOME
357
+ return own && x && isAbsolute(x) ? join(x, "kilo") : join(home, ".config", "kilo")
358
+ }
359
+
360
+ function holdsProvider(path: string): boolean {
361
+ try {
362
+ return getAt(parse(readFileSync(path, "utf8")) as Json, `provider/${PROVIDER}`) !== undefined
363
+ } catch {
364
+ return false
365
+ }
366
+ }
367
+
368
+ /**
369
+ * The file brain edits: the one that already holds brain's provider (connected earlier), else the global file with the
370
+ * highest rank that exists (its "model" is the one Kilo uses), else a new kilo.jsonc (the file Kilo itself creates).
371
+ */
372
+ export function kiloConfigFile(home: string, env: Env): string {
373
+ const dir = kiloConfigDir(home, env)
374
+ const present = KILO_CONFIG_FILES.map((n) => join(dir, n)).filter((p) => existsSync(p))
375
+ return present.find(holdsProvider) ?? present[present.length - 1] ?? join(dir, "kilo.jsonc")
376
+ }
377
+
378
+ /**
379
+ * A config that would keep brain's provider from loading: disabled by name, or left out of an allow-list. Asked only
380
+ * before brain's provider is there (a disconnect must still be able to take it out).
381
+ */
382
+ function providerAllowed(obj: Json): string | undefined {
383
+ if (getAt(obj, `provider/${PROVIDER}`) !== undefined) return undefined
384
+ const off = obj.disabled_providers
385
+ if (Array.isArray(off) && off.includes(PROVIDER)) return `its disabled_providers turns off "${PROVIDER}"; left as it is`
386
+ const on = obj.enabled_providers
387
+ if (Array.isArray(on) && !on.includes(PROVIDER)) return `its enabled_providers allows only the providers it names (not "${PROVIDER}"); add "${PROVIDER}" there to connect`
388
+ return undefined
389
+ }
390
+
391
+ const kiloCodec = jsoncInPlace(
392
+ {
393
+ [`provider/${PROVIDER}`]: { npm: "@ai-sdk/openai-compatible", name: "100xprompt gateway", options: { baseURL: "https://x/v1", apiKey: "{file:/x}", headers: { [EDIT_HEADER]: "on" } }, models: { m: { name: "m" } } },
394
+ model: `${PROVIDER}/m`,
395
+ },
396
+ providerAllowed,
397
+ )
398
+
399
+ /** One Kilo connector: Kilo CLI and the Kilo Code extension read the same file, so both are built here. */
400
+ export function kiloConnector(id: string, label: string, notes: string[]): Connector {
401
+ return {
402
+ id,
403
+ label,
404
+ file: kiloConfigFile,
405
+ // Kilo's data folder (sessions, auth.json): made by the CLI and by the extension's bundled CLI on their first run.
406
+ probe: [".local/share/kilo"],
407
+ authHeader: "authorization",
408
+ edits: (home, s, env) => [
409
+ {
410
+ path: kiloConfigFile(home, env),
411
+ format: "custom",
412
+ codec: kiloCodec,
413
+ values: {
414
+ [`provider/${PROVIDER}`]: {
415
+ npm: "@ai-sdk/openai-compatible",
416
+ name: "100xprompt gateway",
417
+ options: { baseURL: `${routeUrl(s)}/v1`, apiKey: `{file:${keyPath(home)}}`, ...(s.contextEdit ? { headers: { [EDIT_HEADER]: "on" } } : {}) },
418
+ models: { [s.model]: { name: modelName(s.model) } },
419
+ },
420
+ model: `${PROVIDER}/${s.model}`,
421
+ },
422
+ },
423
+ ],
424
+ notes,
425
+ }
426
+ }
427
+
428
+ export const KILO_SHARED_NOTE =
429
+ "Kilo CLI and the Kilo Code extension (7.x, which runs the same CLI) read this one file: connecting either points both at the gateway, and disconnect puts back what was there before."
430
+ export const KILO_KEY_NOTE =
431
+ "Kilo reads the key from the brain's key file each time it starts ({file:...}); nothing is copied. Without that file (after brain gateway logout) Kilo will not start until you sign in again or disconnect."
432
+ const KILO_PROJECT_NOTE = "A project's own kilo.json(c), opencode.json(c) or .kilo/ config outranks this file there; sessions already open keep the model they started with."
433
+
434
+ export const connectors: Connector[] = [kiloConnector("kilo-cli", "Kilo CLI", [KILO_KEY_NOTE, KILO_SHARED_NOTE, KILO_PROJECT_NOTE])]
@@ -0,0 +1,32 @@
1
+ // Gateway connector for Kilo Code, the VS Code extension (kilocode.kilo-code; also installable in Cursor and Windsurf).
2
+ // Checked against Kilo Code 7.8.28: its source at Kilo-Org/kilocode main (packages/kilo-vscode/package.json version
3
+ // 7.8.28) and the Kilo CLI binary of the same version that the extension bundles (~/.cache/brain-wire/kilo-code, the
4
+ // copy the wire harness drives as the extension's <extension>/bin/kilo).
5
+ //
6
+ // Since 7.x the extension is a client of the Kilo CLI: it starts its bundled `bin/kilo serve` and the agent runs there
7
+ // (https://kilo.ai/docs/contributing/architecture/vscode-extension: "Agent runtime behavior belongs in CLI config so TUI,
8
+ // Console, VS Code, and JetBrains can share it"). Providers, models and keys therefore live in the Kilo CLI's global
9
+ // config, ~/.config/kilo/kilo.jsonc and its siblings; the extension's own "Custom provider" dialog writes provider.<id>
10
+ // there too (kilo-vscode src/provider-actions.ts saveCustomProvider -> client.global.config.update). Its VS Code settings
11
+ // are only kilo-code.new.* (UI, autocomplete, and a fallback default model); 7.8.28 has no autoImportSettingsPath. A new
12
+ // session starts on the config's "model" (provider-actions.ts computeDefaultSelection: config.model, then the VS Code
13
+ // setting kilo-code.new.model.providerID/modelID, then Kilo's own auto model; KiloProvider.ts fetchAndSendProviders).
14
+ //
15
+ // So this connector edits exactly what the Kilo CLI connector edits (kilo-cli.ts: provider.brain-gateway reading the key
16
+ // from brain's key file with {file:...}, and model), in the same file: nothing goes into VS Code's settings, state or
17
+ // keychain, and disconnect gives back the file's exact bytes. Connecting one of the two also points the other at the
18
+ // gateway; connecting both is fine (each records what it found and puts that back).
19
+ //
20
+ // Older Kilo Code releases (the Roo Code-based extension, before 7) keep providers in VS Code's own storage and keys in its
21
+ // keychain, like Roo Code (see roo.ts for why brain does not write those); they do not create ~/.config/kilo, so connect
22
+ // refuses there ("has not been run on this machine").
23
+ import type { Connector } from "../common.ts"
24
+ import { KILO_KEY_NOTE, KILO_SHARED_NOTE, kiloConnector } from "./kilo-cli.ts"
25
+
26
+ export const connectors: Connector[] = [
27
+ kiloConnector("kilo-code", "Kilo Code", [
28
+ "Kilo Code 7 runs the Kilo CLI it bundles, which reads this file: nothing goes into VS Code's settings or keychain. New chats start on the gateway's model (this file's model comes before the extension's default-model setting); open chats keep theirs.",
29
+ KILO_KEY_NOTE,
30
+ KILO_SHARED_NOTE,
31
+ ]),
32
+ ]