Merge PR #2280: feat(cli): CLI v4 — Commander.js, 50+ commands, TUI, i18n, plugins (Phases 0-9)

Complete rewrite of the OmniRoute CLI:
- Commander.js-based modular architecture (50+ command files)
- Full i18n support (en + pt-BR, 1222 keys each)
- TUI interactive interface (OAuthFlow, EvalWatch, ProvidersTestAll)
- Plugin system (omniroute-cmd-*)
- OpenAPI codegen (omniroute api <tag> <op>)
- Commands: serve, combo, compression, keys, tunnel, backup, test-provider,
  health, memory, MCP, A2A, oauth, skills, webhooks, usage, cost, eval,
  context-eng, dashboard, doctor, env, files, logs, models, nodes, oneproxy,
  open, openapi, plugin, policy, pricing, providers, quota, registry, repl,
  reset-encrypted-columns, resilience, restart, runtime, sessions, setup,
  simulate, status, stop, stream, sync, tags, telemetry, translator, tray, update
- Code review fixes: C1-C3, I1-I5, M1-M4 applied

# Conflicts:
#	bin/cli/commands/config.mjs
#	bin/omniroute.mjs
#	package-lock.json
#	package.json
This commit is contained in:
diegosouzapw
2026-05-15 10:50:54 -03:00
229 changed files with 30264 additions and 4992 deletions

File diff suppressed because it is too large Load Diff

210
bin/cli/CONVENTIONS.md Normal file
View File

@@ -0,0 +1,210 @@
# 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/{en,pt-BR}.json` (nested objects).
- Detection: `OMNIROUTE_LANG` overrides, otherwise `LC_ALL`, `LC_MESSAGES`,
`LANG`. Fallback: `en`.
- Missing keys return the key itself (no crash). PRs that add new strings
must update both `en` and `pt-BR` catalogs.
## 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.

114
bin/cli/README.md Normal file
View File

@@ -0,0 +1,114 @@
# 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
├── index.mjs ← central command router (will migrate to Commander in 1.1)
├── args.mjs ← legacy arg parser (replaced by Commander in 1.1)
├── 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
│ └── pt-BR.json ← Portuguese (Brazil) strings
└── commands/
├── setup.mjs
├── doctor.mjs
├── providers.mjs
├── config.mjs
├── 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);
```
## Adding a new command
1. Create `bin/cli/commands/your-command.mjs`
2. Export `runYourCommand(argv, context)` (pre-1.1) or `registerYourCommand(program)` (post-1.1)
3. Register in `bin/cli/index.mjs` (pre-1.1) or `bin/cli/program.mjs` (post-1.1)
4. Add strings to both `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.

View File

@@ -0,0 +1,58 @@
// AUTO-GENERATED from docs/reference/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);
});
}

View File

@@ -0,0 +1,52 @@
// AUTO-GENERATED from docs/reference/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);
});
}

View File

@@ -0,0 +1,76 @@
// AUTO-GENERATED from docs/reference/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);
});
}

View File

@@ -0,0 +1,526 @@
// AUTO-GENERATED from docs/reference/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);
});
}

View File

@@ -0,0 +1,110 @@
// AUTO-GENERATED from docs/reference/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);
});
}

View File

@@ -0,0 +1,100 @@
// AUTO-GENERATED from docs/reference/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);
});
}

View File

@@ -0,0 +1,182 @@
// AUTO-GENERATED from docs/reference/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);
});
}

View File

@@ -0,0 +1,54 @@
// AUTO-GENERATED from docs/reference/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);
});
}

View File

@@ -0,0 +1,66 @@
// AUTO-GENERATED from docs/reference/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);
});
}

View File

@@ -0,0 +1,54 @@
// AUTO-GENERATED from docs/reference/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);
});
}

View File

@@ -0,0 +1,52 @@
// AUTO-GENERATED from docs/reference/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);
});
}

View File

@@ -0,0 +1,129 @@
// AUTO-GENERATED from docs/reference/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);
});
}

View File

@@ -0,0 +1,30 @@
// AUTO-GENERATED from docs/reference/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);
});
}

View File

@@ -0,0 +1,162 @@
// AUTO-GENERATED from docs/reference/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);
});
}

View File

@@ -0,0 +1,64 @@
// AUTO-GENERATED from docs/reference/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);
});
}

View File

@@ -0,0 +1,92 @@
// AUTO-GENERATED from docs/reference/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);
});
}

View File

@@ -0,0 +1,164 @@
// AUTO-GENERATED from docs/reference/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);
});
}

View File

@@ -0,0 +1,88 @@
// AUTO-GENERATED. Do not edit.
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_oauth } from "./oauth.mjs";
import { register_cloud } from "./cloud.mjs";
import { register_fallback } from "./fallback.mjs";
import { register_telemetry } from "./telemetry.mjs";
export const API_TAGS = [
"chat",
"messages",
"responses",
"embeddings",
"images",
"audio",
"moderations",
"rerank",
"system",
"models",
"providers",
"provider-nodes",
"api-keys",
"combos",
"settings",
"compression",
"usage",
"pricing",
"translator",
"cli-tools",
"oauth",
"cloud",
"fallback",
"telemetry",
];
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_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_oauth(api);
register_cloud(api);
register_fallback(api);
register_telemetry(api);
}

View File

@@ -0,0 +1,30 @@
// AUTO-GENERATED from docs/reference/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);
});
}

View File

@@ -0,0 +1,30 @@
// AUTO-GENERATED from docs/reference/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);
});
}

View File

@@ -0,0 +1,294 @@
// AUTO-GENERATED from docs/reference/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);
});
}

View File

@@ -0,0 +1,466 @@
// AUTO-GENERATED from docs/reference/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("--limit <limit>", "")
.action(async (opts, cmd) => {
const gOpts = cmd.optsWithGlobals();
let url = "/api/compliance/audit-log";
const qs = new URLSearchParams();
if (opts.limit != null) qs.set("limit", String(opts.limit));
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);
});
}

View File

@@ -0,0 +1,36 @@
// AUTO-GENERATED from docs/reference/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);
});
}

View File

@@ -0,0 +1,88 @@
// AUTO-GENERATED from docs/reference/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);
});
}

View File

@@ -0,0 +1,168 @@
// AUTO-GENERATED from docs/reference/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);
});
}

245
bin/cli/api.mjs Normal file
View File

@@ -0,0 +1,245 @@
import { setTimeout as sleep } from "node:timers/promises";
import { existsSync, readFileSync } from "node:fs";
import { join } from "node:path";
import { resolveDataDir } from "./data-dir.mjs";
import { getCliToken, CLI_TOKEN_HEADER } from "./utils/cliToken.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);
try {
const configPath = join(resolveDataDir(), "config.json");
if (existsSync(configPath)) {
const cfg = JSON.parse(readFileSync(configPath, "utf8"));
const profile = cfg.activeProfile && cfg.profiles?.[cfg.activeProfile];
if (profile?.baseUrl) return stripTrailingSlash(profile.baseUrl);
if (cfg.baseUrl) return stripTrailingSlash(cfg.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) {
return String(value).replace(/\/+$/, "");
}
function resolveUrl(path, opts) {
if (/^https?:\/\//i.test(path)) return path;
return `${getBaseUrl(opts)}${path.startsWith("/") ? path : `/${path}`}`;
}
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");
}
const apiKey = opts.apiKey ?? process.env.OMNIROUTE_API_KEY;
if (apiKey && !headers.has("authorization")) {
headers.set("authorization", `Bearer ${apiKey}`);
}
// 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;
}
}

View File

@@ -1,47 +0,0 @@
export function parseArgs(argv = []) {
const flags = {};
const positionals = [];
for (let i = 0; i < argv.length; i += 1) {
const arg = argv[i];
if (arg.startsWith("-") && !arg.startsWith("--") && arg.length > 1) {
for (const key of arg.slice(1)) {
flags[key] = true;
}
continue;
}
if (!arg.startsWith("--")) {
positionals.push(arg);
continue;
}
const eqIndex = arg.indexOf("=");
if (eqIndex !== -1) {
flags[arg.slice(2, eqIndex)] = arg.slice(eqIndex + 1);
continue;
}
const key = arg.slice(2);
const next = argv[i + 1];
if (next && !next.startsWith("--")) {
flags[key] = next;
i += 1;
} else {
flags[key] = true;
}
}
return { flags, positionals };
}
export function getStringFlag(flags, name, envName = null) {
const value = flags[name] ?? (envName ? process.env[envName] : undefined);
if (typeof value !== "string") return "";
return value.trim();
}
export function hasFlag(flags, name) {
return flags[name] === true;
}

323
bin/cli/commands/a2a.mjs Normal file
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
bin/cli/commands/audit.mjs Normal file
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);
}

View File

@@ -0,0 +1,39 @@
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");
cmd
.command("enable")
.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")
.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("status")
.description(t("autostart.status") || "Show autostart status")
.action(async (opts, c) => {
const globalOpts = c.optsWithGlobals();
const { isAutostartEnabled } = await import("../tray/autostart.mjs");
emit({ enabled: isAutostartEnabled() }, globalOpts);
});
}

431
bin/cli/commands/backup.mjs Normal file
View File

@@ -0,0 +1,431 @@
import {
copyFileSync,
existsSync,
mkdirSync,
readdirSync,
readFileSync,
writeFileSync,
} from "node:fs";
import { createCipheriv, createDecipheriv, randomBytes, scryptSync } from "node:crypto";
import { dirname, join, extname, basename } from "node:path";
import { resolveDataDir } from "../data-dir.mjs";
import { apiFetch, isServerUp } from "../api.mjs";
import { t } from "../i18n.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));
}
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 plaintext = readFileSync(srcPath);
const encrypted = Buffer.concat([cipher.update(plaintext), cipher.final()]);
const authTag = cipher.getAuthTag();
// Format: salt(16) + iv(12) + authTag(16) + ciphertext
writeFileSync(destPath, Buffer.concat([salt, iv, authTag, encrypted]));
}
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 Database;
try {
Database = (await import("better-sqlite3")).default;
} catch {
Database = null;
}
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") && Database) {
const db = new Database(sourcePath, { readonly: true });
const tmpPath = destPath.replace(/\.enc$/, "");
await db.backup(tmpPath);
db.close();
if (opts.encrypt) {
encryptFile(tmpPath, destPath, passphrase);
const { unlinkSync } = await import("node:fs");
unlinkSync(tmpPath);
}
} else if (opts.encrypt) {
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 {
// Read files locally and send as base64 — never send local path to server
const files = {};
for (const fname of readdirSync(backupPath)) {
files[fname] = readFileSync(join(backupPath, fname)).toString("base64");
}
const res = await apiFetch("/api/db-backups/cloud", {
method: "POST",
body: { files, info },
retry: false,
timeout: 30000,
acceptNotOk: true,
});
if (res.ok) {
const data = await res.json();
console.log(t("backup.cloudUploaded", { url: data.url || "(stored)" }));
return 0;
}
return 1;
} catch {
return 1;
}
}
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;
}
}

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
bin/cli/commands/cache.mjs Normal file
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
bin/cli/commands/chat.mjs Normal file
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
bin/cli/commands/cloud.mjs Normal file
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
bin/cli/commands/combo.mjs Normal file
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;
}
}

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;
}

View File

