Files

3875 lines
151 KiB
JavaScript

#!/usr/bin/env node
var __getOwnPropNames = Object.getOwnPropertyNames;
var __commonJS = (cb, mod) => function __require() {
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
};
// src/utils.js
var require_utils = __commonJS({
"src/utils.js"(exports2, module2) {
var { exec } = require("child_process");
var path2 = require("path");
var { promisify } = require("util");
var execAsync = promisify(exec);
var pkg = require(path2.join(__dirname, "..", "package.json"));
function getVersion2() {
return pkg.version;
}
async function runCmd(cmd, options = {}) {
const systemPath = "/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin";
const envPath = process.env.PATH || "";
const opts = {
encoding: "utf8",
timeout: 1e4,
env: {
...process.env,
PATH: envPath.includes("/usr/sbin") ? envPath : `${systemPath}:${envPath}`
},
...options
};
try {
const { stdout } = await execAsync(cmd, opts);
return stdout.trim();
} catch (e) {
if (options.fallback !== void 0) return options.fallback;
throw e;
}
}
function formatBytes(bytes) {
if (bytes >= 1099511627776) return (bytes / 1099511627776).toFixed(1) + " TB";
if (bytes >= 1073741824) return (bytes / 1073741824).toFixed(1) + " GB";
if (bytes >= 1048576) return (bytes / 1048576).toFixed(1) + " MB";
if (bytes >= 1024) return (bytes / 1024).toFixed(1) + " KB";
return bytes + " B";
}
function formatTimeAgo(date) {
const now = /* @__PURE__ */ new Date();
const diffMs = now - date;
const diffMins = Math.round(diffMs / 6e4);
if (diffMins < 1) return "just now";
if (diffMins < 60) return `${diffMins}m ago`;
if (diffMins < 1440) return `${Math.round(diffMins / 60)}h ago`;
return `${Math.round(diffMins / 1440)}d ago`;
}
function formatNumber(n) {
return n.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
}
function formatTokens(n) {
if (n >= 1e6) return (n / 1e6).toFixed(1) + "M";
if (n >= 1e3) return (n / 1e3).toFixed(1) + "k";
return n.toString();
}
module2.exports = {
getVersion: getVersion2,
runCmd,
formatBytes,
formatTimeAgo,
formatNumber,
formatTokens
};
}
});
// src/config.js
var require_config = __commonJS({
"src/config.js"(exports2, module2) {
var fs2 = require("fs");
var path2 = require("path");
var os = require("os");
var HOME = os.homedir();
function getOpenClawDir2(profile = null) {
const effectiveProfile = profile || process.env.OPENCLAW_PROFILE || "";
return effectiveProfile ? path2.join(HOME, `.openclaw-${effectiveProfile}`) : path2.join(HOME, ".openclaw");
}
function detectWorkspace() {
const profile = process.env.OPENCLAW_PROFILE || "";
const openclawDir = getOpenClawDir2();
const defaultWorkspace = path2.join(openclawDir, "workspace");
const profileCandidates = profile ? [
// Profile-specific workspace in home (e.g., ~/.openclaw-<profile>-workspace)
path2.join(HOME, `.openclaw-${profile}-workspace`),
path2.join(HOME, `.${profile}-workspace`)
] : [];
const candidates = [
// Environment variable (highest priority)
process.env.OPENCLAW_WORKSPACE,
// OpenClaw's default workspace location
process.env.OPENCLAW_HOME,
// Gateway config workspace (check early - this is where OpenClaw actually runs)
getWorkspaceFromGatewayConfig(),
// Profile-specific paths (if profile is set)
...profileCandidates,
// Standard OpenClaw workspace location (profile-aware: ~/.openclaw/workspace or ~/.openclaw-<profile>/workspace)
defaultWorkspace,
// Common custom workspace names
path2.join(HOME, "openclaw-workspace"),
path2.join(HOME, ".openclaw-workspace"),
// Legacy/custom names
path2.join(HOME, "molty"),
path2.join(HOME, "clawd"),
path2.join(HOME, "moltbot")
].filter(Boolean);
const foundWorkspace = candidates.find((candidate) => {
if (!candidate || !fs2.existsSync(candidate)) {
return false;
}
const hasMemory = fs2.existsSync(path2.join(candidate, "memory"));
const hasState = fs2.existsSync(path2.join(candidate, "state"));
const hasConfig = fs2.existsSync(path2.join(candidate, ".openclaw"));
return hasMemory || hasState || hasConfig;
});
return foundWorkspace || defaultWorkspace;
}
function getWorkspaceFromGatewayConfig() {
const openclawDir = getOpenClawDir2();
const configPaths = [
path2.join(openclawDir, "config.yaml"),
path2.join(openclawDir, "config.json"),
path2.join(openclawDir, "openclaw.json"),
path2.join(openclawDir, "clawdbot.json"),
// Fallback to standard XDG location
path2.join(HOME, ".config", "openclaw", "config.yaml")
];
for (const configPath of configPaths) {
try {
if (fs2.existsSync(configPath)) {
const content = fs2.readFileSync(configPath, "utf8");
const match = content.match(/workspace[:\s]+["']?([^"'\n]+)/i) || content.match(/workdir[:\s]+["']?([^"'\n]+)/i);
if (match && match[1]) {
const workspace = match[1].trim().replace(/^~/, HOME);
if (fs2.existsSync(workspace)) {
return workspace;
}
}
}
} catch (e) {
}
}
return null;
}
function deepMerge(base, override) {
const result = { ...base };
for (const key of Object.keys(override)) {
if (override[key] && typeof override[key] === "object" && !Array.isArray(override[key]) && base[key] && typeof base[key] === "object") {
result[key] = deepMerge(base[key], override[key]);
} else if (override[key] !== null && override[key] !== void 0) {
result[key] = override[key];
}
}
return result;
}
function loadConfigFile() {
const basePath = path2.join(__dirname, "..", "config", "dashboard.json");
const localPath = path2.join(__dirname, "..", "config", "dashboard.local.json");
let config = {};
try {
if (fs2.existsSync(basePath)) {
const content = fs2.readFileSync(basePath, "utf8");
config = JSON.parse(content);
}
} catch (e) {
console.warn(`[Config] Failed to load ${basePath}:`, e.message);
}
try {
if (fs2.existsSync(localPath)) {
const content = fs2.readFileSync(localPath, "utf8");
const localConfig = JSON.parse(content);
config = deepMerge(config, localConfig);
console.log(`[Config] Loaded local overrides from ${localPath}`);
}
} catch (e) {
console.warn(`[Config] Failed to load ${localPath}:`, e.message);
}
return config;
}
function expandPath(p) {
if (!p) return p;
return p.replace(/^~/, HOME).replace(/\$HOME/g, HOME).replace(/\$\{HOME\}/g, HOME);
}
function loadConfig() {
const fileConfig = loadConfigFile();
const workspace = process.env.OPENCLAW_WORKSPACE || expandPath(fileConfig.paths?.workspace) || detectWorkspace();
const config = {
// Server settings
server: {
port: parseInt(process.env.PORT || fileConfig.server?.port || "3333", 10),
host: process.env.HOST || fileConfig.server?.host || "localhost"
},
// Paths - all relative to workspace unless absolute
paths: {
workspace,
memory: expandPath(process.env.OPENCLAW_MEMORY_DIR || fileConfig.paths?.memory) || path2.join(workspace, "memory"),
state: expandPath(process.env.OPENCLAW_STATE_DIR || fileConfig.paths?.state) || path2.join(workspace, "state"),
cerebro: expandPath(process.env.OPENCLAW_CEREBRO_DIR || fileConfig.paths?.cerebro) || path2.join(workspace, "cerebro"),
skills: expandPath(process.env.OPENCLAW_SKILLS_DIR || fileConfig.paths?.skills) || path2.join(workspace, "skills"),
jobs: expandPath(process.env.OPENCLAW_JOBS_DIR || fileConfig.paths?.jobs) || path2.join(workspace, "jobs"),
logs: expandPath(process.env.OPENCLAW_LOGS_DIR || fileConfig.paths?.logs) || path2.join(HOME, ".openclaw-command-center", "logs")
},
// Auth settings
auth: {
mode: process.env.DASHBOARD_AUTH_MODE || fileConfig.auth?.mode || "none",
token: process.env.DASHBOARD_TOKEN || fileConfig.auth?.token,
allowedUsers: (process.env.DASHBOARD_ALLOWED_USERS || fileConfig.auth?.allowedUsers?.join(",") || "").split(",").map((s) => s.trim().toLowerCase()).filter(Boolean),
allowedIPs: (process.env.DASHBOARD_ALLOWED_IPS || fileConfig.auth?.allowedIPs?.join(",") || "127.0.0.1,::1").split(",").map((s) => s.trim()),
publicPaths: fileConfig.auth?.publicPaths || ["/api/health", "/api/whoami", "/favicon.ico"]
},
// Branding
branding: {
name: fileConfig.branding?.name || "OpenClaw Command Center",
theme: fileConfig.branding?.theme || "default"
},
// Integrations
integrations: {
linear: {
enabled: !!(process.env.LINEAR_API_KEY || fileConfig.integrations?.linear?.apiKey),
apiKey: process.env.LINEAR_API_KEY || fileConfig.integrations?.linear?.apiKey,
teamId: process.env.LINEAR_TEAM_ID || fileConfig.integrations?.linear?.teamId
}
},
// Billing - for cost savings calculation
billing: {
claudePlanCost: parseFloat(
process.env.CLAUDE_PLAN_COST || fileConfig.billing?.claudePlanCost || "200"
),
claudePlanName: process.env.CLAUDE_PLAN_NAME || fileConfig.billing?.claudePlanName || "Claude Code Max"
}
};
return config;
}
var CONFIG2 = loadConfig();
console.log("[Config] Workspace:", CONFIG2.paths.workspace);
console.log("[Config] Auth mode:", CONFIG2.auth.mode);
module2.exports = { CONFIG: CONFIG2, loadConfig, detectWorkspace, expandPath, getOpenClawDir: getOpenClawDir2 };
}
});
// src/jobs.js
var require_jobs = __commonJS({
"src/jobs.js"(exports2, module2) {
var path2 = require("path");
var { CONFIG: CONFIG2 } = require_config();
var JOBS_DIR = CONFIG2.paths.jobs;
var JOBS_STATE_DIR = path2.join(CONFIG2.paths.state, "jobs");
var apiInstance = null;
var forceApiUnavailable = false;
async function getAPI() {
if (forceApiUnavailable) return null;
if (apiInstance) return apiInstance;
try {
const { createJobsAPI } = await import(path2.join(JOBS_DIR, "lib/api.js"));
apiInstance = createJobsAPI({
definitionsDir: path2.join(JOBS_DIR, "definitions"),
stateDir: JOBS_STATE_DIR
});
return apiInstance;
} catch (e) {
console.error("Failed to load jobs API:", e.message);
return null;
}
}
function _resetForTesting(options = {}) {
apiInstance = null;
forceApiUnavailable = options.forceUnavailable || false;
}
function formatRelativeTime(isoString) {
if (!isoString) return null;
const date = new Date(isoString);
const now = /* @__PURE__ */ new Date();
const diffMs = now - date;
const diffMins = Math.round(diffMs / 6e4);
if (diffMins < 0) {
const futureMins = Math.abs(diffMins);
if (futureMins < 60) return `in ${futureMins}m`;
if (futureMins < 1440) return `in ${Math.round(futureMins / 60)}h`;
return `in ${Math.round(futureMins / 1440)}d`;
}
if (diffMins < 1) return "just now";
if (diffMins < 60) return `${diffMins}m ago`;
if (diffMins < 1440) return `${Math.round(diffMins / 60)}h ago`;
return `${Math.round(diffMins / 1440)}d ago`;
}
async function handleJobsRequest2(req, res, pathname, query, method) {
const api = await getAPI();
if (!api) {
res.writeHead(500, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: "Jobs API not available" }));
return;
}
try {
if (pathname === "/api/jobs/scheduler/status" && method === "GET") {
const status = await api.getSchedulerStatus();
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify(status, null, 2));
return;
}
if (pathname === "/api/jobs/stats" && method === "GET") {
const stats = await api.getAggregateStats();
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify(stats, null, 2));
return;
}
if (pathname === "/api/jobs/cache/clear" && method === "POST") {
api.clearCache();
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ success: true, message: "Cache cleared" }));
return;
}
if (pathname === "/api/jobs" && method === "GET") {
const jobs = await api.listJobs();
const enhanced = jobs.map((job) => ({
...job,
lastRunRelative: formatRelativeTime(job.lastRun),
nextRunRelative: formatRelativeTime(job.nextRun)
}));
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ jobs: enhanced, timestamp: Date.now() }, null, 2));
return;
}
const jobMatch = pathname.match(/^\/api\/jobs\/([^/]+)$/);
if (jobMatch && method === "GET") {
const jobId = decodeURIComponent(jobMatch[1]);
const job = await api.getJob(jobId);
if (!job) {
res.writeHead(404, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: "Job not found" }));
return;
}
job.lastRunRelative = formatRelativeTime(job.lastRun);
job.nextRunRelative = formatRelativeTime(job.nextRun);
if (job.recentRuns) {
job.recentRuns = job.recentRuns.map((run) => ({
...run,
startedAtRelative: formatRelativeTime(run.startedAt),
completedAtRelative: formatRelativeTime(run.completedAt)
}));
}
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify(job, null, 2));
return;
}
const historyMatch = pathname.match(/^\/api\/jobs\/([^/]+)\/history$/);
if (historyMatch && method === "GET") {
const jobId = decodeURIComponent(historyMatch[1]);
const limit = parseInt(query.get("limit") || "50", 10);
const runs = await api.getJobHistory(jobId, limit);
const enhanced = runs.map((run) => ({
...run,
startedAtRelative: formatRelativeTime(run.startedAt),
completedAtRelative: formatRelativeTime(run.completedAt)
}));
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ runs: enhanced, timestamp: Date.now() }, null, 2));
return;
}
const runMatch = pathname.match(/^\/api\/jobs\/([^/]+)\/run$/);
if (runMatch && method === "POST") {
const jobId = decodeURIComponent(runMatch[1]);
const result = await api.runJob(jobId);
res.writeHead(result.success ? 200 : 400, { "Content-Type": "application/json" });
res.end(JSON.stringify(result, null, 2));
return;
}
const pauseMatch = pathname.match(/^\/api\/jobs\/([^/]+)\/pause$/);
if (pauseMatch && method === "POST") {
const jobId = decodeURIComponent(pauseMatch[1]);
let body = "";
await new Promise((resolve) => {
req.on("data", (chunk) => body += chunk);
req.on("end", resolve);
});
let reason = null;
try {
const parsed = JSON.parse(body || "{}");
reason = parsed.reason;
} catch (_e) {
}
const result = await api.pauseJob(jobId, {
by: req.authUser?.login || "dashboard",
reason
});
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify(result, null, 2));
return;
}
const resumeMatch = pathname.match(/^\/api\/jobs\/([^/]+)\/resume$/);
if (resumeMatch && method === "POST") {
const jobId = decodeURIComponent(resumeMatch[1]);
const result = await api.resumeJob(jobId);
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify(result, null, 2));
return;
}
const skipMatch = pathname.match(/^\/api\/jobs\/([^/]+)\/skip$/);
if (skipMatch && method === "POST") {
const jobId = decodeURIComponent(skipMatch[1]);
const result = await api.skipJob(jobId);
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify(result, null, 2));
return;
}
const killMatch = pathname.match(/^\/api\/jobs\/([^/]+)\/kill$/);
if (killMatch && method === "POST") {
const jobId = decodeURIComponent(killMatch[1]);
const result = await api.killJob(jobId);
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify(result, null, 2));
return;
}
res.writeHead(404, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: "Not found" }));
} catch (e) {
console.error("Jobs API error:", e);
res.writeHead(500, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: e.message }));
}
}
function isJobsRoute2(pathname) {
return pathname.startsWith("/api/jobs");
}
module2.exports = { handleJobsRequest: handleJobsRequest2, isJobsRoute: isJobsRoute2, _resetForTesting };
}
});
// src/openclaw.js
var require_openclaw = __commonJS({
"src/openclaw.js"(exports2, module2) {
var { execFileSync, execFile } = require("child_process");
var { promisify } = require("util");
var execFileAsync = promisify(execFile);
function getSafeEnv() {
return {
PATH: process.env.PATH,
HOME: process.env.HOME,
USER: process.env.USER,
SHELL: process.env.SHELL,
LANG: process.env.LANG,
NO_COLOR: "1",
TERM: "dumb",
OPENCLAW_PROFILE: process.env.OPENCLAW_PROFILE || "",
OPENCLAW_WORKSPACE: process.env.OPENCLAW_WORKSPACE || "",
OPENCLAW_HOME: process.env.OPENCLAW_HOME || ""
};
}
function buildArgs(args2) {
const profile = process.env.OPENCLAW_PROFILE || "";
const profileArgs = profile ? ["--profile", profile] : [];
const cleanArgs = args2.replace(/\s*2>&1\s*/g, " ").replace(/\s*2>\/dev\/null\s*/g, " ").trim();
return [...profileArgs, ...cleanArgs.split(/\s+/).filter(Boolean)];
}
function runOpenClaw2(args2) {
try {
const result = execFileSync("openclaw", buildArgs(args2), {
encoding: "utf8",
timeout: 3e3,
env: getSafeEnv(),
stdio: ["pipe", "pipe", "pipe"]
});
return result;
} catch (e) {
return null;
}
}
async function runOpenClawAsync2(args2) {
try {
const { stdout } = await execFileAsync("openclaw", buildArgs(args2), {
encoding: "utf8",
timeout: 2e4,
env: getSafeEnv()
});
return stdout;
} catch (e) {
console.error("[OpenClaw Async] Error:", e.message);
return null;
}
}
function extractJSON2(output) {
if (!output) return null;
const jsonStart = output.search(/[[{]/);
if (jsonStart === -1) return null;
return output.slice(jsonStart);
}
module2.exports = {
runOpenClaw: runOpenClaw2,
runOpenClawAsync: runOpenClawAsync2,
extractJSON: extractJSON2,
getSafeEnv
};
}
});
// src/vitals.js
var require_vitals = __commonJS({
"src/vitals.js"(exports2, module2) {
var { runCmd, formatBytes } = require_utils();
var cachedVitals = null;
var lastVitalsUpdate = 0;
var VITALS_CACHE_TTL = 3e4;
var vitalsRefreshing = false;
async function refreshVitalsAsync() {
if (vitalsRefreshing) return;
vitalsRefreshing = true;
const vitals = {
hostname: "",
uptime: "",
disk: { used: 0, free: 0, total: 0, percent: 0, kbPerTransfer: 0, iops: 0, throughputMBps: 0 },
cpu: { loadAvg: [0, 0, 0], cores: 0, usage: 0 },
memory: { used: 0, free: 0, total: 0, percent: 0, pressure: "normal" },
temperature: null
};
const isLinux = process.platform === "linux";
const isMacOS = process.platform === "darwin";
try {
const coresCmd = isLinux ? "nproc" : "sysctl -n hw.ncpu";
const memCmd = isLinux ? "cat /proc/meminfo | grep MemTotal | awk '{print $2}'" : "sysctl -n hw.memsize";
const topCmd = isLinux ? "top -bn1 | head -3 | grep -E '^%?Cpu|^ ?CPU' || echo ''" : 'top -l 1 -n 0 2>/dev/null | grep "CPU usage" || echo ""';
const mpstatCmd = isLinux ? "(command -v mpstat >/dev/null 2>&1 && mpstat 1 1 | tail -1 | sed 's/^Average: *//') || echo ''" : "";
const [hostname, uptimeRaw, coresRaw, memTotalRaw, memInfoRaw, dfRaw, topOutput, mpstatOutput] = await Promise.all([
runCmd("hostname", { fallback: "unknown" }),
runCmd("uptime", { fallback: "" }),
runCmd(coresCmd, { fallback: "1" }),
runCmd(memCmd, { fallback: "0" }),
isLinux ? runCmd("cat /proc/meminfo", { fallback: "" }) : runCmd("vm_stat", { fallback: "" }),
runCmd("df -k ~ | tail -1", { fallback: "" }),
runCmd(topCmd, { fallback: "" }),
isLinux ? runCmd(mpstatCmd, { fallback: "" }) : Promise.resolve("")
]);
vitals.hostname = hostname;
const uptimeMatch = uptimeRaw.match(/up\s+([^,]+)/);
if (uptimeMatch) vitals.uptime = uptimeMatch[1].trim();
const loadMatch = uptimeRaw.match(/load averages?:\s*([\d.]+)[,\s]+([\d.]+)[,\s]+([\d.]+)/);
if (loadMatch)
vitals.cpu.loadAvg = [
parseFloat(loadMatch[1]),
parseFloat(loadMatch[2]),
parseFloat(loadMatch[3])
];
vitals.cpu.cores = parseInt(coresRaw, 10) || 1;
vitals.cpu.usage = Math.min(100, Math.round(vitals.cpu.loadAvg[0] / vitals.cpu.cores * 100));
if (isLinux) {
if (mpstatOutput) {
const parts = mpstatOutput.trim().split(/\s+/);
const user = parts.length > 1 ? parseFloat(parts[1]) : NaN;
const sys = parts.length > 3 ? parseFloat(parts[3]) : NaN;
const idle = parts.length ? parseFloat(parts[parts.length - 1]) : NaN;
if (!Number.isNaN(user)) vitals.cpu.userPercent = user;
if (!Number.isNaN(sys)) vitals.cpu.sysPercent = sys;
if (!Number.isNaN(idle)) {
vitals.cpu.idlePercent = idle;
vitals.cpu.usage = Math.max(0, Math.min(100, Math.round(100 - idle)));
}
}
if (topOutput && (vitals.cpu.idlePercent === null || vitals.cpu.idlePercent === void 0)) {
const userMatch = topOutput.match(/([\d.]+)\s*us/);
const sysMatch = topOutput.match(/([\d.]+)\s*sy/);
const idleMatch = topOutput.match(/([\d.]+)\s*id/);
vitals.cpu.userPercent = userMatch ? parseFloat(userMatch[1]) : null;
vitals.cpu.sysPercent = sysMatch ? parseFloat(sysMatch[1]) : null;
vitals.cpu.idlePercent = idleMatch ? parseFloat(idleMatch[1]) : null;
if (vitals.cpu.userPercent !== null && vitals.cpu.sysPercent !== null) {
vitals.cpu.usage = Math.round(vitals.cpu.userPercent + vitals.cpu.sysPercent);
}
}
} else if (topOutput) {
const userMatch = topOutput.match(/([\d.]+)%\s*user/);
const sysMatch = topOutput.match(/([\d.]+)%\s*sys/);
const idleMatch = topOutput.match(/([\d.]+)%\s*idle/);
vitals.cpu.userPercent = userMatch ? parseFloat(userMatch[1]) : null;
vitals.cpu.sysPercent = sysMatch ? parseFloat(sysMatch[1]) : null;
vitals.cpu.idlePercent = idleMatch ? parseFloat(idleMatch[1]) : null;
if (vitals.cpu.userPercent !== null && vitals.cpu.sysPercent !== null) {
vitals.cpu.usage = Math.round(vitals.cpu.userPercent + vitals.cpu.sysPercent);
}
}
const dfParts = dfRaw.split(/\s+/);
if (dfParts.length >= 4) {
vitals.disk.total = parseInt(dfParts[1], 10) * 1024;
vitals.disk.used = parseInt(dfParts[2], 10) * 1024;
vitals.disk.free = parseInt(dfParts[3], 10) * 1024;
vitals.disk.percent = Math.round(parseInt(dfParts[2], 10) / parseInt(dfParts[1], 10) * 100);
}
if (isLinux) {
const memTotalKB = parseInt(memTotalRaw, 10) || 0;
const memAvailableMatch = memInfoRaw.match(/MemAvailable:\s+(\d+)/);
const memFreeMatch = memInfoRaw.match(/MemFree:\s+(\d+)/);
vitals.memory.total = memTotalKB * 1024;
const memAvailable = parseInt(memAvailableMatch?.[1] || memFreeMatch?.[1] || 0, 10) * 1024;
vitals.memory.used = vitals.memory.total - memAvailable;
vitals.memory.free = memAvailable;
vitals.memory.percent = vitals.memory.total > 0 ? Math.round(vitals.memory.used / vitals.memory.total * 100) : 0;
} else {
const pageSizeMatch = memInfoRaw.match(/page size of (\d+) bytes/);
const pageSize = pageSizeMatch ? parseInt(pageSizeMatch[1], 10) : 4096;
const activePages = parseInt((memInfoRaw.match(/Pages active:\s+(\d+)/) || [])[1] || 0, 10);
const wiredPages = parseInt(
(memInfoRaw.match(/Pages wired down:\s+(\d+)/) || [])[1] || 0,
10
);
const compressedPages = parseInt(
(memInfoRaw.match(/Pages occupied by compressor:\s+(\d+)/) || [])[1] || 0,
10
);
vitals.memory.total = parseInt(memTotalRaw, 10) || 0;
vitals.memory.used = (activePages + wiredPages + compressedPages) * pageSize;
vitals.memory.free = vitals.memory.total - vitals.memory.used;
vitals.memory.percent = vitals.memory.total > 0 ? Math.round(vitals.memory.used / vitals.memory.total * 100) : 0;
}
vitals.memory.pressure = vitals.memory.percent > 90 ? "critical" : vitals.memory.percent > 75 ? "warning" : "normal";
const timeoutPrefix = isLinux ? "timeout 5" : "$(command -v gtimeout >/dev/null 2>&1 && echo gtimeout 5)";
const iostatArgs = isLinux ? "-d -o JSON 1 2" : "-d -c 2 2";
const iostatCmd = `${timeoutPrefix} iostat ${iostatArgs} 2>/dev/null || echo ''`;
const [perfCores, effCores, chip, iostatRaw] = await Promise.all([
isMacOS ? runCmd("sysctl -n hw.perflevel0.logicalcpu 2>/dev/null || echo 0", { fallback: "0" }) : Promise.resolve("0"),
isMacOS ? runCmd("sysctl -n hw.perflevel1.logicalcpu 2>/dev/null || echo 0", { fallback: "0" }) : Promise.resolve("0"),
isMacOS ? runCmd(
'system_profiler SPHardwareDataType 2>/dev/null | grep "Chip:" | cut -d: -f2 || echo ""',
{ fallback: "" }
) : Promise.resolve(""),
runCmd(iostatCmd, { fallback: "", timeout: 5e3 })
]);
if (isLinux) {
const cpuBrand = await runCmd(
"cat /proc/cpuinfo | grep 'model name' | head -1 | cut -d: -f2",
{ fallback: "" }
);
if (cpuBrand) vitals.cpu.brand = cpuBrand.trim();
}
vitals.cpu.pCores = parseInt(perfCores, 10) || null;
vitals.cpu.eCores = parseInt(effCores, 10) || null;
if (chip) vitals.cpu.chip = chip;
if (isLinux) {
try {
const iostatJson = JSON.parse(iostatRaw);
const samples = iostatJson.sysstat.hosts[0].statistics;
const disks = samples[samples.length - 1].disk;
const disk = disks.filter((d) => !d.disk_device.startsWith("loop")).sort((a, b) => b.tps - a.tps)[0];
if (disk) {
const kbReadPerSec = disk["kB_read/s"] || 0;
const kbWrtnPerSec = disk["kB_wrtn/s"] || 0;
vitals.disk.iops = disk.tps || 0;
vitals.disk.throughputMBps = (kbReadPerSec + kbWrtnPerSec) / 1024;
vitals.disk.kbPerTransfer = disk.tps > 0 ? (kbReadPerSec + kbWrtnPerSec) / disk.tps : 0;
}
} catch {
}
} else {
const iostatLines = iostatRaw.split("\n").filter((l) => l.trim());
const lastLine = iostatLines.length > 0 ? iostatLines[iostatLines.length - 1] : "";
const iostatParts = lastLine.split(/\s+/).filter(Boolean);
if (iostatParts.length >= 3) {
vitals.disk.kbPerTransfer = parseFloat(iostatParts[0]) || 0;
vitals.disk.iops = parseFloat(iostatParts[1]) || 0;
vitals.disk.throughputMBps = parseFloat(iostatParts[2]) || 0;
}
}
vitals.temperature = null;
vitals.temperatureNote = null;
const isAppleSilicon = vitals.cpu.chip && /apple/i.test(vitals.cpu.chip);
if (isAppleSilicon) {
vitals.temperatureNote = "Apple Silicon (requires elevated access)";
try {
const pmOutput = await runCmd(
'sudo -n powermetrics --samplers smc -i 1 -n 1 2>/dev/null | grep -i "die temp" | head -1',
{ fallback: "", timeout: 5e3 }
);
const tempMatch = pmOutput.match(/([\d.]+)/);
if (tempMatch) {
vitals.temperature = parseFloat(tempMatch[1]);
vitals.temperatureNote = null;
}
} catch (e) {
}
} else if (isMacOS) {
const home = require("os").homedir();
try {
const temp = await runCmd(
`osx-cpu-temp 2>/dev/null || ${home}/bin/osx-cpu-temp 2>/dev/null`,
{ fallback: "" }
);
if (temp && temp.includes("\xB0")) {
const tempMatch = temp.match(/([\d.]+)/);
if (tempMatch && parseFloat(tempMatch[1]) > 0) {
vitals.temperature = parseFloat(tempMatch[1]);
}
}
} catch (e) {
}
if (!vitals.temperature) {
try {
const ioregRaw = await runCmd(
"ioreg -r -n AppleSmartBattery 2>/dev/null | grep Temperature",
{ fallback: "" }
);
const tempMatch = ioregRaw.match(/"Temperature"\s*=\s*(\d+)/);
if (tempMatch) {
vitals.temperature = Math.round(parseInt(tempMatch[1], 10) / 100);
}
} catch (e) {
}
}
} else if (isLinux) {
try {
const temp = await runCmd("cat /sys/class/thermal/thermal_zone0/temp 2>/dev/null", {
fallback: ""
});
if (temp) {
vitals.temperature = Math.round(parseInt(temp, 10) / 1e3);
}
} catch (e) {
}
}
} catch (e) {
console.error("[Vitals] Async refresh failed:", e.message);
}
vitals.memory.usedFormatted = formatBytes(vitals.memory.used);
vitals.memory.totalFormatted = formatBytes(vitals.memory.total);
vitals.memory.freeFormatted = formatBytes(vitals.memory.free);
vitals.disk.usedFormatted = formatBytes(vitals.disk.used);
vitals.disk.totalFormatted = formatBytes(vitals.disk.total);
vitals.disk.freeFormatted = formatBytes(vitals.disk.free);
cachedVitals = vitals;
lastVitalsUpdate = Date.now();
vitalsRefreshing = false;
console.log("[Vitals] Cache refreshed async");
}
setTimeout(() => refreshVitalsAsync(), 500);
setInterval(() => refreshVitalsAsync(), VITALS_CACHE_TTL);
function getSystemVitals2() {
const now = Date.now();
if (!cachedVitals || now - lastVitalsUpdate > VITALS_CACHE_TTL) {
refreshVitalsAsync();
}
if (cachedVitals) return cachedVitals;
return {
hostname: "loading...",
uptime: "",
disk: {
used: 0,
free: 0,
total: 0,
percent: 0,
usedFormatted: "-",
totalFormatted: "-",
freeFormatted: "-"
},
cpu: { loadAvg: [0, 0, 0], cores: 0, usage: 0 },
memory: {
used: 0,
free: 0,
total: 0,
percent: 0,
pressure: "normal",
usedFormatted: "-",
totalFormatted: "-",
freeFormatted: "-"
},
temperature: null
};
}
var cachedDeps = null;
async function checkOptionalDeps2() {
const isLinux = process.platform === "linux";
const isMacOS = process.platform === "darwin";
const platform = isLinux ? "linux" : isMacOS ? "darwin" : null;
const results = [];
if (!platform) {
cachedDeps = results;
return results;
}
const fs2 = require("fs");
const path2 = require("path");
const depsFile = path2.join(__dirname, "..", "config", "system-deps.json");
let depsConfig;
try {
depsConfig = JSON.parse(fs2.readFileSync(depsFile, "utf8"));
} catch {
cachedDeps = results;
return results;
}
const deps = depsConfig[platform] || [];
const home = require("os").homedir();
let pkgManager = null;
if (isLinux) {
for (const pm of ["apt", "dnf", "yum", "pacman", "apk"]) {
const has = await runCmd(`which ${pm}`, { fallback: "" });
if (has) {
pkgManager = pm;
break;
}
}
} else if (isMacOS) {
const hasBrew = await runCmd("which brew", { fallback: "" });
if (hasBrew) pkgManager = "brew";
}
let isAppleSilicon = false;
if (isMacOS) {
const chip = await runCmd("sysctl -n machdep.cpu.brand_string", { fallback: "" });
isAppleSilicon = /apple/i.test(chip);
}
for (const dep of deps) {
if (dep.condition === "intel" && isAppleSilicon) continue;
let installed = false;
const hasBinary = await runCmd(`which ${dep.binary} 2>/dev/null`, { fallback: "" });
if (hasBinary) {
installed = true;
} else if (isMacOS && dep.binary === "osx-cpu-temp") {
const homebin = await runCmd(`test -x ${home}/bin/osx-cpu-temp && echo ok`, {
fallback: ""
});
if (homebin) installed = true;
}
const installCmd = dep.install[pkgManager] || null;
results.push({
id: dep.id,
name: dep.name,
purpose: dep.purpose,
affects: dep.affects,
installed,
installCmd,
url: dep.url || null
});
}
cachedDeps = results;
const missing = results.filter((d) => !d.installed);
if (missing.length > 0) {
console.log("[Startup] Optional dependencies for enhanced vitals:");
for (const dep of missing) {
const action = dep.installCmd || dep.url || "see docs";
console.log(` \u{1F4A1} ${dep.name} \u2014 ${dep.purpose}: ${action}`);
}
}
return results;
}
function getOptionalDeps2() {
return cachedDeps;
}
module2.exports = {
refreshVitalsAsync,
getSystemVitals: getSystemVitals2,
checkOptionalDeps: checkOptionalDeps2,
getOptionalDeps: getOptionalDeps2,
VITALS_CACHE_TTL
};
}
});
// src/auth.js
var require_auth = __commonJS({
"src/auth.js"(exports2, module2) {
var AUTH_HEADERS = {
tailscale: {
login: "tailscale-user-login",
name: "tailscale-user-name",
pic: "tailscale-user-profile-pic"
},
cloudflare: {
email: "cf-access-authenticated-user-email"
}
};
function checkAuth2(req, authConfig) {
const mode = authConfig.mode;
const remoteAddr = req.socket?.remoteAddress || "";
const isLocalhost = remoteAddr === "127.0.0.1" || remoteAddr === "::1" || remoteAddr === "::ffff:127.0.0.1";
if (isLocalhost) {
return { authorized: true, user: { type: "localhost", login: "localhost" } };
}
if (mode === "none") {
return { authorized: true, user: null };
}
if (mode === "token") {
const authHeader = req.headers["authorization"] || "";
const token = authHeader.replace(/^Bearer\s+/i, "");
if (token && token === authConfig.token) {
return { authorized: true, user: { type: "token" } };
}
return { authorized: false, reason: "Invalid or missing token" };
}
if (mode === "tailscale") {
const login = (req.headers[AUTH_HEADERS.tailscale.login] || "").toLowerCase();
const name = req.headers[AUTH_HEADERS.tailscale.name] || "";
const pic = req.headers[AUTH_HEADERS.tailscale.pic] || "";
if (!login) {
return { authorized: false, reason: "Not accessed via Tailscale Serve" };
}
const isAllowed = authConfig.allowedUsers.some((allowed) => {
if (allowed === "*") return true;
if (allowed === login) return true;
if (allowed.startsWith("*@")) {
const domain = allowed.slice(2);
return login.endsWith("@" + domain);
}
return false;
});
if (isAllowed) {
return { authorized: true, user: { type: "tailscale", login, name, pic } };
}
return { authorized: false, reason: `User ${login} not in allowlist`, user: { login } };
}
if (mode === "cloudflare") {
const email = (req.headers[AUTH_HEADERS.cloudflare.email] || "").toLowerCase();
if (!email) {
return { authorized: false, reason: "Not accessed via Cloudflare Access" };
}
const isAllowed = authConfig.allowedUsers.some((allowed) => {
if (allowed === "*") return true;
if (allowed === email) return true;
if (allowed.startsWith("*@")) {
const domain = allowed.slice(2);
return email.endsWith("@" + domain);
}
return false;
});
if (isAllowed) {
return { authorized: true, user: { type: "cloudflare", email } };
}
return { authorized: false, reason: `User ${email} not in allowlist`, user: { email } };
}
if (mode === "allowlist") {
const clientIP = req.headers["x-forwarded-for"]?.split(",")[0]?.trim() || req.socket?.remoteAddress || "";
const isAllowed = authConfig.allowedIPs.some((allowed) => {
if (allowed === clientIP) return true;
if (allowed.endsWith("/24")) {
const prefix = allowed.slice(0, -3).split(".").slice(0, 3).join(".");
return clientIP.startsWith(prefix + ".");
}
return false;
});
if (isAllowed) {
return { authorized: true, user: { type: "ip", ip: clientIP } };
}
return { authorized: false, reason: `IP ${clientIP} not in allowlist` };
}
return { authorized: false, reason: "Unknown auth mode" };
}
function getUnauthorizedPage2(reason, user, authConfig) {
const userInfo = user ? `<p class="user-info">Detected: ${user.login || user.email || user.ip || "unknown"}</p>` : "";
return `<!DOCTYPE html>
<html>
<head>
<title>Access Denied - Command Center</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
color: #e8e8e8;
}
.container {
text-align: center;
padding: 3rem;
background: rgba(255,255,255,0.05);
border-radius: 16px;
border: 1px solid rgba(255,255,255,0.1);
max-width: 500px;
}
.icon { font-size: 4rem; margin-bottom: 1rem; }
h1 { font-size: 1.8rem; margin-bottom: 1rem; color: #ff6b6b; }
.reason { color: #aaa; margin-bottom: 1.5rem; font-size: 0.95rem; }
.user-info { color: #ffeb3b; margin: 1rem 0; font-size: 0.9rem; }
.instructions { color: #ccc; font-size: 0.85rem; line-height: 1.5; }
.auth-mode { margin-top: 2rem; padding-top: 1rem; border-top: 1px solid rgba(255,255,255,0.1); color: #888; font-size: 0.75rem; }
code { background: rgba(255,255,255,0.1); padding: 2px 6px; border-radius: 4px; }
</style>
</head>
<body>
<div class="container">
<div class="icon">\u{1F510}</div>
<h1>Access Denied</h1>
<div class="reason">${reason}</div>
${userInfo}
<div class="instructions">
<p>This dashboard requires authentication via <strong>${authConfig.mode}</strong>.</p>
${authConfig.mode === "tailscale" ? `<p style="margin-top:1rem">Make sure you're accessing via your Tailscale URL and your account is in the allowlist.</p>` : ""}
${authConfig.mode === "cloudflare" ? `<p style="margin-top:1rem">Make sure you're accessing via Cloudflare Access and your email is in the allowlist.</p>` : ""}
</div>
<div class="auth-mode">Auth mode: <code>${authConfig.mode}</code></div>
</div>
</body>
</html>`;
}
module2.exports = { AUTH_HEADERS, checkAuth: checkAuth2, getUnauthorizedPage: getUnauthorizedPage2 };
}
});
// src/privacy.js
var require_privacy = __commonJS({
"src/privacy.js"(exports2, module2) {
var fs2 = require("fs");
var path2 = require("path");
function getPrivacyFilePath(dataDir) {
return path2.join(dataDir, "privacy-settings.json");
}
function loadPrivacySettings2(dataDir) {
try {
const privacyFile = getPrivacyFilePath(dataDir);
if (fs2.existsSync(privacyFile)) {
return JSON.parse(fs2.readFileSync(privacyFile, "utf8"));
}
} catch (e) {
console.error("Failed to load privacy settings:", e.message);
}
return {
version: 1,
hiddenTopics: [],
hiddenSessions: [],
hiddenCrons: [],
hideHostname: false,
updatedAt: null
};
}
function savePrivacySettings2(dataDir, data) {
try {
if (!fs2.existsSync(dataDir)) {
fs2.mkdirSync(dataDir, { recursive: true });
}
data.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
fs2.writeFileSync(getPrivacyFilePath(dataDir), JSON.stringify(data, null, 2));
return true;
} catch (e) {
console.error("Failed to save privacy settings:", e.message);
return false;
}
}
module2.exports = {
loadPrivacySettings: loadPrivacySettings2,
savePrivacySettings: savePrivacySettings2
};
}
});
// src/operators.js
var require_operators = __commonJS({
"src/operators.js"(exports2, module2) {
var fs2 = require("fs");
var path2 = require("path");
function loadOperators2(dataDir) {
const operatorsFile = path2.join(dataDir, "operators.json");
try {
if (fs2.existsSync(operatorsFile)) {
return JSON.parse(fs2.readFileSync(operatorsFile, "utf8"));
}
} catch (e) {
console.error("Failed to load operators:", e.message);
}
return { version: 1, operators: [], roles: {} };
}
function saveOperators2(dataDir, data) {
try {
if (!fs2.existsSync(dataDir)) {
fs2.mkdirSync(dataDir, { recursive: true });
}
const operatorsFile = path2.join(dataDir, "operators.json");
fs2.writeFileSync(operatorsFile, JSON.stringify(data, null, 2));
return true;
} catch (e) {
console.error("Failed to save operators:", e.message);
return false;
}
}
function getOperatorBySlackId2(dataDir, slackId) {
const data = loadOperators2(dataDir);
return data.operators.find((op) => op.id === slackId || op.metadata?.slackId === slackId);
}
var operatorsRefreshing = false;
async function refreshOperatorsAsync(dataDir, getOpenClawDir2) {
if (operatorsRefreshing) return;
operatorsRefreshing = true;
const toMs = (ts, fallback) => {
if (typeof ts === "number" && Number.isFinite(ts)) return ts;
if (typeof ts === "string") {
const parsed = Date.parse(ts);
if (Number.isFinite(parsed)) return parsed;
}
return fallback;
};
try {
const openclawDir = getOpenClawDir2();
const sessionsDir = path2.join(openclawDir, "agents", "main", "sessions");
if (!fs2.existsSync(sessionsDir)) {
operatorsRefreshing = false;
return;
}
const files = fs2.readdirSync(sessionsDir).filter((f) => f.endsWith(".jsonl"));
const operatorsMap = /* @__PURE__ */ new Map();
const sevenDaysAgo = Date.now() - 7 * 24 * 60 * 60 * 1e3;
for (const file of files) {
const filePath = path2.join(sessionsDir, file);
try {
const stat = fs2.statSync(filePath);
if (stat.mtimeMs < sevenDaysAgo) continue;
const fd = fs2.openSync(filePath, "r");
const buffer = Buffer.alloc(10240);
const bytesRead = fs2.readSync(fd, buffer, 0, 10240, 0);
fs2.closeSync(fd);
const content = buffer.toString("utf8", 0, bytesRead);
const lines = content.split("\n").slice(0, 20);
for (const line of lines) {
if (!line.trim()) continue;
try {
const entry = JSON.parse(line);
if (entry.type !== "message" || !entry.message) continue;
const msg = entry.message;
if (msg.role !== "user") continue;
let text = "";
if (typeof msg.content === "string") {
text = msg.content;
} else if (Array.isArray(msg.content)) {
const textPart = msg.content.find((c) => c.type === "text");
if (textPart) text = textPart.text || "";
}
if (!text) continue;
const slackMatch = text.match(/\[Slack[^\]]*\]\s*([\w.-]+)\s*\(([A-Z0-9]+)\):/);
if (slackMatch) {
const username = slackMatch[1];
const userId = slackMatch[2];
if (!operatorsMap.has(userId)) {
operatorsMap.set(userId, {
id: userId,
name: username,
username,
source: "slack",
firstSeen: toMs(entry.timestamp, stat.mtimeMs),
lastSeen: toMs(entry.timestamp, stat.mtimeMs),
sessionCount: 1
});
} else {
const op = operatorsMap.get(userId);
op.lastSeen = Math.max(op.lastSeen, toMs(entry.timestamp, stat.mtimeMs));
op.sessionCount++;
}
break;
}
const telegramMatch = text.match(/\[Telegram[^\]]*\]\s*([\w.-]+):/);
if (telegramMatch) {
const username = telegramMatch[1];
const operatorId = `telegram:${username}`;
if (!operatorsMap.has(operatorId)) {
operatorsMap.set(operatorId, {
id: operatorId,
name: username,
username,
source: "telegram",
firstSeen: toMs(entry.timestamp, stat.mtimeMs),
lastSeen: toMs(entry.timestamp, stat.mtimeMs),
sessionCount: 1
});
} else {
const op = operatorsMap.get(operatorId);
op.lastSeen = Math.max(op.lastSeen, toMs(entry.timestamp, stat.mtimeMs));
op.sessionCount++;
}
break;
}
const discordSenderMatch = text.match(/"sender":\s*"(\d+)"/);
const discordLabelMatch = text.match(/"label":\s*"([^"]+)"/);
const discordUsernameMatch = text.match(/"username":\s*"([^"]+)"/);
if (discordSenderMatch) {
const userId = discordSenderMatch[1];
const label = discordLabelMatch ? discordLabelMatch[1] : userId;
const username = discordUsernameMatch ? discordUsernameMatch[1] : label;
const opId = `discord:${userId}`;
if (!operatorsMap.has(opId)) {
operatorsMap.set(opId, {
id: opId,
discordId: userId,
name: label,
username,
source: "discord",
firstSeen: toMs(entry.timestamp, stat.mtimeMs),
lastSeen: toMs(entry.timestamp, stat.mtimeMs),
sessionCount: 1
});
} else {
const op = operatorsMap.get(opId);
op.lastSeen = Math.max(op.lastSeen, toMs(entry.timestamp, stat.mtimeMs));
op.sessionCount++;
}
break;
}
} catch (e) {
}
}
} catch (e) {
}
}
const existing = loadOperators2(dataDir);
const existingMap = new Map(existing.operators.map((op) => [op.id, op]));
for (const [id, autoOp] of operatorsMap) {
if (existingMap.has(id)) {
const manual = existingMap.get(id);
manual.lastSeen = Math.max(manual.lastSeen || 0, autoOp.lastSeen);
manual.sessionCount = (manual.sessionCount || 0) + autoOp.sessionCount;
} else {
existingMap.set(id, autoOp);
}
}
const merged = {
version: 1,
operators: Array.from(existingMap.values()).sort(
(a, b) => (b.lastSeen || 0) - (a.lastSeen || 0)
),
roles: existing.roles || {},
lastRefreshed: Date.now()
};
saveOperators2(dataDir, merged);
console.log(`[Operators] Refreshed: ${merged.operators.length} operators detected`);
} catch (e) {
console.error("[Operators] Refresh failed:", e.message);
}
operatorsRefreshing = false;
}
function startOperatorsRefresh2(dataDir, getOpenClawDir2) {
setTimeout(() => refreshOperatorsAsync(dataDir, getOpenClawDir2), 2e3);
setInterval(() => refreshOperatorsAsync(dataDir, getOpenClawDir2), 5 * 60 * 1e3);
}
module2.exports = {
loadOperators: loadOperators2,
saveOperators: saveOperators2,
getOperatorBySlackId: getOperatorBySlackId2,
refreshOperatorsAsync,
startOperatorsRefresh: startOperatorsRefresh2
};
}
});
// src/topics.js
var require_topics = __commonJS({
"src/topics.js"(exports2, module2) {
var TOPIC_PATTERNS = {
dashboard: ["dashboard", "command center", "ui", "interface", "status page"],
scheduling: ["cron", "schedule", "timer", "reminder", "alarm", "periodic", "interval"],
heartbeat: [
"heartbeat",
"heartbeat_ok",
"poll",
"health check",
"ping",
"keepalive",
"monitoring"
],
memory: ["memory", "remember", "recall", "notes", "journal", "log", "context"],
Slack: ["slack", "channel", "#cc-", "thread", "mention", "dm", "workspace"],
email: ["email", "mail", "inbox", "gmail", "send email", "unread", "compose"],
calendar: ["calendar", "event", "meeting", "appointment", "schedule", "gcal"],
coding: [
"code",
"script",
"function",
"debug",
"error",
"bug",
"implement",
"refactor",
"programming"
],
git: [
"git",
"commit",
"branch",
"merge",
"push",
"pull",
"repository",
"pr",
"pull request",
"github"
],
"file editing": ["file", "edit", "write", "read", "create", "delete", "modify", "save"],
API: ["api", "endpoint", "request", "response", "webhook", "integration", "rest", "graphql"],
research: ["search", "research", "lookup", "find", "investigate", "learn", "study"],
browser: ["browser", "webpage", "website", "url", "click", "navigate", "screenshot", "web_fetch"],
"Quip export": ["quip", "export", "document", "spreadsheet"],
finance: ["finance", "investment", "stock", "money", "budget", "bank", "trading", "portfolio"],
home: ["home", "automation", "lights", "thermostat", "smart home", "iot", "homekit"],
health: ["health", "fitness", "workout", "exercise", "weight", "sleep", "nutrition"],
travel: ["travel", "flight", "hotel", "trip", "vacation", "booking", "airport"],
food: ["food", "recipe", "restaurant", "cooking", "meal", "order", "delivery"],
subagent: ["subagent", "spawn", "sub-agent", "delegate", "worker", "parallel"],
tools: ["tool", "exec", "shell", "command", "terminal", "bash", "run"]
};
function detectTopics(text) {
if (!text) return [];
const lowerText = text.toLowerCase();
const scores = {};
for (const [topic, keywords] of Object.entries(TOPIC_PATTERNS)) {
let score = 0;
for (const keyword of keywords) {
if (keyword.length <= 3) {
const regex = new RegExp(`\\b${keyword}\\b`, "i");
if (regex.test(lowerText)) score++;
} else if (lowerText.includes(keyword)) {
score++;
}
}
if (score > 0) {
scores[topic] = score;
}
}
if (Object.keys(scores).length === 0) return [];
const bestScore = Math.max(...Object.values(scores));
const threshold = Math.max(2, bestScore * 0.5);
return Object.entries(scores).filter(([_, score]) => score >= threshold || score >= 1 && bestScore <= 2).sort((a, b) => b[1] - a[1]).map(([topic, _]) => topic);
}
module2.exports = { TOPIC_PATTERNS, detectTopics };
}
});
// src/sessions.js
var require_sessions = __commonJS({
"src/sessions.js"(exports2, module2) {
var fs2 = require("fs");
var path2 = require("path");
var { detectTopics } = require_topics();
var CHANNEL_MAP = {
c0aax7y80np: "#cc-meta",
c0ab9f8sdfe: "#cc-research",
c0aan4rq7v5: "#cc-finance",
c0abxulk1qq: "#cc-properties",
c0ab5nz8mkl: "#cc-ai",
c0aan38tzv5: "#cc-dev",
c0ab7wwhqvc: "#cc-home",
c0ab1pjhxef: "#cc-health",
c0ab7txvcqd: "#cc-legal",
c0aay2g3n3r: "#cc-social",
c0aaxrw2wqp: "#cc-business",
c0ab19f3lae: "#cc-random",
c0ab0r74y33: "#cc-food",
c0ab0qrq3r9: "#cc-travel",
c0ab0sbqqlg: "#cc-family",
c0ab0slqdba: "#cc-games",
c0ab1ps7ef2: "#cc-music",
c0absbnrsbe: "#cc-dashboard"
};
function parseSessionLabel(key) {
const parts = key.split(":");
if (parts.includes("slack")) {
const channelIdx = parts.indexOf("channel");
if (channelIdx >= 0 && parts[channelIdx + 1]) {
const channelId = parts[channelIdx + 1].toLowerCase();
const channelName = CHANNEL_MAP[channelId] || `#${channelId}`;
if (parts.includes("thread")) {
const threadTs = parts[parts.indexOf("thread") + 1];
const ts = parseFloat(threadTs);
const date = new Date(ts * 1e3);
const timeStr = date.toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit" });
return `${channelName} thread @ ${timeStr}`;
}
return channelName;
}
}
if (key.includes("telegram")) {
return "\u{1F4F1} Telegram";
}
if (key === "agent:main:main") {
return "\u{1F3E0} Main Session";
}
return key.length > 40 ? key.slice(0, 37) + "..." : key;
}
function createSessionsModule2(deps) {
const { getOpenClawDir: getOpenClawDir2, getOperatorBySlackId: getOperatorBySlackId2, runOpenClaw: runOpenClaw2, runOpenClawAsync: runOpenClawAsync2, extractJSON: extractJSON2 } = deps;
let sessionsCache = { sessions: [], timestamp: 0, refreshing: false };
const SESSIONS_CACHE_TTL = 1e4;
function getSessionOriginator(sessionId) {
try {
if (!sessionId) return null;
const openclawDir = getOpenClawDir2();
const transcriptPath = path2.join(
openclawDir,
"agents",
"main",
"sessions",
`${sessionId}.jsonl`
);
if (!fs2.existsSync(transcriptPath)) return null;
const content = fs2.readFileSync(transcriptPath, "utf8");
const lines = content.trim().split("\n");
for (let i = 0; i < Math.min(lines.length, 10); i++) {
try {
const entry = JSON.parse(lines[i]);
if (entry.type !== "message" || !entry.message) continue;
const msg = entry.message;
if (msg.role !== "user") continue;
let text = "";
if (typeof msg.content === "string") {
text = msg.content;
} else if (Array.isArray(msg.content)) {
const textPart = msg.content.find((c) => c.type === "text");
if (textPart) text = textPart.text || "";
}
if (!text) continue;
const slackUserMatch = text.match(/\]\s*([\w.-]+)\s*\(([A-Z0-9]+)\):/);
if (slackUserMatch) {
const username = slackUserMatch[1];
const userId = slackUserMatch[2];
const operator = getOperatorBySlackId2(userId);
return {
userId,
username,
displayName: operator?.name || username,
role: operator?.role || "user",
avatar: operator?.avatar || null
};
}
} catch (e) {
}
}
return null;
} catch (e) {
return null;
}
}
function getSessionTopic(sessionId) {
if (!sessionId) return null;
try {
const openclawDir = getOpenClawDir2();
const transcriptPath = path2.join(
openclawDir,
"agents",
"main",
"sessions",
`${sessionId}.jsonl`
);
if (!fs2.existsSync(transcriptPath)) return null;
const fd = fs2.openSync(transcriptPath, "r");
const buffer = Buffer.alloc(5e4);
const bytesRead = fs2.readSync(fd, buffer, 0, 5e4, 0);
fs2.closeSync(fd);
if (bytesRead === 0) return null;
const content = buffer.toString("utf8", 0, bytesRead);
const lines = content.split("\n").filter((l) => l.trim());
let textSamples = [];
for (const line of lines.slice(0, 30)) {
try {
const entry = JSON.parse(line);
if (entry.type === "message" && entry.message?.content) {
const msgContent = entry.message.content;
if (Array.isArray(msgContent)) {
msgContent.forEach((c) => {
if (c.type === "text" && c.text) {
textSamples.push(c.text.slice(0, 500));
}
});
} else if (typeof msgContent === "string") {
textSamples.push(msgContent.slice(0, 500));
}
}
} catch (e) {
}
}
if (textSamples.length === 0) return null;
const topics = detectTopics(textSamples.join(" "));
return topics.length > 0 ? topics.slice(0, 2).join(", ") : null;
} catch (e) {
return null;
}
}
function mapSession(s) {
const minutesAgo = s.ageMs ? s.ageMs / 6e4 : Infinity;
let channel = "other";
if (s.key.includes("slack")) channel = "slack";
else if (s.key.includes("telegram")) channel = "telegram";
else if (s.key.includes("discord")) channel = "discord";
else if (s.key.includes("signal")) channel = "signal";
else if (s.key.includes("whatsapp")) channel = "whatsapp";
let sessionType = "channel";
if (s.key.includes(":subagent:")) sessionType = "subagent";
else if (s.key.includes(":cron:")) sessionType = "cron";
else if (s.key === "agent:main:main") sessionType = "main";
const originator = getSessionOriginator(s.sessionId);
const label = s.groupChannel || s.displayName || parseSessionLabel(s.key);
const topic = getSessionTopic(s.sessionId);
const totalTokens = s.totalTokens || 0;
const sessionAgeMinutes = Math.max(1, Math.min(minutesAgo, 24 * 60));
const burnRate = Math.round(totalTokens / sessionAgeMinutes);
return {
sessionKey: s.key,
sessionId: s.sessionId,
label,
groupChannel: s.groupChannel || null,
displayName: s.displayName || null,
kind: s.kind,
channel,
sessionType,
active: minutesAgo < 15,
recentlyActive: minutesAgo < 60,
minutesAgo: Math.round(minutesAgo),
tokens: s.totalTokens || 0,
model: s.model,
originator,
topic,
metrics: {
burnRate,
toolCalls: 0,
minutesActive: Math.max(1, Math.min(Math.round(minutesAgo), 24 * 60))
}
};
}
async function refreshSessionsCache() {
if (sessionsCache.refreshing) return;
sessionsCache.refreshing = true;
try {
const output = await runOpenClawAsync2("sessions --json 2>/dev/null");
const jsonStr = extractJSON2(output);
if (jsonStr) {
const data = JSON.parse(jsonStr);
const sessions2 = data.sessions || [];
const mapped = sessions2.map((s) => mapSession(s));
sessionsCache = {
sessions: mapped,
timestamp: Date.now(),
refreshing: false
};
console.log(`[Sessions Cache] Refreshed: ${mapped.length} sessions`);
}
} catch (e) {
console.error("[Sessions Cache] Refresh error:", e.message);
}
sessionsCache.refreshing = false;
}
function getSessionsCached() {
const now = Date.now();
const isStale = now - sessionsCache.timestamp > SESSIONS_CACHE_TTL;
if (isStale && !sessionsCache.refreshing) {
refreshSessionsCache();
}
return sessionsCache.sessions;
}
function getSessions(options = {}) {
const limit = Object.prototype.hasOwnProperty.call(options, "limit") ? options.limit : 20;
const returnCount = options.returnCount || false;
if (limit === null) {
const cached = getSessionsCached();
const totalCount = cached.length;
return returnCount ? { sessions: cached, totalCount } : cached;
}
try {
const output = runOpenClaw2("sessions --json 2>/dev/null");
const jsonStr = extractJSON2(output);
if (jsonStr) {
const data = JSON.parse(jsonStr);
const totalCount = data.count || data.sessions?.length || 0;
let sessions2 = data.sessions || [];
if (limit != null) {
sessions2 = sessions2.slice(0, limit);
}
const mapped = sessions2.map((s) => mapSession(s));
return returnCount ? { sessions: mapped, totalCount } : mapped;
}
} catch (e) {
console.error("Failed to get sessions:", e.message);
}
return returnCount ? { sessions: [], totalCount: 0 } : [];
}
function readTranscript(sessionId) {
const openclawDir = getOpenClawDir2();
const transcriptPath = path2.join(
openclawDir,
"agents",
"main",
"sessions",
`${sessionId}.jsonl`
);
try {
if (!fs2.existsSync(transcriptPath)) return [];
const content = fs2.readFileSync(transcriptPath, "utf8");
return content.trim().split("\n").map((line) => {
try {
return JSON.parse(line);
} catch {
return null;
}
}).filter(Boolean);
} catch (e) {
console.error("Failed to read transcript:", e.message);
return [];
}
}
function getSessionDetail(sessionKey) {
try {
const listOutput = runOpenClaw2("sessions --json 2>/dev/null");
let sessionInfo = null;
const jsonStr = extractJSON2(listOutput);
if (jsonStr) {
const data = JSON.parse(jsonStr);
sessionInfo = data.sessions?.find((s) => s.key === sessionKey);
}
if (!sessionInfo) {
return { error: "Session not found" };
}
const transcript = readTranscript(sessionInfo.sessionId);
let messages = [];
let tools = {};
let facts = [];
let needsAttention = [];
let totalInputTokens = 0;
let totalOutputTokens = 0;
let totalCacheRead = 0;
let totalCacheWrite = 0;
let totalCost = 0;
let detectedModel = sessionInfo.model || null;
transcript.forEach((entry) => {
if (entry.type !== "message" || !entry.message) return;
const msg = entry.message;
if (!msg.role) return;
if (msg.usage) {
totalInputTokens += msg.usage.input || msg.usage.inputTokens || 0;
totalOutputTokens += msg.usage.output || msg.usage.outputTokens || 0;
totalCacheRead += msg.usage.cacheRead || msg.usage.cacheReadTokens || 0;
totalCacheWrite += msg.usage.cacheWrite || msg.usage.cacheWriteTokens || 0;
if (msg.usage.cost?.total) totalCost += msg.usage.cost.total;
}
if (msg.role === "assistant" && msg.model && !detectedModel) {
detectedModel = msg.model;
}
let text = "";
if (typeof msg.content === "string") {
text = msg.content;
} else if (Array.isArray(msg.content)) {
const textPart = msg.content.find((c) => c.type === "text");
if (textPart) text = textPart.text || "";
msg.content.filter((c) => c.type === "toolCall" || c.type === "tool_use").forEach((tc) => {
const name = tc.name || tc.tool || "unknown";
tools[name] = (tools[name] || 0) + 1;
});
}
if (text && msg.role !== "toolResult") {
messages.push({ role: msg.role, text, timestamp: entry.timestamp });
}
if (msg.role === "user" && text) {
const lowerText = text.toLowerCase();
if (text.includes("?")) {
const questions = text.match(/[^.!?\n]*\?/g) || [];
questions.slice(0, 2).forEach((q) => {
if (q.length > 15 && q.length < 200) {
needsAttention.push(`\u2753 ${q.trim()}`);
}
});
}
if (lowerText.includes("todo") || lowerText.includes("remind") || lowerText.includes("need to")) {
const match = text.match(/(?:todo|remind|need to)[^.!?\n]*/i);
if (match) needsAttention.push(`\u{1F4CB} ${match[0].slice(0, 100)}`);
}
}
if (msg.role === "assistant" && text) {
const lowerText = text.toLowerCase();
["\u2705", "done", "created", "updated", "fixed", "deployed"].forEach((keyword) => {
if (lowerText.includes(keyword)) {
const lines = text.split("\n").filter((l) => l.toLowerCase().includes(keyword));
lines.slice(0, 2).forEach((line) => {
if (line.length > 5 && line.length < 150) {
facts.push(line.trim().slice(0, 100));
}
});
}
});
}
});
let summary = "No activity yet.";
const userMessages = messages.filter((m) => m.role === "user");
const assistantMessages = messages.filter((m) => m.role === "assistant");
let topics = [];
if (messages.length > 0) {
summary = `${messages.length} messages (${userMessages.length} user, ${assistantMessages.length} assistant). `;
const allText = messages.map((m) => m.text).join(" ");
topics = detectTopics(allText);
if (topics.length > 0) {
summary += `Topics: ${topics.join(", ")}.`;
}
}
const toolsArray = Object.entries(tools).map(([name, count]) => ({ name, count })).sort((a, b) => b.count - a.count);
const ageMs = sessionInfo.ageMs || 0;
const lastActive = ageMs < 6e4 ? "Just now" : ageMs < 36e5 ? `${Math.round(ageMs / 6e4)} minutes ago` : ageMs < 864e5 ? `${Math.round(ageMs / 36e5)} hours ago` : `${Math.round(ageMs / 864e5)} days ago`;
let channelDisplay = "Other";
if (sessionInfo.groupChannel) {
channelDisplay = sessionInfo.groupChannel;
} else if (sessionInfo.displayName) {
channelDisplay = sessionInfo.displayName;
} else if (sessionKey.includes("slack")) {
const parts = sessionKey.split(":");
const channelIdx = parts.indexOf("channel");
if (channelIdx >= 0 && parts[channelIdx + 1]) {
const channelId = parts[channelIdx + 1].toLowerCase();
channelDisplay = CHANNEL_MAP[channelId] || `#${channelId}`;
} else {
channelDisplay = "Slack";
}
} else if (sessionKey.includes("telegram")) {
channelDisplay = "Telegram";
}
const finalTotalTokens = totalInputTokens + totalOutputTokens || sessionInfo.totalTokens || 0;
const finalInputTokens = totalInputTokens || sessionInfo.inputTokens || 0;
const finalOutputTokens = totalOutputTokens || sessionInfo.outputTokens || 0;
const modelDisplay = (detectedModel || sessionInfo.model || "-").replace("anthropic/", "").replace("openai/", "");
return {
key: sessionKey,
kind: sessionInfo.kind,
channel: channelDisplay,
groupChannel: sessionInfo.groupChannel || channelDisplay,
model: modelDisplay,
tokens: finalTotalTokens,
inputTokens: finalInputTokens,
outputTokens: finalOutputTokens,
cacheRead: totalCacheRead,
cacheWrite: totalCacheWrite,
estCost: totalCost > 0 ? `$${totalCost.toFixed(4)}` : null,
lastActive,
summary,
topics,
// Array of detected topics
facts: [...new Set(facts)].slice(0, 8),
needsAttention: [...new Set(needsAttention)].slice(0, 5),
tools: toolsArray.slice(0, 10),
messages: messages.slice(-15).reverse().map((m) => ({
role: m.role,
text: m.text.slice(0, 500)
}))
};
} catch (e) {
console.error("Failed to get session detail:", e.message);
return { error: e.message };
}
}
return {
getSessionOriginator,
getSessionTopic,
mapSession,
refreshSessionsCache,
getSessionsCached,
getSessions,
readTranscript,
getSessionDetail,
parseSessionLabel
};
}
module2.exports = { createSessionsModule: createSessionsModule2, CHANNEL_MAP };
}
});
// src/cron.js
var require_cron = __commonJS({
"src/cron.js"(exports2, module2) {
var fs2 = require("fs");
var path2 = require("path");
function cronToHuman(expr) {
if (!expr || expr === "\u2014") return null;
const parts = expr.split(" ");
if (parts.length < 5) return null;
const [minute, hour, dayOfMonth, month, dayOfWeek] = parts;
const dayNames = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
function formatTime(h, m) {
const hNum = parseInt(h, 10);
const mNum = parseInt(m, 10);
if (isNaN(hNum)) return null;
const ampm = hNum >= 12 ? "pm" : "am";
const h12 = hNum === 0 ? 12 : hNum > 12 ? hNum - 12 : hNum;
return mNum === 0 ? `${h12}${ampm}` : `${h12}:${mNum.toString().padStart(2, "0")}${ampm}`;
}
if (minute === "*" && hour === "*" && dayOfMonth === "*" && month === "*" && dayOfWeek === "*") {
return "Every minute";
}
if (minute.startsWith("*/")) {
const interval = minute.slice(2);
return `Every ${interval} minutes`;
}
if (hour.startsWith("*/")) {
const interval = hour.slice(2);
const minStr = minute === "0" ? "" : `:${minute.padStart(2, "0")}`;
return `Every ${interval} hours${minStr ? " at " + minStr : ""}`;
}
if (minute !== "*" && hour === "*" && dayOfMonth === "*" && month === "*" && dayOfWeek === "*") {
return `Hourly at :${minute.padStart(2, "0")}`;
}
let timeStr = "";
if (minute !== "*" && hour !== "*" && !hour.startsWith("*/")) {
timeStr = formatTime(hour, minute);
}
if (timeStr && dayOfMonth === "*" && month === "*" && dayOfWeek === "*") {
return `Daily at ${timeStr}`;
}
if ((dayOfWeek === "1-5" || dayOfWeek === "MON-FRI") && dayOfMonth === "*" && month === "*") {
return timeStr ? `Weekdays at ${timeStr}` : "Weekdays";
}
if ((dayOfWeek === "0,6" || dayOfWeek === "6,0") && dayOfMonth === "*" && month === "*") {
return timeStr ? `Weekends at ${timeStr}` : "Weekends";
}
if (dayOfMonth === "*" && month === "*" && dayOfWeek !== "*") {
const days = dayOfWeek.split(",").map((d) => {
const num = parseInt(d, 10);
return dayNames[num] || d;
});
const dayStr = days.length === 1 ? days[0] : days.join(", ");
return timeStr ? `${dayStr} at ${timeStr}` : `Every ${dayStr}`;
}
if (dayOfMonth !== "*" && month === "*" && dayOfWeek === "*") {
const day = parseInt(dayOfMonth, 10);
const suffix = day === 1 || day === 21 || day === 31 ? "st" : day === 2 || day === 22 ? "nd" : day === 3 || day === 23 ? "rd" : "th";
return timeStr ? `${day}${suffix} of month at ${timeStr}` : `${day}${suffix} of every month`;
}
if (timeStr) {
return `At ${timeStr}`;
}
return expr;
}
function getCronJobs2(getOpenClawDir2) {
try {
const cronPath = path2.join(getOpenClawDir2(), "cron", "jobs.json");
if (fs2.existsSync(cronPath)) {
const data = JSON.parse(fs2.readFileSync(cronPath, "utf8"));
return (data.jobs || []).map((j) => {
let scheduleStr = "\u2014";
let scheduleHuman = null;
if (j.schedule) {
if (j.schedule.kind === "cron" && j.schedule.expr) {
scheduleStr = j.schedule.expr;
scheduleHuman = cronToHuman(j.schedule.expr);
} else if (j.schedule.kind === "once") {
scheduleStr = "once";
scheduleHuman = "One-time";
}
}
let nextRunStr = "\u2014";
if (j.state?.nextRunAtMs) {
const next = new Date(j.state.nextRunAtMs);
const now = /* @__PURE__ */ new Date();
const diffMs = next - now;
const diffMins = Math.round(diffMs / 6e4);
if (diffMins < 0) {
nextRunStr = "overdue";
} else if (diffMins < 60) {
nextRunStr = `${diffMins}m`;
} else if (diffMins < 1440) {
nextRunStr = `${Math.round(diffMins / 60)}h`;
} else {
nextRunStr = `${Math.round(diffMins / 1440)}d`;
}
}
return {
id: j.id,
name: j.name || j.id.slice(0, 8),
schedule: scheduleStr,
scheduleHuman,
nextRun: nextRunStr,
enabled: j.enabled !== false,
lastStatus: j.state?.lastStatus
};
});
}
} catch (e) {
console.error("Failed to get cron:", e.message);
}
return [];
}
module2.exports = {
cronToHuman,
getCronJobs: getCronJobs2
};
}
});
// src/cerebro.js
var require_cerebro = __commonJS({
"src/cerebro.js"(exports2, module2) {
var fs2 = require("fs");
var path2 = require("path");
var { formatTimeAgo } = require_utils();
function getCerebroTopics2(cerebroDir, options = {}) {
const { offset = 0, limit = 20, status: filterStatus = "all" } = options;
const topicsDir = path2.join(cerebroDir, "topics");
const orphansDir = path2.join(cerebroDir, "orphans");
const topics = [];
const result = {
initialized: false,
cerebroPath: cerebroDir,
topics: { active: 0, resolved: 0, parked: 0, total: 0 },
threads: 0,
orphans: 0,
recentTopics: [],
lastUpdated: null
};
try {
if (!fs2.existsSync(cerebroDir)) {
return result;
}
result.initialized = true;
let latestModified = null;
if (!fs2.existsSync(topicsDir)) {
return result;
}
const topicNames = fs2.readdirSync(topicsDir).filter((name) => {
const topicPath = path2.join(topicsDir, name);
return fs2.statSync(topicPath).isDirectory() && !name.startsWith("_");
});
topicNames.forEach((name) => {
const topicMdPath = path2.join(topicsDir, name, "topic.md");
const topicDirPath = path2.join(topicsDir, name);
let stat;
let content = "";
if (fs2.existsSync(topicMdPath)) {
stat = fs2.statSync(topicMdPath);
content = fs2.readFileSync(topicMdPath, "utf8");
} else {
stat = fs2.statSync(topicDirPath);
}
try {
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
let title = name;
let topicStatus = "active";
let category = "general";
let created = null;
if (frontmatterMatch) {
const frontmatter = frontmatterMatch[1];
const titleMatch = frontmatter.match(/title:\s*(.+)/);
const statusMatch = frontmatter.match(/status:\s*(.+)/);
const categoryMatch = frontmatter.match(/category:\s*(.+)/);
const createdMatch = frontmatter.match(/created:\s*(.+)/);
if (titleMatch) title = titleMatch[1].trim();
if (statusMatch) topicStatus = statusMatch[1].trim().toLowerCase();
if (categoryMatch) category = categoryMatch[1].trim();
if (createdMatch) created = createdMatch[1].trim();
}
const threadsDir = path2.join(topicsDir, name, "threads");
let threadCount = 0;
if (fs2.existsSync(threadsDir)) {
threadCount = fs2.readdirSync(threadsDir).filter((f) => f.endsWith(".md") || f.endsWith(".json")).length;
}
result.threads += threadCount;
if (topicStatus === "active") result.topics.active++;
else if (topicStatus === "resolved") result.topics.resolved++;
else if (topicStatus === "parked") result.topics.parked++;
if (!latestModified || stat.mtime > latestModified) {
latestModified = stat.mtime;
}
topics.push({
name,
title,
status: topicStatus,
category,
created,
threads: threadCount,
lastModified: stat.mtimeMs
});
} catch (e) {
console.error(`Failed to parse topic ${name}:`, e.message);
}
});
result.topics.total = topics.length;
const statusPriority = { active: 0, resolved: 1, parked: 2 };
topics.sort((a, b) => {
const statusDiff = (statusPriority[a.status] || 3) - (statusPriority[b.status] || 3);
if (statusDiff !== 0) return statusDiff;
return b.lastModified - a.lastModified;
});
let filtered = topics;
if (filterStatus !== "all") {
filtered = topics.filter((t) => t.status === filterStatus);
}
const paginated = filtered.slice(offset, offset + limit);
result.recentTopics = paginated.map((t) => ({
name: t.name,
title: t.title,
status: t.status,
threads: t.threads,
age: formatTimeAgo(new Date(t.lastModified))
}));
if (fs2.existsSync(orphansDir)) {
try {
result.orphans = fs2.readdirSync(orphansDir).filter((f) => f.endsWith(".md")).length;
} catch (e) {
}
}
result.lastUpdated = latestModified ? latestModified.toISOString() : null;
} catch (e) {
console.error("Failed to get Cerebro topics:", e.message);
}
return result;
}
function updateTopicStatus2(cerebroDir, topicId, newStatus) {
const topicDir = path2.join(cerebroDir, "topics", topicId);
const topicFile = path2.join(topicDir, "topic.md");
if (!fs2.existsSync(topicDir)) {
return { error: `Topic '${topicId}' not found`, code: 404 };
}
if (!fs2.existsSync(topicFile)) {
const content2 = `---
title: ${topicId}
status: ${newStatus}
category: general
created: ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}
---
# ${topicId}
## Overview
*Topic tracking file.*
## Notes
`;
fs2.writeFileSync(topicFile, content2, "utf8");
return {
topic: {
id: topicId,
name: topicId,
title: topicId,
status: newStatus
}
};
}
let content = fs2.readFileSync(topicFile, "utf8");
let title = topicId;
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
if (frontmatterMatch) {
let frontmatter = frontmatterMatch[1];
const titleMatch = frontmatter.match(/title:\s*["']?([^"'\n]+)["']?/i);
if (titleMatch) title = titleMatch[1];
if (frontmatter.includes("status:")) {
frontmatter = frontmatter.replace(
/status:\s*(active|resolved|parked)/i,
`status: ${newStatus}`
);
} else {
frontmatter = frontmatter.trim() + `
status: ${newStatus}`;
}
content = content.replace(/^---\n[\s\S]*?\n---/, `---
${frontmatter}
---`);
} else {
const headerMatch = content.match(/^#\s*(.+)/m);
if (headerMatch) title = headerMatch[1];
const frontmatter = `---
title: ${title}
status: ${newStatus}
category: general
created: ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}
---
`;
content = frontmatter + content;
}
fs2.writeFileSync(topicFile, content, "utf8");
return {
topic: {
id: topicId,
name: topicId,
title,
status: newStatus
}
};
}
module2.exports = {
getCerebroTopics: getCerebroTopics2,
updateTopicStatus: updateTopicStatus2
};
}
});
// src/tokens.js
var require_tokens = __commonJS({
"src/tokens.js"(exports2, module2) {
var fs2 = require("fs");
var path2 = require("path");
var { formatNumber, formatTokens } = require_utils();
var TOKEN_RATES = {
input: 15,
// $15/1M input tokens
output: 75,
// $75/1M output tokens
cacheRead: 1.5,
// $1.50/1M (90% discount from input)
cacheWrite: 18.75
// $18.75/1M (25% premium on input)
};
var tokenUsageCache = { data: null, timestamp: 0, refreshing: false };
var TOKEN_USAGE_CACHE_TTL = 3e4;
var refreshInterval = null;
function emptyUsageBucket() {
return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, requests: 0 };
}
async function refreshTokenUsageAsync2(getOpenClawDir2) {
if (tokenUsageCache.refreshing) return;
tokenUsageCache.refreshing = true;
try {
const sessionsDir = path2.join(getOpenClawDir2(), "agents", "main", "sessions");
const files = await fs2.promises.readdir(sessionsDir);
const jsonlFiles = files.filter((f) => f.endsWith(".jsonl"));
const now = Date.now();
const oneDayAgo = now - 24 * 60 * 60 * 1e3;
const threeDaysAgo = now - 3 * 24 * 60 * 60 * 1e3;
const sevenDaysAgo = now - 7 * 24 * 60 * 60 * 1e3;
const usage24h = emptyUsageBucket();
const usage3d = emptyUsageBucket();
const usage7d = emptyUsageBucket();
const batchSize = 50;
for (let i = 0; i < jsonlFiles.length; i += batchSize) {
const batch = jsonlFiles.slice(i, i + batchSize);
await Promise.all(
batch.map(async (file) => {
const filePath = path2.join(sessionsDir, file);
try {
const stat = await fs2.promises.stat(filePath);
if (stat.mtimeMs < sevenDaysAgo) return;
const content = await fs2.promises.readFile(filePath, "utf8");
const lines = content.trim().split("\n");
for (const line of lines) {
if (!line) continue;
try {
const entry = JSON.parse(line);
const entryTime = entry.timestamp ? new Date(entry.timestamp).getTime() : 0;
if (entryTime < sevenDaysAgo) continue;
if (entry.message?.usage) {
const u = entry.message.usage;
const input = u.input || 0;
const output = u.output || 0;
const cacheRead = u.cacheRead || 0;
const cacheWrite = u.cacheWrite || 0;
const cost = u.cost?.total || 0;
if (entryTime >= oneDayAgo) {
usage24h.input += input;
usage24h.output += output;
usage24h.cacheRead += cacheRead;
usage24h.cacheWrite += cacheWrite;
usage24h.cost += cost;
usage24h.requests++;
}
if (entryTime >= threeDaysAgo) {
usage3d.input += input;
usage3d.output += output;
usage3d.cacheRead += cacheRead;
usage3d.cacheWrite += cacheWrite;
usage3d.cost += cost;
usage3d.requests++;
}
usage7d.input += input;
usage7d.output += output;
usage7d.cacheRead += cacheRead;
usage7d.cacheWrite += cacheWrite;
usage7d.cost += cost;
usage7d.requests++;
}
} catch (e) {
}
}
} catch (e) {
}
})
);
await new Promise((resolve) => setImmediate(resolve));
}
const finalizeBucket = (bucket) => ({
...bucket,
tokensNoCache: bucket.input + bucket.output,
tokensWithCache: bucket.input + bucket.output + bucket.cacheRead + bucket.cacheWrite
});
const result = {
// Primary (24h) for backward compatibility
...finalizeBucket(usage24h),
// All three windows
windows: {
"24h": finalizeBucket(usage24h),
"3d": finalizeBucket(usage3d),
"7d": finalizeBucket(usage7d)
}
};
tokenUsageCache = { data: result, timestamp: Date.now(), refreshing: false };
console.log(
`[Token Usage] Cached: 24h=${usage24h.requests} 3d=${usage3d.requests} 7d=${usage7d.requests} requests`
);
} catch (e) {
console.error("[Token Usage] Refresh error:", e.message);
tokenUsageCache.refreshing = false;
}
}
function getDailyTokenUsage2(getOpenClawDir2) {
const now = Date.now();
const isStale = now - tokenUsageCache.timestamp > TOKEN_USAGE_CACHE_TTL;
if (isStale && !tokenUsageCache.refreshing && getOpenClawDir2) {
refreshTokenUsageAsync2(getOpenClawDir2);
}
const emptyResult = {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
cost: 0,
requests: 0,
tokensNoCache: 0,
tokensWithCache: 0,
windows: {
"24h": {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
cost: 0,
requests: 0,
tokensNoCache: 0,
tokensWithCache: 0
},
"3d": {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
cost: 0,
requests: 0,
tokensNoCache: 0,
tokensWithCache: 0
},
"7d": {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
cost: 0,
requests: 0,
tokensNoCache: 0,
tokensWithCache: 0
}
}
};
return tokenUsageCache.data || emptyResult;
}
function calculateCostForBucket(bucket, rates = TOKEN_RATES) {
const inputCost = bucket.input / 1e6 * rates.input;
const outputCost = bucket.output / 1e6 * rates.output;
const cacheReadCost = bucket.cacheRead / 1e6 * rates.cacheRead;
const cacheWriteCost = bucket.cacheWrite / 1e6 * rates.cacheWrite;
return {
inputCost,
outputCost,
cacheReadCost,
cacheWriteCost,
totalCost: inputCost + outputCost + cacheReadCost + cacheWriteCost
};
}
function getCostBreakdown2(config, getSessions, getOpenClawDir2) {
const usage = getDailyTokenUsage2(getOpenClawDir2);
if (!usage) {
return { error: "Failed to get usage data" };
}
const costs = calculateCostForBucket(usage);
const planCost = config.billing?.claudePlanCost || 200;
const planName = config.billing?.claudePlanName || "Claude Code Max";
const windowConfigs = {
"24h": { days: 1, label: "24h" },
"3d": { days: 3, label: "3dma" },
"7d": { days: 7, label: "7dma" }
};
const windows = {};
for (const [key, windowConfig] of Object.entries(windowConfigs)) {
const bucket = usage.windows?.[key] || usage;
const bucketCosts = calculateCostForBucket(bucket);
const dailyAvg = bucketCosts.totalCost / windowConfig.days;
const monthlyProjected = dailyAvg * 30;
const monthlySavings = monthlyProjected - planCost;
windows[key] = {
label: windowConfig.label,
days: windowConfig.days,
totalCost: bucketCosts.totalCost,
dailyAvg,
monthlyProjected,
monthlySavings,
savingsPercent: monthlySavings > 0 ? Math.round(monthlySavings / monthlyProjected * 100) : 0,
requests: bucket.requests,
tokens: {
input: bucket.input,
output: bucket.output,
cacheRead: bucket.cacheRead,
cacheWrite: bucket.cacheWrite
}
};
}
return {
// Raw token counts (24h for backward compatibility)
inputTokens: usage.input,
outputTokens: usage.output,
cacheRead: usage.cacheRead,
cacheWrite: usage.cacheWrite,
requests: usage.requests,
// Pricing rates
rates: {
input: TOKEN_RATES.input.toFixed(2),
output: TOKEN_RATES.output.toFixed(2),
cacheRead: TOKEN_RATES.cacheRead.toFixed(2),
cacheWrite: TOKEN_RATES.cacheWrite.toFixed(2)
},
// Cost calculation breakdown (24h)
calculation: {
inputCost: costs.inputCost,
outputCost: costs.outputCost,
cacheReadCost: costs.cacheReadCost,
cacheWriteCost: costs.cacheWriteCost
},
// Totals (24h for backward compatibility)
totalCost: costs.totalCost,
planCost,
planName,
// Period
period: "24 hours",
// Multi-window data for moving averages
windows,
// Top sessions by tokens
topSessions: getTopSessionsByTokens(5, getSessions)
};
}
function getTopSessionsByTokens(limit = 5, getSessions) {
try {
const sessions2 = getSessions({ limit: null });
return sessions2.filter((s) => s.tokens > 0).sort((a, b) => b.tokens - a.tokens).slice(0, limit).map((s) => ({
label: s.label,
tokens: s.tokens,
channel: s.channel,
active: s.active
}));
} catch (e) {
console.error("[TopSessions] Error:", e.message);
return [];
}
}
function getTokenStats2(sessions2, capacity, config = {}) {
let activeMainCount = capacity?.main?.active ?? 0;
let activeSubagentCount = capacity?.subagent?.active ?? 0;
let activeCount = activeMainCount + activeSubagentCount;
let mainLimit = capacity?.main?.max ?? 12;
let subagentLimit = capacity?.subagent?.max ?? 24;
if (!capacity && sessions2 && sessions2.length > 0) {
activeCount = 0;
activeMainCount = 0;
activeSubagentCount = 0;
sessions2.forEach((s) => {
if (s.active) {
activeCount++;
if (s.key && s.key.includes(":subagent:")) {
activeSubagentCount++;
} else {
activeMainCount++;
}
}
});
}
const usage = getDailyTokenUsage2();
const totalInput = usage?.input || 0;
const totalOutput = usage?.output || 0;
const total = totalInput + totalOutput;
const costs = calculateCostForBucket(usage);
const estCost = costs.totalCost;
const planCost = config?.billing?.claudePlanCost ?? 200;
const planName = config?.billing?.claudePlanName ?? "Claude Code Max";
const monthlyApiCost = estCost * 30;
const monthlySavings = monthlyApiCost - planCost;
const savingsPositive = monthlySavings > 0;
const sessionCount = sessions2?.length || 1;
const avgTokensPerSession = Math.round(total / sessionCount);
const avgCostPerSession = estCost / sessionCount;
const windowConfigs = {
"24h": { days: 1, label: "24h" },
"3dma": { days: 3, label: "3dma" },
"7dma": { days: 7, label: "7dma" }
};
const savingsWindows = {};
for (const [key, windowConfig] of Object.entries(windowConfigs)) {
const bucketKey = key.replace("dma", "d").replace("24h", "24h");
const bucket = usage.windows?.[bucketKey === "24h" ? "24h" : bucketKey] || usage;
const bucketCosts = calculateCostForBucket(bucket);
const dailyAvg = bucketCosts.totalCost / windowConfig.days;
const monthlyProjected = dailyAvg * 30;
const windowSavings = monthlyProjected - planCost;
const windowSavingsPositive = windowSavings > 0;
savingsWindows[key] = {
label: windowConfig.label,
estCost: `$${formatNumber(dailyAvg)}`,
estMonthlyCost: `$${Math.round(monthlyProjected).toLocaleString()}`,
estSavings: windowSavingsPositive ? `$${formatNumber(windowSavings)}/mo` : null,
savingsPercent: windowSavingsPositive ? Math.round(windowSavings / monthlyProjected * 100) : 0,
requests: bucket.requests
};
}
return {
total: formatTokens(total),
input: formatTokens(totalInput),
output: formatTokens(totalOutput),
cacheRead: formatTokens(usage?.cacheRead || 0),
cacheWrite: formatTokens(usage?.cacheWrite || 0),
requests: usage?.requests || 0,
activeCount,
activeMainCount,
activeSubagentCount,
mainLimit,
subagentLimit,
estCost: `$${formatNumber(estCost)}`,
planCost: `$${planCost.toFixed(0)}`,
planName,
// 24h savings (backward compatible)
estSavings: savingsPositive ? `$${formatNumber(monthlySavings)}/mo` : null,
savingsPercent: savingsPositive ? Math.round(monthlySavings / monthlyApiCost * 100) : 0,
estMonthlyCost: `$${Math.round(monthlyApiCost).toLocaleString()}`,
// Multi-window savings (24h, 3da, 7da)
savingsWindows,
// Per-session averages
avgTokensPerSession: formatTokens(avgTokensPerSession),
avgCostPerSession: `$${avgCostPerSession.toFixed(2)}`,
sessionCount
};
}
function startTokenUsageRefresh2(getOpenClawDir2) {
refreshTokenUsageAsync2(getOpenClawDir2);
if (refreshInterval) {
clearInterval(refreshInterval);
}
refreshInterval = setInterval(() => {
refreshTokenUsageAsync2(getOpenClawDir2);
}, TOKEN_USAGE_CACHE_TTL);
return refreshInterval;
}
module2.exports = {
TOKEN_RATES,
emptyUsageBucket,
refreshTokenUsageAsync: refreshTokenUsageAsync2,
getDailyTokenUsage: getDailyTokenUsage2,
calculateCostForBucket,
getCostBreakdown: getCostBreakdown2,
getTopSessionsByTokens,
getTokenStats: getTokenStats2,
startTokenUsageRefresh: startTokenUsageRefresh2
};
}
});
// src/llm-usage.js
var require_llm_usage = __commonJS({
"src/llm-usage.js"(exports2, module2) {
var fs2 = require("fs");
var path2 = require("path");
var { execFile } = require("child_process");
var { getSafeEnv } = require_openclaw();
var llmUsageCache = { data: null, timestamp: 0, refreshing: false };
var LLM_CACHE_TTL_MS = 6e4;
function refreshLlmUsageAsync() {
if (llmUsageCache.refreshing) return;
llmUsageCache.refreshing = true;
const profile = process.env.OPENCLAW_PROFILE || "";
const args2 = profile ? ["--profile", profile, "status", "--usage", "--json"] : ["status", "--usage", "--json"];
execFile(
"openclaw",
args2,
{ encoding: "utf8", timeout: 2e4, env: getSafeEnv() },
(err, stdout) => {
llmUsageCache.refreshing = false;
if (err) {
console.error("[LLM Usage] Async refresh failed:", err.message);
return;
}
try {
const jsonStart = stdout.indexOf("{");
const jsonStr = jsonStart >= 0 ? stdout.slice(jsonStart) : stdout;
const parsed = JSON.parse(jsonStr);
if (parsed.usage) {
const result = transformLiveUsageData(parsed.usage);
llmUsageCache.data = result;
llmUsageCache.timestamp = Date.now();
console.log("[LLM Usage] Cache refreshed");
}
} catch (e) {
console.error("[LLM Usage] Parse error:", e.message);
}
}
);
}
function transformLiveUsageData(usage) {
const anthropic = usage.providers?.find((p) => p.provider === "anthropic");
const codexProvider = usage.providers?.find((p) => p.provider === "openai-codex");
if (anthropic?.error) {
return {
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
source: "error",
error: anthropic.error,
errorType: anthropic.error.includes("403") ? "auth" : "unknown",
claude: {
session: { usedPct: null, remainingPct: null, resetsIn: null, error: anthropic.error },
weekly: { usedPct: null, remainingPct: null, resets: null, error: anthropic.error },
sonnet: { usedPct: null, remainingPct: null, resets: null, error: anthropic.error },
lastSynced: null
},
codex: { sessionsToday: 0, tasksToday: 0, usage5hPct: 0, usageDayPct: 0 },
routing: {
total: 0,
claudeTasks: 0,
codexTasks: 0,
claudePct: 0,
codexPct: 0,
codexFloor: 20
}
};
}
const session5h = anthropic?.windows?.find((w) => w.label === "5h");
const weekAll = anthropic?.windows?.find((w) => w.label === "Week");
const sonnetWeek = anthropic?.windows?.find((w) => w.label === "Sonnet");
const codex5h = codexProvider?.windows?.find((w) => w.label === "5h");
const codexDay = codexProvider?.windows?.find((w) => w.label === "Day");
const formatReset = (resetAt) => {
if (!resetAt) return "?";
const diff = resetAt - Date.now();
if (diff < 0) return "now";
if (diff < 36e5) return Math.round(diff / 6e4) + "m";
if (diff < 864e5) return Math.round(diff / 36e5) + "h";
return Math.round(diff / 864e5) + "d";
};
return {
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
source: "live",
claude: {
session: {
usedPct: Math.round(session5h?.usedPercent || 0),
remainingPct: Math.round(100 - (session5h?.usedPercent || 0)),
resetsIn: formatReset(session5h?.resetAt)
},
weekly: {
usedPct: Math.round(weekAll?.usedPercent || 0),
remainingPct: Math.round(100 - (weekAll?.usedPercent || 0)),
resets: formatReset(weekAll?.resetAt)
},
sonnet: {
usedPct: Math.round(sonnetWeek?.usedPercent || 0),
remainingPct: Math.round(100 - (sonnetWeek?.usedPercent || 0)),
resets: formatReset(sonnetWeek?.resetAt)
},
lastSynced: (/* @__PURE__ */ new Date()).toISOString()
},
codex: {
sessionsToday: 0,
tasksToday: 0,
usage5hPct: Math.round(codex5h?.usedPercent || 0),
usageDayPct: Math.round(codexDay?.usedPercent || 0)
},
routing: { total: 0, claudeTasks: 0, codexTasks: 0, claudePct: 0, codexPct: 0, codexFloor: 20 }
};
}
function getLlmUsage2(statePath) {
const now = Date.now();
if (!llmUsageCache.data || now - llmUsageCache.timestamp > LLM_CACHE_TTL_MS) {
refreshLlmUsageAsync();
}
if (llmUsageCache.data && llmUsageCache.data.source !== "error") {
return llmUsageCache.data;
}
const stateFile = path2.join(statePath, "llm-routing.json");
try {
if (fs2.existsSync(stateFile)) {
const data = JSON.parse(fs2.readFileSync(stateFile, "utf8"));
const sessionValid = data.claude?.session?.resets_in && data.claude.session.resets_in !== "unknown";
const weeklyValid = data.claude?.weekly_all_models?.resets && data.claude.weekly_all_models.resets !== "unknown";
if (sessionValid || weeklyValid) {
return {
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
source: "file",
claude: {
session: {
usedPct: Math.round((data.claude?.session?.used_pct || 0) * 100),
remainingPct: Math.round((data.claude?.session?.remaining_pct || 1) * 100),
resetsIn: data.claude?.session?.resets_in || "?"
},
weekly: {
usedPct: Math.round((data.claude?.weekly_all_models?.used_pct || 0) * 100),
remainingPct: Math.round((data.claude?.weekly_all_models?.remaining_pct || 1) * 100),
resets: data.claude?.weekly_all_models?.resets || "?"
},
sonnet: {
usedPct: Math.round((data.claude?.weekly_sonnet?.used_pct || 0) * 100),
remainingPct: Math.round((data.claude?.weekly_sonnet?.remaining_pct || 1) * 100),
resets: data.claude?.weekly_sonnet?.resets || "?"
},
lastSynced: data.claude?.last_synced || null
},
codex: {
sessionsToday: data.codex?.sessions_today || 0,
tasksToday: data.codex?.tasks_today || 0,
usage5hPct: data.codex?.usage_5h_pct || 0,
usageDayPct: data.codex?.usage_day_pct || 0
},
routing: {
total: data.routing?.total_tasks || 0,
claudeTasks: data.routing?.claude_tasks || 0,
codexTasks: data.routing?.codex_tasks || 0,
claudePct: data.routing?.total_tasks > 0 ? Math.round(data.routing.claude_tasks / data.routing.total_tasks * 100) : 0,
codexPct: data.routing?.total_tasks > 0 ? Math.round(data.routing.codex_tasks / data.routing.total_tasks * 100) : 0,
codexFloor: Math.round((data.routing?.codex_floor_pct || 0.2) * 100)
}
};
}
}
} catch (e) {
console.error("[LLM Usage] File fallback failed:", e.message);
}
return {
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
source: "error",
error: "API key lacks user:profile OAuth scope",
errorType: "auth",
claude: {
session: { usedPct: null, remainingPct: null, resetsIn: null, error: "Auth required" },
weekly: { usedPct: null, remainingPct: null, resets: null, error: "Auth required" },
sonnet: { usedPct: null, remainingPct: null, resets: null, error: "Auth required" },
lastSynced: null
},
codex: { sessionsToday: 0, tasksToday: 0, usage5hPct: 0, usageDayPct: 0 },
routing: { total: 0, claudeTasks: 0, codexTasks: 0, claudePct: 0, codexPct: 0, codexFloor: 20 }
};
}
function getRoutingStats2(skillsPath, statePath, hours = 24) {
const safeHours = parseInt(hours, 10) || 24;
try {
const { execFileSync } = require("child_process");
const skillDir = path2.join(skillsPath, "llm_routing");
const output = execFileSync(
"python",
["-m", "llm_routing", "stats", "--hours", String(safeHours), "--json"],
{
encoding: "utf8",
timeout: 1e4,
cwd: skillDir,
env: getSafeEnv()
}
);
return JSON.parse(output);
} catch (e) {
try {
const logFile = path2.join(statePath, "routing-log.jsonl");
if (!fs2.existsSync(logFile)) {
return { total_requests: 0, by_model: {}, by_task_type: {} };
}
const cutoff = Date.now() - hours * 3600 * 1e3;
const lines = fs2.readFileSync(logFile, "utf8").trim().split("\n").filter(Boolean);
const stats = {
total_requests: 0,
by_model: {},
by_task_type: {},
escalations: 0,
avg_latency_ms: 0,
success_rate: 0
};
let latencies = [];
let successes = 0;
for (const line of lines) {
try {
const entry = JSON.parse(line);
const ts = new Date(entry.timestamp).getTime();
if (ts < cutoff) continue;
stats.total_requests++;
const model = entry.selected_model || "unknown";
stats.by_model[model] = (stats.by_model[model] || 0) + 1;
const tt = entry.task_type || "unknown";
stats.by_task_type[tt] = (stats.by_task_type[tt] || 0) + 1;
if (entry.escalation_reason) stats.escalations++;
if (entry.latency_ms) latencies.push(entry.latency_ms);
if (entry.success === true) successes++;
} catch {
}
}
if (latencies.length > 0) {
stats.avg_latency_ms = Math.round(latencies.reduce((a, b) => a + b, 0) / latencies.length);
}
if (stats.total_requests > 0) {
stats.success_rate = Math.round(successes / stats.total_requests * 100);
}
return stats;
} catch (e2) {
console.error("Failed to read routing stats:", e2.message);
return { error: e2.message };
}
}
}
function startLlmUsageRefresh2() {
setTimeout(() => refreshLlmUsageAsync(), 1e3);
setInterval(() => refreshLlmUsageAsync(), LLM_CACHE_TTL_MS);
}
module2.exports = {
refreshLlmUsageAsync,
transformLiveUsageData,
getLlmUsage: getLlmUsage2,
getRoutingStats: getRoutingStats2,
startLlmUsageRefresh: startLlmUsageRefresh2
};
}
});
// src/actions.js
var require_actions = __commonJS({
"src/actions.js"(exports2, module2) {
var ALLOWED_ACTIONS = /* @__PURE__ */ new Set([
"gateway-status",
"gateway-restart",
"sessions-list",
"cron-list",
"health-check",
"clear-stale-sessions"
]);
function executeAction2(action, deps) {
const { runOpenClaw: runOpenClaw2, extractJSON: extractJSON2, PORT: PORT2 } = deps;
const results = { success: false, action, output: "", error: null };
if (!ALLOWED_ACTIONS.has(action)) {
results.error = `Unknown action: ${action}`;
return results;
}
try {
switch (action) {
case "gateway-status":
results.output = runOpenClaw2("gateway status 2>&1") || "Unknown";
results.success = true;
break;
case "gateway-restart":
results.output = "To restart gateway, run: openclaw gateway restart";
results.success = true;
results.note = "Dashboard cannot restart gateway for safety";
break;
case "sessions-list":
results.output = runOpenClaw2("sessions 2>&1") || "No sessions";
results.success = true;
break;
case "cron-list":
results.output = runOpenClaw2("cron list 2>&1") || "No cron jobs";
results.success = true;
break;
case "health-check": {
const gateway = runOpenClaw2("gateway status 2>&1");
const sessions2 = runOpenClaw2("sessions --json 2>&1");
let sessionCount = 0;
try {
const data = JSON.parse(sessions2);
sessionCount = data.sessions?.length || 0;
} catch (e) {
}
results.output = [
`Gateway: ${gateway?.includes("running") ? "OK Running" : "NOT Running"}`,
`Sessions: ${sessionCount}`,
`Dashboard: OK Running on port ${PORT2}`
].join("\n");
results.success = true;
break;
}
case "clear-stale-sessions": {
const staleOutput = runOpenClaw2("sessions --json 2>&1");
let staleCount = 0;
try {
const staleJson = extractJSON2(staleOutput);
if (staleJson) {
const data = JSON.parse(staleJson);
staleCount = (data.sessions || []).filter((s) => s.ageMs > 24 * 60 * 60 * 1e3).length;
}
} catch (e) {
}
results.output = `Found ${staleCount} stale sessions (>24h old).
To clean: openclaw sessions prune`;
results.success = true;
break;
}
}
} catch (e) {
results.error = e.message;
}
return results;
}
module2.exports = { executeAction: executeAction2, ALLOWED_ACTIONS };
}
});
// src/data.js
var require_data = __commonJS({
"src/data.js"(exports2, module2) {
var fs2 = require("fs");
var path2 = require("path");
function migrateDataDir2(dataDir, legacyDataDir) {
try {
if (!fs2.existsSync(legacyDataDir)) return;
if (!fs2.existsSync(dataDir)) {
fs2.mkdirSync(dataDir, { recursive: true });
}
const legacyFiles = fs2.readdirSync(legacyDataDir);
if (legacyFiles.length === 0) return;
let migrated = 0;
for (const file of legacyFiles) {
const srcPath = path2.join(legacyDataDir, file);
const destPath = path2.join(dataDir, file);
if (fs2.existsSync(destPath)) continue;
const stat = fs2.statSync(srcPath);
if (stat.isFile()) {
fs2.copyFileSync(srcPath, destPath);
migrated++;
console.log(`[Migration] Copied ${file} to profile-aware data dir`);
}
}
if (migrated > 0) {
console.log(`[Migration] Migrated ${migrated} file(s) to ${dataDir}`);
console.log(`[Migration] Legacy data preserved at ${legacyDataDir}`);
}
} catch (e) {
console.error("[Migration] Failed to migrate data:", e.message);
}
}
module2.exports = { migrateDataDir: migrateDataDir2 };
}
});
// src/state.js
var require_state = __commonJS({
"src/state.js"(exports2, module2) {
var fs2 = require("fs");
var os = require("os");
var path2 = require("path");
var { execFileSync } = require("child_process");
var { formatBytes, formatTimeAgo } = require_utils();
function createStateModule2(deps) {
const {
CONFIG: CONFIG2,
getOpenClawDir: getOpenClawDir2,
getSessions,
getSystemVitals: getSystemVitals2,
getCronJobs: getCronJobs2,
loadOperators: loadOperators2,
getLlmUsage: getLlmUsage2,
getDailyTokenUsage: getDailyTokenUsage2,
getTokenStats: getTokenStats2,
getCerebroTopics: getCerebroTopics2,
runOpenClaw: runOpenClaw2,
extractJSON: extractJSON2,
readTranscript
} = deps;
const PATHS2 = CONFIG2.paths;
let cachedState = null;
let lastStateUpdate = 0;
const STATE_CACHE_TTL = 3e4;
let stateRefreshInterval = null;
function getSystemStatus() {
const hostname = os.hostname();
let uptime = "\u2014";
try {
const uptimeRaw = execFileSync("uptime", [], { encoding: "utf8" });
const match = uptimeRaw.match(/up\s+([^,]+)/);
if (match) uptime = match[1].trim();
} catch (e) {
}
let gateway = "Unknown";
try {
const status = runOpenClaw2("gateway status 2>/dev/null");
if (status && status.includes("running")) {
gateway = "Running";
} else if (status && status.includes("stopped")) {
gateway = "Stopped";
}
} catch (e) {
}
return {
hostname,
gateway,
model: "claude-opus-4-5",
uptime
};
}
function getRecentActivity() {
const activities = [];
const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
const memoryFile = path2.join(PATHS2.memory, `${today}.md`);
try {
if (fs2.existsSync(memoryFile)) {
const content = fs2.readFileSync(memoryFile, "utf8");
const lines = content.split("\n").filter((l) => l.startsWith("- "));
lines.slice(-5).forEach((line) => {
const text = line.replace(/^- /, "").slice(0, 80);
activities.push({
icon: text.includes("\u2705") ? "\u2705" : text.includes("\u274C") ? "\u274C" : "\u{1F4DD}",
text: text.replace(/[\u2705\u274C\uD83D\uDCDD\uD83D\uDD27]/g, "").trim(),
time: today
});
});
}
} catch (e) {
console.error("Failed to read activity:", e.message);
}
return activities.reverse();
}
function getCapacity() {
const result = {
main: { active: 0, max: 12 },
subagent: { active: 0, max: 24 }
};
const openclawDir = getOpenClawDir2();
try {
const configPath = path2.join(openclawDir, "openclaw.json");
if (fs2.existsSync(configPath)) {
const config = JSON.parse(fs2.readFileSync(configPath, "utf8"));
if (config?.agents?.defaults?.maxConcurrent) {
result.main.max = config.agents.defaults.maxConcurrent;
}
if (config?.agents?.defaults?.subagents?.maxConcurrent) {
result.subagent.max = config.agents.defaults.subagents.maxConcurrent;
}
}
} catch (e) {
}
try {
const output = runOpenClaw2("sessions --json 2>/dev/null");
const jsonStr = extractJSON2(output);
if (jsonStr) {
const data = JSON.parse(jsonStr);
const sessions2 = data.sessions || [];
const fiveMinMs = 5 * 60 * 1e3;
for (const s of sessions2) {
if (s.ageMs > fiveMinMs) continue;
const key = s.key || "";
if (key.includes(":subagent:") || key.includes(":cron:")) {
result.subagent.active++;
} else {
result.main.active++;
}
}
return result;
}
} catch (e) {
console.error("Failed to get capacity from sessions, falling back to filesystem:", e.message);
}
try {
const sessionsDir = path2.join(openclawDir, "agents", "main", "sessions");
if (fs2.existsSync(sessionsDir)) {
const fiveMinAgo = Date.now() - 5 * 60 * 1e3;
const files = fs2.readdirSync(sessionsDir).filter((f) => f.endsWith(".jsonl"));
let mainActive = 0;
let subActive = 0;
for (const file of files) {
try {
const filePath = path2.join(sessionsDir, file);
const stat = fs2.statSync(filePath);
if (stat.mtimeMs < fiveMinAgo) continue;
let isSubagent = false;
try {
const fd = fs2.openSync(filePath, "r");
const buffer = Buffer.alloc(512);
fs2.readSync(fd, buffer, 0, 512, 0);
fs2.closeSync(fd);
const firstLine = buffer.toString("utf8").split("\n")[0];
const parsed = JSON.parse(firstLine);
const key = parsed.key || parsed.id || "";
isSubagent = key.includes(":subagent:") || key.includes(":cron:");
} catch (parseErr) {
isSubagent = file.includes("subagent");
}
if (isSubagent) {
subActive++;
} else {
mainActive++;
}
} catch (e) {
}
}
result.main.active = mainActive;
result.subagent.active = subActive;
}
} catch (e) {
console.error("Failed to count active sessions from filesystem:", e.message);
}
return result;
}
function getMemoryStats() {
const memoryDir = PATHS2.memory;
const memoryFile = path2.join(PATHS2.workspace, "MEMORY.md");
const stats = {
totalFiles: 0,
totalSize: 0,
totalSizeFormatted: "0 B",
memoryMdSize: 0,
memoryMdSizeFormatted: "0 B",
memoryMdLines: 0,
recentFiles: [],
oldestFile: null,
newestFile: null
};
try {
const collectMemoryFiles = (dir, baseDir) => {
const entries = fs2.readdirSync(dir, { withFileTypes: true });
const files = [];
for (const entry of entries) {
const entryPath = path2.join(dir, entry.name);
if (entry.isDirectory()) {
files.push(...collectMemoryFiles(entryPath, baseDir));
} else if (entry.isFile() && (entry.name.endsWith(".md") || entry.name.endsWith(".json"))) {
const stat = fs2.statSync(entryPath);
const relativePath = path2.relative(baseDir, entryPath);
files.push({
name: relativePath,
size: stat.size,
sizeFormatted: formatBytes(stat.size),
modified: stat.mtime
});
}
}
return files;
};
if (fs2.existsSync(memoryFile)) {
const memStat = fs2.statSync(memoryFile);
stats.memoryMdSize = memStat.size;
stats.memoryMdSizeFormatted = formatBytes(memStat.size);
const content = fs2.readFileSync(memoryFile, "utf8");
stats.memoryMdLines = content.split("\n").length;
stats.totalSize += memStat.size;
stats.totalFiles++;
}
if (fs2.existsSync(memoryDir)) {
const files = collectMemoryFiles(memoryDir, memoryDir).sort(
(a, b) => b.modified - a.modified
);
stats.totalFiles += files.length;
files.forEach((f) => stats.totalSize += f.size);
stats.recentFiles = files.slice(0, 5).map((f) => ({
name: f.name,
sizeFormatted: f.sizeFormatted,
age: formatTimeAgo(f.modified)
}));
if (files.length > 0) {
stats.newestFile = files[0].name;
stats.oldestFile = files[files.length - 1].name;
}
}
stats.totalSizeFormatted = formatBytes(stats.totalSize);
} catch (e) {
console.error("Failed to get memory stats:", e.message);
}
return stats;
}
function getData() {
const allSessions = getSessions({ limit: null });
const pageSize = 20;
const displaySessions = allSessions.slice(0, pageSize);
const tokenStats = getTokenStats2(allSessions);
const capacity = getCapacity();
const memory = getMemoryStats();
const statusCounts = {
all: allSessions.length,
live: allSessions.filter((s) => s.active).length,
recent: allSessions.filter((s) => !s.active && s.recentlyActive).length,
idle: allSessions.filter((s) => !s.active && !s.recentlyActive).length
};
const totalPages = Math.ceil(allSessions.length / pageSize);
return {
sessions: displaySessions,
tokenStats,
capacity,
memory,
pagination: {
page: 1,
pageSize,
total: allSessions.length,
totalPages,
hasPrev: false,
hasNext: totalPages > 1
},
statusCounts
};
}
function getFullState() {
const now = Date.now();
if (cachedState && now - lastStateUpdate < STATE_CACHE_TTL) {
return cachedState;
}
let sessions2 = [];
let tokenStats = {};
let statusCounts = { all: 0, live: 0, recent: 0, idle: 0 };
let vitals = {};
let capacity = {};
let operators = { operators: [], roles: {} };
let llmUsage = {};
let cron = [];
let memory = {};
let cerebro = {};
let subagents = [];
let allSessions = [];
let totalSessionCount = 0;
try {
allSessions = getSessions({ limit: null });
totalSessionCount = allSessions.length;
sessions2 = allSessions.slice(0, 20);
} catch (e) {
console.error("[State] sessions:", e.message);
}
try {
vitals = getSystemVitals2();
} catch (e) {
console.error("[State] vitals:", e.message);
}
try {
capacity = getCapacity();
} catch (e) {
console.error("[State] capacity:", e.message);
}
try {
tokenStats = getTokenStats2(allSessions, capacity, CONFIG2);
} catch (e) {
console.error("[State] tokenStats:", e.message);
}
try {
const liveSessions = allSessions.filter((s) => s.active);
const recentSessions = allSessions.filter((s) => !s.active && s.recentlyActive);
const idleSessions = allSessions.filter((s) => !s.active && !s.recentlyActive);
statusCounts = {
all: totalSessionCount,
live: liveSessions.length,
recent: recentSessions.length,
idle: idleSessions.length
};
} catch (e) {
console.error("[State] statusCounts:", e.message);
}
try {
const operatorData = loadOperators2();
const operatorsWithStats = operatorData.operators.map((op) => {
const userSessions = allSessions.filter(
(s) => s.originator?.userId === op.id || s.originator?.userId === op.metadata?.slackId
);
return {
...op,
stats: {
activeSessions: userSessions.filter((s) => s.active).length,
totalSessions: userSessions.length,
lastSeen: userSessions.length > 0 ? new Date(
Date.now() - Math.min(...userSessions.map((s) => s.minutesAgo)) * 6e4
).toISOString() : op.lastSeen
}
};
});
operators = { ...operatorData, operators: operatorsWithStats };
} catch (e) {
console.error("[State] operators:", e.message);
}
try {
llmUsage = getLlmUsage2();
} catch (e) {
console.error("[State] llmUsage:", e.message);
}
try {
cron = getCronJobs2();
} catch (e) {
console.error("[State] cron:", e.message);
}
try {
memory = getMemoryStats();
} catch (e) {
console.error("[State] memory:", e.message);
}
try {
cerebro = getCerebroTopics2();
} catch (e) {
console.error("[State] cerebro:", e.message);
}
try {
const retentionHours = parseInt(process.env.SUBAGENT_RETENTION_HOURS || "12", 10);
const retentionMs = retentionHours * 60 * 60 * 1e3;
subagents = allSessions.filter((s) => s.sessionKey && s.sessionKey.includes(":subagent:")).filter((s) => (s.minutesAgo || 0) * 6e4 < retentionMs).map((s) => {
const match = s.sessionKey.match(/:subagent:([a-f0-9-]+)$/);
const subagentId = match ? match[1] : s.sessionId;
return {
id: subagentId,
shortId: subagentId.slice(0, 8),
task: s.label || s.displayName || "Sub-agent task",
tokens: s.tokens || 0,
ageMs: (s.minutesAgo || 0) * 6e4,
active: s.active,
recentlyActive: s.recentlyActive
};
});
} catch (e) {
console.error("[State] subagents:", e.message);
}
cachedState = {
vitals,
sessions: sessions2,
tokenStats,
statusCounts,
capacity,
operators,
llmUsage,
cron,
memory,
cerebro,
subagents,
pagination: {
page: 1,
pageSize: 20,
total: totalSessionCount,
totalPages: Math.max(1, Math.ceil(totalSessionCount / 20)),
hasPrev: false,
hasNext: totalSessionCount > 20
},
timestamp: now
};
lastStateUpdate = now;
return cachedState;
}
function refreshState() {
lastStateUpdate = 0;
return getFullState();
}
function startStateRefresh(broadcastSSE2, intervalMs = 3e4) {
if (stateRefreshInterval) return;
stateRefreshInterval = setInterval(() => {
try {
const newState = refreshState();
broadcastSSE2("update", newState);
} catch (e) {
console.error("[State] Refresh error:", e.message);
}
}, intervalMs);
console.log(`[State] Background refresh started (${intervalMs}ms interval)`);
}
function stopStateRefresh() {
if (stateRefreshInterval) {
clearInterval(stateRefreshInterval);
stateRefreshInterval = null;
console.log("[State] Background refresh stopped");
}
}
function getSubagentStatus() {
const subagents = [];
try {
const output = runOpenClaw2("sessions --json 2>/dev/null");
const jsonStr = extractJSON2(output);
if (jsonStr) {
const data = JSON.parse(jsonStr);
const subagentSessions = (data.sessions || []).filter(
(s) => s.key && s.key.includes(":subagent:")
);
for (const s of subagentSessions) {
const ageMs = s.ageMs || Infinity;
const isActive = ageMs < 5 * 60 * 1e3;
const isRecent = ageMs < 30 * 60 * 1e3;
const match = s.key.match(/:subagent:([a-f0-9-]+)$/);
const subagentId = match ? match[1] : s.sessionId;
const shortId = subagentId.slice(0, 8);
let taskSummary = "Unknown task";
let label = null;
const transcript = readTranscript(s.sessionId);
for (const entry of transcript.slice(0, 15)) {
if (entry.type === "message" && entry.message?.role === "user") {
const content = entry.message.content;
let text = "";
if (typeof content === "string") {
text = content;
} else if (Array.isArray(content)) {
const textPart = content.find((c) => c.type === "text");
if (textPart) text = textPart.text || "";
}
if (!text) continue;
const labelMatch = text.match(/Label:\s*([^\n]+)/i);
if (labelMatch) {
label = labelMatch[1].trim();
}
let taskMatch = text.match(/You were created to handle:\s*\*\*([^*]+)\*\*/i);
if (taskMatch) {
taskSummary = taskMatch[1].trim();
break;
}
taskMatch = text.match(/\*\*([A-Z]{2,5}-\d+:\s*[^*]+)\*\*/);
if (taskMatch) {
taskSummary = taskMatch[1].trim();
break;
}
const firstLine = text.split("\n")[0].replace(/^\*\*|\*\*$/g, "").trim();
if (firstLine.length > 10 && firstLine.length < 100) {
taskSummary = firstLine;
break;
}
}
}
const messageCount = transcript.filter(
(e) => e.type === "message" && e.message?.role
).length;
subagents.push({
id: subagentId,
shortId,
sessionId: s.sessionId,
label: label || shortId,
task: taskSummary,
model: s.model?.replace("anthropic/", "") || "unknown",
status: isActive ? "active" : isRecent ? "idle" : "stale",
ageMs,
ageFormatted: ageMs < 6e4 ? "Just now" : ageMs < 36e5 ? `${Math.round(ageMs / 6e4)}m ago` : `${Math.round(ageMs / 36e5)}h ago`,
messageCount,
tokens: s.totalTokens || 0
});
}
}
} catch (e) {
console.error("Failed to get subagent status:", e.message);
}
return subagents.sort((a, b) => a.ageMs - b.ageMs);
}
return {
getSystemStatus,
getRecentActivity,
getCapacity,
getMemoryStats,
getFullState,
refreshState,
startStateRefresh,
stopStateRefresh,
getData,
getSubagentStatus
};
}
module2.exports = { createStateModule: createStateModule2 };
}
});
// src/index.js
var http = require("http");
var fs = require("fs");
var path = require("path");
var args = process.argv.slice(2);
var cliProfile = null;
var cliPort = null;
for (let i = 0; i < args.length; i++) {
switch (args[i]) {
case "--profile":
case "-p":
cliProfile = args[++i];
break;
case "--port":
cliPort = parseInt(args[++i], 10);
break;
case "--help":
case "-h":
console.log(`
OpenClaw Command Center
Usage: node lib/server.js [options]
Options:
--profile, -p <name> OpenClaw profile (uses ~/.openclaw-<name>)
--port <port> Server port (default: 3333)
--help, -h Show this help
Environment:
OPENCLAW_PROFILE Same as --profile
PORT Same as --port
Examples:
node lib/server.js --profile production
node lib/server.js -p dev --port 3334
`);
process.exit(0);
}
}
if (cliProfile) {
process.env.OPENCLAW_PROFILE = cliProfile;
}
if (cliPort) {
process.env.PORT = cliPort.toString();
}
var { getVersion } = require_utils();
var { CONFIG, getOpenClawDir } = require_config();
var { handleJobsRequest, isJobsRoute } = require_jobs();
var { runOpenClaw, runOpenClawAsync, extractJSON } = require_openclaw();
var { getSystemVitals, checkOptionalDeps, getOptionalDeps } = require_vitals();
var { checkAuth, getUnauthorizedPage } = require_auth();
var { loadPrivacySettings, savePrivacySettings } = require_privacy();
var {
loadOperators,
saveOperators,
getOperatorBySlackId,
startOperatorsRefresh
} = require_operators();
var { createSessionsModule } = require_sessions();
var { getCronJobs } = require_cron();
var { getCerebroTopics, updateTopicStatus } = require_cerebro();
var {
getDailyTokenUsage,
getTokenStats,
getCostBreakdown,
startTokenUsageRefresh,
refreshTokenUsageAsync
} = require_tokens();
var { getLlmUsage, getRoutingStats, startLlmUsageRefresh } = require_llm_usage();
var { executeAction } = require_actions();
var { migrateDataDir } = require_data();
var { createStateModule } = require_state();
var PORT = CONFIG.server.port;
var DASHBOARD_DIR = path.join(__dirname, "../public");
var PATHS = CONFIG.paths;
var AUTH_CONFIG = {
mode: CONFIG.auth.mode,
token: CONFIG.auth.token,
allowedUsers: CONFIG.auth.allowedUsers,
allowedIPs: CONFIG.auth.allowedIPs,
publicPaths: CONFIG.auth.publicPaths
};
var DATA_DIR = path.join(getOpenClawDir(), "command-center", "data");
var LEGACY_DATA_DIR = path.join(DASHBOARD_DIR, "data");
var sseClients = /* @__PURE__ */ new Set();
function sendSSE(res, event, data) {
try {
res.write(`event: ${event}
data: ${JSON.stringify(data)}
`);
} catch (e) {
}
}
function broadcastSSE(event, data) {
for (const client of sseClients) {
sendSSE(client, event, data);
}
}
var sessions = createSessionsModule({
getOpenClawDir,
getOperatorBySlackId: (slackId) => getOperatorBySlackId(DATA_DIR, slackId),
runOpenClaw,
runOpenClawAsync,
extractJSON
});
var state = createStateModule({
CONFIG,
getOpenClawDir,
getSessions: (opts) => sessions.getSessions(opts),
getSystemVitals,
getCronJobs: () => getCronJobs(getOpenClawDir),
loadOperators: () => loadOperators(DATA_DIR),
getLlmUsage: () => getLlmUsage(PATHS.state),
getDailyTokenUsage: () => getDailyTokenUsage(getOpenClawDir),
getTokenStats,
getCerebroTopics: (opts) => getCerebroTopics(PATHS.cerebro, opts),
runOpenClaw,
extractJSON,
readTranscript: (sessionId) => sessions.readTranscript(sessionId)
});
process.nextTick(() => migrateDataDir(DATA_DIR, LEGACY_DATA_DIR));
startOperatorsRefresh(DATA_DIR, getOpenClawDir);
startLlmUsageRefresh();
startTokenUsageRefresh(getOpenClawDir);
function serveStatic(req, res) {
const requestUrl = new URL(req.url, `http://${req.headers.host || "localhost"}`);
const pathname = requestUrl.pathname === "/" ? "/index.html" : requestUrl.pathname;
if (pathname.includes("..")) {
res.writeHead(400);
res.end("Bad request");
return;
}
const normalizedPath = path.normalize(pathname).replace(/^[/\\]+/, "");
const filePath = path.join(DASHBOARD_DIR, normalizedPath);
const resolvedDashboardDir = path.resolve(DASHBOARD_DIR);
const resolvedFilePath = path.resolve(filePath);
if (!resolvedFilePath.startsWith(resolvedDashboardDir + path.sep) && resolvedFilePath !== resolvedDashboardDir) {
res.writeHead(403);
res.end("Forbidden");
return;
}
const ext = path.extname(filePath);
const contentTypes = {
".html": "text/html",
".css": "text/css",
".js": "text/javascript",
".json": "application/json",
".png": "image/png",
".svg": "image/svg+xml"
};
fs.readFile(filePath, (err, content) => {
if (err) {
res.writeHead(404);
res.end("Not found");
return;
}
const headers = { "Content-Type": contentTypes[ext] || "text/plain" };
if ([".html", ".css", ".js", ".json"].includes(ext)) {
headers["Cache-Control"] = "no-store";
}
res.writeHead(200, headers);
res.end(content);
});
}
function handleApi(req, res) {
const sessionsList = sessions.getSessions();
const capacity = state.getCapacity();
const tokenStats = getTokenStats(sessionsList, capacity, CONFIG);
const data = {
sessions: sessionsList,
cron: getCronJobs(getOpenClawDir),
system: state.getSystemStatus(),
activity: state.getRecentActivity(),
tokenStats,
capacity,
timestamp: (/* @__PURE__ */ new Date()).toISOString()
};
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify(data, null, 2));
}
var server = http.createServer((req, res) => {
res.setHeader("Access-Control-Allow-Origin", "*");
const urlParts = req.url.split("?");
const pathname = urlParts[0];
const query = new URLSearchParams(urlParts[1] || "");
if (pathname === "/api/health") {
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ status: "ok", port: PORT, timestamp: (/* @__PURE__ */ new Date()).toISOString() }));
return;
}
const isPublicPath = AUTH_CONFIG.publicPaths.some(
(p) => pathname === p || pathname.startsWith(p + "/")
);
if (!isPublicPath && AUTH_CONFIG.mode !== "none") {
const authResult = checkAuth(req, AUTH_CONFIG);
if (!authResult.authorized) {
console.log(`[AUTH] Denied: ${authResult.reason} (path: ${pathname})`);
res.writeHead(403, { "Content-Type": "text/html" });
res.end(getUnauthorizedPage(authResult.reason, authResult.user, AUTH_CONFIG));
return;
}
req.authUser = authResult.user;
if (authResult.user?.login || authResult.user?.email) {
console.log(
`[AUTH] Allowed: ${authResult.user.login || authResult.user.email} (path: ${pathname})`
);
} else {
console.log(`[AUTH] Allowed: ${req.socket?.remoteAddress} (path: ${pathname})`);
}
}
if (pathname === "/api/status") {
handleApi(req, res);
} else if (pathname === "/api/session") {
const sessionKey = query.get("key");
if (!sessionKey) {
res.writeHead(400, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: "Missing session key" }));
return;
}
const detail = sessions.getSessionDetail(sessionKey);
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify(detail, null, 2));
} else if (pathname === "/api/cerebro") {
const offset = parseInt(query.get("offset") || "0", 10);
const limit = parseInt(query.get("limit") || "20", 10);
const statusFilter = query.get("status") || "all";
const data = getCerebroTopics(PATHS.cerebro, { offset, limit, status: statusFilter });
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify(data, null, 2));
} else if (pathname.startsWith("/api/cerebro/topic/") && pathname.endsWith("/status") && req.method === "POST") {
const topicId = decodeURIComponent(
pathname.replace("/api/cerebro/topic/", "").replace("/status", "")
);
let body = "";
req.on("data", (chunk) => {
body += chunk;
});
req.on("end", () => {
try {
const { status: newStatus } = JSON.parse(body);
if (!newStatus || !["active", "resolved", "parked"].includes(newStatus)) {
res.writeHead(400, { "Content-Type": "application/json" });
res.end(
JSON.stringify({ error: "Invalid status. Must be: active, resolved, or parked" })
);
return;
}
const result = updateTopicStatus(PATHS.cerebro, topicId, newStatus);
if (result.error) {
res.writeHead(result.code || 500, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: result.error }));
return;
}
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify(result, null, 2));
} catch (e) {
res.writeHead(400, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: "Invalid JSON body" }));
}
});
return;
} else if (pathname === "/api/llm-quota") {
const data = getLlmUsage(PATHS.state);
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify(data, null, 2));
} else if (pathname === "/api/cost-breakdown") {
const data = getCostBreakdown(CONFIG, (opts) => sessions.getSessions(opts), getOpenClawDir);
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify(data, null, 2));
} else if (pathname === "/api/subagents") {
const data = state.getSubagentStatus();
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ subagents: data }, null, 2));
} else if (pathname === "/api/action") {
const action = query.get("action");
if (!action) {
res.writeHead(400, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: "Missing action parameter" }));
return;
}
const result = executeAction(action, { runOpenClaw, extractJSON, PORT });
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify(result, null, 2));
} else if (pathname === "/api/events") {
res.writeHead(200, {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive",
"X-Accel-Buffering": "no"
});
sseClients.add(res);
console.log(`[SSE] Client connected (total: ${sseClients.size})`);
sendSSE(res, "connected", { message: "Connected to Command Center", timestamp: Date.now() });
const cachedState = state.getFullState();
if (cachedState) {
sendSSE(res, "update", cachedState);
} else {
sendSSE(res, "update", { sessions: [], loading: true });
}
req.on("close", () => {
sseClients.delete(res);
console.log(`[SSE] Client disconnected (total: ${sseClients.size})`);
});
return;
} else if (pathname === "/api/whoami") {
res.writeHead(200, { "Content-Type": "application/json" });
res.end(
JSON.stringify(
{
authMode: AUTH_CONFIG.mode,
user: req.authUser || null
},
null,
2
)
);
} else if (pathname === "/api/about") {
res.writeHead(200, { "Content-Type": "application/json" });
res.end(
JSON.stringify(
{
name: "OpenClaw Command Center",
version: getVersion(),
description: "A Starcraft-inspired dashboard for AI agent orchestration",
license: "MIT",
repository: "https://github.com/jontsai/openclaw-command-center",
builtWith: ["OpenClaw", "Node.js", "Vanilla JS"],
inspirations: ["Starcraft", "Inside Out", "iStatMenus", "DaisyDisk", "Gmail"]
},
null,
2
)
);
} else if (pathname === "/api/state") {
const fullState = state.getFullState();
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify(fullState, null, 2));
} else if (pathname === "/api/vitals") {
const vitals = getSystemVitals();
const optionalDeps = getOptionalDeps();
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ vitals, optionalDeps }, null, 2));
} else if (pathname === "/api/capacity") {
const capacity = state.getCapacity();
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify(capacity, null, 2));
} else if (pathname === "/api/sessions") {
const page = parseInt(query.get("page")) || 1;
const pageSize = parseInt(query.get("pageSize")) || 20;
const statusFilter = query.get("status");
const allSessions = sessions.getSessions({ limit: null });
const statusCounts = {
all: allSessions.length,
live: allSessions.filter((s) => s.active).length,
recent: allSessions.filter((s) => !s.active && s.recentlyActive).length,
idle: allSessions.filter((s) => !s.active && !s.recentlyActive).length
};
let filteredSessions = allSessions;
if (statusFilter === "live") {
filteredSessions = allSessions.filter((s) => s.active);
} else if (statusFilter === "recent") {
filteredSessions = allSessions.filter((s) => !s.active && s.recentlyActive);
} else if (statusFilter === "idle") {
filteredSessions = allSessions.filter((s) => !s.active && !s.recentlyActive);
}
const total = filteredSessions.length;
const totalPages = Math.ceil(total / pageSize);
const offset = (page - 1) * pageSize;
const displaySessions = filteredSessions.slice(offset, offset + pageSize);
const tokenStats = getTokenStats(allSessions, state.getCapacity(), CONFIG);
const capacity = state.getCapacity();
res.writeHead(200, { "Content-Type": "application/json" });
res.end(
JSON.stringify(
{
sessions: displaySessions,
pagination: {
page,
pageSize,
total,
totalPages,
hasPrev: page > 1,
hasNext: page < totalPages
},
statusCounts,
tokenStats,
capacity
},
null,
2
)
);
} else if (pathname === "/api/cron") {
const cron = getCronJobs(getOpenClawDir);
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ cron }, null, 2));
} else if (pathname === "/api/operators") {
const method = req.method;
const data = loadOperators(DATA_DIR);
if (method === "GET") {
const allSessions = sessions.getSessions({ limit: null });
const operatorsWithStats = data.operators.map((op) => {
const userSessions = allSessions.filter(
(s) => s.originator?.userId === op.id || s.originator?.userId === op.metadata?.slackId
);
return {
...op,
stats: {
activeSessions: userSessions.filter((s) => s.active).length,
totalSessions: userSessions.length,
lastSeen: userSessions.length > 0 ? new Date(
Date.now() - Math.min(...userSessions.map((s) => s.minutesAgo)) * 6e4
).toISOString() : op.lastSeen
}
};
});
res.writeHead(200, { "Content-Type": "application/json" });
res.end(
JSON.stringify(
{
operators: operatorsWithStats,
roles: data.roles,
timestamp: Date.now()
},
null,
2
)
);
} else if (method === "POST") {
let body = "";
req.on("data", (chunk) => body += chunk);
req.on("end", () => {
try {
const newOp = JSON.parse(body);
const existingIdx = data.operators.findIndex((op) => op.id === newOp.id);
if (existingIdx >= 0) {
data.operators[existingIdx] = { ...data.operators[existingIdx], ...newOp };
} else {
data.operators.push({
...newOp,
createdAt: (/* @__PURE__ */ new Date()).toISOString()
});
}
if (saveOperators(DATA_DIR, data)) {
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ success: true, operator: newOp }));
} else {
res.writeHead(500, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: "Failed to save" }));
}
} catch (e) {
res.writeHead(400, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: "Invalid JSON" }));
}
});
return;
} else {
res.writeHead(405, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: "Method not allowed" }));
}
return;
} else if (pathname === "/api/llm-usage") {
const usage = getLlmUsage(PATHS.state);
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify(usage, null, 2));
} else if (pathname === "/api/routing-stats") {
const hours = parseInt(query.get("hours") || "24", 10);
const stats = getRoutingStats(PATHS.skills, PATHS.state, hours);
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify(stats, null, 2));
} else if (pathname === "/api/memory") {
const memory = state.getMemoryStats();
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ memory }, null, 2));
} else if (pathname === "/api/privacy") {
if (req.method === "GET") {
const settings = loadPrivacySettings(DATA_DIR);
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify(settings, null, 2));
} else if (req.method === "POST" || req.method === "PUT") {
let body = "";
req.on("data", (chunk) => body += chunk);
req.on("end", () => {
try {
const updates = JSON.parse(body);
const current = loadPrivacySettings(DATA_DIR);
const merged = {
version: current.version || 1,
hiddenTopics: updates.hiddenTopics ?? current.hiddenTopics ?? [],
hiddenSessions: updates.hiddenSessions ?? current.hiddenSessions ?? [],
hiddenCrons: updates.hiddenCrons ?? current.hiddenCrons ?? [],
hideHostname: updates.hideHostname ?? current.hideHostname ?? false
};
if (savePrivacySettings(DATA_DIR, merged)) {
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ success: true, settings: merged }));
} else {
res.writeHead(500, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: "Failed to save privacy settings" }));
}
} catch (e) {
res.writeHead(400, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: "Invalid JSON: " + e.message }));
}
});
return;
} else {
res.writeHead(405, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: "Method not allowed" }));
}
return;
} else if (isJobsRoute(pathname)) {
handleJobsRequest(req, res, pathname, query, req.method);
} else {
serveStatic(req, res);
}
});
server.listen(PORT, () => {
const profile = process.env.OPENCLAW_PROFILE;
console.log(`\u{1F99E} OpenClaw Command Center running at http://localhost:${PORT}`);
if (profile) {
console.log(` Profile: ${profile} (~/.openclaw-${profile})`);
}
console.log(` Press Ctrl+C to stop`);
setTimeout(async () => {
console.log("[Startup] Pre-warming caches in background...");
try {
await Promise.all([sessions.refreshSessionsCache(), refreshTokenUsageAsync(getOpenClawDir)]);
getSystemVitals();
console.log("[Startup] Caches warmed.");
} catch (e) {
console.log("[Startup] Cache warming error:", e.message);
}
checkOptionalDeps();
}, 100);
const SESSIONS_CACHE_TTL = 1e4;
setInterval(() => sessions.refreshSessionsCache(), SESSIONS_CACHE_TTL);
});
var sseRefreshing = false;
setInterval(() => {
if (sseClients.size > 0 && !sseRefreshing) {
sseRefreshing = true;
try {
const fullState = state.refreshState();
broadcastSSE("update", fullState);
broadcastSSE("heartbeat", { clients: sseClients.size, timestamp: Date.now() });
} catch (e) {
console.error("[SSE] Broadcast error:", e.message);
}
sseRefreshing = false;
}
}, 15e3);