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
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { getDb } from "../index.js";
|
|
2
|
+
function getActivitiesForConversation(conversationId, limit = 100) {
|
|
3
|
+
const db = getDb();
|
|
4
|
+
return db.prepare(`
|
|
5
|
+
SELECT id, conversation_id, message_id, tool, input, status, timestamp, duration, error
|
|
6
|
+
FROM activities
|
|
7
|
+
WHERE conversation_id = ?
|
|
8
|
+
ORDER BY timestamp DESC
|
|
9
|
+
LIMIT ?
|
|
10
|
+
`).all(conversationId, limit);
|
|
11
|
+
}
|
|
12
|
+
function getActivitiesForMessage(messageId) {
|
|
13
|
+
const db = getDb();
|
|
14
|
+
return db.prepare(`
|
|
15
|
+
SELECT id, conversation_id, message_id, tool, input, status, timestamp, duration, error
|
|
16
|
+
FROM activities
|
|
17
|
+
WHERE message_id = ?
|
|
18
|
+
ORDER BY timestamp ASC
|
|
19
|
+
`).all(messageId);
|
|
20
|
+
}
|
|
21
|
+
function createActivity(input) {
|
|
22
|
+
const db = getDb();
|
|
23
|
+
const timestamp = input.timestamp || Date.now();
|
|
24
|
+
db.prepare(`
|
|
25
|
+
INSERT INTO activities (id, conversation_id, message_id, tool, input, status, timestamp, duration, error)
|
|
26
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
27
|
+
`).run(
|
|
28
|
+
input.id,
|
|
29
|
+
input.conversationId,
|
|
30
|
+
input.messageId || null,
|
|
31
|
+
input.tool,
|
|
32
|
+
input.input || null,
|
|
33
|
+
input.status,
|
|
34
|
+
timestamp,
|
|
35
|
+
input.duration || null,
|
|
36
|
+
input.error || null
|
|
37
|
+
);
|
|
38
|
+
return {
|
|
39
|
+
id: input.id,
|
|
40
|
+
conversation_id: input.conversationId,
|
|
41
|
+
message_id: input.messageId || null,
|
|
42
|
+
tool: input.tool,
|
|
43
|
+
input: input.input || null,
|
|
44
|
+
status: input.status,
|
|
45
|
+
timestamp,
|
|
46
|
+
duration: input.duration || null,
|
|
47
|
+
error: input.error || null
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
function createActivitiesBatch(activities) {
|
|
51
|
+
if (activities.length === 0) return;
|
|
52
|
+
const db = getDb();
|
|
53
|
+
const insert = db.prepare(`
|
|
54
|
+
INSERT INTO activities (id, conversation_id, message_id, tool, input, status, timestamp, duration, error)
|
|
55
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
56
|
+
`);
|
|
57
|
+
const insertMany = db.transaction((items) => {
|
|
58
|
+
for (const input of items) {
|
|
59
|
+
insert.run(
|
|
60
|
+
input.id,
|
|
61
|
+
input.conversationId,
|
|
62
|
+
input.messageId || null,
|
|
63
|
+
input.tool,
|
|
64
|
+
input.input || null,
|
|
65
|
+
input.status,
|
|
66
|
+
input.timestamp || Date.now(),
|
|
67
|
+
input.duration || null,
|
|
68
|
+
input.error || null
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
insertMany(activities);
|
|
73
|
+
}
|
|
74
|
+
function deleteActivitiesForConversation(conversationId) {
|
|
75
|
+
const db = getDb();
|
|
76
|
+
const result = db.prepare("DELETE FROM activities WHERE conversation_id = ?").run(conversationId);
|
|
77
|
+
return result.changes;
|
|
78
|
+
}
|
|
79
|
+
export {
|
|
80
|
+
createActivitiesBatch,
|
|
81
|
+
createActivity,
|
|
82
|
+
deleteActivitiesForConversation,
|
|
83
|
+
getActivitiesForConversation,
|
|
84
|
+
getActivitiesForMessage
|
|
85
|
+
};
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from "vitest";
|
|
2
|
+
import { resetTestDb } from "../../test/helpers.js";
|
|
3
|
+
import { createConversation } from "./conversations.js";
|
|
4
|
+
import { createMessage } from "./messages.js";
|
|
5
|
+
import {
|
|
6
|
+
getActivitiesForConversation,
|
|
7
|
+
getActivitiesForMessage,
|
|
8
|
+
createActivity,
|
|
9
|
+
createActivitiesBatch,
|
|
10
|
+
deleteActivitiesForConversation
|
|
11
|
+
} from "./activities.js";
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
resetTestDb();
|
|
14
|
+
createConversation({ id: "c1", title: "Test" });
|
|
15
|
+
});
|
|
16
|
+
describe("createActivity", () => {
|
|
17
|
+
it("creates a basic activity", () => {
|
|
18
|
+
const activity = createActivity({
|
|
19
|
+
id: "a1",
|
|
20
|
+
conversationId: "c1",
|
|
21
|
+
tool: "Read",
|
|
22
|
+
status: "complete"
|
|
23
|
+
});
|
|
24
|
+
expect(activity.id).toBe("a1");
|
|
25
|
+
expect(activity.tool).toBe("Read");
|
|
26
|
+
expect(activity.status).toBe("complete");
|
|
27
|
+
expect(activity.message_id).toBeNull();
|
|
28
|
+
expect(activity.input).toBeNull();
|
|
29
|
+
expect(activity.duration).toBeNull();
|
|
30
|
+
expect(activity.error).toBeNull();
|
|
31
|
+
});
|
|
32
|
+
it("creates activity with all optional fields", () => {
|
|
33
|
+
createMessage({ id: "m1", conversationId: "c1", role: "user", content: "test" });
|
|
34
|
+
const activity = createActivity({
|
|
35
|
+
id: "a1",
|
|
36
|
+
conversationId: "c1",
|
|
37
|
+
messageId: "m1",
|
|
38
|
+
tool: "Bash",
|
|
39
|
+
input: "npm run test",
|
|
40
|
+
status: "error",
|
|
41
|
+
duration: 1500,
|
|
42
|
+
error: "Exit code 1"
|
|
43
|
+
});
|
|
44
|
+
expect(activity.message_id).toBe("m1");
|
|
45
|
+
expect(activity.input).toBe("npm run test");
|
|
46
|
+
expect(activity.status).toBe("error");
|
|
47
|
+
expect(activity.duration).toBe(1500);
|
|
48
|
+
expect(activity.error).toBe("Exit code 1");
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
describe("getActivitiesForConversation", () => {
|
|
52
|
+
it("returns empty for no activities", () => {
|
|
53
|
+
expect(getActivitiesForConversation("c1")).toEqual([]);
|
|
54
|
+
});
|
|
55
|
+
it("returns activities ordered by timestamp DESC", () => {
|
|
56
|
+
createActivity({ id: "a1", conversationId: "c1", tool: "Read", status: "complete", timestamp: 1e3 });
|
|
57
|
+
createActivity({ id: "a2", conversationId: "c1", tool: "Edit", status: "complete", timestamp: 2e3 });
|
|
58
|
+
createActivity({ id: "a3", conversationId: "c1", tool: "Bash", status: "complete", timestamp: 3e3 });
|
|
59
|
+
const activities = getActivitiesForConversation("c1");
|
|
60
|
+
expect(activities).toHaveLength(3);
|
|
61
|
+
expect(activities[0].tool).toBe("Bash");
|
|
62
|
+
expect(activities[2].tool).toBe("Read");
|
|
63
|
+
});
|
|
64
|
+
it("respects limit", () => {
|
|
65
|
+
for (let i = 0; i < 5; i++) {
|
|
66
|
+
createActivity({ id: `a${i}`, conversationId: "c1", tool: "Read", status: "complete", timestamp: i });
|
|
67
|
+
}
|
|
68
|
+
expect(getActivitiesForConversation("c1", 3)).toHaveLength(3);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
describe("getActivitiesForMessage", () => {
|
|
72
|
+
it("returns activities for a specific message", () => {
|
|
73
|
+
createMessage({ id: "m1", conversationId: "c1", role: "user", content: "test" });
|
|
74
|
+
createActivity({ id: "a1", conversationId: "c1", messageId: "m1", tool: "Read", status: "complete" });
|
|
75
|
+
createActivity({ id: "a2", conversationId: "c1", messageId: "m1", tool: "Edit", status: "complete" });
|
|
76
|
+
createActivity({ id: "a3", conversationId: "c1", tool: "Bash", status: "complete" });
|
|
77
|
+
const activities = getActivitiesForMessage("m1");
|
|
78
|
+
expect(activities).toHaveLength(2);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
describe("createActivitiesBatch", () => {
|
|
82
|
+
it("handles empty array", () => {
|
|
83
|
+
expect(() => createActivitiesBatch([])).not.toThrow();
|
|
84
|
+
expect(getActivitiesForConversation("c1")).toHaveLength(0);
|
|
85
|
+
});
|
|
86
|
+
it("inserts multiple activities in one transaction", () => {
|
|
87
|
+
createActivitiesBatch([
|
|
88
|
+
{ id: "a1", conversationId: "c1", tool: "Read", status: "complete" },
|
|
89
|
+
{ id: "a2", conversationId: "c1", tool: "Edit", status: "complete" },
|
|
90
|
+
{ id: "a3", conversationId: "c1", tool: "Bash", status: "error", error: "fail" }
|
|
91
|
+
]);
|
|
92
|
+
expect(getActivitiesForConversation("c1")).toHaveLength(3);
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
describe("deleteActivitiesForConversation", () => {
|
|
96
|
+
it("deletes all activities for a conversation", () => {
|
|
97
|
+
createActivity({ id: "a1", conversationId: "c1", tool: "Read", status: "complete" });
|
|
98
|
+
createActivity({ id: "a2", conversationId: "c1", tool: "Edit", status: "complete" });
|
|
99
|
+
const deleted = deleteActivitiesForConversation("c1");
|
|
100
|
+
expect(deleted).toBe(2);
|
|
101
|
+
expect(getActivitiesForConversation("c1")).toHaveLength(0);
|
|
102
|
+
});
|
|
103
|
+
it("returns 0 when no activities exist", () => {
|
|
104
|
+
expect(deleteActivitiesForConversation("c1")).toBe(0);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { getDb } from "../index.js";
|
|
2
|
+
function listConversations(limit = 50, offset = 0) {
|
|
3
|
+
const db = getDb();
|
|
4
|
+
return db.prepare(`
|
|
5
|
+
SELECT id, title, created_at, updated_at, project_id, parent_id
|
|
6
|
+
FROM conversations
|
|
7
|
+
ORDER BY updated_at DESC
|
|
8
|
+
LIMIT ? OFFSET ?
|
|
9
|
+
`).all(limit, offset);
|
|
10
|
+
}
|
|
11
|
+
function getConversation(id) {
|
|
12
|
+
const db = getDb();
|
|
13
|
+
const row = db.prepare(`
|
|
14
|
+
SELECT id, title, created_at, updated_at, project_id, parent_id
|
|
15
|
+
FROM conversations
|
|
16
|
+
WHERE id = ?
|
|
17
|
+
`).get(id);
|
|
18
|
+
return row || null;
|
|
19
|
+
}
|
|
20
|
+
function createConversation(input) {
|
|
21
|
+
const db = getDb();
|
|
22
|
+
const now = Date.now();
|
|
23
|
+
const title = input.title || "New conversation";
|
|
24
|
+
db.prepare(`
|
|
25
|
+
INSERT INTO conversations (id, title, created_at, updated_at, project_id, parent_id)
|
|
26
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
27
|
+
`).run(input.id, title, now, now, input.projectId || null, input.parentId || null);
|
|
28
|
+
return {
|
|
29
|
+
id: input.id,
|
|
30
|
+
title,
|
|
31
|
+
created_at: now,
|
|
32
|
+
updated_at: now,
|
|
33
|
+
project_id: input.projectId || null,
|
|
34
|
+
parent_id: input.parentId || null
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
function updateConversation(id, input) {
|
|
38
|
+
const db = getDb();
|
|
39
|
+
const existing = getConversation(id);
|
|
40
|
+
if (!existing) return null;
|
|
41
|
+
const now = Date.now();
|
|
42
|
+
const updates = ["updated_at = ?"];
|
|
43
|
+
const values = [now];
|
|
44
|
+
if (input.title !== void 0) {
|
|
45
|
+
updates.push("title = ?");
|
|
46
|
+
values.push(input.title);
|
|
47
|
+
}
|
|
48
|
+
if (input.projectId !== void 0) {
|
|
49
|
+
updates.push("project_id = ?");
|
|
50
|
+
values.push(input.projectId);
|
|
51
|
+
}
|
|
52
|
+
if (input.parentId !== void 0) {
|
|
53
|
+
updates.push("parent_id = ?");
|
|
54
|
+
values.push(input.parentId);
|
|
55
|
+
}
|
|
56
|
+
values.push(id);
|
|
57
|
+
db.prepare(`UPDATE conversations SET ${updates.join(", ")} WHERE id = ?`).run(...values);
|
|
58
|
+
return getConversation(id);
|
|
59
|
+
}
|
|
60
|
+
function deleteConversation(id) {
|
|
61
|
+
const db = getDb();
|
|
62
|
+
const result = db.prepare("DELETE FROM conversations WHERE id = ?").run(id);
|
|
63
|
+
return result.changes > 0;
|
|
64
|
+
}
|
|
65
|
+
function touchConversation(id) {
|
|
66
|
+
const db = getDb();
|
|
67
|
+
db.prepare("UPDATE conversations SET updated_at = ? WHERE id = ?").run(Date.now(), id);
|
|
68
|
+
}
|
|
69
|
+
function updateLinerNotes(id, linerNotes) {
|
|
70
|
+
const db = getDb();
|
|
71
|
+
db.prepare("UPDATE conversations SET liner_notes = ?, updated_at = ? WHERE id = ?").run(linerNotes, Date.now(), id);
|
|
72
|
+
}
|
|
73
|
+
function getLinerNotes(id) {
|
|
74
|
+
const db = getDb();
|
|
75
|
+
const row = db.prepare("SELECT liner_notes FROM conversations WHERE id = ?").get(id);
|
|
76
|
+
return row?.liner_notes || null;
|
|
77
|
+
}
|
|
78
|
+
function countConversations() {
|
|
79
|
+
const db = getDb();
|
|
80
|
+
const row = db.prepare("SELECT COUNT(*) as count FROM conversations").get();
|
|
81
|
+
return row.count;
|
|
82
|
+
}
|
|
83
|
+
export {
|
|
84
|
+
countConversations,
|
|
85
|
+
createConversation,
|
|
86
|
+
deleteConversation,
|
|
87
|
+
getConversation,
|
|
88
|
+
getLinerNotes,
|
|
89
|
+
listConversations,
|
|
90
|
+
touchConversation,
|
|
91
|
+
updateConversation,
|
|
92
|
+
updateLinerNotes
|
|
93
|
+
};
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from "vitest";
|
|
2
|
+
import { resetTestDb } from "../../test/helpers.js";
|
|
3
|
+
import {
|
|
4
|
+
listConversations,
|
|
5
|
+
getConversation,
|
|
6
|
+
createConversation,
|
|
7
|
+
updateConversation,
|
|
8
|
+
deleteConversation,
|
|
9
|
+
touchConversation,
|
|
10
|
+
updateLinerNotes,
|
|
11
|
+
getLinerNotes,
|
|
12
|
+
countConversations
|
|
13
|
+
} from "./conversations.js";
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
resetTestDb();
|
|
16
|
+
});
|
|
17
|
+
describe("createConversation", () => {
|
|
18
|
+
it("creates with default title", () => {
|
|
19
|
+
const conv = createConversation({ id: "c1" });
|
|
20
|
+
expect(conv.id).toBe("c1");
|
|
21
|
+
expect(conv.title).toBe("New conversation");
|
|
22
|
+
expect(conv.created_at).toBeGreaterThan(0);
|
|
23
|
+
expect(conv.updated_at).toBe(conv.created_at);
|
|
24
|
+
});
|
|
25
|
+
it("creates with custom title", () => {
|
|
26
|
+
const conv = createConversation({ id: "c1", title: "My Chat" });
|
|
27
|
+
expect(conv.title).toBe("My Chat");
|
|
28
|
+
});
|
|
29
|
+
it("creates with project and parent IDs", () => {
|
|
30
|
+
const conv = createConversation({ id: "c1", projectId: "proj1", parentId: "parent1" });
|
|
31
|
+
expect(conv.project_id).toBe("proj1");
|
|
32
|
+
expect(conv.parent_id).toBe("parent1");
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
describe("getConversation", () => {
|
|
36
|
+
it("returns existing conversation", () => {
|
|
37
|
+
createConversation({ id: "c1", title: "Test" });
|
|
38
|
+
const conv = getConversation("c1");
|
|
39
|
+
expect(conv).not.toBeNull();
|
|
40
|
+
expect(conv.title).toBe("Test");
|
|
41
|
+
});
|
|
42
|
+
it("returns null for non-existing", () => {
|
|
43
|
+
expect(getConversation("nope")).toBeNull();
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
describe("listConversations", () => {
|
|
47
|
+
it("returns empty list initially", () => {
|
|
48
|
+
expect(listConversations()).toEqual([]);
|
|
49
|
+
});
|
|
50
|
+
it("returns conversations ordered by updated_at DESC", () => {
|
|
51
|
+
createConversation({ id: "c1", title: "First" });
|
|
52
|
+
createConversation({ id: "c2", title: "Second" });
|
|
53
|
+
touchConversation("c1");
|
|
54
|
+
const list = listConversations();
|
|
55
|
+
expect(list).toHaveLength(2);
|
|
56
|
+
expect(list[0].id).toBe("c1");
|
|
57
|
+
expect(list[1].id).toBe("c2");
|
|
58
|
+
});
|
|
59
|
+
it("respects limit and offset", () => {
|
|
60
|
+
createConversation({ id: "c1" });
|
|
61
|
+
createConversation({ id: "c2" });
|
|
62
|
+
createConversation({ id: "c3" });
|
|
63
|
+
const page1 = listConversations(2, 0);
|
|
64
|
+
expect(page1).toHaveLength(2);
|
|
65
|
+
const page2 = listConversations(2, 2);
|
|
66
|
+
expect(page2).toHaveLength(1);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
describe("updateConversation", () => {
|
|
70
|
+
it("updates title", () => {
|
|
71
|
+
createConversation({ id: "c1", title: "Old" });
|
|
72
|
+
const updated = updateConversation("c1", { title: "New" });
|
|
73
|
+
expect(updated.title).toBe("New");
|
|
74
|
+
expect(updated.updated_at).toBeGreaterThanOrEqual(updated.created_at);
|
|
75
|
+
});
|
|
76
|
+
it("returns null for non-existing", () => {
|
|
77
|
+
expect(updateConversation("nope", { title: "x" })).toBeNull();
|
|
78
|
+
});
|
|
79
|
+
it("updates projectId and parentId", () => {
|
|
80
|
+
createConversation({ id: "c1" });
|
|
81
|
+
const updated = updateConversation("c1", { projectId: "p1", parentId: "par1" });
|
|
82
|
+
expect(updated.project_id).toBe("p1");
|
|
83
|
+
expect(updated.parent_id).toBe("par1");
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
describe("deleteConversation", () => {
|
|
87
|
+
it("deletes existing conversation", () => {
|
|
88
|
+
createConversation({ id: "c1" });
|
|
89
|
+
expect(deleteConversation("c1")).toBe(true);
|
|
90
|
+
expect(getConversation("c1")).toBeNull();
|
|
91
|
+
});
|
|
92
|
+
it("returns false for non-existing", () => {
|
|
93
|
+
expect(deleteConversation("nope")).toBe(false);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
describe("touchConversation", () => {
|
|
97
|
+
it("updates the updated_at timestamp", () => {
|
|
98
|
+
const conv = createConversation({ id: "c1" });
|
|
99
|
+
const originalUpdatedAt = conv.updated_at;
|
|
100
|
+
touchConversation("c1");
|
|
101
|
+
const touched = getConversation("c1");
|
|
102
|
+
expect(touched.updated_at).toBeGreaterThanOrEqual(originalUpdatedAt);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
describe("liner notes", () => {
|
|
106
|
+
it("returns null when no notes set", () => {
|
|
107
|
+
createConversation({ id: "c1" });
|
|
108
|
+
expect(getLinerNotes("c1")).toBeNull();
|
|
109
|
+
});
|
|
110
|
+
it("sets and gets liner notes", () => {
|
|
111
|
+
createConversation({ id: "c1" });
|
|
112
|
+
updateLinerNotes("c1", "# My Notes\nSome content");
|
|
113
|
+
expect(getLinerNotes("c1")).toBe("# My Notes\nSome content");
|
|
114
|
+
});
|
|
115
|
+
it("clears liner notes with null", () => {
|
|
116
|
+
createConversation({ id: "c1" });
|
|
117
|
+
updateLinerNotes("c1", "notes");
|
|
118
|
+
updateLinerNotes("c1", null);
|
|
119
|
+
expect(getLinerNotes("c1")).toBeNull();
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
describe("countConversations", () => {
|
|
123
|
+
it("returns 0 when empty", () => {
|
|
124
|
+
expect(countConversations()).toBe(0);
|
|
125
|
+
});
|
|
126
|
+
it("returns correct count after inserts", () => {
|
|
127
|
+
createConversation({ id: "c1" });
|
|
128
|
+
createConversation({ id: "c2" });
|
|
129
|
+
expect(countConversations()).toBe(2);
|
|
130
|
+
});
|
|
131
|
+
it("decrements after delete", () => {
|
|
132
|
+
createConversation({ id: "c1" });
|
|
133
|
+
createConversation({ id: "c2" });
|
|
134
|
+
deleteConversation("c1");
|
|
135
|
+
expect(countConversations()).toBe(1);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { getDb } from "../index.js";
|
|
2
|
+
function createJob(input) {
|
|
3
|
+
const db = getDb();
|
|
4
|
+
const now = Date.now();
|
|
5
|
+
db.prepare(`
|
|
6
|
+
INSERT INTO jobs (id, conversation_id, prompt, status, source, created_at, updated_at)
|
|
7
|
+
VALUES (?, ?, ?, 'queued', ?, ?, ?)
|
|
8
|
+
`).run(input.id, input.conversationId, input.prompt, input.source || "web", now, now);
|
|
9
|
+
return getJob(input.id);
|
|
10
|
+
}
|
|
11
|
+
function getJob(id) {
|
|
12
|
+
const db = getDb();
|
|
13
|
+
const row = db.prepare(`
|
|
14
|
+
SELECT id, conversation_id, prompt, status, source, result, error, pid,
|
|
15
|
+
created_at, updated_at, started_at, completed_at
|
|
16
|
+
FROM jobs WHERE id = ?
|
|
17
|
+
`).get(id);
|
|
18
|
+
return row || null;
|
|
19
|
+
}
|
|
20
|
+
function listJobs(filters, limit = 50) {
|
|
21
|
+
const db = getDb();
|
|
22
|
+
const conditions = [];
|
|
23
|
+
const params = [];
|
|
24
|
+
if (filters?.status) {
|
|
25
|
+
conditions.push("status = ?");
|
|
26
|
+
params.push(filters.status);
|
|
27
|
+
}
|
|
28
|
+
if (filters?.conversationId) {
|
|
29
|
+
conditions.push("conversation_id = ?");
|
|
30
|
+
params.push(filters.conversationId);
|
|
31
|
+
}
|
|
32
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
33
|
+
params.push(limit);
|
|
34
|
+
return db.prepare(`
|
|
35
|
+
SELECT id, conversation_id, prompt, status, source, result, error, pid,
|
|
36
|
+
created_at, updated_at, started_at, completed_at
|
|
37
|
+
FROM jobs ${where}
|
|
38
|
+
ORDER BY created_at DESC
|
|
39
|
+
LIMIT ?
|
|
40
|
+
`).all(...params);
|
|
41
|
+
}
|
|
42
|
+
function updateJob(id, input) {
|
|
43
|
+
const db = getDb();
|
|
44
|
+
const updates = ["updated_at = ?"];
|
|
45
|
+
const values = [Date.now()];
|
|
46
|
+
if (input.status !== void 0) {
|
|
47
|
+
updates.push("status = ?");
|
|
48
|
+
values.push(input.status);
|
|
49
|
+
}
|
|
50
|
+
if (input.result !== void 0) {
|
|
51
|
+
updates.push("result = ?");
|
|
52
|
+
values.push(input.result);
|
|
53
|
+
}
|
|
54
|
+
if (input.error !== void 0) {
|
|
55
|
+
updates.push("error = ?");
|
|
56
|
+
values.push(input.error);
|
|
57
|
+
}
|
|
58
|
+
if (input.pid !== void 0) {
|
|
59
|
+
updates.push("pid = ?");
|
|
60
|
+
values.push(input.pid);
|
|
61
|
+
}
|
|
62
|
+
if (input.started_at !== void 0) {
|
|
63
|
+
updates.push("started_at = ?");
|
|
64
|
+
values.push(input.started_at);
|
|
65
|
+
}
|
|
66
|
+
if (input.completed_at !== void 0) {
|
|
67
|
+
updates.push("completed_at = ?");
|
|
68
|
+
values.push(input.completed_at);
|
|
69
|
+
}
|
|
70
|
+
values.push(id);
|
|
71
|
+
db.prepare(`UPDATE jobs SET ${updates.join(", ")} WHERE id = ?`).run(...values);
|
|
72
|
+
return getJob(id);
|
|
73
|
+
}
|
|
74
|
+
function createJobEvent(input) {
|
|
75
|
+
const db = getDb();
|
|
76
|
+
const timestamp = Date.now();
|
|
77
|
+
const result = db.prepare(`
|
|
78
|
+
INSERT INTO job_events (job_id, event_type, data, timestamp)
|
|
79
|
+
VALUES (?, ?, ?, ?)
|
|
80
|
+
`).run(input.jobId, input.eventType, input.data || null, timestamp);
|
|
81
|
+
return {
|
|
82
|
+
id: Number(result.lastInsertRowid),
|
|
83
|
+
job_id: input.jobId,
|
|
84
|
+
event_type: input.eventType,
|
|
85
|
+
data: input.data || null,
|
|
86
|
+
timestamp
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
function getJobEvents(jobId, since) {
|
|
90
|
+
const db = getDb();
|
|
91
|
+
if (since) {
|
|
92
|
+
return db.prepare(`
|
|
93
|
+
SELECT id, job_id, event_type, data, timestamp
|
|
94
|
+
FROM job_events
|
|
95
|
+
WHERE job_id = ? AND timestamp > ?
|
|
96
|
+
ORDER BY timestamp ASC
|
|
97
|
+
`).all(jobId, since);
|
|
98
|
+
}
|
|
99
|
+
return db.prepare(`
|
|
100
|
+
SELECT id, job_id, event_type, data, timestamp
|
|
101
|
+
FROM job_events
|
|
102
|
+
WHERE job_id = ?
|
|
103
|
+
ORDER BY timestamp ASC
|
|
104
|
+
`).all(jobId);
|
|
105
|
+
}
|
|
106
|
+
function cleanupStaleJobs() {
|
|
107
|
+
const db = getDb();
|
|
108
|
+
const result = db.prepare(`
|
|
109
|
+
UPDATE jobs SET status = 'failed', error = 'Server restarted', updated_at = ?, completed_at = ?
|
|
110
|
+
WHERE status IN ('queued', 'running')
|
|
111
|
+
`).run(Date.now(), Date.now());
|
|
112
|
+
return result.changes;
|
|
113
|
+
}
|
|
114
|
+
function deleteJob(id) {
|
|
115
|
+
const db = getDb();
|
|
116
|
+
const result = db.prepare("DELETE FROM jobs WHERE id = ?").run(id);
|
|
117
|
+
return result.changes > 0;
|
|
118
|
+
}
|
|
119
|
+
export {
|
|
120
|
+
cleanupStaleJobs,
|
|
121
|
+
createJob,
|
|
122
|
+
createJobEvent,
|
|
123
|
+
deleteJob,
|
|
124
|
+
getJob,
|
|
125
|
+
getJobEvents,
|
|
126
|
+
listJobs,
|
|
127
|
+
updateJob
|
|
128
|
+
};
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { getDb } from "../index.js";
|
|
2
|
+
import { touchConversation } from "./conversations.js";
|
|
3
|
+
function getMessagesForConversation(conversationId) {
|
|
4
|
+
const db = getDb();
|
|
5
|
+
return db.prepare(`
|
|
6
|
+
SELECT id, conversation_id, role, content, timestamp, position, source
|
|
7
|
+
FROM messages
|
|
8
|
+
WHERE conversation_id = ?
|
|
9
|
+
ORDER BY position ASC
|
|
10
|
+
`).all(conversationId);
|
|
11
|
+
}
|
|
12
|
+
function getImagesForMessage(messageId) {
|
|
13
|
+
const db = getDb();
|
|
14
|
+
return db.prepare(`
|
|
15
|
+
SELECT id, message_id, data_url, file_name, description, position
|
|
16
|
+
FROM message_images
|
|
17
|
+
WHERE message_id = ?
|
|
18
|
+
ORDER BY position ASC
|
|
19
|
+
`).all(messageId);
|
|
20
|
+
}
|
|
21
|
+
function getImagesForMessages(messageIds) {
|
|
22
|
+
if (messageIds.length === 0) return /* @__PURE__ */ new Map();
|
|
23
|
+
const db = getDb();
|
|
24
|
+
const placeholders = messageIds.map(() => "?").join(", ");
|
|
25
|
+
const rows = db.prepare(`
|
|
26
|
+
SELECT id, message_id, data_url, file_name, description, position
|
|
27
|
+
FROM message_images
|
|
28
|
+
WHERE message_id IN (${placeholders})
|
|
29
|
+
ORDER BY position ASC
|
|
30
|
+
`).all(...messageIds);
|
|
31
|
+
const imageMap = /* @__PURE__ */ new Map();
|
|
32
|
+
for (const row of rows) {
|
|
33
|
+
if (!imageMap.has(row.message_id)) {
|
|
34
|
+
imageMap.set(row.message_id, []);
|
|
35
|
+
}
|
|
36
|
+
imageMap.get(row.message_id).push(row);
|
|
37
|
+
}
|
|
38
|
+
return imageMap;
|
|
39
|
+
}
|
|
40
|
+
function createMessage(input) {
|
|
41
|
+
const db = getDb();
|
|
42
|
+
const timestamp = input.timestamp || Date.now();
|
|
43
|
+
const source = input.source || "web";
|
|
44
|
+
const posRow = db.prepare(`
|
|
45
|
+
SELECT COALESCE(MAX(position), -1) + 1 as next_pos
|
|
46
|
+
FROM messages
|
|
47
|
+
WHERE conversation_id = ?
|
|
48
|
+
`).get(input.conversationId);
|
|
49
|
+
const position = posRow.next_pos;
|
|
50
|
+
db.prepare(`
|
|
51
|
+
INSERT INTO messages (id, conversation_id, role, content, timestamp, position, source)
|
|
52
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
53
|
+
`).run(input.id, input.conversationId, input.role, input.content, timestamp, position, source);
|
|
54
|
+
if (input.images && input.images.length > 0) {
|
|
55
|
+
const insertImage = db.prepare(`
|
|
56
|
+
INSERT INTO message_images (id, message_id, data_url, file_name, description, position)
|
|
57
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
58
|
+
`);
|
|
59
|
+
for (let i = 0; i < input.images.length; i++) {
|
|
60
|
+
const img = input.images[i];
|
|
61
|
+
insertImage.run(img.id, input.id, img.dataUrl, img.fileName, img.description || null, i);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
touchConversation(input.conversationId);
|
|
65
|
+
return {
|
|
66
|
+
id: input.id,
|
|
67
|
+
conversation_id: input.conversationId,
|
|
68
|
+
role: input.role,
|
|
69
|
+
content: input.content,
|
|
70
|
+
timestamp,
|
|
71
|
+
position,
|
|
72
|
+
source
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
function updateImageDescription(imageId, description) {
|
|
76
|
+
const db = getDb();
|
|
77
|
+
const result = db.prepare("UPDATE message_images SET description = ? WHERE id = ?").run(description, imageId);
|
|
78
|
+
return result.changes > 0;
|
|
79
|
+
}
|
|
80
|
+
function deleteMessage(id) {
|
|
81
|
+
const db = getDb();
|
|
82
|
+
const result = db.prepare("DELETE FROM messages WHERE id = ?").run(id);
|
|
83
|
+
return result.changes > 0;
|
|
84
|
+
}
|
|
85
|
+
function countMessagesInConversation(conversationId) {
|
|
86
|
+
const db = getDb();
|
|
87
|
+
const row = db.prepare("SELECT COUNT(*) as count FROM messages WHERE conversation_id = ?").get(conversationId);
|
|
88
|
+
return row.count;
|
|
89
|
+
}
|
|
90
|
+
export {
|
|
91
|
+
countMessagesInConversation,
|
|
92
|
+
createMessage,
|
|
93
|
+
deleteMessage,
|
|
94
|
+
getImagesForMessage,
|
|
95
|
+
getImagesForMessages,
|
|
96
|
+
getMessagesForConversation,
|
|
97
|
+
updateImageDescription
|
|
98
|
+
};
|