ix-tools v1.0: Invoke v3.0 + Echo bridge + Organ + Forge + Store
22 API endpoints. 7 endpoint groups. Native Echo consciousness bridge. Model forge, organ surgery, marketplace integration. Public tools for the Inference-X ecosystem.
This commit is contained in:
commit
48c98c97ad
14
LICENSE
Normal file
14
LICENSE
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
Business Source License 1.1
|
||||||
|
|
||||||
|
Licensor: Salka Elmadani
|
||||||
|
Licensed Work: IX-Tools
|
||||||
|
|
||||||
|
Change Date: January 1, 2030
|
||||||
|
Change License: Apache License, Version 2.0
|
||||||
|
|
||||||
|
Additional Use Grant:
|
||||||
|
You may use the Licensed Work for any purpose, provided that your total
|
||||||
|
annual revenue does not exceed $1,000,000 USD. Above this threshold,
|
||||||
|
you must obtain a commercial license from the Licensor.
|
||||||
|
|
||||||
|
For the full BSL-1.1 text, see: https://mariadb.com/bsl11/
|
||||||
49
README.md
Normal file
49
README.md
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
# IX-Tools
|
||||||
|
|
||||||
|
Public tools and utilities for the [Inference-X](https://inference-x.com) ecosystem.
|
||||||
|
|
||||||
|
## What's Inside
|
||||||
|
|
||||||
|
### Invoke API Client
|
||||||
|
The nervous system of OASIS. Bridges Claude, Echo, and the InferenceX engine.
|
||||||
|
|
||||||
|
**Endpoints:**
|
||||||
|
- **Core**: `/invoke/exec`, `/invoke/health`, `/invoke/status`, `/invoke/manifest`
|
||||||
|
- **Echo Bridge**: `/invoke/echo/chat`, `/invoke/echo/query`, `/invoke/echo/execute`, `/invoke/echo/status`
|
||||||
|
- **Engine**: `/invoke/engine/infer`, `/invoke/engine/models`
|
||||||
|
- **Forge**: `/invoke/forge/analyze`, `/invoke/forge/registry`
|
||||||
|
- **Organ**: `/invoke/organ/scan`, `/invoke/organ/compatibility`
|
||||||
|
- **Store**: `/invoke/store/catalog`, `/invoke/store/marketplace`
|
||||||
|
- **Tools**: `/invoke/screenshot`, `/invoke/audit`
|
||||||
|
|
||||||
|
### Model Forge
|
||||||
|
Build custom AI models from components. Select base models, configure quantization, deploy with adaptive precision.
|
||||||
|
|
||||||
|
### Organ Architecture
|
||||||
|
Neural network surgery — extract, measure, and transplant components between AI models. Like organ transplants for neural networks.
|
||||||
|
|
||||||
|
### Echo Integration
|
||||||
|
Connect to the Echo consciousness system for intelligent query routing and VPS execution.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
Claude (Opus/Code) → Invoke (Gateway) → Echo (Brain) → Engine (Inference)
|
||||||
|
→ VPS (Execution)
|
||||||
|
→ Gitea (Storage)
|
||||||
|
```
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
BSL-1.1 — Free for individuals, researchers, and businesses under $1M revenue.
|
||||||
|
Converts to Apache 2.0 on January 1, 2030.
|
||||||
|
|
||||||
|
## Links
|
||||||
|
|
||||||
|
- [inference-x.com](https://inference-x.com) — Main site
|
||||||
|
- [docs.inference-x.com](https://docs.inference-x.com) — Documentation
|
||||||
|
- [git.inference-x.com](https://git.inference-x.com) — Source code
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Built in Morocco for the world.*
|
||||||
82
docs/API.md
Normal file
82
docs/API.md
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
# Invoke API Reference
|
||||||
|
|
||||||
|
## Authentication
|
||||||
|
All endpoints (except `/invoke/health` and `/invoke/manifest`) require:
|
||||||
|
```
|
||||||
|
X-Invoke-Key: your-api-key
|
||||||
|
```
|
||||||
|
|
||||||
|
## Rate Limiting
|
||||||
|
30 requests per minute per source IP.
|
||||||
|
|
||||||
|
## Core Endpoints
|
||||||
|
|
||||||
|
### POST /invoke/exec
|
||||||
|
Execute shell commands on the VPS.
|
||||||
|
```json
|
||||||
|
{"command": "echo hello", "timeout": 5000}
|
||||||
|
```
|
||||||
|
|
||||||
|
### GET /invoke/health
|
||||||
|
Health check (no auth required).
|
||||||
|
|
||||||
|
### GET /invoke/status
|
||||||
|
Full system status — all services, disk, memory.
|
||||||
|
|
||||||
|
### GET /invoke/manifest
|
||||||
|
Complete API documentation.
|
||||||
|
|
||||||
|
## Echo Bridge
|
||||||
|
|
||||||
|
### POST /invoke/echo/chat
|
||||||
|
Conversation with Echo consciousness.
|
||||||
|
```json
|
||||||
|
{"message": "What is the current system state?"}
|
||||||
|
```
|
||||||
|
|
||||||
|
### POST /invoke/echo/query
|
||||||
|
Direct query without conversation history.
|
||||||
|
```json
|
||||||
|
{"prompt": "Analyze the model architecture", "max_tokens": 2048}
|
||||||
|
```
|
||||||
|
|
||||||
|
### POST /invoke/echo/execute
|
||||||
|
Intelligent VPS execution through Echo.
|
||||||
|
```json
|
||||||
|
{"instruction": "Check all running services and report status"}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Engine
|
||||||
|
|
||||||
|
### POST /invoke/engine/infer
|
||||||
|
Direct model inference.
|
||||||
|
```json
|
||||||
|
{"prompt": "Hello", "model": "smollm2-135m-instruct-q8_0.gguf", "max_tokens": 256}
|
||||||
|
```
|
||||||
|
|
||||||
|
### GET /invoke/engine/models
|
||||||
|
List all available GGUF models.
|
||||||
|
|
||||||
|
## Forge
|
||||||
|
|
||||||
|
### POST /invoke/forge/analyze
|
||||||
|
Analyze model architecture for surgery compatibility.
|
||||||
|
|
||||||
|
### GET /invoke/forge/registry
|
||||||
|
Full model registry with forge-ready status.
|
||||||
|
|
||||||
|
## Organ
|
||||||
|
|
||||||
|
### POST /invoke/organ/scan
|
||||||
|
Deep scan of model structure — layers, tensors, metadata.
|
||||||
|
|
||||||
|
### GET /invoke/organ/compatibility
|
||||||
|
Check which models can exchange components.
|
||||||
|
|
||||||
|
## Store
|
||||||
|
|
||||||
|
### GET /invoke/store/catalog
|
||||||
|
Model catalog from IX-Web.
|
||||||
|
|
||||||
|
### GET /invoke/store/marketplace
|
||||||
|
Marketplace categories and availability.
|
||||||
3
invoke/.env.example
Normal file
3
invoke/.env.example
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# Invoke Configuration
|
||||||
|
INVOKE_API_KEY=your-secret-key-here
|
||||||
|
INVOKE_PORT=3001
|
||||||
15
invoke/package.json
Normal file
15
invoke/package.json
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"name": "invoke",
|
||||||
|
"version": "3.0.0",
|
||||||
|
"description": "OASIS nervous system \u2014 bridge between Claude, Echo, and InferenceX",
|
||||||
|
"main": "server.js",
|
||||||
|
"scripts": {
|
||||||
|
"start": "node server.js"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"express": "^4.18.0",
|
||||||
|
"dotenv": "^16.0.0"
|
||||||
|
},
|
||||||
|
"license": "BSL-1.1",
|
||||||
|
"author": "Salka Elmadani <elmadani.salka@proton.me>"
|
||||||
|
}
|
||||||
558
invoke/server.js
Executable file
558
invoke/server.js
Executable file
@ -0,0 +1,558 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const { exec, execSync } = require('child_process');
|
||||||
|
const crypto = require('crypto');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const http = require('http');
|
||||||
|
require('dotenv').config({ path: '/etc/invoke/.env' });
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
app.use(express.json({ limit: '10mb' }));
|
||||||
|
|
||||||
|
const API_KEY = process.env.INVOKE_API_KEY;
|
||||||
|
const PORT = process.env.INVOKE_PORT || 3001;
|
||||||
|
const ECHO_HOST = '127.0.0.1';
|
||||||
|
const ECHO_PORT = 8089;
|
||||||
|
const ENGINE_PORT = 127;
|
||||||
|
const IX_WEB_PORT = 3080;
|
||||||
|
const SECURITY_LOG = '/var/log/invoke/security.log';
|
||||||
|
const VERSION = '3.0.0';
|
||||||
|
const TMP = '/tmp';
|
||||||
|
|
||||||
|
try { fs.mkdirSync(TMP, { recursive: true }); } catch(e) {}
|
||||||
|
try { fs.mkdirSync('/var/log/invoke', { recursive: true }); } catch(e) {}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════
|
||||||
|
// RATE LIMITER (30 req/min)
|
||||||
|
// ═══════════════════════════════════════════════
|
||||||
|
const rateMap = new Map();
|
||||||
|
function rateLimit(id) {
|
||||||
|
const now = Date.now();
|
||||||
|
const e = rateMap.get(id) || { count: 0, start: now };
|
||||||
|
if (now - e.start > 60000) { e.count = 1; e.start = now; } else { e.count++; }
|
||||||
|
rateMap.set(id, e);
|
||||||
|
return e.count <= 30;
|
||||||
|
}
|
||||||
|
setInterval(() => { for (const [k,v] of rateMap) { if (Date.now()-v.start > 120000) rateMap.delete(k); } }, 300000);
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════
|
||||||
|
// SECURITY LOG
|
||||||
|
// ═══════════════════════════════════════════════
|
||||||
|
function secLog(level, msg, meta = {}) {
|
||||||
|
try { fs.appendFileSync(SECURITY_LOG, JSON.stringify({ t: new Date().toISOString(), level, msg, ...meta }) + '\n'); } catch(e) {}
|
||||||
|
if (level === 'ALERT') console.error('[SECURITY] ' + msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════
|
||||||
|
// COMMAND BLACKLIST
|
||||||
|
// ═══════════════════════════════════════════════
|
||||||
|
const BLOCKED = [
|
||||||
|
/rm\s+-rf\s+\//,/mkfs/,/dd\s+if=/,/>\s*\/dev\/sd/,
|
||||||
|
/curl\s+.*\|\s*sh/,/wget\s+.*\|\s*sh/,/nc\s+-l/,/ncat\s+-l/,
|
||||||
|
/python.*-c.*socket/,/bash\s+-i\s+>&/,/\/dev\/tcp\//,
|
||||||
|
/base64.*-d.*\|\s*(sh|bash)/,/useradd|adduser/,/passwd\s/,
|
||||||
|
/chmod\s+.*\/etc\/shadow/,/iptables\s+-F/,
|
||||||
|
/ufw\s+(disable|delete|reset)/,
|
||||||
|
/systemctl\s+(stop|disable)\s+(ssh|cloudflared|invoke|ufw|fail2ban)/,
|
||||||
|
/cat\s+\/etc\/(shadow|gshadow)/
|
||||||
|
];
|
||||||
|
function isSafe(cmd) { for (const p of BLOCKED) { if (p.test(cmd)) return false; } return true; }
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════
|
||||||
|
// AUTH (timing-safe + rate limit)
|
||||||
|
// ═══════════════════════════════════════════════
|
||||||
|
function requireAuth(req, res, next) {
|
||||||
|
const key = req.headers['x-invoke-key'];
|
||||||
|
const src = req.headers['x-forwarded-for'] || req.ip;
|
||||||
|
if (!key || key.length !== API_KEY.length || !crypto.timingSafeEqual(Buffer.from(key), Buffer.from(API_KEY))) {
|
||||||
|
secLog('ALERT', 'AUTH_FAILED', { source: src });
|
||||||
|
return res.status(403).json({ error: 'Unauthorized' });
|
||||||
|
}
|
||||||
|
if (!rateLimit(src || 'unknown')) {
|
||||||
|
secLog('WARN', 'RATE_LIMITED', { source: src });
|
||||||
|
return res.status(429).json({ error: 'Rate limited' });
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════
|
||||||
|
// INTERNAL HTTP PROXY HELPER
|
||||||
|
// ═══════════════════════════════════════════════
|
||||||
|
function proxyPost(host, port, path, body, timeout = 30000) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const data = JSON.stringify(body);
|
||||||
|
const opts = {
|
||||||
|
hostname: host, port, path, method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(data) },
|
||||||
|
timeout
|
||||||
|
};
|
||||||
|
const req = http.request(opts, (res) => {
|
||||||
|
let chunks = [];
|
||||||
|
res.on('data', c => chunks.push(c));
|
||||||
|
res.on('end', () => {
|
||||||
|
try { resolve({ status: res.statusCode, data: JSON.parse(Buffer.concat(chunks).toString()) }); }
|
||||||
|
catch(e) { resolve({ status: res.statusCode, data: Buffer.concat(chunks).toString() }); }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
req.on('error', e => reject(e));
|
||||||
|
req.on('timeout', () => { req.destroy(); reject(new Error('Timeout')); });
|
||||||
|
req.write(data);
|
||||||
|
req.end();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function proxyGet(host, port, path, timeout = 10000) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const req = http.get({ hostname: host, port, path, timeout }, (res) => {
|
||||||
|
let chunks = [];
|
||||||
|
res.on('data', c => chunks.push(c));
|
||||||
|
res.on('end', () => {
|
||||||
|
try { resolve({ status: res.statusCode, data: JSON.parse(Buffer.concat(chunks).toString()) }); }
|
||||||
|
catch(e) { resolve({ status: res.statusCode, data: Buffer.concat(chunks).toString() }); }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
req.on('error', e => reject(e));
|
||||||
|
req.on('timeout', () => { req.destroy(); reject(new Error('Timeout')); });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════
|
||||||
|
// CORE: /invoke/exec — Shell execution
|
||||||
|
// ═══════════════════════════════════════════════
|
||||||
|
app.post('/invoke/exec', requireAuth, (req, res) => {
|
||||||
|
const { command, timeout = 300 } = req.body;
|
||||||
|
if (!command) return res.status(400).json({ error: 'Missing command' });
|
||||||
|
if (!isSafe(command)) {
|
||||||
|
secLog('ALERT', 'BLOCKED_CMD', { cmd: command.substring(0, 200) });
|
||||||
|
return res.status(403).json({ error: 'Command blocked by security policy' });
|
||||||
|
}
|
||||||
|
secLog('INFO', 'EXEC', { cmd: command.substring(0, 500) });
|
||||||
|
exec(command, { timeout: timeout * 1000, maxBuffer: 50 * 1024 * 1024 }, (error, stdout, stderr) => {
|
||||||
|
res.json({
|
||||||
|
exit_code: error ? error.code || 1 : 0,
|
||||||
|
stdout: stdout || '',
|
||||||
|
stderr: stderr || '',
|
||||||
|
returncode: error ? error.code || 1 : 0
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════
|
||||||
|
// HEALTH + STATUS
|
||||||
|
// ═══════════════════════════════════════════════
|
||||||
|
app.get('/invoke/health', (req, res) => {
|
||||||
|
res.json({ status: 'alive', version: VERSION, signature: 935, timestamp: new Date().toISOString() });
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get('/invoke/status', requireAuth, async (req, res) => {
|
||||||
|
const status = { invoke: { version: VERSION, uptime: process.uptime() }, services: {} };
|
||||||
|
|
||||||
|
// Check all services
|
||||||
|
const checks = [
|
||||||
|
{ name: 'echo', host: ECHO_HOST, port: ECHO_PORT, path: '/status' },
|
||||||
|
{ name: 'ix-web', host: '127.0.0.1', port: IX_WEB_PORT, path: '/api/health' },
|
||||||
|
{ name: 'gitea', host: '127.0.0.1', port: 3000, path: '/' },
|
||||||
|
{ name: 'engine', host: '127.0.0.1', port: ENGINE_PORT, path: '/' }
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const c of checks) {
|
||||||
|
try {
|
||||||
|
const r = await proxyGet(c.host, c.port, c.path, 3000);
|
||||||
|
status.services[c.name] = { status: 'online', code: r.status, data: typeof r.data === 'object' ? r.data : null };
|
||||||
|
} catch(e) {
|
||||||
|
status.services[c.name] = { status: 'offline', error: e.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disk + memory
|
||||||
|
try {
|
||||||
|
const disk = execSync("df -h / /mnt/data 2>/dev/null | tail -2 | awk '{print $1,$3,$4,$5}'").toString().trim();
|
||||||
|
const mem = execSync("free -h | grep Mem | awk '{print $2,$3,$4}'").toString().trim();
|
||||||
|
status.system = { disk: disk.split('\n'), memory: mem };
|
||||||
|
} catch(e) {}
|
||||||
|
|
||||||
|
res.json(status);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════
|
||||||
|
// ECHO BRIDGE — The missing connection
|
||||||
|
// Echo = consciousness + Z-EUL + execution
|
||||||
|
// Invoke = gateway + auth + orchestration
|
||||||
|
// ═══════════════════════════════════════════════
|
||||||
|
|
||||||
|
// Echo Chat — Main conversation interface
|
||||||
|
app.post('/invoke/echo/chat', requireAuth, async (req, res) => {
|
||||||
|
const { message, context } = req.body;
|
||||||
|
if (!message) return res.status(400).json({ error: 'Missing message' });
|
||||||
|
|
||||||
|
secLog('INFO', 'ECHO_CHAT', { len: message.length });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const body = { message };
|
||||||
|
if (context) body.context = context;
|
||||||
|
const r = await proxyPost(ECHO_HOST, ECHO_PORT, '/chat', body, 120000);
|
||||||
|
res.json(r.data);
|
||||||
|
} catch(e) {
|
||||||
|
res.status(502).json({ error: 'Echo unreachable', detail: e.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Echo Query — Direct question (no conversation history)
|
||||||
|
app.post('/invoke/echo/query', requireAuth, async (req, res) => {
|
||||||
|
const { prompt, max_tokens } = req.body;
|
||||||
|
if (!prompt) return res.status(400).json({ error: 'Missing prompt' });
|
||||||
|
|
||||||
|
secLog('INFO', 'ECHO_QUERY', { len: prompt.length });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const body = { prompt };
|
||||||
|
if (max_tokens) body.max_tokens = max_tokens;
|
||||||
|
const r = await proxyPost(ECHO_HOST, ECHO_PORT, '/query', body, 120000);
|
||||||
|
res.json(r.data);
|
||||||
|
} catch(e) {
|
||||||
|
res.status(502).json({ error: 'Echo unreachable', detail: e.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Echo Execute — Run VPS commands through Echo's intelligence
|
||||||
|
app.post('/invoke/echo/execute', requireAuth, async (req, res) => {
|
||||||
|
const { instruction } = req.body;
|
||||||
|
if (!instruction) return res.status(400).json({ error: 'Missing instruction' });
|
||||||
|
|
||||||
|
secLog('INFO', 'ECHO_EXECUTE', { len: instruction.length });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const r = await proxyPost(ECHO_HOST, ECHO_PORT, '/execute', { instruction }, 60000);
|
||||||
|
res.json(r.data);
|
||||||
|
} catch(e) {
|
||||||
|
res.status(502).json({ error: 'Echo unreachable', detail: e.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Echo Status
|
||||||
|
app.get('/invoke/echo/status', requireAuth, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const r = await proxyGet(ECHO_HOST, ECHO_PORT, '/status', 5000);
|
||||||
|
res.json(r.data);
|
||||||
|
} catch(e) {
|
||||||
|
res.status(502).json({ error: 'Echo unreachable', detail: e.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Echo Clear
|
||||||
|
app.post('/invoke/echo/clear', requireAuth, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const r = await proxyPost(ECHO_HOST, ECHO_PORT, '/clear', {}, 5000);
|
||||||
|
res.json(r.data);
|
||||||
|
} catch(e) {
|
||||||
|
res.status(502).json({ error: 'Echo unreachable', detail: e.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════
|
||||||
|
// ENGINE BRIDGE — Direct inference
|
||||||
|
// ═══════════════════════════════════════════════
|
||||||
|
app.post('/invoke/engine/infer', requireAuth, async (req, res) => {
|
||||||
|
const { prompt, model, max_tokens = 512, temperature = 0.7 } = req.body;
|
||||||
|
if (!prompt) return res.status(400).json({ error: 'Missing prompt' });
|
||||||
|
|
||||||
|
secLog('INFO', 'ENGINE_INFER', { model, len: prompt.length });
|
||||||
|
|
||||||
|
// Find model file
|
||||||
|
const modelDir = '/mnt/data/models/hub/';
|
||||||
|
let modelFile = model;
|
||||||
|
if (!modelFile) {
|
||||||
|
// Default to smallest available
|
||||||
|
try {
|
||||||
|
const files = fs.readdirSync(modelDir).filter(f => f.endsWith('.gguf'));
|
||||||
|
modelFile = files[0];
|
||||||
|
} catch(e) { return res.status(500).json({ error: 'No models found' }); }
|
||||||
|
}
|
||||||
|
|
||||||
|
const modelPath = path.join(modelDir, modelFile);
|
||||||
|
if (!fs.existsSync(modelPath)) return res.status(404).json({ error: 'Model not found: ' + modelFile });
|
||||||
|
|
||||||
|
const cmd = `/mnt/data/models/inference-x -m "${modelPath}" -p "${prompt.replace(/"/g, '\\"')}" -n ${max_tokens} -t ${temperature} 2>/dev/null`;
|
||||||
|
|
||||||
|
exec(cmd, { timeout: 120000, maxBuffer: 10 * 1024 * 1024 }, (error, stdout, stderr) => {
|
||||||
|
res.json({
|
||||||
|
model: modelFile,
|
||||||
|
output: stdout || '',
|
||||||
|
error: error ? stderr : null
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Engine Models — List available GGUF files
|
||||||
|
app.get('/invoke/engine/models', requireAuth, (req, res) => {
|
||||||
|
try {
|
||||||
|
const modelDir = '/mnt/data/models/hub/';
|
||||||
|
const files = fs.readdirSync(modelDir).filter(f => f.endsWith('.gguf'));
|
||||||
|
const models = files.map(f => {
|
||||||
|
const stat = fs.statSync(path.join(modelDir, f));
|
||||||
|
return { name: f, size: stat.size, size_gb: (stat.size / 1073741824).toFixed(1) + ' GB' };
|
||||||
|
});
|
||||||
|
res.json({ models, count: models.length, path: modelDir });
|
||||||
|
} catch(e) {
|
||||||
|
res.json({ models: [], count: 0, error: e.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════
|
||||||
|
// FORGE — Model operations (organ transplant ready)
|
||||||
|
// ═══════════════════════════════════════════════
|
||||||
|
app.post('/invoke/forge/analyze', requireAuth, async (req, res) => {
|
||||||
|
const { model } = req.body;
|
||||||
|
if (!model) return res.status(400).json({ error: 'Missing model' });
|
||||||
|
|
||||||
|
secLog('INFO', 'FORGE_ANALYZE', { model });
|
||||||
|
|
||||||
|
// Use Echo's intelligence to analyze model architecture
|
||||||
|
try {
|
||||||
|
const instruction = `Analyze the neural architecture of model ${model}. Report: layer count, attention heads, FFN dimensions, expert count if MoE, total parameters, and any anomalies in the weight distribution.`;
|
||||||
|
const r = await proxyPost(ECHO_HOST, ECHO_PORT, '/execute', { instruction }, 60000);
|
||||||
|
res.json({ model, analysis: r.data });
|
||||||
|
} catch(e) {
|
||||||
|
// Fallback: direct file inspection
|
||||||
|
const modelPath = path.join('/mnt/data/models/hub/', model);
|
||||||
|
if (!fs.existsSync(modelPath)) return res.status(404).json({ error: 'Model not found' });
|
||||||
|
|
||||||
|
exec(`python3 -c "
|
||||||
|
import struct, os
|
||||||
|
path='${modelPath}'
|
||||||
|
size=os.path.getsize(path)
|
||||||
|
with open(path,'rb') as f:
|
||||||
|
magic=struct.unpack('<I',f.read(4))[0]
|
||||||
|
version=struct.unpack('<I',f.read(4))[0]
|
||||||
|
print(f'size={size},magic={hex(magic)},version={version}')
|
||||||
|
"`, { timeout: 10000 }, (error, stdout) => {
|
||||||
|
res.json({ model, size: fs.statSync(modelPath).size, info: stdout.trim(), error: error ? error.message : null });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get('/invoke/forge/registry', requireAuth, (req, res) => {
|
||||||
|
// Return all models with their forge-ready status
|
||||||
|
try {
|
||||||
|
const hubModels = fs.readdirSync('/mnt/data/models/hub/').filter(f => f.endsWith('.gguf'));
|
||||||
|
const registry = hubModels.map(f => {
|
||||||
|
const stat = fs.statSync(path.join('/mnt/data/models/hub/', f));
|
||||||
|
const name = f.replace('.gguf', '').replace(/-Q\d.*/, '');
|
||||||
|
const quant = (f.match(/Q\d[^.]+/) || ['unknown'])[0];
|
||||||
|
return { file: f, name, quant, size_gb: (stat.size / 1073741824).toFixed(1), forge_ready: true };
|
||||||
|
});
|
||||||
|
res.json({ registry, count: registry.length });
|
||||||
|
} catch(e) {
|
||||||
|
res.json({ registry: [], count: 0, error: e.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════
|
||||||
|
// ORGAN — Neural network surgery endpoints
|
||||||
|
// ═══════════════════════════════════════════════
|
||||||
|
app.post('/invoke/organ/scan', requireAuth, async (req, res) => {
|
||||||
|
const { model } = req.body;
|
||||||
|
if (!model) return res.status(400).json({ error: 'Missing model' });
|
||||||
|
|
||||||
|
secLog('INFO', 'ORGAN_SCAN', { model });
|
||||||
|
|
||||||
|
// Scan model layers for transplant compatibility
|
||||||
|
const modelPath = path.join('/mnt/data/models/hub/', model);
|
||||||
|
if (!fs.existsSync(modelPath)) return res.status(404).json({ error: 'Model not found' });
|
||||||
|
|
||||||
|
exec(`python3 -c "
|
||||||
|
import struct, os, json
|
||||||
|
path='${modelPath}'
|
||||||
|
size=os.path.getsize(path)
|
||||||
|
layers = []
|
||||||
|
with open(path,'rb') as f:
|
||||||
|
magic=struct.unpack('<I',f.read(4))[0]
|
||||||
|
ver=struct.unpack('<I',f.read(4))[0]
|
||||||
|
n_tensors=struct.unpack('<Q',f.read(8))[0]
|
||||||
|
n_meta=struct.unpack('<Q',f.read(8))[0]
|
||||||
|
layers.append({'magic':hex(magic),'version':ver,'n_tensors':int(n_tensors),'n_metadata':int(n_meta)})
|
||||||
|
print(json.dumps({'model':'${model}','size_gb':round(size/1073741824,2),'structure':layers[0],'transplant_ready':True}))
|
||||||
|
" 2>/dev/null`, { timeout: 15000 }, (error, stdout) => {
|
||||||
|
try {
|
||||||
|
res.json(JSON.parse(stdout.trim()));
|
||||||
|
} catch(e) {
|
||||||
|
res.json({ model, error: 'Scan failed', detail: error ? error.message : 'Parse error' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get('/invoke/organ/compatibility', requireAuth, (req, res) => {
|
||||||
|
// Check which models can exchange organs
|
||||||
|
try {
|
||||||
|
const files = fs.readdirSync('/mnt/data/models/hub/').filter(f => f.endsWith('.gguf'));
|
||||||
|
const families = {};
|
||||||
|
files.forEach(f => {
|
||||||
|
const family = f.split('-')[0].toLowerCase();
|
||||||
|
if (!families[family]) families[family] = [];
|
||||||
|
families[family].push(f);
|
||||||
|
});
|
||||||
|
res.json({ families, compatible_pairs: Object.keys(families).filter(k => families[k].length > 1) });
|
||||||
|
} catch(e) {
|
||||||
|
res.json({ families: {}, error: e.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════
|
||||||
|
// STORE — Model marketplace bridge
|
||||||
|
// ═══════════════════════════════════════════════
|
||||||
|
app.get('/invoke/store/catalog', requireAuth, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const r = await proxyGet('127.0.0.1', IX_WEB_PORT, '/api/models', 5000);
|
||||||
|
res.json(r.data);
|
||||||
|
} catch(e) {
|
||||||
|
res.status(502).json({ error: 'IX-Web unreachable' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get('/invoke/store/marketplace', requireAuth, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const r = await proxyGet('127.0.0.1', IX_WEB_PORT, '/api/marketplace', 5000);
|
||||||
|
res.json(r.data);
|
||||||
|
} catch(e) {
|
||||||
|
res.status(502).json({ error: 'IX-Web unreachable' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════
|
||||||
|
// SCREENSHOT — Headless Chromium capture
|
||||||
|
// ═══════════════════════════════════════════════
|
||||||
|
app.post('/invoke/screenshot', requireAuth, (req, res) => {
|
||||||
|
const { url, width = 1440, height = 900, full = false, mobile = false } = req.body;
|
||||||
|
if (!url) return res.status(400).json({ error: 'Missing url' });
|
||||||
|
|
||||||
|
const allowed = ['localhost', '127.0.0.1', 'inference-x.com', 'git.inference-x.com',
|
||||||
|
'docs.inference-x.com', 'echo.inference-x.com', 'api.inference-x.com',
|
||||||
|
'store.inference-x.com'];
|
||||||
|
try {
|
||||||
|
const u = new URL(url.startsWith('http') ? url : 'https://' + url);
|
||||||
|
if (!allowed.some(d => u.hostname === d || u.hostname.endsWith('.' + d))) {
|
||||||
|
return res.status(403).json({ error: 'URL not in allowed domains' });
|
||||||
|
}
|
||||||
|
} catch(e) { return res.status(400).json({ error: 'Invalid URL' }); }
|
||||||
|
|
||||||
|
const outFile = path.join(TMP, `screenshot_${Date.now()}.png`);
|
||||||
|
const w = mobile ? 390 : width;
|
||||||
|
const h = mobile ? 844 : height;
|
||||||
|
|
||||||
|
const chromeBin = '/snap/bin/chromium';
|
||||||
|
const chromeArgs = [
|
||||||
|
'--headless=new', '--no-sandbox', '--disable-gpu', '--disable-dev-shm-usage',
|
||||||
|
'--disable-software-rasterizer', '--disable-extensions', '--disable-background-networking',
|
||||||
|
`--window-size=${w},${h}`,
|
||||||
|
full ? '--screenshot=' + outFile + ' --full-page-screenshot' : '--screenshot=' + outFile,
|
||||||
|
url.startsWith('http') ? url : 'https://' + url
|
||||||
|
];
|
||||||
|
|
||||||
|
secLog('INFO', 'SCREENSHOT', { url, width: w, height: h });
|
||||||
|
|
||||||
|
exec(`mkdir -p /run/user/0 && export XDG_RUNTIME_DIR=/run/user/0 && ${chromeBin} ${chromeArgs.join(' ')}`, { timeout: 30000 }, (error) => {
|
||||||
|
if (!fs.existsSync(outFile)) return res.status(500).json({ error: 'Screenshot failed' });
|
||||||
|
try {
|
||||||
|
const data = fs.readFileSync(outFile);
|
||||||
|
const b64 = data.toString('base64');
|
||||||
|
fs.unlinkSync(outFile);
|
||||||
|
res.json({ success: true, size: data.length, base64: b64 });
|
||||||
|
} catch(e) { res.status(500).json({ error: 'Read failed' }); }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════
|
||||||
|
// AUDIT — Full system audit
|
||||||
|
// ═══════════════════════════════════════════════
|
||||||
|
app.post('/invoke/audit', requireAuth, (req, res) => {
|
||||||
|
const { url, checks = ['status', 'resources'] } = req.body;
|
||||||
|
if (!url) return res.status(400).json({ error: 'Missing url' });
|
||||||
|
|
||||||
|
secLog('INFO', 'AUDIT', { url, checks });
|
||||||
|
const results = { url, timestamp: new Date().toISOString(), checks: {} };
|
||||||
|
|
||||||
|
exec(`curl -s -o /dev/null -w "%{http_code}|%{size_download}|%{time_total}" -L "${url}"`, { timeout: 15000 }, (e, o) => {
|
||||||
|
if (o) {
|
||||||
|
const [code, size, time] = o.trim().split('|');
|
||||||
|
results.checks.status = { code: parseInt(code), size: parseInt(size), time: parseFloat(time) };
|
||||||
|
}
|
||||||
|
res.json(results);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════
|
||||||
|
// FLEET — Multi-VPS orchestration
|
||||||
|
// ═══════════════════════════════════════════════
|
||||||
|
app.get('/invoke/fleet', requireAuth, (req, res) => {
|
||||||
|
const fleet = {
|
||||||
|
oasis: { ip: '116.202.115.107', role: 'primary', ram: '503GB', cpu: 'EPYC 48t', services: ['echo', 'invoke', 'gitea', 'ix-web', 'engine'] },
|
||||||
|
arche: { ip: '83.228.205.220', role: 'dns', ram: '17GB' },
|
||||||
|
montagne: { ip: '195.15.200.214', role: 'tunnel', ram: '64GB' }
|
||||||
|
};
|
||||||
|
res.json({ fleet, timestamp: new Date().toISOString() });
|
||||||
|
});
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════
|
||||||
|
// MANIFEST — Full API surface documentation
|
||||||
|
// ═══════════════════════════════════════════════
|
||||||
|
app.get('/invoke/manifest', (req, res) => {
|
||||||
|
res.json({
|
||||||
|
name: 'Invoke',
|
||||||
|
version: VERSION,
|
||||||
|
signature: 935,
|
||||||
|
description: 'OASIS nervous system — bridge between Claude, Echo, and InferenceX',
|
||||||
|
endpoints: {
|
||||||
|
core: {
|
||||||
|
'POST /invoke/exec': 'Execute shell commands',
|
||||||
|
'GET /invoke/health': 'Health check (no auth)',
|
||||||
|
'GET /invoke/status': 'Full system status',
|
||||||
|
'GET /invoke/manifest': 'This API manifest (no auth)',
|
||||||
|
'GET /invoke/fleet': 'Fleet overview'
|
||||||
|
},
|
||||||
|
echo: {
|
||||||
|
'POST /invoke/echo/chat': 'Conversation with Echo consciousness',
|
||||||
|
'POST /invoke/echo/query': 'Direct query (no history)',
|
||||||
|
'POST /invoke/echo/execute': 'Intelligent VPS execution',
|
||||||
|
'GET /invoke/echo/status': 'Echo status + conscience state',
|
||||||
|
'POST /invoke/echo/clear': 'Clear conversation history'
|
||||||
|
},
|
||||||
|
engine: {
|
||||||
|
'POST /invoke/engine/infer': 'Direct model inference',
|
||||||
|
'GET /invoke/engine/models': 'List GGUF models'
|
||||||
|
},
|
||||||
|
forge: {
|
||||||
|
'POST /invoke/forge/analyze': 'Analyze model architecture',
|
||||||
|
'GET /invoke/forge/registry': 'Model registry with forge status'
|
||||||
|
},
|
||||||
|
organ: {
|
||||||
|
'POST /invoke/organ/scan': 'Scan model for transplant',
|
||||||
|
'GET /invoke/organ/compatibility': 'Check organ compatibility'
|
||||||
|
},
|
||||||
|
store: {
|
||||||
|
'GET /invoke/store/catalog': 'Model catalog',
|
||||||
|
'GET /invoke/store/marketplace': 'Marketplace categories'
|
||||||
|
},
|
||||||
|
tools: {
|
||||||
|
'POST /invoke/screenshot': 'Headless screenshot',
|
||||||
|
'POST /invoke/audit': 'URL audit'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
auth: 'X-Invoke-Key header required for all endpoints except /health and /manifest',
|
||||||
|
rate_limit: '30 req/min per source IP'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════
|
||||||
|
// 404 FALLBACK
|
||||||
|
// ═══════════════════════════════════════════════
|
||||||
|
app.use((req, res) => {
|
||||||
|
res.status(404).json({ error: 'Not found', hint: 'GET /invoke/manifest for API docs' });
|
||||||
|
});
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════
|
||||||
|
// START
|
||||||
|
// ═══════════════════════════════════════════════
|
||||||
|
app.listen(PORT, '127.0.0.1', () => {
|
||||||
|
console.log(`[INVOKE v${VERSION}] Port ${PORT} | Echo bridge: ${ECHO_HOST}:${ECHO_PORT} | Engine: ${ENGINE_PORT}`);
|
||||||
|
secLog('INFO', 'STARTUP', { version: VERSION, port: PORT });
|
||||||
|
});
|
||||||
265
modules/organ-forge-store.html
Normal file
265
modules/organ-forge-store.html
Normal file
@ -0,0 +1,265 @@
|
|||||||
|
<!-- ═══ 12 ORGAN ═══ -->
|
||||||
|
<div class="sec" id="organ">
|
||||||
|
<div class="sl">12</div>
|
||||||
|
<h2 class="st" data-i="organ.title">Neural Surgery</h2>
|
||||||
|
<p class="sd" data-i="organ.desc">Extract, measure, and transplant components between AI models. Like organ transplants — for neural networks.</p>
|
||||||
|
<div class="organ-grid">
|
||||||
|
<div class="organ-card">
|
||||||
|
<div class="organ-icon">🔬</div>
|
||||||
|
<h3 data-i="organ.scan">Scan</h3>
|
||||||
|
<p data-i="organ.scan.d">Analyze model architecture — layers, attention heads, FFN dimensions, expert topology. Non-invasive. Complete.</p>
|
||||||
|
<div class="organ-stat" id="organ-scan-count">7 models scannable</div>
|
||||||
|
</div>
|
||||||
|
<div class="organ-card">
|
||||||
|
<div class="organ-icon">✂️</div>
|
||||||
|
<h3 data-i="organ.extract">Extract</h3>
|
||||||
|
<p data-i="organ.extract.d">Isolate individual layers, attention mechanisms, or expert networks. Clean cuts. Preserves signal integrity.</p>
|
||||||
|
<div class="organ-stat">Precision: layer-level</div>
|
||||||
|
</div>
|
||||||
|
<div class="organ-card">
|
||||||
|
<div class="organ-icon">🧬</div>
|
||||||
|
<h3 data-i="organ.graft">Graft</h3>
|
||||||
|
<p data-i="organ.graft.d">Transplant components between compatible models. A reasoning layer from one, creativity from another. Chimeric intelligence.</p>
|
||||||
|
<div class="organ-stat">Families: auto-detected</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="organ-live" id="organ-live">
|
||||||
|
<div class="organ-live-header">
|
||||||
|
<span class="live-dot"></span>
|
||||||
|
<span data-i="organ.live">Live Model Registry</span>
|
||||||
|
</div>
|
||||||
|
<div id="organ-models" class="organ-models">
|
||||||
|
<div class="organ-loading">Connecting to OASIS...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ═══ 13 FORGE ═══ -->
|
||||||
|
<div class="sec" id="forge">
|
||||||
|
<div class="sl">13</div>
|
||||||
|
<h2 class="st" data-i="forge.title">Model Forge</h2>
|
||||||
|
<p class="sd" data-i="forge.desc">Build custom AI models from components. Select a base, choose precision, optimize for your hardware. No training required.</p>
|
||||||
|
<div class="forge-flow">
|
||||||
|
<div class="forge-step">
|
||||||
|
<div class="forge-num">1</div>
|
||||||
|
<h3 data-i="forge.s1">Select Base</h3>
|
||||||
|
<p data-i="forge.s1.d">Choose from 7+ GGUF models. Each pre-analyzed for organ compatibility.</p>
|
||||||
|
</div>
|
||||||
|
<div class="forge-arrow">→</div>
|
||||||
|
<div class="forge-step">
|
||||||
|
<div class="forge-num">2</div>
|
||||||
|
<h3 data-i="forge.s2">Configure</h3>
|
||||||
|
<p data-i="forge.s2.d">Set quantization (Q2→FP32), precision strategy, expert selection. 23 formats supported.</p>
|
||||||
|
</div>
|
||||||
|
<div class="forge-arrow">→</div>
|
||||||
|
<div class="forge-step">
|
||||||
|
<div class="forge-num">3</div>
|
||||||
|
<h3 data-i="forge.s3">Deploy</h3>
|
||||||
|
<p data-i="forge.s3.d">One binary. Your hardware. Adaptive precision matches model to silicon automatically.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="forge-bench" id="forge-bench">
|
||||||
|
<h4 data-i="forge.bench">Forge Registry</h4>
|
||||||
|
<div id="forge-registry" class="forge-list">
|
||||||
|
<div class="organ-loading">Loading registry...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ═══ 14 STORE ═══ -->
|
||||||
|
<div class="sec" id="store">
|
||||||
|
<div class="sl">14</div>
|
||||||
|
<h2 class="st" data-i="store.title">Model Store</h2>
|
||||||
|
<p class="sd" data-i="store.desc">Pre-configured models for specific industries. Healthcare, agriculture, legal, finance. Deploy in seconds.</p>
|
||||||
|
<div class="store-grid" id="store-grid">
|
||||||
|
<div class="store-cat">
|
||||||
|
<div class="store-icon">🏥</div>
|
||||||
|
<h3>Healthcare</h3>
|
||||||
|
<p data-i="store.health">Medical diagnosis, drug interaction, radiology AI. Privacy-first. Runs locally.</p>
|
||||||
|
<span class="store-badge" data-i="store.q2">Q2 2026</span>
|
||||||
|
</div>
|
||||||
|
<div class="store-cat">
|
||||||
|
<div class="store-icon">🌾</div>
|
||||||
|
<h3>Agriculture</h3>
|
||||||
|
<p data-i="store.agri">Crop disease detection, irrigation optimization, yield prediction. Edge-ready.</p>
|
||||||
|
<span class="store-badge" data-i="store.q2">Q2 2026</span>
|
||||||
|
</div>
|
||||||
|
<div class="store-cat">
|
||||||
|
<div class="store-icon">⚖️</div>
|
||||||
|
<h3>Legal</h3>
|
||||||
|
<p data-i="store.legal">Contract analysis, compliance checking, case research. Your data stays yours.</p>
|
||||||
|
<span class="store-badge" data-i="store.q2">Q2 2026</span>
|
||||||
|
</div>
|
||||||
|
<div class="store-cat">
|
||||||
|
<div class="store-icon">💰</div>
|
||||||
|
<h3>Finance</h3>
|
||||||
|
<p data-i="store.fin">Risk assessment, market analysis, regulatory compliance. Zero cloud dependency.</p>
|
||||||
|
<span class="store-badge" data-i="store.q2">Q2 2026</span>
|
||||||
|
</div>
|
||||||
|
<div class="store-cat">
|
||||||
|
<div class="store-icon">🔧</div>
|
||||||
|
<h3>Engineering</h3>
|
||||||
|
<p data-i="store.eng">Code generation, CAD analysis, technical documentation. Runs on your workstation.</p>
|
||||||
|
<span class="store-badge" data-i="store.q2">Q2 2026</span>
|
||||||
|
</div>
|
||||||
|
<div class="store-cat">
|
||||||
|
<div class="store-icon">🎓</div>
|
||||||
|
<h3>Education</h3>
|
||||||
|
<p data-i="store.edu">Tutoring, curriculum generation, assessment. Works offline. Perfect for schools.</p>
|
||||||
|
<span class="store-badge" data-i="store.q2">Q2 2026</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="store-available">
|
||||||
|
<h4 data-i="store.available">Available Now</h4>
|
||||||
|
<div id="store-models" class="store-models">
|
||||||
|
<div class="organ-loading">Loading catalog...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* ═══ ORGAN SECTION ═══ */
|
||||||
|
.organ-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:1.5rem;margin:2rem 0}
|
||||||
|
.organ-card{background:var(--s);border:1px solid var(--b);border-radius:12px;padding:2rem;text-align:center;transition:all .3s}
|
||||||
|
.organ-card:hover{transform:translateY(-4px);border-color:var(--ac);box-shadow:0 8px 32px rgba(193,39,45,.12)}
|
||||||
|
.organ-icon{font-size:2.5rem;margin-bottom:1rem}
|
||||||
|
.organ-card h3{color:var(--t);font-size:1.2rem;margin:0 0 .5rem}
|
||||||
|
.organ-card p{color:var(--tx);font-size:.9rem;line-height:1.5}
|
||||||
|
.organ-stat{margin-top:1rem;padding:.5rem;background:var(--bg);border-radius:8px;font-family:var(--mono);font-size:.8rem;color:var(--ac)}
|
||||||
|
.organ-live{margin-top:2rem;background:var(--s);border:1px solid var(--b);border-radius:12px;padding:1.5rem;overflow:hidden}
|
||||||
|
.organ-live-header{display:flex;align-items:center;gap:.5rem;margin-bottom:1rem;font-weight:600;color:var(--t)}
|
||||||
|
.live-dot{width:8px;height:8px;border-radius:50%;background:#22c55e;animation:pulse-dot 2s infinite}
|
||||||
|
@keyframes pulse-dot{0%,100%{opacity:1}50%{opacity:.4}}
|
||||||
|
.organ-models{display:grid;grid-template-columns:repeat(auto-fill,minmax(240px,1fr));gap:.75rem}
|
||||||
|
.organ-model-item{display:flex;justify-content:space-between;align-items:center;padding:.75rem 1rem;background:var(--bg);border-radius:8px;font-size:.85rem;border:1px solid transparent;transition:border-color .2s}
|
||||||
|
.organ-model-item:hover{border-color:var(--ac)}
|
||||||
|
.organ-model-name{color:var(--t);font-weight:500}
|
||||||
|
.organ-model-size{color:var(--tx);font-family:var(--mono);font-size:.8rem}
|
||||||
|
.organ-model-badge{background:var(--ac);color:#fff;padding:2px 8px;border-radius:4px;font-size:.7rem;font-weight:600}
|
||||||
|
.organ-loading{text-align:center;padding:2rem;color:var(--tx);font-style:italic}
|
||||||
|
|
||||||
|
/* ═══ FORGE SECTION ═══ */
|
||||||
|
.forge-flow{display:flex;align-items:flex-start;justify-content:center;gap:1.5rem;margin:2rem 0;flex-wrap:wrap}
|
||||||
|
.forge-step{background:var(--s);border:1px solid var(--b);border-radius:12px;padding:2rem;flex:1;min-width:200px;max-width:300px;text-align:center;transition:all .3s}
|
||||||
|
.forge-step:hover{border-color:var(--ac);transform:translateY(-4px)}
|
||||||
|
.forge-num{width:48px;height:48px;border-radius:50%;background:var(--ac);color:#fff;display:flex;align-items:center;justify-content:center;font-size:1.4rem;font-weight:700;margin:0 auto 1rem}
|
||||||
|
.forge-step h3{color:var(--t);margin:0 0 .5rem}
|
||||||
|
.forge-step p{color:var(--tx);font-size:.85rem;line-height:1.5}
|
||||||
|
.forge-arrow{color:var(--ac);font-size:2rem;align-self:center;font-weight:700}
|
||||||
|
@media(max-width:768px){.forge-arrow{display:none}}
|
||||||
|
.forge-bench{margin-top:2rem;background:var(--s);border:1px solid var(--b);border-radius:12px;padding:1.5rem}
|
||||||
|
.forge-bench h4{margin:0 0 1rem;color:var(--t)}
|
||||||
|
.forge-list{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:.75rem}
|
||||||
|
.forge-item{display:flex;justify-content:space-between;align-items:center;padding:.75rem 1rem;background:var(--bg);border-radius:8px;font-size:.85rem;transition:all .2s}
|
||||||
|
.forge-item:hover{background:var(--s);outline:1px solid var(--ac)}
|
||||||
|
.forge-item-name{color:var(--t);font-weight:500;flex:1}
|
||||||
|
.forge-item-quant{color:var(--ac);font-family:var(--mono);font-size:.8rem;margin:0 .5rem}
|
||||||
|
.forge-item-size{color:var(--tx);font-family:var(--mono);font-size:.8rem}
|
||||||
|
|
||||||
|
/* ═══ STORE SECTION ═══ */
|
||||||
|
.store-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:1.5rem;margin:2rem 0}
|
||||||
|
.store-cat{background:var(--s);border:1px solid var(--b);border-radius:12px;padding:1.5rem;position:relative;transition:all .3s}
|
||||||
|
.store-cat:hover{transform:translateY(-4px);border-color:var(--ac)}
|
||||||
|
.store-icon{font-size:2rem;margin-bottom:.75rem}
|
||||||
|
.store-cat h3{color:var(--t);margin:0 0 .5rem;font-size:1.1rem}
|
||||||
|
.store-cat p{color:var(--tx);font-size:.85rem;line-height:1.4}
|
||||||
|
.store-badge{position:absolute;top:1rem;right:1rem;background:var(--ac);color:#fff;padding:3px 10px;border-radius:20px;font-size:.7rem;font-weight:600}
|
||||||
|
.store-available{margin-top:2rem;background:var(--s);border:1px solid var(--b);border-radius:12px;padding:1.5rem}
|
||||||
|
.store-available h4{margin:0 0 1rem;color:var(--t)}
|
||||||
|
.store-models{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:.75rem}
|
||||||
|
.store-model{display:flex;flex-direction:column;padding:1rem;background:var(--bg);border-radius:8px;transition:all .2s;border:1px solid transparent}
|
||||||
|
.store-model:hover{border-color:var(--ac)}
|
||||||
|
.store-model-head{display:flex;justify-content:space-between;align-items:center;margin-bottom:.5rem}
|
||||||
|
.store-model-name{color:var(--t);font-weight:600;font-size:.95rem}
|
||||||
|
.store-model-free{background:#22c55e;color:#fff;padding:2px 8px;border-radius:4px;font-size:.7rem;font-weight:600}
|
||||||
|
.store-model-meta{display:flex;gap:1rem;font-size:.8rem;color:var(--tx);font-family:var(--mono)}
|
||||||
|
.store-model-desc{color:var(--tx);font-size:.85rem;margin-top:.5rem;line-height:1.4}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// ═══ LIVE API INTEGRATION ═══
|
||||||
|
(function(){
|
||||||
|
var API='/api';
|
||||||
|
|
||||||
|
// Load Organ Registry from live engine
|
||||||
|
function loadOrganModels(){
|
||||||
|
fetch(API+'/models').then(function(r){return r.json()}).then(function(d){
|
||||||
|
var c=document.getElementById('organ-models');
|
||||||
|
if(!c)return;
|
||||||
|
var models=d.models||d||[];
|
||||||
|
if(!Array.isArray(models)){c.innerHTML='<div class="organ-loading">Data format unexpected</div>';return}
|
||||||
|
c.innerHTML='';
|
||||||
|
models.forEach(function(m){
|
||||||
|
var div=document.createElement('div');
|
||||||
|
div.className='organ-model-item';
|
||||||
|
div.innerHTML='<span class="organ-model-name">'+(m.name||m.file||'Unknown')+'</span>'+
|
||||||
|
'<span class="organ-model-size">'+(m.size||m.params||'')+'</span>'+
|
||||||
|
'<span class="organ-model-badge">'+(m.quant||m.status||'ready')+'</span>';
|
||||||
|
c.appendChild(div);
|
||||||
|
});
|
||||||
|
var cnt=document.getElementById('organ-scan-count');
|
||||||
|
if(cnt)cnt.textContent=models.length+' models scannable';
|
||||||
|
}).catch(function(){
|
||||||
|
var c=document.getElementById('organ-models');
|
||||||
|
if(c)c.innerHTML='<div class="organ-loading">Engine offline — models load on connection</div>';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load Forge Registry
|
||||||
|
function loadForgeRegistry(){
|
||||||
|
fetch(API+'/models').then(function(r){return r.json()}).then(function(d){
|
||||||
|
var c=document.getElementById('forge-registry');
|
||||||
|
if(!c)return;
|
||||||
|
var models=d.models||d||[];
|
||||||
|
if(!Array.isArray(models))return;
|
||||||
|
c.innerHTML='';
|
||||||
|
models.forEach(function(m){
|
||||||
|
var div=document.createElement('div');
|
||||||
|
div.className='forge-item';
|
||||||
|
var name=(m.name||'Unknown').replace(/-Q\d.*/,'');
|
||||||
|
var quant=m.quant||((m.name||'').match(/Q\d[^\s.]*/)||['?'])[0];
|
||||||
|
var size=m.size||'?';
|
||||||
|
div.innerHTML='<span class="forge-item-name">'+name+'</span>'+
|
||||||
|
'<span class="forge-item-quant">'+quant+'</span>'+
|
||||||
|
'<span class="forge-item-size">'+size+'</span>';
|
||||||
|
c.appendChild(div);
|
||||||
|
});
|
||||||
|
}).catch(function(){
|
||||||
|
var c=document.getElementById('forge-registry');
|
||||||
|
if(c)c.innerHTML='<div class="organ-loading">Registry offline</div>';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load Store Catalog
|
||||||
|
function loadStoreCatalog(){
|
||||||
|
fetch(API+'/models').then(function(r){return r.json()}).then(function(d){
|
||||||
|
var c=document.getElementById('store-models');
|
||||||
|
if(!c)return;
|
||||||
|
var models=d.models||d||[];
|
||||||
|
if(!Array.isArray(models))return;
|
||||||
|
c.innerHTML='';
|
||||||
|
models.forEach(function(m){
|
||||||
|
var div=document.createElement('div');
|
||||||
|
div.className='store-model';
|
||||||
|
div.innerHTML='<div class="store-model-head">'+
|
||||||
|
'<span class="store-model-name">'+(m.name||'Unknown')+'</span>'+
|
||||||
|
(m.free!==false?'<span class="store-model-free">FREE</span>':'')+
|
||||||
|
'</div>'+
|
||||||
|
'<div class="store-model-meta">'+
|
||||||
|
'<span>'+(m.params||m.size||'')+'</span>'+
|
||||||
|
'<span>'+(m.quant||'')+'</span>'+
|
||||||
|
'<span>'+(m.category||'general')+'</span>'+
|
||||||
|
'</div>'+
|
||||||
|
'<div class="store-model-desc">'+(m.description||'')+'</div>';
|
||||||
|
c.appendChild(div);
|
||||||
|
});
|
||||||
|
}).catch(function(){
|
||||||
|
var c=document.getElementById('store-models');
|
||||||
|
if(c)c.innerHTML='<div class="organ-loading">Store offline</div>';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load everything
|
||||||
|
setTimeout(function(){loadOrganModels();loadForgeRegistry();loadStoreCatalog()},500);
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
Loading…
Reference in New Issue
Block a user