Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 15 additions & 10 deletions packages/opencode/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,9 @@ export namespace Config {
}

// Global user config overrides remote config
result = mergeConfigConcatArrays(result, await global())
if (!Flag.OPENCODE_DISABLE_GLOBAL_CONFIG) {
result = mergeConfigConcatArrays(result, await global())
}

// Custom config path overrides global
if (Flag.OPENCODE_CONFIG) {
Expand Down Expand Up @@ -115,7 +117,8 @@ export namespace Config {
result.plugin = result.plugin || []

const directories = [
Global.Path.config,
// Only scan global config directory when global config is enabled
...(!Flag.OPENCODE_DISABLE_GLOBAL_CONFIG ? [Global.Path.config] : []),
// Only scan project .opencode/ directories when project discovery is enabled
...(!Flag.OPENCODE_DISABLE_PROJECT_CONFIG
? await Array.fromAsync(
Expand All @@ -126,14 +129,16 @@ export namespace Config {
}),
)
: []),
// Always scan ~/.opencode/ (user home directory)
...(await Array.fromAsync(
Filesystem.up({
targets: [".opencode"],
start: Global.Path.home,
stop: Global.Path.home,
}),
)),
// Only scan ~/.opencode/ (user home directory) when global config is enabled
...(!Flag.OPENCODE_DISABLE_GLOBAL_CONFIG
? await Array.fromAsync(
Filesystem.up({
targets: [".opencode"],
start: Global.Path.home,
stop: Global.Path.home,
}),
)
: []),
]

if (Flag.OPENCODE_CONFIG_DIR) {
Expand Down
12 changes: 12 additions & 0 deletions packages/opencode/src/flag/flag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export namespace Flag {
export const OPENCODE_DISABLE_CLAUDE_CODE_SKILLS =
OPENCODE_DISABLE_CLAUDE_CODE || truthy("OPENCODE_DISABLE_CLAUDE_CODE_SKILLS")
export declare const OPENCODE_DISABLE_PROJECT_CONFIG: boolean
export declare const OPENCODE_DISABLE_GLOBAL_CONFIG: boolean
export const OPENCODE_FAKE_VCS = process.env["OPENCODE_FAKE_VCS"]
export const OPENCODE_CLIENT = process.env["OPENCODE_CLIENT"] ?? "cli"
export const OPENCODE_SERVER_PASSWORD = process.env["OPENCODE_SERVER_PASSWORD"]
Expand Down Expand Up @@ -67,6 +68,17 @@ Object.defineProperty(Flag, "OPENCODE_DISABLE_PROJECT_CONFIG", {
configurable: false,
})

// Dynamic getter for OPENCODE_DISABLE_GLOBAL_CONFIG
// This must be evaluated at access time, not module load time,
// because external tooling may set this env var at runtime
Object.defineProperty(Flag, "OPENCODE_DISABLE_GLOBAL_CONFIG", {
get() {
return truthy("OPENCODE_DISABLE_GLOBAL_CONFIG")
},
enumerable: true,
configurable: false,
})

// Dynamic getter for OPENCODE_CONFIG_DIR
// This must be evaluated at access time, not module load time,
// because external tooling may set this env var at runtime
Expand Down
10 changes: 6 additions & 4 deletions packages/opencode/src/session/instruction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,10 +81,12 @@ export namespace InstructionPrompt {
}
}

for (const file of globalFiles()) {
if (await Bun.file(file).exists()) {
paths.add(path.resolve(file))
break
if (!Flag.OPENCODE_DISABLE_GLOBAL_CONFIG) {
for (const file of globalFiles()) {
if (await Bun.file(file).exists()) {
paths.add(path.resolve(file))
break
}
}
}

Expand Down
119 changes: 117 additions & 2 deletions packages/opencode/src/session/system.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,44 @@
import { Ripgrep } from "../file/ripgrep"
import { Global } from "../global"
import { Filesystem } from "../util/filesystem"
import { Config } from "../config/config"
import { Log } from "../util/log"

import { Instance } from "../project/instance"
import path from "path"
import os from "os"

import PROMPT_ANTHROPIC from "./prompt/anthropic.txt"
import PROMPT_ANTHROPIC_WITHOUT_TODO from "./prompt/qwen.txt"
import PROMPT_BEAST from "./prompt/beast.txt"
import PROMPT_GEMINI from "./prompt/gemini.txt"
import PROMPT_ANTHROPIC_SPOOF from "./prompt/anthropic_spoof.txt"

import PROMPT_CODEX from "./prompt/codex_header.txt"
import type { Provider } from "@/provider/provider"
import { Flag } from "@/flag/flag"

const log = Log.create({ service: "system-prompt" })

async function resolveRelativeInstruction(instruction: string): Promise<string[]> {
if (!Flag.OPENCODE_DISABLE_PROJECT_CONFIG) {
return Filesystem.globUp(instruction, Instance.directory, Instance.worktree).catch(() => [])
}
if (!Flag.OPENCODE_CONFIG_DIR) {
log.warn(
`Skipping relative instruction "${instruction}" - no OPENCODE_CONFIG_DIR set while project config is disabled`,
)
return []
}
return Filesystem.globUp(instruction, Flag.OPENCODE_CONFIG_DIR, Flag.OPENCODE_CONFIG_DIR).catch(() => [])
}

export namespace SystemPrompt {
export function header(providerID: string) {
if (providerID.includes("anthropic")) return [PROMPT_ANTHROPIC_SPOOF.trim()]
return []
}

export function instructions() {
return PROMPT_CODEX.trim()
}
Expand All @@ -24,11 +52,10 @@ export namespace SystemPrompt {
return [PROMPT_ANTHROPIC_WITHOUT_TODO]
}

export async function environment(model: Provider.Model) {
export async function environment() {
const project = Instance.project
return [
[
`You are powered by the model named ${model.api.id}. The exact model ID is ${model.providerID}/${model.api.id}`,
`Here is some useful information about the environment you are running in:`,
`<env>`,
` Working directory: ${Instance.directory}`,
Expand All @@ -49,4 +76,92 @@ export namespace SystemPrompt {
].join("\n"),
]
}

const LOCAL_RULE_FILES = [
"AGENTS.md",
"CLAUDE.md",
"CONTEXT.md", // deprecated
]
const GLOBAL_RULE_FILES = [path.join(Global.Path.config, "AGENTS.md")]
if (!Flag.OPENCODE_DISABLE_CLAUDE_CODE_PROMPT) {
GLOBAL_RULE_FILES.push(path.join(os.homedir(), ".claude", "CLAUDE.md"))
}

if (Flag.OPENCODE_CONFIG_DIR) {
GLOBAL_RULE_FILES.push(path.join(Flag.OPENCODE_CONFIG_DIR, "AGENTS.md"))
}

export async function custom() {
const config = await Config.get()
const paths = new Set<string>()

// Only scan local rule files when project discovery is enabled
if (!Flag.OPENCODE_DISABLE_PROJECT_CONFIG) {
for (const localRuleFile of LOCAL_RULE_FILES) {
const matches = await Filesystem.findUp(localRuleFile, Instance.directory, Instance.worktree)
if (matches.length > 0) {
matches.forEach((path) => paths.add(path))
break
}
}
}

// Only scan global rule files when global config is enabled
if (!Flag.OPENCODE_DISABLE_GLOBAL_CONFIG) {
for (const globalRuleFile of GLOBAL_RULE_FILES) {
if (await Bun.file(globalRuleFile).exists()) {
paths.add(globalRuleFile)
break
}
}
}

// Always load AGENTS.md from OPENCODE_CONFIG_DIR if set (explicit override)
if (Flag.OPENCODE_CONFIG_DIR) {
const configDirAgents = path.join(Flag.OPENCODE_CONFIG_DIR, "AGENTS.md")
if (await Bun.file(configDirAgents).exists()) {
paths.add(configDirAgents)
}
}

const urls: string[] = []
if (config.instructions) {
for (let instruction of config.instructions) {
if (instruction.startsWith("https://") || instruction.startsWith("http://")) {
urls.push(instruction)
continue
}
if (instruction.startsWith("~/")) {
instruction = path.join(os.homedir(), instruction.slice(2))
}
let matches: string[] = []
if (path.isAbsolute(instruction)) {
matches = await Array.fromAsync(
new Bun.Glob(path.basename(instruction)).scan({
cwd: path.dirname(instruction),
absolute: true,
onlyFiles: true,
}),
).catch(() => [])
} else {
matches = await resolveRelativeInstruction(instruction)
}
matches.forEach((path) => paths.add(path))
}
}

const foundFiles = Array.from(paths).map((p) =>
Bun.file(p)
.text()
.catch(() => "")
.then((x) => "Instructions from: " + p + "\n" + x),
)
const foundUrls = urls.map((url) =>
fetch(url, { signal: AbortSignal.timeout(5000) })
.then((res) => (res.ok ? res.text() : ""))
.catch(() => "")
.then((x) => (x ? "Instructions from: " + url + "\n" + x : "")),
)
return Promise.all([...foundFiles, ...foundUrls]).then((result) => result.filter(Boolean))
}
}
Loading
Loading