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