Files
ozan a81a450e7e feat: monorepo consolidation — merge CLI, bot, admin, team-tool, website, docs, runner, proxy
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.
2026-05-22 04:55:50 +00:00

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`);
});