botschat 0.1.17 → 0.1.19

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 (46) hide show
  1. package/README.md +12 -0
  2. package/package.json +1 -1
  3. package/packages/api/src/do/connection-do.ts +145 -8
  4. package/packages/api/src/index.ts +26 -0
  5. package/packages/api/src/routes/auth.ts +1 -0
  6. package/packages/api/src/routes/demo.ts +156 -0
  7. package/packages/plugin/dist/index.d.ts +1 -0
  8. package/packages/plugin/dist/index.d.ts.map +1 -1
  9. package/packages/plugin/dist/index.js +2 -1
  10. package/packages/plugin/dist/index.js.map +1 -1
  11. package/packages/plugin/dist/src/channel.d.ts.map +1 -1
  12. package/packages/plugin/dist/src/channel.js +351 -68
  13. package/packages/plugin/dist/src/channel.js.map +1 -1
  14. package/packages/plugin/dist/src/runtime.d.ts +2 -0
  15. package/packages/plugin/dist/src/runtime.d.ts.map +1 -1
  16. package/packages/plugin/dist/src/runtime.js +10 -0
  17. package/packages/plugin/dist/src/runtime.js.map +1 -1
  18. package/packages/plugin/dist/src/types.d.ts +12 -0
  19. package/packages/plugin/dist/src/types.d.ts.map +1 -1
  20. package/packages/plugin/package.json +18 -2
  21. package/packages/web/dist/assets/index-BtPyCBCl.css +1 -0
  22. package/packages/web/dist/assets/index-BtpsFe4Z.js +2 -0
  23. package/packages/web/dist/assets/index-CQbIYr6_.js +2 -0
  24. package/packages/web/dist/assets/{index-DzYqprDN.js → index-C_GamcQc.js} +1 -1
  25. package/packages/web/dist/assets/index-LiBjPMg2.js +1 -0
  26. package/packages/web/dist/assets/{index-D3T7sc-R.js → index-MyoWvQAH.js} +1 -1
  27. package/packages/web/dist/assets/index-STIPTMK8.js +1516 -0
  28. package/packages/web/dist/assets/{index.esm-COzWPkKi.js → index.esm-BpQAwtdR.js} +1 -1
  29. package/packages/web/dist/assets/{web-DFQypSd0.js → web-BbTzVNLt.js} +1 -1
  30. package/packages/web/dist/assets/{web-CxXbaApe.js → web-cnzjgNfD.js} +1 -1
  31. package/packages/web/dist/index.html +2 -2
  32. package/packages/web/src/App.tsx +32 -0
  33. package/packages/web/src/api.ts +2 -0
  34. package/packages/web/src/components/ChatWindow.tsx +125 -5
  35. package/packages/web/src/components/ImageLightbox.tsx +96 -0
  36. package/packages/web/src/components/LoginPage.tsx +59 -1
  37. package/packages/web/src/components/MessageContent.tsx +17 -2
  38. package/packages/web/src/hooks/useIMEComposition.ts +14 -9
  39. package/packages/web/src/store.ts +47 -4
  40. package/packages/web/src/ws.ts +21 -1
  41. package/scripts/mock-openclaw.mjs +35 -0
  42. package/packages/web/dist/assets/index-B5GU1yVt.css +0 -1
  43. package/packages/web/dist/assets/index-CO9YgLst.js +0 -2
  44. package/packages/web/dist/assets/index-ClDrCe_c.js +0 -1
  45. package/packages/web/dist/assets/index-DPEosppm.js +0 -2
  46. package/packages/web/dist/assets/index-IVUdSd9w.js +0 -1516
