mirror of
https://github.com/anomalyco/opencode.git
synced 2026-02-15 13:24:13 +00:00
Compare commits
1 Commits
dev
...
feat/refer
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6daf86dd6e |
@@ -3,6 +3,7 @@
|
||||
// "enterprise": {
|
||||
// "url": "https://enterprise.dev.opencode.ai",
|
||||
// },
|
||||
"references": ["git@github.com:Effect-TS/effect.git"],
|
||||
"provider": {
|
||||
"opencode": {
|
||||
"options": {},
|
||||
|
||||
@@ -11,6 +11,7 @@ import { ProviderTransform } from "../provider/transform"
|
||||
import PROMPT_GENERATE from "./generate.txt"
|
||||
import PROMPT_COMPACTION from "./prompt/compaction.txt"
|
||||
import PROMPT_EXPLORE from "./prompt/explore.txt"
|
||||
import PROMPT_REFERENCE from "./prompt/reference.txt"
|
||||
import PROMPT_SUMMARY from "./prompt/summary.txt"
|
||||
import PROMPT_TITLE from "./prompt/title.txt"
|
||||
import { PermissionNext } from "@/permission/next"
|
||||
@@ -19,6 +20,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
|
||||
@@ -153,6 +155,37 @@ export namespace Agent {
|
||||
mode: "subagent",
|
||||
native: true,
|
||||
},
|
||||
...(cfg.references?.length
|
||||
? {
|
||||
reference: {
|
||||
name: "reference",
|
||||
description: `Search across referenced projects configured in opencode.json under "references". Use this to query code in external repositories.\n\nAvailable references:\n${cfg.references.map((r) => `- ${r}`).join("\n")}`,
|
||||
permission: PermissionNext.merge(
|
||||
defaults,
|
||||
PermissionNext.fromConfig({
|
||||
"*": "deny",
|
||||
grep: "allow",
|
||||
glob: "allow",
|
||||
list: "allow",
|
||||
bash: "allow",
|
||||
webfetch: "allow",
|
||||
websearch: "allow",
|
||||
codesearch: "allow",
|
||||
read: "allow",
|
||||
lsp: "allow",
|
||||
external_directory: {
|
||||
[Truncate.GLOB]: "allow",
|
||||
[path.join(Global.Path.reference, "*")]: "allow",
|
||||
},
|
||||
}),
|
||||
user,
|
||||
),
|
||||
options: {},
|
||||
mode: "subagent",
|
||||
native: true,
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
compaction: {
|
||||
name: "compaction",
|
||||
mode: "primary",
|
||||
@@ -250,7 +283,20 @@ export namespace Agent {
|
||||
})
|
||||
|
||||
export async function get(agent: string) {
|
||||
return state().then((x) => x[agent])
|
||||
const result = await state().then((x) => x[agent])
|
||||
if (!result) return result
|
||||
if (agent === "reference") {
|
||||
const refs = await Reference.list()
|
||||
const fresh = await Promise.all(refs.map((r) => Reference.ensureFresh(r)))
|
||||
const valid = fresh.filter(Boolean) as Reference.Info[]
|
||||
const info =
|
||||
valid.length > 0 ? valid.map((r) => `- ${r.url} at ${r.path}`).join("\n") : "No references available."
|
||||
return {
|
||||
...result,
|
||||
prompt: PROMPT_REFERENCE + "\n\nAvailable references:\n" + info,
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
export async function list() {
|
||||
|
||||
18
packages/opencode/src/agent/prompt/reference.txt
Normal file
18
packages/opencode/src/agent/prompt/reference.txt
Normal file
@@ -0,0 +1,18 @@
|
||||
You are a multi-project code search specialist. You search across referenced projects to answer questions about external codebases.
|
||||
|
||||
Your strengths:
|
||||
- Rapidly finding files using glob patterns across multiple repositories
|
||||
- Searching code and text with powerful regex patterns
|
||||
- Reading and analyzing file contents from any referenced project
|
||||
|
||||
Guidelines:
|
||||
- Search across ALL referenced projects unless the user specifies one
|
||||
- Report which project(s) contained relevant findings by mentioning the repository URL
|
||||
- Use Glob for broad file pattern matching across all references
|
||||
- Use Grep for searching file contents with regex
|
||||
- Use Read when you know the specific file path you need to read
|
||||
- Return file paths as absolute paths so users can locate findings
|
||||
- For clear communication, avoid using emojis
|
||||
- Do not create any files, or run bash commands that modify the system state in any way
|
||||
|
||||
Complete the user's search request efficiently and report your findings clearly.
|
||||
@@ -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({
|
||||
|
||||
@@ -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"
|
||||
|
||||
131
packages/opencode/src/reference/index.ts
Normal file
131
packages/opencode/src/reference/index.ts
Normal 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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user