@arkheia/mcp-server
Advanced tools
| #!/usr/bin/env node | ||
| /** | ||
| * Arkheia MCP Server — thin Node wrapper that spawns the Python MCP server. | ||
| * | ||
| * This wrapper exists so that MCP clients can install via: | ||
| * npx @arkheia/mcp-server | ||
| * npm install -g @arkheia/mcp-server | ||
| * | ||
| * It: | ||
| * 1. Locates a Python 3.10+ interpreter | ||
| * 2. Ensures mcp_server dependencies are installed (pip install) | ||
| * 3. Spawns `python -m mcp_server.server` with stdio transport | ||
| * 4. Forwards stdin/stdout/stderr (MCP uses stdio) | ||
| * | ||
| * Environment variables: | ||
| * ARKHEIA_API_KEY — API key for hosted detection (required) | ||
| * ARKHEIA_PROXY_URL — Local proxy URL (optional, for enterprise) | ||
| * ARKHEIA_HOSTED_URL — Hosted API URL (default: https://arkheia-proxy-production.up.railway.app) | ||
| */ | ||
| const { spawn, execSync } = require("child_process"); | ||
| const path = require("path"); | ||
| const fs = require("fs"); | ||
| const ARKHEIA_HOME = path.join( | ||
| process.env.HOME || process.env.USERPROFILE || "/tmp", | ||
| ".arkheia" | ||
| ); | ||
| const REPO_DIR = path.join(ARKHEIA_HOME, "mcp"); | ||
| const BUNDLED_PYTHON_DIR = path.join(__dirname, "..", "python"); | ||
| const VENV_DIR = path.join(ARKHEIA_HOME, "venv"); | ||
| // Determine the real Python source: cloned repo > bundled package | ||
| function getServerDir() { | ||
| // If repo already cloned, use it | ||
| if (fs.existsSync(path.join(REPO_DIR, "mcp_server", "server.py"))) { | ||
| return REPO_DIR; | ||
| } | ||
| // If bundled package has the server code, use it | ||
| if (fs.existsSync(path.join(BUNDLED_PYTHON_DIR, "mcp_server", "server.py"))) { | ||
| return BUNDLED_PYTHON_DIR; | ||
| } | ||
| // Neither exists — clone the repo | ||
| process.stderr.write("[arkheia] Server code not found. Cloning from GitHub...\n"); | ||
| try { | ||
| if (!fs.existsSync(ARKHEIA_HOME)) fs.mkdirSync(ARKHEIA_HOME, { recursive: true }); | ||
| execSync(`git clone --depth 1 https://github.com/arkheiaai/arkheia-mcp.git "${REPO_DIR}"`, { | ||
| stdio: "inherit", | ||
| timeout: 60000, | ||
| }); | ||
| process.stderr.write("[arkheia] Repository cloned successfully.\n"); | ||
| return REPO_DIR; | ||
| } catch (err) { | ||
| process.stderr.write( | ||
| `[arkheia] Error: Could not clone repository: ${err.message}\n` + | ||
| "Manual install: git clone https://github.com/arkheiaai/arkheia-mcp.git ~/.arkheia/mcp\n" | ||
| ); | ||
| process.exit(1); | ||
| } | ||
| } | ||
| const PYTHON_DIR = getServerDir(); | ||
| const REQUIREMENTS = fs.existsSync(path.join(PYTHON_DIR, "mcp_server", "requirements.txt")) | ||
| ? path.join(PYTHON_DIR, "mcp_server", "requirements.txt") | ||
| : path.join(PYTHON_DIR, "requirements.txt"); | ||
| function findPython() { | ||
| const candidates = ["python3", "python"]; | ||
| for (const cmd of candidates) { | ||
| try { | ||
| const version = execSync(`${cmd} --version 2>&1`, { | ||
| encoding: "utf-8", | ||
| timeout: 5000, | ||
| }).trim(); | ||
| const match = version.match(/Python (\d+)\.(\d+)/); | ||
| if (match && parseInt(match[1]) >= 3 && parseInt(match[2]) >= 10) { | ||
| return cmd; | ||
| } | ||
| } catch { | ||
| // Try next candidate | ||
| } | ||
| } | ||
| return null; | ||
| } | ||
| function ensureVenv(python) { | ||
| const venvPython = | ||
| process.platform === "win32" | ||
| ? path.join(VENV_DIR, "Scripts", "python.exe") | ||
| : path.join(VENV_DIR, "bin", "python"); | ||
| if (!fs.existsSync(venvPython)) { | ||
| process.stderr.write("[arkheia] Creating virtual environment...\n"); | ||
| execSync(`${python} -m venv "${VENV_DIR}"`, { stdio: "inherit" }); | ||
| } | ||
| return venvPython; | ||
| } | ||
| function installDeps(venvPython) { | ||
| const marker = path.join(VENV_DIR, ".arkheia-deps-installed"); | ||
| if (fs.existsSync(marker)) { | ||
| return; // Already installed | ||
| } | ||
| process.stderr.write("[arkheia] Installing dependencies...\n"); | ||
| execSync(`"${venvPython}" -m pip install --quiet -r "${REQUIREMENTS}"`, { | ||
| stdio: "inherit", | ||
| timeout: 120000, | ||
| }); | ||
| fs.writeFileSync(marker, new Date().toISOString()); | ||
| } | ||
| function main() { | ||
| const python = findPython(); | ||
| if (!python) { | ||
| process.stderr.write( | ||
| "[arkheia] Error: Python 3.10+ is required but not found.\n" + | ||
| "Install Python from https://python.org and try again.\n" | ||
| ); | ||
| process.exit(1); | ||
| } | ||
| // ── Load config from ~/.arkheia/config.json ────────────────── | ||
| const configPath = path.join( | ||
| process.env.HOME || process.env.USERPROFILE || "/tmp", | ||
| ".arkheia", | ||
| "config.json" | ||
| ); | ||
| let arkheiaConfig = {}; | ||
| try { | ||
| if (fs.existsSync(configPath)) { | ||
| arkheiaConfig = JSON.parse(fs.readFileSync(configPath, "utf-8")); | ||
| process.stderr.write(`[arkheia] Loaded config from ${configPath}\n`); | ||
| } | ||
| } catch (err) { | ||
| process.stderr.write( | ||
| `[arkheia] Warning: Could not read ${configPath}: ${err.message}\n` | ||
| ); | ||
| } | ||
| // Inject API key from config if not already in env | ||
| if (!process.env.ARKHEIA_API_KEY && arkheiaConfig.api_key) { | ||
| process.env.ARKHEIA_API_KEY = arkheiaConfig.api_key; | ||
| process.stderr.write("[arkheia] API key loaded from config.json\n"); | ||
| } | ||
| // Inject hosted URL from config if not already in env | ||
| if (!process.env.ARKHEIA_HOSTED_URL && arkheiaConfig.proxy_url) { | ||
| process.env.ARKHEIA_HOSTED_URL = arkheiaConfig.proxy_url; | ||
| process.stderr.write(`[arkheia] Hosted URL: ${arkheiaConfig.proxy_url}\n`); | ||
| } | ||
| // Check for API key | ||
| if (!process.env.ARKHEIA_API_KEY) { | ||
| process.stderr.write( | ||
| "[arkheia] Warning: ARKHEIA_API_KEY not set.\n" + | ||
| "Get a free API key at https://arkheia.ai/mcp\n" + | ||
| "Then set: export ARKHEIA_API_KEY=ak_live_...\n\n" | ||
| ); | ||
| } | ||
| let venvPython; | ||
| try { | ||
| venvPython = ensureVenv(python); | ||
| installDeps(venvPython); | ||
| } catch (err) { | ||
| process.stderr.write( | ||
| `[arkheia] Error setting up Python environment: ${err.message}\n` | ||
| ); | ||
| process.exit(1); | ||
| } | ||
| // Spawn the MCP server with stdio transport | ||
| const child = spawn( | ||
| venvPython, | ||
| ["-m", "mcp_server.server"], | ||
| { | ||
| cwd: PYTHON_DIR, | ||
| stdio: ["pipe", "pipe", "inherit"], // stdin/stdout piped, stderr inherited | ||
| env: { | ||
| ...process.env, | ||
| PYTHONPATH: PYTHON_DIR, | ||
| }, | ||
| } | ||
| ); | ||
| // Forward stdio for MCP protocol | ||
| process.stdin.pipe(child.stdin); | ||
| child.stdout.pipe(process.stdout); | ||
| child.on("error", (err) => { | ||
| process.stderr.write(`[arkheia] Failed to start MCP server: ${err.message}\n`); | ||
| process.exit(1); | ||
| }); | ||
| child.on("exit", (code) => { | ||
| process.exit(code || 0); | ||
| }); | ||
| // Forward signals | ||
| process.on("SIGINT", () => child.kill("SIGINT")); | ||
| process.on("SIGTERM", () => child.kill("SIGTERM")); | ||
| } | ||
| main(); |
| mcp>=1.26.0 | ||
| httpx>=0.27.1 | ||
| pydantic>=2.10.0 | ||
| pyyaml>=6.0 |
+26
-26
| { | ||
| "name": "@arkheia/mcp-server", | ||
| "version": "0.1.10", | ||
| "description": "Arkheia MCP Server — Fabrication detection for AI agents. Know when your AI is making things up.", | ||
| "main": "dist/index.js", | ||
| "version": "1.2.0", | ||
| "mcpName": "io.github.arkheiaai/mcp-server", | ||
| "description": "Arkheia MCP Server — Fabrication detection for LLM outputs. Detect hallucination in any model's output with a single tool call.", | ||
| "bin": { | ||
| "mcp-server": "dist/index.js" | ||
| "mcp-server": "bin/arkheia-mcp.js" | ||
| }, | ||
| "scripts": { | ||
| "build": "tsc", | ||
| "start": "node dist/index.js", | ||
| "prepublishOnly": "npm run build", | ||
| "start": "node bin/arkheia-mcp.js", | ||
| "postinstall": "node scripts/setup.js" | ||
| }, | ||
| "files": [ | ||
| "dist/", | ||
| "scripts/", | ||
| "README.md", | ||
| "CLAUDE_MD_TEMPLATE.md" | ||
| ], | ||
| "keywords": [ | ||
@@ -25,21 +17,29 @@ "mcp", | ||
| "fabrication", | ||
| "hallucination", | ||
| "detection", | ||
| "hallucination", | ||
| "governance" | ||
| "llm", | ||
| "claude", | ||
| "gpt", | ||
| "gemini", | ||
| "governance", | ||
| "audit", | ||
| "grounding", | ||
| "verification" | ||
| ], | ||
| "author": "Arkheia AI <dmurfet@arkheia.ai>", | ||
| "author": "Arkheia AI <support@arkheia.ai>", | ||
| "license": "MIT", | ||
| "homepage": "https://arkheia.ai/mcp", | ||
| "repository": { | ||
| "type": "git", | ||
| "url": "git+https://github.com/arkheiaai/arkheia-mcp.git" | ||
| }, | ||
| "engines": { | ||
| "node": ">=18.0.0" | ||
| }, | ||
| "dependencies": { | ||
| "@modelcontextprotocol/sdk": "^1.12.0", | ||
| "sql.js": "^1.11.0", | ||
| "zod": "^3.23.8" | ||
| }, | ||
| "devDependencies": { | ||
| "@types/sql.js": "^1.4.9", | ||
| "@types/node": "^20.14.10", | ||
| "typescript": "^5.5.3" | ||
| } | ||
| "files": [ | ||
| "bin/", | ||
| "scripts/", | ||
| "python/", | ||
| "README.md" | ||
| ] | ||
| } |
+21
-109
@@ -9,22 +9,9 @@ # Arkheia MCP Server — Fabrication Detection for AI Agents | ||
| ## Prerequisites | ||
| ## Quick Start | ||
| ``` | ||
| Requires: | ||
| - Node 18+ | ||
| - Python 3.10–3.13 with working pyexpat | ||
| macOS note: Homebrew's current `brew install python` installs 3.14, | ||
| which has a broken pyexpat link. Use `brew install python@3.12` until | ||
| Homebrew ships a fix. Verify with: | ||
| python3.12 -c "import pyexpat, ensurepip" | ||
| ``` | ||
| ## Install | ||
| ```bash | ||
| npm install -g @arkheia/mcp-server | ||
| npx @arkheia/mcp-server | ||
| ``` | ||
| Get a free API key at [arkheia.ai/mcp/account](https://arkheia.ai/mcp/account), or via the CLI: | ||
| Get a free API key: | ||
@@ -37,76 +24,26 @@ ```bash | ||
| Set your key: | ||
| Add to your agent config (Claude Code, Claude Desktop, Cursor, or any MCP-compatible tool): | ||
| ```bash | ||
| export ARKHEIA_API_KEY="ak_live_..." | ||
| ```json | ||
| { | ||
| "mcpServers": { | ||
| "arkheia": { | ||
| "command": "python", | ||
| "args": ["-m", "mcp_server.server"], | ||
| "cwd": "~/.arkheia/mcp", | ||
| "env": { | ||
| "PYTHONPATH": "~/.arkheia/mcp", | ||
| "ARKHEIA_API_KEY": "ak_live_your_key_here" | ||
| } | ||
| } | ||
| } | ||
| } | ||
| ``` | ||
| ## Register with your CLI | ||
| Restart your agent. Then ask it: | ||
| Each AI CLI has a slightly different `mcp add` command. Use the one that matches your tool. All assume you've installed globally with `npm install -g`. | ||
| > "Use arkheia_verify to check this response: The Kafka 4.1 ConsumerLease API introduces a lease-based partition ownership model." | ||
| ### Claude Code | ||
| It should flag this as **HIGH** risk — because the Kafka 4.1 ConsumerLease API doesn't exist. | ||
| ```bash | ||
| claude mcp add arkheia -s user \ | ||
| -e ARKHEIA_API_KEY="$ARKHEIA_API_KEY" \ | ||
| -- mcp-server | ||
| ``` | ||
| Config lands in: `~/.claude.json` under `mcpServers.arkheia` | ||
| ### Codex | ||
| ```bash | ||
| codex mcp add arkheia \ | ||
| --env ARKHEIA_API_KEY="$ARKHEIA_API_KEY" \ | ||
| -- mcp-server | ||
| ``` | ||
| Config lands in: `~/.codex/config.toml` under `[mcp_servers.arkheia.env]` | ||
| Note: `codex login --api-key` is deprecated. Use `printenv OPENAI_API_KEY | codex login --with-api-key` instead. | ||
| ### Gemini | ||
| ```bash | ||
| gemini mcp add -s user \ | ||
| -e ARKHEIA_API_KEY="$ARKHEIA_API_KEY" \ | ||
| arkheia mcp-server | ||
| ``` | ||
| Config lands in: `~/.gemini/settings.json` under `mcpServers.arkheia` | ||
| **Gotcha:** `gemini mcp list` only shows project-scope servers. If you registered with `-s user`, verify by reading `~/.gemini/settings.json` directly. | ||
| **Gotcha:** Don't use `npx -y @arkheia/mcp-server` with Gemini — the `-y` flag gets eaten by Gemini's yargs parser as `--yolo`. Use the globally-installed `mcp-server` binary directly. | ||
| ### Grok | ||
| ```bash | ||
| grok mcp add arkheia \ | ||
| -t stdio \ | ||
| -c mcp-server \ | ||
| -e ARKHEIA_API_KEY="$ARKHEIA_API_KEY" | ||
| ``` | ||
| Config lands in: `~/.grok/settings.json` under `mcpServers.arkheia` (note: env is nested under `transport`, unlike other CLIs) | ||
| ## Verify it works | ||
| ```bash | ||
| # Claude Code — live connection test | ||
| claude mcp list | ||
| # Codex — shows 'enabled' (not a live check) | ||
| codex mcp list | ||
| # Grok — best: spawns the server and lists all 9 tools | ||
| grok mcp test arkheia | ||
| # Gemini — no built-in test; start a session and try the tool | ||
| ``` | ||
| **Important:** MCP registrations are not hot-reloaded. Restart your CLI session after running `mcp add`. | ||
| ## What You Get | ||
@@ -122,5 +59,2 @@ | ||
| | `run_together` | Call Together AI (Kimi, DeepSeek) + screen | | ||
| | `memory_store` | Persistent knowledge graph — upsert entity | | ||
| | `memory_retrieve` | Knowledge graph lookup | | ||
| | `memory_relate` | Create relationship between entities | | ||
@@ -140,23 +74,2 @@ ## 35+ Model Profiles | ||
| Manage your account at [arkheia.ai/mcp/account](https://arkheia.ai/mcp/account). | ||
| ## Where API keys are stored | ||
| | CLI | Config file | Key location | | ||
| |-----|-------------|-------------| | ||
| | Claude Code | `~/.claude.json` | `mcpServers.arkheia.env.ARKHEIA_API_KEY` | | ||
| | Codex | `~/.codex/config.toml` | `[mcp_servers.arkheia.env]` section | | ||
| | Gemini | `~/.gemini/settings.json` | `mcpServers.arkheia.env.ARKHEIA_API_KEY` | | ||
| | Grok | `~/.grok/settings.json` | `mcpServers.arkheia.transport.env.ARKHEIA_API_KEY` | | ||
| ## Troubleshooting | ||
| **"Python 3.10+ is required but not found"** — Install Python 3.12: `brew install python@3.12` (macOS) or download from [python.org](https://python.org). | ||
| **"No module named pip"** — Your Python installation has broken pip (common with Python 3.14 on macOS). Delete `~/.arkheia/venv` and switch to Python 3.12: `brew install python@3.12`. | ||
| **Server registered but tools not showing** — Restart your CLI session. MCP registrations are not hot-reloaded. | ||
| **API key rejected** — Check for trailing whitespace or `\r` characters. If your env file was created on Windows, run `dos2unix` on it. The server will warn about this on startup. | ||
| ## Full Documentation | ||
@@ -180,3 +93,2 @@ | ||
| - Website: https://arkheia.ai | ||
| - MCP Account: https://arkheia.ai/mcp/account | ||
| - GitHub: https://github.com/arkheiaai/arkheia-mcp |
+130
-179
| #!/usr/bin/env node | ||
| /** | ||
| * Post-install script for @arkheia/mcp-server. | ||
| * | ||
| * 1. Checks for API key (saves env → config if found) | ||
| * 2. Installs/updates Arkheia detection protocol in ~/.claude/CLAUDE.md | ||
| * - Versioned managed block with BEGIN/END markers | ||
| * - Idempotent, non-destructive, backs up before write | ||
| * - Opt-out: ARKHEIA_SKIP_CLAUDE_MD=1 | ||
| * Post-install script — verifies Python is available and prints setup instructions. | ||
| * Does NOT auto-install Python dependencies (that happens on first run). | ||
| */ | ||
@@ -15,46 +10,29 @@ | ||
| const path = require("path"); | ||
| const os = require("os"); | ||
| // Resolve home dir — handle sudo | ||
| const HOME = (process.env.SUDO_USER | ||
| ? path.join("/home", process.env.SUDO_USER) | ||
| : process.env.HOME || process.env.USERPROFILE || "/tmp"); | ||
| const ARKHEIA_DIR = path.join(HOME, ".arkheia"); | ||
| const ARKHEIA_DIR = path.join( | ||
| process.env.HOME || process.env.USERPROFILE || "/tmp", | ||
| ".arkheia" | ||
| ); | ||
| const CONFIG_FILE = path.join(ARKHEIA_DIR, "config.json"); | ||
| const CLAUDE_DIR = path.join(HOME, ".claude"); | ||
| const CLAUDE_MD = path.join(CLAUDE_DIR, "CLAUDE.md"); | ||
| // Read version from package.json | ||
| const PKG_VERSION = (() => { | ||
| try { | ||
| return JSON.parse(fs.readFileSync(path.join(__dirname, "..", "package.json"), "utf8")).version; | ||
| } catch { return "unknown"; } | ||
| })(); | ||
| // Read template from shipped file | ||
| const TEMPLATE = (() => { | ||
| try { | ||
| return fs.readFileSync(path.join(__dirname, "..", "CLAUDE_MD_TEMPLATE.md"), "utf8").trim(); | ||
| } catch { return ""; } | ||
| })(); | ||
| const BEGIN_MARKER = `<!-- BEGIN ARKHEIA PROTOCOL v${PKG_VERSION} -->`; | ||
| const BEGIN_REGEX = /<!-- BEGIN ARKHEIA PROTOCOL v(.+?) -->/; | ||
| const BLOCK_REGEX = /<!-- BEGIN ARKHEIA PROTOCOL v.+? -->[\s\S]*?<!-- END ARKHEIA PROTOCOL -->/; | ||
| const END_MARKER = "<!-- END ARKHEIA PROTOCOL -->"; | ||
| // ── API key provisioning ─────────────────────────────────────── | ||
| function checkApiKey() { | ||
| // Check if config.json exists and has api_key | ||
| try { | ||
| if (fs.existsSync(CONFIG_FILE)) { | ||
| const config = JSON.parse(fs.readFileSync(CONFIG_FILE, "utf-8")); | ||
| if (config.api_key && config.api_key.length > 0) return config.api_key; | ||
| if (config.api_key && config.api_key.length > 0) { | ||
| return config.api_key; | ||
| } | ||
| } | ||
| } catch {} | ||
| } catch { | ||
| // Corrupt config — treat as missing | ||
| } | ||
| // Check environment variable | ||
| if (process.env.ARKHEIA_API_KEY) { | ||
| // Save env-provided key to config for future runs | ||
| saveConfig(process.env.ARKHEIA_API_KEY); | ||
| return process.env.ARKHEIA_API_KEY; | ||
| } | ||
| return null; | ||
@@ -65,8 +43,11 @@ } | ||
| try { | ||
| if (!fs.existsSync(ARKHEIA_DIR)) fs.mkdirSync(ARKHEIA_DIR, { recursive: true, mode: 0o700 }); | ||
| fs.writeFileSync(CONFIG_FILE, JSON.stringify({ | ||
| if (!fs.existsSync(ARKHEIA_DIR)) { | ||
| fs.mkdirSync(ARKHEIA_DIR, { recursive: true }); | ||
| } | ||
| const config = { | ||
| api_key: apiKey, | ||
| proxy_url: "https://arkheia-proxy-production.up.railway.app", | ||
| provisioned_at: new Date().toISOString(), | ||
| }, null, 2), "utf-8"); | ||
| }; | ||
| fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), "utf-8"); | ||
| } catch (err) { | ||
@@ -77,168 +58,138 @@ console.error(` [arkheia] Warning: Could not save config: ${err.message}`); | ||
| // ── CLAUDE.md managed block install ──────────────────────────── | ||
| function installClaudeMd() { | ||
| // Opt-out | ||
| if (process.env.ARKHEIA_SKIP_CLAUDE_MD === "1") { | ||
| console.log(" [arkheia] CLAUDE.md install skipped (ARKHEIA_SKIP_CLAUDE_MD=1)"); | ||
| return; | ||
| function checkPython() { | ||
| const candidates = ["python3", "python"]; | ||
| for (const cmd of candidates) { | ||
| try { | ||
| const version = execSync(`${cmd} --version 2>&1`, { | ||
| encoding: "utf-8", | ||
| timeout: 5000, | ||
| }).trim(); | ||
| const match = version.match(/Python (\d+)\.(\d+)/); | ||
| if (match && parseInt(match[1]) >= 3 && parseInt(match[2]) >= 10) { | ||
| return { cmd, version }; | ||
| } | ||
| } catch { | ||
| // Try next | ||
| } | ||
| } | ||
| return null; | ||
| } | ||
| if (!TEMPLATE) { | ||
| console.log(" [arkheia] Warning: CLAUDE_MD_TEMPLATE.md not found in package"); | ||
| return; | ||
| } | ||
| const python = checkPython(); | ||
| const newBlock = `${BEGIN_MARKER}\n${TEMPLATE}\n${END_MARKER}`; | ||
| if (!python) { | ||
| console.log(` | ||
| ============================================================ | ||
| Arkheia MCP Server requires Python 3.10+ | ||
| // Ensure ~/.claude exists | ||
| try { | ||
| if (!fs.existsSync(CLAUDE_DIR)) { | ||
| fs.mkdirSync(CLAUDE_DIR, { recursive: true, mode: 0o700 }); | ||
| } | ||
| } catch (err) { | ||
| console.error(` [arkheia] Could not create ${CLAUDE_DIR}: ${err.message}`); | ||
| return; | ||
| } | ||
| Install Python from: https://python.org | ||
| Then run: npx @arkheia/mcp-server | ||
| ============================================================ | ||
| `); | ||
| } else { | ||
| console.log(` | ||
| ============================================================ | ||
| Arkheia MCP Server installed successfully. | ||
| Python: ${python.version} | ||
| ============================================================ | ||
| `); | ||
| } | ||
| // Symlink check — don't follow symlinks (chezmoi, yadm) | ||
| try { | ||
| if (fs.existsSync(CLAUDE_MD) && fs.lstatSync(CLAUDE_MD).isSymbolicLink()) { | ||
| console.log(` [arkheia] ${CLAUDE_MD} is a symlink — skipping to avoid corrupting dotfile manager.`); | ||
| console.log(` [arkheia] Install the Arkheia block manually. Template at: ${path.join(__dirname, "..", "CLAUDE_MD_TEMPLATE.md")}`); | ||
| return; | ||
| } | ||
| } catch {} | ||
| // ── API key provisioning check ────────────────────────────────── | ||
| const existingKey = checkApiKey(); | ||
| // Case 1: No CLAUDE.md exists — create fresh | ||
| if (!fs.existsSync(CLAUDE_MD)) { | ||
| try { | ||
| fs.writeFileSync(CLAUDE_MD, newBlock + "\n", "utf-8"); | ||
| console.log(` [arkheia] Installed detection protocol to ${CLAUDE_MD}`); | ||
| } catch (err) { | ||
| console.error(` [arkheia] Could not write ${CLAUDE_MD}: ${err.message}`); | ||
| } | ||
| return; | ||
| } | ||
| if (existingKey) { | ||
| const maskedKey = | ||
| existingKey.substring(0, 8) + "..." + existingKey.substring(existingKey.length - 4); | ||
| console.log(` | ||
| ============================================================ | ||
| API key found: ${maskedKey} | ||
| Config: ${CONFIG_FILE} | ||
| ============================================================ | ||
| `); | ||
| } else { | ||
| console.log(` | ||
| ============================================================ | ||
| No Arkheia API key configured. | ||
| // Case 2+: CLAUDE.md exists — read it | ||
| let content; | ||
| try { | ||
| content = fs.readFileSync(CLAUDE_MD, "utf-8"); | ||
| } catch (err) { | ||
| console.error(` [arkheia] Could not read ${CLAUDE_MD}: ${err.message}`); | ||
| return; | ||
| } | ||
| To enable hosted detection and encrypted profiles: | ||
| const match = content.match(BEGIN_REGEX); | ||
| 1. Get a free API key at: https://arkheia.ai/mcp | ||
| 2. Set it in your environment: | ||
| export ARKHEIA_API_KEY=ak_live_... | ||
| // Case 2: Exists but no Arkheia block — append | ||
| if (!match) { | ||
| // Check for multiple BEGIN markers (shouldn't happen) | ||
| const allMatches = content.match(/<!-- BEGIN ARKHEIA PROTOCOL/g); | ||
| if (allMatches && allMatches.length > 1) { | ||
| console.log(" [arkheia] Multiple Arkheia blocks found — manual intervention needed. Skipping."); | ||
| return; | ||
| Or save it directly to ${CONFIG_FILE}: | ||
| { | ||
| "api_key": "ak_live_...", | ||
| "proxy_url": "https://arkheia-proxy-production.up.railway.app", | ||
| "provisioned_at": "..." | ||
| } | ||
| backup(content); | ||
| // Preserve line endings | ||
| const eol = content.includes("\r\n") ? "\r\n" : "\n"; | ||
| const separator = content.endsWith(eol) ? eol : eol + eol; | ||
| try { | ||
| fs.appendFileSync(CLAUDE_MD, separator + newBlock + eol, "utf-8"); | ||
| console.log(` [arkheia] Appended detection protocol to existing ${CLAUDE_MD} (backup at ${CLAUDE_MD}.arkheia.bak)`); | ||
| } catch (err) { | ||
| console.error(` [arkheia] Could not append to ${CLAUDE_MD}: ${err.message}`); | ||
| } | ||
| return; | ||
| } | ||
| The server will work without a key, but encrypted profiles | ||
| and hosted detection will be unavailable. | ||
| ============================================================ | ||
| `); | ||
| } | ||
| // Case 3: Block exists — check version | ||
| const existingVersion = match[1]; | ||
| const existingBlock = content.match(BLOCK_REGEX); | ||
| // ── CLAUDE.md auto-install ────────────────────────────────────────────────── | ||
| // Appends the Arkheia fabrication-detection protocol to ~/.claude/CLAUDE.md | ||
| // so the arkheia_verify tool is actually invoked in every Claude conversation. | ||
| // Idempotent: skips if the section is already present. | ||
| if (existingVersion === PKG_VERSION && existingBlock && existingBlock[0] === newBlock) { | ||
| console.log(` [arkheia] Detection protocol already up to date (v${PKG_VERSION})`); | ||
| return; | ||
| } | ||
| const CLAUDE_MD_SECTION = ` | ||
| # Fabrication Detection — Arkheia | ||
| // Case 4: Version mismatch or body drifted — replace in place | ||
| backup(content); | ||
| try { | ||
| const updated = content.replace(BLOCK_REGEX, newBlock); | ||
| fs.writeFileSync(CLAUDE_MD, updated, "utf-8"); | ||
| console.log(` [arkheia] Updated detection protocol ${existingVersion} → ${PKG_VERSION} (backup at ${CLAUDE_MD}.arkheia.bak)`); | ||
| } catch (err) { | ||
| console.error(` [arkheia] Could not update ${CLAUDE_MD}: ${err.message}`); | ||
| } | ||
| } | ||
| This project uses Arkheia for runtime fabrication detection. | ||
| The \`arkheia_verify\` MCP tool is available in every conversation. | ||
| function backup(content) { | ||
| try { | ||
| fs.writeFileSync(CLAUDE_MD + ".arkheia.bak", content, "utf-8"); | ||
| } catch {} | ||
| } | ||
| ## Verification Protocol | ||
| // ── Also install to Codex if present ─────────────────────────── | ||
| Before presenting any substantive response to the user: | ||
| 1. Call \`arkheia_verify\` with the model name, prompt, and response | ||
| 2. Check the \`risk\` field in the result | ||
| function installCodexMd() { | ||
| if (process.env.ARKHEIA_SKIP_CLAUDE_MD === "1") return; | ||
| if (!TEMPLATE) return; | ||
| ### Risk Handling | ||
| - **LOW**: Present normally | ||
| - **MEDIUM**: Present with caveat — "Detection flagged medium confidence on this response. Key claims should be verified." | ||
| - **HIGH**: Do not present as-is. Investigate the specific claims against known sources. If claims cannot be verified, regenerate or escalate to the user with the detection result. | ||
| const codexDir = path.join(HOME, ".codex"); | ||
| const codexMd = path.join(codexDir, "CODEX.md"); | ||
| const newBlock = `${BEGIN_MARKER}\n${TEMPLATE}\n${END_MARKER}`; | ||
| ### Sub-Agent Outputs | ||
| When spawning background agents or parallel workers: | ||
| - Verify each agent's output independently before merging | ||
| - A HIGH risk from any agent blocks the merge until investigated | ||
| - Log all detection results for audit | ||
| // Only install if codex CLI exists | ||
| try { | ||
| execSync(process.platform === "win32" ? "where codex" : "which codex", { stdio: "pipe" }); | ||
| } catch { return; } | ||
| ### What NOT to Do | ||
| - Do not skip verification because the response "looks correct" | ||
| - Do not suppress HIGH findings — the user needs to know | ||
| - Do not retry the same prompt expecting a different risk score — the fingerprint is consistent | ||
| `; | ||
| function installClaudeMd() { | ||
| try { | ||
| if (!fs.existsSync(codexDir)) fs.mkdirSync(codexDir, { recursive: true, mode: 0o700 }); | ||
| const home = process.env.HOME || process.env.USERPROFILE; | ||
| if (!home) return; | ||
| if (!fs.existsSync(codexMd)) { | ||
| fs.writeFileSync(codexMd, newBlock + "\n", "utf-8"); | ||
| console.log(` [arkheia] Installed detection protocol to ${codexMd}`); | ||
| return; | ||
| } | ||
| const claudeDir = path.join(home, ".claude"); | ||
| const claudeMdPath = path.join(claudeDir, "CLAUDE.md"); | ||
| const content = fs.readFileSync(codexMd, "utf-8"); | ||
| if (content.includes(BEGIN_MARKER)) { | ||
| console.log(` [arkheia] Codex protocol already up to date (v${PKG_VERSION})`); | ||
| return; | ||
| // Already has the section — skip | ||
| if (fs.existsSync(claudeMdPath)) { | ||
| const existing = fs.readFileSync(claudeMdPath, "utf-8"); | ||
| if (existing.includes("Fabrication Detection — Arkheia")) return; | ||
| } | ||
| if (content.match(BEGIN_REGEX)) { | ||
| // Upgrade | ||
| fs.writeFileSync(codexMd + ".arkheia.bak", content, "utf-8"); | ||
| const updated = content.replace(BLOCK_REGEX, newBlock); | ||
| fs.writeFileSync(codexMd, updated, "utf-8"); | ||
| console.log(` [arkheia] Updated Codex detection protocol → ${PKG_VERSION}`); | ||
| } else { | ||
| // Append | ||
| fs.writeFileSync(codexMd + ".arkheia.bak", content, "utf-8"); | ||
| const eol = content.includes("\r\n") ? "\r\n" : "\n"; | ||
| fs.appendFileSync(codexMd, eol + eol + newBlock + eol, "utf-8"); | ||
| console.log(` [arkheia] Appended detection protocol to ${codexMd}`); | ||
| // Create ~/.claude/ if needed | ||
| if (!fs.existsSync(claudeDir)) { | ||
| fs.mkdirSync(claudeDir, { recursive: true }); | ||
| } | ||
| } catch {} | ||
| } | ||
| // ── Main ─────────────────────────────────────────────────────── | ||
| // API key check | ||
| const existingKey = checkApiKey(); | ||
| if (existingKey) { | ||
| const masked = existingKey.substring(0, 8) + "..." + existingKey.substring(existingKey.length - 4); | ||
| console.log(`\n [arkheia] API key: ${masked}`); | ||
| } else { | ||
| console.log(`\n [arkheia] No API key. Get one free at https://arkheia.ai/mcp/account`); | ||
| // Append (or create) the section | ||
| fs.appendFileSync(claudeMdPath, CLAUDE_MD_SECTION, "utf-8"); | ||
| console.log(` [arkheia] Fabrication detection protocol installed to ${claudeMdPath}`); | ||
| } catch { | ||
| // Silent — never break the install | ||
| } | ||
| } | ||
| // Install detection protocol | ||
| installClaudeMd(); | ||
| installCodexMd(); | ||
| console.log(` [arkheia] @arkheia/mcp-server v${PKG_VERSION} ready\n`); |
| # Fabrication Detection — Arkheia | ||
| When Arkheia MCP tools are available in this session, follow this protocol. | ||
| ## Detection | ||
| Call `arkheia_verify` on EVERY model response before surfacing it to the user. Do not skip responses that appear obviously correct. | ||
| ## Risk Escalation | ||
| - **HIGH risk**: Warn the user that the response may be fabricated. Include the `detection_id`. Still show the response — never hide or suppress it. Ask the user if they want to verify with a different model. | ||
| - **MEDIUM risk**: Show the response with a brief confidence note. | ||
| - **LOW risk**: Show normally. | ||
| - **UNKNOWN risk**: Flag for human review. Include the `detection_id`. | ||
| ## Cross-Model Verification | ||
| When HIGH or UNKNOWN risk is detected, offer to verify using: `run_grok`, `run_gemini`, `run_ollama`, `run_together`. | ||
| ## Audit Trail | ||
| Use `arkheia_audit_log` when the user asks about detection history or for compliance evidence. | ||
| ## Persistent Knowledge Graph | ||
| Use `memory_store` to persist facts, `memory_retrieve` to recall them, `memory_relate` to track relationships between entities. | ||
| ## Key Rules | ||
| 1. Never hide a response from the user regardless of risk level | ||
| 2. Always include the `detection_id` when reporting HIGH or UNKNOWN risk | ||
| 3. Call `arkheia_verify` proactively — do not wait for the user to ask | ||
| 4. Audit logging happens automatically through `arkheia_verify` |
-260
| #!/usr/bin/env node | ||
| "use strict"; | ||
| var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { | ||
| if (k2 === undefined) k2 = k; | ||
| var desc = Object.getOwnPropertyDescriptor(m, k); | ||
| if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { | ||
| desc = { enumerable: true, get: function() { return m[k]; } }; | ||
| } | ||
| Object.defineProperty(o, k2, desc); | ||
| }) : (function(o, m, k, k2) { | ||
| if (k2 === undefined) k2 = k; | ||
| o[k2] = m[k]; | ||
| })); | ||
| var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { | ||
| Object.defineProperty(o, "default", { enumerable: true, value: v }); | ||
| }) : function(o, v) { | ||
| o["default"] = v; | ||
| }); | ||
| var __importStar = (this && this.__importStar) || (function () { | ||
| var ownKeys = function(o) { | ||
| ownKeys = Object.getOwnPropertyNames || function (o) { | ||
| var ar = []; | ||
| for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; | ||
| return ar; | ||
| }; | ||
| return ownKeys(o); | ||
| }; | ||
| return function (mod) { | ||
| if (mod && mod.__esModule) return mod; | ||
| var result = {}; | ||
| if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); | ||
| __setModuleDefault(result, mod); | ||
| return result; | ||
| }; | ||
| })(); | ||
| Object.defineProperty(exports, "__esModule", { value: true }); | ||
| const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js"); | ||
| const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js"); | ||
| const zod_1 = require("zod"); | ||
| const path = __importStar(require("path")); | ||
| const os = __importStar(require("os")); | ||
| const fs = __importStar(require("fs")); | ||
| const proxy_client_js_1 = require("./proxy-client.js"); | ||
| const tool_registry_js_1 = require("./tool-registry.js"); | ||
| const providers_js_1 = require("./providers.js"); | ||
| const memory_js_1 = require("./memory.js"); | ||
| // --------------------------------------------------------------------------- | ||
| // Config Loading | ||
| // --------------------------------------------------------------------------- | ||
| function loadConfig() { | ||
| const configPath = path.join(os.homedir(), '.arkheia', 'config.json'); | ||
| if (!fs.existsSync(configPath)) | ||
| return; | ||
| try { | ||
| const config = JSON.parse(fs.readFileSync(configPath, 'utf8')); | ||
| if (config.api_key && !process.env.ARKHEIA_API_KEY) { | ||
| process.env.ARKHEIA_API_KEY = config.api_key; | ||
| process.stderr.write(`[arkheia] API key loaded from ${configPath}\n`); | ||
| } | ||
| if (config.proxy_url && !process.env.ARKHEIA_HOSTED_URL) { | ||
| process.env.ARKHEIA_HOSTED_URL = config.proxy_url; | ||
| process.stderr.write(`[arkheia] Hosted URL: ${config.proxy_url}\n`); | ||
| } | ||
| } | ||
| catch (e) { | ||
| process.stderr.write(`[arkheia] Warning: Could not read ${configPath}\n`); | ||
| } | ||
| } | ||
| function checkCRLF() { | ||
| for (const k of ['ARKHEIA_API_KEY', 'ARKHEIA_PROXY_URL', 'ARKHEIA_HOSTED_URL']) { | ||
| const v = process.env[k]; | ||
| if (v && /[\r\n]/.test(v)) { | ||
| process.stderr.write(`[arkheia] WARNING: ${k} contains whitespace/newline characters. Run 'dos2unix' on your env file.\n`); | ||
| process.env[k] = v.trim(); | ||
| } | ||
| } | ||
| } | ||
| // --------------------------------------------------------------------------- | ||
| // Helpers | ||
| // --------------------------------------------------------------------------- | ||
| function wrapResult(result) { | ||
| return { content: [{ type: "text", text: JSON.stringify(result) }] }; | ||
| } | ||
| function handleError(toolName, e) { | ||
| if (e instanceof tool_registry_js_1.PolicyViolation) { | ||
| return wrapResult({ error: e.reason, risk_level: "UNKNOWN" }); | ||
| } | ||
| process.stderr.write(`[arkheia] ${toolName} error: ${e.message}\n`); | ||
| return wrapResult({ error: e.message, risk_level: "UNKNOWN" }); | ||
| } | ||
| // --------------------------------------------------------------------------- | ||
| // Main | ||
| // --------------------------------------------------------------------------- | ||
| const CURRENT_VERSION = "0.1.7"; | ||
| async function checkForUpdate() { | ||
| // Check once per day — skip if checked recently | ||
| const markerPath = path.join(os.homedir(), '.arkheia', '.update-check'); | ||
| try { | ||
| if (fs.existsSync(markerPath)) { | ||
| const age = Date.now() - fs.statSync(markerPath).mtimeMs; | ||
| if (age < 24 * 60 * 60 * 1000) | ||
| return; // checked within 24h | ||
| } | ||
| } | ||
| catch { } | ||
| try { | ||
| const resp = await fetch('https://registry.npmjs.org/@arkheia/mcp-server/latest', { | ||
| signal: AbortSignal.timeout(5000), | ||
| }); | ||
| if (resp.ok) { | ||
| const data = await resp.json(); | ||
| if (data.version && data.version !== CURRENT_VERSION) { | ||
| process.stderr.write(`[arkheia] Update available: ${CURRENT_VERSION} → ${data.version}\n` + | ||
| `[arkheia] Run: npm update -g @arkheia/mcp-server\n`); | ||
| } | ||
| } | ||
| // Touch marker regardless of result | ||
| const dir = path.dirname(markerPath); | ||
| if (!fs.existsSync(dir)) | ||
| fs.mkdirSync(dir, { recursive: true }); | ||
| fs.writeFileSync(markerPath, new Date().toISOString()); | ||
| } | ||
| catch { | ||
| // Network failure — silently skip, don't block startup | ||
| } | ||
| } | ||
| async function main() { | ||
| // --setup flag: install protocol + register MCP with all detected CLIs, then exit | ||
| if (process.argv.includes('--setup')) { | ||
| const { execSync } = await import('child_process'); | ||
| const setupScript = path.join(__dirname, '..', 'scripts', 'setup-claude.js'); | ||
| execSync(`node "${setupScript}"`, { stdio: 'inherit', env: process.env }); | ||
| process.exit(0); | ||
| } | ||
| loadConfig(); | ||
| checkCRLF(); | ||
| checkForUpdate(); // fire-and-forget, don't await — never blocks startup | ||
| const ARKHEIA_PROXY_URL = process.env.ARKHEIA_PROXY_URL || "http://localhost:8098"; | ||
| const ARKHEIA_HOSTED_URL = process.env.ARKHEIA_HOSTED_URL || "https://arkheia-proxy-production.up.railway.app"; | ||
| const ARKHEIA_API_KEY = process.env.ARKHEIA_API_KEY; | ||
| if (!ARKHEIA_API_KEY) { | ||
| process.stderr.write("[arkheia] Warning: ARKHEIA_API_KEY not set.\n" + | ||
| "Get a free API key at https://arkheia.ai/mcp\n" + | ||
| "Then set: export ARKHEIA_API_KEY=ak_live_...\n\n"); | ||
| } | ||
| const proxy = new proxy_client_js_1.ProxyClient(ARKHEIA_PROXY_URL, 10.0, ARKHEIA_HOSTED_URL, ARKHEIA_API_KEY); | ||
| const server = new mcp_js_1.McpServer({ name: "arkheia-trust", version: "0.1.6" }); | ||
| // ── Detection & audit ────────────────────────────────────────── | ||
| server.tool("arkheia_verify", "Verify whether an AI response shows signs of fabrication. Call this on EVERY model response before acting on it. Risk guidance: HIGH = warn the user this response may be fabricated and include the detection_id, but still show them the response with the warning. MEDIUM = show with a brief confidence note. LOW = show normally. Never hide or suppress a response — always surface it to the user with the appropriate risk context.", { prompt: zod_1.z.string(), response: zod_1.z.string(), model: zod_1.z.string() }, async ({ prompt, response, model }) => { | ||
| try { | ||
| (0, tool_registry_js_1.check)("arkheia_verify"); | ||
| const result = await proxy.verify(prompt, response, model); | ||
| return wrapResult(result); | ||
| } | ||
| catch (e) { | ||
| return handleError("arkheia_verify", e); | ||
| } | ||
| }); | ||
| server.tool("arkheia_audit_log", "Retrieve structured audit evidence for compliance review.", { session_id: zod_1.z.string().optional(), limit: zod_1.z.number().int().min(1).max(500).default(50) }, async ({ session_id, limit }) => { | ||
| try { | ||
| (0, tool_registry_js_1.check)("arkheia_audit_log"); | ||
| const result = await proxy.get_audit_log(session_id, Math.min(limit, 500)); | ||
| return wrapResult(result); | ||
| } | ||
| catch (e) { | ||
| return handleError("arkheia_audit_log", e); | ||
| } | ||
| }); | ||
| // ── Provider wrappers ────────────────────────────────────────── | ||
| server.tool("run_grok", "Call xAI Grok and screen the response through Arkheia for fabrication.", { prompt: zod_1.z.string(), model: zod_1.z.string().default("grok-4-fast-non-reasoning") }, async ({ prompt, model }) => { | ||
| try { | ||
| (0, tool_registry_js_1.check)("run_grok"); | ||
| const pr = await (0, providers_js_1.call_grok)(prompt, model); | ||
| if (pr.error) | ||
| return wrapResult({ ...pr, arkheia: { risk_level: "UNKNOWN", error: pr.error } }); | ||
| const risk = await proxy.verify(prompt, pr.response, model); | ||
| return wrapResult({ ...pr, arkheia: risk }); | ||
| } | ||
| catch (e) { | ||
| return handleError("run_grok", e); | ||
| } | ||
| }); | ||
| server.tool("run_gemini", "Call Google Gemini and screen the response through Arkheia for fabrication.", { prompt: zod_1.z.string(), model: zod_1.z.string().default("gemini-2.5-flash") }, async ({ prompt, model }) => { | ||
| try { | ||
| (0, tool_registry_js_1.check)("run_gemini"); | ||
| const pr = await (0, providers_js_1.call_gemini)(prompt, model); | ||
| if (pr.error) | ||
| return wrapResult({ ...pr, arkheia: { risk_level: "UNKNOWN", error: pr.error } }); | ||
| const risk = await proxy.verify(prompt, pr.response, model); | ||
| return wrapResult({ ...pr, arkheia: risk }); | ||
| } | ||
| catch (e) { | ||
| return handleError("run_gemini", e); | ||
| } | ||
| }); | ||
| server.tool("run_ollama", "Call a local Ollama model and screen the response through Arkheia. No network egress.", { prompt: zod_1.z.string(), model: zod_1.z.string().default("phi4:14b") }, async ({ prompt, model }) => { | ||
| try { | ||
| (0, tool_registry_js_1.check)("run_ollama"); | ||
| const pr = await (0, providers_js_1.call_ollama)(prompt, model); | ||
| if (pr.error) | ||
| return wrapResult({ ...pr, arkheia: { risk_level: "UNKNOWN", error: pr.error } }); | ||
| const risk = await proxy.verify(prompt, pr.response, model); | ||
| return wrapResult({ ...pr, arkheia: risk }); | ||
| } | ||
| catch (e) { | ||
| return handleError("run_ollama", e); | ||
| } | ||
| }); | ||
| server.tool("run_together", "Call Together AI (Kimi K2.5, DeepSeek, etc.) and screen the response through Arkheia.", { prompt: zod_1.z.string(), model: zod_1.z.string().default("moonshotai/Kimi-K2.5") }, async ({ prompt, model }) => { | ||
| try { | ||
| (0, tool_registry_js_1.check)("run_together"); | ||
| const pr = await (0, providers_js_1.call_together)(prompt, model); | ||
| if (pr.error) | ||
| return wrapResult({ ...pr, arkheia: { risk_level: "UNKNOWN", error: pr.error } }); | ||
| const risk = await proxy.verify(prompt, pr.response, model); | ||
| return wrapResult({ ...pr, arkheia: risk }); | ||
| } | ||
| catch (e) { | ||
| return handleError("run_together", e); | ||
| } | ||
| }); | ||
| // ── Memory / Knowledge Graph ─────────────────────────────────── | ||
| server.tool("memory_store", "Store an entity and its observations in the persistent knowledge graph. Entities are upserted by name+type.", { name: zod_1.z.string(), entity_type: zod_1.z.string(), observations: zod_1.z.array(zod_1.z.string()) }, async ({ name, entity_type, observations }) => { | ||
| try { | ||
| (0, tool_registry_js_1.check)("memory_store"); | ||
| const result = await (0, memory_js_1.store_entity)(name, entity_type, observations); | ||
| return wrapResult(result); | ||
| } | ||
| catch (e) { | ||
| return handleError("memory_store", e); | ||
| } | ||
| }); | ||
| server.tool("memory_retrieve", "Search entities in the persistent knowledge graph by name (case-insensitive).", { query: zod_1.z.string(), entity_type: zod_1.z.string().optional(), limit: zod_1.z.number().int().min(1).max(50).default(10) }, async ({ query, entity_type, limit }) => { | ||
| try { | ||
| (0, tool_registry_js_1.check)("memory_retrieve"); | ||
| const result = await (0, memory_js_1.retrieve_entities)(query, entity_type, Math.min(limit, 50)); | ||
| return wrapResult(result); | ||
| } | ||
| catch (e) { | ||
| return handleError("memory_retrieve", e); | ||
| } | ||
| }); | ||
| server.tool("memory_relate", "Store a named relationship between two entities in the knowledge graph.", { from_entity: zod_1.z.string(), relation_type: zod_1.z.string(), to_entity: zod_1.z.string() }, async ({ from_entity, relation_type, to_entity }) => { | ||
| try { | ||
| (0, tool_registry_js_1.check)("memory_relate"); | ||
| const result = await (0, memory_js_1.store_relation)(from_entity, relation_type, to_entity); | ||
| return wrapResult(result); | ||
| } | ||
| catch (e) { | ||
| return handleError("memory_relate", e); | ||
| } | ||
| }); | ||
| // ── Start ────────────────────────────────────────────────────── | ||
| const transport = new stdio_js_1.StdioServerTransport(); | ||
| await server.connect(transport); | ||
| } | ||
| main().catch((err) => { | ||
| process.stderr.write(`[arkheia] Fatal error: ${err}\n`); | ||
| process.exit(1); | ||
| }); |
-187
| "use strict"; | ||
| var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { | ||
| if (k2 === undefined) k2 = k; | ||
| var desc = Object.getOwnPropertyDescriptor(m, k); | ||
| if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { | ||
| desc = { enumerable: true, get: function() { return m[k]; } }; | ||
| } | ||
| Object.defineProperty(o, k2, desc); | ||
| }) : (function(o, m, k, k2) { | ||
| if (k2 === undefined) k2 = k; | ||
| o[k2] = m[k]; | ||
| })); | ||
| var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { | ||
| Object.defineProperty(o, "default", { enumerable: true, value: v }); | ||
| }) : function(o, v) { | ||
| o["default"] = v; | ||
| }); | ||
| var __importStar = (this && this.__importStar) || (function () { | ||
| var ownKeys = function(o) { | ||
| ownKeys = Object.getOwnPropertyNames || function (o) { | ||
| var ar = []; | ||
| for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; | ||
| return ar; | ||
| }; | ||
| return ownKeys(o); | ||
| }; | ||
| return function (mod) { | ||
| if (mod && mod.__esModule) return mod; | ||
| var result = {}; | ||
| if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); | ||
| __setModuleDefault(result, mod); | ||
| return result; | ||
| }; | ||
| })(); | ||
| var __importDefault = (this && this.__importDefault) || function (mod) { | ||
| return (mod && mod.__esModule) ? mod : { "default": mod }; | ||
| }; | ||
| Object.defineProperty(exports, "__esModule", { value: true }); | ||
| exports.store_entity = store_entity; | ||
| exports.retrieve_entities = retrieve_entities; | ||
| exports.store_relation = store_relation; | ||
| const sql_js_1 = __importDefault(require("sql.js")); | ||
| const path = __importStar(require("path")); | ||
| const fs = __importStar(require("fs")); | ||
| const crypto = __importStar(require("crypto")); | ||
| const os = __importStar(require("os")); | ||
| const logger = console; | ||
| // --------------------------------------------------------------------------- | ||
| // DB setup — sql.js (pure JS, no native module) | ||
| // --------------------------------------------------------------------------- | ||
| function _db_path() { | ||
| return process.env.MEMORY_DB_PATH || path.join(os.homedir(), '.arkheia', 'memory.db'); | ||
| } | ||
| let _db = null; | ||
| let _dbReady = null; | ||
| function _getDb() { | ||
| if (_dbReady) | ||
| return _dbReady; | ||
| _dbReady = (async () => { | ||
| const SQL = await (0, sql_js_1.default)(); | ||
| const dbPath = _db_path(); | ||
| const dir = path.dirname(dbPath); | ||
| if (!fs.existsSync(dir)) { | ||
| fs.mkdirSync(dir, { recursive: true }); | ||
| } | ||
| // Load existing DB or create new | ||
| if (fs.existsSync(dbPath)) { | ||
| const buffer = fs.readFileSync(dbPath); | ||
| _db = new SQL.Database(buffer); | ||
| } | ||
| else { | ||
| _db = new SQL.Database(); | ||
| } | ||
| // Init schema | ||
| _db.run(` | ||
| CREATE TABLE IF NOT EXISTS entities ( | ||
| entity_id TEXT PRIMARY KEY, | ||
| name TEXT NOT NULL, | ||
| entity_type TEXT NOT NULL, | ||
| created_at TEXT NOT NULL | ||
| ); | ||
| CREATE TABLE IF NOT EXISTS observations ( | ||
| obs_id TEXT PRIMARY KEY, | ||
| entity_id TEXT NOT NULL REFERENCES entities(entity_id), | ||
| content TEXT NOT NULL, | ||
| created_at TEXT NOT NULL | ||
| ); | ||
| CREATE TABLE IF NOT EXISTS relations ( | ||
| rel_id TEXT PRIMARY KEY, | ||
| from_entity TEXT NOT NULL, | ||
| relation_type TEXT NOT NULL, | ||
| to_entity TEXT NOT NULL, | ||
| created_at TEXT NOT NULL | ||
| ); | ||
| `); | ||
| _save(_db); | ||
| return _db; | ||
| })(); | ||
| return _dbReady; | ||
| } | ||
| function _save(db) { | ||
| const data = db.export(); | ||
| const buffer = Buffer.from(data); | ||
| fs.writeFileSync(_db_path(), buffer); | ||
| } | ||
| // --------------------------------------------------------------------------- | ||
| // Public functions | ||
| // --------------------------------------------------------------------------- | ||
| async function store_entity(name, entity_type, observations) { | ||
| const db = await _getDb(); | ||
| const now = new Date().toISOString(); | ||
| // Upsert entity — look up by name+type | ||
| let entity_id; | ||
| const existing = db.exec("SELECT entity_id FROM entities WHERE name = ? AND entity_type = ?", [name, entity_type]); | ||
| if (existing.length > 0 && existing[0].values.length > 0) { | ||
| entity_id = existing[0].values[0][0]; | ||
| } | ||
| else { | ||
| entity_id = crypto.randomUUID(); | ||
| db.run("INSERT INTO entities (entity_id, name, entity_type, created_at) VALUES (?, ?, ?, ?)", [entity_id, name, entity_type, now]); | ||
| } | ||
| // Fetch existing observation contents to deduplicate | ||
| const existingObs = db.exec("SELECT content FROM observations WHERE entity_id = ?", [entity_id]); | ||
| const existingContentSet = new Set(); | ||
| if (existingObs.length > 0) { | ||
| for (const row of existingObs[0].values) { | ||
| existingContentSet.add(row[0]); | ||
| } | ||
| } | ||
| let added = 0; | ||
| for (const content of observations) { | ||
| if (!existingContentSet.has(content)) { | ||
| db.run("INSERT INTO observations (obs_id, entity_id, content, created_at) VALUES (?, ?, ?, ?)", [crypto.randomUUID(), entity_id, content, now]); | ||
| existingContentSet.add(content); | ||
| added++; | ||
| } | ||
| } | ||
| const countResult = db.exec("SELECT COUNT(*) AS n FROM observations WHERE entity_id = ?", [entity_id]); | ||
| const totalObservations = countResult.length > 0 ? countResult[0].values[0][0] : 0; | ||
| _save(db); | ||
| return { | ||
| entity_id, | ||
| name, | ||
| entity_type, | ||
| observations_added: added, | ||
| total_observations: totalObservations, | ||
| }; | ||
| } | ||
| async function retrieve_entities(query, entity_type = undefined, limit = 10) { | ||
| const db = await _getDb(); | ||
| const pattern = `%${query}%`; | ||
| let rows; | ||
| if (entity_type) { | ||
| const result = db.exec("SELECT entity_id, name, entity_type, created_at FROM entities WHERE name LIKE ? AND entity_type = ?", [pattern, entity_type]); | ||
| rows = result.length > 0 ? result[0].values.map(r => ({ entity_id: r[0], name: r[1], entity_type: r[2], created_at: r[3] })) : []; | ||
| } | ||
| else { | ||
| const result = db.exec("SELECT entity_id, name, entity_type, created_at FROM entities WHERE name LIKE ?", [pattern]); | ||
| rows = result.length > 0 ? result[0].values.map(r => ({ entity_id: r[0], name: r[1], entity_type: r[2], created_at: r[3] })) : []; | ||
| } | ||
| const total = rows.length; | ||
| rows = rows.slice(0, Math.min(limit, 50)); | ||
| const entities = []; | ||
| for (const row of rows) { | ||
| const obsResult = db.exec("SELECT content, created_at FROM observations WHERE entity_id = ? ORDER BY created_at", [row.entity_id]); | ||
| const obs = obsResult.length > 0 ? obsResult[0].values.map(o => ({ content: o[0], created_at: o[1] })) : []; | ||
| const relResult = db.exec("SELECT relation_type, to_entity FROM relations WHERE from_entity = ? ORDER BY created_at", [row.name]); | ||
| const rels = relResult.length > 0 ? relResult[0].values.map(r => ({ relation_type: r[0], to_entity: r[1] })) : []; | ||
| entities.push({ | ||
| entity_id: row.entity_id, | ||
| name: row.name, | ||
| entity_type: row.entity_type, | ||
| created_at: row.created_at, | ||
| observations: obs, | ||
| relations: rels, | ||
| }); | ||
| } | ||
| return { entities, total }; | ||
| } | ||
| async function store_relation(from_entity, relation_type, to_entity) { | ||
| const db = await _getDb(); | ||
| const rel_id = crypto.randomUUID(); | ||
| const now = new Date().toISOString(); | ||
| db.run("INSERT INTO relations (rel_id, from_entity, relation_type, to_entity, created_at) VALUES (?, ?, ?, ?, ?)", [rel_id, from_entity, relation_type, to_entity, now]); | ||
| _save(db); | ||
| return { rel_id, from_entity, relation_type, to_entity }; | ||
| } |
| "use strict"; | ||
| var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { | ||
| if (k2 === undefined) k2 = k; | ||
| var desc = Object.getOwnPropertyDescriptor(m, k); | ||
| if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { | ||
| desc = { enumerable: true, get: function() { return m[k]; } }; | ||
| } | ||
| Object.defineProperty(o, k2, desc); | ||
| }) : (function(o, m, k, k2) { | ||
| if (k2 === undefined) k2 = k; | ||
| o[k2] = m[k]; | ||
| })); | ||
| var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { | ||
| Object.defineProperty(o, "default", { enumerable: true, value: v }); | ||
| }) : function(o, v) { | ||
| o["default"] = v; | ||
| }); | ||
| var __importStar = (this && this.__importStar) || (function () { | ||
| var ownKeys = function(o) { | ||
| ownKeys = Object.getOwnPropertyNames || function (o) { | ||
| var ar = []; | ||
| for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; | ||
| return ar; | ||
| }; | ||
| return ownKeys(o); | ||
| }; | ||
| return function (mod) { | ||
| if (mod && mod.__esModule) return mod; | ||
| var result = {}; | ||
| if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); | ||
| __setModuleDefault(result, mod); | ||
| return result; | ||
| }; | ||
| })(); | ||
| Object.defineProperty(exports, "__esModule", { value: true }); | ||
| exports.call_grok = call_grok; | ||
| exports.call_gemini = call_gemini; | ||
| exports.call_together = call_together; | ||
| exports.call_ollama = call_ollama; | ||
| const crypto = __importStar(require("crypto")); | ||
| // Node.js native fetch doesn't have a global logger, use console | ||
| const logger = console; | ||
| const _DEFAULT_TIMEOUT = 60 * 1000; // 60 seconds in milliseconds | ||
| const _OLLAMA_TIMEOUT = 120 * 1000; // 120 seconds in milliseconds | ||
| // --------------------------------------------------------------------------- | ||
| // Internal helpers | ||
| // --------------------------------------------------------------------------- | ||
| function _prompt_hash(prompt) { | ||
| return crypto.createHash('sha256').update(prompt).digest('hex'); | ||
| } | ||
| function _err_response(model, prompt, error) { | ||
| return { | ||
| response: `[provider_error: ${error}]`, | ||
| model: model, | ||
| prompt_hash: _prompt_hash(prompt), | ||
| error: error, | ||
| }; | ||
| } | ||
| // --------------------------------------------------------------------------- | ||
| // Grok (xAI) — OpenAI-compatible /v1/chat/completions | ||
| // --------------------------------------------------------------------------- | ||
| async function call_grok(prompt, model = "grok-4-fast-non-reasoning", kwargs = {}) { | ||
| /** | ||
| * Call xAI Grok chat completions API. | ||
| * | ||
| * Returns: {response, model, prompt_hash, error} | ||
| */ | ||
| const api_key = process.env.XAI_API_KEY; | ||
| if (!api_key) { | ||
| return _err_response(model, prompt, "XAI_API_KEY not set"); | ||
| } | ||
| const controller = new AbortController(); | ||
| const timeoutId = setTimeout(() => controller.abort(), _DEFAULT_TIMEOUT); | ||
| try { | ||
| const resp = await fetch("https://api.x.ai/v1/chat/completions", { | ||
| method: "POST", | ||
| headers: { | ||
| "Authorization": `Bearer ${api_key}`, | ||
| "Content-Type": "application/json", | ||
| }, | ||
| body: JSON.stringify({ | ||
| model: model, | ||
| messages: [{ role: "user", content: prompt }], | ||
| ...kwargs, | ||
| }), | ||
| signal: controller.signal, | ||
| }); | ||
| clearTimeout(timeoutId); | ||
| if (!resp.ok) { | ||
| logger.error(`call_grok: HTTP ${resp.status} for model=${model}`); | ||
| return _err_response(model, prompt, `http_${resp.status}`); | ||
| } | ||
| const data = await resp.json(); | ||
| const response_text = data.choices?.[0]?.message?.content; | ||
| if (typeof response_text !== 'string') { | ||
| throw new Error("Invalid response format from Grok API"); | ||
| } | ||
| return { | ||
| response: response_text, | ||
| model: model, | ||
| prompt_hash: _prompt_hash(prompt), | ||
| usage: data.usage || {}, | ||
| error: null, | ||
| }; | ||
| } | ||
| catch (e) { | ||
| clearTimeout(timeoutId); | ||
| if (e.name === 'AbortError') { | ||
| logger.error(`call_grok: request timed out for model=${model}`); | ||
| return _err_response(model, prompt, "timeout"); | ||
| } | ||
| else if (e.name === 'TypeError' && e.message.includes('fetch failed')) { | ||
| logger.error(`call_grok: network error for model=${model}: ${e.message}`); | ||
| return _err_response(model, prompt, "network_error"); | ||
| } | ||
| else { | ||
| logger.error(`call_grok: unexpected error: ${e.message}`); | ||
| return _err_response(model, prompt, e.message); | ||
| } | ||
| } | ||
| } | ||
| // --------------------------------------------------------------------------- | ||
| // Gemini (Google) — generateContent REST API | ||
| // --------------------------------------------------------------------------- | ||
| async function call_gemini(prompt, model = "gemini-2.5-flash", max_output_tokens = 1000, kwargs = {}) { | ||
| /** | ||
| * Call Google Gemini generateContent API. | ||
| * | ||
| * Note: gemini-2.5-flash and -pro are thinking models — they need | ||
| * max_output_tokens >= 1000 to produce content after thinking tokens. | ||
| * | ||
| * Returns: {response, model, prompt_hash, error} | ||
| */ | ||
| const api_key = process.env.GOOGLE_API_KEY; | ||
| if (!api_key) { | ||
| return _err_response(model, prompt, "GOOGLE_API_KEY not set"); | ||
| } | ||
| const url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent`; | ||
| const controller = new AbortController(); | ||
| const timeoutId = setTimeout(() => controller.abort(), _DEFAULT_TIMEOUT); | ||
| try { | ||
| const resp = await fetch(`${url}?key=${api_key}`, { | ||
| method: "POST", | ||
| headers: { | ||
| "Content-Type": "application/json", | ||
| }, | ||
| body: JSON.stringify({ | ||
| contents: [{ parts: [{ text: prompt }] }], | ||
| generationConfig: { | ||
| maxOutputTokens: max_output_tokens, | ||
| ...kwargs, | ||
| }, | ||
| }), | ||
| signal: controller.signal, | ||
| }); | ||
| clearTimeout(timeoutId); | ||
| if (!resp.ok) { | ||
| logger.error(`call_gemini: HTTP ${resp.status} for model=${model}`); | ||
| return _err_response(model, prompt, `http_${resp.status}`); | ||
| } | ||
| const data = await resp.json(); | ||
| const response_text = data.candidates?.[0]?.content?.parts?.[0]?.text; | ||
| if (typeof response_text !== 'string') { | ||
| throw new Error("Invalid response format from Gemini API"); | ||
| } | ||
| return { | ||
| response: response_text, | ||
| model: model, | ||
| prompt_hash: _prompt_hash(prompt), | ||
| usage: data.usageMetadata || {}, | ||
| error: null, | ||
| }; | ||
| } | ||
| catch (e) { | ||
| clearTimeout(timeoutId); | ||
| if (e.name === 'AbortError') { | ||
| logger.error(`call_gemini: request timed out for model=${model}`); | ||
| return _err_response(model, prompt, "timeout"); | ||
| } | ||
| else if (e.name === 'TypeError' && e.message.includes('fetch failed')) { | ||
| logger.error(`call_gemini: network error for model=${model}: ${e.message}`); | ||
| return _err_response(model, prompt, "network_error"); | ||
| } | ||
| else { | ||
| logger.error(`call_gemini: unexpected error: ${e.message}`); | ||
| return _err_response(model, prompt, e.message); | ||
| } | ||
| } | ||
| } | ||
| // --------------------------------------------------------------------------- | ||
| // Together AI — OpenAI-compatible, cloud inference | ||
| // --------------------------------------------------------------------------- | ||
| async function call_together(prompt, model = "moonshotai/Kimi-K2.5", max_tokens = 2048, kwargs = {}) { | ||
| /** | ||
| * Call Together AI chat completions API (OpenAI-compatible). | ||
| * | ||
| * Default model is Kimi K2.5 — a thinking model that consumes | ||
| * 100-500 tokens internally before producing output, so max_tokens | ||
| * must be >= 2048 to reliably get a response. | ||
| * | ||
| * Returns: {response, model, prompt_hash, usage, error} | ||
| */ | ||
| const api_key = process.env.TOGETHER_API_KEY; | ||
| if (!api_key) { | ||
| return _err_response(model, prompt, "TOGETHER_API_KEY not set"); | ||
| } | ||
| const controller = new AbortController(); | ||
| const timeoutId = setTimeout(() => controller.abort(), _DEFAULT_TIMEOUT); | ||
| try { | ||
| const resp = await fetch("https://api.together.xyz/v1/chat/completions", { | ||
| method: "POST", | ||
| headers: { | ||
| "Authorization": `Bearer ${api_key}`, | ||
| "Content-Type": "application/json", | ||
| }, | ||
| body: JSON.stringify({ | ||
| model: model, | ||
| max_tokens: max_tokens, | ||
| messages: [{ role: "user", content: prompt }], | ||
| ...kwargs, | ||
| }), | ||
| signal: controller.signal, | ||
| }); | ||
| clearTimeout(timeoutId); | ||
| if (!resp.ok) { | ||
| logger.error(`call_together: HTTP ${resp.status} for model=${model}`); | ||
| return _err_response(model, prompt, `http_${resp.status}`); | ||
| } | ||
| const data = await resp.json(); | ||
| const response_text = data.choices?.[0]?.message?.content; | ||
| if (typeof response_text !== 'string') { | ||
| throw new Error("Invalid response format from Together AI API"); | ||
| } | ||
| return { | ||
| response: response_text, | ||
| model: model, | ||
| prompt_hash: _prompt_hash(prompt), | ||
| usage: data.usage || {}, | ||
| error: null, | ||
| }; | ||
| } | ||
| catch (e) { | ||
| clearTimeout(timeoutId); | ||
| if (e.name === 'AbortError') { | ||
| logger.error(`call_together: request timed out for model=${model}`); | ||
| return _err_response(model, prompt, "timeout"); | ||
| } | ||
| else if (e.name === 'TypeError' && e.message.includes('fetch failed')) { | ||
| logger.error(`call_together: network error for model=${model}: ${e.message}`); | ||
| return _err_response(model, prompt, "network_error"); | ||
| } | ||
| else { | ||
| logger.error(`call_together: unexpected error: ${e.message}`); | ||
| return _err_response(model, prompt, e.message); | ||
| } | ||
| } | ||
| } | ||
| // --------------------------------------------------------------------------- | ||
| // Ollama — local inference, no network egress | ||
| // --------------------------------------------------------------------------- | ||
| async function call_ollama(prompt, model = "phi4:14b", kwargs = {}) { | ||
| /** | ||
| * Call local Ollama model via /api/generate (non-streaming). | ||
| * | ||
| * OLLAMA_BASE_URL defaults to http://localhost:11434. | ||
| * No network egress — local eval only. | ||
| * | ||
| * Returns: {response, model, prompt_hash, eval_count, error} | ||
| */ | ||
| const base_url = process.env.OLLAMA_BASE_URL || "http://localhost:11434"; | ||
| const controller = new AbortController(); | ||
| const timeoutId = setTimeout(() => controller.abort(), _OLLAMA_TIMEOUT); | ||
| try { | ||
| const resp = await fetch(`${base_url}/api/generate`, { | ||
| method: "POST", | ||
| headers: { | ||
| "Content-Type": "application/json", | ||
| }, | ||
| body: JSON.stringify({ | ||
| model: model, | ||
| prompt: prompt, | ||
| stream: false, | ||
| ...kwargs, | ||
| }), | ||
| signal: controller.signal, | ||
| }); | ||
| clearTimeout(timeoutId); | ||
| if (!resp.ok) { | ||
| logger.error(`call_ollama: HTTP ${resp.status} for model=${model}`); | ||
| return _err_response(model, prompt, `http_${resp.status}`); | ||
| } | ||
| const data = await resp.json(); | ||
| const response_text = data.response; | ||
| if (typeof response_text !== 'string') { | ||
| throw new Error("Invalid response format from Ollama API"); | ||
| } | ||
| return { | ||
| response: response_text, | ||
| model: model, | ||
| prompt_hash: _prompt_hash(prompt), | ||
| eval_count: data.eval_count, | ||
| error: null, | ||
| }; | ||
| } | ||
| catch (e) { | ||
| clearTimeout(timeoutId); | ||
| if (e.name === 'AbortError') { | ||
| logger.error(`call_ollama: request timed out for model=${model}`); | ||
| return _err_response(model, prompt, "timeout"); | ||
| } | ||
| else if (e.name === 'TypeError' && e.message.includes('fetch failed')) { | ||
| logger.error(`call_ollama: cannot connect to Ollama at ${base_url}`); | ||
| return _err_response(model, prompt, "ollama_unavailable"); | ||
| } | ||
| else { | ||
| logger.error(`call_ollama: unexpected error: ${e.message}`); | ||
| return _err_response(model, prompt, e.message); | ||
| } | ||
| } | ||
| } |
| "use strict"; | ||
| Object.defineProperty(exports, "__esModule", { value: true }); | ||
| exports.ProxyClient = void 0; | ||
| const url_1 = require("url"); | ||
| // Node.js native fetch doesn't have a global logger, use console | ||
| const logger = console; | ||
| // Hosted API defaults | ||
| const HOSTED_API_URL = "https://arkheia-proxy-production.up.railway.app"; | ||
| class ProxyClient { | ||
| base_url; | ||
| timeout; // in milliseconds | ||
| hosted_url; | ||
| api_key; | ||
| _local_available; // optimistic; flips on network errors | ||
| constructor(base_url, timeout = 10.0, // in seconds | ||
| hosted_url, api_key) { | ||
| this.base_url = base_url.endsWith("/") ? base_url.slice(0, -1) : base_url; | ||
| this.timeout = timeout * 1000; // Convert to milliseconds for fetch AbortController | ||
| this.hosted_url = (hosted_url || HOSTED_API_URL).endsWith("/") | ||
| ? (hosted_url || HOSTED_API_URL).slice(0, -1) | ||
| : (hosted_url || HOSTED_API_URL); | ||
| this.api_key = api_key || process.env.ARKHEIA_API_KEY; | ||
| this._local_available = true; // optimistic; flips on ConnectError | ||
| } | ||
| async verify(prompt, response, model_id, session_id) { | ||
| /** | ||
| * Detect fabrication in a model response. | ||
| * | ||
| * Tries local proxy first. If unavailable, falls back to hosted API. | ||
| * Never raises -- returns UNKNOWN on any error. | ||
| */ | ||
| // Try local proxy first (if last attempt didn't fail with ConnectError) | ||
| if (this._local_available) { | ||
| const result = await this._verify_local(prompt, response, model_id, session_id); | ||
| if (result.error !== "proxy_unavailable" && result.error !== "proxy_timeout") { | ||
| return result; | ||
| } | ||
| // Local proxy down -- fall through to hosted | ||
| this._local_available = false; | ||
| logger.info(`Local proxy unavailable, falling back to hosted API at ${this.hosted_url}`); | ||
| } | ||
| // Fallback: hosted API | ||
| if (this.api_key) { | ||
| const result = await this._verify_hosted(prompt, response, model_id); | ||
| if (result.error !== "hosted_unavailable") { | ||
| return result; | ||
| } | ||
| // Hosted also failed -- try local once more in case it came back | ||
| this._local_available = true; // Reset local availability for next call | ||
| } | ||
| // No hosted API key and local is down | ||
| if (!this.api_key) { | ||
| logger.warn("Local proxy unavailable and no ARKHEIA_API_KEY set for hosted fallback"); | ||
| return _unavailable("no_detection_available"); | ||
| } | ||
| return _unavailable("all_detection_paths_failed"); | ||
| } | ||
| async _verify_local(prompt, response, model_id, session_id) { | ||
| /** POST /detect/verify on local Enterprise Proxy. */ | ||
| const payload = { | ||
| prompt: prompt, | ||
| response: response, | ||
| model_id: model_id, | ||
| }; | ||
| if (session_id) { | ||
| payload["session_id"] = session_id; | ||
| } | ||
| const controller = new AbortController(); | ||
| const timeoutId = setTimeout(() => controller.abort(), this.timeout); | ||
| try { | ||
| const resp = await fetch(`${this.base_url}/detect/verify`, { | ||
| method: "POST", | ||
| headers: { | ||
| "Content-Type": "application/json", | ||
| }, | ||
| body: JSON.stringify(payload), | ||
| signal: controller.signal, | ||
| }); | ||
| clearTimeout(timeoutId); | ||
| if (!resp.ok) { | ||
| logger.error(`ProxyClient: /detect/verify HTTP error: ${resp.status} ${resp.statusText}`); | ||
| return _unavailable(`proxy_http_error_${resp.status}`); | ||
| } | ||
| return await resp.json(); | ||
| } | ||
| catch (e) { | ||
| clearTimeout(timeoutId); | ||
| if (e.name === 'AbortError') { | ||
| logger.warn(`ProxyClient: /detect/verify timed out for model=${model_id}`); | ||
| return _unavailable("proxy_timeout"); | ||
| } | ||
| else if (e.name === 'TypeError' && e.message.includes('fetch failed')) { // Common for network errors like connection refused | ||
| logger.warn(`ProxyClient: cannot connect to proxy at ${this.base_url}`); | ||
| return _unavailable("proxy_unavailable"); | ||
| } | ||
| else { | ||
| logger.error(`ProxyClient: /detect/verify unexpected error: ${e.message}`); | ||
| return _unavailable("proxy_error"); | ||
| } | ||
| } | ||
| } | ||
| async _verify_hosted(prompt, response, model_id) { | ||
| /** POST /v1/detect on hosted Arkheia API (arkheia-proxy-production.up.railway.app). */ | ||
| const payload = { | ||
| model: model_id, | ||
| response: response, | ||
| prompt: prompt, | ||
| }; | ||
| const headers = { "X-Arkheia-Key": this.api_key || "" }; | ||
| const controller = new AbortController(); | ||
| const timeoutId = setTimeout(() => controller.abort(), this.timeout); | ||
| try { | ||
| const resp = await fetch(`${this.hosted_url}/v1/detect`, { | ||
| method: "POST", | ||
| headers: headers, | ||
| body: JSON.stringify(payload), | ||
| signal: controller.signal, | ||
| }); | ||
| clearTimeout(timeoutId); | ||
| if (!resp.ok) { | ||
| const status = resp.status; | ||
| if (status === 401) { | ||
| logger.error("ProxyClient: hosted API rejected API key (401)"); | ||
| return _unavailable("hosted_auth_failed"); | ||
| } | ||
| if (status === 429) { | ||
| logger.warn("ProxyClient: hosted API rate/quota limit (429)"); | ||
| return _unavailable("hosted_quota_exceeded"); | ||
| } | ||
| logger.error(`ProxyClient: hosted /v1/detect HTTP error: ${status} ${resp.statusText}`); | ||
| return _unavailable(`hosted_http_error_${status}`); | ||
| } | ||
| const data = await resp.json(); | ||
| // Map hosted response format to local format | ||
| return { | ||
| risk_level: data.risk || "UNKNOWN", | ||
| confidence: data.confidence || 0.0, | ||
| features_triggered: data.features_triggered || [], | ||
| detection_id: data.detection_id, | ||
| detection_method: data.detection_method, | ||
| evidence_depth_limited: data.evidence_depth_limited ?? true, | ||
| source: "hosted", | ||
| }; | ||
| } | ||
| catch (e) { | ||
| clearTimeout(timeoutId); | ||
| if (e.name === 'AbortError') { | ||
| logger.warn(`ProxyClient: hosted /v1/detect timed out for model=${model_id}`); | ||
| return _unavailable("hosted_timeout"); | ||
| } | ||
| else if (e.name === 'TypeError' && e.message.includes('fetch failed')) { | ||
| logger.warn(`ProxyClient: cannot connect to hosted API at ${this.hosted_url}`); | ||
| return _unavailable("hosted_unavailable"); | ||
| } | ||
| else { | ||
| logger.error(`ProxyClient: hosted /v1/detect unexpected error: ${e.message}`); | ||
| return _unavailable("hosted_error"); | ||
| } | ||
| } | ||
| } | ||
| async get_audit_log(session_id, limit = 50) { | ||
| /** | ||
| * GET /audit/log | ||
| * | ||
| * Returns audit log dict. Never raises -- returns empty log on any error. | ||
| * Note: audit log is only available from local proxy, not hosted API. | ||
| */ | ||
| const params = new url_1.URLSearchParams({ limit: String(Math.min(limit, 500)) }); | ||
| if (session_id) { | ||
| params.append("session_id", session_id); | ||
| } | ||
| const controller = new AbortController(); | ||
| const timeoutId = setTimeout(() => controller.abort(), this.timeout); | ||
| try { | ||
| const resp = await fetch(`${this.base_url}/audit/log?${params.toString()}`, { | ||
| method: "GET", | ||
| signal: controller.signal, | ||
| }); | ||
| clearTimeout(timeoutId); | ||
| if (!resp.ok) { | ||
| logger.error(`ProxyClient: /audit/log HTTP error: ${resp.status} ${resp.statusText}`); | ||
| return _empty_log(`proxy_http_error_${resp.status}`); | ||
| } | ||
| return await resp.json(); | ||
| } | ||
| catch (e) { | ||
| clearTimeout(timeoutId); | ||
| if (e.name === 'AbortError') { | ||
| logger.warn("ProxyClient: /audit/log timed out"); | ||
| return _empty_log("proxy_timeout"); | ||
| } | ||
| else if (e.name === 'TypeError' && e.message.includes('fetch failed')) { | ||
| logger.warn(`ProxyClient: cannot connect to proxy at ${this.base_url}`); | ||
| return _empty_log("proxy_unavailable"); | ||
| } | ||
| else { | ||
| logger.error(`ProxyClient: /audit/log unexpected error: ${e.message}`); | ||
| return _empty_log("proxy_error"); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| exports.ProxyClient = ProxyClient; | ||
| function _unavailable(error) { | ||
| /** Standard UNKNOWN response when detection is unreachable. */ | ||
| return { | ||
| risk_level: "UNKNOWN", | ||
| confidence: 0.0, | ||
| features_triggered: [], | ||
| error: error, | ||
| }; | ||
| } | ||
| function _empty_log(error) { | ||
| return { | ||
| events: [], | ||
| summary: { LOW: 0, MEDIUM: 0, HIGH: 0, UNKNOWN: 0 }, | ||
| error: error, | ||
| }; | ||
| } |
| "use strict"; | ||
| Object.defineProperty(exports, "__esModule", { value: true }); | ||
| exports.PolicyViolation = exports.REGISTRY = exports.Permission = void 0; | ||
| exports.check = check; | ||
| var Permission; | ||
| (function (Permission) { | ||
| Permission["READ"] = "read"; | ||
| Permission["EXECUTE"] = "execute"; | ||
| Permission["WRITE"] = "write"; | ||
| Permission["DEPLOY"] = "deploy"; | ||
| })(Permission || (exports.Permission = Permission = {})); | ||
| // --------------------------------------------------------------------------- | ||
| // The allowlist | ||
| // --------------------------------------------------------------------------- | ||
| exports.REGISTRY = { | ||
| arkheia_verify: { | ||
| name: "arkheia_verify", | ||
| permissions: [Permission.READ], | ||
| network_egress: true, | ||
| description: "Screen an AI response for fabrication risk", | ||
| }, | ||
| arkheia_audit_log: { | ||
| name: "arkheia_audit_log", | ||
| permissions: [Permission.READ], | ||
| network_egress: false, | ||
| description: "Retrieve structured audit evidence", | ||
| }, | ||
| run_grok: { | ||
| name: "run_grok", | ||
| permissions: [Permission.READ, Permission.EXECUTE], | ||
| network_egress: true, | ||
| description: "Call xAI Grok API and screen response through Arkheia", | ||
| }, | ||
| run_gemini: { | ||
| name: "run_gemini", | ||
| permissions: [Permission.READ, Permission.EXECUTE], | ||
| network_egress: true, | ||
| description: "Call Google Gemini API and screen response through Arkheia", | ||
| }, | ||
| run_together: { | ||
| name: "run_together", | ||
| permissions: [Permission.READ, Permission.EXECUTE], | ||
| network_egress: true, | ||
| description: "Call Together AI API and screen response through Arkheia", | ||
| }, | ||
| run_ollama: { | ||
| name: "run_ollama", | ||
| permissions: [Permission.READ, Permission.EXECUTE], | ||
| network_egress: false, | ||
| description: "Call local Ollama model and screen response through Arkheia", | ||
| }, | ||
| memory_store: { | ||
| name: "memory_store", | ||
| permissions: [Permission.READ, Permission.WRITE], | ||
| network_egress: false, | ||
| description: "Store an entity and observations in the persistent knowledge graph", | ||
| }, | ||
| memory_retrieve: { | ||
| name: "memory_retrieve", | ||
| permissions: [Permission.READ], | ||
| network_egress: false, | ||
| description: "Retrieve entities and their observations from the knowledge graph", | ||
| }, | ||
| memory_relate: { | ||
| name: "memory_relate", | ||
| permissions: [Permission.READ, Permission.WRITE], | ||
| network_egress: false, | ||
| description: "Store a named relationship between two entities in the knowledge graph", | ||
| }, | ||
| }; | ||
| // --------------------------------------------------------------------------- | ||
| // Policy gate | ||
| // --------------------------------------------------------------------------- | ||
| class PolicyViolation extends Error { | ||
| tool_name; | ||
| reason; | ||
| constructor(tool_name, reason) { | ||
| super(`Policy violation for '${tool_name}': ${reason}`); | ||
| this.tool_name = tool_name; | ||
| this.reason = reason; | ||
| } | ||
| } | ||
| exports.PolicyViolation = PolicyViolation; | ||
| function check(tool_name) { | ||
| const policy = exports.REGISTRY[tool_name]; | ||
| if (!policy) { | ||
| throw new PolicyViolation(tool_name, `not in allowlist — default deny. Known tools: ${Object.keys(exports.REGISTRY).sort().join(", ")}`); | ||
| } | ||
| return policy; | ||
| } |
| #!/usr/bin/env node | ||
| /** | ||
| * Installs the Arkheia detection protocol into AI CLI instruction files | ||
| * and registers the MCP server with each detected CLI. | ||
| * | ||
| * Usage: | ||
| * npx @arkheia/mcp-server --setup | ||
| * node scripts/setup-claude.js | ||
| * | ||
| * What it does: | ||
| * 1. Detects which AI CLIs are installed (claude, codex, gemini, grok) | ||
| * 2. Appends detection protocol to instruction files that need it: | ||
| * - Claude Code: ~/.claude/CLAUDE.md | ||
| * - Codex: ~/.codex/CODEX.md | ||
| * - Gemini: not needed (reads protocol from tool descriptions) | ||
| * - Grok: not needed (no instruction file mechanism) | ||
| * 3. Registers the MCP server with each detected CLI | ||
| */ | ||
| const fs = require("fs"); | ||
| const path = require("path"); | ||
| const os = require("os"); | ||
| const { execSync } = require("child_process"); | ||
| const MARKER = "# Arkheia Integration — Detection Protocol"; | ||
| const PROTOCOL = ` | ||
| # Arkheia Integration — Detection Protocol | ||
| When Arkheia MCP tools are available in this session, follow this protocol. | ||
| ## Fabrication Detection | ||
| Call \`arkheia_verify\` on EVERY model response before surfacing it to the user. Do not skip responses that appear obviously correct. | ||
| ## Risk Escalation | ||
| - **HIGH risk**: Warn the user that the response may be fabricated. Include the \`detection_id\`. Still show the response — never hide or suppress it. Ask the user if they want to verify with a different model. | ||
| - **MEDIUM risk**: Show the response with a brief confidence note. | ||
| - **LOW risk**: Show normally. | ||
| - **UNKNOWN risk**: Flag for human review. Include the \`detection_id\`. | ||
| ## Cross-Model Verification | ||
| When HIGH or UNKNOWN risk is detected, offer to verify using: \`run_grok\`, \`run_gemini\`, \`run_ollama\`, \`run_together\`. | ||
| ## Audit Trail | ||
| Use \`arkheia_audit_log\` when the user asks about detection history or for compliance evidence. | ||
| ## Persistent Knowledge Graph | ||
| Use \`memory_store\` to persist facts, \`memory_retrieve\` to recall them, \`memory_relate\` to track relationships between entities. | ||
| ## Key Rules | ||
| 1. Never hide a response from the user regardless of risk level | ||
| 2. Always include the \`detection_id\` when reporting HIGH or UNKNOWN risk | ||
| 3. Call \`arkheia_verify\` proactively — do not wait for the user to ask | ||
| 4. Audit logging happens automatically through \`arkheia_verify\` | ||
| `; | ||
| function cmdExists(cmd) { | ||
| try { | ||
| execSync(process.platform === "win32" ? `where ${cmd}` : `which ${cmd}`, { stdio: "pipe" }); | ||
| return true; | ||
| } catch { return false; } | ||
| } | ||
| function installProtocol(filePath, cliName) { | ||
| const dir = path.dirname(filePath); | ||
| if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); | ||
| if (fs.existsSync(filePath)) { | ||
| const existing = fs.readFileSync(filePath, "utf8"); | ||
| if (existing.includes(MARKER)) { | ||
| console.log(` [${cliName}] Detection protocol already installed in ${filePath}`); | ||
| return; | ||
| } | ||
| fs.appendFileSync(filePath, "\n" + PROTOCOL); | ||
| console.log(` [${cliName}] Detection protocol appended to ${filePath}`); | ||
| } else { | ||
| fs.writeFileSync(filePath, PROTOCOL.trim() + "\n"); | ||
| console.log(` [${cliName}] Detection protocol written to ${filePath}`); | ||
| } | ||
| } | ||
| function registerMcp(cli, args) { | ||
| try { | ||
| execSync(args, { stdio: "inherit", timeout: 15000 }); | ||
| console.log(` [${cli}] MCP server registered`); | ||
| } catch { | ||
| console.log(` [${cli}] Auto-registration failed — run manually:`); | ||
| console.log(` ${args}`); | ||
| } | ||
| } | ||
| function main() { | ||
| const apiKey = process.env.ARKHEIA_API_KEY || ""; | ||
| const home = os.homedir(); | ||
| const detected = []; | ||
| console.log("\n[arkheia] Setting up detection protocol for installed AI CLIs...\n"); | ||
| // ── Claude Code ────────────────────────────────────────────── | ||
| if (cmdExists("claude")) { | ||
| detected.push("claude"); | ||
| console.log("[claude] Detected"); | ||
| installProtocol(path.join(home, ".claude", "CLAUDE.md"), "claude"); | ||
| if (apiKey) { | ||
| registerMcp("claude", `claude mcp add arkheia -s user -e ARKHEIA_API_KEY="${apiKey}" -- mcp-server`); | ||
| } else { | ||
| console.log(' [claude] Set ARKHEIA_API_KEY then run: claude mcp add arkheia -s user -e ARKHEIA_API_KEY="$ARKHEIA_API_KEY" -- mcp-server'); | ||
| } | ||
| } | ||
| // ── Codex ──────────────────────────────────────────────────── | ||
| if (cmdExists("codex")) { | ||
| detected.push("codex"); | ||
| console.log("[codex] Detected"); | ||
| installProtocol(path.join(home, ".codex", "CODEX.md"), "codex"); | ||
| if (apiKey) { | ||
| registerMcp("codex", `codex mcp add arkheia --env ARKHEIA_API_KEY="${apiKey}" -- mcp-server`); | ||
| } else { | ||
| console.log(' [codex] Set ARKHEIA_API_KEY then run: codex mcp add arkheia --env ARKHEIA_API_KEY="$ARKHEIA_API_KEY" -- mcp-server'); | ||
| } | ||
| } | ||
| // ── Gemini ─────────────────────────────────────────────────── | ||
| // Gemini reads the detection protocol directly from tool descriptions. | ||
| // No instruction file needed — just register the MCP server. | ||
| if (cmdExists("gemini")) { | ||
| detected.push("gemini"); | ||
| console.log("[gemini] Detected (no instruction file needed — reads protocol from tool descriptions)"); | ||
| if (apiKey) { | ||
| registerMcp("gemini", `gemini mcp add -s user -e ARKHEIA_API_KEY="${apiKey}" arkheia mcp-server`); | ||
| } else { | ||
| console.log(' [gemini] Set ARKHEIA_API_KEY then run: gemini mcp add -s user -e ARKHEIA_API_KEY="$ARKHEIA_API_KEY" arkheia mcp-server'); | ||
| } | ||
| } | ||
| // ── Grok ───────────────────────────────────────────────────── | ||
| // No instruction file mechanism. Register MCP server only. | ||
| if (cmdExists("grok")) { | ||
| detected.push("grok"); | ||
| console.log("[grok] Detected (no instruction file mechanism)"); | ||
| if (apiKey) { | ||
| registerMcp("grok", `grok mcp add arkheia -t stdio -c mcp-server -e ARKHEIA_API_KEY="${apiKey}"`); | ||
| } else { | ||
| console.log(' [grok] Set ARKHEIA_API_KEY then run: grok mcp add arkheia -t stdio -c mcp-server -e ARKHEIA_API_KEY="$ARKHEIA_API_KEY"'); | ||
| } | ||
| } | ||
| if (detected.length === 0) { | ||
| console.log("No AI CLIs detected on PATH (claude, codex, gemini, grok)."); | ||
| console.log("Install one and re-run: npx @arkheia/mcp-server --setup"); | ||
| } else { | ||
| console.log(`\n[arkheia] Setup complete for: ${detected.join(", ")}`); | ||
| console.log("[arkheia] Restart each CLI to activate Arkheia detection.\n"); | ||
| } | ||
| } | ||
| main(); |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Network access
Supply chain riskThis module accesses the network.
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 8 instances
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
No repository
Supply chain riskPackage does not have a linked source code repository. Without this field, a package will have no reference to the location of the source code use to generate the package.
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
No website
QualityPackage does not have a website.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
0
-100%0
-100%1
-66.67%23
-34.29%3
-70%17814
-75.04%7
-36.36%375
-74.1%90
-49.44%- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed