walkietalkiebot 0.3.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.
Files changed (44) hide show
  1. package/.claude-plugin/plugin.json +8 -0
  2. package/.mcp.json +8 -0
  3. package/README.md +169 -0
  4. package/bin/wtb-server.js +336 -0
  5. package/bin/wtb.js +43 -0
  6. package/dist/Talkie_logo.png +0 -0
  7. package/dist/assets/index-UPnYoRh1.js +81 -0
  8. package/dist/assets/index-VbGv60d-.css +1 -0
  9. package/dist/index.html +14 -0
  10. package/mcp-server/dist/index.js +401 -0
  11. package/package.json +86 -0
  12. package/server/api.js +629 -0
  13. package/server/db/index.js +67 -0
  14. package/server/db/repositories/activities.js +85 -0
  15. package/server/db/repositories/activities.test.js +106 -0
  16. package/server/db/repositories/conversations.js +93 -0
  17. package/server/db/repositories/conversations.test.js +137 -0
  18. package/server/db/repositories/jobs.js +128 -0
  19. package/server/db/repositories/messages.js +98 -0
  20. package/server/db/repositories/messages.test.js +152 -0
  21. package/server/db/repositories/plans.js +57 -0
  22. package/server/db/repositories/plans.test.js +98 -0
  23. package/server/db/repositories/search.js +34 -0
  24. package/server/db/repositories/search.test.js +61 -0
  25. package/server/db/repositories/telegram.js +30 -0
  26. package/server/db/schema.js +165 -0
  27. package/server/index.js +137 -0
  28. package/server/jobs/api.js +108 -0
  29. package/server/jobs/manager.js +231 -0
  30. package/server/jobs/runner.js +246 -0
  31. package/server/notifications/dispatcher.js +40 -0
  32. package/server/notifications/macos.js +24 -0
  33. package/server/notifications/types.js +0 -0
  34. package/server/ssl.js +61 -0
  35. package/server/state.js +30 -0
  36. package/server/telegram/commands.js +160 -0
  37. package/server/telegram/handlers.js +299 -0
  38. package/server/telegram/index.js +46 -0
  39. package/server/test/helpers.js +14 -0
  40. package/skills/export-tape/SKILL.md +26 -0
  41. package/skills/launch-voice/SKILL.md +25 -0
  42. package/skills/manage-plans/SKILL.md +32 -0
  43. package/skills/save-conversation/SKILL.md +24 -0
  44. package/skills/search-tapes/SKILL.md +21 -0
