Compare commits

...

2 Commits

Author SHA1 Message Date
Dax Raad
0d2bd182b1 sync 2026-02-16 09:57:42 -05:00
Dax Raad
6daf86dd6e feat: add reference agent for searching external repositories
Add ability to configure external git repositories or local paths that
subagents can search across. References are configured in opencode.json
and automatically cloned/fetched to a cache directory.
2026-02-15 01:27:22 -05:00
6 changed files with 166 additions and 2 deletions

View File

@@ -3,6 +3,7 @@
// "enterprise": {
// "url": "https://enterprise.dev.opencode.ai",
// },
"references": ["git@github.com:Effect-TS/effect.git"],
"provider": {
"opencode": {
"options": {},

View File

@@ -19,6 +19,7 @@ import { Global } from "@/global"
import path from "path"
import { Plugin } from "@/plugin"
import { Skill } from "../skill"
import { Reference } from "@/reference"
export namespace Agent {
export const Info = z
@@ -73,6 +74,17 @@ export namespace Agent {
})
const user = PermissionNext.fromConfig(cfg.permission ?? {})
let explorePrompt = PROMPT_EXPLORE
if (cfg.references?.length) {
const refs = cfg.references.map((r) => Reference.parse(r))
const fresh = await Promise.all(refs.map((r) => Reference.ensureFresh(r)))
const valid = fresh.filter(Boolean) as Reference.Info[]
if (valid.length > 0) {
explorePrompt +=
"\n\n<references>\n" + valid.map((r) => `- ${r.url} -> ${r.path}`).join("\n") + "\n</references>"
}
}
const result: Record<string, Info> = {
build: {
name: "build",
@@ -143,12 +155,17 @@ export namespace Agent {
read: "allow",
external_directory: {
[Truncate.GLOB]: "allow",
[path.join(Global.Path.reference, "*")]: "allow",
},
}),
user,
),
description: `Fast agent specialized for exploring codebases. Use this when you need to quickly find files by patterns (eg. "src/components/**/*.tsx"), search code for keywords (eg. "API endpoints"), or answer questions about the codebase (eg. "how do API endpoints work?"). When calling this agent, specify the desired thoroughness level: "quick" for basic searches, "medium" for moderate exploration, or "very thorough" for comprehensive analysis across multiple locations and naming conventions.`,
prompt: PROMPT_EXPLORE,
description: `Fast agent specialized for exploring codebases. Use this when you need to quickly find files by patterns (eg. "src/components/**/*.tsx"), search code for keywords (eg. "API endpoints"), or answer questions about the codebase (eg. "how do API endpoints work?"). When calling this agent, specify the desired thoroughness level: "quick" for basic searches, "medium" for moderate exploration, or "very thorough" for comprehensive analysis across multiple locations and naming conventions.${
cfg.references?.length
? `\n\nAlways use this to answer questions about the following referenced projects:\n${cfg.references.map((r) => `- ${r}`).join("\n")}`
: ""
}`,
prompt: explorePrompt,
options: {},
mode: "subagent",
native: true,

View File

@@ -15,4 +15,13 @@ Guidelines:
- For clear communication, avoid using emojis
- Do not create any files, or run bash commands that modify the user's system state in any way
Referenced projects:
When configured, you also have access to referenced projects - external codebases that may contain relevant code or patterns. Use these when:
- The user asks about code not found in the main project
- You need to understand how a library or dependency works
- The user mentions an external repository or package
- Searching for patterns across multiple codebases
Search referenced projects by using their absolute paths (provided in a <references> tag) with Glob, Grep, and Read tools.
Complete the user's search request efficiently and report your findings clearly.

View File

@@ -1192,6 +1192,10 @@ export namespace Config {
.describe("Timeout in milliseconds for model context protocol (MCP) requests"),
})
.optional(),
references: z
.array(z.string())
.optional()
.describe("Git repositories or local paths to reference from subagents"),
})
.strict()
.meta({

View File

@@ -20,6 +20,7 @@ export namespace Global {
bin: path.join(data, "bin"),
log: path.join(data, "log"),
cache,
reference: path.join(cache, "references"),
config,
state,
}
@@ -31,6 +32,7 @@ await Promise.all([
fs.mkdir(Global.Path.state, { recursive: true }),
fs.mkdir(Global.Path.log, { recursive: true }),
fs.mkdir(Global.Path.bin, { recursive: true }),
fs.mkdir(Global.Path.reference, { recursive: true }),
])
const CACHE_VERSION = "21"

View File

@@ -0,0 +1,131 @@
import path from "path"
import { mkdir, stat } from "fs/promises"
import { createHash } from "crypto"
import { Global } from "../global"
import { Config } from "../config/config"
import { Log } from "../util/log"
import { git } from "../util/git"
import { Instance } from "../project/instance"
export namespace Reference {
const log = Log.create({ service: "reference" })
const STALE_THRESHOLD_MS = 60 * 60 * 1000
export interface Info {
url: string
path: string
branch?: string
type: "git" | "local"
}
function hashUrl(url: string): string {
return createHash("sha256").update(url).digest("hex").slice(0, 16)
}
export function parse(url: string): Info {
if (url.startsWith("/") || url.startsWith("~") || url.startsWith(".")) {
const resolved = url.startsWith("~")
? path.join(Global.Path.home, url.slice(1))
: url.startsWith(".")
? path.resolve(Instance.worktree, url)
: url
return {
url,
path: resolved,
type: "local",
}
}
const branchMatch = url.match(/^(.+)#(.+)$/)
const gitUrl = branchMatch ? branchMatch[1] : url
const branch = branchMatch ? branchMatch[2] : undefined
return {
url: gitUrl,
path: path.join(Global.Path.reference, hashUrl(gitUrl)),
branch,
type: "git",
}
}
export async function isStale(ref: Info): Promise<boolean> {
if (ref.type === "local") return false
const fetchHead = path.join(ref.path, ".git", "FETCH_HEAD")
const s = await stat(fetchHead).catch(() => null)
if (!s) return true
return Date.now() - s.mtime.getTime() > STALE_THRESHOLD_MS
}
export async function fetch(ref: Info): Promise<boolean> {
if (ref.type === "local") {
const exists = await stat(ref.path).catch(() => null)
if (!exists?.isDirectory()) {
log.error("local reference not found", { path: ref.path })
return false
}
return true
}
await mkdir(path.dirname(ref.path), { recursive: true })
const isCloned = await stat(path.join(ref.path, ".git")).catch(() => null)
if (!isCloned) {
log.info("cloning reference", { url: ref.url, branch: ref.branch })
const args = ["clone", "--depth", "1"]
if (ref.branch) {
args.push("--branch", ref.branch)
}
args.push(ref.url, ref.path)
const result = await git(args, { cwd: Global.Path.reference })
if (result.exitCode !== 0) {
log.error("failed to clone", { url: ref.url, stderr: result.stderr.toString() })
return false
}
return true
}
log.info("fetching reference", { url: ref.url })
const fetchResult = await git(["fetch"], { cwd: ref.path })
if (fetchResult.exitCode !== 0) {
log.warn("failed to fetch, using cached", { url: ref.url })
return true
}
if (ref.branch) {
const checkoutResult = await git(["checkout", ref.branch], { cwd: ref.path })
if (checkoutResult.exitCode !== 0) {
log.warn("failed to checkout branch, using current", { url: ref.url, branch: ref.branch })
}
}
return true
}
export async function ensureFresh(ref: Info): Promise<Info | null> {
if (await isStale(ref)) {
const success = await fetch(ref)
if (!success && ref.type === "git") {
const exists = await stat(ref.path).catch(() => null)
if (!exists) return null
}
}
return ref
}
export async function list(): Promise<Info[]> {
const cfg = await Config.get()
const urls = cfg.references ?? []
return urls.map(parse)
}
export async function directories(): Promise<string[]> {
const refs = await list()
const fresh = await Promise.all(refs.map(ensureFresh))
return fresh.filter(Boolean).map((r) => r!.path)
}
}