Compare commits

..

2 Commits

Author SHA1 Message Date
diegosouzapw
48f04073e9 feat(providers): add unit tests, ESLint suppressions, and fix hardcoded userid for TinyCMS Web
- Add unit tests for WASM init, UUID validation, challenge flow (15 tests)
- Add WASM source comment explaining binary origin
- Replace hardcoded userid with dynamic provider-specific data
- Add ESLint suppressions for no-explicit-any in WASM bridge code
- Add explanatory comments for DOM shim (runtime WASM-bindgen, not test mocks)

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-08-04 11:51:52 -03:00
jhordanjw123
d98737578e feat(providers): add support for TinyCMS Web including WASM-based cryptographic signing and Proof-of-Work emulation 2026-07-26 23:12:37 -03:00
2356 changed files with 35964 additions and 239934 deletions

View File

@@ -1,201 +0,0 @@
# codebase-memory-mcp ignore list
#
# Padrão gitignore-style. Linhas começando com `#` são comentários.
# Barra final (`/`) = só diretório. Sem barra = casa arquivo OU diretório.
#
# O CBM também lê `.gitignore` automaticamente — esta lista deixa explícito o que
# os hooks do CBM vão pular. Se uma regra entrar em conflito entre os dois arquivos,
# vale a união. Editar este arquivo é mais barato do que confiar na herança implícita.
#
# Última reconciliação: 2026-07-31, status `ready` (513k nodes / 689k edges),
# `auto_index_limit=50000`, total indexável medido ≈11.546 arquivos (folga 4,3×).
#
# Fontes cruzadas:
# - `codebase-memory-mcp cli index_status --project home-diegosouzapw-dev-proxys-OmniRoute`
# → `not_indexed.dirs` (27) + `not_indexed.files` (336), todos `BY DESIGN`.
# - `.gitignore` deste repo (5.691 B) — fonte canônica secundária.
#
# Como auditar mudanças: depois de editar este arquivo, rodar `index_repository`
# (ou esperar `auto_watch` re-indexar) e re-checar `cli index_status` → comparar
# contagens em `not_indexed.dirs_count` e `not_indexed.files_count`.
# ─────────────────────────────────────────────────────────────────────────────
# 1. Diretorios de runtime / pacote — nao sao codigo-fonte
# ─────────────────────────────────────────────────────────────────────────────
node_modules/
node_modules
# Builds e artefatos reproduziveis (Layer 1 Next.js / Electron)
.build/
dist/
.next/
out/
# Electron especifico
electron/dist-electron/
electron/node_modules/
icon.iconset/
# Workspaces internos que tem proprio node_modules
@omniroute/opencode-plugin/dist/
@omniroute/opencode-plugin/node_modules/
@omniroute/opencode-provider/dist/
@omniroute/opencode-provider/node_modules/
# Recursos nativos compilados (C/JNI/wasm)
src/mitm/tproxy/native/build/
# Artefatos locais do Stryker / Playwright / coverage
.stryker-tmp/
reports/mutation/
stryker-output-*.json
.playwright-mcp/
test-results/
playwright-report/
blob-report/
# Analise / linters / caches
.analysis/
.sisyphus/
.plans/
.gitnexus
.worktrees
.codegraph/
# Quality artifacts (gerados por npm run lint --cache etc)
.eslintcache
.eslintcache-complexity
# Claude Code local state
.claude/scheduled_tasks.lock
.claude/scheduled_tasks/
.claude/sessions/
.claude/state.json
.claude/settings.local.json
# Serena / Antigravity / outras tools locais
.serena/
.antigravitycli/
.gemini/
.config/
# ─────────────────────────────────────────────────────────────────────────────
# 2. Diretorios com prefixo `_` — locais / privados (regra global do .gitignore)
# ─────────────────────────────────────────────────────────────────────────────
_*/
_artifacts/
_cache/
_mono_repo/
_references/
_tasks/
# ─────────────────────────────────────────────────────────────────────────────
# 3. Diretorios de tooling IA (state local, nao codigo)
# ─────────────────────────────────────────────────────────────────────────────
.agents/
.claude/
.vscode/
.idea/
.junie/
.omc/
.data/
.data-dev/
.local-data/
.logs/
.artifacts/
.source/
.superpowers/
.claude-flow/
.omnivscodeagent/
omnirouteCloud/
omnirouteSite/
.omniroute/
.stent/
# Subpaths especificos do Claude Code que nao estao em .claude/ (criados sob repo)
.claude/worktrees/
# ─────────────────────────────────────────────────────────────────────────────
# 4. Diretorios de dados / runtime locais (storage, env, secrets, scratch)
# ─────────────────────────────────────────────────────────────────────────────
data/
# NOTA: src/lib/env/, src/app/api/{cloud,sync/cloud,system/env,agent-skills/coverage}/
# foram removidos daqui (2026-08-05). Os nomes sugerem dados/segredos locais, mas os
# 8 arquivos sao route handlers e modulos rastreados no git — escondia-los do grafo
# criava pontos cegos em buscas e em analise de impacto.
tests/golden-set/data/
# Logs e saida de teste
logs/*
test_output.log
home-diegosouzapw-dev-automacoes-*.txt
# ─────────────────────────────────────────────────────────────────────────────
# 5. Diretorios do monorepo por subprojeto (nao fazem parte do app principal)
# ─────────────────────────────────────────────────────────────────────────────
security-analysis/
vscode-extension/
obsidian-plugin/node_modules/
# ─────────────────────────────────────────────────────────────────────────────
# 6. Diretorios de documentacao interna / workflow
# ─────────────────────────────────────────────────────────────────────────────
docs/superpowers/
# Docs traduzidas: 1.215 arquivos / 94 MB (inclui 20+ copias do CHANGELOG).
# Sao traducoes do tree em ingles, ja indexado — no grafo so geram ruido em
# search_code e consomem o auto_index_limit.
docs/i18n/
# ─────────────────────────────────────────────────────────────────────────────
# 7. Arquivos especificos (nao diretorios inteiros)
# ─────────────────────────────────────────────────────────────────────────────
# Segredos e env — NUNCA indexar
.env
.env.*
!.env.example
!.env.homolog.example
# TypeScript build info e next env declaration
*.tsbuildinfo
next-env.d.ts
typescript
# SQLite transient files (WAL/SHM/journal)
*.sqlite-shm
*.sqlite-wal
*.sqlite-journal
# Mapas e source maps
*.map
# Bun / npm lockfiles ruidosos
bun.lock
# `cheaper-inference-gateway.svg` e arquivos de midia na raiz/asset ja cobertos
# pelos `ignored-suffix` do indexador (svg/png/jpg/ico/etc >50kB ou >500linhas);
# manter a regra explicita aqui ajuda a auditar:
cheaper-inference-gateway.svg
cheaper-inference-gateway-*.svg
# Husky internals
.husky/_/
# CI / quality metric artifacts
config/quality/quality-metrics.json
config/quality/test-impact-map.json
audit-report.json
.gh-discussions.json
# i18n audit (gerado por npm run scripts)
scripts/i18n/_audit.json
scripts/i18n/_pending-keys.json
# NOTA: bin/omniroute.mjs foi removido daqui (2026-08-05). Estava marcado como
# "scratch", mas e o entrypoint real do CLI publicado (package.json -> bin.omniroute)
# e consta em PACK_ARTIFACT_REQUIRED_PATHS. Precisa estar no grafo.
# Deploy / docker backups
deploy.sh
docker-compose.yml.bak
docker-compose.minimal.yml

View File

@@ -1 +0,0 @@
- **docs:** add management authentication terminology guide ([#7786](https://github.com/diegosouzapw/OmniRoute/issues/7786))

View File

@@ -1,41 +0,0 @@
# Management Authentication
OmniRoute uses four distinct credential families for management access. This guide
distinguishes them by purpose, scope, and locality.
| Credential | Scope | Locality | Use Case |
|-------------------------|--------------------|---------------|-----------------------------------|
| Dashboard JWT session | Full management | Localhost | Web dashboard login |
| CLI machine-id token | Full management | Per-machine | `omniroute` CLI commands |
| Scoped `oma_` token | Configurable scope | External | Automation / CI / API access |
| Manage-scope API key | `manage` scope | External | Management API calls |
## Dashboard JWT Session
Generated on dashboard login (`/api/auth/login`). Stored in HTTP-only cookie.
Valid for the session duration. Cannot be used from external hosts.
## CLI Machine-ID Token
Created by `omniroute auth login` on first use. Stored in `~/.omniroute/auth.json`.
Used by the CLI for all management operations. Tied to the machine identity.
## Scoped `oma_` Access Token
Created via dashboard or CLI with configurable scopes (e.g., `manage`, `read`).
Format: `oma_<random-hex>`. Used for programmatic access from external systems.
## Manage-Scope API Key
Standard API key with the `manage` scope enabled. Created in dashboard API Keys page.
Used for management API calls from external hosts.
## Header Examples
```
Authorization: Bearer oma_abc123def456
Authorization: Bearer <standard-api-key-with-manage-scope>
Cookie: omniroute_session=<jwt-token>
```
See `docs/reference/API_REFERENCE.md` for endpoint-specific auth requirements.

View File

@@ -1,27 +0,0 @@
import { describe, it } from "node:test";
import { ok } from "node:assert/strict";
import { readFileSync } from "node:fs";
describe("Management auth documentation (#7786)", () => {
const docPath = "docs/guides/MANAGEMENT-AUTH.md";
const content = readFileSync(docPath, "utf-8");
it("exists and has content", () => {
ok(content.length > 500, "should have substantial content");
ok(content.includes("Dashboard JWT session"));
ok(content.includes("CLI machine-id token"));
ok(content.includes("oma_"));
});
it("documents all four credential families", () => {
const families = ["Dashboard JWT", "CLI machine-id", "oma_", "Manage-scope"];
for (const f of families) {
ok(content.includes(f), `should document ${f}`);
}
});
it("mentions relevant auth header examples", () => {
ok(content.includes("Authorization"));
ok(content.includes("Bearer"));
});
});

View File

@@ -1 +0,0 @@
- **feat(infra):** add systemd autostart unit for Linux ([#8635](https://github.com/diegosouzapw/OmniRoute/issues/8635))

View File

@@ -1,19 +0,0 @@
[Unit]
Description=OmniRoute AI Proxy
After=network.target network-online.target
Wants=network-online.target
[Service]
Type=simple
ExecStart=$(which omniroute) start
Restart=on-failure
RestartSec=5
Environment=NODE_ENV=production
# Security hardening
NoNewPrivileges=true
ProtectSystem=full
PrivateTmp=true
[Install]
WantedBy=default.target

View File

@@ -1,23 +0,0 @@
import { describe, it } from "node:test";
import { ok } from "node:assert/strict";
import { readFileSync, existsSync } from "node:fs";
describe("Systemd autostart (#8635)", () => {
const svcPath = "contrib/systemd/omniroute.service";
const content = readFileSync(svcPath, "utf-8");
it("service file exists", () => {
ok(existsSync(svcPath));
ok(content.length > 200);
});
it("defines required systemd sections", () => {
ok(content.includes("[Unit]"));
ok(content.includes("[Service]"));
ok(content.includes("[Install]"));
});
it("specifies WantedBy=default.target", () => {
ok(content.includes("WantedBy=default.target"));
});
});

View File

@@ -7,13 +7,7 @@
**/.vscode
# Dependencies and build output
# `node_modules` alone matches the ROOT only — Docker's matcher does not cross
# `/` like .gitignore does. Without the `**/` form, nested installs ship in the
# build context (e.g. @omniroute/opencode-provider/node_modules, ~79 MB of
# devDependencies). Both forms are kept: the bare one is the documented root
# rule, the `**/` one covers every nested package.
node_modules
**/node_modules
.next
.build
out
@@ -24,7 +18,6 @@ coverage
# Runtime data and logs
data
logs
.sandbox
# Local env files (inject at runtime via --env-file or -e)
.env
@@ -44,17 +37,6 @@ tests
test-results
playwright-report
blob-report
output
.playwright-cli
.playwright-mcp
.stryker-tmp
reports/mutation
# Local caches and quality-gate artifacts (all gitignored). `_*` does not match
# dot-prefixed names, so these need explicit entries.
.artifacts
.eslintcache
.eslintcache-complexity
# Documentation
# Issue #2348: The Dashboard Docs viewer reads markdown from `/app/docs` at
@@ -67,10 +49,6 @@ reports/mutation
# (English) sources at runtime, so translations are not required in the
# container image.
docs/i18n/**
# Internal planning artifacts (gitignored). `*.md` above only matches the root,
# so without this rule these land in /app/docs and become readable through the
# dashboard's Docs viewer at runtime.
docs/superpowers/**
docs/diagrams/**/*.png
docs/diagrams/**/*.jpg
docs/diagrams/**/*.jpeg

View File

@@ -1,6 +0,0 @@
ENABLE_LIVE_DEVIN_TESTS=0
DEVIN_BRIDGE_MODEL=devin-cli-agentic/swe-1-7
DEVIN_BRIDGE_SONNET_MODEL=devin-cli-agentic/swe-1-7
DEVIN_BRIDGE_OPUS_MODEL=devin-cli-agentic/swe-1-7
DEVIN_BRIDGE_HAIKU_MODEL=devin-cli-agentic/swe-1-7
DEVIN_BRIDGE_SUBAGENT_MODEL=devin-cli-agentic/swe-1-7

View File

@@ -67,14 +67,6 @@ DISABLE_SQLITE_AUTO_BACKUP=false
# Used by: src/shared/utils/rateLimiter.ts
# Example: redis://localhost:6379 (or redis://redis:6379 in Docker)
# REDIS_URL=redis://localhost:6379
# Host interface docker-compose publishes the Redis sidecar on.
# Default: 127.0.0.1 (loopback only). The compose Redis runs WITHOUT
# `requirepass`, and app containers reach it over the compose network
# (redis:6379) — the published port is only for host-side tooling. Setting this
# to 0.0.0.0 exposes an unauthenticated Redis to your whole LAN.
# REDIS_BIND_HOST=127.0.0.1
# Host port for the compose Redis sidecar. Default: 6379.
# REDIS_PORT=6379
# ═══════════════════════════════════════════════════════════════════════════════
# 3. NETWORK & PORTS
@@ -92,12 +84,7 @@ PORT=20128
# endpoint display (useDisplayBaseUrl) shows https://host/omniroute/v1 instead of
# https://host/v1. Rebuild after changing this value (Next basePath is build-time).
# Default: "" (served at the domain root). Example: /omniroute to serve under https://host/omniroute
# Docker: baked at image build time via build-arg; root-path images can also apply this at
# container start (see docs/guides/DOCKER_GUIDE.md).
# OMNIROUTE_BASE_PATH=
# Client fetch/EventSource under this path are rewritten via installBasePathFetch
# (src/shared/utils/basePathFetch.ts) so absolute `/api/*` and `/v1/*` hits work
# without a reverse-proxy rewrite. Rebuild after changing (Next basePath is build-time).
#
# Browser-visible mirror of OMNIROUTE_BASE_PATH, inlined at build time so the
# dashboard endpoint display can read it client-side. Set it to the same value
@@ -248,9 +235,8 @@ NODE_ENV=production
# Container runtime — controls startup script behavior (permissions, advice).
# Values: docker | podman | Default: docker
# Set to "podman" for any Podman topology. The entrypoint cannot determine
# whether the engine is local or reached through Podman Machine, so it prints
# topology-neutral guidance and links contrib/podman/README.md.
# Set to "podman" when running under rootless Podman so the entrypoint
# gives the correct fix instructions (podman unshare chown vs sudo chown).
CONTAINER_HOST=docker
# Container runtime override for skill sandboxing.
@@ -353,7 +339,6 @@ ALLOW_API_KEY_REVEAL=false
# instead of growing an unbounded string until the V8 heap is exhausted.
# Used by: open-sse/handlers/chatCore/nonStreamingResponseBody.ts
# Default: 67108864 (64 MB)
# OMNIROUTE_FORWARDING_HEADER_BUDGET_BYTES=768
# OMNIROUTE_MAX_NONSTREAMING_RESPONSE_BYTES=67108864
# CORS configuration — controls which cross-origin browser clients can call the API.
@@ -454,13 +439,6 @@ ALLOW_API_KEY_REVEAL=false
# Default: false
# OMNIROUTE_PREFER_CLAUDE_CODE_FOR_UNPREFIXED_CLAUDE_MODELS=false
# Per-model concurrency cap for round-robin combos (#9100).
# Used by: open-sse/services/comboConfig.ts — the round-robin combo semaphore
# was hard-capped at 3 concurrent requests per model with no override, which
# serialized higher-concurrency traffic behind that cap.
# Validated to >= 1, clamped to <= 32. | Default: 3
# COMBO_CONCURRENCY_PER_MODEL=3
# ═══════════════════════════════════════════════════════════════════════════════
# 7. URLS & CLOUD SYNC
# ═══════════════════════════════════════════════════════════════════════════════
@@ -827,15 +805,6 @@ PROVIDER_LIMITS_SYNC_SPACING_MS=1500
# Default: <repo>/src/lib/db/migrations.
#OMNIROUTE_MIGRATIONS_DIR=
# Additional migration directories, as `namespace=dir` entries separated by the
# platform path delimiter (`:` on POSIX, `;` on Windows). Files found there are
# recorded as `<namespace>-<number>` (e.g. `ee-134`), a version space that cannot
# collide with the upstream numeric slots — so a distribution shipping its own
# migrations never silently loses one to a number the upstream set also claimed.
# A malformed entry, an invalid namespace or a missing directory aborts startup
# rather than skipping the schema. Unset = no extra directories (the default).
#OMNIROUTE_EXTRA_MIGRATIONS_DIRS=ee=/opt/app/enterprise/db/migrations
# Mass-pending-migrations safety threshold (#3416). If more than this many
# migrations are pending on an existing DB, startup aborts (a wiped tracking
# table could cause data loss). Raise it to restore an older backup; set to 0
@@ -858,12 +827,6 @@ PROVIDER_LIMITS_SYNC_SPACING_MS=1500
# (>= 3 retrievals = never compressed). 1 disables the ramp (binary skip at the threshold only).
# Used by: open-sse/services/compression/engines/ccr/index.ts. Default: 2.
#COMPRESSION_CCR_RETRIEVAL_RAMP_FACTOR=2
# CCR durable block store (#9061). The in-memory store loses blocks to LRU eviction, the TTL, a
# restart, or a retrieve landing on another instance, while the model is told it can retrieve them
# verbatim. Set to false to keep blocks in memory only, at the cost of that promise. Blocks over
# 512KB and cloud runtimes are memory-only regardless.
# Used by: open-sse/services/compression/engines/ccr/index.ts. Default: true.
#COMPRESSION_CCR_DURABLE_STORE=true
# T08/H5 — usage-observed prefix freeze (OPT-IN, default off). When enabled, a system prompt seen
# >= THRESHOLD times is treated as a stable cacheable prefix and preserved from compression even
# for providers the static cache-aware heuristic does not recognize (freeze = preserve, never
@@ -1048,17 +1011,6 @@ GITHUB_OAUTH_CLIENT_ID=Iv1.b507a08c87ecfe98
# VISION_BRIDGE_BASE_URL=
# VISION_BRIDGE_API_KEY=
# ── Raycast Pro (local auto-import) ──
# Raycast Pro AI is a reverse-engineered, unofficial API — local/personal use
# only (no OAuth client_id/secret; token is captured via macOS Auto-Import
# from the Keychain + local Raycast SQLite DB, or pasted manually). These
# vars are optional manual overrides used by open-sse/services/raycast.ts
# and the direct-probe benchmark script scripts/raycast/usage-benchmark.mjs.
# RAYCAST_BEARER_TOKEN=
# RAYCAST_DEVICE_ID=
# RAYCAST_AID=
# RAYCAST_SIG_SECRET=
# ─────────────────────────────────────────────────────────────────────────────
# ⚠️ GOOGLE OAUTH (Antigravity) & OTHER PROVIDERS — REMOTE SERVERS
# ─────────────────────────────────────────────────────────────────────────────
@@ -1199,17 +1151,6 @@ CURSOR_USER_AGENT="Cursor/3.4"
# fallback when FETCH_TIMEOUT_MS is unset. Default: 120000 (2 min).
# OMNIROUTE_DEFAULT_FETCH_TIMEOUT_MS=120000
# ── Proxy/relay fetch (connection pooling, #9158) ──
# Used by: open-sse/utils/proxyFetch.ts.
# A hung relay must fail BEFORE the client/agent timeout (typically 30s) so the
# caller sees a relay-specific failure instead of a generic upstream timeout.
# Capped at 29000ms so this timeout always fires first. Default: 25000 (25s).
# OMNIROUTE_RELAY_FETCH_TIMEOUT_MS=25000
# Shared retry backoff (ms) for the direct/relay/proxy retry-once paths.
# 0 = retry immediately. Default: 10.
# OMNIROUTE_RETRY_BACKOFF_MS=10
# ── Firecrawl web-fetch executor ──
# Point at a self-hosted Firecrawl instance (defaults to the public cloud API).
# When set to a non-cloud base URL, the API key becomes optional.
@@ -1271,14 +1212,6 @@ CURSOR_USER_AGENT="Cursor/3.4"
# OMNIROUTE_BROWSER_POOL=on
# WEB_COOKIE_USE_BROWSER=0
# ── Adobe Firefly browser sign-in (system Chrome/Edge CDP) ──
# Used by: open-sse/services/adobeFireflyBrowserLogin.ts. The Firefly login
# flow drives a real, system-installed Chrome or Microsoft Edge via CDP so the
# user can sign in interactively; the executable is auto-detected from common
# install paths per OS. Set this to override that detection (e.g. a portable
# install or a non-standard path) when auto-detection fails.
# OMNIROUTE_LOGIN_BROWSER_PATH=
# ── Circuit breaker thresholds and reset windows ──
# Used by: open-sse/config/constants.ts → src/lib/resilience/settings.ts.
# Defaults match historical PROVIDER_PROFILES values (post-scaling for
@@ -1390,10 +1323,6 @@ APP_LOG_TO_FILE=true
# Default: 100000
# CALL_LOGS_TABLE_MAX_ROWS=100000
# Force detailed request logging on or off, overriding the dashboard setting.
# Values: true | false | Default: unset (follow dashboard setting)
# ENABLE_REQUEST_LOGS=false
# Maximum age for orphaned active request log entries before the in-memory
# pending-request reaper removes them. Accepts milliseconds.
# Default: 3600000 (1 hour)
@@ -1470,6 +1399,10 @@ APP_LOG_TO_FILE=true
# Default: ~/.omniroute/plugins/ Override in dev/CI to point at a local plugin tree.
# OMNIROUTE_PLUGIN_PATH=
# Allow plugins to request the 'exec' permission (spawn child processes from the
# plugin worker sandbox). Disabled by default; set to 1 to enable (local operator only).
# OMNIROUTE_PLUGINS_ALLOW_EXEC=0
# ── Prompt cache (system prompt deduplication) ──
# Used by: open-sse/services — caches identical system prompts across requests.
# PROMPT_CACHE_MAX_SIZE=50 # Max cached entries (default: 50)
@@ -1549,14 +1482,6 @@ APP_LOG_TO_FILE=true
# Default: 86400000 (24 hours)
# OPENROUTER_CATALOG_TTL_MS=86400000
# Enrich the dashboard providers list with OpenRouter weekly ranking stats.
# ON by default; set false to skip the background fetch entirely (#9324).
# Used by: src/lib/catalog/openrouterProviderStats.ts
# OPENROUTER_PROVIDER_STATS_ENABLED=true
# Cache TTL for the OpenRouter provider stats snapshot, in ms.
# Default: 86400000 (24 hours)
# OPENROUTER_PROVIDER_STATS_TTL_MS=86400000
# ── Model catalog response shape ──
# Include display-friendly name fields in /v1/models responses.
# Disable for clients that expect model IDs only.
@@ -1577,13 +1502,6 @@ APP_LOG_TO_FILE=true
# DESIGNER_WEB_POLL_TIMEOUT_MS=60000 # Max wait for job completion (default: 60s)
# DESIGNER_WEB_POLL_INTERVAL_MS=2000 # Poll frequency (default: 2s)
# ── Adobe Firefly (Image Upscale) ──
# Base delay (ms) for the submit-retry exponential backoff when Adobe Firefly's
# upscale job submission is rate-limited. Used by:
# open-sse/services/adobeFireflyUpscale.ts::submitRetryDelayMs.
# Default: 8000 (20 under NODE_ENV=test/VITEST/NODE_TEST_CONTEXT).
# ADOBE_FIREFLY_SUBMIT_BASE_DELAY_MS=8000
# ── AWS Bedrock (Kiro / Audio) ──
# Region used to construct AWS Bedrock endpoints. Used by:
# src/lib/providers/validation.ts and open-sse/handlers/audioSpeech.ts.
@@ -1678,26 +1596,6 @@ APP_LOG_TO_FILE=true
# Used by: src/lib/services/bootstrap.ts, src/app/api/services/mux/_lib.ts
# MUX_SERVICE_PORT=8322
# ── Dario embedded service ──
# Override the host/port the embedded Dario (Claude Code subscription proxy)
# daemon binds to and is reached at. Always bound to 127.0.0.1 — never
# configurable to 0.0.0.0. Rarely needed — defaults to 127.0.0.1:3456.
# Used by: src/lib/services/installers/dario.ts, src/lib/services/bootstrap.ts,
# src/app/api/services/dario/_lib.ts, src/app/api/services/dario/admin/_lib.ts,
# open-sse/executors/dario.ts
# DARIO_HOST=127.0.0.1
# DARIO_PORT=3456
# ── Dario embedded service ──
# Override the host/port the embedded Dario (Claude Code subscription proxy)
# daemon binds to and is reached at. Always bound to 127.0.0.1 — never
# configurable to 0.0.0.0. Rarely needed — defaults to 127.0.0.1:3456.
# Used by: src/lib/services/installers/dario.ts, src/lib/services/bootstrap.ts,
# src/app/api/services/dario/_lib.ts, src/app/api/services/dario/admin/_lib.ts,
# open-sse/executors/dario.ts
# DARIO_HOST=127.0.0.1
# DARIO_PORT=3456
# ── Local hostnames (Docker networking) ──
# Comma-separated additional hostnames treated as "local" for provider routing.
# Used by: open-sse/config/providerRegistry.ts — allows Docker service names.
@@ -1917,10 +1815,6 @@ APP_LOG_TO_FILE=true
# Tokens reserved for completion output when computing prompt budgets.
# Used by: open-sse/services/contextManager.ts. Default: 1024.
# CONTEXT_RESERVE_TOKENS=1024
# How many of the newest inline images to keep when pruning older ones to fit
# the context window (#8560). Used by: open-sse/services/contextManager.ts.
# Default: 2.
# CONTEXT_KEEP_LATEST_IMAGES=2
# ── Model alias rewriting (legacy compatibility) ──
# Toggle the legacy model-alias compatibility layer used by older clients.
@@ -1930,18 +1824,6 @@ APP_LOG_TO_FILE=true
# ── Devin CLI binary path ──
# Used by: open-sse/executors/devin-cli.ts. Default: looked up via PATH.
# CLI_DEVIN_BIN=devin
# Agentic bridge-only binary override. The bridge still executes ACP stdio only.
# CLI_DEVIN_AGENTIC_BIN=devin
# Required isolated HOME for the agentic Devin child process.
# DEVIN_AGENTIC_HOME=/home/bridge
# Bounded ACP turn timeout in milliseconds. Default: 120000.
# DEVIN_AGENTIC_ACP_TIMEOUT_MS=120000
# Agentic bridge model aliases. Values must keep the devin-cli-agentic/ prefix.
# DEVIN_BRIDGE_MODEL=devin-cli-agentic/swe-1-7
# DEVIN_BRIDGE_SONNET_MODEL=devin-cli-agentic/swe-1-7
# DEVIN_BRIDGE_OPUS_MODEL=devin-cli-agentic/swe-1-7
# DEVIN_BRIDGE_HAIKU_MODEL=devin-cli-agentic/swe-1-7
# DEVIN_BRIDGE_SUBAGENT_MODEL=devin-cli-agentic/swe-1-7
# ── Command Code (custom CLI) callback ──
# Local port used for OAuth-style callbacks from the Command Code CLI helper.
@@ -1996,15 +1878,6 @@ APP_LOG_TO_FILE=true
# CHANGELOG_BASE_REF=origin/release/v0.0.0
# ALLOW_CHANGELOG_REMOVALS=1
# ── Remote audio provider nodes ──
# Used by: src/app/api/v1/_shared/audioProviderNodes.ts — lets the /v1/audio/*
# routes use an OpenAI-compatible provider node hosted outside localhost.
# OFF by default: routing audio to a remote host changes egress identity, so it
# must be an explicit operator decision. Loopback/private nodes (localhost,
# 127.0.0.1, 172.16-31.x) are always allowed and unaffected by this flag.
# When enabled, the node authenticates with the API key stored on its connection.
# AUDIO_REMOTE_PROVIDER_NODES=false
# ── 1Proxy egress pool ──
# Used by: src/lib/oneproxySync.ts — fetches proxy nodes from the OmniRoute
# CrofAI 1Proxy service. Disable, override URL, or tune the import quality.
@@ -2212,11 +2085,6 @@ PLAYGROUND_COMPARE_MAX_COLUMNS=4
# MEMORY_TYPED_DECAY_EPISODIC_DAYS=30 # episodic TTL in days; 0 = episodic immune too
# MEMORY_TYPED_DECAY_ACCESS_IMMUNITY=3 # access_count >= N → immune; 0 disables access immunity
# MEMORY_TYPED_DECAY_SWEEP_INTERVAL=0 # periodic sweep interval (seconds); 0 = no periodic sweep
# ─── Memory Backend Connectors (Generic HTTP) ──────────────────────────────
# NOTION_API_KEY=
# NOTION_API_URL=
# OBSIDIAN_API_KEY=
# OBSIDIAN_API_URL=
# AgentBridge + Traffic Inspector (Group A)
# AgentBridge
@@ -2232,15 +2100,6 @@ INSPECTOR_MAX_BODY_KB=1024
INSPECTOR_MASK_SECRETS=true
INSPECTOR_LLM_HOSTS_EXTRA=
INSPECTOR_INTERNAL_INGEST_TOKEN=
# Shared secret for identity-preserving internal REST hops (#9260): when an
# OmniRoute component calls another local OmniRoute route, this token (sent as
# x-omniroute-internal-service-token) marks the request as internal so the
# original caller identity is preserved. OPT-IN: unset disables the mechanism.
# Used by: src/lib/api/internalServiceAuth.ts
# OMNIROUTE_INTERNAL_SERVICE_TOKEN=
# File-based variant (secret-file pattern; wins only when the inline var is
# unset): path to a file whose trimmed content is the token.
# OMNIROUTE_INTERNAL_SERVICE_TOKEN_FILE=
# Quota Sharing (Group B — planos 16+22)
QUOTA_STORE_DRIVER=sqlite # sqlite | redis
# QUOTA_STORE_REDIS_URL= # ex.: redis://localhost:6379 (apenas quando driver=redis)
@@ -2351,11 +2210,6 @@ QUOTA_STORE_DRIVER=sqlite # sqlite | redis
# Host port for the 1-click Redis launcher. Default: 6379. Bump if the host
# already binds 6379. The container's internal port stays 6379.
# OMNIROUTE_REDIS_HOST_PORT=
# Host interface the 1-click Redis launcher publishes on. Default: 127.0.0.1
# (loopback only). The launcher starts Redis WITHOUT a password, so binding
# 0.0.0.0 hands every host on your LAN an unauthenticated Redis — only widen
# this if you also set a password on the instance yourself.
# OMNIROUTE_REDIS_BIND_HOST=
# Redis image used by the 1-click Redis launcher. Default: redis:7-alpine.
# Override to redis:8-alpine or a private registry mirror as needed.
# OMNIROUTE_REDIS_IMAGE=
@@ -2457,29 +2311,3 @@ QUOTA_STORE_DRIVER=sqlite # sqlite | redis
# OMNIROUTE_DATA_DIR are both unset. Locates the Notion web-thread session cache.
# ─────────────────────────────────────────────────────────────────────────────
# VIBEPROXY_DATA_DIR=
# ── Internal service auth (management-plane service-to-service calls) ─────────
# Inline token for internal service authentication; prefer the _FILE variant in
# containerized deployments so the secret never lands in the environment table.
# OMNIROUTE_INTERNAL_SERVICE_TOKEN=
# Path to a file containing the internal service token (overrides the inline var).
# OMNIROUTE_INTERNAL_SERVICE_TOKEN_FILE=
# ═══════════════════════════════════════════════════════════════════════════════
# 26. RADAR FEED (SELF-HOSTING)
# ═══════════════════════════════════════════════════════════════════════════════
# Optional add-on (feature flag RADAR_ENABLED, default off — see feature flag
# settings, not an env var) that overlays a signed, freshly-curated free-model
# catalog on top of the release baseline. Both variables below are optional and
# only needed to point the client at a self-hosted/forked feed instead of the
# default OmniRoute Radar feed. Used by: src/lib/radar/sync.ts,
# src/lib/radar/pinnedKeys.ts.
# Base URL of the Radar feed service. Overrides the built-in default so forks
# and self-hosters can point at their own signed feed.
# RADAR_FEED_URL=https://radar.omniroute.online
# Ed25519 public key (base64-DER SPKI or PEM) used to verify the feed
# signature, replacing the pinned default key. Required when self-hosting a
# feed signed with a different key pair.
# RADAR_FEED_PUBKEY=

View File

@@ -1,4 +0,0 @@
#!/usr/bin/env bash
if [ "$1" = "view" ]; then echo "3.8.99"; exit 0; fi
if [ "$1" = "install" ]; then echo "added 1 package"; exit 0; fi
exit 0

View File

@@ -39,17 +39,6 @@ updates:
# the duplication gate — migrate the gate intentionally, not via dependabot.
- dependency-name: "jscpd"
update-types: ["version-update:semver-major"]
# ioredis is a SOFT/optional dependency loaded through a dynamic import
# (src/lib/quota/redisQuotaStore.ts — "Redis driver requires ioredis package"),
# so a breaking major never fails at build or typecheck time: the only consumers
# are the distributed quota store (redisQuotaStore.ts, storeFactory.ts) and the
# `import type Redis` in src/shared/utils/rateLimiter.ts. Nothing in the unit or
# vitest suites exercises a live Redis connection, so a v5→v6 API break would ship
# green and only surface at runtime for operators running distributed quota — the
# exact users least able to absorb it. #9310 grouped that major with 9 harmless
# bumps; majors here need their own PR and a deliberate migration review.
- dependency-name: "ioredis"
update-types: ["version-update:semver-major"]
# @huggingface/transformers is HARD-PINNED at 3.5.2 (exact, no caret) — FROZEN.
# It is load-bearing for the LLMLingua ONNX compression engine (open-sse/services/
# compression/engines/llmlingua/ — worker.ts pins @huggingface/transformers@3.5.2)

View File

@@ -9,14 +9,11 @@
## Validation
Choose the change type and focused loop from the
[Contribution Golden Path](../docs/dev/CONTRIBUTION_GOLDEN_PATH.md). The full unit suite,
Vitest, the 60% coverage gate, and the production build all run in CI on this PR (#8329):
Run only the focused loop for what you changed — the full unit suite, Vitest, the
60% coverage gate, and the production build all run in CI on this PR (#8329):
- [ ] Change type: provider / routing / UI / i18n / CLI / DB / build-deploy / other
- [ ] Focused tests and category gates from the golden path
- [ ] Focused tests for the change: `node --import tsx/esm --test tests/unit/<file>.test.ts`
- [ ] `npm run lint`
- [ ] Reconciled with the current active release base; focused checks rerun afterward
- [ ] Production-code changes include a new or updated automated test in this PR
- [ ] SonarQube PR analysis is green or any remaining issues are explicitly documented below
@@ -32,4 +29,4 @@ Vitest, the 60% coverage gate, and the production build all run in CI on this PR
## Reviewer Notes
- Call out any risky areas, migrations, feature flags, or manual validation that reviewers should know about.
- Call out any risky areas, migrations, feature flags, or manual validation that reviewers should know about.

71
.github/workflows/build-fork.yml vendored Normal file
View File

@@ -0,0 +1,71 @@
name: Publish Fork Image to GHCR
on:
push:
branches: [main]
tags:
- "v*"
workflow_dispatch:
# Least-privilege default: read-only at the top level; the build job that pushes to
# GHCR grants packages: write itself (Scorecard TokenPermissions).
permissions:
contents: read
env:
IMAGE_NAME: ghcr.io/kang-heewon/omniroute
jobs:
build:
name: Build and Push Fork Image
if: github.repository == 'kang-heewon/OmniRoute'
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout
uses: actions/checkout@v7
with:
persist-credentials: false
- name: Set up QEMU
uses: docker/setup-qemu-action@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Login to GitHub Container Registry
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@v6
with:
images: ${{ env.IMAGE_NAME }}
tags: |
type=raw,value=latest,enable={{is_default_branch}}
type=sha,prefix=sha-
type=ref,event=tag
labels: |
org.opencontainers.image.title=omniroute
org.opencontainers.image.description=Unified AI proxy/router — fork image
org.opencontainers.image.url=https://github.com/kang-heewon/OmniRoute
org.opencontainers.image.source=https://github.com/kang-heewon/OmniRoute
org.opencontainers.image.licenses=MIT
- name: Build and push
uses: docker/build-push-action@v7
with:
context: .
target: runner-base
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max

View File

@@ -1,57 +0,0 @@
name: Build App
on:
workflow_dispatch:
push:
branches: ["**"]
permissions:
contents: read
jobs:
build:
name: Fast Production Build
runs-on: ubuntu-latest
steps:
- name: Expand Virtual Memory (Native 10GB Swap)
run: |
sudo swapoff -a || true
sudo rm -f /mnt/swapfile /swapfile
sudo fallocate -l 10G /mnt/swapfile || sudo dd if=/dev/zero of=/mnt/swapfile bs=1M count=10240
sudo chmod 600 /mnt/swapfile
sudo mkswap /mnt/swapfile
sudo swapon /mnt/swapfile
free -h
- name: Checkout repository
uses: actions/checkout@v7
with:
persist-credentials: false
- name: Setup Node.js
uses: actions/setup-node@v7
with:
node-version: "24"
cache: npm
- name: Install dependencies
run: npm ci
- name: Build Next.js app & CLI bundle
run: |
npm run build:release
env:
NODE_OPTIONS: "--max-old-space-size=12288"
OMNIROUTE_BUILD_MEMORY_MB: "12288"
OMNIROUTE_USE_TURBOPACK: "1"
- name: Archive build outputs
run: |
tar -czf omniroute-build.tar.gz .build dist
- name: Upload build artifact
uses: actions/upload-artifact@v4
with:
name: omniroute-build
path: omniroute-build.tar.gz
retention-days: 7

View File

@@ -35,25 +35,13 @@ jobs:
workflow: ${{ steps.classify.outputs.workflow }}
testsOnly: ${{ steps.classify.outputs.testsOnly }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
with:
persist-credentials: false
fetch-depth: 0
- uses: actions/setup-node@v7
with:
node-version: ${{ env.CI_NODE_VERSION }}
# Refuse a PR that targets its own head branch before spending anything on it. #8912 has
# head == base == release/v3.8.50: no diff, can never merge, and it sits in the queue with
# a full check board attached on every push to that branch. One field comparison.
- name: Reject a PR that targets its own branch
if: github.event_name == 'pull_request'
env:
HEAD_REF: ${{ github.head_ref }}
BASE_REF: ${{ github.base_ref }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
BASE_SHA: ${{ github.event.pull_request.base.sha }}
run: node scripts/check/check-pr-self-target.mjs
- id: classify
env:
EVENT_NAME: ${{ github.event_name }}
@@ -137,17 +125,6 @@ jobs:
- run: npm run check:route-guard-membership
- run: npm run check:test-discovery
- run: npm run check:tracked-artifacts
# (gap 30) Also lives in quality.yml's PR-only "Merge integrity" job — because the
# CHANGELOG half of that job needs a base to diff against. This half does NOT: the
# generator either reproduces the committed SKILL.md files or it does not.
#
# Keeping it PR-only left a real hole. This cycle's merge trains landed in batches with
# `--admin`, which bypasses required checks, so three SKILL.md files drifted from the route
# catalog, rode the release squash into `main`, and the next cycle's sync-back turned them
# into a base-red that blocked EVERY PR into release/v3.8.50 until #8954. Running it here
# means a push to `main` catches the drift at the source instead of the next cycle
# inheriting it.
- run: npm run check:agent-skills-sync
# WS1.7 (v3.8.49 plan): Dockerfile lint (hadolint, pinned by digest).
# failure-threshold=error keeps the 5 pre-existing warnings (DL3008/DL3003/
# DL3016 version pinning / WORKDIR) visible without blocking; any ERROR fails.
@@ -353,16 +330,8 @@ jobs:
install -m 0755 /tmp/osv/*linux_amd64 "$HOME/.local/bin/osv-scanner"
# actionlint — official download script
bash <(curl -fsSL https://raw.githubusercontent.com/rhysd/actionlint/main/scripts/download-actionlint.bash) latest "$HOME/.local/bin"
# zizmor — PyPI (pipx preferred, pip --user fallback); lands in ~/.local/bin.
# PINNED on purpose. Unpinned, the runner installed whatever PyPI served that day and
# measured 1 finding MORE than the devbox on the identical commit (190 vs 189) during
# the v3.8.49 cycle — which cost a second rebaseline push per release, chasing a
# number that was never the code's. The ratchet compares counts across machines, so
# the auditor version has to be the same on both. Bump this deliberately, and
# rebaseline in the same commit: check-workflows.mjs now prints `zizmorVersion=` next
# to the count so the new number is traceable to the tool that produced it.
ZIZMOR_VERSION=1.25.2
pipx install "zizmor==$ZIZMOR_VERSION" || pip install --user "zizmor==$ZIZMOR_VERSION"
# zizmor — PyPI (pipx preferred, pip --user fallback); lands in ~/.local/bin
pipx install zizmor || pip install --user zizmor
# oasdiff — download latest linux amd64 tarball via gh (authed), extract binary
rm -rf /tmp/oasd && mkdir -p /tmp/oasd
gh release download --repo oasdiff/oasdiff --pattern '*linux_amd64.tar.gz' --dir /tmp/oasd
@@ -480,26 +449,15 @@ jobs:
# UI keys move with dashboard code OR message catalogs.
if: ${{ github.event_name != 'pull_request' || (github.event.pull_request.draft == false && (needs.changes.outputs.i18n == 'true' || needs.changes.outputs.code == 'true')) }}
steps:
# fetch-depth: 0 — the value-drift gate diffs en.json against the merge base to
# find rewritten English strings. On a shallow clone the base ref is missing and
# the gate self-skips (base-unresolved), so it would never actually run.
- uses: actions/checkout@v7
with:
persist-credentials: false
fetch-depth: 0
- uses: actions/setup-node@v7
with:
node-version: ${{ env.CI_NODE_VERSION }}
cache: npm
- uses: ./.github/actions/npm-ci-retry
- run: node scripts/i18n/check-ui-keys-coverage.mjs --threshold=65
# #8463: a rewritten English value used to leave its 39 translations behind
# silently (googleOAuthWarning shipped wrong copy in 39 locales for months).
# Key parity above cannot see it — a stale translation counts as covered.
- name: i18n UI value drift (stale translations)
env:
BASE_REF: ${{ github.base_ref && format('origin/{0}', github.base_ref) || '' }}
run: node scripts/i18n/check-ui-value-drift.mjs
# #8038: cheap single-locale glossary/protected-terms consistency gate —
# complements i18n-ui-coverage (key parity) and the ICU `i18n` job below
@@ -751,13 +709,7 @@ jobs:
test-unit:
name: Unit Tests (${{ matrix.shard }}/8)
# Same dynamic-runner rule as Build (own-origin only; fallback ubuntu-latest).
# PINNED to hosted, deliberately not on the USE_VPS_RUNNER switch (gap 19). One variable
# governed the build and the test jobs, which want OPPOSITE machines: the build needs the
# .113's RAM, the tests need the hosted runner's link. Measured on 2026-07-29 —
# actions/setup-node took 20m06s on .113 with 4 concurrent runners versus 16s hosted (npm
# cache restore saturating the link), while the tests themselves tied, 2m54 vs 2m31. So
# self-hosted is strictly worse here and there is nothing to configure.
runs-on: ubuntu-latest
runs-on: ${{ (vars.USE_VPS_RUNNER == 'true' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)) && fromJSON('["self-hosted","omni-release"]') || 'ubuntu-latest' }}
timeout-minutes: 25
# needs: changes (not build) — this job never downloads the next-build artifact;
# gating it on Build only serialized ~20min of wall-clock for nothing. Jobs that
@@ -829,13 +781,7 @@ jobs:
test-vitest:
name: Vitest (MCP / autoCombo / UI components)
# Same dynamic-runner rule as Build (own-origin only; fallback ubuntu-latest).
# PINNED to hosted, deliberately not on the USE_VPS_RUNNER switch (gap 19). One variable
# governed the build and the test jobs, which want OPPOSITE machines: the build needs the
# .113's RAM, the tests need the hosted runner's link. Measured on 2026-07-29 —
# actions/setup-node took 20m06s on .113 with 4 concurrent runners versus 16s hosted (npm
# cache restore saturating the link), while the tests themselves tied, 2m54 vs 2m31. So
# self-hosted is strictly worse here and there is nothing to configure.
runs-on: ubuntu-latest
runs-on: ${{ (vars.USE_VPS_RUNNER == 'true' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)) && fromJSON('["self-hosted","omni-release"]') || 'ubuntu-latest' }}
timeout-minutes: 15
# needs: changes (not build) — no artifact consumed; see test-unit note.
needs: changes
@@ -1169,8 +1115,7 @@ jobs:
if FILES=$(node scripts/quality/balance-e2e-shards.mjs "$SHARD" 9); then
if [ -z "$FILES" ]; then echo "[e2e-balance] shard $SHARD has no files"; exit 0; fi
echo "[e2e-balance] shard $SHARD runs:"; echo "$FILES"
# FILES is our own newline-separated path list, so word-splitting is intended
# shellcheck disable=SC2086,SC2046
# shellcheck disable=SC2086 — FILES is our own newline-separated path list
npx playwright test $(echo "$FILES" | tr '\n' ' ') --reporter=line,junit
else
echo "[e2e-balance] balancer unavailable — plain --shard fallback"
@@ -1272,8 +1217,6 @@ jobs:
- name: Generate dashboard
env:
EVENT_NAME: ${{ github.event_name }}
# Workflow-controlled data (job results), not user input — safe to read here.
NEEDS_JSON: ${{ toJSON(needs) }}
run: |
status() {
case "$1" in
@@ -1288,29 +1231,6 @@ jobs:
echo "# 🚀 CI Dashboard" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
# (gap 12) A cancelled job never reported a verdict, and in a long table that reads the
# same as a green one. `cancel-in-progress` plus incremental fixing cancels jobs on every
# push, and this cycle the Vitest job was cancelled in rounds 1, 2 and 3 — it only ran to
# completion in round 4, where it revealed a suite that had been broken the whole cycle
# plus two production bugs. A gate that never finishes is indistinguishable from one that
# passes, so name them at the TOP instead of leaving them to be spotted mid-table.
CANCELLED_JOBS=$(printf '%s' "$NEEDS_JSON" \
| jq -r 'to_entries | map(select(.value.result == "cancelled")) | .[].key' 2>/dev/null \
| sort | paste -sd", " -) || CANCELLED_JOBS=""
if [ -n "$CANCELLED_JOBS" ]; then
{
echo "> ### ⚫ Cancelled — no verdict was reported"
echo ">"
echo "> \`$CANCELLED_JOBS\`"
echo ">"
echo "> These did not fail; they never finished, so nothing was checked. Treat this"
echo "> run as INCOMPLETE for those gates. If the cancellation came from"
echo "> \`cancel-in-progress\` on a newer push, the newer run covers it — otherwise"
echo "> re-run them before reading this dashboard as green."
echo ""
} >> "$GITHUB_STEP_SUMMARY"
fi
echo "## 🧱 Core Checks" >> "$GITHUB_STEP_SUMMARY"
echo "| Job | Status |" >> "$GITHUB_STEP_SUMMARY"
echo "|-----|--------|" >> "$GITHUB_STEP_SUMMARY"

View File

@@ -19,7 +19,7 @@ jobs:
actions: read
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- uses: github/codeql-action/init@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81 # v4.37.3

View File

@@ -30,7 +30,7 @@ jobs:
JWT_SECRET: ci-build-secret-with-sufficient-length-for-validation
API_KEY_SECRET: ci-api-key-secret-with-sufficient-length-aaaa
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
@@ -59,14 +59,8 @@ jobs:
- run: pip install schemathesis
- name: Schemathesis smoke (high-risk endpoints, blocking)
run: |
# /api/auth/oidc/* is a BROWSER redirect flow (302 to the IdP, 302 back to
# /login?oidc_error=... on every failure), not a REST endpoint: Schemathesis reads
# those 302s as "the API accepted a schema-violating request" and the configured-off
# 400 as "rejected a schema-compliant request". Documenting the flow in the spec is
# still right (operators need it); fuzzing it is not what this smoke is for.
schemathesis run docs/openapi.yaml --url http://localhost:20128 \
--include-path-regex '^/v1/(chat/completions|models)$|^/api/(auth|keys)' \
--exclude-path-regex '^/api/auth/oidc/' \
--max-examples 8 --workers 4 --checks all --max-response-time 30 \
--request-timeout 20 --suppress-health-check all --no-color
- name: promptfoo injection-guard (blocking)

View File

@@ -4,7 +4,6 @@ on:
push:
branches:
- main
- "release/v*"
tags:
- "v*"
paths-ignore:
@@ -58,20 +57,39 @@ jobs:
REF_TYPE: ${{ github.ref_type }}
INPUT_VERSION: ${{ inputs.version }}
PROMOTE_INPUT: ${{ inputs.promote_latest }}
DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
run: |
set -euo pipefail
# 1) Resolve version/channel from the trigger. Only the current default
# release branch publishes the mutable `next` channel; main keeps `main`.
VERSION=$(bash scripts/ci/resolve-docker-publish-version.sh \
"$EVENT_NAME" "$REF_TYPE" "$REF_NAME" "$INPUT_VERSION" "$DEFAULT_BRANCH")
# 1) Resolve version string from the trigger (all inputs come via env).
case "$EVENT_NAME" in
workflow_dispatch)
VERSION="${INPUT_VERSION#v}"
;;
push)
if [ "$REF_TYPE" = "tag" ]; then
VERSION="${REF_NAME#v}"
else
# Push to main → build & tag as `main` only. Never touch :latest.
VERSION="main"
fi
;;
release)
VERSION="${REF_NAME#v}"
;;
*)
VERSION="${REF_NAME#v}"
;;
esac
# Sanity-check: only allow [A-Za-z0-9._-] in VERSION (defense in depth).
if ! printf '%s' "$VERSION" | grep -qE '^[A-Za-z0-9._-]+$'; then
echo "Refusing to use unsafe VERSION value: $VERSION" >&2
exit 1
fi
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
# 2) Decide whether to promote :latest. Floating channels are never
# eligible, and the helper independently fails closed for non-semver.
# 2) Decide whether to promote :latest.
PROMOTE="false"
if [ "$VERSION" = "main" ] || [ "$VERSION" = "next" ]; then
if [ "$VERSION" = "main" ]; then
PROMOTE="false"
elif printf '%s' "$VERSION" | grep -qE -- '-(rc|alpha|beta|pre|next)'; then
echo "Pre-release identifier detected — skipping :latest."
@@ -91,10 +109,10 @@ jobs:
fi
echo "promote_latest=$PROMOTE" >> "$GITHUB_OUTPUT"
# 3) Skip immutable version tags that already exist. Floating `main`
# and `next` channels are intentionally rebuilt on every matching push.
# 3) Skip if this exact version is already published in Docker Hub.
# `main` is always rebuilt (mutable floating tag).
SKIP="false"
if [ "$VERSION" != "main" ] && [ "$VERSION" != "next" ]; then
if [ "$VERSION" != "main" ]; then
if docker manifest inspect "diegosouzapw/omniroute:${VERSION}" >/dev/null 2>&1; then
echo "Image diegosouzapw/omniroute:${VERSION} already exists on Docker Hub — skipping rebuild."
SKIP="true"
@@ -137,13 +155,13 @@ jobs:
uses: docker/setup-buildx-action@v4
- name: Login to Docker Hub
uses: docker/login-action@v4.5.2
uses: docker/login-action@v4
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v4.5.2
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
@@ -237,13 +255,13 @@ jobs:
uses: docker/setup-buildx-action@v4
- name: Login to Docker Hub
uses: docker/login-action@v4.5.2
uses: docker/login-action@v4
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v4.5.2
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
@@ -372,14 +390,14 @@ jobs:
- name: Upload Trivy SARIF to Security tab
if: needs.prepare.outputs.version != 'main'
continue-on-error: true
uses: github/codeql-action/upload-sarif@v4.37.3
uses: github/codeql-action/upload-sarif@v4
with:
sarif_file: trivy-results.sarif
category: trivy-image
- name: Update Docker Hub description
# Only refresh README/description when we actually promote :latest
# (avoids overwriting from main, next, or back-fill builds).
# (avoids overwriting from main pushes or back-fill builds).
if: needs.prepare.outputs.promote_latest == 'true'
uses: peter-evans/dockerhub-description@v5
with:

View File

@@ -120,18 +120,6 @@ jobs:
env:
JWT_SECRET: ci-build-secret-with-sufficient-length-for-validation
NODE_OPTIONS: "--max_old_space_size=6144"
# Linux builds with webpack, not Turbopack. Turbopack's production build
# allocates natively (Rust, off the V8 heap), so --max_old_space_size does
# not bound it, and on this module graph it peaks above what the hosted
# runner can give — the VM is reclaimed mid-compile with "The runner has
# received a shutdown signal", no exit code. That is what silently took the
# whole desktop channel out of v3.8.49: the linux leg died, `release` was
# skipped, and the release shipped with ZERO assets. Measured on a 32 GB
# box the same build passes and peaks past 14 GB. The webpack fallback is
# the project's documented escape hatch for RAM-constrained machines
# (docs/reference/ENVIRONMENT.md, #6409) and is the same remedy already
# applied to nightly-compat's Node 26 build (#8090).
OMNIROUTE_USE_TURBOPACK: ${{ matrix.platform == 'linux' && '0' || '1' }}
run: npm run build
- name: Sync version in electron/package.json
@@ -229,16 +217,6 @@ jobs:
release:
name: Create Release
needs: [validate, build]
# Fail-partial, not fail-closed. `build` is a 4-leg matrix with `fail-fast: false`,
# so the legs that succeed still upload their artifacts — but a default `needs:`
# gate skips this job the moment ANY leg fails, discarding all of them. That is
# exactly what happened to v3.8.49: the linux leg died and the release shipped with
# ZERO assets, throwing away 1.7 GB of good Windows/macOS installers **and** the
# source archives + SBOM, which do not depend on a build at all. The result was
# indistinguishable from "this version has no desktop channel".
# Now: attach everything that did build, then fail the job loudly (see the last
# step) so an incomplete channel is visible instead of silent.
if: ${{ !cancelled() && needs.validate.result == 'success' }}
runs-on: ubuntu-latest
permissions:
contents: write # softprops/action-gh-release creates the GitHub Release
@@ -249,33 +227,11 @@ jobs:
persist-credentials: false
fetch-depth: 0
# `merge-multiple` is deliberately OFF. It resolves same-name collisions by ARRIVAL
# ORDER, and the two macOS jobs each emit their own `latest-mac.yml` listing only their
# own dmg (measured: 338 and 350 bytes, different content, identical name). One silently
# overwrote the other — arm64 won in the published v3.8.48, and since the Intel dmg
# carries no arch suffix in its name, electron-updater's
# `files.find(url includes process.arch) ?? files.shift()` sends every Intel Mac to the
# ARM dmg. Downloading into per-artifact subdirectories keeps both, so they can be
# merged on purpose instead of by luck.
- name: Download all artifacts
uses: actions/download-artifact@v8
with:
path: artifacts
# Writes release-assets/latest-mac.yml with BOTH dmgs, un-suffixed entry first (that is
# the one electron-updater can only reach through its fallback). Refuses to write when the
# inputs disagree on version — a manifest stitched from two builds is worse than none.
- name: Merge the per-arch macOS updater manifests
run: node scripts/release/merge-mac-update-manifest.mjs artifacts release-assets
# Everything else moves across as-is. The partial latest-mac.yml files are excluded so
# they cannot clobber the merged one; -n is a second belt on the same braces.
- name: Collect the remaining artifacts
run: |
mkdir -p release-assets
find artifacts -type f ! -name latest-mac.yml -exec cp -n {} release-assets/ \;
echo "release-assets:"
ls -la release-assets/
path: release-assets
merge-multiple: true
- name: Create source archives
env:
@@ -319,47 +275,6 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
verify-desktop-assets:
name: Verify desktop assets landed
needs: [validate, release]
# Deliberately a SEPARATE job, not a final step of `release`: failing inside
# `release` would cascade into `publish-npm` (which gates on `needs: release`) and
# block the npm channel over a desktop-only gap. Here the assets are attached, npm
# still publishes, and an incomplete desktop channel shows up as a red job instead
# of passing unnoticed — the v3.8.49 release had ZERO assets and every gate was
# green, because nothing ever asserted the release HAS binaries.
if: ${{ !cancelled() && needs.release.result == 'success' }}
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Assert every platform is present on the release
env:
# Regex-validated (^v[0-9]+\.[0-9]+\.[0-9]+$) in the `validate` job, and
# passed via env rather than interpolated into the script body.
VERSION: ${{ needs.validate.outputs.version }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
names=$(gh release view "$VERSION" --repo "$GITHUB_REPOSITORY" \
--json assets --jq '.assets[].name')
echo "Assets on $VERSION:"
echo "$names" | sed 's/^/ /'
missing=""
# `[ ... ] && missing=...` as the last command in a branch returns 1 and
# would abort the whole script under Actions' default `set -e`. Use if/fi.
for want in '\.exe$' '\.dmg$' '\.AppImage$' '\.deb$' '^latest.*\.yml$' '\.source\.tar\.gz$'; do
if ! echo "$names" | grep -qE "$want"; then
missing="$missing $want"
fi
done
if [ -n "$missing" ]; then
echo "::error::Desktop channel incomplete on $VERSION — no asset matching:$missing"
exit 1
fi
echo "✓ every platform present on $VERSION"
publish-npm:
name: Publish to npm
needs: [validate, release]

View File

@@ -71,20 +71,7 @@ jobs:
node-version: "26"
cache: npm
- uses: ./.github/actions/npm-ci-retry
# #8090 — this is the ONLY Node 26 build in the whole CI matrix (ci.yml pins
# CI_NODE_VERSION=24). It failed every nightly with the runner-reclaimed
# signature ("The runner has received a shutdown signal" / "The operation was
# canceled", no exit code) always at the same Turbopack compile phase — a
# classic OOM kill on the memory-constrained ubuntu-latest runner. Turbopack's
# native (Rust, off-V8-heap) allocation is NOT bounded by --max-old-space-size
# and peaks far higher than webpack on this large module graph (#6409), and is
# heavier still under Node 26. Use the documented webpack fallback here: it still
# validates that the app *builds* on Node 26 (the point of this compat job) at a
# much lower memory peak. Turbopack-on-Node-24 stays covered by ci.yml's build
# job. See docs/reference/ENVIRONMENT.md (OMNIROUTE_USE_TURBOPACK) and #6409.
- run: npm run build
env:
OMNIROUTE_USE_TURBOPACK: "0"
compat-tests:
name: Node ${{ matrix.node }} Compat Tests (${{ matrix.shard }}/4)

View File

@@ -148,7 +148,7 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v6
with:
persist-credentials: false
- uses: actions/setup-node@v7

View File

@@ -143,8 +143,7 @@ jobs:
MODE="--with-build --full-ci"
fi
echo "[release-green] mode: $MODE (event: $EVENT_NAME)"
# MODE is an intentional flag list, so word-splitting is wanted here
# shellcheck disable=SC2086
# shellcheck disable=SC2086 — MODE is an intentional flag list
node scripts/quality/validate-release-green.mjs --json --hermetic $MODE \
1> release-green.json 2> release-green.log
echo "exit=$?" >> "$GITHUB_OUTPUT"
@@ -193,7 +192,7 @@ jobs:
gh issue comment "$EXISTING" --repo "$GITHUB_REPOSITORY" --body-file issue-body.md
echo "Updated existing issue #$EXISTING"
else
gh issue create --repo "$GITHUB_REPOSITORY" --title "$TITLE" --label base-red --body-file issue-body.md
gh issue create --repo "$GITHUB_REPOSITORY" --title "$TITLE" --body-file issue-body.md
fi
- name: Upload report artifact
@@ -249,8 +248,7 @@ jobs:
MODE="--with-build --full-ci"
fi
echo "[main-green] mode: $MODE (event: $EVENT_NAME)"
# MODE is an intentional flag list, so word-splitting is wanted here
# shellcheck disable=SC2086
# shellcheck disable=SC2086 — MODE is an intentional flag list
node scripts/quality/validate-release-green.mjs --json --hermetic $MODE \
1> main-green.json 2> main-green.log
echo "exit=$?" >> "$GITHUB_OUTPUT"
@@ -291,7 +289,7 @@ jobs:
gh issue comment "$EXISTING" --repo "$GITHUB_REPOSITORY" --body-file issue-body.md
echo "Updated existing issue #$EXISTING"
else
gh issue create --repo "$GITHUB_REPOSITORY" --title "$TITLE" --label base-red --body-file issue-body.md
gh issue create --repo "$GITHUB_REPOSITORY" --title "$TITLE" --body-file issue-body.md
fi
- name: Upload report artifact
@@ -303,145 +301,3 @@ jobs:
main-green.json
main-green.log
if-no-files-found: ignore
# ── Banking lane (#8584) ──────────────────────────────────────────────────
# The ratchet is asymmetric: RAISING a cap is a ten-second manual JSON edit made
# under merge pressure, LOWERING one requires someone to run `--update` and commit
# — which no workflow does. Grep `.github/workflows/` for `--update`: only
# wiki-sync.yml (unrelated) and ci.yml's check-quality-ratchet.mjs --require-tighten
# (a different script, a different metric). So a cap outlives the code that earned
# it and every completed decomposition silently becomes a growth allowance for
# whoever touches the file next. Measured on 2026-07-25: 18 frozen files already at
# or under the 800-line new-file cap, up to 132x (schemas.ts, 19 lines / 2523 cap),
# and 31 unfulfilled "tighten via --update next cycle" notes honoured exactly once
# (-1 unit) in six weeks.
#
# This job closes that loop by making the DOWNWARD direction as automatic as the
# upward one is easy. It measures the active release branch, runs the shrink-only
# `--update` paths, and opens ONE always-current PR with the result. It never
# pushes to release/* — a human still merges, so a bad measurement cannot land
# unreviewed. verify-ratchet-bank.mjs is the hard guarantee that the automation can
# only ever write in the shrink direction; if anything was raised, added, or a
# rebaseline note was touched, the job aborts and opens nothing.
#
# Schedule/dispatch only, deliberately NOT on push: banking has no latency
# requirement (a shrink banked within 8h is fine) and a per-merge run would rebuild
# the PR branch repeatedly during merge campaigns while paying for a full ESLint
# walk each time. Detection stays on push (release-green above); banking is batched.
bank-ratchet-shrinks:
name: Bank ratchet shrinks
if: ${{ github.event_name != 'push' }}
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Resolve active release branch
id: branch
env:
INPUT_BRANCH: ${{ github.event.inputs.branch }}
run: |
set -euo pipefail
if [ -n "${INPUT_BRANCH:-}" ]; then
TARGET="$INPUT_BRANCH"
else
TARGET=$(git for-each-ref --format='%(refname:short)' 'refs/remotes/origin/release/v*' \
| sed 's#origin/##' \
| sort -t/ -k2 -V \
| tail -1)
fi
if [ -z "$TARGET" ]; then echo "No release/v* branch found"; exit 1; fi
# Same strict guard as the validation job — blocks ref/command injection
# through the workflow_dispatch input.
if ! printf '%s' "$TARGET" | grep -qE '^release/v[0-9]+\.[0-9]+\.[0-9]+$'; then
echo "Refusing non-canonical branch name: $TARGET"; exit 1
fi
echo "target=$TARGET" >> "$GITHUB_OUTPUT"
echo "bank_branch=chore/bank-ratchet-${TARGET#release/}" >> "$GITHUB_OUTPUT"
- name: Checkout the release branch
env:
TARGET: ${{ steps.branch.outputs.target }}
run: |
set -euo pipefail
git checkout "$TARGET"
git log -1 --oneline
- uses: actions/setup-node@v7
with:
node-version: "24"
cache: npm
- uses: ./.github/actions/npm-ci-retry
- name: Ratchet the baselines down
run: |
# Both --update paths are shrink-only by construction (file-size writes only
# on `improvements`, complexity-ratchets only when `.improved`), and both exit
# non-zero while the branch is over baseline — which is exactly when there is
# nothing to bank. Their exit code is not the signal; the verifier below is.
set +e
node scripts/check/check-file-size.mjs --update
node scripts/check/check-complexity-ratchets.mjs --update
exit 0
- name: Verify the write only went downward
id: verify
run: |
set -euo pipefail
# Exits 1 if ANYTHING was raised/added or a rebaseline note was touched.
# `set -e` then aborts the job before a commit exists — no PR is opened.
node scripts/quality/verify-ratchet-bank.mjs > bank-summary.md
if [ -n "$(git status --porcelain config/quality/)" ]; then
echo "changed=true" >> "$GITHUB_OUTPUT"
else
echo "changed=false" >> "$GITHUB_OUTPUT"
echo "Nothing to bank — baselines already match the code."
fi
- name: Open / update the banking PR
if: steps.verify.outputs.changed == 'true'
env:
GH_TOKEN: ${{ github.token }}
TARGET: ${{ steps.branch.outputs.target }}
BANK_BRANCH: ${{ steps.branch.outputs.bank_branch }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: |
set -euo pipefail
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git checkout -B "$BANK_BRANCH"
git add config/quality/
git commit -m "chore(quality): bank ratchet shrinks measured on ${TARGET}"
git push --force origin "$BANK_BRANCH"
{
echo "Automated banking of quality-ratchet **shrinks** already present in"
echo "\`${TARGET}\` — the downward half of the ratchet, which nothing else runs (#8584)."
echo ""
echo "Produced by \`check:file-size --update\` + \`check:complexity-ratchets --update\`,"
echo "then verified by \`scripts/quality/verify-ratchet-bank.mjs\`: **nothing was raised,"
echo "nothing was added, no rebaseline note was touched** — the job aborts without"
echo "opening a PR if any of those is violated."
echo ""
echo "No product code changes. Merging retires growth allowances that the code no"
echo "longer needs; not merging leaves them available to whoever edits those files next."
echo ""
cat bank-summary.md
echo ""
echo "**Run:** ${RUN_URL}"
} > pr-body.md
EXISTING=$(gh pr list --repo "$GITHUB_REPOSITORY" --head "$BANK_BRANCH" \
--state open --json number --jq '.[0].number' 2>/dev/null || echo "")
if [ -n "$EXISTING" ]; then
gh pr edit "$EXISTING" --repo "$GITHUB_REPOSITORY" --body-file pr-body.md
echo "Updated existing PR #$EXISTING"
else
gh pr create --repo "$GITHUB_REPOSITORY" --base "$TARGET" --head "$BANK_BRANCH" \
--title "chore(quality): bank ratchet shrinks (${TARGET})" --body-file pr-body.md
fi

View File

@@ -56,15 +56,8 @@ env:
jobs:
publish:
# Same dynamic-runner rule as ci.yml's `build`/`test-unit`: `build:cli` falls back to a
# full `next build`, whose working set outgrew the 16 GB hosted runner during the
# v3.8.49 cycle — the publish died with "The runner has received a shutdown signal"
# mid-"Creating an optimized production build" while v3.8.48 had still fit in 16min.
# This job never runs on `pull_request`, so the fork-safety clause is always true here;
# it is kept verbatim so the expression stays greppable against ci.yml.
runs-on: ${{ (vars.USE_VPS_RUNNER == 'true' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)) && fromJSON('["self-hosted","omni-release"]') || 'ubuntu-latest' }}
runs-on: ubuntu-latest
permissions:
actions: read # find + download the CI run's next-build artifact for this SHA
contents: write # gh release upload (attach SBOM to the GitHub Release)
id-token: write # npm provenance
packages: write # publish to npm.pkg.github.com
@@ -152,74 +145,6 @@ jobs:
run: |
npm version "$VERSION" --no-git-tag-version --allow-same-version
# Fast path: CI already built the standalone tree for THIS commit and uploaded it as
# `next-build`. `build:cli` (scripts/build/prepublish.ts) only shells out to a full
# `next build` when `.build/next/standalone/server.js` is missing — restoring the
# artifact turns the heaviest step of the publish into a download. Matching on
# `head_sha` is the tree-equality guarantee: same commit, same tree.
# Best-effort by design (retention is 1 day): every miss falls through to the build
# step below, which is why the dynamic runner above matters as the backstop.
#
# The `head_repository.full_name == env.REPO` clause is a supply-chain guard, not a
# filter refinement. This artifact becomes the published npm tarball. `pull_request`
# runs from forks execute in THIS repository's context and upload their own
# `next-build` built from fork-controlled source, and the runs API returns them for a
# matching `head_sha` — 57 such runs exist in this repo today. Without the clause,
# anything that made a fork's head commit coincide with the publish commit could put
# attacker-built bytes on npm. Requiring the run to originate from this repository
# excludes every fork run while keeping the fast path intact (verified: the same
# single run is selected either way for the current tip).
# CodeQL: actions/artifact-poisoning/critical.
- name: Reuse CI's next-build artifact (skips the heavy rebuild)
if: steps.resolve.outputs.skip != 'true'
continue-on-error: true
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
HEAD_SHA: ${{ github.sha }}
REPO: ${{ github.repository }}
run: |
set -uo pipefail
# The question is "which run HAS the artifact", not "which run passed" (gap 16).
# Requiring `conclusion == "success"` on the whole run discarded a perfectly good tree
# whenever any unrelated shard went red — one flaky test then pushed the publish into
# the 40-minute build this step exists to avoid. The artifact is only uploaded if the
# Build job itself succeeded, so its PRESENCE is the accurate signal; the run's overall
# conclusion is noise from jobs that have nothing to do with the tree.
#
# `head_repository.full_name == env.REPO` stays, and it is not a filter refinement:
# this tree becomes the published npm tarball, and fork `pull_request` runs execute in
# THIS repository's context uploading their own next-build. That clause is the
# supply-chain guard (CodeQL actions/artifact-poisoning).
CANDIDATES=$(gh api "repos/$REPO/actions/runs?head_sha=$HEAD_SHA&per_page=100" \
--jq '[.workflow_runs[]
| select(.name == "CI"
and .head_repository.full_name == env.REPO)]
| sort_by(.run_started_at) | reverse | .[0:5] | .[].id') || CANDIDATES=""
if [ -z "$CANDIDATES" ]; then
echo "::notice::no CI run from this repository for $HEAD_SHA — falling back to a full build"
exit 0
fi
RUN=""
for candidate in $CANDIDATES; do
if gh run download "$candidate" --repo "$REPO" --name next-build --dir /tmp/next-build 2>/dev/null; then
RUN="$candidate"
break
fi
echo " run $candidate carries no usable next-build — trying the next"
done
if [ -z "$RUN" ]; then
echo "::notice::none of the candidate runs still carries next-build (1-day retention) — falling back to a full build"
exit 0
fi
tar -xzf /tmp/next-build/e2e-build.tar.gz -C .
rm -rf /tmp/next-build
if [ -f .build/next/standalone/server.js ]; then
echo "✅ standalone tree restored from CI run $RUN — build:cli will skip next build"
else
echo "::notice::extract did not yield .build/next/standalone — falling back to a full build"
rm -rf .build
fi
- name: Build CLI bundle (standalone app)
if: steps.resolve.outputs.skip != 'true'
env:
@@ -256,18 +181,6 @@ jobs:
if: steps.resolve.outputs.skip != 'true'
run: npm run check:pack-boot
# The boot-smoke above proves a CLEAN install boots. It does not prove the path that
# actually broke us: installing over an existing version, where ~110 SQLite migrations
# run against a populated database. v3.8.48 shipped as a hotfix because the published
# 3.8.47 crashed on boot, and the v3.8.49 upgrade path was first exercised end-to-end
# by hand on a real 3.8.48 box (VPS .16) — after publishing, which is exactly backwards.
# Runs BEFORE `npm stage publish` so a broken upgrade never reaches the registry at all;
# a staged package that is never approved simply expires, with no `npm deprecate` needed.
- name: Prove clean-install AND upgrade-over-previous both boot
if: steps.resolve.outputs.skip != 'true'
timeout-minutes: 30
run: npm run check:install-upgrade
# WS1.3 (D2, v3.8.49 plan): STAGED publishing by default — `npm stage publish`
# parks the exact bytes on the registry WITHOUT making them installable; the
# owner then verifies and approves with 2FA (`npm stage approve`), moving the

View File

@@ -57,31 +57,6 @@ jobs:
git diff --name-only "$BASE_SHA" "$HEAD_SHA" > changed-files.txt
node scripts/quality/classify-pr-changes.mjs changed-files.txt >> "$GITHUB_OUTPUT"
build:
name: Build (advisory)
needs: changes
if: ${{ github.event_name != 'pull_request' || ((github.event.pull_request.draft == false || startsWith(github.head_ref, 'mergify/merge-queue/')) && needs.changes.outputs.code == 'true') }}
# Dynamic runner — same fork-safe rule as ci.yml / fast-gates.
runs-on: ${{ (vars.USE_VPS_RUNNER == 'true' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)) && fromJSON('["self-hosted","omni-release"]') || 'ubuntu-latest' }}
# #7307: advisory for the first week of release-PR runs; remove
# continue-on-error after the production-build signal is stable.
continue-on-error: true
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
persist-credentials: false
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
with:
node-version: ${{ env.CI_NODE_VERSION }}
cache: npm
- uses: ./.github/actions/npm-ci-retry
- run: npm run check:node-runtime
- run: npm run build
env:
OMNIROUTE_USE_TURBOPACK: "1"
# No artifact upload here: the PR-to-release quality workflow has no
# downstream package/e2e jobs that consume the Next.js build output.
# Docs/OpenAPI contract gates only — existence reason is doc accuracy + route refs.
# Split out of fast-gates so pure-docs PRs skip typecheck/unit while still validating docs.
docs-gates:
@@ -112,20 +87,7 @@ jobs:
# release captain has USE_VPS_RUNNER=true AND this is not a fork PR (own-origin
# branches only — a fork PR must never execute on the LAN runner). Var unset/false
# or a fork PR falls back to ubuntu-latest, so this is inert until the flag flips.
# PINNED to hosted (gap 19). This job carried the USE_VPS_RUNNER expression, and that
# expression was DEAD CONFIGURATION: across 160 quality.yml runs the job never once landed on
# a self-hosted runner — every non-skipped sample is `GitHub Actions NNNN`. The classifier is
# not at fault: in the same window ci.yml's Build demonstrably ran on omniroute-113-7 and
# omniroute-113-6, so self-hosted runs are visible when they happen.
#
# And if it ever HAD fired it would have inherited the measured penalty, because this job's
# first two steps are exactly the bottleneck: actions/setup-node + npm ci took 20m06s on .113
# with 4 concurrent runners versus 16s hosted (npm cache restore saturating the link). Median
# here is 5.6 min hosted across 72 successful runs.
#
# With this pinned, USE_VPS_RUNNER governs ONLY build-like jobs — one variable, one coherent
# purpose. That is what gap 19 asked for; a second variable turned out to be unnecessary.
runs-on: ubuntu-latest
runs-on: ${{ (vars.USE_VPS_RUNNER == 'true' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)) && fromJSON('["self-hosted","omni-release"]') || 'ubuntu-latest' }}
# tsx gates (known-symbols, route-guard-membership) import modules that open
# SQLite on load; provide DB env so a fresh CI DB initializes cleanly.
env:
@@ -155,18 +117,7 @@ jobs:
- run: npm run check:fetch-targets
# docs-all / openapi-routes / docs-symbols live in docs-gates (path-filtered).
- run: npm run check:deps
# #8522: --base-ref mode for PR events — compare against max(frozen, base) so
# inherited drift (base already over frozen cap) doesn't red an innocent PR.
# workflow_dispatch (no PR base) falls back to absolute comparison.
- name: File-size ratchet (base-relative on PR)
env:
PR_BASE_SHA: ${{ github.event.pull_request.base.sha }}
run: |
if [ -n "$PR_BASE_SHA" ]; then
npm run check:file-size -- --base-ref "$PR_BASE_SHA"
else
npm run check:file-size
fi
- run: npm run check:file-size
- run: npm run check:error-helper
- run: npm run check:migration-numbering
- run: npm run check:public-creds
@@ -190,81 +141,6 @@ jobs:
# Complexity + cognitive-complexity: ONE ESLint walk (both baselines still
# enforced separately by ruleId). Avoids two cold tree walks on fast-path.
- run: npm run check:complexity-ratchets
# ── G0 (trilho .50): gates do trilho A que faltavam no trilho B ──────────────
# The god-file refactor happens in PRs→release/**; without these, the release
# rail never sees a new import cycle, dead code, duplication or a security
# regression until the release PR to main. Deliberately NOT brought here:
# bundle-size (self-skips without a build — this rail's build job is advisory
# and uploads nothing, so it would be dead configuration) and the coverage
# run (fast-unit already runs the full suite; the coverage ratchet stays on
# the main rail via --allow-missing in lint-guard).
- run: npm run check:cycles
- run: npm run check:lockfile
- name: Duplication ratchet
run: npm run check:duplication
- name: Dead-code ratchet (knip)
run: npm run check:dead-code
- name: Type coverage ratchet
run: npm run check:type-coverage
- name: Compression budget ratchet
run: npm run check:compression-budget
# Security scanners — same hardened install as ci.yml quality-extended
# (gh release download = authenticated, 5000 req/hr; curl to api.github.com
# is rate-limited to 60/hr and silently no-ops when throttled). The blocking
# gates below SKIP (exit 0) when their binary is absent — only a measured
# regression vs config/quality/quality-baseline.json blocks.
- name: Install security scanners (gitleaks/osv/actionlint/zizmor/oasdiff)
continue-on-error: true
env:
GH_TOKEN: ${{ github.token }}
run: |
set +e
mkdir -p "$HOME/.local/bin"
# Ratchets compare scanner COUNTS across runs. Pin every auditor: a rule-set
# update must be an explicit PR that re-measures/rebaselines, never a random
# red (or green) caused by whatever "latest" served that morning.
GITLEAKS_VERSION=v8.30.1
OSV_SCANNER_VERSION=v2.3.8
ACTIONLINT_VERSION=v1.7.12
ZIZMOR_VERSION=1.25.2
OASDIFF_VERSION=v1.19.1
# gitleaks — pinned linux x64 tarball via gh (authed), extract binary
rm -rf /tmp/gl && mkdir -p /tmp/gl
gh release download "$GITLEAKS_VERSION" --repo gitleaks/gitleaks --pattern '*linux_x64.tar.gz' --dir /tmp/gl
tar -xzf /tmp/gl/*linux_x64.tar.gz -C "$HOME/.local/bin" gitleaks
# osv-scanner — pinned linux amd64 bare binary via gh (authed)
rm -rf /tmp/osv && mkdir -p /tmp/osv
gh release download "$OSV_SCANNER_VERSION" --repo google/osv-scanner --pattern '*linux_amd64' --dir /tmp/osv
install -m 0755 /tmp/osv/*linux_amd64 "$HOME/.local/bin/osv-scanner"
# actionlint — official installer from a pinned release tag (never main)
bash <(curl -fsSL "https://raw.githubusercontent.com/rhysd/actionlint/${ACTIONLINT_VERSION}/scripts/download-actionlint.bash") "$ACTIONLINT_VERSION" "$HOME/.local/bin"
# zizmor — pinned PyPI package (same version as ci.yml quality-extended)
pipx install "zizmor==$ZIZMOR_VERSION" || pip install --user "zizmor==$ZIZMOR_VERSION"
# oasdiff — pinned linux amd64 tarball via gh (authed), extract binary
rm -rf /tmp/oasd && mkdir -p /tmp/oasd
gh release download "$OASDIFF_VERSION" --repo Tufin/oasdiff --pattern '*linux_amd64.tar.gz' --dir /tmp/oasd
tar -xzf /tmp/oasd/*linux_amd64.tar.gz -C "$HOME/.local/bin" oasdiff
# ALWAYS export the bin dir (even if any step above failed)
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
"$HOME/.local/bin/gitleaks" version || true
"$HOME/.local/bin/actionlint" -version || true
"$HOME/.local/bin/osv-scanner" --version || true
"$HOME/.local/bin/oasdiff" --version || true
zizmor --version || true
- name: Secret scan (gitleaks, ratchet, blocking)
run: npm run check:secrets -- --ratchet
- name: Vulnerability ratchet (osv-scanner, ratchet, blocking)
run: npm run check:vuln-ratchet -- --ratchet
- name: Workflow lint (actionlint+zizmor, ratchet, blocking)
run: npm run check:workflows -- --ratchet
# BASE_REF is read by the script from the env (never interpolated into a
# shell body) — workflow-injection-safe. actions/checkout fetches remote
# refs, not a local branch named github.base_ref, so prefix origin/ or this
# gate self-skips every PR with reason=base-unresolved.
- name: OpenAPI breaking-change (oasdiff, ratchet, blocking)
env:
BASE_REF: ${{ github.base_ref && format('origin/{0}', github.base_ref) || '' }}
run: npm run check:openapi-breaking -- --ratchet
- name: Typecheck (core)
run: npm run typecheck:core
# #7033: dashboard-scoped typecheck gate — src/app/(dashboard) TSX is not
@@ -359,13 +235,7 @@ jobs:
needs: changes
if: ${{ github.event_name != 'pull_request' || ((github.event.pull_request.draft == false || startsWith(github.head_ref, 'mergify/merge-queue/')) && needs.changes.outputs.code == 'true') }}
# Dynamic runner — see fast-gates (own-origin + flag; fork/unset → ubuntu-latest).
# PINNED to hosted, deliberately not on the USE_VPS_RUNNER switch (gap 19). One variable
# governed the build and the test jobs, which want OPPOSITE machines: the build needs the
# .113's RAM, the tests need the hosted runner's link. Measured on 2026-07-29 —
# actions/setup-node took 20m06s on .113 with 4 concurrent runners versus 16s hosted (npm
# cache restore saturating the link), while the tests themselves tied, 2m54 vs 2m31. So
# self-hosted is strictly worse here and there is nothing to configure.
runs-on: ubuntu-latest
runs-on: ${{ (vars.USE_VPS_RUNNER == 'true' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)) && fromJSON('["self-hosted","omni-release"]') || 'ubuntu-latest' }}
env:
JWT_SECRET: ci-lint-secret-with-sufficient-length-for-validation
API_KEY_SECRET: ci-lint-api-key-secret-long
@@ -401,13 +271,7 @@ jobs:
# critical path again (~8.5min → ~4.5min on ubuntu-latest; ~2min on the 8-slot
# runner box). Node's native --test-shard=N/total takes any denominator — only
# this matrix and the TEST_SHARD env below encode the shard count.
# PINNED to hosted, deliberately not on the USE_VPS_RUNNER switch (gap 19). One variable
# governed the build and the test jobs, which want OPPOSITE machines: the build needs the
# .113's RAM, the tests need the hosted runner's link. Measured on 2026-07-29 —
# actions/setup-node took 20m06s on .113 with 4 concurrent runners versus 16s hosted (npm
# cache restore saturating the link), while the tests themselves tied, 2m54 vs 2m31. So
# self-hosted is strictly worse here and there is nothing to configure.
runs-on: ubuntu-latest
runs-on: ${{ (vars.USE_VPS_RUNNER == 'true' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)) && fromJSON('["self-hosted","omni-release"]') || 'ubuntu-latest' }}
strategy:
fail-fast: false
matrix:
@@ -450,12 +314,6 @@ jobs:
if: ${{ github.event_name != 'pull_request' || ((github.event.pull_request.draft == false || startsWith(github.head_ref, 'mergify/merge-queue/')) && needs.changes.outputs.code == 'true') }}
runs-on: ubuntu-latest
continue-on-error: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true }}
# G0 (trilho .50): security-events:read lets the CodeQL ratchet below read open
# code-scanning alerts via `gh api .../code-scanning/alerts` (same as ci.yml's
# quality-gate job). contents: read keeps checkout working.
permissions:
contents: read
security-events: read
steps:
- uses: actions/checkout@v7
with:
@@ -477,29 +335,6 @@ jobs:
- name: ESLint (baseline congelado — warning novo = vermelho)
# lint:json writes the report; --max-warnings 0 keeps no-new-warnings policy.
run: npm run lint:json -- --max-warnings 0
# ── G0 (trilho .50): motor de ratchet também no trilho B ─────────────────────
# This job just wrote .artifacts/eslint-results.json — collect-metrics prefers
# that file, so the ratchet engine lands here at ZERO extra ESLint cost (one
# inventory, two consumers; same reason ci.yml chains lint → quality-gate).
# The coverage-report artifact does not exist on this rail, so both ratchet
# invocations run --allow-missing: coverage.* metrics skip gracefully while
# the deterministic ones (eslint / openapi-coverage / i18n-ui) stay BLOCKING.
# Coverage authority remains on the main rail (ci.yml test-coverage → quality-gate).
- run: npm run quality:collect
- name: Ratchet check (blocking)
run: node scripts/quality/check-quality-ratchet.mjs --allow-missing --summary .artifacts/quality-ratchet.md
- name: Require-tighten (blocking)
run: node scripts/quality/check-quality-ratchet.mjs --allow-missing --require-tighten
# CodeQL alerts ratchet — same semantics as ci.yml quality-gate: exits 1 ONLY
# on a real regression (open alerts > baseline in quality-baseline.json);
# a measurement failure (gh/auth/api) self-skips with exit 0.
- name: CodeQL alerts ratchet (blocking)
run: npm run check:codeql-ratchet
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Append ratchet summary
if: always()
run: cat .artifacts/quality-ratchet.md >> "$GITHUB_STEP_SUMMARY" || true
# Merge-integrity: pega no PR os dois vazamentos crônicos de merge que hoje só
# explodem na release-PR. (1) CHANGELOG-eat — o auto-resolve do merge come
@@ -523,7 +358,7 @@ jobs:
API_KEY_SECRET: ci-lint-api-key-secret-long
DISABLE_SQLITE_AUTO_BACKUP: "true"
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
fetch-depth: 0
persist-credentials: false

View File

@@ -18,7 +18,7 @@ jobs:
container:
image: semgrep/semgrep
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Run semgrep (advisory)

21
.gitignore vendored
View File

@@ -72,7 +72,6 @@ yarn-error.log*
# env files (can opt-in for committing if needed)
.env*
!.env.example
!.env.devin-bridge.example
!.env.homolog.example
# Provider API keys (never commit)
*.api-key
@@ -172,6 +171,7 @@ config/quality/test-impact-map.json
# GitNexus local index
.gitnexus
.worktrees
bin/omniroute.mjs
# Consistent with .dockerignore / .npmignore
.omc/
@@ -201,17 +201,12 @@ scripts/i18n/_pending-keys.json
.codegraph/
# Fumadocs generated source
/.source/
# Temporary local worktrees used to build unpublished npm tarballs
/.deploy-build-*/
.source/
# AI agent local settings and configs
.agents/
.antigravitycli/
.claude/
!tests/fixtures/devin-bridge/e2e-workspace/.claude/
!tests/fixtures/devin-bridge/e2e-workspace/.claude/**
# PR Reviews and local feedback files
pr_reviews*.json
@@ -238,10 +233,7 @@ omniroute.md
# mise configuration
mise.toml
# release-green artifacts (.gitignore has no inline comments — a trailing
# `# ...` becomes part of the pattern, so it must sit on its own line).
# Already covered by /_*/ above; kept explicit for discoverability.
_artifacts/
_artifacts/ # release-green artifacts
.claude-flow/
# ESLint file cache (npm run lint --cache / complexity ratchets)
@@ -251,8 +243,6 @@ _artifacts/
# CI/local quality artifacts (eslint-results.json, quality-ratchet.md, etc.)
.artifacts/
# Isolated Devin bridge workspaces, evidence, and test databases
.sandbox/
# Homologation E2E suite (npm run homolog) — real-environment credentials + report output
.env.homolog
@@ -260,8 +250,3 @@ tests/homolog/.auth/
tests/homolog/ui/.auth/
homolog-report/
docker-compose.yml.bak
.playwright-cli/
# Playwright screenshot/log output. Today every artifact happens to land inside
# output/**/.playwright-cli/ (covered above), but anything written directly to
# output/ would otherwise show up as untracked.
/output/

View File

@@ -86,10 +86,4 @@
regexes = [
'''latencyP\d{2}Ms''',
'''interleaved-thinking-2025-05-14''',
# v3.8.49 pre-flight (2026-07-28). Nenhum dos dois e credencial:
# - chave de localStorage do banner de patrocinio (#8723), so um identificador de UI;
# - x-api-key PUBLICO do Firefly web (documentado em open-sse/utils/publicCreds.ts:207);
# as duas ocorrencias sinalizadas estao em COMENTARIOS JSDoc, o runtime le de resolvePublicCred().
'''omniroute-kimi-sponsor-banner-dismissed-v1''',
'''SunbreakWebUI1''',
]

View File

@@ -17,13 +17,6 @@
# • Fallback path if Mergify misbehaves or the OSS plan changes: the manual
# merge-train runbook (docs/ops/MERGE_TRAIN.md) — remove labels, proceed by hand.
# Auto-enqueue (current Mergify model, 2026): auto_merge_conditions in
# merge_protections_settings — the rules-based queue action / autoqueue path is
# deprecated (EOL 2026-07-16). The owner-applied `queue` label IS the approval.
merge_protections_settings:
auto_merge_conditions:
- label = queue
queue_rules:
- name: release
# Any current or future release branch — the reason GitHub's native queue was
@@ -41,26 +34,14 @@ queue_rules:
# is intentionally NOT a condition here: the owner-applied `queue` label IS the
# approval in this repo's single-maintainer model (see governance header).
merge_conditions:
# "Zero failures" — EXCEPT the advisory "Build (advisory)" job (quality.yml):
# continue-on-error by design, and its GH-hosted Turbopack build hangs
# recurrently mid-"Creating an optimized production build" (100% failure rate
# across every sampled PR since the job was added 2026-07-27, always killed by
# a runner timeout/shutdown signal, never a real compile error). Any OTHER
# failure still blocks (anti-fail-open kept). The prior dast-smoke exception
# (#7225) was dropped here: dast-smoke's hang (#7226) has been dormant for
# weeks (0 failures in the last 30 runs; 2 all-time, none since 2026-07-13) —
# carrying its tolerance forward would mask problems it no longer causes.
- or:
- "#check-failure=0"
- and:
- "#check-failure=1"
- check-failure=Build (advisory)
- "#check-failure=0"
- "#check-pending=0"
- "#check-success>=1"
- check-success=Merge integrity (changelog + generated skills)
# NO batching: 'Merge Queue Batch' requires a paid Mergify tier (live finding
# 2026-07-15 — the queue command fails with "Cannot use Merge Queue batch" on
# the free plan). Serial queue (1 PR at a time) still automates the train.
# Batching: validate up to 10 queued PRs together (the manual train's sweet spot);
# don't hold a lone PR hostage waiting for siblings.
batch_size: 10
batch_max_wait_time: 5 min
# Squash keeps the one-commit-per-PR history the CHANGELOG reconciliation expects.
merge_method: squash

View File

@@ -4,14 +4,11 @@ data/
**/db.json
# VS Code extension test runtime (large binary, not needed in npm package)
app/vscode-extension/
**/data/
**/db.json
# Source code (pre-built dist/ is published instead)
#
# NOTA (2026-08-05): as entradas `app/*` foram removidas — o diretorio `app/`
# foi renomeado para `dist/` na Layer 1 e nao existe mais. Elas sugeriam um
# layout que ja nao e o do projeto.
# Source code (pre-built app/ is published instead)
#
# NOTE (#3578 / #3821-review): package.json "files" is the source of truth for what
# ships. It now allowlists the backend source closure the MCP server needs at runtime
@@ -52,6 +49,8 @@ scripts/
.vscode/
.agents/
.env*
app/.env
app/.env*
eslint.config.mjs
prettier.config.mjs
postcss.config.mjs
@@ -83,6 +82,8 @@ bun.lock
*.deb
*.rpm
electron/
app/electron/
app/vscode-extension/
# Subprojects
clipr/
@@ -92,6 +93,10 @@ vscode-extension/
# Root-level underscore-prefixed directories (private/draft — never publish)
/_*/
app/_*/
app/coverage/
app/logs/
app/tests/
# Consistent with .gitignore and .dockerignore
.DS_Store

View File

@@ -1,11 +1,6 @@
# Long reference tables are manually aligned; formatting the whole file causes noisy diffs.
docs/reference/ENVIRONMENT.md
# Generated by `npm run gen:provider-reference`; the generator aligns the tables and
# is their formatter of record. Without this, lint-staged reformats the file whenever
# it is staged and the next generator run reverts it — a diff ping-pong.
docs/reference/PROVIDER_REFERENCE.md
# Dense auto-generated free-tier budget rows (one object per line) — prettier multi-line expand blows past file-size cap 800.
open-sse/config/freeModelCatalog.data.ts

8
.source/dynamic.ts Normal file
View File

@@ -0,0 +1,8 @@
// @ts-nocheck
import { dynamic } from 'fumadocs-mdx/runtime/dynamic';
import * as Config from '../source.config';
const create = await dynamic<typeof Config, import("fumadocs-mdx/runtime/types").InternalTypeConfig & {
DocData: {
}
}>(Config, {"configPath":"source.config.ts","environment":"next","outDir":".source"}, {"doc":{"passthroughs":["extractedReferences"]}});

22
.source/source.config.mjs Normal file
View File

@@ -0,0 +1,22 @@
// source.config.ts
import { defineDocs, defineConfig } from "fumadocs-mdx/config";
var docs = defineDocs({
dir: "docs",
docs: {
files: [
"./architecture/**/*.md",
"./guides/**/*.md",
"./reference/**/*.md",
"./frameworks/**/*.md",
"./routing/**/*.md",
"./security/**/*.md",
"./compression/**/*.md",
"./ops/**/*.md"
]
}
});
var source_config_default = defineConfig();
export {
source_config_default as default,
docs
};

View File

@@ -196,7 +196,6 @@ npm install --prefix ~/.config/opencode/plugins/omniroute-opencode-plugin-prepro
| Compression pipeline tags | Combo names get tagged with their compression pipeline (e.g. `Combo: claude-primary [rtk🟡 → caveman🟠]`) when `features.compressionMetadata: true`. Intensity tokens render as a traffic-light emoji: 🟢 lite/minimal · 🟡 standard · 🟠 aggressive/full · 🔴 ultra | both hooks |
| Provider-tag prefix | Prepend short upstream-provider label to enriched names (e.g. `Claude - Claude Opus 4.7` vs `Kiro - Claude Opus 4.7`, `GHM - GPT 5`) so same-id models routed via different upstream connections group visibly in the picker (default-on, opt-out via `features.providerTag: false`) | both hooks |
| Usable-only filter | Filter to providers with at least one healthy connection in `/api/providers` (opt-in via `features.usableOnly`) | both hooks |
| Model allowlist/blocklist | Curate the model picker to a fixed set of IDs via `features.visibleModels` (allowlist) and/or `features.hiddenModels` (blocklist). Bare suffixes like `claude-opus-4-7` match any `{prefix}/claude-opus-4-7`. Both compose with `usableOnly` (all filters AND together). Blocklist wins over allowlist (deny takes precedence) | both hooks |
| Disk-cache fallback | Last-known-good catalog persisted to disk; hydrates on a cold start when `/v1/models` is unreachable (default-on, opt-out via `features.diskCache: false`) | `config` |
| Bearer injection + suffix-spoof guard | Adds `Authorization` on baseURL-matched requests only | `auth.loader.fetch` |
| Gemini schema sanitization | Strips `$schema`/`$ref`/`additionalProperties` for `gemini-*`/`google-vertex-gemini/*` | `auth.loader.fetch` wrap |
@@ -227,8 +226,6 @@ Every field is optional. Defaults mirror v0.1.0 behaviour so existing `opencode.
| `compressionMetadata` | `boolean` | `false` | Pull `/api/context/combos` so combo names get tagged with their compression pipeline, e.g. `Combo: claude-primary [rtk🟡 → caveman🟠]`. Intensity tokens render as traffic-light emoji (🟢 lite/minimal · 🟡 standard · 🟠 aggressive/full · 🔴 ultra) so the picker advertises "how compressed" each combo is at a glance. |
| `providerTag` | `boolean` | `true` | Prepend a short upstream-provider label to the enriched display name with `" - "` separator, so `cc/claude-opus-4-7 → Claude - Claude Opus 4.7` differs visibly from `kr/claude-opus-4-7 → Kiro - Claude Opus 4.7` in the OC TUI model picker. Label resolution: use `/api/pricing/models[<alias>].name` verbatim when ≤8 chars (e.g. `Claude`, `Kiro`, `Codex`, `Qwen`), otherwise fall back to `UPPER(alias)` (e.g. `GitHub Models``GHM`, `Gemini``GEMINI`). Idempotent. Combos intentionally skipped (the `Combo:` prefix already conveys multi-upstream). |
| `usableOnly` | `boolean` | `false` | Read `/api/providers` and filter the catalog to providers that have at least one connection with `isActive: true` AND `testStatus: 'active'`. Subtract-filter semantics: providers unknown to BOTH the pricing-models catalog AND the connection table pass through (so synthetic prefixes like `agentrouter/*` survive). On fetch failure the filter is disabled for the refresh — never hides the whole catalog. |
| `visibleModels` | `string[]` | _unset_ | Allowlist — when set and non-empty, only models whose raw `/v1/models` ID matches are emitted. Bare IDs (no slash, e.g. `claude-opus-4-7`) match any `{prefix}/claude-opus-4-7`; full IDs (e.g. `cc/claude-opus-4-7`) match exactly. Composes with `usableOnly` and `hiddenModels` (all filters AND together). Unset or empty = no filter. |
| `hiddenModels` | `string[]` | _unset_ | Blocklist — models whose raw ID matches are dropped. Same matching rules as `visibleModels`. When a model is in both `visibleModels` and `hiddenModels`, the blocklist wins (deny takes precedence). Composes with `usableOnly` and `visibleModels` (all filters AND together). Unset or empty = no filter. |
| `diskCache` | `boolean` | `true` | Persist the last successful `/v1/models` + `/api/combos` + enrichment + connections + compression snapshot to `${OPENCODE_DATA_DIR ?? ~/.local/share/opencode}/plugins/omniroute-<providerId>.json`. On a subsequent cold start where `/v1/models` throws (network down / IP whitelist drop / 5xx) the static block hydrates from the snapshot so OC's model picker survives offline. Soft-fail on read/write — never blocks publishing. |
| `geminiSanitization` | `boolean` | `true` | Strip `$schema`/`$ref`/`additionalProperties` from tool params when the model id matches `gemini` |
| `mcpAutoEmit` | `boolean` | `false` | Auto-write an `mcp.<providerId>` remote entry into the OC config pointing at `<baseURL>/api/mcp/stream` with the resolved Bearer token |
@@ -301,45 +298,7 @@ If you want a narrower-scoped Bearer for MCP (different from the chat/inference
- `compressionMetadata: true` annotates combo display names with their pipeline using traffic-light emoji for intensity (e.g. `Combo: claude-primary [rtk🟡 → caveman🟠]`) so the picker advertises which compression each combo applies and how heavy it is at a glance. Palette: 🟢 lite/minimal · 🟡 standard · 🟠 aggressive/full · 🔴 ultra. Unknown intensities fall through to raw text (`[rtk:custom-thing]`) so the plugin never hides a value OmniRoute knows but the plugin doesn't.
- `providerTag: true` (default) prepends a short upstream-provider label so the picker shows `Claude - Claude Opus 4.7` for `cc/claude-opus-4-7`, `Kiro - Claude Opus 4.7` for `kr/claude-opus-4-7`, and `GHM - GPT 5` for `ghm/gpt-5` (slot.name `GitHub Models` > 8 chars → abbreviated). Critical when the same model id is sold through multiple upstream connections with different cost/auth/rate-limit profiles. Set to `false` to keep the pre-v3.8.3 unsuffixed format.
#### Example — curating the model picker (allowlist + blocklist)
A typical OmniRoute instance serves 600+ models. The OpenCode TUI/CLI picker becomes unusable when you need to scroll through hundreds of entries to find the ~30 models you actually use. `visibleModels` and `hiddenModels` let you curate the picker to a fixed set of model IDs that persists in `opencode.json` across config resets.
```jsonc
{
"plugin": [
[
"@omniroute/opencode-plugin",
{
"providerId": "omniroute",
"baseURL": "https://or.example.com",
"features": {
"combos": true,
"enrichment": true,
"usableOnly": true,
"visibleModels": [
"claude-opus-4-7", // bare suffix: matches cc/claude-opus-4-7, kr/claude-opus-4-7, etc.
"cc/claude-sonnet-4-6", // exact: only the cc/ alias
"gemini-2.5-pro",
"gpt-5",
"o3",
"o3-pro",
"o4-mini",
],
"hiddenModels": [
"o3-mini", // hide the mini variant even if visibleModels is unset
],
},
},
],
],
}
```
- `visibleModels` is an allowlist — only models whose raw ID matches are emitted. Bare IDs (no slash) match any provider prefix; full IDs (with slash) match exactly.
- `hiddenModels` is a blocklist — listed models are dropped. When a model is in both lists, the blocklist wins (deny takes precedence).
- Both compose with `usableOnly` (all filters AND together: a model must pass usableOnly AND visibleModels AND not be in hiddenModels).
- Unset or empty = no filter (current behavior).
## Comparison vs `@omniroute/opencode-provider`
[`@omniroute/opencode-provider`](https://github.com/diegosouzapw/OmniRoute/tree/main/%40omniroute/opencode-provider) is the existing config-generator package — it writes a frozen `provider.<id>` block into `opencode.json` at build time. This plugin is the runtime integration.

View File

@@ -1,12 +1,12 @@
{
"name": "@omniroute/opencode-plugin",
"version": "0.2.1",
"version": "0.2.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@omniroute/opencode-plugin",
"version": "0.2.1",
"version": "0.2.0",
"license": "MIT",
"dependencies": {
"zod": "^4.4.3"

View File

@@ -23,7 +23,7 @@
"scripts": {
"build": "tsup",
"clean": "rm -rf dist",
"test": "node --import tsx/esm --test tests/scaffold.test.ts tests/auth.test.ts tests/options-schema.test.ts tests/multi-instance.test.ts tests/fetch-interceptor.test.ts tests/provider.test.ts tests/gemini-sanitize.test.ts tests/combos.test.ts tests/config-shim.test.ts tests/features.test.ts tests/feature-defaults.test.ts tests/usable-combo.test.ts tests/disk-snapshot-perms.test.ts tests/fork-features.test.ts tests/auto-combo-context.test.ts tests/provider-id-routing.test.ts tests/management-read-token.test.ts tests/auto-sync.test.ts tests/model-allowlist.test.ts",
"test": "node --import tsx/esm --test tests/scaffold.test.ts tests/auth.test.ts tests/options-schema.test.ts tests/multi-instance.test.ts tests/fetch-interceptor.test.ts tests/provider.test.ts tests/gemini-sanitize.test.ts tests/combos.test.ts tests/config-shim.test.ts tests/features.test.ts tests/usable-combo.test.ts tests/disk-snapshot-perms.test.ts tests/fork-features.test.ts tests/auto-combo-context.test.ts tests/provider-id-routing.test.ts tests/management-read-token.test.ts tests/auto-sync.test.ts",
"prepublishOnly": "npm run clean && npm run build && npm test"
},
"keywords": [

View File

@@ -177,8 +177,6 @@ const featuresSchema = z
mcpToken: z.string().min(1).optional(),
fetchInterceptor: z.boolean().optional(),
usableOnly: z.boolean().optional(),
visibleModels: z.array(z.string().min(1)).optional(),
hiddenModels: z.array(z.string().min(1)).optional(),
diskCache: z.boolean().optional(),
providerTag: z.boolean().optional(),
debugLog: z.boolean().optional(),
@@ -218,64 +216,6 @@ const optionsSchema = z
*/
export type OmniRoutePluginOptions = z.infer<typeof optionsSchema>;
/**
* Explicit default state for every boolean `features.*` toggle.
*
* #7624: `featuresSchema` marks every flag `.optional()` with no default, and
* the effective value is applied implicitly at each read site (default-ON flags
* use the `features.X !== false` convention, default-OFF flags use
* `features.X === true`). That implicit convention is scattered across the file,
* so an operator who omits the `features` block cannot tell whether
* combos / autoCombos / enrichment are enabled — they think features are
* disabled when they are actually on. Centralising the declared defaults here
* (mirroring the read-site conventions exactly, so runtime behaviour is
* unchanged) makes the effective flags introspectable and self-documenting.
*/
export const OMNIROUTE_FEATURE_DEFAULTS = {
// default-ON (read sites use `features.X !== false`)
combos: true,
autoCombos: true,
enrichment: true,
diskCache: true,
providerTag: true,
fetchInterceptor: true,
geminiSanitization: true,
// default-OFF (read sites use `features.X === true`)
compressionMetadata: false,
usableOnly: false,
// Array flags: unset/empty = no filter. These are not boolean toggles —
// they are operator-curated model-ID lists applied in the dynamic and static
// hooks alongside usableOnly (all filters AND together).
// visibleModels: undefined, // allowlist — only listed IDs pass
// hiddenModels: undefined, // blocklist — listed IDs are dropped
mcpAutoEmit: false,
debugLog: false,
startupDebug: false,
} as const;
/** Union of the boolean feature-flag keys declared in `OMNIROUTE_FEATURE_DEFAULTS`. */
export type OmniRouteFeatureFlag = keyof typeof OMNIROUTE_FEATURE_DEFAULTS;
/**
* Resolve the EFFECTIVE boolean state of every feature toggle, applying the
* declared default for any flag the operator omitted. A missing `features`
* block (or an empty one) yields the full default set — so callers and the
* startup diagnostics can surface exactly which features are active instead of
* relying on the implicit `!== false` / `=== true` conventions. Purely
* derived: it does not mutate options nor change any read-site behaviour.
*/
export function resolveEffectiveFeatureFlags(
features?: OmniRoutePluginOptions["features"]
): Record<OmniRouteFeatureFlag, boolean> {
const f: Partial<Record<OmniRouteFeatureFlag, boolean>> = features ?? {};
const out = {} as Record<OmniRouteFeatureFlag, boolean>;
for (const key of Object.keys(OMNIROUTE_FEATURE_DEFAULTS) as OmniRouteFeatureFlag[]) {
const val = f[key];
out[key] = typeof val === "boolean" ? val : OMNIROUTE_FEATURE_DEFAULTS[key];
}
return out;
}
export const OMNIROUTE_PROVIDER_KEY = "omniroute" as const;
/** Deployed plugin version (injected at build time by tsup define). */
@@ -337,10 +277,7 @@ function trimLeadingDashes(value: string): string {
* sees a consistent identifier.
*/
export function resolveOmniRoutePluginOptions(opts?: OmniRoutePluginOptions): Required<
Pick<
OmniRoutePluginOptions,
"providerId" | "displayName" | "modelCacheTtl" | "autoSyncIntervalMs"
>
Pick<OmniRoutePluginOptions, "providerId" | "displayName" | "modelCacheTtl" | "autoSyncIntervalMs">
> & {
/**
* #6859: the UNPREFIXED provider id ("omniroute", "omniroute-preprod", …).
@@ -631,7 +568,7 @@ export function createOmniRouteAuthHook(opts?: OmniRoutePluginOptions): AuthHook
*/
export function invalidateOmniRouteFetchCache(
cache: OmniRouteFetchCache,
baseURL?: string
baseURL?: string,
): number {
if (!baseURL) {
const n = cache.size;
@@ -655,7 +592,7 @@ export function invalidateOmniRouteFetchCache(
*/
export async function resolveOmniRouteRuntimeAuth(
resolved: ResolvedOmniRoutePluginOptions,
readAuthJson?: OmniRouteReadAuthJson
readAuthJson?: OmniRouteReadAuthJson,
): Promise<{ apiKey: string; baseURL: string; managementReadToken: string } | null> {
const reader = readAuthJson ?? defaultReadAuthJson;
let authJson: AuthJsonShape | undefined | null;
@@ -682,7 +619,7 @@ export async function resolveOmniRouteRuntimeAuth(
e &&
(e as { type?: unknown }).type === "api" &&
typeof (e as { key?: unknown }).key === "string" &&
(e as { key: string }).key.length > 0
((e as { key: string }).key).length > 0
) {
entry = e as AuthJsonApiEntry;
break;
@@ -747,7 +684,7 @@ export async function forceSyncOmniRouteModels(args: {
const auth = await resolveOmniRouteRuntimeAuth(
resolved,
args.readAuthJson ?? defaultReadAuthJson
args.readAuthJson ?? defaultReadAuthJson,
);
if (!auth) {
return {
@@ -805,7 +742,7 @@ export async function forceSyncOmniRouteModels(args: {
rawCompressionCombos = await compressionMetaFetcher(
auth.baseURL,
auth.managementReadToken,
10_000
10_000,
);
} catch {
rawCompressionCombos = [];
@@ -830,7 +767,10 @@ export async function forceSyncOmniRouteModels(args: {
rawConnections,
expiresAt: t + resolved.modelCacheTtl,
};
const cacheKey = modelsCacheKey(auth.baseURL, `${auth.apiKey}\0${auth.managementReadToken}`);
const cacheKey = modelsCacheKey(
auth.baseURL,
`${auth.apiKey}\0${auth.managementReadToken}`,
);
cache.set(cacheKey, entry);
if (wantDiskCache) {
@@ -838,7 +778,7 @@ export async function forceSyncOmniRouteModels(args: {
const fingerprint = diskSnapshotIdentityFingerprint(
auth.baseURL,
auth.apiKey,
auth.managementReadToken
auth.managementReadToken,
);
const { expiresAt: _expiresAt, ...diskEntry } = entry;
await defaultDiskSnapshotWriter(resolved.providerId, diskEntry, fingerprint);
@@ -850,7 +790,7 @@ export async function forceSyncOmniRouteModels(args: {
console.warn(
`[omniroute-plugin] force sync ok providerId=${resolved.providerId} ` +
`models=${rawModels.length} combos=${rawCombos.length} ` +
`clearedMemory=${clearedMemory + clearedAll} disk=${clearedDisk}`
`clearedMemory=${clearedMemory + clearedAll} disk=${clearedDisk}`,
);
return {
@@ -951,7 +891,7 @@ export function startOmniRouteAutoSync(args: {
const result = await forceSyncOmniRouteModels({ resolved, cache });
if (!result.ok) {
console.warn(
`[omniroute-plugin] auto-sync failed providerId=${resolved.providerId}: ${result.error}`
`[omniroute-plugin] auto-sync failed providerId=${resolved.providerId}: ${result.error}`,
);
return;
}
@@ -962,7 +902,7 @@ export function startOmniRouteAutoSync(args: {
if (result.count !== lastCount) {
console.warn(
`[omniroute-plugin] auto-sync catalog size changed ${lastCount}${result.count} ` +
`(providerId=${resolved.providerId})`
`(providerId=${resolved.providerId})`,
);
lastCount = result.count;
}
@@ -983,7 +923,7 @@ export function startOmniRouteAutoSync(args: {
}
console.warn(
`[omniroute-plugin] auto-sync enabled intervalMs=${intervalMs} providerId=${resolved.providerId}`
`[omniroute-plugin] auto-sync enabled intervalMs=${intervalMs} providerId=${resolved.providerId}`,
);
return () => {
@@ -1039,13 +979,7 @@ export const OmniRoutePlugin: Plugin = async (_input, options) => {
const cfg = input as Config & {
command?: Record<
string,
{
template: string;
description?: string;
agent?: string;
model?: string;
subtask?: boolean;
}
{ template: string; description?: string; agent?: string; model?: string; subtask?: boolean }
>;
};
if (!cfg.command) cfg.command = {};
@@ -2066,7 +2000,6 @@ async function writeStartupDiagnostics(params: {
autoComboCount: number;
enrichment: OmniRouteEnrichmentMap;
autoCombos: OmniRouteRawAutoCombo[];
features?: OmniRoutePluginOptions["features"];
}): Promise<void> {
const {
providerId,
@@ -2077,7 +2010,6 @@ async function writeStartupDiagnostics(params: {
autoComboCount,
enrichment,
autoCombos,
features,
} = params;
const enriched = [...enrichment.entries()];
const withName = enriched.filter(([, e]) => e.name);
@@ -2090,16 +2022,6 @@ async function writeStartupDiagnostics(params: {
lines.push(
`models=${modelCount} combos=${comboCount} enrichment=${enrichmentSize} autoCombos=${autoComboCount}`
);
// #7624: surface the EFFECTIVE feature flags so an operator who omitted the
// `features` block can see combos/autoCombos/enrichment are on (the counts
// above can read 0 for reasons unrelated to the flags — e.g. missing auth).
const effectiveFlags = resolveEffectiveFeatureFlags(features);
lines.push(
`features(effective): ` +
(Object.keys(effectiveFlags) as OmniRouteFeatureFlag[])
.map((k) => `${k}=${effectiveFlags[k] ? "on" : "off"}`)
.join(" ")
);
lines.push(
`enrichment: ${withName.length} with name, ${withPricing.length} with pricing, ${withFree.length} free`
);
@@ -2833,118 +2755,6 @@ export function isUsableCombo(
return false;
}
// ─────────────────────────────────────────────────────────────────────────
// #9473 — Model allowlist / blocklist filter helpers
// ─────────────────────────────────────────────────────────────────────────
/**
* Pre-compiled filter structure for the model allowlist/blocklist.
*
* "exact" holds full raw IDs (e.g. "cc/claude-opus-4-7") for O(1) match.
* "suffixes" holds bare model IDs (e.g. "claude-opus-4-7") that match any
* "{prefix}/claude-opus-4-7" — so operators can curate by model name without
* knowing the provider prefix.
*/
export interface ModelListFilter {
exact: Set<string>;
suffixes: Set<string>;
}
/**
* Compile a string[] of model IDs into a pre-computed filter structure.
* Returns undefined when the list is empty or undefined — the "no filter"
* state that callers use as a passthrough.
*
* IDs containing a "/" are stored in "exact"; bare IDs (no slash) go into
* "suffixes" and match any "{prefix}/<suffix>" at check time.
*/
export function compileModelListFilter(list?: string[]): ModelListFilter | undefined {
if (!list || list.length === 0) return undefined;
const exact = new Set<string>();
const suffixes = new Set<string>();
for (const id of list) {
if (id.includes("/")) {
exact.add(id);
} else {
suffixes.add(id);
}
}
if (exact.size === 0 && suffixes.size === 0) return undefined;
return { exact, suffixes };
}
/**
* Decide whether a raw model ID passes the allowlist/blocklist filter.
*
* Rules (all filters AND together with usableOnly):
* - No visible filter and no hidden filter → keep (passthrough).
* - Visible filter set: id must match either the exact set or the suffix
* set (bare suffix "claude-opus-4-7" matches any "{prefix}/claude-opus-4-7").
* - Hidden filter set: id must NOT match either the exact or suffix set.
* - If id is in BOTH visible and hidden → DROP (deny wins — safer).
* - No-slash ids (e.g. combo names like "claude-primary") are checked
* against the exact set directly, and against the suffix set as a bare
* match.
*
* Pure function — exported so static + dynamic hooks share the same
* verdict logic without divergence.
*/
export function passesModelAllowlist(
id: string,
visible?: ModelListFilter,
hidden?: ModelListFilter
): boolean {
// Hidden filter takes precedence (deny wins over allow).
if (hidden) {
if (hidden.exact.has(id) || matchesSuffix(id, hidden.suffixes)) return false;
}
// Visible filter: if set, id must match.
if (visible) {
if (!visible.exact.has(id) && !matchesSuffix(id, visible.suffixes)) return false;
}
return true;
}
/**
* Decide whether a combo passes the allowlist filter. A combo keeps when
* AT LEAST ONE of its members matches the visible filter. When no visible
* filter is set, all combos pass. Combos with zero resolvable members pass
* (mirrors `isUsableCombo` semantics).
*/
export function passesComboAllowlist(
combo: OmniRouteRawCombo,
visible?: ModelListFilter
): boolean {
if (!visible) return true;
const steps = Array.isArray(combo.models) ? combo.models : [];
if (steps.length === 0) return true;
let sawResolvableMember = false;
for (const step of steps) {
if (step?.kind === "combo-ref") continue;
const modelId = typeof step?.model === "string" ? step.model : "";
if (modelId.length === 0) continue;
sawResolvableMember = true;
if (visible.exact.has(modelId) || matchesSuffix(modelId, visible.suffixes)) return true;
}
// No resolvable member → can't prove it should be hidden; keep.
if (!sawResolvableMember) return true;
// Every resolvable member failed the allowlist → drop.
return false;
}
/**
* Check whether a raw model ID matches any suffix in the set.
* For an id like `cc/claude-opus-4-7`, the suffix after the first `/`
* is checked against the suffixes set. For a bare id like `claude-primary`,
* the id itself is checked against the suffixes set.
*/
function matchesSuffix(id: string, suffixes: Set<string>): boolean {
if (suffixes.size === 0) return false;
const slash = id.indexOf("/");
const suffix = slash > 0 ? id.slice(slash + 1) : id;
return suffixes.has(suffix);
}
/**
* Slugify a combo display name into a copy/paste-friendly URL-safe segment.
* Lowercases, replaces any run of non-alphanumeric chars with a single dash,
@@ -3128,9 +2938,6 @@ export function createOmniRouteProviderHook(
const wantCompressionMeta = features.compressionMetadata === true;
const wantUsableOnly = features.usableOnly === true;
const wantProviderTag = features.providerTag !== false;
// #9473: model allowlist/blocklist — compile once per hook instance.
const visibleFilter = compileModelListFilter(features.visibleModels);
const hiddenFilter = compileModelListFilter(features.hiddenModels);
const now = deps.now ?? Date.now;
// T-07: cache holds RAW fetch results (not pre-derived ModelV2) so that
// the config-shim hook can share the same cache and derive its stripped
@@ -3323,7 +3130,6 @@ export function createOmniRouteProviderHook(
autoComboCount: rawAutoCombos.length,
enrichment: rawEnrichment,
autoCombos: rawAutoCombos,
features: resolved.features,
});
}
}
@@ -3365,8 +3171,6 @@ export function createOmniRouteProviderHook(
if (!entry.id) continue;
if (canonicalDedup.has(entry.id)) continue;
if (usable && !isUsableRawModelId(entry.id, usable, rawEnrichment)) continue;
// #9473: allowlist/blocklist filter (AND with usableOnly).
if (!passesModelAllowlist(entry.id, visibleFilter, hiddenFilter)) continue;
const model = mapRawModelToModelV2(entry, {
// #6859: server-facing id — NOT the OC-gate-prefixed `resolved.providerId`.
providerId: resolved.omnirouteProviderId,
@@ -3442,8 +3246,6 @@ export function createOmniRouteProviderHook(
if (!combo.id) return false;
if (combo.isHidden === true) return false;
if (usable && !isUsableCombo(combo, usable)) return false;
// #9473: combo allowlist — drop when no member matches visible filter.
if (visibleFilter && !passesComboAllowlist(combo, visibleFilter)) return false;
return true;
});
// Resolved nested combos keyed by their friendly name, so parent
@@ -4261,9 +4063,6 @@ export function buildStaticProviderEntry(
wantUsableOnly && connections && connections.length > 0
? usableProviderAliasSet(connections, enrichment)
: undefined;
// #9473: model allowlist/blocklist — compile once per static-block build.
const visibleFilter = compileModelListFilter(opts.features?.visibleModels);
const hiddenFilter = compileModelListFilter(opts.features?.hiddenModels);
// Provider-tag suffix — default-on, opt-out via `features.providerTag: false`.
// Prepends e.g. `Claude - ` to enriched raw-model names so the picker
// can tell `cc/claude-opus-4-7` (Anthropic) apart from `kr/claude-opus-4-7`
@@ -4301,8 +4100,6 @@ export function buildStaticProviderEntry(
// Skip canonical-named twins when the alias-keyed enriched row exists.
if (canonicalDedup.has(raw.id)) continue;
if (usable && !isUsableRawModelId(raw.id, usable, enrichment)) continue;
// #9473: allowlist/blocklist filter (AND with usableOnly).
if (!passesModelAllowlist(raw.id, visibleFilter, hiddenFilter)) continue;
const caps = raw.capabilities ?? {};
// Enrichment overlay: `/api/pricing/models` carries human display names
// (e.g. "Claude Opus 4.7" for raw id "cc/claude-opus-4-7"). The OC TUI
@@ -4408,7 +4205,7 @@ export function buildStaticProviderEntry(
// has no corresponding provider block. So bare keys (no `/`) MUST be
// prefixed with the resolved providerId. Already-prefixed keys
// (e.g. `cc/claude-opus-4-7`) are left as-is to avoid double-prefixing.
models[raw.id] = entry;
models[raw.id.includes("/") ? raw.id : `${opts.providerId}/${raw.id}`] = entry;
}
// Combo entries → stripped LCD shape. Each combo is keyed as
@@ -4455,8 +4252,6 @@ export function buildStaticProviderEntry(
if (!combo.id) return false;
if (combo.isHidden === true) return false;
if (usable && !isUsableCombo(combo, usable)) return false;
// #9473: combo allowlist — drop when no member matches visible filter.
if (visibleFilter && !passesComboAllowlist(combo, visibleFilter)) return false;
return true;
});
@@ -4605,8 +4400,7 @@ export function buildStaticProviderEntry(
// (`opencode-omniroute/opencode-omniroute/<slug>`), and `parseModel()`
// resolves credentials for the nonexistent provider `opencode-omniroute`
// instead of `omniroute`. See #7976.
models[buildComboKey(combo, usedComboKeys, opts.omnirouteProviderId).split("/").pop()!] =
entry;
models[buildComboKey(combo, usedComboKeys, opts.omnirouteProviderId)] = entry;
// Make this combo's resolved entry available to parent combos
// that reference it via combo-ref. Use the friendly name since
@@ -5414,7 +5208,6 @@ export function createOmniRouteConfigHook(
autoComboCount: rawAutoCombos.length,
enrichment: rawEnrichment,
autoCombos: rawAutoCombos,
features: resolved.features,
});
}

View File

@@ -1,95 +0,0 @@
/**
* #7624 — explicit feature-flag defaults.
*
* The `features` block in opencode.json marks every toggle `.optional()` with
* no default. The effective value was previously only knowable by tracing the
* implicit `features.X !== false` (default-ON) / `features.X === true`
* (default-OFF) convention scattered across each read site, which left
* operators unsure whether combos / autoCombos / enrichment were enabled when
* they omitted the block.
*
* `OMNIROUTE_FEATURE_DEFAULTS` declares those defaults explicitly and
* `resolveEffectiveFeatureFlags(features)` derives the effective state for any
* (possibly-undefined) features object, mirroring the read-site conventions
* exactly. These are purely derived — runtime routing behaviour is unchanged.
*/
import test from "node:test";
import assert from "node:assert/strict";
import {
OMNIROUTE_FEATURE_DEFAULTS,
resolveEffectiveFeatureFlags,
} from "../src/index.js";
const DEFAULT_ON = [
"combos",
"autoCombos",
"enrichment",
"diskCache",
"providerTag",
"fetchInterceptor",
"geminiSanitization",
] as const;
const DEFAULT_OFF = [
"compressionMetadata",
"usableOnly",
"mcpAutoEmit",
"debugLog",
"startupDebug",
] as const;
test("OMNIROUTE_FEATURE_DEFAULTS: declares each flag with its documented default", () => {
for (const key of DEFAULT_ON) {
assert.equal(OMNIROUTE_FEATURE_DEFAULTS[key], true, `${key} defaults ON`);
}
for (const key of DEFAULT_OFF) {
assert.equal(OMNIROUTE_FEATURE_DEFAULTS[key], false, `${key} defaults OFF`);
}
});
test("resolveEffectiveFeatureFlags: undefined features → full declared default set", () => {
const flags = resolveEffectiveFeatureFlags(undefined);
for (const key of DEFAULT_ON) {
assert.equal(flags[key], true, `${key} effective ON when features omitted`);
}
for (const key of DEFAULT_OFF) {
assert.equal(flags[key], false, `${key} effective OFF when features omitted`);
}
});
test("resolveEffectiveFeatureFlags: empty features object → same as omitted", () => {
assert.deepEqual(
resolveEffectiveFeatureFlags({}),
resolveEffectiveFeatureFlags(undefined)
);
});
test("resolveEffectiveFeatureFlags: explicit false disables a default-ON flag", () => {
const flags = resolveEffectiveFeatureFlags({ autoCombos: false });
assert.equal(flags.autoCombos, false, "explicit autoCombos:false honoured");
// Untouched flags keep their declared defaults.
assert.equal(flags.combos, true);
assert.equal(flags.enrichment, true);
});
test("resolveEffectiveFeatureFlags: explicit true enables a default-OFF flag", () => {
const flags = resolveEffectiveFeatureFlags({ compressionMetadata: true });
assert.equal(flags.compressionMetadata, true, "explicit compressionMetadata:true honoured");
assert.equal(flags.usableOnly, false, "other opt-in flags stay OFF");
});
test("resolveEffectiveFeatureFlags: non-boolean sibling keys do not leak into flags", () => {
// features may also carry mcpToken/logLevel/apiFormat — the resolver must
// only ever return the boolean toggle keys.
const flags = resolveEffectiveFeatureFlags({
mcpAutoEmit: true,
mcpToken: "sk-mcp-token-abc",
logLevel: "debug",
});
assert.equal(flags.mcpAutoEmit, true);
assert.equal(Object.keys(flags).length, Object.keys(OMNIROUTE_FEATURE_DEFAULTS).length);
assert.equal("mcpToken" in flags, false);
assert.equal("logLevel" in flags, false);
});

View File

@@ -1,317 +0,0 @@
/**
* #9473 — Model allowlist/blocklist for the opencode-plugin.
*
* Tests for the pure filter helpers (`compileModelListFilter`,
* `passesModelAllowlist`, `passesComboAllowlist`) and the schema + hook-level
* integration. The allowlist/blocklist composes with `usableOnly` (all filters
* AND together), blocklist wins over allowlist (deny takes precedence), and
* bare-suffix entries (e.g. "claude-opus-4-7") match any "{prefix}/claude-opus-4-7".
*/
import test from "node:test";
import assert from "node:assert/strict";
import {
compileModelListFilter,
passesModelAllowlist,
passesComboAllowlist,
parseOmniRoutePluginOptions,
buildStaticProviderEntry,
resolveOmniRoutePluginOptions,
type OmniRouteRawCombo,
type OmniRouteRawModelEntry,
} from "../src/index.js";
// ─────────────────────────────────────────────────────────────────────────
// compileModelListFilter
// ─────────────────────────────────────────────────────────────────────────
test("compileModelListFilter: undefined list → undefined", () => {
assert.equal(compileModelListFilter(undefined), undefined);
});
test("compileModelListFilter: empty array → undefined", () => {
assert.equal(compileModelListFilter([]), undefined);
});
test("compileModelListFilter: raw IDs with slash → exact set populated", () => {
const f = compileModelListFilter(["cc/claude-opus-4-7", "glm/gpt-5"]);
assert.ok(f);
assert.equal(f.exact.has("cc/claude-opus-4-7"), true);
assert.equal(f.exact.has("glm/gpt-5"), true);
assert.equal(f.suffixes.size, 0);
});
test("compileModelListFilter: bare IDs (no slash) → suffixes set populated", () => {
const f = compileModelListFilter(["claude-opus-4-7", "gpt-5"]);
assert.ok(f);
assert.equal(f.suffixes.has("claude-opus-4-7"), true);
assert.equal(f.suffixes.has("gpt-5"), true);
assert.equal(f.exact.size, 0);
});
test("compileModelListFilter: mixed raw + bare → both sets populated", () => {
const f = compileModelListFilter(["cc/claude-opus-4-7", "gpt-5"]);
assert.ok(f);
assert.equal(f.exact.has("cc/claude-opus-4-7"), true);
assert.equal(f.suffixes.has("gpt-5"), true);
});
// ─────────────────────────────────────────────────────────────────────────
// passesModelAllowlist
// ─────────────────────────────────────────────────────────────────────────
test("passesModelAllowlist: no visible, no hidden → keep (passthrough)", () => {
assert.equal(passesModelAllowlist("cc/claude-opus-4-7", undefined, undefined), true);
});
test("passesModelAllowlist: visible undefined, hidden undefined → keep", () => {
assert.equal(passesModelAllowlist("cc/claude-opus-4-7", undefined, undefined), true);
});
test("passesModelAllowlist: visible set, id matches exact → keep", () => {
const vis = compileModelListFilter(["cc/claude-opus-4-7"]);
assert.equal(passesModelAllowlist("cc/claude-opus-4-7", vis, undefined), true);
});
test("passesModelAllowlist: visible set, id matches suffix → keep", () => {
const vis = compileModelListFilter(["claude-opus-4-7"]);
assert.equal(passesModelAllowlist("cc/claude-opus-4-7", vis, undefined), true);
});
test("passesModelAllowlist: visible set, id does NOT match → drop", () => {
const vis = compileModelListFilter(["cc/claude-opus-4-7"]);
assert.equal(passesModelAllowlist("glm/gpt-5", vis, undefined), false);
});
test("passesModelAllowlist: visible set, bare suffix matches different prefix → keep", () => {
const vis = compileModelListFilter(["claude-opus-4-7"]);
assert.equal(passesModelAllowlist("kr/claude-opus-4-7", vis, undefined), true);
});
test("passesModelAllowlist: hidden set, id matches exact → drop", () => {
const hid = compileModelListFilter(["cc/claude-opus-4-7"]);
assert.equal(passesModelAllowlist("cc/claude-opus-4-7", undefined, hid), false);
});
test("passesModelAllowlist: hidden set, id matches suffix → drop", () => {
const hid = compileModelListFilter(["claude-opus-4-7"]);
assert.equal(passesModelAllowlist("cc/claude-opus-4-7", undefined, hid), false);
});
test("passesModelAllowlist: hidden set, id does NOT match → keep", () => {
const hid = compileModelListFilter(["cc/claude-opus-4-7"]);
assert.equal(passesModelAllowlist("glm/gpt-5", undefined, hid), true);
});
test("passesModelAllowlist: id in BOTH visible and hidden → DROP (deny wins)", () => {
const vis = compileModelListFilter(["cc/claude-opus-4-7"]);
const hid = compileModelListFilter(["cc/claude-opus-4-7"]);
assert.equal(passesModelAllowlist("cc/claude-opus-4-7", vis, hid), false);
});
test("passesModelAllowlist: visible allows, hidden blocks different id → keep the visible one", () => {
const vis = compileModelListFilter(["cc/claude-opus-4-7"]);
const hid = compileModelListFilter(["glm/gpt-5"]);
assert.equal(passesModelAllowlist("cc/claude-opus-4-7", vis, hid), true);
assert.equal(passesModelAllowlist("glm/gpt-5", vis, hid), false);
});
test("passesModelAllowlist: bare-suffix hidden blocks exact match too", () => {
const hid = compileModelListFilter(["claude-opus-4-7"]);
assert.equal(passesModelAllowlist("cc/claude-opus-4-7", undefined, hid), false);
assert.equal(passesModelAllowlist("kr/claude-opus-4-7", undefined, hid), false);
});
test("passesModelAllowlist: no-slash id, visible set has bare match → keep", () => {
const vis = compileModelListFilter(["claude-primary"]);
assert.equal(passesModelAllowlist("claude-primary", vis, undefined), true);
});
test("passesModelAllowlist: no-slash id, visible set has no match → drop", () => {
const vis = compileModelListFilter(["cc/claude-opus-4-7"]);
assert.equal(passesModelAllowlist("claude-primary", vis, undefined), false);
});
// ─────────────────────────────────────────────────────────────────────────
// passesComboAllowlist
// ─────────────────────────────────────────────────────────────────────────
function combo(models: OmniRouteRawCombo["models"]): OmniRouteRawCombo {
return { id: "c1", name: "Test Combo", models };
}
test("passesComboAllowlist: visible undefined → keep", () => {
const c = combo([{ kind: "model", model: "cc/claude-opus-4-7" }]);
assert.equal(passesComboAllowlist(c, undefined), true);
});
test("passesComboAllowlist: ≥1 member matches visible → keep", () => {
const vis = compileModelListFilter(["cc/claude-opus-4-7"]);
const c = combo([
{ kind: "model", model: "dead/legacy" },
{ kind: "model", model: "cc/claude-opus-4-7" },
]);
assert.equal(passesComboAllowlist(c, vis), true);
});
test("passesComboAllowlist: zero members match visible → drop", () => {
const vis = compileModelListFilter(["cc/claude-opus-4-7"]);
const c = combo([
{ kind: "model", model: "glm/gpt-5" },
{ kind: "model", model: "kr/claude-opus-4-7" },
]);
assert.equal(passesComboAllowlist(c, vis), false);
});
test("passesComboAllowlist: bare suffix matches any prefix → keep", () => {
const vis = compileModelListFilter(["claude-opus-4-7"]);
const c = combo([{ kind: "model", model: "kr/claude-opus-4-7" }]);
assert.equal(passesComboAllowlist(c, vis), true);
});
test("passesComboAllowlist: zero members → keep", () => {
const vis = compileModelListFilter(["cc/claude-opus-4-7"]);
assert.equal(passesComboAllowlist(combo([]), vis), true);
assert.equal(passesComboAllowlist(combo(undefined), vis), true);
});
test("passesComboAllowlist: only combo-ref steps → keep", () => {
const vis = compileModelListFilter(["cc/claude-opus-4-7"]);
const c = combo([{ kind: "combo-ref", comboName: "nested" }]);
assert.equal(passesComboAllowlist(c, vis), true);
});
// ─────────────────────────────────────────────────────────────────────────
// Schema — visibleModels / hiddenModels
// ─────────────────────────────────────────────────────────────────────────
test("parseOmniRoutePluginOptions: visibleModels string[] → preserved", () => {
const r = parseOmniRoutePluginOptions({
features: { visibleModels: ["cc/claude-opus-4-7", "gpt-5"] },
});
assert.deepEqual(r.features?.visibleModels, ["cc/claude-opus-4-7", "gpt-5"]);
});
test("parseOmniRoutePluginOptions: hiddenModels string[] → preserved", () => {
const r = parseOmniRoutePluginOptions({
features: { hiddenModels: ["glm/gpt-5"] },
});
assert.deepEqual(r.features?.hiddenModels, ["glm/gpt-5"]);
});
test("parseOmniRoutePluginOptions: both lists together → preserved", () => {
const r = parseOmniRoutePluginOptions({
features: {
visibleModels: ["cc/claude-opus-4-7"],
hiddenModels: ["glm/gpt-5"],
},
});
assert.deepEqual(r.features?.visibleModels, ["cc/claude-opus-4-7"]);
assert.deepEqual(r.features?.hiddenModels, ["glm/gpt-5"]);
});
test("parseOmniRoutePluginOptions: empty string in visibleModels → rejects", () => {
assert.throws(
() =>
parseOmniRoutePluginOptions({
features: { visibleModels: [""] },
}),
/Invalid @omniroute\/opencode-plugin options/
);
});
test("parseOmniRoutePluginOptions: empty string in hiddenModels → rejects", () => {
assert.throws(
() =>
parseOmniRoutePluginOptions({
features: { hiddenModels: [""] },
}),
/Invalid @omniroute\/opencode-plugin options/
);
});
test("parseOmniRoutePluginOptions: unknown features key still rejects (strict invariant)", () => {
assert.throws(
() =>
parseOmniRoutePluginOptions({
features: { visibleModels: ["x"], unknownKey: true },
}),
/Invalid @omniroute\/opencode-plugin options/
);
});
// ─────────────────────────────────────────────────────────────────────────
// buildStaticProviderEntry — allowlist/blocklist integration
// ─────────────────────────────────────────────────────────────────────────
const FAKE_RAW_MODELS: OmniRouteRawModelEntry[] = [
{ id: "cc/claude-opus-4-7", owned_by: "anthropic" },
{ id: "glm/gpt-5", owned_by: "openai" },
{ id: "kr/claude-opus-4-7", owned_by: "anthropic" },
{ id: "claude-primary", owned_by: "combo" },
];
test("buildStaticProviderEntry: no allowlist → all models emitted", () => {
const opts = resolveOmniRoutePluginOptions({ features: {} });
const entry = buildStaticProviderEntry(FAKE_RAW_MODELS, [], opts, "http://localhost:20128/v1", "sk-test");
const ids = Object.keys(entry.models);
assert.ok(ids.includes("cc/claude-opus-4-7"), "cc/claude-opus-4-7 should be present");
assert.ok(ids.includes("glm/gpt-5"), "glm/gpt-5 should be present");
assert.ok(ids.includes("kr/claude-opus-4-7"), "kr/claude-opus-4-7 should be present");
});
test("buildStaticProviderEntry: visibleModels filters to only listed IDs", () => {
const opts = resolveOmniRoutePluginOptions({
features: { visibleModels: ["cc/claude-opus-4-7"] },
});
const entry = buildStaticProviderEntry(FAKE_RAW_MODELS, [], opts, "http://localhost:20128/v1", "sk-test");
const ids = Object.keys(entry.models);
assert.ok(ids.includes("cc/claude-opus-4-7"), "cc/claude-opus-4-7 should be present");
assert.equal(ids.includes("glm/gpt-5"), false, "glm/gpt-5 should be filtered out");
assert.equal(ids.includes("kr/claude-opus-4-7"), false, "kr/claude-opus-4-7 should be filtered out");
});
test("buildStaticProviderEntry: hiddenModels drops listed IDs", () => {
const opts = resolveOmniRoutePluginOptions({
features: { hiddenModels: ["glm/gpt-5"] },
});
const entry = buildStaticProviderEntry(FAKE_RAW_MODELS, [], opts, "http://localhost:20128/v1", "sk-test");
const ids = Object.keys(entry.models);
assert.ok(ids.includes("cc/claude-opus-4-7"), "cc/claude-opus-4-7 should be present");
assert.equal(ids.includes("glm/gpt-5"), false, "glm/gpt-5 should be hidden");
assert.ok(ids.includes("kr/claude-opus-4-7"), "kr/claude-opus-4-7 should be present");
});
test("buildStaticProviderEntry: bare-suffix visibleModels matches any prefix", () => {
const opts = resolveOmniRoutePluginOptions({
features: { visibleModels: ["claude-opus-4-7"] },
});
const entry = buildStaticProviderEntry(FAKE_RAW_MODELS, [], opts, "http://localhost:20128/v1", "sk-test");
const ids = Object.keys(entry.models);
assert.ok(ids.includes("cc/claude-opus-4-7"), "cc/claude-opus-4-7 should match via suffix");
assert.ok(ids.includes("kr/claude-opus-4-7"), "kr/claude-opus-4-7 should match via suffix");
assert.equal(ids.includes("glm/gpt-5"), false, "glm/gpt-5 should be filtered out");
});
test("buildStaticProviderEntry: id in both visible and hidden → hidden wins", () => {
const opts = resolveOmniRoutePluginOptions({
features: {
visibleModels: ["cc/claude-opus-4-7"],
hiddenModels: ["cc/claude-opus-4-7"],
},
});
const entry = buildStaticProviderEntry(FAKE_RAW_MODELS, [], opts, "http://localhost:20128/v1", "sk-test");
const ids = Object.keys(entry.models);
assert.equal(ids.includes("cc/claude-opus-4-7"), false, "deny takes precedence");
});
test("buildStaticProviderEntry: empty visibleModels → no filter (passthrough)", () => {
const opts = resolveOmniRoutePluginOptions({
features: { visibleModels: [] },
});
const entry = buildStaticProviderEntry(FAKE_RAW_MODELS, [], opts, "http://localhost:20128/v1", "sk-test");
const ids = Object.keys(entry.models);
assert.ok(ids.includes("cc/claude-opus-4-7"), "empty visibleModels should not filter");
assert.ok(ids.includes("glm/gpt-5"), "empty visibleModels should not filter");
});

1169
AGENTS.md

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

577
CLAUDE.md
View File

@@ -1,42 +1,406 @@
# CLAUDE.md
@AGENTS.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
**All project rules live in [`AGENTS.md`](AGENTS.md)** — the single source of truth for every AI
assistant (architecture, conventions, testing, quality gates, git workflow, the 22 Hard Rules,
PII learnings). Read it in full; do not re-add project rules here. Everything below applies ONLY
to Claude Code — operational refinements of rules already defined in `AGENTS.md`.
## Quick Start
## Worktree isolation — Claude Code specifics
```bash
npm install # Install deps (auto-generates .env from .env.example)
npm run dev # Dev server at http://localhost:20128
npm run build # Production build (Next.js 16 standalone)
npm run lint # ESLint (0 errors expected; warnings are pre-existing)
npm run typecheck:core # TypeScript check (should be clean)
npm run typecheck:noimplicit:core # Strict check (no implicit any)
npm run test:coverage # Unit tests + coverage gate (60/60/60/60 — statements/lines/functions/branches)
npm run check # lint + test combined
npm run check:cycles # Detect circular dependencies
```
The full mandatory worktree protocol (base-branch confirmation, `.claude/worktrees/` canonical
path, `cp -al` node_modules, teardown rules) is in `AGENTS.md` → Git Workflow → "Worktree
isolation". Claude-Code-specific points:
### Running Tests
- Confirm the base branch with the operator via `AskUserQuestion` (Hard Rule #19) unless they
already told you.
- Prefer the native `EnterWorktree` tool — it already creates worktrees under
`.claude/worktrees/` (the canonical path). Create the worktree with the documented `git
worktree add` command, then call `EnterWorktree` with its `path`.
```bash
# Single test file (Node.js native test runner — most tests)
node --import tsx/esm --test tests/unit/your-file.test.ts
## Cross-session safety — Claude Code specifics
# Vitest (MCP server, autoCombo, cache)
npm run test:vitest
Hard Rules #19/#21/#22 (in `AGENTS.md`) govern parallel sessions. Operational reminders for this
harness:
# All suites
npm run test:all
```
- **Replicate the `git stash` ban verbatim in the prompt of every subagent that touches git**
(Agent tool / Workflow scripts) — subagents do not inherit this file, and the recorded
recurrence of the stash incident came through a subagent.
- Before merging or pushing to any PR you did not create _this session_, run `git worktree list`
and re-check `gh pr view <N> --json state,headRefOid` (Hard Rule #22b).
- End every session with the main checkout on the branch it started on.
For full test matrix, see `CONTRIBUTING.md` → "Running Tests". For deep architecture, see `AGENTS.md`.
## Superpowers / planning artifacts — path overrides
---
The `_tasks/` convention is defined in `AGENTS.md` → "Planning & Research Artifacts". The
superpowers skills ship with defaults that point at `docs/…` — those defaults are **overridden
here**. When a superpowers skill announces a path like "saved to `docs/superpowers/plans/…`",
rewrite it to the `_tasks/…` equivalent before writing:
## Project at a Glance
**OmniRoute** — unified AI proxy/router. One endpoint, 290 LLM providers, auto-fallback.
| Layer | Location | Purpose |
| ------------- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| API Routes | `src/app/api/v1/` | Next.js App Router — entry points |
| Handlers | `open-sse/handlers/` | Request processing (chat, embeddings, etc) |
| Executors | `open-sse/executors/` | Provider-specific HTTP dispatch |
| Translators | `open-sse/translator/` | Format conversion (OpenAI↔Claude↔Gemini) |
| Transformer | `open-sse/transformer/` | Responses API ↔ Chat Completions |
| Services | `open-sse/services/` | Combo routing, rate limits, caching, etc |
| Database | `src/lib/db/` | SQLite domain modules (95 files, 110 migrations) |
| Domain/Policy | `src/domain/` | Policy engine, cost rules, fallback logic |
| MCP Server | `open-sse/mcp-server/` | 104 tools (42 base + memory/skill/agentSkill/pool/notion/obsidian/gamification/plugin modules), 3 transports (stdio / SSE / Streamable HTTP), 31 scopes |
| A2A Server | `src/lib/a2a/` | JSON-RPC 2.0 agent protocol |
| Skills | `src/lib/skills/` | Extensible skill framework |
| Memory | `src/lib/memory/` | Persistent conversational memory |
Monorepo: `src/` (Next.js 16 app), `open-sse/` (streaming engine workspace), `electron/` (desktop app), `tests/`, `bin/` (CLI entry point).
---
## Request Pipeline
```
Client → /v1/chat/completions (Next.js route)
→ CORS → Zod validation → auth? → policy check → prompt injection guard
→ handleChatCore() [open-sse/handlers/chatCore.ts]
→ cache check → rate limit → combo routing?
→ resolveComboTargets() → handleSingleModel() per target
→ translateRequest() → getExecutor() → executor.execute()
→ fetch() upstream → retry w/ backoff
→ response translation → SSE stream or JSON
→ If Responses API: responsesTransformer.ts TransformStream
```
API routes follow a consistent pattern: `Route → CORS preflight → Zod body validation → Optional auth (extractApiKey/isValidApiKey) → API key policy enforcement → Handler delegation (open-sse)`. No global Next.js middleware — interception is route-specific.
**Combo routing** (`open-sse/services/combo.ts`): 18 strategies (priority, weighted, fill-first, round-robin, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline). Each target calls `handleSingleModel()` which wraps `handleChatCore()` with per-target error handling and circuit breaker checks. The `fusion` strategy is the exception: it fans out to a panel of models in parallel, then a judge model synthesizes one final answer (`open-sse/services/fusion.ts`). See `docs/routing/AUTO-COMBO.md` for the 12-factor Auto-Combo scoring + the full strategy table and `docs/architecture/RESILIENCE_GUIDE.md` for the 3 resilience layers.
---
## Resilience Runtime State
OmniRoute has three related but distinct temporary-failure mechanisms. Keep their
scope separate when debugging routing behavior. See the
[3-layer resilience diagram](./docs/diagrams/exported/resilience-3layers.svg)
(source: [docs/diagrams/resilience-3layers.mmd](./docs/diagrams/resilience-3layers.mmd))
for an at-a-glance map.
### Provider Circuit Breaker
**Scope**: whole provider, e.g. `glm`, `openai`, `anthropic`.
**Purpose**: stop sending traffic to a provider that is repeatedly failing at the
upstream/service level, so one unhealthy provider does not slow down every request.
**Implementation**:
- Core class: `src/shared/utils/circuitBreaker.ts`
- Chat gate/execution wiring: `src/sse/handlers/chatHelpers.ts`, `src/sse/handlers/chat.ts`
- Runtime status API: `src/app/api/monitoring/health/route.ts`
- Shared wrappers: `open-sse/services/accountFallback.ts`
- Persisted state table: `domain_circuit_breakers`
**States**:
- `CLOSED`: normal traffic is allowed.
- `OPEN`: provider is temporarily blocked; callers get a provider-circuit-open response
or combo routing skips to another target.
- `HALF_OPEN`: reset timeout has elapsed; allow a probe request. Success closes the
breaker, failure opens it again.
**Defaults** (`open-sse/config/constants.ts`):
- OAuth providers: threshold `3`, reset timeout `60s`.
- API-key providers: threshold `5`, reset timeout `30s`.
- Local providers: threshold `2`, reset timeout `15s`.
Only provider-level failure statuses should trip the provider breaker:
```ts
(408, 500, 502, 503, 504);
```
Do not trip the whole-provider breaker for normal account/key/model errors like most
`401`, `403`, or `429` cases. Those usually belong to connection cooldown or model
lockout. A generic API-key provider `403` should be recoverable unless it is classified
as a terminal provider/account error.
The breaker uses lazy recovery, not a background timer. When `OPEN` expires, reads such
as `getStatus()`, `canExecute()`, and `getRetryAfterMs()` refresh the state to
`HALF_OPEN`, so dashboards and combo candidate builders do not keep excluding an
expired provider forever.
### Connection Cooldown
**Scope**: one provider connection/account/key.
**Purpose**: temporarily skip one bad key/account while allowing other connections for
the same provider to continue serving requests.
**Implementation**:
- Write/update path: `src/sse/services/auth.ts::markAccountUnavailable()`
- Account selection/filtering: `src/sse/services/auth.ts::getProviderCredentials...`
- Cooldown calculation: `open-sse/services/accountFallback.ts::checkFallbackError()`
- Settings: `src/lib/resilience/settings.ts`
Important fields on provider connections:
```ts
rateLimitedUntil;
testStatus: "unavailable";
lastError;
lastErrorType;
errorCode;
backoffLevel;
```
During account selection, a connection is skipped while:
```ts
new Date(rateLimitedUntil).getTime() > Date.now();
```
Cooldowns are also lazy: when `rateLimitedUntil` is in the past, the connection becomes
eligible again. On successful use, `clearAccountError()` clears `testStatus`,
`rateLimitedUntil`, error fields, and `backoffLevel`.
Default connection cooldown behavior:
- OAuth base cooldown: `5s`.
- API-key base cooldown: `3s`.
- API-key `429` should prefer upstream retry hints (`Retry-After`, reset headers, or
parseable reset text) when available.
- Repeated recoverable failures use exponential backoff:
```ts
baseCooldownMs * 2 ** failureIndex;
```
The anti-thundering-herd guard prevents concurrent failures on the same connection from
repeatedly extending the cooldown or double-incrementing `backoffLevel`.
Terminal states are not cooldowns. `banned`, `expired`, and `credits_exhausted` are
intended to stay unavailable until credentials/settings change or an operator resets
them. Do not overwrite terminal states with transient cooldown state.
### Model Lockout
**Scope**: provider + connection + model.
**Purpose**: avoid disabling a whole connection when only one model is unavailable or
quota-limited for that connection.
Examples:
- Per-model quota providers returning `429`.
- Local providers returning `404` for one missing model.
- Provider-specific mode/model permission failures such as selected Grok modes.
Model lockout lives in `open-sse/services/accountFallback.ts` and lets the same
connection continue serving other models.
### Debugging Guidance
- If all keys for a provider are skipped, inspect both provider breaker state and each
connection's `rateLimitedUntil`/`testStatus`.
- If a provider appears permanently excluded after the reset window, check whether code
is reading raw `state` instead of using `getStatus()`/`canExecute()`.
- If one provider key fails but others should work, prefer connection cooldown over
provider breaker.
- If only one model fails, prefer model lockout over connection cooldown.
- If a state should self-recover, it should have a future timestamp/reset timeout and a
read path that refreshes expired state. Permanent statuses require manual credential
or config changes.
---
## Key Conventions
### Code Style
- **2 spaces**, semicolons, double quotes, 100 char width, es5 trailing commas (enforced by lint-staged via Prettier)
- **Imports**: external → internal (`@/`, `@omniroute/open-sse`) → relative
- **Naming**: files=camelCase/kebab, components=PascalCase, constants=UPPER_SNAKE
- **ESLint**: `no-eval`, `no-implied-eval`, `no-new-func` = error everywhere; `no-explicit-any` = **error** in `open-sse/` and `tests/` (since #6218 — pre-existing violations are frozen in `config/quality/eslint-suppressions.json`, new ones must be fixed; `npm run lint` applies the suppressions and is what CI runs)
- **TypeScript**: `strict: false`, target ES2022, module esnext, resolution bundler. Prefer explicit types.
### Database
- **Always** go through `src/lib/db/` domain modules — **never** write raw SQL in routes or handlers
- **Never** add logic to `src/lib/localDb.ts` (re-export layer only)
- **Never** barrel-import from `localDb.ts` — import specific `db/` modules instead
- DB singleton: `getDbInstance()` from `src/lib/db/core.ts` (WAL journaling)
- Migrations: `src/lib/db/migrations/` — versioned SQL files, idempotent, run in transactions
### Error Handling
- try/catch with specific error types, log with pino context
- Never swallow errors in SSE streams — use abort signals for cleanup
- Return proper HTTP status codes (4xx/5xx)
### Security
- **Never** use `eval()`, `new Function()`, or implied eval
- Validate all inputs with Zod schemas
- Encrypt credentials at rest (AES-256-GCM)
- Upstream header denylist: `src/shared/constants/upstreamHeaders.ts` — keep sanitize, Zod schemas, and unit tests aligned when editing
- **Public upstream credentials** (Gemini/Antigravity/Windsurf-style OAuth client_id/secret + Firebase Web keys extracted from public CLIs): **MUST** be embedded via `resolvePublicCred()` from `open-sse/utils/publicCreds.ts`**never** as string literals. See `docs/security/PUBLIC_CREDS.md` for the mandatory pattern.
- **Error responses** (HTTP / SSE / executor / MCP handler): **MUST** route through `buildErrorBody()` or `sanitizeErrorMessage()` from `open-sse/utils/error.ts`**never** put raw `err.stack` or `err.message` in a response body. See `docs/security/ERROR_SANITIZATION.md`.
- **Shell commands built from variables**: when calling `exec()`/`spawn()` with a script that needs runtime values, pass them via the `env` option (shell-escaped automatically) — **never** string-interpolate untrusted/external paths into the script body. Reference: `src/mitm/cert/install.ts::updateNssDatabases`.
- **Secure-by-default libraries** ([tldrsec/awesome-secure-defaults](https://github.com/tldrsec/awesome-secure-defaults)): prefer Helmet.js, DOMPurify, ssrf-req-filter, safe-regex, Google Tink over custom implementations whenever adding new security-sensitive surfaces.
---
## Common Modification Scenarios
### Adding a New Provider
1. Register in `src/shared/constants/providers.ts` (Zod-validated at load)
2. Add executor in `open-sse/executors/` if custom logic needed (extend `BaseExecutor`)
3. Add translator in `open-sse/translator/` if non-OpenAI format
4. Add OAuth config in `src/lib/oauth/constants/oauth.ts` if OAuth-based — if the upstream CLI ships a public client_id/secret, embed via `resolvePublicCred()` (see `docs/security/PUBLIC_CREDS.md`), **never** as a literal
5. Register models in `open-sse/config/providerRegistry.ts`
6. Write tests in `tests/unit/` (include the publicCreds shape assertion if you added a new embedded default)
### Adding a New API Route
1. Create directory under `src/app/api/v1/your-route/`
2. Create `route.ts` with `GET`/`POST` handlers
3. Follow pattern: CORS → Zod body validation → optional auth → handler delegation
4. Handler goes in `open-sse/handlers/` (import from there, not inline)
5. Error responses use `buildErrorBody()` / `errorResponse()` from `open-sse/utils/error.ts` (auto-sanitized — never put `err.stack` or `err.message` raw in the body). See `docs/security/ERROR_SANITIZATION.md`.
6. Add tests — including at least one assertion that error responses do not leak stack traces (`!body.error.message.includes("at /")`)
### Adding a New DB Module
1. Create `src/lib/db/yourModule.ts` — import `getDbInstance` from `./core.ts`
2. Export CRUD functions for your domain table(s)
3. Add migration in `src/lib/db/migrations/` if new tables needed
4. Re-export from `src/lib/localDb.ts` (add to the re-export list only)
5. Write tests
### Adding a New MCP Tool
1. Add tool definition in `open-sse/mcp-server/tools/` with Zod input schema + async handler
2. Register in tool set (wired by `createMcpServer()`)
3. Assign to appropriate scope(s)
4. Write tests (tool invocation logged to `mcp_audit` table)
### Adding a New A2A Skill
1. Create skill in `src/lib/a2a/skills/` (5 already exist: smart-routing, quota-management, provider-discovery, cost-analysis, health-report)
2. Skill receives task context (messages, metadata) → returns structured result
3. Register in `A2A_SKILL_HANDLERS` in `src/lib/a2a/taskExecution.ts`
4. Expose in `src/app/.well-known/agent.json/route.ts` (Agent Card)
5. Write tests in `tests/unit/`
6. Document in `docs/frameworks/A2A-SERVER.md` skill table
### Adding a New Cloud Agent
1. Create agent class in `src/lib/cloudAgent/agents/` extending `CloudAgentBase` (3 already exist: codex-cloud, devin, jules)
2. Implement `createTask`, `getStatus`, `approvePlan`, `sendMessage`, `listSources`
3. Register in `src/lib/cloudAgent/registry.ts`
4. Add OAuth/credentials handling if needed (`src/lib/oauth/providers/`)
5. Tests + document in `docs/frameworks/CLOUD_AGENT.md`
### Adding a New Embedded Service
1. Create installer in `src/lib/services/installers/{name}.ts` modeled on `ninerouter.ts` (use `runNpm` from `installers/utils.ts` — no shell interpolation, hard rule #13).
2. Register the service in `src/lib/services/bootstrap.ts` (add to `SERVICES[]` array and extend `buildSpawnArgsFactory()`).
3. Add a DB seed row for the new service in `src/lib/db/migrations/` (`version_manager` table, `status='not_installed'`, `auto_start=0`).
4. Create 7 API endpoints under `src/app/api/services/{name}/` (`_lib.ts`, `install`, `start`, `stop`, `restart`, `update`, `status`, `auto-start`). All delegate errors through `createErrorResponse()`. The shared `logs` endpoint is already wired via `[name]/logs/route.ts`.
5. Verify `/api/services/` is in `LOCAL_ONLY_API_PREFIXES` in `src/server/authz/routeGuard.ts`; add a test asserting `isLocalOnlyPath()` returns `true` for the new prefix if you add one (hard rule #17).
6. Add a UI tab in `src/app/(dashboard)/dashboard/providers/services/tabs/` reusing `ServiceStatusCard`, `ServiceLifecycleButtons`, `ServiceLogsPanel`.
7. Document in `docs/frameworks/EMBEDDED-SERVICES.md` (update §1 service table + §4 API reference) and `docs/openapi.yaml`.
8. Write tests: unit (`tests/unit/services/`), integration (`tests/integration/services/`, gated by `RUN_SERVICES_INT=1`), and update `docs/ops/RELEASE_CHECKLIST.md` smoke section.
### Adding a New Guardrail / Eval / Skill / Webhook event
- Guardrail: `src/lib/guardrails/` → docs: `docs/security/GUARDRAILS.md`
- Eval suite: `src/lib/evals/` → docs: `docs/frameworks/EVALS.md`
- Skill (sandbox): `src/lib/skills/` → docs: `docs/frameworks/SKILLS.md`
- Webhook event: `src/lib/webhookDispatcher.ts` → docs: `docs/frameworks/WEBHOOKS.md`
---
## Reference Documentation
For any non-trivial change, read the matching deep-dive first:
| Area | Doc |
| --------------------------------------------- | ------------------------------------------------------- |
| Repo navigation | `docs/architecture/REPOSITORY_MAP.md` |
| Architecture | `docs/architecture/ARCHITECTURE.md` |
| Engineering reference | `docs/architecture/CODEBASE_DOCUMENTATION.md` |
| Auto-Combo (12-factor scoring, 18 strategies) | `docs/routing/AUTO-COMBO.md` |
| Resilience (3 mechanisms) | `docs/architecture/RESILIENCE_GUIDE.md` |
| Reasoning replay | `docs/routing/REASONING_REPLAY.md` |
| Skills framework | `docs/frameworks/SKILLS.md` |
| Memory system (FTS5 + Qdrant) | `docs/frameworks/MEMORY.md` |
| Cloud agents | `docs/frameworks/CLOUD_AGENT.md` |
| Guardrails (PII / injection / vision) | `docs/security/GUARDRAILS.md` |
| Public upstream credentials (Gemini/etc.) | `docs/security/PUBLIC_CREDS.md` |
| Error message sanitization | `docs/security/ERROR_SANITIZATION.md` |
| Evals | `docs/frameworks/EVALS.md` |
| Compliance / audit | `docs/security/COMPLIANCE.md` |
| Webhooks | `docs/frameworks/WEBHOOKS.md` |
| Authorization pipeline | `docs/architecture/AUTHZ_GUIDE.md` |
| Stealth (TLS / fingerprint) | `docs/security/STEALTH_GUIDE.md` |
| Agent protocols (A2A / ACP / Cloud) | `docs/frameworks/AGENT_PROTOCOLS_GUIDE.md` |
| MCP server | `docs/frameworks/MCP-SERVER.md` |
| A2A server | `docs/frameworks/A2A-SERVER.md` |
| API reference + OpenAPI | `docs/reference/API_REFERENCE.md` + `docs/openapi.yaml` |
| Provider catalog (auto-generated) | `docs/reference/PROVIDER_REFERENCE.md` |
| Release flow | `docs/ops/RELEASE_CHECKLIST.md` |
| Embedded services | `docs/frameworks/EMBEDDED-SERVICES.md` |
| Quality gates (~48 scripts, allowlist policy) | `docs/architecture/QUALITY_GATES.md` |
---
## Testing
| What | Command |
| ----------------------- | --------------------------------------------------------------------------- |
| Unit tests | `npm run test:unit` |
| Single file | `node --import tsx/esm --test tests/unit/file.test.ts` |
| Vitest (MCP, autoCombo) | `npm run test:vitest` |
| E2E (Playwright) | `npm run test:e2e` |
| Protocol E2E (MCP+A2A) | `npm run test:protocols:e2e` |
| Ecosystem | `npm run test:ecosystem` |
| Coverage gate | `npm run test:coverage` (60/60/60/60 — statements/lines/functions/branches) |
| Coverage report | `npm run coverage:report` |
**PR rule**: If you change production code in `src/`, `open-sse/`, `electron/`, or `bin/`, you must include or update tests in the same PR.
**Test layer preference**: unit first → integration (multi-module or DB state) → e2e (UI/workflow only). Encode bug reproductions as automated tests before or alongside the fix.
**Both test runners must pass**: `npm run test:unit` (Node native — most tests) AND `npm run test:vitest` (MCP server, autoCombo, cache) cover **non-overlapping files**. Both are wired in CI (jobs `test-unit` and `test-vitest`) and must be green before merging. A PR where only one suite passes may silently ship broken MCP tools or routing regressions.
**Bug fix / issue triage protocol (Hard Rule #18)**: Every fix for a reported issue must be validated by one of the following — no exceptions:
1. **TDD (preferred)** — write a failing test reproducing the bug → fix it → confirm the test passes. The test becomes the permanent regression guard. Touch only the files the test proves need changing; nothing more.
2. **Real-environment test (when TDD is not possible)** — deploy to the production VPS (`root@192.168.0.15`) and run a documented live test. Record the exact command + result in the PR description. Applies to: OAuth upstream flows, Cloudflare/WS upstream behavior, UI-only regressions, hardware-dependent behavior.
3. "It worked locally without a test" does not count. A fix without a test or a VPS validation record is not a fix — it is a guess.
Why this matters: fixing bug A while opening bug B is worse than not fixing at all. The TDD/VPS gate enforces surgical scope — you touch only what the failing test proves is broken. Examples where this paid off: #3090 (claude-web 403), #3113 (WS HTTP fallback), #3052 (heap-guard auto-calibration).
**Copilot coverage policy**: When a PR changes production code and coverage is below 60% (statements/lines/functions/branches), do not just report — add or update tests, rerun the coverage gate, then ask for confirmation. Include commands run, changed test files, and final coverage result in the PR report.
---
## Planning & Research Artifacts (superpowers, deep-research)
`_tasks/` is a **separate, isolated git repository** that is gitignored by the main
repo (`.gitignore``_tasks/`). It is the canonical home for working artifacts —
plans, specs/designs, research, hand-offs — so they stay **versioned in their own
repo** instead of polluting the main OmniRoute tree.
**Hard rule — never write superpowers / planning / research output under `docs/` or
the repo root.** The superpowers skills ship with defaults that point at `docs/…`
(`writing-plans``docs/superpowers/plans/`, `brainstorming``docs/superpowers/specs/`).
Those defaults are **overridden here**. Whenever you invoke superpowers (or any
plan/spec/research generator) in this project, save to `_tasks/` instead, using the
same filename convention:
| Artifact (skill) | Default (do NOT use) | Save here instead |
| ---------------------------------- | ------------------------- | ------------------------------------------------------------- |
@@ -45,11 +409,156 @@ rewrite it to the `_tasks/…` equivalent before writing:
| Research (`deep-research`, ad-hoc) | `docs/research/` | `_tasks/research/…` |
| Hand-offs (`/handoff`) | — | `_tasks/hands-off/<YYYY-MM-DD>_<branch>_v<versão>_sess-<id>/` |
Commit those artifacts inside the `_tasks/` repo (`git -C _tasks …`), never in the main repo.
When a superpowers skill announces a path like "saved to `docs/superpowers/plans/…`",
rewrite it to the `_tasks/…` equivalent before writing. Commit those artifacts inside
the `_tasks/` repo (`git -C _tasks …`), never in the main repo.
## Base-green before opening PRs
## Git Workflow
Before cutting a branch or opening a PR, run the base-green check (`AGENTS.md` → Git Workflow →
"Base-green check"; project skills reference it as `.agents/skills/_shared/base-green.md`). A PR
opened while the base tip is red must carry `⚠️ base-red inherited: #<issue>` in its body. To
drain an accumulated red state (base tip + red PRs), use the `/sweep-reds` skill.
```bash
# Never commit directly to main
git checkout -b feat/your-feature
git commit -m "feat: describe your change"
git push -u origin feat/your-feature
```
**Branch prefixes**: `feat/`, `fix/`, `refactor/`, `docs/`, `test/`, `chore/`
**Commit format** (Conventional Commits): `feat(db): add circuit breaker` — scopes: `db`, `sse`, `oauth`, `dashboard`, `api`, `cli`, `docker`, `ci`, `mcp`, `a2a`, `memory`, `skills`
**Husky hooks**:
- **pre-commit**: lint-staged + `check-docs-sync` + `check:any-budget:t11` + `check:tracked-artifacts`
- **pre-push**: intentionally light (PATH/npm sanity only). `any-budget` + `tracked-artifacts`
already run on pre-commit; re-running them on every push was pure double-pay. CI still
enforces both. (Was Fase 6A.12 full pre-push gate; folded into pre-commit in #6716.)
### Worktree isolation (MANDATORY for every development task)
Multiple sessions/agents work this repo in parallel. The main checkout is **shared**, so a
`git checkout`/branch switch in it silently discards another session's uncommitted work and
yanks the branch out from under whatever else is running (incidents: 2026-06-05, 2026-06-13).
**Rule: never develop on the shared main checkout. Every task gets its own git worktree on its
own dedicated branch, and you MUST confirm the base branch with the operator before creating it.**
1. **Ask first — which base branch?** Before creating anything, ask the operator (via
`AskUserQuestion`, unless they already told you) from which branch the new worktree/branch
should be cut. Do NOT assume `main` or "whatever I'm on" — the answer is usually the active
`release/vX.Y.Z`, but it can be another feature/release branch. Get the base explicitly.
2. **Create an isolated worktree + branch off that base** (never reuse the main checkout).
**🔴 MANDATORY PATH: every worktree lives under `.claude/worktrees/` — and nowhere else.**
This is the single canonical location (the same dir the native `EnterWorktree` tool uses). It
is gitignored AND in the `tsconfig.json` / `.dockerignore` excludes, so worktrees never leak
into the build scope. **Never** use `.worktrees/`, repo-root, or any other path — a worktree
outside `.claude/worktrees/` (a) escapes the build-scope excludes and poisons `next build` (the
`tsconfig` `include: **/*` globs ~70× the codebase → OOM; incident 2026-06-25) and (b) scatters
worktrees across two dirs.
```bash
BASE_BRANCH="release/vX.Y.Z" # ← the branch the operator confirmed in step 1
TASK="feat/your-feature" # feat/ fix/ refactor/ docs/ test/ chore/
git fetch origin "$BASE_BRANCH"
git worktree add ".claude/worktrees/${TASK##*/}" -b "$TASK" "origin/$BASE_BRANCH"
cd ".claude/worktrees/${TASK##*/}"
# symlink node_modules from the main checkout to skip a per-worktree npm install:
ln -s "$(git -C <main_checkout> rev-parse --show-toplevel)/node_modules" node_modules
```
In Claude Code prefer the native `EnterWorktree` tool (it already creates worktrees under
`.claude/worktrees/`): create the worktree with the command above, then call `EnterWorktree`
with its `path`.
3. **Work, commit, push, open the PR — all from inside the worktree.** Never `git checkout` a
different branch inside a worktree another session might share.
4. **Tear down only your own** worktree + branch when done, from the main checkout:
`git worktree remove .claude/worktrees/<dir>` then `git branch -D <task>`. Never blanket-delete
`fix/*`/`feat/*` — other sessions keep their own; delete only the branches you created, by name.
5. **Never touch another session's worktree, branch, or uncommitted changes.** If `git worktree
list` shows worktrees you didn't create, leave them alone. End every session with the main
checkout back on the branch it started on (the active `release/vX.Y.Z`, never `main`).
---
## Environment
- **Runtime**: Node.js ≥22.0.0 <23 || ≥24.0.0 <27, ES Modules. This is the **only supported** runtime for the published `omniroute` CLI, the server, and the test suites (`node:test` + vitest) — `engines.node` is authoritative and end users never need Bun. A **best-effort `bun:sqlite` compatibility path** exists so a global Bun install (`bun install -g omniroute`) can start without `better-sqlite3` (driver adapter + Bun-aware process spawning); it is **not** a supported runtime — no support guarantees — and every Bun-specific runtime change MUST preserve the Node driver/fallback chain and ship a Bun test (`test:bun:db`) or an explicit reason why the path is Node-only.
- **Bun (build/dev script runner + compatibility smoke only)**: Bun `1.3.14` is pinned as an **exact devDependency** (provisioned through the existing `npm ci` via the lockfile's `@oven/bun-*` platform binaries — no `setup-bun`/ad-hoc install). It is used **only** to execute a small, allow-listed set of TypeScript **gate/generator scripts** (replacing `node --import tsx` for startup speed): the CI checks `check:provider-consistency`, `check:compression-budget`, `check:known-symbols`, and the non-CI `gen:provider-reference`, `bench:compression` — plus the focused `test:bun:db` compatibility smoke suite for the best-effort `bun:sqlite` path. **Do NOT** widen Bun to `npm install`, the build (`build:cli*`), `check:pack-artifact`, the supported published runtime, or the main test runners — those stay on Node. Any new Bun-invoking gate/generator script must be validated byte-identical against its `node --import tsx` output first. After pulling the lockfile change, run `npm install` so `bun` resolves locally (a stale `node_modules` will fail those scripts with `bun: not found`).
- **TypeScript**: 6.0+, target ES2022, module esnext, resolution bundler
- **Path aliases**: `@/*` → `src/`, `@omniroute/open-sse` → `open-sse/`, `@omniroute/open-sse/*` → `open-sse/*`
- **Default port**: 20128 (API + dashboard on same port)
- **Data directory**: `DATA_DIR` env var, defaults to `~/.omniroute/`
- **Key env vars**: `PORT`, `JWT_SECRET`, `API_KEY_SECRET`, `INITIAL_PASSWORD`, `REQUIRE_API_KEY`, `APP_LOG_LEVEL`
- Setup: `cp .env.example .env` then generate `JWT_SECRET` (`openssl rand -base64 48`) and `API_KEY_SECRET` (`openssl rand -hex 32`)
---
## Quality Gates & Ratchets
OmniRoute has **~48 quality-gate scripts** (`scripts/check/` + `scripts/quality/`) wired
across **9 gate-running jobs** in `.github/workflows/ci.yml` (`lint`, `quality-gate`,
`quality-extended`, `docs-sync-strict`, `i18n-ui-coverage`, `i18n`, `pr-test-policy`,
`test-vitest`, `sonarqube`), plus the `quality.yml` fast-gates job (PR→`release/**`) and
3 nightly workflows (`nightly-property`, `nightly-resilience`, `nightly-llm-security`;
`nightly-mutation` once merged). Full inventory, per-job breakdown, and operational
procedures are in [`docs/architecture/QUALITY_GATES.md`](docs/architecture/QUALITY_GATES.md).
**Quick reference:**
- Gates in jobs `lint` + `docs-sync-strict`: pass/fail policy gates —
fix the violation or add an allowlist entry with a justification comment + tracking issue.
- Gates in job `quality-gate`: ratchet — metrics (ESLint warnings, code coverage, duplication,
complexity) must not regress vs `quality-baseline.json`. Update via
`npm run quality:ratchet -- --update` when a metric genuinely improves.
- Job `test-vitest` runs `npm run test:vitest` (MCP tools, autoCombo, cache) — blocking.
`test:vitest:ui` is advisory until UI component tests are triaged.
**Allowlist policy (short form):** Fix the cause; use the allowlist only for pre-existing
violations you cannot fix in the same PR. Add a comment with justification + issue number.
Stale allowlist entries (suppressing a violation that no longer exists) will be caught by
the stale-enforcement added in Fase 6A.3.
---
## Hard Rules
1. Never commit secrets or credentials
2. Never add logic to `localDb.ts`
3. Never use `eval()` / `new Function()` / implied eval
4. Never commit directly to `main`
5. Never write raw SQL in routes — use `src/lib/db/` modules
6. Never silently swallow errors in SSE streams
7. Always validate inputs with Zod schemas
8. Always include tests when changing production code
9. Coverage must not regress below the baseline frozen in `quality-baseline.json` (ratchet); absolute floor is 60% (statements/lines/functions/branches). Update the baseline via `npm run quality:ratchet -- --update` only when coverage genuinely improves. See `docs/architecture/QUALITY_GATES.md`.
10. Never bypass Husky hooks (`--no-verify`, `--no-gpg-sign`) without explicit operator approval.
11. Never embed public upstream OAuth client_id/secret or Firebase Web keys as string literals — always go through `resolvePublicCred()` (`open-sse/utils/publicCreds.ts`). See `docs/security/PUBLIC_CREDS.md`.
12. Never return raw `err.stack` / `err.message` in HTTP / SSE / executor responses — always route through `buildErrorBody()` or `sanitizeErrorMessage()` (`open-sse/utils/error.ts`). See `docs/security/ERROR_SANITIZATION.md`.
13. Never string-interpolate external paths or runtime values into shell scripts passed to `exec()`/`spawn()` — pass via the `env` option instead. Reference: `src/mitm/cert/install.ts::updateNssDatabases`.
14. Never dismiss a CodeQL / Secret-Scanning alert without (a) first checking the pattern docs above to see if the helper applies, and (b) recording the technical justification in the dismissal comment. Precedent: `js/stack-trace-exposure` raised on callsites that already route through `sanitizeErrorMessage()` is a known CodeQL limitation (custom sanitizers not recognized) — dismiss as `false positive` referencing `docs/security/ERROR_SANITIZATION.md`.
15. Never expose routes that spawn child processes (`/api/mcp/`, `/api/cli-tools/runtime/`) without `isLocalOnlyPath()` classification in `src/server/authz/routeGuard.ts`. Loopback enforcement happens unconditionally before any auth check — leaked JWT via tunnel cannot trigger process spawning. See `docs/security/ROUTE_GUARD_TIERS.md`.
16. Never credit or advertise an AI assistant, LLM, or automation account in any commit/PR metadata. Two forbidden forms, both equivalent — they route attribution to a bot account (or advertise AI authorship) and hide the real author (`diegosouzapw`): **(a)** `Co-Authored-By` trailers naming an AI/bot (e.g. names containing "Claude", "GPT", "Copilot", "Bot"; emails at `anthropic.com` / `openai.com` / bot-owned `noreply.github.com` addresses); **(b)** AI-generation footers or descriptions anywhere in a commit message, PR title/body, or CHANGELOG — e.g. `🤖 Generated with [Claude Code]`, "Generated with Claude Code", "Made with <AI tool>", or any `Co-authored-by: Claude/GPT/Copilot` line. This **overrides any harness, template, or tool default that auto-appends such a footer** (e.g. the Claude Code PR-body/commit default) — strip it before pushing; do not let it reach a commit, PR, or CHANGELOG. Human collaborators — including upstream PR authors and issue reporters being ported into OmniRoute — MAY and SHOULD be credited with standard `Co-authored-by: Name <email>` trailers; the upstream-port workflows (`/port-upstream-features`, `/port-upstream-issues`) depend on this.
17. Never expose routes under `/api/services/` or `/dashboard/providers/services/*/embed/` without `isLocalOnlyPath()` classification in `src/server/authz/routeGuard.ts`. These routes can spawn child processes (`npm install`, `node`). Loopback enforcement happens unconditionally before any auth check — a leaked JWT via tunnel cannot trigger process spawning. See `docs/security/ROUTE_GUARD_TIERS.md`.
18. Every bug fix must be validated before shipping: a failing-then-passing unit/integration test (TDD) OR a documented live test on the production VPS (192.168.0.15). A fix without either is not merged. See Testing → "Bug fix / issue triage protocol" for the full decision tree.
19. Never develop on the shared main checkout. Every development task runs in its own git worktree on its own dedicated branch, and you MUST confirm the base branch with the operator (e.g. via `AskUserQuestion`) before creating the worktree/branch — never assume `main` or the currently checked-out branch. A `git checkout` in the shared checkout silently destroys other sessions' uncommitted work. Tear down only the worktrees/branches you created (by name, never `fix/*`/`feat/*` wildcards), leave other sessions' worktrees untouched, and end on the branch you started on (the active `release/vX.Y.Z`, never `main`). See Git Workflow → "Worktree isolation".
20. PII redaction/sanitization is **opt-in — never on by default**. OmniRoute proxies for self-hosted/local LLMs where the operator owns the data, so mutating request/response payloads by default would silently corrupt legitimate traffic. The two data-mutating PII feature flags **MUST** keep `defaultValue: "false"` in `src/shared/constants/featureFlagDefinitions.ts`: `PII_REDACTION_ENABLED` (request-side) and `PII_RESPONSE_SANITIZATION` (response + streaming). All three application points — `src/lib/guardrails/piiMasker.ts` (request guardrail), `src/lib/piiSanitizer.ts` (response), `src/lib/streamingPiiTransform.ts` (SSE) — are gated on these flags; with both off the `pii-masker` guardrail still runs but never mutates payloads (data passes through untouched). Flipping either default to `"true"` requires explicit operator approval. The regression guard is `tests/unit/pii-opt-in-default.test.ts` (asserts both definition defaults + behavioral pass-through). Opt-in is per-operator via env or the settings/DB override (`src/lib/db/featureFlags.ts`), never a silent default. See `docs/security/GUARDRAILS.md`.
21. **Release-freeze — the FROZEN release branch belongs to the release captain; development does NOT stop (parallel-cycle model, 2026-07-04).** `/generate-release` opens a marker issue labeled `release-freeze` at the start of reconciliation (Phase 0a), **immediately cuts the next cycle's branch `release/vX+1` from the frozen tip (Phase 0a.0b — bump + living release PR + re-home of open PRs)**, and closes the freeze once the release PR squash-merges to `main`. Before merging **any** PR, every campaign workflow (`/review-prs`, `/review-group-prs`, `/merge-prs`, `/triage-fix-bugs`, `/implement-fix-bugs`, `/triage-features`, `/implement-features`, `/green-prs`, `/port-upstream-*`) **MUST** check `gh issue list --repo diegosouzapw/OmniRoute --label release-freeze --state open` — if a freeze is active: **NEVER merge into the frozen `release/vX.Y.Z` named in the freeze title**; instead resolve the ACTIVE development branch (the **highest** `release/v*` by semver — normally `release/vX+1`, announced in a freeze-issue comment) and **retarget the PR there** (`gh pr edit <N> --base release/vX+1`, then VERIFY with `gh pr view <N> --json baseRefName` — the edit fails silently) and merge normally. **HOLD only when the highest release/v\* branch IS the frozen one** (the short window before 0a.0b completes, or a pre-parallel-cycle release) — in that case leave the PR ready and open, tell the operator, and resume when the next branch appears or the freeze lifts. The just-shipped fixes reach `release/vX+1` via the Phase 5 sync-back (`scripts/release/sync-next-cycle.mjs`); do not try to sync mid-release. This is a **coordination signal, not a permission lock**: the release captain and the campaign sessions share the `diegosouzapw` identity, so a GitHub branch-protection lock cannot distinguish them — only this honored marker prevents the mid-release commit races that forced full CHANGELOG re-reconciliation in v3.8.40/v3.8.41 (a parallel campaign advanced `release/vX.Y.Z` by 34 commits mid-run). The release captain's own reconciliation/cycle-open pushes are exempt — they _are_ the release. Fixes that must land during a freeze (a homologation finding) follow the post-merge read-only rule: land on `main` first via `fix/release-vX.Y.Z-*`. **⛔ ONLY `/generate-release` may raise a release-freeze, and ONLY at its Phase 0a (start of generating a new version) — lifted at Phase 12c after the squash-merge to `main`.** No campaign, session, or agent may open a `release-freeze` marker at any other time — a freeze is **never** a mid-development coordination tool. If a session ever believes a freeze is genuinely, unavoidably necessary outside the `/generate-release` flow, it **MUST first ask the operator (`diegosouzapw`) in chat, explicitly alert "estou criando um freeze" and get an explicit yes** — never open, extend, or re-open a `release-freeze` autonomously. Conversely, do **not** close/lift an active `/generate-release` freeze to unblock campaign merges: it protects the captain's single clean CI run and auto-lifts at Phase 12c — closing it early re-triggers the exact commit race it prevents. Verify a freeze is legitimate before acting on it: an open `release-freeze` whose title/body references an **OPEN** release PR (`gh pr view <N> --json state`) is the authorized captain freeze — hold, don't touch.
22. **Cross-session safety — this repo is worked by MANY parallel sessions/agents at once; never step on another's in-flight work.** Two absolute bans, both recurring incidents (this rule exists because they keep happening):
- **(a) Never `git stash` / `git stash pop` — ANYWHERE in this repo, including inside an isolated worktree, and including inside any subagent you dispatch.** `git stash` operates on the **shared repository object store**, not the per-worktree working tree — so a stash pushed or popped in one session can silently clobber or resurrect another parallel session's uncommitted changes. This is not hypothetical: 2026-07-02 a `#5923` quotaCache change leaked into the unrelated `#2296` worktree via a global `stash pop`, and the same class reincided through a **subagent**. To compare working changes against a base ref **without** stashing, use `git show <ref>:<path>` or `git diff <ref> -- <path>`; to confirm a typecheck/lint error is pre-existing on the base, inspect the base ref directly (`git show origin/release/vX.Y.Z:<path>`) — never stash your tree away to "get it clean". **Put this ban verbatim in the prompt of every subagent that touches git** (agents don't inherit this file's context — the recurrence was a subagent).
- **(b) Never merge, push, rebase, or force-push a PR / branch / worktree that another session is actively working.** An open PR whose head is a live fix worktree in `.claude/worktrees/` you did **not** create (e.g. `fix-5852`/`fix-5923` carrying fresh commits, even when they share your `diegosouzapw` identity), or any branch another session owns, is **off-limits — HOLD**, and let the owning session merge it. **Before** merging or pushing to any PR you did not create _this_ session, run `git worktree list` to check for a matching in-flight worktree and re-check `gh pr view <N> --json state,headRefOid`. Only the owning session merges its own in-flight PR; mid-flight merges race the owner and re-trigger the exact commit/CHANGELOG races Rule #19 and Rule #21 guard against. (Reinforces Rule #19.)
---
## PII & Stream Sanitization Learnings
### 1. Regex Security (ReDoS)
All regex patterns matching variable-length strings (e.g. IPv6 address, credit cards) must use strictly bounded, non-overlapping sequences (e.g., limit occurrences with bounded ranges `{1,7}`) to prevent catastrophic backtracking when processing untrusted inputs.
### 2. SSE Snapshot Handling
When parsing streaming LLM responses (e.g. Responses API), check if a chunk represents a final snapshot (`done` or `completed` events). Snapshot text must be sanitized directly as a standalone string (bypassing rolling delta buffers) to prevent text duplication at the end of the stream.
### 3. Database Handles in Tests
Ensure that any unit tests that trigger database migrations or establish SQLite connections call `resetDbInstance()` and properly clean up/close all DB handles in a `test.after(...)` hook. Failure to release database connection handles will cause Node's native test runner to hang indefinitely.

View File

@@ -2,11 +2,6 @@
Thank you for your interest in contributing! This guide covers everything you need to get started.
For the official per-change workflow, start with the
[Contribution Golden Path](docs/dev/CONTRIBUTION_GOLDEN_PATH.md). It maps provider, routing,
UI/UX, i18n, CLI, database, and build/deploy changes to their contracts, focused tests, CI
coverage, and reconciliation steps.
---
## Development Setup
@@ -15,12 +10,6 @@ coverage, and reconciliation steps.
- **Node.js** `>=22.22.3 <23`, or `>=24.0.0 <27` (recommended: 24 LTS)
- **npm** 10+
> **npm v11+ users (Node 24+):** After `npm install`, verify native modules were installed:
> `node -e "require('better-sqlite3')"`. If it fails with `MODULE_NOT_FOUND`,
> run `npm approve-scripts better-sqlite3 && npm install`. See
> [Troubleshooting](docs/guides/TROUBLESHOOTING.md#npm-v11-better-sqlite3-not-installed-cannot-find-module).
- **Git**
### Clone & Install
@@ -209,11 +198,10 @@ Coverage notes:
### Pull Request Requirements
Before opening a PR, use the
[Contribution Golden Path](docs/dev/CONTRIBUTION_GOLDEN_PATH.md) to run the focused loop for
what you changed. The full unit suite (4 CI shards), Vitest, the **60%+** coverage gate, and
the production build are CI's responsibility — running them locally adds no signal the PR
checks will not already give you, and on smaller machines it can saturate the host (#8084):
Before opening a PR, run the focused loop for what you changed. The full unit suite
(4 CI shards), Vitest, the **60%+** coverage gate, and the production build are CI's
responsibility — running them locally adds no signal the PR checks will not already
give you, and on smaller machines it can saturate the host (#8084):
- Run the test files that cover your change: `node --import tsx/esm --test tests/unit/<file>.test.ts`
- Run `npm run lint`
@@ -283,7 +271,7 @@ src/ # TypeScript (.ts / .tsx)
│ ├── a2a/ # Agent-to-Agent v0.3 protocol server
│ ├── acp/ # Agent Communication Protocol registry
│ ├── compliance/ # Compliance policy engine
│ ├── db/ # SQLite domain modules + 130 migrations
│ ├── db/ # SQLite database layer (21 modules + 16 migrations)
│ ├── memory/ # Persistent conversational memory
│ ├── oauth/ # OAuth providers, services, and utilities
│ ├── skills/ # Extensible skill framework
@@ -293,7 +281,7 @@ src/ # TypeScript (.ts / .tsx)
├── mitm/ # MITM proxy (cert, DNS, target routing)
├── shared/
│ ├── components/ # React components (.tsx)
│ ├── constants/ # Provider definitions (290), MCP scopes, 19 routing strategies
│ ├── constants/ # Provider definitions (177), MCP scopes, 14 routing strategies
│ ├── utils/ # Circuit breaker, sanitizer, auth helpers
│ └── validation/ # Zod v4 schemas
└── sse/ # SSE proxy pipeline
@@ -301,7 +289,7 @@ src/ # TypeScript (.ts / .tsx)
open-sse/ # @omniroute/open-sse workspace
├── executors/ # 14 provider-specific request executors
├── handlers/ # 11 request handlers (chat, responses, embeddings, images, etc.)
├── mcp-server/ # MCP server (104 tools, 3 transports, 31 scopes)
├── mcp-server/ # MCP server (25 tools, 3 transports, 10 scopes)
├── services/ # 36+ services (combo, autoCombo, rateLimitManager, etc.)
├── translator/ # Format translators (OpenAI ↔ Claude ↔ Gemini ↔ Responses ↔ Ollama)
├── transformer/ # Responses API transformer

View File

@@ -1,5 +1,5 @@
# ── Common base with runtime deps ──────────────────────────────────────────
FROM node:26-trixie-slim AS base
FROM node:24-trixie-slim AS base
WORKDIR /app
# `apt-get upgrade` pulls the security-patched versions of the Debian (trixie)
@@ -8,8 +8,8 @@ WORKDIR /app
# that already have a fix published in trixie. CVEs without an upstream fix yet
# (local-only TOCTOU, etc.) remain until the distro patches them and the image
# is rebuilt; none are reachable from the proxy's request surface at runtime.
RUN --mount=type=cache,id=apt-cache,target=/var/cache/apt,sharing=locked \
--mount=type=cache,id=apt-lists,target=/var/lib/apt/lists,sharing=locked \
RUN --mount=type=cache,id=apt-cache,target=/var/cache/apt,sharing=shared \
--mount=type=cache,id=apt-lists,target=/var/lib/apt/lists,sharing=shared \
apt-get update \
&& apt-get upgrade -y \
&& apt-get install -y --no-install-recommends libsecret-1-0 ca-certificates \
@@ -29,8 +29,8 @@ FROM base AS builder
# Build tools for native module compilation
# apt-get update needed here because base's rm -rf clears the shared cache
RUN --mount=type=cache,id=apt-cache,target=/var/cache/apt,sharing=locked \
--mount=type=cache,id=apt-lists,target=/var/lib/apt/lists,sharing=locked \
RUN --mount=type=cache,id=apt-cache,target=/var/cache/apt,sharing=shared \
--mount=type=cache,id=apt-lists,target=/var/lib/apt/lists,sharing=shared \
apt-get update \
&& apt-get install -y --no-install-recommends python3 make g++ \
&& rm -rf /var/lib/apt/lists/*
@@ -77,7 +77,7 @@ RUN test -f package-lock.json \
# a broken/rate-limited fetch fails the BUILD loudly instead of shipping a
# broken image.
RUN --mount=type=cache,id=npm-cache,target=/root/.npm \
npm ci --include=optional --no-audit --no-fund --legacy-peer-deps --ignore-scripts \
npm ci --no-audit --no-fund --legacy-peer-deps --ignore-scripts \
&& (cd node_modules/better-sqlite3 \
&& node /usr/local/lib/node_modules/npm/node_modules/node-gyp/bin/node-gyp.js rebuild) \
&& node -e "require('better-sqlite3')(':memory:').close()" \
@@ -95,11 +95,6 @@ RUN --mount=type=cache,id=npm-cache,target=/root/.npm \
# See docs/ops/QUALITY_GATE_PLAYBOOK.md Parte 6.
ENV OMNIROUTE_USE_TURBOPACK=1
# Next.js basePath is fixed at build time; pass OMNIROUTE_BASE_PATH here when the
# image should serve under a reverse-proxy subpath without a runtime patch.
ARG OMNIROUTE_BASE_PATH=""
ENV OMNIROUTE_BASE_PATH=$OMNIROUTE_BASE_PATH
# Docker containers cannot run the MITM/Agent-Bridge stack (no host DNS/cert
# access), so keep @/mitm/manager on the graceful stub (#3390). This flag is
# Docker-only: npm/Electron/VPS builds must bundle the REAL manager (#6344).
@@ -119,9 +114,7 @@ ENV NODE_OPTIONS="--max-old-space-size=${OMNIROUTE_BUILD_MEMORY_MB}"
COPY . ./
RUN --mount=type=cache,id=next-cache,target=/app/.build/next/cache \
mkdir -p /app/data \
&& npm run build \
&& node --input-type=module -e "import { createRequire } from 'node:module'; import { pathToFileURL } from 'node:url'; const standaloneRoot = '/app/.build/next/standalone/node_modules/'; const require = createRequire('/app/.build/next/standalone/package.json'); for (const pkg of ['@atjsh/llmlingua-2', '@huggingface/transformers', '@tensorflow/tfjs', 'js-tiktoken']) { const resolved = require.resolve(pkg); if (!resolved.startsWith(standaloneRoot)) throw new Error(pkg + ' resolved outside standalone: ' + resolved); await import(pathToFileURL(resolved).href); } const onnxRuntime = require.resolve('onnxruntime-node'); if (!onnxRuntime.startsWith(standaloneRoot)) throw new Error('onnxruntime-node resolved outside standalone: ' + onnxRuntime); await import(pathToFileURL(onnxRuntime).href);"
mkdir -p /app/data && npm run build
# ── Runner base ────────────────────────────────────────────────────────────
FROM base AS runner-base
@@ -181,8 +174,8 @@ EXPOSE 20128
USER node
# Warns if the mounted data volume has wrong ownership
COPY --chmod=755 scripts/check-permissions.sh /app/check-permissions.sh
ENTRYPOINT ["/app/check-permissions.sh"]
COPY --chmod=755 scripts/check-permissions.sh /tmp/check-permissions.sh
ENTRYPOINT ["/tmp/check-permissions.sh"]
HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \
CMD ["node", "healthcheck.mjs"]

View File

@@ -1,13 +1,50 @@
# GEMINI.md
# Security and Cleanliness Rules for AI Assistants
> **Single source of truth:** all project rules for AI assistants live in
> [`AGENTS.md`](AGENTS.md). Read it in full before any change — it contains the 22 Hard Rules,
> quality gates, code conventions, file-placement / repo-root hygiene rules, the repository map
> and the local development access notes that used to live in this file.
> **Scope:** rules for Gemini-based agents. For Claude Code, see `CLAUDE.md`. For other AI assistants, see `AGENTS.md`.
Gemini-specific notes:
## 1. File Placement & Organization
- Skills activate via the `activate_skill` tool (skill metadata is loaded at session start and
the full content is activated on demand).
- There are no other Gemini-only rules today. Do not re-add project rules here — edit
`AGENTS.md` instead, so every assistant sees the same instructions.
- **Test Files**: ALL unit tests, integration tests, ecosystem tests, or Vitest files MUST strictly be placed within the `tests/` directory (e.g., `tests/unit/`, `tests/integration/`). NEVER create test files in the project root (`/`).
- **Scripts and Utilities**: ALL maintenance, debugging, generation, or experimental scripts (`.cjs`, `.mjs`, `.js`, `.ts`) MUST be placed strictly inside one of the `scripts/` subfolders (`build/`, `dev/`, `check/`, `docs/`, `i18n/`, `ad-hoc/`). One-shot or experimental code goes under `scripts/ad-hoc/`. NEVER dump loose scripts in the project root (`/`) or the top-level `scripts/` folder.
**The Project Root MUST ONLY CONTAIN:**
- Configuration files (`vitest.config.ts`, `next.config.mjs`, `eslint.config.mjs`, `tsconfig*.json`, `playwright.config.ts`, `prettier.config.mjs`, `postcss.config.mjs`, `sonar-project.properties`, `fly.toml`, `docker-compose*.yml`, `Dockerfile`)
- Dependency files (`package.json`, `package-lock.json`)
- Documentation files (`README.md`, `CHANGELOG.md`, `LICENSE`, `AGENTS.md`, `CLAUDE.md`, `GEMINI.md`, `CONTRIBUTING.md`, `SECURITY.md`, `CODE_OF_CONDUCT.md`, `llm.txt`, `Tuto_Qdrant.md`)
- CI/CD files and ignore definitions (`.gitignore`, `.dockerignore`, `.npmignore`, `.npmrc`, `.node-version`, `.nvmrc`, `.env.example`)
When creating _any_ validation tests or one-off logic scripts, default to using `scripts/ad-hoc/` or the `tests/unit/` directories according to your goals. Do not pollute the `/` root context.
## 2. Hard Rules (mirror of `CLAUDE.md`)
1. **Never commit secrets or credentials.** Use `.env` (auto-generated from `.env.example`) or a vault. Passwords, OAuth secrets, API keys, and Cookie values must never appear in committed files.
2. **Never add logic to `src/lib/localDb.ts`.** It is a re-export barrel only.
3. **Never use `eval()`, `new Function()`, or any implied eval.** ESLint enforces this.
4. **Never commit directly to `main`.** Use `feat/`, `fix/`, `refactor/`, `docs/`, `test/`, or `chore/` branches.
5. **Never write raw SQL in routes** — always go through `src/lib/db/` domain modules.
6. **Never silently swallow errors in SSE streams** — propagate them or abort the stream cleanly.
7. **Never bypass Husky hooks** (`--no-verify`, `--no-gpg-sign`) without explicit operator approval.
8. **Always validate inputs with Zod schemas** from `src/shared/validation/schemas.ts`.
9. **Always include tests when changing production code** (`src/`, `open-sse/`, `electron/`, `bin/`).
10. **Coverage must stay** ≥ 60 % statements / lines / functions / branches — the official CI gate (`npm run test:coverage`). The ratchet baseline in `quality-baseline.json` may freeze a higher floor; never regress it.
## 3. Codebase navigation
| Task | Read this first |
| ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Understand the codebase | `docs/architecture/REPOSITORY_MAP.md` |
| Architecture overview | `docs/architecture/ARCHITECTURE.md` |
| Engineering reference | `docs/architecture/CODEBASE_DOCUMENTATION.md` |
| Add a feature | `CONTRIBUTING.md` + the matching `docs/<area>.md` |
| Per-area deep dives | `docs/frameworks/SKILLS.md`, `docs/frameworks/MEMORY.md`, `docs/frameworks/EVALS.md`, `docs/security/GUARDRAILS.md`, `docs/security/COMPLIANCE.md`, `docs/frameworks/CLOUD_AGENT.md`, `docs/frameworks/MCP-SERVER.md`, `docs/frameworks/A2A-SERVER.md`, `docs/architecture/AUTHZ_GUIDE.md`, `docs/architecture/RESILIENCE_GUIDE.md`, `docs/routing/AUTO-COMBO.md`, `docs/frameworks/WEBHOOKS.md`, `docs/routing/REASONING_REPLAY.md`, `docs/security/STEALTH_GUIDE.md`, `docs/ops/TUNNELS_GUIDE.md`, `docs/guides/ELECTRON_GUIDE.md`, `docs/reference/PROVIDER_REFERENCE.md` |
| Release flow | `docs/ops/RELEASE_CHECKLIST.md` |
## 4. Local development access
The dashboard is reachable at the operator's chosen URL/port (default `http://localhost:20128`). Credentials are operator-specific:
- **Initial admin password** is read from the `INITIAL_PASSWORD` env var on first install (defaults to `CHANGEME` in `.env.example`; rotate immediately after first login).
- **Local VPS / shared dev environments**: ask the operator for the URL and current credentials — they live in their personal vault, NOT in this repo.
> Any credential observed in a previous version of this file was a non-production demo value; treat it as compromised and do not reuse it.

176
README.md
View File

@@ -7,7 +7,7 @@
# 🚀 OmniRoute — The Free AI Gateway
<img src="./docs/diagrams/readme-hero.svg" width="100%" alt="OmniRoute — Never stop coding. Every AI tool → 291 providers — 90+ free — through one endpoint. Claude Code, Codex, Cursor, Cline, Copilot & Antigravity into FREE Claude / GPT / Gemini with auto-fallback. RTK + Caveman stacked compression saves 1595% tokens (~89% avg) — never hit limits. 291 AI providers · 90+ free tiers · ~1.53B free tokens/mo · 19 routing strategies · $0 to start."/>
<img src="./docs/diagrams/readme-hero.svg" width="100%" alt="OmniRoute — Never stop coding. Every AI tool → 290 providers — 90+ free — through one endpoint. Claude Code, Codex, Cursor, Cline, Copilot & Antigravity into FREE Claude / GPT / Gemini with auto-fallback. RTK + Caveman stacked compression saves 1595% tokens (~89% avg) — never hit limits. 290 AI providers · 90+ free tiers · ~1.53B free tokens/mo · 19 routing strategies · $0 to start."/>
</div>
@@ -67,46 +67,32 @@
<table>
<tr>
<td align="right"><b>🚀 Start</b></td>
<td align="center"><a href="#-quick-start">🚀 Quick Start</a></td>
<td align="center"><a href="#-more-install-methods--docker-source-pnpm-arch">📦 Install</a></td>
<td align="center"><a href="#-works-the-second-you-install-it--no-keys-no-config">🆓 Zero-config</a></td>
<td align="center"><a href="#-quick-start"><b>🚀 Quick Start</b></a></td>
<td align="center"><a href="#-combos--the-flagship"><b>🎯 Combos</b></a></td>
<td align="center"><a href="#-290-ai-providers--90-free"><b>🌐 Providers</b></a></td>
</tr>
<tr>
<td align="right"><b>💡 Learn</b></td>
<td align="center"><a href="#-full-cli--a2a--mcp"><b>🔌 CLI &amp; MCP</b></a></td>
<td align="center"><a href="#%EF%B8%8F-save-1595-tokens--automatically"><b>🗜️ Compression</b></a></td>
<td align="center"><a href="https://omniroute.online"><b>🌍 Website</b></a></td>
</tr>
</table>
<table>
<tr>
<td align="center"><a href="#-the-promise">💥 The Promise</a></td>
<td align="center"><a href="#-why-omniroute">🤔 Why OmniRoute</a></td>
<td align="center"><a href="#-why-omniroute">🤔 Why</a></td>
<td align="center"><a href="#-what-sets-omniroute-apart">🏆 What Sets Apart</a></td>
</tr>
<tr>
<td align="right"><b>⚙️ Features</b></td>
<td align="center"><a href="#-combos--the-flagship">🎯 Combos</a></td>
<td align="center"><a href="#-291-ai-providers--90-free">🌐 Providers</a></td>
<td align="center"><a href="#-full-cli--a2a--mcp">🔌 CLI &amp; MCP</a></td>
</tr>
<tr>
<td align="right"></td>
<td align="center"><a href="#%EF%B8%8F-save-1595-tokens--automatically">🗜️ Compression</a></td>
<td align="center"><a href="#-compatible-clis--coding-agents">🤖 Compatible CLIs</a></td>
<td align="center"><a href="#%EF%B8%8F-where-omniroute-runs--anywhere">🖥️ Where It Runs</a></td>
<td align="center"><a href="#-private--local-first">🔒 Private</a></td>
</tr>
<tr>
<td align="right"><b>👀 See it</b></td>
<td align="center"><a href="#-omniroute-in-action">🎬 In Action</a></td>
<td align="center"><a href="#-whats-new">✨ What's New</a></td>
<td align="center"><a href="#-compatible-clis--coding-agents">🤖 Compatible CLIs</a></td>
</tr>
<tr>
<td align="right"><b>💚 Support</b></td>
<td align="center"><a href="#-support-omniroute">💚 Support / Donate</a></td>
<td align="center"><a href="#-community--help">💬 Community</a></td>
<td align="center"><a href="#-sponsors">💖 Sponsors</a></td>
</tr>
<tr>
<td align="right"><b>📦 Project</b></td>
<td align="center"><a href="#%EF%B8%8F-tech-stack">🛠️ Tech Stack</a></td>
<td align="center"><a href="#-documentation">📖 Docs</a></td>
<td align="center"><a href="#-500-contributors">👥 Contributors</a></td>
<td align="center"><a href="#-dashboard-screenshots">📸 Screenshots</a></td>
<td align="center"><a href="#-support--community">📧 Support</a></td>
</tr>
</table>
@@ -188,7 +174,7 @@ curl http://localhost:20128/v1/chat/completions \
</div>
<img src="./docs/diagrams/promise-pillars.svg" width="100%" alt="The Promise — One endpoint. 291 providers. Never stop building — OmniRoute picks the cheapest one that works. Six pillars: Never hit limits (auto-fallback across 291 providers in milliseconds, zero downtime) · Save up to 95% tokens (RTK + Caveman stacked compression cuts 1595%, ~89% avg on tool-heavy sessions) · $0 to start (90+ free tiers, 40+ free forever — no card needed) · Every tool works (33 coding agents through one config) · One endpoint (OpenAI ↔ Claude ↔ Gemini ↔ Responses API at /v1) · Production-grade (circuit breakers, TLS stealth, MCP 104 tools, A2A, memory, guardrails, evals — 25,000+ tests)."/>
<img src="./docs/diagrams/promise-pillars.svg" width="100%" alt="The Promise — One endpoint. 290 providers. Never stop building — OmniRoute picks the cheapest one that works. Six pillars: Never hit limits (auto-fallback across 290 providers in milliseconds, zero downtime) · Save up to 95% tokens (RTK + Caveman stacked compression cuts 1595%, ~89% avg on tool-heavy sessions) · $0 to start (90+ free tiers, 40+ free forever — no card needed) · Every tool works (33 coding agents through one config) · One endpoint (OpenAI ↔ Claude ↔ Gemini ↔ Responses API at /v1) · Production-grade (circuit breakers, TLS stealth, MCP 104 tools, A2A, memory, guardrails, evals — 25,000+ tests)."/>
<br/>
<br/>
@@ -241,53 +227,12 @@ curl http://localhost:20128/v1/chat/completions \
<b>What Kimi's support powers:</b> Kimi's API credits power OmniRoute's AI-validated release pipeline — the <i>merge validation powered by Kimi K3</i> stage that reviews every pull request before it ships — plus day-to-day feature development. First-class Kimi support ships on both rails: the direct <a href="https://platform.kimi.ai?aff=omniroute">Kimi API</a> (<code>kimi-k3</code>) and the <a href="https://www.kimi.com/code?aff=omniroute">Kimi Code coding plan</a> (OAuth and API key). OmniRoute is also the first Brazilian open-source project in Kimi's support program. <a href="https://platform.kimi.ai?aff=omniroute"><b>Get a Kimi API key →</b></a>
</td>
</tr>
<tr>
<td align="center" width="150">
<a href="https://cheaperinference.com/?utm_source=omniroute">
<img src="public/providers/cheaperinference.svg" width="64" alt="Cheaper Inference"/>
</a>
<br/><b>Cheaper Inference</b><br/><sub>cheaperinference.com</sub><br/><br/>
<img src="https://img.shields.io/badge/Open_Source_Friend-31f889?style=flat-square&labelColor=04170d" alt="Open Source Friend"/>
</td>
<td>
Thanks to <b>Cheaper Inference</b>, an OmniRoute Open Source Friend, for backing this project! Cheaper Inference is a cost-ranked gateway that resells 42 frontier models — Claude, GPT-5.x, Gemini, Kimi K3, GLM, DeepSeek, Grok and MiniMax — behind one OpenAI-compatible endpoint, routing each request to the cheapest eligible provider without ever charging above the model maker's list price.
<br/><br/>
<b>First-class support in OmniRoute:</b> Chat Completions, the native <code>/v1/responses</code> endpoint, vision, tool calling and 3 image models (<code>grok-imagine</code>, <code>nano-banana-pro</code>, <code>nano-banana-2</code>, reachable as <code>cheaperinference/&lt;model&gt;</code>). <a href="https://cheaperinference.com/?utm_source=omniroute"><b>Get an API key →</b></a>
</td>
</tr>
</table>
<sub>Links tagged <code>aff=omniroute</code> are partner links. They fund the project at no extra cost to you.</sub>
<br/>
<details open>
<summary><sub><b>🎟️ Affiliates Promo</b> — free signup coupons from providers we don't sponsor (click to expand)</sub></summary>
<sub><i>This section is for referral/coupon codes only. Sponsored partnerships live in <b>🤝 Supported by our Open Source Friends</b> above. OmniRoute has no sponsorship or partnership with the providers listed here — these are public coupons anyone can use.</i></sub>
<table>
<tr>
<td align="center" width="120">
<a href="https://agentrouter.org/register?aff=70LM">
<img src="public/providers/agentrouter.png" width="32" alt="AgentRouter"/>
</a>
<br/><sub><b>AgentRouter</b></sub><br/><sub>agentrouter.org</sub>
</td>
<td>
<sub><b><a href="https://agentrouter.org/register?aff=70LM">AgentRouter</a></b> — affiliate signup · <b>$100 free credits</b> on signup (free server, expect higher latency — best for testing, not production). First-class support in OmniRoute since <b>v3.8.50</b>: Chat Completions, the Anthropic-compatible wire format and the OpenAI-compatible path. Available models include <code>claude-opus-4-8</code>, <code>claude-opus-5</code>, <code>gpt-5.6-sol</code> and more. <b><a href="https://agentrouter.org/register?aff=70LM">Grab your $100 →</a></b></sub>
<br/><br/>
<sub>⚠️ <i>Affiliate link — OmniRoute has no sponsorship or partnership with this provider.</i></sub>
</td>
</tr>
</table>
<sub>Know another provider with a generous free signup coupon that benefits OmniRoute users? Open an issue and we'll add it here.</sub>
</details>
<br/>
<div align="center">
## 🎯 Combos — The Flagship
@@ -439,55 +384,19 @@ All **19** strategies — mix & match per combo step:
</div>
<img src="./docs/diagrams/comparison-table.svg" width="100%" alt="What sets OmniRoute apart — comparison table vs 9router, OpenRouter, CLIProxyAPI and LiteLLM across 13 capabilities. OmniRoute: 291 providers, 90+ free providers built-in, 19 routing strategies, 12-engine token compression, built-in MCP server with 104 tools, A2A agent protocol, persistent memory, guardrails, cloud agents, TLS fingerprint stealth, Desktop/Termux/PWA, 43 i18n UI locales, 100% MIT self-hosted. OmniRoute is the only one with the full set; competitors show a mix of checks, partials and crosses. Verified from each project&apos;s docs."/>
<img src="./docs/diagrams/comparison-table.svg" width="100%" alt="What sets OmniRoute apart — comparison table vs 9router, OpenRouter, CLIProxyAPI and LiteLLM across 13 capabilities. OmniRoute: 290 providers, 90+ free providers built-in, 19 routing strategies, 12-engine token compression, built-in MCP server with 104 tools, A2A agent protocol, persistent memory, guardrails, cloud agents, TLS fingerprint stealth, Desktop/Termux/PWA, 43 i18n UI locales, 100% MIT self-hosted. OmniRoute is the only one with the full set; competitors show a mix of checks, partials and crosses. Verified from each project&apos;s docs."/>
<sub>📊 Full methodology &amp; per-feature detail vs 9router, OpenRouter, CLIProxyAPI &amp; LiteLLM → [`docs/comparison/OMNIROUTE_VS_ALTERNATIVES.md`](docs/comparison/OMNIROUTE_VS_ALTERNATIVES.md)</sub>
<br/>
## 💚 Support OmniRoute
## ❤️ Support
OmniRoute is MIT-licensed and maintained in the open. If it saves you time or money, here's how to keep it independent — pick whatever fits you. Sponsorship never affects routing priority; it buys visibility, not ranking.
OmniRoute is free and open source, built and maintained in the open. If it saves you time or money, consider supporting development:
<table>
<tr><td nowrap>⭐ <b>Star the repo</b></td><td>Free — genuinely helps visibility</td><td><a href="https://github.com/diegosouzapw/OmniRoute">Star OmniRoute</a></td></tr>
<tr><td nowrap>🐙 <b>GitHub Sponsors</b></td><td>One-off or monthly · zero platform fee</td><td><a href="https://github.com/sponsors/diegosouzapw">github.com/sponsors/diegosouzapw</a></td></tr>
<tr><td nowrap>☕ <b>Ko-fi</b></td><td>Quick one-off tip, no signup for the donor</td><td><a href="https://ko-fi.com/diegosouzapw">ko-fi.com/diegosouzapw</a></td></tr>
<tr><td nowrap>🧋 <b>Buy Me a Coffee</b></td><td>Small, informal gesture</td><td><a href="https://www.buymeacoffee.com/diegosouzapw">buymeacoffee.com/diegosouzapw</a></td></tr>
<tr><td nowrap>🖐 <b>Liberapay</b></td><td>Recurring · non-profit · open source</td><td><a href="https://liberapay.com/diegosouzapw">liberapay.com/diegosouzapw</a></td></tr>
<tr><td nowrap>🇧🇷 <b>PIX</b> (Brazil)</td><td>Instant, no fees</td><td>key &amp; QR below</td></tr>
<tr><td nowrap>₿ <b>Crypto</b></td><td>BTC · ETH · USDT-TRC20 · USDC-Solana</td><td>addresses below</td></tr>
</table>
**🇧🇷 PIX** — instant, no fees (Brazil)
<img src="docs/assets/pix-qr.png" width="140" align="right" alt="OmniRoute PIX QR code"/>
Key (random): `5d865059-bc44-483a-962d-43ceb80126eb`
Pix copia-e-cola:
```
00020101021126580014br.gov.bcb.pix01365d865059-bc44-483a-962d-43ceb80126eb5204000053039865802BR5922OMNIROUTE CONTRIBUICAO6006BRASIL62070503***630475DD
```
<br clear="right"/>
<details>
<summary><b>₿ Crypto</b> — BTC · ETH · USDT-TRC20 · USDC-Solana (click to expand)</summary>
<table>
<tr><td nowrap><b>₿ BTC</b></td><td nowrap>Bitcoin (SegWit)</td><td><code>bc1qh00smz004sy85wyl28v77tenkt3ckl6eaep7fd</code></td></tr>
<tr><td nowrap><b>Ξ ETH</b></td><td nowrap>Ethereum (ERC20)</td><td><code>0x64Cf6B68A6Ff34288e89172950a2d00102337a84</code></td></tr>
<tr><td nowrap><b>₮ USDT</b></td><td nowrap>Tron (TRC20)</td><td><code>TKAF41JpuQrHbKTnsQa9svJE2T192Hvsc2</code></td></tr>
<tr><td nowrap><b>$ USDC</b></td><td nowrap>Solana</td><td><code>2emNNZzVVWQc3FQ2wk9M6qXUQmW8AKdjjL174fXR28Tu</code></td></tr>
</table>
<sub>⚠️ Send each coin only on the network shown — sending on the wrong network can lose the funds.</sub>
</details>
🐛 Found a bug or have feedback? Open a [Discussion](https://github.com/diegosouzapw/OmniRoute/discussions).
-**Star the repo** — it genuinely helps visibility
- 💖 **[GitHub Sponsors](https://github.com/sponsors/diegosouzapw)** — fund ongoing maintenance and new providers
- 🐛 **Report bugs and share feedback** in [Discussions](https://github.com/diegosouzapw/OmniRoute/discussions)
<br/>
@@ -509,12 +418,11 @@ Pix copia-e-cola:
- **🕵️ Transparent MITM decrypt (TPROXY)** — capture CLIs that ignore proxy env vars, with a per-SNI CA + trust-store installer. → [MITM/TPROXY](docs/security/MITM-TPROXY-DECRYPT.md)
- **💸 Cost telemetry everywhere** — `X-OmniRoute-*` cost/usage headers on every endpoint, cache-HIT savings header, per-key USD spend quotas. → [API Reference](docs/reference/API_REFERENCE.md)
- **🧠 Memory you control** — off by default, opt-in int8 vector quantization + typed decay, per-request `x-omniroute-no-memory`. → [Memory](docs/frameworks/MEMORY.md)
- **🛡️ Security** — prompt-injection guard on every LLM route (red-team suite), opt-in credential-masking guardrail (redacts leaked API keys/secrets in both directions), free DuckDuckGo last-resort web search, and an optional OIDC login gate for the dashboard (password login always stays available). → [Guardrails](docs/security/GUARDRAILS.md)
- **🛡️ Security** — prompt-injection guard on every LLM route (red-team suite) + free DuckDuckGo last-resort web search. → [Guardrails](docs/security/GUARDRAILS.md)
- **🖼️ New endpoints** — `/v1/ocr` (Mistral OCR) and `/v1/audio/translations` (Whisper-style) round out the media surface. → [API Reference](docs/reference/API_REFERENCE.md)
- **🎨 Image / video / audio generation** — one API for media: xAI Grok Imagine & Novita AI video, ComfyUI, Freepik, Adobe Firefly, Microsoft Designer, Google Imagen, Segmind, EdgeTTS. → [API Reference](docs/reference/API_REFERENCE.md)
- **🌍 Deployment & ops** — reverse-proxy `basePath`, browser-language auto-detect, per-key device tracking, root-less MITM trust, zh-TW localization. → [Environment](docs/reference/ENVIRONMENT.md)
- **🤝 More providers & agents** — Cursor Cloud Agent, Grok Build (xAI) with browser + OAuth login, Ollama first-class card, Claude Opus 5 & Sonnet 5, Kimi official partnership (Code/Web/Moonshot), Zed, Requesty, SenseNova, Yuanbao, Agnes AI… and a refreshed **291-provider catalog**. → [Providers](docs/reference/PROVIDER_REFERENCE.md)
- **📡 Routing transparency** — every response carries an `X-OmniRoute-Decision` header naming the strategy/provider/latency that served it, a new `cache-optimized` combo strategy + Auto-Combo `cacheAffinity` factor route repeat requests back to the connection holding the cached prefix, and a read-only `/v1/auto-combo/{channel}/candidates` endpoint exposes an `auto/*` channel's live candidate pool. → [Auto-Combo](docs/routing/AUTO-COMBO.md)
- **🤝 More providers & agents** — Cursor Cloud Agent, Grok Build (xAI), Ollama first-class card, Claude Sonnet 5, Zed, Requesty, SenseNova, Yuanbao, Agnes AI… and a refreshed **290-provider catalog**. → [Providers](docs/reference/PROVIDER_REFERENCE.md)
- **⚡ Local performance & infra** — one-click local Redis, Cloudflare Workers / Deno Deploy relay deployers, Bifrost & Mux as supervised embedded services. → [Embedded Services](docs/frameworks/EMBEDDED-SERVICES.md)
<br/>
@@ -532,7 +440,7 @@ Pix copia-e-cola:
<td align="center" width="76"><a href="https://github.com/openai/codex"><img src="./public/providers/codex.svg" width="40" alt="Codex CLI"/><br/><sub><b>Codex CLI</b></sub><br/><sub>                           </sub></a></td>
<td align="center" width="76"><picture><source media="(prefers-color-scheme:dark)" srcset="https://cdn.jsdelivr.net/npm/@lobehub/icons-static-png@1.91.0/dark/cline.png"/><img src="https://cdn.jsdelivr.net/npm/@lobehub/icons-static-svg@1.91.0/icons/cline.svg" width="40" alt="Cline"/></picture><br/><sub><b>Cline</b></sub><br/><sub>                           </sub></td>
<td align="center" width="76"><a href="https://github.com/Kilo-Org/kilocode"><img src="./public/providers/kilocode.svg" width="40" alt="Kilo Code"/><br/><sub><b>Kilo Code</b></sub><br/><sub>                           </sub></a></td>
<td align="center" width="76"><a href="https://github.com/Zoo-Code-Org/Zoo-Code"><img src="./public/providers/zoocode.png" width="40" alt="Zoo Code"/><br/><sub><b>Zoo Code</b></sub><br/><sub>                           </sub></a></td>
<td align="center" width="76"><img src="https://cdn.jsdelivr.net/npm/@lobehub/icons-static-png@1.91.0/dark/roocode.png#gh-dark-mode-only" width="40" alt="Roo Code"/><img src="https://cdn.jsdelivr.net/npm/@lobehub/icons-static-svg@1.91.0/icons/roocode.svg#gh-light-mode-only" width="40" alt="Roo Code"/><br/><sub><b>Roo Code</b></sub><br/><sub>                           </sub></td>
<td align="center" width="76"><img src="./public/providers/continue.svg" width="40" alt="Continue"/><br/><sub><b>Continue</b></sub><br/><sub>                           </sub></td>
</tr>
<tr>
@@ -574,11 +482,11 @@ Pix copia-e-cola:
<div align="center">
## 🌐 291 AI Providers — 90+ Free
## 🌐 290 AI Providers — 90+ Free
</div>
> The most complete catalog of any open-source router: **291 providers**, **90+ with a free tier**, **40+ free forever**.
> The most complete catalog of any open-source router: **290 providers**, **90+ with a free tier**, **40+ free forever**.
<div align="center">
@@ -890,12 +798,6 @@ docker run -d --name omniroute --restart unless-stopped --stop-timeout 40 \
-p 127.0.0.1:20128:20128 -v omniroute-data:/app/data diegosouzapw/omniroute:latest
```
> **Pre-release Docker channel:** `diegosouzapw/omniroute:next` and
> `diegosouzapw/omniroute:next-web` follow the current default `release/v*`
> branch. These mutable tags are intended only for testing unreleased fixes and
> are **not supported for production**. See
> [Docker Release Channels](docs/guides/DOCKER_RELEASE_CHANNELS.md).
**🛠️ From source**
```bash
@@ -931,22 +833,18 @@ devbox run npm run dev
**🦭 Podman**
```bash
# 1. Prepare the bind-mounted data directory
mkdir -p data
# 1. Build the image
podman build --target runner-base -t omniroute:base .
# 2. Linux + local rootless Podman only (never a remote Podman Machine client):
podman unshare chown 1000:1000 ./data
# 2. Fix data directory permissions for rootless Podman
mkdir -p data && podman unshare chown 1000:1000 ./data
# 3. Set the runtime hint, build the local Compose image, and start
# 3. Set runtime in .env, then run (see contrib/podman/ for Quadlet)
echo "CONTAINER_HOST=podman" >> .env
podman compose --profile base up -d --build
podman compose --profile base up -d
```
On macOS or Windows, Podman uses a remote Podman Machine: skip `podman unshare` and
follow the [topology-specific data directory guidance](contrib/podman/README.md#data-directory-permissions-by-topology).
📖 [Podman Guide](contrib/podman/README.md) — Compose builds, Podman Machine, and
Linux/systemd Quadlet setup.
📖 [Podman Guide](contrib/podman/README.md) — Quadlet setup, podman-compose, Quadlet.
**⚡ Faster / leaner install (skip the native build)**
@@ -1002,7 +900,7 @@ same process on one port, so there is no separate CLI-only package today.
<div align="center">
# 📧 Community & Help
# 📧 Support & Community
> Everything in one place — follow the maintainer, chat with the community, or open an issue.
@@ -1018,7 +916,7 @@ same process on one port, so there is no separate CLI-only package today.
| 📦 **Source code** | [github.com/diegosouzapw/OmniRoute](https://github.com/diegosouzapw/OmniRoute) |
| 🐛 **Report a bug** | [open an issue](https://github.com/diegosouzapw/OmniRoute/issues) — attach `npm run system-info` output |
| 🤝 **Contribute** | [CONTRIBUTING.md](CONTRIBUTING.md) · [Branching & Release Model](docs/ops/BRANCHING_MODEL.md) · pick a `good first issue` |
| 💚 **Support the project** | [Ways to support ↑](#-support-omniroute) · [GitHub Sponsors](https://github.com/sponsors/diegosouzapw) |
| **Support the project** | [Star the repo](https://github.com/diegosouzapw/OmniRoute) · [GitHub Sponsors](https://github.com/sponsors/diegosouzapw) |
</div>

View File

@@ -1,296 +0,0 @@
# Relatorio de pesquisa: repositorios de CLI integraveis com OmniRoute
> **Status final (2026-08-03):** este documento preserva o inventário inicial. A pesquisa foi concluída para `104/104` casos. Para resultados por projeto, use `04-tracker-integracoes-clis.md`; para o fechamento executivo e a estratégia de publicação, use `06-relatorio-final-104-clis-e-estrategia-prs.md`.
**Data da pesquisa:** 2026-08-01
**Escopo:** agentes de codigo de terminal, CLIs de LLM, runtimes de agentes e harnesses que possam consumir um endpoint HTTP compativel com OpenAI, Anthropic ou Gemini, ou que possam ser adaptados por provider/plugin/ACP/MITM.
**Fonte local principal:** `_tasks/hands-off/2026-08-01_release-v3.8.50_v3.8.50_sess-e1846bc2/handoff.md`
**Fontes externas principais:** GitHub Search/API, READMEs dos repositorios e a lista publica `bradAGI/awesome-cli-coding-agents` (atualizada em 2026-07-29).
## 1. Resumo executivo
O OmniRoute ja possui uma integracao funcional com o jcode e um catalogo local de ferramentas CLI. O proximo ganho de maior valor e transformar o OmniRoute em um endpoint reconhecido pelos principais agentes de terminal, priorizando configuracao nativa e PR upstream quando o projeto aceitar contribuicoes.
A pesquisa encontrou:
- **33 entradas de ferramentas no registro local `CLI_TOOLS`**, contando o registro extraido de Grok Build em `src/shared/constants/cliToolsGrokBuild.ts`, incluindo Claude Code, Codex CLI, Cline, Kilo, Continue, OpenCode, Aider, jcode, Smelt, Pi, Crush, Goose, Open Interpreter, OpenClaw, Hermes Agent, Letta CLI e outros.
- **Mais de 90 projetos publicos** no inventario externo consultado, entre agentes de codigo, CLIs generalistas, forks, runtimes e orquestradores.
- **Candidatos com evidencia forte de endpoint customizavel:** Gemini CLI, Claw Code, Plandex, MiMo Code, Trae Agent, Kimi CLI, Every Code, Open Codex, VT Code, OpenHands CLI, gptme, Nanocoder, RA.Aid, CoreCoder, Grok CLI, Gitlawb Zero, DeepSeek Reasonix, KlaatCode, CodeMini, DvalinCode, Coro Code, Mini-Kode, Late CLI, Agentty, Aizen, Minacode, YottaCode, aichat, ShellGPT, Mistral Vibe, OpenSquilla, Kode CLI e outros.
- **Candidatos que exigem pesquisa confirmatoria:** projetos com README generico, configuracao recente, repositorio ambiguo, binario fechado ou sem evidencia textual suficiente de `base_url`/provider.
- **Candidatos que podem ser integrados por outros caminhos:** ACP, MCP, wrapper/launcher, provider adapter, proxy MITM ou apenas documentacao; eles nao devem ser classificados automaticamente como OpenAI-compatible.
Conclusao: devemos pesquisar e tentar todos os candidatos tecnicamente viaveis, mas separar claramente `suporte no catalogo OmniRoute`, `configuracao generica`, `adaptacao upstream publicada` e `PR/issue aceita`. O tracker acompanha essas dimensoes separadamente.
## 2. Metodo e limites
### 2.1 Como a busca foi feita
1. Leitura integral do handoff do caso jcode para capturar o padrao de integracao, validacao, publicacao e as restricoes de worktree.
2. Inspecao do catalogo local em `src/shared/constants/cliTools.ts`, da documentacao de CLI e do fluxo de setup em `docs/guides/CLI-INTEGRATIONS.md`.
3. Consulta do GitHub Search/API para resolver o repositorio canonico de cada nome, evitando homonimos.
4. Leitura de README/raw quando disponivel, procurando sinais como `base_url`, `baseURL`, `OPENAI_BASE_URL`, `OPENAI_API_BASE`, `LLM_BASE_URL`, `provider`, `gateway`, `model provider`, `Anthropic` e `Gemini`.
5. Consulta da lista `https://github.com/bradAGI/awesome-cli-coding-agents`, que serve como descoberta ampla, nao como prova de compatibilidade.
6. Classificacao por adocao, manutencao, licenca, evidencia de endpoint, maturidade, potencial de PR e utilidade para o ecossistema OmniRoute.
### 2.2 O que ainda nao foi afirmado
- Nao foi feita implementacao ou abertura de PR/issue para os candidatos abaixo; o unico caso publicado nesta sessao anterior e o jcode.
- A presenca da palavra `provider` no README nao prova que uma URL arbitraria funciona em runtime.
- Estrelas e datas sao snapshots aproximados obtidos em 2026-08-01 e podem mudar.
- Repositorios fechados ou com EULA entram no inventario para avaliacao de configuracao, mas nao implicam possibilidade de fork ou PR.
- Cada task de integracao precisa repetir a pesquisa no upstream antes de editar codigo.
## 3. Baseline do OmniRoute
### 3.1 Superficie que o OmniRoute oferece
- Endpoint OpenAI em `/v1`.
- Superficie Anthropic na raiz, usada por clientes que esperam `/v1/messages` a partir do `ANTHROPIC_BASE_URL`.
- Superficie Gemini em `/v1beta`.
- Catalogo de modelos consultavel pelos comandos de setup quando o cliente suporta descoberta.
- Chave via `OMNIROUTE_API_KEY` ou chave selecionada no dashboard.
- Traducao entre formatos, streaming SSE, tool calling, fallback, combos, custos e politicas de autenticacao.
- Modos de consumo: configuracao de ambiente, arquivo nativo do cliente, provider customizado, ACP/MCP e MITM.
### 3.2 Catalogo local ja registrado
Fonte: `src/shared/constants/cliTools.ts` e `src/shared/constants/cliToolsGrokBuild.ts`.
**Codigo/CLI:** Claude Code, OpenAI Codex CLI, Factory Droid, OpenClaw, Cursor, Cline, Kilo Code, Continue, Antigravity, GitHub Copilot CLI, OpenCode, Kiro, Qwen Code, Aider, ForgeCode, Cursor Agent CLI, Roo Code, jcode, DeepSeek TUI, CodeWhale, Smelt, Pi, Crush.
**Agentes:** Hermes, Hermes Agent, Goose, Open Interpreter, Oh My Pi, Letta CLI, Warp AI, Agent Deck.
Os documentos do catalogo tambem mantem um backlog MITM para ferramentas sem base URL, como Windsurf, Amp, Amazon Q/Kiro CLI e Cowork. Esses casos devem permanecer separados de uma integracao direta.
### 3.3 Caso jcode (referencia validada)
- Upstream: `https://github.com/1jehuang/jcode`
- Mecanismo: perfil OpenAI-compatible dirigido por metadados; nao foi criado um plugin de runtime.
- Branch: `feat/omniroute-provider`
- Commit: `ee4f904e6`
- PR no fork: `https://github.com/diegosouzapw/jcode/pull/1`
- Issue no upstream: `https://github.com/1jehuang/jcode/issues/704`
- Diff: 6 arquivos, `+56/-3`.
- Validacao: `cargo check --workspace` limpo; 205 testes passaram e uma falha foi preexistente/ambiental.
- Estado: aguardando mantenedor; o upstream nao aceita PR de forks externos, por isso a issue e o artefato oficial.
- Pendencia prometida: adicionar no README do OmniRoute a secao "Tools & repositories that work with OmniRoute".
Licao: o trabalho deve comecar descobrindo o mecanismo real de providers do upstream. Nem todos os clientes precisam de mudanca no OmniRoute; alguns precisam somente de um perfil local, e outros exigirao um adaptador especifico.
## 4. Candidatos prioritarios com evidencia concreta
As evidencias abaixo sao sinais de README/configuracao observados na pesquisa inicial. A task individual deve abrir o arquivo exato, confirmar a versao atual e executar um smoke test.
| Projeto | Repositorio | Evidencia inicial | Rota provavel |
|---|---|---|---|
| Gemini CLI | `google-gemini/gemini-cli` | `GOOGLE_GEMINI_BASE_URL` | configuracao direta; possivel PR/documentacao |
| Claw Code | `ultraworkers/claw-code` | `OPENAI_BASE_URL`, provider compativel | configuracao direta ou provider |
| Plandex | `plandex-ai/plandex` | providers customizados com `baseUrl` | provider/preset |
| MiMo Code | `XiaomiMiMo/MiMo-Code` | `@ai-sdk/openai-compatible` e `baseURL` | provider customizado |
| Trae Agent | `bytedance/trae-agent` | `model_providers` e `base_url` | provider/config |
| Kimi CLI | `MoonshotAI/kimi-cli` | modos `openai_legacy`, `openai_responses`, `anthropic` e `base_url` | provider nativo/config |
| Every Code | `just-every/code` | fork Codex com providers OpenAI/Claude/Gemini | perfil/provider |
| Open Codex | `ymichael/open-codex` | multi-provider e OpenAI-compatible | fork/provider |
| VT Code | `vinhnx/vtcode` | `custom_providers[].base_url`, failover | provider customizado |
| OpenHands CLI | `OpenHands/OpenHands-CLI` | `LLM_BASE_URL` | configuracao direta |
| gptme | `gptme/gptme` | `OPENAI_BASE_URL` e providers | configuracao direta |
| Nanocoder | `Nano-Collective/nanocoder` | qualquer API OpenAI-compatible | configuracao direta |
| RA.Aid | `ai-christianson/RA.Aid` | `OPENAI_API_BASE` | configuracao direta |
| CoreCoder | `he-yufeng/CoreCoder` | `OPENAI_BASE_URL` | configuracao direta |
| Grok CLI | `superagent-ai/grok-cli` | `GROK_BASE_URL`/`baseURL` | configuracao direta |
| Gitlawb Zero | `Gitlawb/zero` | provider `custom-openai-compatible`, `--base-url` | provider/flag |
| DeepSeek Reasonix | `esengine/DeepSeek-Reasonix` | provider compativel e endpoint | confirmar configuracao |
| KlaatCode | `KlaatAI/klaatcode` | `customModels` OpenAI-compatible | configuracao JSON |
| CodeMini CLI | `havingautism/Codemini-CLI` | `gateway.base_url` | gateway/config |
| Zot | `patriceckhart/zot` | `--base-url` e provider custom em `models.json` | flag/config |
| Pool | `poolsideai/pool` | `POOLSIDE_STANDALONE_BASE_URL`; licenca proprietaria | configuracao, sem PR assumido |
| Octomind | `Muvon/octomind` | `<PROVIDER>_API_URL`/`LOCAL_API_URL` | provider/env |
| Coro Code | `Blushyes/coro-code` | `OPENAI_BASE_URL` | configuracao direta |
| Mini-Kode | `minmaxflow/mini-kode` | `MINIKODE_BASE_URL` | configuracao direta |
| Late CLI | `mlhher/late-cli` | `OPENAI_BASE_URL`/`api-url` | env/flag |
| Agentty | `1ay1/agentty` | modelo agnostico e endpoints compativeis | confirmar arquivo de config |
| Aizen | `aizen-stack/aizen` | CLI Rust OpenAI-compatible; `AIZEN_BASE_URL` | configuracao direta |
| Clif-Code | `DLhugly/Clif-Code` | OpenRouter/OpenAI/Anthropic/Ollama | provider/config |
| Minacode | `hit9/minacode` | provider e compatibilidade no README | confirmar URL |
| YottaCode | `yottadynamics/yottacode` | modelo escolhido, gateway/provider | confirmar config |
| aichat | `sigoden/aichat` | providers OpenAI/Claude/Gemini e compatibilidade | `models.yaml`/provider |
| ShellGPT | `TheR1D/shell_gpt` | `API_BASE_URL` | env/config |
| Mistral Vibe | `mistralai/mistral-vibe` | `base_url`, API base e provider | config/env |
| OpenSquilla | `opensquilla/opensquilla` | 20+ providers e gateway | provider/config |
| Kode CLI | `shareAI-lab/Kode-cli` | provider, endpoint e Anthropic/OpenAI/Gemini | config |
| Crush | `charmbracelet/crush` | `base_url`, provider compativel | ja catalogado no OmniRoute; validar upstream |
| Hermes Agent | `NousResearch/hermes-agent` | endpoint/gateway e 300+ modelos | ja catalogado; validar modo de endpoint |
| OpenClaw | `openclaw/openclaw` | providers, gateway e endpoints | ja catalogado; validar configuracao atual |
## 5. Inventario amplo localizado
### 5.1 Agentes de terminal e coding CLIs
Os projetos desta tabela foram encontrados na lista curada ou no GitHub Search. `Pesquisa` indica o proximo gate; nao significa que a integracao ja esta pronta.
| Projeto | Repositorio | Licenca/sinal publico | Situacao inicial |
|---|---|---|---|
| OpenCode | `anomalyco/opencode` | multi-provider, 75+ providers | ja suportado; acompanhar provider/plugin |
| Codex CLI | `openai/codex` | Apache-2.0, provider configuravel | ja suportado |
| OpenHands principal | `All-Hands-AI/OpenHands` | OSS, CLI e web | pesquisar CLI e `LLM_BASE_URL` |
| Pi | `badlogic/pi-mono` | harness multi-provider | ja suportado; confirmar repo atual |
| Open Interpreter | `OpenInterpreter/open-interpreter` | Apache-2.0, `--api_base` | ja suportado |
| Cline | `cline/cline` | Apache-2.0, base URL/gateway | ja suportado |
| Goose | `aaif-goose/goose` | Apache-2.0, providers | ja suportado |
| Aider | `Aider-AI/aider` | Apache-2.0, Anthropic/OpenAI | ja suportado |
| Continue | `continuedev/continue` | Apache-2.0, multi-model | ja suportado |
| Deep Agents Code | `langchain-ai/deepagents` | MIT, tool-calling LLM | pesquisar pacote `deepagents-code` |
| Crush | `charmbracelet/crush` | provider/base URL | ja suportado |
| Kilo Code | `Kilo-Org/kilocode` | MIT, providers | ja suportado |
| Qwen Code | `QwenLM/qwen-code` | Apache-2.0, providers | ja suportado |
| Roo Code | `RooCodeInc/Roo-Code` | Apache-2.0 | ja catalogado; validar CLI |
| Grok Build | `xai-org/grok-build` | Apache-2.0, provider | ja suportado |
| Oh My Pi | `can1357/oh-my-pi` | provider custom em YAML | ja suportado |
| SWE-agent | `SWE-agent/SWE-agent` | MIT | pesquisar backend e base URL |
| Smol Developer | `smol-ai/developer` | embeddable agent | adapter/SDK, nao necessariamente CLI |
| Claude Engineer | `Doriandarko/claude-engineer` | CLI Claude | pesquisar provider |
| Claurst | `Kuberwastaken/claurst` | GPL-3.0, provider | confirmar endpoint e politica de fork |
| Free Code | `paoloanzn/free-code` | fork de Claude Code | pesquisar licenca e endpoint |
| Codebuff | `CodebuffAI/codebuff` | multi-agent CLI | pesquisar provider |
| ForgeCode | `antinomyhq/forge` | 300+ modelos | ja suportado |
| OpenSquilla | `opensquilla/opensquilla` | Apache-2.0, gateway | candidato forte |
| Kode CLI | `shareAI-lab/Kode-cli` | Apache-2.0, endpoint | candidato forte |
| Devon | `entropy-research/Devon` | pair programmer TUI | pesquisar backend |
| AutoCodeRover | `AutoCodeRoverSG/auto-code-rover` | agente de issues | pesquisar configuracao de modelos |
| Letta Code | `letta-ai/letta-code` | Apache-2.0, model-agnostic | pesquisar API base |
| CodeMachine CLI | `moazbuilds/CodeMachine-CLI` | multi-agent local | pesquisar provider |
| Codel | `semanser/codel` | AGPL-3.0, Docker/web UI | confirmar servidor OpenAI e restricoes AGPL |
| Agentless | `OpenAutoCoder/Agentless` | workflow sem loop persistente | pesquisar entrada de modelo |
| Amazon Q Developer CLI | `aws/amazon-q-developer-cli` | Apache-2.0 | provavelmente auth/ecossistema AWS; pesquisar |
| Neovate Code | `neovateai/neovate-code` | MIT, plugin/multi-provider | candidato forte |
| Groq Code CLI | `build-with-groq/groq-code-cli` | multi-model | pesquisar endpoint |
| Dexto | `truffle-ai/dexto` | CLI/web/API, subagentes | pesquisar provider |
| claw-code-agent | `HarnessLab/claw-code-agent` | Python, sem dependencias | confirmar endpoint |
| g3 | `dhanji/g3` | Rust, provider abstraction | confirmar licenca e URL |
| Coro Code | `Blushyes/coro-code` | base URL/OpenAI | candidato |
| Mini-Kode | `minmaxflow/mini-kode` | MIT, referencia educacional | candidato |
| zot | `patriceckhart/zot` | MIT, TUI/JSON/RPC | candidato |
| agentty | `1ay1/agentty` | MIT, ACP e multi-provider | candidato |
| nori-cli | `tilework-tech/nori-cli` | multi-provider sobre Codex | pesquisar base URL |
| cursor-agent clone | `civai-technologies/cursor-agent` | OpenAI/Claude/Ollama | pesquisar maturidade e licenca |
| DvalinCode | `arthurpanhku/dvalincode` | MIT, OpenAI-compatible | candidato |
| OpenHarness | `zhijiewong/openharness` | Apache-2.0, any LLM | candidato |
| Octomind | `Muvon/octomind` | Apache-2.0, 13+ providers | candidato |
| Codex Infinity | `lee101/codex-infinity` | fork Codex | pesquisar endpoint |
| San | `genai-io/san` | Apache-2.0, provider-neutral | pesquisar endpoint |
| Waveloom | `Menfre01/waveloom` | Apache-2.0, DeepSeek-focused | pesquisar provider |
| picocode | `jondot/picocode` | Rust, multi-LLM | pesquisar provider |
| QQCode | `qnguyen3/qqcode` | Rust, skills | pesquisar provider |
| Keen Code | `mochow13/keen-code` | MIT, 9+ providers | pesquisar provider |
| Smelt | `leonardcser/smelt` | MIT, OpenAI-compatible | ja suportado |
| Grinta | `josephsenior/Grinta-Coding-Agent` | MIT, Python | pesquisar provider |
| Zap | `zap-coding-agent/zap-coding-agent` | MIT, MCP, local/OpenAI | pesquisar endpoint |
| Binharic | `CogitatorTech/binharic-cli` | multi-provider | pesquisar endpoint |
| Darce | `AmerSarhan/darce-cli` | MIT, multi-model | pesquisar endpoint |
| CLAII | `agencyswarm/CLAII` | multi-agent/MCP | pesquisar endpoint |
### 5.2 Agentes generalistas e ecossistema OpenClaw
Estes podem consumir OmniRoute como backend, mas a task deve confirmar se a interface de configuracao e realmente uma CLI de codigo ou apenas um gateway de agente.
| Projeto | Repositorio | Possivel caminho |
|---|---|---|
| OpenClaw | `openclaw/openclaw` | provider/gateway; ja catalogado |
| nanobot | `HKUDS/nanobot` | provider OpenAI-compatible |
| ZeroClaw | `zeroclaw-labs/zeroclaw` | trait de provider |
| NanoClaw | `gavrielc/nanoclaw` | Anthropic SDK; pesquisar base |
| PicoClaw | `sipeed/picoclaw` | provider/config |
| IronClaw | `nearai/ironclaw` | provider Rust |
| NullClaw | `nullclaw/nullclaw` | 23+ providers |
| Clawith | `dataelement/Clawith` | gateway/teams |
| claw0 | `shareAI-lab/claw0` | tutorial/runtime; pesquisa de viabilidade |
| Moltis | `moltis-org/moltis` | provider Rust |
| GitClaw | `open-gitagent/gitclaw` | agente Git-native; pesquisar |
| LionClaw | `moshthepitt/lionclaw` | CLI local; pesquisar |
| Aizen | `aizen-stack/aizen` | OpenAI-compatible |
| aichat | `sigoden/aichat` | provider/model YAML |
| ShellGPT | `TheR1D/shell_gpt` | `API_BASE_URL` |
| gptme | `gptme/gptme` | `OPENAI_BASE_URL` |
### 5.3 Orquestradores, wrappers e ferramentas adjacentes
Nao sao todos alvos de um provider OmniRoute. Devem ser avaliados para launcher, ACP, MCP, observabilidade ou configuracao de seus agentes filhos.
| Projeto | Repositorio | Tipo de integracao a investigar |
|---|---|---|
| Agent Deck | `asheshgoplani/agent-deck` | config dos CLIs filhos; ja catalogado |
| VibePod | `VibePod/vibepod-cli` | wrapper Docker e metricas |
| zeroshot | `the-open-engine/zeroshot` | launcher/worktrees |
| Fractal | `plasma-ai/fractal` | orquestrador de CLIs |
| Bernstein | `chernistry/bernstein` | orquestrador/verificador |
| Traycer | `traycerai/traycer` | CLI custom e agentes filhos |
| h5i | `h5i-dev/h5i` | execucao paralela |
| OMK | `dmae97/open-multi-agent-kit` | control plane/provider-neutral |
| kodo | `ikamensh/kodo` | orquestrador |
| ORCH | `oxgeneral/ORCH` | fila de tarefas |
| LoopTroop | `LoopTroop-ai/LoopTroop` | orchestration sobre OpenCode |
| Galley | `shinpr/galley` | worktree/PR handoff |
| Relay | `jcast90/relay` | MCP/orquestracao |
| sage | `youwangd/SageCLI` | runtime-agnostic |
| 5dive | `5dive-ai/5dive` | agentes em servidor |
| agx | `ramarlina/agx` | checkpoints e agentes |
| claude-code-router | `musistudio/claude-code-router` | proxy/roteamento; possivel upstream consumidor |
| cc-router | `finch-xu/cc-router` | proxy Anthropic multi-provider |
| OneCLI | `onecli/onecli` | broker de credenciais, nao agente |
| agent-browser | `vercel-labs/agent-browser` | ferramenta MCP/plugin |
| OpenWork | `different-ai/openwork` | desktop sobre OpenCode |
| Mistral Vibe | `mistralai/mistral-vibe` | provider/base URL |
| Junie CLI | `junie.jetbrains.com` | fechado; configuracao BYOK a confirmar |
| Pool | `poolsideai/pool` | binario/EULA; sem PR presumido |
## 6. Evidencias tecnicas e mapeamento para OmniRoute
### 6.1 Padroes de endpoint encontrados
| Padrao observado | Exemplos | Acao OmniRoute |
|---|---|---|
| `OPENAI_BASE_URL`/`OPENAI_API_BASE` | Claw Code, RA.Aid, CoreCoder, Coro Code | fornecer root ou `/v1` conforme o cliente; testar append de path |
| `base_url`/`baseURL` em provider | Plandex, MiMo Code, Trae Agent, VT Code, KlaatCode | gerar bloco de provider e modelo |
| `LLM_BASE_URL` | OpenHands CLI | configurar surface OpenAI e validar streaming/tool calling |
| `GOOGLE_GEMINI_BASE_URL` | Gemini CLI | usar superficie `/v1beta`/Gemini; confirmar formato esperado |
| `GROK_BASE_URL` | Grok CLI | decidir se o cliente fala xAI ou OpenAI; testar traducoes |
| `--base-url` | Gitlawb Zero, Zot, jcode | launcher ou perfil persistido |
| `API_BASE_URL` | ShellGPT | config/env direta |
| `<PROVIDER>_API_URL`/gateway | Octomind, Pool, OpenSquilla | provider selecionavel; testar cada preset |
| ACP/MCP sem URL direta | Agentty, Kimi CLI, Goose, OpenCode | avaliar se OmniRoute deve ser provider ou backend ACP |
| endpoint nao customizavel | Cursor desktop, Antigravity, Kiro, Windsurf, Amp | somente MITM/guide; nao prometer integracao direta |
### 6.2 Superficies e riscos de protocolo
- **`/v1` duplicado:** alguns clientes recebem a raiz e acrescentam `/v1/chat/completions`; outros exigem a URL final com `/v1`. Cada task deve registrar o resultado real.
- **Chat Completions vs Responses:** forks do Codex e clientes modernos podem usar Responses; testar ambas quando o cliente permitir.
- **Anthropic:** clientes que mandam `/v1/messages` esperam `ANTHROPIC_BASE_URL` sem `/v1` no valor. A traducao Anthropic do OmniRoute deve ser validada com streaming e tool use.
- **Gemini:** Gemini CLI pode esperar uma base Gemini nativa, nao somente OpenAI-compatible; validar `generateContent`, streaming e headers.
- **Tool calling:** o agente pode exigir nomes/ids de ferramenta estaveis, JSON estrito, `tool_choice` ou blocos de pensamento especificos.
- **Descoberta de modelos:** `/v1/models` pode ser obrigatorio, opcional ou inexistente. O setup precisa aceitar `--model` fixo quando a descoberta nao for suportada.
- **Autenticacao:** alguns projetos leem somente env, outros gravam tokens em arquivo/keyring e alguns usam OAuth proprietario. Nunca reutilizar credenciais de um upstream sem verificar escopo.
- **Streaming e retry:** SSE, timeouts, abort signals e re-tentativas podem divergir do cliente. Validar uma chamada longa e uma falha de provider.
- **Licenca:** GPL/AGPL, EULA e repositorios sem SPDX exigem decisao de distribuicao antes de enviar patch.
## 7. Riscos de pesquisa e integracao
1. **Homonomimos e clones:** usar sempre URL canonica, organizacao, release e README do repositorio correto.
2. **Repositorios que mudam rapidamente:** congelar commit/versao no relatorio da task e repetir a consulta no dia da implementacao.
3. **README divergente do codigo:** procurar schema, parser de config, testes e comando de execucao; README sozinho e evidencia Tier 1.
4. **Clientes fechados:** registrar como `needs-mitm` ou `config-only`, nunca como PR upstream.
5. **Forks com historia de origem controversa:** avaliar politica, licenca e aceite de contribuicoes antes de reproduzir componentes.
6. **Segredos no ambiente:** limpar `OMNIROUTE_API_KEY` e chaves de teste quando a suite assume ambiente sem credencial, como ocorreu no jcode.
7. **Mudancas no checkout:** usar worktree em `.claude/worktrees/` por projeto; nao editar o checkout compartilhado do OmniRoute nem usar `git stash`.
## 8. Recomendacao
Executar primeiro os lotes P0/P1 do documento de prioridade. Cada lote pode ter ate tres subagentes, um repositorio por worktree. O agente principal deve revisar a pesquisa, o smoke test e a licenca antes de permitir implementacao. O resultado de cada caso deve atualizar o tracker com commit, PR/issue, validacao e status upstream, sem preencher campos externos por suposicao.
## 9. Referencias
- OmniRoute CLI catalogo: `src/shared/constants/cliTools.ts`
- OmniRoute CLI reference: `docs/reference/CLI-TOOLS.md`
- OmniRoute setup guide: `docs/guides/CLI-INTEGRATIONS.md`
- Handoff jcode: `_tasks/hands-off/2026-08-01_release-v3.8.50_v3.8.50_sess-e1846bc2/handoff.md`
- Inventario curado: `https://github.com/bradAGI/awesome-cli-coding-agents`
- GitHub Search API: `https://api.github.com/search/repositories`

View File

@@ -1,167 +0,0 @@
# Prioridade de integracoes de CLIs com OmniRoute
> **Status final (2026-08-03):** esta é a priorização inicial que orientou a execução. Todos os `104/104` casos já foram pesquisados. A classificação final está no tracker `04`; a estratégia revisada de contribuição está no relatório `06`.
**Snapshot:** 2026-08-01
**Objetivo:** ordenar do melhor para o pior todos os projetos tecnicamente candidatos a consumir OmniRoute, sem remover projetos pequenos. A ordem e uma fila de pesquisa/execucao; ela nao e promessa de que todo upstream aceitara um PR.
## Como ler a prioridade
- **P0:** ja esta no catalogo OmniRoute ou tem evidencia muito forte de endpoint customizavel; executar/consolidar primeiro.
- **P1:** forte candidato novo, com provider/base URL evidente e bom retorno para o ecossistema.
- **P2:** tecnicamente promissor, mas requer confirmacao de protocolo, config, maturidade ou licenca.
- **P3:** possivel via ACP/MCP/wrapper/launcher, ou com menor adocao; pesquisar depois dos P0-P2.
- **P4:** cliente fechado, EULA, MITM ou pesquisa exploratoria; manter no inventario, mas nao bloquear os demais.
Os fatores usados foram: evidencia de endpoint arbitrario, adocao/atividade, facilidade de teste, compatibilidade OpenAI/Anthropic/Gemini, maturidade, licenca, chance de PR upstream, valor para usuarios OmniRoute e risco de protocolo.
## A. Catalogo OmniRoute ja existente
Estas entradas ja aparecem no registro local. A prioridade aqui significa consolidar documentacao, smoke tests, detector/configurador e eventual upstream nominal; nao significa recriar uma integracao que ja existe.
| Ordem | Projeto | Repositorio/documentacao | Estado local | Proximo foco |
|---:|---|---|---|---|
| A1 | Claude Code | `anthropics/claude-code` | catalogado; Anthropic base URL | manter compatibilidade Anthropic, streaming e tools |
| A2 | Codex CLI | `openai/codex` | catalogado; OpenAI-compatible | Responses, profiles e `/v1` |
| A3 | OpenCode | `anomalyco/opencode` | catalogado; provider | provider nativo/plugin e model discovery |
| A4 | Cline | `cline/cline` | catalogado; base URL | validar CLI/extension e append de `/v1` |
| A5 | Goose | `aaif-goose/goose` | catalogado; `OPENAI_HOST` | validar schema atual e ACP |
| A6 | Aider | `Aider-AI/aider` | catalogado; `OPENAI_API_BASE` | LiteLLM path, tools e custo |
| A7 | Continue | `continuedev/continue` | catalogado; provider OpenAI | CLI e config YAML atual |
| A8 | Kilo Code | `Kilo-Org/kilocode` | catalogado; custom URL | CLI, extension e auth |
| A9 | Roo Code | `RooCodeInc/Roo-Code` | catalogado; custom URL | CLI/headless e provider |
| A10 | Qwen Code | `QwenLM/qwen-code` | catalogado; `modelProviders` | V4 schema, Responses e env |
| A11 | Open Interpreter | `OpenInterpreter/open-interpreter` | catalogado; `--api_base` | streaming e tool execution |
| A12 | OpenClaw | `openclaw/openclaw` | catalogado; gateway/provider | config atual e segurança |
| A13 | Hermes Agent | `NousResearch/hermes-agent` | catalogado; provider/gateway | endpoint custom e modelos |
| A14 | Hermes | `NousResearch/hermes-agent` | catalogado/dual entry | distinguir CLI e agente |
| A15 | Oh My Pi | `can1357/oh-my-pi` | catalogado; YAML provider | auto-discovery e tool calling |
| A16 | Pi | `badlogic/pi-mono` | catalogado; provider | confirmar repositorio/CLI atual |
| A17 | Crush | `charmbracelet/crush` | catalogado; `base_url` | config TOML/JSON atual |
| A18 | Smelt | `leonardcser/smelt` | catalogado; OpenAI-compatible | headless e subagents |
| A19 | ForgeCode | `antinomyhq/forge` | catalogado; multi-provider | base URL e custom agents |
| A20 | jcode | `1jehuang/jcode` | integrado e proposto upstream | aguardar issue #704; manter README OmniRoute |
| A21 | DeepSeek TUI | `hunterbown/deepseek-tui` | catalogado legado | confirmar sucessor CodeWhale |
| A22 | CodeWhale | `Hmbown/CodeWhale` | catalogado | config primaria e legado |
| A23 | Grok Build | `xai-org/grok-build` | catalogado; `~/.grok/config.toml` | provider OmniRoute e modelos |
| A24 | Cursor Agent CLI | `cursor.com/cli` | catalogado parcial | confirmar limites de endpoint |
| A25 | Factory Droid | `Factory-AI/factory` | catalogado parcial | BYOK e endpoint suportado |
| A26 | GitHub Copilot CLI | `github/copilot-cli` | catalogado | provider base URL atual |
| A27 | Letta CLI | `letta-ai/letta-code` | catalogado | config pi-ai/local mode |
| A28 | Warp AI | `warpdotdev/Warp` | catalogado parcial | somente BYOK/desktop |
| A29 | Agent Deck | `asheshgoplani/agent-deck` | catalogado | agentes filhos e ACP |
| A30 | Antigravity | produto Google | MITM backlog | nao tratar como endpoint direto |
| A31 | Kiro AI | produto AWS | MITM backlog | auth/SSO e MITM |
| A32 | Cursor desktop | produto Anysphere | cloud/MITM | manter separado do Cursor CLI |
## B. Novos candidatos em ordem de execucao
| Ordem | Prioridade | Projeto | Repositorio | Evidencia inicial | Rota esperada |
|---:|:---:|---|---|---|---|
| 1 | P0 | Gemini CLI | `google-gemini/gemini-cli` | `GOOGLE_GEMINI_BASE_URL` | config direta/Gemini |
| 2 | P0 | Claw Code | `ultraworkers/claw-code` | `OPENAI_BASE_URL`, provider | OpenAI-compatible |
| 3 | P0 | Plandex | `plandex-ai/plandex` | provider com `baseUrl` | preset/provider |
| 4 | P0 | MiMo Code | `XiaomiMiMo/MiMo-Code` | `@ai-sdk/openai-compatible`, `baseURL` | provider |
| 5 | P0 | Trae Agent | `bytedance/trae-agent` | `model_providers`, `base_url` | provider/config |
| 6 | P0 | Kimi CLI | `MoonshotAI/kimi-cli` | OpenAI legacy/Responses/Anthropic, `base_url` | provider nativo |
| 7 | P0 | Every Code | `just-every/code` | fork Codex, OpenAI/Claude/Gemini | profile/provider |
| 8 | P0 | Open Codex | `ymichael/open-codex` | OpenAI/Gemini/OpenRouter/Ollama | profile/provider |
| 9 | P0 | VT Code | `vinhnx/vtcode` | `custom_providers[].base_url` | provider/failover |
| 10 | P0 | OpenHands CLI | `OpenHands/OpenHands-CLI` | `LLM_BASE_URL` | config direta |
| 11 | P0 | gptme | `gptme/gptme` | `OPENAI_BASE_URL` | config direta |
| 12 | P0 | Nanocoder | `Nano-Collective/nanocoder` | qualquer OpenAI-compatible | config direta |
| 13 | P0 | RA.Aid | `ai-christianson/RA.Aid` | `OPENAI_API_BASE` | config direta |
| 14 | P0 | CoreCoder | `he-yufeng/CoreCoder` | `OPENAI_BASE_URL` | config direta |
| 15 | P1 | Grok CLI | `superagent-ai/grok-cli` | `GROK_BASE_URL`/`baseURL` | config direta |
| 16 | P1 | Gitlawb Zero | `Gitlawb/zero` | `custom-openai-compatible`, `--base-url` | provider/flag |
| 17 | P1 | DeepSeek Reasonix | `esengine/DeepSeek-Reasonix` | endpoint/provider compativel | provider |
| 18 | P1 | KlaatCode | `KlaatAI/klaatcode` | `customModels` OpenAI-compatible | config |
| 19 | P1 | CodeMini CLI | `havingautism/Codemini-CLI` | `gateway.base_url` | gateway |
| 20 | P1 | Zot | `patriceckhart/zot` | `--base-url`, `models.json` | flag/config |
| 21 | P1 | Octomind | `Muvon/octomind` | provider URL envs | provider/env |
| 22 | P1 | DvalinCode | `arthurpanhku/dvalincode` | qualquer OpenAI-compatible | config direta |
| 23 | P1 | Coro Code | `Blushyes/coro-code` | `OPENAI_BASE_URL` | env |
| 24 | P1 | Mini-Kode | `minmaxflow/mini-kode` | `MINIKODE_BASE_URL` | env |
| 25 | P1 | Late CLI | `mlhher/late-cli` | `OPENAI_BASE_URL`, `api-url` | env/flag |
| 26 | P1 | Agentty | `1ay1/agentty` | provider-agnostic, ACP | config/ACP |
| 27 | P1 | Aizen | `aizen-stack/aizen` | Rust OpenAI-compatible, `AIZEN_BASE_URL` | config |
| 28 | P1 | Clif-Code | `DLhugly/Clif-Code` | OpenAI/Anthropic/Ollama | provider |
| 29 | P1 | Minacode | `hit9/minacode` | provider/compatibilidade | confirmar URL |
| 30 | P1 | YottaCode | `yottadynamics/yottacode` | modelo escolhido/gateway | provider |
| 31 | P1 | aichat | `sigoden/aichat` | OpenAI/Claude/Gemini | models YAML |
| 32 | P1 | ShellGPT | `TheR1D/shell_gpt` | `API_BASE_URL` | env |
| 33 | P1 | Mistral Vibe | `mistralai/mistral-vibe` | `base_url`, API base | config |
| 34 | P1 | OpenSquilla | `opensquilla/opensquilla` | gateway, 20+ providers | provider |
| 35 | P1 | Kode CLI | `shareAI-lab/Kode-cli` | endpoint/Anthropic/OpenAI/Gemini | config |
| 36 | P1 | Neovate Code | `neovateai/neovate-code` | plugin/multi-provider | plugin/provider |
| 37 | P1 | Deep Agents Code | `langchain-ai/deepagents` | qualquer tool-calling LLM | provider SDK |
| 38 | P1 | Kode fork/variants | `shareAI-lab/Kode-cli` | multi-provider | confirmar upstream |
| 39 | P1 | OpenHands principal | `All-Hands-AI/OpenHands` | CLI/web; pesquisar LLM base | config/CLI |
| 40 | P1 | SWE-agent | `SWE-agent/SWE-agent` | agente de issues | backend/provider |
| 41 | P1 | AutoCodeRover | `AutoCodeRoverSG/auto-code-rover` | agente de patches | backend/provider |
| 42 | P2 | Claurst | `Kuberwastaken/claurst` | provider/Anthropic | config; licenca GPL |
| 43 | P2 | Codebuff | `CodebuffAI/codebuff` | multi-agent CLI | provider |
| 44 | P2 | Devon | `entropy-research/Devon` | TUI pair programmer | backend |
| 45 | P2 | Letta Code | `letta-ai/letta-code` | model-agnostic | provider |
| 46 | P2 | CodeMachine CLI | `moazbuilds/CodeMachine-CLI` | multi-agent local | provider |
| 47 | P2 | Groq Code CLI | `build-with-groq/groq-code-cli` | multi-model | endpoint |
| 48 | P2 | Dexto | `truffle-ai/dexto` | CLI/web/API | provider |
| 49 | P2 | claw-code-agent | `HarnessLab/claw-code-agent` | endpoint/gateway | provider |
| 50 | P2 | g3 | `dhanji/g3` | Rust provider abstraction | provider |
| 51 | P2 | San | `genai-io/san` | provider-neutral | provider |
| 52 | P2 | Waveloom | `Menfre01/waveloom` | DeepSeek/provider | endpoint |
| 53 | P2 | picocode | `jondot/picocode` | multi-LLM | config |
| 54 | P2 | QQCode | `qnguyen3/qqcode` | skills, Rust | config |
| 55 | P2 | Keen Code | `mochow13/keen-code` | 9+ providers | config |
| 56 | P2 | Grinta | `josephsenior/Grinta-Coding-Agent` | provider-agnostic | config |
| 57 | P2 | Zap | `zap-coding-agent/zap-coding-agent` | Claude/Gemini/OpenAI/LM Studio | provider |
| 58 | P2 | Binharic | `CogitatorTech/binharic-cli` | multi-provider | config |
| 59 | P2 | Darce | `AmerSarhan/darce-cli` | multi-model/streaming | config |
| 60 | P2 | CLAII | `agencyswarm/CLAII` | multi-agent/MCP | provider |
| 61 | P2 | nori-cli | `tilework-tech/nori-cli` | multi-provider sobre Codex | config |
| 62 | P2 | cursor-agent clone | `civai-technologies/cursor-agent` | Claude/OpenAI/Ollama | provider |
| 63 | P2 | Free Code | `paoloanzn/free-code` | fork Claude Code | licenca/config |
| 64 | P2 | Claude Engineer | `Doriandarko/claude-engineer` | CLI Claude | provider |
| 65 | P2 | Smol Developer | `smol-ai/developer` | agent embutivel | SDK/adaptador |
| 66 | P2 | Agentless | `OpenAutoCoder/Agentless` | workflow sem loop | entrada de modelo |
| 67 | P2 | Amazon Q Developer CLI | `aws/amazon-q-developer-cli` | CLI AWS | auth/provider |
| 68 | P2 | nanobot | `HKUDS/nanobot` | OpenClaw rewrite | provider |
| 69 | P2 | ZeroClaw | `zeroclaw-labs/zeroclaw` | providers pluggable | provider |
| 70 | P2 | NanoClaw | `gavrielc/nanoclaw` | Anthropic SDK | base URL |
| 71 | P2 | PicoClaw | `sipeed/picoclaw` | provider/config | provider |
| 72 | P2 | IronClaw | `nearai/ironclaw` | provider Rust | provider |
| 73 | P2 | NullClaw | `nullclaw/nullclaw` | 23+ providers | provider |
| 74 | P2 | Moltis | `moltis-org/moltis` | Rust agent | provider |
| 75 | P2 | GitClaw | `open-gitagent/gitclaw` | Git-native agent | provider |
| 76 | P2 | LionClaw | `moshthepitt/lionclaw` | CLI local | provider |
| 77 | P3 | VibePod | `VibePod/vibepod-cli` | wrapper Docker | launcher |
| 78 | P3 | zeroshot | `the-open-engine/zeroshot` | worktrees/orchestration | launcher |
| 79 | P3 | Fractal | `plasma-ai/fractal` | orquestra CLIs | launcher |
| 80 | P3 | Bernstein | `chernistry/bernstein` | executa/verifica agentes | launcher |
| 81 | P3 | Traycer | `traycerai/traycer` | agentes paralelos | launcher |
| 82 | P3 | h5i | `h5i-dev/h5i` | sandbox e peer review | launcher |
| 83 | P3 | OMK | `dmae97/open-multi-agent-kit` | control plane | ACP/MCP |
| 84 | P3 | kodo | `ikamensh/kodo` | orquestrador | launcher |
| 85 | P3 | ORCH | `oxgeneral/ORCH` | fila de tarefas | launcher |
| 86 | P3 | LoopTroop | `LoopTroop-ai/LoopTroop` | orquestrador OpenCode | launcher |
| 87 | P3 | Galley | `shinpr/galley` | worktree/PR | launcher |
| 88 | P3 | Relay | `jcast90/relay` | MCP/orquestracao | MCP |
| 89 | P3 | SageCLI | `youwangd/SageCLI` | runtime-agnostic | launcher/ACP |
| 90 | P3 | 5dive | `5dive-ai/5dive` | agentes em servidor | launcher |
| 91 | P3 | agx | `ramarlina/agx` | checkpoints | launcher |
| 92 | P3 | claude-code-router | `musistudio/claude-code-router` | proxy multi-provider | integrar como consumidor/proxy |
| 93 | P3 | cc-router | `finch-xu/cc-router` | proxy Anthropic | interoperabilidade |
| 94 | P3 | OneCLI | `onecli/onecli` | broker de credenciais | seguranca/integ. adjacente |
| 95 | P3 | agent-browser | `vercel-labs/agent-browser` | ferramenta para agentes | MCP/plugin |
| 96 | P3 | OpenWork | `different-ai/openwork` | desktop sobre OpenCode | config do agente filho |
| 97 | P4 | Pool | `poolsideai/pool` | `POOLSIDE_STANDALONE_BASE_URL`; EULA | config sem PR presumido |
| 98 | P4 | Junie CLI | `junie.jetbrains.com` | fechado/EAP | BYOK/endpoint a confirmar |
| 99 | P4 | Cursor desktop | `Anysphere` | cloud endpoint | MITM/guide |
| 100 | P4 | Windsurf | produto Codeium | sem base URL geral | MITM |
| 101 | P4 | Amp | `sourcegraph.com/amp` | fechado | MITM/sem PR |
| 102 | P4 | Amazon Q/Kiro CLI | AWS | SSO/ecossistema AWS | MITM/adapter |
| 103 | P4 | Cowork | produto Anthropic | endpoint opaco | MITM |
## C. Regra de promocao/rebaixamento
Um projeto sobe de prioridade quando a pesquisa individual confirma: configuracao documentada, teste local com OmniRoute, licenca permissiva e contribuicao aceita. Desce quando: a URL e fixa, o endpoint e somente SaaS, o README nao corresponde ao codigo, a autenticacao e inseparavel do provedor, ou a licenca/EULA impede redistribuicao. Nenhum projeto e marcado como impossivel sem registrar a evidencia no tracker.

View File

@@ -1,314 +0,0 @@
# Plano executavel de integracao de CLIs
> **Status final (2026-08-03):** a fase de pesquisa foi concluída em lotes de até três worktrees/agentes, cobrindo `104/104` casos. Este documento continua válido como processo operacional para implementação/publicação. Consulte `06-relatorio-final-104-clis-e-estrategia-prs.md` para o resultado final.
**Data:** 2026-08-01
**Objetivo:** pesquisar, integrar, validar e publicar suporte ao OmniRoute em todos os projetos tecnicamente possiveis, mantendo uma fila que permite ate tres subagentes simultaneos.
O ciclo especifico de preparacao, revisao, envio e acompanhamento das contribuicoes upstream esta
em `05-plano-publicacao-prs-upstream.md`.
## 1. Principios operacionais
- Um repositorio por subagente e por worktree.
- No maximo tres tasks de repositorios em execucao ao mesmo tempo.
- Cada task pesquisa o upstream novamente antes de editar; o relatorio inicial e somente contexto.
- O agente principal revisa licenca, arquitetura, smoke test e diff antes do proximo lote.
- Nao usar checkout compartilhado para desenvolvimento e nao usar `git stash`/`git pop`.
- Usar worktrees em `.claude/worktrees/` e branches especificas.
- Nao inventar PR, issue, commit ou aceite de mantenedor.
- Nao adicionar trailers ou rodapes de IA em commits/PRs.
## 2. Fases obrigatorias por projeto
### Fase 0 - Preparacao da task
Criar uma task com nome do projeto, URL canonica, prioridade, evidencia inicial, estado no catalogo OmniRoute e objetivo de integrar. Definir a worktree e o agente responsavel.
### Fase 1 - Pesquisa individual fresca
O agente deve verificar no upstream atual:
- arquitetura de providers e ponto de entrada do CLI;
- arquivo/schema de configuracao e suporte a `base_url`, `baseURL`, `OPENAI_BASE_URL`, `OPENAI_API_BASE`, `LLM_BASE_URL` ou equivalente;
- protocolo real (Chat Completions, Responses, Anthropic Messages, Gemini, ACP, MCP ou outro);
- descoberta de modelos e necessidade de `/v1/models`;
- autenticacao, keyring, OAuth e variaveis de ambiente;
- streaming, tool calling, reasoning e limites conhecidos;
- politica de contribuicao, licenca e se PR de fork externo e aceito;
- atividade, releases, issues/PRs sobre providers customizados ou endpoints locais;
- comandos de build, lint, teste e smoke test;
- possibilidade de fork/PR, issue de proposta, documentacao ou apenas wrapper/MITM.
Registrar commit/release pesquisado e links de evidencia.
### Fase 2 - Gate de viabilidade
Classificar exatamente um caminho inicial:
`viable-direct` (somente configuracao), `viable-upstream` (mudanca no upstream), `viable-acp`, `viable-mcp`, `needs-wrapper`, `needs-mitm`, `config-only`, `blocked` ou `research-more`.
Nao implementar antes de haver uma conclusao de viabilidade e uma razao verificavel.
### Fase 3 - Baseline e TDD
- Executar a suite recomendada pelo upstream antes das mudancas.
- Registrar falhas preexistentes, dependencias ausentes e comandos exatos.
- Limpar `OMNIROUTE_API_KEY` e demais credenciais quando os testes pressupuserem ambiente sem chaves.
- Adicionar primeiro um teste de configuracao, endpoint e selecao de modelo que falhe sem a integracao.
### Fase 4 - Implementacao minima
Implementar apenas o necessario para o caso pesquisado:
- perfil/preset `omniroute` ou provider custom;
- base URL correta (raiz, `/v1` ou `/v1beta` conforme o cliente);
- chave via ambiente ou mecanismo seguro do cliente;
- modelo fixo ou descoberta de modelos;
- selecao/login/report se o CLI tiver esses fluxos;
- documentacao de uso e limites;
- testes de config e chamada.
Se o upstream nao aceitar mudanca, preparar wrapper/launcher ou documentacao local e registrar a limitacao.
### Fase 5 - Validacao funcional
Executar, conforme o protocolo:
- build, lint, typecheck e testes do upstream;
- smoke request com OmniRoute;
- streaming SSE e encerramento por abort;
- tool calling e JSON de argumentos;
- `/v1/models` ou equivalente;
- Chat Completions, Responses, Anthropic Messages e Gemini `generateContent` quando aplicavel;
- fallback/erro, timeout, retry e modelo inexistente;
- teste com chave limpa e teste com `OMNIROUTE_API_KEY` real fora dos logs.
### Fase 6 - Publicacao upstream
- Criar fork somente quando permitido e branch especifica.
- Abrir PR upstream se contribuicoes externas forem aceitas.
- Se PR externo for bloqueado, abrir issue com proposta, patch/referencia e smoke test.
- Se o projeto for fechado/EULA, registrar config manual ou issue de produto; nao criar PR ficticio.
- Atualizar o tracker com URL, commit, estado e resposta do mantenedor.
### Fase 7 - Catalogo e integracao OmniRoute
Quando houver valor para usuarios OmniRoute:
- criar worktree propria do OmniRoute;
- atualizar `src/shared/constants/cliTools.ts` ou `src/shared/constants/cliToolsGrokBuild.ts`;
- atualizar detector em `src/lib/cli-helper/tool-detector.ts` se necessario;
- adicionar gerador/configurador e rota de settings somente se o caso exigir;
- adicionar testes do catalogo, detector, settings, `baseUrlSupport` e `/v1`;
- atualizar `docs/reference/CLI-TOOLS.md`, `docs/guides/CLI-INTEGRATIONS.md` e README quando apropriado;
- atualizar o tracker com a integracao local e evidencias.
### Fase 8 - Fechamento
Registrar commit, branch, PR/issue, testes, limitacoes, status do upstream, status do catalogo OmniRoute e proximo passo. O agente principal faz uma revisao final de seguranca, licenca e factualidade.
## 3. Lotes de ate tres subagentes
O lote e uma unidade operacional. A fila abaixo e ordenada pelo documento `02-prioridade-integracoes-clis.md`; cada linha representa uma task individual.
### Lote 0 - consolidacao do caso de referencia
- `CLI-000` - jcode - manter a issue #704, validar resposta do mantenedor e concluir a secao do README OmniRoute.
### Lote P0.1
- `CLI-001` - Gemini CLI - integrar provider/base URL Gemini.
- `CLI-002` - Claw Code - integrar `OPENAI_BASE_URL`/provider OmniRoute.
- `CLI-003` - Plandex - integrar provider custom com `baseUrl`.
### Lote P0.2
- `CLI-004` - MiMo Code - integrar provider OpenAI-compatible.
- `CLI-005` - Trae Agent - integrar `model_providers` e `base_url`.
- `CLI-006` - Kimi CLI - integrar modos OpenAI/Responses/Anthropic.
### Lote P0.3
- `CLI-007` - Every Code - integrar perfil derivado do Codex.
- `CLI-008` - Open Codex - integrar provider multi-modelo.
- `CLI-009` - VT Code - integrar `custom_providers` e failover.
### Lote P0.4
- `CLI-010` - OpenHands CLI - integrar `LLM_BASE_URL`.
- `CLI-011` - gptme - integrar `OPENAI_BASE_URL`.
- `CLI-012` - Nanocoder - integrar API OpenAI-compatible.
### Lote P0.5
- `CLI-013` - RA.Aid - integrar `OPENAI_API_BASE`.
- `CLI-014` - CoreCoder - integrar `OPENAI_BASE_URL`.
- `CLI-015` - Grok CLI - integrar `GROK_BASE_URL`.
### Lote P1.1
- `CLI-016` - Gitlawb Zero - integrar provider custom e `--base-url`.
- `CLI-017` - DeepSeek Reasonix - confirmar e integrar endpoint.
- `CLI-018` - KlaatCode - integrar `customModels`.
### Lote P1.2
- `CLI-019` - CodeMini CLI - integrar `gateway.base_url`.
- `CLI-020` - Zot - integrar flag/config `--base-url`.
- `CLI-021` - Octomind - integrar provider URL envs.
### Lote P1.3
- `CLI-022` - DvalinCode - integrar OpenAI-compatible.
- `CLI-023` - Coro Code - integrar `OPENAI_BASE_URL`.
- `CLI-024` - Mini-Kode - integrar `MINIKODE_BASE_URL`.
### Lote P1.4
- `CLI-025` - Late CLI - integrar `OPENAI_BASE_URL`/`api-url`.
- `CLI-026` - Agentty - integrar provider e/ou ACP.
- `CLI-027` - Aizen - integrar `AIZEN_BASE_URL`.
### Lote P1.5
- `CLI-028` - Clif-Code - integrar providers OpenAI/Anthropic/Ollama.
- `CLI-029` - Minacode - confirmar provider e integrar URL.
- `CLI-030` - YottaCode - integrar gateway/provider.
### Lote P1.6
- `CLI-031` - aichat - integrar models YAML/provider.
- `CLI-032` - ShellGPT - integrar `API_BASE_URL`.
- `CLI-033` - Mistral Vibe - integrar base URL/provider.
### Lote P1.7
- `CLI-034` - OpenSquilla - integrar gateway/provider.
- `CLI-035` - Kode CLI - integrar endpoint multi-provider.
- `CLI-036` - Neovate Code - integrar plugin/provider.
### Lote P1.8
- `CLI-037` - Deep Agents Code - integrar provider do pacote CLI.
- `CLI-038` - OpenHands principal - integrar CLI/config.
- `CLI-039` - SWE-agent - integrar backend/provider.
### Lote P1.9
- `CLI-040` - AutoCodeRover - integrar backend/provider.
- `CLI-041` - Claurst - integrar provider, respeitando GPL.
- `CLI-042` - Codebuff - integrar provider.
### Lote P2.1
- `CLI-043` - Devon - integrar backend.
- `CLI-044` - Letta Code - integrar provider.
- `CLI-045` - CodeMachine CLI - integrar provider.
### Lote P2.2
- `CLI-046` - Groq Code CLI - integrar endpoint.
- `CLI-047` - Dexto - integrar provider.
- `CLI-048` - claw-code-agent - integrar endpoint.
### Lote P2.3
- `CLI-049` - g3 - integrar provider Rust.
- `CLI-050` - San - integrar provider-neutral.
- `CLI-051` - Waveloom - integrar provider/endpoint.
### Lote P2.4
- `CLI-052` - picocode - integrar multi-LLM.
- `CLI-053` - QQCode - integrar config.
- `CLI-054` - Keen Code - integrar provider.
### Lote P2.5
- `CLI-055` - Grinta - integrar provider.
- `CLI-056` - Zap - integrar Claude/Gemini/OpenAI.
- `CLI-057` - Binharic - integrar multi-provider.
### Lote P2.6
- `CLI-058` - Darce - integrar multi-modelo.
- `CLI-059` - CLAII - integrar provider/MCP.
- `CLI-060` - nori-cli - integrar provider baseado em Codex.
### Lote P2.7
- `CLI-061` - cursor-agent clone - integrar provider.
- `CLI-062` - Free Code - pesquisar licenca e integrar se viavel.
- `CLI-063` - Claude Engineer - integrar provider.
### Lote P2.8
- `CLI-064` - Smol Developer - integrar SDK/adaptador.
- `CLI-065` - Agentless - integrar entrada de modelo.
- `CLI-066` - Amazon Q Developer CLI - pesquisar auth/provider.
### Lote P2.9
- `CLI-067` - nanobot - integrar provider OpenClaw-compatible.
- `CLI-068` - ZeroClaw - integrar trait de provider.
- `CLI-069` - NanoClaw - confirmar base Anthropic.
### Lote P2.10
- `CLI-070` - PicoClaw - integrar provider/config.
- `CLI-071` - IronClaw - integrar provider Rust.
- `CLI-072` - NullClaw - integrar provider.
### Lote P2.11
- `CLI-073` - Moltis - integrar provider Rust.
- `CLI-074` - GitClaw - integrar provider Git-native.
- `CLI-075` - LionClaw - integrar provider CLI.
### Lote P3.1 - wrappers e orquestradores
- `CLI-076` - VibePod; `CLI-077` - zeroshot; `CLI-078` - Fractal.
### Lote P3.2
- `CLI-079` - Bernstein; `CLI-080` - Traycer; `CLI-081` - h5i.
### Lote P3.3
- `CLI-082` - OMK; `CLI-083` - kodo; `CLI-084` - ORCH.
### Lote P3.4
- `CLI-085` - LoopTroop; `CLI-086` - Galley; `CLI-087` - Relay.
### Lote P3.5
- `CLI-088` - SageCLI; `CLI-089` - 5dive; `CLI-090` - agx.
### Lote P3.6
- `CLI-091` - claude-code-router; `CLI-092` - cc-router; `CLI-093` - OneCLI.
### Lote P3.7
- `CLI-094` - agent-browser; `CLI-095` - OpenWork; `CLI-096` - Agent Deck (revisao de agente filho).
### Lote P4 - fechados/MITM
- `CLI-097` - Pool; `CLI-098` - Junie CLI; `CLI-099` - Cursor desktop.
- `CLI-100` - Windsurf; `CLI-101` - Amp; `CLI-102` - Amazon Q/Kiro CLI; `CLI-103` - Cowork.
## 4. Criterio para iniciar o lote seguinte
O lote seguinte pode iniciar quando os tres agentes do lote atual tiverem: pesquisa upstream anexada, gate de viabilidade preenchido, baseline registrado, resultado de smoke test ou bloqueio reproduzivel, e tracker atualizado. Uma falha de um agente nao deve paralisar os outros dois; o agente principal deve marcar `blocked` ou `research-more` com evidencia e seguir a fila.
## 5. Entregaveis de cada task
1. Nota de pesquisa fresca com commit/release e links.
2. Classificacao de viabilidade.
3. Diff minimo ou conclusao documentada de que nao ha diff necessario.
4. Testes e comandos executados, incluindo falhas preexistentes.
5. PR/issue upstream ou justificativa de config-only/MITM.
6. Entrada no catalogo OmniRoute quando aplicavel.
7. Atualizacao do tracker `04-tracker-integracoes-clis.md`.

View File

@@ -1,144 +0,0 @@
# Tracker de integracoes de CLIs com OmniRoute
**Status final da pesquisa:** `104/104` concluídos (`100%`), `0` casos `not-started`. Este é o registro individual autoritativo. O relatório executivo está em `06-relatorio-final-104-clis-e-estrategia-prs.md`.
**Snapshot inicial:** 2026-08-01
**Legenda de status:** `not-started`, `researching`, `research-more`, `viable-direct`, `viable-upstream`, `viable-acp`, `viable-mcp`, `needs-wrapper`, `needs-mitm`, `blocked`, `implementing`, `validating`, `published-pr`, `published-issue`, `awaiting-maintainer`, `accepted`, `rejected`, `integrated`.
Os campos externos (`branch`, `commit`, `PR`, `issue`) ficam como `—` ate haver evidencia real. “Catalogo OmniRoute” significa entrada local, nao necessariamente suporte upstream publicado.
| ID | Prio | Projeto | Repositorio | Pesquisa | Tipo | Upstream | Branch | Commit | PR | Issue | Catalogo OmniRoute | Observacoes/proximo passo |
|---|:---:|---|---|---|---|---|---|---|---|---|---|---|
| CLI-000 | P0 | jcode | `1jehuang/jcode` | concluida | `viable-upstream` | `awaiting-maintainer` | `feat/omniroute-provider` | `ee4f904e6` | [fork PR](https://github.com/diegosouzapw/jcode/pull/1) | [upstream #704](https://github.com/1jehuang/jcode/issues/704) | integrated | acompanhar mantenedor e concluir secao do README |
## Caso publicado: jcode
| Campo | Valor |
|---|---|
| Projeto | jcode |
| Repositorio | `https://github.com/1jehuang/jcode` |
| Status geral | `awaiting-maintainer` |
| Tipo | `viable-upstream`; perfil OpenAI-compatible dirigido por metadados |
| Branch | `feat/omniroute-provider` |
| Commit | `ee4f904e6` |
| PR | `https://github.com/diegosouzapw/jcode/pull/1` (fork de referencia) |
| Issue | `https://github.com/1jehuang/jcode/issues/704` |
| Catalogo OmniRoute | `integrated` / entrada existente |
| Validacao | `cargo check --workspace` limpo; 205 testes passaram; 1 falha preexistente/ambiental |
| Diff | 6 arquivos, `+56/-3` |
| Proximo passo | acompanhar issue #704 e criar secao de README do OmniRoute |
## Tabela principal
| ID | Prio | Projeto | Repositorio | Pesquisa | Tipo | Upstream | Branch | Commit | PR | Issue | Catalogo OmniRoute | Observacoes/proximo passo |
|---|:---:|---|---|---|---|---|---|---|---|---|---|---|
| CLI-001 | P0 | Gemini CLI | `google-gemini/gemini-cli` | concluida | `pr-generic` | `published-issue` | `fix/omniroute-gateway-auth` | `8138105c38cc1637fe9e8a9bd520eb835f1620e6` | — | [upstream #27550](https://github.com/google-gemini/gemini-cli/issues/27550#issuecomment-5152312278) | not-in-catalog | regression `AuthType.GATEWAY`; patch +26; auth 10/10, non-interactive 17/17, content generator 55/55, Gemini `/v1beta` stream/tools smoke verde; aguardar `help wanted` antes de terceira PR |
| CLI-002 | P0 | Claw Code | `ultraworkers/claw-code` | concluida | `pr-docs` | `published-issue` | `docs/omniroute-setup` | `de857038b2f9ff9b319132e2241549e86215c351` | — | [upstream #3283](https://github.com/ultraworkers/claw-code/issues/3283) | not-in-catalog | generic OpenAI Chat Completions; docs +37; 1.415 testes, fmt, docs/release checks e clippy oficial verdes; fork bloqueado pelo GitHub, issue-first; smoke OmniRoute parcial/timeout; chave do smoke deve ser rotacionada |
| CLI-003 | P0 | Plandex | `plandex-ai/plandex` | concluida | `pr-docs` | `published-pr` | `feat/omniroute-provider-docs` | `f8f0694bdf7d1cb6e65a1f1c5bc39f84921a4507` | [upstream #359](https://github.com/plandex-ai/plandex/pull/359) | — | not-in-catalog | custom provider OpenAI-compatible ja existia; docs com `/v1`, `OMNIROUTE_API_KEY`, Docker reachability e model mapping; Go indisponivel; Docusaurus build verde; acompanhar mantenedor |
| CLI-004 | P0 | MiMo Code | `XiaomiMiMo/MiMo-Code` | concluida | `config-only` | not-applicable | `research/omniroute-mimo-code` | — | — | — | not-in-catalog | SHA `ce124cb`; provider customizado `@ai-sdk/openai-compatible` já suporta `baseURL`, `apiKey` e modelo; 116 testes focados + typecheck verdes; smoke CLI inconclusivo por travamento ambiental; sem PR artificial |
| CLI-005 | P0 | Trae Agent | `bytedance/trae-agent` | concluida | `pr-docs` | `published-pr` | `research/omniroute-trae-agent` | `4801e48b69d7583300eb86ec5c69235506d7f205` | [upstream #449](https://github.com/bytedance/trae-agent/pull/449) | — | not-in-catalog | README +39; `provider: openai` + mapping `base_url=/v1`; `/v1/responses`, `/v1/models`, Bearer, tools e limitação sem streaming; 62 testes/17 skips, pre-commit e mocks verdes; CLA pendente |
| CLI-006 | P0 | Kimi CLI | `MoonshotAI/kimi-cli` | concluida | `pr-docs` | `published-issue` | `research/omniroute-kimi-cli` | `a2f62bf6108a6954e798db992411aa06670e224f` | — | [upstream #2576](https://github.com/MoonshotAI/kimi-cli/issues/2576) | not-in-catalog | docs EN/ZH +63; `openai_legacy` `/v1`, chave via `OPENAI_API_KEY`, modelo manual; Responses/Anthropic alternativos; 47 testes e VitePress verdes; aguardar direção do mantenedor antes da PR |
| CLI-007 | P0 | Every Code | `just-every/code` | concluida | `pr-docs` / `config-only` | `published-pr` | `feat/omniroute-integration` | `8fbc8dab5fb76bf05535055801af0c3ccfea6f3b` | [upstream #614](https://github.com/just-every/code/pull/614) | — | not-in-catalog | PR documental aberta e mergeable; release `v0.6.162`; `./build-fast.sh` baseline/pós-patch verdes; smoke mock Responses/SSE/tools verde; acompanhar CI/mantenedor |
| CLI-008 | P0 | Open Codex | `ymichael/open-codex` | concluida | `pr-generic` / `issue-first` | `published-issue` | `feat/omniroute-integration` | `f25de99f991c0e4d9d6ae2811d307cdbff92f869` | — | [upstream #4](https://github.com/ymichael/open-codex/issues/4#issuecomment-5152804104) | not-in-catalog | patch genérico pronto localmente; issue-first por firewall de container e PR #19 fechada; 132 testes, typecheck/build/format verdes; lint bloqueado por ambiente; aguardar mantenedor antes de PR |
| CLI-009 | P0 | VT Code | `vinhnx/vtcode` | concluida | `pr-docs` / `config-only` | `published-pr` | `feat/omniroute-integration` | `256682d10c72f3e6e145d852b6d9d53f5c471988` | [upstream #717](https://github.com/vinhnx/VTCode/pull/717) | — | not-in-catalog | PR documental aberta e mergeable; release `0.141.10`; custom provider `/v1`, Bearer, `auto`, discovery manual, streaming/tools; 10 testes config verdes; nextest/docs checks bloqueados por ambiente; acompanhar CI/mantenedor |
| CLI-010 | P0 | OpenHands CLI | `OpenHands/OpenHands-CLI` | concluida | `config-only` | not-applicable | `feat/omniroute-openhands-cli-integration` | — | — | — | not-in-catalog | SHA `2df8a283`; `LLM_BASE_URL=/v1`, `LLM_API_KEY`, modelo obrigatório `openai/auto`, Chat Completions/SSE/tools; 63 testes focados e mock verdes; sem PR artificial |
| CLI-011 | P0 | gptme | `gptme/gptme` | concluida | `config-only` | not-applicable | `feat/omniroute-gptme-integration` | — | — | — | not-in-catalog | SHA `7fe250529`; provider TOML nomeado, `/v1/chat/completions`, `/v1/models`, Bearer, streaming/tools; compileall verde, pytest bloqueado por deps; docs genericas ja cobrem |
| CLI-012 | P0 | Nanocoder | `Nano-Collective/nanocoder` | concluida | `config-only` | not-applicable | `feat/omniroute-nanocoder-integration` | — | — | — | not-in-catalog | SHA `becae998`; `createOpenAICompatible`, `/v1/models`, streaming/native tools + XML/JSON fallback; types/format/lint/build verdes; suite ampla com falhas preexistentes; sem PR artificial |
| CLI-013 | P0 | RA.Aid | `ai-christianson/RA.Aid` | concluida | `config-only` | not-applicable | `feat/omniroute-ra-aid-integration` | — | — | — | not-in-catalog | SHA `e71bb83`; provider `openai-compatible`, `/v1/chat/completions`, Bearer, modelo explicito/`auto`, function tools; 762 testes + 62 focados e smoke verdes; sem Responses/stream HTTP garantido; Aider exige config separada; sem PR artificial |
| CLI-014 | P0 | CoreCoder | `he-yufeng/CoreCoder` | concluida | `pr-docs` / `config-only` | `published-pr` | `feat/omniroute-integration` | `f4d2851649e5dda20738c313a8a94337b24eeb9d` | [upstream #20](https://github.com/he-yufeng/CoreCoder/pull/20) | — | not-in-catalog | PR documental aberta, nao draft e mergeable; `/v1/chat/completions`, Bearer, `auto`, streaming/native tools; 86 testes, compileall, build, twine e smoke verdes; Ruff mantem 41 falhas preexistentes; acompanhar CI/mantenedor |
| CLI-015 | P1 | Grok CLI | `superagent-ai/grok-cli` | concluida | `config-only` | not-applicable | `feat/omniroute-grok-cli-integration` | — | — | — | not-in-catalog | SHA `fb97af8`; `GROK_BASE_URL`/`--base-url`, Chat Completions/SSE, Bearer, `auto` e tools confirmados; 47/48 suites e 246 testes no gate isolado, 6 arquivos/39 testes focados verdes; Node não carrega `bun:sqlite`; Responses/search/STT/Batch/midia não garantidos; monitorar PRs #290/#349 |
| CLI-016 | P1 | Gitlawb Zero | `Gitlawb/zero` | concluida | `config-only` | not-applicable | `feat/omniroute-gitlawb-zero-integration` | — | — | — | not-in-catalog | SHA `8e266797`; release `v0.6.0`; provider custom `/v1`, Bearer, `auto`, Chat/SSE/tools, usage e `/v1/models` confirmados; Go test/vet/fmt e smoke verdes; release build bloqueado por falta de espaco; politica exige issue aprovada; sem contribuicao nominal artificial |
| CLI-017 | P1 | DeepSeek Reasonix | `esengine/DeepSeek-Reasonix` | concluida | `config-only` | not-applicable | `feat/omniroute-deepseek-reasonix-integration` | — | — | — | not-in-catalog | SHA `1c62489d`; release `v1.19.1`; `kind=openai`, `/v1/chat/completions`, Bearer, `auto`, SSE/tools, `/v1/models` e reasoning confirmados; suite completa, vet, fmt, build e smoke verdes apos remover env SSH do runner; sem PR/issue redundante |
| CLI-018 | P1 | KlaatCode | `KlaatAI/klaatcode` | concluida | `config-only` | not-applicable | `feat/omniroute-klaatcode-integration` | — | — | — | not-in-catalog | SHA `0d20f24a`; release `V2.4.0`; `customModels` com `/v1`, Bearer, `auto`, Chat/SSE/tools confirmados; 316 testes, 33 fixtures e build verdes; typecheck local divergiu do CI verde; custom endpoint e apenas TUI; divergencia de metadata de licenca registrada; sem contribuicao nominal artificial |
| CLI-019 | P1 | CodeMini CLI | `havingautism/Codemini-CLI` | concluida | `config-only` | not-applicable | `feat/omniroute-codemini-cli-integration` | — | — | — | not-in-catalog | SHA `a3764b21`; package `0.8.3`; gateway `/v1`, Bearer persistido, `auto`, Chat/SSE/usage/tools e tool round trip confirmados; `/models` e probe, nao picker; 122/123 testes, 10 focados e pack-imports verdes; sem PR nominal redundante |
| CLI-020 | P1 | Zot | `patriceckhart/zot` | concluida | `config-only` | not-applicable | `feat/omniroute-zot-integration` | — | — | — | not-in-catalog | SHA `f3d8eb66`; release `v0.3.29`; custom provider `omniroute` em `models.json`, `/v1`, Bearer, `auto`, Chat/SSE/tools/reasoning opt-in e cache usage confirmados; `--base-url` e so override; PR #36 ja cita OmniRoute; race suite/build/vet/fmt verdes |
| CLI-021 | P1 | Octomind | `Muvon/octomind` | concluida | `config-only` | not-applicable | `feat/omniroute-octomind-integration` | — | — | — | not-in-catalog | SHA `65ab1db1`; release `0.39.0`; provider `local:auto` usa endpoint completo `/v1/chat/completions`, Bearer opcional, Chat JSON buffered, tools/reasoning/usage; sem SSE/Responses/discovery; fmt/fetch e smokes com/sem auth verdes; suite ampla nao executada por disco/contencao |
| CLI-022 | P1 | DvalinCode | `arthurpanhku/dvalincode` | concluida | `config-only` | not-applicable | `feat/omniroute-dvalincode-integration` | — | — | — | not-in-catalog | SHA `7d42664a`; release `v0.14.1`; provider OpenAI-compatible custom com `/v1`, Bearer via env, `auto`, Chat/SSE/usage/tools e tool round trip confirmados; `provider test` bloqueado por trusted presets; issues #109/#118/#135 ja cobrem melhorias genericas; sem PR nominal |
| CLI-023 | P1 | Coro Code | `Blushyes/coro-code` | concluida | `config-only` | not-applicable | `feat/omniroute-coro-code-integration` | — | — | — | not-in-catalog | SHA `679c57af`; release `v0.0.8`; `OPENAI_BASE_URL=/v1`, Bearer, `auto`, Chat JSON e function tools/tool loop confirmados; streaming existe mas nao e usado pelo agente; sem Responses/discovery; `cargo check`/fmt bloqueados por drift preexistente; risco de LICENSE ausente; sem PR nominal |
| CLI-024 | P1 | Mini-Kode | `minmaxflow/mini-kode` | concluida | `config-only` | not-applicable | `feat/omniroute-mini-kode-integration` | — | — | — | not-in-catalog | SHA `4e7f9767`; release/tag npm `0.2.3`; provider custom por `MINIKODE_BASE_URL=/v1`, Bearer, `auto`, Chat/SSE e tools/tool loop confirmados; sem Responses/discovery/reasoning dedicado; sem PR nominal redundante |
| CLI-025 | P1 | Late CLI | `mlhher/late-cli` | concluida | `config-only` | not-applicable | `feat/omniroute-late-cli-integration` | — | — | — | not-in-catalog | SHA `26814e62`; release `v1.4.2`; `OPENAI_BASE_URL=/v1`, Bearer, `auto`, Chat/SSE/usage/reasoning_content/tools e tool round trip confirmados; probes `/props`/`/v1/models` nao sao picker; BSL 1.1/CLA; sem PR nominal |
| CLI-026 | P1 | Agentty | `1ay1/agentty` | concluida | `config-only` | not-applicable | `feat/omniroute-agentty-integration` | — | — | — | not-in-catalog | SHA `e947b26c`; release `v0.2.10`; custom host `127.0.0.1:20128`, Bearer, Chat/SSE/tools e `/v1/models` confirmados; Responses/reasoning/tool round trip dinamico nao confirmados; MIT; sem PR nominal |
| CLI-027 | P1 | Aizen | `aizen-stack/aizen` | concluida | `config-only` | not-applicable | `feat/omniroute-aizen-integration` | — | — | — | not-in-catalog | SHA `3d8ae0f6`; release `v0.5.4`; `AIZEN_BASE_URL=/v1`, Bearer, `auto`/modelo literal, Chat/SSE/reasoning_content e `/v1/models`; tools confirmadas estaticamente, sem smoke dinamico; PolyForm Noncommercial/CLA; sem PR nominal |
| CLI-028 | P1 | Clif-Code | `DLhugly/Clif-Code` | concluida | `config-only` | not-applicable | `feat/omniroute-clif-code-integration` | — | — | — | not-in-catalog | SHA `282a787a`; release `v1.72.0`; `CLIFCODE_API_URL=/v1`, Bearer, `auto`, Chat/SSE/usage/tools e tool loop confirmados por fonte; smoke bloqueado por binario ausente; sem Responses/reasoning; licença proprietária conflitante com FSL declarada exige revisão jurídica; sem PR nominal |
| CLI-029 | P1 | Minacode | `hit9/minacode` | concluida | `config-only` | not-applicable | `feat/omniroute-minacode-integration` | — | — | — | not-in-catalog | SHA `d4ea4a97`; release `v0.18.1`; TOML custom `/v1`, key obrigatória, `auto`, Chat/Responses/Anthropic, SSE/tools/reasoning/discovery confirmados; smoke de protocolo Chat+Responses+models e compileall verdes; CI remoto verde; sem PR nominal |
| CLI-030 | P1 | YottaCode | `yottadynamics/yottacode` | concluida | `config-only` | not-applicable | `feat/omniroute-yottacode-integration` | — | — | — | not-in-catalog | SHA `039f61ce`; release `v0.3.1`; provider `openai-compatible`, `/v1`, Bearer, `/v1/models`, Chat/SSE/tools/reasoning parsing confirmados; smoke oficial com mock passou; Go 1.26 nao instalado e gates completos nao executados por espaco; sem PR nominal |
| CLI-031 | P1 | aichat | `sigoden/aichat` | concluida | `config-only` | not-applicable | `feat/omniroute-aichat-integration` | — | — | — | not-in-catalog | SHA `82976d3`; package/release `v0.30.0`; provider `openai-compatible` com base `/v1`, Bearer opcional e modelo `auto`; Chat stream/JSON, reasoning e tool round-trip confirmados; Responses ausente (#1431); limites de tool SSE ja cobertos por #1454/#1495 e PR #1496; sem publicacao nominal |
| CLI-032 | P1 | ShellGPT | `TheR1D/shell_gpt` | concluida | `config-only` | not-applicable | `feat/omniroute-shellgpt-integration` | — | — | — | not-in-catalog | SHA `a082bd53`; release `1.5.1`; `API_BASE_URL=/v1`, `OPENAI_API_KEY`, `DEFAULT_MODEL=auto` e `USE_LITELLM=false`; smoke real confirmou env e `.sgptrc`, Chat/SSE e Bearer; issue #718 nao reproduz no HEAD; CI baseline vermelho por temperatura default independente; sem publicacao nominal |
| CLI-033 | P1 | Mistral Vibe | `mistralai/mistral-vibe` | concluida | `config-only` | not-applicable | `feat/omniroute-mistral-vibe-integration` | — | — | — | not-in-catalog | SHA/release `99a6efa9` / `v2.23.2`; `GenericBackend` custom com base `/v1`, Bearer, Chat/SSE, usage, tools e reasoning; smoke do binario oficial verde; #790 cobre somente discovery `/v1/models`; upstream nao aceita contribuicoes de codigo no momento; sem publicacao |
| CLI-034 | P1 | OpenSquilla | `opensquilla/opensquilla` | concluida | `config-only` | not-applicable | `feat/omniroute-opensquilla-integration` | — | — | — | not-in-catalog | `custom` com `/v1`, Bearer opcional, Chat/SSE, tools, reasoning recebido, usage e `/v1/models`; smoke provider-level verde; monitorar issue #912 do probe custom; sem publicacao nominal |
| CLI-035 | P1 | Kode CLI | `shareAI-lab/Kode-cli` | concluida | `config-only` | not-applicable | `feat/omniroute-kode-cli-integration` | — | — | — | not-in-catalog | `custom-openai` com `/v1`, discovery `/v1/models`, fallback manual, Bearer, Chat/SSE, tools/tool round-trip e persistencia; smoke runtime bloqueado por Bun/artefato ausente; CI baseline vermelho por formatacao; sem publicacao nominal |
| CLI-036 | P1 | Neovate Code | `neovateai/neovate-code` | concluida | `config-only` | not-applicable | `feat/omniroute-neovate-code-integration` | — | — | — | not-in-catalog | provider JSON custom normalizado para OpenAI-compatible, `/v1`, Bearer, Chat/SSE, tools/tool round-trip; model catalog declarado (sem discovery); smoke do pacote publicado verde; sem publicacao nominal |
| CLI-037 | P1 | Deep Agents Code | `langchain-ai/deepagents` | concluida | `config-only` | not-applicable | `feat/omniroute-deepagents-code-integration` | — | — | — | not-in-catalog | SHA `46ee772b4`; `deepagents-code==0.1.51`; provider `openai`, base OmniRoute `/v1`, model `openai:auto`; Responses e default, Chat usa `use_responses_api=false`; smoke de config verde, sem HTTP/runtime por deps e disco; #3973/#3287 ja cobrem os pontos genericos; sem publicacao nominal |
| CLI-038 | P1 | OpenHands principal | `OpenHands/OpenHands` | concluida | `config-only` | not-applicable | `feat/omniroute-openhands-main-integration` | — | — | — | not-in-catalog | SHA `1708efc44`; Agent Canvas `1.8.0`; `openai/auto` + base `/v1` + API key + `api_mode=chat`; LiteLLM envia `model=auto`, Chat/SSE/tools estruturais; sem discovery generico `/v1/models`; PRs OmniRoute [#15189](https://github.com/OpenHands/OpenHands/pull/15189)/[#15211](https://github.com/OpenHands/OpenHands/pull/15211) fechadas sem merge; sem nova publicacao |
| CLI-039 | P1 | SWE-agent | `SWE-agent/SWE-agent` | concluida | `config-only` | not-applicable | `feat/omniroute-swe-agent-integration` | — | — | — | not-in-catalog | SHA `3ea751c08`; release `v1.1.0`; LiteLLM com `openai/<model-id>`, `api_base=/v1` e chave por env; Chat/tools/tool round-trip e batch confirmados por fonte; reasoning parcial; smoke HTTP bloqueado por deps ausentes; sem publicacao nominal |
| CLI-040 | P1 | AutoCodeRover | `AutoCodeRoverSG/auto-code-rover` | concluida | `pr-generic` | `validating` | `feat/omniroute-auto-code-rover-integration` | — | — | — | not-in-catalog | SHA `585d3e639`; patch local sem commit em 4 arquivos corrige `litellm-generic-openai/auto`, base `/v1`, precedencia da chave e pricing desconhecido; 9 testes focados com stubs, tracer source-only, compileall e diff-check verdes; sem HTTP real; licenca SONAR Source-Available exige gate juridico antes de publicar |
| CLI-041 | P2 | Claurst | `Kuberwastaken/claurst` | concluida | `config-only` | not-applicable | `feat/omniroute-claurst-integration` | — | — | — | not-in-catalog | SHA `595b0ebe3`; `custom-openai` com settings persistidos, base `/v1`, `CUSTOM_OPENAI_API_KEY`, modelo `auto`, Chat/SSE/tools e `/v1/models`; CI upstream verde; sem build/smoke local e sem publicacao nominal; monitorar PR #365 sem duplicar |
| CLI-042 | P2 | Codebuff | `CodebuffAI/codebuff` | concluida | `blocked` / `issue-first` | `blocked` | `feat/omniroute-codebuff-integration` | — | — | — | not-in-catalog | SHA `195b9bef6`; main nao expoe base/chave/provider custom na CLI/SDK; PR upstream existente [#693](https://github.com/CodebuffAI/codebuff/pull/693) cobre a lacuna, observada OPEN/CONFLICTING/DIRTY; nao criar patch concorrente; acompanhar #693 e validar apos merge/port |
| CLI-043 | P2 | Devon | `entropy-research/Devon` | concluida | `pr-generic` | validating | `feat/omniroute-devon-integration` | — | — | [upstream #100](https://github.com/entropy-research/Devon/issues/100) | not-in-catalog | SHA `8f68f1d74`; diff local genérico em 5 arquivos, sem commit; reprodução literal DeepSeek/OpenRouter e resume corrigidos; 9 testes focados, compileall e diff-check verdes; Standards/Spec aprovados; aguardar autorização antes de fork/push/PR |
| CLI-044 | P2 | Letta Code | `letta-ai/letta-code` | concluida | `config-only` | not-applicable | `feat/omniroute-letta-code-integration` | — | — | — | integrated | SHA `09aff1bb4`; já coberta pelo provider local `lmstudio` (`lmstudio_openai`), discovery `/api/v0/models``/v1/models`, Chat/SSE/tools; 8 testes OmniRoute verdes; sem PR nominal |
| CLI-045 | P2 | CodeMachine CLI | `moazbuilds/CodeMachine-CLI` | concluida | `config-only` | not-applicable | `feat/omniroute-codemachine-cli-integration` | — | — | — | not-in-catalog | SHA `572def63e`; integração indireta por OpenCode custom `@ai-sdk/openai-compatible`, base `/v1`, chave por env e `omniroute/auto`; provider/model reconhecidos no smoke de config; alternativa Claude Code; sem PR nominal |
| CLI-046 | P2 | Groq Code CLI | `build-with-groq/groq-code-cli` | concluida | `pr-generic` | `awaiting-maintainer` | `feat/omniroute-groq-code-cli-integration` | — | — | — | not-in-catalog | SHA `a303eb4be`; `groq-sdk@0.27.0` fixa `/openai/v1/chat/completions`, logo não há config-only para OmniRoute; mock confirmou path/Bearer; PR existente [#7](https://github.com/build-with-groq/groq-code-cli/pull/7) é a duplicata natural, mas precisa distinguir Groq-compatible de OpenAI-compatible; 17 testes oficiais + 5 testes de contexto, build e mock verdes; clone limpo, sem patch/publicação |
| CLI-047 | P2 | Dexto | `truffle-ai/dexto` | concluida | `config-only` | `not-applicable` | `feat/omniroute-dexto-integration` | — | — | — | not-in-catalog | SHA `4108a9c73`; provider `openai-compatible` nativo exige `baseURL`, aceita modelo arbitrário, Bearer opcional, Chat/SSE/tools e reasoning effort; receita `/v1` + `auto`; 175 testes focados e builds llm/core verdes; TS2741 em chatgpt-oauth é baseline; ELv2; sem PR/issue nominal |
| CLI-048 | P2 | claw-code-agent | `HarnessLab/claw-code-agent` | concluida | `config-only` | `not-applicable` | `feat/omniroute-claw-code-agent-integration` | — | — | — | not-in-catalog | SHA `167571da8`; `OPENAI_BASE_URL=http://127.0.0.1:20128/v1`, Bearer, model manual/`auto`, Chat/SSE/tools/usage confirmados; smoke `MOCK_SMOKE_OK`, 80 testes focados; sem discovery/Responses API; licença não identificada (`license: null`); sem PR/issue |
| CLI-049 | P2 | g3 | `dhanji/g3` | concluida | `pr-generic` | `validating` | `feat/omniroute-g3-integration` | — | — | [upstream #70](https://github.com/dhanji/g3/issues/70) | not-in-catalog | SHA `0ddb052d2`; diff local provider-neutral em `provider_registration.rs`, 1 arquivo `+25/-1`, corrige registro `custom``custom.default`; `cargo check -p g3-config`, 6 testes config e diff-check verdes; teste focal escrito mas build bloqueado em `x11.pc`; manifesto declara MIT sem arquivo LICENSE; Standards/Spec centrais aprovados; sem publicação |
| CLI-050 | P2 | San | `genai-io/san` | concluida | `config-only` | `not-applicable` | `feat/omniroute-san-integration` | — | — | — | not-in-catalog | SHA `e45ec0ef7`; Apache-2.0/release v1.22.1; provider Custom com base `/v1`, Bearer, `/models`, Chat/SSE/tools/tool result e reasoning best-effort; smoke HTTP de dois turnos e gates Go focados verdes; sem provider nominal ou publicação |
| CLI-051 | P2 | Waveloom | `Menfre01/waveloom` | concluida | `config-only` | `not-applicable` | `feat/omniroute-waveloom-integration` | — | — | — | not-in-catalog | SHA `293d5cd11`; Apache-2.0/release v0.5.1; adapter OpenAI com `/v1`, Bearer, `/models`, SSE, 14 tools, tool-result round-trip e sessões; smoke do binário oficial verde e CI remoto do HEAD verde; reasoning/cache avançados não são projetados; sem publicação |
| CLI-052 | P2 | picocode | `jondot/picocode` | concluida | `config-only` | `not-applicable` | `feat/omniroute-picocode-integration` | — | — | — | not-in-catalog | SHA `064a2a6ea`; MIT/release v0.6.0; Rig 0.28 lê `OPENAI_BASE_URL` e usa Responses `/v1/responses`; smoke confirmou Bearer, `auto`, 11 tools e function_call_output; 7 testes/doc-tests verdes; fmt/clippy só baseline; sem PR/issue |
| CLI-053 | P2 | QQCode | `qnguyen3/qqcode` | concluida | `config-only` | `not-applicable` | `feat/omniroute-qqcode-integration` | — | — | — | not-in-catalog | SHA `be6a96ce7`; Apache-2.0/release v1.2.0; provider arbitrário + `GENERIC`/OpenAI com base `/v1`; smoke confirmou JSON/SSE, Bearer, extra_body, reasoning e tool-result; backend 20/20, ACP 13+1 skip, observer 11/11, compileall/helps verdes; sem PR/issue |
| CLI-054 | P2 | Keen Code | `mochow13/keen-code` | concluida | `config-only` | `not-applicable` | `feat/omniroute-keen-code-integration` | — | — | — | not-in-catalog | SHA `ee2eaf0f4`; MIT/release v0.40.0; receita manual `openai-compatible` + `/v1` + Bearer + model arbitrário; smoke oficial confirmou Chat/SSE, tools/tool-result, usage e reasoning replay; provider oculto apenas no picker; CI remoto verde; sem PR/issue |
| CLI-055 | P2 | Grinta | `josephsenior/Grinta-Coding-Agent` | concluida | `config-only` | `not-applicable` | `feat/omniroute-grinta-integration` | — | — | — | not-in-catalog | SHA `df7437524`; provider OpenAI-compatible com `LLM_API_KEY`, model `auto`, base `/v1`; smoke Chat/SSE/tools/tool-result/reasoning/usage/cache verde; 183 testes focados, compileall e Ruff verdes; sem PR/issue nominal |
| CLI-056 | P2 | Zap | `zap-coding-agent/zap-coding-agent` | concluida | `config-only` | `not-applicable` | `feat/omniroute-zap-integration` | — | — | — | not-in-catalog | SHA `f0203f872`; provider arbitrário `kind=openai`, base `/v1`, Bearer, discovery `/models`, Chat JSON/SSE, tools/tool-result, reasoning e usage confirmados; cargo check + 16 testes/gates focados verdes; issue #2 confirma arquitetura; sem PR nominal |
| CLI-057 | P2 | Binharic | `CogitatorTech/binharic-cli` | concluida | `pr-generic` | `validating` | `feat/omniroute-binharic-integration` | — | — | — | not-in-catalog | SHA `52ccca70b`; patch sem commit em `provider.ts` + teste: aplica `baseURL` ao OpenAI/Anthropic e usa Chat Completions para base customizada; RED→GREEN, 14 focal, 88 arquivos/774 testes, typecheck/build e smoke wire verdes; lint upstream bloqueado; sem publicação |
| CLI-058 | P2 | Darce | `AmerSarhan/darce-cli` | concluida | `config-only` | `not-applicable` | `feat/omniroute-darce-integration` | — | — | — | not-in-catalog | SHA `1b90c379a`; MIT declarada no package/npm sem arquivo LICENSE; `DARCE_API_BASE` raiz sem `/v1`, `DARCE_API_KEY`, `DARCE_MODEL=auto`; smoke PTY do binário confirmou 2 Chat/SSE, 7 tools, tool-result e Bearer; 106 testes/build verdes; sem MCP/ACP/A2A; sem PR/issue |
| CLI-059 | P2 | CLAII | `agencyswarm/CLAII` | concluida | `pr-generic` | `blocked` | `feat/omniroute-claii-integration` | — | — | — | not-in-catalog | SHA `89d42311b`; patch sem commit em README/config/providers/test: `CLAII_API_KEY`, `CLAII_BASE_URL` origem sem `/v1beta`, model runtime e reject explícito; 4 wire/loop + 10 calculator + pip install + smoke CLI verdes; unittest discover falha só baseline `calculator`/`pkg`; sem MCP/ACP/A2A; **All Rights Reserved**, não publicar sem autorização jurídica |
| CLI-060 | P2 | nori-cli | `tilework-tech/nori-cli` | concluida | `config-only` | `not-applicable` | `feat/omniroute-nori-cli-integration` | — | — | — | not-in-catalog | SHA `829ecf3fd`; Apache-2.0/v0.24.0; Nori custom ACP → OpenCode `opencode-ai@1.18.11` → OmniRoute `/v1`; MCP separado por `/api/mcp/stream` ou stdio; 5 testes focados, cargo build nori e smoke ACP Nori→OpenCode verdes; sem patch/publicação |
| CLI-061 | P2 | cursor-agent clone | `civai-technologies/cursor-agent` | concluida | `config-only` | `not-applicable` | `feat/omniroute-cursor-agent-clone-integration` | — | — | — | not-in-catalog | SHA `d21a8f3d4`; MIT/v0.1.39; SDK OpenAI usa base `/v1`, Anthropic usa raiz; smokes de 2 turnos/tools verdes; factory rejeita `auto` puro; 23 testes, mypy/build verdes; sem patch/publicação |
| CLI-062 | P2 | Free Code | `freecodexyz/free-code` | concluida | `config-only` | `blocked` | `feat/omniroute-free-code-integration` | — | — | [upstream #20](https://github.com/freecodexyz/free-code/issues/20) | not-in-catalog | SHA `6b25ab68b`; URL antiga `paoloanzn/free-code` redireciona; base Anthropic raiz, `model=auto`, stream/tools/MCP; build verde; sem LICENSE/campo license e código atribuído à Anthropic, não publicar |
| CLI-063 | P2 | Claude Engineer | `Doriandarko/claude-engineer` | concluida | `config-only` / `pr-generic` | `blocked` | `feat/omniroute-claude-engineer-integration` | — | [upstream #250](https://github.com/Doriandarko/claude-engineer/pull/250) | [upstream #116](https://github.com/Doriandarko/claude-engineer/issues/116) | not-in-catalog | SHA `0a9e4b309`; v3 funciona por base Anthropic raiz com modelo fixo; #250 já adiciona `ANTHROPIC_MODEL`; arquivo LICENSE ausente apesar de declaração MIT; sem patch concorrente/publicação |
| CLI-064 | P2 | Smol Developer | `smol-ai/developer` | concluida | `config-only` | `not-applicable` | `feat/omniroute-smol-developer-integration` | — | — | — | not-in-catalog | SHA `a6747d1a6`; `OPENAI_API_BASE=/v1`, `auto`, 3 Chat calls, SSE/function calling e Agent Protocol validados; gates de runtime verdes, build metadata preexistente; sem patch/publicação |
| CLI-065 | P2 | Agentless | `OpenAutoCoder/Agentless` | concluida | `config-only` | `not-applicable` | `feat/omniroute-agentless-integration` | — | — | — | not-in-catalog | SHA `5ce5888b9`; OpenAI chat + embeddings funcionam com bases distintas; Anthropic normal/cache histórico validados; DeepSeek fixa host; pre-commit/compileall verdes; sem patch/publicação |
| CLI-066 | P2 | Amazon Q Developer CLI | `aws/amazon-q-developer-cli` | concluida | `viable-mcp` / `needs-wrapper` | `not-applicable` | `feat/omniroute-amazon-q-developer-cli-integration` | — | — | — | not-in-catalog | SHA `15cc8f3cd`; modelo usa AWS JSON/EventStream Bearer/SigV4 e não `/v1`; MCP stdio imediato, HTTP legado com ressalva; upstream issue-first/manutenção crítica; sem patch/publicação |
| CLI-067 | P2 | nanobot | `HKUDS/nanobot` | concluida | `config-only` | `not-applicable` | `feat/omniroute-nanobot-integration` | — | — | — | not-in-catalog | HEAD `44b7e1bf4`; provider dinâmico OpenAI-compatible com base `/api/v1` e modelo `omniroute/auto`; Chat/SSE/tools/reasoning/usage/images/discovery e retry validados; 424 testes + Ruff; sem PR nominal |
| CLI-068 | P2 | ZeroClaw | `zeroclaw-labs/zeroclaw` | concluida | `config-only` | `not-applicable` | `feat/omniroute-zeroclaw-integration` | — | — | — | not-in-catalog | HEAD `4770420ab`; `custom.omniroute`, base `/v1`, Bearer, `auto`, Chat/Responses e tools nativas opt-in; 1.173 unit + 1 integração, fmt/config/smoke verdes; sem PR nominal |
| CLI-069 | P2 | NanoClaw | `gavrielc/nanoclaw` | concluida | `config-only` | `not-applicable` | `feat/omniroute-nanoclaw-integration` | — | — | — | not-in-catalog | HEAD `dfac7e0af`; provider Claude existente aponta para raiz Anthropic OmniRoute e OneCLI guarda a chave; baseline e 49 testes OmniRoute verdes; Codex #3155/#1984 e OpenCode #2985 ficam como follow-ups; sem PR |
| CLI-070 | P2 | PicoClaw | `sipeed/picoclaw` | concluida | `config-only` | `not-applicable` | `feat/omniroute-picoclaw-integration` | — | — | — | not-in-catalog | HEAD `49183d7`, `/api/v1`, `openai/auto``auto`; Chat/SSE/tools/usage/images/discovery; Go ausente, testes locais não executados; issue router #3298; sem publicação |
| CLI-071 | P2 | IronClaw | `nearai/ironclaw` | concluida | `config-only` | `not-applicable` | `feat/omniroute-ironclaw-integration` | — | — | — | not-in-catalog | HEAD `4b71aaae`; `openai_compatible` `/api/v1`, Chat/SSE/tools/images/discovery; 889+5 testes e fmt verdes; reasoning #3673; sem publicação |
| CLI-072 | P2 | NullClaw | `nullclaw/nullclaw` | concluida | `config-only` | `not-applicable` | `feat/omniroute-nullclaw-integration` | — | — | — | not-in-catalog | HEAD `d8a802fd`; custom `/api/v1`, Chat/Responses/Anthropic, tools/streaming/usage/images; Zig ausente, CI run 30788444193 verde; sem publicação |
| CLI-073 | P2 | Moltis | `moltis-org/moltis` | concluida | `config-only` | `not-applicable` | `feat/omniroute-moltis-integration` | — | — | — | not-in-catalog | HEAD `678d407`; `custom-omniroute`, `/api/v1`, `auto`, Chat/SSE/tools/reasoning/usage/images; 401 testes + fmt verdes; MCP/ACP separados; sem publicação |
| CLI-074 | P2 | GitClaw | `open-gitagent/gitclaw` | concluida | `config-only` | `not-applicable` | `feat/omniroute-gitclaw-integration` | — | — | — | not-in-catalog | GitAgent HEAD `d3e25d7`; base `/api/v1`, `omniroute:auto`, Chat/SSE/tools/images; build + 65 testes + smoke verdes; reasoning=false no descriptor; sem publicação |
| CLI-075 | P2 | LionClaw | `moshthepitt/lionclaw` | concluida | `patch-required` / `issue-first` | `awaiting-maintainer` | `feat/omniroute-lionclaw-integration` | — | — | — | not-in-catalog | HEAD `cb59b23d`; Codex app-server não projeta config.toml/secret para runtime confinado; patch seguro necessário, alinhado à #157; gates locais bloqueados por uv/podman; CI verde; sem publicação |
| CLI-076 | P3 | VibePod | `VibePod/vibepod-cli` | concluida | `config-only` | `not-applicable` | `feat/omniroute-vibepod-integration` | — | — | — | not-in-catalog | Claude Code via `/api`, container usa `host.docker.internal`; Codex não injeta chave; compileall verde, pytest bloqueado por typer; sem publicação |
| CLI-077 | P3 | zeroshot | `the-open-engine/zeroshot` | concluida | `config-only` | `not-applicable` | `feat/omniroute-zeroshot-integration` | — | — | — | not-in-catalog | Gateway OpenAI `/api/v1`, `auto`, tools fail-closed; 22 testes + build verdes; sem streaming JSON/reasoning/MCP no gateway; sem publicação |
| CLI-078 | P3 | Fractal | `plasma-ai/fractal` | concluida | `config-only` / `needs-wrapper` | `awaiting-maintainer` | `feat/omniroute-fractal-integration` | — | — | — | not-in-catalog | Codex Responses por node `CODEX_HOME`; caveat tmux quente não encaminha `OMNIROUTE_API_KEY`; fix genérico recomendado, sem PR |
| CLI-079 | P3 | Bernstein | `chernistry/bernstein` | concluida | `config-only` | `not-applicable` | `feat/omniroute-bernstein-integration` | — | — | — | not-in-catalog | Canonical `sipyourdrink-ltd/bernstein`; openai_agents `/api/v1`, auto, api_key_env allowlisted; testes bloqueados por openai ausente; sem publicação |
| CLI-080 | P3 | Traycer | `traycerai/traycer` | concluida | `config-only` indireto | `not-applicable` | `feat/omniroute-traycer-integration` | — | — | — | not-in-catalog | Harness OpenCode + provider `@ai-sdk/openai-compatible`, `/api/v1`, `omniroute/auto`; host central fechado; sem publicação |
| CLI-081 | P3 | h5i | `h5i-dev/h5i` | concluida | `patch-required` | `awaiting-maintainer` | `feat/omniroute-h5i-integration` | — | — | — | not-in-catalog | Auth proxy/egress Codex fixos em OpenAI anulam base custom; patch seguro/policy-pinned necessário; CI externa verde; sem publicação |
| CLI-082 | P3 | OMK | `dmae97/open-multi-agent-kit` | concluida | `viable-mcp` | `not-applicable` | `feat/omniroute-omk-integration` | — | — | — | not-in-catalog | pesquisa concluída neste lote; controle multiagente, MCP é caminho primário; sem provider nominal |
| CLI-083 | P3 | kodo | `ikamensh/kodo` | concluida | `config-only` indireto | `not-applicable` | `feat/omniroute-kodo-integration` | — | — | — | not-in-catalog | pesquisa concluída neste lote; orquestrador/agent child, propagar env/base/model ao agente filho |
| CLI-084 | P3 | ORCH | `oxgeneral/ORCH` | concluida | `needs-wrapper` | `awaiting-maintainer` | `feat/omniroute-orch-integration` | — | — | — | not-in-catalog | pesquisa concluída neste lote; fila/controle sem provider LLM direto, wrapper/adaptador necessário |
| CLI-085 | P3 | LoopTroop | `LoopTroop-ai/LoopTroop` | concluida | `config-only` indireto | `not-applicable` | `feat/omniroute-looptroop-integration` | — | — | — | not-in-catalog | HEAD `cbfc81c5`; OpenCode recebe provider `@ai-sdk/openai-compatible`, `/api/v1`, `omniroute/auto`; 16 testes verdes; sem publicação |
| CLI-086 | P3 | Galley | `shinpr/galley` | concluida | `patch-required` | `awaiting-maintainer` | `feat/omniroute-galley-integration` | — | — | — | not-in-catalog | HEAD `6bcc593d`; registry/transports fechados, requer transport OpenAI-compatible para executor e supervisor; Go ausente; sem publicação |
| CLI-087 | P3 | Relay | `jcast90/relay` | concluida | `config-only` | `not-applicable` | `feat/omniroute-relay-integration` | — | — | — | not-in-catalog | HEAD `7bd5a2f6`; provider profile Codex com `OPENAI_BASE_URL`, key ref e modelo; smoke Responses obrigatório; MCP separado; sem publicação |
| CLI-088 | P3 | SageCLI | `youwangd/SageCLI` | concluida | `config-only` indireto | `not-applicable` | `feat/omniroute-sagecli-integration` | — | — | — | not-in-catalog | HEAD `c167712d`; Codex runtime, base/key configuradas fora do Sage; env plaintext caveat; 45 testes verdes; sem publicação |
| CLI-089 | P3 | 5dive | `5dive-ai/5dive` | concluida | `patch-required` | `awaiting-maintainer` | `feat/omniroute-5dive-integration` | — | — | — | not-in-catalog | HEAD `b64b6dac`; provider/base maps fechados; patch OpenAI-compatible genérico; 50 testes focados verdes; sem publicação |
| CLI-090 | P3 | agx | `ramarlina/agx` | concluida | `config-only` indireto | `not-applicable` | `feat/omniroute-agx-integration` | — | — | — | not-in-catalog | HEAD `e674cec1`; Codex herda base/key/model; smoke Responses e governança `--full-auto`; Jest ausente; sem publicação |
| CLI-091 | P3 | claude-code-router | `musistudio/claude-code-router` | concluida | `config-only` | `not-applicable` | `feat/omniroute-claude-code-router-integration` | — | — | — | not-in-catalog | HEAD `bc8a8e62`; provider custom OpenAI/Anthropic/Gemini, Chat/Responses; smoke por protocolo; sem publicação |
| CLI-092 | P3 | cc-router | `finch-xu/cc-router` | concluida | `config-only` | `not-applicable` | `feat/omniroute-cc-router-integration` | — | — | — | not-in-catalog | HEAD `c4c7579`; custom Responses/Chat com base/path/header, SSE/tools/reasoning; cargo bloqueado por glib; sem publicação |
| CLI-093 | P3 | OneCLI | `onecli/onecli` | concluida | `config-only` | `not-applicable` | `feat/omniroute-onecli-integration` | — | — | — | not-in-catalog | HEAD `84ccaf74`; MITM credential gateway, generic host injection; MCP separado; sem publicação |
| CLI-094 | P3 | agent-browser | `vercel-labs/agent-browser` | concluida | `config-only` | `not-applicable` | `feat/omniroute-agent-browser-integration` | — | — | — | not-in-catalog | HEAD `01c1147d`; chat usa gateway Chat/SSE/tools com env key/model; base precisa validar sufixo `/v1` para não duplicar path; cargo test exit 0; sem publicação |
| CLI-095 | P3 | OpenWork | `different-ai/openwork` | concluida | `config-only` indireto | `not-applicable` | `feat/omniroute-openwork-integration` | — | — | — | not-in-catalog | HEAD `ecb7a5f0`; OpenCode custom provider `/api/v1`, auth gerenciada; sem testes/deps; sem publicação |
| CLI-096 | P3 | Agent Deck review | `asheshgoplani/agent-deck` | concluida | `config-only` indireto | `not-applicable` | `feat/omniroute-agent-deck-review` | — | — | — | integrated | HEAD `46300807`; env/model propagados a Codex/OpenCode; Go ausente; sem publicação |
| CLI-097 | P4 | Pool | `poolsideai/pool` | concluida | `config-only` | `not-applicable` | `feat/omniroute-pool-integration` | — | — | — | not-in-catalog | HEAD `a6fe0ca1`; `pool exec --api-url` OpenAI-compatible, sandbox required, MCP/ACP separado; EULA; sem publicação |
| CLI-098 | P4 | Junie CLI | `junie.jetbrains.com` | concluida | `config-only` | `not-applicable` | `feat/omniroute-junie-integration` | — | — | — | not-in-catalog | HEAD `d2701be6`; custom profile OpenAICompletion/Responses com baseUrl full e env ref; runtime proprietário/EAP; sem publicação |
| CLI-099 | P4 | Cursor desktop | Anysphere | concluida | `config-only` limitado | `awaiting-maintainer` | `feat/omniroute-cursor-desktop-integration` | — | — | — | integrated | disclosure-only; BYO key/chat panel; Composer/Tab nativos; privado/MITM proibido; sem publicação |
| CLI-100 | P4 | Windsurf | Codeium | concluida | `blocked-closed` / MCP-only | `awaiting-maintainer` | `feat/omniroute-windsurf-integration` | — | — | — | not-in-catalog | sem upstream/base custom; BYOK Anthropic específico; MCP separado; MITM proibido; sem publicação |
| CLI-101 | P4 | Amp | Sourcegraph | concluida | `config-only` parcial / Enterprise-gated | `awaiting-maintainer` | `feat/omniroute-amp-integration` | — | — | — | not-in-catalog | CLI fechada/Amp Server; confirmar provider custom com suporte; MCP viável; sem publicação |
| CLI-102 | P4 | Amazon Q/Kiro CLI | AWS | concluida | `patch-required` legado / `blocked-closed` Kiro | `awaiting-maintainer` | `feat/omniroute-amazon-q-integration` | — | — | — | integrated | Q usa AWS EventStream/SigV4; Kiro fechado sem base custom; MCP-only seguro; sem publicação |
| CLI-103 | P4 | Cowork | Anthropic | concluida | `blocked-closed` / MCP-only | `not-applicable` | — | — | — | — | not-in-catalog | inferência gerida pela Anthropic sem BYOK/base custom; Custom Connector MCP remoto; MITM proibido; sem publicação |
## Como atualizar
Ao terminar uma fase, alterar somente os campos comprovados e deixar os demais como `—`. Para uma integracao concluida, registrar: versao/commit pesquisado, mecanismo, arquivos modificados, testes, branch, commit, URL de PR/issue e resposta do mantenedor. Se o caso for apenas configuracao, registrar o comando/config real e marcar `config-only` ou `viable-direct`, sem criar uma PR artificial.
Antes de publicar uma contribuicao, aplicar o gate e o checklist de
`05-plano-publicacao-prs-upstream.md`.

View File

@@ -1,659 +0,0 @@
# Plano de publicacao de integracoes OmniRoute nos repositorios upstream
> **Status da campanha de pesquisa:** `104/104` casos concluídos. Este plano continua sendo o procedimento de execução e publicação. A matriz final, inclusive os casos em que PR é inadequada ou impossível, está em `06-relatorio-final-104-clis-e-estrategia-prs.md`.
**Data:** 2026-08-01
**Escopo:** transformar a fila `CLI-000` a `CLI-103` em contribuicoes upstream verificadas,
publicando PR, issue, guia de configuracao, adaptador ou conclusao de bloqueio conforme o mecanismo
real de cada projeto.
**Documentos-base:** `01-relatorio-pesquisa-clis-omniroute.md`,
`02-prioridade-integracoes-clis.md`, `03-plano-integracao-em-lotes.md` e
`04-tracker-integracoes-clis.md`.
## 1. Resultado esperado
Para cada repositorio pesquisado, a campanha deve produzir exatamente um resultado principal:
1. **PR upstream de integracao nominal:** adiciona provider/preset `omniroute`, configuracao,
documentacao e testes quando isso combina com a arquitetura do projeto.
2. **PR upstream de compatibilidade generica:** melhora suporte a endpoint customizado sem acoplar
o projeto ao nome OmniRoute, acompanhado de documentacao comprovando o uso com OmniRoute.
3. **PR somente de documentacao:** registra uma configuracao funcional quando o codigo ja suporta
OmniRoute e o upstream aceita guias de terceiros.
4. **Issue-first:** solicita decisao de arquitetura ou permissao antes do patch quando a politica do
repositorio, o desenho de providers ou o tamanho da mudanca exigirem alinhamento.
5. **Configuracao sem PR:** documenta no OmniRoute um fluxo que ja funciona e para o qual uma mudanca
upstream seria redundante ou rejeitada pela politica do projeto.
6. **Adaptador ACP/MCP/wrapper:** contribui no ponto de extensao correto quando o projeto nao consome
diretamente APIs de modelos.
7. **MITM, produto fechado ou bloqueado:** registra evidencia e nao fabrica uma contribuicao que o
upstream nao pode receber.
O objetivo e tentar integrar todos os casos tecnicamente possiveis. O objetivo nao e abrir uma PR em
todo repositorio independentemente da arquitetura, licenca ou politica de contribuicao.
## 2. Regras da campanha
- Trabalhar em lotes de no maximo tres repositorios, com um subagente por repositorio.
- Usar uma worktree isolada por repositorio dentro de `.claude/worktrees/`.
- Nao editar implementacoes no checkout compartilhado.
- Nao usar `git stash` ou `git pop`.
- Fazer pesquisa fresca no commit atual do upstream antes de criar branch ou editar arquivos.
- Ler `README`, `CONTRIBUTING`, templates de issue/PR, `SECURITY`, licenca e instrucoes locais de
agentes antes da implementacao.
- Procurar issues e PRs abertas/fechadas sobre custom provider, base URL, OpenAI-compatible,
Anthropic-compatible, Gemini endpoint, proxy, gateway e OmniRoute antes de propor uma mudanca.
- Registrar a base pesquisada por commit SHA ou release. Nao usar apenas `main` como evidencia.
- Executar baseline antes da mudanca e distinguir falhas preexistentes de regressao.
- Nunca expor `OMNIROUTE_API_KEY` ou qualquer outra credencial em comandos publicados, fixtures,
logs, commits, screenshots, PRs ou issues.
- Nao inserir trailers, assinaturas ou rodapes de IA em commits, PRs ou issues.
- Nao afirmar que uma integracao funciona sem um teste reproduzivel ou uma limitacao explicitamente
registrada.
- Nao inventar fork, branch, commit, PR, issue, CI ou resposta de mantenedor.
- Atualizar `04-tracker-integracoes-clis.md` ao concluir cada fase material.
## 3. Unidade de trabalho por repositorio
Cada item `CLI-NNN` deve possuir uma task individual. A task e o pacote de contexto entregue ao
subagente e o registro que permite retomar o trabalho sem repetir ou perder evidencias.
### 3.1 Cabecalho obrigatorio da task
```md
# CLI-NNN - <projeto> - integracao OmniRoute upstream
- Repositorio canonico: <URL>
- Prioridade/lote: <P0-P4 / lote>
- Estado no catalogo OmniRoute: <integrated/not-in-catalog/parcial>
- Evidencia inicial: <resumo vindo do relatorio; ainda nao confirmado>
- Worktree: <caminho isolado>
- Branch planejada: <definir somente depois de ler as regras upstream>
- Commit/release pesquisado: —
- Responsavel: <agente>
- Estado: researching
```
### 3.2 Pesquisa obrigatoria dentro da task
O subagente deve responder, com links e caminhos de codigo:
1. Qual e o repositorio canonico, commit/release atual, licenca e nivel de atividade?
2. Contribuicoes de forks externos sao aceitas? Ha CLA, DCO, sign-off ou issue previa obrigatoria?
3. Qual e a arquitetura de providers e qual e o menor ponto de extensao?
4. O cliente usa Chat Completions, Responses, Anthropic Messages, Gemini, ACP, MCP ou protocolo
proprietario?
5. A base URL esperada e raiz, `/v1`, `/v1beta` ou uma URL completa por operacao?
6. O cliente acrescenta algum sufixo automaticamente? Pode duplicar `/v1` ou `/v1beta`?
7. Como a autenticacao e resolvida: variavel de ambiente, arquivo, keyring, OAuth ou header custom?
8. Como os modelos sao definidos ou descobertos? O cliente chama um endpoint de modelos?
9. Streaming, tool calling, reasoning, imagens e cancelamento funcionam pelo caminho escolhido?
10. Ja existe issue, PR, discussao ou documentacao para endpoints customizados ou OmniRoute?
11. Quais comandos oficiais executam install, format, lint, typecheck, build e testes?
12. Qual contribuicao agrega valor real: codigo nominal, compatibilidade generica, docs, issue,
wrapper, MCP/ACP, somente configuracao ou nenhum patch?
### 3.3 Gate de contribuicao
Antes de editar, preencher uma decisao:
| Decisao | Quando usar | Saida esperada |
|---|---|---|
| `pr-provider` | O upstream possui catalogo/presets de providers | Provider/preset OmniRoute, docs e testes |
| `pr-generic` | Falta uma capacidade generica necessaria, como base URL customizavel | Patch generico, docs e teste com OmniRoute |
| `pr-docs` | O codigo ja funciona e o upstream aceita guias de integracao | Guia minimo e validado |
| `issue-first` | Mudanca arquitetural, politica incerta ou mantenedor exige proposta | Issue com evidencia e desenho do patch |
| `config-only` | Tudo funciona por configuracao e um PR seria redundante | Guia no OmniRoute e smoke test |
| `adapter-acp` | ACP e o ponto real de integracao | Adaptador/registro ACP e testes |
| `adapter-mcp` | MCP e o ponto real de integracao | Config/servidor MCP e testes |
| `wrapper` | O projeto apenas lanca outro agente | Wrapper/env forwarding e teste do filho |
| `needs-mitm` | Endpoint fechado ou fixo | Pesquisa/guia MITM separado; sem PR artificial |
| `blocked` | Licenca, politica, build ou protocolo impedem progresso | Evidencia reproduzivel e proximo desbloqueio |
O gate deve incluir a alternativa rejeitada. Exemplo: `pr-provider` escolhido porque o repositorio
mantem presets nomeados; `pr-docs` rejeitado porque a configuracao exigiria cinco campos internos e
nao seria uma experiencia suportada.
## 4. Ciclo completo da PR
### Fase PR-0 - Preparar o contexto
- Reservar o item no tracker e marcar pesquisa em andamento.
- Confirmar que nenhum outro agente esta trabalhando no mesmo repositorio.
- Resolver o repositorio canonico, fork existente e permissao de contribuicao.
- Criar a task individual com a evidencia inicial marcada como hipotese.
- Criar a worktree isolada somente depois de confirmar o upstream correto.
### Fase PR-1 - Pesquisar upstream e contribuicoes existentes
- Ler integralmente as regras do repositorio aplicaveis aos arquivos que podem mudar.
- Mapear provider registry, configuracao, transporte HTTP, auth, modelo, streaming e ferramentas.
- Pesquisar issues/PRs por termos de compatibilidade e pelo nome OmniRoute.
- Registrar commit/release, caminhos e links de evidencia na task.
- Escolher o gate de contribuicao da secao 3.3.
### Fase PR-2 - Baseline reproduzivel
- Instalar dependencias de acordo com o upstream.
- Rodar format check, lint, typecheck/build e testes relevantes antes do patch.
- Rodar um smoke test do caminho existente, mesmo que ele falhe por falta da integracao.
- Limpar chaves do ambiente nos testes que validem o comportamento sem credenciais.
- Registrar comando, codigo de saida, testes aprovados e falhas preexistentes.
- Se o projeto nao puder ser construido, tentar o ambiente documentado e registrar o bloqueio; nao
declarar regressao nem compatibilidade com base apenas na leitura do README.
### Fase PR-3 - Desenhar o menor patch aceitavel
A ordem de preferencia e:
1. Reusar a abstracao de provider ja existente.
2. Adicionar metadados/preset antes de criar codigo especial.
3. Reusar cliente OpenAI/Anthropic/Gemini ja presente.
4. Adicionar capacidade generica quando ela beneficiar outros gateways e for coerente com o projeto.
5. Criar executor/adapter dedicado somente quando o protocolo realmente divergir.
O patch normalmente deve cobrir:
- identificador e nome de exibicao `omniroute`, se presets nomeados forem aceitos;
- base URL correta e sem dupla concatenacao de versao;
- chave obtida de ambiente ou storage seguro;
- configuracao/descoberta de modelo;
- headers estritamente necessarios;
- streaming e tool calling preservados;
- mensagens de erro sem expor segredo;
- documentacao curta e executavel;
- testes unitarios/integracao alinhados ao padrao upstream.
Nao adicionar telemetria, dependencia, fluxo de login ou codigo de rede novo quando o provider
generico existente ja resolve o caso.
### Fase PR-4 - Implementar com teste primeiro
- Criar teste que demonstre a ausencia do preset, config ou comportamento requerido.
- Confirmar a falha pelo motivo esperado.
- Implementar o menor patch.
- Fazer o teste passar e executar testes adjacentes.
- Refatorar apenas o necessario para manter o padrao do upstream.
- Formatar somente os arquivos tocados, salvo exigencia contraria do repositorio.
Para PR somente de documentacao, substituir o teste vermelho por uma validacao real dos comandos e
do arquivo de configuracao documentado. Nao sintetizar exemplos que nao foram executados.
### Fase PR-5 - Validar contra OmniRoute
Escolher a matriz compativel com o cliente:
| Superficie | Base inicial esperada | Validacoes minimas |
|---|---|---|
| OpenAI Chat Completions | confirmar se o cliente espera raiz ou `/v1` | chamada simples, stream, tool call, erro de modelo |
| OpenAI Responses | confirmar regra de concatenacao do cliente | resposta simples, stream/eventos, tool call |
| Anthropic Messages | normalmente base antes de `/v1/messages`; confirmar no codigo | mensagem, stream, tools, headers de versao |
| Gemini | normalmente base antes das operacoes `v1beta`; confirmar no codigo | generateContent, streamGenerateContent, tools |
| ACP | endpoint/transport definido pelo protocolo | discovery, sessao, request e cancelamento |
| MCP | stdio, SSE ou Streamable HTTP conforme suporte | inicializacao, listagem e invocacao de ferramenta |
Registrar no resultado quais linhas da matriz foram executadas, omitidas ou bloqueadas. Um smoke
test simples nao deve ser apresentado como prova de tool calling ou streaming.
### Fase PR-6 - Revisar o diff antes de publicar
O agente responsavel faz uma auto-revisao e o agente principal verifica:
- aderencia a `CONTRIBUTING` e instrucoes locais;
- escopo minimo e ausencia de refactor oportunista;
- testes cobrindo config, URL, auth sem segredo e modelo;
- documentacao consistente com o codigo executado;
- ausencia de arquivos gerados, caches, logs ou credenciais;
- licenca e atribuicao preservadas;
- branch baseada no upstream atual;
- commits pequenos e com mensagem no estilo do projeto;
- ausencia de trailers ou texto de IA;
- `git diff --check` e gates oficiais limpos, ou falhas preexistentes documentadas.
Uma PR nao deve ser publicada enquanto houver alteracao sem explicacao, teste essencial faltando ou
duvida material sobre a politica do upstream.
### Fase PR-7 - Preparar a publicacao
- Confirmar fork e remotes sem sobrescrever branches existentes.
- Atualizar a branch sobre o ponto exigido pelo upstream usando operacao nao destrutiva.
- Enviar a branch ao fork somente depois da revisao.
- Criar PR contra a branch correta do repositorio canonico.
- Se a contribuicao externa estiver bloqueada, abrir issue-first e anexar o commit/patch de
referencia somente quando isso for permitido.
- Registrar URLs reais no tracker imediatamente apos a publicacao.
Convencoes de branch sugeridas, sujeitas ao padrao de cada upstream:
- `feat/omniroute-provider` para provider/preset nominal;
- `feat/custom-base-url` para capacidade generica;
- `docs/omniroute-setup` para documentacao validada;
- `fix/custom-endpoint-versioning` para correcao de raiz versus `/v1`/`/v1beta`.
### Fase PR-8 - Corpo da PR
Usar o template oficial do repositorio quando existir. Na ausencia de template, adaptar:
```md
## Why
Explain the user problem and the existing extension point. Avoid marketing claims.
## What changed
- Add or enable the smallest provider/configuration path required.
- Document the verified setup.
- Cover URL, authentication and model selection behavior with tests.
## Verification
- `<official upstream command>`
- `<focused test command>`
- `<sanitized OmniRoute smoke test and result>`
## Compatibility notes
- API surface: `<Chat Completions/Responses/Anthropic/Gemini/ACP/MCP>`
- Base URL rule: `<root, /v1, /v1beta or full operation URL>`
- Streaming: `<verified/not applicable/not verified>`
- Tool calling: `<verified/not applicable/not verified>`
## Scope
No unrelated refactors or credential changes.
```
O titulo deve descrever a mudanca, nao a campanha. Exemplos de formato, sujeitos ao estilo do
upstream: `Add OmniRoute provider preset`, `Support configurable OpenAI-compatible base URLs` ou
`Document OmniRoute as a custom endpoint`.
### Fase PR-9 - Issue-first ou fallback
Quando uma PR direta nao for apropriada, a issue deve conter:
- problema reproduzivel e publico afetado;
- ponto de extensao encontrado no codigo;
- proposta minima;
- compatibilidade esperada e protocolo;
- evidencia de teste ou prototipo;
- pergunta objetiva ao mantenedor;
- link para patch de referencia apenas se permitido.
Nao abrir simultaneamente issue e PR sem necessidade. Se o template exigir issue previa, esperar a
decisao ou seguir a politica declarada.
### Fase PR-10 - Acompanhar ate a decisao
Depois da publicacao:
- observar CI e checks obrigatorios;
- responder perguntas tecnicas com evidencia;
- corrigir somente o escopo da contribuicao ou pedidos claros do mantenedor;
- reexecutar testes depois de cada mudanca;
- registrar novos commits, revisoes e estado no tracker;
- marcar `accepted` somente depois de merge/aceite comprovado;
- marcar `rejected` com o motivo fornecido pelo upstream;
- se a PR ficar inativa, registrar `awaiting-maintainer`, sem declarar abandono prematuramente;
- manter o guia/catalogo OmniRoute coerente com o estado real do upstream.
O acompanhamento pode usar a skill `babysit` individualmente para uma PR aberta. Como essa skill
acompanha uma unica PR, nunca agrupar tres PRs em uma mesma execucao dela.
### Fase PR-11 - Fechar a task
Uma task individual termina com:
- pesquisa fresca e gate registrados;
- diff, configuracao ou bloqueio documentado;
- baseline e validacao final comparados;
- branch/commit reais, quando criados;
- PR/issue reais, quando publicados;
- status no catalogo OmniRoute;
- limitacoes e proximo passo;
- linha correspondente no tracker atualizada.
## 5. Estrategia de paralelizacao
### 5.1 Papeis por lote
- **Subagente A:** primeiro repositorio do lote; dono exclusivo da worktree e do diff upstream.
- **Subagente B:** segundo repositorio do lote; dono exclusivo da worktree e do diff upstream.
- **Subagente C:** terceiro repositorio do lote; dono exclusivo da worktree e do diff upstream.
- **Agente principal:** coordena o tracker, revisa gates/diffs, impede duplicacao e autoriza a
publicacao depois das evidencias.
Todos os agentes devem ser avisados de que nao estao sozinhos no workspace e nao podem reverter ou
sobrescrever mudancas de outros agentes.
### 5.2 Barreira do lote
O lote seguinte pode comecar quando os tres itens atuais tiverem, no minimo:
1. commit/release upstream pesquisado;
2. gate de contribuicao definido;
3. baseline registrado;
4. patch validado, configuracao comprovada ou bloqueio reproduzivel;
5. decisao de publicacao tomada;
6. tracker atualizado.
A espera por resposta de mantenedor nao bloqueia o lote seguinte. Depois de uma PR/issue publicada,
o item passa para acompanhamento e libera o slot de implementacao.
### 5.3 Limite de trabalho em progresso
- No maximo tres pesquisas/implementacoes ativas.
- Publicacoes aguardando mantenedor nao contam como slot de implementacao, mas ficam no tracker.
- No maximo uma task ativa por repositorio, inclusive forks ou variantes do mesmo upstream.
- Se dois itens resolverem o mesmo repositorio, consolidar a pesquisa e decidir se ha uma ou duas
contribuicoes antes de abrir branches.
## 6. Fila de publicacao
A ordem detalhada continua sendo a do `03-plano-integracao-em-lotes.md`. Esta secao define o objetivo
de publicacao de cada onda; a pesquisa individual pode promover, rebaixar ou mudar o tipo de
contribuicao.
### Onda 0 - referencia e infraestrutura da campanha
- `CLI-000` jcode: acompanhar issue upstream e PR de referencia; concluir a secao prometida no
README do OmniRoute.
- Preparar o modelo de task individual e aplicar o mesmo tracker a todos os novos repositorios.
### Onda 1 - P0.1 a P0.5
- `CLI-001` Gemini CLI: confirmar se o endpoint Gemini customizado pede apenas docs/config ou um
preset nominal.
- `CLI-002` Claw Code: confirmar provider OpenAI-compatible e propor preset/docs minimos.
- `CLI-003` Plandex: confirmar o registro de providers customizados e propor provider/preset.
- `CLI-004` MiMo Code: confirmar o adapter OpenAI-compatible e propor configuracao/provider.
- `CLI-005` Trae Agent: confirmar `model_providers` e propor entrada OmniRoute/documentacao.
- `CLI-006` Kimi CLI: escolher uma superficie suportada e evitar um patch que misture tres
protocolos sem testes.
- `CLI-007` Every Code: reutilizar a arquitetura herdada do Codex quando ainda aplicavel.
- `CLI-008` Open Codex: confirmar upstream canonico e propor provider multi-modelo.
- `CLI-009` VT Code: validar provider customizado, modelo e failover.
- `CLI-010` OpenHands CLI: verificar se `LLM_BASE_URL` torna o caso docs/config-only.
- `CLI-011` gptme: verificar se `OPENAI_BASE_URL` torna o caso docs/config-only.
- `CLI-012` Nanocoder: confirmar compatibilidade de tool calling e decidir preset versus docs.
- `CLI-013` RA.Aid: verificar se `OPENAI_API_BASE` torna o caso docs/config-only.
- `CLI-014` CoreCoder: verificar se `OPENAI_BASE_URL` torna o caso docs/config-only.
- `CLI-015` Grok CLI: confirmar se o endpoint e genericamente configuravel ou preso ao protocolo
Grok antes de propor patch.
### Onda 2 - P1.1 a P1.9
- `CLI-016` Gitlawb Zero: provider custom/flag; preferir docs ou preset pequeno.
- `CLI-017` DeepSeek Reasonix: confirmar repositorio, atividade e endpoint antes de qualquer PR.
- `CLI-018` KlaatCode: integrar via `customModels` ou preset se o catalogo aceitar nomes.
- `CLI-019` CodeMini CLI: validar `gateway.base_url` e sua regra de versao.
- `CLI-020` Zot: validar `--base-url` e `models.json`; docs-first se ja suficiente.
- `CLI-021` Octomind: confirmar variaveis de URL por provider e propor configuracao minima.
- `CLI-022` DvalinCode: confirmar o cliente OpenAI-compatible e testes disponiveis.
- `CLI-023` Coro Code: confirmar `OPENAI_BASE_URL`; docs-first se nao houver lacuna de codigo.
- `CLI-024` Mini-Kode: confirmar `MINIKODE_BASE_URL`; docs-first se nao houver lacuna de codigo.
- `CLI-025` Late CLI: testar ambiente e flag `api-url`; corrigir precedencia apenas se necessario.
- `CLI-026` Agentty: escolher entre provider direto e ACP conforme a arquitetura atual.
- `CLI-027` Aizen: validar `AIZEN_BASE_URL` e propor docs/preset.
- `CLI-028` Clif-Code: selecionar um unico protocolo principal para a primeira contribuicao.
- `CLI-029` Minacode: pesquisa confirmatoria antes de definir o tipo de PR.
- `CLI-030` YottaCode: confirmar gateway/provider e selecao de modelo.
- `CLI-031` aichat: integrar via configuracao de modelos ou provider nominal, conforme a politica.
- `CLI-032` ShellGPT: validar `API_BASE_URL` e decidir docs/config-only.
- `CLI-033` Mistral Vibe: confirmar base URL customizada e separar suporte generico de marca.
- `CLI-034` OpenSquilla: localizar o registro de gateways e propor provider/preset.
- `CLI-035` Kode CLI: escolher OpenAI, Anthropic ou Gemini com base na implementacao mais nativa.
- `CLI-036` Neovate Code: preferir plugin/provider oficial ao patch no core, se existir.
- `CLI-037` Deep Agents Code: contribuir no pacote CLI/provider correto, nao apenas no SDK generico.
- `CLI-038` OpenHands principal: evitar duplicar `CLI-010`; consolidar se ambos apontarem para o
mesmo mecanismo e upstream.
- `CLI-039` SWE-agent: confirmar backend de modelos e interface publica suportada.
- `CLI-040` AutoCodeRover: confirmar backend e propor config/provider minimo.
- `CLI-041` Claurst: revisar GPL e politica antes de redistribuir qualquer adaptacao.
- `CLI-042` Codebuff: confirmar se o provider e extensivel e se contribuicoes externas sao aceitas.
### Onda 3 - P2.1 a P2.11
- `CLI-043` Devon, `CLI-044` Letta Code e `CLI-045` CodeMachine CLI: pesquisar backend real;
revisar a entrada local ja existente de Letta antes de nova PR.
- `CLI-046` Groq Code CLI, `CLI-047` Dexto e `CLI-048` claw-code-agent: confirmar endpoints,
protocolos e maturidade antes do patch.
- `CLI-049` g3, `CLI-050` San e `CLI-051` Waveloom: localizar a abstracao de provider e preferir
implementacao generica.
- `CLI-052` picocode, `CLI-053` QQCode e `CLI-054` Keen Code: validar configuracao multi-modelo e
documentar o caminho minimo.
- `CLI-055` Grinta, `CLI-056` Zap e `CLI-057` Binharic: escolher o provider compativel com melhor
cobertura de streaming/tools.
- `CLI-058` Darce, `CLI-059` CLAII e `CLI-060` nori-cli: separar integracao de modelo de MCP e de
codigo herdado do Codex.
Resultado P2.6:
- `CLI-058` Darce: `config-only`, sem PR necessária; usar `DARCE_API_BASE` na raiz e `DARCE_MODEL`.
- `CLI-059` CLAII: patch genérico local validado, mas publicação bloqueada pela declaração upstream
`All Rights Reserved`/ausência de licença OSS; só reconsiderar com autorização jurídica explícita.
- `CLI-060` nori-cli: `config-only` via agente ACP customizado OpenCode; não alterar backend Codex;
MCP deve ser configurado uma vez, em Nori ou OpenCode, para evitar duplicação de tools.
- `CLI-061` cursor-agent clone, `CLI-062` Free Code e `CLI-063` Claude Engineer: revisar origem,
licenca e politica do fork antes de publicar.
Lote P2.7 reservado em 2026-08-02, na branch-base local `release/v3.8.50` em
`35405be6020696a7c66158ea7a25f06d61ff88ff`. Os três upstreams foram clonados em worktrees
separadas, indexados e delegados. Nenhuma publicação está autorizada; patches só podem surgir após
prova RED→GREEN e permanecem sem commit até revisão central.
Resultado P2.7:
- `CLI-061` cursor-agent clone: `config-only`; OpenAI usa base com `/v1`, Anthropic usa raiz sem
`/v1`; tools/tool-result foram comprovados nos dois protocolos. O factory rejeita `auto` puro,
mas isso não impede uso com modelos reconhecíveis ou classes diretas. Sem PR.
- `CLI-062` Free Code: `config-only` com `ANTHROPIC_BASE_URL` na raiz e `model=auto`; stream,
tools/tool-result e MCP nativo foram comprovados. O repo canônico agora é `freecodexyz/free-code`,
mas não há licença e o README atribui o código à Anthropic; publicação bloqueada.
- `CLI-063` Claude Engineer: endpoint/chave funcionam como `config-only` com modelo fixo. A lacuna
de `ANTHROPIC_MODEL` já está coberta pela PR #250; não criar patch concorrente. Arquivo de licença
segue ausente apesar da issue #116, portanto publicação permanece bloqueada.
- `CLI-064` Smol Developer, `CLI-065` Agentless e `CLI-066` Amazon Q Developer CLI: decidir entre
SDK/adaptador, config de modelo ou bloqueio por autenticacao.
Lote P2.8 iniciado em 2026-08-02 na branch-base local `release/v3.8.50`, SHA
`35405be6020696a7c66158ea7a25f06d61ff88ff`, com clones limpos e separados. Smol Developer será
testado primeiro como integração do SDK OpenAI legado; Agentless será avaliado por backend
OpenAI/Anthropic/DeepSeek; Amazon Q Developer CLI será tratado como protocolo AWS próprio, com MCP
avaliado separadamente. Não criar adaptador grande para Amazon Q nem qualquer publicação antes de
issue-first/coordenação exigida por `CONTRIBUTING.md`. Estado inicial: nenhum commit, fork, push,
PR, issue ou Discussion.
Resultado P2.8:
- `CLI-064` Smol Developer: `config-only`; `OPENAI_API_BASE` com `/v1` e `model=auto` passaram no
CLI, biblioteca e Agent Protocol histórico. Não há lacuna provider-specific e a PR #134 já cobre
uma expansão LiteLLM. Sem publicação.
- `CLI-065` Agentless: `config-only` pelo backend OpenAI, incluindo embeddings. Anthropic normal
também funciona; cache/tools exige SDK histórico e DeepSeek possui host fixo, mas essas melhorias
não são necessárias para integrar o projeto e propostas LiteLLM anteriores foram fechadas. Sem
publicação.
- `CLI-066` Amazon Q Developer CLI: MCP stdio é a integração direta; o backend de modelo fala AWS
JSON/EventStream e precisa de wrapper/backend novo. O upstream está em manutenção crítica e exige
issue-first; não preparar PR nominal ou adaptador surpresa. Sem publicação.
Estado final P2.8: commits `0`, pushes `0`, forks `0`, PRs `0`, issues `0`, Discussions `0`.
Próxima fila: P2.9 (`CLI-067` nanobot, `CLI-068` ZeroClaw, `CLI-069` NanoClaw), usando no máximo
três worktrees/agentes e repetindo a pesquisa individual antes de qualquer patch.
Lote P2.9 iniciado em 2026-08-03 sobre a branch-base local `release/v3.8.50`, SHA
`84b1e5e12f238269e698f400766230f985f4a07b`. O checkout principal já continha uma alteração do
operador em `CLAUDE.md`, preservada fora do escopo. As worktrees foram recriadas e os upstreams
foram clonados nos HEADs `44b7e1bf4` (nanobot), `4770420ab` (ZeroClaw) e `dfac7e0af` (NanoClaw).
Os três índices Codebase Memory moderate estão ready, sem skipped, e a pesquisa foi delegada a um
agente por repositório. Nenhuma publicação está autorizada; o estado inicial continua: commits `0`,
pushes `0`, forks `0`, PRs `0`, issues `0`, Discussions `0`.
- `CLI-067` nanobot, `CLI-068` ZeroClaw e `CLI-069` NanoClaw: validar providers OpenClaw/Anthropic
e evitar assumir que todos aceitam a mesma base URL.
Resultado P2.9:
- `CLI-067` nanobot: `config-only` pelo provider dinâmico OpenAI-compatible. A base correta inclui
`/api/v1`; `omniroute/auto` seleciona o provider custom e envia `auto` no wire. Chat, SSE, tools,
reasoning, usage, imagens, discovery e retry foram validados. Sem publicação upstream.
- `CLI-068` ZeroClaw: `config-only` pela família `custom`, com `uri=/v1`, modelo `auto`, wire Chat e
`native_tools=true`. Responses é opt-in. Suite de provider, config, fmt e smoke HTTP passaram.
Sem provider nominal ou publicação upstream.
- `CLI-069` NanoClaw: `config-only` pelo provider Claude existente, apontando a raiz Anthropic do
OmniRoute sem `/v1/messages` e usando OneCLI para a credencial. Codex e OpenCode têm bloqueios
upstream reproduzidos (#3155/#1984/#2985) e ficam fora do caminho de produção atual.
Estado final P2.9: commits `0`, pushes `0`, forks `0`, PRs `0`, issues `0`, Discussions `0`.
Progresso da pesquisa: `70/104` (`67,3%`); pendentes: `34/104` (`32,7%`). Próxima fila: P2.10
(`CLI-070` PicoClaw, `CLI-071` IronClaw, `CLI-072` NullClaw).
- `CLI-070` PicoClaw, `CLI-071` IronClaw e `CLI-072` NullClaw: localizar traits/registries e propor
um provider pequeno com testes.
- `CLI-073` Moltis, `CLI-074` GitClaw e `CLI-075` LionClaw: confirmar atividade, provider e comandos
de validacao antes da publicacao.
### Onda 4 - P3, integracoes indiretas
- `CLI-076`, `CLI-077`, `CLI-078`, `CLI-079`, `CLI-080` e `CLI-081`: pesquisar forwarding de
ambiente/configuracao para os agentes filhos;
publicar wrapper ou docs somente quando houver um ponto de extensao real.
- `CLI-082`, `CLI-083`, `CLI-084`, `CLI-085`, `CLI-086`, `CLI-087`, `CLI-088`, `CLI-089` e
`CLI-090`: escolher ACP, MCP, launcher ou integracao do agente filho; nao apresentar uma
integracao de orquestrador como provider de modelo.
- `CLI-091` e `CLI-092`: tratar como interoperabilidade entre proxies; documentar loops, headers,
auth e riscos antes de propor codigo.
- `CLI-093` e `CLI-094`: integrar como broker/ferramenta MCP somente se isso estiver no escopo dos
projetos.
- `CLI-095` e `CLI-096`: configurar o agente filho e revisar a entrada existente de Agent Deck.
### Onda 5 - P4, fechados, EULA e MITM
- `CLI-097` Pool: confirmar o que a EULA permite; priorizar configuracao local e nao presumir PR.
- `CLI-098` Junie CLI: pesquisar canal oficial de feedback; sem repositorio publico confirmado, nao
existe fila de PR.
- `CLI-099` Cursor desktop, `CLI-100` Windsurf, `CLI-101` Amp, `CLI-102` Amazon Q/Kiro CLI e
`CLI-103` Cowork: tratar como MITM, configuracao de produto ou pedido oficial de feature. So mover
para PR se um repositorio publico e uma politica de contribuicao forem comprovados.
## 7. Prompt operacional para cada subagente
O agente principal deve adaptar e enviar este prompt para cada item:
```text
Voce e responsavel exclusivamente por CLI-NNN - <projeto> no repositorio <URL>.
Voce nao esta sozinho no workspace: nao reverta, sobrescreva ou reorganize mudancas de outros
agentes. Trabalhe somente na worktree isolada atribuida dentro de .claude/worktrees/ e nunca use
git stash/pop.
Primeiro pesquise o upstream atual. Leia README, CONTRIBUTING, licenca, templates e instrucoes locais.
Registre commit/release, arquitetura de providers, config/base URL, protocolo, auth, modelos,
streaming, tool calling, issues/PRs existentes e comandos oficiais de build/test. A evidencia inicial
do relatorio e uma hipotese, nao uma conclusao.
Antes de editar, classifique o caso como pr-provider, pr-generic, pr-docs, issue-first, config-only,
adapter-acp, adapter-mcp, wrapper, needs-mitm ou blocked, com justificativa. Execute o baseline e
registre falhas preexistentes. Se houver patch, trabalhe com teste primeiro e implemente somente a
menor integracao coerente com o upstream. Confirme raiz versus /v1 versus /v1beta, autenticacao,
modelo, streaming e tool calling conforme aplicavel.
Nao publique nada antes da revisao do agente principal. Entregue: pesquisa com links/caminhos,
gate, baseline, diff, testes, smoke test sanitizado, riscos, branch/commit local se criados e a
atualizacao proposta para 04-tracker-integracoes-clis.md. Nao invente dados e nao exponha chaves.
```
## 8. Checklist de autorizacao para enviar uma PR
O agente principal somente autoriza a publicacao quando todas as respostas forem `sim` ou houver
uma excecao registrada:
- [ ] O repositorio canonico e a branch-alvo foram confirmados.
- [ ] A politica aceita o tipo de contribuicao planejado.
- [ ] Issues/PRs duplicadas foram pesquisadas.
- [ ] O commit/release de base esta registrado.
- [ ] O gate de contribuicao esta justificado.
- [ ] O baseline foi executado e falhas preexistentes estao separadas.
- [ ] O patch e o menor necessario e segue a arquitetura upstream.
- [ ] A base URL e sua regra de versao foram verificadas no codigo e em runtime.
- [ ] Auth/modelos foram testados sem vazar segredo.
- [ ] Streaming/tool calling foram testados ou marcados explicitamente como nao aplicaveis.
- [ ] Testes, lint, format, typecheck/build relevantes foram executados.
- [ ] A documentacao foi executada e corresponde ao codigo.
- [ ] O diff nao contem caches, builds, logs, credenciais ou refactors sem relacao.
- [ ] O titulo e o corpo seguem o template upstream e nao contêm marketing ou texto de IA.
- [ ] O tracker esta pronto para receber branch, commit e URL reais.
## 9. Campos adicionais recomendados no tracker
O tracker atual deve continuar como fonte principal. Durante a execucao, registrar nas observacoes ou
em uma nota individual:
- commit/release pesquisado;
- decisao `pr-provider`, `pr-generic`, `pr-docs`, `issue-first`, `config-only`, adapter, wrapper,
MITM ou bloqueio;
- protocolo e regra da base URL;
- comandos de baseline e resultado;
- comandos finais e resultado;
- smoke tests realizados;
- arquivos modificados;
- fork, branch e commit;
- PR/issue e estado de CI/review;
- limitacoes e proximo passo.
Campos ainda nao comprovados permanecem `—`.
## 10. Inicio recomendado
O primeiro ciclo de publicacao deve usar o lote P0.1:
1. `CLI-001` - Gemini CLI (`google-gemini/gemini-cli`)
2. `CLI-002` - Claw Code (`ultraworkers/claw-code`)
3. `CLI-003` - Plandex (`plandex-ai/plandex`)
Os tres subagentes fazem pesquisa fresca e implementacao em paralelo, mas nenhuma PR e enviada antes
da revisao individual do agente principal. Ao publicar ou concluir config-only/bloqueio, atualizar o
tracker e liberar os mesmos tres slots para o lote P0.2.
## Lote P2.10 iniciado em 2026-08-03
Base local: `release/v3.8.50` em `84b1e5e12f238269e698f400766230f985f4a07b`. Worktrees isoladas e um agente por upstream foram criadas para `CLI-070` PicoClaw, `CLI-071` IronClaw e `CLI-072` NullClaw. Nenhuma publicação está autorizada; os agentes devem pesquisar o HEAD atual, provar `config-only` ou RED→GREEN e registrar governança, gates, smoke e estado limpo.
Resultado P2.10:
- `CLI-070` PicoClaw: `config-only`, `openai/auto` com base `/api/v1`; Chat/SSE/tools/usage/images/discovery. Go ausente impediu execução local; monitorar #3298, sem PR.
- `CLI-071` IronClaw: `config-only`, `openai_compatible` com `/api/v1` e `auto`; 889 testes do crate LLM, 5 de resolução e fmt passaram. Sem PR; reasoning proprietário segue limitado por #3673.
- `CLI-072` NullClaw: `config-only`, provider custom com Chat Completions recomendado e Responses/Anthropic como alternativas. Zig ausente; CI do mesmo HEAD verde. Sem PR.
Estado final P2.10: commits `0`, pushes `0`, forks `0`, PRs `0`, issues `0`, Discussions `0`.
Pesquisa acumulada: `73/104` (`70,2%`); pendentes: `31/104` (`29,8%`). Próxima fila: P2.11 (`CLI-073` Moltis, `CLI-074` GitClaw, `CLI-075` LionClaw).
Resultado P3.1:
- `CLI-076` VibePod: `config-only` pelo agente Claude Code com raiz Anthropic `/api`; wrapper injeta env no container. Codex sem chave automática permanece não comprovado.
- `CLI-077` zeroshot: `config-only` pelo gateway OpenAI `/api/v1`; 22 testes focados verdes; limitações de streaming JSON, reasoning e MCP registradas.
- `CLI-078` Fractal: `config-only` por Codex Responses em `CODEX_HOME` por node; servidores tmux quentes podem perder `OMNIROUTE_API_KEY`, recomendando fix genérico upstream.
Estado final P3.1: commits `0`, pushes `0`, forks `0`, PRs `0`, issues `0`, Discussions `0`. Pesquisa acumulada: `79/104` (`76,0%`); pendentes: `25/104` (`24,0%`).
Resultado P3.2: Bernstein `config-only` por openai_agents; Traycer `config-only` indireto pelo harness OpenCode; h5i `patch-required` porque auth proxy/egress são fixados em OpenAI. Nenhuma publicação externa. Pesquisa acumulada `82/104` (`78,8%`), pendentes `22/104` (`21,2%`).
Resultado P2.11:
- `CLI-073` Moltis: `config-only`, provider `custom-omniroute`, `/api/v1`, `auto`, Chat/SSE/tools e capacidades multimodais. 401 testes e fmt passaram. Sem publicação.
- `CLI-074` GitClaw/GitAgent: `config-only`, loader OpenAI-compatible com `GITAGENT_MODEL_BASE_URL`, `OPENAI_API_KEY` e `omniroute:auto`. Build, 65 testes e smoke passaram. Sem publicação.
- `CLI-075` LionClaw: `patch-required`/`issue-first`. O runtime Codex confinado não recebe `config.toml`/provider secret; preparar proposta genérica alinhada à [#157](https://github.com/moshthepitt/lionclaw/issues/157), sem PR até revisão do mantenedor.
Estado final P2.11: commits `0`, pushes `0`, forks `0`, PRs `0`, issues `0`, Discussions `0`. Pesquisa acumulada: `76/104` (`73,1%`); pendentes: `28/104` (`26,9%`).
Resultado P3.3: OMK `viable-mcp`; kodo `config-only` indireto; ORCH `needs-wrapper`. Pesquisa acumulada `85/104` (`81,7%`), pendentes `19/104` (`18,3%`). Nenhuma publicação externa.
Resultado P3.4: LoopTroop `config-only` indireto via provider OpenCode; Galley `patch-required` por não possuir transport OpenAI-compatible configurável; Relay `config-only` via provider profile/Codex, condicionado a smoke da Responses API e controles sobre ferramentas nativas. Nenhuma publicação externa. Pesquisa acumulada `88/104` (`84,6%`), pendentes `16/104` (`15,4%`).
Resultado P3.5: SageCLI `config-only` indireto via Codex, com caveat de env plaintext; 5dive `patch-required` por mapas fechados de provider/base; agx `config-only` indireto via Codex e com gates de Responses/sandbox. Pesquisa acumulada `91/104` (`87,5%`), pendentes `13/104` (`12,5%`). Nenhuma publicação externa.
Resultado P3.6: claude-code-router, cc-router e OneCLI são config-only; os dois primeiros oferecem endpoints custom OpenAI-compatible e OneCLI injeta credenciais por proxy MITM. Pesquisa acumulada `94/104` (`90,4%`), pendentes `10/104` (`9,6%`). Nenhuma publicação externa.
Resultado P3.7: agent-browser `config-only` direto por Chat Completions; OpenWork `config-only` via OpenCode custom; Agent Deck `config-only` via CLIs filhos. Pesquisa acumulada `97/104` (`93,3%`), pendentes `7/104` (`6,7%`). Nenhuma publicação externa.
Resultado P4.1: Pool e Junie são `config-only` OpenAI-compatible; Cursor é `config-only` limitado ao BYO chat panel, sem MITM/protocolo privado. Pesquisa acumulada `100/104` (`96,2%`), pendentes `4/104` (`3,8%`). Nenhuma publicação externa.
Resultado P4.2: Windsurf está bloqueado para inferência e permite apenas MCP; Amp depende de confirmação Enterprise; Amazon Q legado requer patch substancial e Kiro atual é MCP-only seguro. Pesquisa acumulada `103/104` (`99,0%`), pendente `1/104` (`1,0%`). Nenhuma publicação externa.
Resultado P4.3: Cowork não permite substituir oficialmente a inferência; Custom Connector MCP remoto é o único caminho suportado e permanece separado do modelo. Pesquisa concluída `104/104` (`100%`), pendentes `0/104` (`0%`). Nenhuma publicação externa nesta fase de pesquisa.

View File

@@ -1,131 +0,0 @@
# Relatório final — campanha de 104 integrações CLI OmniRoute
**Data de fechamento:** 2026-08-03
**Escopo:** `CLI-000` a `CLI-103`
**Resultado:** `104/104` pesquisados (`100%`), `0` pendentes de pesquisa.
## Como consultar o resultado individual
O documento autoritativo, com uma linha para cada caso, é o [tracker completo](./04-tracker-integracoes-clis.md). Ele contém para cada ID:
- prioridade;
- projeto e repositório;
- classificação de integração;
- estado de contribuição upstream;
- branch e commit quando existentes;
- URL de PR e/ou issue quando publicados;
- estado no catálogo OmniRoute;
- observações, limitações, testes e próximo passo.
Além do tracker, existem fichas técnicas individuais em `_tasks/cli-integrations/`. A cobertura foi auditada e agora há uma ficha para cada ID `CLI-000``CLI-103`; o caso `CLI-000` jcode foi adicionado como ficha de referência nesta revisão.
## Resumo quantitativo
| Grupo operacional | Quantidade | Tratamento |
|---|---:|---|
| Configuração direta ou indireta | 76 | Documentar receita, validar smoke e só abrir PR se houver melhoria upstream real |
| Contribuição upstream (PR/issue/docs/patch) | 17 | Preparar diff mínimo, validar, revisar e publicar conforme política do repositório |
| Patch obrigatório | 4 | Implementar genericamente, com RED→GREEN/TDD e revisão do mantenedor |
| Bloqueados/fechados | 4 | Registrar bloqueio; usar apenas MCP ou canal oficial, sem MITM |
| MCP/wrapper/ACP como caminho principal | 2 | Integrar a camada de ferramentas/orquestração, sem falsificar provider de inferência |
| Outros casos híbridos | 1 | Seguir a combinação específica descrita no tracker |
Os números são derivados do campo `Tipo` do tracker; categorias podem se sobrepor em casos híbridos. Atualmente há **7 PRs reais** e **9 issues reais** registrados no tracker, além de cinco entradas locais marcadas como integradas ao catálogo OmniRoute. Nenhum link foi inventado para os 97 casos sem publicação externa.
## O que foi feito na campanha
1. Inventário inicial e busca extensa de CLIs, runtimes, harnesses e control-planes.
2. Priorização P0P4 considerando compatibilidade de protocolo, adoção, licença, maturidade e risco.
3. Pesquisa fresca, uma a uma, em worktrees isoladas, em lotes de no máximo três agentes.
4. Uso de Codebase Memory para índices upstream e verificação de cobertura; faixas parciais foram lidas diretamente quando aplicável.
5. Classificação por configuração, patch, PR documental, issue-first, MCP, wrapper ou bloqueio.
6. Registro de comandos, base URL, autenticação, modelos, streaming, tools, reasoning, imagens, MCP/ACP/A2A, testes e limitações.
7. Consolidação de cada lote com commit separado no OmniRoute e no repositório `_tasks`.
8. Atualização final do tracker, plano de integração, plano de publicação e handoff.
9. Nenhuma credencial real, publicação externa ou técnica de interceptação não autorizada foi utilizada.
## Estratégia para abrir PRs em 100% dos casos
“Abrir PR para 100%” deve ser interpretado como **dar um destino upstream apropriado a 100% dos casos**, e não criar 104 PRs artificiais. Há quatro trilhas:
### Trilha A — PR de código ou documentação
Aplicar aos casos `viable-upstream`, `pr-generic`, `pr-docs`, `patch-required` e híbridos que tenham superfície pública e política de contribuição compatível.
Processo por caso:
1. Reconfirmar HEAD, licença, branch default, política de contribuição e duplicatas.
2. Criar worktree/branch baseada na versão local vigente.
3. Executar baseline upstream e registrar falhas preexistentes.
4. Escrever teste RED que demonstre a lacuna.
5. Implementar o menor patch genérico possível — preferir `openai-compatible`, `base_url` ou provider abstrato a um provider nominal OmniRoute.
6. Executar GREEN: testes focados, suite upstream, lint, format, typecheck/build e smoke com fake server ou OmniRoute local usando placeholder.
7. Revisar segurança: nenhuma chave em argv, logs, fixtures, URL ou artefato; erros sanitizados; streaming/tools/cancelamento cobertos.
8. Abrir PR somente se contribuições externas forem aceitas. O corpo deve explicar problema, solução genérica, compatibilidade, testes, limitações e não conter marketing/texto de IA.
9. Se o repositório bloquear fork/PR ou pedir discussão prévia, abrir issue de proposta com o mesmo patch/reprodução, sem enviar PR prematuramente.
10. Atualizar tracker com branch, commit, URL, CI, revisão e resposta do mantenedor; acompanhar até `accepted`, `merged`, `rejected` ou `awaiting-maintainer`.
### Trilha B — Issue-first, discussão ou suporte ao mantenedor
Aplicar quando a arquitetura é adequada, mas há bloqueio de governança, firewall, CLA, fork fechado, dúvida de protocolo ou necessidade de decisão do autor. A issue deve conter:
- caso de uso OmniRoute;
- configuração atualmente possível;
- lacuna reproduzível;
- proposta genérica;
- impacto de segurança;
- testes/fake server;
- disposição para enviar PR após aprovação.
Não abrir uma PR paralela enquanto a política exigir issue-first.
### Trilha C — Config-only documentado
Aplicar aos casos em que o upstream já suporta a integração e uma mudança de código seria redundante. O entregável é:
- ficha individual;
- receita validada;
- smoke test e limitações;
- eventual documentação externa/local do OmniRoute;
- issue somente se houver pedido de documentação ou descoberta de bug real.
Não criar provider nominal ou PR apenas para adicionar a palavra “OmniRoute”.
### Trilha D — MCP, wrapper ou bloqueio seguro
Aplicar a control-planes, produtos fechados e CLIs sem rota de inferência substituível. O resultado pode ser:
- MCP remoto/stdio do OmniRoute;
- wrapper local claramente identificado como wrapper;
- solicitação oficial de custom provider;
- registro de bloqueio e gate legal/ToS.
Nunca mascarar OmniRoute como Claude/Codex, falsificar executável, interceptar TLS ou reutilizar tokens privados para fabricar uma PR upstream.
## Ordem recomendada de execução
1. **Primeiro:** PRs e issues já preparadas ou com alto retorno e baixo risco — jcode, Gemini CLI, Claw Code, Plandex, Trae Agent, Every Code, VT Code e CoreCoder.
2. **Segundo:** patches genéricos com boa superfície OSS — AutoCodeRover, Galley, 5dive e demais casos `pr-generic`/`patch-required`.
3. **Terceiro:** issues aguardando decisão — Open Codex, Kimi CLI, Devon, g3, Free Code, Claude Engineer e casos com `awaiting-maintainer`.
4. **Quarto:** documentação e receitas config-only agrupadas por ecossistema — OpenCode, Codex, LiteLLM, AI SDK, OpenAI-compatible e Anthropic-compatible.
5. **Quinto:** MCP/plugins para produtos fechados — Windsurf, Amp, Kiro, Cowork e Cursor, sempre pela superfície oficial.
Cada rodada deve manter no máximo três agentes ativos. O agente principal revisa o resultado do trio antes de liberar o próximo.
## Critério de encerramento por caso
Um caso só pode ser marcado como finalizado quando possui: pesquisa, classificação, evidência de protocolo, baseline ou limitação reproduzível, receita/patch/bloqueio, validação proporcional, estado de publicação e próximo passo. Para produtos fechados, `blocked-closed` ou `MCP-only` é um resultado válido e preferível a uma PR não autorizada.
## Estado de publicação atual
Os únicos links de publicação comprovados devem continuar sendo os registrados no tracker. O fato de existir uma branch local de pesquisa não significa que exista PR upstream. A matriz de verdade é:
- PR/issue preenchida: publicação real;
- campo `—`: nenhuma publicação externa comprovada;
- `not-applicable`: configuração ou bloqueio sem contribuição upstream;
- `awaiting-maintainer`: contato feito, aguardando decisão;
- `published-pr`/`published-issue`: URL real presente no tracker.
## Próxima fase
A pesquisa está encerrada. A próxima fase é execução controlada da Trilha A/B/C/D, começando pelos casos com maior retorno e menor risco, com revisão central antes de qualquer push, PR, issue ou contato externo.

View File

@@ -299,15 +299,6 @@ async function checkNativeBinary(rootDir) {
"Release",
"better_sqlite3.node"
),
path.join(
rootDir,
"dist",
"node_modules",
"better-sqlite3",
"build",
"Release",
"better_sqlite3.node"
),
path.join(rootDir, "node_modules", "better-sqlite3", "build", "Release", "better_sqlite3.node"),
];
const binaryPath = candidates.find((candidate) => fs.existsSync(candidate));
@@ -404,10 +395,7 @@ async function checkServerLiveness(options = {}) {
// First attempt: configured health endpoint (may require auth token).
const primary = await probeUrl(url);
if (primary.ok) {
return ok("Server liveness", "Server health endpoint is reachable", {
url,
status: primary.status,
});
return ok("Server liveness", "Server health endpoint is reachable", { url, status: primary.status });
}
// #6162: /api/health and /api/health/degradation require a management token.
@@ -438,12 +426,7 @@ async function checkServerLiveness(options = {}) {
return ok(
"Server liveness",
`Server reachable (health endpoint returned ${primary.status}, likely requires MANAGEMENT_TOKEN)`,
{
primaryUrl: url,
primaryStatus: primary.status,
fallbackUrl,
fallbackStatus: fallback.status,
}
{ primaryUrl: url, primaryStatus: primary.status, fallbackUrl, fallbackStatus: fallback.status }
);
}
@@ -456,7 +439,8 @@ async function checkServerLiveness(options = {}) {
export async function collectDoctorChecks(context = {}, options = {}) {
const rootDir =
context.rootDir || path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..", "..");
context.rootDir ||
path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..", "..");
const dataDir = resolveDataDir();
const dbPath = resolveStoragePath(dataDir);

View File

@@ -1,36 +1,6 @@
import { spawn, execFileSync } from "node:child_process";
import { spawn } from "node:child_process";
import { t } from "../i18n.mjs";
import { resolveActiveContext } from "../contexts.mjs";
import { quoteShellArgs } from "../utils/winShellArgs.mjs";
/**
* Probe PATH for a Windows executable via `where.exe`, preferring a `.exe` over
* a `.cmd`/`.bat` shim. Returns the absolute path to the preferred binary, or
* `null` when `where.exe` finds nothing (or cannot run). Mirrors the same probe
* in launch.mjs and `locateCommand()` in `src/shared/services/cliRuntime.ts`.
*
* @param {string} command bare command name to look up
* @returns {Promise<string|null>} absolute path to the preferred match, or null
*/
function probeWindowsBinary(command) {
try {
const out = execFileSync("where.exe", [command], {
stdio: ["ignore", "pipe", "ignore"],
encoding: "utf8",
timeout: 3000,
windowsHide: true,
});
const lines = out
.split(/\r?\n/)
.map((l) => l.trim())
.filter(Boolean);
if (lines.length === 0) return null;
const winExt = /\.(exe|cmd|bat|com)$/i;
return lines.find((l) => winExt.test(l)) || null;
} catch {
return null;
}
}
/** OpenAI/Codex env keys stripped from the child so a stale OpenAI key/base-url
* in the shell can't shadow the omniroute provider (defense-in-depth). Mirrors
@@ -52,42 +22,11 @@ const NO_AUTH_SENTINEL = "omniroute-no-auth";
// On Windows the `codex` binary is an npm `.cmd` shim that `spawn` cannot resolve
// without a shell (bare "codex" → ENOENT). Mirror the qodercli Windows fix (#6263):
// spawn `codex.cmd` through a shell on win32, and the bare binary elsewhere.
//
// #9454: the native codex installer may ship a real `codex.exe` instead of the
// npm `.cmd` shim. Probe PATH for `codex` first: when `where.exe` resolves a
// `.exe`, spawn it directly (no shell — cmd.exe would split an absolute path
// with spaces); otherwise fall back to `codex.cmd` + shell. Off Windows the bare
// binary is spawned unchanged (no shell, no probe).
/**
* @param {NodeJS.Platform|string} platform
* @param {{ probe?: (command: string) => Promise<string|null> }} [opts] injectable probe for tests
* @returns {Promise<{ command: string, shell: true|undefined }>}
*/
export async function resolveCodexSpawn(platform, opts = {}) {
if (platform !== "win32") return { command: "codex", shell: undefined };
const probe = opts.probe ?? probeWindowsBinary;
const located = await probe("codex");
if (located && /\.exe$/i.test(located)) {
return { command: located, shell: undefined };
export function resolveCodexSpawn(platform) {
if (platform === "win32") {
return { command: "codex.cmd", shell: true };
}
return { command: "codex.cmd", shell: true };
}
/**
* `shell: true` makes Node join argv with plain spaces and no escaping (the
* DEP0190 warning). That mangles every launch-codex invocation on Windows, not
* just the ones with a multi-word user argument: the injected `-c` provider
* flags carry TOML values whose quotes cmd.exe strips
* (`model_providers.omniroute.name="OmniRoute"` arrives unquoted and no longer
* parses as TOML). Quote the args ourselves on that path; off Windows there is
* no shell, so argv is passed through untouched. Same fix as `launch` (#8837).
*
* @param {string[]} args
* @param {NodeJS.Platform|string} platform
* @returns {string[]}
*/
export function quoteCodexArgs(args, platform) {
return quoteShellArgs(args, platform);
return { command: "codex", shell: undefined };
}
function stripTrailingSlash(value) {
@@ -212,10 +151,9 @@ export async function runLaunchCodexCommand(opts = {}, codexArgs = []) {
const extraArgs = [...providerArgs, ...profileArgs, ...codexArgs];
const env = buildCodexEnv(process.env, authToken);
const { command: codexLaunch, shell: shellValue } = await resolveCodexSpawn(process.platform);
return await new Promise((resolve) => {
const child = spawn(codexLaunch, quoteCodexArgs(extraArgs, process.platform), {
const { command: codexLaunch, shell: shellValue } = resolveCodexSpawn(process.platform);
const child = spawn(codexLaunch, extraArgs, {
env,
stdio: "inherit",
shell: shellValue,
@@ -257,10 +195,7 @@ export function registerLaunchCodex(program) {
.argument("[codexArgs...]", "arguments passed through to the codex binary")
.action(async (codexArgs, opts) => {
const merged = { ...opts, profile: opts.profile ?? opts.p };
// process.exit() here aborted the process with a libuv assertion on
// Windows (`!(handle->flags & UV_HANDLE_CLOSING)`, async.c:94): it tears
// the loop down while the inherited stdio handles of the just-exited
// child are still closing. Setting exitCode lets the loop drain first.
process.exitCode = await runLaunchCodexCommand(merged, codexArgs ?? []);
const exitCode = await runLaunchCodexCommand(merged, codexArgs ?? []);
if (exitCode !== 0) process.exit(exitCode);
});
}

View File

@@ -1,9 +1,8 @@
import { spawn, execFileSync } from "node:child_process";
import { spawn } from "node:child_process";
import { join } from "node:path";
import os from "node:os";
import { t } from "../i18n.mjs";
import { resolveActiveContext } from "../contexts.mjs";
import { quoteShellArgs } from "../utils/winShellArgs.mjs";
function stripTrailingSlash(value) {
let s = String(value);
@@ -91,78 +90,6 @@ export function resolveLaunchTarget(opts = {}) {
return { baseUrl, authToken };
}
/**
* Probe PATH for a Windows executable via `where.exe`, preferring a `.exe` over
* a `.cmd`/`.bat` shim. Returns the absolute path to the preferred binary, or
* `null` when `where.exe` finds nothing (or cannot run).
*
* The native Anthropic installer (#9454) creates only `claude.exe` (no npm
* `.cmd` shim), so the launcher must look for the real PE and spawn it without
* a shell. Mirrors the existing `locateCommand()` probe in
* `src/shared/services/cliRuntime.ts`.
*
* @param {string} command bare command name to look up
* @returns {Promise<string|null>} absolute path to the preferred match, or null
*/
function probeWindowsBinary(command) {
try {
const out = execFileSync("where.exe", [command], {
stdio: ["ignore", "pipe", "ignore"],
encoding: "utf8",
timeout: 3000,
windowsHide: true,
});
const lines = out
.split(/\r?\n/)
.map((l) => l.trim())
.filter(Boolean);
if (lines.length === 0) return null;
const winExt = /\.(exe|cmd|bat|com)$/i;
return lines.find((l) => winExt.test(l)) || null;
} catch {
return null;
}
}
/**
* #8246 / #9454: on Windows, npm installs claude as a `.cmd` shim — spawn()
* without a shell cannot resolve PATHEXT shims (and Node refuses to exec `.cmd`
* directly since CVE-2024-27980), so the npm-shim path must go through cmd.exe.
* But the native installer creates only `claude.exe`, which is a real PE that
* must NOT go through a shell (cmd.exe would split an absolute path with spaces).
*
* So probe PATH for `claude` first: when `where.exe` resolves a `.exe`, spawn it
* directly (no shell); otherwise fall back to the npm `claude.cmd` + shell. Off
* Windows the bare binary is spawned unchanged (no shell, no probe).
*
* @param {NodeJS.Platform|string} platform
* @param {{ probe?: (command: string) => Promise<string|null> }} [opts] injectable probe for tests
* @returns {Promise<{ command: string, shell: true|undefined }>}
*/
export async function resolveClaudeSpawn(platform, opts = {}) {
if (platform !== "win32") return { command: "claude", shell: undefined };
const probe = opts.probe ?? probeWindowsBinary;
const located = await probe("claude");
if (located && /\.exe$/i.test(located)) {
return { command: located, shell: undefined };
}
return { command: "claude.cmd", shell: true };
}
/**
* `shell: true` makes Node join argv with plain spaces and no escaping (the
* DEP0190 warning), so `-p "two words"` used to reach claude as `-p two` plus
* three stray positional arguments. Quote the args ourselves on that path.
* Off Windows there is no shell, so argv is passed through untouched.
*
* @param {string[]} args
* @param {NodeJS.Platform|string} platform
* @returns {string[]}
*/
export function quoteClaudeArgs(args, platform) {
return quoteShellArgs(args, platform);
}
/**
* @param {{port?:string, remote?:string, token?:string, apiKey?:string, profile?:string, claudeHome?:string}} opts
* @param {string[]} claudeArgs pass-through args for the claude binary
@@ -192,14 +119,14 @@ export async function runLaunchCommand(opts = {}, claudeArgs = []) {
: undefined;
const env = buildClaudeEnv(process.env, baseUrl, authToken, { configDir });
const { command, shell } = await resolveClaudeSpawn(process.platform);
return await new Promise((resolve) => {
const child = spawn(command, quoteClaudeArgs(claudeArgs, process.platform), {
// #8246: on Windows, npm installs claude as a .cmd shim — spawn() without
// shell:true cannot resolve PATHEXT shims and fails with ENOENT.
const claudeCommand = process.platform === "win32" ? "claude.cmd" : "claude";
const child = spawn(claudeCommand, claudeArgs, {
env,
stdio: "inherit",
shell,
...(process.platform === "win32" ? { windowsHide: true } : {}),
...(process.platform === "win32" ? { shell: true, windowsHide: true } : {}),
});
child.on("error", (err) => {
if (err && err.code === "ENOENT") {
@@ -232,10 +159,7 @@ export function registerLaunch(program) {
.allowExcessArguments(true)
.argument("[claudeArgs...]", "arguments passed through to the claude binary")
.action(async (claudeArgs, opts) => {
// process.exit() here aborted the process with a libuv assertion on
// Windows (`!(handle->flags & UV_HANDLE_CLOSING)`, async.c:94): it tears
// the loop down while the inherited stdio handles of the just-exited
// child are still closing. Setting exitCode lets the loop drain first.
process.exitCode = await runLaunchCommand(opts, claudeArgs ?? []);
const exitCode = await runLaunchCommand(opts, claudeArgs ?? []);
if (exitCode !== 0) process.exit(exitCode);
});
}

View File

@@ -19,19 +19,6 @@ import { randomUUID } from "node:crypto";
*
* It talks ONLY to Google (no OmniRoute server needed locally), so it works even
* if the remote VPS is firewalled from the user's machine.
*
* Push mode: when an active remote context exists (`omniroute connect <host>`), the
* blob is POSTed straight to that install instead of being printed for a manual
* copy-paste — every piece was already in place:
*
* - the context carries an admin-scoped token, and `apiFetch()` injects it;
* - `/api/oauth` requires admin scope (src/server/authz/accessScopes.ts) and stays
* remote-reachable — routeGuard.ts loopback-gates only `/api/oauth/cursor/auto-import`;
* - `/api/oauth/<provider>/paste-credentials` already decodes the blob and persists.
*
* The push NEVER becomes a hard requirement: this helper exists precisely because it
* needs no route to the VPS, so a failed push falls back to printing the blob rather
* than losing an authorization the operator just completed in their browser.
*/
const PROVIDER = "antigravity";
@@ -67,7 +54,7 @@ function defaultStartServer(preferredPort) {
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
res.end(
"<!doctype html><meta charset=utf-8><title>OmniRoute</title>" +
'<body style="font-family:system-ui;padding:2rem">' +
"<body style=\"font-family:system-ui;padding:2rem\">" +
"<h2>✅ Authorization received</h2>" +
"<p>Return to your terminal — you can close this tab.</p></body>"
);
@@ -86,51 +73,6 @@ function defaultStartServer(preferredPort) {
});
}
/**
* Is this context pointing at another machine? Loopback (and an unresolvable value)
* counts as local, so we never auto-push somewhere we cannot reason about.
*/
export function isRemoteBaseUrl(baseUrl) {
if (!baseUrl) return false;
try {
const { hostname } = new URL(baseUrl);
const host = hostname.replace(/^\[|\]$/g, ""); // strip IPv6 brackets
return host !== "localhost" && host !== "127.0.0.1" && host !== "::1";
} catch {
return false;
}
}
/**
* POST a credential blob to the active context's install. Never throws: the caller
* decides whether a failure is fatal (it is not — it falls back to printing).
*/
export async function pushCredentialBlob(provider, blob, deps = {}) {
try {
const fetchImpl = deps.fetchImpl ?? (await import("../api.mjs")).apiFetch;
const res = await fetchImpl(`/api/oauth/${provider}/paste-credentials`, {
method: "POST",
body: { blob },
});
const data = await res.json().catch(() => ({}));
if (!res.ok || data?.success === false) {
const message =
(typeof data?.error === "string" ? data.error : data?.error?.message) ||
`HTTP ${res.status}`;
return { ok: false, error: message };
}
return { ok: true, connectionId: data?.connection?.id };
} catch (err) {
return { ok: false, error: err?.message || String(err) };
}
}
/** Read the active CLI context (baseUrl + scoped token) written by `omniroute connect`. */
async function defaultResolveContext(overrideName) {
const { resolveActiveContext } = await import("../contexts.mjs");
return resolveActiveContext(overrideName);
}
/** Lazy-load the antigravity provider + blob codec (TS source via tsx). */
async function loadDeps() {
const { antigravity } = await import("../../../src/lib/oauth/providers/antigravity.ts");
@@ -211,41 +153,10 @@ export async function runAntigravityLogin(opts = {}, deps = {}) {
const tokens = await exchange(params.code, redirectUri);
const blob = encodeCredentialBlob({ provider: PROVIDER, tokens });
// Push when the operator explicitly asked, or when the active context already points
// at another machine — that is exactly the situation this helper was built for.
const resolveContext = deps.resolveContext ?? defaultResolveContext;
const push = deps.push ?? pushCredentialBlob;
let context = null;
try {
context = await resolveContext(opts.context);
} catch {
// No usable context store — fall through to printing.
}
const wantsPush =
opts.push === true || (opts.push !== false && isRemoteBaseUrl(context?.baseUrl));
if (wantsPush) {
log(`\nSending the credential to ${context?.baseUrl || "the active context"}...\n`);
const result = await push(PROVIDER, blob, { context });
if (result?.ok) {
log(
`Antigravity connected on ${context?.baseUrl || "the remote install"}` +
`${result.connectionId ? ` (connection ${result.connectionId})` : ""}.\n` +
"Nothing to paste — you can close this terminal.\n"
);
// Deliberately NOT printed: the blob wraps a refresh token and it already landed.
return blob;
}
log(
`\nCould not deliver the credential automatically: ${result?.error || "unknown error"}\n` +
"Falling back to manual paste — the authorization itself is still valid.\n"
);
}
print(
"\n" +
"Antigravity authorized. Copy the line below and paste it into your remote\n" +
'OmniRoute dashboard: Providers → Antigravity → Connect → "Paste credentials".\n' +
"OmniRoute dashboard: Providers → Antigravity → Connect → \"Paste credentials\".\n" +
"(This contains a refresh token — treat it like a password.)\n\n" +
blob +
"\n\n"
@@ -259,8 +170,6 @@ async function runLoginAntigravity(opts) {
browser: opts.browser,
timeout: opts.timeout,
port: opts.port,
push: opts.push,
context: opts.context,
});
} catch (err) {
process.stderr.write(`\nLogin failed: ${err?.message || err}\n`);
@@ -279,11 +188,5 @@ export function registerLogin(program) {
.option("--no-browser", "Do not auto-open the browser; print the URL instead")
.option("--port <n>", "Fixed loopback port (default: OS-assigned)", (v) => parseInt(v, 10))
.option("--timeout <ms>", "How long to wait for the callback", (v) => parseInt(v, 10), 300000)
.option(
"--push",
"Send the credential to the active context instead of printing it (default when that context is remote)"
)
.option("--no-push", "Always print the blob, never contact the server")
.option("--context <name>", "Push to this context instead of the active one")
.action(runLoginAntigravity);
}

View File

@@ -10,28 +10,11 @@ const PROVIDERS_WITH_OAUTH = [
{ id: "cursor", name: "Cursor", flow: "import" },
{ id: "zed", name: "Zed", flow: "import" },
{ id: "kiro", name: "Amazon Kiro", flow: "social" },
{ id: "claude-code", name: "Claude Code (OAuth)", flow: "browser" },
{ id: "claude-code", name: "Claude Code (OAuth)", flow: "device" },
{ id: "codex", name: "OpenAI Codex (OAuth)", flow: "device" },
{ id: "copilot", name: "GitHub Copilot", flow: "device" },
];
// The user-facing provider id (the one shown by `omniroute oauth providers`)
// is NOT always the backend OAuth provider key the server's /api/oauth/[provider]/...
// route expects. `claude-code` is the CLI-facing alias for Anthropic's Claude
// OAuth, which the server registers under the key `claude` (see
// src/lib/oauth/providers/index.ts). Routing `claude-code` to the unrelated
// `command-code` (CommandCode.ai) provider — as the previous code did — sent
// the device-flow request to /api/providers/command-code/auth/start, which is
// gated by requireManagementAuth and returned 401 for a fresh CLI context
// (issue #9474). Map the alias to the real backend key instead.
const BACKEND_OAUTH_KEY = {
"claude-code": "claude",
};
function resolveBackendKey(id) {
return BACKEND_OAUTH_KEY[id] ?? id;
}
const oauthProviderSchema = [
{ key: "id", header: "Provider ID", width: 16 },
{ key: "name", header: "Name", width: 28 },
@@ -73,107 +56,32 @@ async function pollStatus(endpoint, timeoutMs) {
}
async function runBrowserFlow(def, opts) {
// The user-facing id (`def.id`, e.g. "claude-code") must be translated to the
// backend OAuth provider key the server's /api/oauth/[provider]/... route
// expects (e.g. "claude"). The previous implementation called a non-existent
// `/api/oauth/${def.id}/start` action — no such action exists on the server
// (src/app/api/oauth/[provider]/[action]/route.ts), so the browser flow was
// broken for every browser-flow provider. Use the real `authorize` action and
// complete the PKCE (authorization_code / authorization_code_pkce) flow with a
// manual code paste, mirroring the dashboard's manual "input" step.
const backendKey = resolveBackendKey(def.id);
const redirectUri = opts.redirectUri ?? null;
const authorizeUrl = `/api/oauth/${backendKey}/authorize${
redirectUri ? `?redirect_uri=${encodeURIComponent(redirectUri)}` : ""
}`;
const startRes = await apiFetch(authorizeUrl, { method: "GET" });
const startRes = await apiFetch(`/api/oauth/${def.id}/start`, { method: "POST" });
if (!startRes.ok) {
const detail = await safeErrorBody(startRes);
process.stderr.write(`Failed to start OAuth for ${def.id}: ${startRes.status}${detail}\n`);
process.stderr.write(`Failed to start OAuth for ${def.id}: ${startRes.status}\n`);
process.exit(1);
}
const start = await startRes.json();
const url = start.authUrl ?? start.authorizeUrl ?? start.url;
if (!url) {
const hint = start.error ?? "no authUrl returned by the server";
process.stderr.write(`OAuth unavailable for ${def.id}: ${hint}\n`);
process.exit(1);
}
const { codeVerifier, state, redirectUri: returnedRedirectUri } = start;
const finalRedirectUri = returnedRedirectUri || redirectUri;
const url = start.authorizeUrl ?? start.url;
process.stdout.write(`\nOpen this URL to authorize:\n ${url}\n\n`);
if (opts.browser !== false) await openBrowser(url);
if (process.stdout.isTTY && opts.browser !== false) {
const { startOAuthTui } = await import("../tui/OAuthFlow.jsx");
await openBrowser(url);
const tuiResult = await startOAuthTui({ provider: def.name ?? def.id, url });
if (tuiResult.status === "cancelled") return;
} else {
process.stdout.write(`\nOpen this URL to authorize:\n ${url}\n\n`);
if (opts.browser !== false) await openBrowser(url);
process.stderr.write("Waiting for authorization... (Ctrl+C to cancel)\n");
}
const result = await pollStatus(
`/api/oauth/${def.id}/status?state=${encodeURIComponent(start.state ?? "")}`,
opts.timeout ?? 300000
);
process.stdout.write(
"After authorizing, paste the callback URL (or the Authentication Code\n" +
"shown on the confirmation page) here:\n"
`Authorized: ${result.email ?? result.userId ?? result.account ?? "connected"}\n`
);
const { createPrompt } = await import("../io.mjs");
const prompt = createPrompt();
const input = await prompt.ask("Callback URL or code");
prompt.close();
const trimmed = input.trim();
if (!trimmed) {
process.stderr.write("No authorization code provided.\n");
process.exit(1);
}
// The Anthropic Claude confirmation page (platform.claude.com/oauth/code/callback)
// shows a raw "Authentication Code" like `code#state` rather than a full URL.
// The dashboard's manual submit (src/shared/components/OAuthModal.tsx) parses
// both forms; mirror that here.
let code = null;
let codeState = state || null;
try {
const cbUrl = new URL(trimmed);
code = cbUrl.searchParams.get("code");
const stateParam = cbUrl.searchParams.get("state") || cbUrl.hash.replace(/^#/, "");
if (stateParam) codeState = stateParam;
} catch {
const [rawCode, rawState] = trimmed.split("#", 2);
code = rawCode || null;
if (rawState) codeState = rawState;
}
if (!code) {
process.stderr.write(
"No authorization code found. Paste the callback URL or the Authentication Code.\n"
);
process.exit(1);
}
const exchangeRes = await apiFetch(`/api/oauth/${backendKey}/exchange`, {
method: "POST",
body: {
code,
redirectUri: finalRedirectUri,
codeVerifier,
...(codeState ? { state: codeState } : {}),
},
});
if (!exchangeRes.ok) {
const detail = await safeErrorBody(exchangeRes);
process.stderr.write(`Token exchange failed: ${exchangeRes.status}${detail}\n`);
process.exit(1);
}
const result = await exchangeRes.json();
const conn = result.connection ?? {};
process.stdout.write(`Authorized: ${conn.email ?? conn.displayName ?? conn.id ?? "connected"}\n`);
}
async function safeErrorBody(res) {
try {
const data = await res.json();
if (data?.error) {
const msg = typeof data.error === "string" ? data.error : data.error?.message;
if (msg) return `: ${msg}`;
}
if (data?.message) return `: ${data.message}`;
} catch {
/* ignore */
}
return "";
}
async function runImportFlow(def, opts) {
@@ -216,7 +124,7 @@ async function runSocialFlow(def, opts) {
}
async function runDeviceFlow(def, opts) {
const providerKey = resolveBackendKey(def.id);
const providerKey = def.id === "claude-code" ? "command-code" : def.id;
const startRes = await apiFetch(`/api/providers/${providerKey}/auth/start`, { method: "POST" });
if (!startRes.ok) {
process.stderr.write(`Failed to start device flow: ${startRes.status}\n`);

View File

@@ -10,25 +10,9 @@ const DEFAULT_IMAGE = "docker.io/redis:7-alpine";
const DEFAULT_NAME = "omniroute-redis";
const DEFAULT_PORT = "6379";
const DEFAULT_VOLUME = "omniroute-redis-data";
// The launcher starts Redis without AUTH unless --password is given, so the
// published port stays on loopback. `-p 6379:6379` would bind 0.0.0.0 and hand
// the whole LAN an unauthenticated Redis.
const DEFAULT_BIND = "127.0.0.1";
const RUNTIME_PREFERENCE = ["podman", "docker"];
/**
* Build the `-p` publish spec for the Redis container.
* Always host-qualified so the runtime never falls back to 0.0.0.0.
*/
export function buildRedisPublishSpec(bind = DEFAULT_BIND, port = DEFAULT_PORT) {
const host = String(bind || DEFAULT_BIND).trim() || DEFAULT_BIND;
const hostPort = String(port || DEFAULT_PORT).trim() || DEFAULT_PORT;
// Bracket IPv6 literals (e.g. ::1) so `host:port:port` stays unambiguous.
const normalizedHost = host.includes(":") && !host.startsWith("[") ? `[${host}]` : host;
return `${normalizedHost}:${hostPort}:6379`;
}
async function detectRuntime() {
for (const candidate of RUNTIME_PREFERENCE) {
try {
@@ -43,14 +27,7 @@ async function detectRuntime() {
async function containerExists(runtime, name) {
try {
const { stdout } = await execFile(runtime, [
"ps",
"-a",
"--filter",
`name=^${name}$`,
"--format",
"{{.Names}}",
]);
const { stdout } = await execFile(runtime, ["ps", "-a", "--filter", `name=^${name}$`, "--format", "{{.Names}}"]);
return stdout.trim() === name;
} catch {
return false;
@@ -59,13 +36,7 @@ async function containerExists(runtime, name) {
async function containerRunning(runtime, name) {
try {
const { stdout } = await execFile(runtime, [
"ps",
"--filter",
`name=^${name}$`,
"--format",
"{{.Names}}",
]);
const { stdout } = await execFile(runtime, ["ps", "--filter", `name=^${name}$`, "--format", "{{.Names}}"]);
return stdout.trim() === name;
} catch {
return false;
@@ -129,11 +100,6 @@ export function registerRedis(program) {
.command("up")
.description("Start the local Redis container")
.option("-p, --port <port>", "Host port to expose", DEFAULT_PORT)
.option(
"-b, --bind <host>",
"Host interface to publish on (use 0.0.0.0 only together with --password)",
DEFAULT_BIND
)
.option("-n, --name <name>", "Container name", DEFAULT_NAME)
.option("-i, --image <image>", "Container image", DEFAULT_IMAGE)
.option("--no-pull", "Skip pulling the image if it is missing")
@@ -194,7 +160,6 @@ export async function runRedisUpCommand(opts = {}) {
const name = opts.name || DEFAULT_NAME;
const port = opts.port || DEFAULT_PORT;
const bind = opts.bind || DEFAULT_BIND;
const image = opts.image || DEFAULT_IMAGE;
const exists = await containerExists(runtime, name);
@@ -221,11 +186,7 @@ export async function runRedisUpCommand(opts = {}) {
info(`Checking if image '${image}' is present locally…`);
let present = false;
try {
const { stdout } = await execFile(runtime, [
"images",
"--format",
"{{.Repository}}:{{.Tag}}",
]);
const { stdout } = await execFile(runtime, ["images", "--format", "{{.Repository}}:{{.Tag}}"]);
present = stdout.split("\n").some((line) => line.trim() === image);
} catch {
// ignore — fall through to pull
@@ -244,14 +205,10 @@ export async function runRedisUpCommand(opts = {}) {
const args = [
"run",
"-d",
"--name",
name,
"--restart",
"unless-stopped",
"-p",
buildRedisPublishSpec(bind, port),
"-v",
`${DEFAULT_VOLUME}:/data`,
"--name", name,
"--restart", "unless-stopped",
"-p", `${port}:6379`,
"-v", `${DEFAULT_VOLUME}:/data`,
];
if (opts.password) {
args.push("-e", `REDIS_PASSWORD=${opts.password}`);
@@ -262,13 +219,8 @@ export async function runRedisUpCommand(opts = {}) {
info(`Launching ${runtime} run ${args.join(" ")}`);
try {
await execFile(runtime, args);
success(`Container '${name}' is now running on redis://${bind}:${port}`);
info(`Set OMNIROUTE_REDIS_URL=redis://${bind}:${port} in your .env to wire OmniRoute to it.`);
if (bind !== DEFAULT_BIND && !opts.password) {
info(
`Warning: '${bind}' publishes Redis beyond loopback without AUTH. Re-run with --password <secret>.`
);
}
success(`Container '${name}' is now running on redis://127.0.0.1:${port}`);
info(`Set OMNIROUTE_REDIS_URL=redis://127.0.0.1:${port} in your .env to wire OmniRoute to it.`);
return 0;
} catch (err) {
fail(`Failed to launch container: ${err.message}`);
@@ -315,13 +267,7 @@ export async function runRedisStatusCommand(opts = {}) {
const exists = await containerExists(runtime, name);
if (!exists) {
console.log(
JSON.stringify(
{ runtime, name, port, exists: false, running: false, reachable: false },
null,
2
)
);
console.log(JSON.stringify({ runtime, name, port, exists: false, running: false, reachable: false }, null, 2));
return 0;
}
@@ -339,12 +285,10 @@ export async function runRedisStatusCommand(opts = {}) {
console.log(` Running: ${running ? "yes" : "no"}`);
console.log(` Reachable: ${reachable ? "yes" : "no"} (port ${port})`);
if (running && !reachable) {
warn(
"Container is running but the port is not reachable. Is REDIS_PASSWORD set or another process bound?"
);
warn("Container is running but the port is not reachable. Is REDIS_PASSWORD set or another process bound?");
}
if (!running) {
info(`Run 'omniroute redis up' to launch it.`);
}
return 0;
}
}

View File

@@ -34,14 +34,7 @@ async function runRepairAction(opts, cmd) {
if (ok) {
process.stdout.write("✓ better-sqlite3 repaired OK\n");
} else {
process.stderr.write("✗ Repair failed\n");
process.stderr.write(
" Possible causes:\n" +
" • npm not available — check that Node.js/npm are on your PATH\n" +
" • npm install scripts are blocked — run: npm install-scripts approve better-sqlite3\n" +
" • Network issue — check your internet connection\n" +
" Try: npm install-scripts ls (to see if better-sqlite3 is blocked)\n"
);
process.stderr.write("✗ Repair failed — check npm availability\n");
process.exit(1);
}
}

View File

@@ -7,16 +7,11 @@ import { t } from "../i18n.mjs";
import { writePidFile, cleanupPidFile, waitForServer } from "../utils/pid.mjs";
import { ServerSupervisor, detectMitmCrash } from "../runtime/processSupervisor.mjs";
import { isTermux } from "../../../scripts/build/postinstallSupport.mjs";
import {
ensureAndroidCacheDir,
isFatalInstrumentationHookFailure,
formatAndroidInstrumentationFailureHint,
} from "../utils/ensureAndroidCacheDir.mjs";
import {
resolveMaxOldSpaceMb,
calibrateHeapFallbackMb,
buildServerNodeOptions,
buildNodeRuntimeArgs,
buildNodeHeapArgs,
} from "../../../scripts/build/runtime-env.mjs";
import { resolveTlsOptions } from "../../../scripts/dev/tls-options.mjs";
@@ -67,37 +62,9 @@ export function registerServe(program) {
});
}
/** Once-per-process guard so the Android/Termux cache hint is not spammed. */
let instrumentationFailureHintPrinted = false;
/**
* If child output looks like Next.js failed to load its instrumentation hook
* on Android/Termux, print a clear operator-facing fix hint.
* Exported for unit tests.
*
* @param {string} text
* @returns {boolean} true when a hint was printed
*/
export function maybeReportInstrumentationHookFailure(text) {
if (instrumentationFailureHintPrinted) return false;
if (!isFatalInstrumentationHookFailure(text)) return false;
instrumentationFailureHintPrinted = true;
process.stderr.write(formatAndroidInstrumentationFailureHint(process.env.XDG_CACHE_HOME));
return true;
}
/** Test-only reset for the once-per-process hint guard. */
export function resetInstrumentationFailureHintForTests() {
instrumentationFailureHintPrinted = false;
}
export async function runServe(opts = {}) {
const startedAt = performance.now();
// Same prep as bin/omniroute.mjs — keep it here so a direct `runServe()` call
// (tests / programmatic) still gets a writable Next.js cache dir before spawn.
ensureAndroidCacheDir({ env: process.env });
const { isNativeBinaryCompatible } =
await import("../../../scripts/build/native-binary-compat.mjs");
const { getNodeRuntimeSupport, getNodeRuntimeWarning } =
@@ -167,11 +134,7 @@ export async function runServe(opts = {}) {
"Release",
"better_sqlite3.node"
);
if (
!process.versions.bun &&
existsSync(sqliteBinary) &&
!isNativeBinaryCompatible(sqliteBinary)
) {
if (!process.versions.bun && existsSync(sqliteBinary) && !isNativeBinaryCompatible(sqliteBinary)) {
console.error(
"\x1b[31m✖ better-sqlite3 native module is incompatible with this platform.\x1b[0m"
);
@@ -267,16 +230,15 @@ export async function runServe(opts = {}) {
function runDaemon(serverJs, env, memoryLimit, dashboardPort, apiPort) {
// #5238: skip the explicit CLI --max-old-space-size when the user pinned the
// heap via NODE_OPTIONS (a CLI arg would shadow/override their value).
const server = spawn(
process.versions.bun ? process.execPath : "node",
process.versions.bun ? [serverJs] : buildNodeRuntimeArgs(process.env, memoryLimit, serverJs),
{
cwd: APP_DIR,
env,
stdio: "ignore",
detached: true,
}
);
const server = spawn(process.versions.bun ? process.execPath : "node", [
...(process.versions.bun ? [] : buildNodeHeapArgs(process.env, memoryLimit)),
serverJs,
], {
cwd: APP_DIR,
env,
stdio: "ignore",
detached: true,
});
writePidFile("server", server.pid);
server.unref();
console.log(`\x1b[32m✔ OmniRoute started in background (PID: ${server.pid})\x1b[0m`);
@@ -287,15 +249,14 @@ function runDaemon(serverJs, env, memoryLimit, dashboardPort, apiPort) {
function runWithoutRecovery(serverJs, env, memoryLimit, dashboardPort, apiPort, noOpen, startedAt) {
// #5238: skip the explicit CLI --max-old-space-size when the user pinned the
// heap via NODE_OPTIONS (a CLI arg would shadow/override their value).
const server = spawn(
process.versions.bun ? process.execPath : "node",
process.versions.bun ? [serverJs] : buildNodeRuntimeArgs(process.env, memoryLimit, serverJs),
{
cwd: APP_DIR,
env,
stdio: "pipe",
}
);
const server = spawn(process.versions.bun ? process.execPath : "node", [
...(process.versions.bun ? [] : buildNodeHeapArgs(process.env, memoryLimit)),
serverJs,
], {
cwd: APP_DIR,
env,
stdio: "pipe",
});
writePidFile("server", server.pid);
@@ -304,7 +265,6 @@ function runWithoutRecovery(serverJs, env, memoryLimit, dashboardPort, apiPort,
server.stdout.on("data", (data) => {
const text = data.toString();
process.stdout.write(text);
maybeReportInstrumentationHookFailure(text);
if (
!started &&
(text.includes("Ready") || text.includes("started") || text.includes("listening"))
@@ -314,11 +274,7 @@ function runWithoutRecovery(serverJs, env, memoryLimit, dashboardPort, apiPort,
}
});
server.stderr.on("data", (data) => {
const text = data.toString();
process.stderr.write(text);
maybeReportInstrumentationHookFailure(text);
});
server.stderr.on("data", (data) => process.stderr.write(data));
server.on("error", (err) => {
console.error("\x1b[31m✖ Failed to start server:\x1b[0m", err.message);
@@ -387,19 +343,12 @@ async function runWithSupervisor(
supervisor.start();
// #9455: persist the supervisor's own PID so `omniroute stop` can SIGTERM it
// before the child — the supervisor's SIGTERM handler sets isShuttingDown=true,
// kills the child, and exits cleanly, so the child is never respawned after stop.
writePidFile("supervisor", process.pid);
process.on("SIGINT", () => {
killTrayIfActive();
cleanupPidFile("supervisor");
supervisor.stop();
});
process.on("SIGTERM", () => {
killTrayIfActive();
cleanupPidFile("supervisor");
supervisor.stop();
});
@@ -434,9 +383,6 @@ export function reportReadinessTimeout(dashboardPort, supervisor) {
console.error("--- Recent server output ---");
recentLog.forEach((l) => console.error(l));
console.error("--- End recent output ---\n");
// If the buffered log already shows the Android instrumentation failure,
// print the actionable hint even when --log was off (default).
maybeReportInstrumentationHookFailure(recentLog.join("\n"));
}
}

View File

@@ -156,15 +156,7 @@ export async function runSetupClaudeCommand(opts = {}) {
headers,
signal: AbortSignal.timeout(10000),
});
if (!res.ok) {
let detail = `HTTP ${res.status}`;
try {
const errorBody = await res.json();
const serverMsg = errorBody?.error?.message || errorBody?.error || errorBody?.message || "";
if (serverMsg) detail += `${serverMsg}`;
} catch {}
throw new Error(detail);
}
if (!res.ok) throw new Error(`HTTP ${res.status} ${res.statusText}`);
const body = await res.json();
models = body.data ?? body.models ?? [];
} catch (err) {

View File

@@ -24,35 +24,18 @@ export function registerStop(program) {
export async function runStopCommand(opts = {}) {
const pid = readPidFile("server");
// #9455: when the server was started with a supervisor (the default), killing only
// the child lets the supervisor respawn it immediately. The supervisor's PID is
// persisted separately by serve.mjs; SIGTERM it FIRST so its handler sets
// isShuttingDown=true and stops the child cleanly without respawning.
const supervisorPid = readPidFile("supervisor");
if (pid && isPidRunning(pid)) {
console.log(t("stop.stopping", { pid }));
try {
if (supervisorPid && isPidRunning(supervisorPid)) {
try {
process.kill(supervisorPid, "SIGTERM");
} catch {}
// Give the supervisor a moment to cascade the shutdown to its child so we
// don't race the child kill against the supervisor's own child stop.
await sleep(300);
}
// #8045: on win32, process.kill(pid, "SIGTERM") unconditionally force-terminates
// the target instead of delivering an interceptable signal, racing (and beating)
// the server's own async graceful shutdown / WAL checkpoint. stopProcessGracefully
// skips the immediate SIGTERM on win32 and just polls before escalating to SIGKILL.
if (isPidRunning(pid)) {
await stopProcessGracefully({ pid, timeoutMs: 5000, isPidRunning, sleep });
}
await stopProcessGracefully({ pid, timeoutMs: 5000, isPidRunning, sleep });
killAllSubprocesses();
cleanupPidFile("server");
cleanupPidFile("supervisor");
console.log(t("stop.stopped"));
return 0;
} catch (err) {
@@ -66,24 +49,10 @@ export async function runStopCommand(opts = {}) {
const port = opts.port ? parseInt(String(opts.port), 10) : 20128;
if (pid === null) {
console.log(t("stop.portFallback"));
// #9455: a stale supervisor PID file would let the port-fallback stop also
// leave the supervisor running and respawning. Stop it first.
if (supervisorPid && isPidRunning(supervisorPid)) {
try {
process.kill(supervisorPid, "SIGTERM");
} catch {}
}
const portFreed = await killByPort(port);
await killByPort(port);
killAllSubprocesses();
cleanupPidFile("server");
cleanupPidFile("supervisor");
// #9455: only report success when the port is actually free — previously stop
// printed "Server stopped." even when killByPort was a no-op (win32).
if (portFreed) {
console.log(t("stop.stopped"));
} else {
console.log(t("stop.notRunning"));
}
console.log(t("stop.stopped"));
return 0;
}
@@ -91,84 +60,31 @@ export async function runStopCommand(opts = {}) {
return 0;
}
/**
* Kill the process listening on `port`. Returns true once the port is free
* (or no listener was found), false if it could not be freed.
*
* #9455: previously this was a no-op on win32 (`if (win32) return;`) yet the
* caller still reported "Server stopped." — a lie. The win32 branch now uses
* `netstat -ano` to find LISTENING PIDs and `process.kill()` (SIGTERM then
* SIGKILL), mirroring the POSIX `lsof` path.
*/
export async function killByPort(port, deps = {}) {
const exec = deps.execFileAsync || execFileAsync;
const kill = deps.processKill || ((p, sig) => process.kill(p, sig));
const running = deps.isPidRunning || isPidRunning;
const wait = deps.sleep || sleep;
const platform = deps.platform || process.platform;
if (platform === "win32") {
return killByPortWin32(port, { exec, kill, running, wait });
}
return killByPortPosix(port, { exec, kill, running, wait });
}
async function killByPortPosix(port, { exec, kill, running, wait }) {
let pids = [];
async function killByPort(port) {
if (process.platform === "win32") return;
try {
const { stdout } = await exec("lsof", ["-ti", `:${port}`]);
pids = stdout
const { stdout } = await execFileAsync("lsof", ["-ti", `:${port}`]);
const pids = stdout
.trim()
.split("\n")
.map((p) => parseInt(p, 10))
.filter((p) => Number.isFinite(p) && p > 0);
for (const p of pids) {
try {
process.kill(p, "SIGTERM");
} catch {}
}
if (pids.length > 0) {
await sleep(1000);
for (const p of pids) {
try {
if (isPidRunning(p)) process.kill(p, "SIGKILL");
} catch {}
}
}
} catch {
// lsof not available or no process on port
}
return terminatePids(pids, { kill, running, wait });
}
async function killByPortWin32(port, { exec, kill, running, wait }) {
let pids = [];
try {
const { stdout } = await exec("netstat", ["-ano"]);
pids = parseNetstatPids(stdout, port);
} catch {
// netstat not available or empty
}
return terminatePids(pids, { kill, running, wait });
}
function parseNetstatPids(stdout, port) {
const portCol = `:${port}`;
const pids = [];
for (const line of stdout.split(/\r?\n/)) {
const cols = line.trim().split(/\s+/);
// Expected columns: Proto LocalAddress ForeignAddress State PID
if (cols.length < 5) continue;
if (cols[0] !== "TCP" && cols[0] !== "TCPv6") continue;
const local = cols[1] || "";
if (!local.endsWith(portCol)) continue;
if ((cols[cols.length - 2] || "").toUpperCase() !== "LISTENING") continue;
const pid = parseInt(cols[cols.length - 1], 10);
if (Number.isFinite(pid) && pid > 0 && !pids.includes(pid)) pids.push(pid);
}
return pids;
}
async function terminatePids(pids, { kill, running, wait }) {
if (pids.length === 0) return true;
for (const p of pids) {
try {
kill(p, "SIGTERM");
} catch {}
}
await wait(1000);
for (const p of pids) {
try {
if (running(p)) kill(p, "SIGKILL");
} catch {}
}
// Confirm the port is free: any PID still alive means we failed.
return pids.every((p) => !running(p));
}

View File

@@ -181,28 +181,6 @@ export async function runUpdateCommand(opts = {}) {
// --include=optional keeps the optionalDependencies (better-sqlite3, keytar,
// tls-client, llmlingua SLM stack) on update so an omit=optional config can't drop them.
execSync("npm install -g omniroute@latest --include=optional", { stdio: "inherit" });
// Trust-but-verify: `npm install -g` exits 0 even when a shadowing local install
// (e.g. ~/node_modules/omniroute ahead of the global prefix on PATH) means the
// binary the user actually runs was not touched. Re-read the running binary's
// version and warn instead of lying about success (#9475).
const afterVersion = await getCurrentVersion();
if (afterVersion && compareVersions(afterVersion, latest) < 0) {
printError(
`Global install updated to ${latest}, but the running binary still reports ${afterVersion}.`
);
console.log(
" A local `node_modules/omniroute` is likely shadowing the global install on PATH."
);
console.log(" Diagnose with:");
console.log(" which -a omniroute");
console.log(" command -v omniroute");
console.log(" npm prefix -g");
console.log(
" Then remove the shadowing local copy (e.g. `npm uninstall omniroute` from its directory)"
);
console.log(" or reorder PATH so the global bin comes first.");
return 1;
}
printSuccess(`Updated to version ${latest}`);
printInfo("Run `omniroute --version` to verify.");
return 0;

View File

@@ -94,12 +94,10 @@ export function isBetterSqliteBinaryValid() {
const magic = buf.toString("hex");
const os = platform();
let formatOk;
if (os === "linux")
formatOk = magic.startsWith("7f454c46"); // ELF
if (os === "linux") formatOk = magic.startsWith("7f454c46"); // ELF
else if (os === "darwin")
formatOk = magic.startsWith("cffaedfe") || magic.startsWith("cefaedfe"); // Mach-O
else if (os === "win32")
formatOk = magic.startsWith("4d5a"); // PE/MZ
else if (os === "win32") formatOk = magic.startsWith("4d5a"); // PE/MZ
else formatOk = true;
if (!formatOk) return false;
// File-format magic bytes alone do not guarantee the binary was built for the Node ABI
@@ -154,18 +152,9 @@ export function ensureBetterSqliteRuntime({ silent = false, force = false } = {}
if (!silent) process.stdout.write("[omniroute][runtime] better-sqlite3 OK\n");
return { betterSqlite: true };
}
if (!silent) {
process.stdout.write(
`[omniroute][runtime] Installing better-sqlite3@${BETTER_SQLITE3_VERSION} into runtime...\n`
);
}
const ok = npmInstallRuntime([`better-sqlite3@${BETTER_SQLITE3_VERSION}`], { silent });
if (!ok && !silent) {
process.stderr.write(
"[omniroute][runtime] better-sqlite3 install failed.\n" +
" This usually means npm install scripts are blocked.\n" +
" Try: npm install-scripts approve better-sqlite3\n"
);
process.stderr.write("[omniroute][runtime] better-sqlite3 install failed\n");
}
return { betterSqlite: ok && hasModule("better-sqlite3") && isBetterSqliteBinaryValid() };
}

View File

@@ -8,23 +8,13 @@ import {
computeRestartDelayMs,
waitUntilPortFree,
} from "./supervisorPolicy.mjs";
import { buildNodeRuntimeArgs } from "../../../scripts/build/runtime-env.mjs";
import { buildNodeHeapArgs } from "../../../scripts/build/runtime-env.mjs";
import { stopProcessGracefully } from "../../../src/shared/platform/windowsProcess.ts";
import {
isFatalInstrumentationHookFailure,
formatAndroidInstrumentationFailureHint,
} from "../utils/ensureAndroidCacheDir.mjs";
const CRASH_LOG_LINES = 50;
export class ServerSupervisor {
constructor({
serverPath,
env,
maxRestarts = DEFAULT_MAX_RESTARTS,
memoryLimit = 512,
onCrashCallback,
}) {
constructor({ serverPath, env, maxRestarts = DEFAULT_MAX_RESTARTS, memoryLimit = 512, onCrashCallback }) {
this.serverPath = serverPath;
this.env = env;
this.maxRestarts = maxRestarts;
@@ -35,54 +25,39 @@ export class ServerSupervisor {
this.crashLog = [];
this.child = null;
this.isShuttingDown = false;
this.instrumentationFailureHintPrinted = false;
}
start() {
this.startedAt = Date.now();
this.crashLog = [];
this.instrumentationFailureHintPrinted = false;
const showLog = process.env.OMNIROUTE_SHOW_LOG === "1";
// #5238: skip the explicit CLI --max-old-space-size when the user pinned the
// heap via NODE_OPTIONS (a CLI arg would shadow/override their value). The
// calibrated heap is already carried by env.NODE_OPTIONS either way.
const heapArgs = buildNodeHeapArgs(process.env, this.memoryLimit);
// #6321: stdout used to be discarded (`"ignore"`) whenever `--log`/OMNIROUTE_SHOW_LOG
// wasn't set (the default) — any debug/pino output written to stdout vanished
// silently, so a boot that never becomes ready looked like a dead hang with zero
// output even at APP_LOG_LEVEL=debug. Pipe stdout too and buffer it alongside
// stderr so a readiness timeout can surface what the child actually printed.
this.child = spawn(
process.versions.bun ? process.execPath : "node",
process.versions.bun
? [this.serverPath]
: buildNodeRuntimeArgs(process.env, this.memoryLimit, this.serverPath),
{
cwd: dirname(this.serverPath),
env: this.env,
stdio: showLog ? "inherit" : ["ignore", "pipe", "pipe"],
}
);
this.child = spawn(process.versions.bun ? process.execPath : "node", [
...(process.versions.bun ? [] : heapArgs),
this.serverPath,
], {
cwd: dirname(this.serverPath),
env: this.env,
stdio: showLog ? "inherit" : ["ignore", "pipe", "pipe"],
});
writePidFile("server", this.child.pid);
const bufferOutput = (data) => {
const text = data.toString();
const lines = text.split("\n").filter(Boolean);
const lines = data.toString().split("\n").filter(Boolean);
this.crashLog.push(...lines);
if (this.crashLog.length > CRASH_LOG_LINES) {
this.crashLog = this.crashLog.slice(-CRASH_LOG_LINES);
}
// Surface Android/Termux instrumentation-hook failures even when --log is
// off (output is only buffered otherwise).
if (!this.instrumentationFailureHintPrinted && isFatalInstrumentationHookFailure(text)) {
this.instrumentationFailureHintPrinted = true;
process.stderr.write(
formatAndroidInstrumentationFailureHint(
this.env?.XDG_CACHE_HOME || process.env.XDG_CACHE_HOME
)
);
}
};
if (this.child.stdout) {

View File

@@ -1,122 +0,0 @@
/**
* Next.js cache-dir prep for Android / Termux.
*
* Next.js `getCacheDirectory()` has no dedicated branch for
* `process.platform === "android"`. On that path it only accepts a cache root
* that *already* exists (`fs.existsSync` on `~/.cache` or a generic tmp dir).
* If neither exists it prints `Unsupported platform: android` and exits — the
* CLI can still look "running" while every request returns a bare HTTP 500
* because the instrumentation hook never loads (and so neither does logging).
*
* Termux Node sometimes reports `platform === "android"` and sometimes
* `"linux"` with Termux env signals (`TERMUX_VERSION` / `PREFIX`). Creating
* `~/.cache` (and pointing `XDG_CACHE_HOME` at it when unset) makes the probe
* succeed on both shapes.
*
* Call this *before* spawning or loading Next.js. Safe no-op on desktop
* platforms that are not Termux.
*/
import { existsSync, mkdirSync } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";
import { isTermux } from "../../../scripts/build/postinstallSupport.mjs";
/**
* @param {string} [platform]
* @param {NodeJS.ProcessEnv} [env]
* @returns {boolean}
*/
export function needsAndroidCacheDirPrep(platform = process.platform, env = process.env) {
return platform === "android" || isTermux(env);
}
/**
* @param {() => string} [homedirFn]
* @param {NodeJS.ProcessEnv} [env]
* @returns {string}
*/
export function resolveAndroidCacheDir(homedirFn = homedir, env = process.env) {
if (typeof env.XDG_CACHE_HOME === "string" && env.XDG_CACHE_HOME.trim()) {
return env.XDG_CACHE_HOME;
}
return join(homedirFn(), ".cache");
}
/**
* Ensure a writable cache directory exists for Next.js on Android/Termux.
*
* @param {object} [options]
* @param {string} [options.platform]
* @param {NodeJS.ProcessEnv} [options.env]
* @param {() => string} [options.homedirFn]
* @param {typeof mkdirSync} [options.mkdirSyncFn]
* @param {typeof existsSync} [options.existsSyncFn]
* @param {boolean} [options.setEnv] When true (default), set `XDG_CACHE_HOME` on `env`
* if unset so child processes inherit a known-writable cache root.
* @returns {{ prepared: boolean, cacheDir: string | null, created: boolean }}
*/
export function ensureAndroidCacheDir(options = {}) {
const {
platform = process.platform,
env = process.env,
homedirFn = homedir,
mkdirSyncFn = mkdirSync,
existsSyncFn = existsSync,
setEnv = true,
} = options;
if (!needsAndroidCacheDirPrep(platform, env)) {
return { prepared: false, cacheDir: null, created: false };
}
const cacheDir = resolveAndroidCacheDir(homedirFn, env);
let created = false;
if (!existsSyncFn(cacheDir)) {
mkdirSyncFn(cacheDir, { recursive: true });
created = true;
}
if (setEnv && !(typeof env.XDG_CACHE_HOME === "string" && env.XDG_CACHE_HOME.trim())) {
env.XDG_CACHE_HOME = cacheDir;
}
return { prepared: true, cacheDir, created };
}
/**
* Detect Next.js instrumentation-hook failures that leave the server looking
* "up" while requests get silent HTTP 500s (typical when the Android cache
* probe failed before logging started).
*
* @param {string} text
* @returns {boolean}
*/
export function isFatalInstrumentationHookFailure(text) {
if (!text) return false;
return (
/Unsupported platform:\s*android/i.test(text) ||
/error occurred while loading instrumentation hook/i.test(text)
);
}
/**
* Operator-facing hint when that instrumentation failure shows up in child
* output — defense in depth if prep was skipped or a future Next.js probe
* regresses.
*
* @param {string} [cacheDir]
* @returns {string}
*/
export function formatAndroidInstrumentationFailureHint(cacheDir) {
const dir = cacheDir || join(homedir(), ".cache");
return (
`\n\x1b[31m✖ Next.js instrumentation failed on Android/Termux (likely missing cache dir).\x1b[0m\n` +
` OmniRoute tried to create a writable cache at:\n` +
` \x1b[36m${dir}\x1b[0m\n` +
` Manual workaround (survives reinstalls — do NOT patch dist/server.js):\n` +
` \x1b[36mmkdir -p ~/.cache\x1b[0m\n` +
` then restart: \x1b[36momniroute serve\x1b[0m\n` +
` See: docs/guides/TERMUX_GUIDE.md → Troubleshooting → Unsupported platform: android\n`
);
}

View File

@@ -2,9 +2,7 @@ import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "
import { join } from "node:path";
import { resolveDataDir } from "../data-dir.mjs";
// #9455: "supervisor" must be tracked so killAllSubprocesses() can stop the
// supervisor process, not just the child server it spawned (and respawns).
const SERVICES = ["server", "supervisor", "mitm", "tunnel/cloudflared", "tunnel/tailscale"];
const SERVICES = ["server", "mitm", "tunnel/cloudflared", "tunnel/tailscale"];
function getServicePidPath(service) {
return join(resolveDataDir(), service, ".pid");

View File

@@ -1,47 +0,0 @@
/**
* Argument escaping for child processes spawned with `shell: true` on Windows.
*
* The launchers (`omniroute launch`, `omniroute launch-codex`) must go through
* cmd.exe on win32 because the target binaries are npm `.cmd` shims that Node
* cannot exec directly (CVE-2024-27980). With `shell: true` Node joins argv with
* plain spaces and no escaping at all (the DEP0190 warning), so anything with a
* space, a quote or a cmd metacharacter reaches the child mangled.
*/
/** cmd.exe metacharacters that stay live inside a quoted argument. */
const WIN_META_CHARS = /([()\][%!^"`<>&|;, *?])/g;
/**
* Escape one argument for a cmd.exe command line built by `shell: true`.
*
* Two layers, in order:
* 1. the CRT argv rules the target binary parses (double the backslashes that
* precede a quote, escape embedded quotes, wrap in quotes);
* 2. cmd.exe's metacharacters, caret-escaped — applied TWICE because the
* target is an npm `.cmd` shim that forwards `%*` to node, so the line is
* parsed by cmd a second time. Single-escaping truncated any argument at
* the first `&` or `|`. (Same rule as cross-spawn's doubleEscapeMetaChars.)
*
* @param {unknown} arg
* @returns {string}
*/
export function escapeWindowsShellArg(arg) {
const s = String(arg);
if (s === "") return '""';
let out = s.replace(/(\\*)"/g, '$1$1\\"').replace(/(\\*)$/, "$1$1");
out = `"${out}"`;
return out.replace(WIN_META_CHARS, "^$1").replace(WIN_META_CHARS, "^$1");
}
/**
* Escape a whole argv for the `shell: true` path. Off Windows there is no shell,
* so argv is passed through untouched.
*
* @param {string[]} args
* @param {NodeJS.Platform|string} platform
* @returns {string[]}
*/
export function quoteShellArgs(args, platform) {
const list = [...(args ?? [])];
return platform === "win32" ? list.map(escapeWindowsShellArg) : list;
}

View File

@@ -145,15 +145,6 @@ function loadEnvFile() {
loadEnvFile();
// Next.js has no android branch in getCacheDirectory(): if ~/.cache (and tmp)
// do not already exist it aborts the instrumentation hook, and every request
// then returns a silent HTTP 500 even though the CLI still looks "running".
// Create the cache dir (and set XDG_CACHE_HOME when unset) before serve/Next.
{
const { ensureAndroidCacheDir } = await import("./cli/utils/ensureAndroidCacheDir.mjs");
ensureAndroidCacheDir();
}
// Generate STORAGE_ENCRYPTION_KEY if not set (persisted to ~/.omniroute/.env)
// This ensures the key survives across upgrades and is not regenerated on each install.
// See: https://github.com/diegosouzapw/OmniRoute/issues/1622

View File

@@ -0,0 +1 @@
- **feat(sse):** every completion response now carries an `X-OmniRoute-Decision: strategy=<name>; provider=<alias>; latency_ms=<n>` header exposing the routing decision — `<name>` is the combo strategy (`priority`, `weighted`, `fusion`, etc.) or `single` for a non-combo request — for client-side debugging/analytics without server log access (#6022 — thanks @chirag127).

View File

@@ -0,0 +1 @@
- **feat(ws):** the live-dashboard WebSocket server now auto-starts in-process (via `instrumentation-node.ts`) across every deployment mode — dev, production, Docker, Electron — with no separate sidecar script; the default WS port moved from 20129 to **20132** to avoid colliding with `API_PORT` in split-port setups, the deprecated `OMNIROUTE_DISABLE_LIVE_WS` env was consolidated into `OMNIROUTE_ENABLE_LIVE_WS` (default enabled), and the WS path is now derived from `NEXT_PUBLIC_LIVE_WS_PUBLIC_URL`'s pathname (`/live-ws` fallback) (#6072 — thanks @ianriizky).

View File

@@ -0,0 +1 @@
- feat(sse): configurable per-model upstream header-response timeout override, precedence model > provider > global; applied to codex reasoning-heavy tiers (gpt-5.5-high/xhigh, gpt-5.6-\*-high/xhigh) (#6354)

View File

@@ -0,0 +1 @@
- **feat(dashboard):** Replace free-text model inputs in the Routing (web search route), Combo Defaults (handoff model), and Background Degradation tabs with a `hidePaidModels`-aware `ModelSelectField`, add a fail-open "paid-only pattern" warning to the per-model routing rule pattern field, and reject paid-only model targets at save time on `PATCH /api/settings`, `PATCH /api/settings/combo-defaults`, and `PUT /api/settings/background-degradation` when `hidePaidModels` is on ([#6540](https://github.com/diegosouzapw/OmniRoute/issues/6540))

View File

@@ -0,0 +1 @@
- **feat(compression):** new **omniglyph** engine (context-as-image) — renders system prompt, tool docs, and dense history as compact PNG pages the model reads instead of text (~10× fewer tokens on the converted block; 5970% end-to-end measured). Works stacked with RTK/Caveman (`stackPriority: 90`) or standalone (`mode: omniglyph`); restricted to Claude Fable 5 over the direct Anthropic route, fail-closed gates with `skip:<reason>` techniques, preview (`stable: false`, off by default) (#6556). Dependency bumped to `omniglyph@^1.0.2` for upstream ReDoS fixes (#6661).

View File

@@ -0,0 +1 @@
- **feat(sse):** rate-limit request queue admission control — `resilienceSettings.requestQueue.maxQueueDepth` (default `0` = disabled, opt-in 0100000) fast-rejects a request with a typed `RATE_LIMIT_QUEUE_FULL` error once the local per-provider+connection queue already holds `maxQueueDepth` requests, instead of growing the queue unboundedly; the factory default for `requestQueue.maxWaitMs` (how long a request may wait before being dropped) also fell from 120s to 15s so a saturated queue fails fast (#6593 — thanks @chirag127).

View File

@@ -0,0 +1 @@
- **feat(sandbox):** the skill sandbox gained a container-provider abstraction that auto-detects and uses the best native runtime per host — Apple Container (macOS 26+), WSL container (`wslc.exe`), OrbStack, Podman — instead of hardcoding `docker run`, removing the Docker Desktop requirement on macOS/Windows (#6611 — thanks @KooshaPari).

View File

@@ -0,0 +1 @@
- feat(oauth): accept the full ChatGPT session JSON (not just a bare access token) when pasting Codex credentials manually or via `POST /api/oauth/codex/import-token` (#6636)

View File

@@ -0,0 +1 @@
- feat(sse): add 5 no-key g4f.space gateway providers — `g4f-groq`, `g4f-gemini`, `g4f-pollinations`, `g4f-ollama`, `g4f-nvidia` — a free, no-signup reverse proxy (gpt4free project) fronting Groq, Gemini, Pollinations, Ollama, and NVIDIA NIM, rate-limited to 5 req/min per IP (#6650 — thanks @chirag127).

View File

@@ -0,0 +1 @@
- feat(sse): add DeepInfra as a video-generation provider via its native synchronous inference endpoint (#6653)

View File

@@ -0,0 +1 @@
- feat(providers): add Freepik (Magnific Mystic) API-key image generation provider — async submit/poll flow with realism/fluid/zen/flexible/super_real/editorial_portraits models (#6654)

View File

@@ -0,0 +1 @@
- feat(providers): add Rev AI speech-to-text provider with async job upload/poll/transcript flow (#6655)

View File

@@ -0,0 +1 @@
- **feat(providers):** add **Segmind** as an image + video generation provider — `x-api-key` auth against `POST https://api.segmind.com/v1/{model}`, with a curated starter model list (Flux, Stable Diffusion XL/3.5, Kandinsky for image; Wan, Hunyuan, LTX, Kling for video) (#6656).

View File

@@ -0,0 +1 @@
- feat(providers): add Gladia as an async speech-to-text provider (#6657)

View File

@@ -0,0 +1 @@
- feat(video): add Novita AI as a video-generation provider (Wan/Kling async submit-poll) (#6658)

View File

@@ -0,0 +1 @@
- **feat(providers):** add Speechmatics as an STT provider — async batch transcription (Enhanced operating point), 8 hours/month free tier, no credit card required. Streaming (real-time) mode is out of scope for v1. (#6659)

View File

@@ -0,0 +1 @@
- feat(providers): add Mixedbread AI as an embeddings provider (`mxbai-embed-large-v1`, `mxbai-embed-2d-large-v1`, free tier) (#6660)

View File

@@ -0,0 +1 @@
- **feat(providers):** add Felo (felo.ai) as a free, no-signup, no-API-key chat/search-agent aggregator provider (`felo-web`) — joins the existing `-web` family (DuckDuckGo AI Chat, Blackbox, etc). Five models (`felo-chat`, `felo-search`, `felo-scholar`, `felo-social`, `felo-document`) map to Felo's search categories; the executor opens a search thread then translates Felo's bespoke SSE stream into OpenAI-compatible chunks (#6666).

View File

@@ -0,0 +1 @@
- **feat(providers):** add gTTS (Google Translate TTS) as a free, no-signup audio-speech provider — routes through Google's current `batchexecute` RPC endpoint (the previously proposed `translate_tts` endpoint is deprecated), splitting input at the 100-char-per-request limit. (#6667)

View File

@@ -0,0 +1 @@
- **feat(sse):** add EdgeTTS (Microsoft Edge "Read Aloud") as a free, no-API-key `audio-tts` provider — the first WebSocket-transport speech provider, with per-client-IP rate limiting. (#6668)

View File

@@ -0,0 +1 @@
- feat(providers): add FreeTheAi as an OpenAI-compatible gateway provider with a free Discord-signup tier (#6670)

View File

@@ -0,0 +1 @@
- feat(sse): add Microsoft Designer as an unofficial web-session image provider, reverse-engineered submit-then-poll DallE.ashx flow (#6672)

View File

@@ -0,0 +1 @@
- **feat(providers):** add Hailuo Web (`hailuo-web`) — a free, `_token`-based web-cookie chat provider for the MiniMax consumer chat product at hailuo.ai, ported from the g4f reference implementation (MD5-chain request signing, custom `event:`/`data:` SSE parsing). Distinct from the existing paid API-key `minimax`/`minimax-cn` providers. (#6673)

View File

@@ -0,0 +1 @@
- **feat(cli):** new `omniroute auth export` command dumps DECRYPTED provider credentials (`apiKey`/`accessToken`/`refreshToken`/`idToken`) for one connection (`--id <id>`) or all connections, as `json` or `env` (`--format`), local-only and gated behind `--force` — no DB access happens without it, a stderr warning banner prints before any plaintext, `--out <file>` writes with `0600` permissions, and per-field decrypt failures surface as a `<field>DecryptFailed` boolean instead of aborting the export or leaking the caught error text (#6683)

View File

@@ -0,0 +1 @@
- **feat(mitm):** the AgentBridge static MITM server (`server.cjs`) can now issue a per-host TLS leaf from a persisted local root CA (`src/mitm/cert/rootCa.ts`, reusing the CA/leaf crypto already proven for TPROXY in `tproxy/dynamicCert.ts`) instead of a single static self-signed leaf scoped only to the 4 antigravity hosts — a fresh install covers the full `MITM_TOOL_HOSTS` set automatically; an install that already trusted the old leaf keeps using it until the operator opts in via `MITM_ROOT_CA_ENABLED=true` (`src/mitm/cert/migration.ts`), so no existing install is silently upgraded to the more powerful any-host-signing CA trust model (#6684).

View File

@@ -0,0 +1 @@
- **fix(sse):** skip `thinkingConfig` for Gemma models on the OpenAI→Gemini path so OpenAI-shape clients no longer get a 400 from Vertex. (thanks @chy1211)

View File

@@ -0,0 +1 @@
- **feat(xai):** route xAI clients to Grok's native `/v1/responses` endpoint instead of the chat-completions bridge. (thanks @ryanngit)

View File

@@ -0,0 +1 @@
- **feat(models):** add a Settings → AI "Model Overrides" UI plus `/api/model-capability-overrides` CRUD and a `model_capability_overrides` table, letting operators set a manual max-output-token override per provider/model (#6727 — thanks @xz-dev).

View File

@@ -0,0 +1 @@
- **feat(api):** add `Vary: Accept-Encoding` to token-authenticated `/v1*`/`/v1beta*` responses so downstream caches distinguish compressed vs uncompressed variants (RFC 9110 §12.5.5). (thanks @chirag127)

View File

@@ -0,0 +1 @@
- feat(sse): add Notion AI Web (Unofficial/Experimental) cookie-session provider (#6758)

View File

@@ -0,0 +1 @@
- **feat(dashboard):** add per-routing-combo compression-mode override to the Compression Combos page under Context & Cache, alongside the existing combo-card quick override. (#6760)

Some files were not shown because too many files have changed in this diff Show More