feat: Add multi-host config and dynamic project scanning

Backend:
- Load hosts from config/hosts.json
- New /api/hosts endpoint listing available hosts
- Dynamic project scanning with configurable depth
- Support for local and SSH hosts (SSH execution coming next)

Frontend (by Web-UI Claude):
- Slash commands: /clear, /help, /export, /scroll, /new, /info
- Chat export as Markdown

Config:
- hosts.json defines hosts with connection info and base paths
- hosts.example.json as template (real config is gitignored)
- Each host has name, description, color, icon, basePaths

Next steps:
- SSH command execution for remote hosts
- Frontend host selector UI
- Multi-agent collaboration

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-15 22:11:22 +01:00
parent 52792268fa
commit 38ab89932a
7 changed files with 354 additions and 21 deletions

View File

@@ -5,7 +5,7 @@ import { spawn } from 'child_process';
import { v4 as uuidv4 } from 'uuid';
import cors from 'cors';
import { existsSync, readFileSync, readdirSync, statSync } from 'fs';
import { join } from 'path';
import { join, basename } from 'path';
const app = express();
app.use(cors());
@@ -14,27 +14,114 @@ app.use(express.json());
const PORT = process.env.PORT || 3001;
const HOST = process.env.HOST || '0.0.0.0';
// Load hosts configuration
const CONFIG_PATH = process.env.CONFIG_PATH || '/app/config/hosts.json';
let hostsConfig = { hosts: {}, defaults: { scanSubdirs: true, maxDepth: 1 } };
function loadConfig() {
try {
if (existsSync(CONFIG_PATH)) {
hostsConfig = JSON.parse(readFileSync(CONFIG_PATH, 'utf-8'));
console.log('Loaded hosts config:', Object.keys(hostsConfig.hosts));
} else {
console.log('No hosts.json found, using defaults');
}
} catch (err) {
console.error('Error loading config:', err);
}
}
loadConfig();
// Store active Claude sessions
const sessions = new Map();
const server = createServer(app);
const wss = new WebSocketServer({ server });
// REST endpoint to list available projects
// Scan directory for projects
function scanProjects(basePath, depth = 0, maxDepth = 1) {
const projects = [];
if (!existsSync(basePath)) return projects;
try {
const entries = readdirSync(basePath, { withFileTypes: true });
for (const entry of entries) {
if (entry.isDirectory() && !entry.name.startsWith('.')) {
const fullPath = join(basePath, entry.name);
projects.push({
path: fullPath,
name: entry.name,
type: 'directory'
});
// Recurse if not at max depth
if (depth < maxDepth - 1) {
projects.push(...scanProjects(fullPath, depth + 1, maxDepth));
}
}
}
} catch (err) {
console.error(`Error scanning ${basePath}:`, err.message);
}
return projects;
}
// REST endpoint to list hosts
app.get('/api/hosts', (req, res) => {
const hosts = Object.entries(hostsConfig.hosts).map(([id, host]) => ({
id,
name: host.name,
description: host.description,
color: host.color,
icon: host.icon,
connectionType: host.connection.type,
isLocal: host.connection.type === 'local'
}));
res.json({ hosts, defaultHost: hostsConfig.defaults?.host || 'neko' });
});
// REST endpoint to list projects for a host
app.get('/api/projects', (req, res) => {
const baseDirs = [
{ path: '/projects', name: 'projects', description: 'Development projects' },
{ path: '/docker', name: 'docker', description: 'Docker configurations' },
{ path: '/stacks', name: 'stacks', description: 'Production stacks' }
];
const hostId = req.query.host || hostsConfig.defaults?.host || 'neko';
const host = hostsConfig.hosts[hostId];
if (!host) {
return res.status(404).json({ error: `Host '${hostId}' not found` });
}
// Only local hosts for now
if (host.connection.type !== 'local') {
return res.json({
projects: [],
host: hostId,
message: 'SSH hosts not yet supported for project listing'
});
}
const projects = [];
for (const dir of baseDirs) {
if (existsSync(dir.path)) {
projects.push({ ...dir, type: 'directory' });
const scanSubdirs = hostsConfig.defaults?.scanSubdirs ?? true;
const maxDepth = hostsConfig.defaults?.maxDepth ?? 1;
for (const basePath of host.basePaths) {
// Add base path itself
if (existsSync(basePath)) {
projects.push({
path: basePath,
name: basename(basePath),
type: 'base',
isBase: true
});
// Scan subdirectories if enabled
if (scanSubdirs) {
projects.push(...scanProjects(basePath, 0, maxDepth));
}
}
}
res.json(projects);
res.json({ projects, host: hostId, hostInfo: { name: host.name, color: host.color } });
});
// Health check