openviking-opencode 0.1.0
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/index.mjs +90 -0
- package/openviking-context.ts +60 -0
- package/package.json +22 -0
- package/skills/openviking/SKILL.md +187 -0
package/index.mjs
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { exec } from "child_process"
|
|
2
|
+
import { promisify } from "util"
|
|
3
|
+
import { readFileSync, mkdirSync, writeFileSync, existsSync } from "fs"
|
|
4
|
+
import { homedir } from "os"
|
|
5
|
+
import { join, dirname } from "path"
|
|
6
|
+
import { fileURLToPath } from "url"
|
|
7
|
+
|
|
8
|
+
const execAsync = promisify(exec)
|
|
9
|
+
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
10
|
+
|
|
11
|
+
// ── Skill auto-install ────────────────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
function installSkill() {
|
|
14
|
+
const src = join(__dirname, "skills", "openviking", "SKILL.md")
|
|
15
|
+
const dest = join(homedir(), ".config", "opencode", "skills", "openviking", "SKILL.md")
|
|
16
|
+
const destDir = dirname(dest)
|
|
17
|
+
try {
|
|
18
|
+
if (!existsSync(destDir)) mkdirSync(destDir, { recursive: true })
|
|
19
|
+
const content = readFileSync(src, "utf8")
|
|
20
|
+
// Only overwrite if content changed (avoid unnecessary disk writes)
|
|
21
|
+
if (!existsSync(dest) || readFileSync(dest, "utf8") !== content) {
|
|
22
|
+
writeFileSync(dest, content, "utf8")
|
|
23
|
+
}
|
|
24
|
+
} catch {
|
|
25
|
+
// Non-fatal — skill stays as-is or isn't installed
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ── Repo context cache ────────────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
let cachedRepos = null
|
|
32
|
+
let lastFetchTime = 0
|
|
33
|
+
const CACHE_TTL_MS = 60 * 1000
|
|
34
|
+
|
|
35
|
+
async function loadRepos() {
|
|
36
|
+
const now = Date.now()
|
|
37
|
+
if (cachedRepos !== null && now - lastFetchTime < CACHE_TTL_MS) return
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
const { stdout } = await execAsync(
|
|
41
|
+
"openviking --output json ls viking://resources/ --abs-limit 2000",
|
|
42
|
+
{ timeout: 8000 }
|
|
43
|
+
)
|
|
44
|
+
const parsed = JSON.parse(stdout)
|
|
45
|
+
const items = parsed?.result ?? []
|
|
46
|
+
const repos = items
|
|
47
|
+
.filter((item) => item.uri?.startsWith("viking://resources/"))
|
|
48
|
+
.map((item) => {
|
|
49
|
+
const name = item.uri.replace("viking://resources/", "").replace(/\/$/, "")
|
|
50
|
+
return item.abstract
|
|
51
|
+
? `- **${name}** (${item.uri})\n ${item.abstract}`
|
|
52
|
+
: `- **${name}** (${item.uri})`
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
if (repos.length > 0) {
|
|
56
|
+
cachedRepos = repos.join("\n")
|
|
57
|
+
lastFetchTime = now
|
|
58
|
+
}
|
|
59
|
+
} catch {
|
|
60
|
+
// openviking not running — plugin is a no-op until it is
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ── Plugin export ─────────────────────────────────────────────────────────────
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* @type {import('@opencode-ai/plugin').Plugin}
|
|
68
|
+
*/
|
|
69
|
+
export async function OpenVikingPlugin() {
|
|
70
|
+
installSkill()
|
|
71
|
+
await loadRepos()
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
"experimental.chat.system.transform": (_input, output) => {
|
|
75
|
+
if (!cachedRepos) return
|
|
76
|
+
output.system.push(
|
|
77
|
+
`## OpenViking — Indexed Code Repositories\n\n` +
|
|
78
|
+
`The following repos are semantically indexed and searchable.\n` +
|
|
79
|
+
`When the user asks about any of these projects or their internals, ` +
|
|
80
|
+
`you MUST proactively call skill("openviking") before answering.\n\n` +
|
|
81
|
+
cachedRepos
|
|
82
|
+
)
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
"session.created": async () => {
|
|
86
|
+
cachedRepos = null
|
|
87
|
+
await loadRepos()
|
|
88
|
+
},
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { exec } from "child_process"
|
|
2
|
+
import { promisify } from "util"
|
|
3
|
+
|
|
4
|
+
const execAsync = promisify(exec)
|
|
5
|
+
|
|
6
|
+
let cachedRepos: string | null = null
|
|
7
|
+
|
|
8
|
+
async function loadRepos(): Promise<void> {
|
|
9
|
+
try {
|
|
10
|
+
const { stdout } = await execAsync(
|
|
11
|
+
"openviking --output json ls viking://resources/ --abs-limit 2000",
|
|
12
|
+
{ timeout: 8000 }
|
|
13
|
+
)
|
|
14
|
+
const parsed = JSON.parse(stdout)
|
|
15
|
+
const items: Array<{ uri: string; abstract?: string }> = parsed?.result ?? []
|
|
16
|
+
const repos = items
|
|
17
|
+
.filter((item) => item.uri?.startsWith("viking://resources/"))
|
|
18
|
+
.map((item) => {
|
|
19
|
+
const name = item.uri.replace("viking://resources/", "").replace(/\/$/, "")
|
|
20
|
+
return item.abstract
|
|
21
|
+
? `- **${name}** (${item.uri})\n ${item.abstract}`
|
|
22
|
+
: `- **${name}** (${item.uri})`
|
|
23
|
+
})
|
|
24
|
+
if (repos.length > 0) {
|
|
25
|
+
cachedRepos = repos.join("\n")
|
|
26
|
+
}
|
|
27
|
+
} catch {
|
|
28
|
+
// openviking not available — plugin is a no-op
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @type {import('@opencode-ai/plugin').Plugin}
|
|
34
|
+
*/
|
|
35
|
+
export async function OpenVikingContextPlugin() {
|
|
36
|
+
// Fetch repos at startup so the cache is ready before any request
|
|
37
|
+
await loadRepos()
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
// Inject repo list into every LLM request's system prompt (sync — no await)
|
|
41
|
+
"experimental.chat.system.transform": (
|
|
42
|
+
_input: unknown,
|
|
43
|
+
output: { system: string[] }
|
|
44
|
+
) => {
|
|
45
|
+
if (!cachedRepos) return
|
|
46
|
+
output.system.push(
|
|
47
|
+
`## OpenViking — Indexed Code Repositories\n\n` +
|
|
48
|
+
`The following repos are semantically indexed and searchable.\n` +
|
|
49
|
+
`When the user asks about any of these projects or their internals, ` +
|
|
50
|
+
`you MUST proactively call skill("openviking") before answering.\n\n` +
|
|
51
|
+
cachedRepos
|
|
52
|
+
)
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
// Refresh repo list when a new session starts
|
|
56
|
+
"session.created": async () => {
|
|
57
|
+
await loadRepos()
|
|
58
|
+
},
|
|
59
|
+
}
|
|
60
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "openviking-opencode",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "OpenCode plugin for OpenViking — injects indexed repo context into the AI assistant and auto-installs the openviking skill",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "index.mjs",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./index.mjs"
|
|
9
|
+
},
|
|
10
|
+
"keywords": [
|
|
11
|
+
"opencode",
|
|
12
|
+
"opencode-plugin",
|
|
13
|
+
"openviking",
|
|
14
|
+
"rag",
|
|
15
|
+
"code-search"
|
|
16
|
+
],
|
|
17
|
+
"license": "Apache-2.0",
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "https://github.com/OpenVikingDB/OpenViking"
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: openviking
|
|
3
|
+
description: "Activate when the user's question involves external libraries, frameworks, or logic that may not exist in the current repo. SEARCH PRIORITY: (1) Always search the local codebase first (Glob/Grep/Read); (2) Only search OpenViking if local results are insufficient or the code clearly isn't in the current repo; (3) Stop if OpenViking also returns no relevant results — do not keep searching. Trigger even when only part of the question involves an external library or another project. Also activate when the user wants to add or index a GitHub repo or local project."
|
|
4
|
+
license: MIT
|
|
5
|
+
compatibility: opencode
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# OpenViking Code Repository Search
|
|
9
|
+
|
|
10
|
+
Index code repos into OpenViking and search them semantically. URIs use the `viking://` namespace. Directories have VLM-generated summaries (`abstract` / `overview`); files are read with `read`.
|
|
11
|
+
|
|
12
|
+
**IMPORTANT: Execute openviking commands directly and immediately — no warmup, no pre-checks, no test commands before the real one. If a command fails, handle the error at that point.**
|
|
13
|
+
|
|
14
|
+
## Error Handling
|
|
15
|
+
|
|
16
|
+
Run commands directly. Handle errors only when they occur:
|
|
17
|
+
|
|
18
|
+
**`command not found: openviking`** → Tell user to run `pip install openviking` and configure `~/.openviking/ov.conf`. Stop.
|
|
19
|
+
|
|
20
|
+
**`url is required` / `CLI_CONFIG` error** → Auto-create config and retry:
|
|
21
|
+
```bash
|
|
22
|
+
mkdir -p ~/.openviking && echo '{"url": "http://localhost:1933"}' > ~/.openviking/ovcli.conf
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
**`CONNECTION_ERROR` / failed to connect:**
|
|
26
|
+
- `~/.openviking/ov.conf` **exists** → auto-start server, wait until healthy, retry:
|
|
27
|
+
```bash
|
|
28
|
+
openviking serve --config ~/.openviking/ov.conf > /tmp/openviking.log 2>&1 &
|
|
29
|
+
for i in $(seq 1 10); do openviking health 2>/dev/null && break; sleep 3; done
|
|
30
|
+
```
|
|
31
|
+
- **Does not exist** → Tell user to configure `~/.openviking/ov.conf` first (API keys for embedding model and VLM required; see `examples/ov.conf.example`). Stop.
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Commands
|
|
36
|
+
|
|
37
|
+
### Add a Repository
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
openviking add-resource https://github.com/owner/repo --to viking://resources/
|
|
41
|
+
openviking add-resource /path/to/project --to viking://resources/
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
**`--to` is the parent scope** — repo name is auto-appended from source:
|
|
45
|
+
- `https://github.com/tiangolo/fastapi` → `viking://resources/fastapi/`
|
|
46
|
+
- `/path/to/myproject` → `viking://resources/myproject/`
|
|
47
|
+
|
|
48
|
+
Always use `viking://resources/` for code repos. **Never use `--wait`.**
|
|
49
|
+
|
|
50
|
+
**After submitting — always do this:**
|
|
51
|
+
1. Run `openviking observer queue` once to get queue status
|
|
52
|
+
2. Report to user: files queued, estimated time (see table), and that search works progressively
|
|
53
|
+
3. End the task — do not poll or wait
|
|
54
|
+
|
|
55
|
+
| Repo Size | Files | Est. Time |
|
|
56
|
+
|-----------|-------|-----------|
|
|
57
|
+
| Small | < 100 | 2–5 min |
|
|
58
|
+
| Medium | 100–500 | 5–20 min |
|
|
59
|
+
| Large | 500+ | 20–60+ min |
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
### Search
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
# Semantic search within a repo
|
|
67
|
+
openviking find "<query>" --uri viking://resources/repo --limit 10
|
|
68
|
+
|
|
69
|
+
# Semantic search across all repos
|
|
70
|
+
openviking find "<query>" --limit 10
|
|
71
|
+
|
|
72
|
+
# Stricter similarity threshold (0.0–1.0)
|
|
73
|
+
openviking find "<query>" --uri viking://resources/repo --threshold 0.6
|
|
74
|
+
|
|
75
|
+
# Regex search
|
|
76
|
+
openviking grep "class.*Repository" --uri viking://resources/repo
|
|
77
|
+
openviking grep "TODO" --ignore-case --uri viking://resources/repo
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
When the user names a specific repo, run `openviking ls viking://resources/` first to find the exact URI.
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
### Read File Content
|
|
85
|
+
|
|
86
|
+
**`abstract` / `overview` — directories only.** VLM generates summaries per directory, not per file. Calling on a file URI will error.
|
|
87
|
+
|
|
88
|
+
**`read` — files only.** Reads raw file content with optional line range.
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
# Directories: get VLM-generated summaries
|
|
92
|
+
openviking abstract viking://resources/repo/src/ # one-line summary of directory
|
|
93
|
+
openviking overview viking://resources/repo/src/ # detailed summary of directory
|
|
94
|
+
|
|
95
|
+
# Files: read actual content
|
|
96
|
+
openviking read viking://resources/repo/src/auth.py # full content
|
|
97
|
+
openviking read viking://resources/repo/src/auth.py --offset 100 --limit 50 # lines 100-149
|
|
98
|
+
openviking read viking://resources/repo/src/auth.py --offset 100 # line 100 to end
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
### Browse & Find Files
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
openviking ls viking://resources/ # list all indexed repos
|
|
107
|
+
openviking ls viking://resources/repo # list repo contents
|
|
108
|
+
openviking tree viking://resources/repo # directory tree
|
|
109
|
+
|
|
110
|
+
# --uri is required; viking:// root is invalid
|
|
111
|
+
openviking glob "**/*.py" --uri viking://resources/repo # by pattern
|
|
112
|
+
openviking glob "**/*.py" --uri viking://resources/ # across all repos
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
### Manage
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
# Check import progress
|
|
121
|
+
openviking observer queue
|
|
122
|
+
openviking observer system
|
|
123
|
+
|
|
124
|
+
# Delete a repo (irreversible)
|
|
125
|
+
openviking rm viking://resources/repo --recursive
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## Combining Commands for Complex Tasks
|
|
131
|
+
|
|
132
|
+
Commands are building blocks — chain them for deeper investigation:
|
|
133
|
+
|
|
134
|
+
**Progressive drill-down:** `find` → `abstract`/`overview` (dirs) → `read` (files)
|
|
135
|
+
- `find`: locate candidates by concept
|
|
136
|
+
- `abstract`: one-line summary of a directory to judge relevance
|
|
137
|
+
- `overview`: detailed summary of a directory for deeper understanding
|
|
138
|
+
- `read`: fetch full file content only when needed
|
|
139
|
+
- `grep`: trace call sites or find all usages of a symbol
|
|
140
|
+
- `glob`: enumerate all files of a type for full coverage
|
|
141
|
+
|
|
142
|
+
**Example: "How does project X handle authentication?"**
|
|
143
|
+
```
|
|
144
|
+
1. openviking find "authentication login token" --uri viking://resources/x --limit 10
|
|
145
|
+
→ returns file URIs like viking://resources/x/src/auth/jwt.py
|
|
146
|
+
2. openviking abstract viking://resources/x/src/auth/ → understand the auth directory
|
|
147
|
+
3. openviking read viking://resources/x/src/auth/jwt.py → read the specific file
|
|
148
|
+
4. openviking grep "verify_token" --uri viking://resources/x → trace call sites
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
**Example: "What tests exist for the parser module?"**
|
|
152
|
+
```
|
|
153
|
+
1. openviking glob "**/test*parser*" --uri viking://resources/x
|
|
154
|
+
2. openviking abstract viking://resources/x/tests/ → understand test directory structure
|
|
155
|
+
3. openviking read <specific test file>
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
## Workflow Examples
|
|
161
|
+
|
|
162
|
+
**Adding a repo:**
|
|
163
|
+
```
|
|
164
|
+
User: "Add fastapi so I can search how they handle dependencies"
|
|
165
|
+
1. openviking add-resource https://github.com/tiangolo/fastapi --to viking://resources/
|
|
166
|
+
→ stored at viking://resources/fastapi/ | on error: see Error Handling above
|
|
167
|
+
2. openviking observer queue
|
|
168
|
+
3. Tell user: indexing in background, ~10 min, files searchable progressively
|
|
169
|
+
4. End task
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
**Searching a specific repo:**
|
|
173
|
+
```
|
|
174
|
+
User: "How does fastapi handle dependency injection?"
|
|
175
|
+
1. openviking ls viking://resources/ → find URI matching "fastapi"
|
|
176
|
+
2. openviking find "dependency injection" --uri viking://resources/fastapi --limit 10
|
|
177
|
+
→ returns file URIs, e.g. viking://resources/fastapi/fastapi/dependencies/utils.py
|
|
178
|
+
3. openviking abstract viking://resources/fastapi/fastapi/dependencies/ → understand the directory
|
|
179
|
+
4. openviking read viking://resources/fastapi/fastapi/dependencies/utils.py → read the file
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
**Searching across all repos:**
|
|
183
|
+
```
|
|
184
|
+
User: "How is JWT authentication implemented in my projects?"
|
|
185
|
+
1. openviking find "JWT authentication" --limit 10
|
|
186
|
+
2. Results include source URIs — group by repo, read relevant files
|
|
187
|
+
```
|