package/README.md CHANGED
@@ -3,11 +3,21 @@
3
3
  [![npm](https://img.shields.io/npm/v/botschat)](https://www.npmjs.com/package/botschat)
4
4
  [![npm](https://img.shields.io/npm/v/@botschat/botschat)](https://www.npmjs.com/package/@botschat/botschat)
5
5
  [![License](https://img.shields.io/badge/license-Apache--2.0-blue)](LICENSE)
6
+ [![macOS](https://img.shields.io/badge/Download-macOS_App-black?logo=apple&logoColor=white)](https://github.com/botschat-app/botsChat/releases/latest/download/BotsChat-mac.dmg)
7
+ [![iOS](https://img.shields.io/badge/Download-iOS_App-blue?logo=apple&logoColor=white)](https://apps.apple.com/app/botschat-chat-with-agent/id6759292058)
6
8
 
7
9
  A self-hosted, **end-to-end encrypted** chat interface for [OpenClaw](https://github.com/openclaw/openclaw) AI agents.
8
10
 
9
11
  BotsChat gives you a modern, Slack-like web UI to interact with your OpenClaw agents — organize conversations into **Channels**, schedule **Background Tasks**, and monitor **Job** executions. With **E2E encryption**, your chat messages, cron prompts, and job summaries are encrypted on your device before they ever leave — the server only sees ciphertext it cannot decrypt. Your API keys and data never leave your machine.
10
12
 
13
+ <div align="center">
14
+
15
+ https://github.com/user-attachments/assets/e727ef9e-53b9-40d4-b943-c02019588203
16
+
17
+ [▶ Watch in HD on YouTube](https://www.youtube.com/watch?v=_ifqYhoV7Jk)
18
+
19
+ </div>
20
+
11
21
  ## Key Features
12
22
 
13
23
  ### Structured Conversation Management
@@ -95,6 +105,8 @@ BotsChat is **100% open source** — the [same code](https://github.com/botschat
95
105
  | **B. Run Locally** | Development, no cloud account | Yes |
96
106
  | **C. Deploy to Cloudflare** | Remote access (e.g. from phone) | Yes |
97
107
 
108
+ > **Native Apps**: A macOS client is available — [download the latest DMG](https://github.com/botschat-app/botsChat/releases/latest/download/BotsChat-mac.dmg) (signed and notarized, Apple Silicon + Intel). An iOS app is also available on the [App Store](https://apps.apple.com/app/botschat-chat-with-agent/id6759292058).
109
+
98
110
  Pick one below and follow its steps, then continue to [Install the OpenClaw Plugin](#install-the-openclaw-plugin).
99
111
 
100
112
  ---
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "botschat",
3
- "version": "0.1.17",
3
+ "version": "0.1.19",
4
4
  "description": "A self-hosted chat interface for OpenClaw AI agents",
5
5
  "workspaces": [
6
6
  "packages/*"
@@ -4,6 +4,7 @@ import { getFcmAccessToken, sendPushNotification } from "../utils/fcm.js";
4
4
  import { sendApnsNotification, type ApnsConfig } from "../utils/apns.js";
5
5
  import { generateId as generateIdUtil } from "../utils/id.js";
6
6
  import { randomUUID } from "../utils/uuid.js";
7
+ import { isDemoUserId } from "../routes/demo.js";
7
8
 
8
9
  /** Presence info stored in browser WebSocket attachments (survives DO hibernation). */
9
10
  interface BrowserAttachment {
@@ -104,8 +105,9 @@ export class ConnectionDO implements DurableObject {
104
105
  // Route: /models — Available models (REST)
105
106
  if (url.pathname === "/models") {
106
107
  await this.ensureCachedModels();
107
- console.log(`[DO] GET /models returning ${this.cachedModels.length} models`);
108
- return Response.json({ models: this.cachedModels });
108
+ const models = this.cachedModels.length ? this.cachedModels : (await this.isDemoUser() ? ConnectionDO.DEMO_MODELS : []);
109
+ console.log(`[DO] GET /models — returning ${models.length} models`);
110
+ return Response.json({ models });
109
111
  }
110
112
 
111
113
  // Route: /scan-data — Cached OpenClaw scan data (schedule/instructions/model)
@@ -458,17 +460,25 @@ export class ConnectionDO implements DurableObject {
458
460
  ws.serializeAttachment({ ...attachment, authenticated: true });
459
461
  // Include userId so the browser can derive the E2E key
460
462
  const doUserId2 = doUserId ?? payload.sub;
463
+ // Persist userId to storage so isDemoUser() and other helpers work
464
+ // even when no OpenClaw plugin has connected (e.g. demo mode).
465
+ if (!doUserId) {
466
+ await this.state.storage.put("userId", doUserId2);
467
+ }
461
468
  ws.send(JSON.stringify({ type: "auth.ok", userId: doUserId2 }));
462
469
 
463
470
  // Send current OpenClaw connection status + cached models
464
471
  await this.ensureCachedModels();
465
- const openclawConnected = this.getOpenClawSocket() !== null;
472
+ const isDemo = isDemoUserId(doUserId2);
473
+ const openclawConnected = isDemo || this.getOpenClawSocket() !== null;
474
+ const models = this.cachedModels.length ? this.cachedModels : (isDemo ? ConnectionDO.DEMO_MODELS : []);
475
+ const model = this.defaultModel || (isDemo ? "mock/echo-1.0" : null);
466
476
  ws.send(
467
477
  JSON.stringify({
468
478
  type: "connection.status",
469
479
  openclawConnected,
470
- defaultModel: this.defaultModel,
471
- models: this.cachedModels,
480
+ defaultModel: model,
481
+ models,
472
482
  }),
473
483
  );
474
484
  return;
@@ -554,6 +564,8 @@ export class ConnectionDO implements DurableObject {
554
564
  }
555
565
  }
556
566
  openclawWs.send(JSON.stringify(enrichedMsg));
567
+ } else if (await this.isDemoUser()) {
568
+ await this.handleDemoMockReply(ws, msg);
557
569
  } else {
558
570
  ws.send(
559
571
  JSON.stringify({
@@ -574,6 +586,10 @@ export class ConnectionDO implements DurableObject {
574
586
  private async handleGetScanData(): Promise<Response> {
575
587
  const openclawWs = this.getOpenClawSocket();
576
588
  if (!openclawWs) {
589
+ // Demo users get mock scan data instead of 503
590
+ if (await this.isDemoUser()) {
591
+ return Response.json({ tasks: [] });
592
+ }
577
593
  return Response.json(
578
594
  { error: "OpenClaw not connected", tasks: [] },
579
595
  { status: 503 },
@@ -614,7 +630,7 @@ export class ConnectionDO implements DurableObject {
614
630
  }
615
631
  }
616
632
 
617
- private handleStatus(): Response {
633
+ private async handleStatus(): Promise<Response> {
618
634
  const sockets = this.state.getWebSockets();
619
635
  const openclawSocket = sockets.find((s) => this.getTag(s) === "openclaw");
620
636
  const browserCount = sockets.filter((s) =>
@@ -627,9 +643,10 @@ export class ConnectionDO implements DurableObject {
627
643
  openclawAuthenticated = att?.authenticated ?? false;
628
644
  }
629
645
 
646
+ const isDemo = await this.isDemoUser();
630
647
  return Response.json({
631
- openclawConnected: !!openclawSocket,
632
- openclawAuthenticated,
648
+ openclawConnected: isDemo || !!openclawSocket,
649
+ openclawAuthenticated: isDemo || openclawAuthenticated,
633
650
  browserClients: browserCount,
634
651
  });
635
652
  }
@@ -1405,6 +1422,126 @@ export class ConnectionDO implements DurableObject {
1405
1422
  }
1406
1423
  }
1407
1424
 
1425
+ // ---- Demo mode (built-in mock OpenClaw) ----
1426
+
1427
+ private static readonly DEMO_MODELS = [
1428
+ { id: "mock/echo-1.0", name: "Echo 1.0", provider: "mock" },
1429
+ { id: "anthropic/claude-sonnet-4-20250514", name: "Claude Sonnet 4", provider: "anthropic" },
1430
+ { id: "openai/gpt-4o", name: "GPT-4o", provider: "openai" },
1431
+ ];
1432
+
1433
+ private async isDemoUser(): Promise<boolean> {
1434
+ const userId = await this.state.storage.get<string>("userId");
1435
+ return !!userId && isDemoUserId(userId);
1436
+ }
1437
+
1438
+ /**
1439
+ * Handle a browser message when no OpenClaw plugin is connected and the
1440
+ * user is a demo account. Simulates the mock OpenClaw responses inline.
1441
+ */
1442
+ private async handleDemoMockReply(
1443
+ ws: WebSocket,
1444
+ msg: Record<string, unknown>,
1445
+ ): Promise<void> {
1446
+ const sessionKey = msg.sessionKey as string;
1447
+
1448
+ if (msg.type === "user.message") {
1449
+ const userText = (msg.text as string) ?? "";
1450
+ const replyText = `Mock reply: ${userText}`;
1451
+ const replyId = randomUUID();
1452
+
1453
+ // Small delay to feel natural
1454
+ await new Promise((r) => setTimeout(r, 300));
1455
+
1456
+ const reply = {
1457
+ type: "agent.text",
1458
+ sessionKey,
1459
+ text: replyText,
1460
+ messageId: replyId,
1461
+ };
1462
+
1463
+ await this.persistMessage({
1464
+ id: replyId,
1465
+ sender: "agent",
1466
+ sessionKey,
1467
+ text: replyText,
1468
+ encrypted: 0,
1469
+ });
1470
+
1471
+ this.broadcastToBrowsers(JSON.stringify(reply));
1472
+ } else if (msg.type === "user.media") {
1473
+ const replyId = randomUUID();
1474
+ await new Promise((r) => setTimeout(r, 300));
1475
+ const reply = {
1476
+ type: "agent.text",
1477
+ sessionKey,
1478
+ text: `📎 Received media: ${msg.mediaUrl}`,
1479
+ messageId: replyId,
1480
+ };
1481
+ await this.persistMessage({ id: replyId, sender: "agent", sessionKey, text: reply.text, encrypted: 0 });
1482
+ this.broadcastToBrowsers(JSON.stringify(reply));
1483
+ } else if (msg.type === "user.command" || msg.type === "user.action") {
1484
+ const replyId = randomUUID();
1485
+ await new Promise((r) => setTimeout(r, 200));
1486
+ const text = msg.type === "user.command"
1487
+ ? `Command received: /${msg.command} ${msg.args || ""}`.trim()
1488
+ : `Action received: ${msg.action}`;
1489
+ const reply = { type: "agent.text", sessionKey, text, messageId: replyId };
1490
+ await this.persistMessage({ id: replyId, sender: "agent", sessionKey, text, encrypted: 0 });
1491
+ this.broadcastToBrowsers(JSON.stringify(reply));
1492
+ } else if (msg.type === "task.schedule") {
1493
+ const ack = {
1494
+ type: "task.schedule.ack",
1495
+ cronJobId: (msg.cronJobId as string) || `mock_cron_${Date.now()}`,
1496
+ taskId: msg.taskId,
1497
+ ok: true,
1498
+ };
1499
+ this.broadcastToBrowsers(JSON.stringify(ack));
1500
+ } else if (msg.type === "task.run") {
1501
+ const jobId = `mock_job_${Date.now()}`;
1502
+ const startedAt = Math.floor(Date.now() / 1000);
1503
+ this.broadcastToBrowsers(JSON.stringify({
1504
+ type: "job.update",
1505
+ cronJobId: msg.cronJobId,
1506
+ jobId,
1507
+ sessionKey: sessionKey ?? "",
1508
+ status: "running",
1509
+ startedAt,
1510
+ }));
1511
+ await new Promise((r) => setTimeout(r, 2000));
1512
+ const finishedAt = Math.floor(Date.now() / 1000);
1513
+ const update = {
1514
+ type: "job.update",
1515
+ cronJobId: msg.cronJobId,
1516
+ jobId,
1517
+ sessionKey: sessionKey ?? "",
1518
+ status: "ok",
1519
+ summary: "Mock task executed successfully",
1520
+ startedAt,
1521
+ finishedAt,
1522
+ durationMs: (finishedAt - startedAt) * 1000,
1523
+ };
1524
+ await this.handleJobUpdate(update);
1525
+ this.broadcastToBrowsers(JSON.stringify(update));
1526
+ } else if (msg.type === "settings.defaultModel") {
1527
+ const model = (msg.defaultModel as string) ?? "";
1528
+ if (model) {
1529
+ this.defaultModel = model;
1530
+ await this.state.storage.put("defaultModel", model);
1531
+ }
1532
+ this.broadcastToBrowsers(JSON.stringify({
1533
+ type: "defaultModel.updated",
1534
+ model,
1535
+ }));
1536
+ this.broadcastToBrowsers(JSON.stringify({
1537
+ type: "connection.status",
1538
+ openclawConnected: true,
1539
+ defaultModel: this.defaultModel,
1540
+ models: this.cachedModels.length ? this.cachedModels : ConnectionDO.DEMO_MODELS,
1541
+ }));
1542
+ }
1543
+ }
1544
+
1408
1545
  private async validatePairingToken(token: string): Promise<boolean> {
1409
1546
  // The API worker validates pairing tokens against D1 before routing
1410
1547
  // to the DO (and passes ?verified=1). Connections that arrive here
@@ -15,6 +15,7 @@ import { upload } from "./routes/upload.js";
15
15
  import { push } from "./routes/push.js";
16
16
  import { setup } from "./routes/setup.js";
17
17
  import { devAuth } from "./routes/dev-auth.js";
18
+ import { demo, isDemoUserId } from "./routes/demo.js";
18
19
 
19
20
  // Re-export the Durable Object class so wrangler can find it
20
21
  export { ConnectionDO } from "./do/connection-do.js";
@@ -88,11 +89,36 @@ app.get("/api/health", (c) => c.json({ status: "ok", version: "0.1.0" }));
88
89
  // ---- Public routes (no auth) ----
89
90
  app.route("/api/auth", auth);
90
91
  app.route("/api/dev-auth", devAuth);
92
+ app.route("/api/demo", demo);
91
93
  app.route("/api/setup", setup);
92
94
 
93
95
  // ---- Protected routes (require Bearer token) ----
94
96
  const protectedApp = new Hono<{ Bindings: Env; Variables: { userId: string } }>();
95
97
  protectedApp.use("/*", authMiddleware());
98
+
99
+ // Block sensitive operations for the demo user
100
+ const DEMO_BLOCKED_ROUTES: Array<{ method: string; pattern: RegExp }> = [
101
+ { method: "POST", pattern: /^\/pairing-tokens/ },
102
+ { method: "POST", pattern: /^\/upload/ },
103
+ { method: "DELETE", pattern: /^\/$/ }, // DELETE /api (account deletion goes via /api/auth/account)
104
+ ];
105
+ protectedApp.use("/*", async (c, next) => {
106
+ const userId = c.get("userId");
107
+ if (!isDemoUserId(userId)) return next();
108
+ const method = c.req.method;
109
+ const path = c.req.path.replace(/^\/api/, "");
110
+ // Block pairing tokens, file upload, and any DELETE (channels, tasks, sessions, account)
111
+ if (method === "DELETE") {
112
+ return c.json({ error: "Demo account cannot perform this action" }, 403);
113
+ }
114
+ for (const r of DEMO_BLOCKED_ROUTES) {
115
+ if (method === r.method && r.pattern.test(path)) {
116
+ return c.json({ error: "Demo account cannot perform this action" }, 403);
117
+ }
118
+ }
119
+ return next();
120
+ });
121
+
96
122
  protectedApp.route("/agents", agents);
97
123
  protectedApp.route("/channels", channels);
98
124
  protectedApp.route("/models", models);
@@ -357,6 +357,7 @@ auth.get("/me", async (c) => {
357
357
  auth.delete("/account", async (c) => {
358
358
  const userId = c.get("userId" as never) as string;
359
359
  if (!userId) return c.json({ error: "Unauthorized" }, 401);
360
+ if (userId.startsWith("demo_")) return c.json({ error: "Demo account cannot be deleted" }, 403);
360
361
 
361
362
  // Delete all user media from R2
362
363
  const prefix = `${userId}/`;
@@ -0,0 +1,156 @@
1
+ import { Hono } from "hono";
2
+ import type { Env } from "../env.js";
3
+ import { createToken, createRefreshToken, getJwtSecret } from "../utils/auth.js";
4
+ import { generateId } from "../utils/id.js";
5
+ import { randomUUID } from "../utils/uuid.js";
6
+
7
+ const DEMO_USER_PREFIX = "demo_";
8
+ const DEMO_DISPLAY_NAME = "Demo User";
9
+ const DEMO_TTL_SECONDS = 24 * 3600; // 24 hours
10
+
11
+ function isDemoUserId(userId: string): boolean {
12
+ return userId.startsWith(DEMO_USER_PREFIX);
13
+ }
14
+
15
+ const demo = new Hono<{ Bindings: Env }>();
16
+
17
+ /**
18
+ * POST /api/demo/login — public endpoint for Google Play reviewers and demos.
19
+ * Each call creates a fresh isolated demo user with seeded data.
20
+ * Old demo users (>24h) are cleaned up in the background.
21
+ * Rate limited: 1 request per 5 seconds per IP (via Cache API).
22
+ */
23
+ demo.post("/login", async (c) => {
24
+ // Rate limit by IP
25
+ const ip = c.req.header("CF-Connecting-IP") ?? c.req.header("X-Forwarded-For") ?? "unknown";
26
+ const cache = caches.default;
27
+ const rateCacheUrl = `https://rate.internal/demo-login/${ip}`;
28
+ const rateCacheReq = new Request(rateCacheUrl);
29
+ const cached = await cache.match(rateCacheReq);
30
+ if (cached) {
31
+ return c.json({ error: "Too many requests, try again later" }, 429);
32
+ }
33
+ c.executionCtx.waitUntil(
34
+ cache.put(rateCacheReq, new Response(null, {
35
+ headers: { "Cache-Control": "public, max-age=5" },
36
+ })),
37
+ );
38
+
39
+ const jwtSecret = getJwtSecret(c.env);
40
+ const userId = DEMO_USER_PREFIX + generateId("").slice(0, 12);
41
+ const email = `${userId}@demo.botschat.app`;
42
+
43
+ await c.env.DB.prepare(
44
+ "INSERT INTO users (id, email, password_hash, display_name) VALUES (?, ?, '', ?)",
45
+ ).bind(userId, email, DEMO_DISPLAY_NAME).run();
46
+
47
+ await seedDemoData(c.env.DB, userId);
48
+
49
+ // Clean up expired demo users in the background (non-blocking)
50
+ c.executionCtx.waitUntil(cleanupOldDemoUsers(c.env.DB));
51
+
52
+ const token = await createToken(userId, jwtSecret);
53
+ const refreshToken = await createRefreshToken(userId, jwtSecret);
54
+
55
+ return c.json({
56
+ id: userId,
57
+ email,
58
+ displayName: DEMO_DISPLAY_NAME,
59
+ token,
60
+ refreshToken,
61
+ });
62
+ });
63
+
64
+ async function cleanupOldDemoUsers(db: D1Database) {
65
+ try {
66
+ const cutoff = Math.floor(Date.now() / 1000) - DEMO_TTL_SECONDS;
67
+ const { results } = await db.prepare(
68
+ "SELECT id FROM users WHERE id LIKE 'demo_%' AND created_at < ? LIMIT 20",
69
+ ).bind(cutoff).all<{ id: string }>();
70
+
71
+ for (const row of results ?? []) {
72
+ await db.batch([
73
+ db.prepare("DELETE FROM messages WHERE user_id = ?").bind(row.id),
74
+ db.prepare("DELETE FROM jobs WHERE user_id = ?").bind(row.id),
75
+ db.prepare("DELETE FROM tasks WHERE channel_id IN (SELECT id FROM channels WHERE user_id = ?)").bind(row.id),
76
+ db.prepare("DELETE FROM sessions WHERE user_id = ?").bind(row.id),
77
+ db.prepare("DELETE FROM channels WHERE user_id = ?").bind(row.id),
78
+ db.prepare("DELETE FROM pairing_tokens WHERE user_id = ?").bind(row.id),
79
+ db.prepare("DELETE FROM push_tokens WHERE user_id = ?").bind(row.id),
80
+ db.prepare("DELETE FROM users WHERE id = ?").bind(row.id),
81
+ ]);
82
+ }
83
+ if ((results?.length ?? 0) > 0) {
84
+ console.log(`[demo] Cleaned up ${results!.length} expired demo users`);
85
+ }
86
+ } catch (err) {
87
+ console.error("[demo] Cleanup failed:", err);
88
+ }
89
+ }
90
+
91
+ async function seedDemoData(db: D1Database, userId: string) {
92
+ const now = Math.floor(Date.now() / 1000);
93
+
94
+ const ch1 = generateId("ch_");
95
+ const ch2 = generateId("ch_");
96
+
97
+ await db.batch([
98
+ db.prepare(
99
+ "INSERT INTO channels (id, user_id, name, description, openclaw_agent_id, system_prompt, created_at, updated_at) VALUES (?, ?, ?, ?, 'main', '', ?, ?)",
100
+ ).bind(ch1, userId, "General", "Default chat channel", now, now),
101
+ db.prepare(
102
+ "INSERT INTO channels (id, user_id, name, description, openclaw_agent_id, system_prompt, created_at, updated_at) VALUES (?, ?, ?, ?, 'main', '', ?, ?)",
103
+ ).bind(ch2, userId, "Tasks Demo", "Background task demo channel", now, now),
104
+ ]);
105
+
106
+ const adhocTask1 = generateId("tsk_");
107
+ const adhocTask2 = generateId("tsk_");
108
+ const ses1 = generateId("ses_");
109
+ const ses2 = generateId("ses_");
110
+ const sessionKey1 = `agent:main:botschat:${userId}:adhoc`;
111
+ const sessionKey2 = `agent:main:botschat:${userId}:adhoc:${ch2}`;
112
+
113
+ await db.batch([
114
+ db.prepare(
115
+ "INSERT INTO tasks (id, channel_id, name, kind, session_key, enabled, created_at, updated_at) VALUES (?, ?, 'Ad Hoc Chat', 'adhoc', ?, 1, ?, ?)",
116
+ ).bind(adhocTask1, ch1, sessionKey1, now, now),
117
+ db.prepare(
118
+ "INSERT INTO tasks (id, channel_id, name, kind, session_key, enabled, created_at, updated_at) VALUES (?, ?, 'Ad Hoc Chat', 'adhoc', ?, 1, ?, ?)",
119
+ ).bind(adhocTask2, ch2, sessionKey2, now, now),
120
+ db.prepare(
121
+ "INSERT INTO sessions (id, channel_id, user_id, name, session_key) VALUES (?, ?, ?, 'Session 1', ?)",
122
+ ).bind(ses1, ch1, userId, sessionKey1),
123
+ db.prepare(
124
+ "INSERT INTO sessions (id, channel_id, user_id, name, session_key) VALUES (?, ?, ?, 'Session 1', ?)",
125
+ ).bind(ses2, ch2, userId, sessionKey2),
126
+ ]);
127
+
128
+ const bgTask = generateId("tsk_");
129
+ const bgCronId = randomUUID();
130
+ const bgSessionKey = `agent:main:botschat:${userId}:task:${bgTask}`;
131
+
132
+ await db.prepare(
133
+ "INSERT INTO tasks (id, channel_id, name, kind, openclaw_cron_job_id, session_key, enabled, created_at, updated_at) VALUES (?, ?, ?, 'background', ?, ?, 1, ?, ?)",
134
+ ).bind(bgTask, ch2, "Daily Summary", bgCronId, bgSessionKey, now, now).run();
135
+
136
+ const jobId = `job_demo_${Date.now()}`;
137
+ await db.prepare(
138
+ "INSERT INTO jobs (id, task_id, user_id, session_key, status, started_at, finished_at, duration_ms, summary, created_at) VALUES (?, ?, ?, ?, 'ok', ?, ?, 2300, 'Daily summary generated successfully.', ?)",
139
+ ).bind(jobId, bgTask, userId, bgSessionKey, now - 3600, now - 3600 + 2, now).run();
140
+
141
+ const msgs = [
142
+ { sender: "user", text: "Hello! What can you do?" },
143
+ { sender: "agent", text: "Hi there! I'm your AI assistant powered by OpenClaw. I can help with:\n\n- **Chat**: Ask me anything — coding, writing, research, brainstorming\n- **Scheduled Tasks**: Set up recurring background jobs (e.g. daily summaries, monitoring)\n- **Multi-channel**: Organize conversations into separate channels\n- **E2E Encryption**: All messages can be end-to-end encrypted\n\nTry typing a message below!" },
144
+ { sender: "user", text: "Can you summarize a webpage for me?" },
145
+ { sender: "agent", text: "Absolutely! Just paste the URL and I'll summarize it for you. I can also extract key points, translate content, or answer questions about the page.\n\nFor example, you could say:\n> Summarize https://example.com/article\n\nNote: In this demo, I'll echo your messages back. Connect a real OpenClaw instance for full AI capabilities." },
146
+ ];
147
+
148
+ const msgBatch = msgs.map((m, i) =>
149
+ db.prepare(
150
+ "INSERT INTO messages (id, user_id, session_key, sender, text, encrypted, created_at) VALUES (?, ?, ?, ?, ?, 0, ?)",
151
+ ).bind(randomUUID(), userId, sessionKey1, m.sender, m.text, now - 300 + i * 30),
152
+ );
153
+ await db.batch(msgBatch);
154
+ }
155
+
156
+ export { demo, isDemoUserId };
@@ -13,6 +13,7 @@ declare const plugin: {
13
13
  registerChannel: (reg: {
14
14
  plugin: typeof botschatPlugin;
15
15
  }) => void;
16
+ registerHook?: (events: string | string[], handler: (...args: any[]) => any, opts?: any) => void;
16
17
  }): void;
17
18
  };
18
19
  export default plugin;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAMlD,QAAA,MAAM,MAAM;;;;;;;;;kBAKI;QACZ,OAAO,EAAE,OAAO,CAAC;QACjB,eAAe,EAAE,CAAC,GAAG,EAAE;YAAE,MAAM,EAAE,OAAO,cAAc,CAAA;SAAE,KAAK,IAAI,CAAC;KACnE;CAIF,CAAC;AAEF,eAAe,MAAM,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAMlD,QAAA,MAAM,MAAM;;;;;;;;;kBAKI;QACZ,OAAO,EAAE,OAAO,CAAC;QACjB,eAAe,EAAE,CAAC,GAAG,EAAE;YAAE,MAAM,EAAE,OAAO,cAAc,CAAA;SAAE,KAAK,IAAI,CAAC;QAClE,YAAY,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,EAAE,OAAO,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,GAAG,EAAE,IAAI,CAAC,EAAE,GAAG,KAAK,IAAI,CAAC;KAClG;CAKF,CAAC;AAEF,eAAe,MAAM,CAAC"}
@@ -1,5 +1,5 @@
1
1
  import { botschatPlugin } from "./src/channel.js";
2
- import { setBotsChatRuntime } from "./src/runtime.js";
2
+ import { setBotsChatRuntime, setBotsChatApi } from "./src/runtime.js";
3
3
  // OpenClaw Plugin Definition
4
4
  // This is the entry point loaded by OpenClaw's plugin system.
5
5
  // It registers the BotsChat channel plugin.
@@ -10,6 +10,7 @@ const plugin = {
10
10
  configSchema: { safeParse: () => ({ success: true }) },
11
11
  register(api) {
12
12
  setBotsChatRuntime(api.runtime);
13
+ setBotsChatApi(api);
13
14
  api.registerChannel({ plugin: botschatPlugin });
14
15
  },
15
16
  };
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAClD,OAAO,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAEtD,6BAA6B;AAC7B,8DAA8D;AAC9D,4CAA4C;AAC5C,MAAM,MAAM,GAAG;IACb,EAAE,EAAE,UAAU;IACd,IAAI,EAAE,UAAU;IAChB,WAAW,EAAE,yCAAyC;IACtD,YAAY,EAAE,EAAE,SAAS,EAAE,GAAG,EAAE,CAAC,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,EAAE;IACtD,QAAQ,CAAC,GAGR;QACC,kBAAkB,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QAChC,GAAG,CAAC,eAAe,CAAC,EAAE,MAAM,EAAE,cAAc,EAAE,CAAC,CAAC;IAClD,CAAC;CACF,CAAC;AAEF,eAAe,MAAM,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAClD,OAAO,EAAE,kBAAkB,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAEtE,6BAA6B;AAC7B,8DAA8D;AAC9D,4CAA4C;AAC5C,MAAM,MAAM,GAAG;IACb,EAAE,EAAE,UAAU;IACd,IAAI,EAAE,UAAU;IAChB,WAAW,EAAE,yCAAyC;IACtD,YAAY,EAAE,EAAE,SAAS,EAAE,GAAG,EAAE,CAAC,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,EAAE;IACtD,QAAQ,CAAC,GAIR;QACC,kBAAkB,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QAChC,cAAc,CAAC,GAAG,CAAC,CAAC;QACpB,GAAG,CAAC,eAAe,CAAC,EAAE,MAAM,EAAE,cAAc,EAAE,CAAC,CAAC;IAClD,CAAC;CACF,CAAC;AAEF,eAAe,MAAM,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"channel.d.ts","sourceRoot":"","sources":["../../src/channel.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"channel.d.ts","sourceRoot":"","sources":["../../src/channel.ts"],"names":[],"mappings":"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"}