sync
parent
656fa191c1
commit
e8ee1e239f
|
|
@ -2,4 +2,5 @@ node_modules
|
|||
package.json
|
||||
bun.lock
|
||||
.gitignore
|
||||
package-lock.json
|
||||
package-lock.json
|
||||
plans
|
||||
|
|
|
|||
|
|
@ -1,209 +0,0 @@
|
|||
# Plan: Project References Feature
|
||||
|
||||
Add support for referencing external git repositories in `opencode.json` that can be queried via a dedicated subagent.
|
||||
|
||||
## Overview
|
||||
|
||||
Users can specify references in config:
|
||||
|
||||
```json
|
||||
{
|
||||
"references": ["git@github.com:Effect-TS/effect.git", "git@github.com:foo/bar.git#v1.0.0", "/local/path/to/repo"]
|
||||
}
|
||||
```
|
||||
|
||||
These get cloned/cached to `~/.cache/opencode/references/` and a `reference` subagent can search across them.
|
||||
|
||||
---
|
||||
|
||||
## Files to Create
|
||||
|
||||
### `src/reference/index.ts`
|
||||
|
||||
Core reference management module with:
|
||||
|
||||
- `parse(url: string): Reference` - Parse git URL or local path, extract branch if present (`#branch` suffix)
|
||||
- `cachePath(ref: Reference): string` - SHA-256 hash of URL for deterministic directory names
|
||||
- `isStale(ref: Reference): Promise<boolean>` - Check if `FETCH_HEAD` mtime > 1 hour old
|
||||
- `fetch(ref: Reference): Promise<void>` - Clone with `--depth 1` or `git fetch`, handle branch checkout
|
||||
- `ensureFresh(ref: Reference): Promise<void>` - Lazy fetch wrapper (skip if not stale)
|
||||
- `list(): Promise<Reference[]>` - Get references from config
|
||||
- `directories(): Promise<string[]>` - Get all cached reference paths for permissions
|
||||
|
||||
Types:
|
||||
|
||||
```ts
|
||||
interface Reference {
|
||||
url: string // Original URL/path
|
||||
path: string // Cache directory (for git) or resolved path (for local)
|
||||
branch?: string // Optional branch/tag from URL fragment
|
||||
type: "git" | "local"
|
||||
}
|
||||
```
|
||||
|
||||
URL parsing supports:
|
||||
|
||||
- `git@github.com:foo/bar.git` → git
|
||||
- `git@github.com:foo/bar.git#main` → git with branch
|
||||
- `https://github.com/foo/bar.git#v1.0.0` → git with tag
|
||||
- `/absolute/path` → local
|
||||
- `~/relative/path` → local (expanded)
|
||||
|
||||
### `src/agent/prompt/reference.txt`
|
||||
|
||||
Prompt for reference subagent:
|
||||
|
||||
```
|
||||
You are a multi-project code search specialist. You search across referenced projects
|
||||
to answer questions about external codebases.
|
||||
|
||||
Available references: {references}
|
||||
|
||||
Guidelines:
|
||||
- Search across ALL referenced projects unless the user specifies one
|
||||
- Report which project(s) contained relevant findings
|
||||
- Use Glob for file patterns, Grep for content search, Read for specific files
|
||||
- Return absolute paths so users can locate findings
|
||||
- Do not modify any files or run destructive commands
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Files to Modify
|
||||
|
||||
### `src/config/config.ts`
|
||||
|
||||
Add `references` field to `Config.Info` schema (around line 1174):
|
||||
|
||||
```ts
|
||||
references: z.array(z.string()).optional().describe(
|
||||
"Git repositories or local paths to reference from subagents"
|
||||
),
|
||||
```
|
||||
|
||||
### `src/global/index.ts`
|
||||
|
||||
Add reference cache path and create directory:
|
||||
|
||||
```ts
|
||||
// Add to Path object:
|
||||
reference: path.join(cache, "references"),
|
||||
|
||||
// Add to mkdir promise array:
|
||||
fs.mkdir(path.join(cache, "references"), { recursive: true }),
|
||||
```
|
||||
|
||||
### `src/agent/agent.ts`
|
||||
|
||||
Add `reference` agent to built-in agents (after `explore` around line 155):
|
||||
|
||||
```ts
|
||||
reference: {
|
||||
name: "reference",
|
||||
description: `Search across referenced projects configured in opencode.json under "references". Use this to query code in external repositories.`,
|
||||
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",
|
||||
// Reference paths added dynamically via Reference.directories()
|
||||
},
|
||||
}),
|
||||
user,
|
||||
),
|
||||
prompt: PROMPT_REFERENCE,
|
||||
options: {},
|
||||
mode: "subagent",
|
||||
native: true,
|
||||
},
|
||||
```
|
||||
|
||||
Need to inject reference paths into `external_directory` permissions. This requires:
|
||||
|
||||
1. Loading references via `Reference.list()`
|
||||
2. Ensuring they're fresh via `Reference.ensureFresh()`
|
||||
3. Adding their paths to `external_directory` permissions
|
||||
|
||||
**Challenge**: The agent definition is loaded once at startup, but references may change. Solution: Inject reference paths dynamically in the Task tool when creating the subagent session.
|
||||
|
||||
### `src/tool/task.ts`
|
||||
|
||||
When creating session for `reference` subagent (around line 72):
|
||||
|
||||
```ts
|
||||
// Before Session.create:
|
||||
let extraPermissions = []
|
||||
if (params.subagent_type === "reference") {
|
||||
const refs = await Reference.list()
|
||||
await Promise.all(refs.map((r) => Reference.ensureFresh(r)))
|
||||
const paths = refs.map((r) => path.join(r.path, "*"))
|
||||
extraPermissions = paths.map((p) => ({
|
||||
permission: "external_directory" as const,
|
||||
pattern: p,
|
||||
action: "allow" as const,
|
||||
}))
|
||||
}
|
||||
|
||||
// Add extraPermissions to the permission array in Session.create
|
||||
```
|
||||
|
||||
Also need to inject available references into the prompt. Modify `SessionPrompt.resolvePromptParts` or add a way to inject context.
|
||||
|
||||
### `src/session/prompt.ts` (or similar)
|
||||
|
||||
Inject reference list into reference agent's system prompt:
|
||||
|
||||
```ts
|
||||
// When agent.name === "reference", prepend reference info to prompt
|
||||
const refs = await Reference.list()
|
||||
const refList = refs.map((r) => `- ${r.url} at ${r.path}`).join("\n")
|
||||
// Inject into prompt or parts
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Edge Cases
|
||||
|
||||
1. **Clone failures**: Log warning, continue with other references. Don't block subagent.
|
||||
2. **Network failures**: Use cached version if exists, even if stale. Only error if never cloned.
|
||||
3. **Branch not found**: Log error, skip that reference.
|
||||
4. **Local path missing**: Log warning, skip reference.
|
||||
5. **Invalid URL format**: Log warning, skip reference.
|
||||
6. **Shallow clone**: Use `--depth 1` for faster clones. For branch-specific URLs, use `--branch <ref> --depth 1`.
|
||||
|
||||
---
|
||||
|
||||
## Verification
|
||||
|
||||
1. Add to `opencode.json`: `{"references": ["git@github.com:effect-ts/effect.git"]}`
|
||||
2. Invoke `@reference` with query like "show me the Effect schema module"
|
||||
3. Verify repo cloned to `~/.cache/opencode/references/<hash>/`
|
||||
4. Verify subagent can search/read files in the cloned repo
|
||||
5. Wait 1+ hour, invoke again, verify fetch happens
|
||||
6. Test with local path: `{"references": ["/path/to/local/repo"]}`
|
||||
7. Test with branch: `{"references": ["git@github.com:foo/bar.git#main"]}`
|
||||
8. Verify error handling with invalid URL
|
||||
|
||||
---
|
||||
|
||||
## Files Summary
|
||||
|
||||
| File | Action |
|
||||
| -------------------------------- | ------------------------------------------- |
|
||||
| `src/reference/index.ts` | Create |
|
||||
| `src/agent/prompt/reference.txt` | Create |
|
||||
| `src/config/config.ts` | Add `references` to schema |
|
||||
| `src/global/index.ts` | Add `reference` path |
|
||||
| `src/agent/agent.ts` | Add `reference` agent |
|
||||
| `src/tool/task.ts` | Inject ref permissions + paths into session |
|
||||
| `src/session/prompt.ts` | Inject reference list into system prompt |
|
||||
Loading…
Reference in New Issue