openclaw-manager 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/bin/openclaw-manager.js +198 -0
  2. package/dist/app.js +39 -0
  3. package/dist/controllers/auth.controller.js +19 -0
  4. package/dist/controllers/cli.controller.js +10 -0
  5. package/dist/controllers/discord.controller.js +21 -0
  6. package/dist/controllers/jobs.controller.js +138 -0
  7. package/dist/controllers/process.controller.js +26 -0
  8. package/dist/controllers/quickstart.controller.js +11 -0
  9. package/dist/controllers/status.controller.js +10 -0
  10. package/dist/deps.js +1 -0
  11. package/dist/dev.js +156 -0
  12. package/dist/index.js +37 -0
  13. package/dist/lib/auth.js +57 -0
  14. package/dist/lib/commands.js +123 -0
  15. package/dist/lib/config.js +48 -0
  16. package/dist/lib/constants.js +9 -0
  17. package/dist/lib/gateway.js +37 -0
  18. package/dist/lib/jobs.js +117 -0
  19. package/dist/lib/onboarding.js +96 -0
  20. package/dist/lib/runner.js +99 -0
  21. package/dist/lib/static.js +69 -0
  22. package/dist/lib/system.js +31 -0
  23. package/dist/lib/utils.js +31 -0
  24. package/dist/middlewares/auth.js +23 -0
  25. package/dist/routes/auth.js +5 -0
  26. package/dist/routes/cli.js +4 -0
  27. package/dist/routes/discord.js +5 -0
  28. package/dist/routes/health.js +9 -0
  29. package/dist/routes/index.js +18 -0
  30. package/dist/routes/jobs.js +11 -0
  31. package/dist/routes/processes.js +6 -0
  32. package/dist/routes/quickstart.js +4 -0
  33. package/dist/routes/status.js +4 -0
  34. package/dist/services/auth.service.js +24 -0
  35. package/dist/services/cli.service.js +21 -0
  36. package/dist/services/discord.service.js +38 -0
  37. package/dist/services/jobs.service.js +307 -0
  38. package/dist/services/process.service.js +9 -0
  39. package/dist/services/quickstart.service.js +124 -0
  40. package/dist/services/resource.service.js +46 -0
  41. package/dist/services/status.service.js +32 -0
  42. package/package.json +18 -0
  43. package/web-dist/assets/index-BabnD_ew.js +13 -0
  44. package/web-dist/assets/index-CBtcOjoT.css +1 -0
  45. package/web-dist/docker.sh +62 -0
  46. package/web-dist/index.html +13 -0
  47. package/web-dist/install.ps1 +110 -0
  48. package/web-dist/install.sh +261 -0
  49. package/web-dist/stop.sh +52 -0
