Merge release/v3.8.50 into fix/release-v3.8.50-basereds-2 (base versions win — #10198 landed the newer fixes; dead cachePolicy param dropped)

This commit is contained in:
backryun
2026-08-12 16:05:46 -03:00
711 changed files with 16974 additions and 16168 deletions

View File

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

View File

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

View File

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

View File

@@ -36,7 +36,6 @@ const DOCS_ROOT = path.join(REPO_ROOT, "docs");
const EXCLUDE_PREFIXES = [
path.join(DOCS_ROOT, "i18n") + path.sep,
path.join(DOCS_ROOT, "screenshots") + path.sep,
path.join(DOCS_ROOT, "superpowers") + path.sep,
path.join(DOCS_ROOT, "diagrams", "exported") + path.sep,
];

View File

@@ -18,9 +18,13 @@
// Exits 0 on success, 1 on STRICT drift (or any drift with --strict).
// Run: node scripts/check/check-docs-counts-sync.mjs
//
// NOTE: the provider check trusts PROVIDER_REFERENCE.md as the canonical total. If a
// provider is added to the code but the reference is not regenerated, this guard will
// not catch it — regenerate with `npm run gen:provider-reference` before relying on it.
// NOTE: PROVIDER_REFERENCE.md is no longer blindly trusted — a STRICT check compares
// the doc's `Total providers` against the live provider modules (the same collections
// the generator reads), so a hand-stale doc is a red, not a silently propagated total.
// Fix by running `npm run gen:provider-reference`. Additional STRICT coverage added in
// the 2026-08-12 hardening: llm.txt + package.json description (providers), migration
// count (README/AGENTS/llm.txt), and canonical numbers inside the README SVG diagrams
// (providers / MCP tools / routing strategies / free-tier pools).
import fs from "node:fs";
import { spawnSync } from "node:child_process";
@@ -76,6 +80,13 @@ export function readProviderTotal() {
return parseProviderTotal(fs.readFileSync(abs, "utf8"));
}
// STRICT: number of SQL migration files shipped with the app.
export function countMigrations() {
const abs = path.join(ROOT, "src", "lib", "db", "migrations");
if (!fs.existsSync(abs)) return 0;
return fs.readdirSync(abs).filter((f) => f.endsWith(".sql")).length;
}
// STRICT: canonical i18n locale count, read from the shared config.
export function countLocales() {
const abs = path.join(ROOT, "config", "i18n.json");
@@ -148,6 +159,15 @@ function readCodeFacts() {
'import {notionTools} from "./open-sse/mcp-server/tools/notionTools.ts";',
'import {obsidianTools} from "./open-sse/mcp-server/tools/obsidianTools.ts";',
'import {compressionTools} from "./open-sse/mcp-server/tools/compressionTools.ts";',
// Live provider total — the SAME collections gen-provider-reference.ts unions, so the
// doc-vs-live check below cannot drift from the generator's definition of "provider".
'import * as PROV from "./src/shared/constants/providers.ts";',
"const provCols=[PROV.FREE_PROVIDERS,PROV.NOAUTH_PROVIDERS,PROV.OAUTH_PROVIDERS,",
"PROV.WEB_COOKIE_PROVIDERS,PROV.APIKEY_PROVIDERS,PROV.LOCAL_PROVIDERS,PROV.SEARCH_PROVIDERS,",
"PROV.AUDIO_ONLY_PROVIDERS,PROV.UPSTREAM_PROXY_PROVIDERS,PROV.CLOUD_AGENT_PROVIDERS,",
"PROV.SYSTEM_PROVIDERS];",
"const pids=new Set();",
"for(const c of provCols)for(const p of Object.values(c||{}))if(p&&p.id)pids.add(p.id);",
"const cols={MCP_TOOLS,memoryTools,skillTools,agentSkillTools,githubSkillTools,poolTools,",
"gamificationTools,pluginTools,notionTools,obsidianTools,compressionTools};",
"const sc=new Set();",
@@ -158,7 +178,7 @@ function readCodeFacts() {
'console.log("@@"+JSON.stringify({freeSteady:t.steadyRecurringTokens,',
"freeFirst:t.firstMonthRealisticTokens,freePools:t.poolCount,engines:ENGINE_IDS.length,",
"cliTotal:cli.length,cliCode:by('code'),cliAgent:by('agent'),",
"mcpTools:countUniqueMcpTools(cols),mcpScopes:sc.size}));",
"mcpTools:countUniqueMcpTools(cols),mcpScopes:sc.size,providers:pids.size}));",
].join("");
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "docs-counts-"));
try {
@@ -252,6 +272,82 @@ export function makeNumberClaimValidator(expected, opts) {
};
}
// --- v3.8.50 hardening validators --------------------------------------------
// PURE: doc total must equal the live provider-module total (closes the falso-verde
// found in the 2026-08-12 audit: the doc sat hand-stale at 291 while the modules
// defined 338, and every downstream check inherited the stale total).
export function makeProviderReferenceValidator(expected) {
return (content) => {
const total = parseProviderTotal(content);
if (!total) return { ok: false, detail: "no `Total providers: **N**` marker found" };
if (total === expected)
return { ok: true, detail: `doc total ${total} matches the live provider modules` };
return {
ok: false,
detail:
`doc total ${total} is stale — the live provider modules define ${expected} ` +
`(run npm run gen:provider-reference)`,
};
};
}
// PURE: the npm package description must carry the live provider count.
export function makePackageDescriptionValidator(expected) {
return (content) => {
let desc = "";
try {
desc = String(JSON.parse(content).description || "");
} catch {
return { ok: false, detail: "package.json could not be parsed" };
}
if (desc.includes(String(expected)))
return { ok: true, detail: `description mentions the live provider count ${expected}` };
return {
ok: false,
detail: `description does not mention the live provider count ${expected}: "${desc}"`,
};
};
}
// PURE: sweep an SVG's text/aria content for the canonical numbers. Patterns are
// deliberately narrow — they anchor on the surrounding words so path coordinates,
// width/font-size attributes and small unrelated counts ("15 providers ToS-flagged",
// "100+ providers") can never register as claims. Providers require 3+ digits for the
// same reason.
const SVG_CANONICAL_PATTERNS = [
{ key: "providers", what: "providers", pattern: /(\d{3,4}) (?:AI )?providers\b/g },
{ key: "mcpTools", what: "MCP tools", pattern: /MCP (?:server with |with |\()(\d+)/g },
{ key: "strategies", what: "routing strategies", pattern: /(\d+) routing strategies\b/g },
{ key: "pools", what: "free-tier pools", pattern: /(\d+) provider pools\b/g },
];
export function checkSvgCanonicalNumbers(content, expected) {
const stale = [];
let claims = 0;
for (const { key, what, pattern } of SVG_CANONICAL_PATTERNS) {
if (expected[key] == null) continue;
for (const m of content.matchAll(pattern)) {
claims++;
const value = Number(m[1]);
if (value !== expected[key]) stale.push(`"${m[0]}" (${what} — code has ${expected[key]})`);
}
}
if (!claims) return { ok: true, detail: "no canonical-number claims in this SVG" };
if (!stale.length) return { ok: true, detail: `${claims} canonical claim(s) match the code` };
return { ok: false, detail: `stale: ${[...new Set(stale)].join(", ")}` };
}
// The README-embedded diagrams that historically rotted because no gate read them
// (the alt-text in README.md is checked, the SVG text nodes never were).
const SVG_DIAGRAM_FILES = [
"docs/diagrams/readme-hero.svg",
"docs/diagrams/free-tier-budget.svg",
"docs/diagrams/promise-pillars.svg",
"docs/diagrams/comparison-table.svg",
"docs/diagrams/cli-terminal.svg",
"docs/diagrams/tier-cascade.svg",
];
export function buildChecks() {
return [
{
@@ -259,7 +355,26 @@ export function buildChecks() {
actual: readProviderTotal(),
docKey: "providers",
strict: true,
files: ["README.md", "AGENTS.md"],
files: ["README.md", "AGENTS.md", "llm.txt"],
},
{
label: "Provider count (package.json description)",
actual: readProviderTotal(),
docKey: "providers",
strict: true,
files: ["package.json"],
validate: makePackageDescriptionValidator(readProviderTotal()),
},
{
label: "DB migrations count",
actual: countMigrations(),
docKey: "migrations",
strict: true,
files: ["README.md", "AGENTS.md", "llm.txt"],
validate: makeNumberClaimValidator(countMigrations(), {
what: "migrations",
pattern: /(\d+)\+? migrations?\b/gi,
}),
},
{
label: "i18n locales count",
@@ -289,6 +404,30 @@ export function buildChecks() {
validate: makeNumberClaimValidator(expected, { what, ...opts }),
});
return [
{
label: "Provider reference total (doc vs live modules)",
actual: f.providers,
docKey: "providers (live)",
strict: true,
files: ["docs/reference/PROVIDER_REFERENCE.md"],
validate: makeProviderReferenceValidator(f.providers),
},
{
label: "SVG canonical numbers (live code)",
actual:
`${f.providers} providers / ${f.mcpTools} MCP tools / ` +
`${countRoutingStrategies()} strategies / ${f.freePools} pools`,
docKey: "SVG canonical numbers",
strict: true,
files: SVG_DIAGRAM_FILES,
validate: (content) =>
checkSvgCanonicalNumbers(content, {
providers: f.providers,
mcpTools: f.mcpTools,
strategies: countRoutingStrategies(),
pools: f.freePools,
}),
},
{
label: "Free-tier headline (live catalog)",
actual: `~${(f.freeSteady / 1e9).toFixed(2)}B steady / ${f.freePools} pools`,

View File

@@ -363,19 +363,6 @@ const SKIP_DOC_FILES = new Set([
"docs/reference/PROVIDER_REFERENCE.md", // auto-generated from providers.ts
"docs/openapi.yaml",
"docs/i18n", // translations — separate workflow
// Design / research / plan docs: by definition describe not-yet-built files and
// proposed (not-yet-shipped) endpoints (each carries a `Status: Design`/`Active
// research`/`Plano` header). Same rationale as the audit report above — these are
// forward-looking specs, not living API docs, so their forward references are
// expected, not fabrications.
"docs/research", // DISCOVERY_TOOL_DESIGN.md, UNLIMITED_LLM_ACCESS.md, …
"docs/superpowers/plans", // dated implementation plans (files described before they exist)
"docs/superpowers/specs", // dated research/spec reports (point-in-time findings, may cite proposed/not-yet-built endpoints, env vars, and files) — same rationale as the plans/research dirs above
// Release notes are historical, point-in-time records: they intentionally describe
// modules/paths as they were at that release (e.g. a module later moved or renamed).
// Rewriting them to today's layout would falsify history — out of scope for a
// living-docs accuracy gate.
"docs/releases",
// Forward-looking coverage plan: a `- [ ]` checklist of test targets and helper
// components to be created. Same rationale as the design/plan docs above.
"docs/ops/COVERAGE_PLAN.md",

View File

@@ -10,14 +10,39 @@
// - coverage/ — relatórios de cobertura gerados pelo c8
// - quality-metrics.json — saída do collect-metrics.mjs (gerado, não-versionado)
// - symlinks rastreados (mode 120000) — indício de `git add -A` em worktree
// - _tasks (exato E prefixo) — repo git SEPARADO; o blob symlink rastreado causou DOIS
// wipes do diretório real (2026-08-08 e 2026-08-10; Hard Rule #23)
// - _references/ _mono_repo/ _ideia/ _cache/ — diretórios privados de raiz (regra /_*/)
// - .claude/worktrees/ — worktrees de sessão nunca entram no repo
// - docs/superpowers/ — artefatos de planejamento vivem em _tasks/, não em docs/
// - .eslintcache* .fakebin-* dist/ .build/ .artifacts/ logs/ — caches e outputs gerados
//
// Todos os prefixos são ancorados na raiz (startsWith sobre paths do `git ls-files`):
// paths aninhados legítimos como `src/lib/logs/` NÃO são atingidos.
import { execFileSync } from "node:child_process";
import { pathToFileURL } from "node:url";
const FORBIDDEN_PREFIXES = ["node_modules/", ".next/", "coverage/"];
const FORBIDDEN_PREFIXES = [
"node_modules/",
".next/",
"coverage/",
// "_" na raiz é GENÉRICO (regra abaixo em checkTrackedArtifacts): _tasks/, _references/,
// _mono_repo/, _ideia/, _cache/ e qualquer _<novo>/ futuro — dirs privados, alguns com
// repo git próprio (_tasks). Nunca rastrear nada dentro deles (Hard Rule #23).
".claude/worktrees/",
"docs/superpowers/",
".eslintcache", // matches .eslintcache, .eslintcache-complexity, .eslintcache-probe, …
".fakebin-", // test executable shim dirs (.fakebin-<pid>/)
"dist/",
".build/",
".artifacts/",
"logs/",
];
const FORBIDDEN_EXACT = new Set([
"quality-metrics.json", // legacy root location (still forbidden if a stale run writes it)
"config/quality/quality-metrics.json", // current generated location (collect-metrics.mjs)
"_tasks", // separate git repo — a tracked blob/symlink here wiped the real dir twice (HR#23)
]);
/**
@@ -36,6 +61,13 @@ export function checkTrackedArtifacts(trackedFiles, trackedSymlinks = []) {
violations.push(`forbidden tracked artifact: ${file}`);
continue;
}
// Regra genérica: NENHUM caminho de raiz prefixado com "_" pode ser rastreado
// (dir ou arquivo). Cobre _tasks, _references, _mono_repo e qualquer _<novo> futuro;
// paths aninhados legítimos (src/lib/_x) não são atingidos.
if (file.startsWith("_")) {
violations.push(`forbidden tracked artifact (root underscore path): ${file}`);
continue;
}
for (const prefix of FORBIDDEN_PREFIXES) {
if (file.startsWith(prefix)) {
violations.push(`forbidden tracked artifact (${prefix}*): ${file}`);

View File

@@ -0,0 +1,322 @@
#!/usr/bin/env node
// Blocks TypeScript 7 diagnostic regressions without requiring the existing
// migration backlog to be clean. The PR base and checked-out head are compiled
// with the same compiler and tsconfig, then compared as duplicate-preserving
// multisets of: relative file | TS code | normalized message.
import { spawnSync } from "node:child_process";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { pathToFileURL } from "node:url";
const ROOT = process.cwd();
const DEFAULT_TSCONFIG = "tsconfig.typecheck-core.json";
const DEFAULT_COMPILER_VERSION = "7.0.2";
const DIAGNOSTIC_START = /^(.+?)\((\d+),(\d+)\): error (TS\d+):\s*(.*)$/;
const GLOBAL_DIAGNOSTIC_START = /^error (TS\d+):\s*(.*)$/;
function normalizeSlashes(value) {
return String(value).replaceAll("\\", "/");
}
function stripRoot(value, root) {
const normalizedValue = normalizeSlashes(value);
const normalizedRoot = normalizeSlashes(path.resolve(root)).replace(/\/$/, "");
return normalizedValue === normalizedRoot
? "."
: normalizedValue.startsWith(`${normalizedRoot}/`)
? normalizedValue.slice(normalizedRoot.length + 1)
: normalizedValue;
}
export function normalizeDiagnosticMessage(message, root = ROOT) {
const normalizedRoot = normalizeSlashes(path.resolve(root)).replace(/\/$/, "");
return normalizeSlashes(message)
.replaceAll(normalizedRoot, "<repo>")
.replace(/((?:[A-Za-z]:)?[^()\s]+\.(?:[cm]?[jt]sx?|json))\(\d+,\d+\)/gi, "$1")
.replace(/\s+/g, " ")
.trim();
}
/** Parse complete `tsc --pretty false` diagnostic blocks. */
export function parseTscDiagnostics(raw, { root = ROOT } = {}) {
const diagnostics = [];
let current = null;
const flush = () => {
if (!current) return;
const message = normalizeDiagnosticMessage(current.messageLines.join("\n"), root);
diagnostics.push({
file: current.file,
code: current.code,
message,
key: `${current.file}\u0000${current.code}\u0000${message}`,
});
current = null;
};
for (const line of String(raw).split(/\r?\n/)) {
const located = DIAGNOSTIC_START.exec(line);
if (located) {
flush();
current = {
file: stripRoot(located[1], root),
code: located[4],
messageLines: [located[5]],
};
continue;
}
const global = GLOBAL_DIAGNOSTIC_START.exec(line);
if (global) {
flush();
current = { file: "<global>", code: global[1], messageLines: [global[2]] };
continue;
}
if (current && /^\s/.test(line) && line.trim()) current.messageLines.push(line);
}
flush();
return diagnostics;
}
export function toDiagnosticMultiset(diagnostics) {
const counts = new Map();
for (const diagnostic of diagnostics) {
const entry = counts.get(diagnostic.key) ?? { ...diagnostic, count: 0 };
entry.count += 1;
counts.set(diagnostic.key, entry);
}
return counts;
}
export function diffDiagnosticMultisets(baseDiagnostics, headDiagnostics) {
const base = toDiagnosticMultiset(baseDiagnostics);
const head = toDiagnosticMultiset(headDiagnostics);
const added = [];
const removed = [];
for (const [key, entry] of head) {
const baseCount = base.get(key)?.count ?? 0;
if (entry.count > baseCount) {
added.push({ ...entry, baseCount, headCount: entry.count, delta: entry.count - baseCount });
}
}
for (const [key, entry] of base) {
const headCount = head.get(key)?.count ?? 0;
if (entry.count > headCount) {
removed.push({ ...entry, baseCount: entry.count, headCount, delta: entry.count - headCount });
}
}
const order = (a, b) => a.key.localeCompare(b.key);
return { added: added.sort(order), removed: removed.sort(order) };
}
export function hasParserPrerequisite(diagnostics) {
return diagnostics.some((diagnostic) => diagnostic.code === "TS1005");
}
function argument(name, fallback = "") {
const index = process.argv.indexOf(name);
return index >= 0 && process.argv[index + 1] ? process.argv[index + 1] : fallback;
}
function run(command, args, options = {}) {
return spawnSync(command, args, {
cwd: ROOT,
encoding: "utf8",
maxBuffer: 64 * 1024 * 1024,
...options,
});
}
function resolveCommit(ref) {
const result = run("git", ["rev-parse", "--verify", `${ref}^{commit}`]);
if (result.status !== 0) {
throw new Error(`cannot resolve base ref ${ref}: ${result.stderr.trim()}`);
}
return result.stdout.trim();
}
function sameLockfile(baseRoot) {
const head = path.join(ROOT, "package-lock.json");
const base = path.join(baseRoot, "package-lock.json");
return (
fs.existsSync(head) &&
fs.existsSync(base) &&
fs.readFileSync(head).equals(fs.readFileSync(base))
);
}
function linkDependencies(baseRoot) {
const source = path.join(ROOT, "node_modules");
const target = path.join(baseRoot, "node_modules");
if (!fs.existsSync(source)) throw new Error("node_modules is missing; run npm ci first");
fs.mkdirSync(target);
for (const entry of fs.readdirSync(source)) {
if (entry === "@omniroute") continue;
fs.symlinkSync(path.join(source, entry), path.join(target, entry), "junction");
}
const scope = path.join(target, "@omniroute");
fs.mkdirSync(scope);
fs.symlinkSync(path.join(baseRoot, "open-sse"), path.join(scope, "open-sse"), "junction");
fs.symlinkSync(
path.join(baseRoot, "packages", "browser-pool"),
path.join(scope, "browser-pool"),
"junction"
);
}
function installBaseDependencies(baseRoot) {
const npm = process.platform === "win32" ? "npm.cmd" : "npm";
const result = run(
npm,
["ci", "--ignore-scripts", "--prefer-offline", "--no-audit", "--no-fund"],
{ cwd: baseRoot, stdio: "inherit" }
);
if (result.status !== 0) throw new Error(`npm ci for the base worktree exited ${result.status}`);
}
function runTypeScript(root, tsconfig, compilerVersion) {
const npm = process.platform === "win32" ? "npm.cmd" : "npm";
const result = run(
npm,
[
"exec",
"--yes",
`--package=typescript@${compilerVersion}`,
"--",
"tsc",
"--pretty",
"false",
"--noEmit",
"-p",
tsconfig,
],
{ cwd: root }
);
if (result.error) throw result.error;
const output = `${result.stdout ?? ""}${result.stderr ?? ""}`;
const diagnostics = parseTscDiagnostics(output, { root });
if (result.status !== 0 && diagnostics.length === 0) {
throw new Error(
`TypeScript exited ${result.status} without a parseable diagnostic:\n${output}`
);
}
return { diagnostics, status: result.status ?? 0 };
}
function formatEntry(entry) {
return `${entry.file} ${entry.code}: ${entry.message} (${entry.baseCount} -> ${entry.headCount})`;
}
function appendSummary({ baseRef, baseCount, headCount, added, removed, skipped }) {
const summary = process.env.GITHUB_STEP_SUMMARY;
if (!summary) return;
const lines = [
"## TypeScript 7 zero-new-diagnostics ratchet",
"",
`- Base: \`${baseRef}\` (${baseCount} diagnostics)`,
`- Head: ${headCount} diagnostics`,
`- Added: ${added.reduce((sum, entry) => sum + entry.delta, 0)}`,
`- Removed: ${removed.reduce((sum, entry) => sum + entry.delta, 0)}`,
];
if (skipped) lines.push("- Status: parser prerequisite unresolved; comparison is advisory");
if (added.length) {
lines.push("", "### Added diagnostics", "", ...added.map((entry) => `- ${formatEntry(entry)}`));
}
fs.appendFileSync(summary, `${lines.join("\n")}\n`);
}
function main() {
const baseRef = argument("--base-ref", process.env.TS7_BASE_REF ?? "");
const tsconfig = argument("--tsconfig", DEFAULT_TSCONFIG);
const compilerVersion = argument("--compiler-version", DEFAULT_COMPILER_VERSION);
if (!baseRef) {
console.log("[ts7-ratchet] SKIP — --base-ref is required outside a pull request");
return 0;
}
if (!fs.existsSync(path.join(ROOT, tsconfig))) {
throw new Error(`tsconfig not found: ${tsconfig}`);
}
const baseCommit = resolveCommit(baseRef);
const temporaryRoot = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-ts7-ratchet-"));
const baseRoot = path.join(temporaryRoot, "base");
let worktreeAdded = false;
try {
const add = run("git", ["worktree", "add", "--detach", baseRoot, baseCommit]);
if (add.status !== 0) throw new Error(`cannot create base worktree: ${add.stderr.trim()}`);
worktreeAdded = true;
if (sameLockfile(baseRoot)) linkDependencies(baseRoot);
else installBaseDependencies(baseRoot);
console.log(
`[ts7-ratchet] TypeScript ${compilerVersion}; base=${baseCommit}; config=${tsconfig}`
);
const base = runTypeScript(baseRoot, tsconfig, compilerVersion);
const head = runTypeScript(ROOT, tsconfig, compilerVersion);
const { added, removed } = diffDiagnosticMultisets(base.diagnostics, head.diagnostics);
const parserBlocked = hasParserPrerequisite(base.diagnostics);
console.log(`ts7DiagnosticsBase=${base.diagnostics.length}`);
console.log(`ts7DiagnosticsHead=${head.diagnostics.length}`);
console.log(`ts7DiagnosticsAdded=${added.reduce((sum, entry) => sum + entry.delta, 0)}`);
console.log(`ts7DiagnosticsRemoved=${removed.reduce((sum, entry) => sum + entry.delta, 0)}`);
appendSummary({
baseRef: baseCommit,
baseCount: base.diagnostics.length,
headCount: head.diagnostics.length,
added,
removed,
skipped: parserBlocked,
});
if (parserBlocked) {
console.warn(
"[ts7-ratchet] SKIP — the release base still has TS1005 parser diagnostics. " +
"Resolve #10094 before making this comparison blocking; those errors are not accepted as baseline."
);
return 0;
}
if (added.length) {
console.error(
`[ts7-ratchet] FAIL — the PR adds ${added.reduce((sum, entry) => sum + entry.delta, 0)} ` +
`normalized TypeScript 7 diagnostic(s):\n${added.map((entry) => `${formatEntry(entry)}`).join("\n")}`
);
return 1;
}
console.log(
`[ts7-ratchet] OK — no new normalized diagnostics; ` +
`${removed.reduce((sum, entry) => sum + entry.delta, 0)} removed.`
);
return 0;
} finally {
if (worktreeAdded) {
const remove = run("git", ["worktree", "remove", "--force", baseRoot]);
if (remove.status !== 0) {
console.warn(`[ts7-ratchet] WARN — temporary worktree cleanup: ${remove.stderr.trim()}`);
}
run("git", ["worktree", "prune"]);
}
fs.rmSync(temporaryRoot, { recursive: true, force: true });
}
}
if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
try {
process.exitCode = main();
} catch (error) {
console.error(`[ts7-ratchet] FAIL — ${error instanceof Error ? error.message : String(error)}`);
process.exitCode = 2;
}
}

View File

@@ -7,6 +7,7 @@ import path from "node:path";
import { fileURLToPath } from "node:url";
import {
FREE_PROVIDERS,
NOAUTH_PROVIDERS,
OAUTH_PROVIDERS,
WEB_COOKIE_PROVIDERS,
APIKEY_PROVIDERS,
@@ -136,6 +137,7 @@ function buildHeader(total: number): string {
"## Categories",
"",
"- **Free** — free tier with API key (configured via dashboard)",
"- **No-auth** — public endpoints that require no key or sign-in at all",
"- **OAuth** — sign-in flow handled by OmniRoute, no API key needed",
"- **Web cookie** — wraps the provider's web app via cookie auth",
"- **API key** — paid provider configured via API key (free credits may apply)",
@@ -159,8 +161,19 @@ function buildHeader(total: number): string {
].join("\n");
}
function countExecutorImpls(): number {
const dir = path.join(ROOT, "open-sse", "executors");
const nonImpl = new Set(["index.ts", "index.mts", "types.ts", "base.ts", "constants.ts"]);
return fs
.readdirSync(dir)
.filter(
(f) => f.endsWith(".ts") && !f.endsWith(".test.ts") && !f.startsWith("__") && !nonImpl.has(f)
).length;
}
function main() {
const free = asRecords(FREE_PROVIDERS);
const noauth = asRecords(NOAUTH_PROVIDERS as Record<string, ProviderRecord>);
const oauth = asRecords(OAUTH_PROVIDERS);
const webCookie = asRecords(WEB_COOKIE_PROVIDERS);
const apiKey = asRecords(APIKEY_PROVIDERS);
@@ -173,6 +186,7 @@ function main() {
const allIds = new Set<string>([
...free.map((p) => p.id),
...noauth.map((p) => p.id),
...oauth.map((p) => p.id),
...webCookie.map((p) => p.id),
...apiKey.map((p) => p.id),
@@ -186,6 +200,7 @@ function main() {
const sections = [
buildSection("Free Tier (OAuth-first or no-key)", free, "Free"),
buildSection("No-auth Providers (no key required)", noauth, "No-auth"),
buildSection("OAuth Providers", oauth, "OAuth"),
buildSection("Web Cookie Providers", webCookie, "Web cookie"),
buildSection("API Key Providers (paid / paid-with-free-credits)", apiKey, "API key"),
@@ -202,7 +217,7 @@ function main() {
"",
"- Catalog: [`src/shared/constants/providers.ts`](../../src/shared/constants/providers.ts)",
"- Registry (per-model details): [`open-sse/config/providerRegistry.ts`](../../open-sse/config/providerRegistry.ts)",
"- Executors: [`open-sse/executors/`](../../open-sse/executors/) (31 files)",
`- Executors: [\`open-sse/executors/\`](../../open-sse/executors/) (${countExecutorImpls()} implementations)`,
"- Translators: [`open-sse/translator/`](../../open-sse/translator/)",
"",
"## See Also",
@@ -218,9 +233,10 @@ function main() {
console.log(`✓ Wrote ${OUT_FILE}`);
console.log(` Providers: ${allIds.size} unique IDs`);
console.log(
` Sections: free=${free.length}, oauth=${oauth.length}, web=${webCookie.length}, ` +
`apikey=${apiKey.length}, local=${local.length}, search=${search.length}, ` +
`audio=${audio.length}, proxy=${upstreamProxy.length}, cloud=${cloudAgent.length}, system=${system.length}`
` Sections: free=${free.length}, noauth=${noauth.length}, oauth=${oauth.length}, ` +
`web=${webCookie.length}, apikey=${apiKey.length}, local=${local.length}, ` +
`search=${search.length}, audio=${audio.length}, proxy=${upstreamProxy.length}, ` +
`cloud=${cloudAgent.length}, system=${system.length}`
);
}

View File

@@ -1,29 +0,0 @@
#!/bin/bash
set -e
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PLUGIN_SRC="$(dirname "$SCRIPT_DIR")/obsidian-plugin"
DESKTOP_VAULT="${1:-$HOME/Documents/Vault/Omniroute-Test}"
MOBILE_VAULT="${2:-$HOME/Documents/Vault/Test}"
echo "Building plugin..."
cd "$PLUGIN_SRC"
npm run build 2>&1 | tail -3
echo "Installing to desktop vault: $DESKTOP_VAULT"
mkdir -p "$DESKTOP_VAULT/.obsidian/plugins/omniroute-sync"
cp "$PLUGIN_SRC/dist/main.js" "$DESKTOP_VAULT/.obsidian/plugins/omniroute-sync/"
cp "$PLUGIN_SRC/manifest.json" "$DESKTOP_VAULT/.obsidian/plugins/omniroute-sync/"
cp "$PLUGIN_SRC/styles.css" "$DESKTOP_VAULT/.obsidian/plugins/omniroute-sync/"
echo " ✓ Desktop plugin installed"
if [ -d "$MOBILE_VAULT" ]; then
echo "Installing to mobile vault: $MOBILE_VAULT"
mkdir -p "$MOBILE_VAULT/.obsidian/plugins/omniroute-sync"
cp "$PLUGIN_SRC/dist/main.js" "$MOBILE_VAULT/.obsidian/plugins/omniroute-sync/"
cp "$PLUGIN_SRC/manifest.json" "$MOBILE_VAULT/.obsidian/plugins/omniroute-sync/"
cp "$PLUGIN_SRC/styles.css" "$MOBILE_VAULT/.obsidian/plugins/omniroute-sync/"
echo " ✓ Mobile plugin installed"
fi
echo "Done! Restart Obsidian on both devices to load the plugin."

View File

@@ -1,12 +0,0 @@
const Database = require('better-sqlite3');
const path = require('path');
const dbPath = path.resolve(process.env.USERPROFILE, '.omniroute', 'storage.sqlite');
try {
const db = new Database(dbPath, { readonly: true });
const rows = db.prepare(`SELECT id, provider, name, auth_type, is_active, last_error, test_status, updated_at FROM provider_connections ORDER BY updated_at DESC LIMIT 200`).all();
console.log(JSON.stringify(rows, null, 2));
db.close();
} catch (err) {
console.error('ERROR', err && err.message);
process.exit(2);
}

View File

@@ -1,12 +0,0 @@
const Database = require('better-sqlite3');
const path = require('path');
const dbPath = path.resolve(process.env.USERPROFILE, '.omniroute', 'storage.sqlite');
try {
const db = new Database(dbPath, { readonly: true });
const rows = db.prepare(`SELECT id, provider, name, auth_type, is_active, api_key IS NOT NULL as has_api_key, access_token IS NOT NULL as has_access_token, refresh_token IS NOT NULL as has_refresh, last_error, test_status, provider_specific_data, updated_at FROM provider_connections WHERE provider LIKE '%anthropic%' OR provider='anthropic' OR provider LIKE '%claude%' OR provider='claude'`).all();
console.log(JSON.stringify(rows, null, 2));
db.close();
} catch (err) {
console.error('ERROR', err && err.message);
process.exit(2);
}

View File

@@ -1,16 +0,0 @@
const Database = require("better-sqlite3");
const path = require("path");
const dbPath = path.resolve(process.env.USERPROFILE, ".omniroute", "storage.sqlite");
try {
const db = new Database(dbPath, { readonly: true });
const rows = db
.prepare(
`SELECT id, provider, name, auth_type, is_active, api_key IS NOT NULL as has_api_key, access_token IS NOT NULL as has_access_token, refresh_token IS NOT NULL as has_refresh, last_error, test_status, provider_specific_data, updated_at FROM provider_connections WHERE provider LIKE '%anthropic%' OR provider='anthropic' OR provider LIKE '%claude%' OR provider='claude'`
)
.all();
console.log(JSON.stringify(rows, null, 2));
db.close();
} catch (err) {
console.error("ERROR", err && err.message);
process.exit(2);
}