botschat 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.
Files changed (88) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +213 -0
  3. package/migrations/0001_initial.sql +88 -0
  4. package/migrations/0002_rename_projects_to_channels.sql +53 -0
  5. package/migrations/0003_messages.sql +14 -0
  6. package/migrations/0004_jobs.sql +15 -0
  7. package/migrations/0005_deleted_cron_jobs.sql +6 -0
  8. package/migrations/0006_tasks_add_model.sql +2 -0
  9. package/migrations/0007_sessions.sql +25 -0
  10. package/migrations/0008_remove_openclaw_fields.sql +8 -0
  11. package/package.json +53 -0
  12. package/packages/api/package.json +17 -0
  13. package/packages/api/src/do/connection-do.ts +929 -0
  14. package/packages/api/src/env.ts +8 -0
  15. package/packages/api/src/index.ts +297 -0
  16. package/packages/api/src/routes/agents.ts +68 -0
  17. package/packages/api/src/routes/auth.ts +105 -0
  18. package/packages/api/src/routes/channels.ts +185 -0
  19. package/packages/api/src/routes/jobs.ts +65 -0
  20. package/packages/api/src/routes/models.ts +22 -0
  21. package/packages/api/src/routes/pairing.ts +76 -0
  22. package/packages/api/src/routes/projects.ts +177 -0
  23. package/packages/api/src/routes/sessions.ts +171 -0
  24. package/packages/api/src/routes/tasks.ts +375 -0
  25. package/packages/api/src/routes/upload.ts +52 -0
  26. package/packages/api/src/utils/auth.ts +101 -0
  27. package/packages/api/src/utils/id.ts +19 -0
  28. package/packages/api/tsconfig.json +18 -0
  29. package/packages/plugin/dist/index.d.ts +19 -0
  30. package/packages/plugin/dist/index.d.ts.map +1 -0
  31. package/packages/plugin/dist/index.js +17 -0
  32. package/packages/plugin/dist/index.js.map +1 -0
  33. package/packages/plugin/dist/src/accounts.d.ts +12 -0
  34. package/packages/plugin/dist/src/accounts.d.ts.map +1 -0
  35. package/packages/plugin/dist/src/accounts.js +103 -0
  36. package/packages/plugin/dist/src/accounts.js.map +1 -0
  37. package/packages/plugin/dist/src/channel.d.ts +206 -0
  38. package/packages/plugin/dist/src/channel.d.ts.map +1 -0
  39. package/packages/plugin/dist/src/channel.js +1248 -0
  40. package/packages/plugin/dist/src/channel.js.map +1 -0
  41. package/packages/plugin/dist/src/runtime.d.ts +3 -0
  42. package/packages/plugin/dist/src/runtime.d.ts.map +1 -0
  43. package/packages/plugin/dist/src/runtime.js +18 -0
  44. package/packages/plugin/dist/src/runtime.js.map +1 -0
  45. package/packages/plugin/dist/src/types.d.ts +179 -0
  46. package/packages/plugin/dist/src/types.d.ts.map +1 -0
  47. package/packages/plugin/dist/src/types.js +6 -0
  48. package/packages/plugin/dist/src/types.js.map +1 -0
  49. package/packages/plugin/dist/src/ws-client.d.ts +51 -0
  50. package/packages/plugin/dist/src/ws-client.d.ts.map +1 -0
  51. package/packages/plugin/dist/src/ws-client.js +170 -0
  52. package/packages/plugin/dist/src/ws-client.js.map +1 -0
  53. package/packages/plugin/openclaw.plugin.json +11 -0
  54. package/packages/plugin/package.json +39 -0
  55. package/packages/plugin/tsconfig.json +20 -0
  56. package/packages/web/dist/assets/index-C-wI8eHy.css +1 -0
  57. package/packages/web/dist/assets/index-CbPEKHLG.js +93 -0
  58. package/packages/web/dist/index.html +17 -0
  59. package/packages/web/index.html +16 -0
  60. package/packages/web/package.json +29 -0
  61. package/packages/web/postcss.config.js +6 -0
  62. package/packages/web/src/App.tsx +827 -0
  63. package/packages/web/src/api.ts +242 -0
  64. package/packages/web/src/components/ChatWindow.tsx +864 -0
  65. package/packages/web/src/components/CronDetail.tsx +943 -0
  66. package/packages/web/src/components/CronSidebar.tsx +123 -0
  67. package/packages/web/src/components/DebugLogPanel.tsx +258 -0
  68. package/packages/web/src/components/IconRail.tsx +163 -0
  69. package/packages/web/src/components/JobList.tsx +120 -0
  70. package/packages/web/src/components/LoginPage.tsx +178 -0
  71. package/packages/web/src/components/MessageContent.tsx +1082 -0
  72. package/packages/web/src/components/ModelSelect.tsx +87 -0
  73. package/packages/web/src/components/ScheduleEditor.tsx +403 -0
  74. package/packages/web/src/components/SessionTabs.tsx +246 -0
  75. package/packages/web/src/components/Sidebar.tsx +331 -0
  76. package/packages/web/src/components/TaskBar.tsx +413 -0
  77. package/packages/web/src/components/ThreadPanel.tsx +212 -0
  78. package/packages/web/src/debug-log.ts +58 -0
  79. package/packages/web/src/index.css +170 -0
  80. package/packages/web/src/main.tsx +10 -0
  81. package/packages/web/src/store.ts +492 -0
  82. package/packages/web/src/ws.ts +99 -0
  83. package/packages/web/tailwind.config.js +65 -0
  84. package/packages/web/tsconfig.json +18 -0
  85. package/packages/web/vite.config.ts +20 -0
  86. package/scripts/dev.sh +122 -0
  87. package/tsconfig.json +18 -0
  88. package/wrangler.toml +40 -0
