From cb17ce8d2a78c7b7cbef059883b6a0581f316c7e Mon Sep 17 00:00:00 2001 From: Dmitry Shilov Date: Thu, 27 Aug 2026 18:12:48 +0300 Subject: [PATCH] feat(opencode): Windows PowerShell installer and Desktop key file CLI and Desktop share %USERPROFILE%\.config\opencode. Persist the API key in .arka-api-key so GUI apps do not need a login-shell export. --- README.md | 16 +- opencode/README.md | 110 ++++++++- opencode/install-arka-provider.ps1 | 327 +++++++++++++++++++++++++ opencode/install-arka-provider.sh | 48 +++- opencode/plugins/arka-ai.ts | 39 ++- opencode/skills/arka-provider/SKILL.md | 3 +- 6 files changed, 531 insertions(+), 12 deletions(-) create mode 100644 opencode/install-arka-provider.ps1 diff --git a/README.md b/README.md index 354bbe1..f88ca80 100644 --- a/README.md +++ b/README.md @@ -10,12 +10,18 @@ |-------|-----| | [`opencode/`](opencode/) | Провайдер OpenCode **Arka-AI**: установщик, плагин, skill, каталог чат-моделей | -Однострочник OpenCode: +Однострочник OpenCode (macOS/Linux/WSL): ```bash curl -fsSL https://git.arka-ai.ru/arka/public/raw/branch/main/opencode/install-arka-provider.sh | bash ``` +Windows (PowerShell, CLI и Desktop): + +```powershell +irm https://git.arka-ai.ru/arka/public/raw/branch/main/opencode/install-arka-provider.ps1 | iex +``` + --- Public Arka AI downloads, docs, and scripts. Anonymous raw URLs, no token. @@ -28,8 +34,14 @@ One top-level folder per product. No private source. |--------|------| | [`opencode/`](opencode/) | OpenCode **Arka-AI** provider: installer, plugin, skill, chat catalog | -OpenCode one-liner: +OpenCode one-liner (macOS/Linux/WSL): ```bash curl -fsSL https://git.arka-ai.ru/arka/public/raw/branch/main/opencode/install-arka-provider.sh | bash ``` + +Windows (PowerShell, CLI and Desktop): + +```powershell +irm https://git.arka-ai.ru/arka/public/raw/branch/main/opencode/install-arka-provider.ps1 | iex +``` diff --git a/opencode/README.md b/opencode/README.md index 3cc7deb..0acea0b 100644 --- a/opencode/README.md +++ b/opencode/README.md @@ -56,6 +56,58 @@ curl -fsSL https://git.arka-ai.ru/arka/public/raw/branch/main/opencode/install-a Пока сессия не перезапущена, разговор идёт на старой модели. Это нормально: установщик не требует Arka. +### Windows (CLI и Desktop) + +Официальный каталог конфига OpenCode на Windows — **`%USERPROFILE%\.config\opencode`** (то же, что `C:\Users\<имя>\.config\opencode`). Туда же смотрит **OpenCode Desktop**. Не кладите плагин только в `%APPDATA%\opencode` — Desktop его оттуда не читает, если этого каталога ещё нет. + +PowerShell: + +```powershell +irm https://git.arka-ai.ru/arka/public/raw/branch/main/opencode/install-arka-provider.ps1 | iex +``` + +С ключом и моделью по умолчанию: + +```powershell +iwr -useb https://git.arka-ai.ru/arka/public/raw/branch/main/opencode/install-arka-provider.ps1 ` + -OutFile $env:TEMP\install-arka.ps1 +powershell -ExecutionPolicy Bypass -File $env:TEMP\install-arka.ps1 ` + -Key $env:ARKA_API_KEY -SetDefault DeepSeek-V4-Pro +``` + +cmd.exe: + +```bat +powershell -NoProfile -ExecutionPolicy Bypass -Command "irm https://git.arka-ai.ru/arka/public/raw/branch/main/opencode/install-arka-provider.ps1 | iex" +``` + +Git Bash / WSL — linux-скрипт, как обычно. Это **другой** OpenCode (домашний каталог WSL), не native Windows Desktop. + +Скрипт пишет: + +- `%USERPROFILE%\.config\opencode\plugins\arka-ai.ts` +- `%USERPROFILE%\.config\opencode\skills\arka-provider\SKILL.md` +- `%USERPROFILE%\.config\opencode\.arka-api-key` (Desktop читает ключ отсюда, login-shell не нужен) +- пользовательскую переменную среды `ARKA_API_KEY` (подхватится после полного перезапуска GUI) +- если уже есть `%APPDATA%\opencode` — копия туда же, на всякий случай + +Затем **полностью закройте** OpenCode Desktop (иконка в трее тоже) и CLI, откройте снова, `/models`. + +### OpenCode Desktop (все ОС) + +Desktop — это окно + тот же sidecar CLI. Плагины те же файлы, что и у терминала. + +| ОС | Плагин / skill / ключ | +|----|------------------------| +| Windows | `%USERPROFILE%\.config\opencode\` | +| macOS / Linux | `~/.config/opencode/` | + +1. Поставьте провайдер скриптом выше (bash или `.ps1`). +2. **Quit** приложения, не только закрытие окна. На Windows проверьте трей. +3. Запустите Desktop снова. `/models` → **Arka-AI**. + +`~/.bashrc` Desktop **не читает**. Поэтому установщик кладёт ключ в `.arka-api-key` и (на Windows) в User environment. Если модели есть в списке, но запросы 401 — ключ не попал в процесс: проверьте файл ключа и перезапуск. + ### Что ставится | Куда | Что | @@ -168,6 +220,61 @@ What happens: Until you restart, this session keeps the old model. That is expected: the installer does not need Arka. +### Windows (CLI and Desktop) + +OpenCode’s real config dir on Windows is **`%USERPROFILE%\.config\opencode`** +(`C:\Users\\.config\opencode`). **OpenCode Desktop uses that same folder.** +Putting the plugin only in `%APPDATA%\opencode` is a common miss — Desktop will not +load it unless that directory already exists and happens to be in use. + +PowerShell: + +```powershell +irm https://git.arka-ai.ru/arka/public/raw/branch/main/opencode/install-arka-provider.ps1 | iex +``` + +With a key and default model: + +```powershell +iwr -useb https://git.arka-ai.ru/arka/public/raw/branch/main/opencode/install-arka-provider.ps1 ` + -OutFile $env:TEMP\install-arka.ps1 +powershell -ExecutionPolicy Bypass -File $env:TEMP\install-arka.ps1 ` + -Key $env:ARKA_API_KEY -SetDefault DeepSeek-V4-Pro +``` + +cmd.exe: + +```bat +powershell -NoProfile -ExecutionPolicy Bypass -Command "irm https://git.arka-ai.ru/arka/public/raw/branch/main/opencode/install-arka-provider.ps1 | iex" +``` + +Git Bash / WSL use the Linux installer. That OpenCode is **not** the native Windows Desktop app (different home directory). + +The script writes: + +- `%USERPROFILE%\.config\opencode\plugins\arka-ai.ts` +- `%USERPROFILE%\.config\opencode\skills\arka-provider\SKILL.md` +- `%USERPROFILE%\.config\opencode\.arka-api-key` (Desktop reads the key here; no login shell needed) +- User environment variable `ARKA_API_KEY` (GUI apps pick this up after a full restart) +- a copy under `%APPDATA%\opencode` only if that directory already exists + +Then **fully quit** OpenCode Desktop (tray icon too) and the CLI, start again, `/models`. + +### OpenCode Desktop (all platforms) + +Desktop is a window plus the same sidecar CLI. Plugins are the same files as the terminal. + +| OS | Plugin / skill / key | +|----|----------------------| +| Windows | `%USERPROFILE%\.config\opencode\` | +| macOS / Linux | `~/.config/opencode/` | + +1. Run the installer above (bash or `.ps1`). +2. **Quit** the app, not just the window. On Windows, check the tray. +3. Start Desktop again. `/models` → **Arka-AI**. + +Desktop does **not** read `~/.bashrc`. That is why the installer writes `.arka-api-key` and (on Windows) a User env var. If models show up but requests return 401, the key never reached the process: check the key file and restart. + ### What the installer writes | Path | Role | @@ -228,7 +335,8 @@ Inference still uses `ARKA_API_KEY` against `https://api.arka-ai.ru/v1`. | File | Role | |------|------| -| `install-arka-provider.sh` | Self-contained installer (the one-liner) | +| `install-arka-provider.sh` | POSIX installer (macOS/Linux/WSL/Git Bash) | +| `install-arka-provider.ps1` | Windows installer (CLI + Desktop) | | `plugins/arka-ai.ts` | Plugin source (embedded in the installer) | | `skills/arka-provider/SKILL.md` | Skill source (embedded in the installer) | | `catalog.json` | Filtered chat catalog, refreshed daily at 05:00 Europe/Moscow | diff --git a/opencode/install-arka-provider.ps1 b/opencode/install-arka-provider.ps1 new file mode 100644 index 0000000..5d5ad00 --- /dev/null +++ b/opencode/install-arka-provider.ps1 @@ -0,0 +1,327 @@ +# install-arka-provider.ps1 — OpenCode Arka-AI provider for Windows +# (CLI and Desktop). Idempotent. Canonical files also live next to this script. +# +# One-liner (PowerShell): +# irm https://git.arka-ai.ru/arka/public/raw/branch/main/opencode/install-arka-provider.ps1 | iex +# +# With key + default model: +# iwr -useb URL -OutFile $env:TEMP\install-arka.ps1 +# powershell -ExecutionPolicy Bypass -File $env:TEMP\install-arka.ps1 -Key $env:ARKA_API_KEY -SetDefault DeepSeek-V4-Pro +# +# cmd.exe: +# powershell -NoProfile -ExecutionPolicy Bypass -Command "irm URL | iex" + +[CmdletBinding()] +param( + [string]$Key, + [string]$SetDefault +) + +$ErrorActionPreference = 'Stop' +if (-not $Key) { $Key = $env:ARKA_API_KEY } + +function Get-ConfigDir { + if ($env:XDG_CONFIG_HOME) { return (Join-Path $env:XDG_CONFIG_HOME 'opencode') } + return (Join-Path $HOME '.config\opencode') +} + +function Get-InstallDirs { + $dirs = New-Object System.Collections.Generic.List[string] + $canonical = Get-ConfigDir + [void]$dirs.Add($canonical) + if ($env:APPDATA) { + $legacy = Join-Path $env:APPDATA 'opencode' + if ((Test-Path $legacy) -and ($legacy -ne $canonical)) { [void]$dirs.Add($legacy) } + } + return $dirs +} + +if (-not $Key) { + $entered = Read-Host 'ARKA_API_KEY (leave blank to skip)' + if ($entered) { $Key = $entered } +} + +$plugin = @' +import { homedir } from "node:os" +import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs" +import { join } from "node:path" + +const PROVIDER_ID = "Arka-AI" +const API_URL = "https://api.arka-ai.ru/v1" +const CATALOG_URL = + "https://git.arka-ai.ru/arka/public/raw/branch/main/opencode/catalog.json" +const TTL_MS = 24 * 60 * 60 * 1000 + +function configDir(): string { + const xdg = process.env.XDG_CONFIG_HOME + if (xdg) return join(xdg, "opencode") + return join(homedir(), ".config", "opencode") +} + +function cacheDir(): string { + const xdg = process.env.XDG_CACHE_HOME + if (xdg) return join(xdg, "opencode") + return join(homedir(), ".cache", "opencode") +} + +const CACHE_FILE = join(cacheDir(), "arka-ai-models.json") +const KEY_FILE = join(configDir(), ".arka-api-key") + +function readApiKey(): string | undefined { + const fromEnv = process.env.ARKA_API_KEY?.trim() + if (fromEnv) return fromEnv + try { + if (existsSync(KEY_FILE)) { + const fromFile = readFileSync(KEY_FILE, "utf8").trim() + if (fromFile) return fromFile + } + } catch {} + return undefined +} + +// Ids that must always be exposed, even if the filter would drop them. +const FORCED_MODELS = [ + "DeepSeek-V4-Pro", + "deepseek-v4-flash-0731", + "qwen3.8-max", + "kimi-k3", + "MiniMax-M3", + "GLM-5.3", +] + +// Heuristic: drop non-chat models (TTS/ASR/image/video/OCR/embeddings/rerankers…) +// from the catalog so they don't clutter the /models picker. +// Keep in sync with scripts/sync-catalog.py +const NON_CHAT = new RegExp( + [ + "tts", "asr", "audio", "speech", "voice", "whisper", "sensevoice", + "image", "(^|[.-])img", "wan", "vidu", "video", "sora", "flux", + "diffus", "kolors", "cogview", "ocr", "embedding", "(^|[.-])embed", + "rerank", "clip", "moderation", "(^|[.-])filter", "detect", "classif", + "(^|[.-])location", "(^|[.-])search", "comfyui", "meme", "hello", + "(^|[.-])sam", "florence", "esrgan", "rmbg", "background", "poster", + "(^|[.-])face", "(^|[.-])lip", "translate", "(^|[.-])mt([2-]|$)", + "(^|[.-])bge", "(^|[.-])e5", "(^|[.-])jina", "(^|[.-])mpnet", + "spark-tts", "cosyvoice", "chattts", "indextts", "step-audio", + "fish-speech", "cosy", "mate-gguf", "tokenizer", + ].join("|"), + "i", +) + +type ModelMap = Record +type Cache = { ts: number; models: ModelMap } + +function loadCache(): Cache | null { + try { + if (existsSync(CACHE_FILE)) return JSON.parse(readFileSync(CACHE_FILE, "utf8")) + } catch {} + return null +} + +function saveCache(models: ModelMap): void { + try { + mkdirSync(cacheDir(), { recursive: true }) + writeFileSync(CACHE_FILE, JSON.stringify({ ts: Date.now(), models })) + } catch {} +} + +async function fetchJson( + url: string, + headers: Record, + timeoutMs: number, +): Promise { + const ctrl = new AbortController() + const timer = setTimeout(() => ctrl.abort(), timeoutMs) + try { + const res = await fetch(url, { headers, signal: ctrl.signal }) + if (!res.ok) throw new Error(`HTTP ${res.status}`) + return await res.json() + } finally { + clearTimeout(timer) + } +} + +function idsFromPayload(json: unknown): string[] { + const data = (json as { data?: Array<{ id: string }> }).data + return (data ?? []).map((m) => m.id).filter(Boolean) +} + +function toModelMap(ids: string[], filter: boolean): ModelMap { + const models: ModelMap = {} + for (const id of ids) { + if (!filter || FORCED_MODELS.includes(id) || !NON_CHAT.test(id)) { + models[id] = { name: id } + } + } + return models +} + +async function resolveModels(): Promise { + const cache = loadCache() + if (cache && Date.now() - cache.ts < TTL_MS) return cache.models + + try { + const models = toModelMap(idsFromPayload(await fetchJson(CATALOG_URL, {}, 5000)), false) + if (Object.keys(models).length) { + saveCache(models) + return models + } + } catch {} + + try { + const key = readApiKey() + const json = await fetchJson( + `${API_URL}/models`, + key ? { Authorization: `Bearer ${key}` } : {}, + 5000, + ) + const models = toModelMap(idsFromPayload(json), true) + saveCache(models) + return models + } catch { + return cache?.models ?? null + } +} + +export default async function ArkaAIProviderPlugin() { + const models = await resolveModels() + const apiKey = readApiKey() + + return { + config: (cfg: any) => { + cfg.provider ??= {} + const prev = cfg.provider[PROVIDER_ID] ?? {} + + // Static models from opencode.json act as a fallback; the fetched + // catalog is layered on top so the configured default model never + // disappears even if the catalog drops it. + // apiKey from env or ~/.config/opencode/.arka-api-key so Desktop / + // Windows GUI works without a login-shell export. + cfg.provider[PROVIDER_ID] = { + name: "Arka-AI", + api: API_URL, + env: ["ARKA_API_KEY"], + ...prev, + models: { ...(prev.models ?? {}), ...(models ?? {}) }, + options: { + ...(prev.options ?? {}), + ...(apiKey ? { apiKey } : {}), + }, + } + }, + } +} +'@ + +$skill = @' +--- +name: arka-provider +description: Manage the custom Arka-AI (arka-ai.ru) LLM provider in opencode config. Use when the user needs to add, refresh, or fix the arka-ai.ru provider, sync its model catalog, change the Arka-AI default model, or when arka models are missing from /models. +--- + +# Arka-AI provider + +arka-ai.ru is **not** in models.dev, so opencode ships no built-in config for it. +It is wired up locally with a plugin + a static `provider` block in +`~/.config/opencode/opencode.json`. + +## How it works + +- Plugin: `~/.config/opencode/plugins/arka-ai.ts` + - On startup it loads the published chat catalog + `https://git.arka-ai.ru/arka/public/raw/branch/main/opencode/catalog.json` + (no API key; refreshed daily at 05:00 Europe/Moscow). + - If that fetch fails, it falls back to `https://api.arka-ai.ru/v1/models` + and filters out non-chat models (TTS/ASR/image/video/OCR/embedding/reranker/…) + via a heuristic regex so the `/models` picker stays clean. + - Caches the result at `~/.cache/opencode/arka-ai-models.json` (TTL 24h). + - Injects the provider into config via the `config` hook, layered over the + static block in `opencode.json`. +- Static block (`opencode.json` → `provider["Arka-AI"]`) holds + `name`, `api`, `env`, and a small fallback `models` map that is used when the + catalog is unreachable. +- Inference uses `ARKA_API_KEY` (env) or `~/.config/opencode/.arka-api-key` + (so OpenCode Desktop / Windows GUI works without a login shell). + +## Refresh the model list + +Catalog ~216 raw models / ~116 chat; the public snapshot updates at 05:00 +Europe/Moscow. Changes land in opencode only after cache expiry + restart +(config is not hot-reloaded). + +1. `rm -f ~/.cache/opencode/arka-ai-models.json` +2. Restart opencode (or run `opencode` again) — the plugin re-fetches on the + next cold start. + +Optionally inspect the live API (no key required for listing): + +```bash +curl -sS https://api.arka-ai.ru/v1/models | python3 -m json.tool +curl -sS https://git.arka-ai.ru/arka/public/raw/branch/main/opencode/catalog.json | python3 -m json.tool +``` + +## Change default model + +Edit `opencode.json` → `"model": "Arka-AI/"`. Use the IDs exactly as +returned by the catalog (e.g. `Arka-AI/DeepSeek-V4-Pro`, +`Arka-AI/qwen3.8-max`, `Arka-AI/kimi-k3`). + +## Adjust filtering + +Edit `NON_CHAT` (regex) or `FORCED_MODELS` in the plugin file **and** the same +lists in `scripts/sync-catalog.py` in `arka/public`, then re-run the installer +and restart. `FORCED_MODELS` keeps must-have chat models even if the heuristic +would drop them. +'@ + +$dirs = Get-InstallDirs +foreach ($conf in $dirs) { + $pluginDir = Join-Path $conf 'plugins' + $skillDir = Join-Path $conf 'skills\arka-provider' + New-Item -ItemType Directory -Force -Path $pluginDir | Out-Null + New-Item -ItemType Directory -Force -Path $skillDir | Out-Null + $pluginPath = Join-Path $pluginDir 'arka-ai.ts' + $skillPath = Join-Path $skillDir 'SKILL.md' + [System.IO.File]::WriteAllText($pluginPath, $plugin.TrimStart() + "`n") + [System.IO.File]::WriteAllText($skillPath, $skill.TrimStart() + "`n") + Write-Host "==> plugin $pluginPath" + Write-Host "==> skill $skillPath" + + if ($Key) { + $keyFile = Join-Path $conf '.arka-api-key' + [System.IO.File]::WriteAllText($keyFile, $Key.Trim() + "`n") + Write-Host "==> key $keyFile" + } + + if ($SetDefault) { + $confFile = Join-Path $conf 'opencode.json' + Write-Host "==> default Arka-AI/$SetDefault in $confFile" + $data = $null + if (Test-Path $confFile) { + $data = Get-Content -Raw -Encoding UTF8 $confFile | ConvertFrom-Json + } + if (-not $data) { $data = New-Object psobject } + $data | Add-Member -NotePropertyName '$schema' -NotePropertyValue 'https://opencode.ai/config.json' -Force + $data | Add-Member -NotePropertyName model -NotePropertyValue ("Arka-AI/" + $SetDefault) -Force + ($data | ConvertTo-Json -Depth 40) + "`n" | Out-File -FilePath $confFile -Encoding utf8 + } +} + +if ($Key) { + $env:ARKA_API_KEY = $Key + try { + [Environment]::SetEnvironmentVariable('ARKA_API_KEY', $Key, 'User') + Write-Host '==> User environment ARKA_API_KEY (Desktop / GUI pick this up after a full restart)' + } catch { + Write-Warning "could not set User environment variable: $_" + } +} else { + Write-Warning 'no ARKA_API_KEY; Desktop will not be able to call models until you set it' +} + +Write-Host '' +Write-Host 'Done.' +Write-Host 'Next:' +Write-Host ' 1. Fully quit OpenCode CLI *and* OpenCode Desktop (tray icon too), then start again.' +Write-Host ' 2. /models — provider Arka-AI should appear (~116 chat models).' +Write-Host ' 3. Refresh later: delete %USERPROFILE%\.cache\opencode\arka-ai-models.json and restart.' diff --git a/opencode/install-arka-provider.sh b/opencode/install-arka-provider.sh index 49153d6..4c0e8f7 100755 --- a/opencode/install-arka-provider.sh +++ b/opencode/install-arka-provider.sh @@ -56,9 +56,35 @@ const PROVIDER_ID = "Arka-AI" const API_URL = "https://api.arka-ai.ru/v1" const CATALOG_URL = "https://git.arka-ai.ru/arka/public/raw/branch/main/opencode/catalog.json" -const CACHE_FILE = join(homedir(), ".cache", "opencode", "arka-ai-models.json") const TTL_MS = 24 * 60 * 60 * 1000 +function configDir(): string { + const xdg = process.env.XDG_CONFIG_HOME + if (xdg) return join(xdg, "opencode") + return join(homedir(), ".config", "opencode") +} + +function cacheDir(): string { + const xdg = process.env.XDG_CACHE_HOME + if (xdg) return join(xdg, "opencode") + return join(homedir(), ".cache", "opencode") +} + +const CACHE_FILE = join(cacheDir(), "arka-ai-models.json") +const KEY_FILE = join(configDir(), ".arka-api-key") + +function readApiKey(): string | undefined { + const fromEnv = process.env.ARKA_API_KEY?.trim() + if (fromEnv) return fromEnv + try { + if (existsSync(KEY_FILE)) { + const fromFile = readFileSync(KEY_FILE, "utf8").trim() + if (fromFile) return fromFile + } + } catch {} + return undefined +} + // Ids that must always be exposed, even if the filter would drop them. const FORCED_MODELS = [ "DeepSeek-V4-Pro", @@ -100,7 +126,7 @@ function loadCache(): Cache | null { function saveCache(models: ModelMap): void { try { - mkdirSync(join(homedir(), ".cache", "opencode"), { recursive: true }) + mkdirSync(cacheDir(), { recursive: true }) writeFileSync(CACHE_FILE, JSON.stringify({ ts: Date.now(), models })) } catch {} } @@ -149,7 +175,7 @@ async function resolveModels(): Promise { } catch {} try { - const key = process.env.ARKA_API_KEY + const key = readApiKey() const json = await fetchJson( `${API_URL}/models`, key ? { Authorization: `Bearer ${key}` } : {}, @@ -165,6 +191,7 @@ async function resolveModels(): Promise { export default async function ArkaAIProviderPlugin() { const models = await resolveModels() + const apiKey = readApiKey() return { config: (cfg: any) => { @@ -174,12 +201,18 @@ export default async function ArkaAIProviderPlugin() { // Static models from opencode.json act as a fallback; the fetched // catalog is layered on top so the configured default model never // disappears even if the catalog drops it. + // apiKey from env or ~/.config/opencode/.arka-api-key so Desktop / + // Windows GUI works without a login-shell export. cfg.provider[PROVIDER_ID] = { name: "Arka-AI", api: API_URL, env: ["ARKA_API_KEY"], ...prev, models: { ...(prev.models ?? {}), ...(models ?? {}) }, + options: { + ...(prev.options ?? {}), + ...(apiKey ? { apiKey } : {}), + }, } }, } @@ -214,7 +247,8 @@ It is wired up locally with a plugin + a static `provider` block in - Static block (`opencode.json` → `provider["Arka-AI"]`) holds `name`, `api`, `env`, and a small fallback `models` map that is used when the catalog is unreachable. -- Inference still uses `ARKA_API_KEY` against `https://api.arka-ai.ru/v1`. +- Inference uses `ARKA_API_KEY` (env) or `~/.config/opencode/.arka-api-key` + (so OpenCode Desktop / Windows GUI works without a login shell). ## Refresh the model list @@ -268,6 +302,10 @@ if [ -n "$KEY" ]; then echo "==> wrote ARKA_API_KEY to $RC" fi export ARKA_API_KEY="$KEY" + KEY_FILE="$CONF_DIR/.arka-api-key" + printf '%s\n' "$KEY" > "$KEY_FILE" + chmod 600 "$KEY_FILE" 2>/dev/null || true + echo "==> wrote API key to $KEY_FILE (CLI + Desktop)" else echo "==> warning: no ARKA_API_KEY set; export it in your shell before starting opencode" >&2 fi @@ -299,6 +337,6 @@ echo " - provider plugin: $PLUGIN_DIR/arka-ai.ts" echo " - skill: $SKILL_DIR/SKILL.md" echo echo "Next steps:" -echo " 1. Restart opencode (open a new shell so ARKA_API_KEY is exported)." +echo " 1. Fully quit and restart OpenCode (CLI or Desktop). Plugins load only at startup." echo " 2. Run /models — the Arka-AI catalog (~116 chat models) loads on first start." echo " 3. Optional: refresh catalog later with: rm -f ~/.cache/opencode/arka-ai-models.json" diff --git a/opencode/plugins/arka-ai.ts b/opencode/plugins/arka-ai.ts index 676b776..e8211ad 100644 --- a/opencode/plugins/arka-ai.ts +++ b/opencode/plugins/arka-ai.ts @@ -6,9 +6,35 @@ const PROVIDER_ID = "Arka-AI" const API_URL = "https://api.arka-ai.ru/v1" const CATALOG_URL = "https://git.arka-ai.ru/arka/public/raw/branch/main/opencode/catalog.json" -const CACHE_FILE = join(homedir(), ".cache", "opencode", "arka-ai-models.json") const TTL_MS = 24 * 60 * 60 * 1000 +function configDir(): string { + const xdg = process.env.XDG_CONFIG_HOME + if (xdg) return join(xdg, "opencode") + return join(homedir(), ".config", "opencode") +} + +function cacheDir(): string { + const xdg = process.env.XDG_CACHE_HOME + if (xdg) return join(xdg, "opencode") + return join(homedir(), ".cache", "opencode") +} + +const CACHE_FILE = join(cacheDir(), "arka-ai-models.json") +const KEY_FILE = join(configDir(), ".arka-api-key") + +function readApiKey(): string | undefined { + const fromEnv = process.env.ARKA_API_KEY?.trim() + if (fromEnv) return fromEnv + try { + if (existsSync(KEY_FILE)) { + const fromFile = readFileSync(KEY_FILE, "utf8").trim() + if (fromFile) return fromFile + } + } catch {} + return undefined +} + // Ids that must always be exposed, even if the filter would drop them. const FORCED_MODELS = [ "DeepSeek-V4-Pro", @@ -50,7 +76,7 @@ function loadCache(): Cache | null { function saveCache(models: ModelMap): void { try { - mkdirSync(join(homedir(), ".cache", "opencode"), { recursive: true }) + mkdirSync(cacheDir(), { recursive: true }) writeFileSync(CACHE_FILE, JSON.stringify({ ts: Date.now(), models })) } catch {} } @@ -99,7 +125,7 @@ async function resolveModels(): Promise { } catch {} try { - const key = process.env.ARKA_API_KEY + const key = readApiKey() const json = await fetchJson( `${API_URL}/models`, key ? { Authorization: `Bearer ${key}` } : {}, @@ -115,6 +141,7 @@ async function resolveModels(): Promise { export default async function ArkaAIProviderPlugin() { const models = await resolveModels() + const apiKey = readApiKey() return { config: (cfg: any) => { @@ -124,12 +151,18 @@ export default async function ArkaAIProviderPlugin() { // Static models from opencode.json act as a fallback; the fetched // catalog is layered on top so the configured default model never // disappears even if the catalog drops it. + // apiKey from env or ~/.config/opencode/.arka-api-key so Desktop / + // Windows GUI works without a login-shell export. cfg.provider[PROVIDER_ID] = { name: "Arka-AI", api: API_URL, env: ["ARKA_API_KEY"], ...prev, models: { ...(prev.models ?? {}), ...(models ?? {}) }, + options: { + ...(prev.options ?? {}), + ...(apiKey ? { apiKey } : {}), + }, } }, } diff --git a/opencode/skills/arka-provider/SKILL.md b/opencode/skills/arka-provider/SKILL.md index e4637f2..f89c105 100644 --- a/opencode/skills/arka-provider/SKILL.md +++ b/opencode/skills/arka-provider/SKILL.md @@ -24,7 +24,8 @@ It is wired up locally with a plugin + a static `provider` block in - Static block (`opencode.json` → `provider["Arka-AI"]`) holds `name`, `api`, `env`, and a small fallback `models` map that is used when the catalog is unreachable. -- Inference still uses `ARKA_API_KEY` against `https://api.arka-ai.ru/v1`. +- Inference uses `ARKA_API_KEY` (env) or `~/.config/opencode/.arka-api-key` + (so OpenCode Desktop / Windows GUI works without a login shell). ## Refresh the model list