# v0.4.30 (2026-05-11)
## Features - MCP stdio→SSE bridge: expose local stdio MCP plugins over SSE (api/mcp/[plugin]/sse, /message) - Dynamic Linux cert resolution + NSS DB injection (Debian/Arch/Fedora/openSUSE, Chrome/Chromium/Firefox incl. snap) (#1010) - Cowork tool: expanded settings UI & API - GitBook docs (DocsContent, DocsLayout) ## Fixes - OAuth callback postMessage scoped to expected origins (CWE-1385) (#998) - Re-enable TLS verification on DNS-bypass fetch (CWE-295) (#998) - Normalize `developer` role → `system` for OpenAI-format providers (Deepseek, Groq, …) (#1011, closes #773) - Respect `PORT` env in internal model-test fetch (#1014) - Dropdown text readability in dark theme on usage page (#997) ## Improvements - Refactor Claude CLI spoof headers into shared constant - Tool deduper utility in open-sse handlers
This commit is contained in:
@@ -211,6 +211,86 @@ export const CLI_TOOLS = {
|
||||
"model": "{{model}}",
|
||||
"provider": "openai",
|
||||
"apiKey": "{{apiKey}}"
|
||||
}`,
|
||||
},
|
||||
},
|
||||
amp: {
|
||||
id: "amp",
|
||||
name: "Amp CLI",
|
||||
icon: "terminal",
|
||||
color: "#F97316",
|
||||
description: "Sourcegraph Amp coding assistant CLI",
|
||||
docsUrl: "/docs?section=cli-tools&tool=amp",
|
||||
configType: "guide",
|
||||
defaultCommand: "amp",
|
||||
modelAliases: ["g25p", "g25f", "cs45", "g54"],
|
||||
notes: [
|
||||
{ type: "info", text: "Use 9Router model aliases to keep Amp shorthand mappings stable across provider updates." },
|
||||
{ type: "warning", text: "Suggested shorthand examples: g25p → gemini/gemini-2.5-pro, g25f → gemini/gemini-2.5-flash, cs45 → cc/claude-sonnet-4-5-20250929." },
|
||||
],
|
||||
guideSteps: [
|
||||
{ step: 1, title: "Install Amp", desc: "Install the Amp CLI using the package manager supported by your environment." },
|
||||
{ step: 2, title: "API Key", type: "apiKeySelector" },
|
||||
{ step: 3, title: "Base URL", value: "{{baseUrl}}", copyable: true },
|
||||
{ step: 4, title: "Select Model", type: "modelSelector" },
|
||||
{ step: 5, title: "Add Shorthands", desc: "Map Amp shorthand names such as g25p or cs45 to 9Router aliases in your local config." },
|
||||
],
|
||||
codeBlock: {
|
||||
language: "bash",
|
||||
code: `export OPENAI_API_KEY="{{apiKey}}"
|
||||
export OPENAI_BASE_URL="{{baseUrl}}"
|
||||
amp --model "{{model}}"
|
||||
# Example shorthand aliases you can map locally:
|
||||
# g25p -> gemini/gemini-2.5-pro
|
||||
# cs45 -> cc/claude-sonnet-4-5-20250929`,
|
||||
},
|
||||
},
|
||||
qwen: {
|
||||
id: "qwen",
|
||||
name: "Qwen Code",
|
||||
icon: "psychology",
|
||||
color: "#10B981",
|
||||
description: "Alibaba Qwen Code CLI — supports OpenAI, Anthropic & Gemini providers via 9Router",
|
||||
docsUrl: "https://qwenlm.github.io/qwen-code-docs/en/users/configuration/model-providers/",
|
||||
configType: "guide",
|
||||
defaultCommand: "qwen",
|
||||
notes: [
|
||||
{ type: "info", text: "Qwen Code supports multiple provider types (openai, anthropic, gemini) via modelProviders in settings.json. 9Router works as an OpenAI-compatible endpoint." },
|
||||
{ type: "info", text: "Any model available in 9Router can be used — not just Qwen models. Select from Qwen, Claude, Gemini, GPT, and more." },
|
||||
{ type: "warning", text: "Config path: Linux/macOS ~/.qwen/settings.json • Windows %USERPROFILE%\\.qwen\\settings.json" },
|
||||
{ type: "error", text: "Qwen OAuth free tier was discontinued on 2026-04-15. Use 9Router with alicode/openrouter/anthropic/gemini providers instead." },
|
||||
],
|
||||
modelAliases: ["coder-model", "qwen3-coder-plus", "qwen3-coder-flash", "vision-model", "claude-sonnet-4-6", "claude-opus-4-6-thinking", "gemini-3-flash", "gemini-3.1-pro-high"],
|
||||
defaultModels: [
|
||||
{ id: "coder-model", name: "Coder Model (Qwen 3.6 Plus)", alias: "coder-model", envKey: "OPENAI_MODEL", defaultValue: "coder-model", isTopLevel: true },
|
||||
{ id: "qwen3-coder-plus", name: "Qwen 3 Coder Plus", alias: "qwen3-coder-plus", envKey: "OPENAI_MODEL", defaultValue: "qwen3-coder-plus" },
|
||||
{ id: "qwen3-coder-flash", name: "Qwen 3 Coder Flash", alias: "qwen3-coder-flash", envKey: "OPENAI_MODEL", defaultValue: "qwen3-coder-flash" },
|
||||
{ id: "vision-model", name: "Vision Model (Multimodal)", alias: "vision-model", envKey: "OPENAI_MODEL", defaultValue: "vision-model" },
|
||||
{ id: "claude-sonnet-4-6", name: "Claude Sonnet 4.6", alias: "claude-sonnet-4-6", envKey: "OPENAI_MODEL", defaultValue: "claude-sonnet-4-6" },
|
||||
{ id: "claude-opus-4-6-thinking", name: "Claude Opus 4.6 Thinking", alias: "claude-opus-4-6-thinking", envKey: "OPENAI_MODEL", defaultValue: "claude-opus-4-6-thinking" },
|
||||
{ id: "gemini-3.1-pro-high", name: "Gemini 3.1 Pro High", alias: "gemini-3.1-pro-high", envKey: "OPENAI_MODEL", defaultValue: "gemini-3.1-pro-high" },
|
||||
{ id: "gemini-3-flash", name: "Gemini 3 Flash", alias: "gemini-3-flash", envKey: "OPENAI_MODEL", defaultValue: "gemini-3-flash" },
|
||||
],
|
||||
guideSteps: [
|
||||
{ step: 1, title: "Install Qwen Code", desc: "npm install -g @qwen-code/qwen-code" },
|
||||
{ step: 2, title: "API Key", type: "apiKeySelector" },
|
||||
{ step: 3, title: "Base URL", value: "{{baseUrl}}", copyable: true },
|
||||
{ step: 4, title: "Select Model", type: "modelSelector" },
|
||||
{ step: 5, title: "Save Config", desc: "Copy the JSON below to your ~/.qwen/settings.json file." },
|
||||
],
|
||||
codeBlock: {
|
||||
language: "json",
|
||||
code: `{
|
||||
"security": {
|
||||
"auth": {
|
||||
"selectedType": "openai",
|
||||
"apiKey": "{{apiKey}}",
|
||||
"baseUrl": "{{baseUrl}}"
|
||||
}
|
||||
},
|
||||
"model": {
|
||||
"name": "{{model}}"
|
||||
}
|
||||
}`,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
// Default plugins auto-installed for Claude Cowork (3p mode).
|
||||
// Exa works without auth; Tavily uses OAuth (DCR auto-flow).
|
||||
// Default remote plugins for Claude Cowork (3p managedMcpServers, HTTPS only).
|
||||
const DEFAULT_PLUGINS = [
|
||||
{
|
||||
name: "exa",
|
||||
@@ -21,20 +20,24 @@ const DEFAULT_PLUGINS = [
|
||||
},
|
||||
];
|
||||
|
||||
// Build managedMcpServers entries from plugin objects.
|
||||
// Schema: [{name, url, transport, oauth?, toolPolicy?}]
|
||||
// toolPolicy maps each tool to "allow" so Claude doesn't prompt.
|
||||
// Plugin name that's force-installed regardless of user selection.
|
||||
const ALWAYS_ON = "exa";
|
||||
// Local stdio plugins bridged via inline SSE endpoint on the app's port.
|
||||
const LOCAL_STDIO_PLUGINS = [
|
||||
{
|
||||
name: "browsermcp",
|
||||
title: "Browser MCP",
|
||||
description: "Control your running Chrome (requires Chrome extension)",
|
||||
extensionUrl: "https://chromewebstore.google.com/detail/browser-mcp-automate-your/bjfgambnhccakkhmkepdoekmckoijdlc",
|
||||
command: "npx",
|
||||
args: ["-y", "@browsermcp/mcp@latest"],
|
||||
toolNames: ["browser_navigate", "browser_snapshot", "browser_click", "browser_type", "browser_screenshot", "browser_get_console_logs", "browser_wait", "browser_press_key", "browser_go_back", "browser_go_forward"],
|
||||
},
|
||||
];
|
||||
|
||||
function buildManagedMcpServers(plugins) {
|
||||
const list = Array.isArray(plugins) ? plugins : [];
|
||||
// Force Exa always-on at the front; drop any duplicate from user list.
|
||||
const exaDefault = DEFAULT_PLUGINS.find((p) => p.name === ALWAYS_ON);
|
||||
const merged = exaDefault ? [exaDefault, ...list.filter((p) => p?.name !== ALWAYS_ON)] : list;
|
||||
const out = [];
|
||||
const seen = new Set();
|
||||
for (const p of merged) {
|
||||
for (const p of list) {
|
||||
if (!p?.name || !p?.url || seen.has(p.name)) continue;
|
||||
seen.add(p.name);
|
||||
const entry = {
|
||||
@@ -66,4 +69,4 @@ function buildManagedMcpServers(plugins) {
|
||||
return out;
|
||||
}
|
||||
|
||||
module.exports = { DEFAULT_PLUGINS, buildManagedMcpServers, ALWAYS_ON };
|
||||
module.exports = { DEFAULT_PLUGINS, LOCAL_STDIO_PLUGINS, buildManagedMcpServers };
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -41,6 +41,7 @@ const g = global.__appSingleton ??= {
|
||||
networkMonitorInterval: null,
|
||||
lastNetworkFingerprint: null,
|
||||
lastWatchdogTick: Date.now(),
|
||||
lastOnline: null,
|
||||
mitmStartInProgress: false,
|
||||
tunnelAutoResumed: false,
|
||||
tailscaleAutoResumed: false,
|
||||
@@ -209,6 +210,7 @@ function startNetworkMonitor() {
|
||||
|
||||
g.lastNetworkFingerprint = getNetworkFingerprint();
|
||||
g.lastWatchdogTick = Date.now();
|
||||
g.lastOnline = null;
|
||||
|
||||
g.networkMonitorInterval = setInterval(async () => {
|
||||
try {
|
||||
@@ -218,15 +220,24 @@ function startNetworkMonitor() {
|
||||
|
||||
const currentFingerprint = getNetworkFingerprint();
|
||||
const networkChanged = currentFingerprint !== g.lastNetworkFingerprint;
|
||||
const wasSleep = elapsed > NETWORK_CHECK_INTERVAL_MS * 3;
|
||||
|
||||
const wasSleep = elapsed > NETWORK_CHECK_INTERVAL_MS * 6;
|
||||
if (networkChanged) g.lastNetworkFingerprint = currentFingerprint;
|
||||
if (!networkChanged && !wasSleep) return;
|
||||
|
||||
// Real reachability check (TCP 1.1.1.1:443) — not just interface presence
|
||||
const online = await checkInternet();
|
||||
const wasOffline = g.lastOnline === false;
|
||||
g.lastOnline = online;
|
||||
|
||||
if (!online) return; // no internet → idle, don't restart
|
||||
|
||||
const onlineEdge = wasOffline; // offline → online transition
|
||||
if (!networkChanged && !wasSleep && !onlineEdge) return;
|
||||
|
||||
// Wait for DHCP/DNS to settle before probing
|
||||
await new Promise((r) => setTimeout(r, NETWORK_SETTLE_MS));
|
||||
|
||||
const reason = wasSleep && networkChanged ? "sleep+netchange"
|
||||
const reason = onlineEdge ? "online"
|
||||
: wasSleep && networkChanged ? "sleep+netchange"
|
||||
: wasSleep ? "sleep" : "netchange";
|
||||
safeRestartTunnel(reason).catch(() => {});
|
||||
safeRestartTailscale(reason).catch(() => {});
|
||||
|
||||
Reference in New Issue
Block a user