@@ -0,0 +1,167 @@
import { readFileSync } from "node:fs";
import { apiFetch } from "../api.mjs";
import { emit } from "../output.mjs";
import { t } from "../i18n.mjs";
const VALID_ENGINES = ["caveman", "rtk", "hybrid", "none"];
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(`Error: ${res.status}\n`);
process.exit(1);
}
return res.json();
}
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", {});
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);
emit(data, cmd.optsWithGlobals());
}
export async function runCompressionEngineSet(name, opts, cmd) {
if (!VALID_ENGINES.includes(name)) {
process.stderr.write(`Unknown engine: ${name}. Valid: ${VALID_ENGINES.join(", ")}\n`);
process.exit(2);
}
await mcpCall("omniroute_set_compression_engine", { engine: name });
process.stdout.write(`Engine: ${name}\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", {});
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", {});
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",
});
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);
}

View File

@@ -1,31 +1,8 @@
import { parseArgs, getStringFlag, hasFlag } from "../args.mjs";
import { printHeading, printInfo, printSuccess, printError } from "../io.mjs";
import { resolveDataDir } from "../data-dir.mjs";
import { t } from "../i18n.mjs";
import path from "node:path";
import fs from "node:fs";
function printConfigHelp() {
console.log(`
Usage:
omniroute config list List all CLI tools and config status
omniroute config get <tool> Show current config for a tool
omniroute config set <tool> [options] Write config for a tool
omniroute config validate <tool> Validate config format without writing
omniroute config tray <enable|disable> Enable/disable tray autostart on login
omniroute config token Show the machine-derived CLI auth token
Options:
--base-url <url> OmniRoute API base URL (default: http://localhost:20128/v1)
--api-key <key> API key for the tool
--model <model> Model identifier (where applicable)
--json Output as JSON
--non-interactive Do not prompt for confirmation
--yes Skip confirmation prompt
--help Show this help
Tools: claude, codex, opencode, cline, kilocode, continue
`);
}
import { registerContexts } from "./contexts.mjs";
function ensureBackup(configPath) {
if (!fs.existsSync(configPath)) return;
@@ -36,190 +13,183 @@ function ensureBackup(configPath) {
return backupPath;
}
export async function runConfigCommand(argv) {
const { flags, positionals } = parseArgs(argv);
async function runConfigListCommand(opts = {}) {
const { detectAllTools } = await import("../../../src/lib/cli-helper/tool-detector.js");
const tools = await detectAllTools();
if (hasFlag(flags, "help") || hasFlag(flags, "h") || positionals.length === 0) {
printConfigHelp();
return 0;
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;
}
const subcommand = positionals[0];
const toolId = positionals[1];
if (subcommand === "list") {
const { detectAllTools } = await import("../../../src/lib/cli-helper/tool-detector.js");
const tools = await detectAllTools();
if (hasFlag(flags, "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;
}
if (subcommand === "get") {
if (!toolId) {
printError("Tool ID required. Usage: omniroute config get <tool>");
return 1;
}
const { detectTool } = await import("../../../src/lib/cli-helper/tool-detector.js");
const tool = await detectTool(toolId);
if (!tool) {
printError(`Unknown tool: ${toolId}`);
return 1;
}
if (hasFlag(flags, "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;
const { detectTool } = await import("../../../src/lib/cli-helper/tool-detector.js");
const tool = await detectTool(toolId);
if (!tool) {
printError(`Unknown tool: ${toolId}`);
return 1;
}
if (subcommand === "set") {
if (!toolId) {
printError("Tool ID required. Usage: omniroute config set <tool> [options]");
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);
}
const baseUrl =
getStringFlag(flags, "base-url", "OMNIROUTE_BASE_URL") || "http://localhost:20128/v1";
const apiKey = getStringFlag(flags, "api-key", "OMNIROUTE_API_KEY");
const model = getStringFlag(flags, "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 = hasFlag(flags, "non-interactive") || hasFlag(flags, "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;
}
return 0;
}
if (subcommand === "validate") {
if (!toolId) {
printError("Tool ID required. Usage: omniroute config validate <tool>");
return 1;
}
const baseUrl =
getStringFlag(flags, "base-url", "OMNIROUTE_BASE_URL") || "http://localhost:20128/v1";
const apiKey = getStringFlag(flags, "api-key", "OMNIROUTE_API_KEY") || "test-key";
const model = getStringFlag(flags, "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 (hasFlag(flags, "json")) {
console.log(JSON.stringify({ valid: true, content: result.content }, null, 2));
}
return 0;
}
if (subcommand === "token") {
const { getMachineTokenSync } = await import("../../../src/lib/machineToken.ts");
const token = getMachineTokenSync();
if (!token) {
printError("Could not derive machine token (machine-id unavailable).");
return 1;
}
if (hasFlag(flags, "json")) {
console.log(JSON.stringify({ token, header: "x-omniroute-cli-token" }));
} else {
printHeading("CLI Machine Token");
console.log(` Header: x-omniroute-cli-token`);
console.log(` Value: ${token}`);
console.log(`\n Use this token to authenticate management API calls from localhost.`);
}
return 0;
}
if (subcommand === "tray") {
const action = positionals[1];
if (action === "enable") {
const { enableAutoStart } = await import("../tray/autostart.ts");
const ok = await enableAutoStart();
if (ok) {
printSuccess("Autostart enabled — omniroute --tray will launch on login.");
} else {
printError("Autostart failed (unsupported OS or permission denied).");
return 1;
}
return 0;
}
if (action === "disable") {
const { disableAutoStart } = await import("../tray/autostart.ts");
await disableAutoStart();
printSuccess("Autostart disabled.");
return 0;
}
printError("Usage: omniroute config tray <enable|disable>");
async function runConfigSetCommand(toolId, opts = {}) {
if (!toolId) {
printError("Tool ID required. Usage: omniroute config set <tool> [options]");
return 1;
}
printError(`Unknown subcommand: ${subcommand}`);
printConfigHelp();
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;
}
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("--base-url <url>", "OmniRoute API base URL", "http://localhost:20128/v1")
.option("--api-key <key>", "API key for the 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, output: globalOpts.output });
if (exitCode !== 0) process.exit(exitCode);
});
config
.command("validate <tool>")
.description("Validate config format without writing")
.option("--base-url <url>", "OmniRoute API base URL", "http://localhost:20128/v1")
.option("--api-key <key>", "API key for the tool")
.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, output: globalOpts.output });
if (exitCode !== 0) process.exit(exitCode);
});
// Register contexts/profiles CRUD as a subgroup of config.
registerContexts(config);
}

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());
});
}

View File

@@ -0,0 +1,223 @@
import { t } from "../i18n.mjs";
import { emit } from "../output.mjs";
import { loadContexts, saveContexts, configPath } from "../contexts.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);
}
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")
.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: c.apiKey ? "✓" : "✗",
description: c.description || "",
}));
emit(rows, globalOpts, [
{ key: "active", header: "" },
{ key: "name", header: "Name" },
{ key: "baseUrl", header: "Base URL" },
{ key: "auth", header: "Auth" },
{ key: "description", header: "Description" },
]);
});
ctx
.command("add <name>")
.description("Add a new context")
.requiredOption("--url <u>", "Base URL")
.option("--api-key <k>", "API key")
.option("--api-key-stdin", "Read API key from stdin")
.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;
if (opts.apiKeyStdin) {
const chunks = [];
for await (const c of process.stdin) chunks.push(c);
apiKey = chunks.join("").trim() || null;
}
cfg.contexts = cfg.contexts || {};
cfg.contexts[name] = {
baseUrl: opts.url,
apiKey,
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 current active context name")
.action(() => {
const cfg = loadContexts();
process.stdout.write(`${cfg.currentContext || "default"}\n`);
});
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,
apiKey: maskKey(c.apiKey),
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;
}
}
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",
apiKey: typeof c.apiKey === "string" ? c.apiKey : null,
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
bin/cli/commands/cost.mjs Normal file
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;
}

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());
});
}

View File

@@ -4,9 +4,9 @@ import os from "node:os";
import path from "node:path";
import { createDecipheriv, scryptSync } from "node:crypto";
import { pathToFileURL } from "node:url";
import { parseArgs, getStringFlag, hasFlag } from "../args.mjs";
import { resolveDataDir, resolveStoragePath } from "../data-dir.mjs";
import { printHeading } from "../io.mjs";
import { t } from "../i18n.mjs";
const STATIC_SALT = "omniroute-field-encryption-v1";
const KEY_LENGTH = 32;
@@ -181,8 +181,11 @@ function decryptCredentialSample(value, key) {
const [ivHex, encryptedHex, authTagHex] = body.split(":");
if (!ivHex || !encryptedHex || !authTagHex) throw new Error("Malformed encrypted value");
const decipher = createDecipheriv("aes-256-gcm", key, Buffer.from(ivHex, "hex"));
decipher.setAuthTag(Buffer.from(authTagHex, "hex"));
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;
@@ -487,25 +490,6 @@ export async function collectDoctorChecks(context = {}, options = {}) {
};
}
function printDoctorHelp() {
console.log(`
Usage:
omniroute doctor
omniroute doctor --json
omniroute doctor --no-liveness
omniroute doctor --host 0.0.0.0
Options:
--json Print machine-readable JSON
--no-liveness Skip HTTP health endpoint probing
--host <host> Host for server liveness probing (default: 127.0.0.1)
--liveness-url <url> Full health endpoint URL override
Checks:
config, database, storage/encryption, ports, Node runtime, native binary, memory, server liveness, CLI tools
`);
}
function printCheck(check) {
const label = check.status.toUpperCase().padEnd(4);
const color =
@@ -513,20 +497,31 @@ function printCheck(check) {
console.log(`${color}${label}\x1b[0m ${check.name}: ${check.message}`);
}
export async function runDoctorCommand(argv, context = {}) {
const { flags } = parseArgs(argv);
if (hasFlag(flags, "help") || hasFlag(flags, "h")) {
printDoctorHelp();
return 0;
}
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: hasFlag(flags, "no-liveness"),
livenessHost: getStringFlag(flags, "host"),
livenessUrl: getStringFlag(flags, "liveness-url"),
skipLiveness,
livenessHost: opts.host,
livenessUrl: opts.livenessUrl,
});
if (hasFlag(flags, "json")) {
if (isJson) {
console.log(JSON.stringify(result, null, 2));
} else {
printHeading("OmniRoute Doctor");

100
bin/cli/commands/env.mjs Normal file
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
bin/cli/commands/eval.mjs Normal file
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
bin/cli/commands/files.mjs Normal file
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
bin/cli/commands/health.mjs Normal file
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
bin/cli/commands/keys.mjs Normal file
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;
}
}

View File

@@ -1,40 +1,89 @@
import { parseArgs, getStringFlag, hasFlag } from "../args.mjs";
import { printHeading, printInfo, printError } from "../io.mjs";
import { writeFileSync, appendFileSync, existsSync, unlinkSync } from "node:fs";
import { t } from "../i18n.mjs";
function printLogsHelp() {
console.log(`
Usage:
omniroute logs [options]
Options:
--follow Stream logs in real-time
--filter <level> Filter by level (error, warn, info) — comma-separated
--lines <n> Number of lines to fetch (default: 100)
--timeout <ms> Connection timeout in ms (default: 30000)
--base-url <url> OmniRoute API base URL (default: http://localhost:20128)
--json Output as JSON
--help Show this help
`);
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"), "http://localhost:20128")
.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();
const exitCode = await runLogsCommand({ ...opts, output: globalOpts.output });
if (exitCode !== 0) process.exit(exitCode);
});
}
export async function runLogsCommand(argv) {
const { flags } = parseArgs(argv);
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;
if (hasFlag(flags, "help") || hasFlag(flags, "h")) {
printLogsHelp();
return 0;
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 = {}) {
const baseUrl = opts.baseUrl || opts["base-url"] || "http://localhost:20128";
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 baseUrl = getStringFlag(flags, "base-url") || "http://localhost:20128";
const follow = hasFlag(flags, "follow");
const filter = getStringFlag(flags, "filter");
const lines = getStringFlag(flags, "lines") || "100";
const timeout = parseInt(getStringFlag(flags, "timeout") || "30000", 10);
const filters = filter ? filter.split(",").map((f) => f.trim()) : [];
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()) : [];
const { createLogStream } = await import("../../../src/lib/cli-helper/log-streamer.js");
const { stream, stop } = createLogStream({ baseUrl, filters, follow, timeout });
const { stream, stop } = createLogStream({ baseUrl, filters: levelFilters, follow, timeout });
const reader = stream.getReader();
const decoder = new TextDecoder();
@@ -42,21 +91,43 @@ export async function runLogsCommand(argv) {
const processLine = (line) => {
if (!line.trim()) return;
if (hasFlag(flags, "json")) {
console.log(line);
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;
}
try {
const parsed = JSON.parse(line);
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}`);
} catch {
console.log(line);
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 {
@@ -64,16 +135,21 @@ export async function runLogsCommand(argv) {
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) processLine(line);
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") {
printInfo("Log stream stopped.");
console.log(t("logs.stopped"));
} else {
printError(`Log stream error: ${err.message}`);
console.error(
t("logs.streamError", {
message: (err instanceof Error ? err.message : String(err)).slice(0, 100),
})
);
}
} finally {
stop();

273
bin/cli/commands/mcp.mjs Normal file
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;
}
}

210
bin/cli/commands/memory.mjs Normal file
View File

@@ -0,0 +1,210 @@
import { readFileSync } from "node:fs";
import { apiFetch } from "../api.mjs";
import { emit } from "../output.mjs";
import { t } from "../i18n.mjs";
const VALID_TYPES = ["user", "feedback", "project", "reference"];
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) });
if (opts.type) params.set("type", opts.type);
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 body = {
content,
type: opts.type ?? "user",
...(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();
if (opts.type) params.set("type", opts.type);
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) });
if (opts.type) params.set("type", opts.type);
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);
}

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
bin/cli/commands/nodes.mjs Normal file
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
bin/cli/commands/oauth.mjs Normal file
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);
}

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
bin/cli/commands/open.mjs Normal file
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");
}
});
}

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()
);
});
}

192
bin/cli/commands/plugin.mjs Normal file
View File

@@ -0,0 +1,192 @@
import { execSync } 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";
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 {
execSync(`npm install -g ${pkgName}`, { stdio: "inherit" });
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 {
execSync(`npm uninstall -g ${pkgName}`, { stdio: "inherit" });
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) => {
const pkg = name ? `omniroute-cmd-${name}` : "omniroute-cmd-*";
try {
execSync(`npm update -g ${pkg}`, { stdio: "inherit" });
process.stdout.write(`✓ Updated: ${pkg}\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`);
});
}

179
bin/cli/commands/policy.mjs Normal file
View File

@@ -0,0 +1,179 @@
import { readFileSync, writeFileSync } from "node:fs";
import { apiFetch } from "../api.mjs";
import { emit } from "../output.mjs";
import { t } from "../i18n.mjs";
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 = JSON.parse(readFileSync(opts.file, "utf8"));
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 = JSON.parse(readFileSync(opts.file, "utf8"));
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: JSON.parse(opts.context) } : {}),
};
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 = JSON.parse(readFileSync(file, "utf8"));
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);
}

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());
});
}

View File

