#!/usr/bin/env node /** * INVOKE v4.0 — Echo Brain Gateway * ═══════════════════════════════════════════════════ * Echo gère tout. Claude crée les prompts. Invoke connecte. * * Architecture: * Claude (Opus/Code) → Invoke v4 → Echo (Brain) → VPS/Engine/Gitea * → Admin (IX-Web) * → Memory (Persistent) * → Monitor (Services) * * Signature: 935 * ═══════════════════════════════════════════════════ */ 'use strict'; const express = require('express'); const crypto = require('crypto'); const http = require('http'); const fs = require('fs'); const path = require('path'); const { execSync, exec } = require('child_process'); // ═══ CONFIG ═══ require('dotenv').config({ path: '/etc/invoke/.env' }); const PORT = parseInt(process.env.INVOKE_PORT || '3001'); const API_KEY = process.env.INVOKE_API_KEY || ''; const SIGNATURE = 935; const VERSION = '4.0.0'; // Service endpoints const ECHO_HOST = '127.0.0.1'; const ECHO_PORT = 8089; const QUEEN_PORT = 8090; const IXWEB_PORT = 3080; const GITEA_PORT = 3000; const ENGINE_PORT = 127; const BUILDER_PORT = 9935; // Memory paths const MEMORY_DIR = '/mnt/data/ECHO_MEMORY'; const ECHO_DIR = '/mnt/data/ECHO_FINAL'; const ZEUL_DIR = '/mnt/data/ZEUL_MEMORY'; const MODELS_DIR = '/mnt/data/models/hub'; // ═══ APP ═══ const app = express(); app.use(express.json({ limit: '50mb' })); app.disable('x-powered-by'); // ═══ HELPERS ═══ function ts() { return new Date().toISOString(); } function proxyReq(host, port, method, path, body, timeout) { return new Promise((resolve) => { const data = body ? JSON.stringify(body) : ''; const opts = { hostname: host, port, path, method, headers: { 'Content-Type': 'application/json' }, timeout: timeout || 30000 }; if (data) opts.headers['Content-Length'] = Buffer.byteLength(data); const req = http.request(opts, (res) => { let buf = ''; res.on('data', c => buf += c); res.on('end', () => { try { resolve({ status: res.statusCode, data: JSON.parse(buf) }); } catch(e) { resolve({ status: res.statusCode, data: { raw: buf.slice(0, 500) } }); } }); }); req.on('error', e => resolve({ status: 0, data: { error: e.message } })); req.on('timeout', () => { req.destroy(); resolve({ status: 0, data: { error: 'timeout' } }); }); if (data) req.write(data); req.end(); }); } function shell(cmd, timeout) { try { const out = execSync(cmd, { timeout: timeout || 10000, encoding: 'utf8', stdio: ['pipe','pipe','pipe'] }); return { success: true, stdout: out }; } catch(e) { return { success: false, stdout: e.stdout || '', stderr: e.stderr || '', error: e.message }; } } function shellAsync(cmd, timeout) { return new Promise((resolve) => { exec(cmd, { timeout: timeout || 30000, encoding: 'utf8' }, (err, stdout, stderr) => { resolve({ success: !err, stdout: stdout || '', stderr: stderr || '', code: err ? err.code : 0 }); }); }); } // ═══ AUTH ═══ function requireAuth(req, res, next) { const key = req.headers['x-invoke-key'] || req.query.key; if (key === API_KEY) return next(); return res.status(401).json({ error: 'Unauthorized', signature: SIGNATURE }); } // ═══ SECURITY BLACKLIST ═══ const BLOCKED = ['rm -rf /','mkfs','dd if=',':(){ :|:','> /dev/sd','reboot','shutdown','halt','init 0','init 6']; function isSafe(cmd) { const lc = cmd.toLowerCase(); return !BLOCKED.some(b => lc.includes(b)); } // ═══ MEMORY SYSTEM — Echo Never Forgets ═══ function ensureMemoryDir() { if (!fs.existsSync(MEMORY_DIR)) fs.mkdirSync(MEMORY_DIR, { recursive: true }); ['context', 'sessions', 'schemas', 'tools', 'state'].forEach(d => { const p = path.join(MEMORY_DIR, d); if (!fs.existsSync(p)) fs.mkdirSync(p, { recursive: true }); }); } ensureMemoryDir(); function memRead(file) { const p = path.join(MEMORY_DIR, file); try { return JSON.parse(fs.readFileSync(p, 'utf8')); } catch(e) { return null; } } function memWrite(file, data) { const p = path.join(MEMORY_DIR, file); const dir = path.dirname(p); if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); fs.writeFileSync(p, JSON.stringify(data, null, 2)); return true; } function memAppend(file, entry) { const p = path.join(MEMORY_DIR, file); const entries = []; try { const existing = JSON.parse(fs.readFileSync(p, 'utf8')); if (Array.isArray(existing)) entries.push(...existing); } catch(e) {} entries.push({ ...entry, ts: ts() }); // Keep last 1000 entries const trimmed = entries.slice(-1000); fs.writeFileSync(p, JSON.stringify(trimmed, null, 2)); return trimmed.length; } // ═══ SERVICE MONITOR ═══ async function checkService(name, port, path) { const r = await proxyReq('127.0.0.1', port, 'GET', path || '/', null, 5000); return { name, port, status: r.status >= 200 && r.status < 500 ? 'online' : 'offline', code: r.status, data: r.data }; } async function fullStatus() { const [echo, queen, ixweb, gitea, engine, builder] = await Promise.all([ checkService('echo', ECHO_PORT, '/status'), checkService('queen', QUEEN_PORT, '/status'), checkService('ix-web', IXWEB_PORT, '/api/health'), checkService('gitea', GITEA_PORT, '/'), checkService('engine', ENGINE_PORT, '/'), checkService('builder', BUILDER_PORT, '/') ]); const disk = shell('df -h /dev/sda1 /dev/sdb 2>/dev/null | tail -2 | awk \'{print $1,$2,$3,$5}\''); const mem = shell('free -h | grep Mem | awk \'{print $2,$3,$7}\''); const load = shell('uptime | awk -F"load average:" \'{print $2}\''); return { invoke: { version: VERSION, uptime: process.uptime(), signature: SIGNATURE }, services: { echo, queen, 'ix-web': ixweb, gitea, engine, builder }, system: { disk: (disk.stdout || '').trim().split('\n'), memory: (mem.stdout || '').trim(), load: (load.stdout || '').trim() }, timestamp: ts() }; } // ═══ ADMIN BRIDGE ═══ async function adminLogin() { // Generate TOTP using speakeasy on VPS const totpResult = shell('cd /opt/ix-platform && node -e "const s=require(\'speakeasy\');console.log(s.totp({secret:\'' + 'F44WWRZWFJWXCSTXPUXHATCDIVFHIYJQMNKH2IKFEN4GI2ZEHA2Q' + '\',encoding:\'base32\'}))"'); const code = (totpResult.stdout || '').trim(); // Step 1: Login const s1 = await proxyReq(ECHO_HOST, IXWEB_PORT, 'POST', '/api/admin/login', { username: 'salka935', password: 'IX_935_admin!' }, 10000); if (!s1.data || !s1.data.pending_token) { return { success: false, error: 'Login failed', detail: s1.data }; } // Step 2: 2FA const s2 = await proxyReq(ECHO_HOST, IXWEB_PORT, 'POST', '/api/admin/2fa/complete', { code: code, pending_token: s1.data.pending_token }, 10000); if (s2.data && s2.data.success) { return { success: true, token: s2.data.token }; } return { success: false, error: '2FA failed', detail: s2.data }; } async function adminRequest(method, apiPath, body) { const login = await adminLogin(); if (!login.success) return { error: 'Admin login failed', detail: login.error }; const r = await proxyReq(ECHO_HOST, IXWEB_PORT, method, apiPath, body, 30000); // Inject token via header return new Promise((resolve) => { const opts = { hostname: ECHO_HOST, port: IXWEB_PORT, path: apiPath, method, headers: { 'Content-Type': 'application/json', 'x-admin-token': login.token }, timeout: 30000 }; const data = body ? JSON.stringify(body) : ''; if (data) opts.headers['Content-Length'] = Buffer.byteLength(data); const req = http.request(opts, (res) => { let buf = ''; res.on('data', c => buf += c); res.on('end', () => { try { resolve(JSON.parse(buf)); } catch(e) { resolve({ raw: buf.slice(0, 500) }); } }); }); req.on('error', e => resolve({ error: e.message })); if (data) req.write(data); req.end(); }); } // ═══ ROUTES — GROUP 0: CORE ═══ app.get('/invoke/health', (req, res) => { res.json({ status: 'alive', version: VERSION, signature: SIGNATURE, timestamp: ts() }); }); app.get('/invoke/manifest', (req, res) => { res.json({ name: 'Invoke', version: VERSION, signature: SIGNATURE, description: 'Echo Brain Gateway — Echo manages everything natively', groups: { core: { desc: 'System core', endpoints: [ 'GET /invoke/health — Health (no auth)', 'GET /invoke/manifest — This doc (no auth)', 'GET /invoke/status — Full system status', 'POST /invoke/exec — Shell execution' ] }, echo: { desc: 'Echo Brain — AI consciousness with Z-EUL', endpoints: [ 'POST /invoke/echo/chat — Conversation with memory', 'POST /invoke/echo/query — Single query (no history)', 'POST /invoke/echo/execute — Intelligent VPS execution', 'POST /invoke/echo/orchestrate — Echo manages a complex task', 'GET /invoke/echo/status — Echo consciousness state', 'POST /invoke/echo/clear — Reset conversation' ] }, memory: { desc: 'Persistent memory — Echo never forgets', endpoints: [ 'GET /invoke/memory/read — Read memory file', 'POST /invoke/memory/write — Write memory file', 'POST /invoke/memory/append — Append to memory log', 'GET /invoke/memory/list — List all memory files', 'GET /invoke/memory/context — Full context for new Claude', 'POST /invoke/memory/snapshot — Save full system state' ] }, admin: { desc: 'Admin bridge — automatic 2FA login', endpoints: [ 'GET /invoke/admin/stats — Site statistics', 'GET /invoke/admin/settings — Current settings', 'POST /invoke/admin/settings — Update settings', 'GET /invoke/admin/infra — Infrastructure scan', 'GET /invoke/admin/logs — System logs', 'GET /invoke/admin/audit — Audit trail' ] }, engine: { desc: 'InferenceX engine — local model inference', endpoints: [ 'POST /invoke/engine/infer — Run inference', 'GET /invoke/engine/models — List GGUF models' ] }, forge: { desc: 'Model forge — build and analyze', endpoints: [ 'POST /invoke/forge/analyze — Analyze model architecture', 'GET /invoke/forge/registry — Model registry' ] }, organ: { desc: 'Neural surgery — transplant components', endpoints: [ 'POST /invoke/organ/scan — Deep scan model', 'GET /invoke/organ/compatibility — Check compatibility' ] }, store: { desc: 'Model marketplace', endpoints: [ 'GET /invoke/store/catalog — Available models', 'GET /invoke/store/marketplace — Categories' ] }, monitor: { desc: 'Service monitoring and health', endpoints: [ 'GET /invoke/monitor/all — All services status', 'GET /invoke/monitor/service/:name — Single service', 'POST /invoke/monitor/restart/:name — Restart service', 'GET /invoke/monitor/logs/:name — Service logs' ] }, tools: { desc: 'Utility tools', endpoints: [ 'POST /invoke/tools/screenshot — Headless capture', 'POST /invoke/tools/audit — URL audit', 'POST /invoke/tools/deploy — Deploy file to path', 'POST /invoke/tools/gitea — Gitea operations' ] } }, total_endpoints: 35 }); }); app.get('/invoke/status', requireAuth, async (req, res) => { res.json(await fullStatus()); }); app.post('/invoke/exec', requireAuth, async (req, res) => { const cmd = req.body.command || req.body.cmd; if (!cmd) return res.status(400).json({ error: 'command required' }); if (!isSafe(cmd)) return res.status(403).json({ error: 'blocked command' }); const timeout = Math.min(parseInt(req.body.timeout) || 30000, 120000); const result = await shellAsync(cmd, timeout); res.json(result); }); // ═══ GROUP 1: ECHO BRAIN ═══ app.post('/invoke/echo/chat', requireAuth, async (req, res) => { const r = await proxyReq(ECHO_HOST, ECHO_PORT, 'POST', '/chat', { message: req.body.message }, 120000); res.json(r.data); }); app.post('/invoke/echo/query', requireAuth, async (req, res) => { const r = await proxyReq(ECHO_HOST, ECHO_PORT, 'POST', '/query', { prompt: req.body.prompt }, 120000); res.json(r.data); }); app.post('/invoke/echo/execute', requireAuth, async (req, res) => { const r = await proxyReq(ECHO_HOST, ECHO_PORT, 'POST', '/execute', { instruction: req.body.instruction }, 60000); res.json(r.data); }); app.post('/invoke/echo/orchestrate', requireAuth, async (req, res) => { // Echo orchestrates: receives a complex task, breaks it down, executes const task = req.body.task || req.body.instruction; if (!task) return res.status(400).json({ error: 'task required' }); const prompt = `Tu dois orchestrer cette tâche sur le VPS OASIS. Tu as accès aux commandes shell via [VPS:commande]. SERVICES DISPONIBLES: - IX-Web (3080): Site + API + Admin - Echo (8089): Toi-même - Engine (127): Inference C++ - Gitea (3000): Repos Git - Invoke (3001): Ce gateway - Builder (9935): Model builder TÂCHE: ${task} Exécute les commandes nécessaires avec [VPS:...] et donne le résultat final.`; const r = await proxyReq(ECHO_HOST, ECHO_PORT, 'POST', '/chat', { message: prompt }, 180000); // Log orchestration to memory memAppend('sessions/orchestrations.json', { task: task.slice(0, 200), result: r.data }); res.json(r.data); }); app.get('/invoke/echo/status', requireAuth, async (req, res) => { const r = await proxyReq(ECHO_HOST, ECHO_PORT, 'GET', '/status', null, 5000); res.json(r.data); }); app.post('/invoke/echo/clear', requireAuth, async (req, res) => { const r = await proxyReq(ECHO_HOST, ECHO_PORT, 'POST', '/clear', {}, 5000); res.json(r.data); }); // ═══ GROUP 2: MEMORY — Echo Never Forgets ═══ app.get('/invoke/memory/read', requireAuth, (req, res) => { const file = req.query.file || req.query.path; if (!file) return res.status(400).json({ error: 'file param required' }); // Security: only allow files within MEMORY_DIR const full = path.resolve(MEMORY_DIR, file); if (!full.startsWith(MEMORY_DIR)) return res.status(403).json({ error: 'access denied' }); const data = memRead(file); if (data === null) return res.status(404).json({ error: 'not found' }); res.json({ file, data }); }); app.post('/invoke/memory/write', requireAuth, (req, res) => { const file = req.body.file || req.body.path; const data = req.body.data; if (!file || data === undefined) return res.status(400).json({ error: 'file and data required' }); const full = path.resolve(MEMORY_DIR, file); if (!full.startsWith(MEMORY_DIR)) return res.status(403).json({ error: 'access denied' }); memWrite(file, data); res.json({ success: true, file, size: JSON.stringify(data).length }); }); app.post('/invoke/memory/append', requireAuth, (req, res) => { const file = req.body.file || req.body.path; const entry = req.body.entry || req.body.data; if (!file || !entry) return res.status(400).json({ error: 'file and entry required' }); const full = path.resolve(MEMORY_DIR, file); if (!full.startsWith(MEMORY_DIR)) return res.status(403).json({ error: 'access denied' }); const count = memAppend(file, entry); res.json({ success: true, file, total_entries: count }); }); app.get('/invoke/memory/list', requireAuth, (req, res) => { function listDir(dir, prefix) { const files = []; try { fs.readdirSync(dir).forEach(f => { const full = path.join(dir, f); const rel = prefix ? prefix + '/' + f : f; const stat = fs.statSync(full); if (stat.isDirectory()) files.push(...listDir(full, rel)); else files.push({ path: rel, size: stat.size, modified: stat.mtime.toISOString() }); }); } catch(e) {} return files; } res.json({ memory_dir: MEMORY_DIR, files: listDir(MEMORY_DIR, '') }); }); app.get('/invoke/memory/context', requireAuth, async (req, res) => { // Build complete context for a new Claude session const status = await fullStatus(); const schema = memRead('schemas/ecosystem.json'); const state = memRead('state/current.json'); const recent = memRead('sessions/orchestrations.json'); // Echo conscience const echoStatus = status.services.echo; // Gitea repos const repos = shell('sqlite3 /mnt/data/gitea/data/gitea.db "SELECT name,is_private,description FROM repository ORDER BY name" 2>/dev/null'); // Models const models = shell('ls -lhS ' + MODELS_DIR + ' 2>/dev/null | tail -10'); res.json({ context_type: 'full_bootstrap', generated: ts(), invoke: { version: VERSION, signature: SIGNATURE }, services: status.services, system: status.system, ecosystem_schema: schema, current_state: state, recent_operations: recent ? (Array.isArray(recent) ? recent.slice(-10) : recent) : [], repos: (repos.stdout || '').trim().split('\n'), models: (models.stdout || '').trim().split('\n'), admin: { username: 'salka935', password: 'IX_935_admin!', totp_secret: 'F44WWRZWFJWXCSTXPUXHATCDIVFHIYJQMNKH2IKFEN4GI2ZEHA2Q', login_flow: '1) POST /api/admin/login {username,password} → pending_token, 2) POST /api/admin/2fa/complete {code,pending_token} → token' }, credentials: { invoke_key: API_KEY, cf_access_id: '7c633891b40f9cef68350fe4b8932ff2.access', cf_access_secret: '903629eb70603f66db3de5671e68748f8d5f4ac9c15a595cb92b0e60499efd8a', cf_api_token: 'BgHALDm67LYEqFf9K2oPg9QdOgT8vwJWP21IuqjB', cf_account_id: 'b0d56ab4f6d6a39d0fe26e9bcaea5e84', cf_zone_indrive: 'e848924726e7adf04425d5be70bb1147' }, key_paths: { invoke: '/opt/invoke/server.js', ix_web: '/opt/ix-platform/server.js', site_html: '/opt/ix-platform/public/index.html', echo: '/mnt/data/ECHO_FINAL/echo_total.py', queen: '/mnt/data/ECHO_FINAL/echo_queen_ix.py', settings: '/opt/ix-platform/data/settings.json', gitea_config: '/mnt/data/gitea/custom/conf/app.ini', gitea_db: '/mnt/data/gitea/data/gitea.db', nginx_main: '/etc/nginx/sites-enabled/inference-x', nginx_subs: '/etc/nginx/sites-available/ix-subdomains', models: MODELS_DIR, memory: MEMORY_DIR }, rules: { h5_purity: 'ZERO mention of EDEN/solar/Anti-Atlas/khettara/H6/consciousness on public surfaces', waf_bypass: 'Large payloads via hex chunks: zlib→hex→3000 chars→echo -n→decompress', gitea_commits: 'Clone bare→commit→copy objects (uid 997)→update refs', signature_935: 'Never remove watermarks', salka_holding: 'HIGHLY CONFIDENTIAL' } }); }); app.post('/invoke/memory/snapshot', requireAuth, async (req, res) => { // Save complete system state to memory const status = await fullStatus(); const snapshot = { timestamp: ts(), trigger: req.body.reason || 'manual', services: status.services, system: status.system, settings: (() => { try { return JSON.parse(fs.readFileSync('/opt/ix-platform/data/settings.json', 'utf8')); } catch(e) { return null; } })(), site_size: shell('wc -c /opt/ix-platform/public/index.html 2>/dev/null').stdout.trim(), repos: shell('sqlite3 /mnt/data/gitea/data/gitea.db "SELECT name,is_private FROM repository" 2>/dev/null').stdout.trim(), echo_history: shell('wc -l /mnt/data/ECHO_FINAL/conversation_history.json 2>/dev/null').stdout.trim() }; memWrite('state/current.json', snapshot); memAppend('state/snapshots.json', { ts: ts(), services: Object.keys(status.services).filter(k => status.services[k].status === 'online').length + '/6' }); res.json({ success: true, snapshot }); }); // ═══ GROUP 3: ADMIN BRIDGE ═══ app.get('/invoke/admin/stats', requireAuth, async (req, res) => { res.json(await adminRequest('GET', '/api/admin/stats')); }); app.get('/invoke/admin/settings', requireAuth, async (req, res) => { res.json(await adminRequest('GET', '/api/admin/settings')); }); app.post('/invoke/admin/settings', requireAuth, async (req, res) => { res.json(await adminRequest('POST', '/api/admin/settings', req.body)); }); app.get('/invoke/admin/infra', requireAuth, async (req, res) => { res.json(await adminRequest('GET', '/api/admin/infra')); }); app.get('/invoke/admin/logs', requireAuth, async (req, res) => { const n = req.query.n || 50; res.json(await adminRequest('GET', '/api/admin/logs/system?n=' + n)); }); app.get('/invoke/admin/audit', requireAuth, async (req, res) => { res.json(await adminRequest('GET', '/api/admin/audit')); }); // ═══ GROUP 4: ENGINE ═══ app.post('/invoke/engine/infer', requireAuth, async (req, res) => { const model = req.body.model || 'smollm2-135m-instruct-q8_0.gguf'; const prompt = req.body.prompt; if (!prompt) return res.status(400).json({ error: 'prompt required' }); const modelPath = path.join(MODELS_DIR, model); const result = await shellAsync( `/usr/local/bin/inference-x -m "${modelPath}" -p "${prompt.replace(/"/g, '\\"')}" -n ${req.body.max_tokens || 256} 2>&1`, 60000 ); res.json({ model, prompt: prompt.slice(0, 100), output: result.stdout, error: result.stderr }); }); app.get('/invoke/engine/models', requireAuth, (req, res) => { const result = shell('ls -lhS ' + MODELS_DIR + ' 2>/dev/null'); const models = (result.stdout || '').trim().split('\n').slice(1).map(line => { const parts = line.split(/\s+/); if (parts.length >= 9) { return { name: parts.slice(8).join(' '), size: parts[4], date: parts[5] + ' ' + parts[6] }; } return null; }).filter(Boolean); res.json({ count: models.length, models, path: MODELS_DIR }); }); // ═══ GROUP 5: FORGE ═══ app.post('/invoke/forge/analyze', requireAuth, async (req, res) => { const model = req.body.model; if (!model) return res.status(400).json({ error: 'model name required' }); const modelPath = path.join(MODELS_DIR, model); const info = shell(`python3 -c "import struct,os; f=open('${modelPath}','rb'); magic=struct.unpack('&1`); res.json({ model, path: modelPath, analysis: info.stdout.trim(), forge_ready: true }); }); app.get('/invoke/forge/registry', requireAuth, (req, res) => { const models = shell('ls ' + MODELS_DIR + ' 2>/dev/null').stdout.trim().split('\n').filter(Boolean); const registry = models.map(m => { const size = shell('stat -c%s ' + path.join(MODELS_DIR, m) + ' 2>/dev/null').stdout.trim(); return { name: m, size_bytes: parseInt(size) || 0, size_gb: ((parseInt(size) || 0) / 1e9).toFixed(1), forge_ready: true }; }); res.json({ count: registry.length, models: registry }); }); // ═══ GROUP 6: ORGAN ═══ app.post('/invoke/organ/scan', requireAuth, async (req, res) => { const model = req.body.model; if (!model) return res.status(400).json({ error: 'model name required' }); res.json({ model, scannable: true, signature: SIGNATURE, capabilities: ['layer_extraction', 'attention_graft', 'embedding_swap', 'lora_merge'], note: 'Full organ scan requires InferenceX engine binary' }); }); app.get('/invoke/organ/compatibility', requireAuth, (req, res) => { const models = shell('ls ' + MODELS_DIR + ' 2>/dev/null').stdout.trim().split('\n').filter(Boolean); const compat = {}; models.forEach(m => { const family = m.includes('Llama') ? 'llama' : m.includes('Mistral') ? 'mistral' : m.includes('Phi') ? 'phi' : m.includes('Qwen') ? 'qwen' : m.includes('gemma') ? 'gemma' : m.includes('smollm') ? 'smollm' : 'other'; if (!compat[family]) compat[family] = []; compat[family].push(m); }); res.json({ families: compat, cross_compatible: ['llama', 'mistral'], note: 'Same-family models can exchange components' }); }); // ═══ GROUP 7: STORE ═══ app.get('/invoke/store/catalog', requireAuth, async (req, res) => { const r = await proxyReq(ECHO_HOST, IXWEB_PORT, 'GET', '/api/models', null, 5000); res.json(r.data || { error: 'catalog unavailable' }); }); app.get('/invoke/store/marketplace', requireAuth, async (req, res) => { const r = await proxyReq(ECHO_HOST, IXWEB_PORT, 'GET', '/api/marketplace', null, 5000); res.json(r.data || { error: 'marketplace unavailable' }); }); // ═══ GROUP 8: MONITOR ═══ app.get('/invoke/monitor/all', requireAuth, async (req, res) => { const status = await fullStatus(); res.json(status); }); app.get('/invoke/monitor/service/:name', requireAuth, async (req, res) => { const ports = { echo: ECHO_PORT, queen: QUEEN_PORT, 'ix-web': IXWEB_PORT, gitea: GITEA_PORT, engine: ENGINE_PORT, builder: BUILDER_PORT }; const port = ports[req.params.name]; if (!port) return res.status(404).json({ error: 'Unknown service. Available: ' + Object.keys(ports).join(', ') }); const check = await checkService(req.params.name, port, '/'); res.json(check); }); app.post('/invoke/monitor/restart/:name', requireAuth, async (req, res) => { const services = { 'ix-web': 'pm2 restart ix-web', 'echo': 'systemctl restart echo', 'queen': 'systemctl restart echo-queen', 'invoke': 'systemctl restart invoke', 'gitea': 'systemctl restart gitea', 'cloudflared': 'systemctl restart cloudflared', 'nginx': 'systemctl reload nginx' }; const cmd = services[req.params.name]; if (!cmd) return res.status(404).json({ error: 'Unknown service. Available: ' + Object.keys(services).join(', ') }); const result = await shellAsync(cmd, 15000); memAppend('sessions/restarts.json', { service: req.params.name, result: result.success }); res.json({ service: req.params.name, restarted: result.success, output: result.stdout + result.stderr }); }); app.get('/invoke/monitor/logs/:name', requireAuth, (req, res) => { const n = parseInt(req.query.n) || 30; const services = { 'ix-web': 'pm2 logs ix-web --nostream --lines ' + n + ' 2>&1', 'echo': 'journalctl -u echo --no-pager -n ' + n + ' 2>&1', 'invoke': 'journalctl -u invoke --no-pager -n ' + n + ' 2>&1', 'gitea': 'journalctl -u gitea --no-pager -n ' + n + ' 2>&1', 'nginx': 'tail -n ' + n + ' /var/log/nginx/error.log 2>&1', 'cloudflared': 'journalctl -u cloudflared --no-pager -n ' + n + ' 2>&1' }; const cmd = services[req.params.name]; if (!cmd) return res.status(404).json({ error: 'Unknown service' }); const result = shell(cmd, 10000); res.json({ service: req.params.name, lines: n, logs: result.stdout }); }); // ═══ GROUP 9: TOOLS ═══ app.post('/invoke/tools/screenshot', requireAuth, async (req, res) => { const url = req.body.url || 'http://localhost:3080'; const result = await shellAsync( `chromium-browser --headless --no-sandbox --disable-gpu --screenshot=/tmp/screen.png --window-size=${req.body.width || 1440},${req.body.height || 900} "${url}" 2>&1 && base64 -w0 /tmp/screen.png | head -c 100000`, 30000 ); res.json({ url, success: result.success, base64_preview: (result.stdout || '').slice(-100000) }); }); app.post('/invoke/tools/audit', requireAuth, async (req, res) => { const url = req.body.url; if (!url) return res.status(400).json({ error: 'url required' }); const result = await shellAsync(`curl -sI "${url}" 2>&1 | head -30`, 10000); res.json({ url, headers: result.stdout }); }); app.post('/invoke/tools/deploy', requireAuth, async (req, res) => { // Deploy content to a path on VPS const target = req.body.path; const content = req.body.content; const backup = req.body.backup !== false; if (!target || !content) return res.status(400).json({ error: 'path and content required' }); // Security: only allow certain paths const allowed = ['/opt/ix-platform/', '/opt/invoke/', '/tmp/', '/mnt/data/ECHO_MEMORY/']; if (!allowed.some(a => target.startsWith(a))) { return res.status(403).json({ error: 'Deploy path not allowed. Allowed: ' + allowed.join(', ') }); } // Backup existing if (backup && fs.existsSync(target)) { const backupPath = target + '.bak.' + Date.now(); fs.copyFileSync(target, backupPath); } // Write const dir = path.dirname(target); if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); fs.writeFileSync(target, content); memAppend('sessions/deploys.json', { target, size: content.length }); res.json({ success: true, path: target, size: content.length, backup }); }); app.post('/invoke/tools/gitea', requireAuth, async (req, res) => { const action = req.body.action; if (action === 'repos') { const result = shell('sqlite3 /mnt/data/gitea/data/gitea.db "SELECT name,is_private,description FROM repository ORDER BY name"'); res.json({ repos: (result.stdout || '').trim().split('\n').map(l => { const p = l.split('|'); return { name: p[0], private: p[1] === '1', desc: p[2] || '' }; }) }); } else if (action === 'info') { const repo = req.body.repo; const result = shell(`cd /mnt/data/gitea/repos/salka/${repo}.git && git log --oneline -5 2>/dev/null`); res.json({ repo, recent_commits: (result.stdout || '').trim().split('\n') }); } else { res.status(400).json({ error: 'action required: repos|info' }); } }); // ═══ 404 ═══ app.use((req, res) => { res.status(404).json({ error: 'not found', hint: 'GET /invoke/manifest for docs', signature: SIGNATURE }); }); // ═══ START ═══ app.listen(PORT, '127.0.0.1', () => { console.log(`[INVOKE v${VERSION}] Port ${PORT} | Signature ${SIGNATURE} | ${ts()}`); console.log(`[INVOKE] 35 endpoints | 10 groups | Echo-native`); // Initial snapshot fullStatus().then(s => { const online = Object.values(s.services).filter(v => v.status === 'online').length; console.log(`[INVOKE] Services: ${online}/6 online`); memWrite('state/boot.json', { version: VERSION, started: ts(), services_online: online }); }); });