chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 13:39:12 +08:00
commit d8dcd5f6d1
8604 changed files with 2479390 additions and 0 deletions
+224
View File
@@ -0,0 +1,224 @@
# OmniRoute CLI — Internal Conventions
> Status: normative. Source: `_tasks/features-v3.8.0/cli/fase-0-preparacao/0.3-definir-convencoes.md`.
> This file is the authoritative reference for every new or migrated CLI command.
> If reality diverges from this document, fix the code first; only edit this file
> after the discrepancy has been justified in a PR.
## 1. Subcommand style
**Standard**: `git`-style nested verbs.
```
omniroute keys add openai sk-xxx
omniroute combo switch fastest
omniroute memory search "react hooks"
```
**Not allowed**:
```
omniroute --add-key openai sk-xxx # ❌ flag-as-verb
omniroute add-key openai sk-xxx # ❌ hyphen at the top level
```
## 2. Flags
- Only `--long` and `-s` shorts (one-letter shorts reserved for very common
flags: `-h`, `-v`, `-o`, `-q`, `--no-open`).
- Format: `--api-key sk-xxx` (space). `=` accepted for parity but doc uses space.
- Naming: kebab-case (`--api-key`, `--non-interactive`, `--max-tokens`).
- Booleans: `--no-foo` (negative) and `--foo` (positive). Default `false` unless
documented.
- Multi-value: repeat the flag (`--header X-A=1 --header X-B=2`).
## 3. Output (`--output`)
| Value | Use case |
| ------- | -------------------------------------------- |
| `table` | default human-readable |
| `json` | single JSON object, pretty-printed |
| `jsonl` | streamed objects, one per line (logs, lists) |
| `csv` | spreadsheet ingestion |
Related flags:
- `--quiet` / `-q` — suppress headers/spinners (pipe-friendly).
- `--no-color` — force ANSI off (auto-detected if `!stdout.isTTY`).
Helper: `emit(rows, opts)` from `bin/cli/output.mjs` handles all four formats.
## 4. Exit codes
| Code | Meaning |
| ----- | --------------------------------- |
| `0` | success |
| `1` | generic error (uncaught, runtime) |
| `2` | invalid argument / misuse |
| `3` | server offline (when required) |
| `4` | auth / permission (401/403) |
| `5` | rate limit / quota (429) |
| `124` | timeout |
Helper: `exitWith(code, message?)` from `bin/cli/exit.mjs` (added under
`output.mjs` if needed) — always uses these constants. **Never** raw
`process.exit(N)` in command code.
## 5. HTTP errors + retry/backoff
All API calls go through `apiFetch(path, opts)` (`bin/cli/api.mjs`), which:
- Reads base URL from `OMNIROUTE_BASE_URL` env or `~/.omniroute/config.json`
(active profile).
- Injects `Authorization: Bearer ${OMNIROUTE_API_KEY}` when available.
- Injects `x-omniroute-cli-token` when applicable (see task 8.12).
- Applies a per-attempt timeout (`--timeout 30000`, default 30s).
- Maps status → exit code (401→4, 429→5, 5xx→1, etc.).
- Never exposes `err.stack` (CLAUDE.md hard rule #12).
- Applies exponential backoff with jitter on retryable statuses.
### Retry defaults
```js
export const RETRY_DEFAULTS = {
maxAttempts: 3, // 1 initial + 2 retries
baseMs: 500,
maxMs: 8000, // jitter can slightly exceed
jitter: true, // ±25%
retryableStatuses: [408, 425, 429, 502, 503, 504],
retryableErrorCodes: [
"ECONNRESET",
"ECONNREFUSED",
"ETIMEDOUT",
"ENOTFOUND",
"EAI_AGAIN",
"EPIPE",
],
};
```
### Global flags wired
- `--retry` (default on) / `--no-retry`
- `--retry-max <n>` (default 3) — total attempts
- `--timeout <ms>` (default 30000) — per attempt
- `--retry-on <csv>` — extra retryable statuses (e.g. `--retry-on 500`)
### Method semantics
- Mutations (`POST`/`PUT`/`DELETE`) retry **only** on idempotent-ish statuses
(`502`/`503`/`504`/`408`/network), never `409`/`422`. This avoids duplicate
side-effects.
- `GET` retries all `RETRY_DEFAULTS.retryableStatuses`.
- SSE / streaming does **not** auto-retry (operator decides).
- Optional `--idempotency-key <uuid>` for extra-safe mutations.
### Status → exit code map
| Status | Exit | Retry? |
| --------------- | ---- | ------------------------------ |
| 200299 | 0 | n/a |
| 400 | 2 | no |
| 401 | 4 | no |
| 403 | 4 | no |
| 404 | 2 | no |
| 408 | 124 | **yes** |
| 409 | 1 | no (mutations) |
| 422 | 2 | no |
| 425 | 1 | **yes** |
| 429 | 5 | **yes** (respects Retry-After) |
| 500 | 1 | configurable (default no) |
| 502 / 503 / 504 | 1 | **yes** |
| Network errors | 1 | **yes** |
| Timeout | 124 | **yes** |
## 6. Internationalization
- Every user-facing string goes through `t("module.key", vars)`.
- Catalogs live in `bin/cli/locales/{locale}.json` (nested objects).
42 files ship out-of-the-box: `en`, `pt-BR`, and 40 additional locales.
11 locales are scaffold-only (empty `{}`); all keys fall back to `en` automatically.
- Detection order: `--lang` flag → `OMNIROUTE_LANG` env → `LC_ALL``LC_MESSAGES``LANG``en`.
- Locale persisted via `config lang set <code>` — saves `OMNIROUTE_LANG` to `~/.omniroute/.env`.
- Missing keys return the key itself (no crash).
- PRs that add new strings **must** update `en.json` and `pt-BR.json`.
Other locale files are best-effort; missing keys silently fall back to `en`.
- `normalize()` in `i18n.mjs` validates locale codes via `/^[a-zA-Z0-9-]+$/` to
prevent path traversal — never pass raw filesystem paths.
- Canonical locale list: `config/i18n.json` — source of truth used by both CLI and
dashboard i18n pipelines.
### Adding a new locale file
1. Add entry to `config/i18n.json` with `code`, `english`, `native`, `flag`.
2. Run `node bin/cli/scripts/generate-locales.mjs` — creates `bin/cli/locales/{code}.json`.
3. Fill in translations (or leave as `{}` for en-fallback scaffold).
4. The pre-commit hook `check-cli-i18n` will verify all `t()` keys exist in `en.json`.
## 7. Logs / output channels
- `stdout` — useful output (parseable when `--output json|jsonl|csv`).
- `stderr` — progress, warnings, errors, spinners.
- `--verbose` / `-V` — extra detail on stderr.
- `--debug` — stack traces, request bodies (dev-mode only; redacts secrets).
## 8. Server-first / DB-fallback
Single helper:
```js
import { withRuntime } from "./runtime.mjs";
await withRuntime(async ({ kind, api, db }) => {
if (kind === "http")
return api("/api/combos", { retry: false, timeout: 5000, acceptNotOk: true });
return db.combos.getCombos();
});
```
- `kind: "http"` when server is up (preferred). `api` is `apiFetch` bound to
the current profile/base-URL.
- `kind: "db"` when server is offline. `db` exposes typed module exports:
- `db.combos``src/lib/db/combos.ts` (getCombos, getComboByName, createCombo,
deleteComboByName, setActiveCombo, …)
- `db.recovery``src/lib/db/recovery.ts` (countEncryptedCredentials,
resetEncryptedColumns)
- Mutations that require server **must** error with exit code `3` when the
server is down, never silently fall back.
- **Never** write raw SQL in commands — always go through `src/lib/db/` modules.
The Semgrep rule at `.semgrep/rules/cli-no-sqlite.yaml` enforces this at commit time.
## 9. Audit of destructive actions
Commands that mutate state (delete, reset, `--force`) **must**:
- Ask for interactive confirmation (skipped with `--yes`).
- POST to `/api/compliance/audit-log` when the server is up.
- Support `--dry-run` (preview without effect).
## 10. Secrets
- **Never** log secrets. Mask as `sk-***-xxx` via `maskSecret()` from
`bin/cli/output.mjs`.
- **Never** accept a secret via positional without warning. Prefer:
- env (`OMNIROUTE_*_API_KEY`)
- stdin (`--api-key-stdin`)
- interactive `askSecret()` (echo off — already implemented in `io.mjs`)
- Secrets must not appear in `--verbose` / `--debug` output.
## 11. Testing baseline
- Every new command ships with at least one smoke test (happy path + one
error path).
- Use `tests/unit/cli-*.test.ts` naming. Prefer `node:test` for CLI suites
(no extra deps).
- Coverage target: ≥60% for `bin/cli/commands/`, ≥75% for `bin/cli/` overall
after Fase 8.
## 12. References
- CLAUDE.md hard rules — especially #11 (publicCreds), #12 (error
sanitization), #13 (shell injection).
- `docs/security/ERROR_SANITIZATION.md` — the only acceptable error shapes.
- `tests/unit/cli-tools-i18n.test.ts` — current i18n infrastructure (pre-`t()`).
- Commander.js docs — Options & subcommand patterns.
+146
View File
@@ -0,0 +1,146 @@
# bin/cli — OmniRoute CLI internals
This directory contains the CLI runtime, helpers, and commands for the `omniroute` binary.
## Structure
```
bin/cli/
├── CONVENTIONS.md ← normative design rules (read this first)
├── README.md ← this file
├── program.mjs ← Commander setup — global flags, registerCommands()
├── api.mjs ← apiFetch() — all HTTP calls + retry/backoff
├── runtime.mjs ← withRuntime() — server-first / DB-fallback
├── i18n.mjs ← t() — i18n helper + locale detection
├── output.mjs ← emit() — table/json/jsonl/csv + printSuccess/printError
├── io.mjs ← ask() / askSecret() — interactive prompts
├── data-dir.mjs ← resolveDataDir() / resolveStoragePath()
├── sqlite.mjs ← openOmniRouteDb() — DB bootstrap
├── encryption.mjs ← encrypt/decrypt credentials
├── provider-catalog.mjs ← static provider catalog
├── provider-store.mjs ← DB CRUD for provider_connections
├── provider-test.mjs ← testProviderApiKey()
├── settings-store.mjs ← DB CRUD for key_value settings
├── locales/
│ ├── en.json ← English strings (source of truth, 42+ locales)
│ ├── pt-BR.json ← Portuguese (Brazil) — fully translated
│ └── {locale}.json ← 40 additional locales (ar, az, de, es, fr, ja, zh-CN, …)
├── scripts/
│ └── generate-locales.mjs ← scaffold new locale files from config/i18n.json
└── commands/
├── setup.mjs
├── doctor.mjs
├── providers.mjs
├── config.mjs ← includes `config lang get/set/list`
├── status.mjs
├── logs.mjs
└── update.mjs
```
## Key helpers
### `apiFetch(path, opts)` — `api.mjs`
All HTTP calls to the OmniRoute server must go through this wrapper.
```js
import { apiFetch } from "./api.mjs";
const res = await apiFetch("/api/health");
if (!res.ok) await res.assertOk(); // throws ApiError with mapped exit code
const data = await res.json();
```
Options:
- `baseUrl` — override base URL (default: `OMNIROUTE_BASE_URL` env or `localhost:20128`)
- `apiKey` — override API key (default: `OMNIROUTE_API_KEY`)
- `method`, `body`, `headers` — standard fetch options
- `timeout` — per-attempt ms (default: `30000`)
- `retry``false` to disable (default: enabled)
- `retryMax` — total attempts (default: `3`)
- `verbose` — log retry attempts to stderr
### `withRuntime(fn, opts)` — `runtime.mjs`
Provides server-first / DB-fallback transparently.
```js
import { withRuntime } from "./runtime.mjs";
await withRuntime(async (ctx) => {
if (ctx.kind === "http") {
const res = await ctx.api("/v1/providers");
return res.json();
}
return ctx.db.prepare("SELECT * FROM provider_connections").all();
});
```
- `opts.requireServer = true` — throws `ServerOfflineError` (exit 3) if offline
- `opts.preferDb = true` — always use DB (skip server check)
### `t(key, vars)` — `i18n.mjs`
Internationalized strings. Catalog loaded from `locales/{locale}.json`.
```js
import { t } from "./i18n.mjs";
console.log(t("common.serverOffline"));
console.log(t("setup.testFailed", { error: err.message }));
```
Locale detection order: `OMNIROUTE_LANG``LC_ALL``LC_MESSAGES``LANG``en`.
### `emit(data, opts)` — `output.mjs`
Format-aware output. Reads `opts.output` to select table/json/jsonl/csv.
```js
import { emit, printError, EXIT_CODES } from "./output.mjs";
emit(providers, { output: opts.output ?? "table" });
printError("Something went wrong");
process.exit(EXIT_CODES.SERVER_OFFLINE);
```
## Locale selection
The CLI displays text in the user's language. Detection order:
1. `--lang <code>` flag on the command line
2. `OMNIROUTE_LANG` environment variable
3. System env: `LC_ALL``LC_MESSAGES``LANG`
4. Fallback: `en`
**Set permanently:**
```bash
omniroute config lang set pt-BR # saves to ~/.omniroute/.env
omniroute config lang list # show all 42 available locales
omniroute config lang get # show currently active locale
```
**One-time override:**
```bash
omniroute --lang de providers list # run in German, not persisted
OMNIROUTE_LANG=ja omniroute status # same effect via env
```
**Adding a new locale**: add entry to `config/i18n.json`, then run:
```bash
node bin/cli/scripts/generate-locales.mjs
```
## Adding a new command
1. Create `bin/cli/commands/your-command.mjs`
2. Export `registerYourCommand(program)` following the Commander pattern
3. Register in `bin/cli/commands/registry.mjs`
4. Add strings to `locales/en.json` and `locales/pt-BR.json`
5. Write test in `tests/unit/cli-your-command.test.ts`
See `CONVENTIONS.md` for exit codes, flag naming, output format, and destructive-action rules.
+70
View File
@@ -0,0 +1,70 @@
// AUTO-GENERATED from docs/openapi.yaml. Do not edit.
import { apiFetch } from "../api.mjs";
import { emit } from "../output.mjs";
import { readFileSync } from "node:fs";
export function register_agent_skills(parent) {
const tag = parent.command("agent-skills").description("Agent Skills endpoints");
tag.command("get-api-agent-skills")
.description("List agent skills catalog")
.option("--category <category>", "Filter by category (api = REST API skills, cli = CLI skills)")
.option("--area <area>", "Filter by area slug (e.g. \"providers\", \"models\", \"cli-serve\")")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/agent-skills";
const qs = new URLSearchParams();
if (opts.category != null) qs.set("category", String(opts.category));
if (opts.area != null) qs.set("area", String(opts.area));
if (qs.toString()) url += "?" + qs.toString();
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-agent-skills-id-")
.description("Get a single agent skill")
.requiredOption("--id <id>", "Canonical skill ID")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/agent-skills/{id}";
url = url.replace("{id}", encodeURIComponent(opts.id ?? ""));
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-agent-skills-id-raw")
.description("Get raw SKILL.md content")
.requiredOption("--id <id>", "Canonical skill ID")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/agent-skills/{id}/raw";
url = url.replace("{id}", encodeURIComponent(opts.id ?? ""));
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-agent-skills-coverage")
.description("Get SKILL.md coverage stats")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/agent-skills/coverage";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-agent-skills-generate")
.description("Trigger SKILL.md generator")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/agent-skills/generate";
let body;
if (opts.body) {
body = opts.body.startsWith("@")
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "POST", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
}
+155
View File
@@ -0,0 +1,155 @@
// AUTO-GENERATED from docs/openapi.yaml. Do not edit.
import { apiFetch } from "../api.mjs";
import { emit } from "../output.mjs";
import { readFileSync } from "node:fs";
export function register_agentbridge(parent) {
const tag = parent.command("agentbridge").description("AgentBridge endpoints");
tag.command("get-api-tools-agent-bridge-agents")
.description("List all 9 IDE agents with current state")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/tools/agent-bridge/agents";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-tools-agent-bridge-state")
.description("Get global AgentBridge server state")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/tools/agent-bridge/state";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-tools-agent-bridge-server")
.description("Control AgentBridge MITM server")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/tools/agent-bridge/server";
let body;
if (opts.body) {
body = opts.body.startsWith("@")
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "POST", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-tools-agent-bridge-agents-agent-id-dns")
.description("Enable or disable DNS for one agent")
.requiredOption("--agent-id <agentId>", "")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/tools/agent-bridge/agents/{agentId}/dns";
url = url.replace("{agentId}", encodeURIComponent(opts.agentId ?? ""));
let body;
if (opts.body) {
body = opts.body.startsWith("@")
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "POST", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-tools-agent-bridge-agents-agent-id-mappings")
.description("Get model mappings for one agent")
.requiredOption("--agent-id <agentId>", "")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/tools/agent-bridge/agents/{agentId}/mappings";
url = url.replace("{agentId}", encodeURIComponent(opts.agentId ?? ""));
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("put-api-tools-agent-bridge-agents-agent-id-mappings")
.description("Update model mappings for one agent")
.requiredOption("--agent-id <agentId>", "")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/tools/agent-bridge/agents/{agentId}/mappings";
url = url.replace("{agentId}", encodeURIComponent(opts.agentId ?? ""));
let body;
if (opts.body) {
body = opts.body.startsWith("@")
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "PUT", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-tools-agent-bridge-bypass")
.description("List bypass patterns (hosts never decrypted)")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/tools/agent-bridge/bypass";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("put-api-tools-agent-bridge-bypass")
.description("Update user bypass patterns")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/tools/agent-bridge/bypass";
let body;
if (opts.body) {
body = opts.body.startsWith("@")
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "PUT", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-tools-agent-bridge-cert")
.description("Download or regenerate the AgentBridge CA certificate")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/tools/agent-bridge/cert";
let body;
if (opts.body) {
body = opts.body.startsWith("@")
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "POST", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-tools-agent-bridge-upstream-ca")
.description("Get configured upstream CA cert path")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/tools/agent-bridge/upstream-ca";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-tools-agent-bridge-upstream-ca")
.description("Set upstream CA cert path for corporate TLS environments")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/tools/agent-bridge/upstream-ca";
let body;
if (opts.body) {
body = opts.body.startsWith("@")
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "POST", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
}
+42
View File
@@ -0,0 +1,42 @@
// AUTO-GENERATED from docs/openapi.yaml. Do not edit.
import { apiFetch } from "../api.mjs";
import { emit } from "../output.mjs";
import { readFileSync } from "node:fs";
export function register_api_keys(parent) {
const tag = parent.command("api-keys").description("API Keys endpoints");
tag.command("get-api-keys")
.description("List API keys")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/keys";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-keys")
.description("Create API key")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/keys";
let body;
if (opts.body) {
body = opts.body.startsWith("@")
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "POST", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("delete-api-keys-id-")
.description("Delete API key")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/keys/{id}";
const res = await apiFetch(url, { method: "DELETE", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
}
+40
View File
@@ -0,0 +1,40 @@
// AUTO-GENERATED from docs/openapi.yaml. Do not edit.
import { apiFetch } from "../api.mjs";
import { emit } from "../output.mjs";
import { readFileSync } from "node:fs";
export function register_audio(parent) {
const tag = parent.command("audio").description("Audio endpoints");
tag.command("post-api-v1-audio-speech")
.description("Generate speech audio")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/v1/audio/speech";
let body;
if (opts.body) {
body = opts.body.startsWith("@")
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "POST", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-v1-audio-transcriptions")
.description("Transcribe audio")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/v1/audio/transcriptions";
let body;
if (opts.body) {
body = opts.body.startsWith("@")
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "POST", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
}
+58
View File
@@ -0,0 +1,58 @@
// AUTO-GENERATED from docs/openapi.yaml. Do not edit.
import { apiFetch } from "../api.mjs";
import { emit } from "../output.mjs";
import { readFileSync } from "node:fs";
export function register_chat(parent) {
const tag = parent.command("chat").description("Chat endpoints");
tag.command("post-api-v1-chat-completions")
.description("Create chat completion")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/v1/chat/completions";
let body;
if (opts.body) {
body = opts.body.startsWith("@")
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "POST", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-v1-providers-provider-chat-completions")
.description("Create chat completion (provider-specific)")
.requiredOption("--provider <provider>", "")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/v1/providers/{provider}/chat/completions";
url = url.replace("{provider}", encodeURIComponent(opts.provider ?? ""));
let body;
if (opts.body) {
body = opts.body.startsWith("@")
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "POST", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-v1-api-chat")
.description("Ollama-compatible chat endpoint")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/v1/api/chat";
let body;
if (opts.body) {
body = opts.body.startsWith("@")
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "POST", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
}
+361
View File
@@ -0,0 +1,361 @@
// AUTO-GENERATED from docs/openapi.yaml. Do not edit.
import { apiFetch } from "../api.mjs";
import { emit } from "../output.mjs";
import { readFileSync } from "node:fs";
export function register_cli_tools(parent) {
const tag = parent.command("cli-tools").description("CLI Tools endpoints");
tag.command("get-api-cli-tools-backups")
.description("List CLI tool backups")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/cli-tools/backups";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-cli-tools-backups")
.description("Create CLI tool backup")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/cli-tools/backups";
const res = await apiFetch(url, { method: "POST", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-cli-tools-runtime-tool-id-")
.description("Get runtime status for a CLI tool")
.requiredOption("--tool-id <toolId>", "")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/cli-tools/runtime/{toolId}";
url = url.replace("{toolId}", encodeURIComponent(opts.toolId ?? ""));
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-cli-tools-guide-settings-tool-id-")
.description("Get guide settings for a tool")
.requiredOption("--tool-id <toolId>", "")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/cli-tools/guide-settings/{toolId}";
url = url.replace("{toolId}", encodeURIComponent(opts.toolId ?? ""));
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-cli-tools-antigravity-mitm")
.description("Get Antigravity MITM proxy settings")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/cli-tools/antigravity-mitm";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-cli-tools-antigravity-mitm")
.description("Update Antigravity MITM proxy settings")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/cli-tools/antigravity-mitm";
let body;
if (opts.body) {
body = opts.body.startsWith("@")
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "POST", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("delete-api-cli-tools-antigravity-mitm")
.description("Reset Antigravity MITM proxy settings")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/cli-tools/antigravity-mitm";
const res = await apiFetch(url, { method: "DELETE", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-cli-tools-antigravity-mitm-alias")
.description("Get Antigravity MITM alias configuration")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/cli-tools/antigravity-mitm/alias";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("put-api-cli-tools-antigravity-mitm-alias")
.description("Update Antigravity MITM alias configuration")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/cli-tools/antigravity-mitm/alias";
let body;
if (opts.body) {
body = opts.body.startsWith("@")
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "PUT", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-cli-tools-claude-settings")
.description("Get Claude CLI settings")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/cli-tools/claude-settings";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-cli-tools-claude-settings")
.description("Apply Claude CLI settings")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/cli-tools/claude-settings";
let body;
if (opts.body) {
body = opts.body.startsWith("@")
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "POST", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("delete-api-cli-tools-claude-settings")
.description("Reset Claude CLI settings")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/cli-tools/claude-settings";
const res = await apiFetch(url, { method: "DELETE", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-cli-tools-cline-settings")
.description("Get Cline CLI settings")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/cli-tools/cline-settings";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-cli-tools-cline-settings")
.description("Apply Cline CLI settings")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/cli-tools/cline-settings";
let body;
if (opts.body) {
body = opts.body.startsWith("@")
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "POST", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("delete-api-cli-tools-cline-settings")
.description("Reset Cline CLI settings")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/cli-tools/cline-settings";
const res = await apiFetch(url, { method: "DELETE", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-cli-tools-codex-profiles")
.description("Get Codex profiles")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/cli-tools/codex-profiles";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-cli-tools-codex-profiles")
.description("Create Codex profile")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/cli-tools/codex-profiles";
let body;
if (opts.body) {
body = opts.body.startsWith("@")
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "POST", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("put-api-cli-tools-codex-profiles")
.description("Update Codex profile")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/cli-tools/codex-profiles";
let body;
if (opts.body) {
body = opts.body.startsWith("@")
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "PUT", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("delete-api-cli-tools-codex-profiles")
.description("Delete Codex profile")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/cli-tools/codex-profiles";
const res = await apiFetch(url, { method: "DELETE", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-cli-tools-codex-settings")
.description("Get Codex CLI settings")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/cli-tools/codex-settings";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-cli-tools-codex-settings")
.description("Apply Codex CLI settings")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/cli-tools/codex-settings";
let body;
if (opts.body) {
body = opts.body.startsWith("@")
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "POST", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("delete-api-cli-tools-codex-settings")
.description("Reset Codex CLI settings")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/cli-tools/codex-settings";
const res = await apiFetch(url, { method: "DELETE", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-cli-tools-droid-settings")
.description("Get Droid CLI settings")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/cli-tools/droid-settings";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-cli-tools-droid-settings")
.description("Apply Droid CLI settings")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/cli-tools/droid-settings";
let body;
if (opts.body) {
body = opts.body.startsWith("@")
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "POST", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("delete-api-cli-tools-droid-settings")
.description("Reset Droid CLI settings")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/cli-tools/droid-settings";
const res = await apiFetch(url, { method: "DELETE", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-cli-tools-kilo-settings")
.description("Get Kilo CLI settings")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/cli-tools/kilo-settings";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-cli-tools-kilo-settings")
.description("Apply Kilo CLI settings")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/cli-tools/kilo-settings";
let body;
if (opts.body) {
body = opts.body.startsWith("@")
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "POST", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("delete-api-cli-tools-kilo-settings")
.description("Reset Kilo CLI settings")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/cli-tools/kilo-settings";
const res = await apiFetch(url, { method: "DELETE", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-cli-tools-openclaw-settings")
.description("Get OpenClaw CLI settings")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/cli-tools/openclaw-settings";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-cli-tools-openclaw-settings")
.description("Apply OpenClaw CLI settings")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/cli-tools/openclaw-settings";
let body;
if (opts.body) {
body = opts.body.startsWith("@")
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "POST", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("delete-api-cli-tools-openclaw-settings")
.description("Reset OpenClaw CLI settings")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/cli-tools/openclaw-settings";
const res = await apiFetch(url, { method: "DELETE", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
}
+81
View File
@@ -0,0 +1,81 @@
// AUTO-GENERATED from docs/openapi.yaml. Do not edit.
import { apiFetch } from "../api.mjs";
import { emit } from "../output.mjs";
import { readFileSync } from "node:fs";
export function register_cloud(parent) {
const tag = parent.command("cloud").description("Cloud endpoints");
tag.command("post-api-cloud-auth")
.description("Authenticate with cloud worker")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/cloud/auth";
let body;
if (opts.body) {
body = opts.body.startsWith("@")
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "POST", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("put-api-cloud-credentials-update")
.description("Update cloud worker credentials")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/cloud/credentials/update";
let body;
if (opts.body) {
body = opts.body.startsWith("@")
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "PUT", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-cloud-model-resolve")
.description("Resolve model via cloud")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/cloud/model/resolve";
let body;
if (opts.body) {
body = opts.body.startsWith("@")
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "POST", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-cloud-models-alias")
.description("Get cloud model aliases")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/cloud/models/alias";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("put-api-cloud-models-alias")
.description("Update cloud model alias")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/cloud/models/alias";
let body;
if (opts.body) {
body = opts.body.startsWith("@")
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "PUT", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
}
+69
View File
@@ -0,0 +1,69 @@
// AUTO-GENERATED from docs/openapi.yaml. Do not edit.
import { apiFetch } from "../api.mjs";
import { emit } from "../output.mjs";
import { readFileSync } from "node:fs";
export function register_combos(parent) {
const tag = parent.command("combos").description("Combos endpoints");
tag.command("get-api-combos")
.description("List routing combos")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/combos";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-combos")
.description("Create routing combo")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/combos";
let body;
if (opts.body) {
body = opts.body.startsWith("@")
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "POST", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("patch-api-combos-id-")
.description("Update combo")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/combos/{id}";
const res = await apiFetch(url, { method: "PATCH", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("delete-api-combos-id-")
.description("Delete combo")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/combos/{id}";
const res = await apiFetch(url, { method: "DELETE", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-combos-metrics")
.description("Get combo metrics")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/combos/metrics";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-combos-test")
.description("Test a combo configuration")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/combos/test";
const res = await apiFetch(url, { method: "POST", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
}
+128
View File
@@ -0,0 +1,128 @@
// AUTO-GENERATED from docs/openapi.yaml. Do not edit.
import { apiFetch } from "../api.mjs";
import { emit } from "../output.mjs";
import { readFileSync } from "node:fs";
export function register_compression(parent) {
const tag = parent.command("compression").description("Compression endpoints");
tag.command("get-api-settings-compression")
.description("Get global compression settings")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/settings/compression";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("put-api-settings-compression")
.description("Update global compression settings")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/settings/compression";
let body;
if (opts.body) {
body = opts.body.startsWith("@")
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "PUT", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-compression-preview")
.description("Preview compression for a message payload")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/compression/preview";
let body;
if (opts.body) {
body = opts.body.startsWith("@")
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "POST", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-compression-language-packs")
.description("List Caveman compression language packs")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/compression/language-packs";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-compression-rules")
.description("List Caveman compression rule metadata")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/compression/rules";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-context-rtk-config")
.description("Get RTK compression settings")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/context/rtk/config";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("put-api-context-rtk-config")
.description("Update RTK compression settings")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/context/rtk/config";
let body;
if (opts.body) {
body = opts.body.startsWith("@")
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "PUT", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-context-rtk-filters")
.description("List RTK filters and load diagnostics")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/context/rtk/filters";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-context-rtk-test")
.description("Run RTK compression preview for text")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/context/rtk/test";
let body;
if (opts.body) {
body = opts.body.startsWith("@")
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "POST", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-context-rtk-raw-output-id-")
.description("Read retained redacted RTK raw output")
.requiredOption("--id <id>", "")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/context/rtk/raw-output/{id}";
url = url.replace("{id}", encodeURIComponent(opts.id ?? ""));
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
}
+202
View File
@@ -0,0 +1,202 @@
// AUTO-GENERATED from docs/openapi.yaml. Do not edit.
import { apiFetch } from "../api.mjs";
import { emit } from "../output.mjs";
import { readFileSync } from "node:fs";
export function register_embedded_services(parent) {
const tag = parent.command("embedded-services").description("Embedded Services endpoints");
tag.command("post-api-services-9router-install")
.description("Install 9Router from npm")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/services/9router/install";
let body;
if (opts.body) {
body = opts.body.startsWith("@")
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "POST", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-services-9router-start")
.description("Start 9Router")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/services/9router/start";
const res = await apiFetch(url, { method: "POST", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-services-9router-stop")
.description("Stop 9Router")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/services/9router/stop";
const res = await apiFetch(url, { method: "POST", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-services-9router-restart")
.description("Restart 9Router")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/services/9router/restart";
const res = await apiFetch(url, { method: "POST", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-services-9router-update")
.description("Update 9Router to a newer npm version")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/services/9router/update";
let body;
if (opts.body) {
body = opts.body.startsWith("@")
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "POST", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-services-9router-rotate-key")
.description("Rotate the 9Router API key")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/services/9router/rotate-key";
const res = await apiFetch(url, { method: "POST", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-services-9router-status")
.description("Get 9Router status")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/services/9router/status";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-services-9router-auto-start")
.description("Toggle 9Router auto-start")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/services/9router/auto-start";
let body;
if (opts.body) {
body = opts.body.startsWith("@")
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "POST", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-services-cliproxy-install")
.description("Install CLIProxyAPI from npm")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/services/cliproxy/install";
let body;
if (opts.body) {
body = opts.body.startsWith("@")
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "POST", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-services-cliproxy-start")
.description("Start CLIProxyAPI")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/services/cliproxy/start";
const res = await apiFetch(url, { method: "POST", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-services-cliproxy-stop")
.description("Stop CLIProxyAPI")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/services/cliproxy/stop";
const res = await apiFetch(url, { method: "POST", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-services-cliproxy-restart")
.description("Restart CLIProxyAPI")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/services/cliproxy/restart";
const res = await apiFetch(url, { method: "POST", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-services-cliproxy-update")
.description("Update CLIProxyAPI to a newer npm version")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/services/cliproxy/update";
let body;
if (opts.body) {
body = opts.body.startsWith("@")
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "POST", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-services-cliproxy-status")
.description("Get CLIProxyAPI status")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/services/cliproxy/status";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-services-cliproxy-auto-start")
.description("Toggle CLIProxyAPI auto-start")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/services/cliproxy/auto-start";
let body;
if (opts.body) {
body = opts.body.startsWith("@")
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "POST", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-services-name-logs")
.description("Stream service logs via SSE")
.requiredOption("--name <name>", "")
.option("--tail <tail>", "Number of historical lines to include in the initial snapshot")
.option("--filter <filter>", "Case-insensitive substring filter applied to log lines. No regex — ReDoS-safe by design.")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/services/{name}/logs";
url = url.replace("{name}", encodeURIComponent(opts.name ?? ""));
const qs = new URLSearchParams();
if (opts.tail != null) qs.set("tail", String(opts.tail));
if (opts.filter != null) qs.set("filter", String(opts.filter));
if (qs.toString()) url += "?" + qs.toString();
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
}
+42
View File
@@ -0,0 +1,42 @@
// AUTO-GENERATED from docs/openapi.yaml. Do not edit.
import { apiFetch } from "../api.mjs";
import { emit } from "../output.mjs";
import { readFileSync } from "node:fs";
export function register_embeddings(parent) {
const tag = parent.command("embeddings").description("Embeddings endpoints");
tag.command("post-api-v1-embeddings")
.description("Create embeddings")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/v1/embeddings";
let body;
if (opts.body) {
body = opts.body.startsWith("@")
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "POST", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-v1-providers-provider-embeddings")
.description("Create embeddings (provider-specific)")
.requiredOption("--provider <provider>", "")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/v1/providers/{provider}/embeddings";
url = url.replace("{provider}", encodeURIComponent(opts.provider ?? ""));
let body;
if (opts.body) {
body = opts.body.startsWith("@")
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "POST", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
}
+49
View File
@@ -0,0 +1,49 @@
// AUTO-GENERATED from docs/openapi.yaml. Do not edit.
import { apiFetch } from "../api.mjs";
import { emit } from "../output.mjs";
import { readFileSync } from "node:fs";
export function register_fallback(parent) {
const tag = parent.command("fallback").description("Fallback endpoints");
tag.command("get-api-fallback-chains")
.description("List fallback chains")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/fallback/chains";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-fallback-chains")
.description("Create fallback chain")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/fallback/chains";
let body;
if (opts.body) {
body = opts.body.startsWith("@")
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "POST", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("delete-api-fallback-chains")
.description("Delete fallback chain")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/fallback/chains";
let body;
if (opts.body) {
body = opts.body.startsWith("@")
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "DELETE", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
}
+42
View File
@@ -0,0 +1,42 @@
// AUTO-GENERATED from docs/openapi.yaml. Do not edit.
import { apiFetch } from "../api.mjs";
import { emit } from "../output.mjs";
import { readFileSync } from "node:fs";
export function register_images(parent) {
const tag = parent.command("images").description("Images endpoints");
tag.command("post-api-v1-images-generations")
.description("Generate images")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/v1/images/generations";
let body;
if (opts.body) {
body = opts.body.startsWith("@")
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "POST", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-v1-providers-provider-images-generations")
.description("Generate images (provider-specific)")
.requiredOption("--provider <provider>", "")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/v1/providers/{provider}/images/generations";
url = url.replace("{provider}", encodeURIComponent(opts.provider ?? ""));
let body;
if (opts.body) {
body = opts.body.startsWith("@")
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "POST", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
}
+251
View File
@@ -0,0 +1,251 @@
// AUTO-GENERATED from docs/openapi.yaml. Do not edit.
import { apiFetch } from "../api.mjs";
import { emit } from "../output.mjs";
import { readFileSync } from "node:fs";
export function register_memory(parent) {
const tag = parent.command("memory").description("Memory endpoints");
tag.command("get-api-memory")
.description("List memory entries")
.option("--api-key-id <apiKeyId>", "")
.option("--type <type>", "")
.option("--session-id <sessionId>", "")
.option("--q <q>", "")
.option("--limit <limit>", "")
.option("--page <page>", "")
.option("--offset <offset>", "")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/memory";
const qs = new URLSearchParams();
if (opts.apiKeyId != null) qs.set("apiKeyId", String(opts.apiKeyId));
if (opts.type != null) qs.set("type", String(opts.type));
if (opts.sessionId != null) qs.set("sessionId", String(opts.sessionId));
if (opts.q != null) qs.set("q", String(opts.q));
if (opts.limit != null) qs.set("limit", String(opts.limit));
if (opts.page != null) qs.set("page", String(opts.page));
if (opts.offset != null) qs.set("offset", String(opts.offset));
if (qs.toString()) url += "?" + qs.toString();
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-memory")
.description("Create a memory entry")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/memory";
let body;
if (opts.body) {
body = opts.body.startsWith("@")
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "POST", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-memory-id-")
.description("Get a single memory entry")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/memory/{id}";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("put-api-memory-id-")
.description("Update a memory entry")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/memory/{id}";
let body;
if (opts.body) {
body = opts.body.startsWith("@")
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "PUT", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("delete-api-memory-id-")
.description("Delete a memory entry")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/memory/{id}";
const res = await apiFetch(url, { method: "DELETE", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-memory-health")
.description("Memory store health check")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/memory/health";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-memory-retrieve-preview")
.description("Dry-run memory retrieval (Playground)")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/memory/retrieve-preview";
let body;
if (opts.body) {
body = opts.body.startsWith("@")
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "POST", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-memory-embedding-providers")
.description("List embedding providers")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/memory/embedding-providers";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-memory-engine-status")
.description("Memory engine status")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/memory/engine-status";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-memory-summarize")
.description("Compact old memories")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/memory/summarize";
let body;
if (opts.body) {
body = opts.body.startsWith("@")
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "POST", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-memory-reindex")
.description("Trigger vector reindex")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/memory/reindex";
let body;
if (opts.body) {
body = opts.body.startsWith("@")
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "POST", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-settings-memory")
.description("Get memory settings")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/settings/memory";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("put-api-settings-memory")
.description("Update memory settings")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/settings/memory";
let body;
if (opts.body) {
body = opts.body.startsWith("@")
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "PUT", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-settings-qdrant")
.description("Get Qdrant settings")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/settings/qdrant";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("put-api-settings-qdrant")
.description("Update Qdrant settings")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/settings/qdrant";
let body;
if (opts.body) {
body = opts.body.startsWith("@")
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "PUT", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-settings-qdrant-health")
.description("Qdrant health probe")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/settings/qdrant/health";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-settings-qdrant-search")
.description("Qdrant semantic search test")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/settings/qdrant/search";
let body;
if (opts.body) {
body = opts.body.startsWith("@")
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "POST", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-settings-qdrant-cleanup")
.description("Clean up expired Qdrant points")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/settings/qdrant/cleanup";
const res = await apiFetch(url, { method: "POST", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-settings-qdrant-embedding-models")
.description("List Qdrant embedding models")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/settings/qdrant/embedding-models";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
}
+40
View File
@@ -0,0 +1,40 @@
// AUTO-GENERATED from docs/openapi.yaml. Do not edit.
import { apiFetch } from "../api.mjs";
import { emit } from "../output.mjs";
import { readFileSync } from "node:fs";
export function register_messages(parent) {
const tag = parent.command("messages").description("Messages endpoints");
tag.command("post-api-v1-messages")
.description("Create message (Anthropic-compatible)")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/v1/messages";
let body;
if (opts.body) {
body = opts.body.startsWith("@")
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "POST", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-v1-messages-count-tokens")
.description("Count tokens for a message")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/v1/messages/count_tokens";
let body;
if (opts.body) {
body = opts.body.startsWith("@")
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "POST", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
}
+89
View File
@@ -0,0 +1,89 @@
// AUTO-GENERATED from docs/openapi.yaml. Do not edit.
import { apiFetch } from "../api.mjs";
import { emit } from "../output.mjs";
import { readFileSync } from "node:fs";
export function register_models(parent) {
const tag = parent.command("models").description("Models endpoints");
tag.command("get-api-v1-models")
.description("List available models")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/v1/models";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-v1-providers-provider-models")
.description("List models for a specific provider")
.requiredOption("--provider <provider>", "Provider id or alias (for example `openai`, `claude`, `cc`).")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/v1/providers/{provider}/models";
url = url.replace("{provider}", encodeURIComponent(opts.provider ?? ""));
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-models")
.description("List models (management)")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/models";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-models-alias")
.description("Create or update a model alias")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/models/alias";
let body;
if (opts.body) {
body = opts.body.startsWith("@")
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "POST", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-models-catalog")
.description("Get full model catalog")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/models/catalog";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-v1beta-models")
.description("List models (Gemini format)")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/v1beta/models";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-v1beta-models-path-")
.description("Gemini generateContent")
.requiredOption("--path <path>", "")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/v1beta/models/{path}";
url = url.replace("{path}", encodeURIComponent(opts.path ?? ""));
let body;
if (opts.body) {
body = opts.body.startsWith("@")
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "POST", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
}
+24
View File
@@ -0,0 +1,24 @@
// AUTO-GENERATED from docs/openapi.yaml. Do not edit.
import { apiFetch } from "../api.mjs";
import { emit } from "../output.mjs";
import { readFileSync } from "node:fs";
export function register_moderations(parent) {
const tag = parent.command("moderations").description("Moderations endpoints");
tag.command("post-api-v1-moderations")
.description("Create moderation")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/v1/moderations";
let body;
if (opts.body) {
body = opts.body.startsWith("@")
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "POST", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
}
+114
View File
@@ -0,0 +1,114 @@
// AUTO-GENERATED from docs/openapi.yaml. Do not edit.
import { apiFetch } from "../api.mjs";
import { emit } from "../output.mjs";
import { readFileSync } from "node:fs";
export function register_oauth(parent) {
const tag = parent.command("oauth").description("OAuth endpoints");
tag.command("get-api-oauth-provider-action-")
.description("OAuth flow handler")
.requiredOption("--provider <provider>", "")
.requiredOption("--action <action>", "")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/oauth/{provider}/{action}";
url = url.replace("{provider}", encodeURIComponent(opts.provider ?? ""));
url = url.replace("{action}", encodeURIComponent(opts.action ?? ""));
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-oauth-cursor-auto-import")
.description("Auto-import Cursor OAuth credentials")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/oauth/cursor/auto-import";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-oauth-cursor-import")
.description("Get Cursor import status")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/oauth/cursor/import";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-oauth-cursor-import")
.description("Import Cursor OAuth credentials")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/oauth/cursor/import";
let body;
if (opts.body) {
body = opts.body.startsWith("@")
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "POST", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-oauth-kiro-auto-import")
.description("Auto-import Kiro OAuth credentials")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/oauth/kiro/auto-import";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-oauth-kiro-import")
.description("Get Kiro import status")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/oauth/kiro/import";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-oauth-kiro-import")
.description("Import Kiro OAuth credentials")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/oauth/kiro/import";
let body;
if (opts.body) {
body = opts.body.startsWith("@")
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "POST", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-oauth-kiro-social-authorize")
.description("Initiate Kiro social OAuth authorization")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/oauth/kiro/social-authorize";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-oauth-kiro-social-exchange")
.description("Exchange Kiro social OAuth token")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/oauth/kiro/social-exchange";
let body;
if (opts.body) {
body = opts.body.startsWith("@")
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "POST", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
}
+83
View File
@@ -0,0 +1,83 @@
// AUTO-GENERATED from docs/openapi.yaml. Do not edit.
import { apiFetch } from "../api.mjs";
import { emit } from "../output.mjs";
import { readFileSync } from "node:fs";
export function register_playground(parent) {
const tag = parent.command("playground").description("Playground endpoints");
tag.command("post-api-playground-improve-prompt")
.description("Improve prompt via LLM")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/playground/improve-prompt";
let body;
if (opts.body) {
body = opts.body.startsWith("@")
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "POST", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-playground-presets")
.description("List playground presets")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/playground/presets";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-playground-presets")
.description("Create playground preset")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/playground/presets";
let body;
if (opts.body) {
body = opts.body.startsWith("@")
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "POST", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-playground-presets-id-")
.description("Get playground preset")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/playground/presets/{id}";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("put-api-playground-presets-id-")
.description("Update playground preset")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/playground/presets/{id}";
let body;
if (opts.body) {
body = opts.body.startsWith("@")
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "PUT", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("delete-api-playground-presets-id-")
.description("Delete playground preset")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/playground/presets/{id}";
const res = await apiFetch(url, { method: "DELETE", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
}
+44
View File
@@ -0,0 +1,44 @@
// AUTO-GENERATED from docs/openapi.yaml. Do not edit.
import { apiFetch } from "../api.mjs";
import { emit } from "../output.mjs";
import { readFileSync } from "node:fs";
export function register_pricing(parent) {
const tag = parent.command("pricing").description("Pricing endpoints");
tag.command("get-api-pricing")
.description("Get model pricing")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/pricing";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-pricing")
.description("Set model pricing")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/pricing";
const res = await apiFetch(url, { method: "POST", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-pricing-defaults")
.description("Get default pricing")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/pricing/defaults";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-pricing-models")
.description("Get pricing per model")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/pricing/models";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
}
+62
View File
@@ -0,0 +1,62 @@
// AUTO-GENERATED from docs/openapi.yaml. Do not edit.
import { apiFetch } from "../api.mjs";
import { emit } from "../output.mjs";
import { readFileSync } from "node:fs";
export function register_provider_nodes(parent) {
const tag = parent.command("provider-nodes").description("Provider Nodes endpoints");
tag.command("get-api-provider-nodes")
.description("List provider nodes")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/provider-nodes";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-provider-nodes")
.description("Create provider node")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/provider-nodes";
const res = await apiFetch(url, { method: "POST", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("patch-api-provider-nodes-id-")
.description("Update provider node")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/provider-nodes/{id}";
const res = await apiFetch(url, { method: "PATCH", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("delete-api-provider-nodes-id-")
.description("Delete provider node")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/provider-nodes/{id}";
const res = await apiFetch(url, { method: "DELETE", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-provider-nodes-validate")
.description("Validate a provider node")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/provider-nodes/validate";
const res = await apiFetch(url, { method: "POST", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-provider-models")
.description("List provider models")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/provider-models";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
}
+148
View File
@@ -0,0 +1,148 @@
// AUTO-GENERATED from docs/openapi.yaml. Do not edit.
import { apiFetch } from "../api.mjs";
import { emit } from "../output.mjs";
import { readFileSync } from "node:fs";
export function register_providers(parent) {
const tag = parent.command("providers").description("Providers endpoints");
tag.command("get-api-providers")
.description("List provider connections")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/providers";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-providers")
.description("Create provider connection")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/providers";
let body;
if (opts.body) {
body = opts.body.startsWith("@")
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "POST", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-providers-id-")
.description("Get provider connection")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/providers/{id}";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("patch-api-providers-id-")
.description("Update provider connection")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/providers/{id}";
let body;
if (opts.body) {
body = opts.body.startsWith("@")
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "PATCH", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("delete-api-providers-id-")
.description("Delete provider connection")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/providers/{id}";
const res = await apiFetch(url, { method: "DELETE", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-providers-id-test")
.description("Test provider connection")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/providers/{id}/test";
const res = await apiFetch(url, { method: "POST", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-providers-id-models")
.description("List models for a provider")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/providers/{id}/models";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-providers-test-batch")
.description("Test multiple providers at once")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/providers/test-batch";
const res = await apiFetch(url, { method: "POST", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-providers-validate")
.description("Validate provider credentials")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/providers/validate";
const res = await apiFetch(url, { method: "POST", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-providers-client")
.description("Get client-side provider info")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/providers/client";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-providers-agy-auth-import")
.description("Import an Antigravity CLI (agy) token file as an `agy` connection")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/providers/agy-auth/import";
const res = await apiFetch(url, { method: "POST", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-providers-agy-auth-import-bulk")
.description("Bulk-import multiple Antigravity CLI (agy) token files (up to 50)")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/providers/agy-auth/import-bulk";
const res = await apiFetch(url, { method: "POST", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-providers-agy-auth-zip-extract")
.description("Extract `.json` token files from an uploaded ZIP for agy bulk import")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/providers/agy-auth/zip-extract";
const res = await apiFetch(url, { method: "POST", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-providers-agy-auth-apply-local")
.description("Auto-detect and import the local Antigravity CLI (agy) login from disk")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/providers/agy-auth/apply-local";
const res = await apiFetch(url, { method: "POST", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
}
+154
View File
@@ -0,0 +1,154 @@
// AUTO-GENERATED from docs/openapi.yaml. Do not edit.
import { apiFetch } from "../api.mjs";
import { emit } from "../output.mjs";
import { readFileSync } from "node:fs";
export function register_quota(parent) {
const tag = parent.command("quota").description("Quota endpoints");
tag.command("get-api-quota-pools")
.description("List quota pools")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/quota/pools";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-quota-pools")
.description("Create quota pool")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/quota/pools";
let body;
if (opts.body) {
body = opts.body.startsWith("@")
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "POST", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-quota-pools-id-")
.description("Get quota pool by ID")
.requiredOption("--id <id>", "")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/quota/pools/{id}";
url = url.replace("{id}", encodeURIComponent(opts.id ?? ""));
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("patch-api-quota-pools-id-")
.description("Update quota pool (name or allocations)")
.requiredOption("--id <id>", "")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/quota/pools/{id}";
url = url.replace("{id}", encodeURIComponent(opts.id ?? ""));
let body;
if (opts.body) {
body = opts.body.startsWith("@")
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "PATCH", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("delete-api-quota-pools-id-")
.description("Delete quota pool")
.requiredOption("--id <id>", "")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/quota/pools/{id}";
url = url.replace("{id}", encodeURIComponent(opts.id ?? ""));
const res = await apiFetch(url, { method: "DELETE", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-quota-pools-id-usage")
.description("Get pool usage snapshot (per-key consumption + burn rate)")
.requiredOption("--id <id>", "")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/quota/pools/{id}/usage";
url = url.replace("{id}", encodeURIComponent(opts.id ?? ""));
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-quota-plans")
.description("List resolved provider plans (catalog + manual overrides)")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/quota/plans";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-quota-plans-connection-id-")
.description("Get resolved plan for a connection")
.requiredOption("--connection-id <connectionId>", "")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/quota/plans/{connectionId}";
url = url.replace("{connectionId}", encodeURIComponent(opts.connectionId ?? ""));
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("put-api-quota-plans-connection-id-")
.description("Upsert manual plan override for a connection")
.requiredOption("--connection-id <connectionId>", "")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/quota/plans/{connectionId}";
url = url.replace("{connectionId}", encodeURIComponent(opts.connectionId ?? ""));
let body;
if (opts.body) {
body = opts.body.startsWith("@")
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "PUT", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("delete-api-quota-plans-connection-id-")
.description("Delete manual plan override (reverts to catalog/auto)")
.requiredOption("--connection-id <connectionId>", "")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/quota/plans/{connectionId}";
url = url.replace("{connectionId}", encodeURIComponent(opts.connectionId ?? ""));
const res = await apiFetch(url, { method: "DELETE", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-quota-preview")
.description("Dry-run quota enforcement check (preview only, no consumption recorded)")
.requiredOption("--api-key-id <apiKeyId>", "")
.requiredOption("--pool-id <poolId>", "")
.option("--estimated-tokens <estimatedTokens>", "")
.option("--estimated-usd <estimatedUsd>", "")
.option("--estimated-requests <estimatedRequests>", "")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/quota/preview";
const qs = new URLSearchParams();
if (opts.apiKeyId != null) qs.set("apiKeyId", String(opts.apiKeyId));
if (opts.poolId != null) qs.set("poolId", String(opts.poolId));
if (opts.estimatedTokens != null) qs.set("estimatedTokens", String(opts.estimatedTokens));
if (opts.estimatedUsd != null) qs.set("estimatedUsd", String(opts.estimatedUsd));
if (opts.estimatedRequests != null) qs.set("estimatedRequests", String(opts.estimatedRequests));
if (qs.toString()) url += "?" + qs.toString();
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
}
+75
View File
@@ -0,0 +1,75 @@
// AUTO-GENERATED. Do not edit.
import { register_playground } from "./playground.mjs";
import { register_memory } from "./memory.mjs";
import { register_chat } from "./chat.mjs";
import { register_messages } from "./messages.mjs";
import { register_responses } from "./responses.mjs";
import { register_embeddings } from "./embeddings.mjs";
import { register_images } from "./images.mjs";
import { register_audio } from "./audio.mjs";
import { register_moderations } from "./moderations.mjs";
import { register_rerank } from "./rerank.mjs";
import { register_system } from "./system.mjs";
import { register_models } from "./models.mjs";
import { register_providers } from "./providers.mjs";
import { register_provider_nodes } from "./provider-nodes.mjs";
import { register_api_keys } from "./api-keys.mjs";
import { register_combos } from "./combos.mjs";
import { register_settings } from "./settings.mjs";
import { register_compression } from "./compression.mjs";
import { register_usage } from "./usage.mjs";
import { register_pricing } from "./pricing.mjs";
import { register_translator } from "./translator.mjs";
import { register_cli_tools } from "./cli-tools.mjs";
import { register_embedded_services } from "./embedded-services.mjs";
import { register_oauth } from "./oauth.mjs";
import { register_cloud } from "./cloud.mjs";
import { register_fallback } from "./fallback.mjs";
import { register_telemetry } from "./telemetry.mjs";
import { register_quota } from "./quota.mjs";
import { register_agentbridge } from "./agentbridge.mjs";
import { register_traffic_inspector } from "./traffic-inspector.mjs";
import { register_agent_skills } from "./agent-skills.mjs";
export const API_TAGS = ["playground","memory","chat","messages","responses","embeddings","images","audio","moderations","rerank","system","models","providers","provider-nodes","api-keys","combos","settings","compression","usage","pricing","translator","cli-tools","embedded-services","oauth","cloud","fallback","telemetry","quota","agentbridge","traffic-inspector","agent-skills"];
export function registerApiCommands(program) {
const api = program
.command("api")
.description("Direct REST API access (generated from OpenAPI spec)");
api
.command("tags")
.description("List available API tag groups")
.action(() => { API_TAGS.forEach((t) => console.log(t)); });
register_playground(api);
register_memory(api);
register_chat(api);
register_messages(api);
register_responses(api);
register_embeddings(api);
register_images(api);
register_audio(api);
register_moderations(api);
register_rerank(api);
register_system(api);
register_models(api);
register_providers(api);
register_provider_nodes(api);
register_api_keys(api);
register_combos(api);
register_settings(api);
register_compression(api);
register_usage(api);
register_pricing(api);
register_translator(api);
register_cli_tools(api);
register_embedded_services(api);
register_oauth(api);
register_cloud(api);
register_fallback(api);
register_telemetry(api);
register_quota(api);
register_agentbridge(api);
register_traffic_inspector(api);
register_agent_skills(api);
}
+24
View File
@@ -0,0 +1,24 @@
// AUTO-GENERATED from docs/openapi.yaml. Do not edit.
import { apiFetch } from "../api.mjs";
import { emit } from "../output.mjs";
import { readFileSync } from "node:fs";
export function register_rerank(parent) {
const tag = parent.command("rerank").description("Rerank endpoints");
tag.command("post-api-v1-rerank")
.description("Rerank documents")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/v1/rerank";
let body;
if (opts.body) {
body = opts.body.startsWith("@")
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "POST", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
}
+24
View File
@@ -0,0 +1,24 @@
// AUTO-GENERATED from docs/openapi.yaml. Do not edit.
import { apiFetch } from "../api.mjs";
import { emit } from "../output.mjs";
import { readFileSync } from "node:fs";
export function register_responses(parent) {
const tag = parent.command("responses").description("Responses endpoints");
tag.command("post-api-v1-responses")
.description("Create response (OpenAI Responses API)")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/v1/responses";
let body;
if (opts.body) {
body = opts.body.startsWith("@")
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "POST", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
}
+228
View File
@@ -0,0 +1,228 @@
// AUTO-GENERATED from docs/openapi.yaml. Do not edit.
import { apiFetch } from "../api.mjs";
import { emit } from "../output.mjs";
import { readFileSync } from "node:fs";
export function register_settings(parent) {
const tag = parent.command("settings").description("Settings endpoints");
tag.command("get-api-settings")
.description("Get application settings")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/settings";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("patch-api-settings")
.description("Update settings")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/settings";
let body;
if (opts.body) {
body = opts.body.startsWith("@")
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "PATCH", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-settings-payload-rules")
.description("Get payload rules configuration")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/settings/payload-rules";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("put-api-settings-payload-rules")
.description("Update payload rules configuration")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/settings/payload-rules";
let body;
if (opts.body) {
body = opts.body.startsWith("@")
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "PUT", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-settings-combo-defaults")
.description("Get combo default settings")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/settings/combo-defaults";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-settings-proxy")
.description("Get proxy settings")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/settings/proxy";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("patch-api-settings-proxy")
.description("Update proxy settings")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/settings/proxy";
const res = await apiFetch(url, { method: "PATCH", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-settings-proxy-test")
.description("Test proxy connection")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/settings/proxy/test";
const res = await apiFetch(url, { method: "POST", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-settings-require-login")
.description("Toggle login requirement")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/settings/require-login";
const res = await apiFetch(url, { method: "POST", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-settings-ip-filter")
.description("Get IP filter configuration")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/settings/ip-filter";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("put-api-settings-ip-filter")
.description("Update IP filter configuration")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/settings/ip-filter";
let body;
if (opts.body) {
body = opts.body.startsWith("@")
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "PUT", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-settings-system-prompt")
.description("Get system prompt configuration")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/settings/system-prompt";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("put-api-settings-system-prompt")
.description("Update system prompt configuration")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/settings/system-prompt";
let body;
if (opts.body) {
body = opts.body.startsWith("@")
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "PUT", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-settings-thinking-budget")
.description("Get thinking budget configuration")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/settings/thinking-budget";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("put-api-settings-thinking-budget")
.description("Update thinking budget configuration")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/settings/thinking-budget";
let body;
if (opts.body) {
body = opts.body.startsWith("@")
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "PUT", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-rate-limit")
.description("Get rate limit configuration")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/rate-limit";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-rate-limit")
.description("Update rate limit configuration")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/rate-limit";
let body;
if (opts.body) {
body = opts.body.startsWith("@")
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "POST", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-settings-quota-store")
.description("Get current quota store driver settings")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/settings/quota-store";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("put-api-settings-quota-store")
.description("Update quota store driver settings")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/settings/quota-store";
let body;
if (opts.body) {
body = opts.body.startsWith("@")
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "PUT", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
}
+320
View File
@@ -0,0 +1,320 @@
// AUTO-GENERATED from docs/openapi.yaml. Do not edit.
import { apiFetch } from "../api.mjs";
import { emit } from "../output.mjs";
import { readFileSync } from "node:fs";
export function register_system(parent) {
const tag = parent.command("system").description("System endpoints");
tag.command("get-api-v1")
.description("API v1 root endpoint")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/v1";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-tags")
.description("List Ollama-compatible model tags")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/tags";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-auth-login")
.description("Authenticate user")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/auth/login";
let body;
if (opts.body) {
body = opts.body.startsWith("@")
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "POST", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-auth-logout")
.description("Log out")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/auth/logout";
const res = await apiFetch(url, { method: "POST", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-init")
.description("Initialize application")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/init";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-restart")
.description("Restart the application")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/restart";
const res = await apiFetch(url, { method: "POST", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-shutdown")
.description("Shutdown the application")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/shutdown";
const res = await apiFetch(url, { method: "POST", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-db-backups")
.description("List database backups")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/db-backups";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-db-backups")
.description("Create database backup")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/db-backups";
const res = await apiFetch(url, { method: "POST", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-storage-health")
.description("Check storage health")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/storage/health";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-sync-cloud")
.description("Sync with cloud")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/sync/cloud";
const res = await apiFetch(url, { method: "POST", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-sync-initialize")
.description("Initialize cloud sync")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/sync/initialize";
const res = await apiFetch(url, { method: "POST", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-resilience")
.description("Get resilience configuration")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/resilience";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("patch-api-resilience")
.description("Update resilience configuration")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/resilience";
let body;
if (opts.body) {
body = opts.body.startsWith("@")
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "PATCH", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-resilience-reset")
.description("Reset circuit breakers")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/resilience/reset";
const res = await apiFetch(url, { method: "POST", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-monitoring-health")
.description("System health check")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/monitoring/health";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-rate-limits")
.description("Get per-account rate limit status")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/rate-limits";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-sessions")
.description("Get active sessions")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/sessions";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-cache")
.description("Get cache statistics")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/cache";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("delete-api-cache")
.description("Clear all caches")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/cache";
const res = await apiFetch(url, { method: "DELETE", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-cache-stats")
.description("Get detailed cache statistics")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/cache/stats";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("delete-api-cache-stats")
.description("Clear cache statistics")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/cache/stats";
const res = await apiFetch(url, { method: "DELETE", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-evals")
.description("List eval suites")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/evals";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-evals")
.description("Run evaluation")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/evals";
let body;
if (opts.body) {
body = opts.body.startsWith("@")
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "POST", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-evals-suite-id-")
.description("Get eval suite details")
.requiredOption("--suite-id <suiteId>", "")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/evals/{suiteId}";
url = url.replace("{suiteId}", encodeURIComponent(opts.suiteId ?? ""));
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-policies")
.description("List routing policies")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/policies";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-policies")
.description("Create routing policy")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/policies";
let body;
if (opts.body) {
body = opts.body.startsWith("@")
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "POST", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("delete-api-policies")
.description("Delete routing policy")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/policies";
const res = await apiFetch(url, { method: "DELETE", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-compliance-audit-log")
.description("Get compliance audit log")
.option("--level <level>", "high = Activity feed events only; all = all audit events")
.option("--action <action>", "Filter by exact action string (e.g. \"provider.added\")")
.option("--actor <actor>", "Filter by actor identifier")
.option("--limit <limit>", "")
.option("--offset <offset>", "")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/compliance/audit-log";
const qs = new URLSearchParams();
if (opts.level != null) qs.set("level", String(opts.level));
if (opts.action != null) qs.set("action", String(opts.action));
if (opts.actor != null) qs.set("actor", String(opts.actor));
if (opts.limit != null) qs.set("limit", String(opts.limit));
if (opts.offset != null) qs.set("offset", String(opts.offset));
if (qs.toString()) url += "?" + qs.toString();
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-openapi-spec")
.description("Get OpenAPI specification catalog")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/openapi/spec";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
}
+26
View File
@@ -0,0 +1,26 @@
// AUTO-GENERATED from docs/openapi.yaml. Do not edit.
import { apiFetch } from "../api.mjs";
import { emit } from "../output.mjs";
import { readFileSync } from "node:fs";
export function register_telemetry(parent) {
const tag = parent.command("telemetry").description("Telemetry endpoints");
tag.command("get-api-telemetry-summary")
.description("Get telemetry summary")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/telemetry/summary";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-token-health")
.description("Get token health status")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/token-health";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
}
+307
View File
@@ -0,0 +1,307 @@
// AUTO-GENERATED from docs/openapi.yaml. Do not edit.
import { apiFetch } from "../api.mjs";
import { emit } from "../output.mjs";
import { readFileSync } from "node:fs";
export function register_traffic_inspector(parent) {
const tag = parent.command("traffic-inspector").description("Traffic Inspector endpoints");
tag.command("get-api-tools-traffic-inspector-requests")
.description("List intercepted requests (filterable)")
.option("--profile <profile>", "")
.option("--host <host>", "")
.option("--agent <agent>", "")
.option("--status <status>", "")
.option("--source <source>", "")
.option("--session-id <sessionId>", "")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/tools/traffic-inspector/requests";
const qs = new URLSearchParams();
if (opts.profile != null) qs.set("profile", String(opts.profile));
if (opts.host != null) qs.set("host", String(opts.host));
if (opts.agent != null) qs.set("agent", String(opts.agent));
if (opts.status != null) qs.set("status", String(opts.status));
if (opts.source != null) qs.set("source", String(opts.source));
if (opts.sessionId != null) qs.set("sessionId", String(opts.sessionId));
if (qs.toString()) url += "?" + qs.toString();
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("delete-api-tools-traffic-inspector-requests")
.description("Clear the in-memory traffic buffer")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/tools/traffic-inspector/requests";
const res = await apiFetch(url, { method: "DELETE", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-tools-traffic-inspector-requests-id-")
.description("Get a single intercepted request by ID")
.requiredOption("--id <id>", "")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/tools/traffic-inspector/requests/{id}";
url = url.replace("{id}", encodeURIComponent(opts.id ?? ""));
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-tools-traffic-inspector-requests-id-replay")
.description("Replay a captured request through OmniRoute router")
.requiredOption("--id <id>", "")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/tools/traffic-inspector/requests/{id}/replay";
url = url.replace("{id}", encodeURIComponent(opts.id ?? ""));
const res = await apiFetch(url, { method: "POST", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("put-api-tools-traffic-inspector-requests-id-annotation")
.description("Save or update annotation on a request")
.requiredOption("--id <id>", "")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/tools/traffic-inspector/requests/{id}/annotation";
url = url.replace("{id}", encodeURIComponent(opts.id ?? ""));
let body;
if (opts.body) {
body = opts.body.startsWith("@")
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "PUT", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-tools-traffic-inspector-ws")
.description("Live WebSocket stream of intercepted requests")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/tools/traffic-inspector/ws";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-tools-traffic-inspector-export-har")
.description("Export current filtered request list as HAR 1.2")
.option("--profile <profile>", "")
.option("--session-id <sessionId>", "")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/tools/traffic-inspector/export.har";
const qs = new URLSearchParams();
if (opts.profile != null) qs.set("profile", String(opts.profile));
if (opts.sessionId != null) qs.set("sessionId", String(opts.sessionId));
if (qs.toString()) url += "?" + qs.toString();
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-tools-traffic-inspector-hosts")
.description("List custom capture hosts")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/tools/traffic-inspector/hosts";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-tools-traffic-inspector-hosts")
.description("Add a custom capture host (edits /etc/hosts)")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/tools/traffic-inspector/hosts";
let body;
if (opts.body) {
body = opts.body.startsWith("@")
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "POST", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("delete-api-tools-traffic-inspector-hosts-host-")
.description("Remove a custom capture host")
.requiredOption("--host <host>", "")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/tools/traffic-inspector/hosts/{host}";
url = url.replace("{host}", encodeURIComponent(opts.host ?? ""));
const res = await apiFetch(url, { method: "DELETE", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("patch-api-tools-traffic-inspector-hosts-host-")
.description("Toggle enabled state of a custom host")
.requiredOption("--host <host>", "")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/tools/traffic-inspector/hosts/{host}";
url = url.replace("{host}", encodeURIComponent(opts.host ?? ""));
let body;
if (opts.body) {
body = opts.body.startsWith("@")
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "PATCH", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-tools-traffic-inspector-capture-modes")
.description("Get state of all 4 capture modes")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/tools/traffic-inspector/capture-modes";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-tools-traffic-inspector-capture-modes-http-proxy")
.description("Start or stop the HTTP_PROXY listener (port 8080)")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/tools/traffic-inspector/capture-modes/http-proxy";
let body;
if (opts.body) {
body = opts.body.startsWith("@")
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "POST", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-tools-traffic-inspector-capture-modes-system-proxy")
.description("Apply or revert system-wide proxy settings")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/tools/traffic-inspector/capture-modes/system-proxy";
let body;
if (opts.body) {
body = opts.body.startsWith("@")
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "POST", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-tools-traffic-inspector-capture-modes-tls-intercept")
.description("Toggle TLS body decryption in HTTP_PROXY mode")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/tools/traffic-inspector/capture-modes/tls-intercept";
let body;
if (opts.body) {
body = opts.body.startsWith("@")
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "POST", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-tools-traffic-inspector-sessions")
.description("List all saved recording sessions")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/tools/traffic-inspector/sessions";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-tools-traffic-inspector-sessions")
.description("Start a new recording session")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/tools/traffic-inspector/sessions";
let body;
if (opts.body) {
body = opts.body.startsWith("@")
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "POST", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-tools-traffic-inspector-sessions-id-")
.description("Get session snapshot (all captured requests)")
.requiredOption("--id <id>", "")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/tools/traffic-inspector/sessions/{id}";
url = url.replace("{id}", encodeURIComponent(opts.id ?? ""));
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("patch-api-tools-traffic-inspector-sessions-id-")
.description("Stop or rename a recording session")
.requiredOption("--id <id>", "")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/tools/traffic-inspector/sessions/{id}";
url = url.replace("{id}", encodeURIComponent(opts.id ?? ""));
let body;
if (opts.body) {
body = opts.body.startsWith("@")
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "PATCH", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("delete-api-tools-traffic-inspector-sessions-id-")
.description("Delete a recording session")
.requiredOption("--id <id>", "")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/tools/traffic-inspector/sessions/{id}";
url = url.replace("{id}", encodeURIComponent(opts.id ?? ""));
const res = await apiFetch(url, { method: "DELETE", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-tools-traffic-inspector-sessions-id-export-har")
.description("Export a recorded session as HAR 1.2")
.requiredOption("--id <id>", "")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/tools/traffic-inspector/sessions/{id}/export.har";
url = url.replace("{id}", encodeURIComponent(opts.id ?? ""));
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-tools-traffic-inspector-internal-ingest")
.description("Internal ingest endpoint for server.cjs passthrough path")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/tools/traffic-inspector/internal/ingest";
let body;
if (opts.body) {
body = opts.body.startsWith("@")
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "POST", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
}
+65
View File
@@ -0,0 +1,65 @@
// AUTO-GENERATED from docs/openapi.yaml. Do not edit.
import { apiFetch } from "../api.mjs";
import { emit } from "../output.mjs";
import { readFileSync } from "node:fs";
export function register_translator(parent) {
const tag = parent.command("translator").description("Translator endpoints");
tag.command("post-api-translator-detect")
.description("Detect request format")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/translator/detect";
let body;
if (opts.body) {
body = opts.body.startsWith("@")
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "POST", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-translator-translate")
.description("Translate between formats")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/translator/translate";
let body;
if (opts.body) {
body = opts.body.startsWith("@")
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "POST", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-translator-send")
.description("Send translated request to provider")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/translator/send";
let body;
if (opts.body) {
body = opts.body.startsWith("@")
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "POST", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-translator-history")
.description("Get translation history")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/translator/history";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
}
+117
View File
@@ -0,0 +1,117 @@
// AUTO-GENERATED from docs/openapi.yaml. Do not edit.
import { apiFetch } from "../api.mjs";
import { emit } from "../output.mjs";
import { readFileSync } from "node:fs";
export function register_usage(parent) {
const tag = parent.command("usage").description("Usage endpoints");
tag.command("get-api-usage-analytics")
.description("Get usage analytics")
.option("--period <period>", "")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/usage/analytics";
const qs = new URLSearchParams();
if (opts.period != null) qs.set("period", String(opts.period));
if (qs.toString()) url += "?" + qs.toString();
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-usage-call-logs")
.description("Get call logs")
.option("--limit <limit>", "")
.option("--offset <offset>", "")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/usage/call-logs";
const qs = new URLSearchParams();
if (opts.limit != null) qs.set("limit", String(opts.limit));
if (opts.offset != null) qs.set("offset", String(opts.offset));
if (qs.toString()) url += "?" + qs.toString();
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-usage-call-logs-id-")
.description("Get a specific call log")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/usage/call-logs/{id}";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-usage-connection-id-")
.description("Get usage for a specific connection")
.requiredOption("--connection-id <connectionId>", "")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/usage/{connectionId}";
url = url.replace("{connectionId}", encodeURIComponent(opts.connectionId ?? ""));
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-usage-history")
.description("Get usage history")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/usage/history";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-usage-logs")
.description("Get usage logs")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/usage/logs";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-usage-proxy-logs")
.description("Get proxy logs")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/usage/proxy-logs";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-usage-request-logs")
.description("Get request logs")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/usage/request-logs";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("get-api-usage-budget")
.description("Get usage budget status")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/usage/budget";
const res = await apiFetch(url, { method: "GET", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
tag.command("post-api-usage-budget")
.description("Configure usage budget")
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/usage/budget";
let body;
if (opts.body) {
body = opts.body.startsWith("@")
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
: JSON.parse(opts.body);
}
const res = await apiFetch(url, { method: "POST", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = res.ok ? await res.json() : await res.text();
emit(data, gOpts);
});
}
+269
View File
@@ -0,0 +1,269 @@
import { setTimeout as sleep } from "node:timers/promises";
import { getCliToken, CLI_TOKEN_HEADER } from "./utils/cliToken.mjs";
import { resolveActiveContext } from "./contexts.mjs";
export const RETRY_DEFAULTS = Object.freeze({
maxAttempts: 3,
baseMs: 500,
maxMs: 8000,
jitter: true,
retryableStatuses: [408, 425, 429, 502, 503, 504],
retryableErrorCodes: [
"ECONNRESET",
"ECONNREFUSED",
"ETIMEDOUT",
"ENOTFOUND",
"EAI_AGAIN",
"EPIPE",
],
});
const NON_RETRYABLE_ON_MUTATION = new Set([409, 422, 429]);
const MUTATING_METHODS = new Set(["POST", "PUT", "PATCH", "DELETE"]);
export function getBaseUrl(opts = {}) {
if (opts.baseUrl) return stripTrailingSlash(opts.baseUrl);
const envUrl = process.env.OMNIROUTE_BASE_URL;
if (envUrl) return stripTrailingSlash(envUrl);
// Resolve from the active context (canonical store + legacy profile fallback).
// This is what makes "remote mode" work: `omniroute contexts use <remote>`
// routes every command at the remote server's baseUrl.
try {
const ctx = resolveActiveContext(opts.context ?? process.env.OMNIROUTE_CONTEXT);
if (ctx?.baseUrl) return stripTrailingSlash(ctx.baseUrl);
} catch {
// Config read failures are not fatal — fall through to default.
}
const port = process.env.PORT || "20128";
return `http://localhost:${port}`;
}
function stripTrailingSlash(value) {
const s = String(value);
let end = s.length;
while (end > 0 && s.charCodeAt(end - 1) === 47) end--;
return end === s.length ? s : s.slice(0, end);
}
function resolveUrl(path, opts) {
if (/^https?:\/\//i.test(path)) return path;
return `${getBaseUrl(opts)}${path.startsWith("/") ? path : `/${path}`}`;
}
export async function buildHeaders(opts) {
const headers = new Headers(opts.headers || {});
if (!headers.has("accept")) headers.set("accept", "application/json");
if (opts.body && !headers.has("content-type") && typeof opts.body !== "string") {
headers.set("content-type", "application/json");
}
// Auth precedence: explicit key → active-context credential → ambient env key.
//
// The active context's scoped token MUST win over the ambient OMNIROUTE_API_KEY:
// `omniroute connect <remote>` saves the context's token, but users keep
// OMNIROUTE_API_KEY in their shell. The global `--api-key` option is bound to
// that env var (.env("OMNIROUTE_API_KEY")), so commands that spread
// `optsWithGlobals()` into apiFetch carry opts.apiKey === the env value. If that
// echoed value outranked the context, every remote management command would send
// the local inference key and fail with "Invalid management token" — defeating
// remote mode. So an opts.apiKey that merely mirrors the ambient env var is
// treated as ambient (a fallback), NOT as an explicit override; only a DISTINCT
// key — a real `--api-key <x>` flag or a command-supplied token like
// `connect --key` — counts as explicit and wins. Within a context the scoped
// accessToken wins over the legacy apiKey.
const ambientKey = process.env.OMNIROUTE_API_KEY || null;
const explicitKey = opts.apiKey && opts.apiKey !== ambientKey ? opts.apiKey : null;
let auth = explicitKey;
if (!auth) {
try {
const ctx = resolveActiveContext(opts.context ?? process.env.OMNIROUTE_CONTEXT);
auth = ctx?.accessToken || ctx?.apiKey || null;
} catch {
// No context credential available — fall through to the ambient fallback.
}
}
if (!auth) auth = opts.apiKey || ambientKey || null;
if (auth && !headers.has("authorization")) {
headers.set("authorization", `Bearer ${auth}`);
}
// Inject machine-id derived CLI token; env var override for testing.
const cliToken = opts.cliToken ?? process.env.OMNIROUTE_CLI_TOKEN ?? (await getCliToken());
if (cliToken && !headers.has(CLI_TOKEN_HEADER)) {
headers.set(CLI_TOKEN_HEADER, cliToken);
}
if (opts.idempotencyKey && !headers.has("idempotency-key")) {
headers.set("idempotency-key", opts.idempotencyKey);
}
return headers;
}
function serializeBody(body, headers) {
if (body == null) return undefined;
if (typeof body === "string") return body;
if (body instanceof Buffer) return body;
if (body instanceof URLSearchParams) return body;
if (typeof body.pipe === "function") return body; // stream
if (headers.get("content-type")?.includes("application/json")) return JSON.stringify(body);
return JSON.stringify(body);
}
export function computeBackoff(attempt, retryAfterHeader, defaults = RETRY_DEFAULTS) {
if (retryAfterHeader != null) {
const secs = Number.parseFloat(String(retryAfterHeader));
if (Number.isFinite(secs) && secs >= 0) {
return Math.min(secs * 1000, defaults.maxMs);
}
}
const exp = Math.min(defaults.baseMs * 2 ** (attempt - 1), defaults.maxMs);
if (!defaults.jitter) return exp;
const jitter = exp * 0.25 * (Math.random() * 2 - 1);
return Math.max(0, exp + jitter);
}
export function shouldRetryStatus(status, method, opts = {}) {
if (opts.retry === false) return false;
const list = opts.retryableStatuses || RETRY_DEFAULTS.retryableStatuses;
if (!list.includes(status)) return false;
if (MUTATING_METHODS.has(method) && NON_RETRYABLE_ON_MUTATION.has(status)) {
return status === 429 ? Boolean(opts.retryMutationsOn429) : false;
}
return true;
}
export function shouldRetryError(err, opts = {}) {
if (opts.retry === false) return false;
const codes = opts.retryableErrorCodes || RETRY_DEFAULTS.retryableErrorCodes;
if (err?.code && codes.includes(err.code)) return true;
if (err?.name === "AbortError" || /timeout|abort/i.test(err?.message || "")) return true;
return false;
}
export function statusToExitCode(status) {
if (status >= 200 && status < 300) return 0;
if (status === 408) return 124;
if (status === 401 || status === 403) return 4;
if (status === 429) return 5;
if (status === 400 || status === 404 || status === 422) return 2;
if (status >= 500) return 1;
return 1;
}
export class ApiError extends Error {
constructor(message, { status, code, exitCode } = {}) {
super(message);
this.name = "ApiError";
this.status = status;
this.code = code;
this.exitCode = exitCode ?? (status != null ? statusToExitCode(status) : 1);
}
}
async function readResponseBody(res) {
const ct = res.headers.get("content-type") || "";
try {
if (ct.includes("application/json")) return await res.json();
return await res.text();
} catch {
return null;
}
}
function fetchOnce(url, init, timeoutMs) {
if (!timeoutMs) return fetch(url, init);
const ac = new AbortController();
const t = setTimeout(() => ac.abort(), timeoutMs);
const merged = { ...init, signal: ac.signal };
return fetch(url, merged).finally(() => clearTimeout(t));
}
export async function apiFetch(path, opts = {}) {
const method = String(opts.method || "GET").toUpperCase();
const url = resolveUrl(path, opts);
const headers = await buildHeaders(opts);
const body = serializeBody(opts.body, headers);
const timeout =
opts.timeout ?? (Number.parseInt(process.env.OMNIROUTE_HTTP_TIMEOUT_MS || "", 10) || 30000);
const maxAttempts = opts.retry === false ? 1 : (opts.retryMax ?? RETRY_DEFAULTS.maxAttempts);
const verbose = opts.verbose ?? process.env.OMNIROUTE_VERBOSE === "1";
let lastErr;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
const res = await fetchOnce(url, { method, headers, body }, timeout);
if (res.ok) return enrichResponse(res, opts);
if (attempt < maxAttempts && shouldRetryStatus(res.status, method, opts)) {
const delay = computeBackoff(attempt, res.headers.get("retry-after"));
if (verbose) {
process.stderr.write(
`[retry ${attempt}/${maxAttempts - 1}] ${method} ${url} → HTTP ${res.status}; wait ${Math.round(delay)}ms\n`
);
}
await sleep(delay);
continue;
}
return enrichResponse(res, opts);
} catch (err) {
lastErr = err;
if (attempt < maxAttempts && shouldRetryError(err, opts)) {
const delay = computeBackoff(attempt, null);
if (verbose) {
process.stderr.write(
`[retry ${attempt}/${maxAttempts - 1}] ${method} ${url}${err.code || err.message}; wait ${Math.round(delay)}ms\n`
);
}
await sleep(delay);
continue;
}
throw normalizeNetworkError(err);
}
}
throw normalizeNetworkError(lastErr);
}
function enrichResponse(res, opts) {
res.exitCode = statusToExitCode(res.status);
res.json = res.json.bind(res);
res.text = res.text.bind(res);
if (!res.ok && !opts.acceptNotOk) {
res.assertOk = async () => {
const payload = await readResponseBody(res);
const message = extractErrorMessage(payload, res.status);
throw new ApiError(message, { status: res.status });
};
} else {
res.assertOk = async () => res;
}
return res;
}
function extractErrorMessage(payload, status) {
if (payload && typeof payload === "object") {
if (typeof payload.error === "string") return payload.error;
if (payload.error?.message) return String(payload.error.message);
if (payload.message) return String(payload.message);
}
if (typeof payload === "string" && payload.length < 200) return payload;
return `HTTP ${status}`;
}
function normalizeNetworkError(err) {
if (err instanceof ApiError) return err;
const code = err?.code || (err?.name === "AbortError" ? "ETIMEDOUT" : undefined);
const exitCode = code === "ETIMEDOUT" ? 124 : 1;
return new ApiError(err?.message || "network error", { code, exitCode });
}
export async function isServerUp(opts = {}) {
try {
const res = await apiFetch("/api/health", {
...opts,
retry: false,
timeout: opts.timeout ?? 1500,
acceptNotOk: true,
});
return res.ok || res.status < 500;
} catch {
return false;
}
}
+323
View File
@@ -0,0 +1,323 @@
import { readFileSync } from "node:fs";
import { setTimeout as sleep } from "node:timers/promises";
import { apiFetch, isServerUp } from "../api.mjs";
import { emit } from "../output.mjs";
import { t } from "../i18n.mjs";
const A2A_SKILLS = [
{ id: "smart-routing", name: "Smart Request Routing" },
{ id: "quota-management", name: "Quota & Cost Management" },
{ id: "provider-discovery", name: "Provider Discovery" },
{ id: "cost-analysis", name: "Cost Analysis" },
{ id: "health-report", name: "Health Report" },
];
function fmtTs(v) {
if (!v) return "-";
try {
return new Date(v).toLocaleString();
} catch {
return String(v);
}
}
function randomId() {
return Math.random().toString(36).slice(2, 11);
}
async function confirm(q) {
return new Promise((resolve) => {
process.stdout.write(`${q} (yes/no) `);
process.stdin.setEncoding("utf8");
process.stdin.once("data", (c) => resolve(c.toString().trim().toLowerCase().startsWith("y")));
});
}
const taskSchema = [
{ key: "id", header: "Task ID", width: 22 },
{ key: "skill", header: "Skill", width: 22 },
{ key: "status", header: "Status", width: 12 },
{ key: "createdAt", header: "Created", formatter: fmtTs },
{ key: "updatedAt", header: "Updated", formatter: fmtTs },
{ key: "duration", header: "Duration", formatter: (v) => (v != null ? `${v}ms` : "-") },
];
export function registerA2a(program) {
const a2a = program.command("a2a").description("Agent-to-Agent (A2A) server");
a2a
.command("status")
.description("Show A2A server status")
.option("--json", "Output as JSON")
.action(async (opts, cmd) => {
const globalOpts = cmd.parent.optsWithGlobals();
const exitCode = await runA2aStatusCommand({ ...opts, output: globalOpts.output });
if (exitCode !== 0) process.exit(exitCode);
});
a2a
.command("card")
.description("Print the Agent Card JSON")
.action(async (opts, cmd) => {
const globalOpts = cmd.parent.optsWithGlobals();
const exitCode = await runA2aCardCommand({ ...opts, output: globalOpts.output });
if (exitCode !== 0) process.exit(exitCode);
});
// 5.3 — a2a skills + a2a invoke
a2a
.command("skills")
.description(t("a2a.skills.description"))
.action(async (opts, cmd) => {
const res = await apiFetch("/.well-known/agent.json");
if (res.ok) {
const card = await res.json();
emit(card.skills ?? A2A_SKILLS, cmd.optsWithGlobals());
} else {
emit(A2A_SKILLS, cmd.optsWithGlobals());
}
});
a2a
.command("invoke <skill>")
.description(t("a2a.invoke.description"))
.option("--input <json>", t("a2a.invoke.input"))
.option("--input-file <path>", t("a2a.invoke.input_file"))
.option("--wait", t("a2a.invoke.wait"))
.option("--timeout <ms>", t("a2a.invoke.timeout"), parseInt, 60000)
.action(async (skill, opts, cmd) => {
const globalOpts = cmd.optsWithGlobals();
const input = opts.input
? JSON.parse(opts.input)
: opts.inputFile
? JSON.parse(readFileSync(opts.inputFile, "utf8"))
: {};
const rpcBody = {
jsonrpc: "2.0",
id: randomId(),
method: "tasks.create",
params: {
skill,
input,
messages: [{ role: "user", parts: [{ kind: "data", data: input }] }],
},
};
const res = await apiFetch("/api/a2a/tasks", { method: "POST", body: rpcBody });
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
const created = await res.json();
const taskId = created.result?.taskId ?? created.taskId ?? created.id;
if (!opts.wait) {
emit({ taskId }, globalOpts);
return;
}
const deadline = Date.now() + (opts.timeout ?? 60000);
while (Date.now() < deadline) {
await sleep(1000);
const taskRes = await apiFetch(`/api/a2a/tasks/${taskId}`);
if (!taskRes.ok) continue;
const task = (await taskRes.json()).result ?? (await taskRes.clone().json());
const state = task.status?.state ?? task.status;
if (["completed", "failed", "cancelled"].includes(state)) {
emit(task, globalOpts);
return;
}
}
process.stderr.write("Timeout waiting for task completion\n");
process.exit(124);
});
// 5.4 — a2a tasks
const tasks = a2a.command("tasks").description(t("a2a.tasks.description"));
tasks
.command("list")
.option("--status <s>", t("a2a.tasks.list.status"))
.option("--skill <s>", t("a2a.tasks.list.skill"))
.option("--limit <n>", parseInt, 50)
.option("--since <ts>")
.action(async (opts, cmd) => {
const params = new URLSearchParams({ limit: String(opts.limit ?? 50) });
if (opts.status) params.set("status", opts.status);
if (opts.skill) params.set("skill", opts.skill);
if (opts.since) params.set("since", opts.since);
const res = await apiFetch(`/api/a2a/tasks?${params}`);
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
const data = await res.json();
emit(data.tasks ?? data.items ?? data, cmd.optsWithGlobals(), taskSchema);
});
tasks.command("get <id>").action(async (id, opts, cmd) => {
const res = await apiFetch(`/api/a2a/tasks/${id}`);
if (!res.ok) {
process.stderr.write(`Not found: ${id}\n`);
process.exit(1);
}
emit(await res.json(), cmd.optsWithGlobals());
});
tasks
.command("cancel <id>")
.option("--yes")
.action(async (id, opts, cmd) => {
if (!opts.yes) {
const ok = await confirm(`Cancel task ${id}?`);
if (!ok) return;
}
const res = await apiFetch(`/api/a2a/tasks/${id}/cancel`, { method: "POST" });
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
process.stdout.write("Cancelled\n");
});
tasks
.command("watch <id>")
.description(t("a2a.tasks.watch.description"))
.action(async (id, opts, cmd) => {
let lastState = "";
while (true) {
const res = await apiFetch(`/api/a2a/tasks/${id}`);
if (res.ok) {
const data = await res.json();
const state = data.status?.state ?? data.status ?? "";
if (state !== lastState) {
process.stderr.write(`[${new Date().toISOString()}] ${state}\n`);
lastState = state;
}
if (["completed", "failed", "cancelled"].includes(state)) {
emit(data, cmd.optsWithGlobals());
return;
}
}
await sleep(1500);
}
});
tasks
.command("stream <id>")
.description(t("a2a.tasks.stream.description"))
.action(async (id, opts, cmd) => {
const globalOpts = cmd.optsWithGlobals();
const baseUrl = globalOpts.baseUrl ?? "http://localhost:20128";
const apiKey = globalOpts.apiKey ?? "";
const res = await fetch(`${baseUrl}/api/a2a/tasks/${id}?stream=true`, {
headers: {
Accept: "text/event-stream",
...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {}),
},
});
if (!res.ok) {
process.stderr.write(`HTTP ${res.status}\n`);
process.exit(1);
}
const reader = res.body.getReader();
const dec = new TextDecoder();
let buf = "";
while (true) {
const { done, value } = await reader.read();
if (done) break;
buf += dec.decode(value, { stream: true });
const lines = buf.split("\n");
buf = lines.pop() ?? "";
for (const line of lines) {
if (line.startsWith("data: ")) {
const raw = line.slice(6).trim();
if (raw && raw !== "[DONE]") process.stdout.write(raw + "\n");
}
}
}
});
tasks
.command("logs <id>")
.description(t("a2a.tasks.logs.description"))
.action(async (id, opts, cmd) => {
const res = await apiFetch(`/api/a2a/tasks/${id}?include=messages,artifacts`);
if (!res.ok) {
process.stderr.write(`Not found: ${id}\n`);
process.exit(1);
}
const data = await res.json();
emit(data.messages ?? data, cmd.optsWithGlobals());
});
}
export async function runA2aStatusCommand(opts = {}) {
const serverUp = await isServerUp();
if (!serverUp) {
console.error(t("common.serverOffline"));
return 1;
}
try {
const res = await apiFetch("/api/a2a/status", {
retry: false,
timeout: 5000,
acceptNotOk: true,
});
if (!res.ok) {
console.log("A2A status not available.");
return 0;
}
const status = await res.json();
if (opts.json || opts.output === "json") {
console.log(JSON.stringify(status, null, 2));
return 0;
}
const running = status.running ? "\x1b[32mrunning\x1b[0m" : "\x1b[31mstopped\x1b[0m";
console.log(` Status: ${running}`);
console.log(` Protocol: ${status.protocol || "JSON-RPC 2.0"}`);
console.log(` Tasks: ${status.activeTasks || 0} active`);
if (status.skills?.length) {
console.log("\n Skills:");
for (const skill of status.skills) {
console.log(`\x1b[2m - ${skill.name}: ${skill.description || "N/A"}\x1b[0m`);
}
}
return 0;
} catch (err) {
console.error(t("common.error", { message: err instanceof Error ? err.message : String(err) }));
return 1;
}
}
export async function runA2aCardCommand(opts = {}) {
const serverUp = await isServerUp();
if (!serverUp) {
console.error(t("common.serverOffline"));
return 1;
}
try {
const res = await apiFetch("/.well-known/agent.json", {
retry: false,
timeout: 5000,
acceptNotOk: true,
});
if (res.ok) {
const card = await res.json();
console.log(JSON.stringify(card, null, 2));
return 0;
}
console.log("Agent card not available.");
return 0;
} catch (err) {
console.error(t("common.error", { message: err instanceof Error ? err.message : String(err) }));
return 1;
}
}
+203
View File
@@ -0,0 +1,203 @@
import { writeFileSync } from "node:fs";
import { setTimeout as sleep } from "node:timers/promises";
import { apiFetch } from "../api.mjs";
import { emit } from "../output.mjs";
import { t } from "../i18n.mjs";
function truncate(v, len = 40) {
if (v == null) return "-";
const s = String(v);
return s.length > len ? s.slice(0, len - 1) + "…" : s;
}
function fmtTs(v) {
if (!v) return "-";
try {
return new Date(v).toLocaleString();
} catch {
return String(v);
}
}
function maskActor(v) {
if (!v) return "-";
const s = String(v);
if (s.length <= 8) return s;
return `${s.slice(0, 4)}****${s.slice(-4)}`;
}
const auditSchema = [
{ key: "timestamp", header: "Time", width: 22, formatter: fmtTs },
{ key: "source", header: "Source", width: 10 },
{ key: "actor", header: "Actor", width: 16, formatter: maskActor },
{ key: "action", header: "Action", width: 28 },
{ key: "resource", header: "Resource", width: 32, formatter: truncate },
{ key: "result", header: "Result", formatter: (v) => (v === "success" ? "✓" : "✗") },
{ key: "details", header: "Details", formatter: truncate },
];
function endpointFor(source) {
return source === "mcp" ? "/api/mcp/audit" : "/api/compliance/audit-log";
}
async function fetchAuditEntries(sources, params) {
const entries = [];
for (const src of sources) {
const endpoint = endpointFor(src);
const res = await apiFetch(`${endpoint}?${params}`);
if (!res.ok) continue;
const data = await res.json();
for (const e of data.items ?? data) {
entries.push({ ...e, source: src });
}
}
return entries;
}
function resolveSources(source) {
if (source === "all") return ["compliance", "mcp"];
return [source ?? "compliance"];
}
export async function runAuditTail(opts, cmd) {
const globalOpts = cmd.optsWithGlobals();
const sources = resolveSources(opts.source);
const params = new URLSearchParams({ limit: String(opts.limit ?? 100) });
const entries = await fetchAuditEntries(sources, params);
entries.sort((a, b) => String(b.timestamp ?? "").localeCompare(String(a.timestamp ?? "")));
emit(entries.slice(0, opts.limit ?? 100), globalOpts, auditSchema);
if (opts.follow) {
process.stderr.write("\n[following — Ctrl+C to exit]\n");
let lastTs = entries[0]?.timestamp ?? new Date().toISOString();
const loop = async () => {
while (true) {
await sleep(2000);
for (const src of sources) {
const endpoint = endpointFor(src);
const res = await apiFetch(`${endpoint}?since=${encodeURIComponent(lastTs)}&limit=50`);
if (!res.ok) continue;
const data = await res.json();
const newEntries = (data.items ?? data)
.map((e) => ({ ...e, source: src }))
.filter((e) => String(e.timestamp ?? "") > String(lastTs));
for (const e of newEntries) {
if (String(e.timestamp ?? "") > String(lastTs)) lastTs = e.timestamp;
emit([e], globalOpts, auditSchema);
}
}
}
};
process.on("SIGINT", () => process.exit(0));
await loop();
}
}
export async function runAuditSearch(query, opts, cmd) {
const globalOpts = cmd.optsWithGlobals();
const sources = resolveSources(opts.source);
const params = new URLSearchParams({ q: query, limit: String(opts.limit ?? 200) });
if (opts.since) params.set("since", opts.since);
if (opts.until) params.set("until", opts.until);
if (opts.actor) params.set("actor", opts.actor);
if (opts.action) params.set("action", opts.action);
const entries = await fetchAuditEntries(sources, params);
entries.sort((a, b) => String(b.timestamp ?? "").localeCompare(String(a.timestamp ?? "")));
emit(entries, globalOpts, auditSchema);
}
export async function runAuditExport(file, opts, cmd) {
const sources = resolveSources(opts.source === "all" ? "compliance" : opts.source);
const format = opts.format ?? "jsonl";
const params = new URLSearchParams({ format });
if (opts.since) params.set("since", opts.since);
if (opts.until) params.set("until", opts.until);
const allLines = [];
for (const src of sources) {
const endpoint = endpointFor(src);
const res = await apiFetch(`${endpoint}?${params}`);
if (!res.ok) {
process.stderr.write(`Error fetching ${src}: ${res.status}\n`);
continue;
}
const body = await res.text();
allLines.push(body);
}
const combined = allLines.join("\n");
writeFileSync(file, combined);
process.stdout.write(`Exported to ${file} (${combined.length} bytes)\n`);
}
export async function runAuditStats(opts, cmd) {
const globalOpts = cmd.optsWithGlobals();
const source = opts.source ?? "mcp";
const params = new URLSearchParams({ period: opts.period ?? "7d" });
const endpoint = source === "mcp" ? "/api/mcp/audit/stats" : "/api/compliance/audit-log/stats";
const res = await apiFetch(`${endpoint}?${params}`);
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
const data = await res.json();
emit(data, globalOpts);
}
export async function runAuditGet(id, opts, cmd) {
const globalOpts = cmd.optsWithGlobals();
const source = opts.source ?? "compliance";
const endpoint = endpointFor(source);
const res = await apiFetch(`${endpoint}/${id}`);
if (!res.ok) {
process.stderr.write(`Not found: ${id}\n`);
process.exit(1);
}
const data = await res.json();
emit(data, globalOpts, auditSchema);
}
export function registerAudit(program) {
const audit = program.command("audit").description(t("audit.description"));
audit
.command("tail")
.description(t("audit.tail.description"))
.option("--source <s>", t("audit.source"), "all")
.option("--follow", t("audit.tail.follow"))
.option("--limit <n>", t("audit.tail.limit"), parseInt, 100)
.action(runAuditTail);
audit
.command("search <query>")
.description(t("audit.search.description"))
.option("--source <s>", t("audit.source"), "all")
.option("--since <ts>", t("audit.since"))
.option("--until <ts>", t("audit.until"))
.option("--limit <n>", t("audit.search.limit"), parseInt, 200)
.option("--actor <id>", t("audit.search.actor"))
.option("--action <a>", t("audit.search.action"))
.action(runAuditSearch);
audit
.command("export <file>")
.description(t("audit.export.description"))
.option("--source <s>", t("audit.source"), "all")
.option("--format <f>", t("audit.export.format"), "jsonl")
.option("--since <ts>", t("audit.since"))
.option("--until <ts>", t("audit.until"))
.action(runAuditExport);
audit
.command("stats")
.description(t("audit.stats.description"))
.option("--source <s>", t("audit.source"), "mcp")
.option("--period <p>", t("audit.stats.period"), "7d")
.action(runAuditStats);
audit
.command("get <id>")
.description(t("audit.get.description"))
.option("--source <s>", t("audit.source"), "compliance")
.action(runAuditGet);
}
+57
View File
@@ -0,0 +1,57 @@
import { t } from "../i18n.mjs";
import { emit } from "../output.mjs";
export function registerAutostart(program) {
const cmd = program
.command("autostart")
.description(t("autostart.description") || "Manage OmniRoute autostart at login");
// #3331 — autostart could previously only be toggled from the tray
// (`serve --tray`) or the Electron Appearance tab; a plain `omniroute serve`
// user had no path. These subcommands (with `on`/`off`/`true`/`false`
// aliases, e.g. `omniroute autostart on`) make it a first-class CLI action.
cmd
.command("enable")
.aliases(["on", "true"])
.description(t("autostart.enable") || "Enable autostart at login")
.action(async (opts, c) => {
const globalOpts = c.optsWithGlobals();
const { enable } = await import("../tray/autostart.mjs");
const ok = enable();
emit({ enabled: ok }, globalOpts);
if (!ok) process.exit(1);
});
cmd
.command("disable")
.aliases(["off", "false"])
.description(t("autostart.disable") || "Disable autostart at login")
.action(async (opts, c) => {
const globalOpts = c.optsWithGlobals();
const { disable } = await import("../tray/autostart.mjs");
const ok = disable();
emit({ disabled: ok }, globalOpts);
if (!ok) process.exit(1);
});
cmd
.command("toggle")
.description(t("autostart.toggle") || "Toggle autostart at login")
.action(async (opts, c) => {
const globalOpts = c.optsWithGlobals();
const { enable, disable, isAutostartEnabled } = await import("../tray/autostart.mjs");
const next = !isAutostartEnabled();
const ok = next ? enable() : disable();
emit(next ? { enabled: ok } : { disabled: ok }, globalOpts);
if (!ok) process.exit(1);
});
cmd
.command("status", { isDefault: true })
.description(t("autostart.status") || "Show autostart status")
.action(async (opts, c) => {
const globalOpts = c.optsWithGlobals();
const { getAutostartStatus } = await import("../tray/autostart.mjs");
emit(getAutostartStatus(), globalOpts);
});
}
+469
View File
@@ -0,0 +1,469 @@
import {
copyFileSync,
createReadStream,
createWriteStream,
existsSync,
mkdirSync,
readdirSync,
readFileSync,
statSync,
unlinkSync,
writeFileSync,
} from "node:fs";
import { createCipheriv, createDecipheriv, randomBytes, scryptSync } from "node:crypto";
import { dirname, join, extname, basename } from "node:path";
import { Readable } from "node:stream";
import { pipeline } from "node:stream/promises";
import { resolveDataDir } from "../data-dir.mjs";
import { getBaseUrl, isServerUp } from "../api.mjs";
import { t } from "../i18n.mjs";
import { backupSqliteFile } from "../sqlite.mjs";
import { CLI_TOKEN_HEADER, getCliToken } from "../utils/cliToken.mjs";
function getBackupDir() {
return join(resolveDataDir(), "backups");
}
const FILES_TO_BACKUP = [
{ name: "storage.sqlite" },
{ name: "settings.json" },
{ name: "combos.json" },
{ name: "providers.json" },
];
export function registerBackup(program) {
const backup = program.command("backup").description(t("backup.description"));
backup
.command("create")
.description(t("backup.createDescription"))
.option("--name <name>", t("backup.nameOpt"))
.option("--cloud", t("backup.cloudOpt"))
.option("--encrypt", t("backup.encryptOpt"))
.option("--key-file <path>", t("backup.keyFileOpt"))
.option("--exclude <pattern>", t("backup.excludeOpt"), (v, prev = []) => [...prev, v], [])
.option("--retention <n>", t("backup.retentionOpt"), parseInt)
.action(async (opts) => {
const exitCode = await runBackupCommand(opts);
if (exitCode !== 0) process.exit(exitCode);
});
const auto = backup.command("auto").description(t("backup.auto.title"));
auto
.command("enable")
.description(t("backup.auto.enableDescription"))
.option("--cron <expr>", t("backup.auto.cronOpt"), "0 3 * * *")
.option("--cloud", t("backup.cloudOpt"))
.option("--encrypt", t("backup.encryptOpt"))
.option("--retention <n>", t("backup.retentionOpt"), parseInt)
.action(async (opts) => {
const exitCode = await runBackupAutoEnableCommand(opts);
if (exitCode !== 0) process.exit(exitCode);
});
auto
.command("disable")
.description(t("backup.auto.disableDescription"))
.action(async () => {
const exitCode = await runBackupAutoDisableCommand();
if (exitCode !== 0) process.exit(exitCode);
});
auto
.command("status")
.description(t("backup.auto.statusDescription"))
.action(async () => {
const exitCode = await runBackupAutoStatusCommand();
if (exitCode !== 0) process.exit(exitCode);
});
// Legacy: `omniroute backup` without subcommand still creates a backup
backup.action(async (opts) => {
const exitCode = await runBackupCommand(opts);
if (exitCode !== 0) process.exit(exitCode);
});
backup
.option("--name <name>", t("backup.nameOpt"))
.option("--cloud", t("backup.cloudOpt"))
.option("--encrypt", t("backup.encryptOpt"))
.option("--key-file <path>", t("backup.keyFileOpt"))
.option("--exclude <pattern>", t("backup.excludeOpt"), (v, prev = []) => [...prev, v], [])
.option("--retention <n>", t("backup.retentionOpt"), parseInt);
}
export function registerRestore(program) {
program
.command("restore [backupId]")
.description(t("backup.restoreDescription"))
.option("--list", "List available backups")
.option("--yes", "Skip confirmation")
.action(async (backupId, opts) => {
const exitCode = await runRestoreCommand(backupId, opts);
if (exitCode !== 0) process.exit(exitCode);
});
}
function matchesGlob(fileName, pattern) {
if (!pattern.includes("*")) return fileName === pattern;
const parts = pattern.split("*");
let pos = 0;
for (let i = 0; i < parts.length; i++) {
const part = parts[i];
if (!part) continue;
if (i === 0) {
if (!fileName.startsWith(part)) return false;
pos = part.length;
} else if (i === parts.length - 1) {
if (!fileName.endsWith(part)) return false;
if (fileName.length < pos + part.length) return false;
} else {
const idx = fileName.indexOf(part, pos);
if (idx === -1) return false;
pos = idx + part.length;
}
}
return true;
}
function shouldExclude(fileName, patterns) {
if (!patterns || patterns.length === 0) return false;
return patterns.some((p) => matchesGlob(fileName, p));
}
async function encryptFile(srcPath, destPath, passphrase) {
const salt = randomBytes(16);
const iv = randomBytes(12);
const key = scryptSync(passphrase, salt, 32);
const cipher = createCipheriv("aes-256-gcm", key, iv);
const tmpCipherPath = `${destPath}.ciphertext`;
await pipeline(createReadStream(srcPath), cipher, createWriteStream(tmpCipherPath));
const authTag = cipher.getAuthTag();
// Format: salt(16) + iv(12) + authTag(16) + ciphertext
const out = createWriteStream(destPath);
try {
await new Promise((resolve, reject) => {
out.write(Buffer.concat([salt, iv, authTag]), (err) => {
if (err) reject(err);
else resolve();
});
});
await pipeline(createReadStream(tmpCipherPath), out);
} finally {
try {
unlinkSync(tmpCipherPath);
} catch {}
}
}
async function promptPassphrase() {
const readline = await import("node:readline");
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
return new Promise((resolve) =>
rl.question(t("backup.passphrasePrompt"), (ans) => {
rl.close();
resolve(ans.trim());
})
);
}
async function pruneBackups(backupDir, retention) {
if (!retention || retention <= 0 || !existsSync(backupDir)) return;
try {
const dirs = readdirSync(backupDir)
.filter((f) => f.startsWith("omniroute-backup-"))
.sort()
.reverse();
for (const old of dirs.slice(retention)) {
const { rmSync } = await import("node:fs");
rmSync(join(backupDir, old), { recursive: true, force: true });
}
} catch {}
}
export async function runBackupCommand(opts = {}) {
const dataDir = resolveDataDir();
const backupDir = getBackupDir();
const timestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
const safeName = opts.name ? String(opts.name).replace(/[/\\]/g, "_") : null;
const backupName = safeName ? `omniroute-backup-${safeName}` : `omniroute-backup-${timestamp}`;
const backupPath = join(backupDir, backupName);
const excludePatterns = opts.exclude || [];
console.log(t("backup.creating"));
let passphrase = null;
if (opts.encrypt) {
if (opts.keyFile) {
passphrase = readFileSync(opts.keyFile, "utf8").trim();
} else {
passphrase = await promptPassphrase();
if (!passphrase) {
console.error(t("backup.noPassphrase"));
return 1;
}
}
}
try {
if (!existsSync(backupDir)) mkdirSync(backupDir, { recursive: true });
let backedUp = 0;
let skipped = 0;
for (const file of FILES_TO_BACKUP) {
if (shouldExclude(file.name, excludePatterns)) {
skipped++;
continue;
}
const sourcePath = join(dataDir, file.name);
if (existsSync(sourcePath)) {
const destName = opts.encrypt ? `${file.name}.enc` : file.name;
const destPath = join(backupPath, destName);
mkdirSync(dirname(destPath), { recursive: true });
if (file.name.endsWith(".sqlite")) {
const tmpPath = destPath.replace(/\.enc$/, "");
await backupSqliteFile(sourcePath, tmpPath);
if (opts.encrypt) {
await encryptFile(tmpPath, destPath, passphrase);
unlinkSync(tmpPath);
}
} else if (opts.encrypt) {
await encryptFile(sourcePath, destPath, passphrase);
} else {
copyFileSync(sourcePath, destPath);
}
backedUp++;
} else {
skipped++;
}
}
if (backedUp > 0) {
const info = {
timestamp: new Date().toISOString(),
version: "omniroute-cli-v1",
encrypted: !!opts.encrypt,
files: FILES_TO_BACKUP.filter(
(f) => existsSync(join(dataDir, f.name)) && !shouldExclude(f.name, excludePatterns)
).map((f) => (opts.encrypt ? `${f.name}.enc` : f.name)),
};
writeFileSync(join(backupPath, "backup-info.json"), JSON.stringify(info, null, 2), "utf8");
if (opts.cloud) {
const cloudCode = await _uploadBackupToCloud(backupPath, info);
if (cloudCode !== 0) {
console.warn(t("backup.cloudFailed"));
}
}
if (opts.retention) {
await pruneBackups(backupDir, opts.retention);
}
console.log(t("backup.done", { path: backupPath }));
console.log(
`\x1b[2m ${backedUp} backed up, ${skipped} skipped${opts.encrypt ? " (encrypted)" : ""}\x1b[0m`
);
return 0;
}
console.log(t("backup.noFiles"));
return 0;
} catch (err) {
console.error(t("backup.failed", { error: err instanceof Error ? err.message : String(err) }));
return 1;
}
}
async function _uploadBackupToCloud(backupPath, info) {
const serverUp = await isServerUp();
if (!serverUp) {
console.warn(t("common.serverOffline"));
return 1;
}
try {
const boundary = `omniroute-backup-${Date.now().toString(36)}-${randomBytes(8).toString("hex")}`;
const headers = new Headers({
accept: "application/json",
"content-type": `multipart/form-data; boundary=${boundary}`,
});
const apiKey = process.env.OMNIROUTE_API_KEY;
if (apiKey) headers.set("authorization", `Bearer ${apiKey}`);
const cliToken = process.env.OMNIROUTE_CLI_TOKEN ?? (await getCliToken());
if (cliToken) headers.set(CLI_TOKEN_HEADER, cliToken);
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 30000);
const res = await fetch(`${getBaseUrl()}/api/db-backups/cloud`, {
method: "POST",
headers,
body: Readable.from(createBackupMultipartStream(backupPath, info, boundary)),
duplex: "half",
signal: controller.signal,
}).finally(() => clearTimeout(timeout));
if (res.ok) {
const data = await res.json();
console.log(t("backup.cloudUploaded", { url: data.url || "(stored)" }));
return 0;
}
return 1;
} catch {
return 1;
}
}
async function* createBackupMultipartStream(backupPath, info, boundary) {
const encoder = new TextEncoder();
const encode = (value) => encoder.encode(value);
yield encode(
`--${boundary}\r\nContent-Disposition: form-data; name="info"\r\nContent-Type: application/json\r\n\r\n${JSON.stringify(info)}\r\n`
);
for (const fname of readdirSync(backupPath)) {
const fullPath = join(backupPath, fname);
const stat = statSync(fullPath);
if (!stat.isFile()) continue;
const safeName = fname.replace(/["\r\n]/g, "_");
yield encode(
`--${boundary}\r\nContent-Disposition: form-data; name="files"; filename="${safeName}"\r\nContent-Type: application/octet-stream\r\n\r\n`
);
yield* createReadStream(fullPath);
yield encode("\r\n");
}
yield encode(`--${boundary}--\r\n`);
}
function getSchedulePath() {
return join(resolveDataDir(), "backup-schedule.json");
}
export async function runBackupAutoEnableCommand(opts = {}) {
const schedulePath = getSchedulePath();
const schedule = {
enabled: true,
cron: opts.cron || "0 3 * * *",
cloud: !!opts.cloud,
encrypt: !!opts.encrypt,
retention: opts.retention || null,
updatedAt: new Date().toISOString(),
};
mkdirSync(dirname(schedulePath), { recursive: true });
writeFileSync(schedulePath, JSON.stringify(schedule, null, 2), "utf8");
console.log(t("backup.auto.enabled", { cron: schedule.cron }));
console.log(t("backup.auto.hint"));
return 0;
}
export async function runBackupAutoDisableCommand() {
const schedulePath = getSchedulePath();
if (existsSync(schedulePath)) {
const schedule = JSON.parse(readFileSync(schedulePath, "utf8"));
schedule.enabled = false;
schedule.updatedAt = new Date().toISOString();
writeFileSync(schedulePath, JSON.stringify(schedule, null, 2), "utf8");
}
console.log(t("backup.auto.disabled"));
return 0;
}
export async function runBackupAutoStatusCommand() {
const schedulePath = getSchedulePath();
if (!existsSync(schedulePath)) {
console.log(t("backup.auto.notConfigured"));
return 0;
}
const schedule = JSON.parse(readFileSync(schedulePath, "utf8"));
const statusLabel = schedule.enabled ? "\x1b[32m● enabled\x1b[0m" : "\x1b[31m○ disabled\x1b[0m";
console.log(`${t("backup.auto.title")}: ${statusLabel}`);
console.log(` cron: ${schedule.cron}`);
console.log(` cloud: ${schedule.cloud ? "yes" : "no"}`);
console.log(` encrypt: ${schedule.encrypt ? "yes" : "no"}`);
console.log(` retention: ${schedule.retention ?? "unlimited"}`);
return 0;
}
export async function runRestoreCommand(backupId, opts = {}) {
const backupDir = getBackupDir();
if (opts.list || !backupId) {
console.log(`\n\x1b[1m\x1b[36m${t("backup.listTitle")}\x1b[0m\n`);
if (!existsSync(backupDir)) {
console.log(t("backup.noBackups"));
return 0;
}
try {
const dirs = readdirSync(backupDir)
.filter((f) => f.startsWith("omniroute-backup-"))
.sort()
.reverse();
if (dirs.length === 0) {
console.log(t("backup.noBackups"));
return 0;
}
for (const dir of dirs) {
const infoPath = join(backupDir, dir, "backup-info.json");
if (existsSync(infoPath)) {
const info = JSON.parse(readFileSync(infoPath, "utf8"));
const id = dir.replace("omniroute-backup-", "");
const dateStr = new Date(info.timestamp).toLocaleString();
console.log(` ${id}`);
console.log(`\x1b[2m ${dateStr}${info.files?.length || 0} files\x1b[0m`);
} else {
console.log(`\x1b[2m ${dir.replace("omniroute-backup-", "")}\x1b[0m`);
}
}
} catch (err) {
console.error(
t("common.error", { message: err instanceof Error ? err.message : String(err) })
);
return 1;
}
if (!backupId) console.log("\nUsage: omniroute restore <backup-id>");
return 0;
}
const safeBackupId = String(backupId).replace(/[/\\]/g, "_");
const backupPath = join(backupDir, `omniroute-backup-${safeBackupId}`);
if (!existsSync(backupPath)) {
console.error(t("backup.notFound", { name: backupId }));
return 1;
}
const infoPath = join(backupPath, "backup-info.json");
const ts = existsSync(infoPath) ? JSON.parse(readFileSync(infoPath, "utf8")).timestamp : backupId;
if (!opts.yes) {
const readline = await import("node:readline");
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
const answer = await new Promise((resolve) =>
rl.question(t("backup.confirmRestore", { ts }) + " [y/N] ", resolve)
);
rl.close();
if (!/^y(es)?$/i.test(answer)) {
console.log(t("common.cancelled"));
return 0;
}
}
console.log(t("backup.restoring", { path: backupPath }));
const dataDir = resolveDataDir();
try {
for (const file of FILES_TO_BACKUP) {
const sourcePath = join(backupPath, file.name);
if (existsSync(sourcePath)) {
copyFileSync(sourcePath, join(dataDir, file.name));
console.log(`\x1b[2m Restored: ${file.name}\x1b[0m`);
}
}
console.log(t("backup.restored"));
return 0;
} catch (err) {
console.error(t("common.error", { message: err instanceof Error ? err.message : String(err) }));
return 1;
}
}
+235
View File
@@ -0,0 +1,235 @@
import { readFileSync, writeFileSync } from "node:fs";
import { createInterface } from "node:readline";
import { setTimeout as sleep } from "node:timers/promises";
import { apiFetch, getBaseUrl } from "../api.mjs";
import { emit } from "../output.mjs";
import { t } from "../i18n.mjs";
function fmtTs(v) {
if (!v) return "-";
return new Date(typeof v === "number" ? v * 1000 : v).toLocaleString();
}
async function confirm(q) {
const rl = createInterface({ input: process.stdin, output: process.stdout });
return new Promise((resolve) => {
rl.question(`${q} [y/N] `, (a) => {
rl.close();
resolve(a.trim().toLowerCase() === "y");
});
});
}
function authHeaders(opts) {
const h = { accept: "application/json" };
if (opts.apiKey) h["Authorization"] = `Bearer ${opts.apiKey}`;
return h;
}
const batchSchema = [
{ key: "id", header: "Batch ID", width: 28 },
{ key: "status", header: "Status", width: 14 },
{ key: "endpoint", header: "Endpoint", width: 26 },
{
key: "request_counts",
header: "Total",
formatter: (v) => (v?.completed != null ? `${v.completed}/${v.total}` : "-"),
},
{ key: "created_at", header: "Created", formatter: fmtTs },
];
async function uploadFile(filePath, purpose) {
const { fmtBytes } = await import("./files.mjs");
const { statSync, readFileSync: readF } = await import("node:fs");
const { basename } = await import("node:path");
const stat = statSync(filePath);
if (stat.size > 100 * 1024 * 1024) {
process.stderr.write(`Warning: file is ${fmtBytes(stat.size)} (large)\n`);
}
const form = new FormData();
form.append("purpose", purpose);
form.append("file", new Blob([readF(filePath)]), basename(filePath));
const res = await apiFetch("/v1/files", { method: "POST", body: form });
if (!res.ok) {
process.stderr.write(`Upload failed: ${res.status}\n`);
process.exit(1);
}
return res.json();
}
async function fetchFile(fileId, globalOpts = {}) {
const res = await fetch(`${getBaseUrl(globalOpts)}/v1/files/${fileId}/content`, {
headers: authHeaders(globalOpts),
});
if (!res.ok) {
process.stderr.write(`Error fetching file: ${res.status}\n`);
process.exit(1);
}
return res.text();
}
async function waitBatch(id, opts, timeout = 3600000) {
const deadline = Date.now() + timeout;
while (Date.now() < deadline) {
const res = await apiFetch(`/v1/batches/${id}`);
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
const b = await res.json();
const done = b.request_counts?.completed ?? 0;
const total = b.request_counts?.total ?? "?";
process.stderr.write(`[${b.status}] ${done}/${total}\n`);
if (["completed", "failed", "expired", "cancelled"].includes(b.status)) {
emit(b, opts);
return;
}
await sleep(5000);
}
process.stderr.write("Timeout\n");
process.exit(124);
}
export function registerBatches(program) {
const batches = program.command("batches").description(t("batches.description"));
batches
.command("list")
.option("--status <s>", t("batches.list.status"))
.option("--limit <n>", t("batches.list.limit"), parseInt, 50)
.action(async (opts, cmd) => {
const params = new URLSearchParams({ limit: String(opts.limit) });
if (opts.status) params.set("status", opts.status);
const res = await apiFetch(`/v1/batches?${params}`);
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
const data = await res.json();
emit(data.data ?? data.items ?? data, cmd.optsWithGlobals(), batchSchema);
});
batches.command("get <batchId>").action(async (id, opts, cmd) => {
const res = await apiFetch(`/v1/batches/${id}`);
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
emit(await res.json(), cmd.optsWithGlobals());
});
batches
.command("create")
.description(t("batches.create.description"))
.requiredOption("--input-file <fileId>", t("batches.create.inputFile"))
.option("--endpoint <e>", t("batches.create.endpoint"), "/v1/chat/completions")
.option("--completion-window <w>", t("batches.create.window"), "24h")
.option(
"--metadata <kv>",
t("batches.create.metadata"),
(v, prev = {}) => {
const eq = v.indexOf("=");
if (eq < 0) return prev;
const k = v.slice(0, eq);
const val = v.slice(eq + 1);
return { ...prev, [k]: val };
},
{}
)
.action(async (opts, cmd) => {
const body = {
input_file_id: opts.inputFile,
endpoint: opts.endpoint,
completion_window: opts.completionWindow,
metadata: Object.keys(opts.metadata).length ? opts.metadata : undefined,
};
const res = await apiFetch("/v1/batches", { method: "POST", body });
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
emit(await res.json(), cmd.optsWithGlobals());
});
batches
.command("submit")
.description(t("batches.submit.description"))
.requiredOption("--jsonl <path>", t("batches.submit.jsonl"))
.option("--endpoint <e>", t("batches.submit.endpoint"), "/v1/chat/completions")
.option("--wait", t("batches.submit.wait"))
.action(async (opts, cmd) => {
const globalOpts = cmd.optsWithGlobals();
const upload = await uploadFile(opts.jsonl, "batch");
const res = await apiFetch("/v1/batches", {
method: "POST",
body: { input_file_id: upload.id, endpoint: opts.endpoint, completion_window: "24h" },
});
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
const batch = await res.json();
emit(batch, globalOpts);
if (opts.wait) await waitBatch(batch.id, globalOpts);
});
batches
.command("cancel <batchId>")
.option("--yes", t("batches.cancel.yes"))
.action(async (id, opts, cmd) => {
if (!opts.yes) {
const ok = await confirm(`Cancel batch ${id}?`);
if (!ok) return;
}
const res = await apiFetch(`/v1/batches/${id}/cancel`, { method: "POST" });
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
process.stdout.write("Cancelled\n");
});
batches
.command("wait <batchId>")
.option("--timeout <ms>", t("batches.wait.timeout"), parseInt, 3600000)
.action(async (id, opts, cmd) => waitBatch(id, cmd.optsWithGlobals(), opts.timeout));
batches
.command("output <batchId>")
.option("--out <path>", t("batches.output.out"), "batch-output.jsonl")
.action(async (id, opts, cmd) => {
const res = await apiFetch(`/v1/batches/${id}`);
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
const batch = await res.json();
if (!batch.output_file_id) {
process.stderr.write("Not yet completed\n");
process.exit(1);
}
const content = await fetchFile(batch.output_file_id, cmd.optsWithGlobals());
writeFileSync(opts.out, content);
process.stdout.write(`Saved to ${opts.out}\n`);
});
batches
.command("errors <batchId>")
.option("--out <path>", t("batches.errors.out"), "batch-errors.jsonl")
.action(async (id, opts, cmd) => {
const res = await apiFetch(`/v1/batches/${id}`);
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
const batch = await res.json();
if (!batch.error_file_id) {
process.stdout.write("No errors\n");
return;
}
const content = await fetchFile(batch.error_file_id, cmd.optsWithGlobals());
writeFileSync(opts.out, content);
process.stdout.write(`Saved to ${opts.out}\n`);
});
}
+102
View File
@@ -0,0 +1,102 @@
import { apiFetch, isServerUp } from "../api.mjs";
import { t } from "../i18n.mjs";
export function registerCache(program) {
const cache = program.command("cache").description(t("cache.description"));
cache
.command("status")
.alias("stats")
.description("Show cache statistics")
.option("--json", "Output as JSON")
.action(async (opts, cmd) => {
const globalOpts = cmd.parent.optsWithGlobals();
const exitCode = await runCacheStatusCommand({ ...opts, output: globalOpts.output });
if (exitCode !== 0) process.exit(exitCode);
});
cache
.command("clear")
.description("Clear all cached responses")
.option("--yes", "Skip confirmation")
.action(async (opts, cmd) => {
const globalOpts = cmd.parent.optsWithGlobals();
const exitCode = await runCacheClearCommand({ ...opts, output: globalOpts.output });
if (exitCode !== 0) process.exit(exitCode);
});
}
export async function runCacheStatusCommand(opts = {}) {
const serverUp = await isServerUp();
if (!serverUp) {
console.error(t("cache.noServer"));
return 1;
}
try {
const res = await apiFetch("/api/cache/stats", {
retry: false,
timeout: 5000,
acceptNotOk: true,
});
if (!res.ok) {
console.log("Cache stats not available.");
return 0;
}
const stats = await res.json();
if (opts.json || opts.output === "json") {
console.log(JSON.stringify(stats, null, 2));
return 0;
}
console.log(`\n\x1b[1m\x1b[36mCache Status\x1b[0m\n`);
console.log(` Semantic hits: ${stats.semanticHits || 0}`);
console.log(` Signature hits: ${stats.signatureHits || 0}`);
if (stats.size !== undefined) console.log(` Size: ${stats.size}`);
return 0;
} catch (err) {
console.error(t("common.error", { message: err instanceof Error ? err.message : String(err) }));
return 1;
}
}
export async function runCacheClearCommand(opts = {}) {
const serverUp = await isServerUp();
if (!serverUp) {
console.error(t("cache.noServer"));
return 1;
}
if (!opts.yes) {
const readline = await import("node:readline");
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
const answer = await new Promise((resolve) =>
rl.question("Clear all cached responses? [y/N] ", resolve)
);
rl.close();
if (!/^y(es)?$/i.test(answer)) {
console.log(t("common.cancelled"));
return 0;
}
}
try {
const res = await apiFetch("/api/cache/clear", {
method: "POST",
retry: false,
timeout: 5000,
acceptNotOk: true,
});
if (res.ok) {
console.log(t("cache.cleared"));
return 0;
}
console.error(t("cache.clearFailed"));
return 1;
} catch (err) {
console.error(t("common.error", { message: err instanceof Error ? err.message : String(err) }));
return 1;
}
}
+162
View File
@@ -0,0 +1,162 @@
import { appendFileSync, readFileSync } from "node:fs";
import { join } from "node:path";
import { apiFetch } from "../api.mjs";
import { emit } from "../output.mjs";
import { t } from "../i18n.mjs";
import { resolveDataDir } from "../data-dir.mjs";
function resolveHistoryPath() {
return join(resolveDataDir(), "cli-history.jsonl");
}
export function registerChat(program) {
program
.command("chat [prompt]")
.description(t("chat.description"))
.option("--file <path>", t("chat.file"))
.option("--stdin", t("chat.stdin"))
.option("-s, --system <prompt>", t("chat.system"))
.option("-m, --model <id>", t("chat.model"), "auto")
.option("--max-tokens <n>", t("chat.max_tokens"), parseInt)
.option("--temperature <t>", t("chat.temperature"), parseFloat)
.option("--top-p <p>", t("chat.top_p"), parseFloat)
.option("--reasoning-effort <level>", t("chat.reasoning_effort"))
.option("--thinking-budget <tokens>", t("chat.thinking_budget"), parseInt)
.option("--combo <name>", t("chat.combo"))
.option("--responses-api", t("chat.responses_api"))
.option("--stream", t("chat.stream"))
.option("--no-history", t("chat.no_history"))
.action(runChatCommand);
}
export async function runChatCommand(promptArg, opts, cmd) {
const globalOpts = cmd.optsWithGlobals();
const prompt = await resolvePrompt(promptArg, opts);
if (!prompt) {
process.stderr.write(t("chat.error.empty_prompt") + "\n");
process.exit(2);
}
const messages = [];
if (opts.system) messages.push({ role: "system", content: opts.system });
messages.push({ role: "user", content: prompt });
const body = {
model: opts.model,
messages,
...(opts.maxTokens && { max_tokens: opts.maxTokens }),
...(opts.temperature != null && { temperature: opts.temperature }),
...(opts.topP != null && { top_p: opts.topP }),
...(opts.reasoningEffort && { reasoning_effort: opts.reasoningEffort }),
...(opts.thinkingBudget && { thinking: { budget_tokens: opts.thinkingBudget } }),
...(opts.combo && { combo: opts.combo }),
stream: !!opts.stream,
};
const endpoint = opts.responsesApi ? "/v1/responses" : "/v1/chat/completions";
const startedAt = Date.now();
const response = await apiFetch(endpoint, {
method: "POST",
body,
acceptNotOk: true,
timeout: globalOpts.timeout,
});
if (!response.ok) {
const errText = await response.text().catch(() => "");
process.stderr.write(`\x1b[31m✖ ${response.status} ${response.statusText}\x1b[0m\n`);
if (errText) process.stderr.write(errText + "\n");
process.exit(1);
}
const latencyMs = Date.now() - startedAt;
if (opts.stream) {
return streamHandle(response, opts.responsesApi);
}
const data = await response.json();
const text = extractText(data, opts.responsesApi);
if (!opts.noHistory) {
appendHistory({ prompt, model: opts.model, latencyMs, usage: data.usage, response: text });
}
if (globalOpts.output === "json") {
emit(data, globalOpts);
} else if (globalOpts.output === "markdown") {
console.log(
`# Response\n\n${text}\n\n## Metadata\n- Model: ${data.model}\n- Latency: ${latencyMs}ms\n- Usage: ${JSON.stringify(data.usage)}\n`
);
} else {
console.log(text);
if (!globalOpts.quiet) {
process.stderr.write(
`\n[${data.model} · ${latencyMs}ms · ${data.usage?.total_tokens ?? "?"} tok]\n`
);
}
}
}
async function resolvePrompt(arg, opts) {
if (opts.file) return readFileSync(opts.file, "utf8").trim();
if (opts.stdin) return readStdin();
return arg?.trim() || "";
}
function readStdin() {
return new Promise((resolve) => {
let buf = "";
process.stdin.setEncoding("utf8");
process.stdin.on("data", (c) => (buf += c));
process.stdin.on("end", () => resolve(buf.trim()));
});
}
function extractText(data, isResponses) {
if (isResponses) {
return data.output?.[0]?.content?.[0]?.text ?? data.output_text ?? "";
}
return data.choices?.[0]?.message?.content ?? "";
}
async function streamHandle(response, isResponses) {
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split("\n");
buffer = lines.pop() ?? "";
for (const line of lines) {
if (!line.startsWith("data: ")) continue;
const chunk = line.slice(6).trim();
if (chunk === "[DONE]") {
process.stdout.write("\n");
return;
}
try {
const obj = JSON.parse(chunk);
const content = isResponses ? obj.delta?.content : obj.choices?.[0]?.delta?.content;
if (content) process.stdout.write(content);
} catch {}
}
}
process.stdout.write("\n");
}
function appendHistory(entry) {
try {
appendFileSync(
resolveHistoryPath(),
JSON.stringify({ ts: new Date().toISOString(), ...entry }) + "\n"
);
} catch {
// history write failures are non-fatal
}
}
+233
View File
@@ -0,0 +1,233 @@
import { readFileSync } from "node:fs";
import { apiFetch } from "../api.mjs";
import { emit } from "../output.mjs";
import { t } from "../i18n.mjs";
const AGENTS = ["codex", "devin", "jules"];
function truncate(v, len = 35) {
if (v == null) return "-";
const s = String(v);
return s.length > len ? s.slice(0, len - 1) + "…" : s;
}
function fmtTs(v) {
if (!v) return "-";
try {
return new Date(v).toLocaleString();
} catch {
return String(v);
}
}
function fmtStatus(v) {
if (!v) return "-";
const colors = {
running: "\x1b[33m",
completed: "\x1b[32m",
failed: "\x1b[31m",
cancelled: "\x1b[90m",
};
const c = colors[v] ?? "";
return `${c}${v}\x1b[0m`;
}
const taskSchema = [
{ key: "id", header: "Task ID", width: 22 },
{ key: "agent", header: "Agent", width: 8 },
{ key: "status", header: "Status", width: 14, formatter: fmtStatus },
{ key: "title", header: "Title", width: 35, formatter: truncate },
{ key: "createdAt", header: "Created", formatter: fmtTs },
{ key: "updatedAt", header: "Updated", formatter: fmtTs },
];
async function confirm(q) {
return new Promise((resolve) => {
process.stdout.write(`${q} (yes/no) `);
process.stdin.setEncoding("utf8");
process.stdin.once("data", (c) => resolve(c.toString().trim().toLowerCase().startsWith("y")));
});
}
function registerTaskCommands(parent, agent) {
const task = parent.command("task").description(t("cloud.task.description"));
task
.command("create")
.description(t("cloud.task.create.description"))
.option("--title <t>", t("cloud.task.create.title"))
.option("--prompt <p>", t("cloud.task.create.prompt"))
.option("--prompt-file <path>", t("cloud.task.create.prompt_file"))
.option("--repo <url>", t("cloud.task.create.repo"))
.option("--branch <b>", t("cloud.task.create.branch"))
.option("--metadata <json>", t("cloud.task.create.metadata"))
.action(async (opts, cmd) => {
const prompt =
opts.prompt ?? (opts.promptFile ? readFileSync(opts.promptFile, "utf8") : null);
if (!prompt) {
process.stderr.write("--prompt or --prompt-file required\n");
process.exit(2);
}
const body = {
agent,
title: opts.title ?? prompt.slice(0, 80),
prompt,
...(opts.repo ? { repo: opts.repo } : {}),
...(opts.branch ? { branch: opts.branch } : {}),
...(opts.metadata ? { metadata: JSON.parse(opts.metadata) } : {}),
};
const res = await apiFetch("/api/v1/agents/tasks", { method: "POST", body });
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
emit(await res.json(), cmd.optsWithGlobals());
});
task
.command("list")
.description(t("cloud.task.list.description"))
.option("--status <s>", t("cloud.task.list.status"))
.option("--limit <n>", t("cloud.task.list.limit"), parseInt, 50)
.action(async (opts, cmd) => {
const params = new URLSearchParams({ agent, limit: String(opts.limit ?? 50) });
if (opts.status) params.set("status", opts.status);
const res = await apiFetch(`/api/v1/agents/tasks?${params}`);
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
const data = await res.json();
emit(data.items ?? data, cmd.optsWithGlobals(), taskSchema);
});
task
.command("get <taskId>")
.description(t("cloud.task.get.description"))
.action(async (taskId, opts, cmd) => {
const res = await apiFetch(`/api/v1/agents/tasks/${taskId}`);
if (!res.ok) {
process.stderr.write(`Not found: ${taskId}\n`);
process.exit(1);
}
emit(await res.json(), cmd.optsWithGlobals());
});
task
.command("status <taskId>")
.description(t("cloud.task.status.description"))
.action(async (taskId, opts, cmd) => {
const res = await apiFetch(`/api/v1/agents/tasks/${taskId}`);
if (!res.ok) {
process.stderr.write(`Not found: ${taskId}\n`);
process.exit(1);
}
const data = await res.json();
const globalOpts = cmd.optsWithGlobals();
if (globalOpts.output === "json") {
emit({ status: data.status }, globalOpts);
} else {
process.stdout.write(`${data.status}\n`);
}
});
task
.command("cancel <taskId>")
.description(t("cloud.task.cancel.description"))
.option("--yes", t("cloud.task.cancel.yes"))
.action(async (taskId, opts, cmd) => {
if (!opts.yes) {
const ok = await confirm(`Cancel task ${taskId}?`);
if (!ok) return;
}
const res = await apiFetch(`/api/v1/agents/tasks/${taskId}`, {
method: "POST",
body: { op: "cancel" },
});
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
process.stdout.write("Cancelled\n");
});
task
.command("approve <taskId>")
.description(t("cloud.task.approve.description"))
.action(async (taskId, opts, cmd) => {
const res = await apiFetch(`/api/v1/agents/tasks/${taskId}`, {
method: "POST",
body: { op: "approve_plan" },
});
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
process.stdout.write("Plan approved\n");
});
task
.command("message <taskId> <message>")
.description(t("cloud.task.message.description"))
.action(async (taskId, msg, opts, cmd) => {
const res = await apiFetch(`/api/v1/agents/tasks/${taskId}`, {
method: "POST",
body: { op: "message", message: msg },
});
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
process.stdout.write("Message sent\n");
});
parent
.command("sources <taskId>")
.description(t("cloud.sources.description"))
.action(async (taskId, opts, cmd) => {
const res = await apiFetch(`/api/v1/agents/tasks/${taskId}?op=sources`);
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
const data = await res.json();
emit(data.sources ?? data, cmd.optsWithGlobals());
});
}
export function registerCloud(program) {
const cloud = program.command("cloud").description(t("cloud.description"));
cloud
.command("agents")
.description(t("cloud.agents.description"))
.action(async (opts, cmd) => {
const res = await apiFetch("/api/v1/agents/tasks?meta=agents");
if (res.ok) {
const data = await res.json();
emit(data.agents ?? AGENTS.map((id) => ({ id })), cmd.optsWithGlobals());
} else {
emit(
AGENTS.map((id) => ({ id })),
cmd.optsWithGlobals()
);
}
});
for (const agent of AGENTS) {
const agentCmd = cloud
.command(agent)
.description(t("cloud.agent.description").replace("{agent}", agent));
registerTaskCommands(agentCmd, agent);
agentCmd
.command("auth")
.description(t("cloud.agent.auth.description"))
.option("--no-browser", "Skip browser open")
.option("--timeout <ms>", "Auth timeout ms", parseInt, 300000)
.action(async (opts, cmd) => {
const { runOAuthStart } = await import("./oauth.mjs");
await runOAuthStart({ provider: agent, ...opts }, cmd);
});
}
}
+361
View File
@@ -0,0 +1,361 @@
import { Option } from "commander";
import { printHeading } from "../io.mjs";
import { withRuntime } from "../runtime.mjs";
import { t } from "../i18n.mjs";
import { apiFetch } from "../api.mjs";
import { emit } from "../output.mjs";
const VALID_STRATEGIES = [
"priority",
"weighted",
"round-robin",
"p2c",
"random",
"auto",
"lkgp",
"context-optimized",
"context-relay",
"fill-first",
"cost-optimized",
"least-used",
"strict-random",
"reset-aware",
];
const suggestSchema = [
{ key: "rank", header: "#" },
{ key: "name", header: "Combo", width: 24 },
{ key: "strategy", header: "Strategy", width: 16 },
{ key: "score", header: "Score", formatter: (v) => (v != null ? v.toFixed(3) : "-") },
{ key: "latencyP50Ms", header: "Latency P50", formatter: (v) => (v != null ? `${v}ms` : "-") },
{ key: "costPer1k", header: "Cost/1k", formatter: (v) => (v != null ? `$${v.toFixed(5)}` : "-") },
{
key: "rationale",
header: "Rationale",
width: 40,
formatter: (v) => {
if (!v) return "-";
const s = String(v);
return s.length > 40 ? s.slice(0, 39) + "…" : s;
},
},
];
export function extendComboSuggest(combo) {
combo
.command("suggest")
.description(t("combo.suggest.description"))
.requiredOption("--task <description>", t("combo.suggest.task"))
.option("--max-cost <usd>", t("combo.suggest.maxCost"), parseFloat)
.option("--max-latency-ms <ms>", t("combo.suggest.maxLatencyMs"), parseInt)
.option("--weights <json>", t("combo.suggest.weights"))
.option("--top <n>", t("combo.suggest.top"), parseInt, 5)
.option("--explain", t("combo.suggest.explain"))
.option("--switch", t("combo.suggest.switch"))
.action(async (opts, cmd) => {
const body = {
task: opts.task,
constraints: {
maxCostUsd: opts.maxCost,
maxLatencyMs: opts.maxLatencyMs,
},
weights: opts.weights ? JSON.parse(opts.weights) : undefined,
top: opts.top,
};
const res = await apiFetch("/api/mcp/tools/call", {
method: "POST",
body: { name: "omniroute_best_combo_for_task", arguments: body },
});
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
const data = await res.json();
const candidates = data.candidates ?? data;
const rows = (Array.isArray(candidates) ? candidates : []).map((c, i) => ({
rank: i + 1,
...c,
}));
emit(rows, cmd.optsWithGlobals(), suggestSchema);
if (opts.explain && !cmd.optsWithGlobals().quiet) {
process.stderr.write(`\nRationale:\n${data.rationale ?? "(no rationale)"}\n`);
}
if (opts.switch && rows[0]) {
const best = rows[0].name;
const switchRes = await apiFetch("/api/combos/switch", {
method: "POST",
body: { name: best },
});
if (!switchRes.ok) {
process.stderr.write(`Switch failed: ${switchRes.status}\n`);
process.exit(1);
}
process.stderr.write(`\nSwitched to: ${best}\n`);
}
});
}
export function registerCombo(program) {
const combo = program.command("combo").description(t("combo.title"));
combo
.command("list")
.description("List configured routing combos")
.option("--json", "Output as JSON")
.action(async (opts, cmd) => {
const globalOpts = cmd.parent.optsWithGlobals();
const exitCode = await runComboListCommand({ ...opts, output: globalOpts.output });
if (exitCode !== 0) process.exit(exitCode);
});
combo
.command("switch <name>")
.description("Activate a routing combo")
.action(async (name, opts, cmd) => {
const globalOpts = cmd.parent.optsWithGlobals();
const exitCode = await runComboSwitchCommand(name, { ...opts, output: globalOpts.output });
if (exitCode !== 0) process.exit(exitCode);
});
combo
.command("create <name>")
.description("Create a new routing combo")
.addOption(
new Option("--strategy <strategy>", "Routing strategy")
.choices(VALID_STRATEGIES)
.default("priority")
)
.action(async (name, opts, cmd) => {
const globalOpts = cmd.parent.optsWithGlobals();
const exitCode = await runComboCreateCommand(name, opts.strategy, {
...opts,
output: globalOpts.output,
});
if (exitCode !== 0) process.exit(exitCode);
});
combo
.command("delete <name>")
.description("Delete a routing combo")
.option("--yes", "Skip confirmation")
.action(async (name, opts, cmd) => {
const globalOpts = cmd.parent.optsWithGlobals();
const exitCode = await runComboDeleteCommand(name, { ...opts, output: globalOpts.output });
if (exitCode !== 0) process.exit(exitCode);
});
extendComboSuggest(combo);
}
export async function runComboListCommand(opts = {}) {
try {
return await withRuntime(async ({ kind, api, db }) => {
let combos = [];
let activeCombo = null;
if (kind === "http") {
const [listRes, activeRes] = await Promise.all([
api("/api/combos", { retry: false, timeout: 5000, acceptNotOk: true }),
api("/api/settings", { retry: false, timeout: 3000, acceptNotOk: true }),
]);
if (listRes.ok) {
const data = await listRes.json();
combos = Array.isArray(data) ? data : (data.combos ?? []);
}
if (activeRes.ok) {
const settings = await activeRes.json();
activeCombo = settings?.activeCombo ?? null;
}
} else {
combos = await db.combos.getCombos();
}
if (opts.json || opts.output === "json") {
console.log(JSON.stringify({ combos, active: activeCombo }, null, 2));
return 0;
}
printHeading(t("combo.title"));
if (combos.length === 0) {
console.log(t("combo.noCombos"));
return 0;
}
for (const combo of combos) {
const comboName = combo.name ?? combo.id ?? "?";
const isActive = activeCombo && (comboName === activeCombo || combo.id === activeCombo);
const icon = isActive ? "\x1b[32m●\x1b[0m" : "\x1b[2m○\x1b[0m";
const enabled = combo.enabled !== false;
const status = enabled ? "\x1b[32menabled\x1b[0m" : "\x1b[31mdisabled\x1b[0m";
const strategy = (combo.strategy ?? "priority").padEnd(12);
console.log(` ${icon} ${comboName.padEnd(25)} [${strategy}] ${status}`);
}
return 0;
});
} catch (err) {
console.error(t("common.error", { message: err instanceof Error ? err.message : String(err) }));
return 1;
}
}
export async function runComboSwitchCommand(name, opts = {}) {
if (!name) {
console.error("Combo name is required.");
return 1;
}
try {
return await withRuntime(async ({ kind, api, db }) => {
if (kind === "http") {
const listRes = await api("/api/combos", {
retry: false,
timeout: 5000,
acceptNotOk: true,
});
if (!listRes.ok) {
console.error(`Failed to fetch combo list (HTTP ${listRes.status}).`);
return 1;
}
const data = await listRes.json();
const combos = Array.isArray(data) ? data : (data.combos ?? []);
const found = combos.find((c) => c.name === name || c.id === name);
if (!found) {
console.error(`Combo '${name}' not found.`);
return 1;
}
const patchRes = await api("/api/settings", {
method: "PATCH",
body: { activeCombo: name },
retry: false,
acceptNotOk: true,
});
if (!patchRes.ok) {
console.error(`Failed to switch combo (HTTP ${patchRes.status}).`);
return 1;
}
} else {
const combo = await db.combos.getComboByName(name);
if (!combo) {
console.error(`Combo '${name}' not found.`);
return 1;
}
db.combos.setActiveCombo(name);
}
console.log(t("combo.switched", { name }));
return 0;
});
} catch (err) {
console.error(t("common.error", { message: err instanceof Error ? err.message : String(err) }));
return 1;
}
}
export async function runComboCreateCommand(name, strategy = "priority", opts = {}) {
if (!name) {
console.error("Combo name is required.");
return 1;
}
if (!VALID_STRATEGIES.includes(strategy)) {
console.error(`Invalid strategy '${strategy}'. Valid: ${VALID_STRATEGIES.join(", ")}`);
return 1;
}
try {
return await withRuntime(async ({ kind, api, db }) => {
if (kind === "http") {
const res = await api("/api/combos", {
method: "POST",
body: { name, strategy, enabled: true, models: [], config: {} },
retry: false,
acceptNotOk: true,
});
if (!res.ok) {
const body = await res.text().catch(() => "");
const msg = body ? `${body}` : "";
console.error(`Failed to create combo (HTTP ${res.status})${msg}`);
return 1;
}
} else {
const existing = await db.combos.getComboByName(name);
if (existing) {
console.error(`Combo '${name}' already exists. Delete it first.`);
return 1;
}
await db.combos.createCombo({ name, strategy, enabled: true, models: [], config: {} });
}
console.log(t("combo.created", { name }));
return 0;
});
} catch (err) {
console.error(t("common.error", { message: err instanceof Error ? err.message : String(err) }));
return 1;
}
}
export async function runComboDeleteCommand(name, opts = {}) {
if (!name) {
console.error("Combo name is required.");
return 1;
}
if (!opts.yes) {
const readline = await import("node:readline");
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
const answer = await new Promise((resolve) =>
rl.question(t("combo.confirmDelete", { name }) + " [y/N] ", resolve)
);
rl.close();
if (!/^y(es)?$/i.test(answer)) {
console.log(t("common.cancelled"));
return 0;
}
}
try {
return await withRuntime(async ({ kind, api, db }) => {
if (kind === "http") {
const listRes = await api("/api/combos", {
retry: false,
timeout: 5000,
acceptNotOk: true,
});
if (!listRes.ok) {
console.error(`Failed to fetch combo list (HTTP ${listRes.status}).`);
return 1;
}
const data = await listRes.json();
const combos = Array.isArray(data) ? data : (data.combos ?? []);
const found = combos.find((c) => c.name === name || c.id === name);
if (!found) {
console.error(`Combo '${name}' not found.`);
return 1;
}
const delRes = await api(`/api/combos/${encodeURIComponent(found.id)}`, {
method: "DELETE",
retry: false,
acceptNotOk: true,
});
if (!delRes.ok) {
console.error(`Failed to delete combo (HTTP ${delRes.status}).`);
return 1;
}
} else {
const deleted = await db.combos.deleteComboByName(name);
if (!deleted) {
console.error(`Combo '${name}' not found.`);
return 1;
}
}
console.log(t("combo.deleted", { name }));
return 0;
});
} catch (err) {
console.error(t("common.error", { message: err instanceof Error ? err.message : String(err) }));
return 1;
}
}
+346
View File
@@ -0,0 +1,346 @@
import { existsSync, writeFileSync, readFileSync, mkdirSync } from "node:fs";
import { join, dirname } from "node:path";
import { homedir } from "node:os";
import { t } from "../i18n.mjs";
import { apiFetch } from "../api.mjs";
import { resolveDataDir } from "../data-dir.mjs";
const CACHE_TTL_MS = 60 * 60 * 1000; // 1h
function cachePath() {
return join(resolveDataDir(), "completion-cache.json");
}
function readCache() {
try {
const raw = JSON.parse(readFileSync(cachePath(), "utf8"));
if (raw && typeof raw.ts === "number" && Date.now() - raw.ts < CACHE_TTL_MS) return raw;
} catch {}
return null;
}
async function refreshCache(opts = {}) {
let combos = [],
providers = [],
models = [];
try {
const [cr, pr, mr] = await Promise.allSettled([
apiFetch("/api/combos", opts),
apiFetch("/api/providers", opts),
apiFetch("/api/models", opts),
]);
if (cr.status === "fulfilled" && cr.value.ok) {
const j = await cr.value.json();
combos = (j.combos || j.items || []).map((c) => c.name || c.id).filter(Boolean);
}
if (pr.status === "fulfilled" && pr.value.ok) {
const j = await pr.value.json();
providers = (j.providers || j.items || []).map((p) => p.id || p.name).filter(Boolean);
}
if (mr.status === "fulfilled" && mr.value.ok) {
const j = await mr.value.json();
models = (Array.isArray(j) ? j : j.data || []).map((m) => m.id).filter(Boolean);
}
} catch {}
const data = { combos, providers, models, ts: Date.now() };
try {
mkdirSync(dirname(cachePath()), { recursive: true });
writeFileSync(cachePath(), JSON.stringify(data));
} catch {}
return data;
}
function detectShell() {
const shell = process.env.SHELL || "";
if (shell.includes("zsh")) return "zsh";
if (shell.includes("fish")) return "fish";
return "bash";
}
function installPath(shell) {
const home = homedir();
if (shell === "zsh") return join(home, ".zsh", "completions", "_omniroute");
if (shell === "fish") return join(home, ".config", "fish", "completions", "omniroute.fish");
return join(home, ".bash_completion.d", "omniroute");
}
function generateZshScript() {
return `#compdef omniroute
# OmniRoute zsh completion (dynamic)
_omniroute_get_cache() {
local key="$1"
local cache="$HOME/.omniroute/completion-cache.json"
local now=$(date +%s 2>/dev/null || echo 0)
local mtime=0
if [[ -f "$cache" ]]; then
mtime=$(stat -c %Y "$cache" 2>/dev/null || stat -f %m "$cache" 2>/dev/null || echo 0)
fi
if [[ $((now - mtime)) -gt 3600 ]]; then
omniroute completion refresh --quiet >/dev/null 2>&1
fi
if command -v python3 &>/dev/null && [[ -f "$cache" ]]; then
python3 -c "import json,sys;d=json.load(open('$cache'));print(' '.join(d.get('$key',[])))" 2>/dev/null
fi
}
_omniroute() {
local -a commands
commands=(
'serve:Start the OmniRoute server'
'stop:Stop the server'
'restart:Restart the server'
'setup:Configure OmniRoute'
'doctor:Run health diagnostics'
'status:Show server status'
'logs:View application logs'
'providers:Manage providers'
'config:Manage config and contexts'
'keys:Manage API keys'
'models:Browse available models'
'combo:Manage routing combos'
'chat:Send chat completion'
'stream:Stream chat completion'
'dashboard:Open dashboard'
'open:Open UI resource in browser'
'backup:Create a backup'
'restore:Restore from backup'
'health:Show server health'
'quota:Show provider quotas'
'cache:Manage response cache'
'mcp:MCP server management'
'a2a:A2A server management'
'tunnel:Tunnel management'
'env:Environment variables'
'test:Test provider connection'
'update:Check for updates'
'completion:Shell completion'
'memory:Manage memory store'
'skills:Manage skills'
)
_arguments -C \\
'1: :->command' \\
'*:: :->arg' && return 0
case $state in
command) _describe 'command' commands ;;
arg)
case $words[1] in
combo)
case $words[2] in
switch|delete|show)
local -a combos
combos=($(_omniroute_get_cache combos))
_describe 'combo' combos ;;
*) _arguments '1:subcommand:(list switch create delete show suggest)' ;;
esac ;;
providers|keys)
case $words[2] in
add|remove|test)
local -a providers
providers=($(_omniroute_get_cache providers))
_describe 'provider' providers ;;
*) _arguments '1:subcommand:(list add remove test)' ;;
esac ;;
chat|stream)
_arguments \\
'--model[Model ID]:model:->models' \\
'--combo[Combo name]:combo:->combos' \\
'--system[System prompt]:' \\
'--max-tokens[Max tokens]:' ;;
open)
_arguments '1:resource:(combos providers api-manager cli-tools agents settings logs memory skills evals audit cost resilience)' ;;
completion) _arguments '1:subcommand:(zsh bash fish install refresh)' ;;
config) _arguments '1:subcommand:(list get set validate contexts)' ;;
*) ;;
esac
case $state in
models)
local -a models
models=($(_omniroute_get_cache models))
_describe 'model' models ;;
combos)
local -a combos
combos=($(_omniroute_get_cache combos))
_describe 'combo' combos ;;
esac ;;
esac
}
compdef _omniroute omniroute
`;
}
function generateBashScript() {
return `#!/bin/bash
# OmniRoute CLI bash completion (dynamic)
_omniroute_get_cache() {
local key="$1"
local cache="$HOME/.omniroute/completion-cache.json"
local now
now=$(date +%s 2>/dev/null || echo 0)
local mtime=0
[[ -f "$cache" ]] && mtime=$(stat -c %Y "$cache" 2>/dev/null || stat -f %m "$cache" 2>/dev/null || echo 0)
if (( now - mtime > 3600 )); then
omniroute completion refresh --quiet >/dev/null 2>&1
fi
if command -v python3 &>/dev/null && [[ -f "$cache" ]]; then
python3 -c "import json,sys;d=json.load(open('$cache'));print(' '.join(d.get('$key',[])))" 2>/dev/null
fi
}
_omniroute() {
local cur prev cmds
COMPREPLY=()
cur="\${COMP_WORDS[COMP_CWORD]}"
prev="\${COMP_WORDS[COMP_CWORD-1]}"
cmds="setup doctor status logs providers config test update serve stop restart keys models combo chat stream completion dashboard open backup restore health quota cache mcp a2a tunnel env memory skills"
case "\${prev}" in
combo) COMPREPLY=($(compgen -W "list switch create delete show suggest" -- "\${cur}")); return 0 ;;
keys) COMPREPLY=($(compgen -W "add list remove regenerate revoke reveal usage" -- "\${cur}")); return 0 ;;
providers) COMPREPLY=($(compgen -W "available list test test-all" -- "\${cur}")); return 0 ;;
config) COMPREPLY=($(compgen -W "list get set validate contexts" -- "\${cur}")); return 0 ;;
completion) COMPREPLY=($(compgen -W "zsh bash fish install refresh" -- "\${cur}")); return 0 ;;
open) COMPREPLY=($(compgen -W "combos providers api-manager cli-tools agents settings logs memory skills evals audit cost resilience" -- "\${cur}")); return 0 ;;
--model)
local models
models=$(_omniroute_get_cache models)
COMPREPLY=($(compgen -W "\${models}" -- "\${cur}")); return 0 ;;
--combo)
local combos
combos=$(_omniroute_get_cache combos)
COMPREPLY=($(compgen -W "\${combos}" -- "\${cur}")); return 0 ;;
switch|delete)
local combos
combos=$(_omniroute_get_cache combos)
COMPREPLY=($(compgen -W "\${combos}" -- "\${cur}")); return 0 ;;
*)
COMPREPLY=($(compgen -W "\${cmds} --help --version --output --quiet" -- "\${cur}")); return 0 ;;
esac
}
complete -F _omniroute omniroute
`;
}
function generateFishScript() {
return `# OmniRoute CLI fish completion (dynamic)
complete -c omniroute -f
set -l commands serve stop restart setup doctor status logs providers config keys models combo chat stream completion dashboard open backup restore health quota cache mcp a2a tunnel env memory skills update test
for cmd in $commands
complete -c omniroute -n '__fish_is_nth_token 1' -a $cmd
end
# Subcommands
complete -c omniroute -n '__fish_seen_subcommand_from combo' -a 'list switch create delete show suggest'
complete -c omniroute -n '__fish_seen_subcommand_from keys' -a 'add list remove regenerate revoke reveal usage'
complete -c omniroute -n '__fish_seen_subcommand_from providers' -a 'available list test test-all'
complete -c omniroute -n '__fish_seen_subcommand_from config' -a 'list get set validate contexts'
complete -c omniroute -n '__fish_seen_subcommand_from completion' -a 'zsh bash fish install refresh'
complete -c omniroute -n '__fish_seen_subcommand_from open' -a 'combos providers api-manager cli-tools agents settings logs memory skills evals audit cost resilience'
# Dynamic completions from cache (requires python3)
function __omniroute_cache_get
set -l key $argv[1]
set -l cache "$HOME/.omniroute/completion-cache.json"
set -l now (date +%s 2>/dev/null; or echo 0)
set -l mtime 0
test -f $cache; and set mtime (stat -c %Y $cache 2>/dev/null; or stat -f %m $cache 2>/dev/null; or echo 0)
if test (math $now - $mtime) -gt 3600
omniroute completion refresh --quiet >/dev/null 2>&1
end
if command -q python3; and test -f $cache
python3 -c "import json,sys;d=json.load(open('$cache'));print('\\n'.join(d.get('$key',[])))" 2>/dev/null
end
end
complete -c omniroute -n '__fish_seen_subcommand_from combo; and __fish_seen_subcommand_from switch delete' -a '(__omniroute_cache_get combos)'
complete -c omniroute -l model -a '(__omniroute_cache_get models)'
complete -c omniroute -l combo -a '(__omniroute_cache_get combos)'
`;
}
const generators = { zsh: generateZshScript, bash: generateBashScript, fish: generateFishScript };
export function registerCompletion(program) {
const comp = program
.command("completion")
.description(t("completion.description") || "Generate or install shell completion scripts");
comp
.command("zsh")
.description(t("completion.zsh") || "Print zsh completion script")
.action(async () => process.stdout.write(generateZshScript()));
comp
.command("bash")
.description(t("completion.bash") || "Print bash completion script")
.action(async () => process.stdout.write(generateBashScript()));
comp
.command("fish")
.description(t("completion.fish") || "Print fish completion script")
.action(async () => process.stdout.write(generateFishScript()));
comp
.command("install [shell]")
.description(t("completion.install") || "Install completion script globally for detected shell")
.action(async (shell, opts, cmd) => {
const target = shell || detectShell();
const gen = generators[target];
if (!gen) {
process.stderr.write(`Unknown shell: ${target}. Valid: bash, zsh, fish\n`);
process.exit(2);
}
const dest = installPath(target);
mkdirSync(dirname(dest), { recursive: true });
writeFileSync(dest, gen());
process.stdout.write(
`Installed ${target} completion at ${dest}\nRestart your shell or source the file.\n`
);
});
comp
.command("refresh")
.description(t("completion.refresh") || "Refresh cache of combos/providers/models")
.option("--quiet", "Suppress output")
.action(async (opts, cmd) => {
const globalOpts = cmd.optsWithGlobals();
const data = await refreshCache(globalOpts);
if (!opts.quiet && !globalOpts.quiet) {
process.stdout.write(
`Cached: ${data.combos.length} combos, ${data.providers.length} providers, ${data.models.length} models\n`
);
}
});
// Backward-compat: `omniroute completion <shell>` (positional arg form)
comp
.command("<shell>")
.description("Print completion script for shell (bash, zsh, fish)")
.allowUnknownOption(false)
.action(async (shell) => {
const gen = generators[shell];
if (!gen) {
process.stderr.write(`Unknown shell: ${shell}. Valid: bash, zsh, fish\n`);
process.exit(1);
}
process.stdout.write(gen());
});
}
// Legacy export for backward compatibility
export async function runCompletionCommand(shell) {
const gen = generators[shell];
if (!gen) {
process.stderr.write(`Unknown shell: ${shell}. Valid: bash, zsh, fish\n`);
return 1;
}
process.stdout.write(gen());
return 0;
}
+247
View File
@@ -0,0 +1,247 @@
import { readFileSync } from "node:fs";
import { apiFetch } from "../api.mjs";
import { emit } from "../output.mjs";
import { t } from "../i18n.mjs";
// #2688 — CLI no longer assumes MCP is enabled. Engine names are normalized
// to the current core set; legacy aliases continue to work.
const VALID_ENGINES = ["off", "caveman", "rtk", "stacked"];
const ENGINE_ALIASES = { none: "off", hybrid: "stacked" };
function normalizeEngine(name) {
return ENGINE_ALIASES[name] ?? name;
}
// Direct REST fallbacks used when the MCP tool surface is not mounted (404).
// Keeps every subcommand working on minimal builds.
async function restCompressionStatus() {
const [settingsRes, combosRes, analyticsRes] = await Promise.all([
apiFetch("/api/settings/compression"),
apiFetch("/api/context/combos"),
apiFetch("/api/context/analytics?period=7d").catch(() => null),
]);
const settings = settingsRes.ok ? await settingsRes.json() : {};
const combosBody = combosRes.ok ? await combosRes.json() : { combos: [] };
const analytics = analyticsRes && analyticsRes.ok ? await analyticsRes.json() : null;
return {
engine: settings.engine ?? null,
settings,
combos: combosBody.combos ?? combosBody,
analytics,
};
}
async function restCompressionConfigure(config) {
const body = { ...config };
if (body.engine) body.engine = normalizeEngine(body.engine);
const res = await apiFetch("/api/settings/compression", { method: "PUT", body });
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
return res.json();
}
async function restSetEngine(name) {
const res = await apiFetch("/api/settings/compression", {
method: "PUT",
body: { engine: normalizeEngine(name) },
});
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
return res.json();
}
async function restListCombos() {
const res = await apiFetch("/api/context/combos");
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
const body = await res.json();
return body.combos ?? body;
}
async function restComboStats(period) {
const res = await apiFetch(`/api/context/analytics?period=${encodeURIComponent(period ?? "7d")}`);
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
return res.json();
}
async function mcpCall(name, args, restFallback) {
const res = await apiFetch("/api/mcp/tools/call", {
method: "POST",
body: { name, arguments: args },
});
if (res.ok) return res.json();
// 404 = MCP tool surface not mounted on this build; 501 = not implemented.
// Anything else is a genuine error and we surface it.
if ((res.status === 404 || res.status === 501) && typeof restFallback === "function") {
return restFallback();
}
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
async function confirm(q) {
return new Promise((resolve) => {
process.stdout.write(`${q} (yes/no) `);
process.stdin.setEncoding("utf8");
process.stdin.once("data", (c) => resolve(c.toString().trim().toLowerCase().startsWith("y")));
});
}
export async function runCompressionStatus(opts, cmd) {
const data = await mcpCall("omniroute_compression_status", {}, restCompressionStatus);
emit(data, cmd.optsWithGlobals());
}
export async function runCompressionConfigure(opts, cmd) {
const config = {};
if (opts.engine) config.engine = opts.engine;
if (opts.cavemanAggressiveness !== undefined)
config.caveman = { aggressiveness: opts.cavemanAggressiveness };
if (opts.rtkBudget !== undefined) config.rtk = { tokenBudget: opts.rtkBudget };
if (opts.languagePack) config.languagePack = opts.languagePack;
const data = await mcpCall("omniroute_compression_configure", config, () =>
restCompressionConfigure(config)
);
emit(data, cmd.optsWithGlobals());
}
export async function runCompressionEngineSet(name, opts, cmd) {
const normalized = normalizeEngine(name);
if (!VALID_ENGINES.includes(normalized)) {
process.stderr.write(`Unknown engine: ${name}. Valid: ${VALID_ENGINES.join(", ")}\n`);
process.exit(2);
}
await mcpCall("omniroute_set_compression_engine", { engine: normalized }, () =>
restSetEngine(normalized)
);
process.stdout.write(`Engine: ${normalized}\n`);
}
export async function runCompressionPreview(opts, cmd) {
const body = JSON.parse(readFileSync(opts.file, "utf8"));
const res = await apiFetch("/api/compression/preview", { method: "POST", body });
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
const data = await res.json();
emit(data, cmd.optsWithGlobals());
if (cmd.optsWithGlobals().output !== "json") {
process.stderr.write(
`\nOriginal: ${data.beforeTokens ?? "?"} tok → After: ${data.afterTokens ?? "?"} tok (${data.savingsPct ?? "?"}%)\n`
);
}
}
export function registerCompression(program) {
const cmp = program.command("compression").description(t("compression.description"));
cmp
.command("status")
.description(t("compression.status.description"))
.action(runCompressionStatus);
cmp
.command("configure")
.description(t("compression.configure.description"))
.option("--engine <e>", t("compression.configure.engine"))
.option("--caveman-aggressiveness <n>", t("compression.configure.caveman_agg"), parseFloat)
.option("--rtk-budget <n>", t("compression.configure.rtk_budget"), parseInt)
.option("--language-pack <p>", t("compression.configure.language_pack"))
.action(runCompressionConfigure);
const engine = cmp.command("engine").description(t("compression.engine.description"));
engine.command("set <name>").action(runCompressionEngineSet);
engine.command("get").action(async (opts, cmd) => {
const data = await mcpCall("omniroute_compression_status", {}, restCompressionStatus);
process.stdout.write(`${data.engine ?? "(default)"}\n`);
});
const combos = cmp.command("combos").description(t("compression.combos.description"));
combos.command("list").action(async (opts, cmd) => {
const data = await mcpCall("omniroute_list_compression_combos", {}, async () => ({
combos: await restListCombos(),
}));
emit(data.combos ?? data, cmd.optsWithGlobals());
});
combos
.command("stats")
.option("--period <p>", null, "7d")
.action(async (opts, cmd) => {
const data = await mcpCall(
"omniroute_compression_combo_stats",
{ period: opts.period ?? "7d" },
() => restComboStats(opts.period)
);
emit(data, cmd.optsWithGlobals());
});
const rules = cmp.command("rules").description(t("compression.rules.description"));
rules.command("list").action(async (opts, cmd) => {
const res = await apiFetch("/api/compression/rules");
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
emit(await res.json(), cmd.optsWithGlobals());
});
rules
.command("add")
.requiredOption("--pattern <p>", t("compression.rules.add.pattern"))
.requiredOption("--action <a>", t("compression.rules.add.action"))
.option("--replacement <r>")
.action(async (opts, cmd) => {
const body = { pattern: opts.pattern, action: opts.action };
if (opts.replacement) body.replacement = opts.replacement;
const res = await apiFetch("/api/compression/rules", { method: "POST", body });
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
emit(await res.json(), cmd.optsWithGlobals());
});
rules
.command("remove <id>")
.option("--yes")
.action(async (id, opts, cmd) => {
if (!opts.yes) {
const ok = await confirm(`Remove rule ${id}?`);
if (!ok) return;
}
const res = await apiFetch(`/api/compression/rules?id=${encodeURIComponent(id)}`, {
method: "DELETE",
});
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
process.stdout.write("Removed\n");
});
cmp
.command("language-packs")
.description(t("compression.language_packs.description"))
.action(async (opts, cmd) => {
const res = await apiFetch("/api/compression/language-packs");
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
emit(await res.json(), cmd.optsWithGlobals());
});
cmp
.command("preview")
.description(t("compression.preview.description"))
.requiredOption("--file <path>", t("compression.preview.file"))
.action(runCompressionPreview);
}
+351
View File
@@ -0,0 +1,351 @@
import { printHeading, printInfo, printSuccess, printError } from "../io.mjs";
import { t } from "../i18n.mjs";
import path from "node:path";
import fs from "node:fs";
import { fileURLToPath } from "node:url";
import { resolveDataDir } from "../data-dir.mjs";
import { registerContexts } from "./contexts.mjs";
function ensureBackup(configPath) {
if (!fs.existsSync(configPath)) return;
const backupDir = path.join(path.dirname(configPath), ".omniroute.bak");
if (!fs.existsSync(backupDir)) fs.mkdirSync(backupDir, { recursive: true });
const backupPath = path.join(backupDir, path.basename(configPath) + ".bak");
fs.copyFileSync(configPath, backupPath);
return backupPath;
}
async function runConfigListCommand(opts = {}) {
const { detectAllTools } = await import("../../../src/lib/cli-helper/tool-detector.ts");
const tools = await detectAllTools();
if (opts.json) {
console.log(JSON.stringify(tools, null, 2));
} else {
printHeading("CLI Tool Configuration Status");
for (const t of tools) {
const status = t.configured
? "✓ Configured"
: t.installed
? "✗ Not configured"
: "✗ Not installed";
console.log(` ${t.name.padEnd(14)} ${status}`);
if (t.version) console.log(` version: ${t.version}`);
console.log(` config: ${t.configPath}`);
}
}
return 0;
}
async function runConfigGetCommand(toolId, opts = {}) {
if (!toolId) {
printError("Tool ID required. Usage: omniroute config get <tool>");
return 1;
}
const { detectTool } = await import("../../../src/lib/cli-helper/tool-detector.ts");
const tool = await detectTool(toolId);
if (!tool) {
printError(`Unknown tool: ${toolId}`);
return 1;
}
if (opts.json) {
console.log(JSON.stringify(tool, null, 2));
} else {
printHeading(`${tool.name} Configuration`);
console.log(` Installed: ${tool.installed ? "Yes" : "No"}`);
console.log(` Configured: ${tool.configured ? "Yes" : "No"}`);
console.log(` Config: ${tool.configPath}`);
if (tool.version) console.log(` Version: ${tool.version}`);
if (tool.configContents) {
console.log(`\n Contents:`);
console.log(tool.configContents);
}
}
return 0;
}
async function runConfigSetCommand(toolId, opts = {}) {
if (!toolId) {
printError("Tool ID required. Usage: omniroute config set <tool> [options]");
return 1;
}
const baseUrl = opts.baseUrl || "http://localhost:20128/v1";
const apiKey = opts.apiKey;
const model = opts.model;
if (!apiKey) {
printError("API key required. Use --api-key or set OMNIROUTE_API_KEY.");
return 1;
}
const { generateConfig } = await import("../../../src/lib/cli-helper/config-generator/index.js");
const result = await generateConfig(toolId, { baseUrl, apiKey, model });
if (!result.success) {
printError(result.error || "Failed to generate config");
return 1;
}
const nonInteractive = opts.nonInteractive || opts.yes;
if (!nonInteractive) {
console.log(`\n About to write config to: ${result.configPath}`);
console.log(` Content preview:\n`);
console.log(result.content);
console.log("");
const readline = await import("node:readline");
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
const answer = await new Promise((resolve) => rl.question("Proceed? [y/N] ", resolve));
rl.close();
if (!/^y(es)?$/i.test(answer)) {
console.log("Aborted.");
return 0;
}
}
const dir = path.dirname(result.configPath);
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
const backupPath = ensureBackup(result.configPath);
if (backupPath) printInfo(`Backup saved to: ${backupPath}`);
fs.writeFileSync(result.configPath, result.content, "utf-8");
printSuccess(`Config written to ${result.configPath}`);
return 0;
}
async function runConfigValidateCommand(toolId, opts = {}) {
if (!toolId) {
printError("Tool ID required. Usage: omniroute config validate <tool>");
return 1;
}
const baseUrl = opts.baseUrl || "http://localhost:20128/v1";
const apiKey = opts.apiKey || "test-key";
const model = opts.model;
const { generateConfig } = await import("../../../src/lib/cli-helper/config-generator/index.js");
const result = await generateConfig(toolId, { baseUrl, apiKey, model });
if (!result.success) {
printError(`Validation failed: ${result.error}`);
return 1;
}
printSuccess(`Config for ${toolId} is valid`);
if (opts.json) {
console.log(JSON.stringify({ valid: true, content: result.content }, null, 2));
}
return 0;
}
function loadI18nLocales() {
const cfgPath = path.join(
path.dirname(path.dirname(path.dirname(path.dirname(fileURLToPath(import.meta.url))))),
"config",
"i18n.json"
);
try {
return JSON.parse(fs.readFileSync(cfgPath, "utf8")).locales || [];
} catch {
return [];
}
}
function getCliEnvPath() {
return path.join(resolveDataDir(), ".env");
}
function upsertEnvLine(envPath, key, value) {
let content = "";
if (fs.existsSync(envPath)) content = fs.readFileSync(envPath, "utf8");
const lines = content.split("\n");
const idx = lines.findIndex((l) => l.trimStart().startsWith(`${key}=`));
const newLine = `${key}=${value}`;
if (idx >= 0) {
lines[idx] = newLine;
} else {
if (content && !content.endsWith("\n")) lines.push("");
lines.push(newLine);
}
const dir = path.dirname(envPath);
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
const tmp = `${envPath}.tmp`;
fs.writeFileSync(tmp, lines.join("\n"), "utf8");
fs.renameSync(tmp, envPath);
}
export async function runConfigLangGetCommand(opts = {}) {
const { getLocale } = await import("../i18n.mjs");
const code = getLocale();
const locales = loadI18nLocales();
const entry = locales.find((l) => l.code === code);
const name = entry ? entry.english : code;
if (opts.output === "json" || opts.json) {
console.log(JSON.stringify({ code, name }, null, 2));
} else {
console.log(t("config.lang.current", { code, name }));
}
return 0;
}
export async function runConfigLangSetCommand(code, opts = {}) {
if (!code) {
console.error(t("config.lang.noCode"));
return 1;
}
const locales = loadI18nLocales();
const entry = locales.find((l) => l.code === code);
if (!entry) {
console.error(t("config.lang.unknown", { code }));
return 1;
}
const { getLocale, setLocale } = await import("../i18n.mjs");
const current = getLocale();
if (current === code && !opts.force) {
console.log(t("config.lang.alreadySet", { code }));
return 0;
}
const envPath = getCliEnvPath();
upsertEnvLine(envPath, "OMNIROUTE_LANG", code);
setLocale(code);
console.log(t("config.lang.saved", { code, name: entry.english }));
console.log(t("config.lang.envHint", { code }));
return 0;
}
export async function runConfigLangListCommand(opts = {}) {
const { getLocale } = await import("../i18n.mjs");
const current = getLocale();
const locales = loadI18nLocales();
if (opts.output === "json" || opts.json) {
console.log(
JSON.stringify(
locales.map((l) => ({ ...l, active: l.code === current })),
null,
2
)
);
return 0;
}
console.log(`\n\x1b[1m\x1b[36m${t("config.lang.listTitle")}\x1b[0m\n`);
for (const loc of locales) {
const active = loc.code === current ? " \x1b[32m◀ active\x1b[0m" : "";
console.log(
` ${loc.flag} ${loc.code.padEnd(8)} ${loc.english.padEnd(28)} ${loc.native}${active}`
);
}
console.log("");
return 0;
}
export function registerConfig(program) {
const config = program.command("config").description("Show or update CLI tool configuration");
config
.command("list")
.description("List all CLI tools and config status")
.option("--json", "Output as JSON")
.action(async (opts, cmd) => {
const globalOpts = cmd.parent.optsWithGlobals();
const exitCode = await runConfigListCommand({ ...opts, output: globalOpts.output });
if (exitCode !== 0) process.exit(exitCode);
});
config
.command("get <tool>")
.description("Show current config for a tool")
.option("--json", "Output as JSON")
.action(async (tool, opts, cmd) => {
const globalOpts = cmd.parent.optsWithGlobals();
const exitCode = await runConfigGetCommand(tool, { ...opts, output: globalOpts.output });
if (exitCode !== 0) process.exit(exitCode);
});
config
.command("set <tool>")
.description("Write config for a tool")
.option("--model <model>", "Model identifier (where applicable)")
.option("--non-interactive", "Do not prompt for confirmation")
.option("--yes", "Skip confirmation prompt")
.action(async (tool, opts, cmd) => {
const globalOpts = cmd.parent.optsWithGlobals();
const exitCode = await runConfigSetCommand(tool, {
...opts,
apiKey: opts.apiKey || globalOpts.apiKey || process.env.OMNIROUTE_API_KEY,
baseUrl: opts.baseUrl || globalOpts.baseUrl || process.env.OMNIROUTE_BASE_URL,
output: globalOpts.output,
});
if (exitCode !== 0) process.exit(exitCode);
});
config
.command("validate <tool>")
.description("Validate config format without writing")
.option("--model <model>", "Model identifier (where applicable)")
.option("--json", "Output as JSON")
.action(async (tool, opts, cmd) => {
const globalOpts = cmd.parent.optsWithGlobals();
const exitCode = await runConfigValidateCommand(tool, {
...opts,
apiKey: opts.apiKey || globalOpts.apiKey || process.env.OMNIROUTE_API_KEY,
baseUrl: opts.baseUrl || globalOpts.baseUrl || process.env.OMNIROUTE_BASE_URL,
output: globalOpts.output,
});
if (exitCode !== 0) process.exit(exitCode);
});
// Convenience alias: `config opencode` → `config set opencode`
// Matches the documented CLI usage in docs/frameworks/OPENCODE.md.
config
.command("opencode")
.description("Generate OpenCode config (alias for 'config set opencode')")
.option("--model <model>", "Model identifier")
.option("--non-interactive", "Do not prompt for confirmation")
.option("--yes", "Skip confirmation prompt")
.action(async (opts, cmd) => {
const globalOpts = cmd.parent.optsWithGlobals();
const exitCode = await runConfigSetCommand("opencode", {
...opts,
apiKey: opts.apiKey || globalOpts.apiKey || process.env.OMNIROUTE_API_KEY,
baseUrl: opts.baseUrl || globalOpts.baseUrl || process.env.OMNIROUTE_BASE_URL,
output: globalOpts.output,
});
if (exitCode !== 0) process.exit(exitCode);
});
// lang subgroup
const lang = config.command("lang").description(t("config.lang.description"));
lang
.command("get")
.description(t("config.lang.getDescription"))
.option("--json", t("common.jsonOpt"))
.action(async (opts, cmd) => {
const globalOpts = cmd.parent.parent.optsWithGlobals();
const exitCode = await runConfigLangGetCommand({ ...opts, output: globalOpts.output });
if (exitCode !== 0) process.exit(exitCode);
});
lang
.command("set <code>")
.description(t("config.lang.setDescription"))
.option("--force", "Set even if already active")
.action(async (code, opts, cmd) => {
const exitCode = await runConfigLangSetCommand(code, opts);
if (exitCode !== 0) process.exit(exitCode);
});
lang
.command("list")
.description(t("config.lang.listDescription"))
.option("--json", t("common.jsonOpt"))
.action(async (opts, cmd) => {
const globalOpts = cmd.parent.parent.optsWithGlobals();
const exitCode = await runConfigLangListCommand({ ...opts, output: globalOpts.output });
if (exitCode !== 0) process.exit(exitCode);
});
// Register contexts/profiles CRUD as a subgroup of config.
registerContexts(config);
}
+180
View File
@@ -0,0 +1,180 @@
import os from "node:os";
import path from "node:path";
import { existsSync, mkdirSync, writeFileSync, copyFileSync } from "node:fs";
import { apiFetch } from "../api.mjs";
import { createPrompt, printSuccess, printError, printInfo, printHeading } from "../io.mjs";
import { t } from "../i18n.mjs";
/**
* `omniroute configure <cli>` — interactive provider+model picker that writes a
* local CLI config pointed at the ACTIVE OmniRoute context (local or remote).
*
* The model catalog comes from the active context's GET /v1/models, so when you
* are in remote mode (`omniroute connect ...`) you pick from the remote server's
* live models and the profile is written on THIS machine.
*
* v1 targets the Codex CLI (writes ~/.codex/<name>.config.toml). The credential
* is referenced by env var (OMNIROUTE_API_KEY) — never written to disk.
*/
const SUPPORTED = ["codex"];
/** Derive a short, filesystem-safe profile name from a model id. */
export function profileNameFromModel(modelId) {
const afterProvider = String(modelId).includes("/")
? String(modelId).split("/").slice(1).join("/")
: String(modelId);
return afterProvider.replace(/[^a-zA-Z0-9]+/g, "").toLowerCase() || "model";
}
/** Provider id for a catalog entry: explicit owned_by, else the id prefix. */
function providerOf(entry) {
if (entry && typeof entry.owned_by === "string" && entry.owned_by) return entry.owned_by;
const id = typeof entry === "string" ? entry : entry?.id || "";
return id.includes("/") ? id.split("/")[0] : "(none)";
}
function contextWindowOf(entry) {
for (const c of [entry?.context_length, entry?.max_context_window_tokens]) {
if (typeof c === "number" && Number.isFinite(c) && c > 0) return c;
}
return null;
}
async function fetchModels(globalOpts) {
const res = await apiFetch("/v1/models", { ...globalOpts, acceptNotOk: true });
if (!res.ok) {
let msg = `HTTP ${res.status}`;
try {
const b = await res.json();
msg = b?.error?.message || b?.error || msg;
} catch {
/* ignore */
}
throw new Error(`Could not fetch models: ${msg}`);
}
const body = await res.json();
const list = Array.isArray(body) ? body : body.data || body.models || [];
return list.filter((m) => (typeof m === "string" ? m : m?.id));
}
function buildCodexProfile(modelId, ctx) {
const lines = [
`# codex --profile ${profileNameFromModel(modelId)}`,
`# ${modelId} — generated by 'omniroute configure codex'`,
`model = "${modelId}"`,
`model_provider = "omniroute"`,
];
if (ctx && ctx > 0) {
const compact = Math.floor(ctx * 0.85);
lines.push(`model_context_window = ${ctx}`);
lines.push(`model_auto_compact_token_limit = ${compact}`);
}
return lines.join("\n") + "\n";
}
async function configureCodex(modelId, ctxWindow, opts) {
const codexHome = opts.codexHome || path.join(os.homedir(), ".codex");
if (!existsSync(codexHome)) mkdirSync(codexHome, { recursive: true });
const profile = opts.name || profileNameFromModel(modelId);
const filePath = path.join(codexHome, `${profile}.config.toml`);
if (existsSync(filePath)) {
copyFileSync(filePath, `${filePath}.bak`);
}
writeFileSync(filePath, buildCodexProfile(modelId, ctxWindow), "utf8");
printSuccess(`Wrote ${filePath}`);
printInfo(`Use it: codex --profile ${profile}`);
printInfo("Prereq: ~/.codex/config.toml must define the [model_providers.omniroute] block");
printInfo(" (run the Codex setup once — see docs/guides/CODEX-CLI-CONFIGURATION.md).");
}
export async function runConfigureCommand(cli, opts = {}, cmd) {
const target = String(cli || "").toLowerCase();
if (!SUPPORTED.includes(target)) {
printError(`Unsupported CLI '${cli}'. Supported: ${SUPPORTED.join(", ")}.`);
return 2;
}
const globalOpts = cmd ? cmd.optsWithGlobals() : {};
let models;
try {
models = await fetchModels(globalOpts);
} catch (e) {
printError(e instanceof Error ? e.message : String(e));
return 1;
}
if (!models.length) {
printError("The server returned no models.");
return 1;
}
// Resolve model: explicit flags or interactive pick.
let chosenId = opts.model;
if (chosenId && opts.provider && !chosenId.includes("/")) {
chosenId = `${opts.provider}/${chosenId}`;
}
if (!chosenId) {
const ids = models.map((m) => (typeof m === "string" ? m : m.id));
const providers = [...new Set(models.map(providerOf))].sort();
const prompt = createPrompt();
try {
printHeading("Configure Codex CLI");
let providerList = providers;
if (opts.provider) {
providerList = providers.filter((p) => p === opts.provider);
} else {
printInfo(`Providers: ${providers.join(", ")}`);
const p = await prompt.ask("Provider");
if (p) providerList = providers.filter((x) => x === p);
}
const inProvider = ids.filter((id) => providerList.includes(providerOf(byId(models, id))));
const candidates = inProvider.length ? inProvider : ids;
printInfo(`Models: ${candidates.slice(0, 40).join(", ")}${candidates.length > 40 ? " …" : ""}`);
chosenId = await prompt.ask("Model id");
} finally {
prompt.close();
}
}
if (!chosenId) {
printError("No model selected.");
return 2;
}
const entry = byId(models, chosenId);
if (!entry) {
printError(`Model '${chosenId}' is not in the catalog.`);
return 2;
}
const ctxWindow = contextWindowOf(entry);
if (target === "codex") {
await configureCodex(chosenId, ctxWindow, opts);
}
return 0;
}
function byId(models, id) {
for (const m of models) {
const mid = typeof m === "string" ? m : m.id;
if (mid === id) return m;
}
return null;
}
export function registerConfigure(program) {
program
.command("configure <cli>")
.description(
t("configure.description") ||
"Pick a provider+model from the active server and write a local CLI config (v1: codex)"
)
.option("--provider <id>", "Provider id (skips the interactive provider prompt)")
.option("--model <id>", "Model id (skips the interactive model prompt)")
.option("--name <name>", "Profile name to write (default: derived from model)")
.option("--codex-home <dir>", "Codex home dir (default: ~/.codex)")
.action(async (cli, opts, cmd) => {
const code = await runConfigureCommand(cli, opts, cmd);
if (code !== 0) process.exit(code);
});
}
+132
View File
@@ -0,0 +1,132 @@
import { apiFetch } from "../api.mjs";
import { loadContexts, saveContexts } from "../contexts.mjs";
import { createPrompt, printSuccess, printError, printInfo } from "../io.mjs";
import { t } from "../i18n.mjs";
/**
* `omniroute connect <host>` — remote mode.
*
* Logs into a remote OmniRoute server and saves the result as the active context
* so every subsequent command targets that server. Two flows:
* - password: prompts for the management password → POST /api/cli/connect →
* server mints a scoped access token (default scope: admin).
* - token: `--key <oma_...>` validates via GET /api/cli/whoami and saves it.
*/
/** Normalize a host/URL into a server root baseUrl (no trailing path). */
export function normalizeBaseUrl(host, port) {
let value = String(host || "").trim();
if (!value) return "";
const hadScheme = /^https?:\/\//i.test(value);
if (!hadScheme) value = `http://${value}`;
try {
const u = new URL(value);
// Only apply the default port to a bare host; a full URL is taken as-is.
if (!hadScheme && !u.port && port) u.port = String(port);
return u.origin;
} catch {
return value;
}
}
/** Derive a clean context name from a host (strip scheme/port). */
export function hostLabel(host) {
let value = String(host || "").trim().replace(/^https?:\/\//i, "");
value = value.split("/")[0].split(":")[0];
return value || "remote";
}
async function readErrorMessage(res) {
try {
const body = await res.json();
return body?.error?.message || body?.error || `HTTP ${res.status}`;
} catch {
return `HTTP ${res.status}`;
}
}
export async function runConnectCommand(host, opts = {}) {
const baseUrl = normalizeBaseUrl(host, opts.port || "20128");
if (!baseUrl) {
printError("A host is required, e.g. omniroute connect 192.168.0.15");
return 2;
}
const name = opts.name || hostLabel(host);
let accessToken;
let scope;
if (opts.key) {
// Validate the pasted token against the remote.
const res = await apiFetch("/api/cli/whoami", {
baseUrl,
apiKey: opts.key,
acceptNotOk: true,
});
if (!res.ok) {
printError(`Token rejected by ${baseUrl}: ${await readErrorMessage(res)}`);
return res.exitCode || 1;
}
const body = await res.json();
accessToken = opts.key;
scope = body.scope || "unknown";
} else {
const prompt = createPrompt();
let password;
try {
password = await prompt.askSecret(`Management password for ${baseUrl}`);
} finally {
prompt.close();
}
if (!password) {
printError("Password is required (or use --key <token>).");
return 2;
}
const res = await apiFetch("/api/cli/connect", {
baseUrl,
method: "POST",
body: { password, name, scope: opts.scope },
acceptNotOk: true,
retry: false,
});
if (!res.ok) {
printError(`Connect failed (${res.status}): ${await readErrorMessage(res)}`);
return res.exitCode || 1;
}
const body = await res.json();
accessToken = body.token;
scope = body.scope;
}
const cfg = loadContexts();
cfg.contexts = cfg.contexts || {};
cfg.contexts[name] = {
baseUrl,
accessToken,
scope,
description: `Remote OmniRoute (${host})`,
};
cfg.currentContext = name;
saveContexts(cfg);
printSuccess(`Connected to ${baseUrl} — context '${name}' (scope: ${scope})`);
printInfo("All commands now target this server.");
printInfo("Switch back to local with: omniroute contexts use default");
return 0;
}
export function registerConnect(program) {
program
.command("connect <host>")
.description(
t("connect.description") || "Connect to a remote OmniRoute server and enter remote mode"
)
.option("--port <port>", "Server port when the host has none", "20128")
.option("--key <token>", "Use a pre-generated scoped access token (skips the password prompt)")
.option("--name <name>", "Context name to save (default: derived from host)")
.option("--scope <scope>", "Requested scope for the password flow (read|write|admin)")
.action(async (host, opts) => {
const code = await runConnectCommand(host, opts);
if (code !== 0) process.exit(code);
});
}
+182
View File
@@ -0,0 +1,182 @@
import { readFileSync } from "node:fs";
import { createInterface } from "node:readline";
import { apiFetch } from "../api.mjs";
import { emit } from "../output.mjs";
import { t } from "../i18n.mjs";
async function confirm(q) {
const rl = createInterface({ input: process.stdin, output: process.stdout });
return new Promise((resolve) => {
rl.question(`${q} [y/N] `, (a) => {
rl.close();
resolve(a.trim().toLowerCase() === "y");
});
});
}
export function registerContextEng(program) {
const ctx = program.command("context-eng").alias("ctx").description(t("context.description"));
ctx
.command("analytics")
.option("--period <p>", t("context.analytics.period"), "7d")
.action(async (opts, cmd) => {
const res = await apiFetch(`/api/context/analytics?period=${opts.period}`);
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
emit(await res.json(), cmd.optsWithGlobals());
});
const caveman = ctx.command("caveman").description(t("context.caveman.description"));
const cmCfg = caveman.command("config").description(t("context.caveman.config.description"));
cmCfg.command("show").action(async (opts, cmd) => {
const res = await apiFetch("/api/context/caveman/config");
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
emit(await res.json(), cmd.optsWithGlobals());
});
cmCfg
.command("set")
.option("--aggressiveness <n>", t("context.caveman.config.aggressiveness"), parseFloat)
.option("--max-shrink-pct <n>", t("context.caveman.config.maxShrinkPct"), parseInt)
.option("--preserve-tags <list>", t("context.caveman.config.preserveTags"), (v) => v.split(","))
.action(async (opts, cmd) => {
const body = {};
if (opts.aggressiveness !== undefined) body.aggressiveness = opts.aggressiveness;
if (opts.maxShrinkPct !== undefined) body.maxShrinkPct = opts.maxShrinkPct;
if (opts.preserveTags) body.preserveTags = opts.preserveTags;
const res = await apiFetch("/api/context/caveman/config", { method: "PUT", body });
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
emit(await res.json(), cmd.optsWithGlobals());
});
const rtk = ctx.command("rtk").description(t("context.rtk.description"));
const rtkCfg = rtk.command("config").description(t("context.rtk.config.description"));
rtkCfg.command("show").action(async (opts, cmd) => {
const res = await apiFetch("/api/context/rtk/config");
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
emit(await res.json(), cmd.optsWithGlobals());
});
rtkCfg
.command("set")
.option("--token-budget <n>", t("context.rtk.config.tokenBudget"), parseInt)
.option("--reserve-pct <n>", t("context.rtk.config.reservePct"), parseInt)
.action(async (opts, cmd) => {
const body = {};
if (opts.tokenBudget) body.tokenBudget = opts.tokenBudget;
if (opts.reservePct) body.reservePct = opts.reservePct;
const res = await apiFetch("/api/context/rtk/config", { method: "PUT", body });
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
emit(await res.json(), cmd.optsWithGlobals());
});
const filters = rtk.command("filters").description(t("context.rtk.filters.description"));
filters.command("list").action(async (opts, cmd) => {
const res = await apiFetch("/api/context/rtk/filters");
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
emit(await res.json(), cmd.optsWithGlobals());
});
filters
.command("add")
.requiredOption("--pattern <p>", t("context.rtk.filters.pattern"))
.option("--priority <n>", t("context.rtk.filters.priority"), parseInt, 100)
.option("--action <a>", t("context.rtk.filters.action"), "drop")
.action(async (opts, cmd) => {
const body = { pattern: opts.pattern, priority: opts.priority, action: opts.action };
const res = await apiFetch("/api/context/rtk/filters", { method: "POST", body });
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
emit(await res.json(), cmd.optsWithGlobals());
});
filters
.command("remove <id>")
.option("--yes", t("context.rtk.filters.yes"))
.action(async (id, opts, cmd) => {
if (!opts.yes) {
const ok = await confirm(`Remove filter ${id}?`);
if (!ok) return;
}
const res = await apiFetch(`/api/context/rtk/filters/${id}`, { method: "DELETE" });
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
process.stdout.write("Removed\n");
});
rtk
.command("test")
.requiredOption("--file <path>", t("context.rtk.test.file"))
.action(async (opts, cmd) => {
const body = JSON.parse(readFileSync(opts.file, "utf8"));
const res = await apiFetch("/api/context/rtk/test", { method: "POST", body });
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
emit(await res.json(), cmd.optsWithGlobals());
});
rtk.command("raw-output <id>").action(async (id, opts, cmd) => {
const res = await apiFetch(`/api/context/rtk/raw-output/${id}`);
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
emit(await res.json(), cmd.optsWithGlobals());
});
const combos = ctx.command("combos").description(t("context.combos.description"));
combos.command("list").action(async (opts, cmd) => {
const res = await apiFetch("/api/context/combos");
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
emit(await res.json(), cmd.optsWithGlobals());
});
combos.command("get <id>").action(async (id, opts, cmd) => {
const res = await apiFetch(`/api/context/combos/${id}`);
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
emit(await res.json(), cmd.optsWithGlobals());
});
combos.command("assignments <id>").action(async (id, opts, cmd) => {
const res = await apiFetch(`/api/context/combos/${id}/assignments`);
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
emit(await res.json(), cmd.optsWithGlobals());
});
}
+271
View File
@@ -0,0 +1,271 @@
import { t } from "../i18n.mjs";
import { emit } from "../output.mjs";
import { loadContexts, saveContexts, resolveActiveContext } from "../contexts.mjs";
/** Auth label for a context: prefers the scoped accessToken over the legacy apiKey. */
function authLabel(c) {
if (c?.accessToken) return "token";
if (c?.apiKey) return "key";
return "✗";
}
export async function confirm(msg) {
// Non-interactive stdin (pipe, CI, EOF) cannot answer a [y/N] prompt. Asking
// anyway leaves the readline question pending forever — Node then warns about an
// "unsettled top-level await" at exit. Decline cleanly instead and point at the
// non-interactive escape hatch so scripted callers fail safe rather than hang.
if (!process.stdin.isTTY) {
process.stderr.write(`${msg} [y/N] (non-interactive stdin — declined; pass --yes to confirm)\n`);
return false;
}
const readline = await import("node:readline");
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
const answer = await new Promise((r) => rl.question(`${msg} [y/N] `, r));
rl.close();
return /^y(es)?$/i.test(answer);
}
function maskKey(k) {
if (!k) return null;
if (k.length <= 8) return "***";
return `${k.slice(0, 6)}***${k.slice(-4)}`;
}
export function registerContexts(program) {
const ctx = program
.command("contexts")
.alias("context") // singular alias — docs/connect output historically said `context current`
.description(t("config.contexts.description") || "Manage server contexts/profiles");
ctx
.command("list")
.description("List all contexts")
.action(async (opts, cmd) => {
const globalOpts = cmd.optsWithGlobals();
const cfg = loadContexts();
const rows = Object.entries(cfg.contexts || {}).map(([name, c]) => ({
active: name === (cfg.currentContext || "default") ? "●" : "",
name,
baseUrl: c.baseUrl || "",
auth: authLabel(c),
scope: c.scope || "",
description: c.description || "",
}));
emit(rows, globalOpts, [
{ key: "active", header: "" },
{ key: "name", header: "Name" },
{ key: "baseUrl", header: "Base URL" },
{ key: "auth", header: "Auth" },
{ key: "scope", header: "Scope" },
{ key: "description", header: "Description" },
]);
});
ctx
.command("add <name>")
.description("Add a new context")
.requiredOption("--url <u>", "Base URL")
.option("--api-key <k>", "Legacy inference API key")
.option("--api-key-stdin", "Read API key from stdin")
.option("--access-token <t>", "Scoped CLI access token (preferred over --api-key)")
.option("--access-token-stdin", "Read access token from stdin")
.option("--scope <s>", "Token scope hint for display (read|write|admin)")
.option("--description <d>", "Context description")
.action(async (name, opts) => {
const cfg = loadContexts();
if (cfg.contexts?.[name]) {
process.stderr.write(`Context '${name}' already exists. Remove or rename first.\n`);
process.exit(2);
}
let apiKey = opts.apiKey || null;
let accessToken = opts.accessToken || null;
if (opts.apiKeyStdin || opts.accessTokenStdin) {
const chunks = [];
for await (const c of process.stdin) chunks.push(c);
const value = chunks.join("").trim() || null;
if (opts.accessTokenStdin) accessToken = value;
else apiKey = value;
}
cfg.contexts = cfg.contexts || {};
cfg.contexts[name] = {
baseUrl: opts.url,
accessToken: accessToken || undefined,
apiKey,
scope: opts.scope || undefined,
description: opts.description || undefined,
};
saveContexts(cfg);
process.stdout.write(`Added context '${name}'\n`);
});
ctx
.command("use <name>")
.description("Switch active context")
.action((name) => {
const cfg = loadContexts();
if (!cfg.contexts?.[name]) {
process.stderr.write(`No such context: ${name}\n`);
process.exit(2);
}
cfg.currentContext = name;
saveContexts(cfg);
process.stdout.write(`Active context: ${name}\n`);
});
ctx
.command("current")
.description("Show the active context (server, auth, scope)")
.option("--name-only", "Print just the context name (legacy behavior)")
.action((opts, cmd) => {
const globalOpts = cmd.optsWithGlobals();
const cfg = loadContexts();
const name = cfg.currentContext || cfg.activeProfile || "default";
if (opts.nameOnly) {
process.stdout.write(`${name}\n`);
return;
}
const c = resolveActiveContext(name);
emit(
{
name,
baseUrl: c.baseUrl || "",
auth: authLabel(c),
scope: c.scope || "",
description: c.description || "",
},
globalOpts
);
});
ctx
.command("show <name>")
.description("Show context details")
.action((name, opts, cmd) => {
const globalOpts = cmd.optsWithGlobals();
const cfg = loadContexts();
const c = cfg.contexts?.[name];
if (!c) {
process.stderr.write(`No such context: ${name}\n`);
process.exit(2);
}
const display = {
name,
baseUrl: c.baseUrl,
accessToken: maskKey(c.accessToken),
apiKey: maskKey(c.apiKey),
scope: c.scope,
description: c.description,
};
emit(display, globalOpts);
});
ctx
.command("remove <name>")
.description("Remove a context")
.option("--yes", "Skip confirmation")
.action(async (name, opts) => {
if (!opts.yes) {
const ok = await confirm(`Remove context '${name}'?`);
if (!ok) {
process.stdout.write("Cancelled.\n");
return;
}
}
const cfg = loadContexts();
if (!cfg.contexts?.[name]) {
process.stderr.write(`No such context: ${name}\n`);
process.exit(2);
}
if (name === "default") {
process.stderr.write("Cannot remove default context.\n");
process.exit(2);
}
delete cfg.contexts[name];
if (cfg.currentContext === name) cfg.currentContext = "default";
saveContexts(cfg);
process.stdout.write(`Removed context '${name}'\n`);
});
ctx
.command("rename <old> <new>")
.description("Rename a context")
.action((oldName, newName) => {
const cfg = loadContexts();
if (!cfg.contexts?.[oldName]) {
process.stderr.write(`No such context: ${oldName}\n`);
process.exit(2);
}
if (cfg.contexts[newName]) {
process.stderr.write(`Context '${newName}' already exists.\n`);
process.exit(2);
}
cfg.contexts[newName] = cfg.contexts[oldName];
delete cfg.contexts[oldName];
if (cfg.currentContext === oldName) cfg.currentContext = newName;
saveContexts(cfg);
process.stdout.write(`Renamed '${oldName}' → '${newName}'\n`);
});
ctx
.command("export")
.description("Export contexts to JSON")
.option("--out <path>", "Output file path (default: stdout)")
.option("--no-secrets", "Omit API keys from export")
.action(async (opts, cmd) => {
const cfg = loadContexts();
const out = JSON.parse(JSON.stringify(cfg));
if (opts.noSecrets) {
for (const c of Object.values(out.contexts || {})) {
c.apiKey = null;
delete c.accessToken;
}
}
const json = JSON.stringify(out, null, 2);
if (opts.out) {
const { writeFileSync } = await import("node:fs");
writeFileSync(opts.out, json);
process.stdout.write(`Exported to ${opts.out}\n`);
} else {
process.stdout.write(json + "\n");
}
});
ctx
.command("import <file>")
.description("Import contexts from a JSON file")
.option("--merge", "Merge with existing contexts (default: overwrite)")
.action(async (file, opts) => {
const { readFileSync } = await import("node:fs");
let imported;
try {
imported = JSON.parse(readFileSync(file, "utf8"));
} catch (e) {
process.stderr.write(
`Cannot read ${file}: ${e instanceof Error ? e.message : String(e)}\n`
);
process.exit(1);
}
const cfg = opts.merge
? loadContexts()
: { version: 1, currentContext: "default", contexts: {} };
const incoming = imported.contexts || {};
let count = 0;
for (const [name, raw] of Object.entries(incoming)) {
if (typeof name !== "string" || !name) continue;
const c = raw && typeof raw === "object" ? /** @type {Record<string,unknown>} */ (raw) : {};
cfg.contexts[name] = {
baseUrl: typeof c.baseUrl === "string" ? c.baseUrl : "http://localhost:20128",
accessToken: typeof c.accessToken === "string" ? c.accessToken : undefined,
apiKey: typeof c.apiKey === "string" ? c.apiKey : null,
scope: typeof c.scope === "string" ? c.scope : undefined,
description: typeof c.description === "string" ? c.description : undefined,
};
count++;
}
if (!opts.merge && typeof imported.currentContext === "string") {
cfg.currentContext = imported.currentContext;
}
saveContexts(cfg);
process.stdout.write(`Imported ${count} context(s)\n`);
});
}
+144
View File
@@ -0,0 +1,144 @@
import { apiFetch } from "../api.mjs";
import { emit } from "../output.mjs";
import { t } from "../i18n.mjs";
const costSchema = [
{ key: "group", header: "Group", width: 30 },
{ key: "requests", header: "Reqs", formatter: (v) => (v != null ? v.toLocaleString() : "0") },
{ key: "tokensIn", header: "Tokens In", formatter: fmtTokens },
{ key: "tokensOut", header: "Tokens Out", formatter: fmtTokens },
{ key: "costUsd", header: "Cost (USD)", formatter: (v) => (v ? `$${v.toFixed(4)}` : "$0.0000") },
{
key: "costPct",
header: "% of Total",
formatter: (v) => (v != null ? `${v.toFixed(1)}%` : "-"),
},
];
function fmtTokens(v) {
if (!v) return "0";
if (v > 1e6) return `${(v / 1e6).toFixed(1)}M`;
if (v > 1e3) return `${(v / 1e3).toFixed(1)}K`;
return String(v);
}
export function registerCost(program) {
program
.command("cost")
.description(t("cost.description"))
.option("--period <range>", t("cost.period"), "30d")
.option("--since <date>", t("cost.since"))
.option("--until <date>", t("cost.until"))
.option("--group-by <field>", t("cost.group_by"), "provider")
.option("--api-key <key>", t("cost.api_key_filter"))
.option("--limit <n>", t("cost.limit"), parseInt, 100)
.action(runCostCommand);
}
export async function runCostCommand(opts, cmd) {
const globalOpts = cmd.optsWithGlobals();
const params = buildParams(opts);
const res = await apiFetch(`/api/usage/analytics?${params}`, {
timeout: globalOpts.timeout,
acceptNotOk: true,
});
if (!res.ok) {
if (res.status === 401 || res.status === 403) {
process.stderr.write(t("common.authRequired") + "\n");
} else if (res.status >= 500) {
process.stderr.write(t("common.serverOffline") + "\n");
} else {
process.stderr.write(t("common.error", { message: `HTTP ${res.status}` }) + "\n");
}
process.exit(res.exitCode ?? 1);
}
const data = await res.json();
const rows = aggregateByGroup(data, opts.groupBy ?? "provider", opts.limit ?? 100);
emit(rows, globalOpts, costSchema);
if (!globalOpts.quiet && globalOpts.output !== "json" && globalOpts.output !== "jsonl") {
const total = rows.reduce((s, r) => s + (r.costUsd ?? 0), 0);
process.stderr.write(
`\nTotal: $${total.toFixed(4)} across ${rows.length} ${opts.groupBy ?? "provider"}(s)\n`
);
}
}
function buildParams(opts) {
const p = new URLSearchParams();
if (opts.since || opts.until) {
if (opts.since) p.set("startDate", opts.since);
if (opts.until) p.set("endDate", opts.until);
} else {
p.set("range", opts.period ?? "30d");
}
if (opts.apiKey) p.set("apiKeyIds", opts.apiKey);
return p.toString();
}
function aggregateByGroup(data, groupBy, limit) {
const source = pickSource(data, groupBy);
if (!Array.isArray(source)) return [];
const totalCost = source.reduce((s, r) => s + toNum(r.totalCost ?? r.cost ?? r.costUsd), 0);
const rows = source.map((r) => {
const costUsd = toNum(r.totalCost ?? r.cost ?? r.costUsd);
return {
group: groupLabel(r, groupBy),
requests: toNum(r.totalRequests ?? r.requests ?? r.count),
tokensIn: toNum(r.totalTokensIn ?? r.tokensIn ?? r.promptTokens),
tokensOut: toNum(r.totalTokensOut ?? r.tokensOut ?? r.completionTokens),
costUsd,
costPct: totalCost > 0 ? (costUsd / totalCost) * 100 : 0,
};
});
rows.sort((a, b) => b.costUsd - a.costUsd);
return rows.slice(0, limit);
}
function pickSource(data, groupBy) {
switch (groupBy) {
case "model":
return data.byModel ?? data.models ?? [];
case "combo":
return data.byCombo ?? data.combos ?? [];
case "api-key":
case "apiKey":
return data.byApiKey ?? data.apiKeys ?? [];
case "day":
return data.byDay ?? data.daily ?? data.trend ?? [];
default:
return data.byProvider ?? data.providers ?? [];
}
}
function groupLabel(row, groupBy) {
switch (groupBy) {
case "model":
return row.model ?? row.modelId ?? String(row.group ?? "");
case "combo":
return row.comboName ?? row.combo ?? row.name ?? String(row.group ?? "");
case "api-key":
case "apiKey":
return row.keyName ?? row.apiKey ?? row.label ?? String(row.group ?? "");
case "day":
return row.date ?? row.day ?? String(row.group ?? "");
default:
return row.provider ?? row.providerId ?? String(row.group ?? "");
}
}
function toNum(v) {
if (typeof v === "number" && Number.isFinite(v)) return v;
if (typeof v === "string") {
const n = Number(v);
return Number.isFinite(n) ? n : 0;
}
return 0;
}
+65
View File
@@ -0,0 +1,65 @@
import { execFile } from "node:child_process";
import { t } from "../i18n.mjs";
export function registerDashboard(program) {
program
.command("dashboard")
.description(t("dashboard.description"))
.option("--url", t("dashboard.urlOnly"))
.option("--port <port>", "Port the server is running on", "20128")
.option("--tui", t("dashboard.tui") || "Open interactive TUI dashboard (terminal UI)")
.action(async (opts, cmd) => {
if (opts.tui) {
const globalOpts = cmd.optsWithGlobals();
const port = opts.port ? parseInt(String(opts.port), 10) : 20128;
const baseUrl = globalOpts.baseUrl ?? `http://localhost:${port}`;
const apiKey = globalOpts.apiKey ?? null;
const { startInteractiveTui } = await import("../tui/Dashboard.jsx");
await startInteractiveTui({ port, baseUrl, apiKey });
return;
}
const exitCode = await runDashboardCommand(opts);
if (exitCode !== 0) process.exit(exitCode);
});
}
export async function runDashboardCommand(opts = {}) {
const port = opts.port ? parseInt(String(opts.port), 10) : 20128;
const dashboardUrl = `http://localhost:${port}`;
if (opts.url) {
console.log(dashboardUrl);
return 0;
}
console.log(t("dashboard.opening", { url: dashboardUrl }));
try {
const open = await import("open");
await open.default(dashboardUrl);
} catch {
await openFallback(dashboardUrl);
}
return 0;
}
function openFallback(url) {
return new Promise((resolve) => {
const { platform } = process;
let cmd, args;
if (platform === "darwin") {
cmd = "open";
args = [url];
} else if (platform === "win32") {
cmd = "cmd";
args = ["/c", "start", "", url];
} else {
cmd = "xdg-open";
args = [url];
}
execFile(cmd, args, { stdio: "ignore" }, () => resolve());
});
}
+527
View File
@@ -0,0 +1,527 @@
import fs from "node:fs";
import net from "node:net";
import os from "node:os";
import path from "node:path";
import { createDecipheriv, scryptSync } from "node:crypto";
import { fileURLToPath, pathToFileURL } from "node:url";
import { resolveDataDir, resolveStoragePath } from "../data-dir.mjs";
import { printHeading } from "../io.mjs";
import { t } from "../i18n.mjs";
import { readDatabaseHealth, readEncryptedCredentialSamples } from "../sqlite.mjs";
const STATIC_SALT = "omniroute-field-encryption-v1";
const KEY_LENGTH = 32;
const CHECK_TIMEOUT_MS = 2000;
function ok(name, message, details = {}) {
return { name, status: "ok", message, details };
}
function warn(name, message, details = {}) {
return { name, status: "warn", message, details };
}
function fail(name, message, details = {}) {
return { name, status: "fail", message, details };
}
function parsePort(value, fallback) {
const parsed = Number.parseInt(String(value ?? ""), 10);
return Number.isFinite(parsed) && parsed > 0 && parsed <= 65535 ? parsed : fallback;
}
function parseConfiguredPort(value) {
if (value === undefined || value === null || value === "") return { valid: true, port: null };
const parsed = Number.parseInt(String(value), 10);
return {
valid: Number.isFinite(parsed) && parsed > 0 && parsed <= 65535,
port: parsed,
};
}
function formatBytes(bytes) {
const gb = bytes / 1024 / 1024 / 1024;
return `${gb.toFixed(1)} GB`;
}
function findEnvFileCandidates(dataDir) {
const candidates = [];
if (process.env.DATA_DIR) candidates.push(path.join(process.env.DATA_DIR, ".env"));
candidates.push(path.join(dataDir, ".env"));
candidates.push(path.join(process.cwd(), ".env"));
return [...new Set(candidates)];
}
function checkConfig(dataDir) {
const envCandidates = findEnvFileCandidates(dataDir);
const envFile = envCandidates.find((candidate) => fs.existsSync(candidate));
const portChecks = [
["PORT", process.env.PORT],
["API_PORT", process.env.API_PORT],
["DASHBOARD_PORT", process.env.DASHBOARD_PORT],
].map(([name, value]) => ({ name, value, ...parseConfiguredPort(value) }));
const invalidPorts = portChecks.filter((item) => !item.valid);
if (invalidPorts.length > 0) {
return fail(
"Config",
`Invalid port setting: ${invalidPorts.map((item) => item.name).join(", ")}`,
{ envFile: envFile || null, invalidPorts }
);
}
if (!envFile) {
return warn("Config", ".env file not found; using defaults and process environment", {
checked: envCandidates,
});
}
return ok("Config", `.env found at ${envFile}`, { envFile });
}
function resolveMigrationsDir(rootDir) {
const configured = process.env.OMNIROUTE_MIGRATIONS_DIR;
const candidates = [
configured,
path.join(rootDir, "src", "lib", "db", "migrations"),
path.join(rootDir, "app", "src", "lib", "db", "migrations"),
path.join(process.cwd(), "src", "lib", "db", "migrations"),
].filter(Boolean);
return candidates.find((candidate) => fs.existsSync(candidate)) || null;
}
function readMigrationFiles(migrationsDir) {
if (!migrationsDir) return [];
return fs
.readdirSync(migrationsDir)
.filter((file) => /^\d+_.+\.sql$/.test(file))
.sort()
.map((file) => {
const [, version, name] = file.match(/^(\d+)_(.+)\.sql$/) || [];
return { version, name, file };
});
}
async function checkDatabase(dbPath, rootDir) {
if (!fs.existsSync(dbPath)) {
return warn("Database", `SQLite database not found at ${dbPath}`, { dbPath });
}
try {
const { quickCheckValue, hasMigrationTable, appliedMigrationVersions } =
await readDatabaseHealth(dbPath);
if (quickCheckValue !== "ok") {
return fail("Database", `SQLite quick_check failed: ${quickCheckValue}`, { dbPath });
}
const migrationsDir = resolveMigrationsDir(rootDir);
const migrationFiles = readMigrationFiles(migrationsDir);
if (migrationFiles.length === 0) {
return ok("Database", "SQLite quick_check passed", { dbPath, migrations: "not_checked" });
}
if (!hasMigrationTable) {
return warn("Database", "SQLite is readable, but migration table is missing", { dbPath });
}
const applied = new Set(appliedMigrationVersions);
const pending = migrationFiles.filter((migration) => !applied.has(migration.version));
if (pending.length > 0) {
return warn("Database", `${pending.length} migration(s) appear pending`, {
dbPath,
pending: pending.map((migration) => migration.file),
});
}
return ok("Database", "SQLite quick_check passed and migrations look current", { dbPath });
} catch (error) {
return fail("Database", "SQLite database could not be read", {
dbPath,
error: error instanceof Error ? error.message : String(error),
});
}
}
function deriveStorageKey() {
const secret = process.env.STORAGE_ENCRYPTION_KEY;
if (!secret) return null;
return scryptSync(secret, STATIC_SALT, KEY_LENGTH);
}
function decryptCredentialSample(value, key) {
const prefix = "enc:v1:";
const body = value.slice(prefix.length);
const [ivHex, encryptedHex, authTagHex] = body.split(":");
if (!ivHex || !encryptedHex || !authTagHex) throw new Error("Malformed encrypted value");
const authTagBuf = Buffer.from(authTagHex, "hex");
const decipher = createDecipheriv("aes-256-gcm", key, Buffer.from(ivHex, "hex"), {
authTagLength: authTagBuf.length,
});
decipher.setAuthTag(authTagBuf);
let decrypted = decipher.update(encryptedHex, "hex", "utf8");
decrypted += decipher.final("utf8");
return decrypted;
}
async function checkStorageEncryption(dbPath) {
const secret = process.env.STORAGE_ENCRYPTION_KEY;
if (secret !== undefined && String(secret).trim() === "") {
return fail("Storage/encryption", "STORAGE_ENCRYPTION_KEY is set but empty");
}
if (!fs.existsSync(dbPath)) {
return secret
? ok("Storage/encryption", "Encryption key is configured; database not initialized yet")
: warn("Storage/encryption", "No STORAGE_ENCRYPTION_KEY configured; passthrough mode");
}
try {
const { hasProviderTable, encryptedValues } = await readEncryptedCredentialSamples(dbPath);
if (!hasProviderTable) {
return secret
? ok("Storage/encryption", "Encryption key is configured; provider table not initialized")
: warn("Storage/encryption", "No STORAGE_ENCRYPTION_KEY configured; passthrough mode");
}
if (encryptedValues.length === 0) {
return secret
? ok("Storage/encryption", "Encryption key is configured; no encrypted samples found")
: warn(
"Storage/encryption",
"No STORAGE_ENCRYPTION_KEY configured; credentials are plaintext"
);
}
if (!secret) {
return fail(
"Storage/encryption",
"Encrypted credentials exist but STORAGE_ENCRYPTION_KEY is missing",
{ encryptedSamples: encryptedValues.length }
);
}
const key = deriveStorageKey();
for (const value of encryptedValues) {
decryptCredentialSample(value, key);
}
return ok("Storage/encryption", "Encrypted credential samples decrypt successfully", {
encryptedSamples: encryptedValues.length,
});
} catch (error) {
return fail("Storage/encryption", "Encrypted credential check failed", {
error: error instanceof Error ? error.message : String(error),
});
}
}
function checkPort(port, label) {
return new Promise((resolve) => {
const server = net.createServer();
server.once("error", (error) => {
if (error.code === "EADDRINUSE") {
resolve(warn("Port availability", `${label} port ${port} is already in use`, { port }));
} else {
resolve(
warn("Port availability", `${label} port ${port} could not be checked`, {
port,
error: error.message,
})
);
}
});
server.once("listening", () => {
server.close(() => {
resolve(ok("Port availability", `${label} port ${port} is available`, { port }));
});
});
server.listen(port, "127.0.0.1");
});
}
async function checkPorts() {
const port = parsePort(process.env.PORT || "20128", 20128);
const apiPort = parsePort(process.env.API_PORT || String(port), port);
const dashboardPort = parsePort(process.env.DASHBOARD_PORT || String(port), port);
const checks = await Promise.all([
checkPort(dashboardPort, "Dashboard"),
apiPort === dashboardPort ? Promise.resolve(null) : checkPort(apiPort, "API"),
]);
const results = checks.filter(Boolean);
const failResult = results.find((result) => result.status === "fail");
if (failResult) return failResult;
const warnResults = results.filter((result) => result.status === "warn");
if (warnResults.length > 0) {
return warn("Port availability", warnResults.map((result) => result.message).join("; "), {
ports: { apiPort, dashboardPort },
});
}
return ok("Port availability", "Configured port(s) are available", {
ports: { apiPort, dashboardPort },
});
}
async function checkNodeRuntime(rootDir) {
try {
const { getNodeRuntimeSupport } = await import(
pathToFileURL(path.join(rootDir, "bin", "nodeRuntimeSupport.mjs")).href
);
const support = getNodeRuntimeSupport();
if (!support.nodeCompatible) {
return fail("Node runtime", `${support.nodeVersion} is outside supported policy`, support);
}
return ok("Node runtime", `${support.nodeVersion} is supported`, support);
} catch {
// nodeRuntimeSupport.mjs is only available in full source installs, not in Docker images
const version = process.version;
return warn(
"Node runtime",
`${version} (runtime support module unavailable in this environment)`,
{ nodeVersion: version }
);
}
}
async function checkNativeBinary(rootDir) {
const candidates = [
path.join(
rootDir,
"app",
"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));
if (!binaryPath) {
return warn("Native binary", "better-sqlite3 native binary was not found", { candidates });
}
try {
const { isNativeBinaryCompatible } = await import(
pathToFileURL(path.join(rootDir, "scripts", "build", "native-binary-compat.mjs")).href
);
const compatible = isNativeBinaryCompatible(binaryPath);
if (!compatible) {
return fail("Native binary", "better-sqlite3 native binary is incompatible", { binaryPath });
}
return ok("Native binary", "better-sqlite3 native binary is compatible", { binaryPath });
} catch {
// native-binary-compat.mjs is only available in full source installs, not in Docker images
return warn("Native binary", "Compatibility check unavailable in this environment", {
binaryPath,
});
}
}
function checkMemory() {
const configured = process.env.OMNIROUTE_MEMORY_MB || "512";
const memoryMb = Number.parseInt(configured, 10);
if (!Number.isFinite(memoryMb) || memoryMb < 64 || memoryMb > 16384) {
return fail("Memory", `Invalid OMNIROUTE_MEMORY_MB: ${configured}`, { configured });
}
const total = os.totalmem();
const free = os.freemem();
const requestedBytes = memoryMb * 1024 * 1024;
if (requestedBytes > total) {
return warn(
"Memory",
`Requested memory ${memoryMb} MB exceeds total RAM ${formatBytes(total)}`,
{
memoryMb,
totalBytes: total,
freeBytes: free,
}
);
}
return ok("Memory", `${memoryMb} MB limit configured; ${formatBytes(free)} free`, {
memoryMb,
totalBytes: total,
freeBytes: free,
});
}
async function fetchWithTimeout(url) {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), CHECK_TIMEOUT_MS);
try {
return await fetch(url, { signal: controller.signal });
} finally {
clearTimeout(timeout);
}
}
function formatHostForUrl(host) {
return host.includes(":") && !host.startsWith("[") ? `[${host}]` : host;
}
function resolveLivenessUrl(options = {}) {
const explicitUrl = options.livenessUrl || process.env.OMNIROUTE_DOCTOR_LIVENESS_URL;
if (explicitUrl) return explicitUrl;
const port = parsePort(process.env.PORT || "20128", 20128);
const dashboardPort = parsePort(process.env.DASHBOARD_PORT || String(port), port);
const host = String(options.livenessHost || process.env.OMNIROUTE_DOCTOR_HOST || "127.0.0.1")
.trim()
.replace(/^https?:\/\//, "")
.replace(/\/.*$/, "");
return `http://${formatHostForUrl(host || "127.0.0.1")}:${dashboardPort}/api/health/degradation`;
}
async function probeUrl(url) {
try {
const response = await fetchWithTimeout(url);
return { ok: response.ok, status: response.status };
} catch {
return { ok: false, status: 0 };
}
}
async function checkServerLiveness(options = {}) {
const url = resolveLivenessUrl(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 });
}
// #6162: /api/health and /api/health/degradation require a management token.
// When unauthenticated, fall back to probing a publicly served static asset
// (favicon.ico) to confirm the Next.js server is alive and reachable.
// Derive the fallback URL from the primary URL (preserving protocol/host/port)
// so custom liveness URL configurations are honored. Fall back to defaults
// only if the primary URL can't be parsed.
let fallbackUrl;
try {
const parsed = new URL(url);
parsed.pathname = "/favicon.ico";
parsed.search = "";
parsed.hash = "";
fallbackUrl = parsed.toString();
} catch {
const port = parsePort(process.env.PORT || "20128", 20128);
const dashboardPort = parsePort(process.env.DASHBOARD_PORT || String(port), port);
const host = String(options.livenessHost || process.env.OMNIROUTE_DOCTOR_HOST || "127.0.0.1")
.trim()
.replace(/^https?:\/\//, "")
.replace(/\/.*$/, "");
fallbackUrl = `http://${formatHostForUrl(host || "127.0.0.1")}:${dashboardPort}/favicon.ico`;
}
const fallback = await probeUrl(fallbackUrl);
if (fallback.ok) {
return ok(
"Server liveness",
`Server reachable (health endpoint returned ${primary.status}, likely requires MANAGEMENT_TOKEN)`,
{ primaryUrl: url, primaryStatus: primary.status, fallbackUrl, fallbackStatus: fallback.status }
);
}
return warn(
"Server liveness",
`Server health endpoint returned HTTP ${primary.status || "no-response"} and fallback probe failed`,
{ primaryUrl: url, primaryStatus: primary.status, fallbackUrl, fallbackStatus: fallback.status }
);
}
export async function collectDoctorChecks(context = {}, options = {}) {
const rootDir =
context.rootDir ||
path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..", "..");
const dataDir = resolveDataDir();
const dbPath = resolveStoragePath(dataDir);
const checks = [];
checks.push(checkConfig(dataDir));
checks.push(await checkDatabase(dbPath, rootDir));
checks.push(await checkStorageEncryption(dbPath));
checks.push(await checkPorts());
checks.push(await checkNodeRuntime(rootDir));
checks.push(await checkNativeBinary(rootDir));
checks.push(checkMemory());
if (!options.skipLiveness) {
checks.push(await checkServerLiveness(options));
}
// CLI tool health checks
try {
const { collectCliToolChecks } = await import("../../../src/lib/cli-helper/doctor/checks.ts");
const cliChecks = await collectCliToolChecks();
checks.push(...cliChecks);
} catch (err) {
checks.push(warn("CLI Tools", `Could not run CLI tool checks: ${err.message}`));
}
return {
dataDir,
dbPath,
checks,
summary: {
ok: checks.filter((check) => check.status === "ok").length,
warn: checks.filter((check) => check.status === "warn").length,
fail: checks.filter((check) => check.status === "fail").length,
},
};
}
function printCheck(check) {
const label = check.status.toUpperCase().padEnd(4);
const color =
check.status === "ok" ? "\x1b[32m" : check.status === "warn" ? "\x1b[33m" : "\x1b[31m";
console.log(`${color}${label}\x1b[0m ${check.name}: ${check.message}`);
}
export function registerDoctor(program) {
program
.command("doctor")
.description(t("doctor.title"))
.option("--no-liveness", "Skip HTTP health endpoint probing")
.option("--host <host>", "Host for server liveness probing", "127.0.0.1")
.option("--liveness-url <url>", "Full health endpoint URL override")
.action(async (opts, cmd) => {
const globalOpts = cmd.optsWithGlobals();
const exitCode = await runDoctorCommand({ ...opts, output: globalOpts.output });
if (exitCode !== 0) process.exit(exitCode);
});
}
export async function runDoctorCommand(opts = {}, context = {}) {
const isJson = (opts.output ?? "table") === "json";
const skipLiveness = !(opts.liveness ?? true);
const result = await collectDoctorChecks(context, {
skipLiveness,
livenessHost: opts.host,
livenessUrl: opts.livenessUrl,
});
if (isJson) {
console.log(JSON.stringify(result, null, 2));
} else {
printHeading("OmniRoute Doctor");
console.log(`Data dir: ${result.dataDir}`);
console.log(`Database: ${result.dbPath}\n`);
for (const check of result.checks) {
printCheck(check);
}
console.log(
`\nSummary: ${result.summary.ok} ok, ${result.summary.warn} warning(s), ${result.summary.fail} failure(s)`
);
}
return result.summary.fail > 0 ? 1 : 0;
}
+100
View File
@@ -0,0 +1,100 @@
import { t } from "../i18n.mjs";
const OMNIROUTE_ENV_VARS = [
"PORT",
"API_PORT",
"DASHBOARD_PORT",
"DATA_DIR",
"REQUIRE_API_KEY",
"LOG_LEVEL",
"NODE_ENV",
"REQUEST_TIMEOUT_MS",
"ENABLE_SOCKS5_PROXY",
"OMNIROUTE_API_KEY",
"OMNIROUTE_BASE_URL",
"OMNIROUTE_HTTP_TIMEOUT_MS",
];
const ENV_DEFAULTS = {
PORT: "20128",
DASHBOARD_PORT: "20128",
DATA_DIR: "~/.omniroute",
NODE_ENV: "production",
};
export function registerEnv(program) {
const env = program.command("env").description("Show and manage environment variables");
env
.command("show")
.alias("list")
.description("Show current environment variables")
.option("--json", "Output as JSON")
.action(async (opts, cmd) => {
const globalOpts = cmd.parent.optsWithGlobals();
await runEnvShowCommand({ ...opts, output: globalOpts.output });
});
env
.command("get <key>")
.description("Get a single environment variable")
.action(async (key) => {
await runEnvGetCommand(key);
});
env
.command("set <key> <value>")
.description("Set an environment variable (current session only)")
.action(async (key, value) => {
await runEnvSetCommand(key, value);
});
}
export async function runEnvShowCommand(opts = {}) {
const current = {};
for (const key of OMNIROUTE_ENV_VARS) {
if (process.env[key] !== undefined) current[key] = process.env[key];
}
if (opts.json || opts.output === "json") {
console.log(JSON.stringify({ current, defaults: ENV_DEFAULTS }, null, 2));
return 0;
}
console.log("\n\x1b[1m\x1b[36mEnvironment Variables\x1b[0m\n");
console.log(" Current:");
if (Object.keys(current).length === 0) {
console.log("\x1b[2m (none set)\x1b[0m");
} else {
for (const [key, value] of Object.entries(current)) {
const display = key.includes("KEY") || key.includes("SECRET") ? "***" : value;
console.log(`\x1b[2m ${key.padEnd(28)} ${display}\x1b[0m`);
}
}
console.log("\n Defaults:");
for (const [key, value] of Object.entries(ENV_DEFAULTS)) {
console.log(` ${key.padEnd(28)} ${value}`);
}
return 0;
}
export async function runEnvGetCommand(key) {
if (!key) {
console.error("Key is required. Usage: omniroute env get <key>");
return 1;
}
console.log(process.env[key] || "");
return 0;
}
export async function runEnvSetCommand(key, value) {
if (!key || value === undefined) {
console.error("Usage: omniroute env set <key> <value>");
return 1;
}
process.env[key] = String(value);
console.log(`\x1b[33m ${key}=${value} (temporary — current session only)\x1b[0m`);
return 0;
}
+278
View File
@@ -0,0 +1,278 @@
import { readFileSync } from "node:fs";
import { setTimeout as sleep } from "node:timers/promises";
import { apiFetch } from "../api.mjs";
import { emit } from "../output.mjs";
import { t } from "../i18n.mjs";
function truncate(v, len = 30) {
if (v == null) return "-";
const s = String(v);
return s.length > len ? s.slice(0, len - 1) + "…" : s;
}
function fmtTs(v) {
if (!v) return "-";
try {
return new Date(v).toLocaleString();
} catch {
return String(v);
}
}
const suiteSchema = [
{ key: "id", header: "Suite ID", width: 22 },
{ key: "name", header: "Name", width: 30 },
{ key: "samples", header: "Samples" },
{ key: "rubric", header: "Rubric", width: 16 },
{ key: "updatedAt", header: "Updated", formatter: fmtTs },
];
const runSchema = [
{ key: "id", header: "Run ID", width: 22 },
{ key: "suiteId", header: "Suite", width: 18 },
{ key: "status", header: "Status", width: 12 },
{ key: "model", header: "Model", width: 25 },
{ key: "score", header: "Score", formatter: (v) => (v != null ? v.toFixed(3) : "-") },
{
key: "duration",
header: "Duration",
formatter: (v) => (v != null ? `${(v / 1000).toFixed(1)}s` : "-"),
},
{ key: "startedAt", header: "Started", formatter: fmtTs },
];
const sampleSchema = [
{ key: "id", header: "Sample", width: 14 },
{ key: "score", header: "Score", formatter: (v) => (v != null ? v.toFixed(2) : "-") },
{ key: "passed", header: "✓", formatter: (v) => (v ? "✓" : "✗") },
{ key: "input", header: "Input", width: 30, formatter: truncate },
{ key: "output", header: "Output", width: 30, formatter: truncate },
];
async function confirm(q) {
return new Promise((resolve) => {
process.stdout.write(`${q} (yes/no) `);
process.stdin.setEncoding("utf8");
process.stdin.once("data", (c) => resolve(c.toString().trim().toLowerCase().startsWith("y")));
});
}
async function watchRun(runId, globalOpts) {
let lastStatus = "";
while (true) {
await sleep(3000);
const res = await apiFetch(`/api/evals/${runId}`);
if (!res.ok) continue;
const r = await res.json();
if (r.status !== lastStatus) {
const done = r.progress?.completed ?? 0;
const total = r.progress?.total ?? "?";
process.stderr.write(`[${new Date().toISOString()}] ${r.status}${done}/${total}\n`);
lastStatus = r.status;
}
if (["completed", "failed", "cancelled"].includes(r.status)) {
emit(r, globalOpts, runSchema);
return;
}
}
}
function renderScorecard(data) {
const score = data.score ?? data.overallScore ?? null;
const passed = data.passed ?? data.summary?.passed ?? null;
const total = data.total ?? data.summary?.total ?? null;
process.stdout.write("\n=== Scorecard ===\n");
if (score != null) process.stdout.write(`Overall score: ${(score * 100).toFixed(1)}%\n`);
if (passed != null && total != null) {
process.stdout.write(`Passed: ${passed}/${total}\n`);
const bar = "█".repeat(Math.round((passed / total) * 20)).padEnd(20, "░");
process.stdout.write(`[${bar}] ${((passed / total) * 100).toFixed(0)}%\n`);
}
const metrics = data.metrics ?? data.breakdown ?? {};
for (const [k, v] of Object.entries(metrics)) {
process.stdout.write(` ${k}: ${typeof v === "number" ? v.toFixed(3) : v}\n`);
}
process.stdout.write("\n");
}
export async function runEvalSuitesList(opts, cmd) {
const res = await apiFetch("/api/evals/suites");
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
const data = await res.json();
emit(data.items ?? data, cmd.optsWithGlobals(), suiteSchema);
}
export async function runEvalSuitesGet(id, opts, cmd) {
const res = await apiFetch(`/api/evals/suites/${id}`);
if (!res.ok) {
process.stderr.write(`Not found: ${id}\n`);
process.exit(1);
}
emit(await res.json(), cmd.optsWithGlobals());
}
export async function runEvalSuitesCreate(opts, cmd) {
if (!opts.file) {
process.stderr.write("--file required\n");
process.exit(2);
}
const body = JSON.parse(readFileSync(opts.file, "utf8"));
const res = await apiFetch("/api/evals/suites", { method: "POST", body });
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
emit(await res.json(), cmd.optsWithGlobals());
}
export async function runEvalRun(suiteId, opts, cmd) {
const globalOpts = cmd.optsWithGlobals();
const body = {
suiteId,
model: opts.model ?? "auto",
...(opts.combo ? { combo: opts.combo } : {}),
concurrency: opts.concurrency ?? 4,
...(opts.tag ? { tag: opts.tag } : {}),
};
const res = await apiFetch("/api/evals", { method: "POST", body });
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
const run = await res.json();
emit(run, globalOpts, runSchema);
if (opts.watch) {
if (process.stdout.isTTY) {
const { startEvalWatchTui } = await import("../tui/EvalWatch.jsx");
await startEvalWatchTui({
runId: run.id,
suiteId: opts.suite,
baseUrl: globalOpts.baseUrl ?? "http://localhost:20128",
apiKey: globalOpts.apiKey ?? process.env.OMNIROUTE_API_KEY,
});
} else {
process.stderr.write("\nWatching run... (Ctrl+C to detach)\n");
await watchRun(run.id, globalOpts);
}
}
}
export async function runEvalList(opts, cmd) {
const params = new URLSearchParams({ limit: String(opts.limit ?? 50) });
if (opts.suite) params.set("suiteId", opts.suite);
if (opts.status) params.set("status", opts.status);
if (opts.since) params.set("since", opts.since);
const res = await apiFetch(`/api/evals?${params}`);
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
const data = await res.json();
emit(data.items ?? data, cmd.optsWithGlobals(), runSchema);
}
export async function runEvalGet(id, opts, cmd) {
const res = await apiFetch(`/api/evals/${id}`);
if (!res.ok) {
process.stderr.write(`Not found: ${id}\n`);
process.exit(1);
}
emit(await res.json(), cmd.optsWithGlobals());
}
export async function runEvalResults(id, opts, cmd) {
const params = new URLSearchParams();
if (opts.failed) params.set("filter", "failed");
const res = await apiFetch(`/api/evals/${id}?${params}`);
if (!res.ok) {
process.stderr.write(`Not found: ${id}\n`);
process.exit(1);
}
const data = await res.json();
emit(data.samples ?? data.results ?? [], cmd.optsWithGlobals(), sampleSchema);
}
export async function runEvalCancel(id, opts, cmd) {
if (!opts.yes) {
const ok = await confirm(`Cancel run ${id}?`);
if (!ok) return;
}
const res = await apiFetch(`/api/evals/${id}`, { method: "POST", body: { op: "cancel" } });
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
process.stdout.write("Cancelled\n");
}
export async function runEvalScorecard(id, opts, cmd) {
const res = await apiFetch(`/api/evals/${id}?scorecard=true`);
if (!res.ok) {
process.stderr.write(`Not found: ${id}\n`);
process.exit(1);
}
const data = await res.json();
const globalOpts = cmd.optsWithGlobals();
if (globalOpts.output === "json") {
emit(data, globalOpts);
} else {
renderScorecard(data);
}
}
export function registerEval(program) {
const evalCmd = program.command("eval").description(t("eval.description"));
const suites = evalCmd.command("suites").description(t("eval.suites.description"));
suites.command("list").description(t("eval.suites.list.description")).action(runEvalSuitesList);
suites
.command("get <suiteId>")
.description(t("eval.suites.get.description"))
.action(runEvalSuitesGet);
suites
.command("create")
.description(t("eval.suites.create.description"))
.option("--file <path>", t("eval.suites.create.file"))
.action(runEvalSuitesCreate);
evalCmd
.command("run <suiteId>")
.description(t("eval.run.description"))
.option("-m, --model <id>", t("eval.run.model"), "auto")
.option("--combo <name>", t("eval.run.combo"))
.option("--concurrency <n>", t("eval.run.concurrency"), parseInt, 4)
.option("--tag <tag>", t("eval.run.tag"))
.option("--watch", t("eval.run.watch"))
.action(runEvalRun);
evalCmd
.command("list")
.description(t("eval.list.description"))
.option("--suite <id>", t("eval.list.suite"))
.option("--status <s>", t("eval.list.status"))
.option("--since <ts>", t("eval.list.since"))
.option("--limit <n>", t("eval.list.limit"), parseInt, 50)
.action(runEvalList);
evalCmd.command("get <runId>").description(t("eval.get.description")).action(runEvalGet);
evalCmd
.command("results <runId>")
.description(t("eval.results.description"))
.option("--failed", t("eval.results.failed"))
.action(runEvalResults);
evalCmd
.command("cancel <runId>")
.description(t("eval.cancel.description"))
.option("--yes", t("eval.cancel.yes"))
.action(runEvalCancel);
evalCmd
.command("scorecard <runId>")
.description(t("eval.scorecard.description"))
.action(runEvalScorecard);
}
+144
View File
@@ -0,0 +1,144 @@
import { createReadStream, readFileSync, statSync, writeFileSync } from "node:fs";
import { basename } from "node:path";
import { createInterface } from "node:readline";
import { apiFetch, getBaseUrl } from "../api.mjs";
import { emit } from "../output.mjs";
import { t } from "../i18n.mjs";
function fmtTs(v) {
if (!v) return "-";
return new Date(typeof v === "number" ? v * 1000 : v).toLocaleString();
}
function fmtBytes(n) {
if (n == null) return "-";
if (n < 1024) return `${n} B`;
if (n < 1024 ** 2) return `${(n / 1024).toFixed(1)} KB`;
if (n < 1024 ** 3) return `${(n / 1024 ** 2).toFixed(1)} MB`;
return `${(n / 1024 ** 3).toFixed(2)} GB`;
}
async function confirm(q) {
const rl = createInterface({ input: process.stdin, output: process.stdout });
return new Promise((resolve) => {
rl.question(`${q} [y/N] `, (a) => {
rl.close();
resolve(a.trim().toLowerCase() === "y");
});
});
}
function authHeaders(opts) {
const h = { accept: "application/json" };
if (opts.apiKey) h["Authorization"] = `Bearer ${opts.apiKey}`;
return h;
}
const fileSchema = [
{ key: "id", header: "File ID", width: 30 },
{ key: "filename", header: "Filename", width: 35 },
{ key: "purpose", header: "Purpose", width: 14 },
{ key: "bytes", header: "Bytes", formatter: fmtBytes },
{ key: "created_at", header: "Created", formatter: fmtTs },
{ key: "status", header: "Status" },
];
export function registerFiles(program) {
const files = program.command("files").description(t("files.description"));
files
.command("list")
.option("--purpose <p>", t("files.list.purpose"))
.option("--limit <n>", t("files.list.limit"), parseInt, 100)
.action(async (opts, cmd) => {
const params = new URLSearchParams({ limit: String(opts.limit) });
if (opts.purpose) params.set("purpose", opts.purpose);
const res = await apiFetch(`/v1/files?${params}`);
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
const data = await res.json();
emit(data.data ?? data.items ?? data, cmd.optsWithGlobals(), fileSchema);
});
files
.command("get <fileId>")
.description(t("files.get.description"))
.action(async (id, opts, cmd) => {
const res = await apiFetch(`/v1/files/${id}`);
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
emit(await res.json(), cmd.optsWithGlobals());
});
files
.command("upload <path>")
.description(t("files.upload.description"))
.requiredOption("--purpose <p>", t("files.upload.purpose"))
.action(async (filePath, opts, cmd) => {
const stat = statSync(filePath);
if (stat.size > 100 * 1024 * 1024) {
process.stderr.write(
`Warning: file is ${fmtBytes(stat.size)} (${stat.size > 500e6 ? "very " : ""}large)\n`
);
}
const globalOpts = cmd.optsWithGlobals();
const form = new FormData();
form.append("purpose", opts.purpose);
form.append("file", new Blob([readFileSync(filePath)]), basename(filePath));
const res = await fetch(`${getBaseUrl(globalOpts)}/v1/files`, {
method: "POST",
headers: authHeaders(globalOpts),
body: form,
});
if (!res.ok) {
process.stderr.write(`Upload failed: ${res.status}\n`);
process.exit(1);
}
emit(await res.json(), globalOpts);
});
files
.command("content <fileId>")
.description(t("files.content.description"))
.option("--out <path>", t("files.content.out"))
.action(async (id, opts, cmd) => {
const globalOpts = cmd.optsWithGlobals();
const res = await fetch(`${getBaseUrl(globalOpts)}/v1/files/${id}/content`, {
headers: authHeaders(globalOpts),
});
if (!res.ok) {
process.stderr.write(`HTTP ${res.status}\n`);
process.exit(1);
}
if (opts.out) {
const buf = Buffer.from(await res.arrayBuffer());
writeFileSync(opts.out, buf);
process.stdout.write(`Saved ${buf.length} bytes to ${opts.out}\n`);
} else {
process.stdout.write(await res.text());
}
});
files
.command("delete <fileId>")
.option("--yes", t("files.delete.yes"))
.action(async (id, opts, cmd) => {
if (!opts.yes) {
const ok = await confirm(`Delete file ${id}?`);
if (!ok) return;
}
const res = await apiFetch(`/v1/files/${id}`, { method: "DELETE" });
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
process.stdout.write("Deleted\n");
});
}
export { fmtBytes };
+123
View File
@@ -0,0 +1,123 @@
import { apiFetch, isServerUp } from "../api.mjs";
import { t } from "../i18n.mjs";
export function registerHealth(program) {
const health = program
.command("health")
.description(t("health.description"))
.option("-v, --verbose", "Show extended info (memory, breakers)")
.option("--json", "Output as JSON")
.option("--alerts-only", "Show only components with alerts")
.action(async (opts, cmd) => {
const globalOpts = cmd.optsWithGlobals();
const exitCode = await runHealthCommand({ ...opts, output: globalOpts.output });
if (exitCode !== 0) process.exit(exitCode);
});
health
.command("components")
.description("List health components and their status")
.option("--alerts-only", "Show only components with alerts")
.action(async (opts, cmd) => {
const globalOpts = cmd.optsWithGlobals();
const exitCode = await runHealthComponentsCommand({ ...opts, output: globalOpts.output });
if (exitCode !== 0) process.exit(exitCode);
});
health
.command("watch")
.description("Live dashboard — refresh every N seconds")
.option("--interval <s>", "Refresh interval in seconds", "5")
.action(async (opts, cmd) => {
const globalOpts = cmd.optsWithGlobals();
const interval = parseInt(opts.interval, 10) * 1000;
process.stdout.write("\x1B[2J\x1B[0f");
while (true) {
process.stdout.write("\x1B[0f");
await runHealthCommand({ ...globalOpts, verbose: true });
await new Promise((r) => setTimeout(r, interval));
}
});
}
export async function runHealthCommand(opts = {}) {
const serverUp = await isServerUp();
if (!serverUp) {
console.error(t("health.noServer"));
return 1;
}
try {
const res = await apiFetch("/api/health", { retry: false, timeout: 5000, acceptNotOk: true });
if (!res.ok) {
console.error(t("common.error", { message: `HTTP ${res.status}` }));
return 1;
}
const health = await res.json();
if (opts.json || opts.output === "json") {
console.log(JSON.stringify(health, null, 2));
return 0;
}
console.log(`\n\x1b[1m\x1b[36m${t("health.title")}\x1b[0m\n`);
console.log(t("health.status", { status: "\x1b[32mhealthy\x1b[0m" }));
if (health.uptime) console.log(t("health.uptime", { uptime: health.uptime }));
if (health.version) console.log(` Version: ${health.version}`);
if (health.requests !== undefined) {
console.log(t("health.requests", { count: health.requests }));
}
if (health.breakers && opts.verbose) {
console.log("\n \x1b[1mCircuit Breakers\x1b[0m");
for (const [name, status] of Object.entries(health.breakers)) {
const state =
status.state === "closed" ? "\x1b[32m● closed\x1b[0m" : "\x1b[33m○ open\x1b[0m";
console.log(` ${name.padEnd(20)} ${state}`);
}
}
if (health.cache && opts.verbose) {
console.log("\n \x1b[1mCache\x1b[0m");
console.log(` Semantic hits: ${health.cache.semanticHits || 0}`);
console.log(` Signature hits: ${health.cache.signatureHits || 0}`);
}
if (opts.verbose && health.memory) {
console.log("\n \x1b[1mMemory\x1b[0m");
console.log(` RSS: ${health.memory.rss || "N/A"}`);
console.log(` Heap used: ${health.memory.heapUsed || "N/A"}`);
}
return 0;
} catch (err) {
console.error(t("common.error", { message: err instanceof Error ? err.message : String(err) }));
return 1;
}
}
export async function runHealthComponentsCommand(opts = {}) {
try {
const res = await apiFetch("/api/health", { retry: false, timeout: 5000, acceptNotOk: true });
if (!res.ok) {
console.error(`HTTP ${res.status}`);
return 1;
}
const health = await res.json();
const components = health.components || health.breakers || {};
for (const [name, info] of Object.entries(components)) {
const status =
typeof info === "object" ? info.state || info.status || "unknown" : String(info);
const isAlert = status !== "closed" && status !== "ok" && status !== "healthy";
if (opts.alertsOnly && !isAlert) continue;
const icon = isAlert ? "\x1b[33m⚠\x1b[0m" : "\x1b[32m✓\x1b[0m";
console.log(` ${icon} ${name.padEnd(24)} ${status}`);
}
return 0;
} catch (err) {
console.error(err instanceof Error ? err.message : String(err));
return 1;
}
}
+599
View File
@@ -0,0 +1,599 @@
import { printHeading } from "../io.mjs";
import {
ensureProviderSchema,
getProviderApiKey,
listProviderConnections,
removeProviderConnectionByProvider,
upsertApiKeyProviderConnection,
} from "../provider-store.mjs";
import { openOmniRouteDb } from "../sqlite.mjs";
import { loadAvailableProviders } from "../provider-catalog.mjs";
import { apiFetch, isServerUp } from "../api.mjs";
import { t } from "../i18n.mjs";
function getValidProviderIds() {
try {
return new Set(loadAvailableProviders().map((p) => p.id));
} catch {
return null;
}
}
function maskKey(raw) {
if (!raw || raw.length <= 8) return "***";
return raw.slice(0, 6) + "***" + raw.slice(-4);
}
export function registerKeys(program) {
const keys = program.command("keys").description(t("keys.title"));
keys
.command("add <provider> [apiKey]")
.description(t("keys.addDescription"))
.option("--stdin", t("keys.stdinOpt"))
.action(async (provider, apiKey, opts, cmd) => {
const globalOpts = cmd.parent.optsWithGlobals();
const exitCode = await runKeysAddCommand(provider, apiKey, { ...opts, ...globalOpts });
if (exitCode !== 0) process.exit(exitCode);
});
keys
.command("list")
.description(t("keys.listDescription"))
.option("--json", t("common.jsonOpt"))
.action(async (opts, cmd) => {
const globalOpts = cmd.parent.optsWithGlobals();
const exitCode = await runKeysListCommand({ ...opts, ...globalOpts });
if (exitCode !== 0) process.exit(exitCode);
});
keys
.command("remove <provider>")
.description(t("keys.removeDescription"))
.option("--yes", t("common.yesOpt"))
.action(async (provider, opts, cmd) => {
const globalOpts = cmd.parent.optsWithGlobals();
const exitCode = await runKeysRemoveCommand(provider, { ...opts, ...globalOpts });
if (exitCode !== 0) process.exit(exitCode);
});
keys
.command("regenerate <id>")
.description(t("keys.regenerateDescription"))
.option("--yes", t("common.yesOpt"))
.action(async (id, opts, cmd) => {
const globalOpts = cmd.parent.optsWithGlobals();
const exitCode = await runKeysRegenerateCommand(id, { ...opts, ...globalOpts });
if (exitCode !== 0) process.exit(exitCode);
});
keys
.command("revoke <id>")
.description(t("keys.revokeDescription"))
.option("--yes", t("common.yesOpt"))
.action(async (id, opts, cmd) => {
const globalOpts = cmd.parent.optsWithGlobals();
const exitCode = await runKeysRevokeCommand(id, { ...opts, ...globalOpts });
if (exitCode !== 0) process.exit(exitCode);
});
keys
.command("reveal <id>")
.description(t("keys.revealDescription"))
.action(async (id, opts, cmd) => {
const globalOpts = cmd.parent.optsWithGlobals();
const exitCode = await runKeysRevealCommand(id, { ...opts, ...globalOpts });
if (exitCode !== 0) process.exit(exitCode);
});
keys
.command("usage <id>")
.description(t("keys.usageDescription"))
.option("--limit <n>", t("keys.usageLimitOpt"), "20")
.action(async (id, opts, cmd) => {
const globalOpts = cmd.parent.optsWithGlobals();
const exitCode = await runKeysUsageCommand(id, { ...opts, ...globalOpts });
if (exitCode !== 0) process.exit(exitCode);
});
const policy = keys.command("policy").description(t("keys.policy.title"));
policy
.command("show <id>")
.description(t("keys.policy.showDescription"))
.action(async (id, opts, cmd) => {
const globalOpts = cmd.parent.parent.optsWithGlobals();
const exitCode = await runKeysPolicyShowCommand(id, { ...opts, ...globalOpts });
if (exitCode !== 0) process.exit(exitCode);
});
policy
.command("set <id>")
.description(t("keys.policy.setDescription"))
.option("--rate-limit <n>", t("keys.policy.rateLimitOpt"), parseInt)
.option("--max-cost <n>", t("keys.policy.maxCostOpt"), parseFloat)
.option("--allowed-models <list>", t("keys.policy.allowedModelsOpt"))
.action(async (id, opts, cmd) => {
const globalOpts = cmd.parent.parent.optsWithGlobals();
const exitCode = await runKeysPolicySetCommand(id, { ...opts, ...globalOpts });
if (exitCode !== 0) process.exit(exitCode);
});
const expiration = keys.command("expiration").description(t("keys.expiration.title"));
expiration
.command("list")
.description(t("keys.expiration.listDescription"))
.option("--days <n>", t("keys.expiration.daysOpt"), "30")
.option("--json", t("common.jsonOpt"))
.action(async (opts, cmd) => {
const globalOpts = cmd.parent.parent.optsWithGlobals();
const exitCode = await runKeysExpirationListCommand({ ...opts, ...globalOpts });
if (exitCode !== 0) process.exit(exitCode);
});
keys
.command("rotate <id>")
.description(t("keys.rotateDescription"))
.option("--grace-period <ms>", t("keys.graceOpt"), "60000")
.option("--yes", t("common.yesOpt"))
.action(async (id, opts, cmd) => {
const globalOpts = cmd.parent.optsWithGlobals();
const exitCode = await runKeysRotateCommand(id, { ...opts, ...globalOpts });
if (exitCode !== 0) process.exit(exitCode);
});
}
export async function runKeysAddCommand(provider, apiKey, opts = {}) {
if (!provider) {
console.error(t("keys.providerRequired"));
return 1;
}
let key = apiKey;
if (opts.stdin) {
key = await readStdin();
if (!key) {
console.error(t("keys.stdinEmpty"));
return 1;
}
}
if (!key) {
console.error(t("keys.keyRequired"));
return 1;
}
const providerLower = provider.toLowerCase();
const validIds = getValidProviderIds();
if (validIds && !validIds.has(providerLower)) {
console.error(t("keys.unknownProvider", { provider: providerLower }));
return 1;
}
const serverUp = await isServerUp();
if (serverUp) {
try {
const res = await apiFetch("/api/v1/providers/keys", {
method: "POST",
body: { provider: providerLower, apiKey: key },
retry: false,
acceptNotOk: true,
});
if (res.ok) {
console.log(t("keys.added", { provider: providerLower }));
return 0;
}
if (res.status >= 400 && res.status < 500) {
console.error(t("common.error", { message: `HTTP ${res.status}` }));
return 1;
}
} catch {}
}
const { db } = await openOmniRouteDb();
try {
const existing = listProviderConnections(db).find(
(c) => c.provider === providerLower && c.authType === "apikey"
);
upsertApiKeyProviderConnection(db, {
provider: providerLower,
name: existing?.name || providerLower,
apiKey: key,
});
console.log(t("keys.added", { provider: providerLower }));
return 0;
} finally {
db.close();
}
}
export async function runKeysListCommand(opts = {}) {
const serverUp = await isServerUp();
if (serverUp) {
try {
const res = await apiFetch("/api/v1/providers/keys", { retry: false, acceptNotOk: true });
if (res.ok) {
const data = await res.json();
const connections = data.keys || data.connections || data.items || data;
if (Array.isArray(connections)) {
return _printKeysList(connections, opts);
}
}
} catch {}
}
const { db } = await openOmniRouteDb();
try {
ensureProviderSchema(db);
const connections = listProviderConnections(db).filter(
(c) => c.authType === "apikey" && c.apiKey
);
return _printKeysList(connections, opts);
} finally {
db.close();
}
}
function _printKeysList(connections, opts) {
if (opts.json || opts.output === "json") {
const rows = connections.map((c) => ({
id: c.id,
provider: c.provider,
name: c.name,
isActive: c.isActive !== false,
maskedKey: maskKey(c.apiKey || c.maskedKey || ""),
}));
console.log(JSON.stringify({ keys: rows }, null, 2));
return 0;
}
printHeading(t("keys.title"));
if (connections.length === 0) {
console.log(t("keys.noKeys"));
return 0;
}
for (const c of connections) {
let masked = c.maskedKey || "";
if (!masked && c.apiKey) {
try {
masked = maskKey(getProviderApiKey(c));
} catch {
masked = maskKey(c.apiKey);
}
}
const status = c.isActive !== false ? "\x1b[32m● enabled\x1b[0m" : "\x1b[33m○ disabled\x1b[0m";
console.log(` ${(c.provider || "").padEnd(20)} ${masked.padEnd(22)} ${status}`);
}
console.log(`\n${t("keys.listed", { count: connections.length })}`);
return 0;
}
export async function runKeysRemoveCommand(provider, opts = {}) {
if (!provider) {
console.error(t("keys.providerRequired"));
return 1;
}
const providerLower = provider.toLowerCase();
if (!opts.yes) {
const readline = await import("node:readline");
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
const answer = await new Promise((resolve) =>
rl.question(t("keys.confirmRemove", { id: providerLower }) + " [y/N] ", resolve)
);
rl.close();
if (!/^y(es)?$/i.test(answer)) {
console.log(t("common.cancelled"));
return 0;
}
}
const serverUp = await isServerUp();
if (serverUp) {
try {
const res = await apiFetch(`/api/v1/providers/keys/${encodeURIComponent(providerLower)}`, {
method: "DELETE",
retry: false,
acceptNotOk: true,
});
if (res.ok) {
console.log(t("keys.removed"));
return 0;
}
} catch {}
}
const { db } = await openOmniRouteDb();
try {
const changes = removeProviderConnectionByProvider(db, providerLower);
if (changes > 0) {
console.log(t("keys.removed"));
return 0;
}
console.log(t("keys.noKeys"));
return 0;
} finally {
db.close();
}
}
async function readStdin() {
return new Promise((resolve) => {
let data = "";
process.stdin.setEncoding("utf8");
process.stdin.on("data", (chunk) => (data += chunk));
process.stdin.on("end", () => resolve(data.trim()));
});
}
export async function runKeysRegenerateCommand(id, opts = {}) {
if (!opts.yes) {
const readline = await import("node:readline");
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
const answer = await new Promise((r) =>
rl.question(t("keys.confirmRegenerate", { id }) + " [y/N] ", r)
);
rl.close();
if (!/^y(es)?$/i.test(answer)) {
console.log(t("common.cancelled"));
return 0;
}
}
if (!(await isServerUp())) {
console.error(t("common.serverOffline"));
return 1;
}
try {
const res = await apiFetch(`/api/v1/registered-keys/${encodeURIComponent(id)}/regenerate`, {
method: "POST",
retry: false,
});
if (!res.ok) {
console.error(t("common.error", { message: `HTTP ${res.status}` }));
return 1;
}
const data = await res.json();
console.log(t("keys.regenerated", { key: data.key || data.apiKey || "(see dashboard)" }));
return 0;
} catch (err) {
console.error(t("common.error", { message: err instanceof Error ? err.message : String(err) }));
return 1;
}
}
export async function runKeysRevokeCommand(id, opts = {}) {
if (!opts.yes) {
const readline = await import("node:readline");
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
const answer = await new Promise((r) =>
rl.question(t("keys.confirmRevoke", { id }) + " [y/N] ", r)
);
rl.close();
if (!/^y(es)?$/i.test(answer)) {
console.log(t("common.cancelled"));
return 0;
}
}
if (!(await isServerUp())) {
console.error(t("common.serverOffline"));
return 1;
}
try {
const res = await apiFetch(`/api/v1/registered-keys/${encodeURIComponent(id)}/revoke`, {
method: "POST",
retry: false,
});
if (!res.ok) {
console.error(t("common.error", { message: `HTTP ${res.status}` }));
return 1;
}
console.log(t("keys.revoked", { id }));
return 0;
} catch (err) {
console.error(t("common.error", { message: err instanceof Error ? err.message : String(err) }));
return 1;
}
}
export async function runKeysRevealCommand(id, opts = {}) {
process.stderr.write(t("keys.revealWarning") + "\n");
if (!(await isServerUp())) {
console.error(t("common.serverOffline"));
return 1;
}
try {
const res = await apiFetch(`/api/v1/registered-keys/${encodeURIComponent(id)}/reveal`, {
retry: false,
});
if (!res.ok) {
console.error(t("common.error", { message: `HTTP ${res.status}` }));
return 1;
}
const data = await res.json();
console.log(data.key || data.apiKey || "(not available)");
return 0;
} catch (err) {
console.error(t("common.error", { message: err instanceof Error ? err.message : String(err) }));
return 1;
}
}
export async function runKeysUsageCommand(id, opts = {}) {
if (!(await isServerUp())) {
console.error(t("common.serverOffline"));
return 1;
}
const limit = opts.limit || "20";
try {
const res = await apiFetch(
`/api/v1/registered-keys/${encodeURIComponent(id)}/usage?limit=${limit}`,
{ retry: false }
);
if (!res.ok) {
console.error(t("common.error", { message: `HTTP ${res.status}` }));
return 1;
}
const data = await res.json();
const rows = data.usage || data.requests || data.items || [];
if (rows.length === 0) {
console.log(t("keys.noUsage"));
return 0;
}
for (const r of rows) {
const ts = r.timestamp || r.createdAt || "";
const path = r.path || r.endpoint || "";
const status = r.status || r.statusCode || "";
console.log(` ${ts} ${String(status).padEnd(4)} ${path}`);
}
return 0;
} catch (err) {
console.error(t("common.error", { message: err instanceof Error ? err.message : String(err) }));
return 1;
}
}
export async function runKeysPolicyShowCommand(id, opts = {}) {
if (!(await isServerUp())) {
console.error(t("common.serverOffline"));
return 1;
}
try {
const res = await apiFetch(`/api/v1/registered-keys/${encodeURIComponent(id)}/policy`, {
acceptNotOk: true,
retry: false,
});
if (!res.ok) {
console.error(t("common.error", { message: `HTTP ${res.status}` }));
return 1;
}
const data = await res.json();
if (opts.output === "json" || opts.json) {
console.log(JSON.stringify(data, null, 2));
return 0;
}
console.log(t("keys.policy.title") + ` (${id}):`);
console.log(` rate_limit: ${data.rateLimit ?? data.rate_limit ?? "(unset)"}`);
console.log(` max_cost: ${data.maxCost ?? data.max_cost ?? "(unset)"}`);
console.log(
` allowed_models: ${(data.allowedModels ?? data.allowed_models ?? []).join(", ") || "(all)"}`
);
return 0;
} catch (err) {
console.error(t("common.error", { message: err instanceof Error ? err.message : String(err) }));
return 1;
}
}
export async function runKeysPolicySetCommand(id, opts = {}) {
const body = {};
if (opts.rateLimit != null) body.rateLimit = Number(opts.rateLimit);
if (opts.maxCost != null) body.maxCost = Number(opts.maxCost);
if (opts.allowedModels) body.allowedModels = opts.allowedModels.split(",").map((s) => s.trim());
if (Object.keys(body).length === 0) {
console.error(t("keys.policy.nothingToSet"));
return 1;
}
if (!(await isServerUp())) {
console.error(t("common.serverOffline"));
return 1;
}
try {
const res = await apiFetch(`/api/v1/registered-keys/${encodeURIComponent(id)}/policy`, {
method: "PATCH",
body,
acceptNotOk: true,
retry: false,
});
if (!res.ok) {
console.error(t("common.error", { message: `HTTP ${res.status}` }));
return 1;
}
console.log(t("keys.policy.updated"));
return 0;
} catch (err) {
console.error(t("common.error", { message: err instanceof Error ? err.message : String(err) }));
return 1;
}
}
export async function runKeysExpirationListCommand(opts = {}) {
if (!(await isServerUp())) {
console.error(t("common.serverOffline"));
return 1;
}
const days = Number(opts.days || 30);
try {
const res = await apiFetch(`/api/v1/registered-keys?expiring=true&days=${days}`, {
acceptNotOk: true,
retry: false,
});
if (!res.ok) {
console.error(t("common.error", { message: `HTTP ${res.status}` }));
return 1;
}
const data = await res.json();
const rows = data.keys || data.items || data;
if (!Array.isArray(rows) || rows.length === 0) {
console.log(t("keys.expiration.none", { days }));
return 0;
}
if (opts.json || opts.output === "json") {
console.log(JSON.stringify(rows, null, 2));
return 0;
}
console.log(t("keys.expiration.listTitle", { days }));
for (const k of rows) {
const exp = k.expiresAt || k.expires_at || "(unknown)";
console.log(` ${(k.id || "").padEnd(24)} ${(k.name || "").padEnd(20)} expires: ${exp}`);
}
return 0;
} catch (err) {
console.error(t("common.error", { message: err instanceof Error ? err.message : String(err) }));
return 1;
}
}
export async function runKeysRotateCommand(id, opts = {}) {
if (!opts.yes) {
const readline = await import("node:readline");
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
const answer = await new Promise((r) =>
rl.question(t("keys.confirmRotate", { id }) + " [y/N] ", r)
);
rl.close();
if (!/^y(es)?$/i.test(answer)) {
console.log(t("common.cancelled"));
return 0;
}
}
if (!(await isServerUp())) {
console.error(t("common.serverOffline"));
return 1;
}
const gracePeriod = Number(opts.gracePeriod || 60000);
try {
const res = await apiFetch(`/api/v1/registered-keys/${encodeURIComponent(id)}/rotate`, {
method: "POST",
body: { gracePeriodMs: gracePeriod },
acceptNotOk: true,
retry: false,
});
if (!res.ok) {
console.error(t("common.error", { message: `HTTP ${res.status}` }));
return 1;
}
const data = await res.json();
const newId = data.newKeyId || data.id || "(see dashboard)";
console.log(t("keys.rotated", { id, newId }));
return 0;
} catch (err) {
console.error(t("common.error", { message: err instanceof Error ? err.message : String(err) }));
return 1;
}
}
+201
View File
@@ -0,0 +1,201 @@
import { spawn } from "node:child_process";
import { t } from "../i18n.mjs";
import { resolveActiveContext } from "../contexts.mjs";
/** 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
* free-claude-code's codex adapter. NOTE: this does NOT silence codex's
* `refresh_token` log noise — that comes from a stored OpenAI session in
* ~/.codex/auth.json, not the env; it is cosmetic and does not block requests. */
const STRIPPED_CODEX_ENV_KEYS = [
"OPENAI_API_KEY",
"OPENAI_BASE_URL",
"OPENAI_API_BASE",
"OPENAI_ORG_ID",
"OPENAI_ORGANIZATION",
"CODEX_API_KEY",
];
/** Placeholder so codex's `env_key` is always satisfied when the backend is open. */
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.
export function resolveCodexSpawn(platform) {
if (platform === "win32") {
return { command: "codex.cmd", shell: true };
}
return { command: "codex", shell: undefined };
}
function stripTrailingSlash(value) {
let s = String(value);
let end = s.length;
while (end > 0 && s.charCodeAt(end - 1) === 47) end--;
return end === s.length ? s : s.slice(0, end);
}
/** TOML assignment for a `-c key=value` codex flag (strings get quoted). */
function tomlAssign(key, value) {
if (typeof value === "boolean" || typeof value === "number") return `${key}=${value}`;
return `${key}=${JSON.stringify(String(value))}`;
}
/**
* Resolve the OmniRoute root base URL + auth for codex, honouring (in order):
* explicit flags → active context (remote mode) → localhost:<port>.
* @returns {{ baseUrl:string, authToken:string|undefined }}
*/
export function resolveCodexTarget(opts = {}) {
const explicit = opts.remote ?? opts.baseUrl;
let baseUrl;
if (explicit) {
baseUrl = stripTrailingSlash(explicit).replace(/\/v1$/, "");
} else {
let fromCtx;
try {
fromCtx = resolveActiveContext(opts.context ?? process.env.OMNIROUTE_CONTEXT)?.baseUrl;
} catch {
/* no context */
}
baseUrl = fromCtx
? stripTrailingSlash(fromCtx).replace(/\/v1$/, "")
: `http://localhost:${Number(opts.port ?? process.env.PORT ?? 20128) || 20128}`;
}
let authToken = opts.apiKey ?? opts["api-key"];
if (!authToken) {
try {
const ctx = resolveActiveContext(opts.context ?? process.env.OMNIROUTE_CONTEXT);
authToken = ctx?.accessToken || ctx?.apiKey || undefined;
} catch {
/* no context auth */
}
}
if (!authToken) authToken = process.env.OMNIROUTE_API_KEY;
return { baseUrl, authToken };
}
/** Health-check an OmniRoute root URL before launching Codex. */
async function healthCheck(baseUrl, timeoutMs = 3000) {
try {
const res = await fetch(`${baseUrl}/api/monitoring/health`, {
signal: AbortSignal.timeout(timeoutMs),
});
return res.ok;
} catch {
return false;
}
}
/**
* Build the env for the Codex child: strip stale OpenAI/Codex creds, then set
* OMNIROUTE_API_KEY (the provider env_key) to the resolved token or a sentinel.
* @param {Record<string,string>} baseEnv
* @param {string|undefined} authToken
* @returns {Record<string,string>}
*/
export function buildCodexEnv(baseEnv, authToken) {
const env = { ...baseEnv };
for (const key of STRIPPED_CODEX_ENV_KEYS) delete env[key];
env.OMNIROUTE_API_KEY = (authToken && String(authToken).trim()) || NO_AUTH_SENTINEL;
return env;
}
/**
* Codex `-c` flags that define the `omniroute` provider inline, so launch works
* WITHOUT a pre-existing ~/.codex/config.toml. Mirrors free-claude-code.
* @param {string} baseUrl OmniRoute root URL (no /v1)
* @returns {string[]}
*/
export function buildCodexProviderArgs(baseUrl) {
return [
"-c",
tomlAssign("model_provider", "omniroute"),
"-c",
tomlAssign("model_providers.omniroute.name", "OmniRoute"),
"-c",
tomlAssign("model_providers.omniroute.base_url", `${baseUrl}/v1`),
"-c",
tomlAssign("model_providers.omniroute.env_key", "OMNIROUTE_API_KEY"),
"-c",
tomlAssign("model_providers.omniroute.wire_api", "responses"),
"-c",
tomlAssign("model_providers.omniroute.requires_openai_auth", false),
];
}
/**
* @param {{port?:string, remote?:string, profile?:string, apiKey?:string}} opts
* @param {string[]} codexArgs pass-through args for the codex binary
* @returns {Promise<number>} exit code
*/
export async function runLaunchCodexCommand(opts = {}, codexArgs = []) {
const { baseUrl, authToken } = resolveCodexTarget(opts);
if (!(await healthCheck(baseUrl))) {
console.error(
(
t("launch.notRunning") ||
"OmniRoute is not reachable at {port}. Start it with 'omniroute serve'."
).replace("{port}", baseUrl)
);
return 1;
}
// Provider injected via -c (works without config.toml); then the profile (model),
// then the user's pass-through args.
const providerArgs = buildCodexProviderArgs(baseUrl);
const profileArgs = opts.profile ? ["--profile", opts.profile] : [];
const extraArgs = [...providerArgs, ...profileArgs, ...codexArgs];
const env = buildCodexEnv(process.env, authToken);
return await new Promise((resolve) => {
const { command: codexLaunch, shell: shellValue } = resolveCodexSpawn(process.platform);
const child = spawn(codexLaunch, extraArgs, {
env,
stdio: "inherit",
shell: shellValue,
});
child.on("error", (err) => {
if (err?.code === "ENOENT") {
console.error(
"The 'codex' CLI was not found in PATH. Install with:\n npm install -g @openai/codex"
);
resolve(127);
} else {
console.error(String(err?.message || err));
resolve(1);
}
});
child.on("exit", (code) => resolve(code ?? 0));
});
}
export function registerLaunchCodex(program) {
program
.command("launch-codex")
.description(
t("launchCodex.description") || "Launch Codex CLI pointed at OmniRoute (local or remote VPS)"
)
.option("--port <port>", "Local OmniRoute port (ignored when --remote is set)", "20128")
.option(
"--remote <url>",
"Remote OmniRoute base URL, e.g. http://192.168.0.15:20128 (overrides --port + context)"
)
.option("--profile <name>", "Codex profile to activate (passed as --profile <name>)")
.option("-p, --p <name>", "Alias for --profile")
.option(
"--api-key <key>",
"OmniRoute API key (overrides OMNIROUTE_API_KEY env var for this invocation)"
)
.allowUnknownOption(true)
.allowExcessArguments(true)
.argument("[codexArgs...]", "arguments passed through to the codex binary")
.action(async (codexArgs, opts) => {
const merged = { ...opts, profile: opts.profile ?? opts.p };
const exitCode = await runLaunchCodexCommand(merged, codexArgs ?? []);
if (exitCode !== 0) process.exit(exitCode);
});
}
+155
View File
@@ -0,0 +1,155 @@
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";
function stripTrailingSlash(value) {
let s = String(value);
let end = s.length;
while (end > 0 && s.charCodeAt(end - 1) === 47) end--;
return end === s.length ? s : s.slice(0, end);
}
/**
* Build a clean child env for Claude Code pointed at OmniRoute.
*
* Strips inherited ANTHROPIC_* (avoids a stale shell token leaking through), then
* injects the base URL, gateway model discovery, and auto-compact window.
*
* @param {Record<string,string>} baseEnv
* @param {number|string} baseUrlOrPort a port (→ http://localhost:<port>) or a full base URL
* @param {string|undefined} authToken
* @param {{ configDir?:string, model?:string }} [opts]
* @returns {Record<string,string>}
*/
export function buildClaudeEnv(baseEnv, baseUrlOrPort, authToken, opts = {}) {
const env = { ...baseEnv };
for (const key of Object.keys(env)) {
if (key.startsWith("ANTHROPIC_")) delete env[key];
}
// Accept a bare port (number/numeric string → localhost) or a full base URL.
// Claude Code wants the ROOT URL (it appends /v1/messages itself) — no /v1 here.
let baseUrl;
if (typeof baseUrlOrPort === "number" || /^\d+$/.test(String(baseUrlOrPort))) {
baseUrl = `http://localhost:${Number(baseUrlOrPort) || 20128}`;
} else {
baseUrl = stripTrailingSlash(String(baseUrlOrPort)).replace(/\/v1$/, "");
}
env.ANTHROPIC_BASE_URL = baseUrl;
// Always set a token: when none is resolved, a sentinel keeps newer Claude Code
// from stopping at its local login gate before it ever contacts OmniRoute (an
// open backend ignores the value). Mirrors free-claude-code. ANTHROPIC_API_KEY
// stays stripped (above) so it can't shadow the Bearer token.
env.ANTHROPIC_AUTH_TOKEN = (authToken && String(authToken).trim()) || "omniroute-no-auth";
env.CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY = "1";
env.CLAUDE_CODE_AUTO_COMPACT_WINDOW = "190000";
// Profile isolation (Claude Code has no native profiles — CLAUDE_CONFIG_DIR is
// the idiomatic mechanism: separate settings/credentials/history/cache per dir).
if (opts.configDir) env.CLAUDE_CONFIG_DIR = opts.configDir;
if (opts.model) env.ANTHROPIC_MODEL = opts.model;
return env;
}
/**
* Resolve the OmniRoute base URL + auth for launch, honouring (in order):
* explicit flags → the active context (remote mode) → localhost:<port>.
* @param {{port?:string, remote?:string, baseUrl?:string, token?:string, apiKey?:string, context?:string}} opts
* @returns {{ baseUrl:string, authToken:string|undefined }}
*/
export function resolveLaunchTarget(opts = {}) {
const explicit = opts.remote ?? opts.baseUrl;
let baseUrl;
if (explicit) {
baseUrl = stripTrailingSlash(explicit).replace(/\/v1$/, "");
} else {
let fromCtx;
try {
const ctx = resolveActiveContext(opts.context ?? process.env.OMNIROUTE_CONTEXT);
fromCtx = ctx?.baseUrl;
} catch {
/* no context */
}
baseUrl = fromCtx
? stripTrailingSlash(fromCtx).replace(/\/v1$/, "")
: `http://localhost:${Number(opts.port ?? process.env.PORT ?? 20128) || 20128}`;
}
let authToken = opts.token ?? opts.apiKey ?? opts["api-key"];
if (!authToken) {
try {
const ctx = resolveActiveContext(opts.context ?? process.env.OMNIROUTE_CONTEXT);
authToken = ctx?.accessToken || ctx?.apiKey || undefined;
} catch {
/* no context auth */
}
}
if (!authToken) authToken = process.env.ANTHROPIC_AUTH_TOKEN ?? process.env.OMNIROUTE_API_KEY;
return { baseUrl, authToken };
}
/**
* @param {{port?:string, remote?:string, token?:string, apiKey?:string, profile?:string, claudeHome?:string}} opts
* @param {string[]} claudeArgs pass-through args for the claude binary
* @returns {Promise<number>} exit code
*/
export async function runLaunchCommand(opts = {}, claudeArgs = []) {
const { baseUrl, authToken } = resolveLaunchTarget(opts);
// Health check the (possibly remote) proxy before launching.
try {
const res = await fetch(`${baseUrl}/api/monitoring/health`, {
signal: AbortSignal.timeout(3000),
});
if (!res.ok) throw new Error(`status ${res.status}`);
} catch {
console.error(
(t("launch.notRunning") || "OmniRoute is not reachable at {port}. Start it with 'omniroute serve'.").replace(
"{port}",
baseUrl
)
);
return 1;
}
const configDir = opts.profile
? join(opts.claudeHome || join(os.homedir(), ".claude"), "profiles", opts.profile)
: undefined;
const env = buildClaudeEnv(process.env, baseUrl, authToken, { configDir });
return await new Promise((resolve) => {
const child = spawn("claude", claudeArgs, { env, stdio: "inherit" });
child.on("error", (err) => {
if (err && err.code === "ENOENT") {
console.error(t("launch.notFound") || "The 'claude' CLI was not found in PATH.");
resolve(127);
} else {
console.error(String(err?.message || err));
resolve(1);
}
});
child.on("exit", (code) => resolve(code ?? 0));
});
}
export function registerLaunch(program) {
program
.command("launch")
.description(
t("launch.description") || "Launch Claude Code pointed at OmniRoute (local or remote)"
)
.option("--port <port>", t("serve.port") || "Proxy port", "20128")
.option("--remote <url>", "Remote OmniRoute base URL (overrides --port and the active context)")
.option("--profile <name>", "Claude Code profile to use (CLAUDE_CONFIG_DIR ~/.claude/profiles/<name>)")
.option("--token <token>", t("launch.token") || "Token Claude sends (ANTHROPIC_AUTH_TOKEN)")
.option("--api-key <key>", "Alias for --token (OmniRoute access token / API key)")
.allowUnknownOption(true)
.allowExcessArguments(true)
.argument("[claudeArgs...]", "arguments passed through to the claude binary")
.action(async (claudeArgs, opts) => {
const exitCode = await runLaunchCommand(opts, claudeArgs ?? []);
if (exitCode !== 0) process.exit(exitCode);
});
}
+192
View File
@@ -0,0 +1,192 @@
import { createServer } from "node:http";
import { randomUUID } from "node:crypto";
/**
* `omniroute login antigravity` — local OAuth helper for remote installs.
*
* Why this exists: Google's `firstparty/nativeapp` consent for the embedded
* Antigravity desktop client only releases the authorization code when the
* loopback redirect (127.0.0.1:<port>) is REACHABLE. On a remote VPS install the
* loopback is unreachable, so the consent hangs forever and never emits a code —
* the dashboard's "paste the callback URL" fallback has nothing to paste. (The
* same flow works locally and over an SSH tunnel, where the loopback IS reachable.)
*
* This command runs the OAuth on the user's OWN machine — where 127.0.0.1 works —
* captures the code on a local loopback server, exchanges it for tokens, and
* prints a single-line credential blob. The user pastes that blob into the remote
* dashboard (Antigravity → "Paste credentials"), which decodes it, finalizes the
* onboarding server-side, and persists the connection.
*
* 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.
*/
const PROVIDER = "antigravity";
/** Open the system browser; no-op if the optional `open` dependency is missing. */
async function defaultOpenBrowser(url) {
try {
const { default: open } = await import("open");
await open(url);
} catch {
// `open` not available — the caller already printed the URL to paste manually.
}
}
/**
* Start a loopback HTTP server bound to 127.0.0.1 (NOT 0.0.0.0 — we never want to
* expose the callback to the LAN). Resolves to { port, waitForCallback, close }.
*/
function defaultStartServer(preferredPort) {
return new Promise((resolve, reject) => {
let resolveCallback;
const callbackPromise = new Promise((r) => {
resolveCallback = r;
});
const server = createServer((req, res) => {
const url = new URL(req.url, "http://127.0.0.1");
if (url.pathname !== "/callback" && url.pathname !== "/auth/callback") {
res.writeHead(404).end();
return;
}
const params = Object.fromEntries(url.searchParams.entries());
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\">" +
"<h2>✅ Authorization received</h2>" +
"<p>Return to your terminal — you can close this tab.</p></body>"
);
resolveCallback(params);
});
server.on("error", reject);
server.listen(preferredPort || 0, "127.0.0.1", () => {
const { port } = server.address();
resolve({
port,
waitForCallback: () => callbackPromise,
close: () => new Promise((r) => server.close(() => r())),
});
});
});
}
/** Lazy-load the antigravity provider + blob codec (TS source via tsx). */
async function loadDeps() {
const { antigravity } = await import("../../../src/lib/oauth/providers/antigravity.ts");
const { encodeCredentialBlob } = await import("../../../src/lib/oauth/credentialBlob.ts");
return { antigravity, encodeCredentialBlob };
}
/**
* Build the Google authorization request for a given loopback port. Uses a plain
* authorization_code grant (NO PKCE code_challenge) — matching the working flow:
* a code_challenge here would force the exchange to require a code_verifier.
*/
export async function buildAntigravityAuthRequest(port, makeState = randomUUID) {
const { antigravity } = await loadDeps();
const redirectUri = `http://127.0.0.1:${port}/callback`;
const state = makeState();
const authUrl = antigravity.buildAuthUrl(antigravity.config, redirectUri, state);
return { redirectUri, state, authUrl };
}
/** Exchange the captured code for raw Google tokens (no code_verifier — no PKCE). */
export async function exchangeAntigravityCode(code, redirectUri) {
const { antigravity } = await loadDeps();
return antigravity.exchangeToken(antigravity.config, code, redirectUri);
}
/**
* Orchestrate the local login. Dependencies are injectable for testing; the real
* path uses a 127.0.0.1 loopback server, the system browser, and a live token
* exchange against Google. Returns the credential blob string.
*/
export async function runAntigravityLogin(opts = {}, deps = {}) {
const startServer = deps.startServer ?? defaultStartServer;
const openBrowser = deps.openBrowser ?? defaultOpenBrowser;
const exchange = deps.exchange ?? exchangeAntigravityCode;
const makeState = deps.makeState ?? randomUUID;
const print = deps.print ?? ((s) => process.stdout.write(s));
const log = deps.log ?? ((s) => process.stderr.write(s));
const { encodeCredentialBlob } = await loadDeps();
const server = await startServer(opts.port);
const { redirectUri, state, authUrl } = await buildAntigravityAuthRequest(server.port, makeState);
log(`\nOpen this URL to authorize Antigravity (it will open automatically):\n\n ${authUrl}\n\n`);
if (opts.browser !== false) await openBrowser(authUrl);
log("Waiting for Google to redirect back to the local loopback...\n");
const timeoutMs = opts.timeout ?? 300000;
let timer;
let params;
try {
params = await Promise.race([
server.waitForCallback(),
new Promise((_, reject) => {
timer = setTimeout(
() => reject(new Error("Timed out waiting for the OAuth callback")),
timeoutMs
);
// Don't keep the event loop alive solely for this timer.
if (typeof timer.unref === "function") timer.unref();
}),
]);
} finally {
clearTimeout(timer);
await server.close();
}
if (params.error) {
throw new Error(`Authorization failed: ${params.error_description || params.error}`);
}
if (params.state !== state) {
throw new Error("State mismatch — aborting (possible CSRF). Please retry the login.");
}
if (!params.code) {
throw new Error("No authorization code returned by Google.");
}
const tokens = await exchange(params.code, redirectUri);
const blob = encodeCredentialBlob({ provider: PROVIDER, tokens });
print(
"\n" +
"Antigravity authorized. Copy the line below and paste it into your remote\n" +
"OmniRoute dashboard: Providers → Antigravity → Connect → \"Paste credentials\".\n" +
"(This contains a refresh token — treat it like a password.)\n\n" +
blob +
"\n\n"
);
return blob;
}
async function runLoginAntigravity(opts) {
try {
await runAntigravityLogin({
browser: opts.browser,
timeout: opts.timeout,
port: opts.port,
});
} catch (err) {
process.stderr.write(`\nLogin failed: ${err?.message || err}\n`);
process.exit(1);
}
}
export function registerLogin(program) {
const login = program
.command("login")
.description("Local OAuth helpers for remote OmniRoute installs (run on your own machine)");
login
.command("antigravity")
.description("Authorize Antigravity locally and print a credential blob to paste remotely")
.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)
.action(runLoginAntigravity);
}
+183
View File
@@ -0,0 +1,183 @@
import { writeFileSync, appendFileSync, existsSync, unlinkSync } from "node:fs";
import { t } from "../i18n.mjs";
import { getBaseUrl, buildHeaders } from "../api.mjs";
export function registerLogs(program) {
program
.command("logs")
.description(t("logs.description"))
.option("--follow", t("logs.follow"))
.option("--filter <level>", t("logs.filter"))
.option("--lines <n>", t("logs.lines"), "100")
.option("--timeout <ms>", t("logs.timeout"), "30000")
.option("--base-url <url>", t("logs.baseUrl"))
.option("--request-id <id>", t("logs.requestId"))
.option("--api-key <key>", t("logs.apiKey"))
.option("--combo <name>", t("logs.combo"))
.option("--status <code>", t("logs.status"))
.option("--duration-min <ms>", t("logs.durationMin"), parseInt)
.option("--duration-max <ms>", t("logs.durationMax"), parseInt)
.option("--export <path>", t("logs.export"))
.action(async (opts, cmd) => {
const globalOpts = cmd.optsWithGlobals();
// `--context` and `--output` are global options, so forward them explicitly:
// runLogsCommand resolves the base URL via getBaseUrl({ context }), and without
// this a user's `--context` would be silently dropped.
const exitCode = await runLogsCommand({
...opts,
context: globalOpts.context,
output: globalOpts.output,
});
if (exitCode !== 0) process.exit(exitCode);
});
}
function buildLogFilter(opts) {
const levelFilters = opts.filter ? opts.filter.split(",").map((f) => f.trim()) : [];
const requestId = opts.requestId;
const apiKey = opts.apiKey;
const combo = opts.combo;
const statusFilter = opts.status != null ? String(opts.status) : null;
const durationMin = opts.durationMin != null ? Number(opts.durationMin) : null;
const durationMax = opts.durationMax != null ? Number(opts.durationMax) : null;
return function matchesLog(parsed) {
if (levelFilters.length > 0) {
const level = String(parsed.level || "info").toLowerCase();
if (!levelFilters.includes(level)) return false;
}
if (requestId) {
const rid = String(parsed.requestId || parsed.request_id || "");
if (!rid.includes(requestId)) return false;
}
if (apiKey) {
const key = String(parsed.apiKey || parsed.api_key || parsed.key || "");
if (!key.includes(apiKey)) return false;
}
if (combo) {
const c = String(parsed.combo || parsed.comboName || parsed.combo_name || "");
if (!c.includes(combo)) return false;
}
if (statusFilter) {
const s = String(parsed.status || parsed.statusCode || parsed.status_code || "");
if (!s.startsWith(statusFilter)) return false;
}
if (durationMin != null) {
const d = Number(parsed.duration || parsed.durationMs || parsed.latency || 0);
if (d < durationMin) return false;
}
if (durationMax != null) {
const d = Number(parsed.duration || parsed.durationMs || parsed.latency || 0);
if (d > durationMax) return false;
}
return true;
};
}
export async function runLogsCommand(opts = {}) {
// Resolve the base URL the same way every other CLI command does: an explicit
// --base-url wins, otherwise fall back to the active context / env / localhost.
// Without this, `logs` always hit localhost and ignored a connected remote.
const baseUrl = opts.baseUrl || opts["base-url"] || getBaseUrl({ context: opts.context });
const follow = opts.follow ?? false;
const timeout = parseInt(String(opts.timeout || "30000"), 10);
const isJson = opts.output === "json";
const exportPath = opts.export;
// Prepare export file
if (exportPath && existsSync(exportPath)) {
unlinkSync(exportPath);
}
const matchesLog = buildLogFilter(opts);
// Pass only level filters to the stream (server-side); other filters are client-side
const levelFilters = opts.filter ? opts.filter.split(",").map((f) => f.trim()) : [];
// Authenticate the log stream. The /api/cli-tools/logs endpoint requires the
// management token; build the same headers (scoped context token + CLI token)
// that apiFetch uses, so `logs` works against authenticated/remote servers.
// NOTE: --api-key here is a client-side log *filter* (see buildLogFilter), not
// an auth credential, so it is deliberately not forwarded to buildHeaders.
const headers = await buildHeaders({ baseUrl, context: opts.context });
const { createLogStream } = await import("../../../src/lib/cli-helper/log-streamer.js");
const { stream, stop } = createLogStream({
baseUrl,
filters: levelFilters,
follow,
timeout,
headers,
});
const reader = stream.getReader();
const decoder = new TextDecoder();
let buffer = "";
const processLine = (line) => {
if (!line.trim()) return;
let parsed = null;
try {
parsed = JSON.parse(line);
} catch {
// Non-JSON line: only include if no structured filters active
if (
opts.requestId ||
opts.apiKey ||
opts.combo ||
opts.status ||
opts.durationMin != null ||
opts.durationMax != null
)
return;
if (exportPath) appendFileSync(exportPath, line + "\n", "utf8");
else console.log(line);
return;
}
if (!matchesLog(parsed)) return;
if (exportPath) {
appendFileSync(exportPath, JSON.stringify(parsed) + "\n", "utf8");
return;
}
if (isJson) {
console.log(JSON.stringify(parsed));
return;
}
const level = parsed.level || "info";
const ts = parsed.timestamp || new Date().toISOString();
const msg = parsed.message || JSON.stringify(parsed);
const prefix =
{ error: "\x1b[31m[ERR]", warn: "\x1b[33m[WRN]", info: "\x1b[36m[INF]" }[level] || "[INF]";
console.log(`${prefix}\x1b[0m ${ts} ${msg}`);
};
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const parts = buffer.split("\n");
buffer = parts.pop() || "";
for (const line of parts) processLine(line);
}
if (buffer) processLine(buffer);
if (exportPath) console.log(t("logs.exported", { path: exportPath }));
} catch (err) {
if (err.name === "AbortError") {
console.log(t("logs.stopped"));
} else {
console.error(
t("logs.streamError", {
message: (err instanceof Error ? err.message : String(err)).slice(0, 100),
})
);
}
} finally {
stop();
}
return 0;
}
+273
View File
@@ -0,0 +1,273 @@
import { readFileSync } from "node:fs";
import { apiFetch, isServerUp } from "../api.mjs";
import { emit } from "../output.mjs";
import { t } from "../i18n.mjs";
function truncate(v, len = 60) {
if (v == null) return "-";
const s = String(v);
return s.length > len ? s.slice(0, len - 1) + "…" : s;
}
const mcpToolSchema = [
{ key: "name", header: "Tool", width: 36 },
{
key: "scopes",
header: "Scopes",
formatter: (v) => (Array.isArray(v) ? v.join(",") : (v ?? "-")),
},
{ key: "auditLevel", header: "Audit", width: 10 },
{ key: "phase", header: "Phase", width: 6 },
{ key: "description", header: "Description", formatter: truncate },
];
export function registerMcp(program) {
const mcp = program.command("mcp").description(t("mcp.title"));
mcp
.command("status")
.description("Show MCP server status")
.option("--json", "Output as JSON")
.action(async (opts, cmd) => {
const globalOpts = cmd.parent.optsWithGlobals();
const exitCode = await runMcpStatusCommand({ ...opts, output: globalOpts.output });
if (exitCode !== 0) process.exit(exitCode);
});
mcp
.command("restart")
.description("Restart the MCP server")
.action(async (opts, cmd) => {
const globalOpts = cmd.parent.optsWithGlobals();
const exitCode = await runMcpRestartCommand({ ...opts, output: globalOpts.output });
if (exitCode !== 0) process.exit(exitCode);
});
// 5.1 — mcp call + mcp scopes
mcp
.command("call <tool> [argsJson]")
.description(t("mcp.call.description"))
.option("--args <json>", t("mcp.call.args"))
.option("--args-file <path>", t("mcp.call.args_file"))
.option("--stream", t("mcp.call.stream"))
.option("--scope <s>", t("mcp.call.scope"), (v, prev = []) => [...prev, v], [])
.action(async (tool, argsPositional, opts, cmd) => {
const globalOpts = cmd.optsWithGlobals();
const args = opts.args
? JSON.parse(opts.args)
: opts.argsFile
? JSON.parse(readFileSync(opts.argsFile, "utf8"))
: argsPositional
? JSON.parse(argsPositional)
: {};
if (opts.stream) {
await runMcpStream(tool, args, globalOpts);
return;
}
const extraHeaders = opts.scope?.length ? { "X-MCP-Scopes": opts.scope.join(",") } : {};
const res = await apiFetch("/api/mcp/tools/call", {
method: "POST",
body: { name: tool, arguments: args },
headers: extraHeaders,
});
if (res.status === 403) {
process.stderr.write("Scope denied\n");
process.exit(4);
}
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
const data = await res.json();
emit(data, globalOpts);
});
mcp
.command("scopes")
.description(t("mcp.scopes.description"))
.option("--tool <name>", t("mcp.scopes.tool"))
.action(async (opts, cmd) => {
const params = new URLSearchParams({ meta: "scopes" });
if (opts.tool) params.set("tool", opts.tool);
const res = await apiFetch(`/api/mcp/tools?${params}`);
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
const data = await res.json();
emit(data.scopes ?? data, cmd.optsWithGlobals());
});
// 5.2 — mcp tools + mcp audit
const tools = mcp.command("tools").description(t("mcp.tools.description"));
tools
.command("list")
.description(t("mcp.tools.list.description"))
.option("--scope <s>", t("mcp.tools.list.scope"))
.action(async (opts, cmd) => {
const params = new URLSearchParams();
if (opts.scope) params.set("scope", opts.scope);
const res = await apiFetch(`/api/mcp/tools?${params}`);
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
const data = await res.json();
emit(data.tools ?? data, cmd.optsWithGlobals(), mcpToolSchema);
});
tools
.command("info <name>")
.description(t("mcp.tools.info.description"))
.action(async (name, opts, cmd) => {
const res = await apiFetch(`/api/mcp/tools?name=${encodeURIComponent(name)}`);
if (!res.ok) {
process.stderr.write(`Not found: ${name}\n`);
process.exit(1);
}
emit(await res.json(), cmd.optsWithGlobals());
});
tools
.command("schema <name>")
.description(t("mcp.tools.schema.description"))
.option("--io <kind>", t("mcp.tools.schema.io"), "input")
.action(async (name, opts, cmd) => {
const res = await apiFetch(`/api/mcp/tools?name=${encodeURIComponent(name)}&io=${opts.io}`);
if (!res.ok) {
process.stderr.write(`Not found: ${name}\n`);
process.exit(1);
}
const data = await res.json();
const globalOpts = cmd.optsWithGlobals();
if (globalOpts.output === "json") {
process.stdout.write(JSON.stringify(data.schema ?? data, null, 2) + "\n");
} else {
emit(data.schema ?? data, globalOpts);
}
});
const audit = mcp.command("audit").description(t("mcp.audit.description"));
audit
.command("tail")
.option("--follow", t("audit.tail.follow"))
.option("--limit <n>", t("audit.tail.limit"), parseInt, 100)
.action(async (opts, cmd) => {
const { runAuditTail } = await import("./audit.mjs");
await runAuditTail({ ...opts, source: "mcp" }, cmd);
});
audit
.command("stats")
.option("--period <p>", t("audit.stats.period"), "7d")
.action(async (opts, cmd) => {
const res = await apiFetch(`/api/mcp/audit/stats?period=${opts.period}`);
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
emit(await res.json(), cmd.optsWithGlobals());
});
}
async function runMcpStream(tool, args, globalOpts) {
const baseUrl = globalOpts.baseUrl ?? "http://localhost:20128";
const apiKey = globalOpts.apiKey ?? "";
const res = await fetch(`${baseUrl}/api/mcp/stream`, {
method: "POST",
headers: {
"Content-Type": "application/json",
...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {}),
},
body: JSON.stringify({ name: tool, arguments: args }),
});
if (!res.ok) {
process.stderr.write(`HTTP ${res.status}\n`);
process.exit(1);
}
const reader = res.body.getReader();
const dec = new TextDecoder();
let buf = "";
while (true) {
const { done, value } = await reader.read();
if (done) break;
buf += dec.decode(value, { stream: true });
const lines = buf.split("\n");
buf = lines.pop() ?? "";
for (const line of lines) {
if (line.startsWith("data: ")) {
const raw = line.slice(6).trim();
if (raw && raw !== "[DONE]") process.stdout.write(raw + "\n");
}
}
}
}
export async function runMcpStatusCommand(opts = {}) {
const serverUp = await isServerUp();
if (!serverUp) {
console.error(t("common.serverOffline"));
return 1;
}
try {
const res = await apiFetch("/api/mcp/status", {
retry: false,
timeout: 5000,
acceptNotOk: true,
});
if (!res.ok) {
console.log(t("mcp.stopped"));
return 0;
}
const status = await res.json();
if (opts.json || opts.output === "json") {
console.log(JSON.stringify(status, null, 2));
return 0;
}
const transport = status.transport || "stdio";
console.log(status.running ? t("mcp.running", { transport }) : t("mcp.stopped"));
if (status.toolsCount !== undefined) console.log(` Tools: ${status.toolsCount}`);
if (status.scopes?.length) {
console.log(" Scopes:");
for (const scope of status.scopes) console.log(` - ${scope}`);
}
return 0;
} catch (err) {
console.error(t("common.error", { message: err instanceof Error ? err.message : String(err) }));
return 1;
}
}
export async function runMcpRestartCommand(opts = {}) {
const serverUp = await isServerUp();
if (!serverUp) {
console.error(t("common.serverOffline"));
return 1;
}
try {
const res = await apiFetch("/api/mcp/restart", {
method: "POST",
retry: false,
timeout: 10000,
acceptNotOk: true,
});
if (res.ok) {
console.log(t("mcp.restarted"));
return 0;
}
console.error(t("common.error", { message: `HTTP ${res.status}` }));
return 1;
} catch (err) {
console.error(t("common.error", { message: err instanceof Error ? err.message : String(err) }));
return 1;
}
}
+244
View File
@@ -0,0 +1,244 @@
import { readFileSync } from "node:fs";
import { apiFetch } from "../api.mjs";
import { emit } from "../output.mjs";
import { t } from "../i18n.mjs";
const VALID_TYPES = ["factual", "episodic", "procedural", "semantic"];
const LEGACY_TYPE_MAP = {
user: "factual",
feedback: "factual",
project: "factual",
reference: "factual",
};
/**
* Plan 21 Bug#4/D17 fix: remap legacy types in ALL CLI subcommands
* (search/list/clear in addition to add), with a stderr warning on remap.
* Returns the canonical type, or the original value (which the backend will
* 400 on if invalid) — never throws.
*/
function applyLegacyTypeMap(type) {
if (!type) return type;
if (Object.prototype.hasOwnProperty.call(LEGACY_TYPE_MAP, type)) {
const mapped = LEGACY_TYPE_MAP[type];
process.stderr.write(
`Warning: legacy type '${type}' is deprecated; using '${mapped}'. Use --type factual|episodic|procedural|semantic.\n`
);
return mapped;
}
if (!VALID_TYPES.includes(type)) {
process.stderr.write(
`Warning: unknown type '${type}'. Valid types: factual, episodic, procedural, semantic.\n`
);
}
return type;
}
function truncate(v, len = 60) {
if (v == null) return "-";
const s = String(v);
return s.length > len ? s.slice(0, len - 1) + "…" : s;
}
function fmtTs(v) {
if (!v) return "-";
try {
return new Date(v).toLocaleString();
} catch {
return String(v);
}
}
const memorySchema = [
{ key: "id", header: "ID", width: 14 },
{ key: "type", header: "Type", width: 12 },
{ key: "content", header: "Content", width: 60, formatter: truncate },
{ key: "score", header: "Score", formatter: (v) => (v != null ? v.toFixed(3) : "-") },
{ key: "createdAt", header: "Created", formatter: fmtTs },
];
function parseDuration(s) {
const m = String(s).match(/^(\d+)(d|m|y)$/i);
if (!m) return null;
const n = parseInt(m[1], 10);
const unit = m[2].toLowerCase();
const now = Date.now();
if (unit === "d") return new Date(now - n * 86400000).toISOString();
if (unit === "m") return new Date(now - n * 30 * 86400000).toISOString();
if (unit === "y") return new Date(now - n * 365 * 86400000).toISOString();
return null;
}
async function confirm(question) {
return new Promise((resolve) => {
process.stdout.write(`${question} (yes/no) `);
process.stdin.setEncoding("utf8");
process.stdin.once("data", (chunk) => {
resolve(chunk.toString().trim().toLowerCase().startsWith("y"));
});
});
}
export async function runMemorySearch(query, opts, cmd) {
const globalOpts = cmd.optsWithGlobals();
const params = new URLSearchParams({ q: query, limit: String(opts.limit ?? 20) });
const mappedSearchType = applyLegacyTypeMap(opts.type);
if (mappedSearchType) params.set("type", mappedSearchType);
if (opts.apiKey) params.set("apiKey", opts.apiKey);
if (opts.tokenBudget) params.set("tokenBudget", String(opts.tokenBudget));
const res = await apiFetch(`/api/memory?${params}`);
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
const data = await res.json();
emit(data.items ?? data, globalOpts, memorySchema);
}
export async function runMemoryAdd(opts, cmd) {
const globalOpts = cmd.optsWithGlobals();
const content = opts.content ?? (opts.file ? readFileSync(opts.file, "utf8") : null);
if (!content) {
process.stderr.write("--content or --file required\n");
process.exit(2);
}
const resolvedType = opts.type ? applyLegacyTypeMap(opts.type) : "factual";
const body = {
content,
type: resolvedType,
...(opts.metadata ? { metadata: JSON.parse(opts.metadata) } : {}),
...(opts.apiKey ? { apiKey: opts.apiKey } : {}),
};
const res = await apiFetch("/api/memory", { method: "POST", body });
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
const created = await res.json();
emit(created, globalOpts, memorySchema);
}
export async function runMemoryClear(opts, cmd) {
const globalOpts = cmd.optsWithGlobals();
if (!opts.yes) {
const ok = await confirm("This will delete memories. Continue?");
if (!ok) process.exit(0);
}
const params = new URLSearchParams();
const mappedClearType = applyLegacyTypeMap(opts.type);
if (mappedClearType) params.set("type", mappedClearType);
if (opts.olderThan) {
const iso = parseDuration(opts.olderThan);
if (!iso) {
process.stderr.write(`Invalid --older-than value: ${opts.olderThan}\n`);
process.exit(2);
}
params.set("olderThan", iso);
}
if (opts.apiKey) params.set("apiKey", opts.apiKey);
const res = await apiFetch(`/api/memory?${params}`, { method: "DELETE" });
const data = await res.json();
emit(data, globalOpts);
}
export async function runMemoryList(opts, cmd) {
const globalOpts = cmd.optsWithGlobals();
const params = new URLSearchParams({ limit: String(opts.limit ?? 100) });
const mappedListType = applyLegacyTypeMap(opts.type);
if (mappedListType) params.set("type", mappedListType);
if (opts.apiKey) params.set("apiKey", opts.apiKey);
const res = await apiFetch(`/api/memory?${params}`);
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
const data = await res.json();
emit(data.items ?? data, globalOpts, memorySchema);
}
export async function runMemoryGet(id, opts, cmd) {
const globalOpts = cmd.optsWithGlobals();
const res = await apiFetch(`/api/memory/${id}`);
if (!res.ok) {
process.stderr.write(`Not found: ${id}\n`);
process.exit(1);
}
const data = await res.json();
emit(data, globalOpts, memorySchema);
}
export async function runMemoryDelete(id, opts, cmd) {
const globalOpts = cmd.optsWithGlobals();
if (!opts.yes) {
const ok = await confirm(`Delete memory ${id}?`);
if (!ok) process.exit(0);
}
const res = await apiFetch(`/api/memory/${id}`, { method: "DELETE" });
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
process.stdout.write(`Deleted: ${id}\n`);
}
export async function runMemoryHealth(opts, cmd) {
const globalOpts = cmd.optsWithGlobals();
const res = await apiFetch("/api/memory/health");
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
const data = await res.json();
emit(data, globalOpts);
}
export function registerMemory(program) {
const memory = program.command("memory").description(t("memory.description"));
memory
.command("search <query>")
.description(t("memory.search.description"))
.option("--type <type>", t("memory.search.type"))
.option("--limit <n>", t("memory.search.limit"), parseInt, 20)
.option("--api-key <key>", t("memory.search.api_key"))
.option("--token-budget <n>", t("memory.search.token_budget"), parseInt)
.action(runMemorySearch);
memory
.command("add")
.description(t("memory.add.description"))
.option("--content <text>", t("memory.add.content"))
.option("--file <path>", t("memory.add.file"))
.option("--type <type>", t("memory.add.type"))
.option("--metadata <json>", t("memory.add.metadata"))
.option("--api-key <key>", t("memory.add.api_key"))
.action(runMemoryAdd);
memory
.command("clear")
.description(t("memory.clear.description"))
.option("--type <type>", t("memory.clear.type"))
.option("--older-than <duration>", t("memory.clear.older"))
.option("--api-key <key>", t("memory.clear.api_key"))
.option("--yes", t("memory.clear.yes"))
.action(runMemoryClear);
memory
.command("list")
.description(t("memory.list.description"))
.option("--type <type>", t("memory.list.type"))
.option("--limit <n>", t("memory.list.limit"), parseInt, 100)
.option("--api-key <key>", t("memory.list.api_key"))
.action(runMemoryList);
memory.command("get <id>").description(t("memory.get.description")).action(runMemoryGet);
memory
.command("delete <id>")
.description(t("memory.delete.description"))
.option("--yes", t("memory.delete.yes"))
.action(runMemoryDelete);
memory.command("health").description(t("memory.health.description")).action(runMemoryHealth);
}
+92
View File
@@ -0,0 +1,92 @@
import { apiFetch, isServerUp } from "../api.mjs";
import { emit } from "../output.mjs";
import { modelListSchema } from "../schemas/output-schemas.mjs";
import { t } from "../i18n.mjs";
export function registerModels(program) {
program
.command("models [provider]")
.description(t("models.description"))
.option("--search <query>", t("models.search"))
.option("--json", "Output as JSON")
.action(async (provider, opts, cmd) => {
const globalOpts = cmd.optsWithGlobals();
const exitCode = await runModelsCommand(provider, { ...opts, output: globalOpts.output });
if (exitCode !== 0) process.exit(exitCode);
});
}
export async function runModelsCommand(provider, opts = {}) {
const serverUp = await isServerUp();
if (!serverUp) {
console.error(t("models.noServer"));
return 1;
}
let models = [];
try {
const res = await apiFetch("/api/models", { retry: false, timeout: 5000, acceptNotOk: true });
if (res.ok) {
const data = await res.json();
models = Array.isArray(data) ? data : data.models || [];
}
} catch {}
if (models.length === 0) {
try {
const res = await apiFetch("/api/v1/models", {
retry: false,
timeout: 5000,
acceptNotOk: true,
});
if (res.ok) {
const data = await res.json();
models = Array.isArray(data) ? data : data.data || [];
}
} catch {}
}
if (provider) {
const filter = provider.toLowerCase();
models = models.filter(
(m) =>
(m.provider && m.provider.toLowerCase().includes(filter)) ||
(m.id && m.id.toLowerCase().startsWith(filter)) ||
(m.name && m.name.toLowerCase().includes(filter))
);
}
if (opts.search) {
const search = opts.search.toLowerCase();
models = models.filter(
(m) =>
(m.id && m.id.toLowerCase().includes(search)) ||
(m.name && m.name.toLowerCase().includes(search)) ||
(m.provider && m.provider.toLowerCase().includes(search)) ||
(m.description && m.description.toLowerCase().includes(search))
);
}
if (models.length === 0) {
console.log(t("models.noModels"));
return 0;
}
const normalized = models.map((m) => ({
id: m.id || m.name || "unknown",
provider: m.provider || "unknown",
contextWindow: String(m.context_length || m.max_tokens || m.contextWindow || "-"),
}));
const display = normalized.slice(0, 50);
emit(display, opts, modelListSchema);
if (models.length > 50) {
console.log(
`\x1b[2m ... and ${models.length - 50} more. Use --output json for full list.\x1b[0m`
);
}
return 0;
}
+177
View File
@@ -0,0 +1,177 @@
import { createInterface } from "node:readline";
import { apiFetch } from "../api.mjs";
import { emit } from "../output.mjs";
import { t } from "../i18n.mjs";
function fmtTs(v) {
if (!v) return "-";
return new Date(typeof v === "number" ? v * 1000 : v).toLocaleString();
}
async function confirm(q) {
const rl = createInterface({ input: process.stdin, output: process.stdout });
return new Promise((resolve) => {
rl.question(`${q} [y/N] `, (a) => {
rl.close();
resolve(a.trim().toLowerCase() === "y");
});
});
}
function parseHeader(kv) {
const eq = kv.indexOf("=");
if (eq < 0) return { name: kv, value: "" };
return { name: kv.slice(0, eq), value: kv.slice(eq + 1) };
}
const nodeSchema = [
{ key: "id", header: "Node ID", width: 22 },
{ key: "provider", header: "Provider", width: 16 },
{ key: "name", header: "Name", width: 24 },
{ key: "baseUrl", header: "Base URL", width: 38 },
{ key: "region", header: "Region", width: 14 },
{ key: "weight", header: "Weight" },
{ key: "enabled", header: "Enabled", formatter: (v) => (v ? "✓" : "✗") },
{ key: "lastLatencyMs", header: "Latency", formatter: (v) => (v ? `${v}ms` : "-") },
];
export function registerNodes(program) {
const nodes = program
.command("nodes")
.alias("provider-nodes")
.description(t("nodes.description"));
nodes
.command("list")
.option("--provider <p>", t("nodes.list.provider"))
.option("--enabled", t("nodes.list.enabled"))
.action(async (opts, cmd) => {
const params = new URLSearchParams();
if (opts.provider) params.set("provider", opts.provider);
if (opts.enabled) params.set("enabled", "true");
const res = await apiFetch(`/api/provider-nodes?${params}`);
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
const data = await res.json();
emit(data.items ?? data, cmd.optsWithGlobals(), nodeSchema);
});
nodes.command("get <nodeId>").action(async (id, opts, cmd) => {
const res = await apiFetch(`/api/provider-nodes/${id}`);
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
emit(await res.json(), cmd.optsWithGlobals());
});
nodes
.command("add")
.requiredOption("--provider <p>", t("nodes.add.provider"))
.requiredOption("--base-url <url>", t("nodes.add.baseUrl"))
.option("--name <n>", t("nodes.add.name"))
.option("--weight <w>", t("nodes.add.weight"), parseInt, 100)
.option("--region <r>", t("nodes.add.region"))
.option(
"--auth-header <kv>",
t("nodes.add.authHeader"),
(v, prev = []) => [...prev, parseHeader(v)],
[]
)
.action(async (opts, cmd) => {
const body = {
provider: opts.provider,
baseUrl: opts.baseUrl,
name: opts.name,
weight: opts.weight,
region: opts.region,
enabled: true,
headers: opts.authHeader?.length ? opts.authHeader : undefined,
};
const res = await apiFetch("/api/provider-nodes", { method: "POST", body });
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
emit(await res.json(), cmd.optsWithGlobals());
});
nodes
.command("update <nodeId>")
.option("--base-url <url>", t("nodes.update.baseUrl"))
.option("--name <n>", t("nodes.update.name"))
.option("--weight <w>", t("nodes.update.weight"), parseInt)
.option("--region <r>", t("nodes.update.region"))
.option("--enabled <b>", t("nodes.update.enabled"), (v) => v === "true")
.action(async (id, opts, cmd) => {
const body = {};
for (const k of ["baseUrl", "name", "weight", "region", "enabled"]) {
if (opts[k] !== undefined) body[k] = opts[k];
}
const res = await apiFetch(`/api/provider-nodes/${id}`, { method: "PUT", body });
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
emit(await res.json(), cmd.optsWithGlobals());
});
nodes
.command("remove <nodeId>")
.option("--yes", t("nodes.remove.yes"))
.action(async (id, opts, cmd) => {
if (!opts.yes) {
const ok = await confirm(`Remove node ${id}?`);
if (!ok) return;
}
const res = await apiFetch(`/api/provider-nodes/${id}`, { method: "DELETE" });
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
process.stdout.write("Removed\n");
});
nodes
.command("validate")
.requiredOption("--base-url <url>", t("nodes.validate.baseUrl"))
.requiredOption("--provider <p>", t("nodes.validate.provider"))
.action(async (opts, cmd) => {
const res = await apiFetch("/api/provider-nodes/validate", {
method: "POST",
body: { baseUrl: opts.baseUrl, provider: opts.provider },
});
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
emit(await res.json(), cmd.optsWithGlobals());
});
nodes
.command("test <nodeId>")
.description(t("nodes.test.description"))
.action(async (id, opts, cmd) => {
const res = await apiFetch(`/api/provider-nodes/${id}?test=true`);
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
emit(await res.json(), cmd.optsWithGlobals());
});
nodes
.command("metrics <nodeId>")
.description(t("nodes.metrics.description"))
.option("--period <p>", t("nodes.metrics.period"), "24h")
.action(async (id, opts, cmd) => {
const res = await apiFetch(`/api/provider-nodes/${id}?metrics=true&period=${opts.period}`);
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
emit(await res.json(), cmd.optsWithGlobals());
});
}
+258
View File
@@ -0,0 +1,258 @@
import { setTimeout as sleep } from "node:timers/promises";
import { apiFetch } from "../api.mjs";
import { emit } from "../output.mjs";
import { t } from "../i18n.mjs";
const PROVIDERS_WITH_OAUTH = [
{ id: "gemini", name: "Google Gemini", flow: "browser" },
{ id: "antigravity", name: "Antigravity", flow: "browser" },
{ id: "windsurf", name: "Windsurf", flow: "browser" },
{ id: "qwen", name: "Qwen Code", flow: "browser" },
{ 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: "device" },
{ id: "codex", name: "OpenAI Codex (OAuth)", flow: "device" },
{ id: "copilot", name: "GitHub Copilot", flow: "device" },
];
const oauthProviderSchema = [
{ key: "id", header: "Provider ID", width: 16 },
{ key: "name", header: "Name", width: 28 },
{ key: "flow", header: "Flow", width: 10 },
];
const connectionSchema = [
{ key: "id", header: "Connection ID", width: 22 },
{ key: "provider", header: "Provider", width: 16 },
{ key: "name", header: "Name", width: 24 },
{ key: "isActive", header: "Active", formatter: (v) => (v ? "✓" : "✗") },
{ key: "testStatus", header: "Status", width: 12 },
];
async function openBrowser(url) {
try {
const { default: open } = await import("open");
await open(url);
} catch {
// open package not available, ignore silently
}
}
async function pollStatus(endpoint, timeoutMs) {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
await sleep(2000);
const res = await apiFetch(endpoint);
if (!res.ok) continue;
const data = await res.json();
if (data.status === "complete" || data.status === "completed") return data;
if (data.status === "error" || data.status === "failed") {
process.stderr.write(`OAuth failed: ${data.error ?? data.message ?? "unknown"}\n`);
process.exit(1);
}
}
process.stderr.write("Timeout waiting for OAuth callback\n");
process.exit(124);
}
async function runBrowserFlow(def, opts) {
const startRes = await apiFetch(`/api/oauth/${def.id}/start`, { method: "POST" });
if (!startRes.ok) {
process.stderr.write(`Failed to start OAuth for ${def.id}: ${startRes.status}\n`);
process.exit(1);
}
const start = await startRes.json();
const url = start.authorizeUrl ?? start.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(
`Authorized: ${result.email ?? result.userId ?? result.account ?? "connected"}\n`
);
}
async function runImportFlow(def, opts) {
const endpoint = opts.importFromSystem
? `/api/oauth/${def.id}/auto-import`
: `/api/oauth/${def.id}/import`;
const res = await apiFetch(endpoint, { method: "POST" });
if (!res.ok) {
process.stderr.write(`Import failed: ${res.status}\n`);
process.exit(1);
}
const data = await res.json();
process.stdout.write(`Imported ${data.count ?? 0} connection(s) from ${def.name}\n`);
}
async function runSocialFlow(def, opts) {
let social = opts.social;
if (!social) {
process.stderr.write("--social <google|github> required for kiro\n");
process.exit(2);
}
const startRes = await apiFetch(`/api/oauth/${def.id}/social-authorize`, {
method: "POST",
body: { social },
});
if (!startRes.ok) {
process.stderr.write(`Failed: ${startRes.status}\n`);
process.exit(1);
}
const start = await startRes.json();
const url = start.authorizeUrl ?? start.url;
process.stdout.write(`\nOpen this URL:\n ${url}\n\n`);
if (opts.browser !== false) await openBrowser(url);
process.stderr.write("Waiting for social authorization...\n");
const result = await pollStatus(
`/api/oauth/${def.id}/social-exchange?state=${encodeURIComponent(start.state ?? "")}`,
opts.timeout ?? 300000
);
process.stdout.write(`Authorized: ${result.email ?? result.userId ?? "connected"}\n`);
}
async function runDeviceFlow(def, opts) {
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`);
process.exit(1);
}
const start = await startRes.json();
process.stdout.write(
`\nDevice code: ${start.userCode ?? start.user_code ?? ""}\nVisit: ${start.verificationUri ?? start.verification_uri}\n\n`
);
if (opts.browser !== false)
await openBrowser(start.verificationUri ?? start.verification_uri ?? "");
process.stderr.write("Waiting for device authorization...\n");
const deadline = Date.now() + (opts.timeout ?? 300000);
const intervalMs = (start.intervalMs ?? start.interval ?? 5) * 1000;
while (Date.now() < deadline) {
await sleep(intervalMs);
const statusRes = await apiFetch(
`/api/providers/${providerKey}/auth/status?state=${encodeURIComponent(start.state ?? "")}`
);
if (!statusRes.ok) continue;
const status = await statusRes.json();
if (status.status === "complete" || status.status === "authorized") {
await apiFetch(`/api/providers/${providerKey}/auth/apply`, {
method: "POST",
body: { state: start.state },
});
process.stdout.write(`Authorized: ${status.account ?? status.email ?? "connected"}\n`);
return;
}
if (status.status === "error") {
process.stderr.write(`Device auth failed: ${status.error}\n`);
process.exit(1);
}
}
process.stderr.write("Timeout\n");
process.exit(124);
}
export async function runOAuthStart(opts, cmd) {
const def = PROVIDERS_WITH_OAUTH.find((p) => p.id === opts.provider);
if (!def) {
process.stderr.write(
`Unknown OAuth provider: ${opts.provider}\nRun: omniroute oauth providers\n`
);
process.exit(2);
}
switch (def.flow) {
case "browser":
return runBrowserFlow(def, opts);
case "import":
return runImportFlow(def, opts);
case "social":
return runSocialFlow(def, opts);
case "device":
return runDeviceFlow(def, opts);
}
}
export async function runOAuthStatus(opts, cmd) {
const globalOpts = cmd.optsWithGlobals();
const params = new URLSearchParams();
if (opts.provider) params.set("provider", opts.provider);
const res = await apiFetch(`/api/providers?${params}`);
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
const data = await res.json();
const connections = (data.providers ?? data.items ?? data).filter(
(c) => c.authType === "oauth" || c.authType === "oauth2"
);
emit(connections, globalOpts, connectionSchema);
}
export async function runOAuthRevoke(opts, cmd) {
if (!opts.yes) {
process.stdout.write(
`Revoke OAuth for ${opts.provider}${opts.connectionId ? ` (${opts.connectionId})` : ""}? (yes/no) `
);
const answer = await new Promise((resolve) => {
process.stdin.setEncoding("utf8");
process.stdin.once("data", (c) => resolve(c.toString().trim().toLowerCase()));
});
if (!answer.startsWith("y")) process.exit(0);
}
const id = opts.connectionId;
const res = id
? await apiFetch(`/api/providers/${id}`, { method: "DELETE" })
: await apiFetch(`/api/oauth/${opts.provider}/revoke`, { method: "POST" });
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
process.stdout.write(`Revoked\n`);
}
export function registerOAuth(program) {
const oauth = program.command("oauth").description(t("oauth.description"));
oauth
.command("providers")
.description(t("oauth.providers.description"))
.action(async (opts, cmd) => {
emit(PROVIDERS_WITH_OAUTH, cmd.optsWithGlobals(), oauthProviderSchema);
});
oauth
.command("start")
.description(t("oauth.start.description"))
.requiredOption("--provider <id>", t("oauth.start.provider"))
.option("--no-browser", t("oauth.start.no_browser"))
.option("--import-from-system", t("oauth.start.import_system"))
.option("--social <s>", t("oauth.start.social"))
.option("--timeout <ms>", t("oauth.start.timeout"), parseInt, 300000)
.action(runOAuthStart);
oauth
.command("status")
.description(t("oauth.status.description"))
.option("--provider <id>", t("oauth.status.provider"))
.action(runOAuthStatus);
oauth
.command("revoke")
.description(t("oauth.revoke.description"))
.requiredOption("--provider <id>", t("oauth.revoke.provider"))
.option("--connection-id <id>", t("oauth.revoke.connection_id"))
.option("--yes", t("oauth.revoke.yes"))
.action(runOAuthRevoke);
}
+120
View File
@@ -0,0 +1,120 @@
import { apiFetch } from "../api.mjs";
import { emit } from "../output.mjs";
import { t } from "../i18n.mjs";
function fmtTs(v) {
if (!v) return "-";
return new Date(typeof v === "number" ? v * 1000 : v).toLocaleString();
}
async function mcpCall(name, args) {
const res = await apiFetch("/api/mcp/tools/call", {
method: "POST",
body: { name, arguments: args },
});
if (!res.ok) {
process.stderr.write(`MCP error: ${res.status}\n`);
process.exit(1);
}
return res.json();
}
const proxySchema = [
{ key: "host", header: "Host", width: 35 },
{ key: "type", header: "Type", width: 8 },
{ key: "region", header: "Region" },
{ key: "latencyMs", header: "Latency", formatter: (v) => (v ? `${v}ms` : "-") },
{
key: "successRate",
header: "Success%",
formatter: (v) => (v ? `${(v * 100).toFixed(0)}%` : "-"),
},
{ key: "lastUsed", header: "Last Used", formatter: fmtTs },
{ key: "state", header: "State" },
];
export function registerOneProxy(program) {
const op = program.command("oneproxy").description(t("oneproxy.description"));
op.command("status").action(async (opts, cmd) => {
const data = await mcpCall("omniroute_oneproxy_stats", {});
emit(data, cmd.optsWithGlobals());
});
op.command("stats")
.option("--provider <p>", t("oneproxy.stats.provider"))
.option("--period <p>", t("oneproxy.stats.period"), "24h")
.action(async (opts, cmd) => {
const data = await mcpCall("omniroute_oneproxy_stats", {
provider: opts.provider,
period: opts.period,
});
emit(data, cmd.optsWithGlobals());
});
op.command("fetch")
.description(t("oneproxy.fetch.description"))
.option("--count <n>", t("oneproxy.fetch.count"), parseInt, 1)
.option("--type <t>", t("oneproxy.fetch.type"), "http")
.action(async (opts, cmd) => {
const data = await mcpCall("omniroute_oneproxy_fetch", {
count: opts.count,
type: opts.type,
});
emit(data.proxies ?? data, cmd.optsWithGlobals(), proxySchema);
});
op.command("rotate")
.description(t("oneproxy.rotate.description"))
.option("--provider <p>", t("oneproxy.rotate.provider"))
.option("--connection-id <id>", t("oneproxy.rotate.connectionId"))
.action(async (opts, cmd) => {
const data = await mcpCall("omniroute_oneproxy_rotate", {
provider: opts.provider,
connectionId: opts.connectionId,
});
emit(data, cmd.optsWithGlobals());
});
const config = op.command("config").description(t("oneproxy.config.description"));
config.command("show").action(async (opts, cmd) => {
const res = await apiFetch("/api/settings/oneproxy");
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
emit(await res.json(), cmd.optsWithGlobals());
});
config
.command("set")
.option("--enabled <b>", t("oneproxy.config.enabled"), (v) => v === "true")
.option("--pool-size <n>", t("oneproxy.config.poolSize"), parseInt)
.option("--provider-source <url>", t("oneproxy.config.providerSource"))
.option("--rotation-policy <p>", t("oneproxy.config.rotationPolicy"))
.action(async (opts, cmd) => {
const body = {};
for (const k of ["enabled", "poolSize", "providerSource", "rotationPolicy"]) {
if (opts[k] !== undefined) body[k] = opts[k];
}
const res = await apiFetch("/api/settings/oneproxy", { method: "PUT", body });
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
emit(await res.json(), cmd.optsWithGlobals());
});
op.command("pool")
.description(t("oneproxy.pool.description"))
.action(async (opts, cmd) => {
const res = await apiFetch("/api/settings/oneproxy?include=pool");
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
const data = await res.json();
emit(data.pool ?? data, cmd.optsWithGlobals(), proxySchema);
});
}
+75
View File
@@ -0,0 +1,75 @@
import { detectRestrictedEnvironment } from "../utils/environment.mjs";
import { t } from "../i18n.mjs";
const RESOURCES = {
combos: "/dashboard/combos",
providers: "/dashboard/providers",
"api-manager": "/dashboard/api-manager",
"cli-tools": "/dashboard/cli-tools",
agents: "/dashboard/agents",
settings: "/dashboard/settings",
logs: "/dashboard/logs",
memory: "/dashboard/memory",
skills: "/dashboard/skills",
evals: "/dashboard/evals",
audit: "/dashboard/audit",
cost: "/dashboard/cost",
resilience: "/dashboard/resilience",
pricing: "/dashboard/pricing",
tunnels: "/dashboard/tunnels",
quota: "/dashboard/quota",
};
export function registerOpen(program) {
program
.command("open [resource] [id]")
.description(t("open.description"))
.option("--url", t("open.url"))
.action(async (resource, id, opts, cmd) => {
const globalOpts = cmd.optsWithGlobals();
const baseUrl = globalOpts.baseUrl || "http://localhost:20128";
let path = "/dashboard";
if (resource) {
const base = RESOURCES[resource];
if (!base) {
process.stderr.write(
`Unknown resource: ${resource}\nAvailable: ${Object.keys(RESOURCES).join(", ")}\n`
);
process.exit(2);
}
path = base;
if (id) {
if (resource === "logs") {
path += `?request=${encodeURIComponent(id)}`;
} else if (resource === "settings") {
path += `/${encodeURIComponent(id)}`;
} else {
path += `/${encodeURIComponent(id)}`;
}
}
}
const url = `${baseUrl}${path}`;
if (opts.url) {
process.stdout.write(url + "\n");
return;
}
const env = detectRestrictedEnvironment();
if (!env.canOpenBrowser) {
process.stdout.write(url + "\n");
if (env.hint) process.stderr.write(`[${env.type}] ${env.hint}\n`);
return;
}
try {
const openPkg = (await import("open")).default;
await openPkg(url);
process.stderr.write(`Opening: ${url}\n`);
} catch {
process.stdout.write(url + "\n");
}
});
}
+168
View File
@@ -0,0 +1,168 @@
import { readFileSync, writeFileSync } from "node:fs";
import { apiFetch } from "../api.mjs";
import { emit } from "../output.mjs";
import { t } from "../i18n.mjs";
function truncate(v, max = 40) {
if (!v) return "-";
const s = String(v);
return s.length > max ? s.slice(0, max - 1) + "…" : s;
}
function toYaml(obj, indent = 0) {
const pad = " ".repeat(indent);
if (obj === null || obj === undefined) return "null";
if (typeof obj === "boolean") return String(obj);
if (typeof obj === "number") return String(obj);
if (typeof obj === "string") {
if (/[\n:#{}[\],&*?|<>=!%@`]/.test(obj) || obj.trim() !== obj) {
return JSON.stringify(obj);
}
return obj || '""';
}
if (Array.isArray(obj)) {
if (obj.length === 0) return "[]";
return obj.map((v) => `\n${pad}- ${toYaml(v, indent + 1)}`).join("");
}
const entries = Object.entries(obj);
if (entries.length === 0) return "{}";
return entries
.map(([k, v]) => {
const safeKey = /[^a-zA-Z0-9_-]/.test(k) ? JSON.stringify(k) : k;
if (v !== null && typeof v === "object") {
const nested = toYaml(v, indent + 1);
if (Array.isArray(v) && v.length > 0) return `\n${pad}${safeKey}:${nested}`;
if (!Array.isArray(v) && Object.keys(v).length > 0) return `\n${pad}${safeKey}:\n${nested}`;
return `\n${pad}${safeKey}: ${nested}`;
}
return `\n${pad}${safeKey}: ${toYaml(v, indent + 1)}`;
})
.join("")
.trimStart();
}
function validateBasic(spec) {
if (!spec || typeof spec !== "object") throw new Error("spec is not an object");
if (!spec.openapi && !spec.swagger) throw new Error("missing openapi/swagger version field");
if (!spec.info) throw new Error("missing info object");
if (!spec.paths) throw new Error("missing paths object");
}
const endpointSchema = [
{ key: "method", header: "Method", width: 8 },
{ key: "path", header: "Path", width: 45 },
{ key: "operationId", header: "Operation ID", width: 25 },
{ key: "summary", header: "Summary", width: 40, formatter: (v) => truncate(v, 40) },
];
export function registerOpenapi(program) {
const api = program.command("openapi").description(t("openapi.description"));
api
.command("dump")
.description(t("openapi.dump.description"))
.option("--format <f>", t("openapi.dump.format"), "yaml")
.option("--out <path>", t("openapi.dump.out"))
.action(async (opts, cmd) => {
const res = await apiFetch("/api/openapi/spec");
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
const data = await res.json();
const serialized =
opts.format === "yaml" ? toYaml(data) + "\n" : JSON.stringify(data, null, 2);
if (opts.out) {
writeFileSync(opts.out, serialized);
process.stdout.write(`Saved to ${opts.out}\n`);
} else {
process.stdout.write(serialized);
}
});
api
.command("validate")
.description(t("openapi.validate.description"))
.action(async (opts, cmd) => {
const res = await apiFetch("/api/openapi/spec");
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
const spec = await res.json();
try {
validateBasic(spec);
process.stdout.write("Spec is valid\n");
} catch (err) {
process.stderr.write(`Invalid: ${err.message}\n`);
process.exit(1);
}
});
api
.command("try <path>")
.description(t("openapi.try.description"))
.option("--method <m>", t("openapi.try.method"), "GET")
.option("--body <file>", t("openapi.try.body"))
.option("--query <kv>", t("openapi.try.query"), (v, prev = []) => [...prev, v.split("=")], [])
.option("--header <kv>", t("openapi.try.header"), (v, prev = []) => [...prev, v.split("=")], [])
.action(async (path, opts, cmd) => {
const body = opts.body ? JSON.parse(readFileSync(opts.body, "utf8")) : undefined;
const query = Object.fromEntries(opts.query ?? []);
const headers = Object.fromEntries(opts.header ?? []);
const res = await apiFetch("/api/openapi/try", {
method: "POST",
body: { path, method: opts.method, body, query, headers },
});
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
emit(await res.json(), cmd.optsWithGlobals());
});
api
.command("endpoints")
.description(t("openapi.endpoints.description"))
.option("--search <q>", t("openapi.endpoints.search"))
.action(async (opts, cmd) => {
const res = await apiFetch("/api/openapi/spec");
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
const spec = await res.json();
const rows = [];
for (const [path, methods] of Object.entries(spec.paths ?? {})) {
for (const [method, def] of Object.entries(methods)) {
if (["parameters", "summary"].includes(method)) continue;
const summary = def.summary ?? def.description ?? "";
if (
opts.search &&
!path.includes(opts.search) &&
!summary.toLowerCase().includes(opts.search.toLowerCase())
)
continue;
rows.push({ method: method.toUpperCase(), path, summary, operationId: def.operationId });
}
}
emit(rows, cmd.optsWithGlobals(), endpointSchema);
});
api
.command("paths")
.description(t("openapi.paths.description"))
.action(async (opts, cmd) => {
const res = await apiFetch("/api/openapi/spec");
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
const spec = await res.json();
const paths = Object.keys(spec.paths ?? {}).sort();
emit(
paths.map((p) => ({ path: p })),
cmd.optsWithGlobals()
);
});
}
+219
View File
@@ -0,0 +1,219 @@
import { spawnSync } from "node:child_process";
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { t } from "../i18n.mjs";
import { emit } from "../output.mjs";
import { discoverPlugins } from "../plugins.mjs";
// Run npm with an explicit argument array and no shell. Passing args this way
// (instead of string-interpolating into `execSync`) prevents a malicious plugin
// name like `foo; rm -rf ~` or `` foo`id` `` from being interpreted by the shell.
function runNpm(args) {
const res = spawnSync("npm", args, { stdio: "inherit", shell: false });
if (res.error) throw res.error;
if (typeof res.status === "number" && res.status !== 0) {
throw new Error(`npm exited with code ${res.status}`);
}
}
const TEMPLATE_INDEX = `export const meta = {
name: "PLUGIN_NAME",
version: "0.1.0",
description: "OmniRoute plugin",
omnirouteApi: ">=4.0.0",
};
export function register(program, ctx) {
program
.command("PLUGIN_NAME")
.description(meta.description)
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
// ctx provides: ctx.apiFetch, ctx.emit, ctx.t, ctx.withSpinner, ctx.baseUrl, ctx.apiKey
const res = await ctx.apiFetch("/api/health", { baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
const data = await res.json();
ctx.emit(data, gOpts);
});
}
`;
export function registerPlugin(program) {
const plugin = program
.command("plugin")
.description(t("plugin.description") || "Manage CLI plugins (omniroute-cmd-*)");
plugin
.command("list")
.description(t("plugin.list") || "List installed plugins")
.action(async (opts, cmd) => {
const plugins = await discoverPlugins();
emit(
plugins.map((p) => ({ name: p.name, version: p.version, description: p.description })),
cmd.optsWithGlobals()
);
if (plugins.length === 0) {
process.stdout.write("No plugins installed.\n");
process.stdout.write(`Install: omniroute plugin install <name>\n`);
}
});
plugin
.command("install <name>")
.description(t("plugin.install") || "Install a plugin")
.option("-y, --yes", "Skip confirmation prompt")
.action(async (name, opts) => {
const isLocal = name.startsWith("./") || name.startsWith("/") || name.startsWith("../");
const pkgName = isLocal ? name : `omniroute-cmd-${name}`;
if (!opts.yes) {
process.stderr.write(
`⚠ WARNING: Plugins run with the same privileges as omniroute CLI.\n` +
` Only install plugins from sources you trust.\n` +
` Installing: ${pkgName}\n` +
` Pass --yes to skip this prompt.\n`
);
// In non-interactive mode, require explicit --yes
if (!process.stdin.isTTY) {
process.stderr.write("Non-interactive mode: use --yes to confirm.\n");
process.exit(1);
}
}
try {
runNpm(["install", "-g", pkgName]);
process.stdout.write(`\n✓ Installed: ${pkgName}\n`);
} catch {
process.stderr.write(`✗ Failed to install ${pkgName}\n`);
process.exit(1);
}
});
plugin
.command("remove <name>")
.alias("uninstall")
.description(t("plugin.remove") || "Remove a plugin")
.option("-y, --yes", "Skip confirmation")
.action(async (name, opts) => {
const pkgName = name.startsWith("omniroute-cmd-") ? name : `omniroute-cmd-${name}`;
if (!opts.yes) {
process.stderr.write(`Removing: ${pkgName} — pass --yes to confirm.\n`);
if (!process.stdin.isTTY) {
process.exit(1);
}
}
try {
runNpm(["uninstall", "-g", pkgName]);
process.stdout.write(`✓ Removed: ${pkgName}\n`);
} catch {
process.stderr.write(`✗ Failed to remove ${pkgName}\n`);
process.exit(1);
}
});
plugin
.command("info <name>")
.description(t("plugin.info") || "Show plugin details")
.action(async (name, opts, cmd) => {
const plugins = await discoverPlugins();
const p = plugins.find((x) => x.name === name || x.name === `omniroute-cmd-${name}`);
if (!p) {
process.stderr.write(`Plugin '${name}' not found.\n`);
process.exit(1);
}
emit(p.pkg, cmd.optsWithGlobals());
});
plugin
.command("search [query]")
.description(t("plugin.search") || "Search npm for available plugins")
.action(async (query) => {
const q = query ? `omniroute-cmd-${query}` : "omniroute-cmd";
const url = `https://registry.npmjs.org/-/v1/search?text=${encodeURIComponent(q)}&size=50`;
try {
const res = await fetch(url);
if (!res.ok) throw new Error(`npm registry returned ${res.status}`);
const data = await res.json();
const rows = (data.objects || []).map((o) => ({
name: o.package.name,
version: o.package.version,
description: o.package.description,
}));
if (rows.length === 0) {
process.stdout.write(`No plugins found for '${query || "omniroute-cmd"}'.\n`);
} else {
rows.forEach((r) =>
process.stdout.write(` ${r.name}@${r.version} ${r.description || ""}\n`)
);
}
} catch (err) {
process.stderr.write(`Search failed: ${err.message}\n`);
process.exit(1);
}
});
plugin
.command("update [name]")
.description(t("plugin.update") || "Update installed plugin(s)")
.action(async (name) => {
try {
if (name) {
const pkg = `omniroute-cmd-${name}`;
runNpm(["update", "-g", pkg]);
process.stdout.write(`✓ Updated: ${pkg}\n`);
return;
}
// No name → update every installed plugin. Enumerate them explicitly
// instead of relying on a shell glob (`omniroute-cmd-*`), which never
// expands without a shell and would otherwise update nothing.
const plugins = await discoverPlugins();
const names = plugins
.map((p) => p.name)
.filter((n) => typeof n === "string" && n.startsWith("omniroute-cmd-"));
if (names.length === 0) {
process.stdout.write("No plugins installed to update.\n");
return;
}
runNpm(["update", "-g", ...names]);
process.stdout.write(`✓ Updated: ${names.join(", ")}\n`);
} catch {
process.stderr.write(`✗ Update failed\n`);
process.exit(1);
}
});
plugin
.command("scaffold <name>")
.description(t("plugin.scaffold") || "Scaffold a new plugin boilerplate")
.action(async (name) => {
const safeName = name.replace(/[^a-z0-9-]/g, "-");
const dir = join(process.cwd(), `omniroute-cmd-${safeName}`);
if (existsSync(dir)) {
process.stderr.write(`Directory already exists: ${dir}\n`);
process.exit(1);
}
mkdirSync(dir, { recursive: true });
writeFileSync(
join(dir, "package.json"),
JSON.stringify(
{
name: `omniroute-cmd-${safeName}`,
version: "0.1.0",
type: "module",
main: "index.mjs",
description: `OmniRoute CLI plugin: ${safeName}`,
engines: { omniroute: ">=4.0.0" },
keywords: ["omniroute-plugin", "omniroute-cmd"],
},
null,
2
) + "\n"
);
writeFileSync(join(dir, "index.mjs"), TEMPLATE_INDEX.replace(/PLUGIN_NAME/g, safeName));
writeFileSync(
join(dir, "README.md"),
`# omniroute-cmd-${safeName}\n\nAn OmniRoute CLI plugin.\n\n## Install\n\n\`\`\`bash\nomniroute plugin install ${safeName}\n\`\`\`\n`
);
process.stdout.write(`✓ Scaffolded: ${dir}\n`);
process.stdout.write(` Run: cd ${dir} && omniroute plugin install .\n`);
});
}
+216
View File
@@ -0,0 +1,216 @@
import { readFileSync, writeFileSync } from "node:fs";
import { z } from "zod";
import { apiFetch } from "../api.mjs";
import { emit } from "../output.mjs";
import { t } from "../i18n.mjs";
const policyBodySchema = z.record(z.string(), z.unknown());
const importBodySchema = z.record(z.string(), z.unknown());
const contextSchema = z.record(z.string(), z.unknown());
function parseJsonInput(value, label, schema) {
let parsed;
try {
parsed = JSON.parse(value);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
process.stderr.write(`Invalid JSON for ${label}: ${message}\n`);
process.exit(2);
}
const result = schema.safeParse(parsed);
if (!result.success) {
process.stderr.write(
`Invalid ${label}: ${result.error.issues[0]?.message || "schema error"}\n`
);
process.exit(2);
}
return result.data;
}
function readJsonFile(file, schema) {
let raw;
try {
raw = readFileSync(file, "utf8");
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
process.stderr.write(`Unable to read ${file}: ${message}\n`);
process.exit(2);
}
return parseJsonInput(raw, file, schema);
}
function fmtTs(v) {
if (!v) return "-";
try {
return new Date(v).toLocaleString();
} catch {
return String(v);
}
}
async function confirm(q) {
return new Promise((resolve) => {
process.stdout.write(`${q} (yes/no) `);
process.stdin.setEncoding("utf8");
process.stdin.once("data", (c) => resolve(c.toString().trim().toLowerCase().startsWith("y")));
});
}
const policySchema = [
{ key: "id", header: "Policy ID", width: 22 },
{ key: "name", header: "Name", width: 30 },
{ key: "kind", header: "Kind", width: 14 },
{ key: "scope", header: "Scope", width: 16 },
{ key: "enabled", header: "Enabled", formatter: (v) => (v ? "✓" : "✗") },
{ key: "priority", header: "Prio", width: 6 },
{ key: "updatedAt", header: "Updated", formatter: fmtTs },
];
export async function runPolicyList(opts, cmd) {
const params = new URLSearchParams();
if (opts.kind) params.set("kind", opts.kind);
if (opts.scope) params.set("scope", opts.scope);
const res = await apiFetch(`/api/policies?${params}`);
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
const data = await res.json();
emit(data.items ?? data, cmd.optsWithGlobals(), policySchema);
}
export async function runPolicyGet(id, opts, cmd) {
const res = await apiFetch(`/api/policies/${id}`);
if (!res.ok) {
process.stderr.write(`Not found: ${id}\n`);
process.exit(1);
}
emit(await res.json(), cmd.optsWithGlobals());
}
export async function runPolicyCreate(opts, cmd) {
const body = readJsonFile(opts.file, policyBodySchema);
const res = await apiFetch("/api/policies", { method: "POST", body });
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
emit(await res.json(), cmd.optsWithGlobals(), policySchema);
}
export async function runPolicyUpdate(id, opts, cmd) {
const body = readJsonFile(opts.file, policyBodySchema);
const res = await apiFetch(`/api/policies/${id}`, { method: "PUT", body });
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
emit(await res.json(), cmd.optsWithGlobals(), policySchema);
}
export async function runPolicyDelete(id, opts, cmd) {
if (!opts.yes) {
const ok = await confirm(`Delete policy ${id}?`);
if (!ok) return;
}
const res = await apiFetch(`/api/policies/${id}`, { method: "DELETE" });
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
process.stdout.write("Deleted\n");
}
export async function runPolicyEvaluate(opts, cmd) {
const body = {
apiKey: opts.apiKey,
action: opts.action,
...(opts.resource ? { resource: opts.resource } : {}),
...(opts.context ? { context: parseJsonInput(opts.context, "--context", contextSchema) } : {}),
};
const res = await apiFetch("/api/policies/evaluate", { method: "POST", body });
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
const data = await res.json();
emit(data, cmd.optsWithGlobals());
process.exit(data.allowed ? 0 : 4);
}
export async function runPolicyExport(file, opts, cmd) {
const res = await apiFetch("/api/policies?export=true");
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
const data = await res.json();
writeFileSync(file, JSON.stringify(data, null, 2));
process.stdout.write(`Exported ${data.items?.length ?? 0} policies to ${file}\n`);
}
export async function runPolicyImport(file, opts, cmd) {
const body = readJsonFile(file, importBodySchema);
const overwrite = opts.overwrite ? "true" : "false";
const res = await apiFetch(`/api/policies?import=true&overwrite=${overwrite}`, {
method: "POST",
body,
});
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
emit(await res.json(), cmd.optsWithGlobals());
}
export function registerPolicy(program) {
const policy = program.command("policy").description(t("policy.description"));
policy
.command("list")
.description(t("policy.list.description"))
.option("--kind <k>", t("policy.list.kind"))
.option("--scope <s>", t("policy.list.scope"))
.action(runPolicyList);
policy.command("get <id>").description(t("policy.get.description")).action(runPolicyGet);
policy
.command("create")
.description(t("policy.create.description"))
.requiredOption("--file <path>", t("policy.create.file"))
.action(runPolicyCreate);
policy
.command("update <id>")
.description(t("policy.update.description"))
.requiredOption("--file <path>", t("policy.update.file"))
.action(runPolicyUpdate);
policy
.command("delete <id>")
.description(t("policy.delete.description"))
.option("--yes", t("policy.delete.yes"))
.action(runPolicyDelete);
policy
.command("evaluate")
.description(t("policy.evaluate.description"))
.requiredOption("--api-key <k>", t("policy.evaluate.api_key"))
.requiredOption("--action <a>", t("policy.evaluate.action"))
.option("--resource <r>", t("policy.evaluate.resource"))
.option("--context <json>", t("policy.evaluate.context"))
.action(runPolicyEvaluate);
policy
.command("export <file>")
.description(t("policy.export.description"))
.action(runPolicyExport);
policy
.command("import <file>")
.description(t("policy.import.description"))
.option("--overwrite", t("policy.import.overwrite"))
.action(runPolicyImport);
}
+123
View File
@@ -0,0 +1,123 @@
import { apiFetch } from "../api.mjs";
import { emit } from "../output.mjs";
import { t } from "../i18n.mjs";
function fmtTs(v) {
if (!v) return "-";
return new Date(typeof v === "number" ? v * 1000 : v).toLocaleString();
}
function fmtPrice(v) {
if (v == null) return "-";
return `$${Number(v).toFixed(2)}`;
}
const pricingSchema = [
{ key: "model", header: "Model", width: 32 },
{ key: "provider", header: "Provider", width: 16 },
{ key: "inputPer1M", header: "Input/$1M", formatter: fmtPrice },
{ key: "outputPer1M", header: "Output/$1M", formatter: fmtPrice },
{ key: "cacheReadPer1M", header: "Cache R", formatter: fmtPrice },
{ key: "cacheWritePer1M", header: "Cache W", formatter: fmtPrice },
{ key: "source", header: "Source" },
{ key: "updatedAt", header: "Updated", formatter: fmtTs },
];
export async function runPricingSync(opts, cmd) {
const res = await apiFetch("/api/pricing/sync", {
method: "POST",
body: { provider: opts.provider, force: !!opts.force },
});
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
emit(await res.json(), cmd.optsWithGlobals());
}
export async function runPricingList(opts, cmd) {
const params = new URLSearchParams({ limit: String(opts.limit ?? 200) });
if (opts.provider) params.set("provider", opts.provider);
if (opts.model) params.set("model", opts.model);
const res = await apiFetch(`/api/pricing?${params}`);
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
const data = await res.json();
emit(data.items ?? data, cmd.optsWithGlobals(), pricingSchema);
}
export function registerPricing(program) {
const pricing = program.command("pricing").description(t("pricing.description"));
pricing
.command("sync")
.description(t("pricing.sync.description"))
.option("--provider <p>", t("pricing.sync.provider"))
.option("--force", t("pricing.sync.force"))
.action(runPricingSync);
pricing
.command("list")
.option("--provider <p>", t("pricing.list.provider"))
.option("--model <m>", t("pricing.list.model"))
.option("--limit <n>", t("pricing.list.limit"), parseInt, 200)
.action(runPricingList);
pricing.command("get <model>").action(async (model, opts, cmd) => {
const res = await apiFetch(`/api/pricing?model=${encodeURIComponent(model)}`);
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
emit(await res.json(), cmd.optsWithGlobals());
});
const defaults = pricing.command("defaults").description(t("pricing.defaults.description"));
defaults.command("show").action(async (opts, cmd) => {
const res = await apiFetch("/api/pricing/defaults");
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
emit(await res.json(), cmd.optsWithGlobals());
});
defaults
.command("set")
.option("--input <p>", t("pricing.defaults.input"), parseFloat)
.option("--output <p>", t("pricing.defaults.output"), parseFloat)
.option("--cache-read <p>", t("pricing.defaults.cacheRead"), parseFloat)
.option("--cache-write <p>", t("pricing.defaults.cacheWrite"), parseFloat)
.action(async (opts, cmd) => {
const body = {};
if (opts.input != null) body.inputPer1M = opts.input;
if (opts.output != null) body.outputPer1M = opts.output;
if (opts.cacheRead != null) body.cacheReadPer1M = opts.cacheRead;
if (opts.cacheWrite != null) body.cacheWritePer1M = opts.cacheWrite;
const res = await apiFetch("/api/pricing/defaults", { method: "PUT", body });
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
emit(await res.json(), cmd.optsWithGlobals());
});
pricing
.command("diff")
.description(t("pricing.diff.description"))
.option("--model <m>", t("pricing.diff.model"))
.action(async (opts, cmd) => {
const params = new URLSearchParams({ diff: "true" });
if (opts.model) params.set("model", opts.model);
const res = await apiFetch(`/api/pricing?${params}`);
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
const data = await res.json();
emit(data.diff ?? data, cmd.optsWithGlobals());
});
}
+18
View File
@@ -0,0 +1,18 @@
export function registerProvider(program) {
program
.command("provider [subcommand]")
.description("Manage provider connections (use 'providers' for the full interface)")
.allowUnknownOption()
.allowExcessArguments()
.action(() => {
console.log(`
Use \`omniroute providers\` for the full provider management interface:
omniroute providers available — show provider catalog
omniroute providers list — list configured connections
omniroute providers test <name> — test a provider connection
omniroute providers test-all — test all active connections
omniroute providers validate — validate local configuration
`);
});
}
+706
View File
@@ -0,0 +1,706 @@
import { apiFetch, isServerUp } from "../api.mjs";
import { emit } from "../output.mjs";
import { printHeading } from "../io.mjs";
import { getAvailableProviderCategories, loadAvailableProviders } from "../provider-catalog.mjs";
import { testProviderApiKey } from "../provider-test.mjs";
import {
findProviderConnection,
getProviderApiKey,
listProviderConnections,
updateProviderApiKey,
updateProviderTestResult,
} from "../provider-store.mjs";
import { encryptCredential } from "../encryption.mjs";
import { openOmniRouteDb } from "../sqlite.mjs";
import { t } from "../i18n.mjs";
function publicConnection(connection) {
return {
id: connection.id,
provider: connection.provider,
name: connection.name,
authType: connection.authType,
isActive: connection.isActive,
testStatus: connection.testStatus,
lastTested: connection.lastTested,
lastError: connection.lastError,
defaultModel: connection.defaultModel,
};
}
function statusColor(status) {
if (status === "active" || status === "success") return "\x1b[32m";
if (status === "error" || status === "expired" || status === "unavailable") return "\x1b[31m";
return "\x1b[33m";
}
function printProviderTable(connections) {
if (connections.length === 0) {
console.log("No providers configured.");
return;
}
for (const connection of connections) {
const shortId = connection.id.slice(0, 8);
const status = connection.testStatus || "unknown";
const color = statusColor(status);
console.log(
`${shortId.padEnd(10)} ${connection.provider.padEnd(14)} ${String(connection.name).padEnd(
24
)} ${color}${status}\x1b[0m`
);
}
}
function normalizeCategoryFilter(category) {
const normalized = String(category || "")
.trim()
.toLowerCase()
.replaceAll("_", "-");
if (normalized === "apikey") return "api-key";
return normalized;
}
function availableProviderNotes(provider) {
const notes = [];
if (provider.alias) notes.push(`alias:${provider.alias}`);
if (provider.hasFree) notes.push("free");
if (provider.passthroughModels) notes.push("passthrough");
if (provider.deprecated) notes.push("deprecated");
return notes.join(", ");
}
function publicAvailableProvider(provider) {
return {
id: provider.id,
name: provider.name,
category: provider.category,
alias: provider.alias,
website: provider.website,
deprecated: provider.deprecated,
hasFree: provider.hasFree,
passthroughModels: provider.passthroughModels,
};
}
function filterAvailableProviders(providers, opts) {
const search = String(opts.search || opts.q || "")
.trim()
.toLowerCase();
const category = normalizeCategoryFilter(opts.category);
return providers.filter((provider) => {
if (category && provider.category !== category) return false;
if (!search) return true;
return [provider.id, provider.name, provider.category, provider.alias]
.filter(Boolean)
.some((value) => String(value).toLowerCase().includes(search));
});
}
function printAvailableProviderTable(providers, categories) {
if (providers.length === 0) {
console.log("No available providers matched the filters.");
return;
}
console.log(`${providers.length} providers available.`);
console.log(`Categories: ${categories.join(", ")}`);
console.log("Use --search <text> or --category <category> to filter.\n");
console.log(`${"ID".padEnd(24)} ${"Category".padEnd(14)} ${"Name".padEnd(28)} Notes`);
for (const provider of providers) {
console.log(
`${provider.id.padEnd(24)} ${provider.category.padEnd(14)} ${String(provider.name).padEnd(
28
)} ${availableProviderNotes(provider)}`
);
}
}
function buildTestInput(connection, apiKey) {
return {
provider: connection.provider,
apiKey,
defaultModel: connection.defaultModel,
baseUrl: connection.providerSpecificData?.baseUrl || null,
};
}
async function runProviderTest(db, connection) {
try {
const apiKey = getProviderApiKey(connection);
const result = await testProviderApiKey(buildTestInput(connection, apiKey));
updateProviderTestResult(db, connection.id, result);
return {
connection: publicConnection(connection),
...result,
};
} catch (error) {
const result = {
valid: false,
error: error instanceof Error ? error.message : String(error),
statusCode: null,
};
updateProviderTestResult(db, connection.id, result);
return {
connection: publicConnection(connection),
...result,
};
}
}
function validateConnection(connection) {
const issues = [];
const warnings = [];
if (!connection.id) issues.push("Missing id");
if (!connection.provider) issues.push("Missing provider");
if (!connection.authType) warnings.push("Missing auth type");
if (connection.authType === "apikey") {
try {
getProviderApiKey(connection);
} catch (error) {
issues.push(error instanceof Error ? error.message : String(error));
}
} else if (!connection.accessToken && !connection.refreshToken) {
warnings.push("OAuth connection has no access or refresh token visible locally");
}
if (connection.providerSpecificData === null && connection.providerSpecificData !== undefined) {
warnings.push("provider_specific_data is absent or not an object");
}
return {
connection: publicConnection(connection),
valid: issues.length === 0,
issues,
warnings,
};
}
export async function runAvailableCommand(opts = {}) {
const allProviders = loadAvailableProviders();
const providers = filterAvailableProviders(allProviders, opts).map(publicAvailableProvider);
const categories = getAvailableProviderCategories(allProviders);
if (opts.json) {
console.log(JSON.stringify({ count: providers.length, categories, providers }, null, 2));
} else {
printHeading("OmniRoute Available Providers");
printAvailableProviderTable(providers, categories);
}
return 0;
}
export async function runListCommand(opts = {}) {
const { db } = await openOmniRouteDb();
try {
const connections = listProviderConnections(db).map(publicConnection);
if (opts.json) {
console.log(JSON.stringify({ providers: connections }, null, 2));
} else {
printHeading("OmniRoute Providers");
printProviderTable(connections);
}
return 0;
} finally {
db.close();
}
}
export async function runTestCommand(selector, opts = {}) {
if (!selector) {
console.error("Provider id or name is required.");
return 1;
}
const { db } = await openOmniRouteDb();
try {
const connection = findProviderConnection(db, selector);
if (!connection) {
console.error(`Provider connection not found: ${selector}`);
return 1;
}
const result = await runProviderTest(db, connection);
if (opts.json) {
console.log(JSON.stringify(result, null, 2));
} else if (result.valid) {
console.log(`\x1b[32mOK\x1b[0m ${connection.name}: provider test passed`);
} else {
console.log(`\x1b[31mFAIL\x1b[0m ${connection.name}: ${result.error}`);
}
return result.valid ? 0 : 1;
} finally {
db.close();
}
}
export async function runTestAllCommand(opts = {}) {
const { db } = await openOmniRouteDb();
try {
const connections = listProviderConnections(db);
const results = [];
for (const connection of connections) {
if (!connection.isActive) {
results.push({
connection: publicConnection(connection),
valid: false,
skipped: true,
error: "Connection is inactive",
});
continue;
}
results.push(await runProviderTest(db, connection));
}
if (opts.json) {
console.log(JSON.stringify({ results }, null, 2));
} else {
printHeading("OmniRoute Provider Tests");
for (const result of results) {
const label = result.valid
? "\x1b[32mOK\x1b[0m"
: result.skipped
? "\x1b[33mSKIP\x1b[0m"
: "\x1b[31mFAIL\x1b[0m";
console.log(
`${label} ${result.connection.name}: ${result.valid ? "provider test passed" : result.error}`
);
}
}
return results.some((result) => !result.valid && !result.skipped) ? 1 : 0;
} finally {
db.close();
}
}
export async function runValidateCommand(opts = {}) {
const { db } = await openOmniRouteDb();
try {
const results = listProviderConnections(db).map(validateConnection);
if (opts.json) {
console.log(JSON.stringify({ results }, null, 2));
} else {
printHeading("OmniRoute Provider Validation");
if (results.length === 0) {
console.log("No providers configured.");
}
for (const result of results) {
const label = result.valid ? "\x1b[32mOK\x1b[0m" : "\x1b[31mFAIL\x1b[0m";
const messages = [...result.issues, ...result.warnings].join("; ");
console.log(`${label} ${result.connection.name}${messages ? `: ${messages}` : ""}`);
}
}
return results.some((result) => !result.valid) ? 1 : 0;
} finally {
db.close();
}
}
export async function runProvidersRotateCommand(selector, opts = {}) {
if (!selector) {
console.error("Provider connection id or name is required.");
return 2;
}
// --- Resolve connection ---
const { db } = await openOmniRouteDb();
let connection;
try {
connection = findProviderConnection(db, selector);
} finally {
db.close();
}
if (!connection) {
console.error(`Provider connection not found: ${selector}`);
return 2;
}
// --- OAuth short-circuit ---
if (opts.oauth || connection.authType !== "apikey") {
console.log(t("providers.rotate.oauthHint", { provider: connection.provider }));
return 0;
}
// --- Source new key ---
let newKey;
if (opts.fromEnv) {
newKey = process.env[opts.fromEnv];
if (!newKey) {
console.error(t("providers.rotate.envVarEmpty", { var: opts.fromEnv }));
return 2;
}
} else if (opts.newKey) {
newKey = opts.newKey;
} else {
// Interactive prompt (echo-off not strictly needed for a key value, but best practice)
const readline = await import("node:readline");
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
newKey = await new Promise((resolve) =>
rl.question(`New API key for ${connection.name}: `, (a) => {
rl.close();
resolve(a.trim());
})
);
if (!newKey) {
console.error("No key provided.");
return 2;
}
}
// --- Dry-run ---
if (opts.dryRun) {
console.log(
t("providers.rotate.dryRunResult", { name: connection.name, id: connection.id.slice(0, 8) })
);
return 0;
}
// --- Confirm ---
if (!opts.yes) {
const readline = await import("node:readline");
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
const answer = await new Promise((resolve) =>
rl.question(
t("providers.rotate.confirmPrompt", {
name: connection.name,
id: connection.id.slice(0, 8),
}),
resolve
)
);
rl.close();
if (!/^y(es|s)?$/i.test(answer)) {
console.log(t("common.cancelled"));
return 0;
}
}
// --- Write ---
const serverUp = await isServerUp();
if (serverUp) {
try {
const res = await apiFetch(`/api/providers/${encodeURIComponent(connection.id)}`, {
method: "PATCH",
body: {
apiKey: newKey,
testStatus: "unknown",
lastError: null,
rateLimitedUntil: null,
backoffLevel: 0,
},
retry: false,
acceptNotOk: true,
});
if (!res.ok) {
console.error(t("common.error", { message: `HTTP ${res.status}` }));
return 1;
}
} catch {
// Fall through to direct DB write
const { db: db2 } = await openOmniRouteDb();
try {
updateProviderApiKey(db2, connection.id, encryptCredential(newKey));
} finally {
db2.close();
}
}
} else {
const { db: db2 } = await openOmniRouteDb();
try {
updateProviderApiKey(db2, connection.id, encryptCredential(newKey));
} finally {
db2.close();
}
}
console.log(
t("providers.rotate.success", { name: connection.name, id: connection.id.slice(0, 8) })
);
// --- Post-rotation test ---
if (!opts.skipTest) {
const { db: db3 } = await openOmniRouteDb();
try {
const fresh = findProviderConnection(db3, connection.id);
if (fresh) {
const result = await runProviderTest(db3, fresh);
if (result.valid) {
console.log(t("providers.rotate.testPassed"));
} else {
console.error(t("providers.rotate.testFailed", { error: result.error }));
}
}
} finally {
db3.close();
}
}
return 0;
}
export async function runProvidersStatusCommand(opts = {}) {
const serverUp = await isServerUp();
if (!serverUp) {
console.error(t("providers.status.requiresServer"));
return 3;
}
const res = await apiFetch("/api/providers/expiration", { acceptNotOk: true, retry: false });
if (!res.ok) {
console.error(t("common.error", { message: `HTTP ${res.status}` }));
return 1;
}
const data = await res.json();
const list = data.list || [];
// Optional provider filter
const filter = opts.provider ? String(opts.provider).toLowerCase() : null;
const rows = filter ? list.filter((item) => item.provider?.toLowerCase().includes(filter)) : list;
if (opts.json || opts.output === "json") {
console.log(JSON.stringify({ count: rows.length, connections: rows }, null, 2));
return 0;
}
if (rows.length === 0) {
console.log(t("providers.status.noData"));
return 0;
}
console.log(t("providers.status.header"));
for (const item of rows) {
const shortId = (item.connectionId || item.id || "").slice(0, 8);
const expiry = item.expiresAt ? new Date(item.expiresAt).toLocaleDateString() : "-";
const expiryStatus = item.status || "unknown";
const testStatus = item.testStatus || "unknown";
const cooldown = item.rateLimitedUntil ? new Date(item.rateLimitedUntil).toLocaleString() : "-";
const expiryColor = statusColor(expiryStatus);
const testColor = statusColor(testStatus);
console.log(
`${shortId.padEnd(10)} ${String(item.provider || "").padEnd(14)} ${String(item.name || "").padEnd(24)} ` +
`${expiry.padEnd(12)} ${expiryColor}${expiryStatus.padEnd(8)}\x1b[0m ` +
`${testColor}${testStatus.padEnd(12)}\x1b[0m ${cooldown}`
);
}
return 0;
}
export function registerProviders(program) {
const providers = program.command("providers").description(t("providers.title"));
providers
.command("available")
.description("Show available providers in the OmniRoute catalog")
.option("--json", "Print machine-readable JSON")
.option("--search <query>", "Filter by id, name, alias, or category")
.option("-q, --q <query>", "Alias for --search")
.option("--category <category>", "Filter by category (api-key, oauth, free)")
.action(async (opts, cmd) => {
const globalOpts = cmd.parent.optsWithGlobals();
const exitCode = await runAvailableCommand({ ...opts, output: globalOpts.output });
if (exitCode !== 0) process.exit(exitCode);
});
providers
.command("list")
.description("List configured provider connections")
.option("--json", "Print machine-readable JSON")
.action(async (opts, cmd) => {
const globalOpts = cmd.parent.optsWithGlobals();
const exitCode = await runListCommand({ ...opts, output: globalOpts.output });
if (exitCode !== 0) process.exit(exitCode);
});
providers
.command("test <idOrName>")
.description("Test a configured provider connection")
.option("--json", "Print machine-readable JSON")
.action(async (idOrName, opts, cmd) => {
const globalOpts = cmd.parent.optsWithGlobals();
const exitCode = await runTestCommand(idOrName, { ...opts, output: globalOpts.output });
if (exitCode !== 0) process.exit(exitCode);
});
providers
.command("test-all")
.description("Test all active provider connections")
.option("--json", "Print machine-readable JSON")
.action(async (opts, cmd) => {
const globalOpts = cmd.parent.optsWithGlobals();
const exitCode = await runTestAllCommand({ ...opts, output: globalOpts.output });
if (exitCode !== 0) process.exit(exitCode);
});
providers
.command("validate")
.description("Validate local provider configuration without calling upstream")
.option("--json", "Print machine-readable JSON")
.action(async (opts, cmd) => {
const globalOpts = cmd.parent.optsWithGlobals();
const exitCode = await runValidateCommand({ ...opts, output: globalOpts.output });
if (exitCode !== 0) process.exit(exitCode);
});
providers
.command("rotate <idOrName>")
.description(t("providers.rotate.description"))
.option("--new-key <key>", t("providers.rotate.newKeyOpt"))
.option("--from-env <VAR>", t("providers.rotate.fromEnvOpt"))
.option("--oauth", t("providers.rotate.oauthOpt"))
.option("--yes", t("common.yesOpt"))
.option("--skip-test", t("providers.rotate.skipTestOpt"))
.option("--dry-run", t("providers.rotate.dryRunOpt"))
.action(async (idOrName, opts, cmd) => {
const globalOpts = cmd.parent.optsWithGlobals();
const exitCode = await runProvidersRotateCommand(idOrName, {
...opts,
output: globalOpts.output,
});
if (exitCode !== 0) process.exit(exitCode);
});
providers
.command("status")
.description(t("providers.status.description"))
.option("--provider <name>", t("providers.status.providerOpt"))
.option("--json", "Print machine-readable JSON")
.action(async (opts, cmd) => {
const globalOpts = cmd.parent.optsWithGlobals();
const exitCode = await runProvidersStatusCommand({ ...opts, output: globalOpts.output });
if (exitCode !== 0) process.exit(exitCode);
});
extendProvidersMetrics(providers);
}
const providerMetricsSchema = [
{ key: "provider", header: "Provider", width: 20 },
{ key: "requests", header: "Reqs", formatter: (v) => (v != null ? v.toLocaleString() : "0") },
{
key: "successRate",
header: "Success %",
formatter: (v) => (v != null ? `${(v * 100).toFixed(1)}%` : "-"),
},
{
key: "avgLatencyMs",
header: "Avg Latency",
formatter: (v) => (v ? `${Math.round(v)}ms` : "-"),
},
{ key: "latencyP95Ms", header: "P95", formatter: (v) => (v ? `${v}ms` : "-") },
{
key: "costUsd",
header: "Cost (USD)",
formatter: (v) => (v != null ? `$${Number(v).toFixed(4)}` : "-"),
},
{ key: "errors", header: "Errors", formatter: (v) => (v != null ? String(v) : "0") },
{ key: "breakerState", header: "Breaker" },
];
function sortRows(rows, sortBy) {
if (!sortBy) return rows;
return [...rows].sort((a, b) => {
const av = a[sortBy] ?? 0;
const bv = b[sortBy] ?? 0;
return bv > av ? 1 : bv < av ? -1 : 0;
});
}
export async function runProvidersMetrics(opts, cmd) {
const globalOpts = cmd.optsWithGlobals();
const fetchMetrics = async () => {
const params = new URLSearchParams({ period: opts.period ?? "24h" });
if (opts.provider) params.set("provider", opts.provider);
if (opts.connectionId) params.set("connectionId", opts.connectionId);
if (opts.compare) params.set("providers", opts.compare);
const res = await apiFetch(`/api/provider-metrics?${params}`, {
timeout: globalOpts.timeout,
acceptNotOk: true,
});
if (!res.ok) return [];
const data = await res.json();
const raw = data.metrics ?? data.items ?? data.providers ?? data;
if (Array.isArray(raw)) return raw;
return Object.entries(raw).map(([provider, m]) => ({
provider,
...(typeof m === "object" && m !== null ? m : {}),
}));
};
const renderOnce = async () => {
const rows = await fetchMetrics();
const sorted = sortRows(rows, opts.sortBy);
emit(sorted.slice(0, opts.limit ?? 50), globalOpts, providerMetricsSchema);
};
if (opts.watch) {
process.stderr.write("[watching — Ctrl+C to exit]\n");
await renderOnce();
const interval = setInterval(async () => {
process.stdout.write("\x1b[2J\x1b[H");
await renderOnce();
}, 5000);
process.on("SIGINT", () => {
clearInterval(interval);
process.exit(0);
});
return;
}
await renderOnce();
}
export async function runProviderMetricSingle(id, metric, opts, cmd) {
const globalOpts = cmd.optsWithGlobals();
const params = new URLSearchParams({ connectionId: id, period: opts.period ?? "24h" });
const res = await apiFetch(`/api/provider-metrics?${params}`, {
timeout: globalOpts.timeout,
acceptNotOk: true,
});
if (!res.ok) {
process.stderr.write(t("common.serverOffline") + "\n");
process.exit(res.exitCode ?? 1);
}
const data = await res.json();
const raw = data.metrics ?? data;
const connData =
raw[id] ??
(Array.isArray(raw) ? raw.find((r) => r.connectionId === id || r.provider === id) : null);
const value = connData?.[metric] ?? connData;
if (globalOpts.output === "json") {
process.stdout.write(JSON.stringify(value, null, 2) + "\n");
} else {
process.stdout.write(`${metric}: ${JSON.stringify(value)}\n`);
}
}
export function extendProvidersMetrics(providers) {
providers
.command("metrics")
.description(t("providers.metrics.description"))
.option("--provider <id>", t("providers.metrics.provider"))
.option("--connection-id <id>", t("providers.metrics.connection_id"))
.option("--period <p>", t("providers.metrics.period"), "24h")
.option("--metric <m>", t("providers.metrics.metric"))
.option("--sort-by <field>", t("providers.metrics.sort"))
.option("--limit <n>", t("providers.metrics.limit"), parseInt, 50)
.option("--watch", t("providers.metrics.watch"))
.option("--compare <list>", t("providers.metrics.compare"))
.action(runProvidersMetrics);
providers
.command("metric <connectionId> <metric>")
.description(t("providers.metric_single.description"))
.option("--period <p>", t("providers.metrics.period"), "24h")
.action(runProviderMetricSingle);
}
+100
View File
@@ -0,0 +1,100 @@
import { apiFetch, isServerUp } from "../api.mjs";
import { t } from "../i18n.mjs";
export function registerQuota(program) {
program
.command("quota")
.description(t("quota.description"))
.option("--provider <id>", "Filter by provider")
.option("--json", "Output as JSON")
.action(async (opts, cmd) => {
const globalOpts = cmd.optsWithGlobals();
const exitCode = await runQuotaCommand({ ...opts, output: globalOpts.output });
if (exitCode !== 0) process.exit(exitCode);
});
}
export async function runQuotaCommand(opts = {}) {
const serverUp = await isServerUp();
if (!serverUp) {
console.error(t("quota.noServer"));
return 1;
}
let quotaData = null;
try {
const res = await apiFetch("/api/quota", { retry: false, timeout: 5000, acceptNotOk: true });
if (res.ok) quotaData = await res.json();
} catch {}
if (!quotaData) {
try {
const res = await apiFetch("/api/v1/providers", {
retry: false,
timeout: 5000,
acceptNotOk: true,
});
if (res.ok) {
const providers = await res.json();
quotaData = {
providers: providers.map((p) => ({
provider: p.name || p.id,
quota: p.quota || p.remaining || "N/A",
used: p.used || 0,
reset: p.resetAt || "N/A",
})),
};
}
} catch {}
}
if (opts.json || opts.output === "json") {
console.log(JSON.stringify(quotaData || { error: "No quota data" }, null, 2));
return 0;
}
if (!quotaData?.providers) {
console.log(t("quota.noData"));
return 0;
}
let providers = quotaData.providers;
if (opts.provider) {
const filter = opts.provider.toLowerCase();
providers = providers.filter((p) => p.provider.toLowerCase().includes(filter));
}
console.log(`\n\x1b[1m\x1b[36mProvider Quota Usage\x1b[0m\n`);
console.log(
"\x1b[36m" +
" Provider".padEnd(25) +
"Used".padEnd(15) +
"Remaining".padEnd(20) +
"Reset\x1b[0m"
);
console.log(
"\x1b[2m " +
"─".repeat(24) +
" " +
"─".repeat(14) +
" " +
"─".repeat(19) +
" " +
"─".repeat(15) +
"\x1b[0m"
);
for (const p of providers) {
const provider = (p.provider || "unknown").slice(0, 23).padEnd(25);
const used = String(p.used || 0).padEnd(15);
const remaining = String(p.quota || p.remaining || "N/A")
.slice(0, 18)
.padEnd(20);
const reset = p.reset || "N/A";
console.log(` ${provider}${used}${remaining}${reset}`);
}
console.log(`\n \x1b[32mTotal: ${providers.length} providers\x1b[0m`);
return 0;
}
+294
View File
@@ -0,0 +1,294 @@
import { spawn } from "node:child_process";
import { promisify } from "node:util";
import { execFile as execFileCb } from "node:child_process";
import { t } from "../i18n.mjs";
const execFile = promisify(execFileCb);
const DEFAULT_IMAGE = "docker.io/redis:7-alpine";
const DEFAULT_NAME = "omniroute-redis";
const DEFAULT_PORT = "6379";
const DEFAULT_VOLUME = "omniroute-redis-data";
const RUNTIME_PREFERENCE = ["podman", "docker"];
async function detectRuntime() {
for (const candidate of RUNTIME_PREFERENCE) {
try {
await execFile(candidate, ["--version"], { timeout: 3000 });
return candidate;
} catch {
// try next candidate
}
}
return null;
}
async function containerExists(runtime, name) {
try {
const { stdout } = await execFile(runtime, ["ps", "-a", "--filter", `name=^${name}$`, "--format", "{{.Names}}"]);
return stdout.trim() === name;
} catch {
return false;
}
}
async function containerRunning(runtime, name) {
try {
const { stdout } = await execFile(runtime, ["ps", "--filter", `name=^${name}$`, "--format", "{{.Names}}"]);
return stdout.trim() === name;
} catch {
return false;
}
}
async function pingRedis(port) {
// Minimal TCP probe via /dev/tcp — works in bash/zsh but Node has no
// native equivalent, so spawn a short-lived `redis-cli` if available,
// otherwise fall back to a raw socket connect.
return new Promise((resolve) => {
import("node:net").then(({ createConnection }) => {
const socket = createConnection({ port: Number(port), host: "127.0.0.1" });
const timeout = setTimeout(() => {
socket.destroy();
resolve(false);
}, 1500);
socket.once("connect", () => {
clearTimeout(timeout);
socket.end();
resolve(true);
});
socket.once("error", () => {
clearTimeout(timeout);
resolve(false);
});
});
});
}
function colorize(text, code) {
if (process.stdout.isTTY === false) return text;
return `\x1b[${code}m${text}\x1b[0m`;
}
function info(msg) {
console.log(colorize("•", "36") + " " + msg);
}
function success(msg) {
console.log(colorize("✓", "32") + " " + msg);
}
function warn(msg) {
console.error(colorize("!", "33") + " " + msg);
}
function fail(msg) {
console.error(colorize("✗", "31") + " " + msg);
}
export function registerRedis(program) {
const redis = program
.command("redis")
.description(
t("redis.description") ||
"Launch a 1-click local Redis container (Podman or Docker) for OmniRoute caching and quota tracking"
);
redis
.command("up")
.description("Start the local Redis container")
.option("-p, --port <port>", "Host port to expose", DEFAULT_PORT)
.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")
.option("--runtime <runtime>", "Force a specific runtime (podman|docker)")
.option("--password <password>", "Set a Redis password (AUTH)")
.action(async (opts, cmd) => {
const globalOpts = cmd.parent.optsWithGlobals();
const exitCode = await runRedisUpCommand({ ...opts, output: globalOpts.output });
if (exitCode !== 0) process.exit(exitCode);
});
redis
.command("down")
.description("Stop and remove the local Redis container")
.option("-n, --name <name>", "Container name", DEFAULT_NAME)
.option("--keep-data", "Keep the named volume for next start")
.option("--runtime <runtime>", "Force a specific runtime (podman|docker)")
.action(async (opts, cmd) => {
const globalOpts = cmd.parent.optsWithGlobals();
const exitCode = await runRedisDownCommand({ ...opts, output: globalOpts.output });
if (exitCode !== 0) process.exit(exitCode);
});
redis
.command("status")
.description("Show status of the local Redis container")
.option("-n, --name <name>", "Container name", DEFAULT_NAME)
.option("-p, --port <port>", "Host port", DEFAULT_PORT)
.option("--runtime <runtime>", "Force a specific runtime (podman|docker)")
.action(async (opts, cmd) => {
const globalOpts = cmd.parent.optsWithGlobals();
const exitCode = await runRedisStatusCommand({ ...opts, output: globalOpts.output });
if (exitCode !== 0) process.exit(exitCode);
});
}
async function pickRuntime(forced) {
if (forced) {
try {
await execFile(forced, ["--version"], { timeout: 3000 });
return forced;
} catch (err) {
fail(`Forced runtime '${forced}' not available: ${err.message}`);
return null;
}
}
const detected = await detectRuntime();
if (!detected) {
fail("Neither podman nor docker found on PATH. Install one or pass --runtime.");
return null;
}
return detected;
}
export async function runRedisUpCommand(opts = {}) {
const runtime = await pickRuntime(opts.runtime);
if (!runtime) return 1;
const name = opts.name || DEFAULT_NAME;
const port = opts.port || DEFAULT_PORT;
const image = opts.image || DEFAULT_IMAGE;
const exists = await containerExists(runtime, name);
const running = exists && (await containerRunning(runtime, name));
if (running) {
success(`Container '${name}' is already running on port ${port}.`);
return 0;
}
if (exists && !opts.pull) {
info(`Starting existing container '${name}'…`);
try {
await execFile(runtime, ["start", name]);
success(`Container '${name}' started on port ${port}.`);
return 0;
} catch (err) {
fail(`Failed to start existing container: ${err.message}`);
return 1;
}
}
if (!opts.pull) {
info(`Checking if image '${image}' is present locally…`);
let present = false;
try {
const { stdout } = await execFile(runtime, ["images", "--format", "{{.Repository}}:{{.Tag}}"]);
present = stdout.split("\n").some((line) => line.trim() === image);
} catch {
// ignore — fall through to pull
}
if (!present) {
info(`Image not found locally — pulling '${image}'…`);
try {
await execFile(runtime, ["pull", image]);
} catch (err) {
fail(`Failed to pull image: ${err.message}`);
return 1;
}
}
}
const args = [
"run",
"-d",
"--name", name,
"--restart", "unless-stopped",
"-p", `${port}:6379`,
"-v", `${DEFAULT_VOLUME}:/data`,
];
if (opts.password) {
args.push("-e", `REDIS_PASSWORD=${opts.password}`);
}
args.push(image, "redis-server", "--appendonly", "yes");
if (opts.password) args.push("--requirepass", opts.password);
info(`Launching ${runtime} run ${args.join(" ")}`);
try {
await execFile(runtime, args);
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}`);
return 1;
}
}
export async function runRedisDownCommand(opts = {}) {
const runtime = await pickRuntime(opts.runtime);
if (!runtime) return 1;
const name = opts.name || DEFAULT_NAME;
if (!(await containerExists(runtime, name))) {
info(`Container '${name}' does not exist — nothing to do.`);
return 0;
}
try {
await execFile(runtime, ["rm", "-f", name]);
success(`Removed container '${name}'.`);
} catch (err) {
fail(`Failed to remove container: ${err.message}`);
return 1;
}
if (!opts.keepData) {
try {
await execFile(runtime, ["volume", "rm", DEFAULT_VOLUME]);
success(`Removed volume '${DEFAULT_VOLUME}'.`);
} catch (err) {
warn(`Could not remove volume '${DEFAULT_VOLUME}': ${err.message}`);
}
}
return 0;
}
export async function runRedisStatusCommand(opts = {}) {
const runtime = await pickRuntime(opts.runtime);
if (!runtime) return 1;
const name = opts.name || DEFAULT_NAME;
const port = opts.port || DEFAULT_PORT;
const exists = await containerExists(runtime, name);
if (!exists) {
console.log(JSON.stringify({ runtime, name, port, exists: false, running: false, reachable: false }, null, 2));
return 0;
}
const running = await containerRunning(runtime, name);
const reachable = running ? await pingRedis(port) : false;
if (opts.json || opts.output === "json") {
console.log(JSON.stringify({ runtime, name, port, exists, running, reachable }, null, 2));
return 0;
}
console.log(`\n\x1b[1m\x1b[36mRedis (${runtime})\x1b[0m\n`);
console.log(` Container: ${name}`);
console.log(` Exists: ${exists ? "yes" : "no"}`);
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?");
}
if (!running) {
info(`Run 'omniroute redis up' to launch it.`);
}
return 0;
}
+162
View File
@@ -0,0 +1,162 @@
import { registerMemory } from "./memory.mjs";
import { registerSkills } from "./skills.mjs";
import { registerAudit } from "./audit.mjs";
import { registerOAuth } from "./oauth.mjs";
import { registerLogin } from "./login.mjs";
import { registerCloud } from "./cloud.mjs";
import { registerEval } from "./eval.mjs";
import { registerWebhooks } from "./webhooks.mjs";
import { registerPolicy } from "./policy.mjs";
import { registerCompression } from "./compression.mjs";
import { registerFiles } from "./files.mjs";
import { registerBatches } from "./batches.mjs";
import { registerTranslator } from "./translator.mjs";
import { registerPricing } from "./pricing.mjs";
import { registerResilience } from "./resilience.mjs";
import { registerNodes } from "./nodes.mjs";
import { registerSync } from "./sync.mjs";
import { registerContextEng } from "./context-eng.mjs";
import { registerSessions } from "./sessions.mjs";
import { registerTags } from "./tags.mjs";
import { registerOpenapi } from "./openapi.mjs";
import { registerOneProxy } from "./oneproxy.mjs";
import { registerTelemetry } from "./telemetry.mjs";
import { registerOpen } from "./open.mjs";
import { registerChat } from "./chat.mjs";
import { registerStream } from "./stream.mjs";
import { registerSimulate } from "./simulate.mjs";
import { registerCost } from "./cost.mjs";
import { registerUsage } from "./usage.mjs";
import { registerServe } from "./serve.mjs";
import { registerStop } from "./stop.mjs";
import { registerRestart } from "./restart.mjs";
import { registerDashboard } from "./dashboard.mjs";
import { registerDoctor } from "./doctor.mjs";
import { registerSetup } from "./setup.mjs";
import { registerProviders } from "./providers.mjs";
import { registerProvider } from "./provider-cmd.mjs";
import { registerConfig } from "./config.mjs";
import { registerKeys } from "./keys.mjs";
import { registerModels } from "./models.mjs";
import { registerCombo } from "./combo.mjs";
import { registerStatus } from "./status.mjs";
import { registerLogs } from "./logs.mjs";
import { registerUpdate } from "./update.mjs";
import { registerBackup, registerRestore } from "./backup.mjs";
import { registerHealth } from "./health.mjs";
import { registerQuota } from "./quota.mjs";
import { registerCache } from "./cache.mjs";
import { registerRedis } from "./redis.mjs";
import { registerMcp } from "./mcp.mjs";
import { registerA2a } from "./a2a.mjs";
import { registerTunnel } from "./tunnel.mjs";
import { registerEnv } from "./env.mjs";
import { registerTestProvider } from "./test-provider.mjs";
import { registerCompletion } from "./completion.mjs";
import { registerRuntime } from "./runtime.mjs";
import { registerTray } from "./tray.mjs";
import { registerAutostart } from "./autostart.mjs";
import { registerRepl } from "./repl.mjs";
import { registerLaunch } from "./launch.mjs";
import { registerLaunchCodex } from "./launch-codex.mjs";
import { registerSetupCodex } from "./setup-codex.mjs";
import { registerSetupClaude } from "./setup-claude.mjs";
import { registerSetupOpencode } from "./setup-opencode.mjs";
import { registerSetupCline } from "./setup-cline.mjs";
import { registerSetupKilo } from "./setup-kilo.mjs";
import { registerSetupContinue } from "./setup-continue.mjs";
import { registerSetupCursor } from "./setup-cursor.mjs";
import { registerSetupRoo } from "./setup-roo.mjs";
import { registerSetupCrush } from "./setup-crush.mjs";
import { registerSetupGoose } from "./setup-goose.mjs";
import { registerSetupQwen } from "./setup-qwen.mjs";
import { registerSetupAider } from "./setup-aider.mjs";
import { registerConnect } from "./connect.mjs";
import { registerContexts } from "./contexts.mjs";
import { registerTokens } from "./tokens.mjs";
import { registerConfigure } from "./configure.mjs";
import { registerApiCommands } from "../api-commands/registry.mjs";
import { registerPlugin } from "./plugin.mjs";
export function registerCommands(program) {
registerMemory(program);
registerSkills(program);
registerAudit(program);
registerOAuth(program);
registerLogin(program);
registerCloud(program);
registerEval(program);
registerWebhooks(program);
registerPolicy(program);
registerCompression(program);
registerFiles(program);
registerBatches(program);
registerTranslator(program);
registerPricing(program);
registerResilience(program);
registerNodes(program);
registerSync(program);
registerContextEng(program);
registerSessions(program);
registerTags(program);
registerOpenapi(program);
registerOneProxy(program);
registerTelemetry(program);
registerOpen(program);
registerChat(program);
registerStream(program);
registerSimulate(program);
registerCost(program);
registerUsage(program);
registerServe(program);
registerStop(program);
registerRestart(program);
registerDashboard(program);
registerDoctor(program);
registerSetup(program);
registerProviders(program);
registerProvider(program);
registerConfig(program);
registerKeys(program);
registerModels(program);
registerCombo(program);
registerStatus(program);
registerLogs(program);
registerUpdate(program);
registerBackup(program);
registerRestore(program);
registerHealth(program);
registerQuota(program);
registerCache(program);
registerRedis(program);
registerMcp(program);
registerA2a(program);
registerTunnel(program);
registerEnv(program);
registerTestProvider(program);
registerCompletion(program);
registerRuntime(program);
registerTray(program);
registerAutostart(program);
registerRepl(program);
registerLaunch(program);
registerLaunchCodex(program);
registerSetupCodex(program);
registerSetupClaude(program);
registerSetupOpencode(program);
registerSetupCline(program);
registerSetupKilo(program);
registerSetupContinue(program);
registerSetupCursor(program);
registerSetupRoo(program);
registerSetupCrush(program);
registerSetupGoose(program);
registerSetupQwen(program);
registerSetupAider(program);
registerConnect(program);
registerContexts(program);
registerTokens(program);
registerConfigure(program);
registerApiCommands(program);
registerPlugin(program);
}
+19
View File
@@ -0,0 +1,19 @@
import { t } from "../i18n.mjs";
export function registerRepl(program) {
program
.command("repl")
.description(t("repl.description"))
.option("-m, --model <id>", t("repl.model"))
.option("--combo <name>", t("repl.combo"))
.option("-s, --system <prompt>", t("repl.system"))
.option("--resume <session>", t("repl.resume"))
.action(async (opts, cmd) => {
const globalOpts = cmd.optsWithGlobals();
const port = globalOpts.port ? parseInt(String(globalOpts.port), 10) : 20128;
const baseUrl = globalOpts.baseUrl ?? `http://localhost:${port}`;
const apiKey = globalOpts.apiKey ?? null;
const { runRepl } = await import("../tui/Repl.jsx");
await runRepl({ ...opts, baseUrl, apiKey, port });
});
}
@@ -0,0 +1,88 @@
import { existsSync } from "node:fs";
import { resolveDataDir } from "../data-dir.mjs";
import { join } from "node:path";
const ENCRYPTED_PATTERN = "enc:v1:%";
const ENCRYPTED_COLUMNS = ["api_key", "access_token", "refresh_token", "id_token"];
/**
* Direct SQLite implementation for reset-encrypted-columns.
* Uses better-sqlite3 directly to avoid TypeScript source dependencies in production builds.
*/
export async function runResetEncryptedColumns(argv) {
const dataDir = resolveDataDir();
const dbPath = join(dataDir, "storage.sqlite");
if (!existsSync(dbPath)) {
console.log(`\x1b[33m⚠ No database found at ${dbPath}\x1b[0m`);
return 0;
}
const force = Array.isArray(argv) ? argv.includes("--force") : argv?.force === true;
if (!force) {
console.log(`
\x1b[1m\x1b[33m⚠ WARNING: This will erase all encrypted credentials\x1b[0m
This command will NULL out the following columns in provider_connections:
• api_key
• access_token
• refresh_token
• id_token
Provider metadata (name, provider_id, settings) will be preserved.
You will need to re-authenticate all providers after this operation.
Database: ${dbPath}
\x1b[1mTo confirm, run:\x1b[0m
omniroute reset-encrypted-columns --force
`);
return 0;
}
let db;
try {
// Use createRequire to load better-sqlite3 (works in both dev and production)
const { createRequire } = await import("node:module");
const require = createRequire(import.meta.url);
const Database = require("better-sqlite3");
db = new Database(dbPath);
// Build WHERE clause for encrypted values
const whereClause = ENCRYPTED_COLUMNS.map((col) => `${col} LIKE '${ENCRYPTED_PATTERN}'`).join(
" OR "
);
// Count affected rows
const countResult = db
.prepare(`SELECT COUNT(*) AS cnt FROM provider_connections WHERE ${whereClause}`)
.get();
const count = countResult?.cnt ?? 0;
if (count === 0) {
console.log("\x1b[32m✔ No encrypted credentials found — nothing to reset.\x1b[0m");
return 0;
}
// Reset columns
const nullCols = ENCRYPTED_COLUMNS.map((col) => `${col} = NULL`).join(", ");
db.prepare(`UPDATE provider_connections SET ${nullCols} WHERE ${whereClause}`).run();
console.log(
`\x1b[32m✔ Reset ${count} provider connection(s).\x1b[0m\n` +
` Re-authenticate your providers in the dashboard or re-add API keys.\n`
);
return 0;
} catch (err) {
console.error(
`\x1b[31m✖ Failed to reset encrypted columns:\x1b[0m ${err instanceof Error ? err.message : String(err)}`
);
return 1;
} finally {
if (db) {
db.close();
}
}
}
+208
View File
@@ -0,0 +1,208 @@
import { createInterface } from "node:readline";
import { Argument } from "commander";
import { apiFetch } from "../api.mjs";
import { emit } from "../output.mjs";
import { t } from "../i18n.mjs";
function fmtTs(v) {
if (!v) return "-";
return new Date(typeof v === "number" ? v * 1000 : v).toLocaleString();
}
function fmtBreaker(v) {
if (v === "closed") return "● closed";
if (v === "open") return "✗ open";
return "○ half-open";
}
async function confirm(q) {
const rl = createInterface({ input: process.stdin, output: process.stdout });
return new Promise((resolve) => {
rl.question(`${q} [y/N] `, (a) => {
rl.close();
resolve(a.trim().toLowerCase() === "y");
});
});
}
const breakerSchema = [
{ key: "provider", header: "Provider", width: 20 },
{ key: "state", header: "State", formatter: fmtBreaker },
{ key: "failures", header: "Failures" },
{ key: "successesProbe", header: "Probes ✓" },
{ key: "lastFailure", header: "Last Failure", formatter: fmtTs },
{ key: "resetAt", header: "Reset At", formatter: fmtTs },
];
const cooldownSchema = [
{ key: "provider", header: "Provider", width: 20 },
{ key: "connectionId", header: "Connection", width: 28 },
{ key: "testStatus", header: "Status" },
{ key: "rateLimitedUntil", header: "Until", formatter: fmtTs },
{ key: "backoffLevel", header: "Backoff" },
{ key: "lastErrorType", header: "Error Type" },
];
const lockoutSchema = [
{ key: "provider", header: "Provider", width: 16 },
{ key: "connectionId", header: "Connection", width: 24 },
{ key: "model", header: "Model", width: 30 },
{ key: "reason", header: "Reason" },
{ key: "expiresAt", header: "Expires", formatter: fmtTs },
];
export function registerResilience(program) {
const r = program.command("resilience").description(t("resilience.description"));
r.command("status")
.option("--provider <p>", t("resilience.status.provider"))
.action(async (opts, cmd) => {
const params = new URLSearchParams();
if (opts.provider) params.set("provider", opts.provider);
const res = await apiFetch(`/api/resilience?${params}`);
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
emit(await res.json(), cmd.optsWithGlobals());
});
r.command("breakers")
.option("--provider <p>", t("resilience.breakers.provider"))
.action(async (opts, cmd) => {
const res = await apiFetch("/api/resilience?include=breakers");
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
const data = await res.json();
let rows = data.breakers ?? [];
if (opts.provider) rows = rows.filter((x) => x.provider === opts.provider);
emit(rows, cmd.optsWithGlobals(), breakerSchema);
});
r.command("cooldowns")
.option("--provider <p>", t("resilience.cooldowns.provider"))
.option("--connection-id <id>", t("resilience.cooldowns.connectionId"))
.action(async (opts, cmd) => {
const res = await apiFetch("/api/resilience?include=cooldowns");
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
const data = await res.json();
let rows = data.cooldowns ?? [];
if (opts.provider) rows = rows.filter((x) => x.provider === opts.provider);
if (opts.connectionId) rows = rows.filter((x) => x.connectionId === opts.connectionId);
emit(rows, cmd.optsWithGlobals(), cooldownSchema);
});
r.command("lockouts")
.option("--provider <p>", t("resilience.lockouts.provider"))
.option("--model <m>", t("resilience.lockouts.model"))
.action(async (opts, cmd) => {
const res = await apiFetch("/api/resilience/model-cooldowns");
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
const data = await res.json();
let rows = data.items ?? data ?? [];
if (opts.provider) rows = rows.filter((x) => x.provider === opts.provider);
if (opts.model) rows = rows.filter((x) => x.model === opts.model);
emit(rows, cmd.optsWithGlobals(), lockoutSchema);
});
r.command("reset")
.description(t("resilience.reset.description"))
.requiredOption("--provider <p>", t("resilience.reset.provider"))
.option("--connection-id <id>", t("resilience.reset.connectionId"))
.option("--model <m>", t("resilience.reset.model"))
.option("--all-cooldowns", t("resilience.reset.allCooldowns"))
.option("--yes", t("resilience.reset.yes"))
.action(async (opts, cmd) => {
if (!opts.yes) {
const what = opts.connectionId
? `connection ${opts.connectionId}`
: opts.model
? `model ${opts.provider}/${opts.model}`
: `provider ${opts.provider}`;
const ok = await confirm(`Reset ${what}?`);
if (!ok) return;
}
const body = {
provider: opts.provider,
connectionId: opts.connectionId,
model: opts.model,
allCooldowns: !!opts.allCooldowns,
};
const res = await apiFetch("/api/resilience/reset", { method: "POST", body });
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
emit(await res.json(), cmd.optsWithGlobals());
});
const profile = r.command("profile").description(t("resilience.profile.description"));
profile.command("show").action(async (opts, cmd) => {
const res = await apiFetch("/api/resilience?include=profile");
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
emit(await res.json(), cmd.optsWithGlobals());
});
profile
.command("set")
.addArgument(
new Argument("<name>", t("resilience.profile.name")).choices([
"aggressive",
"balanced",
"conservative",
"custom",
])
)
.action(async (name, opts, cmd) => {
const res = await apiFetch("/api/mcp/tools/call", {
method: "POST",
body: { name: "omniroute_set_resilience_profile", arguments: { profile: name } },
});
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
process.stdout.write(`Profile: ${name}\n`);
});
const config = r.command("config").description(t("resilience.config.description"));
config.command("show").action(async (opts, cmd) => {
const res = await apiFetch("/api/resilience?include=config");
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
emit(await res.json(), cmd.optsWithGlobals());
});
config
.command("set")
.option("--threshold <n>", t("resilience.config.threshold"), parseInt)
.option("--reset-timeout <ms>", t("resilience.config.resetTimeout"), parseInt)
.option("--base-cooldown <ms>", t("resilience.config.baseCooldown"), parseInt)
.action(async (opts, cmd) => {
const body = {};
if (opts.threshold != null) body.threshold = opts.threshold;
if (opts.resetTimeout != null) body.resetTimeoutMs = opts.resetTimeout;
if (opts.baseCooldown != null) body.baseCooldownMs = opts.baseCooldown;
const res = await apiFetch("/api/resilience", { method: "PATCH", body });
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
emit(await res.json(), cmd.optsWithGlobals());
});
}
+25
View File
@@ -0,0 +1,25 @@
import { t } from "../i18n.mjs";
import { runStopCommand } from "./stop.mjs";
import { sleep } from "../utils/pid.mjs";
export function registerRestart(program) {
program
.command("restart")
.description(t("restart.description"))
.option("--port <port>", t("serve.port"), "20128")
.action(async (opts) => {
const exitCode = await runRestartCommand(opts);
if (exitCode !== 0) process.exit(exitCode);
});
}
export async function runRestartCommand(opts = {}) {
console.log(t("restart.restarting"));
await runStopCommand(opts);
await sleep(1000);
const { runServe } = await import("./serve.mjs");
await runServe(opts);
return 0;
}
+98
View File
@@ -0,0 +1,98 @@
import { rmSync } from "node:fs";
import { t } from "../i18n.mjs";
import { emit } from "../output.mjs";
import { withSpinner } from "../spinner.mjs";
import {
getRuntimeNodeModules,
hasModule,
isBetterSqliteBinaryValid,
ensureBetterSqliteRuntime,
} from "../runtime/nativeDeps.mjs";
async function confirm(msg) {
const readline = await import("node:readline");
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
const answer = await new Promise((r) => rl.question(`${msg} [y/N] `, r));
rl.close();
return /^y(es)?$/i.test(answer);
}
/**
* Shared repair action used by both `omniroute runtime repair` and the
* top-level `omniroute repair` alias. Reinstalls better-sqlite3 into the
* user-writable runtime directory via the existing engine — no hand-rolled
* npm-rebuild spawn. Exits with code 1 on failure.
*/
async function runRepairAction(opts, cmd) {
const globalOpts = cmd.optsWithGlobals();
await withSpinner(
"Repairing native deps",
async () => ensureBetterSqliteRuntime({ silent: true, force: opts.force }),
globalOpts
);
const ok = hasModule("better-sqlite3") && isBetterSqliteBinaryValid();
if (ok) {
process.stdout.write("✓ better-sqlite3 repaired OK\n");
} else {
process.stderr.write("✗ Repair failed — check npm availability\n");
process.exit(1);
}
}
export function registerRuntime(program) {
const runtime = program
.command("runtime")
.description(t("runtime.description") || "Manage native runtime dependencies");
runtime
.command("check")
.description("Check status of native deps in runtime directory")
.action(async (opts, cmd) => {
const globalOpts = cmd.optsWithGlobals();
const status = {
runtimeDir: getRuntimeNodeModules(),
betterSqlite3: {
installed: hasModule("better-sqlite3"),
valid: isBetterSqliteBinaryValid(),
},
};
emit(status, globalOpts);
});
runtime
.command("repair")
.description("Reinstall native deps in runtime directory")
.option("--force", "Force reinstall even if valid")
.action(runRepairAction);
// Top-level discoverability alias: `omniroute repair` invokes the SAME action
// as `omniroute runtime repair` (no duplicated logic). Surfaced in the
// native-error / startup hints so users with a broken better-sqlite3 binding
// have a single self-heal command that works without a C++ toolchain.
program
.command("repair")
.description("Repair native deps (alias for `runtime repair`)")
.option("--force", "Force reinstall even if valid")
.action(runRepairAction);
runtime
.command("clean")
.description("Remove runtime directory (frees disk space)")
.option("--yes", "Skip confirmation")
.action(async (opts) => {
if (!opts.yes) {
const ok = await confirm(`Remove ${getRuntimeNodeModules()}?`);
if (!ok) {
process.stdout.write("Cancelled.\n");
return;
}
}
try {
rmSync(getRuntimeNodeModules(), { recursive: true, force: true });
process.stdout.write("Cleaned runtime directory.\n");
} catch (e) {
process.stderr.write(`Failed: ${e instanceof Error ? e.message : String(e)}\n`);
process.exit(1);
}
});
}
+426
View File
@@ -0,0 +1,426 @@
import { spawn } from "node:child_process";
import { existsSync, readFileSync } from "node:fs";
import { join, dirname } from "node:path";
import { fileURLToPath } from "node:url";
import { platform, totalmem, hostname as osHostname } from "node:os";
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 {
resolveMaxOldSpaceMb,
calibrateHeapFallbackMb,
buildServerNodeOptions,
buildNodeHeapArgs,
} from "../../../scripts/build/runtime-env.mjs";
import { resolveTlsOptions } from "../../../scripts/dev/tls-options.mjs";
const __dirname = dirname(fileURLToPath(import.meta.url));
const _pkg = JSON.parse(readFileSync(join(__dirname, "..", "..", "..", "package.json"), "utf8"));
// URL scheme for the "OmniRoute is running" banner — flipped to https when
// opt-in TLS (#5242) is active. Process-scoped: one `serve` run = one scheme.
let urlScheme = "http";
const ROOT = join(__dirname, "..", "..", "..");
// The standalone bundle ships in `dist/` (since the build-output-isolation
// refactor). Fall back to the legacy `app/` location so an upgrade over a
// partially-replaced install — or a package built before the rename — still
// boots. Backward-compatible by design: every deployed runtime keeps its path.
const APP_DIR = existsSync(join(ROOT, "dist", "server.js"))
? join(ROOT, "dist")
: join(ROOT, "app");
function parsePort(value, fallback) {
const parsed = parseInt(String(value), 10);
return Number.isFinite(parsed) && parsed > 0 && parsed <= 65535 ? parsed : fallback;
}
export function registerServe(program) {
program
.command("serve", { isDefault: true })
.description(t("serve.description"))
.option("--port <port>", t("serve.port"))
.option("--no-open", t("serve.no_open"))
.option("--daemon", t("serve.daemon"))
.option("--log", t("serve.log"))
.option("--no-recovery", t("serve.no_recovery"))
.option("--max-restarts <n>", t("serve.max_restarts"), parseInt, 2)
.option("--tray", t("serve.tray") || "Show system tray icon (desktop only)")
.option("--no-tray", t("serve.no_tray") || "Disable system tray icon")
.option(
"--tls-cert <path>",
t("serve.tls_cert") ||
"Path to a TLS certificate (PEM) to serve HTTPS (also OMNIROUTE_TLS_CERT)"
)
.option(
"--tls-key <path>",
t("serve.tls_key") ||
"Path to the TLS private key (PEM) to serve HTTPS (also OMNIROUTE_TLS_KEY)"
)
.action(async (opts) => {
await runServe(opts);
});
}
export async function runServe(opts = {}) {
const startedAt = performance.now();
const { isNativeBinaryCompatible } =
await import("../../../scripts/build/native-binary-compat.mjs");
const { getNodeRuntimeSupport, getNodeRuntimeWarning } =
await import("../../nodeRuntimeSupport.mjs");
const port = parsePort(opts.port ?? process.env.PORT ?? "20128", 20128);
const apiPort = parsePort(process.env.API_PORT ?? String(port), port);
const dashboardPort = parsePort(process.env.DASHBOARD_PORT ?? String(port), port);
const noOpen = opts.open === false;
console.log(`
\x1b[36m ____ _ ____ _
/ __ \\ (_) __ \\ | |
| | | |_ __ ___ _ __ _| |__) |___ _ _| |_ ___
| | | | '_ \` _ \\| '_ \\ | _ // _ \\| | | | __/ _ \\
| |__| | | | | | | | | | | | \\ \\ (_) | |_| | || __/
\\____/|_| |_| |_|_| |_|_|_| \\_\\___/ \\__,_|\\__\\___|
\x1b[0m`);
console.log(`\x1b[2m v${_pkg.version}\x1b[0m\n`);
const nodeSupport = getNodeRuntimeSupport();
if (!nodeSupport.nodeCompatible) {
const runtimeWarning = getNodeRuntimeWarning() || "Unsupported Node.js runtime detected.";
console.warn(`\x1b[33m ⚠ Warning: You are running Node.js ${process.versions.node}.
${runtimeWarning}
Supported secure runtimes: ${nodeSupport.supportedDisplay}
Recommended: use Node.js ${nodeSupport.recommendedVersion} or newer on the 22.x LTS line.
Workaround: npm rebuild better-sqlite3
Or run: omniroute runtime repair (rebuilds into a user-writable runtime; works without a C++ toolchain)\x1b[0m
`);
}
const serverWsJs = join(APP_DIR, "server-ws.mjs");
const serverJs = existsSync(serverWsJs) ? serverWsJs : join(APP_DIR, "server.js");
if (!existsSync(serverJs)) {
console.error("\x1b[31m✖ Server not found at:\x1b[0m", serverJs);
console.error(" The package may not have been built correctly.");
console.error("");
const nodeExec = process.execPath || "";
const isMise = nodeExec.includes("mise") || nodeExec.includes(".local/share/mise");
const isNvm = nodeExec.includes(".nvm") || nodeExec.includes("nvm");
if (isMise) {
console.error(
" \x1b[33m⚠ mise detected:\x1b[0m If you installed via `npm install -g omniroute`,"
);
console.error(" try: \x1b[36mnpx omniroute@latest\x1b[0m (downloads a fresh copy)");
console.error(" or: \x1b[36mmise exec -- npx omniroute\x1b[0m");
} else if (isNvm) {
console.error(
" \x1b[33m⚠ nvm detected:\x1b[0m Try reinstalling after loading the correct Node version:"
);
console.error(" \x1b[36mnvm use --lts && npm install -g omniroute\x1b[0m");
} else {
console.error(" Try: \x1b[36mnpm install -g omniroute\x1b[0m (reinstall)");
console.error(" Or: \x1b[36mnpx omniroute@latest\x1b[0m");
}
process.exit(1);
}
const sqliteBinary = join(
APP_DIR,
"node_modules",
"better-sqlite3",
"build",
"Release",
"better_sqlite3.node"
);
if (existsSync(sqliteBinary) && !isNativeBinaryCompatible(sqliteBinary)) {
console.error(
"\x1b[31m✖ better-sqlite3 native module is incompatible with this platform.\x1b[0m"
);
console.error(` Run: cd ${APP_DIR} && npm rebuild better-sqlite3`);
console.error(
" Or run: \x1b[36momniroute runtime repair\x1b[0m" +
" (rebuilds into a user-writable runtime; works without a C++ toolchain)"
);
if (platform() === "darwin") {
console.error(" If build tools are missing: xcode-select --install");
}
process.exit(1);
}
console.log(` \x1b[2m⏳ Starting server...\x1b[0m\n`);
// #5172/#5160/#5152: default the V8 heap to ~35% of physical RAM (clamped
// [512, 4096]) instead of a fixed 512MB, which OOM-crashed boxes with plenty
// of RAM under load. An explicit OMNIROUTE_MEMORY_MB still wins.
const memoryLimit = resolveMaxOldSpaceMb(
process.env.OMNIROUTE_MEMORY_MB,
calibrateHeapFallbackMb(totalmem())
);
// #5242: opt-in native HTTPS. CLI flags take precedence over env; the child
// server (server-ws.mjs) reads these and terminates TLS on the same listener.
const tlsCert = opts.tlsCert ?? process.env.OMNIROUTE_TLS_CERT;
const tlsKey = opts.tlsKey ?? process.env.OMNIROUTE_TLS_KEY;
const env = {
...process.env,
OMNIROUTE_PORT: String(port),
PORT: String(dashboardPort),
DASHBOARD_PORT: String(dashboardPort),
API_PORT: String(apiPort),
// #6194: POSIX shells (bash/zsh) auto-set HOSTNAME to the machine name — the
// .env loader (first-wins) can never override it. Ignore HOSTNAME when it
// matches the OS-reported hostname (the auto-set signature). OMNIROUTE_SERVER_HOST
// takes precedence; legacy HOSTNAME values that don't match os.hostname() are
// still honoured for backward compatibility (e.g. Windows CMD/PowerShell users
// who set HOSTNAME in .env where it is NOT auto-set).
HOSTNAME:
process.env.OMNIROUTE_SERVER_HOST ||
(process.env.HOSTNAME !== osHostname() ? process.env.HOSTNAME : undefined) ||
"0.0.0.0",
NODE_ENV: "production",
// #5238: preserve a user-set NODE_OPTIONS (incl. their own
// `--max-old-space-size=…`) instead of clobbering it with the calibrated
// default — mirror the Electron/standalone launchers.
NODE_OPTIONS: buildServerNodeOptions(process.env, memoryLimit),
...(tlsCert ? { OMNIROUTE_TLS_CERT: tlsCert } : {}),
...(tlsKey ? { OMNIROUTE_TLS_KEY: tlsKey } : {}),
};
// Validate the TLS pair up front so the operator sees a clear warning in the
// CLI (the child re-validates authoritatively). Drives the banner scheme;
// when null we fall through to identical plain-HTTP behavior as before.
urlScheme = resolveTlsOptions(env) ? "https" : "http";
const isDaemon = opts.daemon === true;
const useTray = opts.tray === true;
if (isDaemon) {
return runDaemon(serverJs, env, memoryLimit, dashboardPort, apiPort);
}
if (opts.noRecovery) {
return runWithoutRecovery(
serverJs,
env,
memoryLimit,
dashboardPort,
apiPort,
noOpen,
startedAt
);
}
return runWithSupervisor(
serverJs,
env,
memoryLimit,
dashboardPort,
apiPort,
noOpen,
opts.log === true,
opts.maxRestarts ?? 2,
startedAt,
useTray
);
}
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("node", [...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`);
console.log(` \x1b[1mDashboard:\x1b[0m ${urlScheme}://localhost:${dashboardPort}`);
console.log(` \x1b[1mAPI Base:\x1b[0m ${urlScheme}://localhost:${apiPort}/v1`);
}
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("node", [...buildNodeHeapArgs(process.env, memoryLimit), serverJs], {
cwd: APP_DIR,
env,
stdio: "pipe",
});
writePidFile("server", server.pid);
let started = false;
server.stdout.on("data", (data) => {
const text = data.toString();
process.stdout.write(text);
if (
!started &&
(text.includes("Ready") || text.includes("started") || text.includes("listening"))
) {
started = true;
onReady(dashboardPort, apiPort, noOpen, startedAt);
}
});
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);
process.exit(1);
});
server.on("exit", (code) => {
if (code !== 0 && code !== null) {
console.error(`\x1b[31m✖ Server exited with code ${code}\x1b[0m`);
}
process.exit(code ?? 0);
});
const shutdown = () => {
console.log("\n\x1b[33m⏹ Shutting down OmniRoute...\x1b[0m");
cleanupPidFile("server");
server.kill("SIGTERM");
setTimeout(() => {
server.kill("SIGKILL");
process.exit(0);
}, 5000);
};
process.on("SIGINT", shutdown);
process.on("SIGTERM", shutdown);
setTimeout(() => {
if (!started) {
started = true;
onReady(dashboardPort, apiPort, noOpen, startedAt);
}
}, 15000);
}
async function runWithSupervisor(
serverJs,
env,
memoryLimit,
dashboardPort,
apiPort,
noOpen,
showLog,
maxRestarts,
startedAt,
useTray = false
) {
if (showLog) process.env.OMNIROUTE_SHOW_LOG = "1";
const supervisor = new ServerSupervisor({
serverPath: serverJs,
env,
memoryLimit,
maxRestarts,
onCrashCallback: async (crashLog) => {
if (detectMitmCrash(crashLog)) {
try {
const PROJECT_ROOT = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "..");
const { updateSettings } = await import(`${PROJECT_ROOT}/src/lib/db/settings.ts`);
updateSettings({ mitmEnabled: false });
} catch {}
return "disable-mitm-and-retry";
}
return null;
},
});
supervisor.start();
process.on("SIGINT", () => {
killTrayIfActive();
supervisor.stop();
});
process.on("SIGTERM", () => {
killTrayIfActive();
supervisor.stop();
});
if (!showLog) {
waitForServer(dashboardPort, 60000).then(async (up) => {
if (up) {
if (useTray) await maybeStartTray(dashboardPort, apiPort, supervisor);
onReady(dashboardPort, apiPort, noOpen, startedAt);
}
});
}
}
let _killTray = null;
function killTrayIfActive() {
if (_killTray) {
try {
_killTray();
} catch {}
_killTray = null;
}
}
async function maybeStartTray(port, apiPort, supervisor) {
try {
const { initTray, isTraySupported } = await import("../tray/index.mjs");
if (!isTraySupported()) return;
const { default: open } = await import("open").catch(() => ({ default: null }));
const dashboardUrl = `${urlScheme}://localhost:${port}`;
const tray = await initTray({
port,
onQuit: () => {
killTrayIfActive();
supervisor.stop();
},
onOpenDashboard: () => open?.(dashboardUrl),
onShowLogs: () => {
// In-place: open logs stream (best-effort)
process.stdout.write(`[omniroute][tray] Logs at: ${dashboardUrl}/logs\n`);
},
});
if (tray) {
const { killTray } = await import("../tray/index.mjs");
_killTray = killTray;
}
} catch (err) {
// tray is optional — do not fail the server, but surface why it failed so
// "--tray shows nothing" is diagnosable instead of silent (#4605).
process.stderr.write(`[omniroute][tray] failed to start: ${err?.message ?? String(err)}\n`);
}
}
async function onReady(dashboardPort, apiPort, noOpen, startedAt) {
const dashboardUrl = `${urlScheme}://localhost:${dashboardPort}`;
const apiUrl = `${urlScheme}://localhost:${apiPort}`;
const elapsed =
typeof startedAt === "number" && Number.isFinite(startedAt)
? ((performance.now() - startedAt) / 1000).toFixed(1)
: "0.0";
console.log(`
\x1b[32m✔ OmniRoute is running!\x1b[0m \x1b[2m(started in ${elapsed}s)\x1b[0m
\x1b[1m Dashboard:\x1b[0m ${dashboardUrl}
\x1b[1m API Base:\x1b[0m ${apiUrl}/v1
\x1b[2m Point your CLI tool (Cursor, Cline, Codex) to:\x1b[0m
\x1b[33m ${apiUrl}/v1\x1b[0m
\x1b[2m Press Ctrl+C to stop\x1b[0m
`);
if (!noOpen && !isTermux()) {
try {
const open = await import("open");
await open.default(dashboardUrl);
} catch {
// open is optional — skip if unavailable
}
}
}
+108
View File
@@ -0,0 +1,108 @@
import { createInterface } from "node:readline";
import { apiFetch } from "../api.mjs";
import { emit } from "../output.mjs";
import { t } from "../i18n.mjs";
function fmtTs(v) {
if (!v) return "-";
return new Date(typeof v === "number" ? v * 1000 : v).toLocaleString();
}
function truncate(v, max = 30) {
if (!v) return "-";
const s = String(v);
return s.length > max ? s.slice(0, max - 1) + "…" : s;
}
async function confirm(q) {
const rl = createInterface({ input: process.stdin, output: process.stdout });
return new Promise((resolve) => {
rl.question(`${q} [y/N] `, (a) => {
rl.close();
resolve(a.trim().toLowerCase() === "y");
});
});
}
const sessionSchema = [
{ key: "id", header: "Session ID", width: 28 },
{ key: "user", header: "User", width: 22 },
{ key: "kind", header: "Kind", width: 14 },
{ key: "createdAt", header: "Created", formatter: fmtTs },
{ key: "expiresAt", header: "Expires", formatter: fmtTs },
{ key: "lastSeen", header: "Last Seen", formatter: fmtTs },
{ key: "ip", header: "IP" },
{ key: "userAgent", header: "User Agent", width: 30, formatter: (v) => truncate(v, 30) },
];
export function registerSessions(program) {
const s = program.command("sessions").description(t("sessions.description"));
s.command("list")
.option("--user <u>", t("sessions.list.user"))
.option("--kind <k>", t("sessions.list.kind"))
.option("--active", t("sessions.list.active"))
.option("--limit <n>", t("sessions.list.limit"), parseInt, 100)
.action(async (opts, cmd) => {
const params = new URLSearchParams({ limit: String(opts.limit) });
if (opts.user) params.set("user", opts.user);
if (opts.kind) params.set("kind", opts.kind);
if (opts.active) params.set("active", "true");
const res = await apiFetch(`/api/sessions?${params}`);
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
const data = await res.json();
emit(data.items ?? data, cmd.optsWithGlobals(), sessionSchema);
});
s.command("show <sessionId>").action(async (id, opts, cmd) => {
const res = await apiFetch(`/api/sessions?id=${id}`);
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
emit(await res.json(), cmd.optsWithGlobals());
});
s.command("expire <sessionId>")
.option("--yes", t("sessions.expire.yes"))
.action(async (id, opts, cmd) => {
if (!opts.yes) {
const ok = await confirm(`Expire session ${id}?`);
if (!ok) return;
}
const res = await apiFetch(`/api/sessions?id=${id}`, { method: "DELETE" });
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
process.stdout.write("Expired\n");
});
s.command("expire-all")
.requiredOption("--user <u>", t("sessions.expireAll.user"))
.option("--yes", t("sessions.expireAll.yes"))
.action(async (opts, cmd) => {
if (!opts.yes) {
const ok = await confirm(`Expire ALL sessions for ${opts.user}?`);
if (!ok) return;
}
const res = await apiFetch(`/api/sessions?user=${opts.user}`, { method: "DELETE" });
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
process.stdout.write("Expired all\n");
});
s.command("current").action(async (opts, cmd) => {
const res = await apiFetch("/api/sessions?current=true");
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
emit(await res.json(), cmd.optsWithGlobals());
});
}
+146
View File
@@ -0,0 +1,146 @@
/**
* omniroute setup-aider — configure Aider (aider.chat) for OmniRoute.
*
* Aider (LiteLLM under the hood) talks to an OpenAI-compatible endpoint via env
* `OPENAI_API_BASE` (ROOT url — LiteLLM appends /v1/chat/completions) + the model
* flag `--model openai/<model>`. This writes ~/.aider.conf.yml (openai-api-base +
* model) — the key stays in OPENAI_API_KEY (env, never the file) — and prints the
* guaranteed env recipe + headless command. Remote-aware.
*/
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import os from "node:os";
import { printHeading, printInfo, printSuccess, printError, createPrompt } from "../io.mjs";
import { resolveActiveContext } from "../contexts.mjs";
function stripToRoot(url) {
const s = String(url || "").replace(/\/+$/, "");
return s.endsWith("/v1") ? s.slice(0, -3) : s;
}
/** Resolve OPENAI_API_BASE (ROOT, no /v1 — LiteLLM appends) + apiKey. */
export function resolveAiderTarget(opts = {}) {
let root;
if (opts.remote) root = stripToRoot(opts.remote);
else {
try {
root = stripToRoot(resolveActiveContext(opts.context ?? process.env.OMNIROUTE_CONTEXT)?.baseUrl);
} catch {
/* none */
}
if (!root) root = `http://localhost:${Number(opts.port ?? process.env.PORT ?? 20128) || 20128}`;
}
let apiKey = opts.apiKey ?? opts["api-key"];
if (!apiKey) {
try {
const c = resolveActiveContext(opts.context ?? process.env.OMNIROUTE_CONTEXT);
apiKey = c?.accessToken || c?.apiKey;
} catch {
/* none */
}
}
if (!apiKey) apiKey = process.env.OMNIROUTE_API_KEY || "";
return { apiBase: root, apiKey };
}
/** Merge openai-api-base + model into an .aider.conf.yml object (preserve rest). */
export function buildAiderConfig(existing, { apiBase, model }) {
const cfg = existing && typeof existing === "object" ? { ...existing } : {};
cfg["openai-api-base"] = apiBase;
if (model) cfg.model = `openai/${model}`;
return cfg;
}
/** The guaranteed env + run recipe (pure → testable). */
export function buildAiderRecipe({ apiBase, model }) {
return [
`export OPENAI_API_BASE=${apiBase}`,
"export OPENAI_API_KEY=$OMNIROUTE_API_KEY",
`aider --model openai/${model}`,
`# headless: aider --model openai/${model} --message "reply OK" --yes`,
].join("\n");
}
function readYamlSafe(yaml, path) {
try {
if (existsSync(path)) return yaml.load(readFileSync(path, "utf8")) || {};
} catch {
/* corrupt/missing */
}
return {};
}
async function fetchModelIds(apiBase, apiKey) {
try {
const headers = { "Content-Type": "application/json" };
if (apiKey) headers["Authorization"] = `Bearer ${apiKey}`;
const res = await fetch(`${apiBase}/v1/models`, { headers, signal: AbortSignal.timeout(8000) });
if (!res.ok) return [];
const body = await res.json();
const list = Array.isArray(body) ? body : body.data ?? body.models ?? [];
return list.map((m) => (typeof m === "string" ? m : m?.id)).filter(Boolean);
} catch {
return [];
}
}
export async function runSetupAiderCommand(opts = {}) {
const { apiBase, apiKey } = resolveAiderTarget(opts);
const dryRun = Boolean(opts.dryRun ?? opts["dry-run"]);
const configPath = opts.configPath ?? opts["config-path"] ?? join(os.homedir(), ".aider.conf.yml");
printHeading("OmniRoute → Aider (openai-compatible via LiteLLM)");
printInfo(`OPENAI_API_BASE: ${apiBase} (no /v1 — LiteLLM appends it)`);
let model = opts.model;
if (!model) {
const ids = await fetchModelIds(apiBase, apiKey);
if (ids.length && !opts.yes) {
printInfo(`Examples: ${ids.slice(0, 20).join(", ")}${ids.length > 20 ? " …" : ""}`);
const prompt = createPrompt();
try {
model = await prompt.ask("Model id for Aider (without the openai/ prefix)");
} finally {
prompt.close();
}
}
}
if (!model) {
printError("A model is required. Pass --model <id> (the openai/ prefix is added automatically).");
return 2;
}
const yaml = await import("js-yaml");
const merged = buildAiderConfig(readYamlSafe(yaml, configPath), { apiBase, model });
const out = yaml.dump(merged, { lineWidth: -1 });
if (dryRun) {
console.log("\n" + out);
printInfo(`[dry-run] → ${configPath}`);
} else {
mkdirSync(join(configPath, ".."), { recursive: true });
writeFileSync(configPath, out, "utf8");
printSuccess(`Wrote ${configPath}`);
}
printInfo("\nProvide the key + run (the key stays in the env, never the file):");
console.log(buildAiderRecipe({ apiBase, model }));
return 0;
}
export function registerSetupAider(program) {
program
.command("setup-aider")
.description("Configure Aider for OmniRoute: write ~/.aider.conf.yml + print the env recipe")
.option("--port <port>", "Local OmniRoute port (ignored when --remote is set)", "20128")
.option("--remote <url>", "Remote OmniRoute URL, e.g. http://192.168.0.15:20128")
.option("--api-key <key>", "OmniRoute API key (defaults to OMNIROUTE_API_KEY env var)")
.option("--model <id>", "Model id (the openai/ prefix is added automatically)")
.option("--config-path <path>", ".aider.conf.yml path (default: ~/.aider.conf.yml)")
.option("--yes", "Non-interactive: do not prompt (requires --model)")
.option("--dry-run", "Print what would be written without touching the filesystem")
.action(async (opts) => {
const code = await runSetupAiderCommand(opts);
if (code !== 0) process.exit(code);
});
}
+219
View File
@@ -0,0 +1,219 @@
/**
* omniroute setup-claude — Remote-aware Claude Code profile generator.
*
* Claude Code has no native profile files (unlike Codex). The idiomatic way to
* keep multiple named configs is `CLAUDE_CONFIG_DIR` — a separate config dir per
* profile (its own settings.json, credentials, history, cache). This command
* fetches the live /v1/models catalog from a (possibly remote) OmniRoute and
* writes `~/.claude/profiles/<name>/settings.json` for each supported model,
* reusing the SAME profile names as `setup-codex` (glm52, kimi-k27, …).
*
* Launch a profile with: omniroute launch --profile <name>
* (which injects ANTHROPIC_AUTH_TOKEN from the active context — the token is
* never written to disk). Or export ANTHROPIC_AUTH_TOKEN and run:
* CLAUDE_CONFIG_DIR=~/.claude/profiles/<name> claude
*
* Idempotent: re-running overwrites each profile's settings.json in place.
*/
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import os from "node:os";
import { printHeading, printInfo, printSuccess, printError } from "../io.mjs";
import {
categoriseModel,
isCodexCompatibleTextModel,
profileNameFromModelId,
} from "./setup-codex.mjs";
/** Map a Codex-style effort to a Claude Code settings.json effortLevel. */
function effortLevelFor(cfg) {
// Codex categories use xhigh/high/low/undefined; Claude Code accepts the same
// names (low|medium|high|xhigh). Pass through, omit for the "simple" tier.
return cfg.effort || undefined;
}
/**
* Generic profile for a live-catalog model that `categoriseModel()` doesn't
* recognize (e.g. any provider added after the hardcoded glm/kimi/mimo/…
* pattern list was written). Mirrors setup-codex.mjs's fallbackCodexProfile()
* so setup-claude never silently produces zero profiles for a fresh catalog.
*/
export function fallbackClaudeProfile(modelId, model) {
if (!isCodexCompatibleTextModel(model)) return null;
return { name: profileNameFromModelId(modelId) };
}
/** Build the settings.json content for one Claude Code profile. */
export function buildProfileSettings(modelId, baseUrl, cfg) {
const env = {
ANTHROPIC_BASE_URL: baseUrl,
ANTHROPIC_MODEL: modelId,
CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY: "1",
CLAUDE_CODE_AUTO_COMPACT_WINDOW: "190000",
};
const settings = {
$schema: "https://json.schemastore.org/claude-code-settings.json",
model: modelId,
env,
};
const effort = effortLevelFor(cfg);
if (effort) settings.effortLevel = effort;
// NOTE: ANTHROPIC_AUTH_TOKEN is intentionally NOT written here — `omniroute
// launch --profile` injects it from the active context, keeping the secret off
// disk. For direct `CLAUDE_CONFIG_DIR=… claude` use, export it in your shell.
return JSON.stringify(settings, null, 2) + "\n";
}
/**
* Generate Claude Code profile files for a live model catalog. Shared by the
* `setup-claude` CLI command and the post-model-sync auto-sync so both stay
* behaviorally identical. Writes `<claudeHome>/profiles/<name>/settings.json`
* (directory-per-profile); never touches the active/default Claude config.
* @param {Array} models
* @param {{claudeHome?:string, baseUrl:string, dryRun?:boolean, only?:string, log?:(line:string)=>void}} opts
* @returns {Promise<{written:number, skipped:number, profiles:Array<{name:string, model:string, filePath:string}>}>}
*/
export async function syncClaudeProfilesFromModels(models, opts = {}) {
const claudeHome = opts.claudeHome || join(os.homedir(), ".claude");
const profilesRoot = join(claudeHome, "profiles");
const baseUrl = opts.baseUrl;
const dryRun = Boolean(opts.dryRun);
// Injectable dry-run printer (#5959): under the node:test runner, a child
// process writing multi-byte UTF-8 (the "──" box-drawing heading) to stdout
// corrupts the runner's V8-serialized event stream ~50% of the time
// ("Unable to deserialize cloned data due to invalid or unsupported
// version"). Tests inject a collector; the CLI default stays console.log.
const log = opts.log ?? console.log;
const onlyFilter = opts.only ? opts.only.split(",").map((s) => s.trim()) : null;
if (!dryRun && !existsSync(profilesRoot)) {
mkdirSync(profilesRoot, { recursive: true });
}
let written = 0;
let skipped = 0;
const profiles = [];
for (const m of models) {
const id = typeof m === "string" ? m : (m.id ?? "");
if (!id) {
skipped++;
continue;
}
if (onlyFilter && !onlyFilter.some((f) => id.includes(f))) {
skipped++;
continue;
}
const cfg = categoriseModel(id) ?? fallbackClaudeProfile(id, m);
if (!cfg) {
skipped++;
continue;
}
const dir = join(profilesRoot, cfg.name);
const filePath = join(dir, "settings.json");
const content = buildProfileSettings(id, baseUrl, cfg);
if (dryRun) {
log(`\n── [dry-run] ${filePath} ──`);
log(content);
} else {
mkdirSync(dir, { recursive: true });
writeFileSync(filePath, content, "utf8");
}
profiles.push({ name: cfg.name, model: id, filePath });
written++;
}
return { written, skipped, profiles };
}
/**
* @param {{remote?:string, port?:string, apiKey?:string, claudeHome?:string, dryRun?:boolean, only?:string}} opts
* @returns {Promise<number>}
*/
export async function runSetupClaudeCommand(opts = {}) {
const port = Number(opts.port ?? process.env.PORT ?? 20128) || 20128;
const baseUrl = (opts.remote ?? `http://localhost:${port}`)
.replace(/\/+$/, "")
.replace(/\/v1$/, "");
const apiKey = opts.apiKey ?? opts["api-key"] ?? process.env.OMNIROUTE_API_KEY ?? "";
const claudeHome = opts.claudeHome ?? opts["claude-home"] ?? join(os.homedir(), ".claude");
const profilesRoot = join(claudeHome, "profiles");
const dryRun = Boolean(opts.dryRun ?? opts["dry-run"]);
printHeading("OmniRoute → Claude Code profile generator");
printInfo(`Connecting to ${baseUrl}`);
// ── Fetch model catalog ───────────────────────────────────────────────────
let models;
try {
const headers = { "Content-Type": "application/json" };
if (apiKey) headers["Authorization"] = `Bearer ${apiKey}`;
const res = await fetch(`${baseUrl}/v1/models`, {
headers,
signal: AbortSignal.timeout(10000),
});
if (!res.ok) throw new Error(`HTTP ${res.status} ${res.statusText}`);
const body = await res.json();
models = body.data ?? body.models ?? [];
} catch (err) {
printError(`Failed to fetch models: ${err.message}`);
printInfo(
"Make sure OmniRoute is running and the --remote URL is correct.\n" +
"You may also need --api-key if OmniRoute requires authentication."
);
return 1;
}
printInfo(`Received ${models.length} models from ${baseUrl}`);
const { written, skipped, profiles } = await syncClaudeProfilesFromModels(models, {
claudeHome,
baseUrl,
dryRun,
only: opts.only,
});
if (!dryRun) {
for (const profile of profiles) {
printSuccess(` ✓ profiles/${profile.name}/settings.json (${profile.model})`);
}
console.log("");
printSuccess(`${written} Claude Code profiles written to ${profilesRoot}`);
if (skipped > 0) printInfo(`${skipped} models skipped (no matching profile pattern)`);
console.log("\nTo use a profile:");
console.log(" omniroute launch --profile <name> # e.g. omniroute launch --profile glm52");
console.log(
" # or: CLAUDE_CONFIG_DIR=~/.claude/profiles/<name> claude (export ANTHROPIC_AUTH_TOKEN first)"
);
} else {
console.log(`\n[dry-run] ${written} profiles would be written (${skipped} skipped)`);
}
return 0;
}
export function registerSetupClaude(program) {
program
.command("setup-claude")
.description(
"Fetch the live model catalog from OmniRoute (local or remote VPS) and generate " +
"~/.claude/profiles/<name>/ Claude Code profiles (CLAUDE_CONFIG_DIR) for each model"
)
.option("--port <port>", "Local OmniRoute port (ignored when --remote is set)", "20128")
.option("--remote <url>", "Remote OmniRoute URL, e.g. http://192.168.0.15:20128")
.option("--api-key <key>", "OmniRoute API key (defaults to OMNIROUTE_API_KEY env var)")
.option("--claude-home <dir>", "Claude home dir (default: ~/.claude)")
.option(
"--only <patterns>",
"Comma-separated substrings — only matching model IDs (e.g. glm,kimi)"
)
.option("--dry-run", "Print what would be written without touching the filesystem")
.action(async (opts) => {
const exitCode = await runSetupClaudeCommand(opts);
if (exitCode !== 0) process.exit(exitCode);
});
}
+160
View File
@@ -0,0 +1,160 @@
/**
* omniroute setup-cline — configure the Cline AI coding agent to use OmniRoute.
*
* Cline's VS Code extension keeps its config in VS Code's opaque globalStorage
* (not file-writable). Its CLI/standalone mode reads ~/.cline/data/. This command
* writes the CLI-mode files (matching the OmniRoute dashboard) AND prints the
* Base URL / model to paste into the VS Code extension UI.
*
* Cline uses the OpenAI-compatible provider: openAiBaseUrl is the ROOT URL
* (no /v1 — Cline appends /v1/chat/completions). Plan + Act modes are set to the
* same provider/model. The key goes in secrets.json (Cline has no env ref).
*/
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import os from "node:os";
import { printHeading, printInfo, printSuccess, printError, createPrompt } from "../io.mjs";
import { resolveActiveContext } from "../contexts.mjs";
function stripToRoot(url) {
let s = String(url || "").replace(/\/+$/, "");
return s.endsWith("/v1") ? s.slice(0, -3) : s;
}
/** Resolve baseUrl (ROOT, no /v1) + apiKey from flags → active context → localhost. */
export function resolveClineTarget(opts = {}) {
let baseUrl;
if (opts.remote) baseUrl = stripToRoot(opts.remote);
else {
try {
baseUrl = stripToRoot(resolveActiveContext(opts.context ?? process.env.OMNIROUTE_CONTEXT)?.baseUrl);
} catch {
/* none */
}
if (!baseUrl) baseUrl = `http://localhost:${Number(opts.port ?? process.env.PORT ?? 20128) || 20128}`;
}
let apiKey = opts.apiKey ?? opts["api-key"];
if (!apiKey) {
try {
const c = resolveActiveContext(opts.context ?? process.env.OMNIROUTE_CONTEXT);
apiKey = c?.accessToken || c?.apiKey;
} catch {
/* none */
}
}
if (!apiKey) apiKey = process.env.OMNIROUTE_API_KEY || "";
return { baseUrl, apiKey };
}
/** Merge OmniRoute openai-compatible settings into Cline's globalState (Plan + Act). */
export function buildClineGlobalState(existing, { baseUrl, model }) {
const gs = { ...(existing || {}) };
gs.actModeApiProvider = "openai";
gs.planModeApiProvider = "openai";
gs.openAiBaseUrl = baseUrl; // ROOT — Cline appends /v1/chat/completions
if (model) {
gs.openAiModelId = model;
gs.planModeOpenAiModelId = model;
}
return gs;
}
/** Merge the API key into Cline's secrets (Cline has no env-var reference). */
export function buildClineSecrets(existing, { apiKey }) {
return { ...(existing || {}), openAiApiKey: apiKey || "sk_omniroute" };
}
function readJson(path) {
try {
if (existsSync(path)) return JSON.parse(readFileSync(path, "utf8"));
} catch {
/* corrupt/missing → start fresh */
}
return {};
}
async function fetchModelIds(baseUrl, apiKey) {
try {
const headers = { "Content-Type": "application/json" };
if (apiKey) headers["Authorization"] = `Bearer ${apiKey}`;
const res = await fetch(`${baseUrl}/v1/models`, { headers, signal: AbortSignal.timeout(8000) });
if (!res.ok) return [];
const body = await res.json();
const list = Array.isArray(body) ? body : body.data ?? body.models ?? [];
return list.map((m) => (typeof m === "string" ? m : m?.id)).filter(Boolean);
} catch {
return [];
}
}
export async function runSetupClineCommand(opts = {}) {
const { baseUrl, apiKey } = resolveClineTarget(opts);
const dryRun = Boolean(opts.dryRun ?? opts["dry-run"]);
const clineDir = opts.clineDir ?? opts["cline-dir"] ?? join(os.homedir(), ".cline", "data");
printHeading("OmniRoute → Cline (OpenAI-compatible)");
printInfo(`Server: ${baseUrl}`);
// Resolve the model (Cline needs one explicit id — no auto-discovery).
let model = opts.model;
if (!model) {
const ids = await fetchModelIds(baseUrl, apiKey);
if (ids.length && !opts.yes) {
printInfo(`Examples: ${ids.slice(0, 20).join(", ")}${ids.length > 20 ? " …" : ""}`);
const prompt = createPrompt();
try {
model = await prompt.ask("Model id for Cline");
} finally {
prompt.close();
}
}
}
if (!model) {
printError("A model is required. Pass --model <id> (Cline has no model auto-discovery).");
return 2;
}
const gsPath = join(clineDir, "globalState.json");
const secPath = join(clineDir, "secrets.json");
const globalState = buildClineGlobalState(readJson(gsPath), { baseUrl, model });
const secrets = buildClineSecrets(readJson(secPath), { apiKey });
if (dryRun) {
console.log(`\n── [dry-run] ${gsPath} ──`);
console.log(JSON.stringify({ actModeApiProvider: globalState.actModeApiProvider, planModeApiProvider: globalState.planModeApiProvider, openAiBaseUrl: globalState.openAiBaseUrl, openAiModelId: globalState.openAiModelId }, null, 2));
console.log(`\n── [dry-run] ${secPath} ── (openAiApiKey: ${apiKey ? "set" : "sk_omniroute"})`);
} else {
if (!existsSync(clineDir)) mkdirSync(clineDir, { recursive: true });
writeFileSync(gsPath, JSON.stringify(globalState, null, 2) + "\n", "utf8");
writeFileSync(secPath, JSON.stringify(secrets, null, 2) + "\n", "utf8");
printSuccess(`Wrote ${gsPath}`);
printSuccess(`Wrote ${secPath}`);
}
// The VS Code extension uses opaque globalStorage — can't be file-written.
printInfo("\nFor the Cline VS Code extension, set these in its Settings → API (OpenAI Compatible):");
printInfo(` Base URL: ${baseUrl} (NOT /v1 — Cline appends it)`);
printInfo(` API Key: <your OMNIROUTE_API_KEY>`);
printInfo(` Model: ${model}`);
return 0;
}
export function registerSetupCline(program) {
program
.command("setup-cline")
.description(
"Configure Cline for OmniRoute: write ~/.cline/data (CLI mode) + print VS Code extension settings"
)
.option("--port <port>", "Local OmniRoute port (ignored when --remote is set)", "20128")
.option("--remote <url>", "Remote OmniRoute URL, e.g. http://192.168.0.15:20128")
.option("--api-key <key>", "OmniRoute API key (defaults to OMNIROUTE_API_KEY env var)")
.option("--model <id>", "Model id for Cline (required unless picked interactively)")
.option("--cline-dir <dir>", "Cline data dir (default: ~/.cline/data)")
.option("--yes", "Non-interactive: do not prompt (requires --model)")
.option("--dry-run", "Print what would be written without touching the filesystem")
.action(async (opts) => {
const code = await runSetupClineCommand(opts);
if (code !== 0) process.exit(code);
});
}
+387
View File
@@ -0,0 +1,387 @@
/**
* omniroute setup-codex — Remote-aware Codex CLI profile generator.
*
* Connects to a running OmniRoute instance (local or remote VPS), fetches the
* live model catalog via GET /v1/models, then generates ~/.codex/<name>.config.toml
* profile files for each model — so you can switch providers with a single flag
* (`codex --profile glm52`) without editing config files by hand.
*
* Primary use-case: configure a local Codex CLI to use models from a VPS.
* omniroute setup-codex --remote http://100.67.86.91:20128 --api-key sk-xxx
*
* The command is idempotent: re-running updates existing profile files in place.
*/
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import os from "node:os";
import { printHeading, printInfo, printSuccess, printError } from "../io.mjs";
import { t } from "../i18n.mjs";
// ── Model categorisation ──────────────────────────────────────────────────────
/**
* Map a model ID (as returned by /v1/models) to a Codex profile configuration.
* Returns null for models that should not get their own profile (e.g. aliases).
*
* Exported so the Claude Code profile generator (`setup-claude`) reuses the SAME
* profile names (glm52, kimi-k27, …) for cross-CLI consistency.
*
* @param {string} modelId
* @returns {{ name:string, ctx:number, compact:number, effort?:string, summary?:boolean, toolLimit:number }|null}
*/
export function categoriseModel(modelId) {
const id = modelId.toLowerCase();
// ── Thinking models (max effort, detailed summary) ────────────────────────
const thinkingPatterns = [
{ re: /kmc\/kimi-k2\.7/, name: "kimi-k27", ctx: 131072, compact: 112000, toolLimit: 32768 },
{ re: /kmc\/kimi-k2\.6/, name: "kimi-k26", ctx: 131072, compact: 112000, toolLimit: 32768 },
{ re: /glm\/glm-5\.2-max/, name: "glm52max", ctx: 131072, compact: 112000, toolLimit: 32768 },
{ re: /glm\/glm-5\.2$/, name: "glm52", ctx: 131072, compact: 112000, toolLimit: 32768 },
{
re: /opencode-go\/mimo-v2\.5-pro/,
name: "mimo-pro",
ctx: 131072,
compact: 112000,
toolLimit: 32768,
},
{
re: /opencode-go\/qwen3\.7-plus/,
name: "qwen37plus",
ctx: 32768,
compact: 28000,
toolLimit: 16384,
},
];
// ── Good models (high effort) ─────────────────────────────────────────────
const goodPatterns = [
{
re: /ollamacloud\/deepseek-v4-pro/,
name: "deepseek-pro",
ctx: 131072,
compact: 112000,
toolLimit: 32768,
},
{
re: /opencode-go\/mimo-v2\.5$/,
name: "mimo",
ctx: 131072,
compact: 112000,
toolLimit: 32768,
},
];
// ── Simple models (no effort) ─────────────────────────────────────────────
const simplePatterns = [
{ re: /ollamacloud\/gemma4:31b/, name: "gemma4", ctx: 32768, compact: 28000, toolLimit: 16384 },
{
re: /ollamacloud\/nemotron-3-super/,
name: "nemotron",
ctx: 32768,
compact: 28000,
toolLimit: 16384,
},
{
re: /ollamacloud\/gpt-oss:20b/,
name: "gptoss",
ctx: 32768,
compact: 28000,
toolLimit: 16384,
},
];
// ── Fast models (low effort) ──────────────────────────────────────────────
const fastPatterns = [
{
re: /ollamacloud\/deepseek-v4-flash/,
name: "deepseek-flash",
ctx: 65536,
compact: 56000,
toolLimit: 16384,
},
{
re: /ollamacloud\/gemini-3-flash/,
name: "gemini-flash",
ctx: 1000000,
compact: 850000,
toolLimit: 32768,
},
{ re: /glm\/glm-5-turbo/, name: "glm5turbo", ctx: 131072, compact: 112000, toolLimit: 16384 },
{
re: /glm\/glm-4\.7-flash/,
name: "glm47flash",
ctx: 131072,
compact: 112000,
toolLimit: 16384,
},
];
for (const p of thinkingPatterns) {
if (p.re.test(id)) return { ...p, effort: "xhigh", summary: true };
}
for (const p of goodPatterns) {
if (p.re.test(id)) return { ...p, effort: "high", summary: false };
}
for (const p of simplePatterns) {
if (p.re.test(id)) return { ...p, effort: undefined, summary: false };
}
for (const p of fastPatterns) {
if (p.re.test(id)) return { ...p, effort: "low", summary: false };
}
return null;
}
function firstPositiveNumber(...values) {
for (const value of values) {
if (typeof value === "number" && Number.isFinite(value) && value > 0) {
return value;
}
}
return null;
}
function shortHash(value) {
let hash = 5381;
for (let i = 0; i < value.length; i++) {
hash = ((hash << 5) + hash) ^ value.charCodeAt(i);
}
return (hash >>> 0).toString(36);
}
export function profileNameFromModelId(modelId) {
const normalized = String(modelId)
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-+|-+$/g, "");
const base = normalized || "model";
if (base.length <= 96) return base;
return `${base.slice(0, 84).replace(/-+$/g, "")}-${shortHash(base)}`;
}
function hasAnyValue(values, patterns) {
return values.some((value) => patterns.some((pattern) => pattern.test(value)));
}
export function isCodexCompatibleTextModel(model) {
if (typeof model === "string") return true;
const id = String(model?.id ?? "").toLowerCase();
const type = String(model?.type ?? "").toLowerCase();
const outputModalities = Array.isArray(model?.output_modalities)
? model.output_modalities.map((value) => String(value).toLowerCase())
: [];
if (type && !["chat", "text", "language", "llm", "model"].includes(type)) {
return false;
}
const unsupportedPatterns = [
/(^|[/_-])(image|img|video|veo|seedance|audio|speech|voice|tts|stt|whisper)([/_-]|$)/,
/(^|[/_-])(embedding|embeddings|embed|rerank|moderation|transcription)([/_-]|$)/,
];
if (hasAnyValue([id, type], unsupportedPatterns)) return false;
const nonTextModalities = [/^(image|video|audio)$/];
if (hasAnyValue(outputModalities, nonTextModalities)) return false;
if (outputModalities.length > 0 && !outputModalities.includes("text")) {
return false;
}
return true;
}
export function fallbackCodexProfile(modelId, model) {
if (!isCodexCompatibleTextModel(model)) return null;
const ctx =
typeof model === "string"
? 128000
: (firstPositiveNumber(
model.context_length,
model.max_context_window_tokens,
model.max_input_tokens
) ?? 128000);
const maxOutput =
typeof model === "string"
? null
: firstPositiveNumber(model.max_output_tokens, model.output_token_limit);
const toolLimit = Math.min(Math.max(maxOutput ?? 16384, 8192), 32768);
return {
name: profileNameFromModelId(modelId),
ctx,
compact: Math.floor(ctx * 0.85),
summary: false,
toolLimit,
};
}
/** Build the TOML content for a single profile. */
function buildProfileToml(modelId, cfg) {
const lines = [
`# codex --profile ${cfg.name}`,
`# ${modelId}`,
`model = "${modelId}"`,
`model_provider = "omniroute"`,
];
if (cfg.effort) {
lines.push(`model_reasoning_effort = "${cfg.effort}"`);
}
if (cfg.summary) {
lines.push(`model_reasoning_summary = "detailed"`);
}
lines.push(
`model_context_window = ${cfg.ctx}`,
`model_auto_compact_token_limit = ${cfg.compact}`,
`tool_output_token_limit = ${cfg.toolLimit}`
);
return lines.join("\n") + "\n";
}
export async function syncCodexProfilesFromModels(models, opts = {}) {
const codexHome = opts.codexHome || join(os.homedir(), ".codex");
const dryRun = Boolean(opts.dryRun);
const onlyFilter = opts.only ? opts.only.split(",").map((s) => s.trim()) : null;
if (!dryRun && !existsSync(codexHome)) {
mkdirSync(codexHome, { recursive: true });
}
let written = 0;
let skipped = 0;
const profiles = [];
for (const m of models) {
const id = typeof m === "string" ? m : (m.id ?? "");
if (!id) {
skipped++;
continue;
}
if (onlyFilter && !onlyFilter.some((f) => id.includes(f))) {
skipped++;
continue;
}
const cfg = categoriseModel(id) ?? fallbackCodexProfile(id, m);
if (!cfg) {
skipped++;
continue;
}
const filePath = join(codexHome, `${cfg.name}.config.toml`);
const content = buildProfileToml(id, cfg);
if (dryRun) {
console.log(`\n── [dry-run] ${filePath} ──`);
console.log(content);
} else {
writeFileSync(filePath, content, "utf8");
}
profiles.push({ name: cfg.name, model: id, filePath });
written++;
}
return { written, skipped, profiles };
}
// ── Command ───────────────────────────────────────────────────────────────────
/**
* @param {{remote?:string, port?:string, apiKey?:string, codexHome?:string, dryRun?:boolean, only?:string}} opts
* @returns {Promise<number>}
*/
export async function runSetupCodexCommand(opts = {}) {
const port = Number(opts.port ?? process.env.PORT ?? 20128) || 20128;
const baseUrl = (opts.remote ?? `http://localhost:${port}`).replace(/\/v1$/, "");
const apiKey = opts.apiKey ?? opts["api-key"] ?? process.env.OMNIROUTE_API_KEY ?? "";
const codexHome = opts.codexHome ?? opts["codex-home"] ?? join(os.homedir(), ".codex");
const dryRun = Boolean(opts.dryRun ?? opts["dry-run"]);
const onlyFilter = opts.only ? opts.only.split(",").map((s) => s.trim()) : null;
printHeading(`OmniRoute → Codex CLI profile generator`);
printInfo(`Connecting to ${baseUrl}`);
// ── Fetch model catalog ───────────────────────────────────────────────────
let models;
try {
const headers = { "Content-Type": "application/json" };
if (apiKey) headers["Authorization"] = `Bearer ${apiKey}`;
const res = await fetch(`${baseUrl}/v1/models`, {
headers,
signal: AbortSignal.timeout(10000),
});
if (!res.ok) throw new Error(`HTTP ${res.status} ${res.statusText}`);
const body = await res.json();
models = body.data ?? body.models ?? [];
} catch (err) {
printError(`Failed to fetch models: ${err.message}`);
printInfo(
"Make sure OmniRoute is running and the --remote URL is correct.\n" +
"You may also need --api-key if OmniRoute requires authentication."
);
return 1;
}
printInfo(`Received ${models.length} models from ${baseUrl}`);
// ── Generate profiles ─────────────────────────────────────────────────────
const { written, skipped, profiles } = await syncCodexProfilesFromModels(models, {
codexHome,
dryRun,
only: opts.only,
});
if (!dryRun) {
for (const profile of profiles) {
printSuccess(`${profile.name}.config.toml (${profile.model})`);
}
console.log("");
printSuccess(`${written} profiles written to ${codexHome}`);
if (skipped > 0) {
printInfo(`${skipped} models skipped (no matching profile pattern)`);
}
console.log("\nTo use a profile:");
console.log(" codex --profile <name> # e.g. codex --profile glm52");
console.log(" codex -p <name> # short form");
} else {
console.log(`\n[dry-run] ${written} profiles would be written (${skipped} skipped)`);
}
return 0;
}
export function registerSetupCodex(program) {
program
.command("setup-codex")
.description(
"Fetch the live model catalog from OmniRoute (local or remote VPS) and generate " +
"~/.codex/<name>.config.toml profiles for each supported model"
)
.option("--port <port>", "Local OmniRoute port (ignored when --remote is set)", "20128")
.option(
"--remote <url>",
"Remote OmniRoute URL, e.g. http://100.67.86.91:20128 — fetches models from there"
)
.option(
"--api-key <key>",
"OmniRoute API key for the remote instance (defaults to OMNIROUTE_API_KEY env var)"
)
.option("--codex-home <dir>", "Directory where profile files are written (default: ~/.codex)")
.option(
"--only <patterns>",
"Comma-separated substrings — only generate profiles for matching model IDs (e.g. glm,kimi)"
)
.option("--dry-run", "Print what would be written without touching the filesystem")
.action(async (opts) => {
const exitCode = await runSetupCodexCommand(opts);
if (exitCode !== 0) process.exit(exitCode);
});
}
+173
View File
@@ -0,0 +1,173 @@
/**
* omniroute setup-continue — configure Continue (continue.dev) for OmniRoute.
*
* Continue uses a file-based, mergeable ~/.continue/config.yaml shared by the VS
* Code / JetBrains extensions AND the `cn` CLI. Models use `provider: openai`
* with a custom `apiBase` (WITH /v1 — Continue appends /chat/completions) and an
* `apiKey: ${{ secrets.OMNIROUTE_API_KEY }}` reference (secret never written to
* config.yaml). Remote-aware; curated model set with Continue roles.
*/
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import os from "node:os";
import { printHeading, printInfo, printSuccess, printError } from "../io.mjs";
import { resolveActiveContext } from "../contexts.mjs";
import { categoriseModel } from "./setup-codex.mjs";
const SECRET_REF = "${{ secrets.OMNIROUTE_API_KEY }}";
function ensureV1(url) {
const s = String(url || "").replace(/\/+$/, "");
return s.endsWith("/v1") ? s : `${s}/v1`;
}
/** Resolve apiBase (WITH /v1) + apiKey from flags → active context → localhost. */
export function resolveContinueTarget(opts = {}) {
let root;
if (opts.remote) root = String(opts.remote).replace(/\/+$/, "");
else {
try {
root = resolveActiveContext(opts.context ?? process.env.OMNIROUTE_CONTEXT)?.baseUrl;
} catch {
/* none */
}
if (!root) root = `http://localhost:${Number(opts.port ?? process.env.PORT ?? 20128) || 20128}`;
}
let apiKey = opts.apiKey ?? opts["api-key"];
if (!apiKey) {
try {
const c = resolveActiveContext(opts.context ?? process.env.OMNIROUTE_CONTEXT);
apiKey = c?.accessToken || c?.apiKey;
} catch {
/* none */
}
}
if (!apiKey) apiKey = process.env.OMNIROUTE_API_KEY || "";
return { apiBase: ensureV1(root), apiKey };
}
/** Build Continue model entries (provider: openai) for the given catalog ids. */
export function buildContinueModels(modelIds, apiBase) {
const out = [];
for (const id of modelIds) {
const cfg = categoriseModel(id);
if (!cfg) continue;
const roles = ["chat", "edit", "apply"];
if (cfg.effort === "low") roles.push("autocomplete"); // fast tier → autocomplete
out.push({
name: `OmniRoute: ${id}`,
provider: "openai",
model: id,
apiBase,
apiKey: SECRET_REF,
roles,
});
}
return out;
}
/**
* Merge OmniRoute models into an existing Continue config object: drop any prior
* models pointing at this apiBase, keep everything else, append the new set.
*/
export function mergeContinueConfig(existing, newModels, apiBase) {
const cfg = existing && typeof existing === "object" ? { ...existing } : {};
const prior = Array.isArray(cfg.models) ? cfg.models : [];
const kept = prior.filter((m) => !m || m.apiBase !== apiBase);
cfg.models = [...kept, ...newModels];
if (!cfg.name) cfg.name = "OmniRoute Config";
if (!cfg.version) cfg.version = "1.0";
if (!cfg.schema) cfg.schema = "v1";
return cfg;
}
async function fetchModelIds(apiBase, apiKey) {
try {
const headers = { "Content-Type": "application/json" };
if (apiKey) headers["Authorization"] = `Bearer ${apiKey}`;
const res = await fetch(`${apiBase.replace(/\/v1$/, "")}/v1/models`, {
headers,
signal: AbortSignal.timeout(10000),
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const body = await res.json();
const list = Array.isArray(body) ? body : body.data ?? body.models ?? [];
return list.map((m) => (typeof m === "string" ? m : m?.id)).filter(Boolean);
} catch (e) {
throw new Error(`Could not fetch models: ${e.message}`);
}
}
export async function runSetupContinueCommand(opts = {}) {
const { apiBase, apiKey } = resolveContinueTarget(opts);
const dryRun = Boolean(opts.dryRun ?? opts["dry-run"]);
const only = opts.only ? opts.only.split(",").map((s) => s.trim()).filter(Boolean) : null;
const configPath = opts.configPath ?? opts["config-path"] ?? join(os.homedir(), ".continue", "config.yaml");
printHeading("OmniRoute → Continue (config.yaml)");
printInfo(`apiBase: ${apiBase}`);
let ids;
try {
ids = await fetchModelIds(apiBase, apiKey);
} catch (e) {
printError(e.message);
printInfo("Make sure OmniRoute is running and --remote/--api-key are correct.");
return 1;
}
if (only) ids = ids.filter((id) => only.some((f) => id.includes(f)));
const models = buildContinueModels(ids, apiBase);
if (!models.length) {
printError("No matching curated models in the catalog (try --only or check the server).");
return 1;
}
const yaml = await import("js-yaml");
let existing = {};
if (existsSync(configPath)) {
try {
existing = yaml.load(readFileSync(configPath, "utf8")) || {};
} catch {
printInfo("Existing config.yaml unparseable — starting fresh (a .bak is kept).");
if (!dryRun) writeFileSync(`${configPath}.bak`, readFileSync(configPath));
existing = {};
}
}
const merged = mergeContinueConfig(existing, models, apiBase);
const out = yaml.dump(merged, { lineWidth: -1 });
if (dryRun) {
console.log("\n" + (out.length > 3500 ? out.slice(0, 3500) + "\n… (truncated)" : out));
printInfo(`[dry-run] ${models.length} OmniRoute model(s) → ${configPath}`);
return 0;
}
mkdirSync(join(configPath, ".."), { recursive: true });
writeFileSync(configPath, out, "utf8");
printSuccess(`Wrote ${configPath} (${models.length} OmniRoute models)`);
printInfo("\nProvide the key (config.yaml references it, not stores it):");
printInfo(" cn CLI: export OMNIROUTE_API_KEY=... (read from your shell)");
printInfo(" IDE: echo 'OMNIROUTE_API_KEY=...' >> ~/.continue/.env");
printInfo("Run: cn -p \"reply OK\"");
return 0;
}
export function registerSetupContinue(program) {
program
.command("setup-continue")
.description(
"Generate ~/.continue/config.yaml (Continue / cn CLI) from the OmniRoute model catalog"
)
.option("--port <port>", "Local OmniRoute port (ignored when --remote is set)", "20128")
.option("--remote <url>", "Remote OmniRoute URL, e.g. http://192.168.0.15:20128")
.option("--api-key <key>", "OmniRoute API key (defaults to OMNIROUTE_API_KEY env var)")
.option("--only <patterns>", "Comma-separated substrings — keep only matching model IDs")
.option("--config-path <path>", "config.yaml path (default: ~/.continue/config.yaml)")
.option("--dry-run", "Print what would be written without touching the filesystem")
.action(async (opts) => {
const code = await runSetupContinueCommand(opts);
if (code !== 0) process.exit(code);
});
}
+148
View File
@@ -0,0 +1,148 @@
/**
* omniroute setup-crush — configure Crush (charmbracelet/crush) for OmniRoute.
*
* Crush is a terminal AI agent with a file-based config: ~/.config/crush/crush.json
* (or ./crush.json). It supports a custom `openai-compat` provider. base_url must
* include /v1; the api_key may reference an env var (`$OMNIROUTE_API_KEY`) so the
* secret stays out of the file. Remote-aware; curated catalog models.
*/
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import os from "node:os";
import { printHeading, printInfo, printSuccess, printError } from "../io.mjs";
import { resolveActiveContext } from "../contexts.mjs";
import { categoriseModel } from "./setup-codex.mjs";
const API_KEY_REF = "$OMNIROUTE_API_KEY";
function ensureV1(url) {
const s = String(url || "").replace(/\/+$/, "");
return s.endsWith("/v1") ? s : `${s}/v1`;
}
/** Resolve base_url (WITH /v1) + apiKey from flags → active context → localhost. */
export function resolveCrushTarget(opts = {}) {
let root;
if (opts.remote) root = String(opts.remote).replace(/\/+$/, "");
else {
try {
root = resolveActiveContext(opts.context ?? process.env.OMNIROUTE_CONTEXT)?.baseUrl;
} catch {
/* none */
}
if (!root) root = `http://localhost:${Number(opts.port ?? process.env.PORT ?? 20128) || 20128}`;
}
let apiKey = opts.apiKey ?? opts["api-key"];
if (!apiKey) {
try {
const c = resolveActiveContext(opts.context ?? process.env.OMNIROUTE_CONTEXT);
apiKey = c?.accessToken || c?.apiKey;
} catch {
/* none */
}
}
if (!apiKey) apiKey = process.env.OMNIROUTE_API_KEY || "";
return { baseUrl: ensureV1(root), apiKey };
}
/** Build the Crush `openai-compat` provider block from curated catalog ids. */
export function buildCrushProvider(modelIds, baseUrl) {
const models = [];
for (const id of modelIds) {
const cfg = categoriseModel(id);
if (!cfg) continue;
models.push({ id, name: `OmniRoute: ${id}`, context_window: cfg.ctx });
}
return {
type: "openai-compat",
base_url: baseUrl,
api_key: API_KEY_REF,
models,
};
}
/** Merge the OmniRoute provider into an existing crush.json (preserve the rest). */
export function mergeCrushConfig(existing, provider) {
const cfg = existing && typeof existing === "object" ? { ...existing } : {};
cfg.providers = { ...(cfg.providers || {}), omniroute: provider };
return cfg;
}
function readJson(path) {
try {
if (existsSync(path)) return JSON.parse(readFileSync(path, "utf8"));
} catch {
/* corrupt/missing */
}
return {};
}
async function fetchModelIds(baseUrl, apiKey) {
const headers = { "Content-Type": "application/json" };
if (apiKey) headers["Authorization"] = `Bearer ${apiKey}`;
const res = await fetch(`${baseUrl.replace(/\/v1$/, "")}/v1/models`, {
headers,
signal: AbortSignal.timeout(10000),
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const body = await res.json();
const list = Array.isArray(body) ? body : body.data ?? body.models ?? [];
return list.map((m) => (typeof m === "string" ? m : m?.id)).filter(Boolean);
}
export async function runSetupCrushCommand(opts = {}) {
const { baseUrl, apiKey } = resolveCrushTarget(opts);
const dryRun = Boolean(opts.dryRun ?? opts["dry-run"]);
const only = opts.only ? opts.only.split(",").map((s) => s.trim()).filter(Boolean) : null;
const configPath = opts.configPath ?? opts["config-path"] ?? join(os.homedir(), ".config", "crush", "crush.json");
printHeading("OmniRoute → Crush (openai-compat)");
printInfo(`base_url: ${baseUrl}`);
let ids;
try {
ids = await fetchModelIds(baseUrl, apiKey);
} catch (e) {
printError(`Could not fetch models: ${e.message}`);
printInfo("Make sure OmniRoute is running and --remote/--api-key are correct.");
return 1;
}
if (only) ids = ids.filter((id) => only.some((f) => id.includes(f)));
const provider = buildCrushProvider(ids, baseUrl);
if (!provider.models.length) {
printError("No matching curated models (try --only or check the server).");
return 1;
}
const merged = mergeCrushConfig(readJson(configPath), provider);
const out = JSON.stringify(merged, null, 2) + "\n";
if (dryRun) {
console.log("\n" + (out.length > 3500 ? out.slice(0, 3500) + "\n… (truncated)" : out));
printInfo(`[dry-run] ${provider.models.length} model(s) under providers.omniroute → ${configPath}`);
return 0;
}
mkdirSync(join(configPath, ".."), { recursive: true });
writeFileSync(configPath, out, "utf8");
printSuccess(`Wrote ${configPath} (${provider.models.length} models under providers.omniroute)`);
printInfo("Provide the key (config references $OMNIROUTE_API_KEY): export OMNIROUTE_API_KEY=...");
printInfo("Then run: crush");
return 0;
}
export function registerSetupCrush(program) {
program
.command("setup-crush")
.description("Generate the OmniRoute openai-compat provider in ~/.config/crush/crush.json")
.option("--port <port>", "Local OmniRoute port (ignored when --remote is set)", "20128")
.option("--remote <url>", "Remote OmniRoute URL, e.g. http://192.168.0.15:20128")
.option("--api-key <key>", "OmniRoute API key (defaults to OMNIROUTE_API_KEY env var)")
.option("--only <patterns>", "Comma-separated substrings — keep only matching model IDs")
.option("--config-path <path>", "crush.json path (default: ~/.config/crush/crush.json)")
.option("--dry-run", "Print what would be written without touching the filesystem")
.action(async (opts) => {
const code = await runSetupCrushCommand(opts);
if (code !== 0) process.exit(code);
});
}
+108
View File
@@ -0,0 +1,108 @@
/**
* omniroute setup-cursor — guide Cursor to use OmniRoute.
*
* Cursor stores its OpenAI key + "Override OpenAI Base URL" in an opaque SQLite
* DB (state.vscdb) with no documented stable schema — NOT safe to file-write.
* So this command prints the exact in-app steps (and can list available models
* from /v1/models). Note: Cursor's custom base URL only powers the Chat panel;
* Composer / inline-edit / autocomplete stay on Cursor's own backend.
*/
import { printHeading, printInfo, printSuccess } from "../io.mjs";
import { resolveActiveContext } from "../contexts.mjs";
function ensureV1(url) {
const s = String(url || "").replace(/\/+$/, "");
return s.endsWith("/v1") ? s : `${s}/v1`;
}
/** Resolve apiBase (WITH /v1 — Cursor appends /chat/completions) + apiKey. */
export function resolveCursorTarget(opts = {}) {
let root;
if (opts.remote) root = String(opts.remote).replace(/\/+$/, "");
else {
try {
root = resolveActiveContext(opts.context ?? process.env.OMNIROUTE_CONTEXT)?.baseUrl;
} catch {
/* none */
}
if (!root) root = `http://localhost:${Number(opts.port ?? process.env.PORT ?? 20128) || 20128}`;
}
let apiKey = opts.apiKey ?? opts["api-key"];
if (!apiKey) {
try {
const c = resolveActiveContext(opts.context ?? process.env.OMNIROUTE_CONTEXT);
apiKey = c?.accessToken || c?.apiKey;
} catch {
/* none */
}
}
if (!apiKey) apiKey = process.env.OMNIROUTE_API_KEY || "";
return { apiBase: ensureV1(root), apiKey };
}
/** The step-by-step Cursor UI instructions (pure → testable). */
export function buildCursorInstructions({ apiBase, models }) {
const lines = [
"Cursor stores this config in an opaque database, so configure it in the app:",
"",
" 1. Cursor → Settings (Cmd/Ctrl + ,) → Models",
" 2. Enable “Override OpenAI Base URL” and set it to:",
` ${apiBase} (the /v1 suffix is required)`,
" 3. Set the OpenAI API Key to your OmniRoute key (OMNIROUTE_API_KEY)",
" 4. Add the model name(s) you want under “Models” (Cursor has no auto-discovery):",
];
const sample = (models && models.length ? models : ["glm/glm-5.2", "kmc/kimi-k2.7"]).slice(0, 8);
lines.push(` e.g. ${sample.join(", ")}`);
lines.push(" 5. Use the Chat panel (Cmd/Ctrl + L) to verify.");
lines.push("");
lines.push("⚠ The custom base URL powers the CHAT panel only — Composer, inline edit");
lines.push(" (Cmd/Ctrl+K) and autocomplete keep using Cursor's own backend.");
return lines.join("\n");
}
async function fetchModelIds(apiBase, apiKey) {
try {
const headers = { "Content-Type": "application/json" };
if (apiKey) headers["Authorization"] = `Bearer ${apiKey}`;
const res = await fetch(`${apiBase.replace(/\/v1$/, "")}/v1/models`, {
headers,
signal: AbortSignal.timeout(8000),
});
if (!res.ok) return [];
const body = await res.json();
const list = Array.isArray(body) ? body : body.data ?? body.models ?? [];
return list.map((m) => (typeof m === "string" ? m : m?.id)).filter(Boolean);
} catch {
return [];
}
}
export async function runSetupCursorCommand(opts = {}) {
const { apiBase, apiKey } = resolveCursorTarget(opts);
printHeading("OmniRoute → Cursor");
printInfo(`Server: ${apiBase}`);
let models = [];
const only = opts.only ? opts.only.split(",").map((s) => s.trim()).filter(Boolean) : null;
const ids = await fetchModelIds(apiBase, apiKey);
models = only ? ids.filter((id) => only.some((f) => id.includes(f))) : ids;
console.log("\n" + buildCursorInstructions({ apiBase, models }));
printSuccess("\nCursor is configured manually (no file written — Cursor's storage is opaque).");
return 0;
}
export function registerSetupCursor(program) {
program
.command("setup-cursor")
.description("Print the steps to point Cursor at OmniRoute (chat panel; Cursor config is not file-writable)")
.option("--port <port>", "Local OmniRoute port (ignored when --remote is set)", "20128")
.option("--remote <url>", "Remote OmniRoute URL, e.g. http://192.168.0.15:20128")
.option("--api-key <key>", "OmniRoute API key (defaults to OMNIROUTE_API_KEY env var)")
.option("--only <patterns>", "Comma-separated substrings — suggest only matching model IDs")
.action(async (opts) => {
const code = await runSetupCursorCommand(opts);
if (code !== 0) process.exit(code);
});
}
+150
View File
@@ -0,0 +1,150 @@
/**
* omniroute setup-goose — configure Goose (block/goose) for OmniRoute.
*
* Goose is a terminal AI agent with a file-based config at
* ~/.config/goose/config.yaml and env-var overrides. For a custom OpenAI-
* compatible endpoint it uses provider `openai` with OPENAI_HOST = the ROOT url
* (NO /v1 — Goose appends the path itself). This writes the config.yaml keys and
* prints the guaranteed env-var recipe (the API key lives in the OS keyring / env,
* never the config file).
*/
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import os from "node:os";
import { printHeading, printInfo, printSuccess, printError, createPrompt } from "../io.mjs";
import { resolveActiveContext } from "../contexts.mjs";
function stripToRoot(url) {
const s = String(url || "").replace(/\/+$/, "");
return s.endsWith("/v1") ? s.slice(0, -3) : s;
}
/** Resolve OPENAI_HOST (ROOT, no /v1 — Goose appends) + apiKey. */
export function resolveGooseTarget(opts = {}) {
let root;
if (opts.remote) root = stripToRoot(opts.remote);
else {
try {
root = stripToRoot(resolveActiveContext(opts.context ?? process.env.OMNIROUTE_CONTEXT)?.baseUrl);
} catch {
/* none */
}
if (!root) root = `http://localhost:${Number(opts.port ?? process.env.PORT ?? 20128) || 20128}`;
}
let apiKey = opts.apiKey ?? opts["api-key"];
if (!apiKey) {
try {
const c = resolveActiveContext(opts.context ?? process.env.OMNIROUTE_CONTEXT);
apiKey = c?.accessToken || c?.apiKey;
} catch {
/* none */
}
}
if (!apiKey) apiKey = process.env.OMNIROUTE_API_KEY || "";
return { host: root, apiKey };
}
/** Merge Goose's provider/model keys into config.yaml object (preserve the rest). */
export function buildGooseConfig(existing, { host, model }) {
const cfg = existing && typeof existing === "object" ? { ...existing } : {};
cfg.GOOSE_PROVIDER = "openai";
cfg.GOOSE_MODEL = model;
cfg.OPENAI_HOST = host; // ROOT — Goose appends /v1/chat/completions
return cfg;
}
/** The guaranteed env-var recipe (pure → testable). */
export function buildGooseEnvRecipe({ host, model }) {
return [
"export GOOSE_PROVIDER=openai",
`export OPENAI_HOST=${host}`,
"export OPENAI_API_KEY=$OMNIROUTE_API_KEY",
`export GOOSE_MODEL=${model}`,
].join("\n");
}
function readYamlSafe(yaml, path) {
try {
if (existsSync(path)) return yaml.load(readFileSync(path, "utf8")) || {};
} catch {
/* corrupt/missing */
}
return {};
}
async function fetchModelIds(host, apiKey) {
try {
const headers = { "Content-Type": "application/json" };
if (apiKey) headers["Authorization"] = `Bearer ${apiKey}`;
const res = await fetch(`${host}/v1/models`, { headers, signal: AbortSignal.timeout(8000) });
if (!res.ok) return [];
const body = await res.json();
const list = Array.isArray(body) ? body : body.data ?? body.models ?? [];
return list.map((m) => (typeof m === "string" ? m : m?.id)).filter(Boolean);
} catch {
return [];
}
}
export async function runSetupGooseCommand(opts = {}) {
const { host, apiKey } = resolveGooseTarget(opts);
const dryRun = Boolean(opts.dryRun ?? opts["dry-run"]);
const configPath = opts.configPath ?? opts["config-path"] ?? join(os.homedir(), ".config", "goose", "config.yaml");
printHeading("OmniRoute → Goose (openai-compatible)");
printInfo(`OPENAI_HOST: ${host} (no /v1 — Goose appends it)`);
let model = opts.model;
if (!model) {
const ids = await fetchModelIds(host, apiKey);
if (ids.length && !opts.yes) {
printInfo(`Examples: ${ids.slice(0, 20).join(", ")}${ids.length > 20 ? " …" : ""}`);
const prompt = createPrompt();
try {
model = await prompt.ask("Model id for Goose");
} finally {
prompt.close();
}
}
}
if (!model) {
printError("A model is required. Pass --model <id>.");
return 2;
}
const yaml = await import("js-yaml");
const merged = buildGooseConfig(readYamlSafe(yaml, configPath), { host, model });
const out = yaml.dump(merged, { lineWidth: -1 });
if (dryRun) {
console.log("\n" + out);
printInfo(`[dry-run] → ${configPath}`);
} else {
mkdirSync(join(configPath, ".."), { recursive: true });
writeFileSync(configPath, out, "utf8");
printSuccess(`Wrote ${configPath}`);
}
printInfo("\nProvide the key (Goose reads it from the env / OS keyring):");
console.log(buildGooseEnvRecipe({ host, model }));
printInfo("Then run: goose session (or: goose run -t \"reply OK\")");
return 0;
}
export function registerSetupGoose(program) {
program
.command("setup-goose")
.description("Configure Goose for OmniRoute: write ~/.config/goose/config.yaml + print the env recipe")
.option("--port <port>", "Local OmniRoute port (ignored when --remote is set)", "20128")
.option("--remote <url>", "Remote OmniRoute URL, e.g. http://192.168.0.15:20128")
.option("--api-key <key>", "OmniRoute API key (defaults to OMNIROUTE_API_KEY env var)")
.option("--model <id>", "Model id for Goose (required unless picked interactively)")
.option("--config-path <path>", "config.yaml path (default: ~/.config/goose/config.yaml)")
.option("--yes", "Non-interactive: do not prompt (requires --model)")
.option("--dry-run", "Print what would be written without touching the filesystem")
.action(async (opts) => {
const code = await runSetupGooseCommand(opts);
if (code !== 0) process.exit(code);
});
}
+178
View File
@@ -0,0 +1,178 @@
/**
* omniroute setup-kilo — configure Kilo Code to use OmniRoute.
*
* Kilo Code (kilocode.kilo-code, a Cline/Roo descendant) has two surfaces:
* - CLI/standalone mode reads ~/.local/share/kilo/auth.json.
* - The VS Code extension reads `kilocode.*` keys from VS Code settings.json.
* This writes BOTH (matching the OmniRoute dashboard) and prints the UI settings.
*
* Unlike Cline, Kilo's openAi baseURL INCLUDES /v1 (it appends /chat/completions).
*/
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import os from "node:os";
import { printHeading, printInfo, printSuccess, printError, createPrompt } from "../io.mjs";
import { resolveActiveContext } from "../contexts.mjs";
/** Ensure the URL ends with /v1 (Kilo appends /chat/completions to it). */
function ensureV1(url) {
const s = String(url || "").replace(/\/+$/, "");
return s.endsWith("/v1") ? s : `${s}/v1`;
}
/** Resolve baseUrl (WITH /v1) + apiKey from flags → active context → localhost. */
export function resolveKiloTarget(opts = {}) {
let root;
if (opts.remote) root = String(opts.remote).replace(/\/+$/, "");
else {
try {
root = resolveActiveContext(opts.context ?? process.env.OMNIROUTE_CONTEXT)?.baseUrl;
} catch {
/* none */
}
if (!root) root = `http://localhost:${Number(opts.port ?? process.env.PORT ?? 20128) || 20128}`;
}
let apiKey = opts.apiKey ?? opts["api-key"];
if (!apiKey) {
try {
const c = resolveActiveContext(opts.context ?? process.env.OMNIROUTE_CONTEXT);
apiKey = c?.accessToken || c?.apiKey;
} catch {
/* none */
}
}
if (!apiKey) apiKey = process.env.OMNIROUTE_API_KEY || "";
return { baseUrl: ensureV1(root), apiKey };
}
/** Merge the OmniRoute openai-compatible provider into Kilo's CLI auth.json. */
export function buildKiloAuth(existing, { apiKey, baseUrl, model }) {
const auth = { ...(existing || {}) };
auth["openai-compatible"] = {
...(auth["openai-compatible"] || {}),
apiKey: apiKey || "sk_omniroute",
baseUrl,
model,
};
return auth;
}
/** Merge the kilocode.* keys into VS Code settings.json (extension surface). */
export function buildKiloVscodeSettings(existing, { apiKey, baseUrl, model }) {
const s = { ...(existing || {}) };
s["kilocode.customProvider"] = { name: "OmniRoute", baseURL: baseUrl, apiKey: apiKey || "sk_omniroute" };
s["kilocode.defaultModel"] = model;
return s;
}
function readJson(path) {
try {
if (existsSync(path)) return JSON.parse(readFileSync(path, "utf8"));
} catch {
/* corrupt/missing */
}
return {};
}
async function fetchModelIds(root, apiKey) {
try {
const headers = { "Content-Type": "application/json" };
if (apiKey) headers["Authorization"] = `Bearer ${apiKey}`;
const res = await fetch(`${root.replace(/\/v1$/, "")}/v1/models`, {
headers,
signal: AbortSignal.timeout(8000),
});
if (!res.ok) return [];
const body = await res.json();
const list = Array.isArray(body) ? body : body.data ?? body.models ?? [];
return list.map((m) => (typeof m === "string" ? m : m?.id)).filter(Boolean);
} catch {
return [];
}
}
export async function runSetupKiloCommand(opts = {}) {
const { baseUrl, apiKey } = resolveKiloTarget(opts);
const dryRun = Boolean(opts.dryRun ?? opts["dry-run"]);
const authPath = opts.authPath ?? opts["auth-path"] ?? join(os.homedir(), ".local", "share", "kilo", "auth.json");
const vscodePath =
opts.vscodeSettings ?? opts["vscode-settings"] ?? join(os.homedir(), ".config", "Code", "User", "settings.json");
printHeading("OmniRoute → Kilo Code (OpenAI-compatible)");
printInfo(`Server: ${baseUrl}`);
let model = opts.model;
if (!model) {
const ids = await fetchModelIds(baseUrl, apiKey);
if (ids.length && !opts.yes) {
printInfo(`Examples: ${ids.slice(0, 20).join(", ")}${ids.length > 20 ? " …" : ""}`);
const prompt = createPrompt();
try {
model = await prompt.ask("Model id for Kilo");
} finally {
prompt.close();
}
}
}
if (!model) {
printError("A model is required. Pass --model <id> (Kilo's extension has no model auto-discovery).");
return 2;
}
const auth = buildKiloAuth(readJson(authPath), { apiKey, baseUrl, model });
// Only touch VS Code settings.json if it already exists (avoid creating a
// bogus one for users who don't use that VS Code variant).
const vscodeExists = existsSync(vscodePath);
const vscodeSettings = vscodeExists
? buildKiloVscodeSettings(readJson(vscodePath), { apiKey, baseUrl, model })
: null;
if (dryRun) {
console.log(`\n── [dry-run] ${authPath} ──`);
console.log(
JSON.stringify(
{ "openai-compatible": { ...auth["openai-compatible"], apiKey: apiKey ? "set" : "sk_omniroute" } },
null,
2
)
);
console.log(`\n── [dry-run] ${vscodePath} ── ${vscodeExists ? "(would merge kilocode.* keys)" : "(skipped — file absent)"}`);
} else {
mkdirSync(join(authPath, ".."), { recursive: true });
writeFileSync(authPath, JSON.stringify(auth, null, 2) + "\n", "utf8");
printSuccess(`Wrote ${authPath}`);
if (vscodeSettings) {
writeFileSync(vscodePath, JSON.stringify(vscodeSettings, null, 2) + "\n", "utf8");
printSuccess(`Updated ${vscodePath} (kilocode.customProvider + defaultModel)`);
} else {
printInfo(`Skipped VS Code settings (${vscodePath} not found).`);
}
}
printInfo("\nFor the Kilo Code VS Code extension, set Settings → Providers → OpenAI Compatible:");
printInfo(` Base URL: ${baseUrl} (Kilo expects /v1)`);
printInfo(` API Key: <your OMNIROUTE_API_KEY>`);
printInfo(` Model: ${model}`);
return 0;
}
export function registerSetupKilo(program) {
program
.command("setup-kilo")
.description(
"Configure Kilo Code for OmniRoute: write ~/.local/share/kilo/auth.json (CLI) + VS Code kilocode.* settings"
)
.option("--port <port>", "Local OmniRoute port (ignored when --remote is set)", "20128")
.option("--remote <url>", "Remote OmniRoute URL, e.g. http://192.168.0.15:20128")
.option("--api-key <key>", "OmniRoute API key (defaults to OMNIROUTE_API_KEY env var)")
.option("--model <id>", "Model id for Kilo (required unless picked interactively)")
.option("--auth-path <path>", "Kilo CLI auth.json path (default: ~/.local/share/kilo/auth.json)")
.option("--vscode-settings <path>", "VS Code settings.json (default: ~/.config/Code/User/settings.json)")
.option("--yes", "Non-interactive: do not prompt (requires --model)")
.option("--dry-run", "Print what would be written without touching the filesystem")
.action(async (opts) => {
const code = await runSetupKiloCommand(opts);
if (code !== 0) process.exit(code);
});
}
+398
View File
@@ -0,0 +1,398 @@
/**
* omniroute setup opencode — Wire the bundled @omniroute/opencode-plugin
* into a local OpenCode install.
*
* Closes the gap where `npm install -g omniroute` ships the plugin
* inside the omniroute package (`@omniroute/opencode-plugin/dist/`) but
* OpenCode discovers plugins via `~/.config/opencode/plugins/` or
* via entries in `opencode.json`. Without this command, the user has
* to extract the tarball and wire it up by hand (see the plugin README,
* "Install" section).
*
* What it does, in order:
* 1. Resolves the bundled plugin path (source + built dist).
* 2. Resolves the OpenCode config directory (XDG-aware).
* 3. Copies the built plugin into `<opencode>/plugins/omniroute/`.
* 4. Creates or updates `opencode.json` with a single `plugin` entry
* pointing at the local copy (so OC ≥1.15 picks it up).
* 5. Optionally runs `opencode auth login --provider omniroute`
* so the next `opencode` invocation already has the API key.
*
* Idempotent: re-running with the same `--provider-id` updates the
* entry in place (path + baseURL) without duplicating it.
*/
import { existsSync, mkdirSync, readFileSync, writeFileSync, cpSync } from "node:fs";
import { dirname, isAbsolute, join, resolve } from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
import { spawnSync } from "node:child_process";
import os from "node:os";
import { printHeading, printInfo, printSuccess, printError } from "../io.mjs";
import { t } from "../i18n.mjs";
import { resolveActiveContext } from "../contexts.mjs";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
// We walk up from this file to find the omniroute package root. The script
// lives at `<omniroute>/bin/cli/commands/setup-open-code.mjs`, so the
// package root is three levels up. Using import.meta.url (not process.cwd())
// means the command works the same way whether you run it from the source
// repo, a global install, or a symlinked location.
const PACKAGE_ROOT = resolve(__dirname, "..", "..", "..");
// The bundled plugin ships at PACKAGE_ROOT/@omniroute/opencode-plugin/
// (see root package.json `files`: ["@omniroute/", ...]). The env override
// exists so tests can point at a fixture without building the real plugin.
const BUNDLED_PLUGIN_DIR =
process.env.OMNIROUTE_OPENCODE_PLUGIN_DIR || join(PACKAGE_ROOT, "@omniroute", "opencode-plugin");
/**
* Resolve the OpenCode config directory. Honours XDG_CONFIG_HOME and the
* platform-specific defaults documented at https://opencode.ai/.
*
* @returns {{ configDir: string, dataDir: string }}
*/
function resolveOpenCodeDirs() {
const home = os.homedir();
const xdgConfig = process.env.XDG_CONFIG_HOME;
const xdgData = process.env.XDG_DATA_HOME;
const platform = process.platform;
let configDir;
let dataDir;
if (platform === "darwin") {
// macOS: ~/Library/Application Support/opencode
configDir = join(home, "Library", "Application Support", "opencode");
dataDir = configDir; // OC uses the same root for config + data on macOS
} else if (platform === "win32") {
const appdata = process.env.APPDATA || join(home, "AppData", "Roaming");
const localAppdata = process.env.LOCALAPPDATA || join(home, "AppData", "Local");
configDir = join(appdata, "opencode");
dataDir = join(localAppdata, "opencode");
} else {
// Linux + everything else: XDG-style
configDir = xdgConfig ? join(xdgConfig, "opencode") : join(home, ".config", "opencode");
dataDir = xdgData ? join(xdgData, "opencode") : join(home, ".local", "share", "opencode");
}
return { configDir, dataDir };
}
/**
* Locate the bundled @omniroute/opencode-plugin dist. The plugin may be
* present in two states:
*
* - Built (`dist/index.cjs` + `dist/index.js` exist) — preferred,
* ships from a published omniroute tarball after Step 8.8 of
* `scripts/build/prepublish.ts` runs.
* - Unbuilt (only `src/index.ts`) — local dev / fresh clone. We surface
* a clear error instead of running tsup here, because the CLI runtime
* may not have tsup available (it's a devDependency).
*
* @returns {{ distEntry: string, packageDir: string }}
*/
function resolveBundledPlugin() {
if (!existsSync(BUNDLED_PLUGIN_DIR)) {
throw new Error(
`Bundled @omniroute/opencode-plugin not found at ${BUNDLED_PLUGIN_DIR}.\n` +
`This usually means omniroute was installed from a source tree that does not ` +
`include the workspace package. Try reinstalling omniroute (npm install -g omniroute) ` +
`or run \`cd @omniroute/opencode-plugin && npm install && npm run build\` from the source repo.`
);
}
const esmEntry = join(BUNDLED_PLUGIN_DIR, "dist", "index.js");
if (!existsSync(esmEntry)) {
throw new Error(
`@omniroute/opencode-plugin dist/ not built (looked for ${esmEntry}).\n` +
`Run \`cd ${BUNDLED_PLUGIN_DIR} && npm install && npm run build\` and re-run this command.`
);
}
// ESM-only build (CJS was dropped in tsup config). OpenCode (>=1.15) loads
// ESM modules natively.
return { distEntry: esmEntry, packageDir: BUNDLED_PLUGIN_DIR };
}
/**
* Copy the plugin package into `<opencodeConfig>/plugins/omniroute/`. We
* copy the entire package (dist/ + package.json) so the dist file's
* require/import of `zod` and `@opencode-ai/plugin` resolves against the
* copy's own node_modules. Without the copy, OpenCode would need to
* resolve the peer deps from the omniroute package's tree, which is
* unreliable.
*/
function installPluginToOpenCode(pluginInfo, opencodeConfigDir) {
const targetDir = join(opencodeConfigDir, "plugins", "omniroute");
mkdirSync(dirname(targetDir), { recursive: true });
mkdirSync(targetDir, { recursive: true });
// Copy package.json + dist/. We intentionally do NOT recursively copy
// node_modules from the source — `peerDependenciesMeta` declares zod +
// @opencode-ai/plugin as peers, and the user's OpenCode install already
// provides them. Copying our own node_modules would risk duplicate zod
// instances (the @opencode-ai/plugin contract uses a singleton).
const packageJsonSrc = join(pluginInfo.packageDir, "package.json");
const distSrc = join(pluginInfo.packageDir, "dist");
cpSync(packageJsonSrc, join(targetDir, "package.json"));
cpSync(distSrc, join(targetDir, "dist"), { recursive: true });
return targetDir;
}
/**
* Update `opencode.json` to register the plugin. Idempotent: if an entry
* for the same `providerId` already exists, replace it in place. If the
* user has any other plugin entries, preserve them.
*
* @returns {{ configPath: string, changed: boolean }}
*/
function registerPluginInOpenCodeConfig({
opencodeConfigDir,
pluginTargetDir,
providerId,
baseURL,
displayName,
}) {
const configPath = join(opencodeConfigDir, "opencode.json");
let cfg = {};
if (existsSync(configPath)) {
try {
cfg = JSON.parse(readFileSync(configPath, "utf8"));
} catch (err) {
throw new Error(
`Failed to parse existing ${configPath}: ${err.message}\n` +
`Fix or remove the file manually, then re-run \`omniroute setup opencode\`.`
);
}
}
const plugins = Array.isArray(cfg.plugin) ? cfg.plugin : [];
// Plugin entries can be either a string ("@some/pkg") or a tuple
// ("@some/pkg", { options }). The README documents the tuple form, so
// we use that. The "module path" is a file:// URL relative to the
// opencode config dir — that is what opencode ≥1.15 resolves.
const entry = [
`./plugins/omniroute/dist/index.js`,
{
providerId,
baseURL,
...(displayName ? { displayName } : {}),
},
];
// Idempotency: drop any prior entry for the same providerId. We also
// drop a legacy `opencode-omniroute-auth` entry if present — that
// package is the obsolete predecessor of @omniroute/opencode-plugin
// and was the root cause of issue #3711.
const filtered = plugins.filter((p) => {
if (typeof p === "string") {
return !p.includes("opencode-omniroute-auth");
}
if (Array.isArray(p) && p[1] && typeof p[1] === "object") {
const pid = p[1].providerId;
if (pid === providerId) return false;
// Also drop the legacy auth plugin if it's there.
if (typeof p[0] === "string" && p[0].includes("opencode-omniroute-auth")) {
return false;
}
}
return true;
});
filtered.push(entry);
cfg.plugin = filtered;
// Make sure the config dir exists, then write the updated config.
mkdirSync(dirname(configPath), { recursive: true });
writeFileSync(configPath, JSON.stringify(cfg, null, 2) + "\n", "utf8");
return { configPath, changed: true };
}
/**
* Optionally invoke `opencode auth login --provider <providerId>`. We
* shell out (instead of importing) so this command works even if
* OpenCode's CLI surface shifts between minor versions — the user gets
* a clear "could not run opencode" message instead of a hard import
* failure.
*/
function runOpenCodeAuth(providerId) {
const isWin = process.platform === "win32";
const opencodeBin = isWin ? "opencode.cmd" : "opencode";
const res = spawnSync(opencodeBin, ["auth", "login", "--provider", providerId], {
stdio: "inherit",
shell: false,
});
if (res.error) {
// ENOENT = opencode is not on PATH
if (res.error.code === "ENOENT") {
printInfo(
`opencode CLI not found on PATH. Run \`opencode auth login --provider ${providerId}\` manually after installing OpenCode.`
);
return 1;
}
printError(`opencode auth login failed: ${res.error.message}`);
return 1;
}
return typeof res.status === "number" ? res.status : 1;
}
/**
* Top-level action handler. Kept exported so the integration test can
* drive it without spawning a subprocess.
*
* @param {object} opts
* @param {string} [opts.providerId="omniroute"]
* @param {string} [opts.baseURL="http://localhost:20128"] (Commander camelCases
* `--base-url` into `baseUrl`, so both spellings are accepted.)
* @param {string} [opts.configDir] Override the OpenCode config dir (tests / non-standard installs).
* @param {string} [opts.displayName]
* @param {boolean} [opts.auth=false] Run `opencode auth login` after wiring.
* @param {boolean} [opts.nonInteractive=false] Skip prompts.
* @returns {Promise<{ exitCode: number, configPath?: string, pluginTargetDir?: string }>}
*/
export async function runSetupOpenCodeCommand(opts = {}) {
const providerId = opts.providerId || "omniroute";
// Remote-aware: explicit --remote/--base-url → active context → localhost.
let baseURL = opts.remote || opts.baseURL || opts.baseUrl;
if (!baseURL) {
try {
const ctx = resolveActiveContext(opts.context ?? process.env.OMNIROUTE_CONTEXT);
baseURL = ctx?.baseUrl;
} catch {
/* no context */
}
}
if (!baseURL) baseURL = "http://localhost:20128";
const displayName = opts.displayName || null;
const wantsAuth = Boolean(opts.auth);
const nonInteractive = Boolean(opts.nonInteractive);
printHeading("OmniRoute → OpenCode Plugin Setup");
const resolvedDirs = resolveOpenCodeDirs();
const opencodeConfigDir = opts.configDir || resolvedDirs.configDir;
const opencodeDataDir = resolvedDirs.dataDir;
printInfo(`OpenCode config dir: ${opencodeConfigDir}`);
printInfo(`OpenCode data dir: ${opencodeDataDir}`);
// 1. Resolve bundled plugin
let pluginInfo;
try {
pluginInfo = resolveBundledPlugin();
} catch (err) {
printError(err.message);
return { exitCode: 1 };
}
printInfo(`Bundled plugin: ${pluginInfo.distEntry}`);
// 2. Ensure OpenCode config dir exists (opencode will create it on
// first run, but creating it now means we can write opencode.json
// even if OC has never been launched).
if (!existsSync(opencodeConfigDir)) {
mkdirSync(opencodeConfigDir, { recursive: true });
printInfo(`Created OpenCode config dir (didn't exist yet).`);
}
// 3. Copy plugin into OpenCode's plugin dir
let pluginTargetDir;
try {
pluginTargetDir = installPluginToOpenCode(pluginInfo, opencodeConfigDir);
printSuccess(`Plugin installed at ${pluginTargetDir}`);
} catch (err) {
printError(`Failed to install plugin: ${err.message}`);
return { exitCode: 1 };
}
// 4. Register in opencode.json
let configPath;
try {
const reg = registerPluginInOpenCodeConfig({
opencodeConfigDir,
pluginTargetDir,
providerId,
baseURL,
displayName,
});
configPath = reg.configPath;
printSuccess(`opencode.json updated at ${configPath}`);
} catch (err) {
printError(`Failed to update opencode.json: ${err.message}`);
return { exitCode: 1, pluginTargetDir };
}
// 5. Optionally run auth login
if (wantsAuth) {
if (nonInteractive) {
printInfo(`Skipping \`opencode auth login\` (non-interactive mode).`);
printInfo(`Run manually: opencode auth login --provider ${providerId}`);
} else {
printHeading("Authenticating with OpenCode");
const authExit = runOpenCodeAuth(providerId);
if (authExit !== 0) {
return { exitCode: authExit, configPath, pluginTargetDir };
}
}
} else {
printInfo(
`Next step: opencode auth login --provider ${providerId} (pass --auth to do this automatically)`
);
}
printSuccess("OpenCode plugin setup complete");
printInfo(`Restart OpenCode to pick up the new plugin entry.`);
return { exitCode: 0, configPath, pluginTargetDir };
}
/**
* Register the `omniroute setup opencode` subcommand on the parent
* `setup` command. Commander builds the doc/help from the chain, so
* `omniroute setup --help` automatically shows the new subcommand.
*
* @param {import("commander").Command} setupCommand the registered `setup` command
*/
export function registerSetupOpenCode(setupCommand) {
setupCommand
.command("opencode")
.description(
t("setup.opencode") ||
"Install and register the bundled @omniroute/opencode-plugin with a local OpenCode install"
)
.option(
"--provider-id <id>",
"OpenCode provider id to register (default: omniroute)",
"omniroute"
)
.option(
"--base-url <url>",
"OmniRoute base URL the plugin should talk to (default: active context or http://localhost:20128)"
)
.option(
"--remote <url>",
"Remote OmniRoute URL, e.g. http://192.168.0.15:20128 (overrides --base-url and the context)"
)
.option("--display-name <name>", "Display name in the OpenCode UI (optional)")
.option(
"--auth",
"Run `opencode auth login --provider <providerId>` after wiring (interactive)",
false
)
.option("--non-interactive", "Do not prompt; skip the auth login step", false)
.action(async (opts, cmd) => {
// The parent `setup` command uses cmd.optsWithGlobals(); we mirror
// that here so global flags (--json, --base-url, --api-key) still
// flow through to the runner.
const globalOpts = cmd.parent?.parent?.optsWithGlobals?.() ?? {};
const merged = {
...opts,
output: globalOpts.output,
apiKey: opts.apiKey ?? globalOpts.apiKey,
baseUrl: opts.baseUrl ?? globalOpts.baseUrl,
context: globalOpts.context ?? opts.context,
};
const { exitCode } = await runSetupOpenCodeCommand(merged);
if (exitCode !== 0) process.exit(exitCode);
});
}
+129
View File
@@ -0,0 +1,129 @@
/**
* omniroute setup-opencode — Remote-aware OpenCode provider generator
* (openai-compatible). Distinct from `omniroute setup opencode` (which wires the
* @omniroute/opencode-plugin). This writes the `omniroute` provider into
* ~/.config/opencode/opencode.json with every catalog model, so you can run
* `opencode -m omniroute/<model>`.
*
* Reuses the proven server-side generator (config-generator/opencode.ts) for the
* catalog fetch + merge, then references the API key by env var (never on disk).
*/
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import os from "node:os";
import { printHeading, printInfo, printSuccess, printError } from "../io.mjs";
import { resolveActiveContext } from "../contexts.mjs";
const ENV_KEY_REF = "{env:OMNIROUTE_API_KEY}";
/** Resolve baseUrl + (literal) apiKey from flags → active context → localhost. */
export function resolveOpencodeTarget(opts = {}) {
let baseUrl;
if (opts.remote) {
baseUrl = String(opts.remote).replace(/\/+$/, "");
} else {
try {
const c = resolveActiveContext(opts.context ?? process.env.OMNIROUTE_CONTEXT);
baseUrl = c?.baseUrl;
} catch {
/* no context */
}
if (!baseUrl) baseUrl = `http://localhost:${Number(opts.port ?? process.env.PORT ?? 20128) || 20128}`;
}
let apiKey = opts.apiKey ?? opts["api-key"];
if (!apiKey) {
try {
const c = resolveActiveContext(opts.context ?? process.env.OMNIROUTE_CONTEXT);
apiKey = c?.accessToken || c?.apiKey;
} catch {
/* no context auth */
}
}
if (!apiKey) apiKey = process.env.OMNIROUTE_API_KEY || "";
return { baseUrl: baseUrl.replace(/\/+$/, ""), apiKey };
}
/**
* Post-process the generator output: reference the API key by env var (keep the
* secret off disk) and optionally keep only models whose id matches `only`.
* Pure + testable. Returns the final JSON string.
*
* @param {string} rawJson output of generateOpencodeConfig
* @param {{ only?: string[] }} [opts]
* @returns {{ json: string, modelCount: number }}
*/
export function postProcessOpencodeConfig(rawJson, opts = {}) {
const config = JSON.parse(rawJson);
const prov = config.provider?.omniroute;
if (prov?.options) prov.options.apiKey = ENV_KEY_REF;
if (opts.only && opts.only.length && prov?.models) {
const kept = {};
for (const [id, entry] of Object.entries(prov.models)) {
if (opts.only.some((f) => id.includes(f))) kept[id] = entry;
}
prov.models = kept;
}
const modelCount = prov?.models ? Object.keys(prov.models).length : 0;
return { json: JSON.stringify(config, null, 2) + "\n", modelCount };
}
export async function runSetupOpencodeCommand(opts = {}) {
const { baseUrl, apiKey } = resolveOpencodeTarget(opts);
const dryRun = Boolean(opts.dryRun ?? opts["dry-run"]);
const only = opts.only ? opts.only.split(",").map((s) => s.trim()).filter(Boolean) : null;
printHeading("OmniRoute → OpenCode provider (openai-compatible)");
printInfo(`Connecting to ${baseUrl}`);
// Deferred import: opencode.ts is TypeScript; tsx is registered by
// bin/omniroute.mjs before any command runs, so importing here is safe.
let raw;
try {
const { generateOpencodeConfig } = await import(
"../../../src/lib/cli-helper/config-generator/opencode.ts"
);
raw = await generateOpencodeConfig({ baseUrl, apiKey, model: opts.model, providerId: "omniroute" });
} catch (err) {
printError(`Failed to generate opencode.json: ${err?.message || err}`);
printInfo("Make sure OmniRoute is running and --remote/--api-key are correct.");
return 1;
}
const { json, modelCount } = postProcessOpencodeConfig(raw, { only });
const configDir = join(os.homedir(), ".config", "opencode");
const configPath = join(configDir, "opencode.json");
if (dryRun) {
console.log(json.length > 4000 ? json.slice(0, 4000) + "\n… (truncated)" : json);
printInfo(`[dry-run] ${modelCount} model(s) under provider 'omniroute' → ${configPath}`);
return 0;
}
if (!existsSync(configDir)) mkdirSync(configDir, { recursive: true });
writeFileSync(configPath, json, "utf8");
printSuccess(`opencode.json updated at ${configPath} (${modelCount} models under 'omniroute')`);
printInfo('Use it: opencode -m omniroute/<model> "..." (export OMNIROUTE_API_KEY first)');
return 0;
}
export function registerSetupOpencode(program) {
program
.command("setup-opencode")
.description(
"Generate the OmniRoute openai-compatible provider in ~/.config/opencode/opencode.json " +
"from the live model catalog (local or remote VPS)"
)
.option("--port <port>", "Local OmniRoute port (ignored when --remote is set)", "20128")
.option("--remote <url>", "Remote OmniRoute URL, e.g. http://192.168.0.15:20128")
.option("--api-key <key>", "OmniRoute API key (defaults to OMNIROUTE_API_KEY env var)")
.option("--model <id>", "Set the default top-level model (omniroute/<id>)")
.option("--only <patterns>", "Comma-separated substrings — keep only matching model IDs")
.option("--dry-run", "Print what would be written without touching the filesystem")
.action(async (opts) => {
const code = await runSetupOpencodeCommand(opts);
if (code !== 0) process.exit(code);
});
}
+156
View File
@@ -0,0 +1,156 @@
/**
* omniroute setup-qwen — configure Qwen Code (QwenLM/qwen-code) for OmniRoute.
*
* Qwen Code is a terminal AI agent with a file-based config at
* ~/.qwen/settings.json. For a custom OpenAI-compatible endpoint it uses a
* `modelProviders` entry with authType "openai", baseUrl WITH /v1, and an
* `envKey` naming the env var holding the key (secret stays in the env, never the
* file). Remote-aware; headless test via `qwen -p "..."`.
*/
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import os from "node:os";
import { printHeading, printInfo, printSuccess, printError, createPrompt } from "../io.mjs";
import { resolveActiveContext } from "../contexts.mjs";
function ensureV1(url) {
const s = String(url || "").replace(/\/+$/, "");
return s.endsWith("/v1") ? s : `${s}/v1`;
}
/** Resolve baseUrl (WITH /v1) + apiKey from flags → active context → localhost. */
export function resolveQwenTarget(opts = {}) {
let root;
if (opts.remote) root = String(opts.remote).replace(/\/+$/, "");
else {
try {
root = resolveActiveContext(opts.context ?? process.env.OMNIROUTE_CONTEXT)?.baseUrl;
} catch {
/* none */
}
if (!root) root = `http://localhost:${Number(opts.port ?? process.env.PORT ?? 20128) || 20128}`;
}
let apiKey = opts.apiKey ?? opts["api-key"];
if (!apiKey) {
try {
const c = resolveActiveContext(opts.context ?? process.env.OMNIROUTE_CONTEXT);
apiKey = c?.accessToken || c?.apiKey;
} catch {
/* none */
}
}
if (!apiKey) apiKey = process.env.OMNIROUTE_API_KEY || "";
return { baseUrl: ensureV1(root), apiKey };
}
/** Merge the OmniRoute modelProvider into Qwen's settings.json (preserve rest). */
export function buildQwenSettings(existing, { baseUrl, model }) {
const s = existing && typeof existing === "object" ? { ...existing } : {};
const providers = Array.isArray(s.modelProviders)
? s.modelProviders.filter((p) => p?.id !== "omniroute")
: [];
providers.push({
id: "omniroute",
name: "OmniRoute",
authType: "openai",
baseUrl,
envKey: "OMNIROUTE_API_KEY",
});
s.modelProviders = providers;
if (model) {
s.selectedProvider = "omniroute";
s.model = model;
}
return s;
}
function readJson(path) {
try {
if (existsSync(path)) return JSON.parse(readFileSync(path, "utf8"));
} catch {
/* corrupt/missing */
}
return {};
}
async function fetchModelIds(baseUrl, apiKey) {
try {
const headers = { "Content-Type": "application/json" };
if (apiKey) headers["Authorization"] = `Bearer ${apiKey}`;
const res = await fetch(`${baseUrl.replace(/\/v1$/, "")}/v1/models`, {
headers,
signal: AbortSignal.timeout(8000),
});
if (!res.ok) return [];
const body = await res.json();
const list = Array.isArray(body) ? body : (body.data ?? body.models ?? []);
return list.map((m) => (typeof m === "string" ? m : m?.id)).filter(Boolean);
} catch {
return [];
}
}
export async function runSetupQwenCommand(opts = {}) {
const { baseUrl, apiKey } = resolveQwenTarget(opts);
const dryRun = Boolean(opts.dryRun ?? opts["dry-run"]);
const configPath =
opts.configPath ?? opts["config-path"] ?? join(os.homedir(), ".qwen", "settings.json");
printHeading("OmniRoute → Qwen Code (openai-compatible)");
printInfo(`baseUrl: ${baseUrl}`);
let model = opts.model;
if (!model) {
const ids = await fetchModelIds(baseUrl, apiKey);
if (ids.length && !opts.yes) {
printInfo(`Examples: ${ids.slice(0, 20).join(", ")}${ids.length > 20 ? " …" : ""}`);
const prompt = createPrompt();
try {
model = await prompt.ask("Model id for Qwen");
} finally {
prompt.close();
}
}
}
if (!model) {
printError("A model is required. Pass --model <id>.");
return 2;
}
const merged = buildQwenSettings(readJson(configPath), { baseUrl, model });
const out = JSON.stringify(merged, null, 2) + "\n";
if (dryRun) {
console.log("\n" + out);
printInfo(`[dry-run] → ${configPath}`);
} else {
mkdirSync(join(configPath, ".."), { recursive: true });
writeFileSync(configPath, out, "utf8");
printSuccess(`Wrote ${configPath}`);
}
printInfo(
"\nProvide the key (settings reference OMNIROUTE_API_KEY): export OMNIROUTE_API_KEY=..."
);
printInfo('Then run: qwen (or headless: qwen -p "reply OK")');
return 0;
}
export function registerSetupQwen(program) {
program
.command("setup-qwen")
.description(
"Configure Qwen Code for OmniRoute: write ~/.qwen/settings.json (openai modelProvider)"
)
.option("--port <port>", "Local OmniRoute port (ignored when --remote is set)", "20128")
.option("--remote <url>", "Remote OmniRoute URL, e.g. http://192.168.0.15:20128")
.option("--api-key <key>", "OmniRoute API key (defaults to OMNIROUTE_API_KEY env var)")
.option("--model <id>", "Model id for Qwen (required unless picked interactively)")
.option("--config-path <path>", "settings.json path (default: ~/.qwen/settings.json)")
.option("--yes", "Non-interactive: do not prompt (requires --model)")
.option("--dry-run", "Print what would be written without touching the filesystem")
.action(async (opts) => {
const code = await runSetupQwenCommand(opts);
if (code !== 0) process.exit(code);
});
}
+172
View File
@@ -0,0 +1,172 @@
/**
* omniroute setup-roo — configure Roo Code (RooVeterinaryInc.roo-cline) for OmniRoute.
*
* Roo is a VS Code extension (Cline fork). Its live settings live in opaque VS
* Code globalStorage, but Roo supports **Settings Import** + an
* `roo-cline.autoImportSettingsPath` (VS Code settings.json) that loads a JSON on
* startup. So this writes a best-effort import file + wires autoImport (when a VS
* Code settings.json exists) + prints the UI steps as the guaranteed path.
*
* OpenAI-compatible: baseUrl WITH /v1 (Roo appends /chat/completions). The model
* must support native OpenAI tool-calling (OmniRoute does).
*/
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import os from "node:os";
import { printHeading, printInfo, printSuccess, printError } from "../io.mjs";
import { resolveActiveContext } from "../contexts.mjs";
function ensureV1(url) {
const s = String(url || "").replace(/\/+$/, "");
return s.endsWith("/v1") ? s : `${s}/v1`;
}
/** Resolve baseUrl (WITH /v1) + apiKey from flags → active context → localhost. */
export function resolveRooTarget(opts = {}) {
let root;
if (opts.remote) root = String(opts.remote).replace(/\/+$/, "");
else {
try {
root = resolveActiveContext(opts.context ?? process.env.OMNIROUTE_CONTEXT)?.baseUrl;
} catch {
/* none */
}
if (!root) root = `http://localhost:${Number(opts.port ?? process.env.PORT ?? 20128) || 20128}`;
}
let apiKey = opts.apiKey ?? opts["api-key"];
if (!apiKey) {
try {
const c = resolveActiveContext(opts.context ?? process.env.OMNIROUTE_CONTEXT);
apiKey = c?.accessToken || c?.apiKey;
} catch {
/* none */
}
}
if (!apiKey) apiKey = process.env.OMNIROUTE_API_KEY || "";
return { baseUrl: ensureV1(root), apiKey };
}
/** Build a Roo Settings-Import document (provider profile, openai-compatible). */
export function buildRooImport({ baseUrl, apiKey, model }) {
return {
providerProfiles: {
currentApiConfigName: "OmniRoute",
apiConfigs: {
OmniRoute: {
apiProvider: "openai",
openAiBaseUrl: baseUrl,
openAiApiKey: apiKey || "sk_omniroute",
openAiModelId: model,
openAiCustomModelInfo: { supportsImages: false, supportsPromptCache: false },
},
},
},
};
}
/** Add the autoImport pointer to a VS Code settings.json object. */
export function buildRooVscodeAutoImport(existing, importPath) {
return { ...(existing || {}), "roo-cline.autoImportSettingsPath": importPath };
}
function readJson(path) {
try {
if (existsSync(path)) return JSON.parse(readFileSync(path, "utf8"));
} catch {
/* corrupt/missing */
}
return {};
}
async function fetchModelIds(baseUrl, apiKey) {
try {
const headers = { "Content-Type": "application/json" };
if (apiKey) headers["Authorization"] = `Bearer ${apiKey}`;
const res = await fetch(`${baseUrl.replace(/\/v1$/, "")}/v1/models`, {
headers,
signal: AbortSignal.timeout(8000),
});
if (!res.ok) return [];
const body = await res.json();
const list = Array.isArray(body) ? body : body.data ?? body.models ?? [];
return list.map((m) => (typeof m === "string" ? m : m?.id)).filter(Boolean);
} catch {
return [];
}
}
export async function runSetupRooCommand(opts = {}) {
const { baseUrl, apiKey } = resolveRooTarget(opts);
const dryRun = Boolean(opts.dryRun ?? opts["dry-run"]);
const importPath = opts.importPath ?? opts["import-path"] ?? join(os.homedir(), ".omniroute", "roo-settings.json");
const vscodePath =
opts.vscodeSettings ?? opts["vscode-settings"] ?? join(os.homedir(), ".config", "Code", "User", "settings.json");
printHeading("OmniRoute → Roo Code (OpenAI-compatible)");
printInfo(`Server: ${baseUrl}`);
let model = opts.model;
if (!model) {
const ids = await fetchModelIds(baseUrl, apiKey);
if (ids.length && !opts.yes) {
printInfo(`Examples: ${ids.slice(0, 20).join(", ")}${ids.length > 20 ? " …" : ""}`);
const { createPrompt } = await import("../io.mjs");
const prompt = createPrompt();
try {
model = await prompt.ask("Model id for Roo");
} finally {
prompt.close();
}
}
}
if (!model) {
printError("A model is required. Pass --model <id> (Roo has no model auto-discovery).");
return 2;
}
const importDoc = buildRooImport({ baseUrl, apiKey, model });
const vscodeExists = existsSync(vscodePath);
if (dryRun) {
console.log(`\n── [dry-run] ${importPath} ──`);
console.log(JSON.stringify({ ...importDoc, providerProfiles: { ...importDoc.providerProfiles, apiConfigs: { OmniRoute: { ...importDoc.providerProfiles.apiConfigs.OmniRoute, openAiApiKey: apiKey ? "set" : "sk_omniroute" } } } }, null, 2));
console.log(`\n── [dry-run] ${vscodePath} ── ${vscodeExists ? "(would set roo-cline.autoImportSettingsPath)" : "(skipped — file absent)"}`);
} else {
mkdirSync(join(importPath, ".."), { recursive: true });
writeFileSync(importPath, JSON.stringify(importDoc, null, 2) + "\n", "utf8");
printSuccess(`Wrote ${importPath}`);
if (vscodeExists) {
const merged = buildRooVscodeAutoImport(readJson(vscodePath), importPath);
writeFileSync(vscodePath, JSON.stringify(merged, null, 2) + "\n", "utf8");
printSuccess(`Set roo-cline.autoImportSettingsPath in ${vscodePath}`);
}
}
printInfo("\nIn the Roo Code panel: Settings → Providers → OpenAI Compatible (guaranteed path):");
printInfo(` Base URL: ${baseUrl} (Roo expects /v1)`);
printInfo(` API Key: <your OMNIROUTE_API_KEY>`);
printInfo(` Model: ${model}`);
printInfo(`Or use Roo: “Import Settings” → select ${importPath}`);
return 0;
}
export function registerSetupRoo(program) {
program
.command("setup-roo")
.description(
"Configure Roo Code for OmniRoute: write a Roo import JSON + autoImport pointer + print UI steps"
)
.option("--port <port>", "Local OmniRoute port (ignored when --remote is set)", "20128")
.option("--remote <url>", "Remote OmniRoute URL, e.g. http://192.168.0.15:20128")
.option("--api-key <key>", "OmniRoute API key (defaults to OMNIROUTE_API_KEY env var)")
.option("--model <id>", "Model id for Roo (required unless picked interactively)")
.option("--import-path <path>", "Roo import JSON path (default: ~/.omniroute/roo-settings.json)")
.option("--vscode-settings <path>", "VS Code settings.json (default: ~/.config/Code/User/settings.json)")
.option("--yes", "Non-interactive: do not prompt (requires --model)")
.option("--dry-run", "Print what would be written without touching the filesystem")
.action(async (opts) => {
const code = await runSetupRooCommand(opts);
if (code !== 0) process.exit(code);
});
}
+211
View File
@@ -0,0 +1,211 @@
import { fileURLToPath } from "node:url";
import { dirname, resolve } from "node:path";
import { createPrompt, printHeading, printInfo, printSuccess } from "../io.mjs";
import { openOmniRouteDb } from "../sqlite.mjs";
import { getSettings, hashManagementPassword, updateSettings } from "../settings-store.mjs";
import { testProviderApiKey } from "../provider-test.mjs";
import { updateProviderTestResult, upsertApiKeyProviderConnection } from "../provider-store.mjs";
import {
formatProviderChoices,
getProviderDisplayName,
resolveProviderChoice,
} from "../provider-catalog.mjs";
import { registerSetupOpenCode } from "./setup-open-code.mjs";
import { t } from "../i18n.mjs";
const PROJECT_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "../../..");
async function getListCliTools() {
const { listCliTools } = await import(`${PROJECT_ROOT}/src/shared/constants/cliTools.ts`);
return listCliTools;
}
function wantsProviderSetup(opts) {
return opts.addProvider || Boolean(opts.provider) || Boolean(opts.apiKey);
}
async function resolvePassword(opts, prompt, nonInteractive) {
if (opts.password) return opts.password;
if (nonInteractive) return "";
const answer = await prompt.ask("Set an admin password now? [y/N]", "N");
if (!/^y(es)?$/i.test(answer)) return "";
const password = await prompt.askSecret("Admin password");
const confirm = await prompt.askSecret("Confirm password");
if (password !== confirm) {
throw new Error("Passwords do not match.");
}
return password;
}
async function setupPassword(db, opts, prompt, nonInteractive) {
const password = await resolvePassword(opts, prompt, nonInteractive);
if (!password) {
const settings = getSettings(db);
if (!settings.password) {
updateSettings(db, { requireLogin: false });
}
if (!nonInteractive) {
printInfo("Password setup skipped. Dashboard login remains disabled.");
}
return false;
}
if (password.length < 8) {
throw new Error("Password must be at least 8 characters.");
}
const hashedPassword = await hashManagementPassword(password);
updateSettings(db, {
password: hashedPassword,
requireLogin: true,
});
printSuccess("Admin password configured");
return true;
}
async function resolveProviderInput(opts, prompt, nonInteractive) {
let provider = opts.provider;
let apiKey = opts.apiKey;
let name = opts.providerName;
const defaultModel = opts.defaultModel;
const baseUrl = opts.providerBaseUrl;
if (!provider && !nonInteractive) {
console.log("Choose a provider:");
console.log(formatProviderChoices());
provider = resolveProviderChoice(await prompt.ask("Provider", "1"));
}
provider = provider || "openai";
if (!apiKey && !nonInteractive) {
apiKey = await prompt.ask(`${getProviderDisplayName(provider)} API key`);
}
if (!apiKey) {
throw new Error("Provider API key is required. Pass --api-key or OMNIROUTE_API_KEY.");
}
if (!name) {
name = getProviderDisplayName(provider);
}
return {
provider,
apiKey,
name,
defaultModel: defaultModel || null,
providerSpecificData: baseUrl ? { baseUrl } : null,
};
}
async function setupProvider(db, opts, prompt, nonInteractive) {
if (!wantsProviderSetup(opts) && nonInteractive) return null;
if (!wantsProviderSetup(opts)) {
const answer = await prompt.ask("Add your first provider now? [Y/n]", "Y");
if (/^n(o)?$/i.test(answer)) return null;
}
const input = await resolveProviderInput(opts, prompt, nonInteractive);
const connection = upsertApiKeyProviderConnection(db, input);
printSuccess(`Provider configured: ${connection.name}`);
if (opts.testProvider) {
printInfo(`Testing provider connection: ${connection.provider}`);
const result = await testProviderApiKey({
provider: input.provider,
apiKey: input.apiKey,
defaultModel: input.defaultModel,
baseUrl: input.providerSpecificData?.baseUrl || null,
});
updateProviderTestResult(db, connection.id, result);
if (result.valid) {
printSuccess("Provider test passed");
} else {
printInfo(`Provider test failed: ${result.error || "unknown error"}`);
}
}
return connection;
}
export function registerSetup(program) {
program
.command("setup")
.description(t("setup.title"))
.option("--password <value>", "Set admin password")
.option("--add-provider", "Add an API-key provider connection")
.option("--provider <id>", "Provider id, for example openai or anthropic")
.option("--provider-name <name>", "Display name for the connection")
.option("--api-key <value>", "Provider API key")
.option("--default-model <model>", "Optional default model")
.option("--provider-base-url <url>", "Optional OpenAI-compatible base URL override")
.option("--test-provider", "Test the provider after saving it")
.option("--non-interactive", "Read all inputs from flags/env and do not prompt")
.option("--list", "List all supported CLI tools")
.action(async (opts, cmd) => {
const globalOpts = cmd.optsWithGlobals();
const exitCode = await runSetupCommand({ ...opts, output: globalOpts.output });
if (exitCode !== 0) process.exit(exitCode);
});
// Wire up `omniroute setup opencode` subcommand. Kept inside registerSetup
// so it always travels with the parent command (avoids a separate register
// call in the registry that would silently break if the parent renames).
registerSetupOpenCode(program.commands.find((c) => c.name() === "setup"));
}
export async function runSetupCommand(opts = {}) {
if (opts.list) {
const listCliTools = await getListCliTools();
const tools = listCliTools();
if (opts.json || opts.output === "json") {
console.log(JSON.stringify(tools, null, 2));
} else {
printHeading("Supported CLI Tools");
for (const tool of tools) {
const cmd = tool.defaultCommand || tool.defaultCommands?.[0] || "";
const cmdStr = cmd ? ` \x1b[2m(${cmd})\x1b[0m` : "";
console.log(`${tool.name}${cmdStr}`);
}
}
return 0;
}
const nonInteractive = opts.nonInteractive ?? false;
const prompt = createPrompt();
try {
printHeading("OmniRoute Setup");
const { db, dbPath } = await openOmniRouteDb();
printInfo(`Database: ${dbPath}`);
const before = getSettings(db);
const passwordChanged = await setupPassword(db, opts, prompt, nonInteractive);
const providerConnection = await setupProvider(db, opts, prompt, nonInteractive);
updateSettings(db, { setupComplete: true });
const after = getSettings(db);
db.close();
console.log("");
printSuccess("Setup complete");
printInfo(
`Login: ${after.requireLogin === true ? "enabled" : "disabled"}${
passwordChanged ? " (password updated)" : ""
}`
);
if (providerConnection) {
printInfo(`Provider: ${providerConnection.provider} (${providerConnection.name})`);
} else if (!before.setupComplete) {
printInfo("Provider: skipped");
}
return 0;
} finally {
prompt.close();
}
}

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