@@ -1,278 +1,18 @@
import { parseArgs, getStringFlag, hasFlag } from "../args.mjs";
import { printHeading, printInfo, printSuccess, printError } from "../io.mjs";
import { resolveDataDir, resolveStoragePath } from "../data-dir.mjs";
import path from "node:path";
import fs from "node:fs";
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:
function printProviderHelp() {
console.log(`
Usage:
omniroute provider add <name> [options] Add a provider connection
omniroute provider list List configured providers
omniroute provider remove <name|id> Remove a provider connection
omniroute provider test <name|id> Test a provider connection
omniroute provider default <name|id> Set default provider
Options:
--provider <id> Provider id (e.g., openai, anthropic, omniroute)
--api-key <key> API key for the provider
--provider-name <name> Display name for the connection
--default-model <model> Default model to use
--base-url <url> Custom base URL override
--json Output as JSON
--yes Skip confirmation
--help Show this help
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
`);
}
export async function runProviderCommand(argv) {
const { flags, positionals } = parseArgs(argv);
if (hasFlag(flags, "help") || hasFlag(flags, "h") || positionals.length === 0) {
printProviderHelp();
return 0;
}
const subcommand = positionals[0];
if (subcommand === "add") {
const providerName = positionals[1] || getStringFlag(flags, "provider");
const apiKey = getStringFlag(flags, "api-key");
const displayName = getStringFlag(flags, "provider-name");
const defaultModel = getStringFlag(flags, "default-model");
const baseUrl = getStringFlag(flags, "base-url");
if (!providerName) {
printError("Provider name required. Usage: omniroute provider add <name>");
return 1;
}
if (providerName === "omniroute") {
// Special case: add OmniRoute as a provider in OpenCode config
const opencodePath = path.join(
process.env.HOME || os.homedir(),
".config",
"opencode",
"opencode.json"
);
const { generateConfig } =
await import("../../../src/lib/cli-helper/config-generator/index.js");
const result = await generateConfig("opencode", {
baseUrl: baseUrl || "http://localhost:20128/v1",
apiKey: apiKey || "",
});
if (!result.success) {
printError(result.error || "Failed to generate config");
return 1;
}
if (!hasFlag(flags, "yes")) {
console.log(`\n About to write OpenCode config to: ${opencodePath}`);
console.log(` Content:\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)) {
printInfo("Aborted.");
return 0;
}
}
const dir = path.dirname(opencodePath);
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
fs.writeFileSync(opencodePath, result.content, "utf-8");
printSuccess(`OpenCode config written to ${opencodePath}`);
return 0;
}
// Generic provider addition via SQLite
const dbPath = resolveStoragePath(resolveDataDir());
if (!fs.existsSync(dbPath)) {
printError("Database not found. Run `omniroute setup` first.");
return 1;
}
const { default: Database } = await import("better-sqlite3");
const db = new Database(dbPath);
try {
const stmt = db.prepare(`
INSERT INTO provider_connections (provider, name, api_key, default_model, provider_specific_data)
VALUES (?, ?, ?, ?, ?)
`);
const specificData = baseUrl ? JSON.stringify({ baseUrl }) : null;
stmt.run(
providerName,
displayName || providerName,
apiKey || "",
defaultModel || null,
specificData
);
printSuccess(`Provider "${displayName || providerName}" added`);
} finally {
db.close();
}
return 0;
}
if (subcommand === "list") {
const dbPath = resolveStoragePath(resolveDataDir());
if (!fs.existsSync(dbPath)) {
if (isJson()) console.log(JSON.stringify([]));
else printInfo("No database found. Run `omniroute setup` first.");
return 0;
}
const { default: Database } = await import("better-sqlite3");
const db = new Database(dbPath);
try {
const rows = db
.prepare("SELECT id, provider, name, default_model FROM provider_connections")
.all();
if (isJson()) {
console.log(JSON.stringify(rows, null, 2));
} else {
printHeading("Configured Providers");
for (const r of rows) {
console.log(
` [${r.id}] ${r.name} (${r.provider})${r.default_model ? ` — model: ${r.default_model}` : ""}`
);
}
}
} finally {
db.close();
}
return 0;
}
if (subcommand === "remove") {
const target = positionals[1];
if (!target) {
printError("Provider name or ID required. Usage: omniroute provider remove <name|id>");
return 1;
}
const dbPath = resolveStoragePath(resolveDataDir());
if (!fs.existsSync(dbPath)) {
printError("Database not found.");
return 1;
}
const { default: Database } = await import("better-sqlite3");
const db = new Database(dbPath);
try {
const isId = /^\d+$/.test(target);
const stmt = isId
? db.prepare("DELETE FROM provider_connections WHERE id = ?")
: db.prepare("DELETE FROM provider_connections WHERE name = ? OR provider = ?");
const result = stmt.run(isId ? parseInt(target, 10) : target);
if (result.changes > 0) {
printSuccess(`Removed ${result.changes} provider(s)`);
} else {
printError("Provider not found");
}
} finally {
db.close();
}
return 0;
}
if (subcommand === "test") {
const target = positionals[1];
if (!target) {
printError("Provider name or ID required. Usage: omniroute provider test <name|id>");
return 1;
}
const dbPath = resolveStoragePath(resolveDataDir());
if (!fs.existsSync(dbPath)) {
printError("Database not found.");
return 1;
}
const { default: Database } = await import("better-sqlite3");
const db = new Database(dbPath);
try {
const isId = /^\d+$/.test(target);
const row = isId
? db.prepare("SELECT * FROM provider_connections WHERE id = ?").get(parseInt(target, 10))
: db
.prepare("SELECT * FROM provider_connections WHERE name = ? OR provider = ?")
.get(target);
if (!row) {
printError("Provider not found");
return 1;
}
const { testProviderApiKey } = await import("../provider-test.mjs");
const result = await testProviderApiKey({
provider: row.provider,
apiKey: row.api_key,
defaultModel: row.default_model,
baseUrl: row.provider_specific_data ? JSON.parse(row.provider_specific_data).baseUrl : null,
});
if (isJson()) {
console.log(JSON.stringify(result, null, 2));
} else if (result.valid) {
printSuccess(`Provider "${row.name}" is reachable`);
} else {
printError(`Provider test failed: ${result.error || "unknown error"}`);
}
} finally {
db.close();
}
return 0;
}
if (subcommand === "default") {
const target = positionals[1];
if (!target) {
printError("Provider name or ID required. Usage: omniroute provider default <name|id>");
return 1;
}
const dbPath = resolveStoragePath(resolveDataDir());
if (!fs.existsSync(dbPath)) {
printError("Database not found.");
return 1;
}
const { default: Database } = await import("better-sqlite3");
const db = new Database(dbPath);
try {
const isId = /^\d+$/.test(target);
const row = isId
? db.prepare("SELECT * FROM provider_connections WHERE id = ?").get(parseInt(target, 10))
: db
.prepare("SELECT * FROM provider_connections WHERE name = ? OR provider = ?")
.get(target);
if (!row) {
printError("Provider not found");
return 1;
}
db.prepare("UPDATE provider_connections SET is_default = 0").run();
db.prepare("UPDATE provider_connections SET is_default = 1 WHERE id = ?").run(row.id);
printSuccess(`Default provider set to "${row.name}"`);
} finally {
db.close();
}
return 0;
}
printError(`Unknown subcommand: ${subcommand}`);
printProviderHelp();
return 1;
}
function isJson() {
return process.argv.includes("--json");
});
}

View File

@@ -1,4 +1,5 @@
import { parseArgs, getStringFlag, hasFlag } from "../args.mjs";
import { apiFetch } 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";
@@ -9,6 +10,7 @@ import {
updateProviderTestResult,
} from "../provider-store.mjs";
import { openOmniRouteDb } from "../sqlite.mjs";
import { t } from "../i18n.mjs";
function publicConnection(connection) {
return {
@@ -24,113 +26,6 @@ function publicConnection(connection) {
};
}
function printProvidersHelp() {
console.log(`
Usage:
omniroute providers available
omniroute providers available --search openai
omniroute providers available --category api-key
omniroute providers list
omniroute providers test <id|name>
omniroute providers test-all
omniroute providers validate
Options:
--json Print machine-readable JSON
--search, --q <text> Filter available providers by id, name, alias, or category
--category <category> Filter available providers by category
Notes:
"available" shows the OmniRoute provider catalog.
"list" shows provider connections already configured in local SQLite.
Provider commands read local SQLite directly and do not require the server to be running.
API-key provider tests update test_status, last_tested, and error fields in SQLite.
`);
}
function printAvailableHelp() {
console.log(`
Usage:
omniroute providers available
omniroute providers available --search openai
omniroute providers available --category api-key
omniroute providers available --json
Options:
--json Print machine-readable JSON
--search, --q <text> Filter by id, name, alias, or category
--category <category> Filter by category, for example api-key, oauth, free
Notes:
Shows the OmniRoute provider catalog, not locally configured provider connections.
`);
}
function printListHelp() {
console.log(`
Usage:
omniroute providers list
omniroute providers list --json
Options:
--json Print machine-readable JSON
Notes:
Lists provider connections already configured in local SQLite.
`);
}
function printTestHelp() {
console.log(`
Usage:
omniroute providers test <id|name>
omniroute providers test <id|name> --json
Options:
--json Print machine-readable JSON
Notes:
Tests one configured provider connection and updates test status in local SQLite.
`);
}
function printTestAllHelp() {
console.log(`
Usage:
omniroute providers test-all
omniroute providers test-all --json
Options:
--json Print machine-readable JSON
Notes:
Tests every active configured provider connection and updates test status in local SQLite.
`);
}
function printValidateHelp() {
console.log(`
Usage:
omniroute providers validate
omniroute providers validate --json
Options:
--json Print machine-readable JSON
Notes:
Validates local provider configuration without calling upstream providers.
`);
}
function printProvidersSubcommandHelp(subcommand) {
if (subcommand === "available") printAvailableHelp();
else if (subcommand === "list") printListHelp();
else if (subcommand === "test") printTestHelp();
else if (subcommand === "test-all") printTestAllHelp();
else if (subcommand === "validate") printValidateHelp();
else printProvidersHelp();
}
function statusColor(status) {
if (status === "active" || status === "success") return "\x1b[32m";
if (status === "error" || status === "expired" || status === "unavailable") return "\x1b[31m";
@@ -186,11 +81,11 @@ function publicAvailableProvider(provider) {
};
}
function filterAvailableProviders(providers, flags) {
const search = String(getStringFlag(flags, "search") || getStringFlag(flags, "q") || "")
function filterAvailableProviders(providers, opts) {
const search = String(opts.search || opts.q || "")
.trim()
.toLowerCase();
const category = normalizeCategoryFilter(getStringFlag(flags, "category"));
const category = normalizeCategoryFilter(opts.category);
return providers.filter((provider) => {
if (category && provider.category !== category) return false;
@@ -284,12 +179,12 @@ function validateConnection(connection) {
};
}
async function availableCommand(flags) {
export async function runAvailableCommand(opts = {}) {
const allProviders = loadAvailableProviders();
const providers = filterAvailableProviders(allProviders, flags).map(publicAvailableProvider);
const providers = filterAvailableProviders(allProviders, opts).map(publicAvailableProvider);
const categories = getAvailableProviderCategories(allProviders);
if (hasFlag(flags, "json")) {
if (opts.json) {
console.log(JSON.stringify({ count: providers.length, categories, providers }, null, 2));
} else {
printHeading("OmniRoute Available Providers");
@@ -299,11 +194,11 @@ async function availableCommand(flags) {
return 0;
}
async function listCommand(flags) {
export async function runListCommand(opts = {}) {
const { db } = await openOmniRouteDb();
try {
const connections = listProviderConnections(db).map(publicConnection);
if (hasFlag(flags, "json")) {
if (opts.json) {
console.log(JSON.stringify({ providers: connections }, null, 2));
} else {
printHeading("OmniRoute Providers");
@@ -315,7 +210,7 @@ async function listCommand(flags) {
}
}
async function testCommand(flags, selector) {
export async function runTestCommand(selector, opts = {}) {
if (!selector) {
console.error("Provider id or name is required.");
return 1;
@@ -330,7 +225,7 @@ async function testCommand(flags, selector) {
}
const result = await runProviderTest(db, connection);
if (hasFlag(flags, "json")) {
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`);
@@ -343,7 +238,7 @@ async function testCommand(flags, selector) {
}
}
async function testAllCommand(flags) {
export async function runTestAllCommand(opts = {}) {
const { db } = await openOmniRouteDb();
try {
const connections = listProviderConnections(db);
@@ -361,7 +256,7 @@ async function testAllCommand(flags) {
results.push(await runProviderTest(db, connection));
}
if (hasFlag(flags, "json")) {
if (opts.json) {
console.log(JSON.stringify({ results }, null, 2));
} else {
printHeading("OmniRoute Provider Tests");
@@ -383,11 +278,11 @@ async function testAllCommand(flags) {
}
}
async function validateCommand(flags) {
export async function runValidateCommand(opts = {}) {
const { db } = await openOmniRouteDb();
try {
const results = listProviderConnections(db).map(validateConnection);
if (hasFlag(flags, "json")) {
if (opts.json) {
console.log(JSON.stringify({ results }, null, 2));
} else {
printHeading("OmniRoute Provider Validation");
@@ -406,23 +301,183 @@ async function validateCommand(flags) {
}
}
export async function runProvidersCommand(argv) {
const { flags, positionals } = parseArgs(argv);
const requestedSubcommand = positionals[0];
const subcommand = requestedSubcommand || "list";
export function registerProviders(program) {
const providers = program.command("providers").description(t("providers.title"));
if (hasFlag(flags, "help") || hasFlag(flags, "h")) {
printProvidersSubcommandHelp(requestedSubcommand);
return 0;
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);
});
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;
}
if (subcommand === "available") return availableCommand(flags);
if (subcommand === "list") return listCommand(flags);
if (subcommand === "test") return testCommand(flags, positionals[1]);
if (subcommand === "test-all") return testAllCommand(flags);
if (subcommand === "validate") return validateCommand(flags);
await renderOnce();
}
console.error(`Unknown providers subcommand: ${subcommand}`);
printProvidersHelp();
return 1;
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
bin/cli/commands/quota.mjs Normal file
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;
}

View File

@@ -0,0 +1,122 @@
import { registerMemory } from "./memory.mjs";
import { registerSkills } from "./skills.mjs";
import { registerAudit } from "./audit.mjs";
import { registerOAuth } from "./oauth.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 { 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 { registerApiCommands } from "../api-commands/registry.mjs";
import { registerPlugin } from "./plugin.mjs";
export function registerCommands(program) {
registerMemory(program);
registerSkills(program);
registerAudit(program);
registerOAuth(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);
registerMcp(program);
registerA2a(program);
registerTunnel(program);
registerEnv(program);
registerTestProvider(program);
registerCompletion(program);
registerRuntime(program);
registerTray(program);
registerAutostart(program);
registerRepl(program);
registerApiCommands(program);
registerPlugin(program);
}

19
bin/cli/commands/repl.mjs Normal file
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 });
});
}

View File

@@ -0,0 +1,66 @@
import { fileURLToPath } from "node:url";
import { dirname, resolve } from "node:path";
import { existsSync } from "node:fs";
import { resolveDataDir } from "../data-dir.mjs";
import { join } from "node:path";
const PROJECT_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "../../..");
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;
}
try {
const { countEncryptedCredentials, resetEncryptedColumns } = await import(
`${PROJECT_ROOT}/src/lib/db/recovery.ts`
);
const count = countEncryptedCredentials();
if (count === 0) {
console.log("\x1b[32m✔ No encrypted credentials found — nothing to reset.\x1b[0m");
return 0;
}
const { affected } = resetEncryptedColumns({ dryRun: false });
console.log(
`\x1b[32m✔ Reset ${affected} 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;
}
}

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());
});
}

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;
}

View File

@@ -0,0 +1,80 @@
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);
}
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(async (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);
}
});
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);
}
});
}

341
bin/cli/commands/serve.mjs Normal file
View File

@@ -0,0 +1,341 @@
import { spawn } from "node:child_process";
import { existsSync } from "node:fs";
import { join, dirname } from "node:path";
import { fileURLToPath } from "node:url";
import { platform } from "node:os";
import { t } from "../i18n.mjs";
import { writePidFile, cleanupPidFile, waitForServer } from "../utils/pid.mjs";
import { ServerSupervisor, detectMitmCrash } from "../runtime/processSupervisor.mjs";
const __dirname = dirname(fileURLToPath(import.meta.url));
const ROOT = join(__dirname, "..", "..", "..");
const APP_DIR = 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"), "20128")
.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")
.action(async (opts) => {
await runServe(opts);
});
}
export async function runServe(opts = {}) {
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`);
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\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`);
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`);
const rawMemory = parseInt(process.env.OMNIROUTE_MEMORY_MB || "512", 10);
const memoryLimit =
Number.isFinite(rawMemory) && rawMemory >= 64 && rawMemory <= 16384 ? rawMemory : 512;
const env = {
...process.env,
OMNIROUTE_PORT: String(port),
PORT: String(dashboardPort),
DASHBOARD_PORT: String(dashboardPort),
API_PORT: String(apiPort),
HOSTNAME: "0.0.0.0",
NODE_ENV: "production",
NODE_OPTIONS: `--max-old-space-size=${memoryLimit}`,
};
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);
}
return runWithSupervisor(
serverJs,
env,
memoryLimit,
dashboardPort,
apiPort,
noOpen,
opts.log === true,
opts.maxRestarts ?? 2,
useTray
);
}
function runDaemon(serverJs, env, memoryLimit, dashboardPort, apiPort) {
const server = spawn("node", [`--max-old-space-size=${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 http://localhost:${dashboardPort}`);
console.log(` \x1b[1mAPI Base:\x1b[0m http://localhost:${apiPort}/v1`);
}
function runWithoutRecovery(serverJs, env, memoryLimit, dashboardPort, apiPort, noOpen) {
const server = spawn("node", [`--max-old-space-size=${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);
}
});
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);
}
}, 15000);
}
async function runWithSupervisor(
serverJs,
env,
memoryLimit,
dashboardPort,
apiPort,
noOpen,
showLog,
maxRestarts,
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, 20000).then(async (up) => {
if (up) {
if (useTray) await maybeStartTray(dashboardPort, apiPort, supervisor);
onReady(dashboardPort, apiPort, noOpen);
}
});
}
}
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 = `http://localhost:${port}`;
const tray = 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 {
// tray is optional — do not fail the server
}
}
async function onReady(dashboardPort, apiPort, noOpen) {
const dashboardUrl = `http://localhost:${dashboardPort}`;
const apiUrl = `http://localhost:${apiPort}`;
console.log(`
\x1b[32m✔ OmniRoute is running!\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) {
try {
const open = await import("open");
await open.default(dashboardUrl);
} catch {
// open is optional — skip if unavailable
}
}
}

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());
});
}

View File

@@ -1,4 +1,5 @@
import { parseArgs, getStringFlag, hasFlag } from "../args.mjs";
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";
@@ -9,18 +10,21 @@ import {
getProviderDisplayName,
resolveProviderChoice,
} from "../provider-catalog.mjs";
import { t } from "../i18n.mjs";
function wantsProviderSetup(flags) {
return (
hasFlag(flags, "add-provider") ||
Boolean(getStringFlag(flags, "provider", "OMNIROUTE_PROVIDER")) ||
Boolean(getStringFlag(flags, "api-key", "OMNIROUTE_API_KEY"))
);
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;
}
async function resolvePassword(flags, prompt, nonInteractive) {
const flagPassword = getStringFlag(flags, "password", "OMNIROUTE_SETUP_PASSWORD");
if (flagPassword) return flagPassword;
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");
@@ -34,8 +38,8 @@ async function resolvePassword(flags, prompt, nonInteractive) {
return password;
}
async function setupPassword(db, flags, prompt, nonInteractive) {
const password = await resolvePassword(flags, prompt, nonInteractive);
async function setupPassword(db, opts, prompt, nonInteractive) {
const password = await resolvePassword(opts, prompt, nonInteractive);
if (!password) {
const settings = getSettings(db);
if (!settings.password) {
@@ -60,12 +64,12 @@ async function setupPassword(db, flags, prompt, nonInteractive) {
return true;
}
async function resolveProviderInput(flags, prompt, nonInteractive) {
let provider = getStringFlag(flags, "provider", "OMNIROUTE_PROVIDER");
let apiKey = getStringFlag(flags, "api-key", "OMNIROUTE_API_KEY");
let name = getStringFlag(flags, "provider-name", "OMNIROUTE_PROVIDER_NAME");
const defaultModel = getStringFlag(flags, "default-model", "OMNIROUTE_DEFAULT_MODEL");
const baseUrl = getStringFlag(flags, "provider-base-url", "OMNIROUTE_PROVIDER_BASE_URL");
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:");
@@ -95,19 +99,19 @@ async function resolveProviderInput(flags, prompt, nonInteractive) {
};
}
async function setupProvider(db, flags, prompt, nonInteractive) {
if (!wantsProviderSetup(flags) && nonInteractive) return null;
async function setupProvider(db, opts, prompt, nonInteractive) {
if (!wantsProviderSetup(opts) && nonInteractive) return null;
if (!wantsProviderSetup(flags)) {
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(flags, prompt, nonInteractive);
const input = await resolveProviderInput(opts, prompt, nonInteractive);
const connection = upsertApiKeyProviderConnection(db, input);
printSuccess(`Provider configured: ${connection.name}`);
if (hasFlag(flags, "test-provider")) {
if (opts.testProvider) {
printInfo(`Testing provider connection: ${connection.provider}`);
const result = await testProviderApiKey({
provider: input.provider,
@@ -127,44 +131,45 @@ async function setupProvider(db, flags, prompt, nonInteractive) {
return connection;
}
function printSetupHelp() {
console.log(`
Usage:
omniroute setup
omniroute setup --password <password>
omniroute setup --add-provider --provider openai --api-key <key>
omniroute setup --non-interactive
Options:
--password <value> Set admin password
--add-provider Add an API-key provider connection
--provider <id> Provider id, for example openai or anthropic
--provider-name <name> Display name for the connection
--api-key <value> Provider API key
--default-model <model> Optional default model
--provider-base-url <url> Optional OpenAI-compatible base URL override
--test-provider Test the provider after saving it
--non-interactive Read all inputs from flags/env and do not prompt
Environment:
OMNIROUTE_SETUP_PASSWORD
OMNIROUTE_PROVIDER
OMNIROUTE_PROVIDER_NAME
OMNIROUTE_PROVIDER_BASE_URL
OMNIROUTE_API_KEY
OMNIROUTE_DEFAULT_MODEL
DATA_DIR
`);
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);
});
}
export async function runSetupCommand(argv) {
const { flags } = parseArgs(argv);
if (hasFlag(flags, "help") || hasFlag(flags, "h")) {
printSetupHelp();
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 = hasFlag(flags, "non-interactive");
const nonInteractive = opts.nonInteractive ?? false;
const prompt = createPrompt();
try {
@@ -173,8 +178,8 @@ export async function runSetupCommand(argv) {
printInfo(`Database: ${dbPath}`);
const before = getSettings(db);
const passwordChanged = await setupPassword(db, flags, prompt, nonInteractive);
const providerConnection = await setupProvider(db, flags, prompt, nonInteractive);
const passwordChanged = await setupPassword(db, opts, prompt, nonInteractive);
const providerConnection = await setupProvider(db, opts, prompt, nonInteractive);
updateSettings(db, { setupComplete: true });
const after = getSettings(db);

View File

@@ -0,0 +1,125 @@
import { readFileSync } from "node:fs";
import { apiFetch } from "../api.mjs";
import { emit } from "../output.mjs";
import { t } from "../i18n.mjs";
const simulateSchema = [
{ key: "order", header: "#", width: 4 },
{ key: "provider", header: "Provider", width: 20 },
{ key: "model", header: "Model", width: 35 },
{ key: "probability", header: "Probability", formatter: (v) => `${Math.round(v * 100)}%` },
{ key: "estimatedCost", header: "Est. Cost", formatter: (v) => (v ? `$${v.toFixed(4)}` : "-") },
{ key: "healthStatus", header: "Breaker", width: 10 },
{ key: "quotaAvailable", header: "Quota %", formatter: (v) => `${v}%` },
];
export function registerSimulate(program) {
program
.command("simulate [prompt]")
.description(t("simulate.description"))
.option("--file <path>", t("simulate.file"))
.option("-m, --model <id>", t("simulate.model"), "auto")
.option("--combo <name>", t("simulate.combo"))
.option("--reasoning-effort <level>", t("simulate.reasoning"))
.option("--thinking-budget <n>", t("simulate.thinking"), parseInt)
.option("--explain", t("simulate.explain"))
.action(runSimulateCommand);
}
export async function runSimulateCommand(promptArg, opts, cmd) {
const globalOpts = cmd.optsWithGlobals();
let promptTokenEstimate = 100;
if (opts.file) {
try {
const raw = readFileSync(opts.file, "utf8");
const parsed = JSON.parse(raw);
const text = JSON.stringify(parsed);
promptTokenEstimate = Math.ceil(text.length / 4);
} catch {
promptTokenEstimate = 100;
}
} else if (promptArg) {
promptTokenEstimate = Math.ceil(promptArg.length / 4);
}
const [combosRes, healthRes, quotaRes] = await Promise.allSettled([
apiFetch("/api/combos", { timeout: globalOpts.timeout }).then((r) => r.json()),
apiFetch("/api/monitoring/health", { timeout: globalOpts.timeout }).then((r) => r.json()),
apiFetch("/api/usage/quota", { timeout: globalOpts.timeout }).then((r) => r.json()),
]);
if (combosRes.status === "rejected") {
process.stderr.write(t("common.serverOffline") + "\n");
process.exit(3);
}
const combos = normalizeCombos(combosRes.value);
const health = healthRes.status === "fulfilled" ? healthRes.value : {};
const quota = quotaRes.status === "fulfilled" ? quotaRes.value : {};
const targetCombo = opts.combo
? combos.find((c) => c.id === opts.combo || c.name === opts.combo)
: combos.find((c) => c.enabled !== false);
if (!targetCombo) {
process.stderr.write(t("simulate.noCombo") + "\n");
process.exit(1);
}
const models = getComboModels(targetCombo, opts.model);
const breakers = toArray(health.circuitBreakers ?? health.breakers);
const providers = toArray(quota.providers ?? quota.data);
const simulatedPath = models.map((m, idx) => {
const cb = breakers.find((b) => String(b.provider) === m.provider);
const q = providers.find((p) => p.provider === m.provider);
const inputCost = m.inputCostPer1M ?? 0;
const estimatedCost = Math.round((promptTokenEstimate / 1_000_000) * inputCost * 10000) / 10000;
return {
order: idx + 1,
provider: m.provider,
model: m.model || opts.model,
probability: idx === 0 ? 0.85 : 0.15 / Math.max(models.length - 1, 1),
estimatedCost,
healthStatus: String(cb?.state ?? "CLOSED"),
quotaAvailable: q?.percentRemaining ?? 100,
};
});
emit(simulatedPath, globalOpts, simulateSchema);
if (opts.explain && !globalOpts.quiet) {
const primary = simulatedPath[0];
const fallbacks = simulatedPath.slice(1).map((s) => s.provider);
process.stderr.write(`\nPrimary: ${primary?.provider} / ${primary?.model}\n`);
if (fallbacks.length > 0) {
process.stderr.write(`Fallbacks: ${fallbacks.join(" → ")}\n`);
}
const costs = simulatedPath.map((s) => s.estimatedCost);
process.stderr.write(
`Est. cost range: $${Math.min(...costs).toFixed(4)} $${Math.max(...costs).toFixed(4)}\n`
);
}
}
function normalizeCombos(raw) {
if (Array.isArray(raw)) return raw;
if (raw && Array.isArray(raw.combos)) return raw.combos;
if (raw && Array.isArray(raw.data)) return raw.data;
return [];
}
function getComboModels(combo, modelFallback) {
const steps = combo.steps ?? combo.models ?? combo.targets ?? [];
return steps.map((step) => ({
provider: step.provider ?? step.providerId ?? "",
model: step.model ?? step.modelId ?? modelFallback ?? "auto",
inputCostPer1M: step.inputCostPer1M ?? step.costPer1MInput ?? 0,
}));
}
function toArray(val) {
if (Array.isArray(val)) return val;
return [];
}

373
bin/cli/commands/skills.mjs Normal file
View File

@@ -0,0 +1,373 @@
import { readFileSync } from "node:fs";
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);
}
}
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"));
});
});
}
const skillSchema = [
{ key: "id", header: "ID", width: 22 },
{ key: "name", header: "Name", width: 28 },
{ key: "type", header: "Type", width: 12 },
{ key: "version", header: "Ver", width: 8 },
{ key: "enabled", header: "Enabled", formatter: (v) => (v ? "✓" : "✗") },
{ key: "lastRun", header: "Last Run", formatter: fmtTs },
];
const executionSchema = [
{ key: "id", header: "Exec ID", width: 22 },
{ key: "skillId", header: "Skill", width: 22 },
{ key: "status", header: "Status", width: 12 },
{ key: "startedAt", header: "Started", formatter: fmtTs },
{ key: "duration", header: "Duration", formatter: (v) => (v != null ? `${v}ms` : "-") },
{ key: "error", header: "Error", formatter: truncate },
];
const marketplaceSchema = [
{ key: "id", header: "Package ID", width: 22 },
{ key: "name", header: "Name", width: 28 },
{ key: "category", header: "Category", width: 14 },
{ key: "version", header: "Latest", width: 10 },
{ key: "downloads", header: "DLs", formatter: (v) => (v != null ? v.toLocaleString() : "0") },
{ key: "rating", header: "★", formatter: (v) => (v ? "★".repeat(Math.round(v)) : "-") },
{ key: "author", header: "Author", width: 18 },
];
export async function runSkillsList(opts, cmd) {
const globalOpts = cmd.optsWithGlobals();
const params = new URLSearchParams();
if (opts.type) params.set("type", opts.type);
if (opts.enabled) params.set("enabled", "true");
if (opts.disabled) params.set("enabled", "false");
if (opts.apiKey) params.set("apiKey", opts.apiKey);
const res = await apiFetch(`/api/skills?${params}`);
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
const data = await res.json();
emit(data.items ?? data, globalOpts, skillSchema);
}
export async function runSkillsGet(id, opts, cmd) {
const globalOpts = cmd.optsWithGlobals();
const res = await apiFetch(`/api/skills/${id}`);
if (!res.ok) {
process.stderr.write(`Not found: ${id}\n`);
process.exit(1);
}
const data = await res.json();
emit(data, globalOpts, skillSchema);
}
export async function runSkillsInstall(opts, cmd) {
const globalOpts = cmd.optsWithGlobals();
let body = {};
if (opts.fromFile) {
body = JSON.parse(readFileSync(opts.fromFile, "utf8"));
} else if (opts.fromUrl) {
body = { url: opts.fromUrl };
} else {
process.stderr.write("--from-file or --from-url required\n");
process.exit(2);
}
if (opts.type) body.type = opts.type;
if (opts.enable) body.enabled = true;
const res = await apiFetch("/api/skills/install", { method: "POST", body });
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
const data = await res.json();
emit(data, globalOpts, skillSchema);
}
export async function runSkillsEnable(id, opts, cmd) {
const res = await apiFetch("/api/mcp/tools/call", {
method: "POST",
body: { name: "omniroute_skills_enable", arguments: { skillId: id, enabled: true } },
});
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
process.stdout.write(`Enabled: ${id}\n`);
}
export async function runSkillsDisable(id, opts, cmd) {
if (!opts.yes) {
const ok = await confirm(`Disable ${id}?`);
if (!ok) return;
}
const res = await apiFetch("/api/mcp/tools/call", {
method: "POST",
body: { name: "omniroute_skills_enable", arguments: { skillId: id, enabled: false } },
});
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
process.stdout.write(`Disabled: ${id}\n`);
}
export async function runSkillsDelete(id, opts, cmd) {
if (!opts.yes) {
const ok = await confirm(`Delete skill ${id}?`);
if (!ok) return;
}
const res = await apiFetch(`/api/skills/${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 runSkillsExecute(id, opts, cmd) {
const globalOpts = cmd.optsWithGlobals();
const input = opts.input
? JSON.parse(opts.input)
: opts.inputFile
? JSON.parse(readFileSync(opts.inputFile, "utf8"))
: {};
const res = await apiFetch("/api/mcp/tools/call", {
method: "POST",
body: { name: "omniroute_skills_execute", arguments: { skillId: id, input } },
timeout: opts.timeout ?? 30000,
});
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
const data = await res.json();
emit(data, globalOpts);
}
export async function runSkillsExecutions(opts, cmd) {
const globalOpts = cmd.optsWithGlobals();
const params = new URLSearchParams({ limit: String(opts.limit ?? 50) });
if (opts.skill) params.set("skillId", opts.skill);
if (opts.status) params.set("status", opts.status);
const res = await apiFetch(`/api/skills/executions?${params}`);
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
const data = await res.json();
emit(data.items ?? data, globalOpts, executionSchema);
}
export async function runSkillsshList(opts, cmd) {
const globalOpts = cmd.optsWithGlobals();
const res = await apiFetch("/api/skills/skillssh");
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
const data = await res.json();
emit(data.items ?? data, globalOpts, skillSchema);
}
export async function runSkillsshInstall(url, opts, cmd) {
const res = await apiFetch("/api/skills/skillssh/install", {
method: "POST",
body: { url },
});
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
const data = await res.json();
process.stdout.write(`Installed: ${data.skillId ?? url}\n`);
}
export async function runMarketplaceSearch(query, opts, cmd) {
const globalOpts = cmd.optsWithGlobals();
const params = new URLSearchParams({ limit: String(opts.limit ?? 30) });
if (query) params.set("q", query);
if (opts.category) params.set("category", opts.category);
if (opts.tag) params.set("tag", opts.tag);
if (opts.sort) params.set("sort", opts.sort);
const res = await apiFetch(`/api/skills/marketplace?${params}`);
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
const data = await res.json();
emit(data.items ?? data, globalOpts, marketplaceSchema);
}
export async function runMarketplaceInfo(packageId, opts, cmd) {
const globalOpts = cmd.optsWithGlobals();
const res = await apiFetch(`/api/skills/marketplace?id=${packageId}`);
if (!res.ok) {
process.stderr.write(`Not found: ${packageId}\n`);
process.exit(1);
}
const data = await res.json();
emit(data, globalOpts, marketplaceSchema);
if (globalOpts.output !== "json" && globalOpts.output !== "jsonl") {
process.stdout.write("\nReadme:\n");
process.stdout.write(data.readme ?? "(no readme)");
process.stdout.write("\n");
}
}
export async function runMarketplaceInstall(packageId, opts, cmd) {
if (!opts.yes) {
const infoRes = await apiFetch(`/api/skills/marketplace?id=${packageId}`);
if (infoRes.ok) {
const info = await infoRes.json();
process.stdout.write(`Installing: ${info.name ?? packageId} v${info.version ?? "?"}\n`);
process.stdout.write(`Permissions: ${(info.permissions ?? []).join(", ") || "(none)"}\n`);
}
const ok = await confirm("Continue?");
if (!ok) process.exit(0);
}
const res = await apiFetch("/api/skills/marketplace/install", {
method: "POST",
body: { packageId, version: opts.version ?? "latest", enable: !!opts.enable },
});
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
const data = await res.json();
process.stdout.write(`Installed: ${data.skillId ?? packageId}\n`);
}
export async function runMarketplaceCategories(opts, cmd) {
const globalOpts = cmd.optsWithGlobals();
const res = await apiFetch("/api/skills/marketplace?facets=categories");
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
const data = await res.json();
emit(data.categories ?? data, globalOpts);
}
export async function runMarketplaceFeatured(opts, cmd) {
const globalOpts = cmd.optsWithGlobals();
const res = await apiFetch("/api/skills/marketplace?featured=true");
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
const data = await res.json();
emit(data.items ?? data, globalOpts, marketplaceSchema);
}
function registerSkillsMarketplace(skills) {
const mp = skills.command("marketplace").description(t("skills.marketplace.description"));
mp.command("search [query]")
.description(t("skills.mp.search.description"))
.option("--category <c>", t("skills.mp.search.category"))
.option("--tag <t>", t("skills.mp.search.tag"))
.option("--limit <n>", t("skills.mp.search.limit"), parseInt, 30)
.option("--sort <s>", t("skills.mp.search.sort"))
.action(runMarketplaceSearch);
mp.command("info <packageId>")
.description(t("skills.mp.info.description"))
.action(runMarketplaceInfo);
mp.command("install <packageId>")
.description(t("skills.mp.install.description"))
.option("--version <v>", t("skills.mp.install.version"), "latest")
.option("--enable", t("skills.mp.install.enable"))
.option("--yes", t("skills.mp.install.yes"))
.action(runMarketplaceInstall);
mp.command("categories")
.description(t("skills.mp.categories.description"))
.action(runMarketplaceCategories);
mp.command("featured")
.description(t("skills.mp.featured.description"))
.action(runMarketplaceFeatured);
}
export function registerSkills(program) {
const skills = program.command("skills").description(t("skills.description"));
skills
.command("list")
.description(t("skills.list.description"))
.option("--type <type>", t("skills.list.type"))
.option("--enabled", t("skills.list.enabled"))
.option("--disabled", t("skills.list.disabled"))
.option("--api-key <key>", t("skills.list.api_key"))
.action(runSkillsList);
skills.command("get <id>").description(t("skills.get.description")).action(runSkillsGet);
skills
.command("install")
.description(t("skills.install.description"))
.option("--from-file <path>", t("skills.install.from_file"))
.option("--from-url <url>", t("skills.install.from_url"))
.option("--type <type>", t("skills.install.type"))
.option("--enable", t("skills.install.enable"))
.action(runSkillsInstall);
skills.command("enable <id>").description(t("skills.enable.description")).action(runSkillsEnable);
skills
.command("disable <id>")
.description(t("skills.disable.description"))
.option("--yes", t("skills.disable.yes"))
.action(runSkillsDisable);
skills
.command("delete <id>")
.description(t("skills.delete.description"))
.option("--yes", t("skills.delete.yes"))
.action(runSkillsDelete);
skills
.command("execute <id>")
.description(t("skills.execute.description"))
.option("--input <json>", t("skills.execute.input"))
.option("--input-file <path>", t("skills.execute.input_file"))
.option("--timeout <ms>", t("skills.execute.timeout"), parseInt, 30000)
.action(runSkillsExecute);
skills
.command("executions")
.description(t("skills.executions.description"))
.option("--skill <id>", t("skills.executions.skill"))
.option("--limit <n>", t("skills.executions.limit"), parseInt, 50)
.option("--status <s>", t("skills.executions.status"))
.action(runSkillsExecutions);
const skillssh = skills.command("skillssh").description(t("skills.skillssh.description"));
skillssh.command("list").action(runSkillsshList);
skillssh.command("install <url>").action(runSkillsshInstall);
registerSkillsMarketplace(skills);
}

View File

@@ -1,6 +1,6 @@
import { parseArgs, getStringFlag, hasFlag } from "../args.mjs";
import { printHeading, printInfo, printSuccess } from "../io.mjs";
import { printHeading } from "../io.mjs";
import { resolveDataDir, resolveStoragePath } from "../data-dir.mjs";
import { t } from "../i18n.mjs";
import path from "node:path";
import fs from "node:fs";
import os from "node:os";
@@ -20,10 +20,21 @@ function formatBytes(bytes) {
return `${(bytes / 1048576).toFixed(1)} MB`;
}
export async function runStatusCommand(argv) {
const { flags } = parseArgs(argv);
const isJson = hasFlag(flags, "json");
const isVerbose = hasFlag(flags, "verbose");
export function registerStatus(program) {
program
.command("status")
.description("Show OmniRoute status dashboard")
.option("-v, --verbose", "Show additional details")
.action(async (opts, cmd) => {
const globalOpts = cmd.optsWithGlobals();
const exitCode = await runStatusCommand({ ...opts, output: globalOpts.output });
if (exitCode !== 0) process.exit(exitCode);
});
}
export async function runStatusCommand(opts = {}) {
const isJson = opts.output === "json";
const isVerbose = opts.verbose;
const dataDir = resolveDataDir();
const dbPath = resolveStoragePath(dataDir);
@@ -72,10 +83,10 @@ export async function runStatusCommand(argv) {
if (status.tools) {
console.log("\n CLI Tools:");
for (const t of status.tools) {
const icon = t.configured ? "✓" : t.installed ? "~" : "✗";
for (const tool of status.tools) {
const icon = tool.configured ? "✓" : tool.installed ? "~" : "✗";
console.log(
` ${icon} ${t.name.padEnd(14)} ${t.installed ? "installed" : "not installed"}${t.version ? ` (${t.version})` : ""}`
` ${icon} ${tool.name.padEnd(14)} ${tool.installed ? "installed" : "not installed"}${tool.version ? ` (${tool.version})` : ""}`
);
}
}

96
bin/cli/commands/stop.mjs Normal file
View File

@@ -0,0 +1,96 @@
import { execFile } from "node:child_process";
import { promisify } from "node:util";
import {
readPidFile,
isPidRunning,
cleanupPidFile,
killAllSubprocesses,
sleep,
} from "../utils/pid.mjs";
import { t } from "../i18n.mjs";
const execFileAsync = promisify(execFile);
export function registerStop(program) {
program
.command("stop")
.description(t("stop.description"))
.action(async (opts) => {
const exitCode = await runStopCommand(opts);
if (exitCode !== 0) process.exit(exitCode);
});
}
export async function runStopCommand(opts = {}) {
const pid = readPidFile("server");
if (pid && isPidRunning(pid)) {
console.log(t("stop.stopping", { pid }));
try {
process.kill(pid, "SIGTERM");
let waited = 0;
while (waited < 5000 && isPidRunning(pid)) {
await sleep(100);
waited += 100;
}
if (isPidRunning(pid)) {
process.kill(pid, "SIGKILL");
await sleep(500);
}
killAllSubprocesses();
cleanupPidFile("server");
console.log(t("stop.stopped"));
return 0;
} catch (err) {
console.error(
t("common.error", { message: err instanceof Error ? err.message : String(err) })
);
return 1;
}
}
const port = opts.port ? parseInt(String(opts.port), 10) : 20128;
if (pid === null) {
console.log(t("stop.portFallback"));
await killByPort(port);
killAllSubprocesses();
cleanupPidFile("server");
console.log(t("stop.stopped"));
return 0;
}
console.log(t("stop.notRunning"));
return 0;
}
async function killByPort(port) {
if (process.platform === "win32") return;
try {
const { stdout } = await execFileAsync("lsof", ["-ti", `:${port}`]);
const pids = stdout
.trim()
.split("\n")
.map((p) => parseInt(p, 10))
.filter((p) => Number.isFinite(p) && p > 0);
for (const p of pids) {
try {
process.kill(p, "SIGTERM");
} catch {}
}
if (pids.length > 0) {
await sleep(1000);
for (const p of pids) {
try {
if (isPidRunning(p)) process.kill(p, "SIGKILL");
} catch {}
}
}
} catch {
// lsof not available or no process on port
}
}

166
bin/cli/commands/stream.mjs Normal file
View File

@@ -0,0 +1,166 @@
import { appendFileSync, readFileSync } from "node:fs";
import { apiFetch } from "../api.mjs";
import { t } from "../i18n.mjs";
export function registerStream(program) {
program
.command("stream [prompt]")
.description(t("stream.description"))
.option("--file <path>", t("stream.file"))
.option("--stdin", t("stream.stdin"))
.option("-m, --model <id>", t("stream.model"), "auto")
.option("-s, --system <prompt>", t("stream.system"))
.option("--combo <name>", t("stream.combo"))
.option("--max-tokens <n>", t("stream.max_tokens"), parseInt)
.option("--responses-api", t("stream.responses_api"))
.option("--raw", t("stream.raw"))
.option("--debug", t("stream.debug"))
.option("--save <path>", t("stream.save"))
.action(runStreamCommand);
}
export async function runStreamCommand(promptArg, opts, cmd) {
const globalOpts = cmd.optsWithGlobals();
const prompt = await resolvePrompt(promptArg, opts);
if (!prompt) {
process.stderr.write(t("stream.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,
stream: true,
...(opts.maxTokens && { max_tokens: opts.maxTokens }),
...(opts.combo && { combo: opts.combo }),
};
const endpoint = opts.responsesApi ? "/v1/responses" : "/v1/chat/completions";
const t0 = Date.now();
const res = await apiFetch(endpoint, {
method: "POST",
body,
acceptNotOk: true,
timeout: globalOpts.timeout,
});
if (!res.ok) {
const errText = await res.text().catch(() => "");
process.stderr.write(`[error] HTTP ${res.status}: ${errText.slice(0, 200)}\n`);
process.exit(1);
}
const reader = res.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
let firstTokenAt = null;
let totalContent = "";
const allChunks = [];
const sigintHandler = () => {
reader.cancel();
process.stderr.write("\n[cancelled]\n");
process.exit(0);
};
process.on("SIGINT", sigintHandler);
try {
while (true) {
const { value, done } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
let idx;
while ((idx = buffer.indexOf("\n")) !== -1) {
const line = buffer.slice(0, idx);
buffer = buffer.slice(idx + 1);
if (opts.raw) {
process.stdout.write(line + "\n");
continue;
}
if (!line.startsWith("data:")) continue;
const payload = line.slice(5).trim();
if (payload === "[DONE]") continue;
let event;
try {
event = JSON.parse(payload);
} catch {
continue;
}
if (opts.save) appendFileSync(opts.save, JSON.stringify(event) + "\n");
if (globalOpts.output === "json") allChunks.push(event);
if (opts.debug) {
const sinceStart = Date.now() - t0;
process.stderr.write(`[+${sinceStart}ms] ${JSON.stringify(event).slice(0, 100)}...\n`);
}
const delta = opts.responsesApi
? (event.delta ?? event.output_text?.delta)
: event.choices?.[0]?.delta?.content;
if (delta) {
if (firstTokenAt === null) firstTokenAt = Date.now() - t0;
totalContent += delta;
if (globalOpts.output !== "json") process.stdout.write(delta);
}
}
}
} finally {
process.off("SIGINT", sigintHandler);
}
const totalMs = Date.now() - t0;
const tokens = Math.ceil(totalContent.length / 4);
if (globalOpts.output === "json") {
process.stdout.write(
JSON.stringify(
{
chunks: allChunks,
content: totalContent,
metrics: {
ttftMs: firstTokenAt,
totalMs,
approxTokens: tokens,
tokensPerSec: Math.round(tokens / (totalMs / 1000)),
},
},
null,
2
) + "\n"
);
} else {
if (!globalOpts.quiet) {
process.stderr.write(
`\n\n[TTFT: ${firstTokenAt}ms · Total: ${totalMs}ms · ~${tokens} tok · ~${Math.round(tokens / (totalMs / 1000))} tok/s]\n`
);
}
process.stdout.write("\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()));
});
}

230
bin/cli/commands/sync.mjs Normal file
View File

@@ -0,0 +1,230 @@
import { readFileSync, writeFileSync } from "node:fs";
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();
}
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 BUNDLE_PARTS = ["settings", "combos", "keys", "providers", "policies", "skills", "memory"];
const syncTokenSchema = [
{ key: "id", header: "Token ID", width: 16 },
{ key: "name", header: "Name", width: 24 },
{ key: "scope", header: "Scope", width: 22 },
{ key: "createdAt", header: "Created", formatter: fmtTs },
{ key: "expiresAt", header: "Expires", formatter: fmtTs },
{ key: "lastUsed", header: "Last Used", formatter: fmtTs },
];
export function registerSync(program) {
const sync = program.command("sync").description(t("sync.description"));
sync
.command("push")
.description(t("sync.push.description"))
.option("--target <t>", t("sync.push.target"), "cloud")
.option("--bundle <list>", t("sync.push.bundle"), (v) => v.split(","), BUNDLE_PARTS)
.option("--dry-run", t("sync.push.dryRun"))
.action(async (opts, cmd) => {
const res = await apiFetch("/api/sync/cloud", {
method: "POST",
body: { parts: opts.bundle, dryRun: !!opts.dryRun, target: opts.target },
});
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
emit(await res.json(), cmd.optsWithGlobals());
});
sync
.command("pull")
.description(t("sync.pull.description"))
.option("--source <s>", t("sync.pull.source"), "cloud")
.option("--merge", t("sync.pull.merge"))
.option("--replace", t("sync.pull.replace"))
.option("--dry-run", t("sync.pull.dryRun"))
.action(async (opts, cmd) => {
if (opts.merge && opts.replace) {
process.stderr.write("--merge and --replace are mutually exclusive\n");
process.exit(2);
}
const res = await apiFetch("/api/db-backups/exportAll", {
method: "POST",
body: {
source: opts.source,
strategy: opts.replace ? "replace" : "merge",
dryRun: !!opts.dryRun,
},
});
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
emit(await res.json(), cmd.optsWithGlobals());
});
sync
.command("diff")
.option("--source <s>", t("sync.diff.source"))
.option("--target <t>", t("sync.diff.target"))
.action(async (opts, cmd) => {
const src = opts.source ?? "local";
const tgt = opts.target ?? "cloud";
const res = await apiFetch(`/api/sync/cloud?op=diff&source=${src}&target=${tgt}`);
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
emit(await res.json(), cmd.optsWithGlobals());
});
sync
.command("bundle <outPath>")
.description(t("sync.bundle.description"))
.option("--include <list>", t("sync.bundle.include"), (v) => v.split(","), BUNDLE_PARTS)
.action(async (outPath, opts, cmd) => {
const globalOpts = cmd.optsWithGlobals();
const res = await fetch(
`${getBaseUrl(globalOpts)}/api/sync/bundle?parts=${opts.include.join(",")}`,
{ headers: authHeaders(globalOpts) }
);
if (!res.ok) {
process.stderr.write(`HTTP ${res.status}\n`);
process.exit(1);
}
const buf = Buffer.from(await res.arrayBuffer());
writeFileSync(outPath, buf);
process.stdout.write(`Saved ${buf.length} bytes to ${outPath}\n`);
});
sync
.command("import <bundlePath>")
.description(t("sync.import.description"))
.option("--dry-run", t("sync.import.dryRun"))
.action(async (bundlePath, opts, cmd) => {
const globalOpts = cmd.optsWithGlobals();
const body = readFileSync(bundlePath);
const res = await fetch(
`${getBaseUrl(globalOpts)}/api/db-backups/import?dryRun=${opts.dryRun ? "true" : "false"}`,
{
method: "POST",
headers: {
...authHeaders(globalOpts),
"Content-Type": "application/octet-stream",
},
body,
}
);
if (!res.ok) {
process.stderr.write(`HTTP ${res.status}\n`);
process.exit(1);
}
emit(await res.json(), globalOpts);
});
sync
.command("initialize")
.option("--from-cloud", t("sync.initialize.fromCloud"))
.action(async (opts, cmd) => {
const res = await apiFetch("/api/sync/initialize", {
method: "POST",
body: { fromCloud: !!opts.fromCloud },
});
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
emit(await res.json(), cmd.optsWithGlobals());
});
const tokens = sync.command("tokens").description(t("sync.tokens.description"));
tokens.command("list").action(async (opts, cmd) => {
const res = await apiFetch("/api/sync/tokens");
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
emit(await res.json(), cmd.optsWithGlobals(), syncTokenSchema);
});
tokens
.command("create")
.option("--name <n>", t("sync.tokens.create.name"))
.option("--scope <s>", t("sync.tokens.create.scope"))
.option("--ttl <duration>", t("sync.tokens.create.ttl"), "30d")
.action(async (opts, cmd) => {
const body = { name: opts.name, scope: opts.scope, ttl: opts.ttl };
const res = await apiFetch("/api/sync/tokens", { method: "POST", body });
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
emit(await res.json(), cmd.optsWithGlobals());
});
tokens
.command("revoke <id>")
.option("--yes", t("sync.tokens.revoke.yes"))
.action(async (id, opts, cmd) => {
if (!opts.yes) {
const ok = await confirm(`Revoke ${id}?`);
if (!ok) return;
}
const res = await apiFetch(`/api/sync/tokens/${id}`, { method: "DELETE" });
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
process.stdout.write("Revoked\n");
});
sync.command("status").action(async (opts, cmd) => {
const res = await apiFetch("/api/sync/cloud?op=status");
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
emit(await res.json(), cmd.optsWithGlobals());
});
sync
.command("resolve")
.description(t("sync.resolve.description"))
.action(async (opts, cmd) => {
const res = await apiFetch("/api/sync/cloud?op=conflicts");
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
const conflicts = await res.json();
for (const c of conflicts.items ?? []) {
const choice = await confirm(`Conflict on ${c.path} — keep local?`);
await apiFetch("/api/sync/cloud", {
method: "POST",
body: { op: "resolve", path: c.path, choice: choice ? "local" : "remote" },
});
}
});
}

116
bin/cli/commands/tags.mjs Normal file
View File

@@ -0,0 +1,116 @@
import { createInterface } from "node:readline";
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;
}
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 tagSchema = [
{ key: "id", header: "ID", width: 14 },
{ key: "name", header: "Name", width: 25 },
{ key: "color", header: "Color" },
{ key: "description", header: "Description", width: 40, formatter: (v) => truncate(v, 40) },
{ key: "resourceCount", header: "Resources" },
];
export function registerTags(program) {
const tags = program.command("tags").description(t("tags.description"));
tags.command("list").action(async (opts, cmd) => {
const res = await apiFetch("/api/tags");
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
const data = await res.json();
emit(data.items ?? data, cmd.optsWithGlobals(), tagSchema);
});
tags
.command("add <name>")
.option("--color <c>", t("tags.add.color"))
.option("--description <d>", t("tags.add.description"))
.action(async (name, opts, cmd) => {
const body = { name, color: opts.color, description: opts.description };
const res = await apiFetch("/api/tags", { method: "POST", body });
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
emit(await res.json(), cmd.optsWithGlobals());
});
tags
.command("remove <id>")
.option("--yes", t("tags.remove.yes"))
.action(async (id, opts, cmd) => {
if (!opts.yes) {
const ok = await confirm(`Delete tag ${id}?`);
if (!ok) return;
}
const res = await apiFetch(`/api/tags?id=${id}`, { method: "DELETE" });
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
process.stdout.write("Removed\n");
});
tags
.command("assign")
.requiredOption("--tag <name>", t("tags.assign.tag"))
.requiredOption("--to <resource>", t("tags.assign.to"))
.action(async (opts, cmd) => {
const [resourceType, resourceId] = opts.to.split(":");
const res = await apiFetch("/api/tags?op=assign", {
method: "POST",
body: { tag: opts.tag, resourceType, resourceId },
});
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
process.stdout.write(`Tag '${opts.tag}' → ${opts.to}\n`);
});
tags
.command("unassign")
.requiredOption("--tag <name>", t("tags.unassign.tag"))
.requiredOption("--from <resource>", t("tags.unassign.from"))
.action(async (opts, cmd) => {
const [resourceType, resourceId] = opts.from.split(":");
const res = await apiFetch("/api/tags?op=unassign", {
method: "POST",
body: { tag: opts.tag, resourceType, resourceId },
});
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
process.stdout.write(`Removed tag '${opts.tag}' from ${opts.from}\n`);
});
tags.command("resources <tagName>").action(async (tagName, opts, cmd) => {
const res = await apiFetch(`/api/tags?name=${encodeURIComponent(tagName)}&resources=true`);
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
const data = await res.json();
emit(data.resources ?? data, cmd.optsWithGlobals());
});
}

View File

@@ -0,0 +1,74 @@
import { writeFileSync } from "node:fs";
import { apiFetch } from "../api.mjs";
import { emit } from "../output.mjs";
import { t } from "../i18n.mjs";
function fmtMetric(v) {
if (v == null) return "-";
if (typeof v === "number") {
if (v > 1e9) return `${(v / 1e9).toFixed(2)}B`;
if (v > 1e6) return `${(v / 1e6).toFixed(2)}M`;
if (v > 1e3) return `${(v / 1e3).toFixed(2)}K`;
return Number.isInteger(v) ? v.toLocaleString() : v.toFixed(2);
}
return String(v);
}
function fmtDelta(v) {
if (v == null) return "-";
const arrow = v > 0 ? "↑" : v < 0 ? "↓" : "→";
const sign = v > 0 ? "+" : "";
return `${arrow} ${sign}${(v * 100).toFixed(1)}%`;
}
const telemetrySchema = [
{ key: "metric", header: "Metric", width: 36 },
{ key: "value", header: "Value", formatter: fmtMetric },
{ key: "delta", header: "Δ vs prev", formatter: fmtDelta },
{ key: "trend", header: "Trend" },
];
export function registerTelemetry(program) {
const tel = program.command("telemetry").description(t("telemetry.description"));
tel
.command("summary")
.description(t("telemetry.summary.description"))
.option("--period <p>", t("telemetry.summary.period"), "24h")
.option("--compare-to <p>", t("telemetry.summary.compareTo"))
.action(async (opts, cmd) => {
const params = new URLSearchParams({ period: opts.period });
if (opts.compareTo) params.set("compareTo", opts.compareTo);
const res = await apiFetch(`/api/telemetry/summary?${params}`);
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
const data = await res.json();
const rows = Object.entries(data.metrics ?? data).map(([metric, info]) => ({
metric,
value: info?.value ?? info,
delta: info?.delta,
trend: info?.trend,
}));
emit(rows, cmd.optsWithGlobals(), telemetrySchema);
});
tel
.command("export")
.description(t("telemetry.export.description"))
.option("--out <path>", t("telemetry.export.out"), "telemetry.jsonl")
.option("--period <p>", t("telemetry.export.period"), "7d")
.action(async (opts, cmd) => {
const res = await apiFetch(`/api/telemetry/summary?format=jsonl&period=${opts.period}`);
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
const data = await res.json();
const items = data.events ?? data.items ?? [];
const lines = items.map((e) => JSON.stringify(e)).join("\n");
writeFileSync(opts.out, lines);
process.stdout.write(`Exported ${items.length} events to ${opts.out}\n`);
});
}

View File

@@ -0,0 +1,242 @@
import { writeFileSync } from "node:fs";
import { apiFetch, isServerUp } from "../api.mjs";
import { t } from "../i18n.mjs";
export function registerTestProvider(program) {
program
.command("test [provider] [model]")
.description(t("test.description"))
.option("--all-providers", t("test.allProvidersOpt"))
.option("--json", t("common.jsonOpt"))
.option("--latency", t("test.latencyOpt"))
.option("--repeat <n>", t("test.repeatOpt"), parseInt)
.option("--compare <models>", t("test.compareOpt"))
.option("--save <path>", t("test.saveOpt"))
.action(async (provider, model, opts, cmd) => {
const globalOpts = cmd.optsWithGlobals();
const exitCode = await runTestProviderCommand(provider, model, {
...opts,
output: globalOpts.output,
});
if (exitCode !== 0) process.exit(exitCode);
});
}
export async function runTestProviderCommand(provider, model, opts = {}) {
const serverUp = await isServerUp();
if (!serverUp) {
console.error(t("test.noServer"));
return 1;
}
if (opts.allProviders) {
return _runAllProviders(opts);
}
if (opts.compare) {
return _runCompare(provider, opts);
}
const targetProvider = provider || "anthropic";
const targetModel = model || "claude-haiku-4-5-20251001";
const repeat = opts.repeat && opts.repeat > 0 ? opts.repeat : 1;
const results = [];
for (let i = 0; i < repeat; i++) {
const result = await _runSingleTest(targetProvider, targetModel);
results.push(result);
}
const aggregated = _aggregate(results, opts.latency);
if (opts.save) {
try {
writeFileSync(opts.save, JSON.stringify(aggregated, null, 2), "utf8");
console.log(t("test.saved", { path: opts.save }));
} catch (err) {
console.error(
t("common.error", { message: err instanceof Error ? err.message : String(err) })
);
}
}
if (opts.json || opts.output === "json") {
console.log(JSON.stringify(aggregated, null, 2));
return aggregated.success ? 0 : 1;
}
_printResult(aggregated, opts.latency);
return aggregated.success ? 0 : 1;
}
async function _runAllProviders(opts) {
const res = await apiFetch("/api/providers?limit=200", {
retry: false,
timeout: 5000,
acceptNotOk: true,
});
if (!res.ok) {
console.error(t("test.noServer"));
return 1;
}
const data = await res.json();
const connections = (data.providers ?? data.items ?? data).filter(
(c) => c.authType === "apikey" || c.testStatus !== "unavailable"
);
if (connections.length === 0) {
console.log(t("test.noProviders"));
return 0;
}
const providers = connections.map((c) => ({
provider: c.provider ?? c.id,
model: c.defaultModel ?? c.model,
}));
if (process.stdout.isTTY && !opts.json && opts.output !== "json") {
const { startProvidersTestTui } = await import("../tui/ProvidersTestAll.jsx");
const baseUrl = opts.baseUrl ?? "http://localhost:20128";
const apiKey = opts.apiKey ?? process.env.OMNIROUTE_API_KEY;
await startProvidersTestTui({ providers, baseUrl, apiKey });
return 0;
}
const results = await Promise.all(
providers.map(async ({ provider, model }) => {
const r = await _runSingleTest(provider, model);
return { provider, model, ...r };
})
);
if (opts.json || opts.output === "json") {
console.log(JSON.stringify(results, null, 2));
} else {
for (const r of results) {
const mark = r.success ? "\x1b[32m✔\x1b[0m" : "\x1b[31m✖\x1b[0m";
console.log(`${mark} ${r.provider}/${r.model ?? "-"}`);
}
}
const failed = results.filter((r) => !r.success).length;
return failed > 0 ? 1 : 0;
}
async function _runCompare(provider, opts) {
const targetProvider = provider || "anthropic";
const models = opts.compare
.split(",")
.map((m) => m.trim())
.filter(Boolean);
if (models.length < 2) {
console.error(t("test.compareMinTwo"));
return 1;
}
const repeat = opts.repeat && opts.repeat > 0 ? opts.repeat : 1;
const rows = [];
for (const model of models) {
const results = [];
for (let i = 0; i < repeat; i++) {
const result = await _runSingleTest(targetProvider, model);
results.push(result);
}
rows.push({ model, ..._aggregate(results, true) });
}
if (opts.save) {
try {
writeFileSync(opts.save, JSON.stringify(rows, null, 2), "utf8");
console.log(t("test.saved", { path: opts.save }));
} catch (err) {
console.error(
t("common.error", { message: err instanceof Error ? err.message : String(err) })
);
}
}
if (opts.json || opts.output === "json") {
console.log(JSON.stringify(rows, null, 2));
return rows.every((r) => r.success) ? 0 : 1;
}
console.log(`\n\x1b[1m\x1b[36m${t("test.compareTitle")}\x1b[0m\n`);
const colW = Math.max(...models.map((m) => m.length), 20);
console.log(
` ${"Model".padEnd(colW)} ${"Status".padEnd(8)} ${"Avg ms".padEnd(8)} ${"Min ms".padEnd(8)} Max ms`
);
console.log(` ${"─".repeat(colW)} ──────── ──────── ──────── ──────`);
for (const row of rows) {
const statusMark = row.success ? "\x1b[32m✔\x1b[0m" : "\x1b[31m✖\x1b[0m";
const avg = row.latency?.avgMs != null ? String(row.latency.avgMs) : "N/A";
const min = row.latency?.minMs != null ? String(row.latency.minMs) : "N/A";
const max = row.latency?.maxMs != null ? String(row.latency.maxMs) : "N/A";
console.log(
` ${row.model.padEnd(colW)} ${statusMark} ${avg.padEnd(8)} ${min.padEnd(8)} ${max}`
);
}
console.log();
return rows.every((r) => r.success) ? 0 : 1;
}
async function _runSingleTest(provider, model) {
const startMs = Date.now();
try {
const res = await apiFetch("/api/v1/providers/test", {
method: "POST",
body: { provider, model },
retry: false,
timeout: 30000,
acceptNotOk: true,
});
const durationMs = Date.now() - startMs;
const data = res.ok ? await res.json() : { success: false, error: `HTTP ${res.status}` };
return { ...data, durationMs };
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
return {
success: false,
error: msg.slice(0, 100),
durationMs: Date.now() - startMs,
};
}
}
function _aggregate(results, includeLatency) {
const allOk = results.every((r) => r.success);
const durations = results.map((r) => r.durationMs).filter((d) => d != null);
const base = {
success: allOk,
runs: results.length,
passed: results.filter((r) => r.success).length,
failed: results.filter((r) => !r.success).length,
response: results.find((r) => r.response)?.response,
error: results.find((r) => r.error)?.error,
};
if (includeLatency && durations.length > 0) {
const avgMs = Math.round(durations.reduce((a, b) => a + b, 0) / durations.length);
const minMs = Math.min(...durations);
const maxMs = Math.max(...durations);
return { ...base, latency: { avgMs, minMs, maxMs } };
}
return base;
}
function _printResult(result, showLatency) {
if (result.success) {
const runs = result.runs > 1 ? ` (${result.passed}/${result.runs} passed)` : "";
console.log(`\x1b[32m✔ ${t("test.passed")}\x1b[0m${runs}`);
if (result.response) console.log(`\x1b[2m Response: ${result.response}\x1b[0m`);
} else {
const runs = result.runs > 1 ? ` (${result.passed}/${result.runs} passed)` : "";
console.error(
`\x1b[31m✖ ${t("test.failed", { error: result.error || "Unknown error" })}\x1b[0m${runs}`
);
}
if (showLatency && result.latency) {
console.log(
`\x1b[2m Latency — avg: ${result.latency.avgMs}ms min: ${result.latency.minMs}ms max: ${result.latency.maxMs}ms\x1b[0m`
);
}
}

View File

@@ -0,0 +1,131 @@
import { readFileSync, writeFileSync } from "node:fs";
import { apiFetch, getBaseUrl } from "../api.mjs";
import { emit } from "../output.mjs";
import { t } from "../i18n.mjs";
const FORMATS = ["openai", "anthropic", "gemini", "cohere"];
function authHeaders(opts) {
const h = { accept: "application/json" };
if (opts.apiKey) h["Authorization"] = `Bearer ${opts.apiKey}`;
return h;
}
export function registerTranslator(program) {
const tr = program
.command("translator")
.alias("translate")
.description(t("translator.description"));
tr.command("detect")
.description(t("translator.detect.description"))
.requiredOption("--file <path>", t("translator.file"))
.action(async (opts, cmd) => {
const body = JSON.parse(readFileSync(opts.file, "utf8"));
const res = await apiFetch("/api/translator/detect", { method: "POST", body });
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
emit(await res.json(), cmd.optsWithGlobals());
});
tr.command("translate")
.description(t("translator.translate.description"))
.requiredOption("--from <f>", t("translator.from"))
.requiredOption("--to <f>", t("translator.to"))
.requiredOption("--file <path>", t("translator.file"))
.option("--out <path>", t("translator.out"))
.action(async (opts, cmd) => {
if (!FORMATS.includes(opts.from)) {
process.stderr.write(`Invalid --from: ${opts.from}. Valid: ${FORMATS.join(", ")}\n`);
process.exit(2);
}
if (!FORMATS.includes(opts.to)) {
process.stderr.write(`Invalid --to: ${opts.to}. Valid: ${FORMATS.join(", ")}\n`);
process.exit(2);
}
const body = JSON.parse(readFileSync(opts.file, "utf8"));
const res = await apiFetch("/api/translator/translate", {
method: "POST",
body: { from: opts.from, to: opts.to, payload: body },
});
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
const data = await res.json();
if (opts.out) {
writeFileSync(opts.out, JSON.stringify(data.translated ?? data, null, 2));
process.stdout.write(`Saved to ${opts.out}\n`);
} else {
emit(data.translated ?? data, cmd.optsWithGlobals());
}
});
tr.command("send")
.description(t("translator.send.description"))
.requiredOption("--from <f>", t("translator.from"))
.requiredOption("--to <f>", t("translator.to"))
.requiredOption("--file <path>", t("translator.file"))
.option("--model <m>", t("translator.model"))
.action(async (opts, cmd) => {
const body = JSON.parse(readFileSync(opts.file, "utf8"));
const res = await apiFetch("/api/translator/send", {
method: "POST",
body: { from: opts.from, to: opts.to, model: opts.model, payload: body },
});
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
emit(await res.json(), cmd.optsWithGlobals());
});
tr.command("stream")
.description(t("translator.stream.description"))
.requiredOption("--from <f>", t("translator.from"))
.requiredOption("--to <f>", t("translator.to"))
.requiredOption("--file <path>", t("translator.file"))
.action(async (opts, cmd) => {
const globalOpts = cmd.optsWithGlobals();
const body = JSON.parse(readFileSync(opts.file, "utf8"));
const res = await fetch(`${getBaseUrl(globalOpts)}/api/translator/transform-stream`, {
method: "POST",
headers: { ...authHeaders(globalOpts), "Content-Type": "application/json" },
body: JSON.stringify({ from: opts.from, to: opts.to, payload: body }),
});
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");
}
}
}
});
tr.command("history")
.option("--limit <n>", t("translator.history.limit"), parseInt, 50)
.action(async (opts, cmd) => {
const res = await apiFetch(`/api/translator/history?limit=${opts.limit}`);
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
const data = await res.json();
emit(data.items ?? data, cmd.optsWithGlobals());
});
}

36
bin/cli/commands/tray.mjs Normal file
View File

@@ -0,0 +1,36 @@
import { t } from "../i18n.mjs";
export function registerTray(program) {
const cmd = program
.command("tray")
.description(t("tray.description") || "Control the system tray icon");
cmd
.command("show")
.description(t("tray.show") || "Show the tray icon (if server is running with --tray)")
.action(() => {
process.stderr.write(
"The tray is managed by `omniroute serve --tray`. Start the server with --tray to enable it.\n"
);
});
cmd
.command("hide")
.description(t("tray.hide") || "Hide the tray icon")
.action(() => {
process.stderr.write(
"Send SIGUSR1 to the serve process to toggle the tray, or restart without --tray.\n"
);
});
cmd
.command("quit")
.description(t("tray.quit") || "Quit OmniRoute via tray")
.action(async () => {
const { default: pidUtils } = await import("../utils/pid.mjs").catch(() => ({
default: null,
}));
process.stderr.write("Use `omniroute stop` to stop the server.\n");
process.exit(0);
});
}

347
bin/cli/commands/tunnel.mjs Normal file
View File

@@ -0,0 +1,347 @@
import { Argument } from "commander";
import { apiFetch, isServerUp } from "../api.mjs";
import { t } from "../i18n.mjs";
const VALID_TUNNEL_TYPES = ["cloudflare", "tailscale", "ngrok"];
export function registerTunnel(program) {
const tunnel = program.command("tunnel").description(t("tunnel.title"));
tunnel
.command("list")
.description(t("tunnel.listDescription"))
.option("--json", t("common.jsonOpt"))
.action(async (opts, cmd) => {
const globalOpts = cmd.parent.optsWithGlobals();
const exitCode = await runTunnelListCommand({ ...opts, output: globalOpts.output });
if (exitCode !== 0) process.exit(exitCode);
});
tunnel
.command("create [type]")
.description(t("tunnel.createDescription"))
.addArgument(
new Argument("[type]", "Tunnel type").choices(VALID_TUNNEL_TYPES).default("cloudflare")
)
.action(async (type, opts, cmd) => {
const globalOpts = cmd.parent.optsWithGlobals();
const exitCode = await runTunnelCreateCommand(type, { ...opts, output: globalOpts.output });
if (exitCode !== 0) process.exit(exitCode);
});
tunnel
.command("stop <type>")
.description(t("tunnel.stopDescription"))
.option("--yes", t("common.yesOpt"))
.action(async (type, opts, cmd) => {
const globalOpts = cmd.parent.optsWithGlobals();
const exitCode = await runTunnelStopCommand(type, { ...opts, output: globalOpts.output });
if (exitCode !== 0) process.exit(exitCode);
});
tunnel
.command("status <type>")
.description(t("tunnel.statusDescription"))
.option("--json", t("common.jsonOpt"))
.action(async (type, opts, cmd) => {
const globalOpts = cmd.parent.optsWithGlobals();
const exitCode = await runTunnelStatusCommand(type, { ...opts, output: globalOpts.output });
if (exitCode !== 0) process.exit(exitCode);
});
tunnel
.command("logs <type>")
.description(t("tunnel.logsDescription"))
.option("--tail <n>", t("tunnel.tailOpt"), "50")
.action(async (type, opts, cmd) => {
const globalOpts = cmd.parent.optsWithGlobals();
const exitCode = await runTunnelLogsCommand(type, { ...opts, output: globalOpts.output });
if (exitCode !== 0) process.exit(exitCode);
});
tunnel
.command("info <type>")
.description(t("tunnel.infoDescription"))
.option("--json", t("common.jsonOpt"))
.action(async (type, opts, cmd) => {
const globalOpts = cmd.parent.optsWithGlobals();
const exitCode = await runTunnelInfoCommand(type, { ...opts, output: globalOpts.output });
if (exitCode !== 0) process.exit(exitCode);
});
tunnel
.command("rotate <type>")
.description(t("tunnel.rotateDescription"))
.option("--yes", t("common.yesOpt"))
.action(async (type, opts, cmd) => {
const globalOpts = cmd.parent.optsWithGlobals();
const exitCode = await runTunnelRotateCommand(type, { ...opts, output: globalOpts.output });
if (exitCode !== 0) process.exit(exitCode);
});
}
export async function runTunnelListCommand(opts = {}) {
const serverUp = await isServerUp();
if (!serverUp) {
console.error(t("common.serverOffline"));
return 1;
}
try {
const res = await apiFetch("/api/tunnels", { retry: false, timeout: 5000, acceptNotOk: true });
if (!res.ok) {
console.log(t("tunnel.notAvailable"));
return 0;
}
const tunnels = await res.json();
if (opts.json || opts.output === "json") {
console.log(JSON.stringify(tunnels, null, 2));
return 0;
}
console.log(`\n\x1b[1m\x1b[36m${t("tunnel.title")}\x1b[0m\n`);
if (!Array.isArray(tunnels) || tunnels.length === 0) {
console.log(t("tunnel.noTunnels"));
return 0;
}
for (const tunnel of tunnels) {
const status = tunnel.active ? "\x1b[32m● active\x1b[0m" : "\x1b[2m○ inactive\x1b[0m";
console.log(` ${(tunnel.type || "unknown").padEnd(12)} ${tunnel.url || "N/A"} ${status}`);
}
return 0;
} catch (err) {
console.error(t("common.error", { message: err instanceof Error ? err.message : String(err) }));
return 1;
}
}
export async function runTunnelCreateCommand(type = "cloudflare", opts = {}) {
const serverUp = await isServerUp();
if (!serverUp) {
console.error(t("common.serverOffline"));
return 1;
}
try {
const res = await apiFetch("/api/tunnels", {
method: "POST",
body: { type },
retry: false,
timeout: 15000,
acceptNotOk: true,
});
if (res.ok) {
const result = await res.json();
console.log(t("tunnel.created", { url: result.url }));
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;
}
}
export async function runTunnelStopCommand(type, opts = {}) {
if (!type) {
console.error(t("tunnel.typeRequired"));
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("tunnel.confirmStop", { id: type }) + " [y/N] ", resolve)
);
rl.close();
if (!/^y(es)?$/i.test(answer)) {
console.log(t("common.cancelled"));
return 0;
}
}
const serverUp = await isServerUp();
if (!serverUp) {
console.error(t("common.serverOffline"));
return 1;
}
try {
const res = await apiFetch(`/api/tunnels/${encodeURIComponent(type)}`, {
method: "DELETE",
retry: false,
timeout: 5000,
acceptNotOk: true,
});
if (res.ok) {
console.log(t("tunnel.stopped"));
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;
}
}
export async function runTunnelStatusCommand(type, opts = {}) {
if (!type) {
console.error(t("tunnel.typeRequired"));
return 1;
}
const serverUp = await isServerUp();
if (!serverUp) {
console.error(t("common.serverOffline"));
return 1;
}
try {
const res = await apiFetch(`/api/tunnels/${encodeURIComponent(type)}/status`, {
retry: false,
timeout: 5000,
acceptNotOk: true,
});
if (!res.ok) {
console.error(t("common.error", { message: `HTTP ${res.status}` }));
return 1;
}
const data = await res.json();
if (opts.json || opts.output === "json") {
console.log(JSON.stringify(data, null, 2));
return 0;
}
const uptime = data.uptime ? `${Math.floor(data.uptime / 60)}m` : "N/A";
const statusLabel = data.active ? "\x1b[32m● active\x1b[0m" : "\x1b[31m○ inactive\x1b[0m";
console.log(`\n\x1b[1m${type}\x1b[0m ${statusLabel}`);
console.log(` URL: ${data.url || "N/A"}`);
console.log(` Uptime: ${uptime}`);
console.log(` Requests: ${data.requests ?? data.totalRequests ?? "N/A"}`);
console.log(` Latency: ${data.avgLatencyMs != null ? `${data.avgLatencyMs}ms` : "N/A"}`);
return 0;
} catch (err) {
console.error(t("common.error", { message: err instanceof Error ? err.message : String(err) }));
return 1;
}
}
export async function runTunnelLogsCommand(type, opts = {}) {
if (!type) {
console.error(t("tunnel.typeRequired"));
return 1;
}
const serverUp = await isServerUp();
if (!serverUp) {
console.error(t("common.serverOffline"));
return 1;
}
const tail = Number(opts.tail || 50);
try {
const res = await apiFetch(`/api/tunnels/${encodeURIComponent(type)}/logs?tail=${tail}`, {
retry: false,
timeout: 5000,
acceptNotOk: true,
});
if (!res.ok) {
console.error(t("common.error", { message: `HTTP ${res.status}` }));
return 1;
}
const data = await res.json();
const lines = data.logs || data.lines || data;
if (!Array.isArray(lines) || lines.length === 0) {
console.log(t("tunnel.noLogs"));
return 0;
}
for (const line of lines) {
const ts = line.timestamp || line.ts || "";
const msg = line.message || line.msg || String(line);
console.log(`\x1b[2m${ts}\x1b[0m ${msg}`);
}
return 0;
} catch (err) {
console.error(t("common.error", { message: err instanceof Error ? err.message : String(err) }));
return 1;
}
}
export async function runTunnelInfoCommand(type, opts = {}) {
if (!type) {
console.error(t("tunnel.typeRequired"));
return 1;
}
const serverUp = await isServerUp();
if (!serverUp) {
console.error(t("common.serverOffline"));
return 1;
}
try {
const res = await apiFetch(`/api/tunnels/${encodeURIComponent(type)}`, {
retry: false,
timeout: 5000,
acceptNotOk: true,
});
if (!res.ok) {
console.error(t("common.error", { message: `HTTP ${res.status}` }));
return 1;
}
const data = await res.json();
if (opts.json || opts.output === "json") {
console.log(JSON.stringify(data, null, 2));
return 0;
}
console.log(`\n\x1b[1m\x1b[36m${t("tunnel.infoTitle", { type })}\x1b[0m\n`);
for (const [k, v] of Object.entries(data)) {
console.log(` ${String(k).padEnd(20)} ${JSON.stringify(v)}`);
}
return 0;
} catch (err) {
console.error(t("common.error", { message: err instanceof Error ? err.message : String(err) }));
return 1;
}
}
export async function runTunnelRotateCommand(type, opts = {}) {
if (!type) {
console.error(t("tunnel.typeRequired"));
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("tunnel.confirmRotate", { type }) + " [y/N] ", resolve)
);
rl.close();
if (!/^y(es)?$/i.test(answer)) {
console.log(t("common.cancelled"));
return 0;
}
}
const serverUp = await isServerUp();
if (!serverUp) {
console.error(t("common.serverOffline"));
return 1;
}
try {
const res = await apiFetch(`/api/tunnels/${encodeURIComponent(type)}/rotate`, {
method: "POST",
retry: false,
timeout: 15000,
acceptNotOk: true,
});
if (!res.ok) {
console.error(t("common.error", { message: `HTTP ${res.status}` }));
return 1;
}
const data = await res.json();
console.log(t("tunnel.rotated", { url: data.url || "(see dashboard)" }));
return 0;
} catch (err) {
console.error(t("common.error", { message: err instanceof Error ? err.message : String(err) }));
return 1;
}
}

View File

@@ -1,29 +1,12 @@
import { parseArgs, getStringFlag, hasFlag } from "../args.mjs";
import { printHeading, printInfo, printSuccess, printError } from "../io.mjs";
import { homedir } from "node:os";
import path from "node:path";
import { execFile } from "node:child_process";
import { promisify } from "node:util";
import { t } from "../i18n.mjs";
const execFileAsync = promisify(execFile);
function printUpdateHelp() {
console.log(`
Usage:
omniroute update [options]
Options:
--check Check for available update without applying
--dry-run Show what would be updated without applying
--backup Create backup before updating (default: true)
--no-backup Skip backup creation
--help Show this help
Environment:
OMNIRoute_AUTO_UPDATE Set to "true" to enable auto-update check on startup
`);
}
async function getCurrentVersion() {
try {
const { readFileSync } = await import("node:fs");
@@ -77,17 +60,30 @@ async function createBackup() {
}
}
export async function runUpdateCommand(argv) {
const { flags } = parseArgs(argv);
export function registerUpdate(program) {
program
.command("update")
.description(t("update.checking"))
.option("--check", "Check for available update — exit 0 if up-to-date, exit 1 if outdated")
.option("--apply", "Install latest version automatically (npm install -g)")
.option("--changelog", "Show changelog for the latest release")
.option("--dry-run", "Show what would be updated without applying")
.option("--no-backup", "Skip backup creation")
.option("--yes", "Skip confirmation prompt")
.action(async (opts, cmd) => {
const globalOpts = cmd.optsWithGlobals();
const exitCode = await runUpdateCommand({ ...opts, output: globalOpts.output });
if (exitCode !== 0) process.exit(exitCode);
});
}
if (hasFlag(flags, "help") || hasFlag(flags, "h")) {
printUpdateHelp();
return 0;
}
const checkOnly = hasFlag(flags, "check");
const dryRun = hasFlag(flags, "dry-run");
const skipBackup = hasFlag(flags, "no-backup");
export async function runUpdateCommand(opts = {}) {
const checkOnly = opts.check ?? false;
const applyNow = opts.apply ?? false;
const showChangelog = opts.changelog ?? false;
const dryRun = opts.dryRun ?? false;
const skipBackup = !(opts.backup ?? true);
const skipConfirm = opts.yes ?? applyNow;
const current = await getCurrentVersion();
const latest = await getLatestVersion();
@@ -102,6 +98,22 @@ export async function runUpdateCommand(argv) {
return 1;
}
if (showChangelog) {
try {
const { stdout } = await execFileAsync("npm", ["view", "omniroute", "changelog"], {
timeout: 10000,
});
if (stdout.trim()) {
console.log(stdout.trim());
} else {
console.log(`Changelog: https://github.com/your-org/omniroute/releases/tag/v${latest}`);
}
} catch {
console.log(`Changelog: https://github.com/your-org/omniroute/releases/tag/v${latest}`);
}
return 0;
}
printHeading("OmniRoute Update");
console.log(` Current version: ${current}`);
console.log(` Latest version: ${latest}`);
@@ -115,8 +127,8 @@ export async function runUpdateCommand(argv) {
console.log(`\n Update available: ${current}${latest}`);
if (checkOnly) {
console.log("\n Run `omniroute update` to apply the update.");
return 0;
console.log("\n Run `omniroute update --apply` to install automatically.");
return 1; // exit 1 = outdated (useful for scripts)
}
if (dryRun) {
@@ -136,7 +148,7 @@ export async function runUpdateCommand(argv) {
}
}
if (!hasFlag(flags, "yes")) {
if (!skipConfirm) {
const readline = await import("node:readline");
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
const answer = await new Promise((resolve) =>

331
bin/cli/commands/usage.mjs Normal file
View File

@@ -0,0 +1,331 @@
import { setTimeout as sleep } from "node:timers/promises";
import { apiFetch } from "../api.mjs";
import { emit, maskSecret } from "../output.mjs";
import { t } from "../i18n.mjs";
const fmtTs = (v) => (v ? new Date(v).toISOString().replace("T", " ").slice(0, 19) : "-");
const maskKey = (v) => (typeof v === "string" ? maskSecret(v) : (v ?? "-"));
const fmtCost = (v) => (v ? `$${Number(v).toFixed(4)}` : "-");
const 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);
};
const analyticsSchema = [
{ key: "provider", header: "Provider", width: 20 },
{ 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: fmtCost },
];
const budgetSchema = [
{ key: "scope", header: "Scope", width: 25 },
{ key: "period", header: "Period" },
{ key: "limit", header: "Limit (USD)", formatter: (v) => `$${Number(v).toFixed(2)}` },
{ key: "used", header: "Used (USD)", formatter: (v) => `$${Number(v).toFixed(2)}` },
{ key: "remaining", header: "Remaining", formatter: (v) => `$${Number(v).toFixed(2)}` },
{ key: "pct", header: "%", formatter: (v) => `${(Number(v) * 100).toFixed(1)}%` },
];
const quotaSchema = [
{ key: "provider", header: "Provider", width: 20 },
{ key: "limit", header: "Limit", formatter: fmtTokens },
{ key: "used", header: "Used", formatter: fmtTokens },
{ key: "remaining", header: "Remaining", formatter: fmtTokens },
{ key: "resetAt", header: "Reset At", formatter: fmtTs },
{ key: "state", header: "State" },
];
const logsSchema = [
{ key: "timestamp", header: "Time", width: 20, formatter: fmtTs },
{ key: "apiKey", header: "API Key", width: 16, formatter: maskKey },
{ key: "method", header: "Method", width: 8 },
{ key: "provider", header: "Provider", width: 14 },
{ key: "model", header: "Model", width: 25 },
{ key: "tokens", header: "Tokens", formatter: fmtTokens },
{ key: "costUsd", header: "Cost", formatter: fmtCost },
{ key: "latencyMs", header: "Latency", formatter: (v) => (v ? `${v}ms` : "-") },
{ key: "status", header: "Status" },
];
export function registerUsage(program) {
const usage = program.command("usage").description(t("usage.description"));
// analytics
usage
.command("analytics")
.description(t("usage.analytics.description"))
.option("--period <range>", t("usage.analytics.period"), "30d")
.option("--provider <id>", t("usage.analytics.provider"))
.action(runUsageAnalytics);
// budget
const budget = usage.command("budget").description(t("usage.budget.description"));
budget.command("list").action(runBudgetList);
budget.command("get [scope]").action(runBudgetGet);
budget
.command("set <amount>")
.option("--scope <s>", t("usage.budget.set.scope"), "global")
.option("--period <p>", t("usage.budget.set.period"), "monthly")
.action(runBudgetSet);
budget.command("reset [scope]").action(runBudgetReset);
// quota
usage
.command("quota")
.description(t("usage.quota.description"))
.option("--provider <id>", t("usage.quota.provider"))
.option("--check", t("usage.quota.check"))
.action(runUsageQuota);
// logs
usage
.command("logs")
.description(t("usage.logs.description"))
.option("--limit <n>", t("usage.logs.limit"), parseInt, 100)
.option("--search <q>", t("usage.logs.search"))
.option("--since <ts>", t("usage.logs.since"))
.option("--follow", t("usage.logs.follow"))
.option("--api-key <k>", t("usage.logs.api_key"))
.action(runUsageLogs);
// utilization
usage
.command("utilization")
.description(t("usage.utilization.description"))
.option("--api-key <k>", t("usage.utilization.api_key"))
.action(runUsageUtilization);
// history
usage
.command("history")
.description(t("usage.history.description"))
.option("--limit <n>", t("usage.history.limit"), parseInt, 100)
.action(runUsageHistory);
// proxy-logs
usage
.command("proxy-logs")
.description(t("usage.proxy_logs.description"))
.option("--limit <n>", t("usage.proxy_logs.limit"), parseInt, 100)
.action(runUsageProxyLogs);
}
export async function runUsageAnalytics(opts, cmd) {
const globalOpts = cmd.optsWithGlobals();
const p = new URLSearchParams({ range: opts.period ?? "30d" });
if (opts.provider) p.set("provider", opts.provider);
const res = await fetchOrExit(`/api/usage/analytics?${p}`, globalOpts);
const data = await res.json();
const rows = toArray(data.byProvider ?? data.providers ?? []).map((r) => ({
provider: r.provider ?? r.providerId ?? "",
requests: r.totalRequests ?? r.requests ?? 0,
tokensIn: r.totalTokensIn ?? r.tokensIn ?? 0,
tokensOut: r.totalTokensOut ?? r.tokensOut ?? 0,
costUsd: r.totalCost ?? r.cost ?? r.costUsd ?? 0,
}));
emit(rows, globalOpts, analyticsSchema);
}
export async function runBudgetList(opts, cmd) {
const globalOpts = cmd.optsWithGlobals();
const res = await fetchOrExit("/api/usage/budget", globalOpts);
const data = await res.json();
const rows = normalizeBudgetRows(data);
emit(rows, globalOpts, budgetSchema);
}
export async function runBudgetGet(scope, opts, cmd) {
const globalOpts = cmd.optsWithGlobals();
const p = new URLSearchParams();
if (scope) p.set("scope", scope);
const res = await fetchOrExit(`/api/usage/budget?${p}`, globalOpts);
const data = await res.json();
const rows = normalizeBudgetRows(data);
emit(rows, globalOpts, budgetSchema);
}
export async function runBudgetSet(amount, opts, cmd) {
const globalOpts = cmd.optsWithGlobals();
const res = await apiFetch("/api/usage/budget", {
method: "POST",
body: {
amount: Number(amount),
scope: opts.scope ?? "global",
period: opts.period ?? "monthly",
},
timeout: globalOpts.timeout,
acceptNotOk: true,
});
if (!res.ok) {
const txt = await res.text().catch(() => "");
process.stderr.write(`[error] HTTP ${res.status}: ${txt.slice(0, 200)}\n`);
process.exit(res.exitCode ?? 1);
}
if (!globalOpts.quiet)
process.stdout.write(
`Budget set: $${Number(amount).toFixed(2)} / ${opts.scope ?? "global"} / ${opts.period ?? "monthly"}\n`
);
}
export async function runBudgetReset(scope, opts, cmd) {
const globalOpts = cmd.optsWithGlobals();
const res = await apiFetch("/api/usage/budget", {
method: "DELETE",
body: { scope: scope ?? "global" },
timeout: globalOpts.timeout,
acceptNotOk: true,
});
if (!res.ok) {
const txt = await res.text().catch(() => "");
process.stderr.write(`[error] HTTP ${res.status}: ${txt.slice(0, 200)}\n`);
process.exit(res.exitCode ?? 1);
}
if (!globalOpts.quiet) process.stdout.write(`Budget reset: ${scope ?? "global"}\n`);
}
export async function runUsageQuota(opts, cmd) {
const globalOpts = cmd.optsWithGlobals();
const p = new URLSearchParams();
if (opts.provider) p.set("provider", opts.provider);
if (opts.check) p.set("check", "true");
const res = await fetchOrExit(`/api/usage/quota?${p}`, globalOpts);
const data = await res.json();
const rows = toArray(data.providers ?? data.data ?? (Array.isArray(data) ? data : [])).map(
(r) => ({
provider: r.provider ?? r.providerId ?? "",
limit: r.limit ?? r.quota ?? r.maxTokens ?? null,
used: r.used ?? r.tokensUsed ?? null,
remaining: r.remaining ?? r.percentRemaining ?? null,
resetAt: r.resetAt ?? r.nextReset ?? null,
state: r.state ?? (r.percentRemaining > 0 ? "available" : "exhausted"),
})
);
emit(rows, globalOpts, quotaSchema);
}
export async function runUsageLogs(opts, cmd) {
const globalOpts = cmd.optsWithGlobals();
if (opts.follow) {
await followLogs(opts, globalOpts);
return;
}
const p = buildLogParams(opts);
const res = await fetchOrExit(`/api/usage/call-logs?${p}`, globalOpts);
const data = await res.json();
const rows = toLogRows(toArray(data.logs ?? data.items ?? data));
emit(rows, globalOpts, logsSchema);
}
export async function runUsageUtilization(opts, cmd) {
const globalOpts = cmd.optsWithGlobals();
const p = new URLSearchParams();
if (opts.apiKey) p.set("apiKey", opts.apiKey);
const res = await fetchOrExit(`/api/usage/utilization?${p}`, globalOpts);
const data = await res.json();
const rows = Array.isArray(data) ? data : toArray(data.data ?? data.items ?? [data]);
emit(rows, globalOpts, null);
}
export async function runUsageHistory(opts, cmd) {
const globalOpts = cmd.optsWithGlobals();
const p = new URLSearchParams({ limit: String(opts.limit ?? 100) });
const res = await fetchOrExit(`/api/usage/history?${p}`, globalOpts);
const data = await res.json();
const rows = toArray(data.items ?? data.history ?? (Array.isArray(data) ? data : []));
emit(rows, globalOpts, null);
}
export async function runUsageProxyLogs(opts, cmd) {
const globalOpts = cmd.optsWithGlobals();
const p = new URLSearchParams({ limit: String(opts?.limit ?? 100) });
const res = await fetchOrExit(`/api/usage/proxy-logs?${p}`, globalOpts);
const data = await res.json();
const rows = toArray(data.logs ?? data.items ?? (Array.isArray(data) ? data : []));
emit(rows, globalOpts, null);
}
async function followLogs(opts, globalOpts) {
let lastId = null;
process.stderr.write("[following logs — press Ctrl+C to stop]\n");
const sigint = () => process.exit(0);
process.on("SIGINT", sigint);
try {
while (true) {
const p = buildLogParams({ ...opts, limit: opts.limit ?? 20 });
if (lastId) p.append("afterId", String(lastId));
const res = await apiFetch(`/api/usage/call-logs?${p}`, {
timeout: globalOpts.timeout,
acceptNotOk: true,
});
if (res.ok) {
const data = await res.json();
const rows = toLogRows(toArray(data.logs ?? data.items ?? data));
if (rows.length > 0) {
emit(rows, { ...globalOpts, quiet: true }, logsSchema);
lastId = rows[rows.length - 1]?.id ?? lastId;
}
}
await sleep(2000);
}
} finally {
process.off("SIGINT", sigint);
}
}
function buildLogParams(opts) {
const p = new URLSearchParams({ limit: String(opts.limit ?? 100) });
if (opts.search) p.set("search", opts.search);
if (opts.since) p.set("since", opts.since);
if (opts.apiKey) p.set("apiKey", opts.apiKey);
return p;
}
function toLogRows(items) {
return items.map((r) => ({
id: r.id,
timestamp: r.createdAt ?? r.timestamp ?? r.ts,
apiKey: r.apiKey ?? r.keyId ?? r.apiKeyId,
method: r.method ?? "POST",
provider: r.provider ?? r.providerId,
model: r.model ?? r.modelId,
tokens: (r.tokensIn ?? r.promptTokens ?? 0) + (r.tokensOut ?? r.completionTokens ?? 0),
costUsd: r.cost ?? r.costUsd ?? r.totalCost,
latencyMs: r.latencyMs ?? r.durationMs,
status: r.status ?? r.statusCode,
}));
}
function normalizeBudgetRows(data) {
const items = toArray(data.budgets ?? data.items ?? (Array.isArray(data) ? data : [data]));
return items.map((r) => ({
scope: r.scope ?? r.scopeId ?? "global",
period: r.period ?? "monthly",
limit: r.limit ?? r.amount ?? 0,
used: r.used ?? r.spent ?? 0,
remaining: r.remaining ?? Math.max(0, (r.limit ?? 0) - (r.used ?? 0)),
pct: r.pct ?? (r.limit > 0 ? (r.used ?? 0) / r.limit : 0),
}));
}
async function fetchOrExit(path, globalOpts) {
const res = await apiFetch(path, { timeout: globalOpts.timeout, acceptNotOk: true });
if (!res.ok) {
if (res.status === 401 || res.status === 403) {
process.stderr.write(t("common.authRequired") + "\n");
} else {
process.stderr.write(t("common.serverOffline") + "\n");
}
process.exit(res.exitCode ?? 1);
}
return res;
}
function toArray(val) {
return Array.isArray(val) ? val : [];
}

View File

@@ -0,0 +1,196 @@
import { apiFetch } from "../api.mjs";
import { emit } from "../output.mjs";
import { t } from "../i18n.mjs";
const EVENT_TYPES = [
"request.completed",
"request.failed",
"rate_limit.exceeded",
"budget.exceeded",
"quota.reset",
"provider.down",
"provider.up",
"combo.switched",
"circuit.opened",
"circuit.closed",
"skill.executed",
"memory.added",
"audit.created",
];
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 maskSecret(v) {
if (!v) return "-";
return "***";
}
const webhookSchema = [
{ key: "id", header: "ID", width: 22 },
{ key: "url", header: "URL", width: 40, formatter: truncate },
{
key: "events",
header: "Events",
formatter: (v) => (Array.isArray(v) ? v.join(", ") : String(v ?? "-")),
},
{ key: "enabled", header: "Enabled", formatter: (v) => (v ? "✓" : "✗") },
{ key: "secret", header: "Secret", formatter: maskSecret },
{ key: "lastDelivery", header: "Last Delivery", formatter: fmtTs },
{ key: "lastStatus", header: "Last Status", width: 10 },
];
function parseHeader(kv) {
const eq = kv.indexOf("=");
if (eq === -1) return { name: kv, value: "" };
return { name: kv.slice(0, eq), value: kv.slice(eq + 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 runWebhooksList(opts, cmd) {
const res = await apiFetch("/api/webhooks");
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
const data = await res.json();
emit(data.items ?? data, cmd.optsWithGlobals(), webhookSchema);
}
export async function runWebhooksGet(id, opts, cmd) {
const res = await apiFetch(`/api/webhooks/${id}`);
if (!res.ok) {
process.stderr.write(`Not found: ${id}\n`);
process.exit(1);
}
emit(await res.json(), cmd.optsWithGlobals(), webhookSchema);
}
export async function runWebhooksAdd(opts, cmd) {
const body = {
url: opts.url,
events: opts.events,
...(opts.secret ? { secret: opts.secret } : {}),
headers: opts.header ?? [],
enabled: opts.enabled !== false,
};
const res = await apiFetch("/api/webhooks", { method: "POST", body });
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
emit(await res.json(), cmd.optsWithGlobals(), webhookSchema);
}
export async function runWebhooksUpdate(id, opts, cmd) {
const body = {};
if (opts.url !== undefined) body.url = opts.url;
if (opts.events !== undefined) body.events = opts.events;
if (opts.secret !== undefined) body.secret = opts.secret;
if (opts.enabled !== undefined) body.enabled = opts.enabled;
if (opts.header?.length) body.headers = opts.header.map(parseHeader);
const res = await apiFetch(`/api/webhooks/${id}`, { method: "PUT", body });
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
emit(await res.json(), cmd.optsWithGlobals(), webhookSchema);
}
export async function runWebhooksRemove(id, opts, cmd) {
if (!opts.yes) {
const ok = await confirm(`Delete webhook ${id}?`);
if (!ok) return;
}
const res = await apiFetch(`/api/webhooks/${id}`, { method: "DELETE" });
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
process.stdout.write("Removed\n");
}
export async function runWebhooksTest(id, opts, cmd) {
const body = { event: opts.event ?? "request.completed" };
const res = await apiFetch(`/api/webhooks/${id}/test`, { 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());
}
export function registerWebhooks(program) {
const webhooks = program.command("webhooks").description(t("webhooks.description"));
webhooks
.command("events")
.description(t("webhooks.events.description"))
.action(async (opts, cmd) => {
emit(
EVENT_TYPES.map((e) => ({ event: e })),
cmd.optsWithGlobals()
);
});
webhooks.command("list").description(t("webhooks.list.description")).action(runWebhooksList);
webhooks.command("get <id>").description(t("webhooks.get.description")).action(runWebhooksGet);
webhooks
.command("add")
.description(t("webhooks.add.description"))
.requiredOption("--url <url>", t("webhooks.add.url"))
.requiredOption("--events <list>", t("webhooks.add.events"), (v) => v.split(","))
.option("--secret <s>", t("webhooks.add.secret"))
.option(
"--header <kv>",
t("webhooks.add.header"),
(v, prev) => [...(prev ?? []), parseHeader(v)],
[]
)
.option("--no-enabled", t("webhooks.add.no_enabled"))
.action(runWebhooksAdd);
webhooks
.command("update <id>")
.description(t("webhooks.update.description"))
.option("--url <url>", t("webhooks.add.url"))
.option("--events <list>", t("webhooks.add.events"), (v) => v.split(","))
.option("--secret <s>", t("webhooks.add.secret"))
.option("--header <kv>", t("webhooks.add.header"), (v, prev) => [...(prev ?? []), v], [])
.option("--enabled <bool>", t("webhooks.update.enabled"), (v) => v === "true")
.action(runWebhooksUpdate);
webhooks
.command("remove <id>")
.description(t("webhooks.remove.description"))
.option("--yes", t("webhooks.remove.yes"))
.action(runWebhooksRemove);
webhooks
.command("test <id>")
.description(t("webhooks.test.description"))
.option("--event <e>", t("webhooks.test.event"), "request.completed")
.action(runWebhooksTest);
}

43
bin/cli/contexts.mjs Normal file
View File

@@ -0,0 +1,43 @@
import { existsSync, readFileSync, writeFileSync, mkdirSync, chmodSync } from "node:fs";
import { join, dirname } from "node:path";
import { resolveDataDir } from "./data-dir.mjs";
const CONFIG_VERSION = 1;
export function configPath() {
return join(resolveDataDir(), "config.json");
}
function defaultConfig() {
return {
version: CONFIG_VERSION,
currentContext: "default",
contexts: {
default: { baseUrl: `http://localhost:${process.env.PORT || "20128"}`, apiKey: null },
},
};
}
export function loadContexts() {
try {
if (!existsSync(configPath())) return defaultConfig();
return JSON.parse(readFileSync(configPath(), "utf8"));
} catch {
return defaultConfig();
}
}
export function saveContexts(cfg) {
const path = configPath();
mkdirSync(dirname(path), { recursive: true });
writeFileSync(path, JSON.stringify(cfg, null, 2));
try {
chmodSync(path, 0o600);
} catch {}
}
export function resolveActiveContext(overrideName) {
const cfg = loadContexts();
const name = overrideName || cfg.currentContext || "default";
return cfg.contexts?.[name] || cfg.contexts?.default || { baseUrl: "http://localhost:20128" };
}

106
bin/cli/i18n.mjs Normal file
View File

@@ -0,0 +1,106 @@
import { readFileSync, existsSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
const __dirname = dirname(fileURLToPath(import.meta.url));
const LOCALES_DIR = join(__dirname, "locales");
const FALLBACK_LOCALE = "en";
const cache = new Map();
let activeLocale = null;
let fallbackCatalog = null;
export function detectLocale() {
const raw =
process.env.OMNIROUTE_LANG ||
process.env.LC_ALL ||
process.env.LC_MESSAGES ||
process.env.LANG ||
FALLBACK_LOCALE;
return normalize(raw);
}
function normalize(raw) {
const stripped = String(raw).split(".")[0].replace("_", "-");
if (!stripped) return FALLBACK_LOCALE;
if (hasCatalog(stripped)) return stripped;
const base = stripped.split("-")[0];
if (hasCatalog(base)) return base;
return FALLBACK_LOCALE;
}
function hasCatalog(locale) {
return existsSync(join(LOCALES_DIR, `${locale}.json`));
}
function flattenToMap(obj, prefix, result) {
for (const [key, value] of Object.entries(obj)) {
const fullKey = prefix ? `${prefix}.${key}` : key;
if (value !== null && typeof value === "object" && !Array.isArray(value)) {
flattenToMap(value, fullKey, result);
} else if (typeof value === "string") {
result.set(fullKey, value);
}
}
}
function loadCatalog(locale) {
if (cache.has(locale)) return cache.get(locale);
const file = join(LOCALES_DIR, `${locale}.json`);
if (!existsSync(file)) {
cache.set(locale, null);
return null;
}
try {
const parsed = JSON.parse(readFileSync(file, "utf8"));
const flat = new Map();
flattenToMap(parsed, "", flat);
cache.set(locale, flat);
return flat;
} catch {
cache.set(locale, null);
return null;
}
}
export function setLocale(locale) {
activeLocale = normalize(locale);
loadCatalog(activeLocale);
return activeLocale;
}
export function getLocale() {
if (!activeLocale) activeLocale = detectLocale();
return activeLocale;
}
function interpolate(template, vars) {
if (!vars) return template;
const entries = Object.entries(vars);
if (entries.length === 0) return template;
const varMap = new Map(entries);
return template.replace(/\{(\w+)\}/g, (match, name) => {
const v = varMap.get(name);
return v !== undefined ? String(v) : match;
});
}
export function t(key, vars) {
if (!activeLocale) activeLocale = detectLocale();
const primary = loadCatalog(activeLocale);
const fromPrimary = primary?.get(key);
if (fromPrimary !== undefined) return interpolate(fromPrimary, vars);
if (activeLocale !== FALLBACK_LOCALE) {
if (!fallbackCatalog) fallbackCatalog = loadCatalog(FALLBACK_LOCALE);
const fromFallback = fallbackCatalog?.get(key);
if (fromFallback !== undefined) return interpolate(fromFallback, vars);
}
return key;
}
export function resetForTests() {
cache.clear();
activeLocale = null;
fallbackCatalog = null;
}

View File

@@ -1,44 +0,0 @@
import { runDoctorCommand } from "./commands/doctor.mjs";
import { runProvidersCommand } from "./commands/providers.mjs";
import { runSetupCommand } from "./commands/setup.mjs";
import { runConfigCommand } from "./commands/config.mjs";
import { runStatusCommand } from "./commands/status.mjs";
import { runLogsCommand } from "./commands/logs.mjs";
import { runUpdateCommand } from "./commands/update.mjs";
import { runProviderCommand } from "./commands/provider-cmd.mjs";
export async function runCliCommand(command, argv, context = {}) {
if (command === "doctor") {
return runDoctorCommand(argv, context);
}
if (command === "providers") {
return runProvidersCommand(argv, context);
}
if (command === "setup") {
return runSetupCommand(argv, context);
}
if (command === "config") {
return runConfigCommand(argv);
}
if (command === "status") {
return runStatusCommand(argv);
}
if (command === "logs") {
return runLogsCommand(argv);
}
if (command === "update") {
return runUpdateCommand(argv);
}
if (command === "provider") {
return runProviderCommand(argv);
}
throw new Error(`Unknown CLI command: ${command}`);
}

1222
bin/cli/locales/en.json Normal file

File diff suppressed because it is too large Load Diff

1222
bin/cli/locales/pt-BR.json Normal file

File diff suppressed because it is too large Load Diff

131
bin/cli/output.mjs Normal file
View File

@@ -0,0 +1,131 @@
import Table from "cli-table3";
import { stringify as csvStringify } from "csv-stringify/sync";
const MASK_RE = /sk-[A-Za-z0-9]{4,}/g;
export const EXIT_CODES = Object.freeze({
SUCCESS: 0,
ERROR: 1,
INVALID_ARG: 2,
SERVER_OFFLINE: 3,
AUTH: 4,
RATE_LIMIT: 5,
TIMEOUT: 124,
});
export function maskSecret(value) {
if (typeof value !== "string") return value;
return value.replace(MASK_RE, (m) => `${m.slice(0, 5)}***${m.slice(-4)}`);
}
function toRows(data) {
if (Array.isArray(data)) return data;
if (data !== null && typeof data === "object") return data.items ? data.items : [data];
return [{ value: data }];
}
function pickFormat(opts) {
if (opts.output) return opts.output;
if (opts.json) return "json";
if (!process.stdout.isTTY) return "json";
return "table";
}
function inferSchema(sample) {
return Object.keys(sample).map((k) => ({ key: k, header: k }));
}
function formatCell(v, col) {
if (v == null) return "";
if (col.formatter) return col.formatter(v);
return String(v);
}
function renderTable(rows, schema, opts = {}) {
if (rows.length === 0) {
process.stdout.write("(empty)\n");
return;
}
const cols = schema || inferSchema(rows[0]);
const quiet = opts.quiet === true;
const widths = cols.map((c) => c.width || null);
const hasWidths = widths.some((w) => w !== null);
const tableOpts = {
head: quiet ? [] : cols.map((c) => c.header),
style: { head: quiet ? [] : ["cyan"] },
};
if (hasWidths) tableOpts.colWidths = widths;
const table = new Table(tableOpts);
for (const row of rows) {
table.push(cols.map((c) => formatCell(row[c.key], c)));
}
process.stdout.write(table.toString() + "\n");
}
function renderCsv(rows, schema) {
if (rows.length === 0) {
process.stdout.write("\n");
return;
}
const cols = schema || inferSchema(rows[0]);
const headers = cols.map((c) => c.header);
const records = rows.map((r) => cols.map((c) => formatCell(r[c.key], c)));
process.stdout.write(csvStringify([headers, ...records]));
}
function renderJsonl(rows) {
for (const row of rows) process.stdout.write(JSON.stringify(row) + "\n");
}
/**
* Emit structured data to stdout in the requested format.
* @param {unknown} data - Array of objects or single object
* @param {object} opts - Options: { output, json, quiet }
* @param {Array|null} schema - Column definitions: [{ key, header, width?, formatter? }]
*/
export function emit(data, opts = {}, schema = null) {
const format = pickFormat(opts);
const rows = toRows(data);
switch (format) {
case "json":
process.stdout.write(JSON.stringify(data, null, 2) + "\n");
break;
case "jsonl":
renderJsonl(rows);
break;
case "csv":
renderCsv(rows, schema);
break;
default:
renderTable(rows, schema, opts);
}
}
export function printHeading(title, quiet = false) {
if (quiet) return;
process.stderr.write(`\n\x1b[1m\x1b[36m${title}\x1b[0m\n\n`);
}
export function printSuccess(message, quiet = false) {
if (quiet) return;
process.stderr.write(`\x1b[32m✔ ${message}\x1b[0m\n`);
}
export function printInfo(message, quiet = false) {
if (quiet) return;
process.stderr.write(`\x1b[2m${message}\x1b[0m\n`);
}
export function printWarning(message) {
process.stderr.write(`\x1b[33m⚠ ${message}\x1b[0m\n`);
}
export function printError(message) {
process.stderr.write(`\x1b[31m✖ ${message}\x1b[0m\n`);
}
export function exitWith(code, message) {
if (message) printError(message);
process.exit(code);
}

90
bin/cli/plugins.mjs Normal file
View File

@@ -0,0 +1,90 @@
import { readdirSync, existsSync, readFileSync } from "node:fs";
import { join } from "node:path";
import { homedir } from "node:os";
import { pathToFileURL } from "node:url";
const PLUGIN_PREFIX_RE = /^(@[^/]+\/)?omniroute-cmd-/;
function getPluginDirs() {
return [join(homedir(), ".omniroute", "plugins"), process.env.OMNIROUTE_PLUGIN_PATH].filter(
Boolean
);
}
export async function discoverPlugins() {
const found = [];
for (const dir of getPluginDirs()) {
if (!existsSync(dir)) continue;
let entries;
try {
entries = readdirSync(dir, { withFileTypes: true });
} catch {
continue;
}
for (const entry of entries) {
if (!entry.isDirectory()) continue;
const pkgPath = join(dir, entry.name, "package.json");
if (!existsSync(pkgPath)) continue;
let pkg;
try {
pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
} catch {
continue;
}
if (!pkg.name || !PLUGIN_PREFIX_RE.test(pkg.name)) continue;
found.push({
name: pkg.name,
version: pkg.version || "0.0.0",
description: pkg.description || "",
dir: join(dir, entry.name),
pkg,
});
}
}
return found;
}
export function buildPluginContext(opts = {}) {
return {
apiFetch: async (...args) => {
const { apiFetch } = await import("./api.mjs");
return apiFetch(...args);
},
emit: async (...args) => {
const { emit } = await import("./output.mjs");
return emit(...args);
},
t: async (...args) => {
const { t } = await import("./i18n.mjs");
return t(...args);
},
withSpinner: async (...args) => {
const { withSpinner } = await import("./spinner.mjs");
return withSpinner(...args);
},
baseUrl: opts.baseUrl,
apiKey: opts.apiKey,
};
}
export async function loadPlugins(program, ctx = {}) {
const plugins = await discoverPlugins();
for (const p of plugins) {
try {
const entryPath = join(p.dir, p.pkg.main || "index.mjs");
if (!existsSync(entryPath)) {
process.stderr.write(`[plugin] ${p.name}: entry file not found (${entryPath})\n`);
continue;
}
const mod = await import(pathToFileURL(entryPath).href);
if (typeof mod.register === "function") {
mod.register(program, buildPluginContext(ctx));
} else {
process.stderr.write(`[plugin] ${p.name}: no register() export — skipping\n`);
}
} catch (err) {
process.stderr.write(`[plugin] Failed to load ${p.name}: ${err.message}\n`);
}
}
return plugins.length;
}

39
bin/cli/program.mjs Normal file
View File

@@ -0,0 +1,39 @@
import { Command, Option } from "commander";
import { readFileSync } from "node:fs";
import { join, dirname } from "node:path";
import { fileURLToPath } from "node:url";
import { registerCommands } from "./commands/registry.mjs";
import { t } from "./i18n.mjs";
const __dirname = dirname(fileURLToPath(import.meta.url));
const pkg = JSON.parse(readFileSync(join(__dirname, "..", "..", "package.json"), "utf8"));
export function createProgram() {
const program = new Command();
program
.name("omniroute")
.description(t("program.description"))
.version(pkg.version, "-v, --version", t("program.version"))
.addOption(
new Option("--output <format>", t("program.output"))
.choices(["table", "json", "jsonl", "csv"])
.default("table")
)
.addOption(new Option("-q, --quiet", t("program.quiet")))
.addOption(new Option("--no-color", t("program.no_color")))
.addOption(new Option("--timeout <ms>", t("program.timeout")).default("30000"))
.addOption(new Option("--api-key <key>", t("program.api_key")).env("OMNIROUTE_API_KEY"))
.addOption(new Option("--base-url <url>", t("program.base_url")).env("OMNIROUTE_BASE_URL"))
.addOption(
new Option(
"--context <name>",
t("program.context") || "Server context/profile to use for this command"
).env("OMNIROUTE_CONTEXT")
)
.showHelpAfterError(true)
.exitOverride();
registerCommands(program);
return program;
}

View File

@@ -241,6 +241,16 @@ export function upsertApiKeyProviderConnection(db, input) {
return connection;
}
export function removeProviderConnectionByProvider(db, provider) {
ensureProviderSchema(db);
const result = db
.prepare(
"DELETE FROM provider_connections WHERE provider = ? AND (auth_type = 'apikey' OR (api_key IS NOT NULL AND api_key != ''))"
)
.run(provider);
return result.changes;
}
export function updateProviderTestResult(db, connectionId, result) {
ensureProviderSchema(db);
const now = new Date().toISOString();

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