botschat 0.1.19 → 0.1.20

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 (56) hide show
  1. package/migrations/0013_agents_table.sql +29 -0
  2. package/migrations/0014_agent_sessions.sql +19 -0
  3. package/migrations/0015_message_traces.sql +27 -0
  4. package/migrations/0016_multi_agent_channels_messages.sql +9 -0
  5. package/migrations/0017_rename_cron_job_id.sql +2 -0
  6. package/package.json +1 -1
  7. package/packages/api/src/do/connection-do.ts +382 -186
  8. package/packages/api/src/index.ts +67 -50
  9. package/packages/api/src/protocol-v2.ts +154 -0
  10. package/packages/api/src/routes/agents-v2.ts +192 -0
  11. package/packages/api/src/routes/agents.ts +3 -3
  12. package/packages/api/src/routes/auth.ts +0 -1
  13. package/packages/api/src/routes/channels.ts +11 -11
  14. package/packages/api/src/routes/history-v2.ts +221 -0
  15. package/packages/api/src/routes/migrate-v2.ts +110 -0
  16. package/packages/api/src/routes/sessions.ts +5 -5
  17. package/packages/api/src/routes/tasks.ts +33 -33
  18. package/packages/plugin/dist/src/channel.d.ts +10 -0
  19. package/packages/plugin/dist/src/channel.d.ts.map +1 -1
  20. package/packages/plugin/dist/src/channel.js +10 -51
  21. package/packages/plugin/dist/src/channel.js.map +1 -1
  22. package/packages/plugin/dist/src/types.d.ts +13 -0
  23. package/packages/plugin/dist/src/types.d.ts.map +1 -1
  24. package/packages/plugin/package.json +2 -18
  25. package/packages/web/dist/assets/index-BARPtt0v.css +1 -0
  26. package/packages/web/dist/assets/index-Bf-XL3te.js +2 -0
  27. package/packages/web/dist/assets/{index-C_GamcQc.js → index-CYQMu_-c.js} +1 -1
  28. package/packages/web/dist/assets/index-CYlvfpX9.js +1519 -0
  29. package/packages/web/dist/assets/index-CxcpA4Qo.js +1 -0
  30. package/packages/web/dist/assets/{index-MyoWvQAH.js → index-DYCO-ry1.js} +1 -1
  31. package/packages/web/dist/assets/index-QebPVqwj.js +2 -0
  32. package/packages/web/dist/assets/{index.esm-BpQAwtdR.js → index.esm-CvOpngZM.js} +1 -1
  33. package/packages/web/dist/assets/{web-cnzjgNfD.js → web-1cdhq2RW.js} +1 -1
  34. package/packages/web/dist/assets/{web-BbTzVNLt.js → web-D3LMODYp.js} +1 -1
  35. package/packages/web/dist/index.html +2 -2
  36. package/packages/web/src/App.tsx +56 -9
  37. package/packages/web/src/api.ts +61 -5
  38. package/packages/web/src/components/AgentSettings.tsx +328 -0
  39. package/packages/web/src/components/ChatWindow.tsx +9 -9
  40. package/packages/web/src/components/CronDetail.tsx +1 -1
  41. package/packages/web/src/components/LoginPage.tsx +1 -59
  42. package/packages/web/src/components/MessageContent.tsx +2 -17
  43. package/packages/web/src/components/SessionTabs.tsx +1 -1
  44. package/packages/web/src/components/Sidebar.tsx +3 -1
  45. package/packages/web/src/hooks/useIMEComposition.ts +9 -14
  46. package/packages/web/src/store.ts +39 -7
  47. package/packages/web/src/ws.ts +1 -0
  48. package/scripts/dev.sh +53 -0
  49. package/scripts/mock-openclaw-v2.mjs +486 -0
  50. package/packages/api/src/routes/demo.ts +0 -156
  51. package/packages/web/dist/assets/index-BtPyCBCl.css +0 -1
  52. package/packages/web/dist/assets/index-BtpsFe4Z.js +0 -2
  53. package/packages/web/dist/assets/index-CQbIYr6_.js +0 -2
  54. package/packages/web/dist/assets/index-LiBjPMg2.js +0 -1
  55. package/packages/web/dist/assets/index-STIPTMK8.js +0 -1516
  56. package/packages/web/src/components/ImageLightbox.tsx +0 -96
