@link-assistant/agent 0.0.8
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/EXAMPLES.md +383 -0
- package/LICENSE +24 -0
- package/MODELS.md +95 -0
- package/README.md +388 -0
- package/TOOLS.md +134 -0
- package/package.json +89 -0
- package/src/agent/agent.ts +150 -0
- package/src/agent/generate.txt +75 -0
- package/src/auth/index.ts +64 -0
- package/src/bun/index.ts +96 -0
- package/src/bus/global.ts +10 -0
- package/src/bus/index.ts +119 -0
- package/src/cli/bootstrap.js +41 -0
- package/src/cli/bootstrap.ts +17 -0
- package/src/cli/cmd/agent.ts +165 -0
- package/src/cli/cmd/cmd.ts +5 -0
- package/src/cli/cmd/export.ts +88 -0
- package/src/cli/cmd/mcp.ts +80 -0
- package/src/cli/cmd/models.ts +58 -0
- package/src/cli/cmd/run.ts +359 -0
- package/src/cli/cmd/stats.ts +276 -0
- package/src/cli/error.ts +27 -0
- package/src/command/index.ts +73 -0
- package/src/command/template/initialize.txt +10 -0
- package/src/config/config.ts +705 -0
- package/src/config/markdown.ts +41 -0
- package/src/file/ripgrep.ts +391 -0
- package/src/file/time.ts +38 -0
- package/src/file/watcher.ts +75 -0
- package/src/file.ts +6 -0
- package/src/flag/flag.ts +19 -0
- package/src/format/formatter.ts +248 -0
- package/src/format/index.ts +137 -0
- package/src/global/index.ts +52 -0
- package/src/id/id.ts +72 -0
- package/src/index.js +371 -0
- package/src/mcp/index.ts +289 -0
- package/src/patch/index.ts +622 -0
- package/src/project/bootstrap.ts +22 -0
- package/src/project/instance.ts +67 -0
- package/src/project/project.ts +105 -0
- package/src/project/state.ts +65 -0
- package/src/provider/models-macro.ts +11 -0
- package/src/provider/models.ts +98 -0
- package/src/provider/opencode.js +47 -0
- package/src/provider/provider.ts +636 -0
- package/src/provider/transform.ts +241 -0
- package/src/server/project.ts +48 -0
- package/src/server/server.ts +249 -0
- package/src/session/agent.js +204 -0
- package/src/session/compaction.ts +249 -0
- package/src/session/index.ts +380 -0
- package/src/session/message-v2.ts +758 -0
- package/src/session/message.ts +189 -0
- package/src/session/processor.ts +356 -0
- package/src/session/prompt/anthropic-20250930.txt +166 -0
- package/src/session/prompt/anthropic.txt +105 -0
- package/src/session/prompt/anthropic_spoof.txt +1 -0
- package/src/session/prompt/beast.txt +147 -0
- package/src/session/prompt/build-switch.txt +5 -0
- package/src/session/prompt/codex.txt +318 -0
- package/src/session/prompt/copilot-gpt-5.txt +143 -0
- package/src/session/prompt/gemini.txt +155 -0
- package/src/session/prompt/grok-code.txt +1 -0
- package/src/session/prompt/plan.txt +8 -0
- package/src/session/prompt/polaris.txt +107 -0
- package/src/session/prompt/qwen.txt +109 -0
- package/src/session/prompt/summarize-turn.txt +5 -0
- package/src/session/prompt/summarize.txt +10 -0
- package/src/session/prompt/title.txt +25 -0
- package/src/session/prompt.ts +1390 -0
- package/src/session/retry.ts +53 -0
- package/src/session/revert.ts +108 -0
- package/src/session/status.ts +75 -0
- package/src/session/summary.ts +179 -0
- package/src/session/system.ts +138 -0
- package/src/session/todo.ts +36 -0
- package/src/snapshot/index.ts +197 -0
- package/src/storage/storage.ts +226 -0
- package/src/tool/bash.ts +193 -0
- package/src/tool/bash.txt +121 -0
- package/src/tool/batch.ts +173 -0
- package/src/tool/batch.txt +28 -0
- package/src/tool/codesearch.ts +123 -0
- package/src/tool/codesearch.txt +12 -0
- package/src/tool/edit.ts +604 -0
- package/src/tool/edit.txt +10 -0
- package/src/tool/glob.ts +65 -0
- package/src/tool/glob.txt +6 -0
- package/src/tool/grep.ts +116 -0
- package/src/tool/grep.txt +8 -0
- package/src/tool/invalid.ts +17 -0
- package/src/tool/ls.ts +110 -0
- package/src/tool/ls.txt +1 -0
- package/src/tool/multiedit.ts +46 -0
- package/src/tool/multiedit.txt +41 -0
- package/src/tool/patch.ts +188 -0
- package/src/tool/patch.txt +1 -0
- package/src/tool/read.ts +201 -0
- package/src/tool/read.txt +12 -0
- package/src/tool/registry.ts +87 -0
- package/src/tool/task.ts +126 -0
- package/src/tool/task.txt +60 -0
- package/src/tool/todo.ts +39 -0
- package/src/tool/todoread.txt +14 -0
- package/src/tool/todowrite.txt +167 -0
- package/src/tool/tool.ts +66 -0
- package/src/tool/webfetch.ts +171 -0
- package/src/tool/webfetch.txt +14 -0
- package/src/tool/websearch.ts +133 -0
- package/src/tool/websearch.txt +11 -0
- package/src/tool/write.ts +33 -0
- package/src/tool/write.txt +8 -0
- package/src/util/binary.ts +41 -0
- package/src/util/context.ts +25 -0
- package/src/util/defer.ts +12 -0
- package/src/util/error.ts +54 -0
- package/src/util/eventloop.ts +20 -0
- package/src/util/filesystem.ts +69 -0
- package/src/util/fn.ts +11 -0
- package/src/util/iife.ts +3 -0
- package/src/util/keybind.ts +79 -0
- package/src/util/lazy.ts +11 -0
- package/src/util/locale.ts +39 -0
- package/src/util/lock.ts +98 -0
- package/src/util/log.ts +177 -0
- package/src/util/queue.ts +19 -0
- package/src/util/rpc.ts +42 -0
- package/src/util/scrap.ts +10 -0
- package/src/util/signal.ts +12 -0
- package/src/util/timeout.ts +14 -0
- package/src/util/token.ts +7 -0
- package/src/util/wildcard.ts +54 -0
|
@@ -0,0 +1,705 @@
|
|
|
1
|
+
import { Log } from "../util/log"
|
|
2
|
+
import path from "path"
|
|
3
|
+
import os from "os"
|
|
4
|
+
import z from "zod"
|
|
5
|
+
import { Filesystem } from "../util/filesystem"
|
|
6
|
+
import { ModelsDev } from "../provider/models"
|
|
7
|
+
import { mergeDeep, pipe } from "remeda"
|
|
8
|
+
import { Global } from "../global"
|
|
9
|
+
import fs from "fs/promises"
|
|
10
|
+
import { lazy } from "../util/lazy"
|
|
11
|
+
import { NamedError } from "../util/error"
|
|
12
|
+
import { Flag } from "../flag/flag"
|
|
13
|
+
import { Auth } from "../auth"
|
|
14
|
+
import { type ParseError as JsoncParseError, parse as parseJsonc, printParseErrorCode } from "jsonc-parser"
|
|
15
|
+
import { Instance } from "../project/instance"
|
|
16
|
+
import { ConfigMarkdown } from "./markdown"
|
|
17
|
+
|
|
18
|
+
export namespace Config {
|
|
19
|
+
const log = Log.create({ service: "config" })
|
|
20
|
+
|
|
21
|
+
export const state = Instance.state(async () => {
|
|
22
|
+
const auth = await Auth.all()
|
|
23
|
+
let result = await global()
|
|
24
|
+
|
|
25
|
+
// Override with custom config if provided
|
|
26
|
+
if (Flag.OPENCODE_CONFIG) {
|
|
27
|
+
result = mergeDeep(result, await loadFile(Flag.OPENCODE_CONFIG))
|
|
28
|
+
log.debug("loaded custom config", { path: Flag.OPENCODE_CONFIG })
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
for (const file of ["opencode.jsonc", "opencode.json"]) {
|
|
32
|
+
const found = await Filesystem.findUp(file, Instance.directory, Instance.worktree)
|
|
33
|
+
for (const resolved of found.toReversed()) {
|
|
34
|
+
result = mergeDeep(result, await loadFile(resolved))
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (Flag.OPENCODE_CONFIG_CONTENT) {
|
|
39
|
+
result = mergeDeep(result, JSON.parse(Flag.OPENCODE_CONFIG_CONTENT))
|
|
40
|
+
log.debug("loaded custom config from OPENCODE_CONFIG_CONTENT")
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
for (const [key, value] of Object.entries(auth)) {
|
|
44
|
+
if (value.type === "wellknown") {
|
|
45
|
+
process.env[value.key] = value.token
|
|
46
|
+
const wellknown = (await fetch(`${key}/.well-known/opencode`).then((x) => x.json())) as any
|
|
47
|
+
result = mergeDeep(result, await load(JSON.stringify(wellknown.config ?? {}), process.cwd()))
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
result.agent = result.agent || {}
|
|
52
|
+
result.mode = result.mode || {}
|
|
53
|
+
|
|
54
|
+
const directories = [
|
|
55
|
+
Global.Path.config,
|
|
56
|
+
...(await Array.fromAsync(
|
|
57
|
+
Filesystem.up({
|
|
58
|
+
targets: [".opencode"],
|
|
59
|
+
start: Instance.directory,
|
|
60
|
+
stop: Instance.worktree,
|
|
61
|
+
}),
|
|
62
|
+
)),
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
if (Flag.OPENCODE_CONFIG_DIR) {
|
|
66
|
+
directories.push(Flag.OPENCODE_CONFIG_DIR)
|
|
67
|
+
log.debug("loading config from OPENCODE_CONFIG_DIR", { path: Flag.OPENCODE_CONFIG_DIR })
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const promises: Promise<void>[] = []
|
|
71
|
+
for (const dir of directories) {
|
|
72
|
+
await assertValid(dir)
|
|
73
|
+
|
|
74
|
+
if (dir.endsWith(".opencode") || dir === Flag.OPENCODE_CONFIG_DIR) {
|
|
75
|
+
for (const file of ["opencode.jsonc", "opencode.json"]) {
|
|
76
|
+
log.debug(`loading config from ${path.join(dir, file)}`)
|
|
77
|
+
result = mergeDeep(result, await loadFile(path.join(dir, file)))
|
|
78
|
+
// to satisy the type checker
|
|
79
|
+
result.agent ??= {}
|
|
80
|
+
result.mode ??= {}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
promises.push(installDependencies(dir))
|
|
85
|
+
result.command = mergeDeep(result.command ?? {}, await loadCommand(dir))
|
|
86
|
+
result.agent = mergeDeep(result.agent, await loadAgent(dir))
|
|
87
|
+
result.agent = mergeDeep(result.agent, await loadMode(dir))
|
|
88
|
+
}
|
|
89
|
+
await Promise.allSettled(promises)
|
|
90
|
+
|
|
91
|
+
// Migrate deprecated mode field to agent field
|
|
92
|
+
for (const [name, mode] of Object.entries(result.mode)) {
|
|
93
|
+
result.agent = mergeDeep(result.agent ?? {}, {
|
|
94
|
+
[name]: {
|
|
95
|
+
...mode,
|
|
96
|
+
mode: "primary" as const,
|
|
97
|
+
},
|
|
98
|
+
})
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Permission system removed
|
|
102
|
+
// Share/autoshare removed - no sharing support
|
|
103
|
+
|
|
104
|
+
if (!result.username) result.username = os.userInfo().username
|
|
105
|
+
|
|
106
|
+
if (!result.keybinds) result.keybinds = Info.shape.keybinds.parse({})
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
config: result,
|
|
110
|
+
directories,
|
|
111
|
+
}
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
const INVALID_DIRS = new Bun.Glob(`{${["agents", "commands", "tools"].join(",")}}/`)
|
|
115
|
+
async function assertValid(dir: string) {
|
|
116
|
+
const invalid = await Array.fromAsync(
|
|
117
|
+
INVALID_DIRS.scan({
|
|
118
|
+
onlyFiles: false,
|
|
119
|
+
cwd: dir,
|
|
120
|
+
}),
|
|
121
|
+
)
|
|
122
|
+
for (const item of invalid) {
|
|
123
|
+
throw new ConfigDirectoryTypoError({
|
|
124
|
+
path: dir,
|
|
125
|
+
dir: item,
|
|
126
|
+
suggestion: item.substring(0, item.length - 1),
|
|
127
|
+
})
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function installDependencies(dir: string) {
|
|
132
|
+
// Dependency installation removed - no plugin support
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const COMMAND_GLOB = new Bun.Glob("command/**/*.md")
|
|
136
|
+
async function loadCommand(dir: string) {
|
|
137
|
+
const result: Record<string, Command> = {}
|
|
138
|
+
for await (const item of COMMAND_GLOB.scan({
|
|
139
|
+
absolute: true,
|
|
140
|
+
followSymlinks: true,
|
|
141
|
+
dot: true,
|
|
142
|
+
cwd: dir,
|
|
143
|
+
})) {
|
|
144
|
+
const md = await ConfigMarkdown.parse(item)
|
|
145
|
+
if (!md.data) continue
|
|
146
|
+
|
|
147
|
+
const name = (() => {
|
|
148
|
+
const patterns = ["/.opencode/command/", "/command/"]
|
|
149
|
+
const pattern = patterns.find((p) => item.includes(p))
|
|
150
|
+
|
|
151
|
+
if (pattern) {
|
|
152
|
+
const index = item.indexOf(pattern)
|
|
153
|
+
return item.slice(index + pattern.length, -3)
|
|
154
|
+
}
|
|
155
|
+
return path.basename(item, ".md")
|
|
156
|
+
})()
|
|
157
|
+
|
|
158
|
+
const config = {
|
|
159
|
+
name,
|
|
160
|
+
...md.data,
|
|
161
|
+
template: md.content.trim(),
|
|
162
|
+
}
|
|
163
|
+
const parsed = Command.safeParse(config)
|
|
164
|
+
if (parsed.success) {
|
|
165
|
+
result[config.name] = parsed.data
|
|
166
|
+
continue
|
|
167
|
+
}
|
|
168
|
+
throw new InvalidError({ path: item }, { cause: parsed.error })
|
|
169
|
+
}
|
|
170
|
+
return result
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const AGENT_GLOB = new Bun.Glob("agent/**/*.md")
|
|
174
|
+
async function loadAgent(dir: string) {
|
|
175
|
+
const result: Record<string, Agent> = {}
|
|
176
|
+
|
|
177
|
+
for await (const item of AGENT_GLOB.scan({
|
|
178
|
+
absolute: true,
|
|
179
|
+
followSymlinks: true,
|
|
180
|
+
dot: true,
|
|
181
|
+
cwd: dir,
|
|
182
|
+
})) {
|
|
183
|
+
const md = await ConfigMarkdown.parse(item)
|
|
184
|
+
if (!md.data) continue
|
|
185
|
+
|
|
186
|
+
// Extract relative path from agent folder for nested agents
|
|
187
|
+
let agentName = path.basename(item, ".md")
|
|
188
|
+
const agentFolderPath = item.includes("/.opencode/agent/")
|
|
189
|
+
? item.split("/.opencode/agent/")[1]
|
|
190
|
+
: item.includes("/agent/")
|
|
191
|
+
? item.split("/agent/")[1]
|
|
192
|
+
: agentName + ".md"
|
|
193
|
+
|
|
194
|
+
// If agent is in a subfolder, include folder path in name
|
|
195
|
+
if (agentFolderPath.includes("/")) {
|
|
196
|
+
const relativePath = agentFolderPath.replace(".md", "")
|
|
197
|
+
const pathParts = relativePath.split("/")
|
|
198
|
+
agentName = pathParts.slice(0, -1).join("/") + "/" + pathParts[pathParts.length - 1]
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const config = {
|
|
202
|
+
name: agentName,
|
|
203
|
+
...md.data,
|
|
204
|
+
prompt: md.content.trim(),
|
|
205
|
+
}
|
|
206
|
+
const parsed = Agent.safeParse(config)
|
|
207
|
+
if (parsed.success) {
|
|
208
|
+
result[config.name] = parsed.data
|
|
209
|
+
continue
|
|
210
|
+
}
|
|
211
|
+
throw new InvalidError({ path: item }, { cause: parsed.error })
|
|
212
|
+
}
|
|
213
|
+
return result
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const MODE_GLOB = new Bun.Glob("mode/*.md")
|
|
217
|
+
async function loadMode(dir: string) {
|
|
218
|
+
const result: Record<string, Agent> = {}
|
|
219
|
+
for await (const item of MODE_GLOB.scan({
|
|
220
|
+
absolute: true,
|
|
221
|
+
followSymlinks: true,
|
|
222
|
+
dot: true,
|
|
223
|
+
cwd: dir,
|
|
224
|
+
})) {
|
|
225
|
+
const md = await ConfigMarkdown.parse(item)
|
|
226
|
+
if (!md.data) continue
|
|
227
|
+
|
|
228
|
+
const config = {
|
|
229
|
+
name: path.basename(item, ".md"),
|
|
230
|
+
...md.data,
|
|
231
|
+
prompt: md.content.trim(),
|
|
232
|
+
}
|
|
233
|
+
const parsed = Agent.safeParse(config)
|
|
234
|
+
if (parsed.success) {
|
|
235
|
+
result[config.name] = {
|
|
236
|
+
...parsed.data,
|
|
237
|
+
mode: "primary" as const,
|
|
238
|
+
}
|
|
239
|
+
continue
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return result
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export const McpLocal = z
|
|
246
|
+
.object({
|
|
247
|
+
type: z.literal("local").describe("Type of MCP server connection"),
|
|
248
|
+
command: z.string().array().describe("Command and arguments to run the MCP server"),
|
|
249
|
+
environment: z
|
|
250
|
+
.record(z.string(), z.string())
|
|
251
|
+
.optional()
|
|
252
|
+
.describe("Environment variables to set when running the MCP server"),
|
|
253
|
+
enabled: z.boolean().optional().describe("Enable or disable the MCP server on startup"),
|
|
254
|
+
timeout: z
|
|
255
|
+
.number()
|
|
256
|
+
.int()
|
|
257
|
+
.positive()
|
|
258
|
+
.optional()
|
|
259
|
+
.describe(
|
|
260
|
+
"Timeout in ms for fetching tools from the MCP server. Defaults to 5000 (5 seconds) if not specified.",
|
|
261
|
+
),
|
|
262
|
+
})
|
|
263
|
+
.strict()
|
|
264
|
+
.meta({
|
|
265
|
+
ref: "McpLocalConfig",
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
export const McpRemote = z
|
|
269
|
+
.object({
|
|
270
|
+
type: z.literal("remote").describe("Type of MCP server connection"),
|
|
271
|
+
url: z.string().describe("URL of the remote MCP server"),
|
|
272
|
+
enabled: z.boolean().optional().describe("Enable or disable the MCP server on startup"),
|
|
273
|
+
headers: z.record(z.string(), z.string()).optional().describe("Headers to send with the request"),
|
|
274
|
+
timeout: z
|
|
275
|
+
.number()
|
|
276
|
+
.int()
|
|
277
|
+
.positive()
|
|
278
|
+
.optional()
|
|
279
|
+
.describe(
|
|
280
|
+
"Timeout in ms for fetching tools from the MCP server. Defaults to 5000 (5 seconds) if not specified.",
|
|
281
|
+
),
|
|
282
|
+
})
|
|
283
|
+
.strict()
|
|
284
|
+
.meta({
|
|
285
|
+
ref: "McpRemoteConfig",
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
export const Mcp = z.discriminatedUnion("type", [McpLocal, McpRemote])
|
|
289
|
+
export type Mcp = z.infer<typeof Mcp>
|
|
290
|
+
|
|
291
|
+
export const Permission = z.enum(["ask", "allow", "deny"])
|
|
292
|
+
export type Permission = z.infer<typeof Permission>
|
|
293
|
+
|
|
294
|
+
export const Command = z.object({
|
|
295
|
+
template: z.string(),
|
|
296
|
+
description: z.string().optional(),
|
|
297
|
+
agent: z.string().optional(),
|
|
298
|
+
model: z.string().optional(),
|
|
299
|
+
subtask: z.boolean().optional(),
|
|
300
|
+
})
|
|
301
|
+
export type Command = z.infer<typeof Command>
|
|
302
|
+
|
|
303
|
+
export const Agent = z
|
|
304
|
+
.object({
|
|
305
|
+
model: z.string().optional(),
|
|
306
|
+
temperature: z.number().optional(),
|
|
307
|
+
top_p: z.number().optional(),
|
|
308
|
+
prompt: z.string().optional(),
|
|
309
|
+
tools: z.record(z.string(), z.boolean()).optional(),
|
|
310
|
+
disable: z.boolean().optional(),
|
|
311
|
+
description: z.string().optional().describe("Description of when to use the agent"),
|
|
312
|
+
mode: z.enum(["subagent", "primary", "all"]).optional(),
|
|
313
|
+
color: z
|
|
314
|
+
.string()
|
|
315
|
+
.regex(/^#[0-9a-fA-F]{6}$/, "Invalid hex color format")
|
|
316
|
+
.optional()
|
|
317
|
+
.describe("Hex color code for the agent (e.g., #FF5733)"),
|
|
318
|
+
permission: z
|
|
319
|
+
.object({
|
|
320
|
+
edit: Permission.optional(),
|
|
321
|
+
bash: z.union([Permission, z.record(z.string(), Permission)]).optional(),
|
|
322
|
+
webfetch: Permission.optional(),
|
|
323
|
+
doom_loop: Permission.optional(),
|
|
324
|
+
external_directory: Permission.optional(),
|
|
325
|
+
})
|
|
326
|
+
.optional(),
|
|
327
|
+
})
|
|
328
|
+
.catchall(z.any())
|
|
329
|
+
.meta({
|
|
330
|
+
ref: "AgentConfig",
|
|
331
|
+
})
|
|
332
|
+
export type Agent = z.infer<typeof Agent>
|
|
333
|
+
|
|
334
|
+
export const Keybinds = z
|
|
335
|
+
.object({
|
|
336
|
+
leader: z.string().optional().default("ctrl+x").describe("Leader key for keybind combinations"),
|
|
337
|
+
app_exit: z.string().optional().default("ctrl+c,ctrl+d,<leader>q").describe("Exit the application"),
|
|
338
|
+
editor_open: z.string().optional().default("<leader>e").describe("Open external editor"),
|
|
339
|
+
theme_list: z.string().optional().default("<leader>t").describe("List available themes"),
|
|
340
|
+
sidebar_toggle: z.string().optional().default("<leader>b").describe("Toggle sidebar"),
|
|
341
|
+
status_view: z.string().optional().default("<leader>s").describe("View status"),
|
|
342
|
+
session_export: z.string().optional().default("<leader>x").describe("Export session to editor"),
|
|
343
|
+
session_new: z.string().optional().default("<leader>n").describe("Create a new session"),
|
|
344
|
+
session_list: z.string().optional().default("<leader>l").describe("List all sessions"),
|
|
345
|
+
session_timeline: z.string().optional().default("<leader>g").describe("Show session timeline"),
|
|
346
|
+
session_interrupt: z.string().optional().default("escape").describe("Interrupt current session"),
|
|
347
|
+
session_compact: z.string().optional().default("<leader>c").describe("Compact the session"),
|
|
348
|
+
messages_page_up: z.string().optional().default("pageup").describe("Scroll messages up by one page"),
|
|
349
|
+
messages_page_down: z.string().optional().default("pagedown").describe("Scroll messages down by one page"),
|
|
350
|
+
messages_half_page_up: z.string().optional().default("ctrl+alt+u").describe("Scroll messages up by half page"),
|
|
351
|
+
messages_half_page_down: z
|
|
352
|
+
.string()
|
|
353
|
+
.optional()
|
|
354
|
+
.default("ctrl+alt+d")
|
|
355
|
+
.describe("Scroll messages down by half page"),
|
|
356
|
+
messages_first: z.string().optional().default("ctrl+g,home").describe("Navigate to first message"),
|
|
357
|
+
messages_last: z.string().optional().default("ctrl+alt+g,end").describe("Navigate to last message"),
|
|
358
|
+
messages_copy: z.string().optional().default("<leader>y").describe("Copy message"),
|
|
359
|
+
messages_undo: z.string().optional().default("<leader>u").describe("Undo message"),
|
|
360
|
+
messages_redo: z.string().optional().default("<leader>r").describe("Redo message"),
|
|
361
|
+
messages_toggle_conceal: z
|
|
362
|
+
.string()
|
|
363
|
+
.optional()
|
|
364
|
+
.default("<leader>h")
|
|
365
|
+
.describe("Toggle code block concealment in messages"),
|
|
366
|
+
model_list: z.string().optional().default("<leader>m").describe("List available models"),
|
|
367
|
+
model_cycle_recent: z.string().optional().default("f2").describe("Next recently used model"),
|
|
368
|
+
model_cycle_recent_reverse: z.string().optional().default("shift+f2").describe("Previous recently used model"),
|
|
369
|
+
command_list: z.string().optional().default("ctrl+p").describe("List available commands"),
|
|
370
|
+
agent_list: z.string().optional().default("<leader>a").describe("List agents"),
|
|
371
|
+
agent_cycle: z.string().optional().default("tab").describe("Next agent"),
|
|
372
|
+
agent_cycle_reverse: z.string().optional().default("shift+tab").describe("Previous agent"),
|
|
373
|
+
input_clear: z.string().optional().default("ctrl+c").describe("Clear input field"),
|
|
374
|
+
input_forward_delete: z.string().optional().default("ctrl+d").describe("Forward delete"),
|
|
375
|
+
input_paste: z.string().optional().default("ctrl+v").describe("Paste from clipboard"),
|
|
376
|
+
input_submit: z.string().optional().default("return").describe("Submit input"),
|
|
377
|
+
input_newline: z.string().optional().default("shift+return,ctrl+j").describe("Insert newline in input"),
|
|
378
|
+
history_previous: z.string().optional().default("up").describe("Previous history item"),
|
|
379
|
+
history_next: z.string().optional().default("down").describe("Next history item"),
|
|
380
|
+
session_child_cycle: z.string().optional().default("ctrl+right").describe("Next child session"),
|
|
381
|
+
session_child_cycle_reverse: z.string().optional().default("ctrl+left").describe("Previous child session"),
|
|
382
|
+
})
|
|
383
|
+
.strict()
|
|
384
|
+
.meta({
|
|
385
|
+
ref: "KeybindsConfig",
|
|
386
|
+
})
|
|
387
|
+
|
|
388
|
+
export const TUI = z.object({
|
|
389
|
+
scroll_speed: z.number().min(0.001).optional().default(1).describe("TUI scroll speed"),
|
|
390
|
+
scroll_acceleration: z
|
|
391
|
+
.object({
|
|
392
|
+
enabled: z.boolean().describe("Enable scroll acceleration"),
|
|
393
|
+
})
|
|
394
|
+
.optional()
|
|
395
|
+
.describe("Scroll acceleration settings"),
|
|
396
|
+
})
|
|
397
|
+
|
|
398
|
+
export const Layout = z.enum(["auto", "stretch"]).meta({
|
|
399
|
+
ref: "LayoutConfig",
|
|
400
|
+
})
|
|
401
|
+
export type Layout = z.infer<typeof Layout>
|
|
402
|
+
|
|
403
|
+
export const Info = z
|
|
404
|
+
.object({
|
|
405
|
+
$schema: z.string().optional().describe("JSON schema reference for configuration validation"),
|
|
406
|
+
theme: z.string().optional().describe("Theme name to use for the interface"),
|
|
407
|
+
keybinds: Keybinds.optional().describe("Custom keybind configurations"),
|
|
408
|
+
tui: TUI.optional().describe("TUI specific settings"),
|
|
409
|
+
command: z
|
|
410
|
+
.record(z.string(), Command)
|
|
411
|
+
.optional()
|
|
412
|
+
.describe("Command configuration, see https://opencode.ai/docs/commands"),
|
|
413
|
+
watcher: z
|
|
414
|
+
.object({
|
|
415
|
+
ignore: z.array(z.string()).optional(),
|
|
416
|
+
})
|
|
417
|
+
.optional(),
|
|
418
|
+
snapshot: z.boolean().optional(),
|
|
419
|
+
// share and autoshare fields removed - no sharing support
|
|
420
|
+
autoupdate: z.boolean().optional().describe("Automatically update to the latest version"),
|
|
421
|
+
disabled_providers: z.array(z.string()).optional().describe("Disable providers that are loaded automatically"),
|
|
422
|
+
model: z.string().describe("Model to use in the format of provider/model, eg anthropic/claude-2").optional(),
|
|
423
|
+
small_model: z
|
|
424
|
+
.string()
|
|
425
|
+
.describe("Small model to use for tasks like title generation in the format of provider/model")
|
|
426
|
+
.optional(),
|
|
427
|
+
username: z
|
|
428
|
+
.string()
|
|
429
|
+
.optional()
|
|
430
|
+
.describe("Custom username to display in conversations instead of system username"),
|
|
431
|
+
mode: z
|
|
432
|
+
.object({
|
|
433
|
+
build: Agent.optional(),
|
|
434
|
+
plan: Agent.optional(),
|
|
435
|
+
})
|
|
436
|
+
.catchall(Agent)
|
|
437
|
+
.optional()
|
|
438
|
+
.describe("@deprecated Use `agent` field instead."),
|
|
439
|
+
agent: z
|
|
440
|
+
.object({
|
|
441
|
+
plan: Agent.optional(),
|
|
442
|
+
build: Agent.optional(),
|
|
443
|
+
general: Agent.optional(),
|
|
444
|
+
})
|
|
445
|
+
.catchall(Agent)
|
|
446
|
+
.optional()
|
|
447
|
+
.describe("Agent configuration, see https://opencode.ai/docs/agent"),
|
|
448
|
+
provider: z
|
|
449
|
+
.record(
|
|
450
|
+
z.string(),
|
|
451
|
+
ModelsDev.Provider.partial()
|
|
452
|
+
.extend({
|
|
453
|
+
models: z.record(z.string(), ModelsDev.Model.partial()).optional(),
|
|
454
|
+
options: z
|
|
455
|
+
.object({
|
|
456
|
+
apiKey: z.string().optional(),
|
|
457
|
+
baseURL: z.string().optional(),
|
|
458
|
+
enterpriseUrl: z.string().optional().describe("GitHub Enterprise URL for copilot authentication"),
|
|
459
|
+
timeout: z
|
|
460
|
+
.union([
|
|
461
|
+
z
|
|
462
|
+
.number()
|
|
463
|
+
.int()
|
|
464
|
+
.positive()
|
|
465
|
+
.describe(
|
|
466
|
+
"Timeout in milliseconds for requests to this provider. Default is 300000 (5 minutes). Set to false to disable timeout.",
|
|
467
|
+
),
|
|
468
|
+
z.literal(false).describe("Disable timeout for this provider entirely."),
|
|
469
|
+
])
|
|
470
|
+
.optional()
|
|
471
|
+
.describe(
|
|
472
|
+
"Timeout in milliseconds for requests to this provider. Default is 300000 (5 minutes). Set to false to disable timeout.",
|
|
473
|
+
),
|
|
474
|
+
})
|
|
475
|
+
.catchall(z.any())
|
|
476
|
+
.optional(),
|
|
477
|
+
})
|
|
478
|
+
.strict(),
|
|
479
|
+
)
|
|
480
|
+
.optional()
|
|
481
|
+
.describe("Custom provider configurations and model overrides"),
|
|
482
|
+
mcp: z.record(z.string(), Mcp).optional().describe("MCP (Model Context Protocol) server configurations"),
|
|
483
|
+
formatter: z
|
|
484
|
+
.union([
|
|
485
|
+
z.literal(false),
|
|
486
|
+
z.record(
|
|
487
|
+
z.string(),
|
|
488
|
+
z.object({
|
|
489
|
+
disabled: z.boolean().optional(),
|
|
490
|
+
command: z.array(z.string()).optional(),
|
|
491
|
+
environment: z.record(z.string(), z.string()).optional(),
|
|
492
|
+
extensions: z.array(z.string()).optional(),
|
|
493
|
+
}),
|
|
494
|
+
),
|
|
495
|
+
])
|
|
496
|
+
.optional(),
|
|
497
|
+
instructions: z.array(z.string()).optional().describe("Additional instruction files or patterns to include"),
|
|
498
|
+
layout: Layout.optional().describe("@deprecated Always uses stretch layout."),
|
|
499
|
+
permission: z
|
|
500
|
+
.object({
|
|
501
|
+
edit: Permission.optional(),
|
|
502
|
+
bash: z.union([Permission, z.record(z.string(), Permission)]).optional(),
|
|
503
|
+
webfetch: Permission.optional(),
|
|
504
|
+
doom_loop: Permission.optional(),
|
|
505
|
+
external_directory: Permission.optional(),
|
|
506
|
+
})
|
|
507
|
+
.optional(),
|
|
508
|
+
tools: z.record(z.string(), z.boolean()).optional(),
|
|
509
|
+
experimental: z
|
|
510
|
+
.object({
|
|
511
|
+
hook: z
|
|
512
|
+
.object({
|
|
513
|
+
file_edited: z
|
|
514
|
+
.record(
|
|
515
|
+
z.string(),
|
|
516
|
+
z
|
|
517
|
+
.object({
|
|
518
|
+
command: z.string().array(),
|
|
519
|
+
environment: z.record(z.string(), z.string()).optional(),
|
|
520
|
+
})
|
|
521
|
+
.array(),
|
|
522
|
+
)
|
|
523
|
+
.optional(),
|
|
524
|
+
session_completed: z
|
|
525
|
+
.object({
|
|
526
|
+
command: z.string().array(),
|
|
527
|
+
environment: z.record(z.string(), z.string()).optional(),
|
|
528
|
+
})
|
|
529
|
+
.array()
|
|
530
|
+
.optional(),
|
|
531
|
+
})
|
|
532
|
+
.optional(),
|
|
533
|
+
chatMaxRetries: z.number().optional().describe("Number of retries for chat completions on failure"),
|
|
534
|
+
disable_paste_summary: z.boolean().optional(),
|
|
535
|
+
batch_tool: z.boolean().optional().describe("Enable the batch tool"),
|
|
536
|
+
})
|
|
537
|
+
.optional(),
|
|
538
|
+
})
|
|
539
|
+
.strict()
|
|
540
|
+
.meta({
|
|
541
|
+
ref: "Config",
|
|
542
|
+
})
|
|
543
|
+
|
|
544
|
+
export type Info = z.output<typeof Info>
|
|
545
|
+
|
|
546
|
+
export const global = lazy(async () => {
|
|
547
|
+
let result: Info = pipe(
|
|
548
|
+
{},
|
|
549
|
+
mergeDeep(await loadFile(path.join(Global.Path.config, "config.json"))),
|
|
550
|
+
mergeDeep(await loadFile(path.join(Global.Path.config, "opencode.json"))),
|
|
551
|
+
mergeDeep(await loadFile(path.join(Global.Path.config, "opencode.jsonc"))),
|
|
552
|
+
)
|
|
553
|
+
|
|
554
|
+
await import(path.join(Global.Path.config, "config"), {
|
|
555
|
+
with: {
|
|
556
|
+
type: "toml",
|
|
557
|
+
},
|
|
558
|
+
})
|
|
559
|
+
.then(async (mod) => {
|
|
560
|
+
const { provider, model, ...rest } = mod.default
|
|
561
|
+
if (provider && model) result.model = `${provider}/${model}`
|
|
562
|
+
result["$schema"] = "https://opencode.ai/config.json"
|
|
563
|
+
result = mergeDeep(result, rest)
|
|
564
|
+
await Bun.write(path.join(Global.Path.config, "config.json"), JSON.stringify(result, null, 2))
|
|
565
|
+
await fs.unlink(path.join(Global.Path.config, "config"))
|
|
566
|
+
})
|
|
567
|
+
.catch(() => {})
|
|
568
|
+
|
|
569
|
+
return result
|
|
570
|
+
})
|
|
571
|
+
|
|
572
|
+
async function loadFile(filepath: string): Promise<Info> {
|
|
573
|
+
log.info("loading", { path: filepath })
|
|
574
|
+
let text = await Bun.file(filepath)
|
|
575
|
+
.text()
|
|
576
|
+
.catch((err) => {
|
|
577
|
+
if (err.code === "ENOENT") return
|
|
578
|
+
throw new JsonError({ path: filepath }, { cause: err })
|
|
579
|
+
})
|
|
580
|
+
if (!text) return {}
|
|
581
|
+
return load(text, filepath)
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
async function load(text: string, configFilepath: string) {
|
|
585
|
+
text = text.replace(/\{env:([^}]+)\}/g, (_, varName) => {
|
|
586
|
+
return process.env[varName] || ""
|
|
587
|
+
})
|
|
588
|
+
|
|
589
|
+
const fileMatches = text.match(/\{file:[^}]+\}/g)
|
|
590
|
+
if (fileMatches) {
|
|
591
|
+
const configDir = path.dirname(configFilepath)
|
|
592
|
+
const lines = text.split("\n")
|
|
593
|
+
|
|
594
|
+
for (const match of fileMatches) {
|
|
595
|
+
const lineIndex = lines.findIndex((line) => line.includes(match))
|
|
596
|
+
if (lineIndex !== -1 && lines[lineIndex].trim().startsWith("//")) {
|
|
597
|
+
continue // Skip if line is commented
|
|
598
|
+
}
|
|
599
|
+
let filePath = match.replace(/^\{file:/, "").replace(/\}$/, "")
|
|
600
|
+
if (filePath.startsWith("~/")) {
|
|
601
|
+
filePath = path.join(os.homedir(), filePath.slice(2))
|
|
602
|
+
}
|
|
603
|
+
const resolvedPath = path.isAbsolute(filePath) ? filePath : path.resolve(configDir, filePath)
|
|
604
|
+
const fileContent = (
|
|
605
|
+
await Bun.file(resolvedPath)
|
|
606
|
+
.text()
|
|
607
|
+
.catch((error) => {
|
|
608
|
+
const errMsg = `bad file reference: "${match}"`
|
|
609
|
+
if (error.code === "ENOENT") {
|
|
610
|
+
throw new InvalidError(
|
|
611
|
+
{
|
|
612
|
+
path: configFilepath,
|
|
613
|
+
message: errMsg + ` ${resolvedPath} does not exist`,
|
|
614
|
+
},
|
|
615
|
+
{ cause: error },
|
|
616
|
+
)
|
|
617
|
+
}
|
|
618
|
+
throw new InvalidError({ path: configFilepath, message: errMsg }, { cause: error })
|
|
619
|
+
})
|
|
620
|
+
).trim()
|
|
621
|
+
// escape newlines/quotes, strip outer quotes
|
|
622
|
+
text = text.replace(match, JSON.stringify(fileContent).slice(1, -1))
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
const errors: JsoncParseError[] = []
|
|
627
|
+
const data = parseJsonc(text, errors, { allowTrailingComma: true })
|
|
628
|
+
if (errors.length) {
|
|
629
|
+
const lines = text.split("\n")
|
|
630
|
+
const errorDetails = errors
|
|
631
|
+
.map((e) => {
|
|
632
|
+
const beforeOffset = text.substring(0, e.offset).split("\n")
|
|
633
|
+
const line = beforeOffset.length
|
|
634
|
+
const column = beforeOffset[beforeOffset.length - 1].length + 1
|
|
635
|
+
const problemLine = lines[line - 1]
|
|
636
|
+
|
|
637
|
+
const error = `${printParseErrorCode(e.error)} at line ${line}, column ${column}`
|
|
638
|
+
if (!problemLine) return error
|
|
639
|
+
|
|
640
|
+
return `${error}\n Line ${line}: ${problemLine}\n${"".padStart(column + 9)}^`
|
|
641
|
+
})
|
|
642
|
+
.join("\n")
|
|
643
|
+
|
|
644
|
+
throw new JsonError({
|
|
645
|
+
path: configFilepath,
|
|
646
|
+
message: `\n--- JSONC Input ---\n${text}\n--- Errors ---\n${errorDetails}\n--- End ---`,
|
|
647
|
+
})
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
const parsed = Info.safeParse(data)
|
|
651
|
+
if (parsed.success) {
|
|
652
|
+
if (!parsed.data.$schema) {
|
|
653
|
+
parsed.data.$schema = "https://opencode.ai/config.json"
|
|
654
|
+
await Bun.write(configFilepath, JSON.stringify(parsed.data, null, 2))
|
|
655
|
+
}
|
|
656
|
+
const data = parsed.data
|
|
657
|
+
return data
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
throw new InvalidError({
|
|
661
|
+
path: configFilepath,
|
|
662
|
+
issues: parsed.error.issues,
|
|
663
|
+
})
|
|
664
|
+
}
|
|
665
|
+
export const JsonError = NamedError.create(
|
|
666
|
+
"ConfigJsonError",
|
|
667
|
+
z.object({
|
|
668
|
+
path: z.string(),
|
|
669
|
+
message: z.string().optional(),
|
|
670
|
+
}),
|
|
671
|
+
)
|
|
672
|
+
|
|
673
|
+
export const ConfigDirectoryTypoError = NamedError.create(
|
|
674
|
+
"ConfigDirectoryTypoError",
|
|
675
|
+
z.object({
|
|
676
|
+
path: z.string(),
|
|
677
|
+
dir: z.string(),
|
|
678
|
+
suggestion: z.string(),
|
|
679
|
+
}),
|
|
680
|
+
)
|
|
681
|
+
|
|
682
|
+
export const InvalidError = NamedError.create(
|
|
683
|
+
"ConfigInvalidError",
|
|
684
|
+
z.object({
|
|
685
|
+
path: z.string(),
|
|
686
|
+
issues: z.custom<z.core.$ZodIssue[]>().optional(),
|
|
687
|
+
message: z.string().optional(),
|
|
688
|
+
}),
|
|
689
|
+
)
|
|
690
|
+
|
|
691
|
+
export async function get() {
|
|
692
|
+
return state().then((x) => x.config)
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
export async function update(config: Info) {
|
|
696
|
+
const filepath = path.join(Instance.directory, "config.json")
|
|
697
|
+
const existing = await loadFile(filepath)
|
|
698
|
+
await Bun.write(filepath, JSON.stringify(mergeDeep(existing, config), null, 2))
|
|
699
|
+
await Instance.dispose()
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
export async function directories() {
|
|
703
|
+
return state().then((x) => x.directories)
|
|
704
|
+
}
|
|
705
|
+
}
|