Gateway: add browser control UI
This commit is contained in:
136
src/gateway/control-ui.ts
Normal file
136
src/gateway/control-ui.ts
Normal file
@@ -0,0 +1,136 @@
|
||||
import fs from "node:fs";
|
||||
import type { IncomingMessage, ServerResponse } from "node:http";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const UI_PREFIX = "/ui/";
|
||||
|
||||
function resolveControlUiRoot(): string | null {
|
||||
const here = path.dirname(fileURLToPath(import.meta.url));
|
||||
const candidates = [
|
||||
// Running from dist: dist/gateway/control-ui.js -> dist/control-ui
|
||||
path.resolve(here, "../control-ui"),
|
||||
// Running from source: src/gateway/control-ui.ts -> dist/control-ui
|
||||
path.resolve(here, "../../dist/control-ui"),
|
||||
// Fallback to cwd (dev)
|
||||
path.resolve(process.cwd(), "dist", "control-ui"),
|
||||
];
|
||||
for (const dir of candidates) {
|
||||
if (fs.existsSync(path.join(dir, "index.html"))) return dir;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function contentTypeForExt(ext: string): string {
|
||||
switch (ext) {
|
||||
case ".html":
|
||||
return "text/html; charset=utf-8";
|
||||
case ".js":
|
||||
return "application/javascript; charset=utf-8";
|
||||
case ".css":
|
||||
return "text/css; charset=utf-8";
|
||||
case ".json":
|
||||
case ".map":
|
||||
return "application/json; charset=utf-8";
|
||||
case ".svg":
|
||||
return "image/svg+xml";
|
||||
case ".png":
|
||||
return "image/png";
|
||||
case ".jpg":
|
||||
case ".jpeg":
|
||||
return "image/jpeg";
|
||||
case ".ico":
|
||||
return "image/x-icon";
|
||||
case ".txt":
|
||||
return "text/plain; charset=utf-8";
|
||||
default:
|
||||
return "application/octet-stream";
|
||||
}
|
||||
}
|
||||
|
||||
function respondNotFound(res: ServerResponse) {
|
||||
res.statusCode = 404;
|
||||
res.setHeader("Content-Type", "text/plain; charset=utf-8");
|
||||
res.end("Not Found");
|
||||
}
|
||||
|
||||
function serveFile(res: ServerResponse, filePath: string) {
|
||||
const ext = path.extname(filePath).toLowerCase();
|
||||
res.setHeader("Content-Type", contentTypeForExt(ext));
|
||||
// Static UI should never be cached aggressively while iterating; allow the
|
||||
// browser to revalidate.
|
||||
res.setHeader("Cache-Control", "no-cache");
|
||||
res.end(fs.readFileSync(filePath));
|
||||
}
|
||||
|
||||
function isSafeRelativePath(relPath: string) {
|
||||
if (!relPath) return false;
|
||||
const normalized = path.posix.normalize(relPath);
|
||||
if (normalized.startsWith("../") || normalized === "..") return false;
|
||||
if (normalized.includes("\0")) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
export function handleControlUiHttpRequest(
|
||||
req: IncomingMessage,
|
||||
res: ServerResponse,
|
||||
): boolean {
|
||||
const urlRaw = req.url;
|
||||
if (!urlRaw) return false;
|
||||
if (req.method !== "GET" && req.method !== "HEAD") {
|
||||
res.statusCode = 405;
|
||||
res.setHeader("Content-Type", "text/plain; charset=utf-8");
|
||||
res.end("Method Not Allowed");
|
||||
return true;
|
||||
}
|
||||
|
||||
const url = new URL(urlRaw, "http://localhost");
|
||||
|
||||
if (url.pathname === "/ui") {
|
||||
res.statusCode = 302;
|
||||
res.setHeader("Location", UI_PREFIX);
|
||||
res.end();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!url.pathname.startsWith(UI_PREFIX)) return false;
|
||||
|
||||
const root = resolveControlUiRoot();
|
||||
if (!root) {
|
||||
res.statusCode = 503;
|
||||
res.setHeader("Content-Type", "text/plain; charset=utf-8");
|
||||
res.end(
|
||||
"Control UI assets not found. Build them with `pnpm ui:build` (or run `pnpm ui:dev` during development).",
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
const rel = url.pathname.slice(UI_PREFIX.length);
|
||||
const requested = rel && !rel.endsWith("/") ? rel : `${rel}index.html`;
|
||||
const fileRel = requested || "index.html";
|
||||
if (!isSafeRelativePath(fileRel)) {
|
||||
respondNotFound(res);
|
||||
return true;
|
||||
}
|
||||
|
||||
const filePath = path.join(root, fileRel);
|
||||
if (!filePath.startsWith(root)) {
|
||||
respondNotFound(res);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) {
|
||||
serveFile(res, filePath);
|
||||
return true;
|
||||
}
|
||||
|
||||
// SPA fallback (client-side router): serve index.html for unknown paths.
|
||||
const indexPath = path.join(root, "index.html");
|
||||
if (fs.existsSync(indexPath)) {
|
||||
serveFile(res, indexPath);
|
||||
return true;
|
||||
}
|
||||
|
||||
respondNotFound(res);
|
||||
return true;
|
||||
}
|
||||
@@ -9,6 +9,10 @@ import {
|
||||
ChatEventSchema,
|
||||
ChatHistoryParamsSchema,
|
||||
ChatSendParamsSchema,
|
||||
type ConfigGetParams,
|
||||
ConfigGetParamsSchema,
|
||||
type ConfigSetParams,
|
||||
ConfigSetParamsSchema,
|
||||
type ConnectParams,
|
||||
ConnectParamsSchema,
|
||||
type CronAddParams,
|
||||
@@ -125,6 +129,12 @@ export const validateSessionsListParams = ajv.compile<SessionsListParams>(
|
||||
export const validateSessionsPatchParams = ajv.compile<SessionsPatchParams>(
|
||||
SessionsPatchParamsSchema,
|
||||
);
|
||||
export const validateConfigGetParams = ajv.compile<ConfigGetParams>(
|
||||
ConfigGetParamsSchema,
|
||||
);
|
||||
export const validateConfigSetParams = ajv.compile<ConfigSetParams>(
|
||||
ConfigSetParamsSchema,
|
||||
);
|
||||
export const validateCronListParams =
|
||||
ajv.compile<CronListParams>(CronListParamsSchema);
|
||||
export const validateCronStatusParams = ajv.compile<CronStatusParams>(
|
||||
@@ -181,6 +191,8 @@ export {
|
||||
NodeInvokeParamsSchema,
|
||||
SessionsListParamsSchema,
|
||||
SessionsPatchParamsSchema,
|
||||
ConfigGetParamsSchema,
|
||||
ConfigSetParamsSchema,
|
||||
CronJobSchema,
|
||||
CronListParamsSchema,
|
||||
CronStatusParamsSchema,
|
||||
@@ -218,6 +230,8 @@ export type {
|
||||
NodePairRequestParams,
|
||||
NodePairListParams,
|
||||
NodePairApproveParams,
|
||||
ConfigGetParams,
|
||||
ConfigSetParams,
|
||||
NodePairRejectParams,
|
||||
NodePairVerifyParams,
|
||||
NodeListParams,
|
||||
|
||||
@@ -291,6 +291,18 @@ export const SessionsPatchParamsSchema = Type.Object(
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
export const ConfigGetParamsSchema = Type.Object(
|
||||
{},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
export const ConfigSetParamsSchema = Type.Object(
|
||||
{
|
||||
raw: NonEmptyString,
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
export const CronScheduleSchema = Type.Union([
|
||||
Type.Object(
|
||||
{
|
||||
@@ -541,6 +553,8 @@ export const ProtocolSchemas: Record<string, TSchema> = {
|
||||
NodeInvokeParams: NodeInvokeParamsSchema,
|
||||
SessionsListParams: SessionsListParamsSchema,
|
||||
SessionsPatchParams: SessionsPatchParamsSchema,
|
||||
ConfigGetParams: ConfigGetParamsSchema,
|
||||
ConfigSetParams: ConfigSetParamsSchema,
|
||||
CronJob: CronJobSchema,
|
||||
CronListParams: CronListParamsSchema,
|
||||
CronStatusParams: CronStatusParamsSchema,
|
||||
@@ -582,6 +596,8 @@ export type NodeDescribeParams = Static<typeof NodeDescribeParamsSchema>;
|
||||
export type NodeInvokeParams = Static<typeof NodeInvokeParamsSchema>;
|
||||
export type SessionsListParams = Static<typeof SessionsListParamsSchema>;
|
||||
export type SessionsPatchParams = Static<typeof SessionsPatchParamsSchema>;
|
||||
export type ConfigGetParams = Static<typeof ConfigGetParamsSchema>;
|
||||
export type ConfigSetParams = Static<typeof ConfigSetParamsSchema>;
|
||||
export type CronJob = Static<typeof CronJobSchema>;
|
||||
export type CronListParams = Static<typeof CronListParamsSchema>;
|
||||
export type CronStatusParams = Static<typeof CronStatusParamsSchema>;
|
||||
|
||||
@@ -26,7 +26,15 @@ import { createDefaultDeps } from "../cli/deps.js";
|
||||
import { agentCommand } from "../commands/agent.js";
|
||||
import { getHealthSnapshot, type HealthSummary } from "../commands/health.js";
|
||||
import { getStatusSummary } from "../commands/status.js";
|
||||
import { type ClawdisConfig, loadConfig } from "../config/config.js";
|
||||
import {
|
||||
type ClawdisConfig,
|
||||
CONFIG_PATH_CLAWDIS,
|
||||
loadConfig,
|
||||
parseConfigJson5,
|
||||
readConfigFileSnapshot,
|
||||
validateConfigObject,
|
||||
writeConfigFile,
|
||||
} from "../config/config.js";
|
||||
import {
|
||||
loadSessionStore,
|
||||
resolveStorePath,
|
||||
@@ -90,6 +98,7 @@ import { setHeartbeatsEnabled } from "../web/auto-reply.js";
|
||||
import { sendMessageWhatsApp } from "../web/outbound.js";
|
||||
import { requestReplyHeartbeatNow } from "../web/reply-heartbeat-wake.js";
|
||||
import { buildMessageWithAttachments } from "./chat-attachments.js";
|
||||
import { handleControlUiHttpRequest } from "./control-ui.js";
|
||||
import {
|
||||
type ConnectParams,
|
||||
ErrorCodes,
|
||||
@@ -105,6 +114,8 @@ import {
|
||||
validateChatAbortParams,
|
||||
validateChatHistoryParams,
|
||||
validateChatSendParams,
|
||||
validateConfigGetParams,
|
||||
validateConfigSetParams,
|
||||
validateConnectParams,
|
||||
validateCronAddParams,
|
||||
validateCronListParams,
|
||||
@@ -183,6 +194,8 @@ type SessionsPatchResult = {
|
||||
const METHODS = [
|
||||
"health",
|
||||
"status",
|
||||
"config.get",
|
||||
"config.set",
|
||||
"voicewake.get",
|
||||
"voicewake.set",
|
||||
"sessions.list",
|
||||
@@ -233,6 +246,27 @@ export type GatewayServer = {
|
||||
close: () => Promise<void>;
|
||||
};
|
||||
|
||||
export type GatewayServerOptions = {
|
||||
/**
|
||||
* Bind address policy for the Gateway WebSocket/HTTP server.
|
||||
* - loopback: 127.0.0.1
|
||||
* - lan: 0.0.0.0
|
||||
* - tailnet: bind only to the Tailscale IPv4 address (100.64.0.0/10)
|
||||
* - auto: prefer tailnet, else LAN
|
||||
*/
|
||||
bind?: import("../config/config.js").BridgeBindMode;
|
||||
/**
|
||||
* Advanced override for the bind host, bypassing bind resolution.
|
||||
* Prefer `bind` unless you really need a specific address.
|
||||
*/
|
||||
host?: string;
|
||||
/**
|
||||
* If false, do not serve the browser Control UI under /ui/.
|
||||
* Default: config `gateway.controlUi.enabled` (or true when absent).
|
||||
*/
|
||||
controlUiEnabled?: boolean;
|
||||
};
|
||||
|
||||
function isLoopbackAddress(ip: string | undefined): boolean {
|
||||
if (!ip) return false;
|
||||
if (ip === "127.0.0.1") return true;
|
||||
@@ -242,6 +276,21 @@ function isLoopbackAddress(ip: string | undefined): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
function resolveGatewayBindHost(
|
||||
bind: import("../config/config.js").BridgeBindMode | undefined,
|
||||
): string | null {
|
||||
const mode = bind ?? "loopback";
|
||||
if (mode === "loopback") return "127.0.0.1";
|
||||
if (mode === "lan") return "0.0.0.0";
|
||||
if (mode === "tailnet") return pickPrimaryTailnetIPv4() ?? null;
|
||||
if (mode === "auto") return pickPrimaryTailnetIPv4() ?? "0.0.0.0";
|
||||
return "127.0.0.1";
|
||||
}
|
||||
|
||||
function isLoopbackHost(host: string): boolean {
|
||||
return isLoopbackAddress(host);
|
||||
}
|
||||
|
||||
let presenceVersion = 1;
|
||||
let healthVersion = 1;
|
||||
let healthCache: HealthSummary | null = null;
|
||||
@@ -774,9 +823,44 @@ async function refreshHealthSnapshot(_opts?: { probe?: boolean }) {
|
||||
return healthRefresh;
|
||||
}
|
||||
|
||||
export async function startGatewayServer(port = 18789): Promise<GatewayServer> {
|
||||
const host = "127.0.0.1";
|
||||
const httpServer: HttpServer = createHttpServer();
|
||||
export async function startGatewayServer(
|
||||
port = 18789,
|
||||
opts: GatewayServerOptions = {},
|
||||
): Promise<GatewayServer> {
|
||||
const cfgForServer = loadConfig();
|
||||
const bindMode = opts.bind ?? cfgForServer.gateway?.bind ?? "loopback";
|
||||
const bindHost = opts.host ?? resolveGatewayBindHost(bindMode);
|
||||
if (!bindHost) {
|
||||
throw new Error(
|
||||
"gateway bind is tailnet, but no tailnet interface was found; refusing to start gateway",
|
||||
);
|
||||
}
|
||||
const controlUiEnabled =
|
||||
opts.controlUiEnabled ?? cfgForServer.gateway?.controlUi?.enabled ?? true;
|
||||
if (!isLoopbackHost(bindHost) && !getGatewayToken()) {
|
||||
throw new Error(
|
||||
`refusing to bind gateway to ${bindHost}:${port} without CLAWDIS_GATEWAY_TOKEN`,
|
||||
);
|
||||
}
|
||||
|
||||
const httpServer: HttpServer = createHttpServer((req, res) => {
|
||||
// Don't interfere with WebSocket upgrades; ws handles the 'upgrade' event.
|
||||
if (String(req.headers.upgrade ?? "").toLowerCase() === "websocket") return;
|
||||
|
||||
if (controlUiEnabled) {
|
||||
if (req.url === "/") {
|
||||
res.statusCode = 302;
|
||||
res.setHeader("Location", "/ui/");
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
if (handleControlUiHttpRequest(req, res)) return;
|
||||
}
|
||||
|
||||
res.statusCode = 404;
|
||||
res.setHeader("Content-Type", "text/plain; charset=utf-8");
|
||||
res.end("Not Found");
|
||||
});
|
||||
let bonjourStop: (() => Promise<void>) | null = null;
|
||||
let bridge: Awaited<ReturnType<typeof startNodeBridgeServer>> | null = null;
|
||||
let canvasHost: CanvasHostServer | null = null;
|
||||
@@ -794,18 +878,18 @@ export async function startGatewayServer(port = 18789): Promise<GatewayServer> {
|
||||
};
|
||||
httpServer.once("error", onError);
|
||||
httpServer.once("listening", onListening);
|
||||
httpServer.listen(port, host);
|
||||
httpServer.listen(port, bindHost);
|
||||
});
|
||||
} catch (err) {
|
||||
const code = (err as NodeJS.ErrnoException).code;
|
||||
if (code === "EADDRINUSE") {
|
||||
throw new GatewayLockError(
|
||||
`another gateway instance is already listening on ws://${host}:${port}`,
|
||||
`another gateway instance is already listening on ws://${bindHost}:${port}`,
|
||||
err,
|
||||
);
|
||||
}
|
||||
throw new GatewayLockError(
|
||||
`failed to bind gateway socket on ws://${host}:${port}: ${String(err)}`,
|
||||
`failed to bind gateway socket on ws://${bindHost}:${port}: ${String(err)}`,
|
||||
err,
|
||||
);
|
||||
}
|
||||
@@ -827,6 +911,7 @@ export async function startGatewayServer(port = 18789): Promise<GatewayServer> {
|
||||
{ sessionKey: string; clientRunId: string }
|
||||
>();
|
||||
const chatRunBuffers = new Map<string, string>();
|
||||
const chatDeltaSentAt = new Map<string, number>();
|
||||
const chatAbortControllers = new Map<
|
||||
string,
|
||||
{ controller: AbortController; sessionId: string; sessionKey: string }
|
||||
@@ -1171,6 +1256,63 @@ export async function startGatewayServer(port = 18789): Promise<GatewayServer> {
|
||||
const snap = await refreshHealthSnapshot({ probe: false });
|
||||
return { ok: true, payloadJSON: JSON.stringify(snap) };
|
||||
}
|
||||
case "config.get": {
|
||||
const params = parseParams();
|
||||
if (!validateConfigGetParams(params)) {
|
||||
return {
|
||||
ok: false,
|
||||
error: {
|
||||
code: ErrorCodes.INVALID_REQUEST,
|
||||
message: `invalid config.get params: ${formatValidationErrors(validateConfigGetParams.errors)}`,
|
||||
},
|
||||
};
|
||||
}
|
||||
const snapshot = await readConfigFileSnapshot();
|
||||
return { ok: true, payloadJSON: JSON.stringify(snapshot) };
|
||||
}
|
||||
case "config.set": {
|
||||
const params = parseParams();
|
||||
if (!validateConfigSetParams(params)) {
|
||||
return {
|
||||
ok: false,
|
||||
error: {
|
||||
code: ErrorCodes.INVALID_REQUEST,
|
||||
message: `invalid config.set params: ${formatValidationErrors(validateConfigSetParams.errors)}`,
|
||||
},
|
||||
};
|
||||
}
|
||||
const raw = String((params as { raw?: unknown }).raw ?? "");
|
||||
const parsedRes = parseConfigJson5(raw);
|
||||
if (!parsedRes.ok) {
|
||||
return {
|
||||
ok: false,
|
||||
error: {
|
||||
code: ErrorCodes.INVALID_REQUEST,
|
||||
message: parsedRes.error,
|
||||
},
|
||||
};
|
||||
}
|
||||
const validated = validateConfigObject(parsedRes.parsed);
|
||||
if (!validated.ok) {
|
||||
return {
|
||||
ok: false,
|
||||
error: {
|
||||
code: ErrorCodes.INVALID_REQUEST,
|
||||
message: "invalid config",
|
||||
details: { issues: validated.issues },
|
||||
},
|
||||
};
|
||||
}
|
||||
await writeConfigFile(validated.config);
|
||||
return {
|
||||
ok: true,
|
||||
payloadJSON: JSON.stringify({
|
||||
ok: true,
|
||||
path: CONFIG_PATH_CLAWDIS,
|
||||
config: validated.config,
|
||||
}),
|
||||
};
|
||||
}
|
||||
case "sessions.list": {
|
||||
const params = parseParams();
|
||||
if (!validateSessionsListParams(params)) {
|
||||
@@ -1366,6 +1508,7 @@ export async function startGatewayServer(port = 18789): Promise<GatewayServer> {
|
||||
active.controller.abort();
|
||||
chatAbortControllers.delete(runId);
|
||||
chatRunBuffers.delete(runId);
|
||||
chatDeltaSentAt.delete(runId);
|
||||
const current = chatRunSessions.get(active.sessionId);
|
||||
if (
|
||||
current?.clientRunId === runId &&
|
||||
@@ -1970,6 +2113,23 @@ export async function startGatewayServer(port = 18789): Promise<GatewayServer> {
|
||||
};
|
||||
if (evt.stream === "assistant" && typeof evt.data?.text === "string") {
|
||||
chatRunBuffers.set(clientRunId, evt.data.text);
|
||||
const now = Date.now();
|
||||
const last = chatDeltaSentAt.get(clientRunId) ?? 0;
|
||||
// Throttle UI delta events so slow clients don't accumulate unbounded buffers.
|
||||
if (now - last >= 150) {
|
||||
chatDeltaSentAt.set(clientRunId, now);
|
||||
const payload = {
|
||||
...base,
|
||||
state: "delta" as const,
|
||||
message: {
|
||||
role: "assistant",
|
||||
content: [{ type: "text", text: evt.data.text }],
|
||||
timestamp: now,
|
||||
},
|
||||
};
|
||||
broadcast("chat", payload, { dropIfSlow: true });
|
||||
bridgeSendToSession(sessionKey, "chat", payload);
|
||||
}
|
||||
} else if (
|
||||
evt.stream === "job" &&
|
||||
typeof evt.data?.state === "string" &&
|
||||
@@ -1977,6 +2137,7 @@ export async function startGatewayServer(port = 18789): Promise<GatewayServer> {
|
||||
) {
|
||||
const text = chatRunBuffers.get(clientRunId)?.trim() ?? "";
|
||||
chatRunBuffers.delete(clientRunId);
|
||||
chatDeltaSentAt.delete(clientRunId);
|
||||
if (evt.data.state === "done") {
|
||||
const payload = {
|
||||
...base,
|
||||
@@ -2457,6 +2618,7 @@ export async function startGatewayServer(port = 18789): Promise<GatewayServer> {
|
||||
active.controller.abort();
|
||||
chatAbortControllers.delete(runId);
|
||||
chatRunBuffers.delete(runId);
|
||||
chatDeltaSentAt.delete(runId);
|
||||
const current = chatRunSessions.get(active.sessionId);
|
||||
if (
|
||||
current?.clientRunId === runId &&
|
||||
@@ -2795,6 +2957,69 @@ export async function startGatewayServer(port = 18789): Promise<GatewayServer> {
|
||||
respond(true, status, undefined);
|
||||
break;
|
||||
}
|
||||
case "config.get": {
|
||||
const params = (req.params ?? {}) as Record<string, unknown>;
|
||||
if (!validateConfigGetParams(params)) {
|
||||
respond(
|
||||
false,
|
||||
undefined,
|
||||
errorShape(
|
||||
ErrorCodes.INVALID_REQUEST,
|
||||
`invalid config.get params: ${formatValidationErrors(validateConfigGetParams.errors)}`,
|
||||
),
|
||||
);
|
||||
break;
|
||||
}
|
||||
const snapshot = await readConfigFileSnapshot();
|
||||
respond(true, snapshot, undefined);
|
||||
break;
|
||||
}
|
||||
case "config.set": {
|
||||
const params = (req.params ?? {}) as Record<string, unknown>;
|
||||
if (!validateConfigSetParams(params)) {
|
||||
respond(
|
||||
false,
|
||||
undefined,
|
||||
errorShape(
|
||||
ErrorCodes.INVALID_REQUEST,
|
||||
`invalid config.set params: ${formatValidationErrors(validateConfigSetParams.errors)}`,
|
||||
),
|
||||
);
|
||||
break;
|
||||
}
|
||||
const raw = String((params as { raw?: unknown }).raw ?? "");
|
||||
const parsedRes = parseConfigJson5(raw);
|
||||
if (!parsedRes.ok) {
|
||||
respond(
|
||||
false,
|
||||
undefined,
|
||||
errorShape(ErrorCodes.INVALID_REQUEST, parsedRes.error),
|
||||
);
|
||||
break;
|
||||
}
|
||||
const validated = validateConfigObject(parsedRes.parsed);
|
||||
if (!validated.ok) {
|
||||
respond(
|
||||
false,
|
||||
undefined,
|
||||
errorShape(ErrorCodes.INVALID_REQUEST, "invalid config", {
|
||||
details: { issues: validated.issues },
|
||||
}),
|
||||
);
|
||||
break;
|
||||
}
|
||||
await writeConfigFile(validated.config);
|
||||
respond(
|
||||
true,
|
||||
{
|
||||
ok: true,
|
||||
path: CONFIG_PATH_CLAWDIS,
|
||||
config: validated.config,
|
||||
},
|
||||
undefined,
|
||||
);
|
||||
break;
|
||||
}
|
||||
case "sessions.list": {
|
||||
const params = (req.params ?? {}) as Record<string, unknown>;
|
||||
if (!validateSessionsListParams(params)) {
|
||||
@@ -3814,7 +4039,7 @@ export async function startGatewayServer(port = 18789): Promise<GatewayServer> {
|
||||
});
|
||||
|
||||
defaultRuntime.log(
|
||||
`gateway listening on ws://127.0.0.1:${port} (PID ${process.pid})`,
|
||||
`gateway listening on ws://${bindHost}:${port} (PID ${process.pid})`,
|
||||
);
|
||||
defaultRuntime.log(`gateway log file: ${getResolvedLoggerSettings().file}`);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user