Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
29c5a03
fix(cli-tools): guard modelId type before calling indexOf
diegosouzapw May 19, 2026
ef800ee
fix(offline): avoid SSR/CSR hydration mismatch on navigator.onLine
diegosouzapw May 19, 2026
5f37f0f
chore(i18n): add missing en.json keys for translator, cli-tools, memo…
diegosouzapw May 19, 2026
8d34f4c
chore(i18n): replace hardcoded UI text with t() calls across dashboar…
diegosouzapw May 20, 2026
f8d9873
chore(i18n): replace hardcoded UI text with t() calls across dashboar…
diegosouzapw May 20, 2026
55913d1
chore(i18n): replace hardcoded UI text with t() calls across dashboar…
diegosouzapw May 20, 2026
16277bd
chore(i18n): localize remaining dashboard settings labels
diegosouzapw May 20, 2026
42e1466
chore(i18n): replace hardcoded UI text with t() calls across dashboar…
diegosouzapw May 20, 2026
546d7e9
chore(i18n): replace hardcoded UI text with t() calls across dashboar…
diegosouzapw May 20, 2026
c0c2eff
chore(i18n): resolve last 2 missing providers/[id] keys
diegosouzapw May 20, 2026
6e1105e
chore(i18n): replace hardcoded UI text with t() calls across dashboar…
diegosouzapw May 20, 2026
8a44ed5
chore(i18n): finalize round-6 keys for batch/cache/endpoint/usage
diegosouzapw May 20, 2026
5ef3482
fix(playground): dedupe filteredModels to avoid duplicate React key w…
diegosouzapw May 20, 2026
49fe356
fix(playground): guard against non-string model ids before .split/.st…
diegosouzapw May 20, 2026
3ff3e3d
fix(playground): guard ChatPlayground filteredModels for non-string ids
diegosouzapw May 20, 2026
fbf37ae
fix(claude): drop orphan tool_result after fixToolAdjacency strip (di…
diegosouzapw May 20, 2026
ec23a04
fix(mitm): point runtime manager re-export to js entrypoint
diegosouzapw May 20, 2026
8bd125e
docs: add AgentRouter setup guide (#2422)
leninejunior May 20, 2026
ae94b26
feat: add new feature on combos - falloverBeforeRetry (#2417)
hartmark May 20, 2026
7dfcd0a
feat(batch): implement 10 feature requests harvested (#2414)
diegosouzapw May 20, 2026
8536593
fix(gamification): resolve SQL bug, auth gap, pagination, and anomaly…
oyi77 May 20, 2026
cd9d684
chore(merge): sync release/v3.8.0 with remote — resolve .gitignore co…
diegosouzapw May 20, 2026
78c344b
docs(changelog): add post-release entries for #2414 #2417 #2421 #2422
diegosouzapw May 20, 2026
5735b4d
fix(security): resolve CodeQL random/password-hash alerts and sync do…
diegosouzapw May 20, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
888 changes: 0 additions & 888 deletions .agents/skills/implement-features/SKILL.md

This file was deleted.

881 changes: 0 additions & 881 deletions .agents/workflows/implement-features-ag.md

This file was deleted.

881 changes: 0 additions & 881 deletions .claude/commands/implement-features-cc.md

This file was deleted.

10 changes: 10 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -153,3 +153,13 @@ http-client.private.env.json

# Feature-triage ephemeral artifact (regenerated each run)
_ideia/_triage.json

# i18n audit artifact (generated by scripts/i18n/audit-dashboard-pages.mjs)
scripts/i18n/_audit.json
scripts/i18n/_pending-keys.json

# Private workflow / skill / command implementations
# These contain proprietary multi-phase logic and should not be committed
.agents/workflows/implement-features-ag.md
.agents/skills/implement-features/
.claude/commands/implement-features-cc.md
8 changes: 4 additions & 4 deletions .semgrep/rules/cli-no-sqlite.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ rules:
- pattern: new Database(...)
paths:
include:
- "bin/**"
- "/bin/**"
exclude:
- "bin/cli/sqlite.mjs"
- "/bin/cli/sqlite.mjs"
message: >
Direct SQLite access in bin/ is banned. Use src/lib/db/* helpers or
withRuntime() from bin/cli/runtime.mjs. See CLAUDE.md hard rule #5 and
Expand All @@ -21,9 +21,9 @@ rules:
- pattern: $DB.prepare("UPDATE $TABLE SET ...")
paths:
include:
- "bin/**"
- "/bin/**"
exclude:
- "bin/cli/sqlite.mjs"
- "/bin/cli/sqlite.mjs"
message: >
Raw SQL in bin/ is banned. Use src/lib/db/* helpers. See CLAUDE.md
hard rule #5 and bin/cli/CONVENTIONS.md.
Expand Down
234 changes: 196 additions & 38 deletions CHANGELOG.md

Large diffs are not rendered by default.

16 changes: 4 additions & 12 deletions bin/cli/commands/backup.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@ import {
mkdirSync,
readdirSync,
readFileSync,
unlinkSync,
writeFileSync,
} from "node:fs";
import { createCipheriv, createDecipheriv, randomBytes, scryptSync } from "node:crypto";
import { dirname, join, extname, basename } from "node:path";
import { resolveDataDir } from "../data-dir.mjs";
import { apiFetch, isServerUp } from "../api.mjs";
import { t } from "../i18n.mjs";
import { backupSqliteFile } from "../sqlite.mjs";

function getBackupDir() {
return join(resolveDataDir(), "backups");
Expand Down Expand Up @@ -187,13 +189,6 @@ export async function runBackupCommand(opts = {}) {
try {
if (!existsSync(backupDir)) mkdirSync(backupDir, { recursive: true });

let Database;
try {
Database = (await import("better-sqlite3")).default;
} catch {
Database = null;
}

let backedUp = 0;
let skipped = 0;

Expand All @@ -207,14 +202,11 @@ export async function runBackupCommand(opts = {}) {
const destName = opts.encrypt ? `${file.name}.enc` : file.name;
const destPath = join(backupPath, destName);
mkdirSync(dirname(destPath), { recursive: true });
if (file.name.endsWith(".sqlite") && Database) {
const db = new Database(sourcePath, { readonly: true });
if (file.name.endsWith(".sqlite")) {
const tmpPath = destPath.replace(/\.enc$/, "");
await db.backup(tmpPath);
db.close();
await backupSqliteFile(sourcePath, tmpPath);
if (opts.encrypt) {
encryptFile(tmpPath, destPath, passphrase);
const { unlinkSync } = await import("node:fs");
unlinkSync(tmpPath);
}
} else if (opts.encrypt) {
Expand Down
67 changes: 6 additions & 61 deletions bin/cli/commands/doctor.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { pathToFileURL } from "node:url";
import { resolveDataDir, resolveStoragePath } from "../data-dir.mjs";
import { printHeading } from "../io.mjs";
import { t } from "../i18n.mjs";
import { readDatabaseHealth, readEncryptedCredentialSamples } from "../sqlite.mjs";

const STATIC_SALT = "omniroute-field-encryption-v1";
const KEY_LENGTH = 32;
Expand Down Expand Up @@ -78,14 +79,6 @@ function checkConfig(dataDir) {
return ok("Config", `.env found at ${envFile}`, { envFile });
}

async function loadBetterSqlite() {
try {
return (await import("better-sqlite3")).default;
} catch (error) {
return { error };
}
}

function resolveMigrationsDir(rootDir) {
const configured = process.env.OMNIROUTE_MIGRATIONS_DIR;
const candidates = [
Expand Down Expand Up @@ -115,18 +108,9 @@ async function checkDatabase(dbPath, rootDir) {
return warn("Database", `SQLite database not found at ${dbPath}`, { dbPath });
}

const Database = await loadBetterSqlite();
if (Database.error) {
return fail("Database", "better-sqlite3 could not be loaded", {
error: Database.error instanceof Error ? Database.error.message : String(Database.error),
});
}

let db;
try {
db = new Database(dbPath, { readonly: true, fileMustExist: true });
const quickCheck = db.prepare("PRAGMA quick_check").get();
const quickCheckValue = Object.values(quickCheck || {})[0];
const { quickCheckValue, hasMigrationTable, appliedMigrationVersions } =
await readDatabaseHealth(dbPath);
if (quickCheckValue !== "ok") {
return fail("Database", `SQLite quick_check failed: ${quickCheckValue}`, { dbPath });
}
Expand All @@ -137,18 +121,11 @@ async function checkDatabase(dbPath, rootDir) {
return ok("Database", "SQLite quick_check passed", { dbPath, migrations: "not_checked" });
}

const table = db
.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?")
.get("_omniroute_migrations");
if (!table) {
if (!hasMigrationTable) {
return warn("Database", "SQLite is readable, but migration table is missing", { dbPath });
}

const appliedRows = db
.prepare("SELECT version FROM _omniroute_migrations")
.all()
.map((row) => row.version);
const applied = new Set(appliedRows);
const applied = new Set(appliedMigrationVersions);
const pending = migrationFiles.filter((migration) => !applied.has(migration.version));

if (pending.length > 0) {
Expand All @@ -164,8 +141,6 @@ async function checkDatabase(dbPath, rootDir) {
dbPath,
error: error instanceof Error ? error.message : String(error),
});
} finally {
if (db) db.close();
}
}

Expand Down Expand Up @@ -203,42 +178,14 @@ async function checkStorageEncryption(dbPath) {
: warn("Storage/encryption", "No STORAGE_ENCRYPTION_KEY configured; passthrough mode");
}

const Database = await loadBetterSqlite();
if (Database.error) {
return fail("Storage/encryption", "Could not inspect encrypted credentials", {
error: Database.error instanceof Error ? Database.error.message : String(Database.error),
});
}

let db;
try {
db = new Database(dbPath, { readonly: true, fileMustExist: true });
const hasProviderTable = db
.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?")
.get("provider_connections");
const { hasProviderTable, encryptedValues } = await readEncryptedCredentialSamples(dbPath);
if (!hasProviderTable) {
return secret
? ok("Storage/encryption", "Encryption key is configured; provider table not initialized")
: warn("Storage/encryption", "No STORAGE_ENCRYPTION_KEY configured; passthrough mode");
}

const rows = db
.prepare(
`SELECT api_key, access_token, refresh_token, id_token
FROM provider_connections
WHERE api_key LIKE 'enc:v1:%'
OR access_token LIKE 'enc:v1:%'
OR refresh_token LIKE 'enc:v1:%'
OR id_token LIKE 'enc:v1:%'
LIMIT 20`
)
.all();
const encryptedValues = rows.flatMap((row) =>
["api_key", "access_token", "refresh_token", "id_token"]
.filter((key) => typeof row[key] === "string" && row[key].startsWith("enc:v1:"))
.map((key) => row[key])
);

if (encryptedValues.length === 0) {
return secret
? ok("Storage/encryption", "Encryption key is configured; no encrypted samples found")
Expand Down Expand Up @@ -268,8 +215,6 @@ async function checkStorageEncryption(dbPath) {
return fail("Storage/encryption", "Encrypted credential check failed", {
error: error instanceof Error ? error.message : String(error),
});
} finally {
if (db) db.close();
}
}

Expand Down
Loading