talkiebot 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 +263 -0
- package/bin/talkie-server.js +336 -0
- package/bin/talkie.js +43 -0
- package/dist/Talkie_logo.png +0 -0
- package/dist/assets/index-C-Y2BbXt.css +1 -0
- package/dist/assets/index-JS88FEbt.js +81 -0
- package/dist/index.html +14 -0
- package/mcp-server/index.js +694 -0
- package/package.json +70 -0
- package/server/api.js +614 -0
- package/server/db/index.js +57 -0
- package/server/db/repositories/activities.js +85 -0
- package/server/db/repositories/conversations.js +93 -0
- package/server/db/repositories/jobs.js +128 -0
- package/server/db/repositories/messages.js +98 -0
- package/server/db/repositories/plans.js +57 -0
- package/server/db/repositories/search.js +34 -0
- package/server/db/repositories/telegram.js +30 -0
- package/server/db/schema.js +165 -0
- package/server/index.js +137 -0
- package/server/jobs/api.js +108 -0
- package/server/jobs/manager.js +231 -0
- package/server/jobs/runner.js +246 -0
- package/server/notifications/dispatcher.js +40 -0
- package/server/notifications/macos.js +24 -0
- package/server/notifications/types.js +0 -0
- package/server/ssl.js +58 -0
- package/server/state.js +30 -0
- package/server/telegram/commands.js +160 -0
- package/server/telegram/handlers.js +299 -0
- package/server/telegram/index.js +46 -0
package/package.json
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "talkiebot",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A voice-first, cassette tape-themed interface for Claude Code with conversation management and MCP integration",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"talkie": "bin/talkie.js",
|
|
8
|
+
"talkie-server": "bin/talkie-server.js",
|
|
9
|
+
"talkie-mcp": "mcp-server/index.js"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"bin/talkie.js",
|
|
13
|
+
"bin/talkie-server.js",
|
|
14
|
+
"mcp-server/index.js",
|
|
15
|
+
"server/**/*.js",
|
|
16
|
+
"dist/"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"dev": "vite",
|
|
20
|
+
"build": "tsc && vite build && npm run build:server",
|
|
21
|
+
"build:server": "node scripts/build-server.js",
|
|
22
|
+
"preview": "vite preview",
|
|
23
|
+
"lint": "eslint . --ext ts,tsx",
|
|
24
|
+
"test": "vitest run",
|
|
25
|
+
"test:watch": "vitest",
|
|
26
|
+
"prepublishOnly": "npm run build"
|
|
27
|
+
},
|
|
28
|
+
"keywords": [
|
|
29
|
+
"claude",
|
|
30
|
+
"voice",
|
|
31
|
+
"ai",
|
|
32
|
+
"assistant",
|
|
33
|
+
"mcp",
|
|
34
|
+
"claude-code",
|
|
35
|
+
"speech-recognition",
|
|
36
|
+
"text-to-speech",
|
|
37
|
+
"cassette",
|
|
38
|
+
"retro"
|
|
39
|
+
],
|
|
40
|
+
"author": "",
|
|
41
|
+
"license": "MIT",
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"@hono/node-server": "^1.12.0",
|
|
44
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
45
|
+
"better-sqlite3": "^11.0.0",
|
|
46
|
+
"grammy": "^1.21.0",
|
|
47
|
+
"hono": "^4.4.0",
|
|
48
|
+
"open": "^10.1.0",
|
|
49
|
+
"react": "^18.2.0",
|
|
50
|
+
"react-dom": "^18.2.0",
|
|
51
|
+
"selfsigned": "^2.4.1",
|
|
52
|
+
"undici": "^7.19.2",
|
|
53
|
+
"zustand": "^4.5.0"
|
|
54
|
+
},
|
|
55
|
+
"devDependencies": {
|
|
56
|
+
"@testing-library/jest-dom": "^6.9.1",
|
|
57
|
+
"@testing-library/react": "^16.3.2",
|
|
58
|
+
"@types/better-sqlite3": "^7.6.0",
|
|
59
|
+
"@types/react": "^18.2.0",
|
|
60
|
+
"@types/react-dom": "^18.2.0",
|
|
61
|
+
"@vitejs/plugin-basic-ssl": "^1.1.0",
|
|
62
|
+
"@vitejs/plugin-react": "^4.2.0",
|
|
63
|
+
"esbuild": "^0.21.0",
|
|
64
|
+
"eslint": "^8.55.0",
|
|
65
|
+
"jsdom": "^27.0.1",
|
|
66
|
+
"typescript": "^5.3.0",
|
|
67
|
+
"vite": "^5.0.0",
|
|
68
|
+
"vitest": "^2.1.9"
|
|
69
|
+
}
|
|
70
|
+
}
|
package/server/api.js
ADDED
|
@@ -0,0 +1,614 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import { cors } from "hono/cors";
|
|
3
|
+
import { streamSSE } from "hono/streaming";
|
|
4
|
+
import { spawn } from "child_process";
|
|
5
|
+
import { state, updateState } from "./state.js";
|
|
6
|
+
import { isDbConnected } from "./db/index.js";
|
|
7
|
+
import * as conversations from "./db/repositories/conversations.js";
|
|
8
|
+
import * as messages from "./db/repositories/messages.js";
|
|
9
|
+
import * as activities from "./db/repositories/activities.js";
|
|
10
|
+
import * as search from "./db/repositories/search.js";
|
|
11
|
+
import * as plans from "./db/repositories/plans.js";
|
|
12
|
+
import { spawnClaude } from "./jobs/runner.js";
|
|
13
|
+
import { jobRoutes } from "./jobs/api.js";
|
|
14
|
+
const api = new Hono();
|
|
15
|
+
api.use("*", cors());
|
|
16
|
+
api.route("/jobs", jobRoutes);
|
|
17
|
+
api.get("/status", (c) => {
|
|
18
|
+
return c.json({
|
|
19
|
+
running: true,
|
|
20
|
+
avatarState: state.avatarState,
|
|
21
|
+
dbStatus: isDbConnected() ? "connected" : "unavailable"
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
api.get("/conversations", (c) => {
|
|
25
|
+
const limit = parseInt(c.req.query("limit") || "50", 10);
|
|
26
|
+
const offset = parseInt(c.req.query("offset") || "0", 10);
|
|
27
|
+
const convos = conversations.listConversations(limit, offset);
|
|
28
|
+
const total = conversations.countConversations();
|
|
29
|
+
return c.json({
|
|
30
|
+
conversations: convos.map((conv) => ({
|
|
31
|
+
id: conv.id,
|
|
32
|
+
title: conv.title,
|
|
33
|
+
createdAt: conv.created_at,
|
|
34
|
+
updatedAt: conv.updated_at,
|
|
35
|
+
projectId: conv.project_id,
|
|
36
|
+
parentId: conv.parent_id
|
|
37
|
+
})),
|
|
38
|
+
total,
|
|
39
|
+
limit,
|
|
40
|
+
offset
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
api.get("/conversations/:id", (c) => {
|
|
44
|
+
const id = c.req.param("id");
|
|
45
|
+
const conv = conversations.getConversation(id);
|
|
46
|
+
if (!conv) {
|
|
47
|
+
return c.json({ error: "Conversation not found" }, 404);
|
|
48
|
+
}
|
|
49
|
+
const msgs = messages.getMessagesForConversation(id);
|
|
50
|
+
const messageIds = msgs.map((m) => m.id);
|
|
51
|
+
const imageMap = messages.getImagesForMessages(messageIds);
|
|
52
|
+
const acts = activities.getActivitiesForConversation(id);
|
|
53
|
+
return c.json({
|
|
54
|
+
id: conv.id,
|
|
55
|
+
title: conv.title,
|
|
56
|
+
createdAt: conv.created_at,
|
|
57
|
+
updatedAt: conv.updated_at,
|
|
58
|
+
projectId: conv.project_id,
|
|
59
|
+
parentId: conv.parent_id,
|
|
60
|
+
messages: msgs.map((m) => ({
|
|
61
|
+
id: m.id,
|
|
62
|
+
role: m.role,
|
|
63
|
+
content: m.content,
|
|
64
|
+
timestamp: m.timestamp,
|
|
65
|
+
source: m.source,
|
|
66
|
+
images: (imageMap.get(m.id) || []).map((img) => ({
|
|
67
|
+
id: img.id,
|
|
68
|
+
dataUrl: img.data_url,
|
|
69
|
+
fileName: img.file_name,
|
|
70
|
+
description: img.description
|
|
71
|
+
}))
|
|
72
|
+
})),
|
|
73
|
+
activities: acts.map((a) => ({
|
|
74
|
+
id: a.id,
|
|
75
|
+
tool: a.tool,
|
|
76
|
+
input: a.input,
|
|
77
|
+
status: a.status,
|
|
78
|
+
timestamp: a.timestamp,
|
|
79
|
+
duration: a.duration,
|
|
80
|
+
error: a.error
|
|
81
|
+
}))
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
api.post("/conversations", async (c) => {
|
|
85
|
+
const body = await c.req.json().catch(() => ({}));
|
|
86
|
+
const id = body.id || crypto.randomUUID();
|
|
87
|
+
const title = body.title || "New conversation";
|
|
88
|
+
const conv = conversations.createConversation({ id, title });
|
|
89
|
+
return c.json({
|
|
90
|
+
id: conv.id,
|
|
91
|
+
title: conv.title,
|
|
92
|
+
createdAt: conv.created_at,
|
|
93
|
+
updatedAt: conv.updated_at
|
|
94
|
+
}, 201);
|
|
95
|
+
});
|
|
96
|
+
api.patch("/conversations/:id", async (c) => {
|
|
97
|
+
const id = c.req.param("id");
|
|
98
|
+
const body = await c.req.json();
|
|
99
|
+
const conv = conversations.updateConversation(id, {
|
|
100
|
+
title: body.title,
|
|
101
|
+
projectId: body.projectId,
|
|
102
|
+
parentId: body.parentId
|
|
103
|
+
});
|
|
104
|
+
if (!conv) {
|
|
105
|
+
return c.json({ error: "Conversation not found" }, 404);
|
|
106
|
+
}
|
|
107
|
+
return c.json({
|
|
108
|
+
id: conv.id,
|
|
109
|
+
title: conv.title,
|
|
110
|
+
createdAt: conv.created_at,
|
|
111
|
+
updatedAt: conv.updated_at
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
api.delete("/conversations/:id", (c) => {
|
|
115
|
+
const id = c.req.param("id");
|
|
116
|
+
const deleted = conversations.deleteConversation(id);
|
|
117
|
+
if (!deleted) {
|
|
118
|
+
return c.json({ error: "Conversation not found" }, 404);
|
|
119
|
+
}
|
|
120
|
+
return c.json({ success: true });
|
|
121
|
+
});
|
|
122
|
+
api.get("/conversations/:id/liner-notes", (c) => {
|
|
123
|
+
const id = c.req.param("id");
|
|
124
|
+
const conv = conversations.getConversation(id);
|
|
125
|
+
if (!conv) {
|
|
126
|
+
return c.json({ error: "Conversation not found" }, 404);
|
|
127
|
+
}
|
|
128
|
+
return c.json({
|
|
129
|
+
linerNotes: conv.liner_notes || null
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
api.put("/conversations/:id/liner-notes", async (c) => {
|
|
133
|
+
const id = c.req.param("id");
|
|
134
|
+
const body = await c.req.json();
|
|
135
|
+
const conv = conversations.getConversation(id);
|
|
136
|
+
if (!conv) {
|
|
137
|
+
return c.json({ error: "Conversation not found" }, 404);
|
|
138
|
+
}
|
|
139
|
+
conversations.updateLinerNotes(id, body.linerNotes || null);
|
|
140
|
+
return c.json({ success: true });
|
|
141
|
+
});
|
|
142
|
+
api.post("/conversations/:id/messages", async (c) => {
|
|
143
|
+
const conversationId = c.req.param("id");
|
|
144
|
+
const body = await c.req.json();
|
|
145
|
+
const conv = conversations.getConversation(conversationId);
|
|
146
|
+
if (!conv) {
|
|
147
|
+
return c.json({ error: "Conversation not found" }, 404);
|
|
148
|
+
}
|
|
149
|
+
const msg = messages.createMessage({
|
|
150
|
+
id: body.id || crypto.randomUUID(),
|
|
151
|
+
conversationId,
|
|
152
|
+
role: body.role,
|
|
153
|
+
content: body.content,
|
|
154
|
+
timestamp: body.timestamp,
|
|
155
|
+
source: body.source || "web",
|
|
156
|
+
images: body.images
|
|
157
|
+
});
|
|
158
|
+
if (msg.role === "user" && msg.position === 0) {
|
|
159
|
+
const title = msg.content.length > 40 ? msg.content.slice(0, 40) + "..." : msg.content;
|
|
160
|
+
conversations.updateConversation(conversationId, { title });
|
|
161
|
+
}
|
|
162
|
+
if (body.activities && Array.isArray(body.activities)) {
|
|
163
|
+
activities.createActivitiesBatch(
|
|
164
|
+
body.activities.map((a) => ({
|
|
165
|
+
id: a.id || crypto.randomUUID(),
|
|
166
|
+
conversationId,
|
|
167
|
+
messageId: msg.id,
|
|
168
|
+
tool: a.tool,
|
|
169
|
+
input: a.input,
|
|
170
|
+
status: a.status,
|
|
171
|
+
timestamp: a.timestamp,
|
|
172
|
+
duration: a.duration,
|
|
173
|
+
error: a.error
|
|
174
|
+
}))
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
return c.json({
|
|
178
|
+
id: msg.id,
|
|
179
|
+
role: msg.role,
|
|
180
|
+
content: msg.content,
|
|
181
|
+
timestamp: msg.timestamp,
|
|
182
|
+
source: msg.source
|
|
183
|
+
}, 201);
|
|
184
|
+
});
|
|
185
|
+
api.patch("/images/:id", async (c) => {
|
|
186
|
+
const imageId = c.req.param("id");
|
|
187
|
+
const { description } = await c.req.json();
|
|
188
|
+
if (typeof description !== "string") {
|
|
189
|
+
return c.json({ error: "description required" }, 400);
|
|
190
|
+
}
|
|
191
|
+
const updated = messages.updateImageDescription(imageId, description);
|
|
192
|
+
if (!updated) {
|
|
193
|
+
return c.json({ error: "Image not found" }, 404);
|
|
194
|
+
}
|
|
195
|
+
return c.json({ success: true });
|
|
196
|
+
});
|
|
197
|
+
api.get("/plans", (c) => {
|
|
198
|
+
const limit = parseInt(c.req.query("limit") || "50", 10);
|
|
199
|
+
const offset = parseInt(c.req.query("offset") || "0", 10);
|
|
200
|
+
const planList = plans.listPlans(limit, offset);
|
|
201
|
+
return c.json({
|
|
202
|
+
plans: planList.map((p) => ({
|
|
203
|
+
id: p.id,
|
|
204
|
+
title: p.title,
|
|
205
|
+
content: p.content,
|
|
206
|
+
status: p.status,
|
|
207
|
+
conversationId: p.conversation_id,
|
|
208
|
+
createdAt: p.created_at,
|
|
209
|
+
updatedAt: p.updated_at
|
|
210
|
+
}))
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
api.get("/plans/:id", (c) => {
|
|
214
|
+
const id = c.req.param("id");
|
|
215
|
+
const plan = plans.getPlan(id);
|
|
216
|
+
if (!plan) {
|
|
217
|
+
return c.json({ error: "Plan not found" }, 404);
|
|
218
|
+
}
|
|
219
|
+
return c.json({
|
|
220
|
+
id: plan.id,
|
|
221
|
+
title: plan.title,
|
|
222
|
+
content: plan.content,
|
|
223
|
+
status: plan.status,
|
|
224
|
+
conversationId: plan.conversation_id,
|
|
225
|
+
createdAt: plan.created_at,
|
|
226
|
+
updatedAt: plan.updated_at
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
api.post("/plans", async (c) => {
|
|
230
|
+
const body = await c.req.json();
|
|
231
|
+
const plan = plans.createPlan({
|
|
232
|
+
id: body.id || crypto.randomUUID(),
|
|
233
|
+
title: body.title || "Untitled Plan",
|
|
234
|
+
content: body.content || "",
|
|
235
|
+
status: body.status,
|
|
236
|
+
conversationId: body.conversationId
|
|
237
|
+
});
|
|
238
|
+
return c.json({
|
|
239
|
+
id: plan.id,
|
|
240
|
+
title: plan.title,
|
|
241
|
+
content: plan.content,
|
|
242
|
+
status: plan.status,
|
|
243
|
+
conversationId: plan.conversation_id,
|
|
244
|
+
createdAt: plan.created_at,
|
|
245
|
+
updatedAt: plan.updated_at
|
|
246
|
+
}, 201);
|
|
247
|
+
});
|
|
248
|
+
api.put("/plans/:id", async (c) => {
|
|
249
|
+
const id = c.req.param("id");
|
|
250
|
+
const body = await c.req.json();
|
|
251
|
+
const existing = plans.getPlan(id);
|
|
252
|
+
if (!existing) {
|
|
253
|
+
return c.json({ error: "Plan not found" }, 404);
|
|
254
|
+
}
|
|
255
|
+
plans.updatePlan(id, {
|
|
256
|
+
title: body.title,
|
|
257
|
+
content: body.content,
|
|
258
|
+
status: body.status
|
|
259
|
+
});
|
|
260
|
+
return c.json({ success: true });
|
|
261
|
+
});
|
|
262
|
+
api.delete("/plans/:id", (c) => {
|
|
263
|
+
const id = c.req.param("id");
|
|
264
|
+
plans.deletePlan(id);
|
|
265
|
+
return c.json({ success: true });
|
|
266
|
+
});
|
|
267
|
+
api.get("/search", (c) => {
|
|
268
|
+
const query = c.req.query("q") || "";
|
|
269
|
+
const limit = parseInt(c.req.query("limit") || "50", 10);
|
|
270
|
+
if (!query.trim()) {
|
|
271
|
+
return c.json({ results: [] });
|
|
272
|
+
}
|
|
273
|
+
const results = search.searchMessages(query, limit);
|
|
274
|
+
return c.json({
|
|
275
|
+
query,
|
|
276
|
+
results: results.map((r) => ({
|
|
277
|
+
messageId: r.message_id,
|
|
278
|
+
conversationId: r.conversation_id,
|
|
279
|
+
conversationTitle: r.conversation_title,
|
|
280
|
+
role: r.role,
|
|
281
|
+
content: r.content,
|
|
282
|
+
timestamp: r.timestamp,
|
|
283
|
+
snippet: r.snippet
|
|
284
|
+
}))
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
api.post("/migrate", async (c) => {
|
|
288
|
+
const body = await c.req.json();
|
|
289
|
+
const localConversations = body.conversations;
|
|
290
|
+
if (!localConversations || !Array.isArray(localConversations)) {
|
|
291
|
+
return c.json({ error: "Invalid conversations data" }, 400);
|
|
292
|
+
}
|
|
293
|
+
let imported = 0;
|
|
294
|
+
let skipped = 0;
|
|
295
|
+
for (const conv of localConversations) {
|
|
296
|
+
if (conversations.getConversation(conv.id)) {
|
|
297
|
+
skipped++;
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
conversations.createConversation({
|
|
301
|
+
id: conv.id,
|
|
302
|
+
title: conv.title
|
|
303
|
+
});
|
|
304
|
+
for (const msg of conv.messages || []) {
|
|
305
|
+
messages.createMessage({
|
|
306
|
+
id: msg.id,
|
|
307
|
+
conversationId: conv.id,
|
|
308
|
+
role: msg.role,
|
|
309
|
+
content: msg.content,
|
|
310
|
+
timestamp: msg.timestamp,
|
|
311
|
+
source: "web",
|
|
312
|
+
images: msg.images
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
if (conv.activities && conv.activities.length > 0) {
|
|
316
|
+
activities.createActivitiesBatch(
|
|
317
|
+
conv.activities.map((a) => ({
|
|
318
|
+
id: a.id,
|
|
319
|
+
conversationId: conv.id,
|
|
320
|
+
tool: a.tool,
|
|
321
|
+
input: a.input,
|
|
322
|
+
status: a.status,
|
|
323
|
+
timestamp: a.timestamp,
|
|
324
|
+
duration: a.duration,
|
|
325
|
+
error: a.error
|
|
326
|
+
}))
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
imported++;
|
|
330
|
+
}
|
|
331
|
+
return c.json({
|
|
332
|
+
success: true,
|
|
333
|
+
imported,
|
|
334
|
+
skipped,
|
|
335
|
+
total: localConversations.length
|
|
336
|
+
});
|
|
337
|
+
});
|
|
338
|
+
api.get("/integrations", (c) => {
|
|
339
|
+
let telegramConfigured = !!process.env.TELEGRAM_BOT_TOKEN;
|
|
340
|
+
if (!telegramConfigured) {
|
|
341
|
+
try {
|
|
342
|
+
const { existsSync } = require("fs");
|
|
343
|
+
const { join } = require("path");
|
|
344
|
+
const { homedir } = require("os");
|
|
345
|
+
const tokenPath = join(homedir(), ".talkie", "telegram.token");
|
|
346
|
+
telegramConfigured = existsSync(tokenPath);
|
|
347
|
+
} catch {
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
return c.json({
|
|
351
|
+
mcp: {
|
|
352
|
+
configured: true,
|
|
353
|
+
toolCount: 15,
|
|
354
|
+
tools: [
|
|
355
|
+
"launch_talkie",
|
|
356
|
+
"get_talkie_status",
|
|
357
|
+
"get_transcript",
|
|
358
|
+
"get_conversation_history",
|
|
359
|
+
"get_claude_session",
|
|
360
|
+
"set_claude_session",
|
|
361
|
+
"disconnect_claude_session",
|
|
362
|
+
"get_pending_message",
|
|
363
|
+
"respond_to_talkie",
|
|
364
|
+
"update_talkie_state",
|
|
365
|
+
"analyze_image",
|
|
366
|
+
"open_url",
|
|
367
|
+
"create_talkie_job",
|
|
368
|
+
"get_talkie_job",
|
|
369
|
+
"list_talkie_jobs"
|
|
370
|
+
],
|
|
371
|
+
transport: "stdio"
|
|
372
|
+
},
|
|
373
|
+
telegram: {
|
|
374
|
+
configured: telegramConfigured
|
|
375
|
+
}
|
|
376
|
+
});
|
|
377
|
+
});
|
|
378
|
+
api.get("/transcript", (c) => {
|
|
379
|
+
return c.json({
|
|
380
|
+
transcript: state.transcript,
|
|
381
|
+
lastUserMessage: state.lastUserMessage,
|
|
382
|
+
lastAssistantMessage: state.lastAssistantMessage
|
|
383
|
+
});
|
|
384
|
+
});
|
|
385
|
+
api.get("/history", (c) => {
|
|
386
|
+
return c.json({
|
|
387
|
+
messages: state.messages
|
|
388
|
+
});
|
|
389
|
+
});
|
|
390
|
+
api.post("/state", async (c) => {
|
|
391
|
+
const update = await c.req.json();
|
|
392
|
+
updateState(update);
|
|
393
|
+
return c.json({ success: true });
|
|
394
|
+
});
|
|
395
|
+
api.get("/session", (c) => {
|
|
396
|
+
return c.json({ sessionId: state.claudeSessionId });
|
|
397
|
+
});
|
|
398
|
+
api.post("/session", async (c) => {
|
|
399
|
+
const { sessionId } = await c.req.json();
|
|
400
|
+
updateState({ claudeSessionId: sessionId || null });
|
|
401
|
+
console.log("Claude session ID set:", state.claudeSessionId);
|
|
402
|
+
return c.json({ success: true, sessionId: state.claudeSessionId });
|
|
403
|
+
});
|
|
404
|
+
api.delete("/session", (c) => {
|
|
405
|
+
updateState({ claudeSessionId: null });
|
|
406
|
+
console.log("Claude session ID cleared");
|
|
407
|
+
return c.json({ success: true });
|
|
408
|
+
});
|
|
409
|
+
api.get("/pending", (c) => {
|
|
410
|
+
return c.json({
|
|
411
|
+
pending: state.pendingMessage,
|
|
412
|
+
sessionConnected: !!state.claudeSessionId
|
|
413
|
+
});
|
|
414
|
+
});
|
|
415
|
+
api.post("/respond", async (c) => {
|
|
416
|
+
const { content } = await c.req.json();
|
|
417
|
+
if (!content) {
|
|
418
|
+
return c.json({ error: "Content required" }, 400);
|
|
419
|
+
}
|
|
420
|
+
console.log("IPC response received:", content.slice(0, 100) + "...");
|
|
421
|
+
updateState({ pendingMessage: null });
|
|
422
|
+
for (const callback of state.responseCallbacks) {
|
|
423
|
+
callback(content);
|
|
424
|
+
}
|
|
425
|
+
updateState({ responseCallbacks: [] });
|
|
426
|
+
return c.json({ success: true });
|
|
427
|
+
});
|
|
428
|
+
api.post("/send", async (c) => {
|
|
429
|
+
const { message } = await c.req.json();
|
|
430
|
+
if (!message) {
|
|
431
|
+
return c.json({ error: "Message required" }, 400);
|
|
432
|
+
}
|
|
433
|
+
console.log("IPC message received from frontend:", message.slice(0, 100));
|
|
434
|
+
updateState({
|
|
435
|
+
pendingMessage: {
|
|
436
|
+
content: message,
|
|
437
|
+
timestamp: Date.now()
|
|
438
|
+
}
|
|
439
|
+
});
|
|
440
|
+
return streamSSE(c, async (stream) => {
|
|
441
|
+
const timeout = setTimeout(() => {
|
|
442
|
+
const callbacks = state.responseCallbacks.filter((cb) => cb !== callback);
|
|
443
|
+
updateState({ responseCallbacks: callbacks });
|
|
444
|
+
stream.writeSSE({ data: JSON.stringify({ error: "Timeout waiting for response" }) });
|
|
445
|
+
stream.close();
|
|
446
|
+
}, 12e4);
|
|
447
|
+
const callback = (response) => {
|
|
448
|
+
clearTimeout(timeout);
|
|
449
|
+
stream.writeSSE({ data: JSON.stringify({ text: response }) });
|
|
450
|
+
stream.writeSSE({ data: JSON.stringify({ done: true }) });
|
|
451
|
+
stream.close();
|
|
452
|
+
};
|
|
453
|
+
state.responseCallbacks.push(callback);
|
|
454
|
+
await new Promise((resolve) => {
|
|
455
|
+
const checkClosed = setInterval(() => {
|
|
456
|
+
if (!state.responseCallbacks.includes(callback)) {
|
|
457
|
+
clearInterval(checkClosed);
|
|
458
|
+
resolve();
|
|
459
|
+
}
|
|
460
|
+
}, 100);
|
|
461
|
+
});
|
|
462
|
+
});
|
|
463
|
+
});
|
|
464
|
+
api.post("/analyze-image", async (c) => {
|
|
465
|
+
const { dataUrl, fileName, type, apiKey: clientApiKey } = await c.req.json();
|
|
466
|
+
if (!dataUrl) {
|
|
467
|
+
return c.json({ error: "Image data required" }, 400);
|
|
468
|
+
}
|
|
469
|
+
const apiKey = clientApiKey || process.env.ANTHROPIC_API_KEY;
|
|
470
|
+
if (!apiKey) {
|
|
471
|
+
return c.json({ error: "API key required for image analysis - please add one in Settings even when using Claude Code mode" }, 400);
|
|
472
|
+
}
|
|
473
|
+
const base64Data = dataUrl.split(",")[1];
|
|
474
|
+
const mediaType = type || "image/png";
|
|
475
|
+
const response = await fetch("https://api.anthropic.com/v1/messages", {
|
|
476
|
+
method: "POST",
|
|
477
|
+
headers: {
|
|
478
|
+
"Content-Type": "application/json",
|
|
479
|
+
"x-api-key": apiKey,
|
|
480
|
+
"anthropic-version": "2023-06-01"
|
|
481
|
+
},
|
|
482
|
+
body: JSON.stringify({
|
|
483
|
+
model: "claude-sonnet-4-20250514",
|
|
484
|
+
max_tokens: 1024,
|
|
485
|
+
system: `You are analyzing images for a voice assistant app. Describe the image in detail, focusing on:
|
|
486
|
+
- If it's a UI mockup/wireframe: describe the layout, components, navigation, and user flow
|
|
487
|
+
- If it's a screenshot: describe what app/website it is, the state shown, and key elements
|
|
488
|
+
- If it's a hand-drawn sketch: interpret the drawing and describe what it represents
|
|
489
|
+
- For any image: note colors, text visible, key visual elements
|
|
490
|
+
|
|
491
|
+
Be thorough but concise. This description will be used as context for building or discussing the content.`,
|
|
492
|
+
messages: [
|
|
493
|
+
{
|
|
494
|
+
role: "user",
|
|
495
|
+
content: [
|
|
496
|
+
{
|
|
497
|
+
type: "image",
|
|
498
|
+
source: {
|
|
499
|
+
type: "base64",
|
|
500
|
+
media_type: mediaType,
|
|
501
|
+
data: base64Data
|
|
502
|
+
}
|
|
503
|
+
},
|
|
504
|
+
{
|
|
505
|
+
type: "text",
|
|
506
|
+
text: "Describe this image in detail. If it appears to be a UI design, wireframe, or sketch, focus on the structure and components."
|
|
507
|
+
}
|
|
508
|
+
]
|
|
509
|
+
}
|
|
510
|
+
]
|
|
511
|
+
})
|
|
512
|
+
});
|
|
513
|
+
if (!response.ok) {
|
|
514
|
+
const error = await response.text();
|
|
515
|
+
return c.json({ error: `API error: ${error}` }, response.status);
|
|
516
|
+
}
|
|
517
|
+
const data = await response.json();
|
|
518
|
+
const description = data.content[0]?.text || "Unable to analyze image.";
|
|
519
|
+
return c.json({ description, fileName });
|
|
520
|
+
});
|
|
521
|
+
api.post("/analyze-image-cc", async (c) => {
|
|
522
|
+
const { dataUrl, fileName } = await c.req.json();
|
|
523
|
+
if (!dataUrl) {
|
|
524
|
+
return c.json({ error: "Image data required" }, 400);
|
|
525
|
+
}
|
|
526
|
+
return new Promise((resolve) => {
|
|
527
|
+
let description = "";
|
|
528
|
+
const handle = spawnClaude({
|
|
529
|
+
prompt: "Describe this image in detail. If it appears to be a UI design, wireframe, or sketch, focus on the structure and components. Be thorough but concise. Output ONLY the description, no preamble.",
|
|
530
|
+
images: [{ dataUrl, fileName: fileName || "image.png" }],
|
|
531
|
+
rawMode: true,
|
|
532
|
+
callbacks: {
|
|
533
|
+
onText: (text) => {
|
|
534
|
+
description += text;
|
|
535
|
+
},
|
|
536
|
+
onActivity: () => {
|
|
537
|
+
},
|
|
538
|
+
onError: (error) => {
|
|
539
|
+
console.error("Image analysis via Claude Code failed:", error);
|
|
540
|
+
},
|
|
541
|
+
onComplete: () => {
|
|
542
|
+
resolve(c.json({
|
|
543
|
+
description: description.trim() || "Unable to analyze image.",
|
|
544
|
+
fileName
|
|
545
|
+
}));
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
});
|
|
549
|
+
setTimeout(() => {
|
|
550
|
+
handle.kill();
|
|
551
|
+
resolve(c.json({
|
|
552
|
+
description: description.trim() || "Analysis timed out.",
|
|
553
|
+
fileName
|
|
554
|
+
}));
|
|
555
|
+
}, 6e4);
|
|
556
|
+
});
|
|
557
|
+
});
|
|
558
|
+
api.post("/open-url", async (c) => {
|
|
559
|
+
const { url } = await c.req.json();
|
|
560
|
+
if (!url || typeof url !== "string") {
|
|
561
|
+
return c.json({ error: "URL required" }, 400);
|
|
562
|
+
}
|
|
563
|
+
if (!url.startsWith("http://") && !url.startsWith("https://")) {
|
|
564
|
+
return c.json({ error: "Only http/https URLs allowed" }, 400);
|
|
565
|
+
}
|
|
566
|
+
console.log("Opening URL in browser:", url);
|
|
567
|
+
return new Promise((resolve) => {
|
|
568
|
+
const open = spawn("open", [url]);
|
|
569
|
+
open.on("error", (err) => {
|
|
570
|
+
resolve(c.json({ error: err.message }, 500));
|
|
571
|
+
});
|
|
572
|
+
open.on("close", (code) => {
|
|
573
|
+
if (code === 0) {
|
|
574
|
+
resolve(c.json({ success: true }));
|
|
575
|
+
} else {
|
|
576
|
+
resolve(c.json({ error: `Failed to open URL (code ${code})` }, 500));
|
|
577
|
+
}
|
|
578
|
+
});
|
|
579
|
+
});
|
|
580
|
+
});
|
|
581
|
+
api.post("/claude-code", async (c) => {
|
|
582
|
+
const { message, history, images } = await c.req.json();
|
|
583
|
+
if (!message) {
|
|
584
|
+
return c.json({ error: "Message required" }, 400);
|
|
585
|
+
}
|
|
586
|
+
return streamSSE(c, async (stream) => {
|
|
587
|
+
const handle = spawnClaude({
|
|
588
|
+
prompt: message,
|
|
589
|
+
history: history || state.messages || [],
|
|
590
|
+
images,
|
|
591
|
+
callbacks: {
|
|
592
|
+
onText: (text) => {
|
|
593
|
+
stream.writeSSE({ data: JSON.stringify({ text }) });
|
|
594
|
+
},
|
|
595
|
+
onActivity: (event) => {
|
|
596
|
+
stream.writeSSE({ data: JSON.stringify({ activity: event }) });
|
|
597
|
+
},
|
|
598
|
+
onPlan: (plan) => {
|
|
599
|
+
stream.writeSSE({ data: JSON.stringify({ plan }) });
|
|
600
|
+
},
|
|
601
|
+
onError: (error) => {
|
|
602
|
+
stream.writeSSE({ data: JSON.stringify({ error }) });
|
|
603
|
+
},
|
|
604
|
+
onComplete: (code) => {
|
|
605
|
+
stream.writeSSE({ data: JSON.stringify({ done: true, code }) });
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
});
|
|
609
|
+
await handle.promise;
|
|
610
|
+
});
|
|
611
|
+
});
|
|
612
|
+
export {
|
|
613
|
+
api
|
|
614
|
+
};
|