Compare commits
30 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5763609008 | |||
| 6d672ab09a | |||
| ac68022233 | |||
| c2b31f6b20 | |||
| 54b1d8c8de | |||
| cd1ab696b2 | |||
| d9d0640f6e | |||
| e19046116a | |||
| 82a621ec08 | |||
| ce560ebe9d | |||
| f900a81ec9 | |||
| 2a620b178d | |||
| 5aaaad529b | |||
| 4e15ca6bbd | |||
| 8555a3a106 | |||
| 3a39ae6231 | |||
| 00e6b7cf2c | |||
| ebd1e5421b | |||
| 2feb2df30d | |||
| ba38dc738b | |||
| 9233edb6cd | |||
| f697c56922 | |||
| 5ab6a3b431 | |||
| 25cd0b3612 | |||
| 6e1bf4652d | |||
| 3991c96c78 | |||
| 70008e67e4 | |||
| 0d1e6685f6 | |||
| 60d1d13c6a | |||
| ade1e5f345 |
@@ -6,61 +6,79 @@ description: Create a new release, bump version up to 1.x.10 threshold, update c
|
||||
|
||||
Bump version, finalize CHANGELOG, commit, tag, push, publish to npm, and create GitHub release.
|
||||
|
||||
> **VERSION RULE: Always use PATCH bumps (2.x.y → 2.x.y+1)**
|
||||
> NEVER use `npm version minor` or `npm version major`.
|
||||
> Always use: `npm version patch --no-git-tag-version`
|
||||
> The threshold rule: when `y` reaches 10, bump to `2.(x+1).0` — e.g. `2.1.10` → `2.2.0`.
|
||||
|
||||
## Steps
|
||||
|
||||
### 1. Determine new version
|
||||
|
||||
Check current version in `package.json` and increment the patch number:
|
||||
Check current version in `package.json` and increment the **patch** number only:
|
||||
|
||||
```bash
|
||||
grep '"version"' package.json
|
||||
```
|
||||
|
||||
Version format: `1.x.y` — increment `y` for patch, `x` for minor (threshold: y=10 triggers x+1).
|
||||
Version format: `2.x.y` — examples:
|
||||
|
||||
### 2. Finalize CHANGELOG.md
|
||||
|
||||
Replace `[Unreleased]` header with the new version and date:
|
||||
|
||||
```markdown
|
||||
## [1.x.y] — YYYY-MM-DD
|
||||
```
|
||||
|
||||
### 3. Bump version in package.json
|
||||
- `2.1.2` → `2.1.3` (patch)
|
||||
- `2.1.9` → `2.1.10` (patch)
|
||||
- `2.1.10` → `2.2.0` (minor threshold — do manually with `sed`)
|
||||
|
||||
```bash
|
||||
sed -i 's/"version": "OLD"/"version": "NEW"/' package.json
|
||||
# ALWAYS use patch:
|
||||
npm version patch --no-git-tag-version
|
||||
```
|
||||
|
||||
### 4. Stage, commit, and tag
|
||||
### 2. Regenerate lock file (REQUIRED after version bump)
|
||||
|
||||
**Mandatory** — skipping causes `@swc/helpers` lock mismatch and CI failures:
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
### 3. Finalize CHANGELOG.md
|
||||
|
||||
Replace `[Unreleased]` header with the new version and date.
|
||||
Keep an empty `## [Unreleased]` section above it.
|
||||
|
||||
```markdown
|
||||
## [Unreleased]
|
||||
|
||||
---
|
||||
|
||||
## [2.x.y] — YYYY-MM-DD
|
||||
```
|
||||
|
||||
### 4. Update openapi.yaml version
|
||||
|
||||
```bash
|
||||
sed -i 's/version: OLD/version: NEW/' docs/openapi.yaml
|
||||
```
|
||||
|
||||
### 5. Stage, commit, and tag
|
||||
|
||||
// turbo-all
|
||||
|
||||
```bash
|
||||
git add -A
|
||||
git commit -m "feat(release): vX.Y.Z — summary of changes"
|
||||
git tag -a vX.Y.Z -m "Release vX.Y.Z — summary"
|
||||
git add package.json package-lock.json CHANGELOG.md docs/openapi.yaml
|
||||
git commit -m "chore(release): v2.x.y — summary of changes"
|
||||
git tag -a v2.x.y -m "Release v2.x.y"
|
||||
```
|
||||
|
||||
### 5. Push to GitHub
|
||||
### 6. Push to GitHub
|
||||
|
||||
```bash
|
||||
git push origin main
|
||||
git push origin vX.Y.Z
|
||||
git push origin main --tags
|
||||
```
|
||||
|
||||
### 6. Publish to npm
|
||||
|
||||
```bash
|
||||
npm publish
|
||||
```
|
||||
|
||||
Wait for completion (prepublishOnly runs `npm run build:cli` automatically).
|
||||
|
||||
### 7. Create GitHub release
|
||||
|
||||
```bash
|
||||
gh release create vX.Y.Z --title "Release vX.Y.Z" --notes-file /tmp/release_notes.md
|
||||
gh release create v2.x.y --title "v2.x.y — summary" --notes "..."
|
||||
```
|
||||
|
||||
### 8. Deploy to VPS (if requested)
|
||||
@@ -68,7 +86,7 @@ gh release create vX.Y.Z --title "Release vX.Y.Z" --notes-file /tmp/release_note
|
||||
See `/deploy-vps` workflow for Akamai VPS or use npm for local VPS:
|
||||
|
||||
```bash
|
||||
ssh root@<VPS_IP> "npm install -g omniroute@X.Y.Z && pm2 restart omniroute"
|
||||
ssh root@<VPS_IP> "npm install -g omniroute@2.x.y && pm2 restart omniroute"
|
||||
```
|
||||
|
||||
## Notes
|
||||
@@ -76,3 +94,4 @@ ssh root@<VPS_IP> "npm install -g omniroute@X.Y.Z && pm2 restart omniroute"
|
||||
- Always run `/update-docs` BEFORE this workflow (ensures CHANGELOG and README are current)
|
||||
- The `prepublishOnly` script runs `npm run build:cli` automatically during `npm publish`
|
||||
- After npm publish, verify with `npm info omniroute version`
|
||||
- Lock file sync errors are caused by skipping `npm install` after version bump
|
||||
|
||||
@@ -10,6 +10,9 @@ concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
name: Lint
|
||||
|
||||
@@ -49,6 +49,9 @@ jobs:
|
||||
${{ env.IMAGE_NAME }}:latest
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
no-cache: false
|
||||
env:
|
||||
DOCKER_BUILDKIT_INLINE_CACHE: 1
|
||||
|
||||
- name: Inspect image
|
||||
run: |
|
||||
|
||||
@@ -107,6 +107,10 @@ jobs:
|
||||
"
|
||||
echo "✓ electron/package.json version set to $VERSION_NO_V"
|
||||
|
||||
- name: Install fpm (Linux .deb packaging tool)
|
||||
if: matrix.platform == 'linux'
|
||||
run: sudo gem install fpm --no-document
|
||||
|
||||
- name: Install Electron dependencies
|
||||
working-directory: electron
|
||||
run: npm install --no-audit --no-fund
|
||||
|
||||
@@ -23,7 +23,7 @@ jobs:
|
||||
registry-url: https://registry.npmjs.org
|
||||
|
||||
- name: Install dependencies (skip scripts to avoid heavy build)
|
||||
run: npm ci --ignore-scripts
|
||||
run: npm install --ignore-scripts --no-audit --no-fund
|
||||
|
||||
- name: Sync version from release tag
|
||||
run: |
|
||||
@@ -39,6 +39,13 @@ jobs:
|
||||
run: node scripts/prepublish.mjs
|
||||
|
||||
- name: Publish to npm
|
||||
run: npm publish --access public
|
||||
run: |
|
||||
VERSION=$(node -p "require('./package.json').version")
|
||||
# Check if this version is already published — skip instead of failing with E403
|
||||
if npm view "omniroute@${VERSION}" version --silent 2>/dev/null | grep -q "^${VERSION}$"; then
|
||||
echo "️⚠️ Version ${VERSION} is already published on npm — skipping."
|
||||
exit 0
|
||||
fi
|
||||
npm publish --access public
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
@@ -102,7 +102,6 @@ cloud/
|
||||
security-analysis/
|
||||
|
||||
# Deploy workflow (contains sensitive VPS credentials)
|
||||
.agent/workflows/deploy.md
|
||||
clipr/
|
||||
app.log
|
||||
*.tgz
|
||||
|
||||
@@ -11,6 +11,99 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
---
|
||||
|
||||
## [2.2.4] — 2026-03-10
|
||||
|
||||
> ### 🔧 CI Fixes
|
||||
|
||||
### CI
|
||||
|
||||
- **docs-sync fix** — Updated `docs/openapi.yaml` version from `2.2.0` to `2.2.3` (was out of sync with `package.json`, causing CI lint failure)
|
||||
- **CHANGELOG format** — Added required `## [Unreleased]` section at top of `CHANGELOG.md` (required by `check:docs-sync` script)
|
||||
- **Electron Linux** — Added `gem install fpm` step to `electron-release.yml` Linux build job; `fpm` is required by `electron-builder` to package `.deb` installers but was not pre-installed on `ubuntu-latest` runners
|
||||
- **Docker publish** — Added `DOCKER_BUILDKIT_INLINE_CACHE` env; previous `502 error writing layer blob` was a transient Docker Hub network error
|
||||
|
||||
---
|
||||
|
||||
## [2.2.3] — 2026-03-10
|
||||
|
||||
> ### 🐛 Bug Fixes · 🔧 Reliability
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **Antigravity/Gemini CLI: remove fake projectId fallback (#285)** — OmniRoute was generating random fallback project IDs (e.g. `useful-fuze-a04c5`) when OAuth credentials lacked a real GCP `projectId`. This caused confusing `Permission denied on resource project` and `Verify your account` errors from Google. Now throws a clear actionable error: _reconnect OAuth so OmniRoute can load your real Cloud Code project_. Affects `antigravity.ts`, `openai-to-gemini.ts`, `geminiHelper.ts`.
|
||||
- **Claude Code: filter empty-named tool_use blocks across all message roles (#288)** — Pass 1.4 only filtered empty tool names from `assistant` messages. Extended to all roles (user, system). Also filters `tool_result` blocks missing `tool_use_id`, and top-level `body.tools` declarations with empty names. Prevents `Invalid input[x].name: empty string` 400 errors from Claude API.
|
||||
- **Docker: explicit @swc/helpers copy (#288)** — Added `COPY --from=builder /app/node_modules/@swc/helpers` to Dockerfile `runner-base` stage. The standalone tracer doesn't always include this package, causing runtime `MODULE_NOT_FOUND` crashloops.
|
||||
|
||||
---
|
||||
|
||||
## [2.2.2] — 2026-03-10
|
||||
|
||||
> ### ✨ New Features · 🔀 Model Aliases
|
||||
|
||||
### New Features
|
||||
|
||||
- **system-info.mjs (#280)** — New `npm run system-info` command that collects Node.js version, OmniRoute version, OS info, CLI tool versions (iflow, gemini, claude, codex, antigravity, droid, openclaw, kilo, cursor, aider), Docker/PM2 status, and system packages. Outputs `system-info.txt` for easy attachment to bug reports.
|
||||
|
||||
### Model Aliases
|
||||
|
||||
- **Kimi K2/K2.5 Fireworks aliases (#265)** — Built-in aliases added: `fireworks/accounts/fireworks/models/kimi-k2p5` and `kimi-k2p5` → `moonshotai/Kimi-K2.5`; same for `kimi-k2` → `moonshotai/Kimi-K2`. Fireworks long path model names now auto-resolve.
|
||||
- **Mistral short aliases (#278)** — `mistral-large` → `mistral-large-latest`, `mistral-small` → `mistral-small-latest`, `codestral` → `codestral-latest`.
|
||||
- **Llama short aliases** — `llama-3.3` → `llama-3.3-70b-versatile`, `llama-3-70b` → `llama-3.3-70b-versatile`, `llama-3-8b` → `llama3-8b-8192`.
|
||||
- **Custom aliases** — Users can define their own aliases in **Settings → Model Aliases** tab. Example: `gpt-5.4` → `cx/gpt-5.4`.
|
||||
|
||||
---
|
||||
|
||||
## [2.2.1] — 2026-03-10
|
||||
|
||||
> ### 🐛 Bug Fixes · 🔐 Security · 🔧 CI
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **Gemini image routing (#273)** — `gemini-3.1-flash-image-preview` was missing from the `antigravity` image provider registry in `imageRegistry.ts`, causing image generation to fall through to the chat handler. Added alongside `gemini-2.5-flash-preview-image-generation`.
|
||||
- **Ollama Cloud model listing (#276)** — `ollama-cloud` was absent from `PROVIDER_MODELS_CONFIG` in the models route, causing 400 errors when listing models from `api.ollama.com`. Entry added.
|
||||
- **Missing apiKey error clarity (#277)** — When login is disabled and a provider has no API key configured, the model import route now returns `400` with a clear message instead of a generic `401 Unauthorized`.
|
||||
|
||||
### Security
|
||||
|
||||
- **TLS validation re-enabled (GHSA-50)** — `mitm/server.ts`: `rejectUnauthorized` now defaults to `true`. Opt-out only via `MITM_DISABLE_TLS_VERIFY=1`.
|
||||
- **Path traversal hardening (GHSA-41–49)** — Added `safePath()`, `safeProfilePath()`, `safeLogPath()` helpers across `backupService.ts`, `db/backup.ts`, `codex-profiles/route.ts`, and `mitm/server.ts`. All user-supplied IDs/filenames are now anchored within their allowed directories using `path.resolve()` + bounds check.
|
||||
- **Prototype pollution fix (GHSA-18–20)** — `usageHistory.ts`: `pendingRequests` maps now use `Object.create(null)` + `hasOwnProperty` guards, preventing `__proto__` / `constructor` injection via crafted provider IDs.
|
||||
- **Dependency: dompurify updated to ^3.3.2** — Resolves CVE-2026-0540 (XSS in rendered HTML).
|
||||
- **GitHub Actions: added `permissions: contents: read`** — Prevents token over-permission in CI jobs.
|
||||
|
||||
### CI
|
||||
|
||||
- **Lock file sync** — Added `@swc/helpers: "^0.5.19"` override in `package.json`; regenerated `package-lock.json`. Fixes `npm ci` failures across `ci.yml` and `docker-publish.yml`.
|
||||
- **npm-publish: skip if version exists** — Workflow now checks registry before publishing; exits cleanly with a warning instead of failing with `E403` if the version is already on npm.
|
||||
- **npm-publish: use `npm install` instead of `npm ci`** — Prevents publish failures when a tag commit's lock file is slightly out of sync.
|
||||
- **Lint: `cursor.ts` any-budget** — Replaced `any` with `unknown` + type narrowing in `isToolBoundaryAbort()`.
|
||||
|
||||
---
|
||||
|
||||
## [2.2.0] — 2026-03-10
|
||||
|
||||
> ### 🔧 Bug Fixes · Provider Support · CI Recovery
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **Cursor tool-call loop (#275/#274)** — Stabilized Cursor executor to stop double-translating tool results. Set-based `finalizedIds` for O(1) dedup, byte guard (`0x7b`) before payload `.toString()`, `escapeXml()` to prevent tag injection, and converted all debug `console.log` to `debugLog()`. Fixes the 400 Bad Request loop that corrupted multi-turn Cursor sessions.
|
||||
- **A/V provider validation (#281)** — Added `validateElevenLabsProvider` (GET `/v1/voices` with `xi-api-key`) and `validateInworldProvider` (POST `/tts/v1/voice` with Basic auth) so both providers can be test-connected without false 400 errors.
|
||||
- **OpenAI-compatible Add Connection button (#272)** — "Add Connection" button was hidden behind `!isCompatible` guard in the Connections card. Button now appears for compatible providers when 0 connections exist, limited to 1 (matches single-key-per-node policy).
|
||||
- **CI: unit tests** — Fixed circuit breaker tests using wrong instance keys (`combo:groq` → `combo:groq/llama-3.3-70b`).
|
||||
- **CI: E2E protocol-visibility** — Updated spec to click "Protocols" tab before asserting MCP/A2A links (now tabs inside `/dashboard/endpoint`).
|
||||
- **CI: i18n** — Added missing `header.mcp`, `header.mcpDescription`, `header.a2a`, `header.a2aDescription` keys to `en.json`.
|
||||
|
||||
### New Features
|
||||
|
||||
- **Kimi Coding plan quota display (#279)** — New `getKimiUsage()` with `X-Msh-*` device headers. Parses weekly quota + rate-limit breakdown from `/v1/usages`. Wires `kimi-coding` into the provider usage switch; adds quota capability flag.
|
||||
|
||||
### Dependencies
|
||||
|
||||
- **Dev dependencies** — Bumped `@playwright/test`, `@types/react`, `eslint-plugin-*` and 2 others (#264).
|
||||
- **Prod dependencies** — Bumped 2 production packages (#263).
|
||||
|
||||
---
|
||||
|
||||
## [2.1.2] — 2026-03-09
|
||||
|
||||
> ### 🔨 CI Green + Electron .deb + Link Fixes
|
||||
|
||||
@@ -29,6 +29,8 @@ RUN mkdir -p /app/data
|
||||
COPY --from=builder /app/public ./public
|
||||
COPY --from=builder /app/.next/static ./.next/static
|
||||
COPY --from=builder /app/.next/standalone ./
|
||||
# Explicitly copy @swc/helpers — not always traced by standalone output but needed at runtime
|
||||
COPY --from=builder /app/node_modules/@swc/helpers ./node_modules/@swc/helpers
|
||||
COPY --from=builder /app/scripts/run-standalone.mjs ./run-standalone.mjs
|
||||
COPY --from=builder /app/scripts/runtime-env.mjs ./runtime-env.mjs
|
||||
COPY --from=builder /app/scripts/healthcheck.mjs ./healthcheck.mjs
|
||||
|
||||
@@ -167,6 +167,16 @@ _Connect any AI-powered IDE or CLI tool through OmniRoute — free API gateway f
|
||||
- **Contributing**: See [CONTRIBUTING.md](CONTRIBUTING.md), open a PR, or pick a `good first issue`
|
||||
- **Original Project**: [9router by decolua](https://github.com/decolua/9router)
|
||||
|
||||
### 🐛 Reporting a Bug?
|
||||
|
||||
When opening an issue, please run the system-info command and attach the generated file:
|
||||
|
||||
```bash
|
||||
npm run system-info
|
||||
```
|
||||
|
||||
This generates a `system-info.txt` with your Node.js version, OmniRoute version, OS details, installed CLI tools (iflow, gemini, claude, codex, antigravity, droid, etc.), Docker/PM2 status, and system packages — everything we need to reproduce your issue quickly. Attach the file directly to your GitHub issue.
|
||||
|
||||
---
|
||||
|
||||
## 🔄 How It Works
|
||||
@@ -358,6 +368,7 @@ When a call fails, the dev doesn't know if it was a rate limit, expired token, w
|
||||
- **Translator Playground** — 4 debugging modes: Playground (format translation), Chat Tester (round-trip), Test Bench (batch), Live Monitor (real-time)
|
||||
- **Request Telemetry** — p50/p95/p99 latency + X-Request-Id tracing
|
||||
- **File-Based Logging with Rotation** — Console interceptor captures everything to JSON log with size-based rotation
|
||||
- **System Info Report** — `npm run system-info` generates `system-info.txt` with your full environment (Node version, OmniRoute version, OS, CLI tools, Docker/PM2 status). Attach it when reporting issues for instant triage.
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
openapi: 3.1.0
|
||||
info:
|
||||
title: OmniRoute API
|
||||
version: 2.1.2
|
||||
version: 2.2.3
|
||||
description: |
|
||||
OmniRoute is a local-first AI API proxy router. It provides an OpenAI-compatible
|
||||
endpoint that routes requests to multiple AI providers with load balancing,
|
||||
|
||||
@@ -63,7 +63,10 @@ export const IMAGE_PROVIDERS = {
|
||||
authType: "oauth",
|
||||
authHeader: "bearer",
|
||||
format: "gemini-image", // Special format: uses Gemini generateContent API
|
||||
models: [{ id: "gemini-2.5-flash-preview-image-generation", name: "Nano Banana" }],
|
||||
models: [
|
||||
{ id: "gemini-2.5-flash-preview-image-generation", name: "Gemini 2.5 Flash Image" },
|
||||
{ id: "gemini-3.1-flash-image-preview", name: "Gemini 3.1 Flash Image Preview" },
|
||||
],
|
||||
supportedSizes: ["1024x1024"],
|
||||
},
|
||||
|
||||
|
||||
@@ -38,14 +38,11 @@ export class AntigravityExecutor extends BaseExecutor {
|
||||
transformRequest(model, body, stream, credentials) {
|
||||
const bodyProjectId = body?.project;
|
||||
const credentialsProjectId = credentials?.projectId;
|
||||
const hasExplicitProject = !!(bodyProjectId || credentialsProjectId);
|
||||
const projectId = bodyProjectId || credentialsProjectId || this.generateProjectId();
|
||||
const projectId = bodyProjectId || credentialsProjectId;
|
||||
|
||||
if (!hasExplicitProject) {
|
||||
console.warn(
|
||||
`[Antigravity] ⚠️ No projectId provided via body or credentials — using generated fallback "${projectId}". ` +
|
||||
`This may cause 404 errors if the account has no active GCP project. ` +
|
||||
`Ensure the OAuth token includes a valid project or the request includes a project field.`
|
||||
if (!projectId) {
|
||||
throw new Error(
|
||||
"Missing Google projectId for Antigravity account. Please reconnect OAuth so OmniRoute can fetch your real Cloud Code project (loadCodeAssist)."
|
||||
);
|
||||
}
|
||||
|
||||
@@ -128,12 +125,6 @@ export class AntigravityExecutor extends BaseExecutor {
|
||||
}
|
||||
}
|
||||
|
||||
generateProjectId() {
|
||||
const adj = ["useful", "bright", "swift", "calm", "bold"][Math.floor(Math.random() * 5)];
|
||||
const noun = ["fuze", "wave", "spark", "flow", "core"][Math.floor(Math.random() * 5)];
|
||||
return `${adj}-${noun}-${crypto.randomUUID().slice(0, 5)}`;
|
||||
}
|
||||
|
||||
generateSessionId() {
|
||||
return `-${Math.floor(Math.random() * 9_000_000_000_000_000_000)}`;
|
||||
}
|
||||
|
||||
+169
-38
@@ -29,7 +29,6 @@ import {
|
||||
} from "../utils/cursorProtobuf.ts";
|
||||
import { estimateUsage } from "../utils/usageTracking.ts";
|
||||
import { FORMATS } from "../translator/formats.ts";
|
||||
import { buildCursorRequest } from "../translator/request/openai-to-cursor.ts";
|
||||
import crypto from "crypto";
|
||||
import { v5 as uuidv5 } from "uuid";
|
||||
import zlib from "zlib";
|
||||
@@ -59,13 +58,18 @@ const COMPRESS_FLAG = {
|
||||
GZIP_BOTH: 0x03,
|
||||
};
|
||||
|
||||
const CURSOR_STREAM_DEBUG = process.env.CURSOR_STREAM_DEBUG === "1";
|
||||
const debugLog = (...args: unknown[]) => {
|
||||
if (CURSOR_STREAM_DEBUG) console.log(...args);
|
||||
};
|
||||
|
||||
function decompressPayload(payload, flags) {
|
||||
// Check if payload is JSON error (starts with {"error")
|
||||
if (payload.length > 10 && payload[0] === 0x7b && payload[1] === 0x22) {
|
||||
try {
|
||||
const text = payload.toString("utf-8");
|
||||
if (text.startsWith('{"error"')) {
|
||||
console.log(`[DECOMPRESS] Detected JSON error, skipping decompression`);
|
||||
debugLog(`[DECOMPRESS] Detected JSON error, skipping decompression`);
|
||||
return payload;
|
||||
}
|
||||
} catch {}
|
||||
@@ -89,14 +93,14 @@ function decompressPayload(payload, flags) {
|
||||
try {
|
||||
return zlib.inflateRawSync(payload);
|
||||
} catch (rawErr) {
|
||||
console.log(
|
||||
debugLog(
|
||||
`[DECOMPRESS ERROR] flags=${flags}, payloadSize=${payload.length}, gzip=${gzipErr.message}, deflate=${deflateErr.message}, raw=${rawErr.message}`
|
||||
);
|
||||
console.log(
|
||||
debugLog(
|
||||
`[DECOMPRESS ERROR] First 50 bytes (hex):`,
|
||||
payload.slice(0, 50).toString("hex")
|
||||
);
|
||||
console.log(
|
||||
debugLog(
|
||||
`[DECOMPRESS ERROR] First 50 bytes (utf8):`,
|
||||
payload
|
||||
.slice(0, 50)
|
||||
@@ -136,6 +140,46 @@ function createErrorResponse(jsonError) {
|
||||
);
|
||||
}
|
||||
|
||||
function parseCursorJsonErrorFrame(text: string) {
|
||||
try {
|
||||
return JSON.parse(text);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function isToolBoundaryAbort(jsonError: unknown, toolCallCount: number) {
|
||||
if (!jsonError || toolCallCount <= 0) return false;
|
||||
const e = jsonError as Record<string, unknown>;
|
||||
const err = e?.error as Record<string, unknown> | undefined;
|
||||
const details = (err?.details as Record<string, unknown>[] | undefined)?.[0];
|
||||
const debug = details?.debug as Record<string, unknown> | undefined;
|
||||
const debugDetails = debug?.details as Record<string, unknown> | undefined;
|
||||
const code = (err?.code as string) || "";
|
||||
const debugError = (debug?.error as string) || "";
|
||||
const title = (debugDetails?.title as string) || "";
|
||||
const detail = (debugDetails?.detail as string) || "";
|
||||
const message = `${title} ${detail}`.toLowerCase();
|
||||
const isAbortedCode = code === "aborted" || debugError === "ERROR_USER_ABORTED_REQUEST";
|
||||
return isAbortedCode && message.includes("tool call ended before result was received");
|
||||
}
|
||||
|
||||
function mergeToolCallDelta(existing, incoming) {
|
||||
const mergedName = incoming?.function?.name || existing?.function?.name || "";
|
||||
const existingArgs = existing?.function?.arguments || "";
|
||||
const deltaArgs = incoming?.function?.arguments || "";
|
||||
return {
|
||||
id: incoming.id || existing.id,
|
||||
type: "function",
|
||||
function: {
|
||||
name: mergedName,
|
||||
arguments: `${existingArgs}${deltaArgs}`,
|
||||
},
|
||||
isLast: Boolean(existing?.isLast || incoming?.isLast),
|
||||
index: existing?.index ?? incoming?.index ?? 0,
|
||||
};
|
||||
}
|
||||
|
||||
type CursorHttpResponse = {
|
||||
status: number;
|
||||
headers: Record<string, unknown>;
|
||||
@@ -230,10 +274,10 @@ export class CursorExecutor extends BaseExecutor {
|
||||
}
|
||||
|
||||
transformRequest(model, body, stream, credentials) {
|
||||
// Call translator to convert OpenAI format to Cursor format
|
||||
const translatedBody = buildCursorRequest(model, body, stream, credentials);
|
||||
const messages = translatedBody.messages || [];
|
||||
const tools = translatedBody.tools || body.tools || [];
|
||||
// Messages are already translated by chatCore (claude→openai→cursor)
|
||||
// Do NOT call buildCursorRequest again — double-translation drops tool_results
|
||||
const messages = body.messages || [];
|
||||
const tools = body.tools || [];
|
||||
const reasoningEffort = body.reasoning_effort || null;
|
||||
return generateCursorBody(messages, model, tools, reasoningEffort);
|
||||
}
|
||||
@@ -379,13 +423,14 @@ export class CursorExecutor extends BaseExecutor {
|
||||
let totalContent = "";
|
||||
const toolCalls = [];
|
||||
const toolCallsMap = new Map(); // Track streaming tool calls by ID
|
||||
const finalizedIds = new Set<string>();
|
||||
let frameCount = 0;
|
||||
|
||||
console.log(`[CURSOR BUFFER] Total length: ${buffer.length} bytes`);
|
||||
debugLog(`[CURSOR BUFFER] Total length: ${buffer.length} bytes`);
|
||||
|
||||
while (offset < buffer.length) {
|
||||
if (offset + 5 > buffer.length) {
|
||||
console.log(
|
||||
debugLog(
|
||||
`[CURSOR BUFFER] Reached end, offset=${offset}, remaining=${buffer.length - offset}`
|
||||
);
|
||||
break;
|
||||
@@ -394,12 +439,12 @@ export class CursorExecutor extends BaseExecutor {
|
||||
const flags = buffer[offset];
|
||||
const length = buffer.readUInt32BE(offset + 1);
|
||||
|
||||
console.log(
|
||||
debugLog(
|
||||
`[CURSOR BUFFER] Frame ${frameCount + 1}: flags=0x${flags.toString(16).padStart(2, "0")}, length=${length}`
|
||||
);
|
||||
|
||||
if (offset + 5 + length > buffer.length) {
|
||||
console.log(
|
||||
debugLog(
|
||||
`[CURSOR BUFFER] Incomplete frame, offset=${offset}, length=${length}, buffer.length=${buffer.length}`
|
||||
);
|
||||
break;
|
||||
@@ -411,21 +456,37 @@ export class CursorExecutor extends BaseExecutor {
|
||||
|
||||
payload = decompressPayload(payload, flags);
|
||||
if (!payload) {
|
||||
console.log(`[CURSOR BUFFER] Frame ${frameCount}: decompression failed, skipping`);
|
||||
debugLog(`[CURSOR BUFFER] Frame ${frameCount}: decompression failed, skipping`);
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const text = payload.toString("utf-8");
|
||||
if (text.startsWith("{") && text.includes('"error"')) {
|
||||
return createErrorResponse(JSON.parse(text));
|
||||
}
|
||||
} catch {}
|
||||
// Check for JSON error frames (byte guard: skip toString on non-JSON frames)
|
||||
if (payload.length > 0 && payload[0] === 0x7b) {
|
||||
try {
|
||||
const text = payload.toString("utf-8");
|
||||
if (text.includes('"error"')) {
|
||||
const hasContent = totalContent || toolCallsMap.size > 0;
|
||||
debugLog(
|
||||
`[CURSOR BUFFER] Error frame (hasContent=${hasContent}): ${text.slice(0, 500)}`
|
||||
);
|
||||
if (hasContent) {
|
||||
break;
|
||||
}
|
||||
return createErrorResponse(JSON.parse(text));
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
const result = extractTextFromResponse(new Uint8Array(payload));
|
||||
console.log(`[CURSOR DECODED] Frame ${frameCount}:`, result);
|
||||
debugLog(`[CURSOR DECODED] Frame ${frameCount}:`, result);
|
||||
|
||||
if (result.error) {
|
||||
const hasContent = totalContent || toolCallsMap.size > 0;
|
||||
debugLog(`[CURSOR BUFFER] Decoded error (hasContent=${hasContent}): ${result.error}`);
|
||||
// If we already have content, treat error as stream termination
|
||||
if (hasContent) {
|
||||
break;
|
||||
}
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
error: {
|
||||
@@ -457,6 +518,7 @@ export class CursorExecutor extends BaseExecutor {
|
||||
// Push to final array when isLast is true
|
||||
if (tc.isLast) {
|
||||
const finalToolCall = toolCallsMap.get(tc.id);
|
||||
finalizedIds.add(tc.id);
|
||||
toolCalls.push({
|
||||
id: finalToolCall.id,
|
||||
type: finalToolCall.type,
|
||||
@@ -471,15 +533,15 @@ export class CursorExecutor extends BaseExecutor {
|
||||
if (result.text) totalContent += result.text;
|
||||
}
|
||||
|
||||
console.log(
|
||||
debugLog(
|
||||
`[CURSOR BUFFER] Parsed ${frameCount} frames, toolCallsMap size: ${toolCallsMap.size}, finalized toolCalls: ${toolCalls.length}`
|
||||
);
|
||||
|
||||
// Finalize all remaining tool calls in map (in case stream ended without isLast=true)
|
||||
for (const [id, tc] of toolCallsMap.entries()) {
|
||||
// Check if already in final array
|
||||
if (!toolCalls.find((t) => t.id === id)) {
|
||||
console.log(`[CURSOR BUFFER] Finalizing incomplete tool call: ${id}, isLast=${tc.isLast}`);
|
||||
if (!finalizedIds.has(id)) {
|
||||
debugLog(`[CURSOR BUFFER] Finalizing incomplete tool call: ${id}, isLast=${tc.isLast}`);
|
||||
toolCalls.push({
|
||||
id: tc.id,
|
||||
type: tc.type,
|
||||
@@ -491,7 +553,7 @@ export class CursorExecutor extends BaseExecutor {
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[CURSOR BUFFER] Final toolCalls count: ${toolCalls.length}`);
|
||||
debugLog(`[CURSOR BUFFER] Final toolCalls count: ${toolCalls.length}`);
|
||||
|
||||
const message: Record<string, unknown> = {
|
||||
role: "assistant",
|
||||
@@ -534,13 +596,15 @@ export class CursorExecutor extends BaseExecutor {
|
||||
let totalContent = "";
|
||||
const toolCalls = [];
|
||||
const toolCallsMap = new Map(); // Track streaming tool calls by ID
|
||||
const finalizedIds = new Set<string>();
|
||||
const emittedToolCallIds = new Set<string>();
|
||||
let frameCount = 0;
|
||||
|
||||
console.log(`[CURSOR BUFFER SSE] Total length: ${buffer.length} bytes`);
|
||||
debugLog(`[CURSOR BUFFER SSE] Total length: ${buffer.length} bytes`);
|
||||
|
||||
while (offset < buffer.length) {
|
||||
if (offset + 5 > buffer.length) {
|
||||
console.log(
|
||||
debugLog(
|
||||
`[CURSOR BUFFER SSE] Reached end, offset=${offset}, remaining=${buffer.length - offset}`
|
||||
);
|
||||
break;
|
||||
@@ -549,12 +613,12 @@ export class CursorExecutor extends BaseExecutor {
|
||||
const flags = buffer[offset];
|
||||
const length = buffer.readUInt32BE(offset + 1);
|
||||
|
||||
console.log(
|
||||
debugLog(
|
||||
`[CURSOR BUFFER SSE] Frame ${frameCount + 1}: flags=0x${flags.toString(16).padStart(2, "0")}, length=${length}`
|
||||
);
|
||||
|
||||
if (offset + 5 + length > buffer.length) {
|
||||
console.log(
|
||||
debugLog(
|
||||
`[CURSOR BUFFER SSE] Incomplete frame, offset=${offset}, length=${length}, buffer.length=${buffer.length}`
|
||||
);
|
||||
break;
|
||||
@@ -566,21 +630,37 @@ export class CursorExecutor extends BaseExecutor {
|
||||
|
||||
payload = decompressPayload(payload, flags);
|
||||
if (!payload) {
|
||||
console.log(`[CURSOR BUFFER SSE] Frame ${frameCount}: decompression failed, skipping`);
|
||||
debugLog(`[CURSOR BUFFER SSE] Frame ${frameCount}: decompression failed, skipping`);
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const text = payload.toString("utf-8");
|
||||
if (text.startsWith("{") && text.includes('"error"')) {
|
||||
return createErrorResponse(JSON.parse(text));
|
||||
}
|
||||
} catch {}
|
||||
// Check for JSON error frames (byte-guard: only decode if starts with '{')
|
||||
if (payload[0] === 0x7b) {
|
||||
try {
|
||||
const text = payload.toString("utf-8");
|
||||
if (text.includes('"error"')) {
|
||||
const hasContent = chunks.length > 0 || totalContent || toolCallsMap.size > 0;
|
||||
debugLog(
|
||||
`[CURSOR BUFFER SSE] Error frame (hasContent=${hasContent}): ${text.slice(0, 500)}`
|
||||
);
|
||||
if (hasContent) {
|
||||
break;
|
||||
}
|
||||
return createErrorResponse(JSON.parse(text));
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
const result = extractTextFromResponse(new Uint8Array(payload));
|
||||
console.log(`[CURSOR DECODED SSE] Frame ${frameCount}:`, result);
|
||||
debugLog(`[CURSOR DECODED SSE] Frame ${frameCount}:`, result);
|
||||
|
||||
if (result.error) {
|
||||
const hasContent = chunks.length > 0 || totalContent || toolCallsMap.size > 0;
|
||||
debugLog(`[CURSOR BUFFER SSE] Decoded error (hasContent=${hasContent}): ${result.error}`);
|
||||
// If we already have content, treat error as stream termination
|
||||
if (hasContent) {
|
||||
break;
|
||||
}
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
error: {
|
||||
@@ -626,6 +706,7 @@ export class CursorExecutor extends BaseExecutor {
|
||||
|
||||
// Stream the delta arguments
|
||||
if (tc.function.arguments) {
|
||||
emittedToolCallIds.add(tc.id);
|
||||
chunks.push(
|
||||
`data: ${JSON.stringify({
|
||||
id: responseId,
|
||||
@@ -657,10 +738,12 @@ export class CursorExecutor extends BaseExecutor {
|
||||
} else {
|
||||
// New tool call - assign index and add to map
|
||||
const toolCallIndex = toolCalls.length;
|
||||
finalizedIds.add(tc.id);
|
||||
toolCalls.push({ ...tc, index: toolCallIndex });
|
||||
toolCallsMap.set(tc.id, { ...tc, index: toolCallIndex });
|
||||
|
||||
// Stream initial tool call with name
|
||||
emittedToolCallIds.add(tc.id);
|
||||
chunks.push(
|
||||
`data: ${JSON.stringify({
|
||||
id: responseId,
|
||||
@@ -714,10 +797,58 @@ export class CursorExecutor extends BaseExecutor {
|
||||
}
|
||||
}
|
||||
|
||||
console.log(
|
||||
debugLog(
|
||||
`[CURSOR BUFFER SSE] Parsed ${frameCount} frames, toolCallsMap size: ${toolCallsMap.size}, toolCalls array: ${toolCalls.length}`
|
||||
);
|
||||
|
||||
// Finalize all remaining tool calls in map (stream may have ended without isLast=true)
|
||||
for (const [id, tc] of toolCallsMap.entries()) {
|
||||
if (!finalizedIds.has(id)) {
|
||||
debugLog(`[CURSOR BUFFER SSE] Finalizing incomplete tool call: ${id}, isLast=${tc.isLast}`);
|
||||
const toolCallIndex = toolCalls.length;
|
||||
toolCalls.push({
|
||||
id: tc.id,
|
||||
type: tc.type,
|
||||
index: toolCallIndex,
|
||||
function: {
|
||||
name: tc.function.name,
|
||||
arguments: tc.function.arguments,
|
||||
},
|
||||
});
|
||||
|
||||
// Emit SSE chunk for the finalized tool call if not already emitted
|
||||
if (!emittedToolCallIds.has(tc.id)) {
|
||||
chunks.push(
|
||||
`data: ${JSON.stringify({
|
||||
id: responseId,
|
||||
object: "chat.completion.chunk",
|
||||
created,
|
||||
model,
|
||||
choices: [
|
||||
{
|
||||
index: 0,
|
||||
delta: {
|
||||
tool_calls: [
|
||||
{
|
||||
index: toolCallIndex,
|
||||
id: tc.id,
|
||||
type: "function",
|
||||
function: {
|
||||
name: tc.function.name,
|
||||
arguments: tc.function.arguments,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
finish_reason: null,
|
||||
},
|
||||
],
|
||||
})}\n\n`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (chunks.length === 0 && toolCalls.length === 0) {
|
||||
chunks.push(
|
||||
`data: ${JSON.stringify({
|
||||
|
||||
@@ -31,6 +31,24 @@ const BUILT_IN_ALIASES: Record<string, string> = {
|
||||
"gpt-4-0125-preview": "gpt-4-turbo",
|
||||
"gpt-4-1106-preview": "gpt-4-turbo",
|
||||
"gpt-3.5-turbo-0125": "gpt-3.5-turbo",
|
||||
|
||||
// Kimi/Moonshot — Fireworks long-path aliases (#265)
|
||||
"accounts/fireworks/models/kimi-k2p5": "moonshotai/Kimi-K2.5",
|
||||
"fireworks/accounts/fireworks/models/kimi-k2p5": "moonshotai/Kimi-K2.5",
|
||||
"kimi-k2p5": "moonshotai/Kimi-K2.5",
|
||||
"accounts/fireworks/models/kimi-k2": "moonshotai/Kimi-K2",
|
||||
"fireworks/accounts/fireworks/models/kimi-k2": "moonshotai/Kimi-K2",
|
||||
"kimi-k2": "moonshotai/Kimi-K2",
|
||||
|
||||
// Mistral short aliases
|
||||
"mistral-large": "mistral-large-latest",
|
||||
"mistral-small": "mistral-small-latest",
|
||||
codestral: "codestral-latest",
|
||||
|
||||
// Llama short aliases
|
||||
"llama-3.3": "llama-3.3-70b-versatile",
|
||||
"llama-3-70b": "llama-3.3-70b-versatile",
|
||||
"llama-3-8b": "llama3-8b-8192",
|
||||
};
|
||||
|
||||
// ── Custom Aliases (persisted via Settings API) ─────────────────────────────
|
||||
|
||||
@@ -137,6 +137,7 @@ export async function refreshClineToken(refreshToken, log) {
|
||||
|
||||
/**
|
||||
* Specialized refresh for Kimi Coding OAuth tokens.
|
||||
* Uses custom X-Msh-* headers required by Kimi OAuth API.
|
||||
*/
|
||||
export async function refreshKimiCodingToken(refreshToken, log) {
|
||||
const endpoint = PROVIDERS["kimi-coding"]?.refreshUrl || PROVIDERS["kimi-coding"]?.tokenUrl;
|
||||
@@ -145,6 +146,13 @@ export async function refreshKimiCodingToken(refreshToken, log) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Generate device info for headers (same as OAuth flow)
|
||||
const deviceId = "kimi-refresh-" + Date.now();
|
||||
const platform = "omniroute";
|
||||
const version = "2.1.2";
|
||||
const deviceModel =
|
||||
typeof process !== "undefined" ? `${process.platform} ${process.arch}` : "unknown";
|
||||
|
||||
try {
|
||||
const params = new URLSearchParams({
|
||||
grant_type: "refresh_token",
|
||||
@@ -157,6 +165,10 @@ export async function refreshKimiCodingToken(refreshToken, log) {
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
Accept: "application/json",
|
||||
"X-Msh-Platform": platform,
|
||||
"X-Msh-Version": version,
|
||||
"X-Msh-Device-Model": deviceModel,
|
||||
"X-Msh-Device-Id": deviceId,
|
||||
},
|
||||
body: params,
|
||||
});
|
||||
@@ -181,6 +193,8 @@ export async function refreshKimiCodingToken(refreshToken, log) {
|
||||
accessToken: tokens.access_token,
|
||||
refreshToken: tokens.refresh_token || refreshToken,
|
||||
expiresIn: tokens.expires_in,
|
||||
tokenType: tokens.token_type,
|
||||
scope: tokens.scope,
|
||||
};
|
||||
} catch (error) {
|
||||
log?.error?.("TOKEN_REFRESH", `Network error refreshing Kimi Coding token: ${error.message}`);
|
||||
|
||||
@@ -38,6 +38,13 @@ const CLAUDE_CONFIG = {
|
||||
apiVersion: "2023-06-01",
|
||||
};
|
||||
|
||||
// Kimi Coding API config
|
||||
const KIMI_CONFIG = {
|
||||
baseUrl: "https://api.kimi.com/coding/v1",
|
||||
usageUrl: "https://api.kimi.com/coding/v1/usages",
|
||||
apiVersion: "2023-06-01",
|
||||
};
|
||||
|
||||
type JsonRecord = Record<string, unknown>;
|
||||
type UsageQuota = {
|
||||
used: number;
|
||||
@@ -89,6 +96,8 @@ export async function getUsageForProvider(connection) {
|
||||
return await getCodexUsage(accessToken, providerSpecificData);
|
||||
case "kiro":
|
||||
return await getKiroUsage(accessToken, providerSpecificData);
|
||||
case "kimi-coding":
|
||||
return await getKimiUsage(accessToken);
|
||||
case "qwen":
|
||||
return await getQwenUsage(accessToken, providerSpecificData);
|
||||
case "iflow":
|
||||
@@ -779,6 +788,175 @@ async function getKiroUsage(accessToken, providerSpecificData) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Map Kimi membership level to display name
|
||||
* LEVEL_BASIC = Moderato, LEVEL_INTERMEDIATE = Allegretto,
|
||||
* LEVEL_ADVANCED = Allegro, LEVEL_STANDARD = Vivace
|
||||
*/
|
||||
function getKimiPlanName(level) {
|
||||
if (!level) return "";
|
||||
|
||||
const levelMap = {
|
||||
LEVEL_BASIC: "Moderato",
|
||||
LEVEL_INTERMEDIATE: "Allegretto",
|
||||
LEVEL_ADVANCED: "Allegro",
|
||||
LEVEL_STANDARD: "Vivace",
|
||||
};
|
||||
|
||||
return levelMap[level] || level.replace("LEVEL_", "").toLowerCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* Kimi Coding Usage - Fetch quota from Kimi API
|
||||
* Uses the official /v1/usages endpoint with custom X-Msh-* headers
|
||||
*/
|
||||
async function getKimiUsage(accessToken) {
|
||||
// Generate device info for headers (same as OAuth flow)
|
||||
const deviceId = "kimi-usage-" + Date.now();
|
||||
const platform = "omniroute";
|
||||
const version = "2.1.2";
|
||||
const deviceModel =
|
||||
typeof process !== "undefined" ? `${process.platform} ${process.arch}` : "unknown";
|
||||
|
||||
try {
|
||||
const response = await fetch(KIMI_CONFIG.usageUrl, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
"Content-Type": "application/json",
|
||||
"X-Msh-Platform": platform,
|
||||
"X-Msh-Version": version,
|
||||
"X-Msh-Device-Model": deviceModel,
|
||||
"X-Msh-Device-Id": deviceId,
|
||||
},
|
||||
});
|
||||
|
||||
const responseText = await response.text();
|
||||
|
||||
if (!response.ok) {
|
||||
return {
|
||||
plan: "Kimi Coding",
|
||||
message: `Kimi Coding connected. API Error ${response.status}: ${responseText.slice(0, 100)}`,
|
||||
};
|
||||
}
|
||||
|
||||
let data;
|
||||
try {
|
||||
data = JSON.parse(responseText);
|
||||
} catch {
|
||||
return {
|
||||
plan: "Kimi Coding",
|
||||
message: "Kimi Coding connected. Invalid JSON response from API.",
|
||||
};
|
||||
}
|
||||
|
||||
const quotas: Record<string, UsageQuota> = {};
|
||||
const dataObj = toRecord(data);
|
||||
|
||||
// Parse Kimi usage response format
|
||||
// Format: { user: {...}, usage: { limit: "100", used: "92", remaining: "8", resetTime: "..." }, limits: [...] }
|
||||
const usageObj = toRecord(dataObj.usage);
|
||||
|
||||
// Check for Kimi's actual usage fields (strings, not numbers)
|
||||
const usageLimit = toNumber(usageObj.limit || usageObj.Limit, 0);
|
||||
const usageUsed = toNumber(usageObj.used || usageObj.Used, 0);
|
||||
const usageRemaining = toNumber(usageObj.remaining || usageObj.Remaining, 0);
|
||||
const usageResetTime =
|
||||
usageObj.resetTime || usageObj.ResetTime || usageObj.reset_at || usageObj.resetAt;
|
||||
|
||||
if (usageLimit > 0) {
|
||||
const percentRemaining = usageLimit > 0 ? (usageRemaining / usageLimit) * 100 : 0;
|
||||
|
||||
quotas["Weekly"] = {
|
||||
used: usageUsed,
|
||||
total: usageLimit,
|
||||
remaining: usageRemaining,
|
||||
remainingPercentage: percentRemaining,
|
||||
resetAt: parseResetTime(usageResetTime),
|
||||
unlimited: false,
|
||||
};
|
||||
}
|
||||
|
||||
// Also parse limits array for rate limits
|
||||
const limitsArray = Array.isArray(dataObj.limits) ? dataObj.limits : [];
|
||||
for (let i = 0; i < limitsArray.length; i++) {
|
||||
const limitItem = toRecord(limitsArray[i]);
|
||||
const window = toRecord(limitItem.window);
|
||||
const detail = toRecord(limitItem.detail);
|
||||
|
||||
const limit = toNumber(detail.limit || detail.Limit, 0);
|
||||
const remaining = toNumber(detail.remaining || detail.Remaining, 0);
|
||||
const resetTime = detail.resetTime || detail.reset_at || detail.resetAt;
|
||||
|
||||
if (limit > 0) {
|
||||
quotas["Ratelimit"] = {
|
||||
used: limit - remaining,
|
||||
total: limit,
|
||||
remaining,
|
||||
remainingPercentage: limit > 0 ? (remaining / limit) * 100 : 0,
|
||||
resetAt: parseResetTime(resetTime),
|
||||
unlimited: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Check for quota windows (Claude-like format with utilization) as fallback
|
||||
const hasUtilization = (window: JsonRecord) =>
|
||||
window && typeof window === "object" && safePercentage(window.utilization) !== undefined;
|
||||
|
||||
const createQuotaObject = (window: JsonRecord) => {
|
||||
const remaining = safePercentage(window.utilization) as number;
|
||||
const used = 100 - remaining;
|
||||
return {
|
||||
used,
|
||||
total: 100,
|
||||
remaining,
|
||||
resetAt: parseResetTime(window.resets_at),
|
||||
remainingPercentage: remaining,
|
||||
unlimited: false,
|
||||
};
|
||||
};
|
||||
|
||||
if (hasUtilization(dataObj.five_hour)) {
|
||||
quotas["session (5h)"] = createQuotaObject(dataObj.five_hour);
|
||||
}
|
||||
|
||||
if (hasUtilization(dataObj.seven_day)) {
|
||||
quotas["weekly (7d)"] = createQuotaObject(dataObj.seven_day);
|
||||
}
|
||||
|
||||
// Check for model-specific quotas
|
||||
for (const [key, value] of Object.entries(dataObj)) {
|
||||
const valueRecord = toRecord(value);
|
||||
if (key.startsWith("seven_day_") && key !== "seven_day" && hasUtilization(valueRecord)) {
|
||||
const modelName = key.replace("seven_day_", "");
|
||||
quotas[`weekly ${modelName} (7d)`] = createQuotaObject(valueRecord);
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(quotas).length > 0) {
|
||||
const membershipLevel = dataObj.user?.membership?.level;
|
||||
const planName = getKimiPlanName(membershipLevel);
|
||||
return {
|
||||
plan: planName || "Kimi Coding",
|
||||
quotas,
|
||||
};
|
||||
}
|
||||
|
||||
// No quota data in response
|
||||
const membershipLevel = dataObj.user?.membership?.level;
|
||||
const planName = getKimiPlanName(membershipLevel);
|
||||
return {
|
||||
plan: planName || "Kimi Coding",
|
||||
message: "Kimi Coding connected. Usage tracked per request.",
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
message: `Kimi Coding connected. Unable to fetch usage: ${(error as Error).message}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Qwen Usage
|
||||
*/
|
||||
|
||||
@@ -127,14 +127,24 @@ export function prepareClaudeRequest(body, provider = null) {
|
||||
}
|
||||
|
||||
// Pass 1.4: Filter out tool_use blocks with empty names (causes Claude 400 error)
|
||||
// Apply to ALL roles (assistant tool_use + any user messages that may carry tool_use)
|
||||
// Also filter tool_result blocks with missing tool_use_id
|
||||
for (const msg of filtered) {
|
||||
if (msg.role === "assistant" && Array.isArray(msg.content)) {
|
||||
if (Array.isArray(msg.content)) {
|
||||
msg.content = msg.content.filter(
|
||||
(block) => block.type !== "tool_use" || (block.name && block.name.trim())
|
||||
(block) => block.type !== "tool_use" || (block.name && block.name?.trim())
|
||||
);
|
||||
msg.content = msg.content.filter(
|
||||
(block) => block.type !== "tool_result" || block.tool_use_id
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Also filter top-level tool declarations with empty names
|
||||
if (body.tools && Array.isArray(body.tools)) {
|
||||
body.tools = body.tools.filter((tool) => tool.name && tool.name?.trim());
|
||||
}
|
||||
|
||||
// Pass 1.5: Fix tool_use/tool_result ordering
|
||||
// Each tool_use must have tool_result in the NEXT message (not same message with other content)
|
||||
filtered = fixToolUseOrdering(filtered);
|
||||
|
||||
@@ -126,15 +126,6 @@ export function generateSessionId() {
|
||||
return `-${Math.floor(Math.random() * 9000000000000000000)}`;
|
||||
}
|
||||
|
||||
// Generate project ID
|
||||
export function generateProjectId() {
|
||||
const adjectives = ["useful", "bright", "swift", "calm", "bold"];
|
||||
const nouns = ["fuze", "wave", "spark", "flow", "core"];
|
||||
const adj = adjectives[Math.floor(Math.random() * adjectives.length)];
|
||||
const noun = nouns[Math.floor(Math.random() * nouns.length)];
|
||||
return `${adj}-${noun}-${crypto.randomUUID().slice(0, 5)}`;
|
||||
}
|
||||
|
||||
// Helper: Remove unsupported keywords recursively from object/array
|
||||
function removeUnsupportedKeywords(obj, keywords) {
|
||||
if (!obj || typeof obj !== "object") return;
|
||||
|
||||
@@ -175,6 +175,9 @@ export function openaiToClaudeRequest(model, body, stream) {
|
||||
};
|
||||
});
|
||||
|
||||
// Filter out tools with empty names (would cause Claude 400 error)
|
||||
result.tools = result.tools.filter((tool) => tool.name && tool.name?.trim());
|
||||
|
||||
// Add cache_control to last tool that doesn't have defer_loading
|
||||
// Tools with defer_loading=true cannot have cache_control (API rejects it)
|
||||
for (let i = result.tools.length - 1; i >= 0; i--) {
|
||||
@@ -227,6 +230,8 @@ function getContentBlocksFromMessage(msg, toolNameMap = new Map(), disableToolPr
|
||||
if (part.type === "text" && part.text) {
|
||||
blocks.push({ type: "text", text: part.text });
|
||||
} else if (part.type === "tool_result") {
|
||||
// Skip tool_result with no tool_use_id (would be useless and may cause errors)
|
||||
if (!part.tool_use_id) continue;
|
||||
blocks.push({
|
||||
type: "tool_result",
|
||||
tool_use_id: part.tool_use_id,
|
||||
|
||||
@@ -1,19 +1,84 @@
|
||||
/**
|
||||
* OpenAI to Cursor Request Translator
|
||||
* Converts OpenAI messages to Cursor simple format
|
||||
* Converts OpenAI messages to Cursor ask/agent format.
|
||||
*
|
||||
* Important: Cursor can loop when tool outputs are sent via protobuf tool_results
|
||||
* with partial schema mismatches. For stability, tool outputs are represented as
|
||||
* structured text blocks in user messages.
|
||||
*/
|
||||
import { register } from "../registry.ts";
|
||||
import { FORMATS } from "../formats.ts";
|
||||
|
||||
/**
|
||||
* Convert OpenAI messages to Cursor format with native tool_results support
|
||||
* - system → user with [System Instructions] prefix
|
||||
* - tool → accumulate into tool_results array for next user/assistant message
|
||||
* - assistant with tool_calls → keep tool_calls structure (Cursor supports it natively)
|
||||
*/
|
||||
type TextPart = { type?: string; text?: string };
|
||||
type ToolUsePart = { type?: string; id?: string; name?: string; input?: unknown };
|
||||
type ToolResultPart = { type?: string; tool_use_id?: string; content?: unknown };
|
||||
|
||||
function normalizeToolCallId(id: unknown): string {
|
||||
return typeof id === "string" ? id.split("\n")[0] : "";
|
||||
}
|
||||
|
||||
function extractContent(content: unknown): string {
|
||||
if (typeof content === "string") return content;
|
||||
if (Array.isArray(content)) {
|
||||
return content
|
||||
.filter((part): part is TextPart => {
|
||||
if (!part || typeof part !== "object") return false;
|
||||
const maybe = part as TextPart;
|
||||
return maybe.type === "text" && typeof maybe.text === "string";
|
||||
})
|
||||
.map((part) => part.text as string)
|
||||
.join("");
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function sanitizeToolResultText(text: string): string {
|
||||
// Strip non-printable control chars that can produce backend request errors.
|
||||
return text.replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g, "");
|
||||
}
|
||||
|
||||
function escapeXml(text: string): string {
|
||||
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
||||
}
|
||||
|
||||
function buildToolResultBlock(toolName: string, toolCallId: string, resultText: string): string {
|
||||
const cleanResult = sanitizeToolResultText(resultText || "");
|
||||
return [
|
||||
"<tool_result>",
|
||||
`<tool_name>${escapeXml(toolName || "tool")}</tool_name>`,
|
||||
`<tool_call_id>${escapeXml(toolCallId || "")}</tool_call_id>`,
|
||||
`<result>${escapeXml(cleanResult)}</result>`,
|
||||
"</tool_result>",
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
function convertMessages(messages) {
|
||||
const result = [];
|
||||
let pendingToolResults = [];
|
||||
// Build a map of tool_call_id -> tool name from assistant tool calls.
|
||||
const toolCallMetaMap = new Map();
|
||||
const rememberToolMeta = (toolCallId: string, toolName: string) => {
|
||||
if (!toolCallId) return;
|
||||
const name = toolName || "tool";
|
||||
toolCallMetaMap.set(toolCallId, { name });
|
||||
const normalized = normalizeToolCallId(toolCallId);
|
||||
if (normalized && normalized !== toolCallId) {
|
||||
toolCallMetaMap.set(normalized, { name });
|
||||
}
|
||||
};
|
||||
|
||||
for (const msg of messages) {
|
||||
if (msg.role === "assistant" && msg.tool_calls) {
|
||||
for (const tc of msg.tool_calls) {
|
||||
rememberToolMeta(tc.id || "", tc.function?.name || "tool");
|
||||
}
|
||||
}
|
||||
if (msg.role === "assistant" && Array.isArray(msg.content)) {
|
||||
for (const part of msg.content as ToolUsePart[]) {
|
||||
if (part?.type !== "tool_use") continue;
|
||||
rememberToolMeta(part.id || "", part.name || "tool");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < messages.length; i++) {
|
||||
const msg = messages[i];
|
||||
@@ -21,83 +86,89 @@ function convertMessages(messages) {
|
||||
if (msg.role === "system") {
|
||||
result.push({
|
||||
role: "user",
|
||||
content: `[System Instructions]\n${msg.content}`,
|
||||
content: `[System Instructions]\n${extractContent(msg.content)}`,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
if (msg.role === "tool") {
|
||||
let toolContent = "";
|
||||
if (typeof msg.content === "string") {
|
||||
toolContent = msg.content;
|
||||
} else if (Array.isArray(msg.content)) {
|
||||
for (const part of msg.content) {
|
||||
if (part.type === "text") {
|
||||
toolContent += part.text;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const toolName = msg.name || "tool";
|
||||
const toolContent = extractContent(msg.content);
|
||||
const toolCallId = msg.tool_call_id || "";
|
||||
|
||||
// Accumulate tool result
|
||||
pendingToolResults.push({
|
||||
tool_call_id: toolCallId,
|
||||
name: toolName,
|
||||
index: pendingToolResults.length,
|
||||
raw_args: toolContent,
|
||||
const toolMeta = toolCallMetaMap.get(toolCallId) || {};
|
||||
const toolName = msg.name || toolMeta.name || "tool";
|
||||
result.push({
|
||||
role: "user",
|
||||
content: buildToolResultBlock(toolName, toolCallId, toolContent),
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
if (msg.role === "user" || msg.role === "assistant") {
|
||||
let content = "";
|
||||
|
||||
if (typeof msg.content === "string") {
|
||||
content = msg.content;
|
||||
} else if (Array.isArray(msg.content)) {
|
||||
for (const part of msg.content) {
|
||||
if (part.type === "text") {
|
||||
content += part.text;
|
||||
if (msg.role === "user" && Array.isArray(msg.content)) {
|
||||
const parts: string[] = [];
|
||||
for (const block of msg.content as Array<TextPart | ToolResultPart>) {
|
||||
if (!block || typeof block !== "object") continue;
|
||||
if (block.type === "text") {
|
||||
if (typeof (block as TextPart).text === "string") {
|
||||
parts.push((block as TextPart).text || "");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (block.type === "tool_result") {
|
||||
const tr = block as ToolResultPart;
|
||||
const toolCallId = tr.tool_use_id || "";
|
||||
const toolMeta =
|
||||
toolCallMetaMap.get(toolCallId) ||
|
||||
toolCallMetaMap.get(normalizeToolCallId(toolCallId));
|
||||
const toolName = toolMeta?.name || "tool";
|
||||
const toolContent = extractContent(tr.content);
|
||||
parts.push(buildToolResultBlock(toolName, toolCallId, toolContent));
|
||||
}
|
||||
}
|
||||
const joined = parts.filter(Boolean).join("\n");
|
||||
if (joined) result.push({ role: "user", content: joined });
|
||||
continue;
|
||||
}
|
||||
|
||||
// Keep tool_calls structure for assistant messages
|
||||
const content = extractContent(msg.content);
|
||||
|
||||
if (msg.role === "assistant" && msg.tool_calls && msg.tool_calls.length > 0) {
|
||||
const assistantMsg: {
|
||||
role: string;
|
||||
content?: string;
|
||||
tool_calls?: unknown;
|
||||
tool_results?: Array<Record<string, unknown>>;
|
||||
} = { role: "assistant" };
|
||||
if (content) {
|
||||
assistantMsg.content = content;
|
||||
}
|
||||
assistantMsg.tool_calls = msg.tool_calls;
|
||||
|
||||
// Attach pending tool results to assistant message with tool_calls
|
||||
if (pendingToolResults.length > 0) {
|
||||
assistantMsg.tool_results = pendingToolResults;
|
||||
pendingToolResults = [];
|
||||
}
|
||||
|
||||
} = { role: "assistant", content: content || "" };
|
||||
assistantMsg.tool_calls = msg.tool_calls.map((tc) => {
|
||||
const { index, ...rest } = tc || {};
|
||||
return rest;
|
||||
});
|
||||
result.push(assistantMsg);
|
||||
} else if (content || pendingToolResults.length > 0) {
|
||||
const msgObj: {
|
||||
role: string;
|
||||
content: string;
|
||||
tool_results?: Array<Record<string, unknown>>;
|
||||
} = { role: msg.role, content: content || "" };
|
||||
} else if (msg.role === "assistant" && Array.isArray(msg.content)) {
|
||||
const extractedToolCalls = (msg.content as ToolUsePart[])
|
||||
.filter((b) => b?.type === "tool_use")
|
||||
.map((b) => ({
|
||||
id: b.id || "",
|
||||
type: "function",
|
||||
function: {
|
||||
name: b.name || "tool",
|
||||
arguments: JSON.stringify(b.input || {}),
|
||||
},
|
||||
}))
|
||||
.filter((tc) => tc.id);
|
||||
|
||||
// Attach pending tool results to this message
|
||||
if (pendingToolResults.length > 0) {
|
||||
msgObj.tool_results = pendingToolResults;
|
||||
pendingToolResults = [];
|
||||
if (extractedToolCalls.length > 0) {
|
||||
result.push({
|
||||
role: "assistant",
|
||||
content: content || "",
|
||||
tool_calls: extractedToolCalls,
|
||||
});
|
||||
} else if (content) {
|
||||
result.push({ role: "assistant", content });
|
||||
}
|
||||
} else {
|
||||
if (content) {
|
||||
result.push({ role: msg.role, content });
|
||||
}
|
||||
|
||||
result.push(msgObj);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@ import {
|
||||
tryParseJSON,
|
||||
generateRequestId,
|
||||
generateSessionId,
|
||||
generateProjectId,
|
||||
cleanJSONSchemaForAntigravity,
|
||||
} from "../helpers/geminiHelper.ts";
|
||||
|
||||
@@ -321,13 +320,11 @@ export function openaiToGeminiCLIRequest(model, body, stream) {
|
||||
|
||||
// Wrap Gemini CLI format in Cloud Code wrapper
|
||||
function wrapInCloudCodeEnvelope(model, geminiCLI, credentials = null, isAntigravity = false) {
|
||||
const hasRealProject = !!credentials?.projectId;
|
||||
const projectId = credentials?.projectId || generateProjectId();
|
||||
const projectId = credentials?.projectId;
|
||||
|
||||
if (!hasRealProject) {
|
||||
console.warn(
|
||||
`[${isAntigravity ? "Antigravity" : "GeminiCLI"}] ⚠️ No projectId in credentials — using generated fallback "${projectId}". ` +
|
||||
`This may cause 404 errors. Ensure the OAuth token includes a valid GCP project.`
|
||||
if (!projectId) {
|
||||
throw new Error(
|
||||
`${isAntigravity ? "Antigravity" : "GeminiCLI"} account is missing projectId. Reconnect OAuth to load your real Cloud Code project before sending requests.`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -374,13 +371,11 @@ function wrapInCloudCodeEnvelope(model, geminiCLI, credentials = null, isAntigra
|
||||
}
|
||||
|
||||
function wrapInCloudCodeEnvelopeForClaude(model, claudeRequest, credentials = null) {
|
||||
const hasRealProject = !!credentials?.projectId;
|
||||
const projectId = credentials?.projectId || generateProjectId();
|
||||
const projectId = credentials?.projectId;
|
||||
|
||||
if (!hasRealProject) {
|
||||
console.warn(
|
||||
`[Antigravity/Claude] ⚠️ No projectId in credentials — using generated fallback "${projectId}". ` +
|
||||
`This may cause 404 errors. Ensure the OAuth token includes a valid GCP project.`
|
||||
if (!projectId) {
|
||||
throw new Error(
|
||||
"Antigravity/Claude account is missing projectId. Reconnect OAuth to load your real Cloud Code project before sending requests."
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -10,8 +10,9 @@
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import zlib from "zlib";
|
||||
|
||||
const DEBUG = true;
|
||||
const DEBUG = process.env.CURSOR_PROTOBUF_DEBUG === "1";
|
||||
const log = (tag, ...args) => DEBUG && console.log(`[PROTOBUF:${tag}]`, ...args);
|
||||
const textDecoder = new TextDecoder();
|
||||
|
||||
/**
|
||||
* Schema version — bump when updating field definitions.
|
||||
@@ -28,6 +29,7 @@ const ROLE = { USER: 1, ASSISTANT: 2 };
|
||||
const UNIFIED_MODE = { CHAT: 1, AGENT: 2 };
|
||||
|
||||
const THINKING_LEVEL = { UNSPECIFIED: 0, MEDIUM: 1, HIGH: 2 };
|
||||
const CLIENT_SIDE_TOOL_V2 = { MCP: 19 };
|
||||
|
||||
const FIELD = {
|
||||
// StreamUnifiedChatRequestWithTools (top level)
|
||||
@@ -74,6 +76,28 @@ const FIELD = {
|
||||
TOOL_RESULT_INDEX: 3,
|
||||
TOOL_RESULT_RAW_ARGS: 5,
|
||||
TOOL_RESULT_RESULT: 8,
|
||||
TOOL_RESULT_TOOL_CALL: 11,
|
||||
TOOL_RESULT_MODEL_CALL_ID: 12,
|
||||
|
||||
// ClientSideToolV2Result (nested inside ToolResult.result)
|
||||
CLIENT_RESULT_TOOL: 1,
|
||||
CLIENT_RESULT_MCP_RESULT: 28,
|
||||
CLIENT_RESULT_TOOL_CALL_ID: 35,
|
||||
CLIENT_RESULT_MODEL_CALL_ID: 48,
|
||||
CLIENT_RESULT_TOOL_INDEX: 49,
|
||||
|
||||
// MCPResult (nested inside ClientSideToolV2Result.mcp_result)
|
||||
MCP_RESULT_SELECTED_TOOL: 1,
|
||||
MCP_RESULT_RESULT: 2,
|
||||
|
||||
// ClientSideToolV2Call (nested inside ToolResult.tool_call)
|
||||
CLIENT_CALL_TOOL: 1,
|
||||
CLIENT_CALL_MCP_PARAMS: 27,
|
||||
CLIENT_CALL_TOOL_CALL_ID: 3,
|
||||
CLIENT_CALL_NAME: 9,
|
||||
CLIENT_CALL_RAW_ARGS: 10,
|
||||
CLIENT_CALL_TOOL_INDEX: 48,
|
||||
CLIENT_CALL_MODEL_CALL_ID: 49,
|
||||
|
||||
// Model
|
||||
MODEL_NAME: 1,
|
||||
@@ -120,6 +144,7 @@ const FIELD = {
|
||||
TOOL_NAME: 9,
|
||||
TOOL_RAW_ARGS: 10,
|
||||
TOOL_IS_LAST: 11,
|
||||
TOOL_IS_LAST_ALT: 15,
|
||||
TOOL_MCP_PARAMS: 27,
|
||||
|
||||
// MCPParams
|
||||
@@ -202,16 +227,149 @@ function concatArrays(...arrays) {
|
||||
// ==================== MESSAGE ENCODING ====================
|
||||
|
||||
export function encodeToolResult(toolResult) {
|
||||
const toolCallId = toolResult.tool_call_id || "";
|
||||
const toolName = toolResult.name || "";
|
||||
const toolIndex = toolResult.index || 0;
|
||||
const { toolCallId, modelCallId } = parseToolCallId(toolResult.tool_call_id || "");
|
||||
const rawToolName = toolResult.name || "";
|
||||
const toolName = formatCursorToolName(rawToolName);
|
||||
const { selectedTool, serverName } = parseCursorToolName(toolName);
|
||||
const toolIndex = toolResult.index > 0 ? toolResult.index : 1;
|
||||
const rawArgs = toolResult.raw_args || "{}";
|
||||
const resultContent = toolResult.result || "";
|
||||
const encodedResultMessage = encodeClientSideToolResult(
|
||||
toolCallId,
|
||||
modelCallId,
|
||||
selectedTool,
|
||||
toolIndex,
|
||||
resultContent
|
||||
);
|
||||
const encodedToolCallMessage = encodeClientSideToolCall(
|
||||
toolCallId,
|
||||
modelCallId,
|
||||
toolName,
|
||||
selectedTool,
|
||||
serverName,
|
||||
rawArgs,
|
||||
toolIndex
|
||||
);
|
||||
|
||||
return concatArrays(
|
||||
encodeField(FIELD.TOOL_RESULT_CALL_ID, WIRE_TYPE.LEN, toolCallId),
|
||||
encodeField(FIELD.TOOL_RESULT_NAME, WIRE_TYPE.LEN, toolName),
|
||||
encodeField(FIELD.TOOL_RESULT_INDEX, WIRE_TYPE.VARINT, toolIndex),
|
||||
encodeField(FIELD.TOOL_RESULT_RAW_ARGS, WIRE_TYPE.LEN, rawArgs)
|
||||
...(modelCallId
|
||||
? [encodeField(FIELD.TOOL_RESULT_MODEL_CALL_ID, WIRE_TYPE.LEN, modelCallId)]
|
||||
: []),
|
||||
encodeField(FIELD.TOOL_RESULT_RAW_ARGS, WIRE_TYPE.LEN, rawArgs),
|
||||
...(encodedResultMessage
|
||||
? [encodeField(FIELD.TOOL_RESULT_RESULT, WIRE_TYPE.LEN, encodedResultMessage)]
|
||||
: []),
|
||||
encodeField(FIELD.TOOL_RESULT_TOOL_CALL, WIRE_TYPE.LEN, encodedToolCallMessage)
|
||||
);
|
||||
}
|
||||
|
||||
function parseToolCallId(toolCallIdRaw) {
|
||||
if (typeof toolCallIdRaw !== "string" || toolCallIdRaw.length === 0) {
|
||||
return { toolCallId: "", modelCallId: null };
|
||||
}
|
||||
const delimiter = "\nmc_";
|
||||
const idx = toolCallIdRaw.indexOf(delimiter);
|
||||
if (idx >= 0) {
|
||||
return {
|
||||
toolCallId: toolCallIdRaw.slice(0, idx),
|
||||
modelCallId: toolCallIdRaw.slice(idx + delimiter.length),
|
||||
};
|
||||
}
|
||||
return { toolCallId: toolCallIdRaw, modelCallId: null };
|
||||
}
|
||||
|
||||
function formatCursorToolName(rawName) {
|
||||
const base = typeof rawName === "string" && rawName.length > 0 ? rawName : "tool";
|
||||
|
||||
if (base.startsWith("mcp__")) {
|
||||
const rest = base.slice("mcp__".length);
|
||||
const splitIdx = rest.indexOf("__");
|
||||
if (splitIdx >= 0) {
|
||||
const server = rest.slice(0, splitIdx) || "custom";
|
||||
const name = rest.slice(splitIdx + 2) || "tool";
|
||||
return `mcp_${server}_${name}`;
|
||||
}
|
||||
return `mcp_custom_${rest || "tool"}`;
|
||||
}
|
||||
|
||||
if (base.startsWith("mcp_")) return base;
|
||||
return `mcp_custom_${base}`;
|
||||
}
|
||||
|
||||
function parseCursorToolName(formattedName) {
|
||||
if (typeof formattedName !== "string" || !formattedName.startsWith("mcp_")) {
|
||||
return { serverName: "custom", selectedTool: formattedName || "tool" };
|
||||
}
|
||||
|
||||
const tail = formattedName.slice("mcp_".length);
|
||||
const splitIdx = tail.indexOf("_");
|
||||
if (splitIdx < 0) {
|
||||
return { serverName: "custom", selectedTool: tail || "tool" };
|
||||
}
|
||||
|
||||
return {
|
||||
serverName: tail.slice(0, splitIdx) || "custom",
|
||||
selectedTool: tail.slice(splitIdx + 1) || "tool",
|
||||
};
|
||||
}
|
||||
|
||||
function encodeClientSideToolResult(toolCallId, modelCallId, toolName, toolIndex, resultContent) {
|
||||
const outputText = typeof resultContent === "string" ? resultContent : "";
|
||||
const selectedTool = typeof toolName === "string" && toolName.length > 0 ? toolName : "tool";
|
||||
|
||||
const mcpResult = concatArrays(
|
||||
encodeField(FIELD.MCP_RESULT_SELECTED_TOOL, WIRE_TYPE.LEN, selectedTool),
|
||||
encodeField(FIELD.MCP_RESULT_RESULT, WIRE_TYPE.LEN, outputText)
|
||||
);
|
||||
|
||||
return concatArrays(
|
||||
encodeField(FIELD.CLIENT_RESULT_TOOL, WIRE_TYPE.VARINT, CLIENT_SIDE_TOOL_V2.MCP),
|
||||
encodeField(FIELD.CLIENT_RESULT_MCP_RESULT, WIRE_TYPE.LEN, mcpResult),
|
||||
...(toolCallId
|
||||
? [encodeField(FIELD.CLIENT_RESULT_TOOL_CALL_ID, WIRE_TYPE.LEN, toolCallId)]
|
||||
: []),
|
||||
...(modelCallId
|
||||
? [encodeField(FIELD.CLIENT_RESULT_MODEL_CALL_ID, WIRE_TYPE.LEN, modelCallId)]
|
||||
: []),
|
||||
encodeField(FIELD.CLIENT_RESULT_TOOL_INDEX, WIRE_TYPE.VARINT, toolIndex)
|
||||
);
|
||||
}
|
||||
|
||||
function encodeMcpParamsForCall(toolName, rawArgs, serverName) {
|
||||
const tool = concatArrays(
|
||||
encodeField(FIELD.MCP_TOOL_NAME, WIRE_TYPE.LEN, toolName || "tool"),
|
||||
encodeField(FIELD.MCP_TOOL_PARAMS, WIRE_TYPE.LEN, rawArgs || "{}"),
|
||||
encodeField(FIELD.MCP_TOOL_SERVER, WIRE_TYPE.LEN, serverName || "custom")
|
||||
);
|
||||
return encodeField(FIELD.MCP_TOOLS_LIST, WIRE_TYPE.LEN, tool);
|
||||
}
|
||||
|
||||
function encodeClientSideToolCall(
|
||||
toolCallId,
|
||||
modelCallId,
|
||||
toolName,
|
||||
selectedTool,
|
||||
serverName,
|
||||
rawArgs,
|
||||
toolIndex
|
||||
) {
|
||||
return concatArrays(
|
||||
encodeField(FIELD.CLIENT_CALL_TOOL, WIRE_TYPE.VARINT, CLIENT_SIDE_TOOL_V2.MCP),
|
||||
encodeField(
|
||||
FIELD.CLIENT_CALL_MCP_PARAMS,
|
||||
WIRE_TYPE.LEN,
|
||||
encodeMcpParamsForCall(selectedTool, rawArgs, serverName)
|
||||
),
|
||||
...(toolCallId ? [encodeField(FIELD.CLIENT_CALL_TOOL_CALL_ID, WIRE_TYPE.LEN, toolCallId)] : []),
|
||||
encodeField(FIELD.CLIENT_CALL_NAME, WIRE_TYPE.LEN, toolName || "tool"),
|
||||
encodeField(FIELD.CLIENT_CALL_RAW_ARGS, WIRE_TYPE.LEN, rawArgs || "{}"),
|
||||
encodeField(FIELD.CLIENT_CALL_TOOL_INDEX, WIRE_TYPE.VARINT, toolIndex > 0 ? toolIndex : 1),
|
||||
...(modelCallId
|
||||
? [encodeField(FIELD.CLIENT_CALL_MODEL_CALL_ID, WIRE_TYPE.LEN, modelCallId)]
|
||||
: [])
|
||||
);
|
||||
}
|
||||
|
||||
@@ -311,13 +469,70 @@ export function encodeRequest(messages, modelName, tools = [], reasoningEffort =
|
||||
const isAgentic = hasTools;
|
||||
const formattedMessages = [];
|
||||
const messageIds = [];
|
||||
const normalizedMessages = [];
|
||||
|
||||
// Prepare messages
|
||||
// Guardrail: split mixed assistant payload into separate assistant messages.
|
||||
for (let i = 0; i < messages.length; i++) {
|
||||
const msg = messages[i];
|
||||
const hasToolCalls = Array.isArray(msg?.tool_calls) && msg.tool_calls.length > 0;
|
||||
const hasToolResults = Array.isArray(msg?.tool_results) && msg.tool_results.length > 0;
|
||||
|
||||
if (msg?.role === "assistant" && hasToolCalls && hasToolResults) {
|
||||
log(
|
||||
"ENCODE",
|
||||
`normalizing mixed assistant tool payload at msg[${i}] (calls=${msg.tool_calls.length}, results=${msg.tool_results.length})`
|
||||
);
|
||||
|
||||
// Keep assistant tool call message without embedded results
|
||||
normalizedMessages.push({
|
||||
...msg,
|
||||
tool_results: [],
|
||||
});
|
||||
|
||||
// Avoid inserting duplicate assistant tool-result message if next one already matches
|
||||
const nextMsg = messages[i + 1];
|
||||
const nextHasToolResults =
|
||||
nextMsg?.role === "assistant" &&
|
||||
Array.isArray(nextMsg?.tool_results) &&
|
||||
nextMsg.tool_results.length > 0;
|
||||
const currentIds = new Set(
|
||||
msg.tool_results.map((tr) => tr?.tool_call_id).filter((id) => typeof id === "string")
|
||||
);
|
||||
const nextIds = new Set(
|
||||
(nextMsg?.tool_results || [])
|
||||
.map((tr) => tr?.tool_call_id)
|
||||
.filter((id) => typeof id === "string")
|
||||
);
|
||||
let sameIds = currentIds.size > 0 && currentIds.size === nextIds.size;
|
||||
if (sameIds) {
|
||||
for (const id of currentIds) {
|
||||
if (!nextIds.has(id)) {
|
||||
sameIds = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!(nextHasToolResults && sameIds)) {
|
||||
normalizedMessages.push({
|
||||
role: "assistant",
|
||||
content: "",
|
||||
tool_results: msg.tool_results,
|
||||
});
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
normalizedMessages.push(msg);
|
||||
}
|
||||
|
||||
// Prepare messages
|
||||
for (let i = 0; i < normalizedMessages.length; i++) {
|
||||
const msg = normalizedMessages[i];
|
||||
const role = msg.role === "user" ? ROLE.USER : ROLE.ASSISTANT;
|
||||
const msgId = uuidv4();
|
||||
const isLast = i === messages.length - 1;
|
||||
const isLast = i === normalizedMessages.length - 1;
|
||||
|
||||
formattedMessages.push({
|
||||
content: msg.content,
|
||||
@@ -535,18 +750,19 @@ function extractToolCall(toolCallData) {
|
||||
|
||||
// Extract tool call ID
|
||||
if (toolCall.has(FIELD.TOOL_ID)) {
|
||||
const fullId = new TextDecoder().decode(toolCall.get(FIELD.TOOL_ID)[0].value);
|
||||
toolCallId = fullId.split("\n")[0]; // Cursor returns multi-line ID, take first line
|
||||
toolCallId = textDecoder.decode(toolCall.get(FIELD.TOOL_ID)[0].value);
|
||||
}
|
||||
|
||||
// Extract tool name
|
||||
if (toolCall.has(FIELD.TOOL_NAME)) {
|
||||
toolName = new TextDecoder().decode(toolCall.get(FIELD.TOOL_NAME)[0].value);
|
||||
toolName = textDecoder.decode(toolCall.get(FIELD.TOOL_NAME)[0].value);
|
||||
}
|
||||
|
||||
// Extract is_last flag
|
||||
if (toolCall.has(FIELD.TOOL_IS_LAST)) {
|
||||
isLast = toolCall.get(FIELD.TOOL_IS_LAST)[0].value !== 0;
|
||||
} else if (toolCall.has(FIELD.TOOL_IS_LAST_ALT)) {
|
||||
isLast = toolCall.get(FIELD.TOOL_IS_LAST_ALT)[0].value !== 0;
|
||||
}
|
||||
|
||||
// Extract MCP params - nested real tool info
|
||||
@@ -558,11 +774,11 @@ function extractToolCall(toolCallData) {
|
||||
const tool = decodeMessage(mcpParams.get(FIELD.MCP_TOOLS_LIST)[0].value);
|
||||
|
||||
if (tool.has(FIELD.MCP_NESTED_NAME)) {
|
||||
toolName = new TextDecoder().decode(tool.get(FIELD.MCP_NESTED_NAME)[0].value);
|
||||
toolName = textDecoder.decode(tool.get(FIELD.MCP_NESTED_NAME)[0].value);
|
||||
}
|
||||
|
||||
if (tool.has(FIELD.MCP_NESTED_PARAMS)) {
|
||||
rawArgs = new TextDecoder().decode(tool.get(FIELD.MCP_NESTED_PARAMS)[0].value);
|
||||
rawArgs = textDecoder.decode(tool.get(FIELD.MCP_NESTED_PARAMS)[0].value);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -572,7 +788,7 @@ function extractToolCall(toolCallData) {
|
||||
|
||||
// Fallback to raw_args
|
||||
if (!rawArgs && toolCall.has(FIELD.TOOL_RAW_ARGS)) {
|
||||
rawArgs = new TextDecoder().decode(toolCall.get(FIELD.TOOL_RAW_ARGS)[0].value);
|
||||
rawArgs = textDecoder.decode(toolCall.get(FIELD.TOOL_RAW_ARGS)[0].value);
|
||||
}
|
||||
|
||||
if (toolCallId && toolName) {
|
||||
@@ -597,7 +813,7 @@ function extractTextAndThinking(responseData) {
|
||||
|
||||
// Extract text
|
||||
if (nested.has(FIELD.RESPONSE_TEXT)) {
|
||||
text = new TextDecoder().decode(nested.get(FIELD.RESPONSE_TEXT)[0].value);
|
||||
text = textDecoder.decode(nested.get(FIELD.RESPONSE_TEXT)[0].value);
|
||||
}
|
||||
|
||||
// Extract thinking
|
||||
@@ -605,7 +821,7 @@ function extractTextAndThinking(responseData) {
|
||||
try {
|
||||
const thinkingMsg = decodeMessage(nested.get(FIELD.THINKING)[0].value);
|
||||
if (thinkingMsg.has(FIELD.THINKING_TEXT)) {
|
||||
thinking = new TextDecoder().decode(thinkingMsg.get(FIELD.THINKING_TEXT)[0].value);
|
||||
thinking = textDecoder.decode(thinkingMsg.get(FIELD.THINKING_TEXT)[0].value);
|
||||
}
|
||||
} catch (err) {
|
||||
log("EXTRACT", `Thinking parse error: ${err.message}`);
|
||||
|
||||
Generated
+213
-312
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "omniroute",
|
||||
"version": "2.1.1",
|
||||
"version": "2.2.4",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "omniroute",
|
||||
"version": "2.1.1",
|
||||
"version": "2.2.4",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"workspaces": [
|
||||
@@ -18,6 +18,7 @@
|
||||
"bcryptjs": "^3.0.3",
|
||||
"better-sqlite3": "^12.6.2",
|
||||
"bottleneck": "^2.19.5",
|
||||
"dompurify": "^3.3.2",
|
||||
"express": "^5.2.1",
|
||||
"fetch-socks": "^1.3.2",
|
||||
"http-proxy-middleware": "^3.0.5",
|
||||
@@ -65,7 +66,7 @@
|
||||
"tailwindcss": "^4",
|
||||
"typescript": "^5.9.3",
|
||||
"typescript-eslint": "^8.56.0",
|
||||
"vitest": "^3.2.4",
|
||||
"vitest": "^4.0.18",
|
||||
"wait-on": "^9.0.4"
|
||||
},
|
||||
"engines": {
|
||||
@@ -837,15 +838,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/config-array": {
|
||||
"version": "0.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz",
|
||||
"integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==",
|
||||
"version": "0.21.2",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz",
|
||||
"integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@eslint/object-schema": "^2.1.7",
|
||||
"debug": "^4.3.1",
|
||||
"minimatch": "^3.1.2"
|
||||
"minimatch": "^3.1.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -878,20 +879,20 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/eslintrc": {
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz",
|
||||
"integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==",
|
||||
"version": "3.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz",
|
||||
"integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ajv": "^6.12.4",
|
||||
"ajv": "^6.14.0",
|
||||
"debug": "^4.3.2",
|
||||
"espree": "^10.0.1",
|
||||
"globals": "^14.0.0",
|
||||
"ignore": "^5.2.0",
|
||||
"import-fresh": "^3.2.1",
|
||||
"js-yaml": "^4.1.1",
|
||||
"minimatch": "^3.1.2",
|
||||
"minimatch": "^3.1.5",
|
||||
"strip-json-comments": "^3.1.1"
|
||||
},
|
||||
"engines": {
|
||||
@@ -902,9 +903,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/js": {
|
||||
"version": "9.39.3",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.3.tgz",
|
||||
"integrity": "sha512-1B1VkCq6FuUNlQvlBYb+1jDu/gV297TIs/OeiaSR9l1H27SVW55ONE1e1Vp16NqP683+xEGzxYtv4XCiDPaQiw==",
|
||||
"version": "9.39.4",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz",
|
||||
"integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -2985,9 +2986,9 @@
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@swc/helpers": {
|
||||
"version": "0.5.15",
|
||||
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
|
||||
"integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==",
|
||||
"version": "0.5.19",
|
||||
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.19.tgz",
|
||||
"integrity": "sha512-QamiFeIK3txNjgUTNppE6MiG3p7TdninpZu0E0PbqVh1a9FNLT2FRhisaa4NcaX52XVhA5l7Pk58Ft7Sqi/2sA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"tslib": "^2.8.0"
|
||||
@@ -3481,9 +3482,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "25.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.3.3.tgz",
|
||||
"integrity": "sha512-DpzbrH7wIcBaJibpKo9nnSQL0MTRdnWttGyE5haGwK86xgMOkFLp7vEyfQPGLOJh5wNYiJ3V9PmUMDhV9u8kkQ==",
|
||||
"version": "25.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.4.0.tgz",
|
||||
"integrity": "sha512-9wLpoeWuBlcbBpOY3XmzSTG3oscB6xjBEEtn+pYXTfhyXhIxC5FsBer2KTopBlvKEiW9l13po9fq+SJY/5lkhw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~7.18.0"
|
||||
@@ -3523,17 +3524,17 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "8.56.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.56.1.tgz",
|
||||
"integrity": "sha512-Jz9ZztpB37dNC+HU2HI28Bs9QXpzCz+y/twHOwhyrIRdbuVDxSytJNDl6z/aAKlaRIwC7y8wJdkBv7FxYGgi0A==",
|
||||
"version": "8.57.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.57.0.tgz",
|
||||
"integrity": "sha512-qeu4rTHR3/IaFORbD16gmjq9+rEs9fGKdX0kF6BKSfi+gCuG3RCKLlSBYzn/bGsY9Tj7KE/DAQStbp8AHJGHEQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/regexpp": "^4.12.2",
|
||||
"@typescript-eslint/scope-manager": "8.56.1",
|
||||
"@typescript-eslint/type-utils": "8.56.1",
|
||||
"@typescript-eslint/utils": "8.56.1",
|
||||
"@typescript-eslint/visitor-keys": "8.56.1",
|
||||
"@typescript-eslint/scope-manager": "8.57.0",
|
||||
"@typescript-eslint/type-utils": "8.57.0",
|
||||
"@typescript-eslint/utils": "8.57.0",
|
||||
"@typescript-eslint/visitor-keys": "8.57.0",
|
||||
"ignore": "^7.0.5",
|
||||
"natural-compare": "^1.4.0",
|
||||
"ts-api-utils": "^2.4.0"
|
||||
@@ -3546,7 +3547,7 @@
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@typescript-eslint/parser": "^8.56.1",
|
||||
"@typescript-eslint/parser": "^8.57.0",
|
||||
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
|
||||
"typescript": ">=4.8.4 <6.0.0"
|
||||
}
|
||||
@@ -3562,16 +3563,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/parser": {
|
||||
"version": "8.56.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.56.1.tgz",
|
||||
"integrity": "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==",
|
||||
"version": "8.57.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.57.0.tgz",
|
||||
"integrity": "sha512-XZzOmihLIr8AD1b9hL9ccNMzEMWt/dE2u7NyTY9jJG6YNiNthaD5XtUHVF2uCXZ15ng+z2hT3MVuxnUYhq6k1g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.56.1",
|
||||
"@typescript-eslint/types": "8.56.1",
|
||||
"@typescript-eslint/typescript-estree": "8.56.1",
|
||||
"@typescript-eslint/visitor-keys": "8.56.1",
|
||||
"@typescript-eslint/scope-manager": "8.57.0",
|
||||
"@typescript-eslint/types": "8.57.0",
|
||||
"@typescript-eslint/typescript-estree": "8.57.0",
|
||||
"@typescript-eslint/visitor-keys": "8.57.0",
|
||||
"debug": "^4.4.3"
|
||||
},
|
||||
"engines": {
|
||||
@@ -3587,14 +3588,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/project-service": {
|
||||
"version": "8.56.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.56.1.tgz",
|
||||
"integrity": "sha512-TAdqQTzHNNvlVFfR+hu2PDJrURiwKsUvxFn1M0h95BB8ah5jejas08jUWG4dBA68jDMI988IvtfdAI53JzEHOQ==",
|
||||
"version": "8.57.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.57.0.tgz",
|
||||
"integrity": "sha512-pR+dK0BlxCLxtWfaKQWtYr7MhKmzqZxuii+ZjuFlZlIGRZm22HnXFqa2eY+90MUz8/i80YJmzFGDUsi8dMOV5w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/tsconfig-utils": "^8.56.1",
|
||||
"@typescript-eslint/types": "^8.56.1",
|
||||
"@typescript-eslint/tsconfig-utils": "^8.57.0",
|
||||
"@typescript-eslint/types": "^8.57.0",
|
||||
"debug": "^4.4.3"
|
||||
},
|
||||
"engines": {
|
||||
@@ -3609,14 +3610,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/scope-manager": {
|
||||
"version": "8.56.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.56.1.tgz",
|
||||
"integrity": "sha512-YAi4VDKcIZp0O4tz/haYKhmIDZFEUPOreKbfdAN3SzUDMcPhJ8QI99xQXqX+HoUVq8cs85eRKnD+rne2UAnj2w==",
|
||||
"version": "8.57.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.57.0.tgz",
|
||||
"integrity": "sha512-nvExQqAHF01lUM66MskSaZulpPL5pgy5hI5RfrxviLgzZVffB5yYzw27uK/ft8QnKXI2X0LBrHJFr1TaZtAibw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.56.1",
|
||||
"@typescript-eslint/visitor-keys": "8.56.1"
|
||||
"@typescript-eslint/types": "8.57.0",
|
||||
"@typescript-eslint/visitor-keys": "8.57.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -3627,9 +3628,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/tsconfig-utils": {
|
||||
"version": "8.56.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.56.1.tgz",
|
||||
"integrity": "sha512-qOtCYzKEeyr3aR9f28mPJqBty7+DBqsdd63eO0yyDwc6vgThj2UjWfJIcsFeSucYydqcuudMOprZ+x1SpF3ZuQ==",
|
||||
"version": "8.57.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.57.0.tgz",
|
||||
"integrity": "sha512-LtXRihc5ytjJIQEH+xqjB0+YgsV4/tW35XKX3GTZHpWtcC8SPkT/d4tqdf1cKtesryHm2bgp6l555NYcT2NLvA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -3644,15 +3645,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/type-utils": {
|
||||
"version": "8.56.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.56.1.tgz",
|
||||
"integrity": "sha512-yB/7dxi7MgTtGhZdaHCemf7PuwrHMenHjmzgUW1aJpO+bBU43OycnM3Wn+DdvDO/8zzA9HlhaJ0AUGuvri4oGg==",
|
||||
"version": "8.57.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.57.0.tgz",
|
||||
"integrity": "sha512-yjgh7gmDcJ1+TcEg8x3uWQmn8ifvSupnPfjP21twPKrDP/pTHlEQgmKcitzF/rzPSmv7QjJ90vRpN4U+zoUjwQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.56.1",
|
||||
"@typescript-eslint/typescript-estree": "8.56.1",
|
||||
"@typescript-eslint/utils": "8.56.1",
|
||||
"@typescript-eslint/types": "8.57.0",
|
||||
"@typescript-eslint/typescript-estree": "8.57.0",
|
||||
"@typescript-eslint/utils": "8.57.0",
|
||||
"debug": "^4.4.3",
|
||||
"ts-api-utils": "^2.4.0"
|
||||
},
|
||||
@@ -3669,9 +3670,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/types": {
|
||||
"version": "8.56.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.56.1.tgz",
|
||||
"integrity": "sha512-dbMkdIUkIkchgGDIv7KLUpa0Mda4IYjo4IAMJUZ+3xNoUXxMsk9YtKpTHSChRS85o+H9ftm51gsK1dZReY9CVw==",
|
||||
"version": "8.57.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.57.0.tgz",
|
||||
"integrity": "sha512-dTLI8PEXhjUC7B9Kre+u0XznO696BhXcTlOn0/6kf1fHaQW8+VjJAVHJ3eTI14ZapTxdkOmc80HblPQLaEeJdg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -3683,16 +3684,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree": {
|
||||
"version": "8.56.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.56.1.tgz",
|
||||
"integrity": "sha512-qzUL1qgalIvKWAf9C1HpvBjif+Vm6rcT5wZd4VoMb9+Km3iS3Cv9DY6dMRMDtPnwRAFyAi7YXJpTIEXLvdfPxg==",
|
||||
"version": "8.57.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.57.0.tgz",
|
||||
"integrity": "sha512-m7faHcyVg0BT3VdYTlX8GdJEM7COexXxS6KqGopxdtkQRvBanK377QDHr4W/vIPAR+ah9+B/RclSW5ldVniO1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/project-service": "8.56.1",
|
||||
"@typescript-eslint/tsconfig-utils": "8.56.1",
|
||||
"@typescript-eslint/types": "8.56.1",
|
||||
"@typescript-eslint/visitor-keys": "8.56.1",
|
||||
"@typescript-eslint/project-service": "8.57.0",
|
||||
"@typescript-eslint/tsconfig-utils": "8.57.0",
|
||||
"@typescript-eslint/types": "8.57.0",
|
||||
"@typescript-eslint/visitor-keys": "8.57.0",
|
||||
"debug": "^4.4.3",
|
||||
"minimatch": "^10.2.2",
|
||||
"semver": "^7.7.3",
|
||||
@@ -3721,9 +3722,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
|
||||
"version": "5.0.3",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.3.tgz",
|
||||
"integrity": "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==",
|
||||
"version": "5.0.4",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz",
|
||||
"integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -3763,16 +3764,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/utils": {
|
||||
"version": "8.56.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.56.1.tgz",
|
||||
"integrity": "sha512-HPAVNIME3tABJ61siYlHzSWCGtOoeP2RTIaHXFMPqjrQKCGB9OgUVdiNgH7TJS2JNIQ5qQ4RsAUDuGaGme/KOA==",
|
||||
"version": "8.57.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.57.0.tgz",
|
||||
"integrity": "sha512-5iIHvpD3CZe06riAsbNxxreP+MuYgVUsV0n4bwLH//VJmgtt54sQeY2GszntJ4BjYCpMzrfVh2SBnUQTtys2lQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.9.1",
|
||||
"@typescript-eslint/scope-manager": "8.56.1",
|
||||
"@typescript-eslint/types": "8.56.1",
|
||||
"@typescript-eslint/typescript-estree": "8.56.1"
|
||||
"@typescript-eslint/scope-manager": "8.57.0",
|
||||
"@typescript-eslint/types": "8.57.0",
|
||||
"@typescript-eslint/typescript-estree": "8.57.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -3787,13 +3788,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/visitor-keys": {
|
||||
"version": "8.56.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.56.1.tgz",
|
||||
"integrity": "sha512-KiROIzYdEV85YygXw6BI/Dx4fnBlFQu6Mq4QE4MOH9fFnhohw6wX/OAvDY2/C+ut0I3RSPKenvZJIVYqJNkhEw==",
|
||||
"version": "8.57.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.57.0.tgz",
|
||||
"integrity": "sha512-zm6xx8UT/Xy2oSr2ZXD0pZo7Jx2XsCoID2IUh9YSTFRu7z+WdwYTRk6LhUftm1crwqbuoF6I8zAFeCMw0YjwDg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.56.1",
|
||||
"@typescript-eslint/types": "8.57.0",
|
||||
"eslint-visitor-keys": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
@@ -4087,39 +4088,40 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@vitest/expect": {
|
||||
"version": "3.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz",
|
||||
"integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==",
|
||||
"version": "4.0.18",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.18.tgz",
|
||||
"integrity": "sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@standard-schema/spec": "^1.0.0",
|
||||
"@types/chai": "^5.2.2",
|
||||
"@vitest/spy": "3.2.4",
|
||||
"@vitest/utils": "3.2.4",
|
||||
"chai": "^5.2.0",
|
||||
"tinyrainbow": "^2.0.0"
|
||||
"@vitest/spy": "4.0.18",
|
||||
"@vitest/utils": "4.0.18",
|
||||
"chai": "^6.2.1",
|
||||
"tinyrainbow": "^3.0.3"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/mocker": {
|
||||
"version": "3.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz",
|
||||
"integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==",
|
||||
"version": "4.0.18",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.18.tgz",
|
||||
"integrity": "sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/spy": "3.2.4",
|
||||
"@vitest/spy": "4.0.18",
|
||||
"estree-walker": "^3.0.3",
|
||||
"magic-string": "^0.30.17"
|
||||
"magic-string": "^0.30.21"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"msw": "^2.4.9",
|
||||
"vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0"
|
||||
"vite": "^6.0.0 || ^7.0.0-0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"msw": {
|
||||
@@ -4131,42 +4133,41 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/pretty-format": {
|
||||
"version": "3.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz",
|
||||
"integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==",
|
||||
"version": "4.0.18",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.18.tgz",
|
||||
"integrity": "sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tinyrainbow": "^2.0.0"
|
||||
"tinyrainbow": "^3.0.3"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/runner": {
|
||||
"version": "3.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz",
|
||||
"integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==",
|
||||
"version": "4.0.18",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.18.tgz",
|
||||
"integrity": "sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/utils": "3.2.4",
|
||||
"pathe": "^2.0.3",
|
||||
"strip-literal": "^3.0.0"
|
||||
"@vitest/utils": "4.0.18",
|
||||
"pathe": "^2.0.3"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/snapshot": {
|
||||
"version": "3.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz",
|
||||
"integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==",
|
||||
"version": "4.0.18",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.18.tgz",
|
||||
"integrity": "sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/pretty-format": "3.2.4",
|
||||
"magic-string": "^0.30.17",
|
||||
"@vitest/pretty-format": "4.0.18",
|
||||
"magic-string": "^0.30.21",
|
||||
"pathe": "^2.0.3"
|
||||
},
|
||||
"funding": {
|
||||
@@ -4174,28 +4175,24 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/spy": {
|
||||
"version": "3.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz",
|
||||
"integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==",
|
||||
"version": "4.0.18",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.18.tgz",
|
||||
"integrity": "sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tinyspy": "^4.0.3"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/utils": {
|
||||
"version": "3.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz",
|
||||
"integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==",
|
||||
"version": "4.0.18",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.18.tgz",
|
||||
"integrity": "sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/pretty-format": "3.2.4",
|
||||
"loupe": "^3.1.4",
|
||||
"tinyrainbow": "^2.0.0"
|
||||
"@vitest/pretty-format": "4.0.18",
|
||||
"tinyrainbow": "^3.0.3"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
@@ -4215,9 +4212,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/acorn": {
|
||||
"version": "8.15.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"version": "8.16.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
|
||||
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
@@ -4247,9 +4244,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/ajv": {
|
||||
"version": "6.12.6",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
||||
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
||||
"version": "6.14.0",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz",
|
||||
"integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -4851,16 +4848,6 @@
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cac": {
|
||||
"version": "6.7.14",
|
||||
"resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
|
||||
"integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/call-bind": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz",
|
||||
@@ -4940,18 +4927,11 @@
|
||||
"license": "CC-BY-4.0"
|
||||
},
|
||||
"node_modules/chai": {
|
||||
"version": "5.3.3",
|
||||
"resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz",
|
||||
"integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==",
|
||||
"version": "6.2.2",
|
||||
"resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz",
|
||||
"integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"assertion-error": "^2.0.1",
|
||||
"check-error": "^2.1.1",
|
||||
"deep-eql": "^5.0.1",
|
||||
"loupe": "^3.1.0",
|
||||
"pathval": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
@@ -4973,16 +4953,6 @@
|
||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/check-error": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz",
|
||||
"integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
}
|
||||
},
|
||||
"node_modules/chownr": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
|
||||
@@ -5571,16 +5541,6 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/deep-eql": {
|
||||
"version": "5.0.2",
|
||||
"resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz",
|
||||
"integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/deep-extend": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
|
||||
@@ -5715,10 +5675,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/dompurify": {
|
||||
"version": "3.2.7",
|
||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.7.tgz",
|
||||
"integrity": "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==",
|
||||
"version": "3.3.2",
|
||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.2.tgz",
|
||||
"integrity": "sha512-6obghkliLdmKa56xdbLOpUZ43pAR6xFy1uOrxBaIDjT+yaRuuybLjGS9eVBoSR/UPU5fq3OXClEHLJNGvbxKpQ==",
|
||||
"license": "(MPL-2.0 OR Apache-2.0)",
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@types/trusted-types": "^2.0.7"
|
||||
}
|
||||
@@ -6064,25 +6027,25 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eslint": {
|
||||
"version": "9.39.3",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.3.tgz",
|
||||
"integrity": "sha512-VmQ+sifHUbI/IcSopBCF/HO3YiHQx/AVd3UVyYL6weuwW+HvON9VYn5l6Zl1WZzPWXPNZrSQpxwkkZ/VuvJZzg==",
|
||||
"version": "9.39.4",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz",
|
||||
"integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.8.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
"@eslint/config-array": "^0.21.1",
|
||||
"@eslint/config-array": "^0.21.2",
|
||||
"@eslint/config-helpers": "^0.4.2",
|
||||
"@eslint/core": "^0.17.0",
|
||||
"@eslint/eslintrc": "^3.3.1",
|
||||
"@eslint/js": "9.39.3",
|
||||
"@eslint/eslintrc": "^3.3.5",
|
||||
"@eslint/js": "9.39.4",
|
||||
"@eslint/plugin-kit": "^0.4.1",
|
||||
"@humanfs/node": "^0.16.6",
|
||||
"@humanwhocodes/module-importer": "^1.0.1",
|
||||
"@humanwhocodes/retry": "^0.4.2",
|
||||
"@types/estree": "^1.0.6",
|
||||
"ajv": "^6.12.4",
|
||||
"ajv": "^6.14.0",
|
||||
"chalk": "^4.0.0",
|
||||
"cross-spawn": "^7.0.6",
|
||||
"debug": "^4.3.2",
|
||||
@@ -6101,7 +6064,7 @@
|
||||
"is-glob": "^4.0.0",
|
||||
"json-stable-stringify-without-jsonify": "^1.0.1",
|
||||
"lodash.merge": "^4.6.2",
|
||||
"minimatch": "^3.1.2",
|
||||
"minimatch": "^3.1.5",
|
||||
"natural-compare": "^1.4.0",
|
||||
"optionator": "^0.9.3"
|
||||
},
|
||||
@@ -8089,9 +8052,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/jose": {
|
||||
"version": "6.1.3",
|
||||
"resolved": "https://registry.npmjs.org/jose/-/jose-6.1.3.tgz",
|
||||
"integrity": "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==",
|
||||
"version": "6.2.1",
|
||||
"resolved": "https://registry.npmjs.org/jose/-/jose-6.2.1.tgz",
|
||||
"integrity": "sha512-jUaKr1yrbfaImV7R2TN/b3IcZzsw38/chqMpo2XJ7i2F8AfM/lA4G1goC3JVEwg0H7UldTmSt3P68nt31W7/mw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/panva"
|
||||
@@ -8501,9 +8464,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/lint-staged": {
|
||||
"version": "16.3.1",
|
||||
"resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-16.3.1.tgz",
|
||||
"integrity": "sha512-bqvvquXzFBAlSbluugR4KXAe4XnO/QZcKVszpkBtqLWa2KEiVy8n6Xp38OeUbv/gOJOX4Vo9u5pFt/ADvbm42Q==",
|
||||
"version": "16.3.2",
|
||||
"resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-16.3.2.tgz",
|
||||
"integrity": "sha512-xKqhC2AeXLwiAHXguxBjuChoTTWFC6Pees0SHPwOpwlvI3BH7ZADFPddAdN3pgo3aiKgPUx/bxE78JfUnxQnlg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -8628,13 +8591,6 @@
|
||||
"loose-envify": "cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/loupe": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz",
|
||||
"integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lowdb": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/lowdb/-/lowdb-7.0.1.tgz",
|
||||
@@ -8822,6 +8778,15 @@
|
||||
"marked": "14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/monaco-editor/node_modules/dompurify": {
|
||||
"version": "3.2.7",
|
||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.7.tgz",
|
||||
"integrity": "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==",
|
||||
"license": "(MPL-2.0 OR Apache-2.0)",
|
||||
"optionalDependencies": {
|
||||
"@types/trusted-types": "^2.0.7"
|
||||
}
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
@@ -9216,6 +9181,17 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/obug": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz",
|
||||
"integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
"https://github.com/sponsors/sxzz",
|
||||
"https://opencollective.com/debug"
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/on-exit-leak-free": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz",
|
||||
@@ -9448,16 +9424,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/pathval": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz",
|
||||
"integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 14.16"
|
||||
}
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
@@ -9966,15 +9932,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/recharts": {
|
||||
"version": "3.7.0",
|
||||
"resolved": "https://registry.npmjs.org/recharts/-/recharts-3.7.0.tgz",
|
||||
"integrity": "sha512-l2VCsy3XXeraxIID9fx23eCb6iCBsxUQDnE8tWm6DFdszVAO7WVY/ChAD9wVit01y6B2PMupYiMmQwhgPHc9Ew==",
|
||||
"version": "3.8.0",
|
||||
"resolved": "https://registry.npmjs.org/recharts/-/recharts-3.8.0.tgz",
|
||||
"integrity": "sha512-Z/m38DX3L73ExO4Tpc9/iZWHmHnlzWG4njQbxsF5aSjwqmHNDDIm0rdEBArkwsBvR8U6EirlEHiQNYWCVh9sGQ==",
|
||||
"license": "MIT",
|
||||
"workspaces": [
|
||||
"www"
|
||||
],
|
||||
"dependencies": {
|
||||
"@reduxjs/toolkit": "1.x.x || 2.x.x",
|
||||
"@reduxjs/toolkit": "^1.9.0 || 2.x.x",
|
||||
"clsx": "^2.1.1",
|
||||
"decimal.js-light": "^2.5.1",
|
||||
"es-toolkit": "^1.39.3",
|
||||
@@ -11082,26 +11048,6 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-literal": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz",
|
||||
"integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"js-tokens": "^9.0.1"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-literal/node_modules/js-tokens": {
|
||||
"version": "9.0.1",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz",
|
||||
"integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/styled-jsx": {
|
||||
"version": "5.1.6",
|
||||
"resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz",
|
||||
@@ -11283,30 +11229,10 @@
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/tinypool": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz",
|
||||
"integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^18.0.0 || >=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tinyrainbow": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz",
|
||||
"integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tinyspy": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz",
|
||||
"integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==",
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz",
|
||||
"integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -11559,16 +11485,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/typescript-eslint": {
|
||||
"version": "8.56.1",
|
||||
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.56.1.tgz",
|
||||
"integrity": "sha512-U4lM6pjmBX7J5wk4szltF7I1cGBHXZopnAXCMXb3+fZ3B/0Z3hq3wS/CCUB2NZBNAExK92mCU2tEohWuwVMsDQ==",
|
||||
"version": "8.57.0",
|
||||
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.57.0.tgz",
|
||||
"integrity": "sha512-W8GcigEMEeB07xEZol8oJ26rigm3+bfPHxHvwbYUlu1fUDsGuQ7Hiskx5xGW/xM4USc9Ephe3jtv7ZYPQntHeA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/eslint-plugin": "8.56.1",
|
||||
"@typescript-eslint/parser": "8.56.1",
|
||||
"@typescript-eslint/typescript-estree": "8.56.1",
|
||||
"@typescript-eslint/utils": "8.56.1"
|
||||
"@typescript-eslint/eslint-plugin": "8.57.0",
|
||||
"@typescript-eslint/parser": "8.57.0",
|
||||
"@typescript-eslint/typescript-estree": "8.57.0",
|
||||
"@typescript-eslint/utils": "8.57.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -11856,29 +11782,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/vite-node": {
|
||||
"version": "3.2.4",
|
||||
"resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz",
|
||||
"integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cac": "^6.7.14",
|
||||
"debug": "^4.4.1",
|
||||
"es-module-lexer": "^1.7.0",
|
||||
"pathe": "^2.0.3",
|
||||
"vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0"
|
||||
},
|
||||
"bin": {
|
||||
"vite-node": "vite-node.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.0.0 || ^20.0.0 || >=22.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/fdir": {
|
||||
"version": "6.5.0",
|
||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
||||
@@ -11926,51 +11829,50 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vitest": {
|
||||
"version": "3.2.4",
|
||||
"resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz",
|
||||
"integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==",
|
||||
"version": "4.0.18",
|
||||
"resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.18.tgz",
|
||||
"integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/chai": "^5.2.2",
|
||||
"@vitest/expect": "3.2.4",
|
||||
"@vitest/mocker": "3.2.4",
|
||||
"@vitest/pretty-format": "^3.2.4",
|
||||
"@vitest/runner": "3.2.4",
|
||||
"@vitest/snapshot": "3.2.4",
|
||||
"@vitest/spy": "3.2.4",
|
||||
"@vitest/utils": "3.2.4",
|
||||
"chai": "^5.2.0",
|
||||
"debug": "^4.4.1",
|
||||
"expect-type": "^1.2.1",
|
||||
"magic-string": "^0.30.17",
|
||||
"@vitest/expect": "4.0.18",
|
||||
"@vitest/mocker": "4.0.18",
|
||||
"@vitest/pretty-format": "4.0.18",
|
||||
"@vitest/runner": "4.0.18",
|
||||
"@vitest/snapshot": "4.0.18",
|
||||
"@vitest/spy": "4.0.18",
|
||||
"@vitest/utils": "4.0.18",
|
||||
"es-module-lexer": "^1.7.0",
|
||||
"expect-type": "^1.2.2",
|
||||
"magic-string": "^0.30.21",
|
||||
"obug": "^2.1.1",
|
||||
"pathe": "^2.0.3",
|
||||
"picomatch": "^4.0.2",
|
||||
"std-env": "^3.9.0",
|
||||
"picomatch": "^4.0.3",
|
||||
"std-env": "^3.10.0",
|
||||
"tinybench": "^2.9.0",
|
||||
"tinyexec": "^0.3.2",
|
||||
"tinyglobby": "^0.2.14",
|
||||
"tinypool": "^1.1.1",
|
||||
"tinyrainbow": "^2.0.0",
|
||||
"vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0",
|
||||
"vite-node": "3.2.4",
|
||||
"tinyexec": "^1.0.2",
|
||||
"tinyglobby": "^0.2.15",
|
||||
"tinyrainbow": "^3.0.3",
|
||||
"vite": "^6.0.0 || ^7.0.0",
|
||||
"why-is-node-running": "^2.3.0"
|
||||
},
|
||||
"bin": {
|
||||
"vitest": "vitest.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.0.0 || ^20.0.0 || >=22.0.0"
|
||||
"node": "^20.0.0 || ^22.0.0 || >=24.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@edge-runtime/vm": "*",
|
||||
"@types/debug": "^4.1.12",
|
||||
"@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
|
||||
"@vitest/browser": "3.2.4",
|
||||
"@vitest/ui": "3.2.4",
|
||||
"@opentelemetry/api": "^1.9.0",
|
||||
"@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0",
|
||||
"@vitest/browser-playwright": "4.0.18",
|
||||
"@vitest/browser-preview": "4.0.18",
|
||||
"@vitest/browser-webdriverio": "4.0.18",
|
||||
"@vitest/ui": "4.0.18",
|
||||
"happy-dom": "*",
|
||||
"jsdom": "*"
|
||||
},
|
||||
@@ -11978,13 +11880,19 @@
|
||||
"@edge-runtime/vm": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/debug": {
|
||||
"@opentelemetry/api": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/node": {
|
||||
"optional": true
|
||||
},
|
||||
"@vitest/browser": {
|
||||
"@vitest/browser-playwright": {
|
||||
"optional": true
|
||||
},
|
||||
"@vitest/browser-preview": {
|
||||
"optional": true
|
||||
},
|
||||
"@vitest/browser-webdriverio": {
|
||||
"optional": true
|
||||
},
|
||||
"@vitest/ui": {
|
||||
@@ -12011,13 +11919,6 @@
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/vitest/node_modules/tinyexec": {
|
||||
"version": "0.3.2",
|
||||
"resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz",
|
||||
"integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/wait-on": {
|
||||
"version": "9.0.4",
|
||||
"resolved": "https://registry.npmjs.org/wait-on/-/wait-on-9.0.4.tgz",
|
||||
|
||||
+8
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "omniroute",
|
||||
"version": "2.1.2",
|
||||
"version": "2.2.4",
|
||||
"description": "Smart AI Router with auto fallback — route to FREE & cheap models, zero downtime. Works with Cursor, Cline, Claude Desktop, Codex, and any OpenAI-compatible tool.",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
@@ -76,7 +76,8 @@
|
||||
"check": "npm run lint && npm run test",
|
||||
"prepublishOnly": "npm run build:cli",
|
||||
"postinstall": "node scripts/postinstall.mjs",
|
||||
"prepare": "husky"
|
||||
"prepare": "husky",
|
||||
"system-info": "node scripts/system-info.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.27.1",
|
||||
@@ -84,6 +85,7 @@
|
||||
"bcryptjs": "^3.0.3",
|
||||
"better-sqlite3": "^12.6.2",
|
||||
"bottleneck": "^2.19.5",
|
||||
"dompurify": "^3.3.2",
|
||||
"express": "^5.2.1",
|
||||
"fetch-socks": "^1.3.2",
|
||||
"http-proxy-middleware": "^3.0.5",
|
||||
@@ -127,7 +129,7 @@
|
||||
"tailwindcss": "^4",
|
||||
"typescript": "^5.9.3",
|
||||
"typescript-eslint": "^8.56.0",
|
||||
"vitest": "^3.2.4",
|
||||
"vitest": "^4.0.18",
|
||||
"wait-on": "^9.0.4"
|
||||
},
|
||||
"lint-staged": {
|
||||
@@ -138,5 +140,8 @@
|
||||
"*.{json,md,yml,yaml,css}": [
|
||||
"prettier --write"
|
||||
]
|
||||
},
|
||||
"overrides": {
|
||||
"@swc/helpers": "^0.5.19"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* system-info.mjs — OmniRoute System Information Reporter (#280)
|
||||
*
|
||||
* Collects system/environment info for bug reports.
|
||||
* Usage: node scripts/system-info.mjs [--output system-info.txt]
|
||||
*
|
||||
* Output includes:
|
||||
* - Node.js version
|
||||
* - OmniRoute version
|
||||
* - OS info
|
||||
* - Relevant system packages (if apt available)
|
||||
* - Agent CLI tools (iflow, gemini, claude, codex, antigravity, droid, etc.)
|
||||
* - Docker / PM2 status
|
||||
*/
|
||||
|
||||
import { execSync } from "child_process";
|
||||
import { readFileSync, writeFileSync, existsSync } from "fs";
|
||||
import { join, dirname } from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import os from "os";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const ROOT = join(__dirname, "..");
|
||||
|
||||
// ── Helpers ────────────────────────────────────────────────────────────────
|
||||
|
||||
function run(cmd, fallback = "N/A") {
|
||||
try {
|
||||
return execSync(cmd, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
||||
} catch {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
function toolVersion(cmd, args = "--version") {
|
||||
const version = run(`${cmd} ${args}`, null);
|
||||
if (version === null) return "not installed";
|
||||
// Trim to first line, remove prefixes like "v", "Version: "
|
||||
return version
|
||||
.split("\n")[0]
|
||||
.replace(/^(version\s*:?\s*|v)/i, "")
|
||||
.trim();
|
||||
}
|
||||
|
||||
function section(title) {
|
||||
const line = "─".repeat(60);
|
||||
return `\n${line}\n ${title}\n${line}\n`;
|
||||
}
|
||||
|
||||
// ── Collect Info ──────────────────────────────────────────────────────────
|
||||
|
||||
const lines = [];
|
||||
|
||||
lines.push("OmniRoute System Information Report");
|
||||
lines.push(`Generated: ${new Date().toISOString()}`);
|
||||
|
||||
// ── Node.js & Runtime ────────────────────────────────────────────────────
|
||||
|
||||
lines.push(section("Node.js & Runtime"));
|
||||
lines.push(`Node.js: ${process.version}`);
|
||||
lines.push(`npm: v${run("npm --version")}`);
|
||||
lines.push(`Platform: ${process.platform} (${process.arch})`);
|
||||
lines.push(`OS: ${os.type()} ${os.release()} (${os.arch()})`);
|
||||
lines.push(`Hostname: ${os.hostname()}`);
|
||||
lines.push(`CPUs: ${os.cpus().length}x ${os.cpus()[0]?.model || "unknown"}`);
|
||||
lines.push(`Total RAM: ${Math.round(os.totalmem() / 1024 / 1024)} MB`);
|
||||
lines.push(`Free RAM: ${Math.round(os.freemem() / 1024 / 1024)} MB`);
|
||||
|
||||
// ── OmniRoute Version ────────────────────────────────────────────────────
|
||||
|
||||
lines.push(section("OmniRoute"));
|
||||
try {
|
||||
const pkg = JSON.parse(readFileSync(join(ROOT, "package.json"), "utf-8"));
|
||||
lines.push(`Version: ${pkg.version}`);
|
||||
lines.push(`Name: ${pkg.name}`);
|
||||
} catch {
|
||||
lines.push("Version: unable to read package.json");
|
||||
}
|
||||
|
||||
const installedGlobal = run("npm list -g omniroute --depth=0 2>/dev/null | grep omniroute");
|
||||
lines.push(`Global npm: ${installedGlobal || "not installed globally"}`);
|
||||
|
||||
const pm2Status = run("pm2 list 2>/dev/null | grep omniroute | awk '{print $4, $10, $12}'");
|
||||
lines.push(`PM2 status: ${pm2Status || "not running via PM2"}`);
|
||||
|
||||
// ── Agent CLI Tools ──────────────────────────────────────────────────────
|
||||
|
||||
lines.push(section("Agent CLI Tools"));
|
||||
|
||||
const cliTools = [
|
||||
{ name: "iflow-cli", cmd: "iflow", args: "--version" },
|
||||
{ name: "gemini-cli", cmd: "gemini", args: "--version" },
|
||||
{ name: "claude-code", cmd: "claude", args: "--version" },
|
||||
{ name: "openai-codex", cmd: "codex", args: "--version" },
|
||||
{ name: "antigravity", cmd: "antigravity", args: "--version" },
|
||||
{ name: "droid", cmd: "droid", args: "--version" },
|
||||
{ name: "openclaw", cmd: "openclaw", args: "--version" },
|
||||
{ name: "kilo", cmd: "kilo", args: "--version" },
|
||||
{ name: "cursor", cmd: "cursor", args: "--version" },
|
||||
{ name: "aider", cmd: "aider", args: "--version" },
|
||||
];
|
||||
|
||||
for (const { name, cmd, args } of cliTools) {
|
||||
const v = toolVersion(cmd, args);
|
||||
lines.push(`${name.padEnd(20)} ${v}`);
|
||||
}
|
||||
|
||||
// ── Docker ───────────────────────────────────────────────────────────────
|
||||
|
||||
lines.push(section("Docker"));
|
||||
lines.push(`Docker: ${run("docker --version", "not installed")}`);
|
||||
|
||||
const dockerContainers = run(
|
||||
"docker ps --format 'table {{.Names}}\t{{.Image}}\t{{.Status}}' 2>/dev/null",
|
||||
"N/A"
|
||||
);
|
||||
lines.push(`Containers:\n${dockerContainers}`);
|
||||
|
||||
// ── System Packages ──────────────────────────────────────────────────────
|
||||
|
||||
lines.push(section("System Packages (relevant)"));
|
||||
|
||||
const relevantPkgs = ["build-essential", "libssl-dev", "openssl", "libsqlite3-dev", "python3"];
|
||||
for (const pkg of relevantPkgs) {
|
||||
const ver = run(`dpkg -l ${pkg} 2>/dev/null | grep '^ii' | awk '{print $3}'`, "not found");
|
||||
lines.push(`${pkg.padEnd(24)} ${ver}`);
|
||||
}
|
||||
|
||||
// ── Environment Variables (safe subset) ─────────────────────────────────
|
||||
|
||||
lines.push(section("Environment Variables (non-sensitive)"));
|
||||
|
||||
const safeEnvKeys = [
|
||||
"NODE_ENV",
|
||||
"PORT",
|
||||
"DATA_DIR",
|
||||
"DB_BACKUPS_DIR",
|
||||
"LOG_LEVEL",
|
||||
"NEXT_PUBLIC_APP_URL",
|
||||
"ROUTER_API_KEY_HINT",
|
||||
];
|
||||
|
||||
for (const key of safeEnvKeys) {
|
||||
const val = process.env[key];
|
||||
if (val !== undefined) {
|
||||
// Mask if looks like a secret
|
||||
const masked = val.length > 8 ? val.substring(0, 4) + "****" : "****";
|
||||
lines.push(`${key.padEnd(28)} ${masked}`);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Output ───────────────────────────────────────────────────────────────
|
||||
|
||||
const report = lines.join("\n") + "\n";
|
||||
|
||||
// Write to file
|
||||
const outArg = process.argv.find((a) => a.startsWith("--output="));
|
||||
const outFile = outArg
|
||||
? outArg.replace("--output=", "")
|
||||
: process.argv[process.argv.indexOf("--output") + 1] || "system-info.txt";
|
||||
|
||||
const outPath = join(ROOT, outFile);
|
||||
|
||||
writeFileSync(outPath, report);
|
||||
console.log(report);
|
||||
console.log(`\n✅ Report saved to: ${outPath}`);
|
||||
console.log(
|
||||
`📎 Attach this file when reporting issues at: https://github.com/diegosouzapw/OmniRoute/issues`
|
||||
);
|
||||
@@ -868,7 +868,7 @@ export default function ProviderDetailPage() {
|
||||
: t("providerProxy")}
|
||||
</button>
|
||||
</div>
|
||||
{!isCompatible && (
|
||||
{!isCompatible ? (
|
||||
<Button
|
||||
size="sm"
|
||||
icon="add"
|
||||
@@ -876,6 +876,12 @@ export default function ProviderDetailPage() {
|
||||
>
|
||||
{t("add")}
|
||||
</Button>
|
||||
) : (
|
||||
connections.length === 0 && (
|
||||
<Button size="sm" icon="add" onClick={() => setShowAddApiKeyModal(true)}>
|
||||
{t("add")}
|
||||
</Button>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ const PROVIDER_CONFIG = {
|
||||
kiro: { label: "Kiro AI", color: "#FF6B35" },
|
||||
codex: { label: "OpenAI Codex", color: "#10A37F" },
|
||||
claude: { label: "Claude Code", color: "#D97757" },
|
||||
"kimi-coding": { label: "Kimi Coding", color: "#1E3A8A" },
|
||||
};
|
||||
|
||||
const TIER_FILTERS = [
|
||||
@@ -236,7 +237,7 @@ export default function ProviderLimits() {
|
||||
);
|
||||
|
||||
const sortedConnections = useMemo(() => {
|
||||
const priority = { antigravity: 1, github: 2, codex: 3, claude: 4, kiro: 5 };
|
||||
const priority = { antigravity: 1, github: 2, codex: 3, claude: 4, kiro: 5, "kimi-coding": 6 };
|
||||
return [...filteredConnections].sort(
|
||||
(a, b) => (priority[a.provider] || 9) - (priority[b.provider] || 9)
|
||||
);
|
||||
|
||||
@@ -10,6 +10,19 @@ import { isValidationFailure, validateBody } from "@/shared/validation/helpers";
|
||||
|
||||
const PROFILES_DIR = path.join(resolveDataDir(), "codex-profiles");
|
||||
|
||||
/**
|
||||
* Resolve a path inside PROFILES_DIR and verify it stays within bounds.
|
||||
* Throws on path traversal attempts.
|
||||
*/
|
||||
function safeProfilePath(...segments: string[]): string {
|
||||
const resolved = path.resolve(PROFILES_DIR, ...segments);
|
||||
const base = path.resolve(PROFILES_DIR);
|
||||
if (resolved !== base && !resolved.startsWith(base + path.sep)) {
|
||||
throw new Error("Invalid path: directory traversal detected");
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure profiles directory exists
|
||||
*/
|
||||
|
||||
@@ -247,6 +247,14 @@ const PROVIDER_MODELS_CONFIG: Record<string, ProviderModelsConfigEntry> = {
|
||||
authPrefix: "Bearer ",
|
||||
parseResponse: (data) => data.data || data.models || [],
|
||||
},
|
||||
"ollama-cloud": {
|
||||
url: "https://api.ollama.com/v1/models",
|
||||
method: "GET",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
authHeader: "Authorization",
|
||||
authPrefix: "Bearer ",
|
||||
parseResponse: (data) => data.models || data.data || [],
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -389,7 +397,13 @@ export async function GET(request, { params }) {
|
||||
// Get auth token
|
||||
const token = accessToken || apiKey;
|
||||
if (!token) {
|
||||
return NextResponse.json({ error: "No valid token found" }, { status: 401 });
|
||||
return NextResponse.json(
|
||||
{
|
||||
error:
|
||||
"No API key configured for this provider. Please add an API key in the provider settings.",
|
||||
},
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Build request URL
|
||||
|
||||
@@ -131,6 +131,10 @@
|
||||
"homeDescription": "Welcome to OmniRoute",
|
||||
"endpoint": "Endpoints",
|
||||
"endpointDescription": "Manage proxy endpoints, MCP, A2A, and API endpoints",
|
||||
"mcp": "MCP",
|
||||
"mcpDescription": "Model Context Protocol server management and tools",
|
||||
"a2a": "A2A",
|
||||
"a2aDescription": "Agent-to-Agent protocol tasks and observability",
|
||||
"settings": "Settings",
|
||||
"settingsDescription": "Manage your preferences",
|
||||
"openaiCompatible": "OpenAI Compatible",
|
||||
|
||||
+17
-2
@@ -159,11 +159,26 @@ export async function listDbBackups() {
|
||||
|
||||
export async function restoreDbBackup(backupId: string) {
|
||||
const backupDir = DB_BACKUPS_DIR || path.join(DATA_DIR, "db_backups");
|
||||
const backupPath = path.join(backupDir, backupId);
|
||||
|
||||
if (!backupId.startsWith("db_") || !backupId.endsWith(".sqlite")) {
|
||||
// Validate format: must be db_<timestamp>_<reason>.sqlite, no path separators
|
||||
if (
|
||||
!backupId.startsWith("db_") ||
|
||||
!backupId.endsWith(".sqlite") ||
|
||||
backupId.includes(path.sep) ||
|
||||
backupId.includes("/")
|
||||
) {
|
||||
throw new Error("Invalid backup ID");
|
||||
}
|
||||
|
||||
const backupPath = path.resolve(backupDir, backupId);
|
||||
// Prevent path traversal: resolved path must stay within backupDir
|
||||
if (
|
||||
!backupPath.startsWith(path.resolve(backupDir) + path.sep) &&
|
||||
backupPath !== path.resolve(backupDir)
|
||||
) {
|
||||
throw new Error("Invalid backup ID: path traversal detected");
|
||||
}
|
||||
|
||||
if (!fs.existsSync(backupPath)) {
|
||||
throw new Error(`Backup not found: ${backupId}`);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,26 @@
|
||||
import { KIMI_CODING_CONFIG } from "../constants/oauth";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { hostname } from "node:os";
|
||||
|
||||
// Generate device ID (persistent per installation)
|
||||
const DEVICE_ID = randomUUID();
|
||||
const PLATFORM = "omniroute";
|
||||
const VERSION = "2.1.2";
|
||||
const DEVICE_NAME = hostname();
|
||||
const DEVICE_MODEL = `${process.platform} ${process.arch}`;
|
||||
|
||||
// Custom headers required by Kimi OAuth
|
||||
function getKimiOAuthHeaders() {
|
||||
return {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
Accept: "application/json",
|
||||
"X-Msh-Platform": PLATFORM,
|
||||
"X-Msh-Version": VERSION,
|
||||
"X-Msh-Device-Name": DEVICE_NAME,
|
||||
"X-Msh-Device-Model": DEVICE_MODEL,
|
||||
"X-Msh-Device-Id": DEVICE_ID,
|
||||
};
|
||||
}
|
||||
|
||||
export const kimiCoding = {
|
||||
config: KIMI_CODING_CONFIG,
|
||||
@@ -6,10 +28,7 @@ export const kimiCoding = {
|
||||
requestDeviceCode: async (config) => {
|
||||
const response = await fetch(config.deviceCodeUrl, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
Accept: "application/json",
|
||||
},
|
||||
headers: getKimiOAuthHeaders(),
|
||||
body: new URLSearchParams({
|
||||
client_id: config.clientId,
|
||||
}),
|
||||
@@ -24,10 +43,10 @@ export const kimiCoding = {
|
||||
return {
|
||||
device_code: data.device_code,
|
||||
user_code: data.user_code,
|
||||
verification_uri: data.verification_uri || `https://www.kimi.com/code/authorize_device`,
|
||||
verification_uri: data.verification_uri || `https://auth.kimi.com/activate`,
|
||||
verification_uri_complete:
|
||||
data.verification_uri_complete ||
|
||||
`https://www.kimi.com/code/authorize_device?user_code=${data.user_code}`,
|
||||
`https://auth.kimi.com/activate?user_code=${data.user_code}`,
|
||||
expires_in: data.expires_in,
|
||||
interval: data.interval || 5,
|
||||
};
|
||||
@@ -35,14 +54,11 @@ export const kimiCoding = {
|
||||
pollToken: async (config, deviceCode) => {
|
||||
const response = await fetch(config.tokenUrl, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
Accept: "application/json",
|
||||
},
|
||||
headers: getKimiOAuthHeaders(),
|
||||
body: new URLSearchParams({
|
||||
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
|
||||
client_id: config.clientId,
|
||||
device_code: deviceCode,
|
||||
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -63,5 +79,7 @@ export const kimiCoding = {
|
||||
accessToken: tokens.access_token,
|
||||
refreshToken: tokens.refresh_token,
|
||||
expiresIn: tokens.expires_in,
|
||||
tokenType: tokens.token_type,
|
||||
scope: tokens.scope,
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -250,6 +250,56 @@ async function validateNanoBananaProvider({ apiKey }: any) {
|
||||
}
|
||||
}
|
||||
|
||||
async function validateElevenLabsProvider({ apiKey }: any) {
|
||||
try {
|
||||
// Lightweight auth check endpoint
|
||||
const response = await fetch("https://api.elevenlabs.io/v1/voices", {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"xi-api-key": apiKey,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
if (response.ok) return { valid: true, error: null };
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
return { valid: false, error: "Invalid API key" };
|
||||
}
|
||||
|
||||
return { valid: false, error: `Validation failed: ${response.status}` };
|
||||
} catch (error: any) {
|
||||
return { valid: false, error: error.message || "Validation failed" };
|
||||
}
|
||||
}
|
||||
|
||||
async function validateInworldProvider({ apiKey }: any) {
|
||||
try {
|
||||
// Inworld TTS lacks a simple key-introspection endpoint.
|
||||
// Send a minimal synth request and treat non-auth 4xx as auth-pass.
|
||||
const response = await fetch("https://api.inworld.ai/tts/v1/voice", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Basic ${apiKey}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
text: "test",
|
||||
modelId: "inworld-tts-1.5-mini",
|
||||
audioConfig: { audioEncoding: "MP3" },
|
||||
}),
|
||||
});
|
||||
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
return { valid: false, error: "Invalid API key" };
|
||||
}
|
||||
|
||||
// Any other response indicates auth is accepted (payload/model may still be wrong)
|
||||
return { valid: true, error: null };
|
||||
} catch (error: any) {
|
||||
return { valid: false, error: error.message || "Validation failed" };
|
||||
}
|
||||
}
|
||||
|
||||
async function validateOpenAICompatibleProvider({ apiKey, providerSpecificData = {} }: any) {
|
||||
const baseUrl = normalizeBaseUrl(providerSpecificData.baseUrl);
|
||||
if (!baseUrl) {
|
||||
@@ -416,6 +466,8 @@ export async function validateProviderApiKey({ provider, apiKey, providerSpecifi
|
||||
deepgram: validateDeepgramProvider,
|
||||
assemblyai: validateAssemblyAIProvider,
|
||||
nanobanana: validateNanoBananaProvider,
|
||||
elevenlabs: validateElevenLabsProvider,
|
||||
inworld: validateInworldProvider,
|
||||
};
|
||||
|
||||
if (SPECIALTY_VALIDATORS[provider]) {
|
||||
|
||||
@@ -35,8 +35,8 @@ const pendingRequests: {
|
||||
byModel: Record<string, number>;
|
||||
byAccount: Record<string, Record<string, number>>;
|
||||
} = {
|
||||
byModel: {},
|
||||
byAccount: {},
|
||||
byModel: Object.create(null) as Record<string, number>,
|
||||
byAccount: Object.create(null) as Record<string, Record<string, number>>,
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -50,16 +50,22 @@ export function trackPendingRequest(
|
||||
) {
|
||||
const modelKey = provider ? `${model} (${provider})` : model;
|
||||
|
||||
if (!pendingRequests.byModel[modelKey]) pendingRequests.byModel[modelKey] = 0;
|
||||
// Use hasOwnProperty guard to prevent prototype pollution via crafted keys
|
||||
if (!Object.prototype.hasOwnProperty.call(pendingRequests.byModel, modelKey)) {
|
||||
pendingRequests.byModel[modelKey] = 0;
|
||||
}
|
||||
pendingRequests.byModel[modelKey] = Math.max(
|
||||
0,
|
||||
pendingRequests.byModel[modelKey] + (started ? 1 : -1)
|
||||
);
|
||||
|
||||
if (connectionId) {
|
||||
if (!pendingRequests.byAccount[connectionId]) pendingRequests.byAccount[connectionId] = {};
|
||||
if (!pendingRequests.byAccount[connectionId][modelKey])
|
||||
if (!Object.prototype.hasOwnProperty.call(pendingRequests.byAccount, connectionId)) {
|
||||
pendingRequests.byAccount[connectionId] = Object.create(null) as Record<string, number>;
|
||||
}
|
||||
if (!Object.prototype.hasOwnProperty.call(pendingRequests.byAccount[connectionId], modelKey)) {
|
||||
pendingRequests.byAccount[connectionId][modelKey] = 0;
|
||||
}
|
||||
pendingRequests.byAccount[connectionId][modelKey] = Math.max(
|
||||
0,
|
||||
pendingRequests.byAccount[connectionId][modelKey] + (started ? 1 : -1)
|
||||
|
||||
+17
-3
@@ -45,12 +45,22 @@ const CHAT_URL_PATTERNS = [":generateContent", ":streamGenerateContent"];
|
||||
const LOG_DIR = path.join(__dirname, "../../logs/mitm");
|
||||
if (ENABLE_FILE_LOG && !fs.existsSync(LOG_DIR)) fs.mkdirSync(LOG_DIR, { recursive: true });
|
||||
|
||||
// Safe log filename: only alphanumeric + hyphens, anchored inside LOG_DIR
|
||||
function safeLogPath(name) {
|
||||
const safe = name.replace(/[^a-zA-Z0-9_\-]/g, "_").substring(0, 80);
|
||||
const resolved = path.resolve(LOG_DIR, safe);
|
||||
if (!resolved.startsWith(path.resolve(LOG_DIR) + path.sep)) {
|
||||
throw new Error("Path traversal attempt detected in log filename");
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
|
||||
function saveRequestLog(url, bodyBuffer) {
|
||||
if (!ENABLE_FILE_LOG) return;
|
||||
try {
|
||||
const ts = new Date().toISOString().replace(/[:.]/g, "-");
|
||||
const urlSlug = url.replace(/[^a-zA-Z0-9]/g, "_").substring(0, 60);
|
||||
const filePath = path.join(LOG_DIR, `${ts}_${urlSlug}.json`);
|
||||
const filePath = safeLogPath(`${ts}_${urlSlug}.json`);
|
||||
const body = JSON.parse(bodyBuffer.toString());
|
||||
fs.writeFileSync(filePath, JSON.stringify(body, null, 2));
|
||||
console.log(`💾 Saved request: ${filePath}`);
|
||||
@@ -64,7 +74,7 @@ function saveResponseLog(url, data) {
|
||||
try {
|
||||
const ts = new Date().toISOString().replace(/[:.]/g, "-");
|
||||
const urlSlug = url.replace(/[^a-zA-Z0-9]/g, "_").substring(0, 60);
|
||||
const filePath = path.join(LOG_DIR, `${ts}_${urlSlug}_response.txt`);
|
||||
const filePath = safeLogPath(`${ts}_${urlSlug}_response.txt`);
|
||||
fs.writeFileSync(filePath, data);
|
||||
console.log(`💾 Saved response: ${filePath}`);
|
||||
} catch {
|
||||
@@ -156,6 +166,10 @@ function getMappedModel(model) {
|
||||
async function passthrough(req, res, bodyBuffer) {
|
||||
const targetIP = await resolveTargetIP();
|
||||
|
||||
// TLS validation is enabled by default. Set MITM_DISABLE_TLS_VERIFY=1 only
|
||||
// in controlled local environments where the target uses a self-signed cert.
|
||||
const rejectUnauthorized = process.env.MITM_DISABLE_TLS_VERIFY !== "1";
|
||||
|
||||
const forwardReq = https.request(
|
||||
{
|
||||
hostname: targetIP,
|
||||
@@ -164,7 +178,7 @@ async function passthrough(req, res, bodyBuffer) {
|
||||
method: req.method,
|
||||
headers: { ...req.headers, host: TARGET_HOST },
|
||||
servername: TARGET_HOST,
|
||||
rejectUnauthorized: false,
|
||||
rejectUnauthorized,
|
||||
},
|
||||
(forwardRes) => {
|
||||
res.writeHead(forwardRes.statusCode, forwardRes.headers);
|
||||
|
||||
@@ -400,7 +400,14 @@ export const ID_TO_ALIAS = Object.values(AI_PROVIDERS).reduce((acc, p) => {
|
||||
}, {});
|
||||
|
||||
// Providers that support usage/quota API
|
||||
export const USAGE_SUPPORTED_PROVIDERS = ["antigravity", "kiro", "github", "codex", "claude"];
|
||||
export const USAGE_SUPPORTED_PROVIDERS = [
|
||||
"antigravity",
|
||||
"kiro",
|
||||
"github",
|
||||
"codex",
|
||||
"claude",
|
||||
"kimi-coding",
|
||||
];
|
||||
|
||||
// ── Zod validation at module load (Phase 7.2) ──
|
||||
import { validateProviders } from "../validation/providerSchema";
|
||||
|
||||
@@ -5,11 +5,24 @@ import { resolveDataDir } from "@/lib/dataPaths";
|
||||
const BACKUP_DIR = path.join(resolveDataDir(), "backups");
|
||||
const MAX_BACKUPS_PER_TOOL = 5;
|
||||
|
||||
/**
|
||||
* Resolve a path within BACKUP_DIR and verify it stays within bounds.
|
||||
* Throws if the resolved path escapes BACKUP_DIR (path traversal guard).
|
||||
*/
|
||||
function safePath(...segments: string[]): string {
|
||||
const resolved = path.resolve(BACKUP_DIR, ...segments);
|
||||
const base = path.resolve(BACKUP_DIR);
|
||||
if (resolved !== base && !resolved.startsWith(base + path.sep)) {
|
||||
throw new Error("Invalid path: directory traversal detected");
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get backup directory for a specific tool
|
||||
*/
|
||||
function getToolBackupDir(toolId: string) {
|
||||
return path.join(BACKUP_DIR, toolId);
|
||||
return safePath(toolId);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -136,7 +149,8 @@ export async function listBackups(toolId: string) {
|
||||
*/
|
||||
export async function restoreBackup(toolId: string, backupId: string) {
|
||||
const dir = getToolBackupDir(toolId);
|
||||
const backupPath = path.join(dir, backupId);
|
||||
// Anchor backupId within the tool dir — prevent path traversal via backupId
|
||||
const backupPath = safePath(toolId, backupId);
|
||||
const metaPath = backupPath + ".meta.json";
|
||||
|
||||
// Read metadata to find original path
|
||||
@@ -174,8 +188,8 @@ export async function restoreBackup(toolId: string, backupId: string) {
|
||||
* Delete a specific backup by its id.
|
||||
*/
|
||||
export async function deleteBackup(toolId: string, backupId: string) {
|
||||
const dir = getToolBackupDir(toolId);
|
||||
const backupPath = path.join(dir, backupId);
|
||||
// Anchor backupId within the tool dir — prevent path traversal via backupId
|
||||
const backupPath = safePath(toolId, backupId);
|
||||
const metaPath = backupPath + ".meta.json";
|
||||
|
||||
try {
|
||||
|
||||
@@ -1,24 +1,26 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
|
||||
test.describe("Protocol visibility", () => {
|
||||
test("shows MCP/A2A navigation and protocols tab in endpoint page", async ({ page }) => {
|
||||
test("shows MCP/A2A links inside protocols tab in endpoint page", async ({ page }) => {
|
||||
await page.goto("/dashboard/endpoint");
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
const redirectedToLogin = page.url().includes("/login");
|
||||
test.skip(redirectedToLogin, "Authentication enabled without a login fixture.");
|
||||
|
||||
await expect(page.locator('a[href="/dashboard/mcp"]').first()).toBeVisible();
|
||||
await expect(page.locator('a[href="/dashboard/a2a"]').first()).toBeVisible();
|
||||
|
||||
// MCP and A2A are now shown inside the "Protocols" tab — click it first
|
||||
const protocolTab = page.getByRole("tab", { name: /protocols|protocolos/i });
|
||||
await expect(protocolTab).toBeVisible();
|
||||
await protocolTab.click();
|
||||
|
||||
// Links to MCP and A2A management pages appear after tab switch
|
||||
await expect(page.locator('a[href="/dashboard/mcp"]').first()).toBeVisible();
|
||||
await expect(page.locator('a[href="/dashboard/a2a"]').first()).toBeVisible();
|
||||
|
||||
const mcpLinks = await page.locator('a[href="/dashboard/mcp"]').count();
|
||||
const a2aLinks = await page.locator('a[href="/dashboard/a2a"]').count();
|
||||
expect(mcpLinks).toBeGreaterThanOrEqual(2);
|
||||
expect(a2aLinks).toBeGreaterThanOrEqual(2);
|
||||
expect(mcpLinks).toBeGreaterThanOrEqual(1);
|
||||
expect(a2aLinks).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
test("loads MCP and A2A dashboards without runtime error page", async ({ page }) => {
|
||||
|
||||
@@ -39,10 +39,13 @@ function mockHandler(statusSequence) {
|
||||
}
|
||||
|
||||
// ─── Circuit Breaker Integration Tests ──────────────────────────────────────
|
||||
// NOTE: combo.ts uses the full model string (e.g. "combo:groq/llama-3.3-70b")
|
||||
// as the circuit breaker key, not just the provider prefix.
|
||||
|
||||
test("handleComboChat: circuit breaker opens after repeated 502 errors", async () => {
|
||||
// Reset breaker for this test's provider
|
||||
const breaker = getCircuitBreaker("combo:groq", {
|
||||
// breaker key mirrors what combo.ts uses: "combo:<full-model-string>"
|
||||
const breakerKey = "combo:groq/llama-3.3-70b";
|
||||
const breaker = getCircuitBreaker(breakerKey, {
|
||||
failureThreshold: 3,
|
||||
resetTimeout: 60000,
|
||||
});
|
||||
@@ -77,7 +80,8 @@ test("handleComboChat: circuit breaker opens after repeated 502 errors", async (
|
||||
|
||||
test("handleComboChat: skips models with open circuit breaker", async () => {
|
||||
// Set up: groq breaker is OPEN, fireworks breaker is CLOSED
|
||||
const groqBreaker = getCircuitBreaker("combo:groq", {
|
||||
const groqBreakerKey = "combo:groq/llama-3.3-70b";
|
||||
const groqBreaker = getCircuitBreaker(groqBreakerKey, {
|
||||
failureThreshold: 3,
|
||||
resetTimeout: 60000,
|
||||
});
|
||||
@@ -88,7 +92,8 @@ test("handleComboChat: skips models with open circuit breaker", async () => {
|
||||
groqBreaker._onFailure();
|
||||
assert.equal(groqBreaker.getStatus().state, STATE.OPEN);
|
||||
|
||||
const fireworksBreaker = getCircuitBreaker("combo:fireworks", {
|
||||
const fireworksBreakerKey = "combo:fireworks/deepseek-v3p1";
|
||||
const fireworksBreaker = getCircuitBreaker(fireworksBreakerKey, {
|
||||
failureThreshold: 5,
|
||||
resetTimeout: 30000,
|
||||
});
|
||||
@@ -125,14 +130,14 @@ test("handleComboChat: skips models with open circuit breaker", async () => {
|
||||
});
|
||||
|
||||
test("handleComboChat: returns 503 when all breakers are open", async () => {
|
||||
// Open both breakers
|
||||
const groqBreaker = getCircuitBreaker("combo:groq");
|
||||
// Open both breakers using the full model string keys
|
||||
const groqBreaker = getCircuitBreaker("combo:groq/llama-3.3-70b");
|
||||
groqBreaker.reset();
|
||||
groqBreaker._onFailure();
|
||||
groqBreaker._onFailure();
|
||||
groqBreaker._onFailure();
|
||||
|
||||
const fireworksBreaker = getCircuitBreaker("combo:fireworks");
|
||||
const fireworksBreaker = getCircuitBreaker("combo:fireworks/deepseek-v3p1");
|
||||
fireworksBreaker.reset();
|
||||
for (let i = 0; i < 5; i++) fireworksBreaker._onFailure();
|
||||
|
||||
@@ -163,7 +168,8 @@ test("handleComboChat: returns 503 when all breakers are open", async () => {
|
||||
});
|
||||
|
||||
test("handleComboChat: 429 errors also trigger circuit breaker", async () => {
|
||||
const breaker = getCircuitBreaker("combo:cerebras", {
|
||||
const breakerKey = "combo:cerebras/llama-3.3-70b";
|
||||
const breaker = getCircuitBreaker(breakerKey, {
|
||||
failureThreshold: 5,
|
||||
resetTimeout: 30000,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user