lvwerra's picture
lvwerra HF Staff
settings polish: version self-heal, secrets autosave, de-carded rows, full usage table, skills meta + page scroll, mobile fixes
37775da verified
Raw
History Blame Contribute Delete
7.21 kB
import os from 'node:os';
import path from 'node:path';
import fs from 'node:fs';
import { execSync, execFile } from 'node:child_process';
export const PORT = parseInt(process.env.PORT || '7860', 10);
export const DATA_DIR = process.env.DATA_DIR || path.resolve(process.cwd(), 'data');
export const WORKSPACES_DIR = path.join(DATA_DIR, 'workspaces');
export const STATE_DIR = path.join(DATA_DIR, 'state');
// Shared skills live in the workspace so agents can reach them (../skills).
export const SKILLS_DIR = path.join(WORKSPACES_DIR, 'skills');
export const SESSIONS_FILE = path.join(DATA_DIR, 'sessions.json');
export const PUBLIC_DIR = process.env.PUBLIC_DIR || path.resolve(process.cwd(), '..', 'web', 'dist');
function commandExists(cmd) {
try {
execSync(`command -v ${cmd}`, { stdio: 'ignore', shell: '/bin/sh' });
return true;
} catch {
return false;
}
}
// Installed CLI versions, resolved asynchronously (a few `--version` calls) and
// cached so /api/clis stays fast. Slow starters (hermes = python venv, gemini)
// can miss the boot-time pass — cliCatalog re-runs it for the gaps, throttled.
const versionCache = new Map();
let versionsAttemptedAt = 0;
export function refreshVersions() {
versionsAttemptedAt = Date.now();
for (const c of CLIS) {
if (!c.bin || versionCache.has(c.id) || !commandExists(c.bin)) continue;
execFile(c.bin, ['--version'], { timeout: 30000 }, (err, stdout = '') => {
if (err) return;
const s = String(stdout);
const m = s.match(/\d+\.\d+\.\d+[\w.-]*/);
versionCache.set(c.id, m ? m[0] : s.trim().split('\n')[0].slice(0, 40));
});
}
}
export const TMUX_AVAILABLE = commandExists('tmux');
export const USE_TMUX = process.env.USE_TMUX
? process.env.USE_TMUX === '1'
: TMUX_AVAILABLE;
/**
* CLI catalog.
* - `bin` : binary checked on PATH to report availability
* - `run` : command run inside the session shell on first launch
* - `cont` : command used to resume a prior conversation (null = no resume)
*/
// One setup story for every agent; only the accepted secret names differ.
const setupHint = (...keys) =>
`Launch it once — the first run walks you through sign-in. Or add ${keys.join(' / ')} as a Space secret.`;
export const CLIS = [
{ id: 'shell', label: 'Shell', bin: 'bash', color: '#8aa0ad', run: 'exec bash -il', cont: null },
{ id: 'files', label: 'Files', bin: null, color: '#d99a2b', run: null, cont: null },
{ id: 'claude', label: 'Claude Code', bin: 'claude', color: '#d97757', run: 'claude', cont: 'claude --continue',
setup: setupHint('ANTHROPIC_API_KEY') },
{ id: 'codex', label: 'Codex', bin: 'codex', color: '#5eb6a6', run: 'codex', cont: 'codex resume --last',
setup: setupHint('OPENAI_API_KEY') },
{ id: 'gemini', label: 'Gemini CLI', bin: 'gemini', color: '#4796e3', run: 'gemini', cont: null,
setup: setupHint('GEMINI_API_KEY') },
{ id: 'opencode', label: 'opencode', bin: 'opencode', color: '#8a93a0', run: 'opencode', cont: 'opencode --continue',
setup: setupHint('ANTHROPIC_API_KEY', 'OPENAI_API_KEY', 'OPENROUTER_API_KEY') },
{ id: 'hermes', label: 'Hermes', bin: 'hermes', color: '#a78bfa', run: 'hermes', cont: 'hermes -c',
setup: setupHint('HF_TOKEN', 'ANTHROPIC_API_KEY', 'OPENROUTER_API_KEY', 'NOUS_API_KEY') },
// `chat` = TUI in --local mode: embedded agent, no gateway/daemon needed.
{ id: 'openclaw', label: 'OpenClaw', bin: 'openclaw', color: '#c83636', run: 'openclaw chat', cont: null,
setup: setupHint('ANTHROPIC_API_KEY') },
];
export function cliById(id) {
return CLIS.find((c) => c.id === id) || null;
}
// Is an agent configured (has a credential)? Checks env keys OR an on-disk
// credential file. Detects "configured", not "token still valid".
function isConfigured(id) {
const env = process.env;
const home = env.HOME || os.homedir();
const hasEnv = (...keys) => keys.some((k) => env[k] && env[k].trim());
const fileOk = (p) => { try { return fs.existsSync(p) && fs.statSync(p).size > 0; } catch { return false; } };
switch (id) {
case 'claude':
return hasEnv('ANTHROPIC_API_KEY', 'CLAUDE_CODE_OAUTH_TOKEN')
|| fileOk(path.join(env.CLAUDE_CONFIG_DIR || path.join(home, '.claude'), '.credentials.json'));
case 'codex':
return hasEnv('OPENAI_API_KEY', 'CODEX_API_KEY')
|| fileOk(path.join(env.CODEX_HOME || path.join(home, '.codex'), 'auth.json'));
case 'gemini':
return hasEnv('GEMINI_API_KEY', 'GOOGLE_API_KEY')
|| fileOk(path.join(home, '.gemini', 'oauth_creds.json'));
case 'opencode': {
const xdgConfig = env.XDG_CONFIG_HOME || path.join(home, '.config');
return hasEnv('ANTHROPIC_API_KEY', 'OPENAI_API_KEY', 'OPENROUTER_API_KEY')
|| fileOk(path.join(home, '.local', 'share', 'opencode', 'auth.json')) // pre-1.x
|| fileOk(path.join(xdgConfig, 'opencode', 'opencode.jsonc')) // 1.x user config
|| fileOk(path.join(xdgConfig, 'opencode', 'opencode.json'));
}
case 'hermes':
// HF_TOKEN: hermes has a huggingface provider (HF router models).
return hasEnv('ANTHROPIC_API_KEY', 'OPENAI_API_KEY', 'OPENROUTER_API_KEY', 'NOUS_API_KEY', 'HF_TOKEN')
|| fileOk(path.join(home, '.hermes', '.env'))
|| fileOk(path.join(home, '.hermes', 'auth.json'));
case 'openclaw':
return hasEnv('ANTHROPIC_API_KEY', 'OPENAI_API_KEY', 'OPENROUTER_API_KEY')
|| fileOk(path.join(env.OPENCLAW_STATE_DIR || path.join(home, '.openclaw'), 'openclaw.json'));
default:
return true; // shell / files need no auth
}
}
/** CLI catalog enriched with runtime availability + configured (credential present). */
export function cliCatalog() {
// Fill version gaps left by the boot-time pass (results land for the next poll).
if (Date.now() - versionsAttemptedAt > 60_000
&& CLIS.some((c) => c.bin && !versionCache.has(c.id) && commandExists(c.bin))) {
refreshVersions();
}
return CLIS.map((c) => ({
id: c.id,
label: c.label,
color: c.color,
available: c.bin ? commandExists(c.bin) : true, // no binary (shell/files) = always available
ready: (c.bin ? commandExists(c.bin) : true) && isConfigured(c.id),
version: versionCache.get(c.id) || null,
setup: c.setup || null,
}));
}
export function ensureDirs() {
for (const dir of [DATA_DIR, WORKSPACES_DIR, STATE_DIR, SKILLS_DIR]) {
fs.mkdirSync(dir, { recursive: true });
}
}
export function slugify(s) {
return (s || '').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 40);
}
// A fresh workspace folder named after `base` (suffixed if taken). Used as the
// default location when an agent is created without an explicit path.
export function allocFolder(base) {
const b = slugify(base) || 'workspace';
let name = b;
let n = 1;
while (fs.existsSync(path.join(WORKSPACES_DIR, name))) { n++; name = `${b}-${n}`; }
fs.mkdirSync(path.join(WORKSPACES_DIR, name), { recursive: true });
return name;
}
export function workspacePath(folder) {
return path.join(WORKSPACES_DIR, folder);
}