openviking-opencode 0.1.0 → 0.2.1
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/README.md +148 -0
- package/index.mjs +84 -18
- package/package.json +1 -1
package/README.md
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# openviking-opencode
|
|
2
|
+
|
|
3
|
+
[OpenViking](https://github.com/OpenVikingDB/OpenViking) official plugin for [OpenCode](https://opencode.ai).
|
|
4
|
+
|
|
5
|
+
Automatically injects your indexed code repositories into the AI assistant's context, so it proactively searches them without you having to ask.
|
|
6
|
+
|
|
7
|
+
## What It Does
|
|
8
|
+
|
|
9
|
+
- **Auto-installs the `openviking` skill** into `~/.config/opencode/skills/openviking/` on first load
|
|
10
|
+
- **Injects indexed repo summaries** into the system prompt so the AI knows what's available
|
|
11
|
+
- **Auto-starts the OpenViking server** if it's installed and configured but not running
|
|
12
|
+
- **Shows clear toast notifications** if setup is incomplete (missing install, missing config, failed start)
|
|
13
|
+
|
|
14
|
+
## Prerequisites
|
|
15
|
+
|
|
16
|
+
### 1. Install OpenViking
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pip install openviking
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### 2. Configure OpenViking
|
|
23
|
+
|
|
24
|
+
OpenViking requires an embedding model and a VLM (for generating directory summaries). Create `~/.openviking/ov.conf`:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
cp /path/to/OpenViking/examples/ov.conf.example ~/.openviking/ov.conf
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Then fill in your API keys. Minimum required fields:
|
|
31
|
+
|
|
32
|
+
```json
|
|
33
|
+
{
|
|
34
|
+
"embedding": {
|
|
35
|
+
"provider": "openai",
|
|
36
|
+
"api_key": "sk-..."
|
|
37
|
+
},
|
|
38
|
+
"vlm": {
|
|
39
|
+
"provider": "openai",
|
|
40
|
+
"model": "gpt-4o-mini",
|
|
41
|
+
"api_key": "sk-..."
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
> See the [OpenViking docs](https://github.com/OpenVikingDB/OpenViking) for all supported providers.
|
|
47
|
+
|
|
48
|
+
### 3. Install OpenCode
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
npm install -g opencode-ai
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Installation
|
|
55
|
+
|
|
56
|
+
Add the plugin to your OpenCode config at `~/.config/opencode/opencode.json`:
|
|
57
|
+
|
|
58
|
+
```json
|
|
59
|
+
{
|
|
60
|
+
"plugin": ["openviking-opencode"]
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
OpenCode will automatically install the plugin via bun on next startup. No other steps needed — the skill is installed automatically.
|
|
65
|
+
|
|
66
|
+
## Usage
|
|
67
|
+
|
|
68
|
+
### Index a repository
|
|
69
|
+
|
|
70
|
+
Once OpenViking is running, index any GitHub repo or local project:
|
|
71
|
+
|
|
72
|
+
```
|
|
73
|
+
# In OpenCode, just ask:
|
|
74
|
+
"Add https://github.com/tiangolo/fastapi to OpenViking"
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Indexing runs in the background. Small repos (~100 files) take 2–5 min; large repos (500+ files) can take 20–60 min. Search works progressively as files are indexed.
|
|
78
|
+
|
|
79
|
+
### Search automatically
|
|
80
|
+
|
|
81
|
+
Once repos are indexed, the AI will proactively search them when relevant — no need to ask:
|
|
82
|
+
|
|
83
|
+
```
|
|
84
|
+
"How does fastapi handle dependency injection?"
|
|
85
|
+
→ AI automatically searches viking://resources/fastapi/ before answering
|
|
86
|
+
|
|
87
|
+
"How is authentication implemented in my-project?"
|
|
88
|
+
→ AI automatically searches viking://resources/my-project/ before answering
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Trigger manually
|
|
92
|
+
|
|
93
|
+
You can also explicitly invoke the skill:
|
|
94
|
+
|
|
95
|
+
```
|
|
96
|
+
"Use openviking to find how JWT tokens are verified in my projects"
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Best Practices
|
|
100
|
+
|
|
101
|
+
**Keep the server running persistently**
|
|
102
|
+
|
|
103
|
+
The plugin auto-starts the server when OpenCode launches, but for the best experience start it as a background service:
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
openviking serve --config ~/.openviking/ov.conf > /tmp/openviking.log 2>&1 &
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Or add it to your shell profile (`~/.zshrc` / `~/.bashrc`):
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
# Auto-start OpenViking if not already running
|
|
113
|
+
openviking health 2>/dev/null || openviking serve --config ~/.openviking/ov.conf > /tmp/openviking.log 2>&1 &
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
**Use `viking://resources/` for all code repos**
|
|
117
|
+
|
|
118
|
+
Always index under `viking://resources/` — this is the scope the plugin and skill are configured to search:
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
openviking add-resource https://github.com/owner/repo --to viking://resources/
|
|
122
|
+
openviking add-resource /path/to/project --to viking://resources/
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
**Index repos you reference often**
|
|
126
|
+
|
|
127
|
+
The more repos indexed, the more useful the AI becomes. Good candidates:
|
|
128
|
+
- Internal libraries your project depends on
|
|
129
|
+
- Open source libraries you frequently look up
|
|
130
|
+
- Other projects in your monorepo
|
|
131
|
+
|
|
132
|
+
**Don't index everything**
|
|
133
|
+
|
|
134
|
+
Avoid indexing repos you rarely touch — it adds noise to search results. Use `openviking rm viking://resources/repo --recursive` to remove repos you no longer need.
|
|
135
|
+
|
|
136
|
+
## Troubleshooting
|
|
137
|
+
|
|
138
|
+
| Symptom | Fix |
|
|
139
|
+
|---------|-----|
|
|
140
|
+
| Toast: "openviking 未安装" | Run `pip install openviking` |
|
|
141
|
+
| Toast: "未找到 ov.conf" | Create and configure `~/.openviking/ov.conf` |
|
|
142
|
+
| Toast: "服务启动失败" | Check logs: `cat /tmp/openviking.log` |
|
|
143
|
+
| AI doesn't search automatically | Ask "what repos do you have in openviking?" — if it can't answer, restart OpenCode |
|
|
144
|
+
| Stale repo list | Restart OpenCode to refresh the repo cache |
|
|
145
|
+
|
|
146
|
+
## License
|
|
147
|
+
|
|
148
|
+
Apache-2.0
|
package/index.mjs
CHANGED
|
@@ -7,23 +7,54 @@ import { fileURLToPath } from "url"
|
|
|
7
7
|
|
|
8
8
|
const execAsync = promisify(exec)
|
|
9
9
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
10
|
+
const OV_CONF = join(homedir(), ".openviking", "ov.conf")
|
|
11
|
+
|
|
12
|
+
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
async function run(cmd, opts = {}) {
|
|
15
|
+
return execAsync(cmd, { timeout: 10000, ...opts })
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function isInstalled() {
|
|
19
|
+
try {
|
|
20
|
+
await run("openviking --version")
|
|
21
|
+
return true
|
|
22
|
+
} catch {
|
|
23
|
+
return false
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function isHealthy() {
|
|
28
|
+
try {
|
|
29
|
+
await run("openviking health")
|
|
30
|
+
return true
|
|
31
|
+
} catch {
|
|
32
|
+
return false
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function startServer() {
|
|
37
|
+
// Start in background, wait up to 30s for healthy
|
|
38
|
+
await run("openviking serve --config " + OV_CONF + " > /tmp/openviking.log 2>&1 &")
|
|
39
|
+
for (let i = 0; i < 10; i++) {
|
|
40
|
+
await new Promise((r) => setTimeout(r, 3000))
|
|
41
|
+
if (await isHealthy()) return true
|
|
42
|
+
}
|
|
43
|
+
return false
|
|
44
|
+
}
|
|
10
45
|
|
|
11
46
|
// ── Skill auto-install ────────────────────────────────────────────────────────
|
|
12
47
|
|
|
13
48
|
function installSkill() {
|
|
14
49
|
const src = join(__dirname, "skills", "openviking", "SKILL.md")
|
|
15
50
|
const dest = join(homedir(), ".config", "opencode", "skills", "openviking", "SKILL.md")
|
|
16
|
-
const destDir = dirname(dest)
|
|
17
51
|
try {
|
|
18
|
-
if (!existsSync(
|
|
52
|
+
if (!existsSync(dirname(dest))) mkdirSync(dirname(dest), { recursive: true })
|
|
19
53
|
const content = readFileSync(src, "utf8")
|
|
20
|
-
// Only overwrite if content changed (avoid unnecessary disk writes)
|
|
21
54
|
if (!existsSync(dest) || readFileSync(dest, "utf8") !== content) {
|
|
22
55
|
writeFileSync(dest, content, "utf8")
|
|
23
56
|
}
|
|
24
|
-
} catch {
|
|
25
|
-
// Non-fatal — skill stays as-is or isn't installed
|
|
26
|
-
}
|
|
57
|
+
} catch {}
|
|
27
58
|
}
|
|
28
59
|
|
|
29
60
|
// ── Repo context cache ────────────────────────────────────────────────────────
|
|
@@ -37,12 +68,10 @@ async function loadRepos() {
|
|
|
37
68
|
if (cachedRepos !== null && now - lastFetchTime < CACHE_TTL_MS) return
|
|
38
69
|
|
|
39
70
|
try {
|
|
40
|
-
const { stdout } = await
|
|
41
|
-
"openviking --output json ls viking://resources/ --abs-limit 2000"
|
|
42
|
-
{ timeout: 8000 }
|
|
71
|
+
const { stdout } = await run(
|
|
72
|
+
"openviking --output json ls viking://resources/ --abs-limit 2000"
|
|
43
73
|
)
|
|
44
|
-
const
|
|
45
|
-
const items = parsed?.result ?? []
|
|
74
|
+
const items = JSON.parse(stdout)?.result ?? []
|
|
46
75
|
const repos = items
|
|
47
76
|
.filter((item) => item.uri?.startsWith("viking://resources/"))
|
|
48
77
|
.map((item) => {
|
|
@@ -51,14 +80,43 @@ async function loadRepos() {
|
|
|
51
80
|
? `- **${name}** (${item.uri})\n ${item.abstract}`
|
|
52
81
|
: `- **${name}** (${item.uri})`
|
|
53
82
|
})
|
|
54
|
-
|
|
55
83
|
if (repos.length > 0) {
|
|
56
84
|
cachedRepos = repos.join("\n")
|
|
57
85
|
lastFetchTime = now
|
|
58
86
|
}
|
|
59
|
-
} catch {
|
|
60
|
-
|
|
87
|
+
} catch {}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ── Init: check deps, start server if needed ─────────────────────────────────
|
|
91
|
+
|
|
92
|
+
async function init(client) {
|
|
93
|
+
const toast = (message, variant = "warning") =>
|
|
94
|
+
client.tui.showToast({
|
|
95
|
+
body: { title: "OpenViking", message, variant, duration: 8000 },
|
|
96
|
+
}).catch(() => {})
|
|
97
|
+
|
|
98
|
+
// 1. openviking 没装
|
|
99
|
+
if (!(await isInstalled())) {
|
|
100
|
+
await toast("openviking 未安装,请运行: pip install openviking", "error")
|
|
101
|
+
return false
|
|
61
102
|
}
|
|
103
|
+
|
|
104
|
+
// 2. ov.conf 不存在(装了但没配置)
|
|
105
|
+
if (!existsSync(OV_CONF)) {
|
|
106
|
+
await toast("未找到 ~/.openviking/ov.conf,请先配置 API keys 后再启动服务", "warning")
|
|
107
|
+
return false
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// 3. 服务没跑 → 静默自动启动
|
|
111
|
+
if (!(await isHealthy())) {
|
|
112
|
+
const started = await startServer()
|
|
113
|
+
if (!started) {
|
|
114
|
+
await toast("openviking 服务启动失败,查看日志: /tmp/openviking.log", "error")
|
|
115
|
+
return false
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return true
|
|
62
120
|
}
|
|
63
121
|
|
|
64
122
|
// ── Plugin export ─────────────────────────────────────────────────────────────
|
|
@@ -66,9 +124,14 @@ async function loadRepos() {
|
|
|
66
124
|
/**
|
|
67
125
|
* @type {import('@opencode-ai/plugin').Plugin}
|
|
68
126
|
*/
|
|
69
|
-
export async function OpenVikingPlugin() {
|
|
127
|
+
export async function OpenVikingPlugin({ client }) {
|
|
70
128
|
installSkill()
|
|
71
|
-
|
|
129
|
+
|
|
130
|
+
// 后台初始化,不阻塞 opencode 启动
|
|
131
|
+
Promise.resolve().then(async () => {
|
|
132
|
+
const ready = await init(client)
|
|
133
|
+
if (ready) await loadRepos()
|
|
134
|
+
})
|
|
72
135
|
|
|
73
136
|
return {
|
|
74
137
|
"experimental.chat.system.transform": (_input, output) => {
|
|
@@ -83,8 +146,11 @@ export async function OpenVikingPlugin() {
|
|
|
83
146
|
},
|
|
84
147
|
|
|
85
148
|
"session.created": async () => {
|
|
86
|
-
|
|
87
|
-
|
|
149
|
+
const ready = await init(client)
|
|
150
|
+
if (ready) {
|
|
151
|
+
cachedRepos = null
|
|
152
|
+
await loadRepos()
|
|
153
|
+
}
|
|
88
154
|
},
|
|
89
155
|
}
|
|
90
156
|
}
|
package/package.json
CHANGED