/** * context-panel для OpenCode 2: панель «Токены» в веб-интерфейсе — без отдельного процесса. * * Плагин поднимает внутри процесса OpenCode тот же прокси, что server.mjs (startPanel): он * слушает CTX_PANEL_LISTEN (4196), пропускает всё на CTX_PANEL_TARGET (4197, порт самого * `opencode serve`) и вставляет в страницы panel.js. Встроить панель прямо в порт OpenCode * нельзя: веб-интерфейс зашит в бинарник, а свои HTTP-маршруты плагину добавить нечем. * * Что учтено (проверено на 2.1.3): * - плагин грузится лениво — при первом запросе к API проекта, а не при старте `serve`. * Поэтому обёртка после старта сама дёргает API (bin/opencode-web), иначе на 4196 пусто; * - модуль импортируется заново для каждого проекта — прокси один на процесс (globalThis); * - при dispose прокси не закрывается: браузер ходит через него, и без него OpenCode не * получил бы запрос, который загрузит плагин снова. Живёт, пока жив процесс. * * Выключить: OPENCODE_CONTEXT_PANEL=0. База — $XDG_DATA_HOME/opencode/opencode.db или CTX_PANEL_DB. * Подключение: файл в /opencode/plugins/ — bin/opencode-web делает ссылку сам. */ import { appendFile, mkdir } from "node:fs/promises" import { homedir } from "node:os" import { dirname, join } from "node:path" import { realpathSync } from "node:fs" import { fileURLToPath, pathToFileURL } from "node:url" const LOG_DIR = join(process.env.XDG_DATA_HOME || join(homedir(), ".local/share"), "opencode/log") const LOG_FILE = join(LOG_DIR, "context-panel.log") async function log(line: string) { try { await mkdir(LOG_DIR, { recursive: true }) await appendFile(LOG_FILE, `${new Date().toISOString()} ${line}\n`) } catch {} } const KEY = Symbol.for("opencode-plugins.context-panel") const state = globalThis as { [KEY]?: Promise } function start() { // Файл подключён ссылкой из plugins/ — server.mjs ищем рядом с настоящим файлом const here = dirname(realpathSync(fileURLToPath(import.meta.url))) const opts = { listen: process.env.CTX_PANEL_LISTEN ?? 4196, target: process.env.CTX_PANEL_TARGET ?? 4197, ...(process.env.CTX_PANEL_DB ? { db: process.env.CTX_PANEL_DB } : {}), } return import(pathToFileURL(join(here, "server.mjs")).href) .then((m) => m.startPanel(opts)) .then( () => log(`listening ${opts.listen} → ${opts.target}`), (e) => log(`не запустилась: ${e?.code ?? e?.message ?? e}`), ) } export default { id: "context-panel", setup: async () => { if (process.env.OPENCODE_CONTEXT_PANEL === "0") return state[KEY] ??= start() await state[KEY] }, }