chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,178 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* cursor-tap — capture cursor agent.v1.AgentService/Run wire bytes for tests.
|
||||
*
|
||||
* Usage:
|
||||
* CURSOR_TOKEN=... node scripts/ad-hoc/cursor-tap.cjs <fixture-name> <prompt>
|
||||
*
|
||||
* Examples:
|
||||
* node scripts/ad-hoc/cursor-tap.cjs single-turn-chat "say only PING"
|
||||
* node scripts/ad-hoc/cursor-tap.cjs system-prompt "be brief|hi" # split on first '|'
|
||||
* node scripts/ad-hoc/cursor-tap.cjs tool-call "weather in Paris" --tools=get_weather
|
||||
* node scripts/ad-hoc/cursor-tap.cjs composer-2-fast "hi" --model=composer-2-fast
|
||||
*
|
||||
* Writes the upstream response bytes to tests/fixtures/cursor/<fixture-name>.bin
|
||||
* and prints decoded summary to stdout. Use these fixtures in unit tests to
|
||||
* catch schema drift in cursor-agent's protobuf format.
|
||||
*
|
||||
* Note: this is a one-time / on-demand tool. The .bin output is gitignored
|
||||
* by default; commit fixtures explicitly when you want them in the test
|
||||
* baseline (tests/fixtures/cursor/.gitignore controls this).
|
||||
*/
|
||||
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const http2 = require("http2");
|
||||
const crypto = require("crypto");
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
if (args.length < 2) {
|
||||
console.error(
|
||||
"Usage: cursor-tap.cjs <fixture-name> <prompt> [--model=...] [--tools=name1,name2]"
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const [fixtureName, prompt, ...flags] = args;
|
||||
const flagMap = Object.fromEntries(
|
||||
flags.map((f) => {
|
||||
const m = f.match(/^--([^=]+)=(.*)$/);
|
||||
return m ? [m[1], m[2]] : [f.replace(/^--/, ""), true];
|
||||
})
|
||||
);
|
||||
|
||||
const token = process.env.CURSOR_TOKEN;
|
||||
if (!token) {
|
||||
console.error("Set CURSOR_TOKEN environment variable.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const model = flagMap.model || "auto";
|
||||
const conversationId = crypto.randomUUID();
|
||||
const requestId = crypto.randomUUID();
|
||||
const traceParent = `00-${crypto.randomBytes(16).toString("hex")}-${crypto.randomBytes(8).toString("hex")}-01`;
|
||||
|
||||
// ─── Minimal protobuf encoder (mirrors open-sse/utils/cursorAgentProtobuf.ts) ─
|
||||
|
||||
function encodeVarint(n) {
|
||||
const out = [];
|
||||
let v = BigInt(n);
|
||||
while (v > 0x7fn) {
|
||||
out.push(Number(v & 0x7fn) | 0x80);
|
||||
v >>= 7n;
|
||||
}
|
||||
out.push(Number(v));
|
||||
return Buffer.from(out);
|
||||
}
|
||||
function tag(field, wt) {
|
||||
return encodeVarint((field << 3) | wt);
|
||||
}
|
||||
function lenField(f, payload) {
|
||||
return Buffer.concat([tag(f, 2), encodeVarint(payload.length), payload]);
|
||||
}
|
||||
function strField(f, s) {
|
||||
return lenField(f, Buffer.from(s, "utf8"));
|
||||
}
|
||||
function varintField(f, n) {
|
||||
return Buffer.concat([tag(f, 0), encodeVarint(n)]);
|
||||
}
|
||||
function wrapConnectFrame(payload) {
|
||||
const header = Buffer.alloc(5);
|
||||
header[0] = 0;
|
||||
header.writeUInt32BE(payload.length, 1);
|
||||
return Buffer.concat([header, payload]);
|
||||
}
|
||||
|
||||
// AgentRunRequest body
|
||||
const userMessage = lenField(
|
||||
1,
|
||||
Buffer.concat([
|
||||
strField(1, prompt),
|
||||
strField(2, crypto.randomUUID()),
|
||||
lenField(3, Buffer.alloc(0)),
|
||||
varintField(4, 1),
|
||||
])
|
||||
);
|
||||
const userMessageAction = lenField(1, userMessage);
|
||||
const action = lenField(2, userMessageAction);
|
||||
const conversationState = lenField(1, Buffer.alloc(0));
|
||||
const requestedModel = lenField(9, strField(1, model === "auto" ? "default" : model));
|
||||
const arr = Buffer.concat([
|
||||
conversationState,
|
||||
action,
|
||||
lenField(4, Buffer.alloc(0)), // mcp_tools
|
||||
strField(5, conversationId),
|
||||
requestedModel,
|
||||
varintField(12, 0),
|
||||
strField(16, conversationId),
|
||||
]);
|
||||
const acm = lenField(1, arr);
|
||||
const body = wrapConnectFrame(acm);
|
||||
|
||||
// ─── h2 request ────────────────────────────────────────────────────────────
|
||||
|
||||
const cleanToken = token.includes("::") ? token.split("::")[1] : token;
|
||||
const client = http2.connect("https://agentn.global.api5.cursor.sh");
|
||||
|
||||
const collected = [];
|
||||
let responseStatus = 0;
|
||||
|
||||
const req = client.request({
|
||||
":method": "POST",
|
||||
":path": "/agent.v1.AgentService/Run",
|
||||
":authority": "agentn.global.api5.cursor.sh",
|
||||
":scheme": "https",
|
||||
authorization: `Bearer ${cleanToken}`,
|
||||
"backend-traceparent": traceParent,
|
||||
"connect-accept-encoding": "gzip,br",
|
||||
"connect-protocol-version": "1",
|
||||
"content-type": "application/connect+proto",
|
||||
traceparent: traceParent,
|
||||
"user-agent": "connect-es/1.6.1",
|
||||
"x-cursor-client-type": "cli",
|
||||
"x-cursor-client-version": "cli-2025.10.21-b2dfaef",
|
||||
"x-ghost-mode": "true",
|
||||
"x-original-request-id": requestId,
|
||||
"x-request-id": requestId,
|
||||
});
|
||||
|
||||
req.on("response", (h) => {
|
||||
responseStatus = Number(h[":status"]);
|
||||
});
|
||||
req.on("data", (chunk) => {
|
||||
collected.push(Buffer.from(chunk));
|
||||
});
|
||||
req.on("end", () => {
|
||||
const raw = Buffer.concat(collected);
|
||||
const outDir = path.join(__dirname, "..", "..", "tests", "fixtures", "cursor");
|
||||
fs.mkdirSync(outDir, { recursive: true });
|
||||
const outFile = path.join(outDir, `${fixtureName}.bin`);
|
||||
fs.writeFileSync(outFile, raw);
|
||||
console.log(`[cursor-tap] status=${responseStatus} bytes=${raw.length} → ${outFile}`);
|
||||
client.close();
|
||||
});
|
||||
req.on("error", (err) => {
|
||||
console.error("[cursor-tap] req error:", err);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
req.write(body);
|
||||
// NOTE: we never end the request; cursor closes the stream itself when the
|
||||
// turn is done. For tool-using captures, the script may need to write
|
||||
// follow-up frames before the stream closes — extend as needed.
|
||||
|
||||
// Safety timeout: if cursor doesn't close in 60s, dump what we have.
|
||||
setTimeout(() => {
|
||||
console.warn("[cursor-tap] safety timeout; closing");
|
||||
try {
|
||||
req.close();
|
||||
client.close();
|
||||
} catch {}
|
||||
const raw = Buffer.concat(collected);
|
||||
if (raw.length > 0) {
|
||||
const outDir = path.join(__dirname, "..", "..", "tests", "fixtures", "cursor");
|
||||
fs.mkdirSync(outDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(outDir, `${fixtureName}.bin`), raw);
|
||||
}
|
||||
process.exit(0);
|
||||
}, 60_000);
|
||||
Executable
+27
@@ -0,0 +1,27 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { spawn } from "node:child_process";
|
||||
|
||||
const env = { ...process.env };
|
||||
|
||||
await exec("npx", ["next", "build", "--experimental-build-mode", "generate"]);
|
||||
|
||||
// launch application
|
||||
const [command, ...args] = process.argv.slice(2);
|
||||
if (!command) {
|
||||
throw new Error("Missing command to launch after database setup");
|
||||
}
|
||||
await exec(command, args);
|
||||
|
||||
function exec(command, args = []) {
|
||||
const child = spawn(command, args, { stdio: "inherit", env });
|
||||
return new Promise((resolve, reject) => {
|
||||
child.on("exit", (code) => {
|
||||
if (code === 0) {
|
||||
resolve();
|
||||
} else {
|
||||
reject(new Error(`${[command, ...args].join(" ")} failed rc=${code}`));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { execSync } from "child_process";
|
||||
|
||||
try {
|
||||
console.log("Fetching workflow runs...");
|
||||
const output = execSync("gh run list --limit 100 --json status,conclusion,databaseId", {
|
||||
encoding: "utf8",
|
||||
});
|
||||
const runs = JSON.parse(output);
|
||||
|
||||
console.log(`Found ${runs.length} runs.`);
|
||||
let count = 0;
|
||||
for (const run of runs) {
|
||||
if (run.conclusion !== "success") {
|
||||
console.log(`Deleting run ID ${run.databaseId} with conclusion '${run.conclusion}'...`);
|
||||
try {
|
||||
execSync(`gh run delete ${run.databaseId}`);
|
||||
count++;
|
||||
} catch (err) {
|
||||
console.error(`Failed to delete run ID ${run.databaseId}:`, err.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log(`Deleted ${count} runs successfully.`);
|
||||
} catch (error) {
|
||||
console.error("Error executing script:", error);
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
// Trae SOLO auth diagnostic — probes the live API with your token across several
|
||||
// Authorization variants, so we can see which one (if any) the server accepts.
|
||||
//
|
||||
// Usage:
|
||||
// node scripts/ad-hoc/diag-trae-auth.mjs <YOUR_CLOUD_IDE_JWT>
|
||||
// TRAE_TOKEN=eyJ... node scripts/ad-hoc/diag-trae-auth.mjs
|
||||
//
|
||||
// Paste ONLY the token value (no "Cloud-IDE-JWT " prefix). The token is never
|
||||
// printed in full; only its length + last 6 chars are shown for sanity.
|
||||
|
||||
const token = (process.argv[2] || process.env.TRAE_TOKEN || "").trim();
|
||||
if (!token) {
|
||||
console.error("No token. Pass it as arg or set TRAE_TOKEN.");
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(`token: len=${token.length} …${token.slice(-6)}`);
|
||||
|
||||
const BASE = "https://core-normal.trae.ai/api/remote/v1";
|
||||
const MODELS = `${BASE}/models?functions=solo_agent_remote,solo_work_remote`;
|
||||
|
||||
const commonHeaders = {
|
||||
"Content-Type": "application/json",
|
||||
"X-Trae-Client-Type": "web",
|
||||
"X-Preferenced-Language": "en",
|
||||
"x-user-region": "US",
|
||||
Referer: "https://solo.trae.ai/",
|
||||
"User-Agent":
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 " +
|
||||
"(KHTML, like Gecko) Chrome/149.0.0.0 Safari/537.36",
|
||||
};
|
||||
|
||||
// Each variant = a set of auth-bearing headers to try.
|
||||
const variants = [
|
||||
{
|
||||
name: "Authorization: Cloud-IDE-JWT <t>",
|
||||
headers: { Authorization: `Cloud-IDE-JWT ${token}` },
|
||||
},
|
||||
{ name: "Authorization: Bearer <t>", headers: { Authorization: `Bearer ${token}` } },
|
||||
{ name: "Authorization: <t> (raw)", headers: { Authorization: token } },
|
||||
{ name: "Cloud-IDE-JWT: <t> (header)", headers: { "Cloud-IDE-JWT": token } },
|
||||
{ name: "x-cloud-ide-jwt: <t>", headers: { "x-cloud-ide-jwt": token } },
|
||||
{ name: "x-cloud-ide-token: <t>", headers: { "x-cloud-ide-token": token } },
|
||||
];
|
||||
|
||||
async function probe(label, url, method, headers) {
|
||||
try {
|
||||
const res = await fetch(url, {
|
||||
method,
|
||||
headers: { ...commonHeaders, ...headers },
|
||||
body:
|
||||
method === "POST"
|
||||
? JSON.stringify({ mode: "code", env: "remote", origin: "web" })
|
||||
: undefined,
|
||||
});
|
||||
const text = await res.text();
|
||||
const snippet = text.replace(/\s+/g, " ").slice(0, 160);
|
||||
const ok = res.ok && !/"code"\s*:\s*1001/.test(text) && !/not able to authenticate/i.test(text);
|
||||
console.log(`${ok ? "✅" : "❌"} [${res.status}] ${label}\n ${snippet}`);
|
||||
return ok;
|
||||
} catch (e) {
|
||||
console.log(`💥 ${label} → ${e?.message || e}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\n=== GET ${MODELS} ===`);
|
||||
let anyOk = false;
|
||||
for (const v of variants) {
|
||||
const ok = await probe(v.name, MODELS, "GET", v.headers);
|
||||
anyOk = anyOk || ok;
|
||||
}
|
||||
|
||||
console.log(`\n=== POST ${BASE}/chat_sessions (only the first auth variant, sanity) ===`);
|
||||
await probe(variants[0].name, `${BASE}/chat_sessions`, "POST", variants[0].headers);
|
||||
|
||||
console.log(
|
||||
anyOk
|
||||
? "\n→ At least one variant authenticated. Tell me which ✅ line, I'll set the executor to it."
|
||||
: "\n→ No header-only variant worked. The web client likely authenticates via a COOKIE, not a\n" +
|
||||
" bearer header. Next step: in DevTools → Network, right-click a request to\n" +
|
||||
" core-normal.trae.ai → Copy → Copy as cURL, and paste it here (redact the token). That\n" +
|
||||
" shows the exact auth header/cookie the server actually accepts."
|
||||
);
|
||||
@@ -0,0 +1,58 @@
|
||||
import { execSync } from "child_process";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
const REPO = "diegosouzapw/OmniRoute";
|
||||
const artifactsDir =
|
||||
process.env.ARTIFACTS_DIR ||
|
||||
path.join(process.cwd(), "artifacts");
|
||||
|
||||
async function main() {
|
||||
try {
|
||||
// 1. Get PR numbers
|
||||
console.log("Fetching open PR numbers...");
|
||||
const prNumbersOutput = execSync(
|
||||
`gh pr list --repo ${REPO} --state open --limit 500 --json number --jq '.[].number'`,
|
||||
{ encoding: "utf-8" }
|
||||
);
|
||||
const prNumbers = prNumbersOutput.trim().split("\n").map(Number).filter(Boolean);
|
||||
console.log(`Found ${prNumbers.length} open PRs:`, prNumbers);
|
||||
|
||||
if (!fs.existsSync(artifactsDir)) {
|
||||
fs.mkdirSync(artifactsDir, { recursive: true });
|
||||
}
|
||||
|
||||
// 2. Fetch metadata and diff for each PR
|
||||
for (const prNum of prNumbers) {
|
||||
console.log(`\n--- Fetching PR #${prNum} ---`);
|
||||
|
||||
// Metadata
|
||||
try {
|
||||
const metadataCmd = `gh pr view ${prNum} --repo ${REPO} --json number,title,author,headRefName,baseRefName,body,createdAt,additions,deletions,files`;
|
||||
const metadataJson = execSync(metadataCmd, { encoding: "utf-8" });
|
||||
const metadataPath = path.join(artifactsDir, `pr_${prNum}_meta.json`);
|
||||
fs.writeFileSync(metadataPath, metadataJson);
|
||||
console.log(`Saved metadata to ${metadataPath}`);
|
||||
} catch (err) {
|
||||
console.error(`Failed to fetch metadata for PR #${prNum}:`, err.message);
|
||||
}
|
||||
|
||||
// Diff
|
||||
try {
|
||||
const diffCmd = `gh pr diff ${prNum} --repo ${REPO}`;
|
||||
const diffText = execSync(diffCmd, { encoding: "utf-8", maxBuffer: 100 * 1024 * 1024 });
|
||||
const diffPath = path.join("/tmp", `pr${prNum}.diff`);
|
||||
fs.writeFileSync(diffPath, diffText);
|
||||
console.log(`Saved diff to ${diffPath} (Size: ${diffText.length} bytes)`);
|
||||
} catch (err) {
|
||||
console.error(`Failed to fetch diff for PR #${prNum}:`, err.message);
|
||||
}
|
||||
}
|
||||
|
||||
console.log("\nAll PR data fetched successfully!");
|
||||
} catch (error) {
|
||||
console.error("Error during PR fetching:", error);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
Executable
+67
@@ -0,0 +1,67 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* OmniRoute v3.3 -> v3.4 Environment Migration Script
|
||||
* Resolves breaking changes in environment variables format and validation.
|
||||
*/
|
||||
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import crypto from "crypto";
|
||||
|
||||
const envPath = path.resolve(process.cwd(), ".env");
|
||||
|
||||
if (!fs.existsSync(envPath)) {
|
||||
console.log("No .env file found. Migration skipped.");
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
let content = fs.readFileSync(envPath, "utf8");
|
||||
let modified = false;
|
||||
|
||||
// 1. Migrate NEXTAUTH_SECRET to JWT_SECRET if missing
|
||||
const nextAuthMatch = content.match(/^NEXTAUTH_SECRET=(.+)$/m);
|
||||
const jwtMatch = content.match(/^JWT_SECRET=(.+)$/m);
|
||||
|
||||
if (nextAuthMatch && !jwtMatch) {
|
||||
console.log("Migrating NEXTAUTH_SECRET to JWT_SECRET...");
|
||||
let newJwt = nextAuthMatch[1].trim();
|
||||
|
||||
// Enforce 32 char minimum for secretsValidator.ts
|
||||
if (newJwt.length < 32) {
|
||||
console.warn(
|
||||
`Original NEXTAUTH_SECRET was too short (${newJwt.length} chars). Generating a secure one...`
|
||||
);
|
||||
newJwt = crypto.randomBytes(48).toString("base64");
|
||||
}
|
||||
|
||||
content += `\n# Migrated from NEXTAUTH_SECRET\nJWT_SECRET=${newJwt}\n`;
|
||||
modified = true;
|
||||
} else if (jwtMatch && jwtMatch[1].trim().length < 32) {
|
||||
console.warn(
|
||||
`JWT_SECRET is too short (${jwtMatch[1].trim().length} chars). Generating a secure one for v3.4.0+...`
|
||||
);
|
||||
const newJwt = crypto.randomBytes(48).toString("base64");
|
||||
content = content.replace(/^JWT_SECRET=(.*)$/m, `JWT_SECRET=${newJwt}`);
|
||||
modified = true;
|
||||
}
|
||||
|
||||
// 2. Ensure API_KEY_SECRET exists (required in 3.4.0)
|
||||
if (!content.match(/^API_KEY_SECRET=/m)) {
|
||||
console.log("Adding required API_KEY_SECRET for v3.4.0...");
|
||||
const newApiSecret = crypto.randomBytes(32).toString("hex");
|
||||
content += `\n# Required for v3.4.0 API Key HMAC\nAPI_KEY_SECRET=${newApiSecret}\n`;
|
||||
modified = true;
|
||||
}
|
||||
|
||||
if (modified) {
|
||||
// Backup old .env
|
||||
fs.writeFileSync(envPath + ".bak", fs.readFileSync(envPath));
|
||||
console.log("Created backup at .env.bak");
|
||||
|
||||
// Write new .env
|
||||
fs.writeFileSync(envPath, content, "utf8");
|
||||
console.log("Successfully migrated .env file for OmniRoute 3.4.x.");
|
||||
} else {
|
||||
console.log(".env file is already compatible with OmniRoute 3.4.x.");
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
/**
|
||||
* Diagnóstico NVIDIA NIM — `s.startsWith is not a function` (issue #2463 / report 3.8.5+)
|
||||
*
|
||||
* Como rodar (a chave NUNCA é commitada — vem por env):
|
||||
* NVIDIA_API_KEY="nvapi-..." node --import tsx/esm scripts/ad-hoc/nvidia-startswith-diag.ts
|
||||
*
|
||||
* Opcional — apontar para outra base/model:
|
||||
* NVIDIA_BASE_URL="https://integrate.api.nvidia.com/v1/chat/completions"
|
||||
* NVIDIA_MODEL="openai/gpt-oss-120b"
|
||||
*
|
||||
* O que ele faz:
|
||||
* Parte A — Validação real via validateProviderApiKey() (caminho do botão "testar conexão").
|
||||
* Parte B — Sanidade do upstream: POST direto na NVIDIA (isola a chave/model do nosso pipeline).
|
||||
* Parte C — Probes de type-crash SEM chave: alimenta model malformado em resolveModelAlias +
|
||||
* replica o strip de prefixo de chatCore.ts:3316 e parseModel(), capturando o stack
|
||||
* NÃO-minificado (rodamos contra a fonte TS) para cravar a linha exata do startsWith.
|
||||
*
|
||||
* Parte C não precisa de chave — prova quais linhas são vulneráveis hoje.
|
||||
*/
|
||||
|
||||
const KEY = process.env.NVIDIA_API_KEY ?? "";
|
||||
const BASE_URL = process.env.NVIDIA_BASE_URL || "https://integrate.api.nvidia.com/v1/chat/completions";
|
||||
const MODEL = process.env.NVIDIA_MODEL || "openai/gpt-oss-120b";
|
||||
|
||||
// Neutralize CR/LF before logging so env-derived values (NVIDIA_MODEL, etc.)
|
||||
// cannot forge extra log lines (S5145 log injection).
|
||||
const line = (s = "") => console.log(String(s).replace(/[\r\n]+/g, " "));
|
||||
const hr = () => line("─".repeat(72));
|
||||
|
||||
function show(label: string, value: unknown) {
|
||||
line(` ${label}: ${typeof value === "string" ? value : JSON.stringify(value)}`);
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────────
|
||||
// Parte A — validateProviderApiKey (caminho de validação/teste de conexão)
|
||||
// ──────────────────────────────────────────────────────────────────────────
|
||||
async function partA() {
|
||||
hr();
|
||||
line("PARTE A — validateProviderApiKey({ provider: 'nvidia' })");
|
||||
hr();
|
||||
if (!KEY) {
|
||||
line(" ⏭ pulada — defina NVIDIA_API_KEY para rodar.");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const { validateProviderApiKey } = await import("../../src/lib/providers/validation.ts");
|
||||
const providerSpecificData = { baseUrl: BASE_URL };
|
||||
const result = await validateProviderApiKey({
|
||||
provider: "nvidia",
|
||||
apiKey: KEY,
|
||||
providerSpecificData,
|
||||
});
|
||||
line(" ✅ validateProviderApiKey retornou (sem crash):");
|
||||
show("resultado", result);
|
||||
if (typeof (result as any)?.error === "string" && (result as any).error.includes("startsWith")) {
|
||||
line(" ⚠️ A mensagem de erro contém 'startsWith' → crash CAPTURADO dentro do try/catch da validação.");
|
||||
}
|
||||
} catch (err: any) {
|
||||
line(" ❌ validateProviderApiKey LANÇOU (crash não tratado):");
|
||||
line(` ${err?.message}`);
|
||||
line(err?.stack ?? String(err));
|
||||
}
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────────
|
||||
// Parte B — sanidade do upstream NVIDIA (isola chave/model do nosso pipeline)
|
||||
// ──────────────────────────────────────────────────────────────────────────
|
||||
async function partB() {
|
||||
hr();
|
||||
line("PARTE B — POST direto no upstream NVIDIA (sanidade da chave/model)");
|
||||
hr();
|
||||
if (!KEY) {
|
||||
line(" ⏭ pulada — defina NVIDIA_API_KEY para rodar.");
|
||||
return;
|
||||
}
|
||||
const url = BASE_URL.endsWith("/chat/completions") ? BASE_URL : `${BASE_URL}/chat/completions`;
|
||||
show("url", url);
|
||||
show("model", MODEL);
|
||||
try {
|
||||
const res = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json", Authorization: `Bearer ${KEY}` },
|
||||
body: JSON.stringify({
|
||||
model: MODEL,
|
||||
messages: [{ role: "user", content: "ping" }],
|
||||
max_tokens: 1,
|
||||
}),
|
||||
});
|
||||
const text = await res.text();
|
||||
show("status", res.status);
|
||||
line(` body (256): ${text.slice(0, 256)}`);
|
||||
if (res.ok) line(" ✅ upstream OK — chave e model válidos.");
|
||||
else if (res.status === 401 || res.status === 403) line(" ❌ chave inválida (401/403).");
|
||||
else line(" ⚠️ não-OK não-auth — chave provavelmente válida, ver corpo.");
|
||||
} catch (err: any) {
|
||||
line(` ❌ fetch falhou: ${err?.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────────
|
||||
// Parte C — probes de type-crash (sem chave) — crava a linha do startsWith
|
||||
// ──────────────────────────────────────────────────────────────────────────
|
||||
async function partC() {
|
||||
hr();
|
||||
line("PARTE C — probes de type-crash (resolveModelAlias + strip chatCore:3316 + parseModel)");
|
||||
hr();
|
||||
|
||||
const { resolveModelAlias } = await import("../../open-sse/services/modelDeprecation.ts");
|
||||
const { parseModel } = await import("../../open-sse/services/model.ts");
|
||||
|
||||
// Replica EXATA do trecho de chatCore.ts:3315-3320 (feature #1261), sem guard.
|
||||
function stripPrefixLikeChatCore(effectiveModel: any, provider: string, alias?: string) {
|
||||
let finalModelToUpstream = effectiveModel;
|
||||
if (finalModelToUpstream.startsWith(`${provider}/`)) {
|
||||
finalModelToUpstream = finalModelToUpstream.slice(provider.length + 1);
|
||||
} else if (alias && finalModelToUpstream.startsWith(`${alias}/`)) {
|
||||
finalModelToUpstream = finalModelToUpstream.slice(alias.length + 1);
|
||||
}
|
||||
return finalModelToUpstream;
|
||||
}
|
||||
|
||||
const inputs: Array<{ label: string; model: any }> = [
|
||||
{ label: "string normal (multi-barra NVIDIA)", model: "nvidia/openai/gpt-oss-120b" },
|
||||
{ label: "objeto {} (UI bug / providerSpecificData mal salvo)", model: {} },
|
||||
{ label: "objeto {id: '...'}", model: { id: "openai/gpt-oss-120b" } },
|
||||
{ label: "number", model: 123 },
|
||||
{ label: "array", model: ["openai/gpt-oss-120b"] },
|
||||
{ label: "null", model: null },
|
||||
{ label: "undefined", model: undefined },
|
||||
];
|
||||
|
||||
for (const { label, model } of inputs) {
|
||||
line("");
|
||||
line(` ▶ input: ${label} (typeof=${typeof model})`);
|
||||
|
||||
// 1) resolveModelAlias — deixa não-string passar? (if (!modelId) return modelId)
|
||||
let effective: any;
|
||||
try {
|
||||
effective = resolveModelAlias(model as any);
|
||||
line(` resolveModelAlias → ${typeof effective} ${JSON.stringify(effective)}`);
|
||||
} catch (err: any) {
|
||||
line(` resolveModelAlias THROW: ${err?.message}`);
|
||||
effective = model;
|
||||
}
|
||||
|
||||
// 2) strip de prefixo (chatCore:3316) — captura o stack EXATO
|
||||
try {
|
||||
const out = stripPrefixLikeChatCore(effective, "nvidia", "nvidia");
|
||||
line(` chatCore strip → ${JSON.stringify(out)} ✅ sem crash`);
|
||||
} catch (err: any) {
|
||||
line(` ❌ chatCore:3316 strip THROW: ${err?.message}`);
|
||||
const at = (err?.stack ?? "").split("\n").find((l: string) => l.includes(".ts"));
|
||||
if (at) line(` ${at.trim()}`);
|
||||
}
|
||||
|
||||
// 3) parseModel (model.ts:315) — captura o stack EXATO
|
||||
try {
|
||||
const parsed = parseModel(model as any);
|
||||
line(` parseModel → ${JSON.stringify(parsed)} ✅ sem crash`);
|
||||
} catch (err: any) {
|
||||
line(` ❌ model.ts parseModel THROW: ${err?.message}`);
|
||||
const at = (err?.stack ?? "").split("\n").find((l: string) => l.includes("model.ts"));
|
||||
if (at) line(` ${at.trim()}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
line("");
|
||||
line("NVIDIA NIM — diagnóstico `startsWith is not a function`");
|
||||
// Never echo any portion of the key (js/clear-text-logging) — presence only.
|
||||
show("NVIDIA_API_KEY presente", KEY ? "sim" : "não");
|
||||
show("BASE_URL", BASE_URL);
|
||||
show("MODEL", MODEL);
|
||||
line("");
|
||||
await partA();
|
||||
await partB();
|
||||
await partC();
|
||||
hr();
|
||||
line("FIM.");
|
||||
}
|
||||
|
||||
main().catch((e) => {
|
||||
console.error("erro fatal no diagnóstico:", e);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* One-shot regen of ~/.config/opencode/opencode.json from the live
|
||||
* OmniRoute /v1/models catalog. Run after a catalog change to refresh
|
||||
* the opencode client.
|
||||
*
|
||||
* Usage: bun run scripts/regen-opencode-config.ts
|
||||
* or npx tsx scripts/regen-opencode-config.ts
|
||||
*/
|
||||
import { generateOpencodeConfig } from "../src/lib/cli-helper/config-generator/opencode.ts";
|
||||
|
||||
const baseURL = process.env.OMNIROUTE_URL ?? "http://localhost:20128";
|
||||
const apiKey = process.env.OMNIROUTE_KEY ?? process.env.OPENCODE_API_KEY ?? "";
|
||||
|
||||
if (!apiKey) {
|
||||
console.error(
|
||||
"OMNIROUTE_KEY (or OPENCODE_API_KEY) env var is required. " +
|
||||
"Find it in OmniRoute dashboard → Settings → API Keys."
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const out = await generateOpencodeConfig({ baseUrl: baseURL, apiKey });
|
||||
console.log(out);
|
||||
@@ -0,0 +1,280 @@
|
||||
import fs from "fs";
|
||||
import { execSync } from "child_process";
|
||||
import path from "path";
|
||||
|
||||
const projectRoot = process.env.PROJECT_ROOT || process.cwd();
|
||||
|
||||
const filesToCheckoutOurs = [
|
||||
".source/browser.ts",
|
||||
".source/server.ts",
|
||||
"package-lock.json",
|
||||
"electron/package-lock.json",
|
||||
"src/app/(dashboard)/dashboard/providers/[id]/page.tsx",
|
||||
"src/app/(dashboard)/dashboard/providers/components/ProviderCard.tsx",
|
||||
"src/lib/db/contextHandoffs.ts",
|
||||
"src/app/api/keys/groups/[id]/keys/route.ts",
|
||||
"src/app/api/keys/groups/[id]/permissions/route.ts",
|
||||
"src/app/api/keys/groups/[id]/route.ts",
|
||||
"src/app/api/keys/groups/route.ts",
|
||||
"src/app/api/middleware/hooks/[name]/route.ts",
|
||||
"src/app/api/middleware/hooks/route.ts",
|
||||
"src/app/api/relay/tokens/[id]/route.ts",
|
||||
"src/app/api/relay/tokens/route.ts",
|
||||
"src/app/api/playground/simulate-route/route.ts",
|
||||
];
|
||||
|
||||
function runCmd(cmd) {
|
||||
console.log(`Running: ${cmd}`);
|
||||
return execSync(cmd, { cwd: projectRoot, encoding: "utf-8" });
|
||||
}
|
||||
|
||||
async function main() {
|
||||
// 1. Checkout ours for the files where HEAD is the preferred up-to-date state
|
||||
for (const file of filesToCheckoutOurs) {
|
||||
try {
|
||||
runCmd(`git checkout --ours "${file}"`);
|
||||
runCmd(`git add "${file}"`);
|
||||
} catch (err) {
|
||||
console.error(`Failed to checkout --ours for ${file}:`, err.message);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Resolve .dockerignore (keep release/v3.8.4 doc rules)
|
||||
try {
|
||||
runCmd("git checkout --theirs .dockerignore");
|
||||
runCmd("git add .dockerignore");
|
||||
} catch (err) {
|
||||
console.error("Failed to resolve .dockerignore:", err.message);
|
||||
}
|
||||
|
||||
// 3. Resolve docs/reference/ENVIRONMENT.md (keep release/v3.8.4 table formatting)
|
||||
try {
|
||||
runCmd("git checkout --theirs docs/reference/ENVIRONMENT.md");
|
||||
runCmd("git add docs/reference/ENVIRONMENT.md");
|
||||
} catch (err) {
|
||||
console.error("Failed to resolve docs/reference/ENVIRONMENT.md:", err.message);
|
||||
}
|
||||
|
||||
// 4. Resolve open-sse/executors/index.ts (keep both ClaudeWebExecutor and InnerAiExecutor)
|
||||
const execIndexFile = path.join(projectRoot, "open-sse/executors/index.ts");
|
||||
if (fs.existsSync(execIndexFile)) {
|
||||
let content = fs.readFileSync(execIndexFile, "utf-8");
|
||||
|
||||
// Resolve imports conflict
|
||||
content = content.replace(
|
||||
/<<<<<<< HEAD\r?\nimport \{ ClaudeWebExecutor \} from "\.\/claude-web\.ts";\r?\n=======\r?\nimport \{ InnerAiExecutor \} from "\.\/inner-ai\.ts";\r?\n>>>>>>> release\/v3\.8\.4/g,
|
||||
'import { ClaudeWebExecutor } from "./claude-web.ts";\nimport { InnerAiExecutor } from "./inner-ai.ts";'
|
||||
);
|
||||
|
||||
// Resolve executor registration conflict
|
||||
content = content.replace(
|
||||
/<<<<<<< HEAD\r?\n\s+"claude-web": new ClaudeWebExecutor\(\),\r?\n\s+"cw-web": new ClaudeWebExecutor\(\), \/\/ Alias\r?\n=======\r?\n\s+"inner-ai": new InnerAiExecutor\(\),\r?\n\s+"in-ai": new InnerAiExecutor\(\), \/\/ Alias\r?\n>>>>>>> release\/v3\.8\.4/g,
|
||||
' "claude-web": new ClaudeWebExecutor(),\n "cw-web": new ClaudeWebExecutor(), // Alias\n "inner-ai": new InnerAiExecutor(),\n "in-ai": new InnerAiExecutor(), // Alias'
|
||||
);
|
||||
|
||||
fs.writeFileSync(execIndexFile, content);
|
||||
runCmd("git add open-sse/executors/index.ts");
|
||||
}
|
||||
|
||||
// 7. Resolve src/app/api/providers/[id]/models/route.ts (combine imports)
|
||||
const modelsRoute = path.join(projectRoot, "src/app/api/providers/[id]/models/route.ts");
|
||||
if (fs.existsSync(modelsRoute)) {
|
||||
let content = fs.readFileSync(modelsRoute, "utf-8");
|
||||
content = content.replace(
|
||||
/<<<<<<< HEAD\r?\n=======\r?\nimport \{ sanitizeErrorMessage \} from "@omniroute\/open-sse\/utils\/error";\r?\nimport \{ getStaticQoderModels \} from "@omniroute\/open-sse\/services\/qoderCli\.ts";\r?\n>>>>>>> release\/v3\.8\.4/g,
|
||||
'import { sanitizeErrorMessage } from "@omniroute/open-sse/utils/error";\nimport { getStaticQoderModels } from "@omniroute/open-sse/services/qoderCli.ts";'
|
||||
);
|
||||
fs.writeFileSync(modelsRoute, content);
|
||||
runCmd("git add src/app/api/providers/[id]/models/route.ts");
|
||||
}
|
||||
|
||||
// 8. Resolve src/sse/handlers/chat.ts
|
||||
const sseChat = path.join(projectRoot, "src/sse/handlers/chat.ts");
|
||||
if (fs.existsSync(sseChat)) {
|
||||
let content = fs.readFileSync(sseChat, "utf-8");
|
||||
|
||||
// Resolve comment / modelStr conflict
|
||||
content = content.replace(
|
||||
/<<<<<<< HEAD\r?\n=======\r?\n\s+\/\/ `let` because the middleware-hook pipeline \(line ~319\) may reassign this\r?\n\s+\/\/ when a hook rewrites the target model\. Previously declared `const`, which\r?\n\s+\/\/ broke turbopack\/strict-mode builds \(PR #2670 regression\)\.\r?\n>>>>>>> release\/v3\.8\.4\r?\n\s+let modelStr = body\.model;/g,
|
||||
" // `let` because the middleware-hook pipeline (line ~319) may reassign this\n // when a hook rewrites the target model. Previously declared `const`, which\n // broke turbopack/strict-mode builds (PR [PR #2670](file:///home/diegosouzapw/dev/proxys/OmniRoute/package.json#L2670) regression).\n let modelStr = body.model;"
|
||||
);
|
||||
|
||||
// Resolve trafficType / modelAbortSignal conflict (1st occurrence)
|
||||
content = content.replace(
|
||||
/<<<<<<< HEAD\r?\n\s+trafficType\?: "production" \| "shadow";\r?\n=======\r?\n\s+modelAbortSignal\?: AbortSignal \| null;\r?\n>>>>>>> release\/v3\.8\.4/g,
|
||||
' trafficType?: "production" | "shadow";\n modelAbortSignal?: AbortSignal | null;'
|
||||
);
|
||||
|
||||
fs.writeFileSync(sseChat, content);
|
||||
runCmd("git add src/sse/handlers/chat.ts");
|
||||
}
|
||||
|
||||
// 9. Resolve bin/cli/tray/autostart.mjs (keep execFileSync, combine ignoreFailure and systemd CI fallback)
|
||||
const autostart = path.join(projectRoot, "bin/cli/tray/autostart.mjs");
|
||||
if (fs.existsSync(autostart)) {
|
||||
let content = fs.readFileSync(autostart, "utf-8");
|
||||
|
||||
// runUserSystemctl conflict
|
||||
content = content.replace(
|
||||
/<<<<<<< HEAD\r?\n\s+\} catch \{\r?\n=======\r?\n\s+\} catch \(err\) \{\r?\n\s+if \(!ignoreFailure\) throw err;\r?\n>>>>>>> release\/v3\.8\.4/g,
|
||||
` } catch (err) { \n if (!ignoreFailure) throw err;`
|
||||
);
|
||||
|
||||
// isSystemdServiceEnabled conflict
|
||||
content = content.replace(
|
||||
/<<<<<<< HEAD\r?\n\s+return false;\r?\n=======\r?\n\s+\/\/ systemctl --user can't query the bus \(headless environments \/ CI runners\)\.\r?\n\s+\/\/ Treat the presence of the unit file as the source of truth, matching the\r?\n\s+\/\/ fallback used in enableLinux\(\) where unit-file existence counts as success\.\r?\n\s+return true;\r?\n>>>>>>> release\/v3\.8\.4/g,
|
||||
` // systemctl --user can't query the bus (headless environments / CI runners).\n // Treat the presence of the unit file as the source of truth, matching the\n // fallback used in enableLinux() where unit-file existence counts as success.\n return true;`
|
||||
);
|
||||
|
||||
fs.writeFileSync(autostart, content);
|
||||
runCmd("git add bin/cli/tray/autostart.mjs");
|
||||
}
|
||||
|
||||
// 10. Resolve electron/package.json
|
||||
const electronPkg = path.join(projectRoot, "electron/package.json");
|
||||
if (fs.existsSync(electronPkg)) {
|
||||
let content = fs.readFileSync(electronPkg, "utf-8");
|
||||
content = content.replace(
|
||||
/<<<<<<< HEAD\r?\n\s+"electron": "\^42\.2\.0",\r?\n\s+"electron-builder": "\^26\.11\.0"\r?\n=======\r?\n\s+"electron": "\^41\.2\.0",\r?\n\s+"electron-builder": "\^26\.11\.1"\r?\n>>>>>>> release\/v3\.8\.4/g,
|
||||
' "electron": "^42.2.0",\n "electron-builder": "^26.11.1"'
|
||||
);
|
||||
fs.writeFileSync(electronPkg, content);
|
||||
runCmd("git add electron/package.json");
|
||||
}
|
||||
|
||||
// 11. Resolve .github/workflows/ci.yml
|
||||
const ciYaml = path.join(projectRoot, ".github/workflows/ci.yml");
|
||||
if (fs.existsSync(ciYaml)) {
|
||||
let content = fs.readFileSync(ciYaml, "utf-8");
|
||||
|
||||
// Run c8 over shard title
|
||||
content = content.replace(
|
||||
/<<<<<<< HEAD\r?\n\s+rm -rf coverage-shard coverage-shard-report\r?\n=======\r?\n\s+# `--temp-directory` \(writable via NODE_V8_COVERAGE\) is what the merge\r?\n\s+# job reads with `c8 report --temp-directory \.\.\.`\. Using `--output-dir`\r?\n\s+# only produces the final json \*report\* and leaves the raw v8 files in\r?\n\s+# `coverage\/tmp`, so uploading `coverage-shard\/` was empty\. Pin the temp\r?\n\s+# dir so the raw coverage files live there and the artifact upload picks\r?\n\s+# them up regardless of `--test-force-exit` timing\.\r?\n>>>>>>> release\/v3\.8\.4/g,
|
||||
" rm -rf coverage-shard coverage-shard-report\n # `--temp-directory` (writable via NODE_V8_COVERAGE) is what the merge\n # job reads with `c8 report --temp-directory ...`. Using `--output-dir`\n # only produces the final json *report* and leaves the raw v8 files in\n # `coverage/tmp`, so uploading `coverage-shard/` was empty. Pin the temp\n # dir so the raw coverage files live there and the artifact upload picks\n # them up regardless of `--test-force-exit` timing."
|
||||
);
|
||||
|
||||
// c8 temp-directory arg
|
||||
content = content.replace(
|
||||
/<<<<<<< HEAD\r?\n=======\r?\n\s+--temp-directory=coverage-shard\r?\n>>>>>>> release\/v3\.8\.4/g,
|
||||
" --temp-directory=coverage-shard"
|
||||
);
|
||||
|
||||
fs.writeFileSync(ciYaml, content);
|
||||
runCmd("git add .github/workflows/ci.yml");
|
||||
}
|
||||
|
||||
// 12. Resolve Dockerfile
|
||||
const dockerfile = path.join(projectRoot, "Dockerfile");
|
||||
if (fs.existsSync(dockerfile)) {
|
||||
let content = fs.readFileSync(dockerfile, "utf-8");
|
||||
|
||||
// FROM node
|
||||
content = content.replace(
|
||||
/FROM node:26\.2\.0-trixie-slim AS builder\r?\nFROM node:24-trixie-slim AS builder/g,
|
||||
"FROM node:24-trixie-slim AS builder"
|
||||
);
|
||||
|
||||
// apt-get cache mounts
|
||||
content = content.replace(
|
||||
/<<<<<<< HEAD\r?\nRUN --mount=type=cache,target=\/var\/cache\/apt,sharing=locked \\\r?\n\s+--mount=type=cache,target=\/var\/lib\/apt\/lists,sharing=locked \\\r?\n\s+apt-get update \\\r?\n=======\r?\nRUN apt-get update \\\r?\n>>>>>>> release\/v3\.8\.4/g,
|
||||
"RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \\\n --mount=type=cache,target=/var/lib/apt/lists,sharing=locked \\\n apt-get update \\"
|
||||
);
|
||||
|
||||
// npm ci script ignore and reproducible build check
|
||||
content = content.replace(
|
||||
/<<<<<<< HEAD\r?\nRUN --mount=type=cache,target=\/root\/\.npm \\\r?\n\s+if \[ -f package-lock\.json \]; then \\\r?\n\s+npm ci --no-audit --no-fund --legacy-peer-deps; \\\r?\n\s+else \\\r?\n\s+npm install --no-audit --no-fund --legacy-peer-deps; \\\r?\n\s+fi\r?\n=======\r?\n# `--ignore-scripts` blocks the install\/postinstall hooks of dependencies,[\s\S]*?RUN npm ci --no-audit --no-fund --legacy-peer-deps --ignore-scripts\r?\n>>>>>>> release\/v3\.8\.4/g,
|
||||
`# --ignore-scripts blocks the install/postinstall hooks of dependencies,
|
||||
# closing the supply-chain attack surface where a transitive dep can run
|
||||
# arbitrary code at install time. OmniRoute's own postinstall (
|
||||
# better-sqlite3 binary touchups, @swc/helpers copy) is only needed when
|
||||
# a packaged app/node_modules is unpacked — inside the Docker builder we
|
||||
# are doing a fresh native-platform install, so dropping the scripts is safe.
|
||||
#
|
||||
# We REQUIRE a committed package-lock.json so resolved dependency versions
|
||||
# are reproducible.
|
||||
RUN test -f package-lock.json \\
|
||||
|| (echo "package-lock.json is required for reproducible Docker builds" >&2 && exit 1)
|
||||
RUN --mount=type=cache,target=/root/.npm \\
|
||||
npm ci --no-audit --no-fund --legacy-peer-deps --ignore-scripts`
|
||||
);
|
||||
|
||||
// npm global install
|
||||
content = content.replace(
|
||||
/<<<<<<< HEAD\r?\nRUN --mount=type=cache,target=\/root\/\.npm \\\r?\n\s+npm install -g --no-audit --no-fund @openai\/codex @anthropic-ai\/claude-code droid openclaw@latest\r?\n=======\r?\nRUN npm install -g --no-audit --no-fund @openai\/codex @anthropic-ai\/claude-code droid openclaw@latest\r?\n\r?\nUSER node\r?\n\r?\n>>>>>>> release\/v3\.8\.4/g,
|
||||
"RUN --mount=type=cache,target=/root/.npm \\\n npm install -g --no-audit --no-fund @openai/codex @anthropic-ai/claude-code droid openclaw@latest\n\nUSER node"
|
||||
);
|
||||
|
||||
fs.writeFileSync(dockerfile, content);
|
||||
runCmd("git add Dockerfile");
|
||||
}
|
||||
|
||||
// 13. Resolve open-sse/services/combo.ts
|
||||
const openSseCombo = path.join(projectRoot, "open-sse/services/combo.ts");
|
||||
if (fs.existsSync(openSseCombo)) {
|
||||
let content = fs.readFileSync(openSseCombo, "utf-8");
|
||||
|
||||
// IntentClassifierConfig imports
|
||||
content = content.replace(
|
||||
/<<<<<<< HEAD\r?\nimport \{\r?\n\s+classifyWithConfig,\r?\n\s+DEFAULT_INTENT_CONFIG,\r?\n\s+type IntentClassifierConfig,\r?\n\} from "\.\/intentClassifier\.ts";\r?\n=======\r?\nimport \{ notifyWebhookEvent \} from "\.\.\/\.\.\/src\/lib\/webhookDispatcher";\r?\nimport \{ classifyWithConfig, DEFAULT_INTENT_CONFIG \} from "\.\/intentClassifier\.ts";\r?\n>>>>>>> release\/v3\.8\.4/g,
|
||||
'import { notifyWebhookEvent } from "../../src/lib/webhookDispatcher";\nimport {\n classifyWithConfig,\n DEFAULT_INTENT_CONFIG,\n type IntentClassifierConfig,\n} from "./intentClassifier.ts";'
|
||||
);
|
||||
|
||||
// handlePipelineCombo call
|
||||
content = content.replace(
|
||||
/<<<<<<< HEAD\r?\n\s+handleChatCore: handleSingleModel,\r?\n\s+log: \{\r?\n\s+info: log\.info,\r?\n\s+warn: log\.warn,\r?\n\s+error: log\.error \?\? log\.warn,\r?\n\s+\},\r?\n\s+settings: settings \?\? \{\},\r?\n\s+signal: signal \?\? undefined,\r?\n=======\r?\n\s+handleChatCore: handleSingleModelWithTimeout,\r?\n\s+log,\r?\n\s+settings,\r?\n\s+signal,\r?\n>>>>>>> release\/v3\.8\.4/g,
|
||||
" handleChatCore: handleSingleModelWithTimeout,\n log: {\n info: log.info,\n warn: log.warn,\n error: log.error ?? log.warn,\n },\n settings: settings ?? {},\n signal: signal ?? undefined,"
|
||||
);
|
||||
|
||||
// handleSingleModel call in loop
|
||||
content = content.replace(
|
||||
/<<<<<<< HEAD\r?\n\s+const result = await handleSingleModelWrapped\(attemptBody, modelStr, \{\r?\n=======\r?\n\s+const result = await handleSingleModelWithTimeout\(body, modelStr, \{\r?\n>>>>>>> release\/v3\.8\.4/g,
|
||||
" const result = await handleSingleModelWithTimeout(attemptBody, modelStr, {"
|
||||
);
|
||||
|
||||
// recordSessionModelUsage conflict
|
||||
content = content.replace(
|
||||
/<<<<<<< HEAD\r?\n\s+recordSessionModelUsage\([\s\S]*?\);\r?\n\s+\r?\n=======\r?\n>>>>>>> release\/v3\.8\.4/g,
|
||||
" recordSessionModelUsage(\n relayOptions.sessionId,\n combo.name,\n modelStr,\n provider,\n target.connectionId ?? undefined\n );"
|
||||
);
|
||||
|
||||
fs.writeFileSync(openSseCombo, content);
|
||||
runCmd("git add open-sse/services/combo.ts");
|
||||
}
|
||||
|
||||
// 14. Resolve src/app/api/copilot/chat/route.ts
|
||||
const copilotChatRoute = path.join(projectRoot, "src/app/api/copilot/chat/route.ts");
|
||||
if (fs.existsSync(copilotChatRoute)) {
|
||||
let content = fs.readFileSync(copilotChatRoute, "utf-8");
|
||||
|
||||
// Imports conflict
|
||||
content = content.replace(
|
||||
/<<<<<<< HEAD\r?\nimport \{ requireManagementAuth \} from "@\/lib\/api\/requireManagementAuth";\r?\nimport \{ processCopilotChat \} from "@\/lib\/copilot\/engine";\r?\nimport \{ isValidationFailure, validateBody \} from "@\/shared\/validation\/helpers";\r?\nimport \{ sanitizeErrorMessage \} from "@omniroute\/open-sse\/utils\/error\.ts";\r?\n=======\r?\nimport \{ processCopilotChat \} from "@\/lib\/copilot\/engine";\r?\nimport type \{ CopilotRequest \} from "@\/lib\/copilot\/engine";\r?\nimport \{ buildErrorBody \} from "@omniroute\/open-sse\/utils\/error";\r?\n>>>>>>> release\/v3\.8\.4/g,
|
||||
'import { requireManagementAuth } from "@/lib/api/requireManagementAuth";\nimport { processCopilotChat } from "@/lib/copilot/engine";\nimport { isValidationFailure, validateBody } from "@/shared/validation/helpers";\nimport { sanitizeErrorMessage, buildErrorBody } from "@omniroute/open-sse/utils/error.ts";'
|
||||
);
|
||||
|
||||
// Schema content min length
|
||||
content = content.replace(
|
||||
/<<<<<<< HEAD\r?\n\s+content: z\.string\(\)\.min\(1, "message content is required"\),\r?\n=======\r?\n\s+content: z\.string\(\),\r?\n>>>>>>> release\/v3\.8\.4/g,
|
||||
' content: z.string().min(1, "message content is required"),'
|
||||
);
|
||||
|
||||
// POST implementation conflict
|
||||
content = content.replace(
|
||||
/<<<<<<< HEAD\r?\n\s+const authError = await requireManagementAuth\(request\);\r?\n\s+if \(authError\) return authError;\r?\n\r?\n\s+try \{\r?\n\s+const rawBody = await request.json\(\);\r?\n\s+const validation = validateBody\(copilotRequestSchema, rawBody\);\r?\n\s+if \(isValidationFailure\(validation\)\) \{\r?\n\s+return NextResponse\.json\(\{ error: validation\.error \}, \{ status: 400 \}\);\r?\n=======\r?\n\s+try \{\r?\n\s+const raw = await request.json\(\);\r?\n\s+const parsed = copilotRequestSchema\.safeParse\(raw\);\r?\n\s+if \(!parsed\.success\) \{\r?\n\s+return NextResponse\.json\r?\n\s+buildErrorBody\(400, parsed\.error\.issues\[0\]\?\.message \?\? "Invalid request"\),\r?\n\s+\{ status: 400 \}\r?\n\s+\);\r?\n>>>>>>> release\/v3\.8\.4\r?\n\s+\}\r?\n\s+const body = parsed\.data as CopilotRequest;\r?\n\r?\n\s+const response = await processCopilotChat\(body\);/g,
|
||||
" const authError = await requireManagementAuth(request);\n if (authError) return authError;\n\n try {\n const rawBody = await request.json();\n const validation = validateBody(copilotRequestSchema, rawBody);\n if (isValidationFailure(validation)) {\n return NextResponse.json(\n buildErrorBody(400, validation.error),\n { status: 400 }\n );\n }\n const response = await processCopilotChat(validation.data);"
|
||||
);
|
||||
|
||||
// Error handling conflict
|
||||
content = content.replace(
|
||||
/<<<<<<< HEAD\r?\n\s+const message = sanitizeErrorMessage\(error\);\r?\n\s+return NextResponse\.json\(\{ error: `Copilot error: \$\{message\}` \}, \{ status: 500 \}\);\r?\n=======\r?\n\s+\/\/ buildErrorBody\(\) routes through sanitizeErrorMessage\(\), which strips\r?\n\s+\/\/ stack traces and absolute file paths\. Hard rule #12\.\r?\n\s+const message = error instanceof Error \? error\.message : "Unknown error";\r?\n\s+return NextResponse\.json\(buildErrorBody\(500, message\), \{ status: 500 \}\);\r?\n>>>>>>> release\/v3\.8\.4/g,
|
||||
" const message = sanitizeErrorMessage(error);\n return NextResponse.json(buildErrorBody(500, `Copilot error: ${message}`), { status: 500 });"
|
||||
);
|
||||
|
||||
fs.writeFileSync(copilotChatRoute, content);
|
||||
runCmd("git add src/app/api/copilot/chat/route.ts");
|
||||
}
|
||||
|
||||
console.log("Resolutions written and staged!");
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -0,0 +1,61 @@
|
||||
// End-to-end smoke test: drives TraeExecutor against the real Trae API with your
|
||||
// JWT from trae_solo.env (kept outside the repo), printing content + usage.
|
||||
// Run: node --import tsx/esm scripts/ad-hoc/smoke-trae.mjs
|
||||
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const envPath = path.resolve(__dirname, "../../../trae_solo.env");
|
||||
if (!fs.existsSync(envPath)) {
|
||||
console.error(`Не найден ${envPath}. Положи туда TRAE_TOKEN= и TRAE_WEB_ID= и т.д.`);
|
||||
process.exit(1);
|
||||
}
|
||||
const cfg = Object.fromEntries(
|
||||
fs
|
||||
.readFileSync(envPath, "utf8")
|
||||
.split("\n")
|
||||
.filter((l) => l && !l.startsWith("#") && l.includes("="))
|
||||
.map((l) => {
|
||||
const i = l.indexOf("=");
|
||||
return [l.slice(0, i), l.slice(i + 1)];
|
||||
})
|
||||
);
|
||||
|
||||
const { TraeExecutor } = await import("../../open-sse/executors/trae.ts");
|
||||
const ex = new TraeExecutor();
|
||||
|
||||
const credentials = {
|
||||
accessToken: cfg.TRAE_TOKEN,
|
||||
providerSpecificData: {
|
||||
webId: cfg.TRAE_WEB_ID || "",
|
||||
bizUserId: cfg.TRAE_BIZ_USER_ID || "",
|
||||
userUniqueId: cfg.TRAE_USER_UNIQUE_ID || "",
|
||||
scope: cfg.TRAE_SCOPE || "marscode-us",
|
||||
tenant: cfg.TRAE_TENANT || "marscode",
|
||||
region: cfg.TRAE_REGION || "US-East",
|
||||
aiRegion: cfg.TRAE_AIREGION || cfg.TRAE_REGION || "US-East",
|
||||
appLanguage: cfg.TRAE_APP_LANGUAGE || "en",
|
||||
appVersion: cfg.TRAE_APP_VERSION || "1.0.0.1229",
|
||||
},
|
||||
};
|
||||
|
||||
const model = process.argv[2] || "auto";
|
||||
const prompt = process.argv.slice(3).join(" ") || "Ответь одним словом: столица Франции?";
|
||||
|
||||
console.log(`[smoke] model=${model} prompt=${JSON.stringify(prompt)}`);
|
||||
const { response } = await ex.execute({
|
||||
model,
|
||||
body: { messages: [{ role: "user", content: prompt }] },
|
||||
stream: false,
|
||||
credentials,
|
||||
});
|
||||
const text = await response.text();
|
||||
if (response.status !== 200) {
|
||||
console.error(`[smoke] HTTP ${response.status}\n${text}`);
|
||||
process.exit(1);
|
||||
}
|
||||
const json = JSON.parse(text);
|
||||
console.log("content:", JSON.stringify(json.choices?.[0]?.message?.content));
|
||||
console.log("usage: ", json.usage);
|
||||
@@ -0,0 +1,130 @@
|
||||
#!/usr/bin/env node
|
||||
// Sync the cursor models list in open-sse/config/providerRegistry.ts from
|
||||
// cursor-agent's runtime model list. Triggers an intentional invalid --model
|
||||
// invocation so cursor-agent prints "Available models: ..." on stderr.
|
||||
//
|
||||
// Usage:
|
||||
// node scripts/ad-hoc/sync-cursor-models.mjs # spawn cursor-agent and apply
|
||||
// node scripts/ad-hoc/sync-cursor-models.mjs --dry-run # print proposed block, don't write
|
||||
// node scripts/ad-hoc/sync-cursor-models.mjs --from-stdin # read the error message from stdin
|
||||
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { readFileSync, writeFileSync } from "node:fs";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { dirname, resolve } from "node:path";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const REGISTRY_PATH = resolve(__dirname, "..", "open-sse", "config", "providerRegistry.ts");
|
||||
|
||||
const args = new Set(process.argv.slice(2));
|
||||
const DRY_RUN = args.has("--dry-run");
|
||||
const FROM_STDIN = args.has("--from-stdin");
|
||||
|
||||
function readSource() {
|
||||
if (FROM_STDIN) return readFileSync(0, "utf8");
|
||||
// cursor-agent prints "Available models: ..." to stderr and exits non-zero.
|
||||
const r = spawnSync("cursor-agent", ["--model", "--help"], { encoding: "utf8" });
|
||||
return `${r.stdout || ""}\n${r.stderr || ""}`;
|
||||
}
|
||||
|
||||
// `auto` is a CLI-side abstraction (cursor-agent resolves it locally before
|
||||
// sending) and `composer-*` targets cursor's edit/composer endpoint, neither
|
||||
// of which work via the chat RPC. Filter them so the dashboard never offers
|
||||
// them to the chat path. Pass --include-unsupported to keep them.
|
||||
function isUnsupportedChatModel(id) {
|
||||
if (id === "auto") return true;
|
||||
if (id.startsWith("composer-")) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
function parseModelIds(text) {
|
||||
const m = text.match(/Available models:\s*([^\n]+)/);
|
||||
if (!m) throw new Error("Could not find 'Available models:' line in cursor-agent output");
|
||||
const includeUnsupported = args.has("--include-unsupported");
|
||||
return m[1]
|
||||
.split(",")
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean)
|
||||
.filter((id) => includeUnsupported || !isUnsupportedChatModel(id));
|
||||
}
|
||||
|
||||
const SEGMENT_OVERRIDES = {
|
||||
gpt: "GPT",
|
||||
claude: "Claude",
|
||||
gemini: "Gemini",
|
||||
grok: "Grok",
|
||||
kimi: "Kimi",
|
||||
composer: "Composer",
|
||||
opus: "Opus",
|
||||
sonnet: "Sonnet",
|
||||
haiku: "Haiku",
|
||||
codex: "Codex",
|
||||
mini: "Mini",
|
||||
nano: "Nano",
|
||||
max: "Max",
|
||||
high: "High",
|
||||
low: "Low",
|
||||
medium: "Medium",
|
||||
xhigh: "XHigh",
|
||||
none: "None",
|
||||
fast: "Fast",
|
||||
thinking: "Thinking",
|
||||
extra: "Extra",
|
||||
spark: "Spark",
|
||||
preview: "Preview",
|
||||
flash: "Flash",
|
||||
pro: "Pro",
|
||||
};
|
||||
|
||||
// Pretty-print an id by:
|
||||
// 1) collapsing claude-NAME-X-Y dotted version (e.g. claude-opus-4-7 → claude-opus-4.7)
|
||||
// 2) splitting on '-'
|
||||
// 3) applying SEGMENT_OVERRIDES; falling back to capitalize-first
|
||||
function humanize(id) {
|
||||
if (id === "auto") return "Auto (Server Picks)";
|
||||
|
||||
// Collapse "X-Y" numeric suffix in claude-foo-X-Y- patterns into "X.Y"
|
||||
const collapsed = id.replace(/(\d+)-(\d+)(?=-|$)/g, "$1.$2");
|
||||
const parts = collapsed.split("-");
|
||||
const labelled = parts.map((p) => {
|
||||
if (SEGMENT_OVERRIDES[p]) return SEGMENT_OVERRIDES[p];
|
||||
if (/^\d/.test(p)) return p; // leave version numbers / "k2.5" alone
|
||||
return p.charAt(0).toUpperCase() + p.slice(1);
|
||||
});
|
||||
return labelled.join(" ");
|
||||
}
|
||||
|
||||
function buildModelsArrayLines(ids) {
|
||||
const seen = new Set();
|
||||
const out = [];
|
||||
for (const id of ids) {
|
||||
if (seen.has(id)) continue;
|
||||
seen.add(id);
|
||||
out.push(` { id: ${JSON.stringify(id)}, name: ${JSON.stringify(humanize(id))} },`);
|
||||
}
|
||||
return out.join("\n");
|
||||
}
|
||||
|
||||
function replaceCursorModels(source, modelsBlock) {
|
||||
// Match the `cursor:` provider entry and replace just its `models: [ ... ],` array.
|
||||
const re = /(\n cursor:\s*\{[\s\S]*?\n models:\s*\[)([\s\S]*?)(\n \],)/;
|
||||
if (!re.test(source)) throw new Error("Could not locate cursor.models array in registry");
|
||||
return source.replace(re, `$1\n${modelsBlock}$3`);
|
||||
}
|
||||
|
||||
const ids = parseModelIds(readSource());
|
||||
const block = buildModelsArrayLines(ids);
|
||||
|
||||
if (DRY_RUN) {
|
||||
console.log(block);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const before = readFileSync(REGISTRY_PATH, "utf8");
|
||||
const after = replaceCursorModels(before, block);
|
||||
if (before === after) {
|
||||
console.log("No changes — cursor models already in sync.");
|
||||
process.exit(0);
|
||||
}
|
||||
writeFileSync(REGISTRY_PATH, after);
|
||||
console.log(`Updated ${ids.length} cursor models in ${REGISTRY_PATH}`);
|
||||
Reference in New Issue
Block a user