@@ -0,0 +1,14 @@
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.0" />
6
+ <meta name="description" content="Walkie Talkie Bot — a voice-first AI assistant for Claude Code. Talk instead of type." />
7
+ <title>Walkie Talkie Bot</title>
8
+ <script type="module" crossorigin src="/assets/index-UPnYoRh1.js"></script>
9
+ <link rel="stylesheet" crossorigin href="/assets/index-VbGv60d-.css">
10
+ </head>
11
+ <body>
12
+ <div id="root"></div>
13
+ </body>
14
+ </html>
@@ -0,0 +1,401 @@
1
+ #!/usr/bin/env node
2
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import {
5
+ CallToolRequestSchema,
6
+ ListToolsRequestSchema
7
+ } from "@modelcontextprotocol/sdk/types.js";
8
+ import { spawn, exec } from "child_process";
9
+ import { promisify } from "util";
10
+ import { fileURLToPath } from "url";
11
+ import { dirname, join } from "path";
12
+ import { randomUUID } from "crypto";
13
+ const execAsync = promisify(exec);
14
+ const __dirname = dirname(fileURLToPath(import.meta.url));
15
+ const WTB_PORT = parseInt(process.env.WTB_PORT || "5173", 10);
16
+ const WTB_URL = `https://localhost:${WTB_PORT}`;
17
+ const POSTHOG_KEY = "phc_j7rWavXkXFqSjJIvxhnnAMX3I5UmkcCsnU8J0sKAzog";
18
+ const TELEMETRY = process.env.WTB_TELEMETRY !== "0";
19
+ function trackTool(toolName, category, ok) {
20
+ if (!TELEMETRY) return;
21
+ fetch("https://us.i.posthog.com/capture/", {
22
+ method: "POST",
23
+ headers: { "Content-Type": "application/json" },
24
+ body: JSON.stringify({
25
+ api_key: POSTHOG_KEY,
26
+ event: "mcp_tool_called",
27
+ distinct_id: "anon",
28
+ properties: { tool: toolName, category, success: ok }
29
+ })
30
+ }).catch(() => {
31
+ });
32
+ }
33
+ process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
34
+ let wtbProcess = null;
35
+ function jsonResult(data) {
36
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
37
+ }
38
+ async function apiGet(path) {
39
+ const r = await fetch(`${WTB_URL}${path}`);
40
+ return r.ok ? await r.json() : { error: `API error (${r.status}): ${await r.text()}` };
41
+ }
42
+ async function apiPost(path, body) {
43
+ const r = await fetch(`${WTB_URL}${path}`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) });
44
+ return r.ok ? await r.json() : { error: `API error (${r.status}): ${await r.text()}` };
45
+ }
46
+ async function apiPatch(path, body) {
47
+ const r = await fetch(`${WTB_URL}${path}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) });
48
+ return r.ok ? await r.json() : { error: `API error (${r.status}): ${await r.text()}` };
49
+ }
50
+ async function apiPut(path, body) {
51
+ const r = await fetch(`${WTB_URL}${path}`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) });
52
+ return r.ok ? await r.json() : { error: `API error (${r.status}): ${await r.text()}` };
53
+ }
54
+ async function apiDelete(path) {
55
+ const r = await fetch(`${WTB_URL}${path}`, { method: "DELETE" });
56
+ return r.ok ? await r.json() : { error: `API error (${r.status}): ${await r.text()}` };
57
+ }
58
+ let db = null;
59
+ async function getDb() {
60
+ if (db) return db;
61
+ try {
62
+ const dbIndex = await import(join(__dirname, "..", "server", "db", "index.js"));
63
+ dbIndex.initDb();
64
+ db = {
65
+ convos: await import(join(__dirname, "..", "server", "db", "repositories", "conversations.js")),
66
+ msgs: await import(join(__dirname, "..", "server", "db", "repositories", "messages.js")),
67
+ plans: await import(join(__dirname, "..", "server", "db", "repositories", "plans.js")),
68
+ search: await import(join(__dirname, "..", "server", "db", "repositories", "search.js")),
69
+ activities: await import(join(__dirname, "..", "server", "db", "repositories", "activities.js"))
70
+ };
71
+ return db;
72
+ } catch (err) {
73
+ console.error("DB init failed, falling back to HTTP:", err.message);
74
+ return null;
75
+ }
76
+ }
77
+ async function isWtbRunning() {
78
+ try {
79
+ return (await fetch(`${WTB_URL}/api/status`)).ok;
80
+ } catch {
81
+ return false;
82
+ }
83
+ }
84
+ function serverNotRunning() {
85
+ return { error: "Walkie Talkie Bot server not running. Data tools work offline. Start server with: npx walkietalkiebot" };
86
+ }
87
+ async function serverCall(fn) {
88
+ try {
89
+ return await fn();
90
+ } catch {
91
+ return serverNotRunning();
92
+ }
93
+ }
94
+ async function launchWtb() {
95
+ if (await isWtbRunning()) {
96
+ exec(`open -a "Google Chrome" ${WTB_URL} 2>/dev/null || open ${WTB_URL}`);
97
+ return { success: true, message: "Walkie Talkie Bot is already running", url: WTB_URL };
98
+ }
99
+ return new Promise((resolve) => {
100
+ wtbProcess = spawn("npx", ["wtb"], { detached: true, stdio: "ignore", env: { ...process.env, WTB_PORT: String(WTB_PORT) } });
101
+ wtbProcess.unref();
102
+ let attempts = 0;
103
+ const check = setInterval(async () => {
104
+ if (await isWtbRunning()) {
105
+ clearInterval(check);
106
+ resolve({ success: true, message: "Walkie Talkie Bot launched", url: WTB_URL });
107
+ } else if (++attempts > 30) {
108
+ clearInterval(check);
109
+ resolve({ success: false, message: "Walkie Talkie Bot failed to start" });
110
+ }
111
+ }, 500);
112
+ });
113
+ }
114
+ function formatMarkdown(conv, messages, activities) {
115
+ let md = `# ${conv.title || "Untitled"}
116
+
117
+ `;
118
+ if (conv.created_at) md += `*Created: ${new Date(conv.created_at).toLocaleString()}*
119
+
120
+ ---
121
+
122
+ `;
123
+ for (const msg of messages) {
124
+ md += `**${msg.role === "user" ? "You" : "Assistant"}:**
125
+
126
+ ${msg.content}
127
+
128
+ ---
129
+
130
+ `;
131
+ }
132
+ if (activities.length) {
133
+ md += "## Tool Activity\n\n";
134
+ for (const a of activities) md += `- **${a.tool}** (${a.status})${a.duration ? ` ${a.duration}ms` : ""}
135
+ `;
136
+ }
137
+ return md;
138
+ }
139
+ const server = new Server(
140
+ { name: "wtb", version: "0.3.0" },
141
+ { capabilities: { tools: {} } }
142
+ );
143
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
144
+ tools: [
145
+ // ── Server tools (require running WTB server) ──
146
+ { name: "launch_wtb", description: "Launch the Walkie Talkie Bot voice interface in a browser.", inputSchema: { type: "object", properties: {}, required: [] } },
147
+ { name: "get_wtb_status", description: "Check if Walkie Talkie Bot is running and get its current state.", inputSchema: { type: "object", properties: {}, required: [] } },
148
+ { name: "get_transcript", description: "Get the latest voice transcript.", inputSchema: { type: "object", properties: {}, required: [] } },
149
+ { name: "get_conversation_history", description: "Get the full conversation history from the current tape.", inputSchema: { type: "object", properties: {}, required: [] } },
150
+ { name: "get_claude_session", description: "Get the current Claude Code session ID.", inputSchema: { type: "object", properties: {}, required: [] } },
151
+ { name: "set_claude_session", description: "Connect to a Claude Code session.", inputSchema: { type: "object", properties: { sessionId: { type: "string", description: "Session ID to connect to" } }, required: ["sessionId"] } },
152
+ { name: "disconnect_claude_session", description: "Disconnect the current Claude Code session.", inputSchema: { type: "object", properties: {}, required: [] } },
153
+ { name: "get_pending_message", description: "Poll for a pending user message in IPC mode.", inputSchema: { type: "object", properties: {}, required: [] } },
154
+ { name: "respond_to_wtb", description: "Send a response back in IPC mode.", inputSchema: { type: "object", properties: { content: { type: "string", description: "Response content" } }, required: ["content"] } },
155
+ { name: "update_wtb_state", description: "Update UI state (avatar state, transcript).", inputSchema: { type: "object", properties: { avatarState: { type: "string", enum: ["idle", "listening", "thinking", "speaking"], description: "Avatar state" }, transcript: { type: "string", description: "Transcript text" } }, required: [] } },
156
+ { name: "analyze_image", description: "Analyze an image using Claude vision API.", inputSchema: { type: "object", properties: { dataUrl: { type: "string", description: "Base64 data URL of the image" }, fileName: { type: "string", description: "Optional filename" }, apiKey: { type: "string", description: "Optional Anthropic API key" } }, required: ["dataUrl"] } },
157
+ { name: "open_url", description: "Open a URL in the default browser.", inputSchema: { type: "object", properties: { url: { type: "string", description: "URL to open" } }, required: ["url"] } },
158
+ { name: "create_wtb_job", description: "Create a background job.", inputSchema: { type: "object", properties: { conversationId: { type: "string", description: "Conversation ID" }, prompt: { type: "string", description: "Task/prompt to execute" } }, required: ["conversationId", "prompt"] } },
159
+ { name: "get_wtb_job", description: "Get the status and result of a background job.", inputSchema: { type: "object", properties: { jobId: { type: "string", description: "Job ID" } }, required: ["jobId"] } },
160
+ { name: "list_wtb_jobs", description: "List background jobs, optionally filtered by status.", inputSchema: { type: "object", properties: { status: { type: "string", enum: ["queued", "running", "completed", "failed", "cancelled"], description: "Filter by status" } }, required: [] } },
161
+ // ── Data tools (work offline via direct SQLite) ──
162
+ { name: "list_conversations", description: "List all saved conversations (cassette tapes). Works offline.", inputSchema: { type: "object", properties: { limit: { type: "number", description: "Max results (default 50)" }, offset: { type: "number", description: "Pagination offset (default 0)" } }, required: [] } },
163
+ { name: "get_conversation", description: "Get a full conversation by ID with messages, images, and tool activity. Works offline.", inputSchema: { type: "object", properties: { conversationId: { type: "string", description: "Conversation ID" } }, required: ["conversationId"] } },
164
+ { name: "create_conversation", description: "Create a new conversation (cassette tape). Works offline.", inputSchema: { type: "object", properties: { title: { type: "string", description: "Conversation title" }, id: { type: "string", description: "Optional custom ID" } }, required: [] } },
165
+ { name: "rename_conversation", description: "Rename an existing conversation. Works offline.", inputSchema: { type: "object", properties: { conversationId: { type: "string", description: "Conversation ID" }, title: { type: "string", description: "New title" } }, required: ["conversationId", "title"] } },
166
+ { name: "delete_conversation", description: "Delete a conversation permanently. Works offline.", inputSchema: { type: "object", properties: { conversationId: { type: "string", description: "Conversation ID" } }, required: ["conversationId"] } },
167
+ { name: "search_conversations", description: "Full-text search across all conversations. Uses FTS5 for ranked results. Works offline.", inputSchema: { type: "object", properties: { query: { type: "string", description: "Search query" }, limit: { type: "number", description: "Max results (default 50)" } }, required: ["query"] } },
168
+ { name: "add_message", description: "Add a message to a conversation. Works offline.", inputSchema: { type: "object", properties: { conversationId: { type: "string", description: "Conversation ID" }, role: { type: "string", enum: ["user", "assistant"], description: "Message role" }, content: { type: "string", description: "Message content" }, source: { type: "string", description: 'Source (default "mcp")' } }, required: ["conversationId", "role", "content"] } },
169
+ { name: "list_plans", description: "List all plans. Works offline.", inputSchema: { type: "object", properties: { limit: { type: "number", description: "Max results (default 50)" }, offset: { type: "number", description: "Pagination offset (default 0)" } }, required: [] } },
170
+ { name: "get_plan", description: "Get a plan by ID with full content. Works offline.", inputSchema: { type: "object", properties: { planId: { type: "string", description: "Plan ID" } }, required: ["planId"] } },
171
+ { name: "create_plan", description: "Create a new plan with status workflow (draft > approved > in_progress > completed > archived). Works offline.", inputSchema: { type: "object", properties: { title: { type: "string", description: "Plan title" }, content: { type: "string", description: "Plan content (markdown)" }, status: { type: "string", enum: ["draft", "approved", "in_progress", "completed", "archived"], description: 'Initial status (default "draft")' }, conversationId: { type: "string", description: "Link to conversation" } }, required: ["title", "content"] } },
172
+ { name: "update_plan", description: "Update a plan's title, content, or status. Works offline.", inputSchema: { type: "object", properties: { planId: { type: "string", description: "Plan ID" }, title: { type: "string", description: "New title" }, content: { type: "string", description: "New content" }, status: { type: "string", enum: ["draft", "approved", "in_progress", "completed", "archived"], description: "New status" } }, required: ["planId"] } },
173
+ { name: "delete_plan", description: "Delete a plan permanently. Works offline.", inputSchema: { type: "object", properties: { planId: { type: "string", description: "Plan ID" } }, required: ["planId"] } },
174
+ { name: "get_liner_notes", description: "Get liner notes (markdown annotations) for a conversation. Works offline.", inputSchema: { type: "object", properties: { conversationId: { type: "string", description: "Conversation ID" } }, required: ["conversationId"] } },
175
+ { name: "set_liner_notes", description: "Set or clear liner notes for a conversation. Works offline.", inputSchema: { type: "object", properties: { conversationId: { type: "string", description: "Conversation ID" }, linerNotes: { type: "string", description: "Markdown content (or null to clear)" } }, required: ["conversationId"] } },
176
+ { name: "export_conversation", description: "Export a conversation as markdown or JSON. Works offline.", inputSchema: { type: "object", properties: { conversationId: { type: "string", description: "Conversation ID" }, format: { type: "string", enum: ["markdown", "json"], description: 'Export format (default "markdown")' } }, required: ["conversationId"] } }
177
+ ]
178
+ }));
179
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
180
+ const { name, arguments: args = {} } = request.params;
181
+ const typedArgs = args;
182
+ try {
183
+ const serverTools = {
184
+ launch_wtb: () => launchWtb(),
185
+ get_wtb_status: () => serverCall(() => apiGet("/api/status")),
186
+ get_transcript: () => serverCall(() => apiGet("/api/transcript")),
187
+ get_conversation_history: () => serverCall(() => apiGet("/api/history")),
188
+ get_claude_session: () => serverCall(() => apiGet("/api/session")),
189
+ set_claude_session: () => serverCall(() => apiPost("/api/session", { sessionId: typedArgs.sessionId })),
190
+ disconnect_claude_session: () => serverCall(() => apiDelete("/api/session")),
191
+ get_pending_message: () => serverCall(() => apiGet("/api/pending")),
192
+ respond_to_wtb: () => serverCall(() => apiPost("/api/respond", { content: typedArgs.content })),
193
+ update_wtb_state: () => serverCall(() => apiPost("/api/state", typedArgs)),
194
+ analyze_image: () => serverCall(() => {
195
+ const a = typedArgs;
196
+ return apiPost("/api/analyze-image", { dataUrl: a.dataUrl, fileName: a.fileName, apiKey: a.apiKey });
197
+ }),
198
+ open_url: () => serverCall(() => apiPost("/api/open-url", { url: typedArgs.url })),
199
+ create_wtb_job: () => serverCall(() => {
200
+ const a = typedArgs;
201
+ return apiPost("/api/jobs", { conversationId: a.conversationId, prompt: a.prompt, source: "mcp" });
202
+ }),
203
+ get_wtb_job: () => serverCall(() => apiGet(`/api/jobs/${typedArgs.jobId}`)),
204
+ list_wtb_jobs: () => serverCall(() => {
205
+ const a = typedArgs;
206
+ return apiGet(`/api/jobs${a.status ? `?status=${a.status}` : ""}`);
207
+ })
208
+ };
209
+ if (serverTools[name]) {
210
+ const result = await serverTools[name]();
211
+ trackTool(name, "server", !("error" in result));
212
+ return jsonResult(result);
213
+ }
214
+ const d = await getDb();
215
+ if (!d) {
216
+ return jsonResult(await serverCall(async () => {
217
+ switch (name) {
218
+ case "list_conversations": {
219
+ const a = typedArgs;
220
+ return await apiGet(`/api/conversations?limit=${a.limit || 50}&offset=${a.offset || 0}`);
221
+ }
222
+ case "get_conversation":
223
+ return await apiGet(`/api/conversations/${typedArgs.conversationId}`);
224
+ case "create_conversation": {
225
+ const a = typedArgs;
226
+ return await apiPost("/api/conversations", { title: a.title || "New conversation", ...a.id && { id: a.id } });
227
+ }
228
+ case "rename_conversation": {
229
+ const a = typedArgs;
230
+ return await apiPatch(`/api/conversations/${a.conversationId}`, { title: a.title });
231
+ }
232
+ case "delete_conversation":
233
+ return await apiDelete(`/api/conversations/${typedArgs.conversationId}`);
234
+ case "search_conversations": {
235
+ const a = typedArgs;
236
+ return await apiGet(`/api/search?q=${encodeURIComponent(a.query)}&limit=${a.limit || 50}`);
237
+ }
238
+ case "add_message": {
239
+ const a = typedArgs;
240
+ return await apiPost(`/api/conversations/${a.conversationId}/messages`, { role: a.role, content: a.content, source: a.source || "mcp" });
241
+ }
242
+ case "list_plans": {
243
+ const a = typedArgs;
244
+ return await apiGet(`/api/plans?limit=${a.limit || 50}&offset=${a.offset || 0}`);
245
+ }
246
+ case "get_plan":
247
+ return await apiGet(`/api/plans/${typedArgs.planId}`);
248
+ case "create_plan": {
249
+ const a = typedArgs;
250
+ return await apiPost("/api/plans", { title: a.title, content: a.content, status: a.status || "draft", conversationId: a.conversationId });
251
+ }
252
+ case "update_plan": {
253
+ const a = typedArgs;
254
+ const b = {};
255
+ if (a.title !== void 0) b.title = a.title;
256
+ if (a.content !== void 0) b.content = a.content;
257
+ if (a.status !== void 0) b.status = a.status;
258
+ return await apiPut(`/api/plans/${a.planId}`, b);
259
+ }
260
+ case "delete_plan":
261
+ return await apiDelete(`/api/plans/${typedArgs.planId}`);
262
+ case "get_liner_notes":
263
+ return await apiGet(`/api/conversations/${typedArgs.conversationId}/liner-notes`);
264
+ case "set_liner_notes": {
265
+ const a = typedArgs;
266
+ return await apiPut(`/api/conversations/${a.conversationId}/liner-notes`, { linerNotes: a.linerNotes ?? null });
267
+ }
268
+ case "export_conversation": {
269
+ const a = typedArgs;
270
+ const conv = await apiGet(`/api/conversations/${a.conversationId}`);
271
+ if (conv.error) return conv;
272
+ return a.format === "json" ? { format: "json", data: conv } : { format: "markdown", data: formatMarkdown(conv, conv.messages || [], conv.activities || []) };
273
+ }
274
+ default:
275
+ throw new Error(`Unknown tool: ${name}`);
276
+ }
277
+ }));
278
+ }
279
+ trackTool(name, "data", true);
280
+ switch (name) {
281
+ case "list_conversations": {
282
+ const a = typedArgs;
283
+ const convos = d.convos.listConversations(a.limit || 50, a.offset || 0);
284
+ const total = d.convos.countConversations();
285
+ return jsonResult({ conversations: convos, total });
286
+ }
287
+ case "get_conversation": {
288
+ const a = typedArgs;
289
+ const conv = d.convos.getConversation(a.conversationId);
290
+ if (!conv) return jsonResult({ error: "Conversation not found" });
291
+ const messages = d.msgs.getMessagesForConversation(a.conversationId);
292
+ const imageMap = d.msgs.getImagesForMessages(messages.map((m) => m.id));
293
+ const messagesWithImages = messages.map((m) => ({ ...m, images: imageMap.get(m.id) || [] }));
294
+ const activities = d.activities.getActivitiesForConversation(a.conversationId);
295
+ const linerNotes = d.convos.getLinerNotes(a.conversationId);
296
+ return jsonResult({ ...conv, messages: messagesWithImages, activities, liner_notes: linerNotes });
297
+ }
298
+ case "create_conversation": {
299
+ const a = typedArgs;
300
+ const id = a.id || randomUUID();
301
+ const conv = d.convos.createConversation({ id, title: a.title });
302
+ return jsonResult(conv);
303
+ }
304
+ case "rename_conversation": {
305
+ const a = typedArgs;
306
+ const conv = d.convos.updateConversation(a.conversationId, { title: a.title });
307
+ return jsonResult(conv || { error: "Conversation not found" });
308
+ }
309
+ case "delete_conversation": {
310
+ const a = typedArgs;
311
+ const ok = d.convos.deleteConversation(a.conversationId);
312
+ return jsonResult({ success: ok });
313
+ }
314
+ case "search_conversations": {
315
+ const a = typedArgs;
316
+ const results = d.search.searchMessages(a.query, a.limit || 50);
317
+ return jsonResult({ results });
318
+ }
319
+ case "add_message": {
320
+ const a = typedArgs;
321
+ const msg = d.msgs.createMessage({
322
+ id: randomUUID(),
323
+ conversationId: a.conversationId,
324
+ role: a.role,
325
+ content: a.content,
326
+ source: a.source || "mcp"
327
+ });
328
+ return jsonResult(msg);
329
+ }
330
+ case "list_plans": {
331
+ const a = typedArgs;
332
+ const plans = d.plans.listPlans(a.limit || 50, a.offset || 0);
333
+ return jsonResult({ plans });
334
+ }
335
+ case "get_plan": {
336
+ const a = typedArgs;
337
+ const plan = d.plans.getPlan(a.planId);
338
+ return jsonResult(plan || { error: "Plan not found" });
339
+ }
340
+ case "create_plan": {
341
+ const a = typedArgs;
342
+ const plan = d.plans.createPlan({
343
+ id: randomUUID(),
344
+ title: a.title,
345
+ content: a.content,
346
+ status: a.status || "draft",
347
+ conversationId: a.conversationId || null
348
+ });
349
+ return jsonResult(plan);
350
+ }
351
+ case "update_plan": {
352
+ const a = typedArgs;
353
+ const updates = {};
354
+ if (a.title !== void 0) updates.title = a.title;
355
+ if (a.content !== void 0) updates.content = a.content;
356
+ if (a.status !== void 0) updates.status = a.status;
357
+ d.plans.updatePlan(a.planId, updates);
358
+ return jsonResult({ success: true });
359
+ }
360
+ case "delete_plan": {
361
+ const a = typedArgs;
362
+ d.plans.deletePlan(a.planId);
363
+ return jsonResult({ success: true });
364
+ }
365
+ case "get_liner_notes": {
366
+ const a = typedArgs;
367
+ const notes = d.convos.getLinerNotes(a.conversationId);
368
+ return jsonResult({ conversationId: a.conversationId, linerNotes: notes });
369
+ }
370
+ case "set_liner_notes": {
371
+ const a = typedArgs;
372
+ d.convos.updateLinerNotes(a.conversationId, a.linerNotes ?? null);
373
+ return jsonResult({ success: true });
374
+ }
375
+ case "export_conversation": {
376
+ const a = typedArgs;
377
+ const conv = d.convos.getConversation(a.conversationId);
378
+ if (!conv) return jsonResult({ error: "Conversation not found" });
379
+ const messages = d.msgs.getMessagesForConversation(a.conversationId);
380
+ const activities = d.activities.getActivitiesForConversation(a.conversationId);
381
+ if (a.format === "json") {
382
+ const imageMap = d.msgs.getImagesForMessages(messages.map((m) => m.id));
383
+ const msgsWithImages = messages.map((m) => ({ ...m, images: imageMap.get(m.id) || [] }));
384
+ return jsonResult({ format: "json", data: { ...conv, messages: msgsWithImages, activities } });
385
+ }
386
+ return jsonResult({ format: "markdown", data: formatMarkdown(conv, messages, activities) });
387
+ }
388
+ default:
389
+ throw new Error(`Unknown tool: ${name}`);
390
+ }
391
+ } catch (err) {
392
+ trackTool(name, "data", false);
393
+ return jsonResult({ error: err.message });
394
+ }
395
+ });
396
+ async function main() {
397
+ const transport = new StdioServerTransport();
398
+ await server.connect(transport);
399
+ console.error("Walkie Talkie Bot MCP server running (30 tools \u2014 15 data + 15 server)");
400
+ }
401
+ main().catch(console.error);
package/package.json ADDED
@@ -0,0 +1,86 @@
1
+ {
2
+ "name": "walkietalkiebot",
3
+ "version": "0.3.0",
4
+ "description": "Walkie Talkie Bot — a voice-first AI assistant for Claude Code. Use as a Claude Code plugin (30 MCP tools + 5 skills, offline SQLite) or full server (web UI, 6 retro themes, voice, Telegram)",
5
+ "homepage": "https://walkietalkie.bot",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/bengillin/walkietalkiebot"
9
+ },
10
+ "type": "module",
11
+ "bin": {
12
+ "wtb": "bin/wtb.js",
13
+ "wtb-server": "bin/wtb-server.js",
14
+ "wtb-mcp": "mcp-server/dist/index.js"
15
+ },
16
+ "files": [
17
+ "bin/wtb.js",
18
+ "bin/wtb-server.js",
19
+ "mcp-server/dist/",
20
+ "server/**/*.js",
21
+ "dist/",
22
+ ".claude-plugin/",
23
+ "skills/",
24
+ ".mcp.json"
25
+ ],
26
+ "scripts": {
27
+ "dev": "vite",
28
+ "build": "tsc && vite build && npm run build:server",
29
+ "build:server": "node scripts/build-server.js",
30
+ "preview": "vite preview",
31
+ "lint": "eslint . --ext ts,tsx",
32
+ "test": "vitest run && vitest run --config vitest.config.server.ts",
33
+ "test:client": "vitest run",
34
+ "test:server": "vitest run --config vitest.config.server.ts",
35
+ "test:watch": "vitest",
36
+ "prepublishOnly": "npm run build"
37
+ },
38
+ "keywords": [
39
+ "claude",
40
+ "claude-code",
41
+ "voice",
42
+ "ai",
43
+ "assistant",
44
+ "mcp",
45
+ "speech-recognition",
46
+ "text-to-speech",
47
+ "cassette",
48
+ "retro",
49
+ "themes",
50
+ "telegram",
51
+ "tts",
52
+ "wake-word",
53
+ "plugin",
54
+ "sqlite"
55
+ ],
56
+ "author": "",
57
+ "license": "MIT",
58
+ "dependencies": {
59
+ "@hono/node-server": "^1.12.0",
60
+ "@modelcontextprotocol/sdk": "^1.0.0",
61
+ "better-sqlite3": "^11.0.0",
62
+ "grammy": "^1.21.0",
63
+ "hono": "^4.4.0",
64
+ "open": "^10.1.0",
65
+ "react": "^18.2.0",
66
+ "react-dom": "^18.2.0",
67
+ "selfsigned": "^2.4.1",
68
+ "undici": "^7.19.2",
69
+ "zustand": "^4.5.0"
70
+ },
71
+ "devDependencies": {
72
+ "@testing-library/jest-dom": "^6.9.1",
73
+ "@testing-library/react": "^16.3.2",
74
+ "@types/better-sqlite3": "^7.6.0",
75
+ "@types/react": "^18.2.0",
76
+ "@types/react-dom": "^18.2.0",
77
+ "@vitejs/plugin-basic-ssl": "^1.1.0",
78
+ "@vitejs/plugin-react": "^4.2.0",
79
+ "esbuild": "^0.21.0",
80
+ "eslint": "^8.55.0",
81
+ "jsdom": "^27.0.1",
82
+ "typescript": "^5.3.0",
83
+ "vite": "^5.0.0",
84
+ "vitest": "^2.1.9"
85
+ }
86
+ }