Files
OpenViking/examples/opencode/plugin/index.mjs
T

152 lines
5.0 KiB
JavaScript

import { exec } from "child_process"
import { promisify } from "util"
import { readFileSync, mkdirSync, writeFileSync, existsSync } from "fs"
import { homedir } from "os"
import { join, dirname } from "path"
import { fileURLToPath } from "url"
const execAsync = promisify(exec)
const __dirname = dirname(fileURLToPath(import.meta.url))
const OV_CONF = join(homedir(), ".openviking", "ov.conf")
// ── Helpers ───────────────────────────────────────────────────────────────────
async function run(cmd, opts = {}) {
return execAsync(cmd, { timeout: 10000, ...opts })
}
async function isHealthy() {
try {
await run("ov health", { timeout: 3000 })
return true
} catch {
return false
}
}
async function startServer() {
// Start in background, wait up to 30s for healthy
await run("openviking-server --config " + OV_CONF + " > /tmp/openviking.log 2>&1 &")
for (let i = 0; i < 10; i++) {
await new Promise((r) => setTimeout(r, 3000))
if (await isHealthy()) return true
}
return false
}
// ── Skill auto-install ────────────────────────────────────────────────────────
function installSkill() {
const src = join(__dirname, "skills", "openviking", "SKILL.md")
const dest = join(homedir(), ".config", "opencode", "skills", "openviking", "SKILL.md")
try {
if (!existsSync(dirname(dest))) mkdirSync(dirname(dest), { recursive: true })
const content = readFileSync(src, "utf8")
if (!existsSync(dest) || readFileSync(dest, "utf8") !== content) {
writeFileSync(dest, content, "utf8")
}
} catch {}
}
// ── Repo context cache ────────────────────────────────────────────────────────
let cachedRepos = null
let lastFetchTime = 0
const CACHE_TTL_MS = 60 * 1000
async function loadRepos() {
const now = Date.now()
if (cachedRepos !== null && now - lastFetchTime < CACHE_TTL_MS) return
try {
const { stdout } = await run(
"ov --output json ls viking://resources/ --abs-limit 2000"
)
const items = JSON.parse(stdout)?.result ?? []
const repos = items
.filter((item) => item.uri?.startsWith("viking://resources/"))
.map((item) => {
const name = item.uri.replace("viking://resources/", "").replace(/\/$/, "")
return item.abstract
? `- **${name}** (${item.uri})\n ${item.abstract}`
: `- **${name}** (${item.uri})`
})
if (repos.length > 0) {
cachedRepos = repos.join("\n")
lastFetchTime = now
}
} catch {}
}
// ── Init: check deps, start server if needed ─────────────────────────────────
async function init(client) {
const toast = (message, variant = "warning") =>
client.tui.showToast({
body: { title: "OpenViking", message, variant, duration: 8000 },
}).catch(() => {})
// 服务已在跑,直接返回
if (await isHealthy()) return true
// 没跑,先看是哪种情况
try {
await run("command -v ov", { timeout: 2000 })
} catch {
// command not found → 没装
await toast("openviking 未安装,请运行: pip install openviking", "error")
return false
}
// 装了但没有配置文件 → 无法启动
if (!existsSync(OV_CONF)) {
await toast("未找到 ~/.openviking/ov.conf,请先配置 API keys 后再启动服务", "warning")
return false
}
// 装了有配置,服务没跑 → 静默自动启动
const started = await startServer()
if (!started) {
await toast("openviking 服务启动失败,查看日志: /tmp/openviking.log", "error")
return false
}
return true
}
// ── Plugin export ─────────────────────────────────────────────────────────────
/**
* @type {import('@opencode-ai/plugin').Plugin}
*/
export async function OpenVikingPlugin({ client }) {
installSkill()
// 后台初始化,不阻塞 opencode 启动
Promise.resolve().then(async () => {
const ready = await init(client)
if (ready) await loadRepos()
})
return {
"experimental.chat.system.transform": (_input, output) => {
if (!cachedRepos) return
output.system.push(
`## OpenViking — Indexed Code Repositories\n\n` +
`The following repos are semantically indexed and searchable.\n` +
`When the user asks about any of these projects or their internals, ` +
`you MUST proactively load skill("openviking") and use the correct ov commands to search and retrieve content before answering.\n\n` +
cachedRepos
)
},
"session.created": async () => {
const ready = await init(client)
if (ready) {
cachedRepos = null
await loadRepos()
}
},
}
}