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.
- package/.claude-plugin/plugin.json +8 -0
- package/.mcp.json +8 -0
- package/README.md +169 -0
- package/bin/wtb-server.js +336 -0
- package/bin/wtb.js +43 -0
- package/dist/Talkie_logo.png +0 -0
- package/dist/assets/index-UPnYoRh1.js +81 -0
- package/dist/assets/index-VbGv60d-.css +1 -0
- package/dist/index.html +14 -0
- package/mcp-server/dist/index.js +401 -0
- package/package.json +86 -0
- package/server/api.js +629 -0
- package/server/db/index.js +67 -0
- package/server/db/repositories/activities.js +85 -0
- package/server/db/repositories/activities.test.js +106 -0
- package/server/db/repositories/conversations.js +93 -0
- package/server/db/repositories/conversations.test.js +137 -0
- package/server/db/repositories/jobs.js +128 -0
- package/server/db/repositories/messages.js +98 -0
- package/server/db/repositories/messages.test.js +152 -0
- package/server/db/repositories/plans.js +57 -0
- package/server/db/repositories/plans.test.js +98 -0
- package/server/db/repositories/search.js +34 -0
- package/server/db/repositories/search.test.js +61 -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 +61 -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/server/test/helpers.js +14 -0
- package/skills/export-tape/SKILL.md +26 -0
- package/skills/launch-voice/SKILL.md +25 -0
- package/skills/manage-plans/SKILL.md +32 -0
- package/skills/save-conversation/SKILL.md +24 -0
- package/skills/search-tapes/SKILL.md +21 -0
package/server/index.js
ADDED
|
@@ -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
|
+
};
|