From 239ddc767c96c920288b64db6ceb30e10538c80e Mon Sep 17 00:00:00 2001 From: infosave2007 Date: Sat, 8 Aug 2026 14:05:47 +0300 Subject: [PATCH] feat(keys): harness snippets for every OS + install command when missing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The harness auto-setup scripts were bash-only (heredocs, mkdir -p) — useless on Windows. Every scripty snippet now exists in two flavors behind a 🐧🍎/🪟 switch (auto-detected from the browser): bash heredocs for Linux/macOS and PowerShell here-strings ($env:, New-Item, @'..'@, curl.exe with backtick continuations) for Windows. SDK snippets and GUI instructions stay OS-independent. And when detection says a harness is NOT installed on the gateway machine, the status line now offers its one-line install command for the selected OS (claude.ai install.sh / install.ps1, npm -g for Codex and OpenCode-on- Windows, code --install-extension for Continue/Cline) with a copy button. 1 string ×7 languages. --- web/js/i18n/de.js | 1 + web/js/i18n/en.js | 1 + web/js/i18n/es.js | 1 + web/js/i18n/fr.js | 1 + web/js/i18n/ru.js | 1 + web/js/i18n/tr.js | 1 + web/js/i18n/zh.js | 1 + web/js/views/keys.js | 113 +++++++++++++++++++++++++++++++++++++++++-- 8 files changed, 116 insertions(+), 4 deletions(-) diff --git a/web/js/i18n/de.js b/web/js/i18n/de.js index 16b28b7..462f362 100644 --- a/web/js/i18n/de.js +++ b/web/js/i18n/de.js @@ -643,4 +643,5 @@ export default { "keys.form.budgetHint": "Überschreiten die Monatsausgaben des Kontos die Grenze, werden Anfragen mit 429 insufficient_quota abgelehnt.", "keys.budgetOf": "Budget {b}/Mon.", "dash.kpi.orBalance": "OpenRouter-Guthaben", + "keys.h.install": "Installieren:", }; diff --git a/web/js/i18n/en.js b/web/js/i18n/en.js index fad52b8..f2144f9 100644 --- a/web/js/i18n/en.js +++ b/web/js/i18n/en.js @@ -636,4 +636,5 @@ export default { "keys.form.budgetHint": "Requests are rejected with 429 insufficient_quota once the account's month-to-date spend crosses this.", "keys.budgetOf": "budget {b}/mo", "dash.kpi.orBalance": "OpenRouter balance", + "keys.h.install": "Install:", }; diff --git a/web/js/i18n/es.js b/web/js/i18n/es.js index d3ca8e6..4b36254 100644 --- a/web/js/i18n/es.js +++ b/web/js/i18n/es.js @@ -643,4 +643,5 @@ export default { "keys.form.budgetHint": "Cuando el gasto del mes supera el techo, las peticiones se rechazan con 429 insufficient_quota.", "keys.budgetOf": "presupuesto {b}/mes", "dash.kpi.orBalance": "Saldo OpenRouter", + "keys.h.install": "Instalar:", }; diff --git a/web/js/i18n/fr.js b/web/js/i18n/fr.js index a91fd5d..a02aa72 100644 --- a/web/js/i18n/fr.js +++ b/web/js/i18n/fr.js @@ -643,4 +643,5 @@ export default { "keys.form.budgetHint": "Quand les dépenses du mois dépassent le plafond, les requêtes sont rejetées avec 429 insufficient_quota.", "keys.budgetOf": "budget {b}/mois", "dash.kpi.orBalance": "Solde OpenRouter", + "keys.h.install": "Installer :", }; diff --git a/web/js/i18n/ru.js b/web/js/i18n/ru.js index 5bd19ad..6b43dfa 100644 --- a/web/js/i18n/ru.js +++ b/web/js/i18n/ru.js @@ -635,4 +635,5 @@ export default { "keys.form.budgetHint": "Когда траты аккаунта за месяц превысят потолок, запросы отклоняются с 429 insufficient_quota.", "keys.budgetOf": "бюджет {b}/мес", "dash.kpi.orBalance": "Баланс OpenRouter", + "keys.h.install": "Установить:", }; diff --git a/web/js/i18n/tr.js b/web/js/i18n/tr.js index b9e81ee..5869986 100644 --- a/web/js/i18n/tr.js +++ b/web/js/i18n/tr.js @@ -643,4 +643,5 @@ export default { "keys.form.budgetHint": "Hesabın aylık harcaması tavanı aşınca istekler 429 insufficient_quota ile reddedilir.", "keys.budgetOf": "bütçe {b}/ay", "dash.kpi.orBalance": "OpenRouter bakiyesi", + "keys.h.install": "Kurulum:", }; diff --git a/web/js/i18n/zh.js b/web/js/i18n/zh.js index cd03cff..d1296e9 100644 --- a/web/js/i18n/zh.js +++ b/web/js/i18n/zh.js @@ -643,4 +643,5 @@ export default { "keys.form.budgetHint": "当账户当月支出超过上限时,请求将被拒绝并返回 429 insufficient_quota。", "keys.budgetOf": "预算 {b}/月", "dash.kpi.orBalance": "OpenRouter 余额", + "keys.h.install": "安装:", }; diff --git a/web/js/views/keys.js b/web/js/views/keys.js index c3d4b27..2475c89 100644 --- a/web/js/views/keys.js +++ b/web/js/views/keys.js @@ -55,9 +55,73 @@ export async function renderKeys() { const origin = location.origin; const base = origin + "/v1"; const key = lastCreatedKey || (openMode ? "sk-anything" : "<" + t("keys.snip.keyPh") + ">"); + // Every scripty snippet exists in two OS flavors: bash heredocs for + // Linux/macOS and PowerShell here-strings for Windows. SDK snippets + // (python/js) and GUI instructions are OS-independent. + let os = /win/i.test(navigator.platform || "") ? "ps" : "sh"; // Harness auto-setup scripts first (the gateway speaks OpenAI at /v1/* and // Anthropic at /v1/messages, so Claude Code connects natively), SDKs after. - const snips = { + const buildSnips = (osKind) => osKind === "ps" ? { + "Claude Code": `# ${t("keys.h.claude")} +$env:ANTHROPIC_BASE_URL = "${origin}" +$env:ANTHROPIC_AUTH_TOKEN = "${key}" +$env:ANTHROPIC_MODEL = "cortiq-auto" + +claude`, + Codex: `# ${t("keys.h.codex")} +New-Item -ItemType Directory -Force "$HOME\\.codex" | Out-Null +Add-Content "$HOME\\.codex\\config.toml" @' +model = "cortiq-auto" +model_provider = "cortiq" + +[model_providers.cortiq] +name = "Cortiq Gateway" +base_url = "${base}" +env_key = "CORTIQ_API_KEY" +wire_api = "chat" +'@ +$env:CORTIQ_API_KEY = "${key}" + +codex`, + OpenCode: `# ${t("keys.h.opencode")} +New-Item -ItemType Directory -Force "$HOME\\.config\\opencode" | Out-Null +Set-Content "$HOME\\.config\\opencode\\opencode.json" @' +{ + "$schema": "https://opencode.ai/config.json", + "provider": { + "cortiq": { + "npm": "@ai-sdk/openai-compatible", + "name": "Cortiq Gateway", + "options": { "baseURL": "${base}", "apiKey": "${key}" }, + "models": { "cortiq-auto": { "name": "Cortiq Auto (smart routing)" } } + } + } +} +'@ + +opencode`, + Continue: `# ${t("keys.h.continue")} +New-Item -ItemType Directory -Force "$HOME\\.continue" | Out-Null +Add-Content "$HOME\\.continue\\config.yaml" @' +models: + - name: Cortiq Auto + provider: openai + model: cortiq-auto + apiBase: ${base} + apiKey: ${key} +'@`, + Cline: `${t("keys.h.cline1")} + API Provider: OpenAI Compatible + Base URL: ${base} + API Key: ${key} + Model ID: cortiq-auto + +${t("keys.h.cline2")}`, + curl: `curl.exe ${base}/chat/completions \` + -H "Authorization: Bearer ${key}" \` + -H "Content-Type: application/json" \` + -d '{"model":"cortiq-auto","messages":[{"role":"user","content":"Привет!"}]}'`, + } : { "Claude Code": `# ${t("keys.h.claude")} export ANTHROPIC_BASE_URL="${origin}" export ANTHROPIC_AUTH_TOKEN="${key}" @@ -131,6 +195,11 @@ const resp = await client.chat.completions.create({ }); console.log(resp.choices[0].message.content);`, }; + const shSnips = buildSnips("sh"); + const psSnips = buildSnips("ps"); + // python/js/Cline одинаковы — дополняем ps-набор из sh + for (const k of Object.keys(shSnips)) if (!psSnips[k]) psSnips[k] = shSnips[k]; + let snips = os === "ps" ? psSnips : shSnips; let cur = "Claude Code"; const pre = h("pre", { class: "conv-log snippet" }, snips[cur]); const seg = h( @@ -167,7 +236,15 @@ console.log(resp.choices[0].message.content);`, }).catch(() => {}); // Detected harnesses: ✓ on the tab when installed on the gateway machine, - // status line under the snippet ("installed" / "already configured"). + // status line under the snippet ("installed" / "already configured") — + // and a one-line INSTALL command (per OS) when it's missing. + const INSTALL = { + "Claude Code": { sh: "curl -fsSL https://claude.ai/install.sh | bash", ps: "irm https://claude.ai/install.ps1 | iex" }, + Codex: { sh: "npm install -g @openai/codex", ps: "npm install -g @openai/codex" }, + OpenCode: { sh: "curl -fsSL https://opencode.ai/install | bash", ps: "npm install -g opencode-ai" }, + Continue: { sh: "code --install-extension Continue.continue", ps: "code --install-extension Continue.continue" }, + Cline: { sh: "code --install-extension saoudrizwan.claude-dev", ps: "code --install-extension saoudrizwan.claude-dev" }, + }; let detected = {}; const statusHost = h("div", { class: "small", style: "margin-top:6px" }); function paintStatus() { @@ -179,7 +256,19 @@ console.log(resp.choices[0].message.content);`, statusHost.innerHTML = ""; if (d.configured) mount(statusHost, h("span", { class: "ok" }, "✓ " + t("keys.h.st.configured"))); else if (d.installed) mount(statusHost, h("span", { class: "ok" }, "✓ " + t("keys.h.st.installed"))); - else mount(statusHost, h("span", { class: "muted" }, "○ " + t("keys.h.st.notFound"))); + else { + const cmd = (INSTALL[cur] || {})[os]; + const copyInstall = h("button", { class: "btn ghost sm", onClick: async () => { + await navigator.clipboard.writeText(cmd); + toast(t("common.copied"), "good"); + } }, "⧉"); + mount(statusHost, + h("span", { class: "muted" }, "○ " + t("keys.h.st.notFound")), + cmd ? h("div", { class: "flex", style: "gap:6px; align-items:center; margin-top:4px" }, + h("span", { class: "muted" }, t("keys.h.install")), + h("code", { class: "mono small", style: "background:var(--panel-2,#23252c); padding:2px 8px; border-radius:6px" }, cmd), + copyInstall) : null); + } } api.harnesses().then((d) => { detected = d || {}; @@ -190,6 +279,22 @@ console.log(resp.choices[0].message.content);`, paintStatus(); }).catch(() => {}); + // OS switch: bash for Linux/macOS, PowerShell for Windows (auto-detected) + const osSeg = h( + "div", + { class: "seg" }, + h("button", { class: os === "sh" ? "active" : "" }, "🐧🍎 Linux / macOS"), + h("button", { class: os === "ps" ? "active" : "" }, "🪟 Windows") + ); + osSeg.querySelectorAll("button").forEach((b, i) => + b.addEventListener("click", () => { + os = i === 0 ? "sh" : "ps"; + snips = os === "ps" ? psSnips : shSnips; + osSeg.querySelectorAll("button").forEach((x, j) => x.classList.toggle("active", j === (os === "sh" ? 0 : 1))); + pre.textContent = snips[cur]; + paintStatus(); + })); + const copy = h("button", { class: "btn ghost sm" }, "⧉ " + t("common.copy")); copy.addEventListener("click", async () => { await navigator.clipboard.writeText(pre.textContent); @@ -201,7 +306,7 @@ console.log(resp.choices[0].message.content);`, h("div", { class: "card-title" }, "🔌 " + t("keys.snip.title")), h("div", { class: "muted small", style: "margin-bottom:8px" }, lastCreatedKey ? t("keys.snip.withKey") : openMode ? t("keys.snip.openMode") : t("keys.snip.hint")), - h("div", { class: "flex", style: "gap:8px; margin-bottom:8px; flex-wrap:wrap" }, seg, h("div", { class: "grow" }), copy), + h("div", { class: "flex", style: "gap:8px; margin-bottom:8px; flex-wrap:wrap" }, seg, h("div", { class: "grow" }), osSeg, copy), pre, statusHost, protoWarn,