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,137 @@
1
+ import { Hono } from "hono";
2
+ import { serveStatic } from "@hono/node-server/serve-static";
3
+ import { createServer as createHttpsServer } from "https";
4
+ import { readFileSync, existsSync } from "fs";
5
+ import { fileURLToPath } from "url";
6
+ import { dirname, join } from "path";
7
+ import { getSSLCerts } from "./ssl.js";
8
+ import { api } from "./api.js";
9
+ import { initDb, closeDb } from "./db/index.js";
10
+ import { startTelegramBot, stopTelegramBot } from "./telegram/index.js";
11
+ import { getNotificationDispatcher } from "./notifications/dispatcher.js";
12
+ import { MacOSNotificationChannel } from "./notifications/macos.js";
13
+ import { getJobManager } from "./jobs/manager.js";
14
+ const __dirname = dirname(fileURLToPath(import.meta.url));
15
+ const distPath = join(__dirname, "..", "dist");
16
+ let server = null;
17
+ function startServer(port = 5173) {
18
+ return new Promise((resolve, reject) => {
19
+ if (!existsSync(distPath)) {
20
+ reject(new Error(`dist/ not found at ${distPath}. Run 'npm run build' first.`));
21
+ return;
22
+ }
23
+ try {
24
+ initDb();
25
+ } catch (err) {
26
+ console.error("Failed to initialize database:", err);
27
+ reject(err);
28
+ return;
29
+ }
30
+ const dispatcher = getNotificationDispatcher();
31
+ dispatcher.register(new MacOSNotificationChannel());
32
+ const jobManager = getJobManager();
33
+ jobManager.init();
34
+ startTelegramBot().catch((err) => {
35
+ console.log("Telegram bot not started:", err.message);
36
+ });
37
+ const app = new Hono();
38
+ app.route("/api", api);
39
+ app.use("/*", serveStatic({ root: distPath.replace(process.cwd(), ".") }));
40
+ app.get("*", (c) => {
41
+ const indexPath = join(distPath, "index.html");
42
+ if (existsSync(indexPath)) {
43
+ const html = readFileSync(indexPath, "utf-8");
44
+ return c.html(html);
45
+ }
46
+ return c.text("Not found", 404);
47
+ });
48
+ const certs = getSSLCerts();
49
+ const serverOptions = {
50
+ key: certs.key,
51
+ cert: certs.cert
52
+ };
53
+ server = createHttpsServer(serverOptions, async (req, res) => {
54
+ const url = new URL(req.url || "/", `https://localhost:${port}`);
55
+ const headers = new Headers();
56
+ for (const [key, value] of Object.entries(req.headers)) {
57
+ if (value) {
58
+ if (Array.isArray(value)) {
59
+ value.forEach((v) => headers.append(key, v));
60
+ } else {
61
+ headers.set(key, value);
62
+ }
63
+ }
64
+ }
65
+ let body = null;
66
+ if (req.method && ["POST", "PUT", "PATCH"].includes(req.method)) {
67
+ const chunks = [];
68
+ for await (const chunk of req) {
69
+ chunks.push(chunk);
70
+ }
71
+ body = Buffer.concat(chunks);
72
+ }
73
+ const request = new Request(url.toString(), {
74
+ method: req.method || "GET",
75
+ headers,
76
+ body,
77
+ // @ts-expect-error - Node.js specific
78
+ duplex: "half"
79
+ });
80
+ const response = await app.fetch(request);
81
+ res.writeHead(response.status, Object.fromEntries(response.headers.entries()));
82
+ if (response.body) {
83
+ const reader = response.body.getReader();
84
+ try {
85
+ while (true) {
86
+ const { done, value } = await reader.read();
87
+ if (done) break;
88
+ res.write(value);
89
+ }
90
+ } finally {
91
+ reader.releaseLock();
92
+ }
93
+ }
94
+ res.end();
95
+ });
96
+ server.listen(port, () => {
97
+ console.log(`Talkie server running at https://localhost:${port}`);
98
+ resolve();
99
+ });
100
+ server.on("error", (err) => {
101
+ if (err.code === "EADDRINUSE") {
102
+ reject(new Error(`Port ${port} is already in use`));
103
+ } else {
104
+ reject(err);
105
+ }
106
+ });
107
+ });
108
+ }
109
+ async function stopServer() {
110
+ stopTelegramBot();
111
+ closeDb();
112
+ if (server) {
113
+ await new Promise((resolve) => {
114
+ server.close(() => resolve());
115
+ });
116
+ server = null;
117
+ }
118
+ }
119
+ function setupShutdownHandlers() {
120
+ const shutdown = async (signal) => {
121
+ console.log(`
122
+ Received ${signal}, shutting down gracefully...`);
123
+ await stopServer();
124
+ process.exit(0);
125
+ };
126
+ process.on("SIGINT", () => shutdown("SIGINT"));
127
+ process.on("SIGTERM", () => shutdown("SIGTERM"));
128
+ }
129
+ if (import.meta.url === `file://${process.argv[1]}`) {
130
+ setupShutdownHandlers();
131
+ const port = parseInt(process.env.PORT || "5173", 10);
132
+ startServer(port).catch(console.error);
133
+ }
134
+ export {
135
+ startServer,
136
+ stopServer
137
+ };
@@ -0,0 +1,108 @@
1
+ import { Hono } from "hono";
2
+ import { streamSSE } from "hono/streaming";
3
+ import { getJobManager } from "./manager.js";
4
+ const jobRoutes = new Hono();
5
+ jobRoutes.post("/", async (c) => {
6
+ const { conversationId, prompt, source, history } = await c.req.json();
7
+ if (!conversationId || !prompt) {
8
+ return c.json({ error: "conversationId and prompt are required" }, 400);
9
+ }
10
+ const manager = getJobManager();
11
+ const job = manager.createJob({ conversationId, prompt, source, history });
12
+ return c.json({ id: job.id, status: job.status });
13
+ });
14
+ jobRoutes.get("/", (c) => {
15
+ const status = c.req.query("status");
16
+ const conversationId = c.req.query("conversationId");
17
+ const manager = getJobManager();
18
+ const filters = {};
19
+ if (status) filters.status = status;
20
+ if (conversationId) filters.conversationId = conversationId;
21
+ const jobs = manager.listJobs(filters);
22
+ return c.json({ jobs });
23
+ });
24
+ jobRoutes.get("/:id", (c) => {
25
+ const id = c.req.param("id");
26
+ const manager = getJobManager();
27
+ const job = manager.getJob(id);
28
+ if (!job) {
29
+ return c.json({ error: "Job not found" }, 404);
30
+ }
31
+ return c.json(job);
32
+ });
33
+ jobRoutes.get("/:id/events", async (c) => {
34
+ const id = c.req.param("id");
35
+ const manager = getJobManager();
36
+ const job = manager.getJob(id);
37
+ if (!job) {
38
+ return c.json({ error: "Job not found" }, 404);
39
+ }
40
+ return streamSSE(c, async (stream) => {
41
+ const existingEvents = manager.getJobEvents(id);
42
+ for (const event of existingEvents) {
43
+ await stream.writeSSE({
44
+ data: JSON.stringify({
45
+ type: event.event_type,
46
+ data: event.data,
47
+ timestamp: event.timestamp
48
+ })
49
+ });
50
+ }
51
+ if (["completed", "failed", "cancelled"].includes(job.status)) {
52
+ await stream.writeSSE({ data: JSON.stringify({ done: true, status: job.status }) });
53
+ return;
54
+ }
55
+ let closed = false;
56
+ const unsubscribe = manager.subscribe(id, (event) => {
57
+ if (closed) return;
58
+ stream.writeSSE({
59
+ data: JSON.stringify(event)
60
+ }).catch(() => {
61
+ closed = true;
62
+ unsubscribe();
63
+ });
64
+ if (event.type === "status_change") {
65
+ try {
66
+ const data = JSON.parse(event.data);
67
+ if (["completed", "failed", "cancelled"].includes(data.status)) {
68
+ stream.writeSSE({ data: JSON.stringify({ done: true, status: data.status }) }).catch(() => {
69
+ });
70
+ closed = true;
71
+ unsubscribe();
72
+ }
73
+ } catch {
74
+ }
75
+ }
76
+ });
77
+ await new Promise((resolve) => {
78
+ const check = setInterval(() => {
79
+ if (closed) {
80
+ clearInterval(check);
81
+ resolve();
82
+ }
83
+ }, 500);
84
+ setTimeout(() => {
85
+ clearInterval(check);
86
+ closed = true;
87
+ unsubscribe();
88
+ resolve();
89
+ }, 6e5);
90
+ });
91
+ });
92
+ });
93
+ jobRoutes.delete("/:id", (c) => {
94
+ const id = c.req.param("id");
95
+ const manager = getJobManager();
96
+ const cancelled = manager.cancelJob(id);
97
+ if (!cancelled) {
98
+ const job = manager.getJob(id);
99
+ if (!job) {
100
+ return c.json({ error: "Job not found" }, 404);
101
+ }
102
+ return c.json({ error: `Cannot cancel job with status: ${job.status}` }, 400);
103
+ }
104
+ return c.json({ success: true });
105
+ });
106
+ export {
107
+ jobRoutes
108
+ };
@@ -0,0 +1,231 @@
1
+ import { spawnClaude } from "./runner.js";
2
+ import { getNotificationDispatcher } from "../notifications/dispatcher.js";
3
+ import * as jobsRepo from "../db/repositories/jobs.js";
4
+ import * as messagesRepo from "../db/repositories/messages.js";
5
+ import * as conversationsRepo from "../db/repositories/conversations.js";
6
+ function generateId() {
7
+ return Date.now().toString(36) + Math.random().toString(36).slice(2, 8);
8
+ }
9
+ class JobManager {
10
+ currentHandle = null;
11
+ currentJobId = null;
12
+ subscribers = /* @__PURE__ */ new Map();
13
+ init() {
14
+ const cleaned = jobsRepo.cleanupStaleJobs();
15
+ if (cleaned > 0) {
16
+ console.log(`Cleaned up ${cleaned} stale jobs from previous run`);
17
+ }
18
+ }
19
+ createJob(params) {
20
+ const id = generateId();
21
+ const job = jobsRepo.createJob({
22
+ id,
23
+ conversationId: params.conversationId,
24
+ prompt: params.prompt,
25
+ source: params.source || "web"
26
+ });
27
+ if (params.history && params.history.length > 0) {
28
+ jobsRepo.createJobEvent({
29
+ jobId: id,
30
+ eventType: "context",
31
+ data: JSON.stringify(params.history)
32
+ });
33
+ }
34
+ this.emitEvent(id, {
35
+ type: "status_change",
36
+ data: JSON.stringify({ status: "queued", jobId: id })
37
+ });
38
+ this.processNext();
39
+ return job;
40
+ }
41
+ getJob(id) {
42
+ return jobsRepo.getJob(id);
43
+ }
44
+ listJobs(filters) {
45
+ return jobsRepo.listJobs(filters);
46
+ }
47
+ getJobEvents(jobId, since) {
48
+ return jobsRepo.getJobEvents(jobId, since);
49
+ }
50
+ cancelJob(id) {
51
+ const job = jobsRepo.getJob(id);
52
+ if (!job) return false;
53
+ if (job.status === "queued") {
54
+ jobsRepo.updateJob(id, { status: "cancelled", completed_at: Date.now() });
55
+ this.emitEvent(id, {
56
+ type: "status_change",
57
+ data: JSON.stringify({ status: "cancelled" })
58
+ });
59
+ return true;
60
+ }
61
+ if (job.status === "running" && this.currentJobId === id && this.currentHandle) {
62
+ this.currentHandle.kill();
63
+ jobsRepo.updateJob(id, { status: "cancelled", completed_at: Date.now() });
64
+ this.emitEvent(id, {
65
+ type: "status_change",
66
+ data: JSON.stringify({ status: "cancelled" })
67
+ });
68
+ this.currentHandle = null;
69
+ this.currentJobId = null;
70
+ this.processNext();
71
+ return true;
72
+ }
73
+ return false;
74
+ }
75
+ subscribe(jobId, callback) {
76
+ if (!this.subscribers.has(jobId)) {
77
+ this.subscribers.set(jobId, /* @__PURE__ */ new Set());
78
+ }
79
+ this.subscribers.get(jobId).add(callback);
80
+ return () => {
81
+ const subs = this.subscribers.get(jobId);
82
+ if (subs) {
83
+ subs.delete(callback);
84
+ if (subs.size === 0) {
85
+ this.subscribers.delete(jobId);
86
+ }
87
+ }
88
+ };
89
+ }
90
+ emitEvent(jobId, event) {
91
+ const subs = this.subscribers.get(jobId);
92
+ if (subs) {
93
+ for (const callback of subs) {
94
+ try {
95
+ callback(event);
96
+ } catch (e) {
97
+ console.error("Job event subscriber error:", e);
98
+ }
99
+ }
100
+ }
101
+ }
102
+ processNext() {
103
+ if (this.currentHandle) return;
104
+ const queued = jobsRepo.listJobs({ status: "queued" });
105
+ if (queued.length === 0) return;
106
+ queued.sort((a, b) => a.created_at - b.created_at);
107
+ const job = queued[0];
108
+ this.runJob(job);
109
+ }
110
+ async runJob(job) {
111
+ const jobId = job.id;
112
+ this.currentJobId = jobId;
113
+ jobsRepo.updateJob(jobId, { status: "running", started_at: Date.now() });
114
+ this.emitEvent(jobId, {
115
+ type: "status_change",
116
+ data: JSON.stringify({ status: "running" })
117
+ });
118
+ let history = [];
119
+ const events = jobsRepo.getJobEvents(jobId);
120
+ const contextEvent = events.find((e) => e.event_type === "context");
121
+ if (contextEvent?.data) {
122
+ try {
123
+ history = JSON.parse(contextEvent.data);
124
+ } catch {
125
+ }
126
+ }
127
+ let fullResponse = "";
128
+ const handle = spawnClaude({
129
+ prompt: job.prompt,
130
+ history,
131
+ callbacks: {
132
+ onText: (text) => {
133
+ fullResponse += text;
134
+ jobsRepo.createJobEvent({
135
+ jobId,
136
+ eventType: "text",
137
+ data: text
138
+ });
139
+ this.emitEvent(jobId, {
140
+ type: "text",
141
+ data: JSON.stringify({ text })
142
+ });
143
+ },
144
+ onActivity: (event) => {
145
+ jobsRepo.createJobEvent({
146
+ jobId,
147
+ eventType: event.type,
148
+ data: JSON.stringify(event)
149
+ });
150
+ this.emitEvent(jobId, {
151
+ type: "activity",
152
+ data: JSON.stringify(event)
153
+ });
154
+ },
155
+ onError: (error) => {
156
+ jobsRepo.createJobEvent({
157
+ jobId,
158
+ eventType: "error",
159
+ data: error
160
+ });
161
+ this.emitEvent(jobId, {
162
+ type: "error",
163
+ data: JSON.stringify({ error })
164
+ });
165
+ },
166
+ onComplete: (code) => {
167
+ const currentJob = jobsRepo.getJob(jobId);
168
+ if (currentJob?.status === "cancelled") return;
169
+ const now = Date.now();
170
+ const status = code === 0 ? "completed" : "failed";
171
+ const error = code !== 0 ? `Process exited with code ${code}` : void 0;
172
+ jobsRepo.updateJob(jobId, {
173
+ status,
174
+ result: fullResponse || null,
175
+ error: error || void 0,
176
+ completed_at: now
177
+ });
178
+ if (fullResponse.trim()) {
179
+ try {
180
+ const msgId = generateId();
181
+ messagesRepo.createMessage({
182
+ id: msgId,
183
+ conversationId: job.conversation_id,
184
+ role: "assistant",
185
+ content: fullResponse,
186
+ source: "job"
187
+ });
188
+ conversationsRepo.touchConversation(job.conversation_id);
189
+ } catch (e) {
190
+ console.error("Failed to save job response as message:", e);
191
+ }
192
+ }
193
+ this.emitEvent(jobId, {
194
+ type: "status_change",
195
+ data: JSON.stringify({ status, result: fullResponse, error })
196
+ });
197
+ const dispatcher = getNotificationDispatcher();
198
+ const notification = status === "completed" ? {
199
+ type: "job_completed",
200
+ jobId,
201
+ title: "Talkie: Task complete",
202
+ body: fullResponse.slice(0, 80) || "Done."
203
+ } : {
204
+ type: "job_failed",
205
+ jobId,
206
+ title: "Talkie: Task failed",
207
+ body: error || "Unknown error"
208
+ };
209
+ dispatcher.dispatch(notification).catch((e) => {
210
+ console.error("Notification dispatch failed:", e);
211
+ });
212
+ this.currentHandle = null;
213
+ this.currentJobId = null;
214
+ this.processNext();
215
+ }
216
+ }
217
+ });
218
+ this.currentHandle = handle;
219
+ jobsRepo.updateJob(jobId, { pid: handle.pid });
220
+ }
221
+ }
222
+ let manager = null;
223
+ function getJobManager() {
224
+ if (!manager) {
225
+ manager = new JobManager();
226
+ }
227
+ return manager;
228
+ }
229
+ export {
230
+ getJobManager
231
+ };