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,8 @@
1
+ /** Cloudflare Worker environment bindings */
2
+ export type Env = {
3
+ DB: D1Database;
4
+ MEDIA: R2Bucket;
5
+ CONNECTION_DO: DurableObjectNamespace;
6
+ ENVIRONMENT: string;
7
+ JWT_SECRET?: string;
8
+ };
@@ -0,0 +1,297 @@
1
+ import { Hono } from "hono";
2
+ import { cors } from "hono/cors";
3
+ import type { Env } from "./env.js";
4
+ import { authMiddleware } from "./utils/auth.js";
5
+ import { auth } from "./routes/auth.js";
6
+ import { agents } from "./routes/agents.js";
7
+ import { channels } from "./routes/channels.js";
8
+ import { tasks } from "./routes/tasks.js";
9
+ import { jobs } from "./routes/jobs.js";
10
+ import { models } from "./routes/models.js";
11
+ import { pairing } from "./routes/pairing.js";
12
+ import { sessions } from "./routes/sessions.js";
13
+ import { upload } from "./routes/upload.js";
14
+
15
+ // Re-export the Durable Object class so wrangler can find it
16
+ export { ConnectionDO } from "./do/connection-do.js";
17
+
18
+ const app = new Hono<{ Bindings: Env }>();
19
+
20
+ // Global CORS
21
+ app.use("/*", cors({ origin: "*", allowMethods: ["GET", "POST", "PATCH", "DELETE", "OPTIONS"] }));
22
+
23
+ // Health check
24
+ app.get("/api/health", (c) => c.json({ status: "ok", version: "0.1.0" }));
25
+
26
+ // ---- Public routes (no auth) ----
27
+ app.route("/api/auth", auth);
28
+
29
+ // ---- Protected routes (require Bearer token) ----
30
+ const protectedApp = new Hono<{ Bindings: Env; Variables: { userId: string } }>();
31
+ protectedApp.use("/*", authMiddleware());
32
+ protectedApp.route("/agents", agents);
33
+ protectedApp.route("/channels", channels);
34
+ protectedApp.route("/models", models);
35
+ protectedApp.get("/me", async (c) => {
36
+ // Proxy /api/me to the auth /me handler
37
+ const userId = c.get("userId");
38
+ const row = await c.env.DB.prepare(
39
+ "SELECT id, email, display_name, settings_json, created_at FROM users WHERE id = ?",
40
+ )
41
+ .bind(userId)
42
+ .first<{
43
+ id: string;
44
+ email: string;
45
+ display_name: string | null;
46
+ settings_json: string;
47
+ created_at: number;
48
+ }>();
49
+ if (!row) return c.json({ error: "User not found" }, 404);
50
+ return c.json({
51
+ id: row.id,
52
+ email: row.email,
53
+ displayName: row.display_name,
54
+ settings: JSON.parse(row.settings_json || "{}"),
55
+ createdAt: row.created_at,
56
+ });
57
+ });
58
+
59
+ protectedApp.patch("/me", async (c) => {
60
+ const userId = c.get("userId");
61
+ const body = await c.req.json<{ defaultModel?: string }>();
62
+
63
+ const existing = await c.env.DB.prepare(
64
+ "SELECT settings_json FROM users WHERE id = ?",
65
+ )
66
+ .bind(userId)
67
+ .first<{ settings_json: string }>();
68
+
69
+ const settings = JSON.parse(existing?.settings_json || "{}");
70
+
71
+ if (body.defaultModel !== undefined) {
72
+ settings.defaultModel = body.defaultModel;
73
+ }
74
+
75
+ await c.env.DB.prepare(
76
+ "UPDATE users SET settings_json = ? WHERE id = ?",
77
+ )
78
+ .bind(JSON.stringify(settings), userId)
79
+ .run();
80
+
81
+ return c.json({ ok: true, settings });
82
+ });
83
+
84
+ // OpenClaw scan data — schedule/instructions/model cached in the ConnectionDO.
85
+ // These fields belong to OpenClaw (not stored in D1) and are refreshed whenever
86
+ // the plugin sends a task.scan.result message.
87
+ protectedApp.get("/task-scan", async (c) => {
88
+ const userId = c.get("userId");
89
+ const doId = c.env.CONNECTION_DO.idFromName(userId);
90
+ const stub = c.env.CONNECTION_DO.get(doId);
91
+ const resp = await stub.fetch(new Request("https://internal/scan-data"));
92
+ return new Response(resp.body, { status: resp.status, headers: { "Content-Type": "application/json" } });
93
+ });
94
+
95
+ // Top-level task listing (for Automations view)
96
+ // Note: schedule, instructions, model are NOT stored in D1.
97
+ // They belong to OpenClaw and are retrieved via GET /api/task-scan.
98
+ protectedApp.get("/tasks", async (c) => {
99
+ const userId = c.get("userId");
100
+ const kind = c.req.query("kind") ?? "background";
101
+
102
+ const { results } = await c.env.DB.prepare(
103
+ `SELECT t.id, t.channel_id, t.name, t.kind, t.openclaw_cron_job_id,
104
+ t.session_key, t.enabled, t.created_at, t.updated_at
105
+ FROM tasks t
106
+ JOIN channels ch ON t.channel_id = ch.id
107
+ WHERE ch.user_id = ? AND t.kind = ?
108
+ ORDER BY t.created_at ASC`,
109
+ )
110
+ .bind(userId, kind)
111
+ .all<{
112
+ id: string;
113
+ channel_id: string;
114
+ name: string;
115
+ kind: string;
116
+ openclaw_cron_job_id: string | null;
117
+ session_key: string | null;
118
+ enabled: number;
119
+ created_at: number;
120
+ updated_at: number;
121
+ }>();
122
+
123
+ return c.json({
124
+ tasks: (results ?? []).map((r) => ({
125
+ id: r.id,
126
+ channelId: r.channel_id,
127
+ name: r.name,
128
+ kind: r.kind,
129
+ openclawCronJobId: r.openclaw_cron_job_id,
130
+ sessionKey: r.session_key,
131
+ enabled: !!r.enabled,
132
+ createdAt: r.created_at,
133
+ updatedAt: r.updated_at,
134
+ })),
135
+ });
136
+ });
137
+
138
+ // Top-level job listing for a task (for Automations view)
139
+ protectedApp.get("/tasks/:taskId/jobs", async (c) => {
140
+ const userId = c.get("userId");
141
+ const taskId = c.req.param("taskId");
142
+ const limit = Math.min(Number(c.req.query("limit") ?? 50), 200);
143
+
144
+ // Verify the task belongs to this user
145
+ const task = await c.env.DB.prepare(
146
+ `SELECT t.id, t.kind FROM tasks t
147
+ JOIN channels ch ON t.channel_id = ch.id
148
+ WHERE t.id = ? AND ch.user_id = ?`,
149
+ )
150
+ .bind(taskId, userId)
151
+ .first<{ id: string; kind: string }>();
152
+
153
+ if (!task) return c.json({ error: "Task not found" }, 404);
154
+ if (task.kind !== "background") return c.json({ error: "Only background tasks have jobs" }, 400);
155
+
156
+ const { results } = await c.env.DB.prepare(
157
+ `SELECT id, session_key, status, started_at, finished_at, duration_ms, summary, created_at
158
+ FROM jobs WHERE task_id = ? AND user_id = ?
159
+ ORDER BY started_at DESC LIMIT ?`,
160
+ )
161
+ .bind(taskId, userId, limit)
162
+ .all<{
163
+ id: string;
164
+ session_key: string;
165
+ status: string;
166
+ started_at: number;
167
+ finished_at: number | null;
168
+ duration_ms: number | null;
169
+ summary: string;
170
+ created_at: number;
171
+ }>();
172
+
173
+ return c.json({
174
+ jobs: (results ?? []).map((r, idx, arr) => ({
175
+ id: r.id,
176
+ number: arr.length - idx,
177
+ sessionKey: r.session_key,
178
+ status: r.status,
179
+ startedAt: r.started_at,
180
+ finishedAt: r.finished_at,
181
+ durationMs: r.duration_ms,
182
+ summary: r.summary,
183
+ time: new Date(r.started_at * 1000).toLocaleString(),
184
+ })),
185
+ });
186
+ });
187
+
188
+ // Nested task routes under /api/channels/:channelId/tasks
189
+ protectedApp.route("/channels/:channelId/tasks", tasks);
190
+ // Nested job routes under /api/channels/:channelId/tasks/:taskId/jobs
191
+ protectedApp.route("/channels/:channelId/tasks/:taskId/jobs", jobs);
192
+ // Nested session routes under /api/channels/:channelId/sessions
193
+ protectedApp.route("/channels/:channelId/sessions", sessions);
194
+ protectedApp.route("/pairing-tokens", pairing);
195
+ protectedApp.route("/upload", upload);
196
+
197
+ // ---- Media serving route (public, no auth) ----
198
+ app.get("/api/media/:userId/:filename", async (c) => {
199
+ const userId = c.req.param("userId");
200
+ const filename = c.req.param("filename");
201
+ const key = `media/${userId}/${filename}`;
202
+
203
+ const object = await c.env.MEDIA.get(key);
204
+ if (!object) {
205
+ return c.json({ error: "Not found" }, 404);
206
+ }
207
+
208
+ const headers = new Headers();
209
+ headers.set("Content-Type", object.httpMetadata?.contentType ?? "application/octet-stream");
210
+ headers.set("Cache-Control", "public, max-age=31536000, immutable");
211
+
212
+ return new Response(object.body, { headers });
213
+ });
214
+
215
+ // ---- WebSocket upgrade routes (BEFORE protected middleware) ----
216
+
217
+ // OpenClaw plugin connects to: /api/gateway/:connId
218
+ // connId can be a userId or "default" — in the latter case, we look up
219
+ // the user via the pairing token passed in the Sec-WebSocket-Protocol header
220
+ // or the ?token= query parameter.
221
+ app.all("/api/gateway/:connId", async (c) => {
222
+ let userId = c.req.param("connId");
223
+
224
+ // If connId is not a real user ID (e.g. "default"), resolve via token
225
+ if (!userId.startsWith("u_")) {
226
+ const token =
227
+ c.req.query("token") ??
228
+ c.req.header("X-Pairing-Token") ??
229
+ null;
230
+
231
+ if (!token) {
232
+ return c.json({ error: "Token required for gateway connection" }, 401);
233
+ }
234
+
235
+ // Look up user by pairing token
236
+ const row = await c.env.DB.prepare(
237
+ "SELECT user_id FROM pairing_tokens WHERE token = ?",
238
+ )
239
+ .bind(token)
240
+ .first<{ user_id: string }>();
241
+
242
+ if (!row) {
243
+ return c.json({ error: "Invalid pairing token" }, 401);
244
+ }
245
+ userId = row.user_id;
246
+
247
+ // Update last_connected_at
248
+ await c.env.DB.prepare(
249
+ "UPDATE pairing_tokens SET last_connected_at = unixepoch() WHERE token = ?",
250
+ )
251
+ .bind(token)
252
+ .run();
253
+ }
254
+
255
+ const doId = c.env.CONNECTION_DO.idFromName(userId);
256
+ const stub = c.env.CONNECTION_DO.get(doId);
257
+ const url = new URL(c.req.url);
258
+ url.pathname = `/gateway/${userId}`;
259
+ return stub.fetch(new Request(url.toString(), c.req.raw));
260
+ });
261
+
262
+ // Browser client connects to: /api/ws/:userId/:sessionId
263
+ app.all("/api/ws/:userId/:sessionId", async (c) => {
264
+ const userId = c.req.param("userId");
265
+ const sessionId = c.req.param("sessionId");
266
+ const doId = c.env.CONNECTION_DO.idFromName(userId);
267
+ const stub = c.env.CONNECTION_DO.get(doId);
268
+ const url = new URL(c.req.url);
269
+ url.pathname = `/client/${sessionId}`;
270
+ return stub.fetch(new Request(url.toString(), c.req.raw));
271
+ });
272
+
273
+ // Connection status: /api/connection/:userId/status
274
+ app.get("/api/connection/:userId/status", async (c) => {
275
+ const userId = c.req.param("userId");
276
+ const doId = c.env.CONNECTION_DO.idFromName(userId);
277
+ const stub = c.env.CONNECTION_DO.get(doId);
278
+ const url = new URL(c.req.url);
279
+ url.pathname = "/status";
280
+ return stub.fetch(new Request(url.toString()));
281
+ });
282
+
283
+ // Message history: /api/messages/:userId?sessionKey=xxx
284
+ app.get("/api/messages/:userId", async (c) => {
285
+ const userId = c.req.param("userId");
286
+ const doId = c.env.CONNECTION_DO.idFromName(userId);
287
+ const stub = c.env.CONNECTION_DO.get(doId);
288
+ const url = new URL(c.req.url);
289
+ url.pathname = "/messages";
290
+ // Forward query params (sessionKey, threadId, limit)
291
+ return stub.fetch(new Request(url.toString()));
292
+ });
293
+
294
+ // ---- Protected routes (require Bearer token) — AFTER ws routes ----
295
+ app.route("/api", protectedApp);
296
+
297
+ export default app;
@@ -0,0 +1,68 @@
1
+ import { Hono } from "hono";
2
+ import type { Env } from "../env.js";
3
+
4
+ /**
5
+ * Agents API — OpenClaw-aligned first-level entity.
6
+ * Returns the default agent (always) plus one agent per channel (default session = first adhoc task).
7
+ */
8
+ const agents = new Hono<{ Bindings: Env; Variables: { userId: string } }>();
9
+
10
+ export type Agent = {
11
+ id: string;
12
+ name: string;
13
+ sessionKey: string;
14
+ isDefault: boolean;
15
+ channelId: string | null;
16
+ };
17
+
18
+ /** GET /api/agents — list agents: default + one per channel with default session */
19
+ agents.get("/", async (c) => {
20
+ const userId = c.get("userId");
21
+
22
+ const list: Agent[] = [];
23
+
24
+ // 1. Channel-based agents (each channel = one agent, default session = first adhoc task)
25
+ const { results: channels } = await c.env.DB.prepare(
26
+ "SELECT id, name, openclaw_agent_id FROM channels WHERE user_id = ? ORDER BY created_at ASC",
27
+ )
28
+ .bind(userId)
29
+ .all<{ id: string; name: string; openclaw_agent_id: string }>();
30
+
31
+ // Find the "General" channel to associate with the default agent (for session support).
32
+ // If the user has created a "General" channel (auto-created on first session "+"),
33
+ // link it to the default agent so sessions work.
34
+ const generalChannel = (channels ?? []).find((ch) => ch.name === "General");
35
+
36
+ // Always show the "general" default agent for ad-hoc chat.
37
+ list.push({
38
+ id: "default",
39
+ name: "General",
40
+ sessionKey: `agent:main:botschat:${userId}:default`,
41
+ isDefault: true,
42
+ channelId: generalChannel?.id ?? null,
43
+ });
44
+
45
+ for (const ch of channels ?? []) {
46
+ // Skip the "General" channel — it's linked to the default agent above
47
+ if (generalChannel && ch.id === generalChannel.id) continue;
48
+ const task = await c.env.DB.prepare(
49
+ "SELECT session_key FROM tasks WHERE channel_id = ? AND kind = 'adhoc' AND session_key IS NOT NULL ORDER BY created_at ASC LIMIT 1",
50
+ )
51
+ .bind(ch.id)
52
+ .first<{ session_key: string }>();
53
+
54
+ const sessionKey =
55
+ task?.session_key ?? `agent:${ch.openclaw_agent_id}:botschat:${userId}:adhoc`;
56
+ list.push({
57
+ id: ch.id,
58
+ name: ch.name,
59
+ sessionKey,
60
+ isDefault: false,
61
+ channelId: ch.id,
62
+ });
63
+ }
64
+
65
+ return c.json({ agents: list });
66
+ });
67
+
68
+ export { agents };
@@ -0,0 +1,105 @@
1
+ import { Hono } from "hono";
2
+ import type { Env } from "../env.js";
3
+ import { createToken, hashPassword } from "../utils/auth.js";
4
+ import { generateId } from "../utils/id.js";
5
+
6
+ const auth = new Hono<{ Bindings: Env }>();
7
+
8
+ /** POST /api/auth/register */
9
+ auth.post("/register", async (c) => {
10
+ const { email, password, displayName } = await c.req.json<{
11
+ email: string;
12
+ password: string;
13
+ displayName?: string;
14
+ }>();
15
+
16
+ if (!email?.trim() || !password?.trim()) {
17
+ return c.json({ error: "Email and password are required" }, 400);
18
+ }
19
+
20
+ const id = generateId("u_");
21
+ const passwordHash = await hashPassword(password);
22
+
23
+ try {
24
+ await c.env.DB.prepare(
25
+ "INSERT INTO users (id, email, password_hash, display_name) VALUES (?, ?, ?, ?)",
26
+ )
27
+ .bind(id, email.trim().toLowerCase(), passwordHash, displayName?.trim() ?? null)
28
+ .run();
29
+ } catch (err: unknown) {
30
+ if (err instanceof Error && err.message.includes("UNIQUE")) {
31
+ return c.json({ error: "Email already registered" }, 409);
32
+ }
33
+ throw err;
34
+ }
35
+
36
+ const secret = c.env.JWT_SECRET ?? "botschat-dev-secret";
37
+ const token = await createToken(id, secret);
38
+
39
+ return c.json({ id, email, token }, 201);
40
+ });
41
+
42
+ /** POST /api/auth/login */
43
+ auth.post("/login", async (c) => {
44
+ const { email, password } = await c.req.json<{
45
+ email: string;
46
+ password: string;
47
+ }>();
48
+
49
+ if (!email?.trim() || !password?.trim()) {
50
+ return c.json({ error: "Email and password are required" }, 400);
51
+ }
52
+
53
+ const passwordHash = await hashPassword(password);
54
+
55
+ const row = await c.env.DB.prepare(
56
+ "SELECT id, email, display_name FROM users WHERE email = ? AND password_hash = ?",
57
+ )
58
+ .bind(email.trim().toLowerCase(), passwordHash)
59
+ .first<{ id: string; email: string; display_name: string | null }>();
60
+
61
+ if (!row) {
62
+ return c.json({ error: "Invalid email or password" }, 401);
63
+ }
64
+
65
+ const secret = c.env.JWT_SECRET ?? "botschat-dev-secret";
66
+ const token = await createToken(row.id, secret);
67
+
68
+ return c.json({
69
+ id: row.id,
70
+ email: row.email,
71
+ displayName: row.display_name,
72
+ token,
73
+ });
74
+ });
75
+
76
+ /** GET /api/auth/me — returns current user info */
77
+ auth.get("/me", async (c) => {
78
+ // This route requires auth middleware to be applied upstream
79
+ const userId = c.get("userId" as never) as string;
80
+ if (!userId) return c.json({ error: "Unauthorized" }, 401);
81
+
82
+ const row = await c.env.DB.prepare(
83
+ "SELECT id, email, display_name, settings_json, created_at FROM users WHERE id = ?",
84
+ )
85
+ .bind(userId)
86
+ .first<{
87
+ id: string;
88
+ email: string;
89
+ display_name: string | null;
90
+ settings_json: string;
91
+ created_at: number;
92
+ }>();
93
+
94
+ if (!row) return c.json({ error: "User not found" }, 404);
95
+
96
+ return c.json({
97
+ id: row.id,
98
+ email: row.email,
99
+ displayName: row.display_name,
100
+ settings: JSON.parse(row.settings_json || "{}"),
101
+ createdAt: row.created_at,
102
+ });
103
+ });
104
+
105
+ export { auth };
@@ -0,0 +1,185 @@
1
+ import { Hono } from "hono";
2
+ import type { Env } from "../env.js";
3
+ import { generateId } from "../utils/id.js";
4
+
5
+ const channels = new Hono<{ Bindings: Env; Variables: { userId: string } }>();
6
+
7
+ /** GET /api/channels — list all channels for the current user */
8
+ channels.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 channels 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
+ channels: (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/channels — create a new channel */
39
+ channels.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: "Channel name is required" }, 400);
50
+ }
51
+
52
+ const id = generateId("ch_");
53
+ // Default agent ID derived from channel 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 channels (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, channel_id, name, kind, session_key) VALUES (?, ?, ?, ?, ?)",
73
+ )
74
+ .bind(taskId, id, "Ad Hoc Chat", "adhoc", sessionKey)
75
+ .run();
76
+
77
+ // Auto-create a default session
78
+ const sessionId = generateId("ses_");
79
+ await c.env.DB.prepare(
80
+ "INSERT INTO sessions (id, channel_id, user_id, name, session_key) VALUES (?, ?, ?, ?, ?)",
81
+ )
82
+ .bind(sessionId, id, userId, "Session 1", sessionKey)
83
+ .run();
84
+
85
+ return c.json(
86
+ {
87
+ id,
88
+ name: name.trim(),
89
+ description: description?.trim() ?? "",
90
+ openclawAgentId: agentId,
91
+ systemPrompt: systemPrompt?.trim() ?? "",
92
+ },
93
+ 201,
94
+ );
95
+ });
96
+
97
+ /** GET /api/channels/:id — get a single channel */
98
+ channels.get("/:id", async (c) => {
99
+ const userId = c.get("userId");
100
+ const channelId = c.req.param("id");
101
+
102
+ const row = await c.env.DB.prepare(
103
+ "SELECT id, name, description, openclaw_agent_id, system_prompt, created_at, updated_at FROM channels WHERE id = ? AND user_id = ?",
104
+ )
105
+ .bind(channelId, userId)
106
+ .first<{
107
+ id: string;
108
+ name: string;
109
+ description: string;
110
+ openclaw_agent_id: string;
111
+ system_prompt: string;
112
+ created_at: number;
113
+ updated_at: number;
114
+ }>();
115
+
116
+ if (!row) return c.json({ error: "Channel not found" }, 404);
117
+
118
+ return c.json({
119
+ id: row.id,
120
+ name: row.name,
121
+ description: row.description,
122
+ openclawAgentId: row.openclaw_agent_id,
123
+ systemPrompt: row.system_prompt,
124
+ createdAt: row.created_at,
125
+ updatedAt: row.updated_at,
126
+ });
127
+ });
128
+
129
+ /** PATCH /api/channels/:id — update a channel */
130
+ channels.patch("/:id", async (c) => {
131
+ const userId = c.get("userId");
132
+ const channelId = c.req.param("id");
133
+ const body = await c.req.json<{
134
+ name?: string;
135
+ description?: string;
136
+ systemPrompt?: string;
137
+ }>();
138
+
139
+ const sets: string[] = [];
140
+ const values: unknown[] = [];
141
+
142
+ if (body.name !== undefined) {
143
+ sets.push("name = ?");
144
+ values.push(body.name.trim());
145
+ }
146
+ if (body.description !== undefined) {
147
+ sets.push("description = ?");
148
+ values.push(body.description.trim());
149
+ }
150
+ if (body.systemPrompt !== undefined) {
151
+ sets.push("system_prompt = ?");
152
+ values.push(body.systemPrompt.trim());
153
+ }
154
+
155
+ if (sets.length === 0) {
156
+ return c.json({ error: "No fields to update" }, 400);
157
+ }
158
+
159
+ sets.push("updated_at = unixepoch()");
160
+ values.push(channelId, userId);
161
+
162
+ await c.env.DB.prepare(
163
+ `UPDATE channels SET ${sets.join(", ")} WHERE id = ? AND user_id = ?`,
164
+ )
165
+ .bind(...values)
166
+ .run();
167
+
168
+ return c.json({ ok: true });
169
+ });
170
+
171
+ /** DELETE /api/channels/:id — delete a channel */
172
+ channels.delete("/:id", async (c) => {
173
+ const userId = c.get("userId");
174
+ const channelId = c.req.param("id");
175
+
176
+ await c.env.DB.prepare(
177
+ "DELETE FROM channels WHERE id = ? AND user_id = ?",
178
+ )
179
+ .bind(channelId, userId)
180
+ .run();
181
+
182
+ return c.json({ ok: true });
183
+ });
184
+
185
+ export { channels };