@@ -0,0 +1,65 @@
1
+ import { Hono } from "hono";
2
+ import type { Env } from "../env.js";
3
+
4
+ /**
5
+ * Jobs API — background task execution history.
6
+ * Mounted at /api/channels/:channelId/tasks/:taskId/jobs
7
+ */
8
+ const jobs = new Hono<{ Bindings: Env; Variables: { userId: string } }>();
9
+
10
+ /** GET / — list jobs for a background task */
11
+ jobs.get("/", async (c) => {
12
+ const userId = c.get("userId");
13
+ const channelId = c.req.param("channelId");
14
+ const taskId = c.req.param("taskId");
15
+ const limit = Math.min(Number(c.req.query("limit") ?? 50), 200);
16
+
17
+ // Verify ownership chain
18
+ const channel = await c.env.DB.prepare(
19
+ "SELECT id FROM channels WHERE id = ? AND user_id = ?",
20
+ )
21
+ .bind(channelId, userId)
22
+ .first();
23
+ if (!channel) return c.json({ error: "Channel not found" }, 404);
24
+
25
+ const task = await c.env.DB.prepare(
26
+ "SELECT id, kind FROM tasks WHERE id = ? AND channel_id = ?",
27
+ )
28
+ .bind(taskId, channelId)
29
+ .first<{ id: string; kind: string }>();
30
+ if (!task) return c.json({ error: "Task not found" }, 404);
31
+ if (task.kind !== "background") return c.json({ error: "Only background tasks have jobs" }, 400);
32
+
33
+ const { results } = await c.env.DB.prepare(
34
+ `SELECT id, session_key, status, started_at, finished_at, duration_ms, summary, created_at
35
+ FROM jobs WHERE task_id = ? AND user_id = ?
36
+ ORDER BY started_at DESC LIMIT ?`,
37
+ )
38
+ .bind(taskId, userId, limit)
39
+ .all<{
40
+ id: string;
41
+ session_key: string;
42
+ status: string;
43
+ started_at: number;
44
+ finished_at: number | null;
45
+ duration_ms: number | null;
46
+ summary: string;
47
+ created_at: number;
48
+ }>();
49
+
50
+ return c.json({
51
+ jobs: (results ?? []).map((r, idx, arr) => ({
52
+ id: r.id,
53
+ number: arr.length - idx,
54
+ sessionKey: r.session_key,
55
+ status: r.status,
56
+ startedAt: r.started_at,
57
+ finishedAt: r.finished_at,
58
+ durationMs: r.duration_ms,
59
+ summary: r.summary,
60
+ time: new Date(r.started_at * 1000).toLocaleString(),
61
+ })),
62
+ });
63
+ });
64
+
65
+ export { jobs };
@@ -0,0 +1,22 @@
1
+ import { Hono } from "hono";
2
+ import type { Env } from "../env.js";
3
+
4
+ export type ModelInfo = {
5
+ id: string;
6
+ name: string;
7
+ provider: string;
8
+ };
9
+
10
+ const models = new Hono<{ Bindings: Env; Variables: { userId: string } }>();
11
+
12
+ /** GET /api/models — list available models (fetched from OpenClaw plugin via DO) */
13
+ models.get("/", async (c) => {
14
+ const userId = c.get("userId");
15
+ const doId = c.env.CONNECTION_DO.idFromName(userId);
16
+ const stub = c.env.CONNECTION_DO.get(doId);
17
+ const res = await stub.fetch(new Request("https://internal/models"));
18
+ const data = await res.json<{ models: ModelInfo[] }>();
19
+ return c.json(data);
20
+ });
21
+
22
+ export { models };
@@ -0,0 +1,76 @@
1
+ import { Hono } from "hono";
2
+ import type { Env } from "../env.js";
3
+ import { generateId, generatePairingToken } from "../utils/id.js";
4
+
5
+ const pairing = new Hono<{ Bindings: Env; Variables: { userId: string } }>();
6
+
7
+ /** GET /api/pairing-tokens — list pairing tokens for the current user */
8
+ pairing.get("/", async (c) => {
9
+ const userId = c.get("userId");
10
+
11
+ const { results } = await c.env.DB.prepare(
12
+ "SELECT id, token, label, last_connected_at, created_at FROM pairing_tokens WHERE user_id = ? ORDER BY created_at DESC",
13
+ )
14
+ .bind(userId)
15
+ .all<{
16
+ id: string;
17
+ token: string;
18
+ label: string | null;
19
+ last_connected_at: number | null;
20
+ created_at: number;
21
+ }>();
22
+
23
+ return c.json({
24
+ tokens: (results ?? []).map((r) => ({
25
+ id: r.id,
26
+ // Show only last 8 chars for security
27
+ token: r.token,
28
+ tokenPreview: `bc_pat_...${r.token.slice(-8)}`,
29
+ label: r.label,
30
+ lastConnectedAt: r.last_connected_at,
31
+ createdAt: r.created_at,
32
+ })),
33
+ });
34
+ });
35
+
36
+ /** POST /api/pairing-tokens — generate a new pairing token */
37
+ pairing.post("/", async (c) => {
38
+ const userId = c.get("userId");
39
+ const { label } = await c.req.json<{ label?: string }>().catch(() => ({
40
+ label: undefined,
41
+ }));
42
+
43
+ const id = generateId("pt_");
44
+ const token = generatePairingToken();
45
+
46
+ await c.env.DB.prepare(
47
+ "INSERT INTO pairing_tokens (id, user_id, token, label) VALUES (?, ?, ?, ?)",
48
+ )
49
+ .bind(id, userId, token, label?.trim() ?? null)
50
+ .run();
51
+
52
+ return c.json(
53
+ {
54
+ id,
55
+ token, // Show full token only on creation
56
+ label: label?.trim() ?? null,
57
+ },
58
+ 201,
59
+ );
60
+ });
61
+
62
+ /** DELETE /api/pairing-tokens/:id — revoke a pairing token */
63
+ pairing.delete("/:id", async (c) => {
64
+ const userId = c.get("userId");
65
+ const tokenId = c.req.param("id");
66
+
67
+ await c.env.DB.prepare(
68
+ "DELETE FROM pairing_tokens WHERE id = ? AND user_id = ?",
69
+ )
70
+ .bind(tokenId, userId)
71
+ .run();
72
+
73
+ return c.json({ ok: true });
74
+ });
75
+
76
+ export { pairing };
@@ -0,0 +1,177 @@
1
+ import { Hono } from "hono";
2
+ import type { Env } from "../env.js";
3
+ import { generateId } from "../utils/id.js";
4
+
5
+ const projects = new Hono<{ Bindings: Env; Variables: { userId: string } }>();
6
+
7
+ /** GET /api/projects — list all projects for the current user */
8
+ projects.get("/", async (c) => {
9
+ const userId = c.get("userId");
10
+
11
+ const { results } = await c.env.DB.prepare(
12
+ "SELECT id, name, description, openclaw_agent_id, system_prompt, created_at, updated_at FROM projects WHERE user_id = ? ORDER BY created_at DESC",
13
+ )
14
+ .bind(userId)
15
+ .all<{
16
+ id: string;
17
+ name: string;
18
+ description: string;
19
+ openclaw_agent_id: string;
20
+ system_prompt: string;
21
+ created_at: number;
22
+ updated_at: number;
23
+ }>();
24
+
25
+ return c.json({
26
+ projects: (results ?? []).map((r) => ({
27
+ id: r.id,
28
+ name: r.name,
29
+ description: r.description,
30
+ openclawAgentId: r.openclaw_agent_id,
31
+ systemPrompt: r.system_prompt,
32
+ createdAt: r.created_at,
33
+ updatedAt: r.updated_at,
34
+ })),
35
+ });
36
+ });
37
+
38
+ /** POST /api/projects — create a new project */
39
+ projects.post("/", async (c) => {
40
+ const userId = c.get("userId");
41
+ const { name, description, openclawAgentId, systemPrompt } = await c.req.json<{
42
+ name: string;
43
+ description?: string;
44
+ openclawAgentId?: string;
45
+ systemPrompt?: string;
46
+ }>();
47
+
48
+ if (!name?.trim()) {
49
+ return c.json({ error: "Project name is required" }, 400);
50
+ }
51
+
52
+ const id = generateId("prj_");
53
+ // Default agent ID derived from project name (slug)
54
+ const agentId =
55
+ openclawAgentId?.trim() ||
56
+ name
57
+ .trim()
58
+ .toLowerCase()
59
+ .replace(/[^a-z0-9]+/g, "-")
60
+ .replace(/^-|-$/g, "");
61
+
62
+ await c.env.DB.prepare(
63
+ "INSERT INTO projects (id, user_id, name, description, openclaw_agent_id, system_prompt) VALUES (?, ?, ?, ?, ?, ?)",
64
+ )
65
+ .bind(id, userId, name.trim(), description?.trim() ?? "", agentId, systemPrompt?.trim() ?? "")
66
+ .run();
67
+
68
+ // Auto-create a default "Ad Hoc Chat" task
69
+ const taskId = generateId("tsk_");
70
+ const sessionKey = `agent:${agentId}:botschat:${userId}:adhoc`;
71
+ await c.env.DB.prepare(
72
+ "INSERT INTO tasks (id, project_id, name, kind, session_key) VALUES (?, ?, ?, ?, ?)",
73
+ )
74
+ .bind(taskId, id, "Ad Hoc Chat", "adhoc", sessionKey)
75
+ .run();
76
+
77
+ return c.json(
78
+ {
79
+ id,
80
+ name: name.trim(),
81
+ description: description?.trim() ?? "",
82
+ openclawAgentId: agentId,
83
+ systemPrompt: systemPrompt?.trim() ?? "",
84
+ },
85
+ 201,
86
+ );
87
+ });
88
+
89
+ /** GET /api/projects/:id — get a single project */
90
+ projects.get("/:id", async (c) => {
91
+ const userId = c.get("userId");
92
+ const projectId = c.req.param("id");
93
+
94
+ const row = await c.env.DB.prepare(
95
+ "SELECT id, name, description, openclaw_agent_id, system_prompt, created_at, updated_at FROM projects WHERE id = ? AND user_id = ?",
96
+ )
97
+ .bind(projectId, userId)
98
+ .first<{
99
+ id: string;
100
+ name: string;
101
+ description: string;
102
+ openclaw_agent_id: string;
103
+ system_prompt: string;
104
+ created_at: number;
105
+ updated_at: number;
106
+ }>();
107
+
108
+ if (!row) return c.json({ error: "Project not found" }, 404);
109
+
110
+ return c.json({
111
+ id: row.id,
112
+ name: row.name,
113
+ description: row.description,
114
+ openclawAgentId: row.openclaw_agent_id,
115
+ systemPrompt: row.system_prompt,
116
+ createdAt: row.created_at,
117
+ updatedAt: row.updated_at,
118
+ });
119
+ });
120
+
121
+ /** PATCH /api/projects/:id — update a project */
122
+ projects.patch("/:id", async (c) => {
123
+ const userId = c.get("userId");
124
+ const projectId = c.req.param("id");
125
+ const body = await c.req.json<{
126
+ name?: string;
127
+ description?: string;
128
+ systemPrompt?: string;
129
+ }>();
130
+
131
+ const sets: string[] = [];
132
+ const values: unknown[] = [];
133
+
134
+ if (body.name !== undefined) {
135
+ sets.push("name = ?");
136
+ values.push(body.name.trim());
137
+ }
138
+ if (body.description !== undefined) {
139
+ sets.push("description = ?");
140
+ values.push(body.description.trim());
141
+ }
142
+ if (body.systemPrompt !== undefined) {
143
+ sets.push("system_prompt = ?");
144
+ values.push(body.systemPrompt.trim());
145
+ }
146
+
147
+ if (sets.length === 0) {
148
+ return c.json({ error: "No fields to update" }, 400);
149
+ }
150
+
151
+ sets.push("updated_at = unixepoch()");
152
+ values.push(projectId, userId);
153
+
154
+ await c.env.DB.prepare(
155
+ `UPDATE projects SET ${sets.join(", ")} WHERE id = ? AND user_id = ?`,
156
+ )
157
+ .bind(...values)
158
+ .run();
159
+
160
+ return c.json({ ok: true });
161
+ });
162
+
163
+ /** DELETE /api/projects/:id — delete a project */
164
+ projects.delete("/:id", async (c) => {
165
+ const userId = c.get("userId");
166
+ const projectId = c.req.param("id");
167
+
168
+ await c.env.DB.prepare(
169
+ "DELETE FROM projects WHERE id = ? AND user_id = ?",
170
+ )
171
+ .bind(projectId, userId)
172
+ .run();
173
+
174
+ return c.json({ ok: true });
175
+ });
176
+
177
+ export { projects };
@@ -0,0 +1,171 @@
1
+ import { Hono } from "hono";
2
+ import type { Env } from "../env.js";
3
+ import { generateId } from "../utils/id.js";
4
+
5
+ const sessions = new Hono<{ Bindings: Env; Variables: { userId: string } }>();
6
+
7
+ /** GET /api/channels/:channelId/sessions — list all sessions for a channel */
8
+ sessions.get("/", async (c) => {
9
+ const userId = c.get("userId");
10
+ const channelId = c.req.param("channelId");
11
+
12
+ // Verify channel ownership
13
+ const channel = await c.env.DB.prepare(
14
+ "SELECT id, openclaw_agent_id FROM channels WHERE id = ? AND user_id = ?",
15
+ )
16
+ .bind(channelId, userId)
17
+ .first<{ id: string; openclaw_agent_id: string }>();
18
+
19
+ if (!channel) return c.json({ error: "Channel not found" }, 404);
20
+
21
+ const { results } = await c.env.DB.prepare(
22
+ "SELECT id, name, session_key, created_at, updated_at FROM sessions WHERE channel_id = ? AND user_id = ? ORDER BY created_at ASC",
23
+ )
24
+ .bind(channelId, userId)
25
+ .all<{
26
+ id: string;
27
+ name: string;
28
+ session_key: string;
29
+ created_at: number;
30
+ updated_at: number;
31
+ }>();
32
+
33
+ return c.json({
34
+ sessions: (results ?? []).map((r) => ({
35
+ id: r.id,
36
+ name: r.name,
37
+ sessionKey: r.session_key,
38
+ createdAt: r.created_at,
39
+ updatedAt: r.updated_at,
40
+ })),
41
+ });
42
+ });
43
+
44
+ /** POST /api/channels/:channelId/sessions — create a new session */
45
+ sessions.post("/", async (c) => {
46
+ const userId = c.get("userId");
47
+ const channelId = c.req.param("channelId");
48
+
49
+ // Verify channel ownership
50
+ const channel = await c.env.DB.prepare(
51
+ "SELECT id, openclaw_agent_id FROM channels WHERE id = ? AND user_id = ?",
52
+ )
53
+ .bind(channelId, userId)
54
+ .first<{ id: string; openclaw_agent_id: string }>();
55
+
56
+ if (!channel) return c.json({ error: "Channel not found" }, 404);
57
+
58
+ const { name } = await c.req.json<{ name?: string }>();
59
+
60
+ // Count existing sessions for auto-naming
61
+ const countRow = await c.env.DB.prepare(
62
+ "SELECT COUNT(*) as cnt FROM sessions WHERE channel_id = ? AND user_id = ?",
63
+ )
64
+ .bind(channelId, userId)
65
+ .first<{ cnt: number }>();
66
+ const count = countRow?.cnt ?? 0;
67
+
68
+ const sessionName = name?.trim() || `Session ${count + 1}`;
69
+ const id = generateId("ses_");
70
+ const sessionKey = `agent:${channel.openclaw_agent_id}:botschat:${userId}:ses:${id}`;
71
+
72
+ await c.env.DB.prepare(
73
+ "INSERT INTO sessions (id, channel_id, user_id, name, session_key) VALUES (?, ?, ?, ?, ?)",
74
+ )
75
+ .bind(id, channelId, userId, sessionName, sessionKey)
76
+ .run();
77
+
78
+ return c.json(
79
+ {
80
+ id,
81
+ name: sessionName,
82
+ sessionKey,
83
+ createdAt: Math.floor(Date.now() / 1000),
84
+ updatedAt: Math.floor(Date.now() / 1000),
85
+ },
86
+ 201,
87
+ );
88
+ });
89
+
90
+ /** PATCH /api/channels/:channelId/sessions/:sessionId — rename a session */
91
+ sessions.patch("/:sessionId", async (c) => {
92
+ const userId = c.get("userId");
93
+ const channelId = c.req.param("channelId");
94
+ const sessionId = c.req.param("sessionId");
95
+
96
+ // Verify channel ownership
97
+ const channel = await c.env.DB.prepare(
98
+ "SELECT id FROM channels WHERE id = ? AND user_id = ?",
99
+ )
100
+ .bind(channelId, userId)
101
+ .first();
102
+
103
+ if (!channel) return c.json({ error: "Channel not found" }, 404);
104
+
105
+ const { name } = await c.req.json<{ name: string }>();
106
+
107
+ if (!name?.trim()) {
108
+ return c.json({ error: "Session name is required" }, 400);
109
+ }
110
+
111
+ await c.env.DB.prepare(
112
+ "UPDATE sessions SET name = ?, updated_at = unixepoch() WHERE id = ? AND channel_id = ? AND user_id = ?",
113
+ )
114
+ .bind(name.trim(), sessionId, channelId, userId)
115
+ .run();
116
+
117
+ return c.json({ ok: true });
118
+ });
119
+
120
+ /** DELETE /api/channels/:channelId/sessions/:sessionId — delete a session */
121
+ sessions.delete("/:sessionId", async (c) => {
122
+ const userId = c.get("userId");
123
+ const channelId = c.req.param("channelId");
124
+ const sessionId = c.req.param("sessionId");
125
+
126
+ // Verify channel ownership
127
+ const channel = await c.env.DB.prepare(
128
+ "SELECT id FROM channels WHERE id = ? AND user_id = ?",
129
+ )
130
+ .bind(channelId, userId)
131
+ .first();
132
+
133
+ if (!channel) return c.json({ error: "Channel not found" }, 404);
134
+
135
+ // Don't allow deleting the last session
136
+ const countRow = await c.env.DB.prepare(
137
+ "SELECT COUNT(*) as cnt FROM sessions WHERE channel_id = ? AND user_id = ?",
138
+ )
139
+ .bind(channelId, userId)
140
+ .first<{ cnt: number }>();
141
+
142
+ if ((countRow?.cnt ?? 0) <= 1) {
143
+ return c.json({ error: "Cannot delete the last session" }, 400);
144
+ }
145
+
146
+ // Get session_key before deleting (to clean up messages)
147
+ const session = await c.env.DB.prepare(
148
+ "SELECT session_key FROM sessions WHERE id = ? AND channel_id = ? AND user_id = ?",
149
+ )
150
+ .bind(sessionId, channelId, userId)
151
+ .first<{ session_key: string }>();
152
+
153
+ await c.env.DB.prepare(
154
+ "DELETE FROM sessions WHERE id = ? AND channel_id = ? AND user_id = ?",
155
+ )
156
+ .bind(sessionId, channelId, userId)
157
+ .run();
158
+
159
+ // Clean up messages for this session
160
+ if (session?.session_key) {
161
+ await c.env.DB.prepare(
162
+ "DELETE FROM messages WHERE session_key = ?",
163
+ )
164
+ .bind(session.session_key)
165
+ .run();
166
+ }
167
+
168
+ return c.json({ ok: true });
169
+ });
170
+
171
+ export { sessions };