a81a450e7e
Merged into tinqs/studio: - cmd/tinqs-cli/ — tinqs-cli (Go binary, from bot/cli) - cmd/tea/ — Gitea CLI tool (from tinqs/cli-tea) - services/bot/ — Bot service (from tinqs-ltd/bot on git.arikigame.com) - services/admin/ — Admin panel (from tinqs/admin) - services/team-tool/ — Team Tool (from tinqs/team-tool) - services/proxy/ — tinqs-proxy (from bot/proxy) - web/landing/ — tinqs.com website (from tinqs/website) - web/docs/ — Platform docs (from tinqs/docs) - web/blog/ — Blog (placeholder) - runner/ — Ephemeral CI runner (from tinqs/runner) All source repos will be deleted after verification.
266 lines
10 KiB
JavaScript
266 lines
10 KiB
JavaScript
#!/usr/bin/env node
|
|
// deeptinqs agent-server — HTTP API for Team Tool to read/manage agent sessions
|
|
// Runs on localhost:3138 (next to Team Tool on 3137)
|
|
//
|
|
// Team Tool calls these endpoints to show agents in its UI.
|
|
// Sessions are read from ~/.deeptinqs/sessions/
|
|
//
|
|
// Usage: node agent-server.js
|
|
// or: deeptinqs --serve
|
|
|
|
const http = require('http');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const { execSync, spawn } = require('child_process');
|
|
|
|
const PORT = 3138;
|
|
const HOME = process.env.HOME || process.env.USERPROFILE;
|
|
const SESSIONS_DIR = path.join(HOME, '.deeptinqs', 'sessions');
|
|
const CONFIG_DIR = path.join(HOME, '.deeptinqs');
|
|
|
|
// Ensure dirs
|
|
if (!fs.existsSync(SESSIONS_DIR)) fs.mkdirSync(SESSIONS_DIR, { recursive: true });
|
|
|
|
// ─── Helpers ─────────────────────────────────────────────
|
|
|
|
function readSessions() {
|
|
const sessions = [];
|
|
if (!fs.existsSync(SESSIONS_DIR)) return sessions;
|
|
|
|
for (const entry of fs.readdirSync(SESSIONS_DIR)) {
|
|
if (!entry.startsWith('sess_')) continue;
|
|
const metaPath = path.join(SESSIONS_DIR, entry, 'meta.json');
|
|
if (!fs.existsSync(metaPath)) continue;
|
|
|
|
try {
|
|
const meta = JSON.parse(fs.readFileSync(metaPath, 'utf8'));
|
|
|
|
// Check if process is alive
|
|
if (meta.status === 'active' && meta.pid) {
|
|
try {
|
|
process.kill(parseInt(meta.pid), 0);
|
|
} catch {
|
|
meta.status = 'crashed';
|
|
fs.writeFileSync(metaPath, JSON.stringify(meta, null, 4));
|
|
}
|
|
}
|
|
|
|
sessions.push(meta);
|
|
} catch (e) {
|
|
// skip corrupt sessions
|
|
}
|
|
}
|
|
|
|
// Sort by last_active desc
|
|
sessions.sort((a, b) => (b.last_active || '').localeCompare(a.last_active || ''));
|
|
return sessions;
|
|
}
|
|
|
|
function readThread(sessId) {
|
|
const threadPath = path.join(SESSIONS_DIR, sessId, 'thread.jsonl');
|
|
if (!fs.existsSync(threadPath)) return [];
|
|
|
|
return fs.readFileSync(threadPath, 'utf8')
|
|
.split('\n')
|
|
.filter(line => line.trim())
|
|
.map(line => { try { return JSON.parse(line); } catch { return null; } })
|
|
.filter(Boolean);
|
|
}
|
|
|
|
function getHandoffs() {
|
|
// Sessions that are sleeping or crashed — candidates for "continue?"
|
|
return readSessions().filter(s =>
|
|
s.status === 'sleeping' || s.status === 'crashed'
|
|
);
|
|
}
|
|
|
|
// ─── HTTP Server ─────────────────────────────────────────
|
|
|
|
const server = http.createServer((req, res) => {
|
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
|
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
|
|
if (req.method === 'OPTIONS') {
|
|
res.writeHead(200);
|
|
res.end();
|
|
return;
|
|
}
|
|
|
|
const url = new URL(req.url, `http://localhost:${PORT}`);
|
|
const pathname = url.pathname;
|
|
|
|
// ─── Routes ──────────────────────────────────────────
|
|
|
|
// GET /health
|
|
if (pathname === '/health' && req.method === 'GET') {
|
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify({
|
|
status: 'ok',
|
|
version: '0.1.0',
|
|
sessions: readSessions().length,
|
|
handoffs: getHandoffs().length
|
|
}));
|
|
return;
|
|
}
|
|
|
|
// GET /agents — list all sessions
|
|
if (pathname === '/agents' && req.method === 'GET') {
|
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify({ agents: readSessions() }));
|
|
return;
|
|
}
|
|
|
|
// GET /agents/handoffs — sessions needing attention
|
|
if (pathname === '/agents/handoffs' && req.method === 'GET') {
|
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify({ handoffs: getHandoffs() }));
|
|
return;
|
|
}
|
|
|
|
// GET /agents/:id — single session detail
|
|
const agentMatch = pathname.match(/^\/agents\/(sess_[^/]+)$/);
|
|
if (agentMatch && req.method === 'GET') {
|
|
const sessId = agentMatch[1];
|
|
const metaPath = path.join(SESSIONS_DIR, sessId, 'meta.json');
|
|
if (!fs.existsSync(metaPath)) {
|
|
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify({ error: 'Session not found' }));
|
|
return;
|
|
}
|
|
const meta = JSON.parse(fs.readFileSync(metaPath, 'utf8'));
|
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify({ agent: meta }));
|
|
return;
|
|
}
|
|
|
|
// GET /agents/:id/thread — conversation thread
|
|
const threadMatch = pathname.match(/^\/agents\/(sess_[^/]+)\/thread$/);
|
|
if (threadMatch && req.method === 'GET') {
|
|
const sessId = threadMatch[1];
|
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify({ thread: readThread(sessId) }));
|
|
return;
|
|
}
|
|
|
|
// POST /agents/:id/open — open agent in a new terminal window
|
|
const openMatch = pathname.match(/^\/agents\/(sess_[^/]+)\/open$/);
|
|
if (openMatch && req.method === 'POST') {
|
|
const sessId = openMatch[1];
|
|
const metaPath = path.join(SESSIONS_DIR, sessId, 'meta.json');
|
|
if (!fs.existsSync(metaPath)) {
|
|
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify({ error: 'Session not found' }));
|
|
return;
|
|
}
|
|
|
|
const meta = JSON.parse(fs.readFileSync(metaPath, 'utf8'));
|
|
const deeptinqsPath = path.join(__dirname, 'deeptinqs').replace(/\\/g, '/');
|
|
const repoPath = (meta.repo || '').replace(/\\/g, '/');
|
|
|
|
const isWindows = process.platform === 'win32';
|
|
if (isWindows) {
|
|
// Open new Windows Terminal tab with deeptinqs --resume
|
|
spawn('cmd', ['/c', 'start', 'wt', '-d', repoPath, 'bash', deeptinqsPath, '--resume', sessId], {
|
|
detached: true, stdio: 'ignore'
|
|
}).unref();
|
|
} else {
|
|
// macOS — open new Terminal.app window
|
|
const script = `tell application "Terminal" to do script "cd ${repoPath} && ${deeptinqsPath} --resume ${sessId}"`;
|
|
spawn('osascript', ['-e', script], {
|
|
detached: true, stdio: 'ignore'
|
|
}).unref();
|
|
}
|
|
|
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify({ opened: true, session: sessId, terminal: isWindows ? 'wt' : 'Terminal.app' }));
|
|
return;
|
|
}
|
|
|
|
// POST /agents — start new agent in a new terminal window
|
|
if (pathname === '/agents' && req.method === 'POST') {
|
|
let body = '';
|
|
req.on('data', chunk => body += chunk);
|
|
req.on('end', () => {
|
|
try {
|
|
const { repo, mode } = JSON.parse(body);
|
|
const repoPath = (repo || HOME + '/tinqs-ltd/isleborn').replace('~', HOME).replace(/\\/g, '/');
|
|
const deeptinqsPath = path.join(__dirname, 'deeptinqs').replace(/\\/g, '/');
|
|
|
|
const args = [deeptinqsPath];
|
|
if (mode === 'flash') args.push('--flash');
|
|
if (mode === 'singularity') args.push('--singularity');
|
|
else args.push(repoPath);
|
|
|
|
const isWindows = process.platform === 'win32';
|
|
if (isWindows) {
|
|
// Open new Windows Terminal tab
|
|
spawn('cmd', ['/c', 'start', 'wt', '-d', repoPath, 'bash', ...args], {
|
|
detached: true, stdio: 'ignore'
|
|
}).unref();
|
|
} else {
|
|
const script = `tell application "Terminal" to do script "cd ${repoPath} && bash ${args.join(' ')}"`;
|
|
spawn('osascript', ['-e', script], {
|
|
detached: true, stdio: 'ignore'
|
|
}).unref();
|
|
}
|
|
|
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify({ started: true, terminal: isWindows ? 'wt' : 'Terminal.app' }));
|
|
} catch (e) {
|
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify({ error: e.message }));
|
|
}
|
|
});
|
|
return;
|
|
}
|
|
|
|
// POST /agents/sleep — sleep all
|
|
if (pathname === '/agents/sleep' && req.method === 'POST') {
|
|
try {
|
|
const output = execSync('bash ' + path.join(__dirname, 'deeptinqs') + ' --sleep', {
|
|
encoding: 'utf8', timeout: 120000
|
|
});
|
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify({ ok: true, output }));
|
|
} catch (e) {
|
|
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify({ error: e.message }));
|
|
}
|
|
return;
|
|
}
|
|
|
|
// POST /agents/wake — wake all
|
|
if (pathname === '/agents/wake' && req.method === 'POST') {
|
|
try {
|
|
const output = execSync('bash ' + path.join(__dirname, 'deeptinqs') + ' --wake', {
|
|
encoding: 'utf8', timeout: 120000
|
|
});
|
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify({ ok: true, output }));
|
|
} catch (e) {
|
|
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify({ error: e.message }));
|
|
}
|
|
return;
|
|
}
|
|
|
|
// 404
|
|
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify({ error: 'Not found' }));
|
|
});
|
|
|
|
server.listen(PORT, '127.0.0.1', () => {
|
|
console.log(`deeptinqs agent-server v0.1.0 on http://127.0.0.1:${PORT}`);
|
|
console.log(`Sessions dir: ${SESSIONS_DIR}`);
|
|
console.log(`Endpoints:`);
|
|
console.log(` GET /health — status`);
|
|
console.log(` GET /agents — list all`);
|
|
console.log(` GET /agents/handoffs — sleeping/crashed (continue?)`);
|
|
console.log(` GET /agents/:id — session detail`);
|
|
console.log(` GET /agents/:id/thread — conversation`);
|
|
console.log(` POST /agents — start new {repo, mode, prompt}`);
|
|
console.log(` POST /agents/sleep — sleep all`);
|
|
console.log(` POST /agents/wake — wake all`);
|
|
});
|