@@ -0,0 +1,57 @@
1
+ import { scryptSync, timingSafeEqual } from "node:crypto";
2
+ import fs from "node:fs";
3
+ import { resolveConfigPath } from "./config.js";
4
+ export function resolveAuthState(authDisabled) {
5
+ if (authDisabled) {
6
+ return { configured: false, verify: () => true };
7
+ }
8
+ const envUser = process.env.MANAGER_AUTH_USERNAME?.trim() ?? "";
9
+ const envPass = process.env.MANAGER_AUTH_PASSWORD ?? "";
10
+ if (envUser && envPass) {
11
+ return {
12
+ configured: true,
13
+ verify: (username, password) => username === envUser && password === envPass
14
+ };
15
+ }
16
+ const config = loadManagerConfig();
17
+ const auth = config?.auth;
18
+ if (!auth?.username || !auth?.salt || !auth?.hash) {
19
+ return { configured: false, verify: () => false };
20
+ }
21
+ return {
22
+ configured: true,
23
+ verify: (username, password) => verifyPassword(username, password, auth)
24
+ };
25
+ }
26
+ export function verifyAuthHeader(header, authState) {
27
+ const match = header.match(/^Basic\s+(.+)$/i);
28
+ if (!match)
29
+ return false;
30
+ const decoded = Buffer.from(match[1], "base64").toString("utf-8");
31
+ const [username, ...rest] = decoded.split(":");
32
+ const password = rest.join(":");
33
+ if (!username || !password)
34
+ return false;
35
+ return authState.verify(username, password);
36
+ }
37
+ function verifyPassword(username, password, auth) {
38
+ if (username !== auth.username)
39
+ return false;
40
+ const hashed = scryptSync(password, auth.salt, 64);
41
+ const expected = Buffer.from(auth.hash, "base64");
42
+ if (hashed.length !== expected.length)
43
+ return false;
44
+ return timingSafeEqual(hashed, expected);
45
+ }
46
+ function loadManagerConfig() {
47
+ const configPath = resolveConfigPath();
48
+ try {
49
+ if (!fs.existsSync(configPath))
50
+ return null;
51
+ const raw = fs.readFileSync(configPath, "utf-8");
52
+ return JSON.parse(raw);
53
+ }
54
+ catch {
55
+ return null;
56
+ }
57
+ }
@@ -0,0 +1,123 @@
1
+ import { spawn } from "node:child_process";
2
+ export function buildCommandRegistry(root) {
3
+ return [
4
+ {
5
+ id: "install-cli",
6
+ title: "Install Clawdbot CLI",
7
+ description: "Install the latest Clawdbot CLI (may require sudo)",
8
+ command: "npm",
9
+ args: ["i", "-g", "clawdbot@latest"],
10
+ cwd: root,
11
+ allowRun: true
12
+ },
13
+ {
14
+ id: "gateway-run",
15
+ title: "Start gateway",
16
+ description: "Run the local gateway (loopback only)",
17
+ command: "clawdbot",
18
+ args: ["gateway", "run", "--allow-unconfigured", "--bind", "loopback", "--port", "18789", "--force"],
19
+ cwd: root,
20
+ allowRun: true
21
+ },
22
+ {
23
+ id: "channels-probe",
24
+ title: "Probe channels",
25
+ description: "Check channel connectivity",
26
+ command: "clawdbot",
27
+ args: ["channels", "status", "--probe"],
28
+ cwd: root,
29
+ allowRun: true
30
+ }
31
+ ];
32
+ }
33
+ export function createProcessManager(commandRegistry) {
34
+ const processRegistry = new Map();
35
+ const listProcesses = () => {
36
+ return commandRegistry.map((cmd) => {
37
+ const managed = processRegistry.get(cmd.id);
38
+ return snapshotProcess(cmd, managed ?? null);
39
+ });
40
+ };
41
+ const startProcess = (id, options) => {
42
+ const def = commandRegistry.find((cmd) => cmd.id === id);
43
+ if (!def)
44
+ return { ok: false, error: "unknown id" };
45
+ if (!def.allowRun)
46
+ return { ok: false, error: "not allowed" };
47
+ const existing = processRegistry.get(id);
48
+ if (existing?.child && !existing.child.killed && existing.exitCode == null) {
49
+ return { ok: true, process: snapshotProcess(def, existing) };
50
+ }
51
+ const args = options?.args ?? def.args;
52
+ const child = spawn(def.command, args, {
53
+ cwd: def.cwd,
54
+ env: options?.env ?? process.env
55
+ });
56
+ const managed = {
57
+ def,
58
+ args,
59
+ child,
60
+ logs: [],
61
+ startedAt: new Date(),
62
+ exitCode: null
63
+ };
64
+ processRegistry.set(id, managed);
65
+ const pushLog = (chunk) => {
66
+ const lines = chunk.split(/\r?\n/).filter(Boolean);
67
+ if (!lines.length)
68
+ return;
69
+ const clipped = lines.map((line) => line.slice(0, 1000));
70
+ managed.logs.push(...clipped);
71
+ for (const line of clipped) {
72
+ options?.onLog?.(line);
73
+ }
74
+ if (managed.logs.length > 200) {
75
+ managed.logs.splice(0, managed.logs.length - 200);
76
+ }
77
+ };
78
+ child.stdout?.on("data", (chunk) => pushLog(chunk.toString()));
79
+ child.stderr?.on("data", (chunk) => pushLog(chunk.toString()));
80
+ child.on("error", (err) => {
81
+ managed.exitCode = 1;
82
+ pushLog(`spawn error: ${err.message}`);
83
+ });
84
+ child.on("close", (code) => {
85
+ managed.exitCode = code ?? null;
86
+ });
87
+ return { ok: true, process: snapshotProcess(def, managed) };
88
+ };
89
+ const stopProcess = (id) => {
90
+ const def = commandRegistry.find((cmd) => cmd.id === id);
91
+ if (!def)
92
+ return { ok: false, error: "unknown id" };
93
+ const managed = processRegistry.get(id);
94
+ if (!managed?.child) {
95
+ return { ok: true, process: snapshotProcess(def, managed ?? null) };
96
+ }
97
+ managed.child.kill("SIGTERM");
98
+ setTimeout(() => {
99
+ if (managed.child && !managed.child.killed) {
100
+ managed.child.kill("SIGKILL");
101
+ }
102
+ }, 2000);
103
+ return { ok: true, process: snapshotProcess(def, managed) };
104
+ };
105
+ return { listProcesses, startProcess, stopProcess };
106
+ }
107
+ function snapshotProcess(def, managed) {
108
+ const running = Boolean(managed?.child && !managed.child.killed && managed.exitCode == null);
109
+ return {
110
+ id: def.id,
111
+ title: def.title,
112
+ command: formatCommand(def, managed?.args),
113
+ cwd: def.cwd,
114
+ running,
115
+ pid: managed?.child?.pid ?? null,
116
+ startedAt: managed?.startedAt?.toISOString() ?? null,
117
+ exitCode: managed?.exitCode ?? null,
118
+ lastLines: managed?.logs.slice(-20) ?? []
119
+ };
120
+ }
121
+ function formatCommand(cmd, argsOverride) {
122
+ return [cmd.command, ...(argsOverride ?? cmd.args)].join(" ");
123
+ }
@@ -0,0 +1,48 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { DEFAULT_CONFIG_PATH } from "./constants.js";
5
+ export function resolveRepoRoot() {
6
+ const envRoot = process.env.MANAGER_REPO_ROOT ?? process.env.ONBOARDING_REPO_ROOT;
7
+ if (envRoot)
8
+ return path.resolve(envRoot);
9
+ const startPoints = [process.cwd(), path.dirname(fileURLToPath(import.meta.url))];
10
+ for (const start of startPoints) {
11
+ const found = findRepoRoot(start);
12
+ if (found)
13
+ return found;
14
+ }
15
+ return path.resolve(process.cwd(), "../..");
16
+ }
17
+ export function resolveWebDist(root) {
18
+ const candidate = process.env.MANAGER_WEB_DIST ?? path.join(root, "apps/web/dist");
19
+ const indexPath = path.join(candidate, "index.html");
20
+ if (fs.existsSync(indexPath))
21
+ return candidate;
22
+ return null;
23
+ }
24
+ export function resolveConfigPath() {
25
+ return process.env.MANAGER_CONFIG_PATH ?? DEFAULT_CONFIG_PATH;
26
+ }
27
+ function findRepoRoot(start) {
28
+ let current = start;
29
+ for (let i = 0; i < 6; i += 1) {
30
+ const candidate = path.join(current, "package.json");
31
+ if (fs.existsSync(candidate)) {
32
+ try {
33
+ const raw = fs.readFileSync(candidate, "utf-8");
34
+ const parsed = JSON.parse(raw);
35
+ if (parsed.name === "clawdbot-manager")
36
+ return current;
37
+ }
38
+ catch {
39
+ return current;
40
+ }
41
+ }
42
+ const parent = path.dirname(current);
43
+ if (parent === current)
44
+ break;
45
+ current = parent;
46
+ }
47
+ return null;
48
+ }
@@ -0,0 +1,9 @@
1
+ import os from "node:os";
2
+ import path from "node:path";
3
+ export const REQUIRED_NODE_MAJOR = 22;
4
+ export const DEFAULT_GATEWAY_HOST = "127.0.0.1";
5
+ export const DEFAULT_GATEWAY_PORT = 18789;
6
+ export const DEFAULT_API_HOST = "127.0.0.1";
7
+ export const DEFAULT_API_PORT = 17321;
8
+ export const DEFAULT_CONFIG_PATH = path.join(os.homedir(), ".clawdbot-manager", "config.json");
9
+ export const ONBOARDING_CACHE_MS = 5000;
@@ -0,0 +1,37 @@
1
+ import net from "node:net";
2
+ import { sleep } from "./utils.js";
3
+ export async function checkGateway(host, port) {
4
+ const start = Date.now();
5
+ return new Promise((resolve) => {
6
+ const socket = new net.Socket();
7
+ let finished = false;
8
+ const finish = (ok, error) => {
9
+ if (finished)
10
+ return;
11
+ finished = true;
12
+ socket.destroy();
13
+ resolve({
14
+ ok,
15
+ host,
16
+ port,
17
+ latencyMs: ok ? Date.now() - start : null,
18
+ error: error ?? null
19
+ });
20
+ };
21
+ socket.setTimeout(1200);
22
+ socket.once("connect", () => finish(true));
23
+ socket.once("timeout", () => finish(false, "timeout"));
24
+ socket.once("error", (err) => finish(false, err.message));
25
+ socket.connect(port, host);
26
+ });
27
+ }
28
+ export async function waitForGateway(host, port, timeoutMs) {
29
+ const start = Date.now();
30
+ while (Date.now() - start < timeoutMs) {
31
+ const res = await checkGateway(host, port);
32
+ if (res.ok)
33
+ return true;
34
+ await sleep(400);
35
+ }
36
+ return false;
37
+ }
@@ -0,0 +1,117 @@
1
+ import crypto from "node:crypto";
2
+ const MAX_LOGS = 200;
3
+ const JOB_TTL_MS = 10 * 60 * 1000;
4
+ export function createJobStore() {
5
+ const jobs = new Map();
6
+ const listeners = new Map();
7
+ const cleanup = () => {
8
+ const now = Date.now();
9
+ for (const [id, job] of jobs.entries()) {
10
+ if (job.expiresAt && job.expiresAt <= now) {
11
+ jobs.delete(id);
12
+ listeners.delete(id);
13
+ }
14
+ }
15
+ };
16
+ const emit = (id, event) => {
17
+ const subs = listeners.get(id);
18
+ if (!subs || subs.size === 0)
19
+ return;
20
+ for (const listener of subs) {
21
+ listener(event);
22
+ }
23
+ };
24
+ const getOrThrow = (id) => {
25
+ const job = jobs.get(id);
26
+ if (!job)
27
+ return null;
28
+ return job;
29
+ };
30
+ return {
31
+ createJob: (title) => {
32
+ cleanup();
33
+ const id = crypto.randomUUID();
34
+ const now = new Date().toISOString();
35
+ const entry = {
36
+ id,
37
+ title,
38
+ status: "pending",
39
+ createdAt: now,
40
+ startedAt: null,
41
+ endedAt: null,
42
+ logs: [],
43
+ error: null,
44
+ result: null,
45
+ expiresAt: null
46
+ };
47
+ jobs.set(id, entry);
48
+ return { ...entry };
49
+ },
50
+ startJob: (id) => {
51
+ const job = getOrThrow(id);
52
+ if (!job)
53
+ return;
54
+ job.status = "running";
55
+ job.startedAt = new Date().toISOString();
56
+ emit(id, { type: "status", status: job.status });
57
+ },
58
+ appendLog: (id, message) => {
59
+ const job = getOrThrow(id);
60
+ if (!job)
61
+ return;
62
+ if (!message.trim())
63
+ return;
64
+ job.logs.push(message.slice(0, 2000));
65
+ if (job.logs.length > MAX_LOGS) {
66
+ job.logs.splice(0, job.logs.length - MAX_LOGS);
67
+ }
68
+ emit(id, { type: "log", message });
69
+ },
70
+ completeJob: (id, result) => {
71
+ const job = getOrThrow(id);
72
+ if (!job)
73
+ return;
74
+ job.status = "success";
75
+ job.endedAt = new Date().toISOString();
76
+ job.result = result ?? null;
77
+ job.expiresAt = Date.now() + JOB_TTL_MS;
78
+ emit(id, { type: "status", status: job.status });
79
+ emit(id, { type: "done", result: job.result ?? undefined });
80
+ },
81
+ failJob: (id, error) => {
82
+ const job = getOrThrow(id);
83
+ if (!job)
84
+ return;
85
+ job.status = "failed";
86
+ job.endedAt = new Date().toISOString();
87
+ job.error = error;
88
+ job.expiresAt = Date.now() + JOB_TTL_MS;
89
+ emit(id, { type: "status", status: job.status });
90
+ emit(id, { type: "error", error });
91
+ },
92
+ getJob: (id) => {
93
+ cleanup();
94
+ const job = jobs.get(id);
95
+ if (!job)
96
+ return null;
97
+ return { ...job };
98
+ },
99
+ subscribe: (id, listener) => {
100
+ const job = jobs.get(id);
101
+ if (!job)
102
+ return () => { };
103
+ const set = listeners.get(id) ?? new Set();
104
+ set.add(listener);
105
+ listeners.set(id, set);
106
+ return () => {
107
+ const current = listeners.get(id);
108
+ if (!current)
109
+ return;
110
+ current.delete(listener);
111
+ if (current.size === 0) {
112
+ listeners.delete(id);
113
+ }
114
+ };
115
+ }
116
+ };
117
+ }
@@ -0,0 +1,96 @@
1
+ import { ONBOARDING_CACHE_MS } from "./constants.js";
2
+ let onboardingCache = null;
3
+ let lastProbe = null;
4
+ export function setLastProbe(ok) {
5
+ lastProbe = { ok, at: new Date().toISOString() };
6
+ }
7
+ export async function getOnboardingStatus(cliInstalled, runCommand) {
8
+ const now = Date.now();
9
+ if (onboardingCache && now - onboardingCache.at < ONBOARDING_CACHE_MS) {
10
+ return onboardingCache.data;
11
+ }
12
+ if (!cliInstalled) {
13
+ const data = {
14
+ discord: {
15
+ tokenConfigured: false,
16
+ allowFromConfigured: false,
17
+ pendingPairings: 0
18
+ },
19
+ ai: {
20
+ configured: false,
21
+ missingProviders: []
22
+ },
23
+ probe: lastProbe
24
+ };
25
+ onboardingCache = { at: now, data };
26
+ return data;
27
+ }
28
+ const [tokenConfigured, allowFromConfigured, pendingPairings, aiStatus] = await Promise.all([
29
+ readDiscordTokenConfigured(runCommand),
30
+ readDiscordAllowFromConfigured(runCommand),
31
+ readPendingDiscordPairings(runCommand),
32
+ readAiAuthStatus(runCommand)
33
+ ]);
34
+ const data = {
35
+ discord: {
36
+ tokenConfigured,
37
+ allowFromConfigured,
38
+ pendingPairings
39
+ },
40
+ ai: aiStatus,
41
+ probe: lastProbe
42
+ };
43
+ onboardingCache = { at: now, data };
44
+ return data;
45
+ }
46
+ async function readConfigValue(runCommand, pathKey) {
47
+ try {
48
+ const output = await runCommand("clawdbot", ["config", "get", pathKey, "--json"], 4000);
49
+ return JSON.parse(output);
50
+ }
51
+ catch {
52
+ return null;
53
+ }
54
+ }
55
+ async function readDiscordTokenConfigured(runCommand) {
56
+ const value = await readConfigValue(runCommand, "channels.discord.token");
57
+ return typeof value === "string" ? value.trim().length > 0 : false;
58
+ }
59
+ async function readDiscordAllowFromConfigured(runCommand) {
60
+ const value = await readConfigValue(runCommand, "channels.discord.dm.allowFrom");
61
+ if (Array.isArray(value))
62
+ return value.length > 0;
63
+ if (value === "*")
64
+ return true;
65
+ return false;
66
+ }
67
+ async function readPendingDiscordPairings(runCommand) {
68
+ try {
69
+ const output = await runCommand("clawdbot", ["pairing", "list", "--channel", "discord", "--json"], 4000);
70
+ const parsed = JSON.parse(output);
71
+ return Array.isArray(parsed?.requests) ? parsed.requests.length : 0;
72
+ }
73
+ catch {
74
+ return 0;
75
+ }
76
+ }
77
+ async function readAiAuthStatus(runCommand) {
78
+ try {
79
+ const output = await runCommand("clawdbot", ["models", "status", "--json"], 8000);
80
+ const parsed = JSON.parse(output);
81
+ const missing = Array.isArray(parsed?.auth?.missingProvidersInUse)
82
+ ? parsed.auth?.missingProvidersInUse.map((item) => String(item))
83
+ : [];
84
+ return {
85
+ configured: missing.length === 0,
86
+ missingProviders: missing
87
+ };
88
+ }
89
+ catch (err) {
90
+ return {
91
+ configured: false,
92
+ missingProviders: [],
93
+ error: err instanceof Error ? err.message : String(err)
94
+ };
95
+ }
96
+ }
@@ -0,0 +1,99 @@
1
+ import { spawn } from "node:child_process";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ export function createCommandRunner(repoRoot) {
5
+ return (cmd, args, timeoutMs, env = process.env) => runCommand(cmd, args, timeoutMs, { cwd: repoRoot, env });
6
+ }
7
+ export function findOnPath(binary) {
8
+ const envPath = process.env.PATH ?? "";
9
+ const entries = envPath.split(path.delimiter).filter(Boolean);
10
+ for (const entry of entries) {
11
+ const candidate = path.join(entry, binary);
12
+ if (fs.existsSync(candidate))
13
+ return candidate;
14
+ }
15
+ return null;
16
+ }
17
+ export function runCommandWithLogs(cmd, args, options) {
18
+ return new Promise((resolve, reject) => {
19
+ const child = spawn(cmd, args, {
20
+ cwd: options.cwd,
21
+ stdio: ["ignore", "pipe", "pipe"],
22
+ env: options.env
23
+ });
24
+ let stdoutBuffer = "";
25
+ let stderrBuffer = "";
26
+ const flushLines = (buffer, prefix) => {
27
+ const lines = buffer.split(/\r?\n/);
28
+ const rest = lines.pop() ?? "";
29
+ for (const line of lines) {
30
+ const trimmed = line.trimEnd();
31
+ if (trimmed) {
32
+ options.onLog(prefix ? `${prefix}${trimmed}` : trimmed);
33
+ }
34
+ }
35
+ return rest;
36
+ };
37
+ const timer = setTimeout(() => {
38
+ child.kill("SIGTERM");
39
+ reject(new Error("timeout"));
40
+ }, options.timeoutMs);
41
+ child.stdout?.on("data", (chunk) => {
42
+ stdoutBuffer += chunk.toString();
43
+ stdoutBuffer = flushLines(stdoutBuffer);
44
+ });
45
+ child.stderr?.on("data", (chunk) => {
46
+ stderrBuffer += chunk.toString();
47
+ stderrBuffer = flushLines(stderrBuffer, "stderr: ");
48
+ });
49
+ child.on("error", (err) => {
50
+ clearTimeout(timer);
51
+ reject(err);
52
+ });
53
+ child.on("close", (code) => {
54
+ clearTimeout(timer);
55
+ if (stdoutBuffer.trim()) {
56
+ options.onLog(stdoutBuffer.trimEnd());
57
+ }
58
+ if (stderrBuffer.trim()) {
59
+ options.onLog(`stderr: ${stderrBuffer.trimEnd()}`);
60
+ }
61
+ if (code === 0)
62
+ resolve();
63
+ else
64
+ reject(new Error(`exit ${code ?? "unknown"}`));
65
+ });
66
+ });
67
+ }
68
+ function runCommand(cmd, args, timeoutMs, options) {
69
+ return new Promise((resolve, reject) => {
70
+ const child = spawn(cmd, args, {
71
+ cwd: options.cwd,
72
+ stdio: ["ignore", "pipe", "pipe"],
73
+ env: options.env
74
+ });
75
+ let output = "";
76
+ let error = "";
77
+ const timer = setTimeout(() => {
78
+ child.kill("SIGTERM");
79
+ reject(new Error("timeout"));
80
+ }, timeoutMs);
81
+ child.stdout?.on("data", (chunk) => {
82
+ output += chunk.toString();
83
+ });
84
+ child.stderr?.on("data", (chunk) => {
85
+ error += chunk.toString();
86
+ });
87
+ child.on("error", (err) => {
88
+ clearTimeout(timer);
89
+ reject(err);
90
+ });
91
+ child.on("close", (code) => {
92
+ clearTimeout(timer);
93
+ if (code === 0)
94
+ resolve(output);
95
+ else
96
+ reject(new Error(error || output));
97
+ });
98
+ });
99
+ }
@@ -0,0 +1,69 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ export function serveStaticFile(reqPath, webRoot) {
4
+ const relPath = reqPath === "/" ? "/index.html" : reqPath;
5
+ const safePath = safeJoin(webRoot, relPath);
6
+ if (!safePath)
7
+ return new Response("Not Found", { status: 404 });
8
+ const fileContent = readFileOrNull(safePath);
9
+ if (fileContent) {
10
+ return new Response(toArrayBuffer(fileContent), {
11
+ headers: { "content-type": contentType(path.extname(safePath)) }
12
+ });
13
+ }
14
+ if (relPath !== "/index.html") {
15
+ const indexPath = path.join(webRoot, "index.html");
16
+ const indexContent = readFileOrNull(indexPath);
17
+ if (indexContent) {
18
+ return new Response(toArrayBuffer(indexContent), {
19
+ headers: { "content-type": "text/html; charset=utf-8" }
20
+ });
21
+ }
22
+ }
23
+ return new Response("Not Found", { status: 404 });
24
+ }
25
+ function readFileOrNull(filePath) {
26
+ try {
27
+ return fs.readFileSync(filePath);
28
+ }
29
+ catch {
30
+ return null;
31
+ }
32
+ }
33
+ function toArrayBuffer(data) {
34
+ return data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength);
35
+ }
36
+ function safeJoin(root, reqPath) {
37
+ const normalized = path.normalize(reqPath).replace(/^(\.\.(\/|\\|$))+/, "");
38
+ const resolved = path.join(root, normalized);
39
+ if (!resolved.startsWith(root))
40
+ return null;
41
+ return resolved;
42
+ }
43
+ function contentType(ext) {
44
+ switch (ext) {
45
+ case ".html":
46
+ return "text/html; charset=utf-8";
47
+ case ".css":
48
+ return "text/css; charset=utf-8";
49
+ case ".js":
50
+ return "text/javascript; charset=utf-8";
51
+ case ".map":
52
+ return "application/json; charset=utf-8";
53
+ case ".svg":
54
+ return "image/svg+xml";
55
+ case ".png":
56
+ return "image/png";
57
+ case ".jpg":
58
+ case ".jpeg":
59
+ return "image/jpeg";
60
+ case ".ico":
61
+ return "image/x-icon";
62
+ case ".woff":
63
+ return "font/woff";
64
+ case ".woff2":
65
+ return "font/woff2";
66
+ default:
67
+ return "application/octet-stream";
68
+ }
69
+ }
@@ -0,0 +1,31 @@
1
+ import { REQUIRED_NODE_MAJOR } from "./constants.js";
2
+ import { findOnPath } from "./runner.js";
3
+ export async function getSystemStatus() {
4
+ const major = parseMajor(process.version);
5
+ return {
6
+ node: {
7
+ current: process.version,
8
+ required: `>=${REQUIRED_NODE_MAJOR}`,
9
+ ok: major >= REQUIRED_NODE_MAJOR
10
+ },
11
+ platform: process.platform,
12
+ arch: process.arch
13
+ };
14
+ }
15
+ export async function getCliStatus(runCommand) {
16
+ const pathMatch = findOnPath("clawdbot");
17
+ if (!pathMatch) {
18
+ return { installed: false, path: null, version: null };
19
+ }
20
+ const version = await runCommand(pathMatch, ["--version"], 2000).catch(() => null);
21
+ return {
22
+ installed: true,
23
+ path: pathMatch,
24
+ version: version?.trim() ?? null
25
+ };
26
+ }
27
+ function parseMajor(version) {
28
+ const cleaned = version.replace(/^v/, "");
29
+ const [major] = cleaned.split(".");
30
+ return Number(major);
31
+ }