chore: remove private infrastructure references, clean structure
This commit is contained in:
parent
492ffad297
commit
6fe5c9beb1
14
LICENSE
14
LICENSE
@ -1,14 +0,0 @@
|
|||||||
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/
|
|
||||||
62
README.md
62
README.md
@ -1,49 +1,37 @@
|
|||||||
# IX-Tools
|
# IX-Tools — Community Tools for Inference-X
|
||||||
|
|
||||||
Public tools and utilities for the [Inference-X](https://inference-x.com) ecosystem.
|
Public tools, governance documents, and utilities for the [Inference-X](https://inference-x.com) ecosystem.
|
||||||
|
|
||||||
## What's Inside
|
> **Inference-X** is the universal AI inference engine — 19 hardware backends, 228KB binary, runs anywhere. Community-owned, creator-protected.
|
||||||
|
|
||||||
### Invoke API Client
|
## Repository Structure
|
||||||
The nervous system of OASIS. Bridges Claude, Echo, and the InferenceX engine.
|
|
||||||
|
|
||||||
**Endpoints:**
|
| Directory | Content |
|
||||||
- **Core**: `/invoke/exec`, `/invoke/health`, `/invoke/status`, `/invoke/manifest`
|
|-----------|---------|
|
||||||
- **Echo Bridge**: `/invoke/echo/chat`, `/invoke/echo/query`, `/invoke/echo/execute`, `/invoke/echo/status`
|
| `docs/` | Architecture, manifesto, provider framework, revenue model |
|
||||||
- **Engine**: `/invoke/engine/infer`, `/invoke/engine/models`
|
| `governance/` | Community governance and contribution levels |
|
||||||
- **Forge**: `/invoke/forge/analyze`, `/invoke/forge/registry`
|
| `license/` | SALKA-IX License v1.0 |
|
||||||
- **Organ**: `/invoke/organ/scan`, `/invoke/organ/compatibility`
|
| `watermark/` | Attribution standard for all IX ecosystem projects |
|
||||||
- **Store**: `/invoke/store/catalog`, `/invoke/store/marketplace`
|
|
||||||
- **Tools**: `/invoke/screenshot`, `/invoke/audit`
|
|
||||||
|
|
||||||
### Model Forge
|
## Quick Start
|
||||||
Build custom AI models from components. Select base models, configure quantization, deploy with adaptive precision.
|
|
||||||
|
|
||||||
### Organ Architecture
|
```bash
|
||||||
Neural network surgery — extract, measure, and transplant components between AI models. Like organ transplants for neural networks.
|
# Clone the engine
|
||||||
|
git clone https://git.inference-x.com/elmadani/inference-x.git
|
||||||
### Echo Integration
|
|
||||||
Connect to the Echo consciousness system for intelligent query routing and VPS execution.
|
|
||||||
|
|
||||||
## Architecture
|
|
||||||
|
|
||||||
|
# Clone community tools
|
||||||
|
git clone https://git.inference-x.com/elmadani/ix-tools.git
|
||||||
```
|
```
|
||||||
Claude (Opus/Code) → Invoke (Gateway) → Echo (Brain) → Engine (Inference)
|
|
||||||
→ VPS (Execution)
|
## Community
|
||||||
→ Gitea (Storage)
|
|
||||||
```
|
- Website: [inference-x.com](https://inference-x.com)
|
||||||
|
- Git: [git.inference-x.com](https://git.inference-x.com)
|
||||||
|
- Build: [build.inference-x.com](https://build.inference-x.com)
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
BSL-1.1 — Free for individuals, researchers, and businesses under $1M revenue.
|
SALKA-IX License v1.0 — Free for the IX ecosystem. Commercial use requires a license.
|
||||||
Converts to Apache 2.0 on January 1, 2030.
|
See [license/SALKA-IX-LICENSE.md](license/SALKA-IX-LICENSE.md)
|
||||||
|
|
||||||
## Links
|
**Creator:** Elmadani Salka · All rights reserved
|
||||||
|
|
||||||
- [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
82
docs/API.md
@ -1,82 +0,0 @@
|
|||||||
# 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.
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
# Invoke Configuration
|
|
||||||
INVOKE_API_KEY=your-secret-key-here
|
|
||||||
INVOKE_PORT=3001
|
|
||||||
@ -1,15 +0,0 @@
|
|||||||
{
|
|
||||||
"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>"
|
|
||||||
}
|
|
||||||
778
invoke/server.js
778
invoke/server.js
@ -1,778 +0,0 @@
|
|||||||
#!/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('<I',f.read(4))[0]; print(f'magic={hex(magic)} size={os.path.getsize(\"${modelPath}\")}')" 2>&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 });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -1,265 +0,0 @@
|
|||||||
<!-- ═══ 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