diff --git a/apps/macos/Sources/Clawdis/Utilities.swift b/apps/macos/Sources/Clawdis/Utilities.swift index 5319ca366..03af58a23 100644 --- a/apps/macos/Sources/Clawdis/Utilities.swift +++ b/apps/macos/Sources/Clawdis/Utilities.swift @@ -348,6 +348,7 @@ enum CommandResolver { let quotedArgs = ([subcommand] + extraArgs).map(self.shellQuote).joined(separator: " ") let scriptBody = """ PATH=\(exportedPath); + CLI=""; \(prjVar) if command -v clawdis >/dev/null 2>&1; then \(cdPrefix)clawdis \(quotedArgs); diff --git a/src/auto-reply/status.ts b/src/auto-reply/status.ts index 48a0515e8..f938ef6c8 100644 --- a/src/auto-reply/status.ts +++ b/src/auto-reply/status.ts @@ -150,7 +150,10 @@ const readUsageFromSessionLog = ( if (!line.trim()) continue; try { const parsed = JSON.parse(line) as { - message?: { usage?: { input?: number; output?: number; total?: number }; model?: string }; + message?: { + usage?: { input?: number; output?: number; total?: number }; + model?: string; + }; usage?: { input?: number; output?: number; total?: number }; model?: string; }; diff --git a/src/cli/program.test.ts b/src/cli/program.test.ts index 1e3fe5821..aee606eb0 100644 --- a/src/cli/program.test.ts +++ b/src/cli/program.test.ts @@ -57,7 +57,14 @@ describe("cli program", () => { monitorWebProvider.mockResolvedValue(undefined); const program = buildProgram(); await program.parseAsync( - ["relay", "--web-heartbeat", "90", "--heartbeat-now", "--provider", "web"], + [ + "relay", + "--web-heartbeat", + "90", + "--heartbeat-now", + "--provider", + "web", + ], { from: "user", }, diff --git a/src/cli/program.ts b/src/cli/program.ts index 31666e062..05bf812ca 100644 --- a/src/cli/program.ts +++ b/src/cli/program.ts @@ -17,17 +17,16 @@ import { setHeartbeatsEnabled, type WebMonitorTuning, } from "../provider-web.js"; -import { - startWebChatServer, - getWebChatServer, - ensureWebChatServerFromConfig, -} from "../webchat/server.js"; import { defaultRuntime, type RuntimeEnv } from "../runtime.js"; import { VERSION } from "../version.js"; import { resolveHeartbeatSeconds, resolveReconnectPolicy, } from "../web/reconnect.js"; +import { + ensureWebChatServerFromConfig, + startWebChatServer, +} from "../webchat/server.js"; import { createDefaultDeps, logWebSelfId } from "./deps.js"; export function buildProgram() { @@ -36,10 +35,7 @@ export function buildProgram() { const TAGLINE = "Send, receive, and auto-reply on WhatsApp (web) and Telegram (bot)."; - program - .name("clawdis") - .description("") - .version(PROGRAM_VERSION); + program.name("clawdis").description("").version(PROGRAM_VERSION); const formatIntroLine = (version: string, rich = true) => { const base = `📡 clawdis ${version} — ${TAGLINE}`; @@ -428,7 +424,11 @@ Examples: "Run a heartbeat immediately when relay starts", false, ) - .option("--webhook", "Run Telegram webhook server instead of long-poll", false) + .option( + "--webhook", + "Run Telegram webhook server instead of long-poll", + false, + ) .option( "--webhook-path ", "Telegram webhook path (default /telegram-webhook when webhook enabled)", @@ -478,7 +478,6 @@ Examples: startWeb = true; startTelegram = true; break; - case "auto": default: startWeb = true; startTelegram = Boolean(telegramToken); @@ -581,7 +580,10 @@ Examples: cfg, webTuning.heartbeatSeconds, ); - const effectivePolicy = resolveReconnectPolicy(cfg, webTuning.reconnect); + const effectivePolicy = resolveReconnectPolicy( + cfg, + webTuning.reconnect, + ); defaultRuntime.log( info( `Web relay health: heartbeat ${effectiveHeartbeat}s, retries ${effectivePolicy.maxAttempts || "∞"}, backoff ${effectivePolicy.initialMs}→${effectivePolicy.maxMs}ms x${effectivePolicy.factor} (jitter ${Math.round(effectivePolicy.jitter * 100)}%)`, @@ -745,7 +747,9 @@ Shows token usage per session when the agent reports it; set inbound.reply.agent .option("--port ", "Port to bind (default 18788)") .option("--json", "Return JSON", false) .action(async (opts) => { - const port = opts.port ? Number.parseInt(String(opts.port), 10) : undefined; + const port = opts.port + ? Number.parseInt(String(opts.port), 10) + : undefined; const server = await startWebChatServer(port); const payload = { port: server.port, @@ -755,7 +759,9 @@ Shows token usage per session when the agent reports it; set inbound.reply.agent if (opts.json) { defaultRuntime.log(JSON.stringify(payload)); } else { - defaultRuntime.log(info(`webchat listening on http://127.0.0.1:${server.port}/webchat/`)); + defaultRuntime.log( + info(`webchat listening on http://127.0.0.1:${server.port}/webchat/`), + ); } }); diff --git a/src/webchat/server.ts b/src/webchat/server.ts index 4e1728528..67bcff9f9 100644 --- a/src/webchat/server.ts +++ b/src/webchat/server.ts @@ -1,20 +1,19 @@ -import http from "node:http"; -import path from "node:path"; -import os from "node:os"; -import { fileURLToPath } from "node:url"; import crypto from "node:crypto"; import fs from "node:fs"; +import http from "node:http"; +import os from "node:os"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; import sharp from "sharp"; +import { agentCommand } from "../commands/agent.js"; import { loadConfig } from "../config/config.js"; import { loadSessionStore, resolveStorePath, type SessionEntry, } from "../config/sessions.js"; -import { danger, info } from "../globals.js"; import { logDebug } from "../logger.js"; -import { agentCommand } from "../commands/agent.js"; import type { RuntimeEnv } from "../runtime.js"; const WEBCHAT_DEFAULT_PORT = 18788; @@ -24,6 +23,14 @@ type WebChatServerState = { port: number; }; +type ChatMessage = { role: string; content: string }; +type AttachmentInput = { + content?: string; + mimeType?: string; + fileName?: string; + type?: string; +}; + let state: WebChatServerState | null = null; function resolveWebRoot() { @@ -32,11 +39,17 @@ function resolveWebRoot() { // 1) Packaged app: resources live next to the relay bundle at // Contents/Resources/WebChat. The relay binary runs from // Contents/Resources/Relay/bun, so walk up one and check. - const packagedRoot = path.resolve(path.dirname(process.execPath), "../WebChat"); + const packagedRoot = path.resolve( + path.dirname(process.execPath), + "../WebChat", + ); if (fs.existsSync(packagedRoot)) return packagedRoot; // 2) Dev / source checkout: repo-relative path. - return path.resolve(here, "../../apps/macos/Sources/Clawdis/Resources/WebChat"); + return path.resolve( + here, + "../../apps/macos/Sources/Clawdis/Resources/WebChat", + ); } function readBody(req: http.IncomingMessage): Promise { @@ -49,15 +62,28 @@ function readBody(req: http.IncomingMessage): Promise { }); } -function pickSessionId(sessionKey: string, store: Record): string | null { +function pickSessionId( + sessionKey: string, + store: Record, +): string | null { if (store[sessionKey]?.sessionId) return store[sessionKey].sessionId; const first = Object.values(store)[0]?.sessionId; return first ?? null; } -function readSessionMessages(sessionId: string, storePath: string): any[] { +function readSessionMessages( + sessionId: string, + storePath: string, +): ChatMessage[] { const dir = path.dirname(storePath); - const candidates = [path.join(dir, `${sessionId}.jsonl`), path.join(os.homedir(), ".tau/agent/sessions/clawdis", `${sessionId}.jsonl`)]; + const candidates = [ + path.join(dir, `${sessionId}.jsonl`), + path.join( + os.homedir(), + ".tau/agent/sessions/clawdis", + `${sessionId}.jsonl`, + ), + ]; let content: string | null = null; for (const p of candidates) { if (fs.existsSync(p)) { @@ -71,7 +97,7 @@ function readSessionMessages(sessionId: string, storePath: string): any[] { } if (!content) return []; - const messages: any[] = []; + const messages: ChatMessage[] = []; for (const line of content.split(/\r?\n/)) { if (!line.trim()) continue; try { @@ -87,22 +113,32 @@ function readSessionMessages(sessionId: string, storePath: string): any[] { } async function persistAttachments( - attachments: any[], + attachments: AttachmentInput[], sessionId: string, ): Promise<{ placeholder: string; path: string }[]> { const out: { placeholder: string; path: string }[] = []; if (!attachments?.length) return out; - const root = path.join(os.homedir(), ".clawdis", "webchat-uploads", sessionId); + const root = path.join( + os.homedir(), + ".clawdis", + "webchat-uploads", + sessionId, + ); await fs.promises.mkdir(root, { recursive: true }); let idx = 1; for (const att of attachments) { try { if (!att?.content || typeof att.content !== "string") continue; - const mime = typeof att.mimeType === "string" ? att.mimeType : "application/octet-stream"; + const mime = + typeof att.mimeType === "string" + ? att.mimeType + : "application/octet-stream"; const baseName = att.fileName || `${att.type || "attachment"}-${idx}`; - const ext = mime.startsWith("image/") ? mime.split("/")[1] || "bin" : "bin"; + const ext = mime.startsWith("image/") + ? mime.split("/")[1] || "bin" + : "bin"; const fileName = `${baseName}.${ext}`.replace(/[^a-zA-Z0-9._-]/g, "_"); const buf = Buffer.from(att.content, "base64"); @@ -113,7 +149,8 @@ async function persistAttachments( const image = sharp(buf, { failOn: "none" }); meta = await image.metadata(); const needsResize = - (meta.width && meta.width > 2000) || (meta.height && meta.height > 2000); + (meta.width && meta.width > 2000) || + (meta.height && meta.height > 2000); if (needsResize) { const resized = await image .resize({ width: 2000, height: 2000, fit: "inside" }) @@ -137,7 +174,8 @@ async function persistAttachments( await fs.promises.writeFile(dest, finalBuf); const sizeLabel = `${(finalBuf.length / 1024).toFixed(0)} KB`; - const dimLabel = meta?.width && meta?.height ? `, ${meta.width}x${meta.height}` : ""; + const dimLabel = + meta?.width && meta?.height ? `, ${meta.width}x${meta.height}` : ""; const placeholder = `[Attachment saved: ${dest} (${mime}${dimLabel}, ${sizeLabel})]`; out.push({ placeholder, path: dest }); } catch (err) { @@ -149,16 +187,34 @@ async function persistAttachments( return out; } -function formatMessageWithAttachments(text: string, saved: { placeholder: string }[]): string { +function formatMessageWithAttachments( + text: string, + saved: { placeholder: string }[], +): string { if (!saved || saved.length === 0) return text; const parts = [text, ...saved.map((s) => `\n\n${s.placeholder}`)]; return parts.join(""); } -async function handleRpc(body: any, sessionKey: string): Promise<{ ok: boolean; payloads?: any[]; error?: string }> { - const text: string = (body?.text ?? "").toString(); +type RpcPayload = { role: string; content: string }; + +async function handleRpc( + body: unknown, + sessionKey: string, +): Promise<{ ok: boolean; payloads?: RpcPayload[]; error?: string }> { + const payload = body as { + text?: unknown; + attachments?: unknown; + thinking?: unknown; + deliver?: unknown; + to?: unknown; + }; + + const text: string = (payload.text ?? "").toString(); if (!text.trim()) return { ok: false, error: "empty text" }; - const attachments = Array.isArray(body?.attachments) ? body.attachments : []; + const attachments = Array.isArray(payload.attachments) + ? (payload.attachments as AttachmentInput[]) + : []; const cfg = loadConfig(); const replyCfg = cfg.inbound?.reply; @@ -222,7 +278,10 @@ export async function startWebChatServer(port = WEBCHAT_DEFAULT_PORT) { const server = http.createServer(async (req, res) => { if (!req.url) return notFound(res); // enforce loopback only - if (req.socket.remoteAddress && !req.socket.remoteAddress.startsWith("127.")) { + if ( + req.socket.remoteAddress && + !req.socket.remoteAddress.startsWith("127.") + ) { res.statusCode = 403; res.end("loopback only"); return; @@ -241,7 +300,9 @@ export async function startWebChatServer(port = WEBCHAT_DEFAULT_PORT) { : resolveStorePath(undefined); const store = loadSessionStore(storePath); const sessionId = pickSessionId(sessionKey, store); - const messages = sessionId ? readSessionMessages(sessionId, storePath) : []; + const messages = sessionId + ? readSessionMessages(sessionId, storePath) + : []; res.setHeader("Content-Type", "application/json"); res.end( JSON.stringify({ @@ -258,7 +319,7 @@ export async function startWebChatServer(port = WEBCHAT_DEFAULT_PORT) { if (isRpc && req.method === "POST") { const bodyBuf = await readBody(req); - let body: any = {}; + let body: Record = {}; try { body = JSON.parse(bodyBuf.toString("utf-8")); } catch { @@ -273,7 +334,7 @@ export async function startWebChatServer(port = WEBCHAT_DEFAULT_PORT) { if (url.pathname.startsWith("/webchat")) { let rel = url.pathname.replace(/^\/webchat\/?/, ""); - if (!rel || rel.endsWith("/")) rel = rel + "index.html"; + if (!rel || rel.endsWith("/")) rel = `${rel}index.html`; const filePath = path.join(root, rel); if (!filePath.startsWith(root)) return notFound(res); if (!fs.existsSync(filePath)) return notFound(res);