@@ -5,6 +5,9 @@ import { authMiddleware, verifyToken, getJwtSecret, verifyMediaSignature, signMe
5
5
  import { randomUUID } from "./utils/uuid.js";
6
6
  import { auth } from "./routes/auth.js";
7
7
  import { agents } from "./routes/agents.js";
8
+ import { agentsV2 } from "./routes/agents-v2.js";
9
+ import { migrateV2 } from "./routes/migrate-v2.js";
10
+ import { historyV2 } from "./routes/history-v2.js";
8
11
  import { channels } from "./routes/channels.js";
9
12
  import { tasks } from "./routes/tasks.js";
10
13
  import { jobs } from "./routes/jobs.js";
@@ -15,7 +18,6 @@ import { upload } from "./routes/upload.js";
15
18
  import { push } from "./routes/push.js";
16
19
  import { setup } from "./routes/setup.js";
17
20
  import { devAuth } from "./routes/dev-auth.js";
18
- import { demo, isDemoUserId } from "./routes/demo.js";
19
21
 
20
22
  // Re-export the Durable Object class so wrangler can find it
21
23
  export { ConnectionDO } from "./do/connection-do.js";
@@ -89,37 +91,15 @@ app.get("/api/health", (c) => c.json({ status: "ok", version: "0.1.0" }));
89
91
  // ---- Public routes (no auth) ----
90
92
  app.route("/api/auth", auth);
91
93
  app.route("/api/dev-auth", devAuth);
92
- app.route("/api/demo", demo);
93
94
  app.route("/api/setup", setup);
94
95
 
95
96
  // ---- Protected routes (require Bearer token) ----
96
97
  const protectedApp = new Hono<{ Bindings: Env; Variables: { userId: string } }>();
97
98
  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
-
122
99
  protectedApp.route("/agents", agents);
100
+ protectedApp.route("/v2/agents", agentsV2);
101
+ protectedApp.route("/v2/migrate", migrateV2);
102
+ protectedApp.route("/v2/messages", historyV2);
123
103
  protectedApp.route("/channels", channels);
124
104
  protectedApp.route("/models", models);
125
105
  protectedApp.get("/me", async (c) => {
@@ -235,7 +215,7 @@ protectedApp.get("/tasks", async (c) => {
235
215
  const kind = c.req.query("kind") ?? "background";
236
216
 
237
217
  const { results } = await c.env.DB.prepare(
238
- `SELECT t.id, t.channel_id, t.name, t.kind, t.openclaw_cron_job_id,
218
+ `SELECT t.id, t.channel_id, t.name, t.kind, t.provider_job_id,
239
219
  t.session_key, t.enabled, t.created_at, t.updated_at
240
220
  FROM tasks t
241
221
  JOIN channels ch ON t.channel_id = ch.id
@@ -248,7 +228,7 @@ protectedApp.get("/tasks", async (c) => {
248
228
  channel_id: string;
249
229
  name: string;
250
230
  kind: string;
251
- openclaw_cron_job_id: string | null;
231
+ provider_job_id: string | null;
252
232
  session_key: string | null;
253
233
  enabled: number;
254
234
  created_at: number;
@@ -261,7 +241,7 @@ protectedApp.get("/tasks", async (c) => {
261
241
  channelId: r.channel_id,
262
242
  name: r.name,
263
243
  kind: r.kind,
264
- openclawCronJobId: r.openclaw_cron_job_id,
244
+ providerJobId: r.provider_job_id,
265
245
  sessionKey: r.session_key,
266
246
  enabled: !!r.enabled,
267
247
  createdAt: r.created_at,
@@ -392,6 +372,8 @@ async function verifyUserAccess(c: { req: { header: (n: string) => string | unde
392
372
  app.all("/api/gateway/:connId", async (c) => {
393
373
  let userId = c.req.param("connId");
394
374
 
375
+ let agentId: string | null = null;
376
+
395
377
  if (!userId.startsWith("u_")) {
396
378
  const token =
397
379
  c.req.query("token") ??
@@ -402,16 +384,28 @@ app.all("/api/gateway/:connId", async (c) => {
402
384
  return c.json({ error: "Token required for gateway connection" }, 401);
403
385
  }
404
386
 
405
- const row = await c.env.DB.prepare(
406
- "SELECT user_id FROM pairing_tokens WHERE token = ? AND revoked_at IS NULL",
387
+ // v2: try agents table first (multi-agent), fallback to pairing_tokens (legacy)
388
+ const agentRow = await c.env.DB.prepare(
389
+ "SELECT id, user_id FROM agents WHERE pairing_token = ?",
407
390
  )
408
391
  .bind(token)
409
- .first<{ user_id: string }>();
410
-
411
- if (!row) {
412
- return c.json({ error: "Invalid pairing token" }, 401);
392
+ .first<{ id: string; user_id: string }>();
393
+
394
+ if (agentRow) {
395
+ userId = agentRow.user_id;
396
+ agentId = agentRow.id;
397
+ } else {
398
+ const row = await c.env.DB.prepare(
399
+ "SELECT user_id FROM pairing_tokens WHERE token = ? AND revoked_at IS NULL",
400
+ )
401
+ .bind(token)
402
+ .first<{ user_id: string }>();
403
+
404
+ if (!row) {
405
+ return c.json({ error: "Invalid pairing token" }, 401);
406
+ }
407
+ userId = row.user_id;
413
408
  }
414
- userId = row.user_id;
415
409
  }
416
410
 
417
411
  // --- Worker-level rate limit (Cache API) ---
@@ -428,17 +422,27 @@ app.all("/api/gateway/:connId", async (c) => {
428
422
  });
429
423
  }
430
424
 
431
- // Audit: update pairing token stats (only when not rate-limited)
425
+ // Audit: update connection stats (agents table for v2, pairing_tokens for legacy)
432
426
  const token = c.req.query("token") ?? c.req.header("X-Pairing-Token");
433
427
  if (token) {
434
428
  const clientIp = c.req.header("CF-Connecting-IP") ?? c.req.header("X-Forwarded-For") ?? "unknown";
435
- c.executionCtx.waitUntil(
436
- c.env.DB.prepare(
437
- `UPDATE pairing_tokens
438
- SET last_connected_at = unixepoch(), last_ip = ?, connection_count = connection_count + 1
439
- WHERE token = ?`,
440
- ).bind(clientIp, token).run(),
441
- );
429
+ if (agentId) {
430
+ c.executionCtx.waitUntil(
431
+ c.env.DB.prepare(
432
+ `UPDATE agents
433
+ SET status = 'connected', last_connected_at = unixepoch(), last_ip = ?, connection_count = connection_count + 1, updated_at = unixepoch()
434
+ WHERE id = ?`,
435
+ ).bind(clientIp, agentId).run(),
436
+ );
437
+ } else {
438
+ c.executionCtx.waitUntil(
439
+ c.env.DB.prepare(
440
+ `UPDATE pairing_tokens
441
+ SET last_connected_at = unixepoch(), last_ip = ?, connection_count = connection_count + 1
442
+ WHERE token = ?`,
443
+ ).bind(clientIp, token).run(),
444
+ );
445
+ }
442
446
  }
443
447
 
444
448
  const doId = c.env.CONNECTION_DO.idFromName(userId);
@@ -446,6 +450,7 @@ app.all("/api/gateway/:connId", async (c) => {
446
450
  const url = new URL(c.req.url);
447
451
  url.pathname = `/gateway/${userId}`;
448
452
  url.searchParams.set("verified", "1");
453
+ if (agentId) url.searchParams.set("agentId", agentId);
449
454
  const doResp = await stub.fetch(new Request(url.toString(), c.req.raw));
450
455
 
451
456
  // Cache the rate limit after the DO responds (success or rate-limited).
@@ -518,16 +523,28 @@ app.post("/api/plugin-upload", async (c) => {
518
523
  if (!token) {
519
524
  return c.json({ error: "Missing X-Pairing-Token header" }, 401);
520
525
  }
521
- const row = await c.env.DB.prepare(
522
- "SELECT user_id FROM pairing_tokens WHERE token = ? AND revoked_at IS NULL",
526
+
527
+ // v2: try agents table first, fallback to pairing_tokens
528
+ const agentRow = await c.env.DB.prepare(
529
+ "SELECT user_id FROM agents WHERE pairing_token = ?",
523
530
  )
524
531
  .bind(token)
525
532
  .first<{ user_id: string }>();
526
- if (!row) {
527
- return c.json({ error: "Invalid pairing token" }, 401);
528
- }
529
533
 
530
- const userId = row.user_id;
534
+ let userId: string;
535
+ if (agentRow) {
536
+ userId = agentRow.user_id;
537
+ } else {
538
+ const row = await c.env.DB.prepare(
539
+ "SELECT user_id FROM pairing_tokens WHERE token = ? AND revoked_at IS NULL",
540
+ )
541
+ .bind(token)
542
+ .first<{ user_id: string }>();
543
+ if (!row) {
544
+ return c.json({ error: "Invalid pairing token" }, 401);
545
+ }
546
+ userId = row.user_id;
547
+ }
531
548
  const contentType = c.req.header("Content-Type") ?? "";
532
549
  if (!contentType.includes("multipart/form-data")) {
533
550
  return c.json({ error: "Expected multipart/form-data" }, 400);
@@ -0,0 +1,154 @@
1
+ /**
2
+ * BotsChat v2 Multi-Agent Protocol
3
+ *
4
+ * Core Protocol: all agent bridges MUST implement these message types.
5
+ * Provider Extensions: optional, provider-specific messages (OpenClaw cron, etc.)
6
+ *
7
+ * Agent = Type (engine) × Role (persona). Each agent connects via a separate
8
+ * WebSocket and authenticates with its own agentId.
9
+ */
10
+
11
+ // ── Agent metadata shared across protocol ──
12
+
13
+ export type AgentType = "openclaw" | "cursor_cli" | "cursor_cloud" | "claude_code" | "mock";
14
+
15
+ export type AgentInfo = {
16
+ id: string;
17
+ name: string;
18
+ type: AgentType;
19
+ role: string;
20
+ capabilities: string[];
21
+ status: "connected" | "disconnected";
22
+ };
23
+
24
+ // ── Core Protocol: Agent → Cloud (outbound) ──
25
+
26
+ export type CoreOutbound =
27
+ | {
28
+ type: "auth";
29
+ token: string;
30
+ agentId?: string;
31
+ agentType?: AgentType;
32
+ agents?: string[];
33
+ model?: string;
34
+ }
35
+ | {
36
+ type: "agent.text";
37
+ agentId?: string;
38
+ sessionKey: string;
39
+ text: string;
40
+ requestId?: string;
41
+ replyToId?: string;
42
+ threadId?: string;
43
+ encrypted?: boolean;
44
+ messageId?: string;
45
+ notifyPreview?: string;
46
+ }
47
+ | {
48
+ type: "agent.media";
49
+ agentId?: string;
50
+ sessionKey: string;
51
+ mediaUrl: string;
52
+ caption?: string;
53
+ replyToId?: string;
54
+ threadId?: string;
55
+ encrypted?: boolean;
56
+ mediaEncrypted?: boolean;
57
+ messageId?: string;
58
+ notifyPreview?: string;
59
+ }
60
+ | { type: "agent.stream.start"; agentId?: string; sessionKey: string; runId: string }
61
+ | { type: "agent.stream.chunk"; agentId?: string; sessionKey: string; runId: string; text: string }
62
+ | { type: "agent.stream.end"; agentId?: string; sessionKey: string; runId: string }
63
+ | { type: "status"; connected: boolean; agents?: string[]; model?: string }
64
+ | { type: "pong" };
65
+
66
+ // ── Core Protocol: Cloud → Agent (inbound) ──
67
+
68
+ export type CoreInbound =
69
+ | {
70
+ type: "auth.ok";
71
+ userId: string;
72
+ agentId?: string;
73
+ availableAgents?: AgentInfo[];
74
+ }
75
+ | { type: "auth.fail"; reason: string }
76
+ | {
77
+ type: "user.message";
78
+ sessionKey: string;
79
+ text: string;
80
+ userId: string;
81
+ messageId: string;
82
+ targetAgentId?: string;
83
+ mediaUrl?: string;
84
+ parentMessageId?: string;
85
+ parentText?: string;
86
+ parentSender?: string;
87
+ parentEncrypted?: number;
88
+ }
89
+ | { type: "user.media"; sessionKey: string; mediaUrl: string; userId: string }
90
+ | { type: "user.action"; sessionKey: string; action: string; params: Record<string, unknown> }
91
+ | { type: "user.command"; sessionKey: string; command: string; args?: string }
92
+ | { type: "ping" };
93
+
94
+ // ── Agent-to-Agent Delegation ──
95
+
96
+ export type AgentRequestMessage = {
97
+ type: "agent.request";
98
+ agentId: string;
99
+ targetAgentId: string;
100
+ sessionKey: string;
101
+ text: string;
102
+ requestId: string;
103
+ depth: number;
104
+ context?: {
105
+ summary: string;
106
+ constraints?: string[];
107
+ expectedOutput?: string;
108
+ };
109
+ ephemeral?: boolean;
110
+ };
111
+
112
+ export type AgentResponseMessage = {
113
+ type: "agent.response";
114
+ requestId: string;
115
+ fromAgentId: string;
116
+ text: string;
117
+ sessionKey: string;
118
+ error?: string;
119
+ };
120
+
121
+ // ── Verbose Trace (lv2/lv3 execution traces) ──
122
+
123
+ export type AgentTraceMessage = {
124
+ type: "agent.trace";
125
+ agentId: string;
126
+ sessionKey: string;
127
+ messageId: string;
128
+ verboseLevel: 2 | 3;
129
+ traceType: string;
130
+ // lv2: "thinking" | "planning" | "reasoning" | "decision"
131
+ // lv3: "file_read" | "file_write" | "command_exec" | "search_result" | "tool_call" | "reference"
132
+ content: string;
133
+ metadata?: Record<string, unknown>;
134
+ encrypted?: boolean;
135
+ };
136
+
137
+ // ── Combined v2 Protocol Types ──
138
+
139
+ export type V2Outbound = CoreOutbound | AgentRequestMessage | AgentTraceMessage;
140
+ export type V2Inbound = CoreInbound | AgentResponseMessage;
141
+
142
+ // ── Delegation safety constants ──
143
+
144
+ export const MAX_DELEGATION_DEPTH = 5;
145
+ export const DELEGATION_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
146
+
147
+ // ── Pending request tracking (used by ConnectionDO) ──
148
+
149
+ export type PendingRequest = {
150
+ fromAgentId: string;
151
+ sessionKey: string;
152
+ depth: number;
153
+ createdAt: number;
154
+ };
@@ -0,0 +1,192 @@
1
+ import { Hono } from "hono";
2
+ import type { Env } from "../env.js";
3
+ import { generateId } from "../utils/id.js";
4
+
5
+ /**
6
+ * Agents v2 API — agents are first-class entities (team members),
7
+ * independent of channels. Each agent has a type (engine) and role (persona).
8
+ */
9
+ const agentsV2 = new Hono<{ Bindings: Env; Variables: { userId: string } }>();
10
+
11
+ export type AgentType = "openclaw" | "cursor_cli" | "cursor_cloud" | "claude_code" | "mock";
12
+
13
+ export type AgentV2 = {
14
+ id: string;
15
+ name: string;
16
+ type: AgentType;
17
+ role: string;
18
+ systemPrompt: string;
19
+ skills: Array<{ name: string; description: string }>;
20
+ capabilities: string[];
21
+ status: "connected" | "disconnected";
22
+ lastConnectedAt: number | null;
23
+ createdAt: number;
24
+ updatedAt: number;
25
+ };
26
+
27
+ const CAPABILITIES_BY_TYPE: Record<AgentType, string[]> = {
28
+ openclaw: ["chat", "streaming", "cron", "a2ui", "media", "code_edit", "delegate"],
29
+ cursor_cli: ["chat", "streaming", "code_edit"],
30
+ cursor_cloud: ["chat", "code_edit"],
31
+ claude_code: ["chat", "streaming", "code_edit"],
32
+ mock: ["chat", "streaming"],
33
+ };
34
+
35
+ function dbRowToAgent(row: Record<string, unknown>): AgentV2 {
36
+ return {
37
+ id: row.id as string,
38
+ name: row.name as string,
39
+ type: row.type as AgentType,
40
+ role: row.role as string,
41
+ systemPrompt: row.system_prompt as string,
42
+ skills: JSON.parse((row.skills_json as string) || "[]"),
43
+ capabilities: JSON.parse((row.capabilities as string) || "[]"),
44
+ status: (row.status as "connected" | "disconnected") ?? "disconnected",
45
+ lastConnectedAt: row.last_connected_at as number | null,
46
+ createdAt: row.created_at as number,
47
+ updatedAt: row.updated_at as number,
48
+ };
49
+ }
50
+
51
+ /** GET /api/v2/agents — list all agents for the current user */
52
+ agentsV2.get("/", async (c) => {
53
+ const userId = c.get("userId");
54
+ const { results } = await c.env.DB.prepare(
55
+ `SELECT id, name, type, role, system_prompt, skills_json, capabilities,
56
+ status, last_connected_at, created_at, updated_at
57
+ FROM agents WHERE user_id = ? ORDER BY created_at ASC`,
58
+ )
59
+ .bind(userId)
60
+ .all();
61
+
62
+ return c.json({ agents: (results ?? []).map(dbRowToAgent) });
63
+ });
64
+
65
+ /** POST /api/v2/agents — create a new agent */
66
+ agentsV2.post("/", async (c) => {
67
+ const userId = c.get("userId");
68
+ const body = await c.req.json<{
69
+ name: string;
70
+ type: AgentType;
71
+ role?: string;
72
+ systemPrompt?: string;
73
+ skills?: Array<{ name: string; description: string }>;
74
+ pairingToken?: string;
75
+ apiKey?: string;
76
+ config?: Record<string, unknown>;
77
+ }>();
78
+
79
+ if (!body.name?.trim()) return c.json({ error: "Agent name is required" }, 400);
80
+ if (!body.type) return c.json({ error: "Agent type is required" }, 400);
81
+
82
+ const id = generateId("agt_");
83
+ const capabilities = CAPABILITIES_BY_TYPE[body.type] ?? ["chat"];
84
+
85
+ await c.env.DB.prepare(
86
+ `INSERT INTO agents (id, user_id, name, type, role, system_prompt, skills_json,
87
+ pairing_token, api_key, config_json, capabilities)
88
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
89
+ )
90
+ .bind(
91
+ id,
92
+ userId,
93
+ body.name.trim(),
94
+ body.type,
95
+ body.role ?? "general",
96
+ body.systemPrompt?.trim() ?? "",
97
+ JSON.stringify(body.skills ?? []),
98
+ body.pairingToken ?? null,
99
+ body.apiKey ?? null,
100
+ JSON.stringify(body.config ?? {}),
101
+ JSON.stringify(capabilities),
102
+ )
103
+ .run();
104
+
105
+ const row = await c.env.DB.prepare(
106
+ `SELECT id, name, type, role, system_prompt, skills_json, capabilities,
107
+ status, last_connected_at, created_at, updated_at
108
+ FROM agents WHERE id = ?`,
109
+ )
110
+ .bind(id)
111
+ .first();
112
+
113
+ return c.json(dbRowToAgent(row!), 201);
114
+ });
115
+
116
+ /** GET /api/v2/agents/:id — get a single agent */
117
+ agentsV2.get("/:id", async (c) => {
118
+ const userId = c.get("userId");
119
+ const agentId = c.req.param("id");
120
+
121
+ const row = await c.env.DB.prepare(
122
+ `SELECT id, name, type, role, system_prompt, skills_json, capabilities,
123
+ status, last_connected_at, created_at, updated_at
124
+ FROM agents WHERE id = ? AND user_id = ?`,
125
+ )
126
+ .bind(agentId, userId)
127
+ .first();
128
+
129
+ if (!row) return c.json({ error: "Agent not found" }, 404);
130
+ return c.json(dbRowToAgent(row));
131
+ });
132
+
133
+ /** PATCH /api/v2/agents/:id — update an agent */
134
+ agentsV2.patch("/:id", async (c) => {
135
+ const userId = c.get("userId");
136
+ const agentId = c.req.param("id");
137
+ const body = await c.req.json<{
138
+ name?: string;
139
+ role?: string;
140
+ systemPrompt?: string;
141
+ skills?: Array<{ name: string; description: string }>;
142
+ config?: Record<string, unknown>;
143
+ }>();
144
+
145
+ const sets: string[] = [];
146
+ const values: unknown[] = [];
147
+
148
+ if (body.name !== undefined) { sets.push("name = ?"); values.push(body.name.trim()); }
149
+ if (body.role !== undefined) { sets.push("role = ?"); values.push(body.role); }
150
+ if (body.systemPrompt !== undefined) { sets.push("system_prompt = ?"); values.push(body.systemPrompt.trim()); }
151
+ if (body.skills !== undefined) { sets.push("skills_json = ?"); values.push(JSON.stringify(body.skills)); }
152
+ if (body.config !== undefined) { sets.push("config_json = ?"); values.push(JSON.stringify(body.config)); }
153
+
154
+ if (sets.length === 0) return c.json({ error: "No fields to update" }, 400);
155
+
156
+ sets.push("updated_at = unixepoch()");
157
+ values.push(agentId, userId);
158
+
159
+ await c.env.DB.prepare(
160
+ `UPDATE agents SET ${sets.join(", ")} WHERE id = ? AND user_id = ?`,
161
+ )
162
+ .bind(...values)
163
+ .run();
164
+
165
+ const row = await c.env.DB.prepare(
166
+ `SELECT id, name, type, role, system_prompt, skills_json, capabilities,
167
+ status, last_connected_at, created_at, updated_at
168
+ FROM agents WHERE id = ? AND user_id = ?`,
169
+ )
170
+ .bind(agentId, userId)
171
+ .first();
172
+
173
+ if (!row) return c.json({ error: "Agent not found" }, 404);
174
+ return c.json(dbRowToAgent(row));
175
+ });
176
+
177
+ /** DELETE /api/v2/agents/:id — delete an agent */
178
+ agentsV2.delete("/:id", async (c) => {
179
+ const userId = c.get("userId");
180
+ const agentId = c.req.param("id");
181
+
182
+ const { meta } = await c.env.DB.prepare(
183
+ "DELETE FROM agents WHERE id = ? AND user_id = ?",
184
+ )
185
+ .bind(agentId, userId)
186
+ .run();
187
+
188
+ if (!meta.changes) return c.json({ error: "Agent not found" }, 404);
189
+ return c.json({ ok: true });
190
+ });
191
+
192
+ export { agentsV2 };
@@ -23,10 +23,10 @@ agents.get("/", async (c) => {
23
23
 
24
24
  // 1. Channel-based agents (each channel = one agent, default session = first adhoc task)
25
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",
26
+ "SELECT id, name, provider_agent_id FROM channels WHERE user_id = ? ORDER BY created_at ASC",
27
27
  )
28
28
  .bind(userId)
29
- .all<{ id: string; name: string; openclaw_agent_id: string }>();
29
+ .all<{ id: string; name: string; provider_agent_id: string }>();
30
30
 
31
31
  // Find the "General" channel to associate with the default agent (for session support).
32
32
  // If the user has created a "General" channel (auto-created on first session "+"),
@@ -52,7 +52,7 @@ agents.get("/", async (c) => {
52
52
  .first<{ session_key: string }>();
53
53
 
54
54
  const sessionKey =
55
- task?.session_key ?? `agent:${ch.openclaw_agent_id}:botschat:${userId}:adhoc`;
55
+ task?.session_key ?? `agent:${ch.provider_agent_id}:botschat:${userId}:adhoc`;
56
56
  list.push({
57
57
  id: ch.id,
58
58
  name: ch.name,
@@ -357,7 +357,6 @@ 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);
361
360
 
362
361
  // Delete all user media from R2
363
362
  const prefix = `${userId}/`;
@@ -9,14 +9,14 @@ channels.get("/", async (c) => {
9
9
  const userId = c.get("userId");
10
10
 
11
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",
12
+ "SELECT id, name, description, provider_agent_id, system_prompt, created_at, updated_at FROM channels WHERE user_id = ? ORDER BY created_at DESC",
13
13
  )
14
14
  .bind(userId)
15
15
  .all<{
16
16
  id: string;
17
17
  name: string;
18
18
  description: string;
19
- openclaw_agent_id: string;
19
+ provider_agent_id: string;
20
20
  system_prompt: string;
21
21
  created_at: number;
22
22
  updated_at: number;
@@ -27,7 +27,7 @@ channels.get("/", async (c) => {
27
27
  id: r.id,
28
28
  name: r.name,
29
29
  description: r.description,
30
- openclawAgentId: r.openclaw_agent_id,
30
+ providerAgentId: r.provider_agent_id,
31
31
  systemPrompt: r.system_prompt,
32
32
  createdAt: r.created_at,
33
33
  updatedAt: r.updated_at,
@@ -38,10 +38,10 @@ channels.get("/", async (c) => {
38
38
  /** POST /api/channels — create a new channel */
39
39
  channels.post("/", async (c) => {
40
40
  const userId = c.get("userId");
41
- const { name, description, openclawAgentId, systemPrompt } = await c.req.json<{
41
+ const { name, description, providerAgentId, systemPrompt } = await c.req.json<{
42
42
  name: string;
43
43
  description?: string;
44
- openclawAgentId?: string;
44
+ providerAgentId?: string;
45
45
  systemPrompt?: string;
46
46
  }>();
47
47
 
@@ -52,7 +52,7 @@ channels.post("/", async (c) => {
52
52
  const id = generateId("ch_");
53
53
  // Default agent ID derived from channel name (slug)
54
54
  const agentId =
55
- openclawAgentId?.trim() ||
55
+ providerAgentId?.trim() ||
56
56
  name
57
57
  .trim()
58
58
  .toLowerCase()
@@ -60,7 +60,7 @@ channels.post("/", async (c) => {
60
60
  .replace(/^-|-$/g, "");
61
61
 
62
62
  await c.env.DB.prepare(
63
- "INSERT INTO channels (id, user_id, name, description, openclaw_agent_id, system_prompt) VALUES (?, ?, ?, ?, ?, ?)",
63
+ "INSERT INTO channels (id, user_id, name, description, provider_agent_id, system_prompt) VALUES (?, ?, ?, ?, ?, ?)",
64
64
  )
65
65
  .bind(id, userId, name.trim(), description?.trim() ?? "", agentId, systemPrompt?.trim() ?? "")
66
66
  .run();
@@ -88,7 +88,7 @@ channels.post("/", async (c) => {
88
88
  id,
89
89
  name: name.trim(),
90
90
  description: description?.trim() ?? "",
91
- openclawAgentId: agentId,
91
+ providerAgentId: agentId,
92
92
  systemPrompt: systemPrompt?.trim() ?? "",
93
93
  },
94
94
  201,
@@ -101,14 +101,14 @@ channels.get("/:id", async (c) => {
101
101
  const channelId = c.req.param("id");
102
102
 
103
103
  const row = await c.env.DB.prepare(
104
- "SELECT id, name, description, openclaw_agent_id, system_prompt, created_at, updated_at FROM channels WHERE id = ? AND user_id = ?",
104
+ "SELECT id, name, description, provider_agent_id, system_prompt, created_at, updated_at FROM channels WHERE id = ? AND user_id = ?",
105
105
  )
106
106
  .bind(channelId, userId)
107
107
  .first<{
108
108
  id: string;
109
109
  name: string;
110
110
  description: string;
111
- openclaw_agent_id: string;
111
+ provider_agent_id: string;
112
112
  system_prompt: string;
113
113
  created_at: number;
114
114
  updated_at: number;
@@ -120,7 +120,7 @@ channels.get("/:id", async (c) => {
120
120
  id: row.id,
121
121
  name: row.name,
122
122
  description: row.description,
123
- openclawAgentId: row.openclaw_agent_id,
123
+ providerAgentId: row.provider_agent_id,
124
124
  systemPrompt: row.system_prompt,
125
125
  createdAt: row.created_at,
126
126
  updatedAt: row.updated_at,