diff --git a/.omc/project-memory.json b/.omc/project-memory.json new file mode 100644 index 00000000..7018fa00 --- /dev/null +++ b/.omc/project-memory.json @@ -0,0 +1,250 @@ +{ + "version": "1.0.0", + "lastScanned": 1775016362438, + "projectRoot": "/home/openclaw/omniroute-src", + "techStack": { + "languages": [ + { + "name": "JavaScript/TypeScript", + "version": ">=18.0.0 <24.0.0", + "confidence": "high", + "markers": ["package.json"] + }, + { + "name": "TypeScript", + "version": null, + "confidence": "high", + "markers": ["tsconfig.json"] + } + ], + "frameworks": [ + { + "name": "express", + "version": "5.2.1", + "category": "backend" + }, + { + "name": "next", + "version": "16.0.10", + "category": "fullstack" + }, + { + "name": "react", + "version": "19.2.4", + "category": "frontend" + }, + { + "name": "react-dom", + "version": "19.2.4", + "category": "frontend" + }, + { + "name": "@playwright/test", + "version": "1.58.2", + "category": "testing" + }, + { + "name": "vitest", + "version": "4.0.18", + "category": "testing" + } + ], + "packageManager": "npm", + "runtime": "Node.js 18.0.024.0.0" + }, + "build": { + "buildCommand": "npm run build", + "testCommand": "npm test", + "lintCommand": "npm run lint", + "devCommand": "npm run dev", + "scripts": { + "dev": "node scripts/run-next.mjs dev", + "build": "node scripts/build-next-isolated.mjs", + "build:cli": "node scripts/prepublish.mjs", + "start": "node scripts/run-next.mjs start", + "lint": "eslint .", + "electron:dev": "concurrently \"npm run dev\" \"wait-on http://localhost:20128 && cd electron && npm run dev\"", + "electron:build": "npm run build && cd electron && npm run build", + "electron:build:win": "npm run build && cd electron && npm run build:win", + "electron:build:mac": "npm run build && cd electron && npm run build:mac", + "electron:build:linux": "npm run build && cd electron && npm run build:linux", + "test": "node --import tsx/esm --test tests/unit/*.test.mjs", + "test:unit": "node --import tsx/esm --test tests/unit/*.test.mjs", + "test:plan3": "node --import tsx/esm --test tests/unit/plan3-p0.test.mjs", + "test:fixes": "node --import tsx/esm --test tests/unit/fixes-p1.test.mjs", + "test:security": "node --import tsx/esm --test tests/unit/security-fase01.test.mjs", + "check:cycles": "node scripts/check-cycles.mjs", + "check:route-validation:t06": "node scripts/check-route-validation.mjs", + "check:any-budget:t11": "node scripts/check-t11-any-budget.mjs", + "check:docs-sync": "node scripts/check-docs-sync.mjs", + "typecheck:core": "tsc --pretty false -p tsconfig.typecheck-core.json", + "typecheck:noimplicit:core": "tsc --pretty false -p tsconfig.typecheck-noimplicit-core.json", + "test:integration": "node --import tsx/esm --test tests/integration/*.test.mjs", + "test:e2e": "node scripts/run-playwright-tests.mjs test tests/e2e/*.spec.ts", + "test:protocols:e2e": "node scripts/run-protocol-clients-tests.mjs", + "test:vitest": "vitest run open-sse/mcp-server/__tests__/*.test.ts open-sse/services/autoCombo/__tests__/*.test.ts", + "test:ecosystem": "node scripts/run-ecosystem-tests.mjs", + "test:coverage": "c8 --exclude=tests/** --exclude=**/*.test.* --reporter=text-summary --reporter=html --reporter=json-summary --reporter=lcov --check-coverage --statements 55 --lines 55 --functions 55 --branches 60 node --import tsx/esm --test tests/unit/*.test.mjs", + "test:coverage:legacy": "c8 --exclude=open-sse --check-coverage --lines 50 --functions 50 --branches 50 node --import tsx/esm --test tests/unit/*.test.mjs", + "coverage:report": "c8 report --exclude=tests/** --exclude=**/*.test.* --reporter=text --reporter=text-summary --reporter=html --reporter=json-summary --reporter=lcov", + "coverage:report:legacy": "c8 report --exclude=open-sse --reporter=text --reporter=text-summary", + "test:all": "npm run test:unit && npm run test:vitest && npm run test:ecosystem && npm run test:e2e", + "check": "npm run lint && npm run test", + "prepublishOnly": "npm run build:cli", + "postinstall": "node scripts/postinstall.mjs", + "prepare": "husky", + "system-info": "node scripts/system-info.mjs" + } + }, + "conventions": { + "namingStyle": "camelCase", + "importStyle": null, + "testPattern": null, + "fileOrganization": null + }, + "structure": { + "isMonorepo": true, + "workspaces": ["open-sse"], + "mainDirectories": ["bin", "docs", "public", "scripts", "src", "tests"], + "gitBranches": { + "defaultBranch": "main", + "branchingStrategy": null + } + }, + "customNotes": [], + "directoryMap": { + "bin": { + "path": "bin", + "purpose": "Executable scripts", + "fileCount": 3, + "lastAccessed": 1775016362426, + "keyFiles": ["mcp-server.mjs", "omniroute.mjs", "reset-password.mjs"] + }, + "docs": { + "path": "docs", + "purpose": "Documentation", + "fileCount": 14, + "lastAccessed": 1775016362426, + "keyFiles": [ + "A2A-SERVER.md", + "API_REFERENCE.md", + "ARCHITECTURE.md", + "AUTO-COMBO.md", + "CLI-TOOLS.md" + ] + }, + "electron": { + "path": "electron", + "purpose": null, + "fileCount": 5, + "lastAccessed": 1775016362431, + "keyFiles": ["README.md", "main.js", "package.json", "preload.js", "types.d.ts"] + }, + "images": { + "path": "images", + "purpose": null, + "fileCount": 1, + "lastAccessed": 1775016362434, + "keyFiles": ["omniroute.png"] + }, + "logs": { + "path": "logs", + "purpose": null, + "fileCount": 3, + "lastAccessed": 1775016362434, + "keyFiles": ["build_clean_tools.log", "build_debug.log", "build_force_clean.log"] + }, + "open-sse": { + "path": "open-sse", + "purpose": null, + "fileCount": 5, + "lastAccessed": 1775016362434, + "keyFiles": ["index.ts", "package.json", "tsconfig.json", "types.d.ts"] + }, + "public": { + "path": "public", + "purpose": "Public files", + "fileCount": 3, + "lastAccessed": 1775016362435, + "keyFiles": ["apple-touch-icon.svg", "favicon.svg", "icon-192.svg"] + }, + "scripts": { + "path": "scripts", + "purpose": "Build/utility scripts", + "fileCount": 23, + "lastAccessed": 1775016362435, + "keyFiles": [ + "bootstrap-env.mjs", + "build-next-isolated.mjs", + "check-cycles.mjs", + "check-docs-sync.mjs", + "check-route-validation.mjs" + ] + }, + "src": { + "path": "src", + "purpose": "Source code", + "fileCount": 4, + "lastAccessed": 1775016362435, + "keyFiles": ["instrumentation-node.ts", "instrumentation.ts", "proxy.ts", "server-init.ts"] + }, + "tests": { + "path": "tests", + "purpose": "Test files", + "fileCount": 0, + "lastAccessed": 1775016362435, + "keyFiles": [] + }, + "electron/assets": { + "path": "electron/assets", + "purpose": "Static assets", + "fileCount": 4, + "lastAccessed": 1775016362436, + "keyFiles": ["icon.icns", "icon.ico", "icon.png"] + }, + "open-sse/config": { + "path": "open-sse/config", + "purpose": "Configuration files", + "fileCount": 17, + "lastAccessed": 1775016362436, + "keyFiles": ["audioRegistry.ts", "cliFingerprints.ts", "codexInstructions.ts"] + }, + "open-sse/services": { + "path": "open-sse/services", + "purpose": "Business logic services", + "fileCount": 35, + "lastAccessed": 1775016362437, + "keyFiles": ["accountFallback.ts", "accountSelector.ts", "apiKeyRotator.ts"] + }, + "src/app": { + "path": "src/app", + "purpose": "Application code", + "fileCount": 7, + "lastAccessed": 1775016362438, + "keyFiles": ["error.tsx", "global-error.tsx", "globals.css"] + }, + "src/lib": { + "path": "src/lib", + "purpose": "Library code", + "fileCount": 30, + "lastAccessed": 1775016362438, + "keyFiles": ["apiBridgeServer.ts", "apiKeyExposure.ts", "cacheControlSettings.ts"] + }, + "src/middleware": { + "path": "src/middleware", + "purpose": "Middleware", + "fileCount": 1, + "lastAccessed": 1775016362438, + "keyFiles": ["promptInjectionGuard.ts"] + }, + "src/models": { + "path": "src/models", + "purpose": "Data models", + "fileCount": 1, + "lastAccessed": 1775016362438, + "keyFiles": ["index.ts"] + } + }, + "hotPaths": [], + "userDirectives": [] +} diff --git a/.omc/sessions/53c002c3-36a6-47c3-a52d-a8f756c264eb.json b/.omc/sessions/53c002c3-36a6-47c3-a52d-a8f756c264eb.json new file mode 100644 index 00000000..f3dfd0f2 --- /dev/null +++ b/.omc/sessions/53c002c3-36a6-47c3-a52d-a8f756c264eb.json @@ -0,0 +1,8 @@ +{ + "session_id": "53c002c3-36a6-47c3-a52d-a8f756c264eb", + "ended_at": "2026-04-01T04:06:04.924Z", + "reason": "prompt_input_exit", + "agents_spawned": 0, + "agents_completed": 0, + "modes_used": [] +} diff --git a/src/app/(dashboard)/dashboard/settings/components/CliproxyapiSettingsTab.tsx b/src/app/(dashboard)/dashboard/settings/components/CliproxyapiSettingsTab.tsx index 12094623..43ee16d3 100644 --- a/src/app/(dashboard)/dashboard/settings/components/CliproxyapiSettingsTab.tsx +++ b/src/app/(dashboard)/dashboard/settings/components/CliproxyapiSettingsTab.tsx @@ -1,34 +1,82 @@ "use client"; -import { useState, useEffect } from "react"; +import { useState, useEffect, useCallback } from "react"; import { Card, Button, Input, Toggle } from "@/shared/components"; +interface Settings { + cliproxyapi_fallback_enabled?: boolean; + cliproxyapi_url?: string; + cliproxyapi_fallback_codes?: string; + [key: string]: unknown; +} + +interface VersionManagerEntry { + tool: string; + status: string; + installedVersion: string | null; + healthStatus: string; + port: number; +} + +function isValidUrl(value: string): boolean { + try { + const url = new URL(value); + return url.protocol === "http:" || url.protocol === "https:"; + } catch { + return false; + } +} + export default function CliproxyapiSettingsTab() { - const [settings, setSettings] = useState>({}); + const [settings, setSettings] = useState({}); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); const [message, setMessage] = useState<{ type: string; text: string } | null>(null); - const [toolState, setToolState] = useState(null); + const [toolState, setToolState] = useState(null); + const [toolStateError, setToolStateError] = useState(null); useEffect(() => { fetch("/api/settings") - .then((r) => r.json()) + .then((r) => { + if (!r.ok) throw new Error(`Settings API returned ${r.status}`); + return r.json(); + }) .then((data) => { setSettings(data); setLoading(false); }) - .catch(() => setLoading(false)); + .catch((err) => { + console.error("Failed to load settings:", err); + setLoading(false); + }); fetch("/api/version-manager/status") - .then((r) => r.json()) - .then((data) => { - const entry = Array.isArray(data) ? data.find((t: any) => t.tool === "cliproxyapi") : null; - setToolState(entry); + .then((r) => { + if (!r.ok) throw new Error(`Version manager API returned ${r.status}`); + return r.json(); }) - .catch(() => {}); + .then((data) => { + const entry = Array.isArray(data) + ? data.find((t: VersionManagerEntry) => t.tool === "cliproxyapi") + : null; + setToolState(entry ?? null); + setToolStateError(null); + }) + .catch((err) => { + console.error("Failed to load version manager status:", err); + setToolStateError("Unable to reach version manager service"); + setToolState(null); + }); }, []); - const updateSetting = async (key: string, value: any) => { + const updateSetting = useCallback(async (key: string, value: boolean | string) => { + if (key === "cliproxyapi_url" && typeof value === "string" && value.trim() !== "") { + if (!isValidUrl(value)) { + setMessage({ type: "error", text: "Invalid URL format. Use http:// or https://" }); + return; + } + } + setSaving(true); setMessage(null); try { @@ -37,16 +85,18 @@ export default function CliproxyapiSettingsTab() { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ [key]: value }), }); - if (res.ok) { - setSettings((prev) => ({ ...prev, [key]: value })); - setMessage({ type: "success", text: "Setting saved" }); + if (!res.ok) { + throw new Error(`Server returned ${res.status}`); } + await res.json(); + setSettings((prev) => ({ ...prev, [key]: value })); + setMessage({ type: "success", text: "Setting saved" }); } catch { setMessage({ type: "error", text: "Failed to save setting" }); } finally { setSaving(false); } - }; + }, []); const cpaEnabled = settings.cliproxyapi_fallback_enabled === true; const cpaUrl = settings.cliproxyapi_url || "http://127.0.0.1:8317"; @@ -142,6 +192,8 @@ export default function CliproxyapiSettingsTab() { Loading... + ) : toolStateError ? ( +

{toolStateError}

) : toolState ? (
diff --git a/src/lib/db/migrations/014_create_memories.sql b/src/lib/db/migrations/014_create_memories.sql new file mode 100644 index 00000000..e98b0943 --- /dev/null +++ b/src/lib/db/migrations/014_create_memories.sql @@ -0,0 +1,22 @@ +-- 014_create_memories.sql +-- Memories table for persistent context storage. +-- Stores structured conversation memories with support for different memory types. + +CREATE TABLE IF NOT EXISTS memories ( + id TEXT PRIMARY KEY, + api_key_id TEXT NOT NULL, + session_id TEXT, + type TEXT NOT NULL CHECK(type IN ('factual', 'episodic', 'procedural', 'semantic')), + key TEXT, + content TEXT NOT NULL, + metadata TEXT, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + updated_at TEXT NOT NULL DEFAULT (datetime('now')), + expires_at TEXT +); + +-- Indexes for performance optimization +CREATE INDEX IF NOT EXISTS idx_memories_api_key ON memories(api_key_id); +CREATE INDEX IF NOT EXISTS idx_memories_session ON memories(session_id); +CREATE INDEX IF NOT EXISTS idx_memories_type ON memories(type); +CREATE INDEX IF NOT EXISTS idx_memories_expires ON memories(expires_at); diff --git a/src/lib/db/migrations/014_create_memories_down.sql.bak b/src/lib/db/migrations/014_create_memories_down.sql.bak new file mode 100644 index 00000000..68a218d0 --- /dev/null +++ b/src/lib/db/migrations/014_create_memories_down.sql.bak @@ -0,0 +1,4 @@ +-- 014_create_memories_down.sql +-- DOWN Migration: Remove memories table (Rollback) + +DROP TABLE IF EXISTS memories; diff --git a/src/lib/db/migrations/015_create_skills.sql b/src/lib/db/migrations/015_create_skills.sql new file mode 100644 index 00000000..f765efb2 --- /dev/null +++ b/src/lib/db/migrations/015_create_skills.sql @@ -0,0 +1,37 @@ +-- 015_create_skills.sql +-- Skills table for tool/function capability injection. +-- Stores skill definitions with schemas and execution tracking. + +CREATE TABLE IF NOT EXISTS skills ( + id TEXT PRIMARY KEY, + api_key_id TEXT NOT NULL, + name TEXT NOT NULL, + version TEXT NOT NULL DEFAULT '1.0.0', + description TEXT, + schema TEXT NOT NULL, + handler TEXT NOT NULL, + enabled INTEGER NOT NULL DEFAULT 1, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + updated_at TEXT NOT NULL DEFAULT (datetime('now')) +); + +CREATE TABLE IF NOT EXISTS skill_executions ( + id TEXT PRIMARY KEY, + skill_id TEXT NOT NULL, + api_key_id TEXT NOT NULL, + session_id TEXT, + input TEXT NOT NULL, + output TEXT, + status TEXT NOT NULL CHECK(status IN ('pending', 'running', 'success', 'error', 'timeout')), + error_message TEXT, + duration_ms INTEGER, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + FOREIGN KEY (skill_id) REFERENCES skills(id) ON DELETE CASCADE +); + +CREATE INDEX IF NOT EXISTS idx_skills_api_key ON skills(api_key_id); +CREATE INDEX IF NOT EXISTS idx_skills_name ON skills(name); +CREATE INDEX IF NOT EXISTS idx_skill_executions_skill ON skill_executions(skill_id); +CREATE INDEX IF NOT EXISTS idx_skill_executions_api_key ON skill_executions(api_key_id); +CREATE INDEX IF NOT EXISTS idx_skill_executions_status ON skill_executions(status); +CREATE INDEX IF NOT EXISTS idx_skill_executions_created ON skill_executions(created_at); \ No newline at end of file diff --git a/src/lib/db/migrations/015_create_skills_down.sql.bak b/src/lib/db/migrations/015_create_skills_down.sql.bak new file mode 100644 index 00000000..9bc12d02 --- /dev/null +++ b/src/lib/db/migrations/015_create_skills_down.sql.bak @@ -0,0 +1,5 @@ +-- 015_create_skills_down.sql +-- Rollback skills and skill_executions tables + +DROP TABLE IF EXISTS skill_executions; +DROP TABLE IF EXISTS skills; \ No newline at end of file diff --git a/src/lib/db/upstreamProxy.ts b/src/lib/db/upstreamProxy.ts index fdad87e7..e91a61b0 100644 --- a/src/lib/db/upstreamProxy.ts +++ b/src/lib/db/upstreamProxy.ts @@ -32,8 +32,9 @@ function toRecord(value: unknown): Record { const BLOCKED_HOSTNAMES = ["metadata.google.internal", "169.254.169.254", "metadata.aws.internal"]; function isPrivateHost(hostname: string): boolean { - if (BLOCKED_HOSTNAMES.includes(hostname)) return true; + // CLIProxyAPI runs on localhost:8317 — allow loopback explicitly if (hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1") return false; + if (BLOCKED_HOSTNAMES.includes(hostname)) return true; if ( /^10\./.test(hostname) || /^172\.(1[6-9]|2\d|3[01])\./.test(hostname) || diff --git a/src/lib/db/versionManager.ts b/src/lib/db/versionManager.ts index d72339b8..894efb9d 100644 --- a/src/lib/db/versionManager.ts +++ b/src/lib/db/versionManager.ts @@ -226,10 +226,28 @@ export async function updateVersionManagerTool( const existing = await getVersionManagerTool(tool); if (!existing) return null; + const ALLOWED_COLUMNS = new Set([ + "currentVersion", + "installedVersion", + "pinnedVersion", + "binaryPath", + "status", + "pid", + "port", + "apiKey", + "managementKey", + "autoUpdate", + "autoStart", + "healthStatus", + "configOverrides", + "errorMessage", + ]); + const sets: string[] = ["updated_at = datetime('now')"]; const params: Record = { tool }; for (const [key, value] of Object.entries(updates)) { + if (!ALLOWED_COLUMNS.has(key)) continue; const dbKey = key.replace(/([A-Z])/g, "_$1").toLowerCase(); if (key === "configOverrides") {