@clickonsearch/whatsapp-agent 0.1.0

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.
package/README.md ADDED
@@ -0,0 +1,121 @@
1
+ # whatsapp-agent
2
+
3
+ A personal WhatsApp assistant, usable two ways:
4
+
5
+ - **Over WhatsApp itself** — message **yourself** ("Message yourself" —
6
+ Settings → your own contact) to give it an instruction, and it replies in
7
+ that same chat.
8
+ - **From a browser chat UI** — an AG-UI protocol server (same pattern as
9
+ [`github-agent`](../github-agent)) with a bundled chat page, for when
10
+ typing in a web page is easier than typing on your phone, or you want to
11
+ embed it in your own tooling.
12
+
13
+ Both modes share the same underlying agent loop and the same WhatsApp MCP
14
+ tools, so "send Alex a message asking if she's free tomorrow" works
15
+ identically either way — it really sends it, through your linked account.
16
+
17
+ Requires [`@clickonsearch/whatsapp-mcp-server`](../../mcp-servers/whatsapp-mcp-server)
18
+ running first — that's the piece that actually holds your WhatsApp session.
19
+ This package is just the agent loop(s) on top of it.
20
+
21
+ ## How it works: WhatsApp mode
22
+
23
+ Unlike `github-agent` (which only waits for an HTTP request), this mode is
24
+ push-driven:
25
+
26
+ 1. Connects to `whatsapp-mcp-server`'s `/events` SSE stream and watches for
27
+ messages where `fromMe` is true and the chat is your own JID — i.e.
28
+ messages you sent to yourself.
29
+ 2. Each one becomes the prompt for the normal provider tool-calling loop
30
+ (same `openai/claude/deepseek-remote-mcp-bridge` packages `github-agent`
31
+ uses), with the WhatsApp MCP tools (`send_message`, `list_chats`,
32
+ `get_recent_messages`, `search_contacts`) available — so "send Alex a
33
+ message asking if she's free tomorrow" or "summarize my last 20 messages
34
+ with Sam" both work as tool calls, not just chat.
35
+ 3. The agent's final answer is sent back to you in the same self-chat via
36
+ `send_message`.
37
+
38
+ ```
39
+ you: message yourself on WhatsApp
40
+ │
41
+ ▼
42
+ whatsapp-mcp-server (Baileys) ──/events──▶ whatsapp-agent
43
+ ▲ │
44
+ │ ▼
45
+ └──────── send_message ─────── model + WhatsApp tools
46
+ ```
47
+
48
+ ## Setup
49
+
50
+ ```bash
51
+ # 1. start the MCP server first (separate terminal, separate package)
52
+ cd mcp-servers/whatsapp-mcp-server
53
+ npm install && cp .env.example .env
54
+ npm run dev # scan the QR code it prints
55
+
56
+ # 2. then this agent
57
+ cd agents/whatsapp-agent
58
+ npm install
59
+ cp .env.example .env
60
+ # fill in WHATSAPP_MCP_URL (if not localhost:4100) and your provider's API key
61
+ ```
62
+
63
+ ## Run: WhatsApp mode
64
+
65
+ ```bash
66
+ npm run dev
67
+ ```
68
+
69
+ Then open WhatsApp on your phone, message yourself something like "list my
70
+ most recent chats" or "send a message to +1555... saying I'm running 10
71
+ minutes late", and watch the reply come back in the same chat.
72
+
73
+ ## Run: browser chat UI (AG-UI server)
74
+
75
+ ```bash
76
+ npm run build
77
+ npm run serve # or `npm run dev:serve` for tsx, no build step
78
+ ```
79
+
80
+ This starts an HTTP server (default `http://localhost:3100`) with:
81
+
82
+ - `GET /` — a self-contained chat page (`public/index.html`) with a
83
+ provider dropdown. Open it in a browser and talk to the assistant.
84
+ - `POST /agent` — the AG-UI protocol endpoint (`RunAgentInput` in, AG-UI
85
+ SSE events out) — the exact same shape as `github-agent`'s, so any
86
+ AG-UI client can drive this one too, not just the bundled page.
87
+ - `GET /health` — liveness check.
88
+
89
+ You can run this alongside WhatsApp mode (`npm run dev` in one terminal,
90
+ `npm run serve` in another) — they're independent processes hitting the
91
+ same `whatsapp-mcp-server`, so either one can send/read messages at any
92
+ time.
93
+
94
+ ## Config
95
+
96
+ | Env var | Required | Notes |
97
+ | -------------------- | ----------------------- | --------------------------------------------------- |
98
+ | `WHATSAPP_MCP_URL` | no (default `http://localhost:4100`) | where whatsapp-mcp-server is running |
99
+ | `PORT` | no (default `3100`) | browser chat UI / AG-UI server only |
100
+ | `PROVIDER` | no (default `openai`) | `openai`, `claude`, or `deepseek` |
101
+ | `OPENAI_API_KEY` | when provider=openai | |
102
+ | `OPENAI_MODEL` | no (default `gpt-4.1`) | |
103
+ | `ANTHROPIC_API_KEY` | when provider=claude | |
104
+ | `ANTHROPIC_MODEL` | no (default `claude-sonnet-5`) | |
105
+ | `DEEPSEEK_API_KEY` | when provider=deepseek | |
106
+ | `DEEPSEEK_MODEL` | no (default `deepseek-chat`) | |
107
+
108
+ ## Known limitations (v1)
109
+
110
+ - WhatsApp mode is single-turn per instruction: each self-message is its
111
+ own fresh prompt, with no memory across separate instructions. The
112
+ browser chat UI (AG-UI mode) doesn't have this limitation — like
113
+ `github-agent`, it flattens prior turns in the conversation into a system
114
+ preamble, so follow-up questions in the same chat session work.
115
+ - Only your own "Message yourself" chat triggers the WhatsApp-mode
116
+ assistant. Any other incoming message is ignored by design, so it can't
117
+ be triggered by someone messaging you.
118
+ - `whatsapp-mcp-server`'s chat/message recall is in-memory only (not
119
+ persisted across restarts) and capped at the last 200 messages per chat —
120
+ it does get real history via WhatsApp's own backfill-on-connect, just not
121
+ unbounded history.
package/dist/agui.js ADDED
@@ -0,0 +1,88 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { EventType, contentToText } from "@ag-ui/core";
3
+ import { EventEncoder } from "@ag-ui/encoder";
4
+ import { runAssistantAgent, isProvider } from "./providers.js";
5
+ /**
6
+ * Mirrors agents/github-agent/src/agui.ts. The agent loop only takes a
7
+ * single prompt string, not a structured history, so every prior
8
+ * user/assistant message is flattened into a text transcript passed as a
9
+ * system preamble, and the most recent user message becomes the prompt.
10
+ */
11
+ function extractPrompt(messages) {
12
+ const lastUserIndex = messages.map((m) => m.role).lastIndexOf("user");
13
+ if (lastUserIndex === -1) {
14
+ throw new Error("RunAgentInput.messages must include at least one user message");
15
+ }
16
+ const lastUser = messages[lastUserIndex];
17
+ const userPrompt = contentToText(lastUser.content);
18
+ const transcript = messages
19
+ .slice(0, lastUserIndex)
20
+ .filter((m) => m.role === "user" || m.role === "assistant")
21
+ .map((m) => `${m.role}: ${m.role === "assistant" ? (m.content ?? "") : contentToText(m.content)}`)
22
+ .filter((line) => line.length > "user: ".length || line.length > "assistant: ".length)
23
+ .join("\n");
24
+ return {
25
+ systemPrompt: transcript ? `Prior conversation for context:\n${transcript}` : undefined,
26
+ userPrompt,
27
+ };
28
+ }
29
+ /**
30
+ * Runs the personal assistant for one AG-UI `RunAgentInput`, streaming the
31
+ * result to `res` as AG-UI protocol events (SSE by default).
32
+ */
33
+ export async function runAgUiRequest(params) {
34
+ const { input, mcpServerUrl, defaultProvider, res, acceptHeader } = params;
35
+ const encoder = new EventEncoder({ accept: acceptHeader });
36
+ res.writeHead(200, {
37
+ "Content-Type": encoder.getContentType(),
38
+ "Cache-Control": "no-cache, no-transform",
39
+ Connection: "keep-alive",
40
+ "X-Accel-Buffering": "no",
41
+ });
42
+ const emit = (event) => {
43
+ res.write(encoder.encode(event));
44
+ };
45
+ const threadId = input.threadId ?? randomUUID();
46
+ const runId = input.runId ?? randomUUID();
47
+ emit({ type: EventType.RUN_STARTED, threadId, runId });
48
+ try {
49
+ const provider = isProvider(input.forwardedProps?.provider) ? input.forwardedProps.provider : defaultProvider;
50
+ const { systemPrompt, userPrompt } = extractPrompt(input.messages);
51
+ await runAssistantAgent(provider, {
52
+ prompt: userPrompt,
53
+ systemPrompt,
54
+ mcpServerUrl,
55
+ onAssistantText: (text) => {
56
+ const messageId = randomUUID();
57
+ emit({ type: EventType.TEXT_MESSAGE_START, messageId, role: "assistant" });
58
+ emit({ type: EventType.TEXT_MESSAGE_CONTENT, messageId, delta: text });
59
+ emit({ type: EventType.TEXT_MESSAGE_END, messageId });
60
+ },
61
+ onToolCall: (name, args, toolCallId) => {
62
+ emit({ type: EventType.TOOL_CALL_START, toolCallId, toolCallName: name });
63
+ emit({ type: EventType.TOOL_CALL_ARGS, toolCallId, delta: JSON.stringify(args ?? {}) });
64
+ emit({ type: EventType.TOOL_CALL_END, toolCallId });
65
+ },
66
+ onToolResult: (toolCallId, _name, result) => {
67
+ emit({
68
+ type: EventType.TOOL_CALL_RESULT,
69
+ messageId: randomUUID(),
70
+ toolCallId,
71
+ content: result,
72
+ role: "tool",
73
+ });
74
+ },
75
+ });
76
+ emit({ type: EventType.RUN_FINISHED, threadId, runId });
77
+ }
78
+ catch (error) {
79
+ emit({
80
+ type: EventType.RUN_ERROR,
81
+ message: error instanceof Error ? error.message : String(error),
82
+ });
83
+ }
84
+ finally {
85
+ res.end();
86
+ }
87
+ }
88
+ //# sourceMappingURL=agui.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agui.js","sourceRoot":"","sources":["../src/agui.ts"],"names":[],"mappings":"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"}
package/dist/cli.js ADDED
@@ -0,0 +1,36 @@
1
+ #!/usr/bin/env node
2
+ import "dotenv/config";
3
+ import { startListener } from "./listener.js";
4
+ import { isProvider, PROVIDERS } from "./providers.js";
5
+ function resolveProvider() {
6
+ const fromEnv = process.env.PROVIDER;
7
+ if (isProvider(fromEnv))
8
+ return fromEnv;
9
+ if (fromEnv) {
10
+ throw new Error(`PROVIDER must be one of ${PROVIDERS.join(", ")}, got "${fromEnv}"`);
11
+ }
12
+ return "openai";
13
+ }
14
+ async function main() {
15
+ let provider;
16
+ try {
17
+ provider = resolveProvider();
18
+ }
19
+ catch (error) {
20
+ console.error(error instanceof Error ? error.message : String(error));
21
+ process.exitCode = 1;
22
+ return;
23
+ }
24
+ const mcpServerUrl = process.env.WHATSAPP_MCP_URL ?? "http://localhost:4100";
25
+ console.error(`whatsapp-agent starting (initial provider: ${provider}, whatsapp-mcp-server: ${mcpServerUrl})`);
26
+ await startListener({
27
+ mcpServerUrl,
28
+ provider,
29
+ onLog: (line) => console.error(line),
30
+ });
31
+ }
32
+ main().catch((error) => {
33
+ console.error(error);
34
+ process.exitCode = 1;
35
+ });
36
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA,OAAO,eAAe,CAAC;AACvB,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EAAE,UAAU,EAAE,SAAS,EAAiB,MAAM,gBAAgB,CAAC;AAEtE,SAAS,eAAe;IACtB,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC;IACrC,IAAI,UAAU,CAAC,OAAO,CAAC;QAAE,OAAO,OAAO,CAAC;IACxC,IAAI,OAAO,EAAE,CAAC;QACZ,MAAM,IAAI,KAAK,CAAC,2BAA2B,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,OAAO,GAAG,CAAC,CAAC;IACvF,CAAC;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,KAAK,UAAU,IAAI;IACjB,IAAI,QAAkB,CAAC;IACvB,IAAI,CAAC;QACH,QAAQ,GAAG,eAAe,EAAE,CAAC;IAC/B,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,CAAC,KAAK,CAAC,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;QACtE,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAC;QACrB,OAAO;IACT,CAAC;IAED,MAAM,YAAY,GAAG,OAAO,CAAC,GAAG,CAAC,gBAAgB,IAAI,uBAAuB,CAAC;IAE7E,OAAO,CAAC,KAAK,CAAC,8CAA8C,QAAQ,0BAA0B,YAAY,GAAG,CAAC,CAAC;IAE/G,MAAM,aAAa,CAAC;QAClB,YAAY;QACZ,QAAQ;QACR,KAAK,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC;KACrC,CAAC,CAAC;AACL,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,KAAK,EAAE,EAAE;IACrB,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IACrB,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAC;AACvB,CAAC,CAAC,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ export { runAssistantAgent, whatsappServerConfig, WHATSAPP_SERVER_NAME, isProvider, PROVIDERS } from "./providers.js";
2
+ export { startListener } from "./listener.js";
3
+ export { createServer } from "./server.js";
4
+ export { runAgUiRequest } from "./agui.js";
5
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,oBAAoB,EAAE,oBAAoB,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAEtH,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAE9C,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C,OAAO,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC"}
@@ -0,0 +1,93 @@
1
+ import { setTimeout as sleep } from "node:timers/promises";
2
+ import { McpManager } from "@clickonsearch/openai-remote-mcp-bridge";
3
+ import { runAssistantAgent, whatsappServerConfig } from "./providers.js";
4
+ /** Polls the whatsapp-mcp-server's /health until it reports a connected session, returning the self JID. */
5
+ async function waitForSelfId(mcpServerUrl, onLog) {
6
+ const healthUrl = new URL("/health", mcpServerUrl).toString();
7
+ for (;;) {
8
+ try {
9
+ const res = await fetch(healthUrl);
10
+ const body = (await res.json());
11
+ if (body.connected && body.self)
12
+ return body.self;
13
+ }
14
+ catch {
15
+ // whatsapp-mcp-server isn't up yet
16
+ }
17
+ onLog?.("Waiting for whatsapp-mcp-server to report a connected WhatsApp session...");
18
+ await sleep(2000);
19
+ }
20
+ }
21
+ /** Delivers the assistant's reply back to the user's own "Message yourself" chat. */
22
+ async function replyToSelf(mcpServerUrl, selfId, text) {
23
+ const mcp = new McpManager();
24
+ await mcp.connectAll([whatsappServerConfig(mcpServerUrl)]);
25
+ try {
26
+ await mcp.callTool("whatsapp__send_message", { to: selfId, text });
27
+ }
28
+ finally {
29
+ await mcp.closeAll();
30
+ }
31
+ }
32
+ /**
33
+ * Connects to the whatsapp-mcp-server's SSE /events stream and watches for
34
+ * messages the user sent to themselves — that's the "instruction inbox".
35
+ * Each one runs through the agent loop (with the WhatsApp MCP tools
36
+ * available) and the final answer is sent back to the same self-chat.
37
+ *
38
+ * The provider is fixed for the life of this process (from the PROVIDER
39
+ * env var) — see agents/whatsapp-agent's AG-UI server if you want to pick
40
+ * a provider per-request instead.
41
+ */
42
+ export async function startListener(options) {
43
+ const { mcpServerUrl, provider, onLog } = options;
44
+ const selfId = await waitForSelfId(mcpServerUrl, onLog);
45
+ onLog?.(`Connected. Listening for instructions in your "Message yourself" chat (${selfId}). Provider: ${provider}.`);
46
+ await replyToSelf(mcpServerUrl, selfId, `Assistant is online (provider: ${provider}).`);
47
+ const eventsUrl = new URL("/events", mcpServerUrl).toString();
48
+ const res = await fetch(eventsUrl);
49
+ if (!res.ok || !res.body) {
50
+ throw new Error(`Failed to connect to whatsapp-mcp-server's events stream: HTTP ${res.status}`);
51
+ }
52
+ const reader = res.body.getReader();
53
+ const decoder = new TextDecoder();
54
+ let buffer = "";
55
+ while (true) {
56
+ const { done, value } = await reader.read();
57
+ if (done)
58
+ break;
59
+ buffer += decoder.decode(value, { stream: true });
60
+ let sep;
61
+ while ((sep = buffer.indexOf("\n\n")) !== -1) {
62
+ const frame = buffer.slice(0, sep);
63
+ buffer = buffer.slice(sep + 2);
64
+ const line = frame.split("\n").find((l) => l.startsWith("data:"));
65
+ if (!line)
66
+ continue;
67
+ let event;
68
+ try {
69
+ event = JSON.parse(line.slice(5).trim());
70
+ }
71
+ catch {
72
+ continue;
73
+ }
74
+ if (!event.isSelfChat)
75
+ continue;
76
+ onLog?.(`Instruction (${provider}): ${event.text}`);
77
+ try {
78
+ const answer = await runAssistantAgent(provider, {
79
+ prompt: event.text,
80
+ mcpServerUrl,
81
+ onToolCall: (name, args) => onLog?.(` tool call: ${name}(${JSON.stringify(args)})`),
82
+ });
83
+ await replyToSelf(mcpServerUrl, selfId, answer.trim() || "Done.");
84
+ }
85
+ catch (error) {
86
+ const message = error instanceof Error ? error.message : String(error);
87
+ onLog?.(` error: ${message}`);
88
+ await replyToSelf(mcpServerUrl, selfId, `Sorry, something went wrong: ${message}`);
89
+ }
90
+ }
91
+ }
92
+ }
93
+ //# sourceMappingURL=listener.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"listener.js","sourceRoot":"","sources":["../src/listener.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Mirrors agents/github-agent/src/providers.ts. Adding a new remote MCP
3
+ * bridge means: add its `runWith*` function below, add it here, and add
4
+ * its API key/model env vars to the README/.env.example.
5
+ */
6
+ export const PROVIDERS = ["openai", "claude", "deepseek"];
7
+ export function isProvider(value) {
8
+ return typeof value === "string" && PROVIDERS.includes(value);
9
+ }
10
+ export const WHATSAPP_SERVER_NAME = "whatsapp";
11
+ /**
12
+ * `baseUrl` is whatsapp-mcp-server's origin (e.g. "http://localhost:4100"),
13
+ * the same value used for its /health and /events endpoints elsewhere in
14
+ * this package — its actual MCP endpoint is at /mcp.
15
+ */
16
+ export function whatsappServerConfig(baseUrl) {
17
+ return { name: WHATSAPP_SERVER_NAME, url: new URL("/mcp", baseUrl).toString(), transport: "http" };
18
+ }
19
+ export const DEFAULT_SYSTEM_PROMPT = [
20
+ "You are a personal WhatsApp assistant. The user just messaged themselves",
21
+ "on WhatsApp to give you an instruction. You have tools to send WhatsApp",
22
+ "messages, list chats, read recent messages, and search contacts.",
23
+ "Complete the requested task using those tools, then reply with a short,",
24
+ "plain-text summary of what you did (or the answer, if it was a question).",
25
+ "This summary is sent back to the user as a WhatsApp message, so keep it",
26
+ "conversational and brief.",
27
+ ].join(" ");
28
+ async function runWithOpenAi(options) {
29
+ const { McpManager, runAgent } = await import("@clickonsearch/openai-remote-mcp-bridge");
30
+ const { default: OpenAI } = await import("openai");
31
+ const apiKey = process.env.OPENAI_API_KEY;
32
+ if (!apiKey) {
33
+ throw new Error("Missing OPENAI_API_KEY environment variable");
34
+ }
35
+ const mcp = new McpManager();
36
+ await mcp.connectAll([whatsappServerConfig(options.mcpServerUrl)]);
37
+ try {
38
+ const openai = new OpenAI({ apiKey });
39
+ return await runAgent({
40
+ openai,
41
+ model: process.env.OPENAI_MODEL ?? "gpt-4.1",
42
+ systemPrompt: options.systemPrompt ?? DEFAULT_SYSTEM_PROMPT,
43
+ userPrompt: options.prompt,
44
+ mcp,
45
+ onToolCall: options.onToolCall,
46
+ onToolResult: options.onToolResult,
47
+ onAssistantText: options.onAssistantText,
48
+ });
49
+ }
50
+ finally {
51
+ await mcp.closeAll();
52
+ }
53
+ }
54
+ async function runWithClaude(options) {
55
+ const { McpManager, runAgent } = await import("@clickonsearch/claude-remote-mcp-bridge");
56
+ const { default: Anthropic } = await import("@anthropic-ai/sdk");
57
+ const apiKey = process.env.ANTHROPIC_API_KEY;
58
+ if (!apiKey) {
59
+ throw new Error("Missing ANTHROPIC_API_KEY environment variable");
60
+ }
61
+ const mcp = new McpManager();
62
+ await mcp.connectAll([whatsappServerConfig(options.mcpServerUrl)]);
63
+ try {
64
+ const anthropic = new Anthropic({ apiKey });
65
+ return await runAgent({
66
+ anthropic,
67
+ model: process.env.ANTHROPIC_MODEL ?? "claude-sonnet-5",
68
+ systemPrompt: options.systemPrompt ?? DEFAULT_SYSTEM_PROMPT,
69
+ userPrompt: options.prompt,
70
+ mcp,
71
+ onToolCall: options.onToolCall,
72
+ onToolResult: options.onToolResult,
73
+ onAssistantText: options.onAssistantText,
74
+ });
75
+ }
76
+ finally {
77
+ await mcp.closeAll();
78
+ }
79
+ }
80
+ async function runWithDeepSeek(options) {
81
+ const { McpManager, runAgent } = await import("@clickonsearch/deepseek-remote-mcp-bridge");
82
+ const { default: OpenAI } = await import("openai");
83
+ const apiKey = process.env.DEEPSEEK_API_KEY;
84
+ if (!apiKey) {
85
+ throw new Error("Missing DEEPSEEK_API_KEY environment variable");
86
+ }
87
+ const mcp = new McpManager();
88
+ await mcp.connectAll([whatsappServerConfig(options.mcpServerUrl)]);
89
+ try {
90
+ const deepseek = new OpenAI({
91
+ apiKey,
92
+ baseURL: process.env.DEEPSEEK_BASE_URL ?? "https://api.deepseek.com",
93
+ });
94
+ return await runAgent({
95
+ deepseek,
96
+ model: process.env.DEEPSEEK_MODEL ?? "deepseek-chat",
97
+ systemPrompt: options.systemPrompt ?? DEFAULT_SYSTEM_PROMPT,
98
+ userPrompt: options.prompt,
99
+ mcp,
100
+ onToolCall: options.onToolCall,
101
+ onToolResult: options.onToolResult,
102
+ onAssistantText: options.onAssistantText,
103
+ });
104
+ }
105
+ finally {
106
+ await mcp.closeAll();
107
+ }
108
+ }
109
+ /** Runs the personal assistant through the requested provider's remote MCP bridge. */
110
+ export async function runAssistantAgent(provider, options) {
111
+ if (provider === "claude")
112
+ return runWithClaude(options);
113
+ if (provider === "deepseek")
114
+ return runWithDeepSeek(options);
115
+ return runWithOpenAi(options);
116
+ }
117
+ //# sourceMappingURL=providers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"providers.js","sourceRoot":"","sources":["../src/providers.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,36 @@
1
+ #!/usr/bin/env node
2
+ import "dotenv/config";
3
+ import { createServer } from "./server.js";
4
+ import { isProvider, PROVIDERS } from "./providers.js";
5
+ function resolveDefaultProvider() {
6
+ const fromEnv = process.env.PROVIDER;
7
+ if (isProvider(fromEnv))
8
+ return fromEnv;
9
+ if (fromEnv) {
10
+ throw new Error(`PROVIDER must be one of ${PROVIDERS.join(", ")}, got "${fromEnv}"`);
11
+ }
12
+ return "openai";
13
+ }
14
+ function main() {
15
+ let defaultProvider;
16
+ try {
17
+ defaultProvider = resolveDefaultProvider();
18
+ }
19
+ catch (error) {
20
+ console.error(error instanceof Error ? error.message : String(error));
21
+ process.exitCode = 1;
22
+ return;
23
+ }
24
+ const mcpServerUrl = process.env.WHATSAPP_MCP_URL ?? "http://localhost:4100";
25
+ const port = Number(process.env.PORT ?? 3100);
26
+ const app = createServer({ mcpServerUrl, defaultProvider });
27
+ app.listen(port, () => {
28
+ console.error(`whatsapp-agent AG-UI server listening on http://localhost:${port}`);
29
+ console.error(` AG-UI endpoint: POST http://localhost:${port}/agent`);
30
+ console.error(` Chat UI: http://localhost:${port}/`);
31
+ console.error(` WhatsApp MCP server: ${mcpServerUrl}`);
32
+ console.error(` Default provider: ${defaultProvider} (override per-request via forwardedProps.provider)`);
33
+ });
34
+ }
35
+ main();
36
+ //# sourceMappingURL=server-cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server-cli.js","sourceRoot":"","sources":["../src/server-cli.ts"],"names":[],"mappings":";AACA,OAAO,eAAe,CAAC;AACvB,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,EAAE,UAAU,EAAE,SAAS,EAAiB,MAAM,gBAAgB,CAAC;AAEtE,SAAS,sBAAsB;IAC7B,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC;IACrC,IAAI,UAAU,CAAC,OAAO,CAAC;QAAE,OAAO,OAAO,CAAC;IACxC,IAAI,OAAO,EAAE,CAAC;QACZ,MAAM,IAAI,KAAK,CAAC,2BAA2B,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,OAAO,GAAG,CAAC,CAAC;IACvF,CAAC;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,SAAS,IAAI;IACX,IAAI,eAAyB,CAAC;IAC9B,IAAI,CAAC;QACH,eAAe,GAAG,sBAAsB,EAAE,CAAC;IAC7C,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,CAAC,KAAK,CAAC,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;QACtE,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAC;QACrB,OAAO;IACT,CAAC;IAED,MAAM,YAAY,GAAG,OAAO,CAAC,GAAG,CAAC,gBAAgB,IAAI,uBAAuB,CAAC;IAC7E,MAAM,IAAI,GAAG,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,IAAI,IAAI,CAAC,CAAC;IAE9C,MAAM,GAAG,GAAG,YAAY,CAAC,EAAE,YAAY,EAAE,eAAe,EAAE,CAAC,CAAC;IAE5D,GAAG,CAAC,MAAM,CAAC,IAAI,EAAE,GAAG,EAAE;QACpB,OAAO,CAAC,KAAK,CAAC,6DAA6D,IAAI,EAAE,CAAC,CAAC;QACnF,OAAO,CAAC,KAAK,CAAC,2CAA2C,IAAI,QAAQ,CAAC,CAAC;QACvE,OAAO,CAAC,KAAK,CAAC,sCAAsC,IAAI,GAAG,CAAC,CAAC;QAC7D,OAAO,CAAC,KAAK,CAAC,0BAA0B,YAAY,EAAE,CAAC,CAAC;QACxD,OAAO,CAAC,KAAK,CAAC,uBAAuB,eAAe,qDAAqD,CAAC,CAAC;IAC7G,CAAC,CAAC,CAAC;AACL,CAAC;AAED,IAAI,EAAE,CAAC"}
package/dist/server.js ADDED
@@ -0,0 +1,49 @@
1
+ import path from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+ import express from "express";
4
+ import cors from "cors";
5
+ import { runAgUiRequest } from "./agui.js";
6
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
7
+ export function createServer(options) {
8
+ const app = express();
9
+ app.use(cors());
10
+ app.use(express.json({ limit: "2mb" }));
11
+ app.use(express.static(path.join(__dirname, "..", "public")));
12
+ app.get("/health", (_req, res) => {
13
+ res.json({ ok: true, defaultProvider: options.defaultProvider });
14
+ });
15
+ app.post("/agent", async (req, res) => {
16
+ const body = req.body;
17
+ if (!body || !Array.isArray(body.messages)) {
18
+ res.status(400).json({ error: 'Request body must be a RunAgentInput with a "messages" array' });
19
+ return;
20
+ }
21
+ const input = {
22
+ threadId: body.threadId ?? "",
23
+ runId: body.runId ?? "",
24
+ messages: body.messages,
25
+ tools: body.tools ?? [],
26
+ context: body.context ?? [],
27
+ forwardedProps: body.forwardedProps,
28
+ };
29
+ try {
30
+ await runAgUiRequest({
31
+ input,
32
+ mcpServerUrl: options.mcpServerUrl,
33
+ defaultProvider: options.defaultProvider,
34
+ res,
35
+ acceptHeader: req.headers.accept,
36
+ });
37
+ }
38
+ catch (error) {
39
+ if (!res.headersSent) {
40
+ res.status(500).json({ error: error instanceof Error ? error.message : String(error) });
41
+ }
42
+ else {
43
+ res.end();
44
+ }
45
+ }
46
+ });
47
+ return app;
48
+ }
49
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,OAAO,MAAM,SAAS,CAAC;AAC9B,OAAO,IAAI,MAAM,MAAM,CAAC;AAExB,OAAO,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAG3C,MAAM,SAAS,GAAG,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AAO/D,MAAM,UAAU,YAAY,CAAC,OAA4B;IACvD,MAAM,GAAG,GAAG,OAAO,EAAE,CAAC;IACtB,GAAG,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC;IAChB,GAAG,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC;IACxC,GAAG,CAAC,GAAG,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,IAAI,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC;IAE9D,GAAG,CAAC,GAAG,CAAC,SAAS,EAAE,CAAC,IAAI,EAAE,GAAG,EAAE,EAAE;QAC/B,GAAG,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,eAAe,EAAE,OAAO,CAAC,eAAe,EAAE,CAAC,CAAC;IACnE,CAAC,CAAC,CAAC;IAEH,GAAG,CAAC,IAAI,CAAC,QAAQ,EAAE,KAAK,EAAE,GAAG,EAAE,GAAG,EAAE,EAAE;QACpC,MAAM,IAAI,GAAG,GAAG,CAAC,IAA0C,CAAC;QAE5D,IAAI,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC;YAC3C,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,8DAA8D,EAAE,CAAC,CAAC;YAChG,OAAO;QACT,CAAC;QAED,MAAM,KAAK,GAAkB;YAC3B,QAAQ,EAAE,IAAI,CAAC,QAAQ,IAAI,EAAE;YAC7B,KAAK,EAAE,IAAI,CAAC,KAAK,IAAI,EAAE;YACvB,QAAQ,EAAE,IAAI,CAAC,QAAQ;YACvB,KAAK,EAAE,IAAI,CAAC,KAAK,IAAI,EAAE;YACvB,OAAO,EAAE,IAAI,CAAC,OAAO,IAAI,EAAE;YAC3B,cAAc,EAAE,IAAI,CAAC,cAAc;SACpC,CAAC;QAEF,IAAI,CAAC;YACH,MAAM,cAAc,CAAC;gBACnB,KAAK;gBACL,YAAY,EAAE,OAAO,CAAC,YAAY;gBAClC,eAAe,EAAE,OAAO,CAAC,eAAe;gBACxC,GAAG;gBACH,YAAY,EAAE,GAAG,CAAC,OAAO,CAAC,MAAM;aACjC,CAAC,CAAC;QACL,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,CAAC,GAAG,CAAC,WAAW,EAAE,CAAC;gBACrB,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;YAC1F,CAAC;iBAAM,CAAC;gBACN,GAAG,CAAC,GAAG,EAAE,CAAC;YACZ,CAAC;QACH,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,OAAO,GAAG,CAAC;AACb,CAAC"}
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@clickonsearch/whatsapp-agent",
3
+ "version": "0.1.0",
4
+ "description": "Personal WhatsApp assistant, usable two ways: message yourself on WhatsApp to give it instructions, or drive it from a browser chat UI over the AG-UI protocol. Routed through OpenAI, Claude, or DeepSeek depending on preference.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/ClickOnSearch/ai-apps.git",
10
+ "directory": "agents/whatsapp-agent"
11
+ },
12
+ "publishConfig": {
13
+ "access": "public"
14
+ },
15
+ "files": [
16
+ "dist",
17
+ "public"
18
+ ],
19
+ "main": "dist/index.js",
20
+ "bin": {
21
+ "whatsapp-agent": "dist/cli.js",
22
+ "whatsapp-agent-serve": "dist/server-cli.js"
23
+ },
24
+ "scripts": {
25
+ "build": "tsc -p tsconfig.json",
26
+ "start": "node dist/cli.js",
27
+ "dev": "tsx src/cli.ts",
28
+ "serve": "node dist/server-cli.js",
29
+ "dev:serve": "tsx src/server-cli.ts"
30
+ },
31
+ "dependencies": {
32
+ "@clickonsearch/openai-remote-mcp-bridge": "^0.1.0",
33
+ "@clickonsearch/claude-remote-mcp-bridge": "^0.1.0",
34
+ "@clickonsearch/deepseek-remote-mcp-bridge": "^0.1.0",
35
+ "openai": "^4.68.0",
36
+ "@anthropic-ai/sdk": "^0.32.1",
37
+ "@ag-ui/core": "^1.0.0",
38
+ "@ag-ui/encoder": "^1.0.0",
39
+ "express": "^4.21.1",
40
+ "cors": "^2.8.5",
41
+ "dotenv": "^16.4.5"
42
+ },
43
+ "devDependencies": {
44
+ "typescript": "^5.6.3",
45
+ "tsx": "^4.19.1",
46
+ "@types/node": "^22.7.5",
47
+ "@types/express": "^4.17.21",
48
+ "@types/cors": "^2.8.17"
49
+ },
50
+ "engines": {
51
+ "node": ">=18"
52
+ }
53
+ }
@@ -0,0 +1,314 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>WhatsApp Assistant</title>
7
+ <style>
8
+ :root {
9
+ color-scheme: light dark;
10
+ --bg: #0b0d12;
11
+ --panel: #12151c;
12
+ --border: #232833;
13
+ --text: #e6e9ef;
14
+ --muted: #8b93a3;
15
+ --accent: #25d366;
16
+ --user-bubble: #1f6feb22;
17
+ --assistant-bubble: #171b24;
18
+ --tool-bubble: #1a2a1f;
19
+ --error-bubble: #3a1414;
20
+ }
21
+ * { box-sizing: border-box; }
22
+ body {
23
+ margin: 0;
24
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
25
+ background: var(--bg);
26
+ color: var(--text);
27
+ height: 100vh;
28
+ display: flex;
29
+ flex-direction: column;
30
+ }
31
+ header {
32
+ display: flex;
33
+ align-items: center;
34
+ justify-content: space-between;
35
+ padding: 12px 20px;
36
+ border-bottom: 1px solid var(--border);
37
+ background: var(--panel);
38
+ }
39
+ header h1 {
40
+ font-size: 15px;
41
+ font-weight: 600;
42
+ margin: 0;
43
+ }
44
+ header .sub {
45
+ color: var(--muted);
46
+ font-size: 12px;
47
+ margin-top: 2px;
48
+ }
49
+ select {
50
+ background: var(--panel);
51
+ color: var(--text);
52
+ border: 1px solid var(--border);
53
+ border-radius: 6px;
54
+ padding: 6px 10px;
55
+ font-size: 13px;
56
+ }
57
+ #log {
58
+ flex: 1;
59
+ overflow-y: auto;
60
+ padding: 20px;
61
+ display: flex;
62
+ flex-direction: column;
63
+ gap: 12px;
64
+ max-width: 820px;
65
+ width: 100%;
66
+ margin: 0 auto;
67
+ }
68
+ .bubble {
69
+ padding: 10px 14px;
70
+ border-radius: 10px;
71
+ max-width: 100%;
72
+ white-space: pre-wrap;
73
+ word-break: break-word;
74
+ line-height: 1.45;
75
+ font-size: 14px;
76
+ }
77
+ .user { align-self: flex-end; background: var(--user-bubble); border: 1px solid #1f6feb55; }
78
+ .assistant { align-self: flex-start; background: var(--assistant-bubble); border: 1px solid var(--border); }
79
+ .tool { align-self: flex-start; background: var(--tool-bubble); border: 1px solid #2f6b3f55; font-size: 12.5px; color: var(--muted); font-family: ui-monospace, monospace; }
80
+ .tool .name { color: #8fd19e; }
81
+ .tool .result { display: block; margin-top: 6px; color: var(--text); white-space: pre-wrap; max-height: 220px; overflow-y: auto; }
82
+ .error { align-self: center; background: var(--error-bubble); border: 1px solid #7a2424; color: #ffb4b4; font-size: 13px; }
83
+ .status { align-self: flex-start; color: var(--muted); font-size: 12.5px; font-style: italic; }
84
+ footer {
85
+ border-top: 1px solid var(--border);
86
+ background: var(--panel);
87
+ padding: 14px 20px;
88
+ }
89
+ form {
90
+ max-width: 820px;
91
+ margin: 0 auto;
92
+ display: flex;
93
+ gap: 10px;
94
+ }
95
+ textarea {
96
+ flex: 1;
97
+ resize: none;
98
+ background: #0e1117;
99
+ color: var(--text);
100
+ border: 1px solid var(--border);
101
+ border-radius: 8px;
102
+ padding: 10px 12px;
103
+ font-size: 14px;
104
+ font-family: inherit;
105
+ min-height: 42px;
106
+ max-height: 160px;
107
+ }
108
+ button {
109
+ background: var(--accent);
110
+ color: #06210f;
111
+ border: none;
112
+ border-radius: 8px;
113
+ padding: 0 18px;
114
+ font-size: 14px;
115
+ font-weight: 600;
116
+ cursor: pointer;
117
+ }
118
+ button:disabled { opacity: 0.5; cursor: default; }
119
+ </style>
120
+ </head>
121
+ <body>
122
+ <header>
123
+ <div>
124
+ <h1>WhatsApp assistant</h1>
125
+ <div class="sub">talks to your linked WhatsApp account over its MCP server, via an AG-UI protocol stream</div>
126
+ </div>
127
+ <select id="provider">
128
+ <option value="openai">OpenAI</option>
129
+ <option value="claude">Claude</option>
130
+ <option value="deepseek">DeepSeek</option>
131
+ </select>
132
+ </header>
133
+
134
+ <div id="log"></div>
135
+
136
+ <footer>
137
+ <form id="form">
138
+ <textarea id="input" placeholder="Ask it to check messages, send one, or look someone up..." rows="1"></textarea>
139
+ <button id="send" type="submit">Send</button>
140
+ </form>
141
+ </footer>
142
+
143
+ <script>
144
+ (() => {
145
+ const log = document.getElementById("log");
146
+ const form = document.getElementById("form");
147
+ const input = document.getElementById("input");
148
+ const sendButton = document.getElementById("send");
149
+ const providerSelect = document.getElementById("provider");
150
+
151
+ const threadId = crypto.randomUUID();
152
+ /** @type {{id: string, role: string, content: string}[]} */
153
+ const messages = [];
154
+
155
+ // Reflect the server's actual configured default (PROVIDER env var)
156
+ // instead of always showing the dropdown's first option.
157
+ fetch("/health")
158
+ .then((res) => res.json())
159
+ .then((data) => {
160
+ if (data.defaultProvider && providerSelect.querySelector(`option[value="${data.defaultProvider}"]`)) {
161
+ providerSelect.value = data.defaultProvider;
162
+ }
163
+ })
164
+ .catch(() => {});
165
+
166
+ function bubble(className, text) {
167
+ const el = document.createElement("div");
168
+ el.className = "bubble " + className;
169
+ el.textContent = text;
170
+ log.appendChild(el);
171
+ log.scrollTop = log.scrollHeight;
172
+ return el;
173
+ }
174
+
175
+ function setBusy(busy) {
176
+ sendButton.disabled = busy;
177
+ input.disabled = busy;
178
+ }
179
+
180
+ async function send(text) {
181
+ messages.push({ id: crypto.randomUUID(), role: "user", content: text });
182
+ bubble("user", text);
183
+
184
+ setBusy(true);
185
+ const statusEl = bubble("status", "thinking…");
186
+
187
+ const textBubbles = new Map(); // messageId -> { el, buffer }
188
+ const toolBubbles = new Map(); // toolCallId -> { el, nameEl, resultEl }
189
+
190
+ try {
191
+ const res = await fetch("/agent", {
192
+ method: "POST",
193
+ headers: { "Content-Type": "application/json" },
194
+ body: JSON.stringify({
195
+ threadId,
196
+ runId: crypto.randomUUID(),
197
+ messages,
198
+ forwardedProps: { provider: providerSelect.value },
199
+ }),
200
+ });
201
+
202
+ if (!res.ok || !res.body) {
203
+ throw new Error(`Request failed: ${res.status} ${res.statusText}`);
204
+ }
205
+
206
+ const reader = res.body.getReader();
207
+ const decoder = new TextDecoder();
208
+ let buffer = "";
209
+
210
+ while (true) {
211
+ const { done, value } = await reader.read();
212
+ if (done) break;
213
+ buffer += decoder.decode(value, { stream: true });
214
+
215
+ let sep;
216
+ while ((sep = buffer.indexOf("\n\n")) !== -1) {
217
+ const frame = buffer.slice(0, sep);
218
+ buffer = buffer.slice(sep + 2);
219
+ const line = frame.split("\n").find((l) => l.startsWith("data:"));
220
+ if (!line) continue;
221
+
222
+ const event = JSON.parse(line.slice(5).trim());
223
+ handleEvent(event);
224
+ }
225
+ }
226
+ } catch (err) {
227
+ bubble("error", String(err));
228
+ } finally {
229
+ statusEl.remove();
230
+ setBusy(false);
231
+ }
232
+
233
+ function handleEvent(event) {
234
+ switch (event.type) {
235
+ case "TEXT_MESSAGE_START": {
236
+ const el = bubble("assistant", "");
237
+ textBubbles.set(event.messageId, { el, buffer: "" });
238
+ break;
239
+ }
240
+ case "TEXT_MESSAGE_CONTENT": {
241
+ const entry = textBubbles.get(event.messageId);
242
+ if (entry) {
243
+ entry.buffer += event.delta;
244
+ entry.el.textContent = entry.buffer;
245
+ log.scrollTop = log.scrollHeight;
246
+ }
247
+ break;
248
+ }
249
+ case "TEXT_MESSAGE_END": {
250
+ const entry = textBubbles.get(event.messageId);
251
+ if (entry) {
252
+ messages.push({ id: event.messageId, role: "assistant", content: entry.buffer });
253
+ }
254
+ break;
255
+ }
256
+ case "TOOL_CALL_START": {
257
+ const el = document.createElement("div");
258
+ el.className = "bubble tool";
259
+ const nameEl = document.createElement("span");
260
+ nameEl.className = "name";
261
+ nameEl.textContent = "🔧 " + event.toolCallName;
262
+ el.appendChild(nameEl);
263
+ const resultEl = document.createElement("span");
264
+ resultEl.className = "result";
265
+ resultEl.textContent = "running…";
266
+ el.appendChild(resultEl);
267
+ log.appendChild(el);
268
+ log.scrollTop = log.scrollHeight;
269
+ toolBubbles.set(event.toolCallId, { el, nameEl, resultEl });
270
+ break;
271
+ }
272
+ case "TOOL_CALL_RESULT": {
273
+ const entry = toolBubbles.get(event.toolCallId);
274
+ if (entry) {
275
+ const text = typeof event.content === "string" ? event.content : JSON.stringify(event.content);
276
+ entry.resultEl.textContent = text.length > 2000 ? text.slice(0, 2000) + "…" : text;
277
+ log.scrollTop = log.scrollHeight;
278
+ }
279
+ break;
280
+ }
281
+ case "RUN_ERROR": {
282
+ bubble("error", event.message);
283
+ break;
284
+ }
285
+ default:
286
+ break;
287
+ }
288
+ }
289
+ }
290
+
291
+ form.addEventListener("submit", (e) => {
292
+ e.preventDefault();
293
+ const text = input.value.trim();
294
+ if (!text) return;
295
+ input.value = "";
296
+ input.style.height = "auto";
297
+ send(text);
298
+ });
299
+
300
+ input.addEventListener("keydown", (e) => {
301
+ if (e.key === "Enter" && !e.shiftKey) {
302
+ e.preventDefault();
303
+ form.requestSubmit();
304
+ }
305
+ });
306
+
307
+ input.addEventListener("input", () => {
308
+ input.style.height = "auto";
309
+ input.style.height = Math.min(input.scrollHeight, 160) + "px";
310
+ });
311
+ })();
312
+ </script>
313
+ </body>
314
+ </html>