@swarmclawai/swarmclaw 0.7.2 → 0.7.4

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 (274) hide show
  1. package/README.md +116 -50
  2. package/bin/package-manager.js +157 -0
  3. package/bin/package-manager.test.js +90 -0
  4. package/bin/server-cmd.js +38 -7
  5. package/bin/swarmclaw.js +54 -4
  6. package/bin/update-cmd.js +48 -10
  7. package/bin/update-cmd.test.js +55 -0
  8. package/package.json +8 -3
  9. package/scripts/postinstall.mjs +26 -0
  10. package/src/app/api/agents/[id]/route.ts +43 -0
  11. package/src/app/api/agents/[id]/thread/route.ts +39 -8
  12. package/src/app/api/agents/route.ts +35 -2
  13. package/src/app/api/auth/route.ts +77 -8
  14. package/src/app/api/chatrooms/[id]/chat/route.ts +22 -6
  15. package/src/app/api/chatrooms/[id]/pins/route.ts +2 -1
  16. package/src/app/api/chatrooms/[id]/reactions/route.ts +2 -1
  17. package/src/app/api/chatrooms/[id]/route.ts +6 -0
  18. package/src/app/api/chats/[id]/browser/route.ts +5 -1
  19. package/src/app/api/chats/[id]/chat/route.ts +7 -3
  20. package/src/app/api/chats/[id]/messages/route.ts +19 -13
  21. package/src/app/api/chats/[id]/route.ts +30 -0
  22. package/src/app/api/chats/[id]/stop/route.ts +6 -1
  23. package/src/app/api/chats/heartbeat/route.ts +2 -1
  24. package/src/app/api/chats/route.ts +23 -1
  25. package/src/app/api/connectors/[id]/doctor/route.ts +26 -0
  26. package/src/app/api/connectors/doctor/route.ts +13 -0
  27. package/src/app/api/external-agents/[id]/heartbeat/route.ts +33 -0
  28. package/src/app/api/external-agents/[id]/route.ts +31 -0
  29. package/src/app/api/external-agents/register/route.ts +3 -0
  30. package/src/app/api/external-agents/route.ts +66 -0
  31. package/src/app/api/files/open/route.ts +16 -14
  32. package/src/app/api/gateways/[id]/health/route.ts +28 -0
  33. package/src/app/api/gateways/[id]/route.ts +79 -0
  34. package/src/app/api/gateways/route.ts +57 -0
  35. package/src/app/api/memory/maintenance/route.ts +11 -1
  36. package/src/app/api/openclaw/agent-files/route.ts +27 -4
  37. package/src/app/api/openclaw/gateway/route.ts +10 -7
  38. package/src/app/api/openclaw/skills/route.ts +12 -4
  39. package/src/app/api/plugins/dependencies/route.ts +24 -0
  40. package/src/app/api/plugins/install/route.ts +15 -92
  41. package/src/app/api/plugins/route.ts +3 -26
  42. package/src/app/api/plugins/settings/route.ts +17 -12
  43. package/src/app/api/plugins/ui/route.ts +1 -0
  44. package/src/app/api/providers/[id]/discover-models/route.ts +27 -0
  45. package/src/app/api/schedules/[id]/route.ts +38 -9
  46. package/src/app/api/schedules/route.ts +51 -28
  47. package/src/app/api/settings/route.ts +55 -17
  48. package/src/app/api/setup/doctor/route.ts +6 -4
  49. package/src/app/api/tasks/[id]/route.ts +16 -6
  50. package/src/app/api/tasks/bulk/route.ts +3 -3
  51. package/src/app/api/tasks/route.ts +9 -4
  52. package/src/app/api/webhooks/[id]/route.ts +8 -1
  53. package/src/app/page.tsx +135 -17
  54. package/src/cli/binary.test.js +142 -0
  55. package/src/cli/index.js +38 -11
  56. package/src/cli/index.test.js +195 -0
  57. package/src/cli/index.ts +21 -12
  58. package/src/cli/server-cmd.test.js +59 -0
  59. package/src/cli/spec.js +20 -2
  60. package/src/components/agents/agent-card.tsx +15 -12
  61. package/src/components/agents/agent-chat-list.tsx +101 -1
  62. package/src/components/agents/agent-list.tsx +46 -9
  63. package/src/components/agents/agent-sheet.tsx +456 -23
  64. package/src/components/agents/inspector-panel.tsx +110 -49
  65. package/src/components/agents/sandbox-env-panel.tsx +4 -1
  66. package/src/components/auth/access-key-gate.tsx +36 -97
  67. package/src/components/auth/setup-wizard.tsx +970 -275
  68. package/src/components/chat/chat-area.tsx +70 -27
  69. package/src/components/chat/chat-card.tsx +6 -21
  70. package/src/components/chat/chat-header.tsx +263 -366
  71. package/src/components/chat/chat-list.tsx +62 -26
  72. package/src/components/chat/checkpoint-timeline.tsx +1 -1
  73. package/src/components/chat/message-list.tsx +145 -19
  74. package/src/components/chatrooms/chatroom-input.tsx +96 -33
  75. package/src/components/chatrooms/chatroom-list.tsx +141 -72
  76. package/src/components/chatrooms/chatroom-message.tsx +7 -6
  77. package/src/components/chatrooms/chatroom-sheet.tsx +13 -1
  78. package/src/components/chatrooms/chatroom-tool-request-banner.tsx +5 -2
  79. package/src/components/chatrooms/chatroom-view.tsx +422 -209
  80. package/src/components/chatrooms/reaction-picker.tsx +38 -33
  81. package/src/components/connectors/connector-list.tsx +265 -127
  82. package/src/components/connectors/connector-sheet.tsx +217 -0
  83. package/src/components/gateways/gateway-sheet.tsx +567 -0
  84. package/src/components/home/home-view.tsx +128 -4
  85. package/src/components/input/chat-input.tsx +135 -86
  86. package/src/components/layout/app-layout.tsx +385 -194
  87. package/src/components/layout/mobile-header.tsx +26 -8
  88. package/src/components/memory/memory-browser.tsx +71 -6
  89. package/src/components/memory/memory-card.tsx +18 -0
  90. package/src/components/memory/memory-detail.tsx +58 -31
  91. package/src/components/memory/memory-sheet.tsx +32 -4
  92. package/src/components/plugins/plugin-list.tsx +15 -3
  93. package/src/components/plugins/plugin-sheet.tsx +118 -9
  94. package/src/components/projects/project-detail.tsx +189 -1
  95. package/src/components/providers/provider-list.tsx +158 -2
  96. package/src/components/providers/provider-sheet.tsx +81 -70
  97. package/src/components/shared/agent-picker-list.tsx +2 -2
  98. package/src/components/shared/bottom-sheet.tsx +31 -15
  99. package/src/components/shared/command-palette.tsx +111 -24
  100. package/src/components/shared/confirm-dialog.tsx +45 -30
  101. package/src/components/shared/model-combobox.tsx +90 -8
  102. package/src/components/shared/settings/plugin-manager.tsx +20 -4
  103. package/src/components/shared/settings/section-capability-policy.tsx +105 -0
  104. package/src/components/shared/settings/section-heartbeat.tsx +88 -6
  105. package/src/components/shared/settings/section-orchestrator.tsx +6 -3
  106. package/src/components/shared/settings/section-runtime-loop.tsx +5 -5
  107. package/src/components/shared/settings/section-secrets.tsx +6 -6
  108. package/src/components/shared/settings/section-user-preferences.tsx +1 -1
  109. package/src/components/shared/settings/section-voice.tsx +5 -1
  110. package/src/components/shared/settings/section-web-search.tsx +10 -2
  111. package/src/components/shared/settings/settings-page.tsx +248 -47
  112. package/src/components/tasks/approvals-panel.tsx +211 -18
  113. package/src/components/tasks/task-board.tsx +242 -46
  114. package/src/components/ui/dialog.tsx +2 -2
  115. package/src/components/usage/metrics-dashboard.tsx +74 -1
  116. package/src/components/wallets/wallet-approval-dialog.tsx +59 -54
  117. package/src/components/wallets/wallet-panel.tsx +17 -5
  118. package/src/components/webhooks/webhook-sheet.tsx +7 -7
  119. package/src/lib/auth.ts +17 -0
  120. package/src/lib/chat-streaming-state.test.ts +108 -0
  121. package/src/lib/chat-streaming-state.ts +108 -0
  122. package/src/lib/heartbeat-defaults.ts +48 -0
  123. package/src/lib/memory-presentation.ts +59 -0
  124. package/src/lib/openclaw-agent-id.test.ts +14 -0
  125. package/src/lib/openclaw-agent-id.ts +31 -0
  126. package/src/lib/provider-model-discovery-client.ts +29 -0
  127. package/src/lib/providers/index.ts +12 -5
  128. package/src/lib/runtime-loop.ts +105 -3
  129. package/src/lib/safe-storage.ts +6 -1
  130. package/src/lib/server/agent-assignment.test.ts +112 -0
  131. package/src/lib/server/agent-assignment.ts +169 -0
  132. package/src/lib/server/agent-runtime-config.test.ts +141 -0
  133. package/src/lib/server/agent-runtime-config.ts +277 -0
  134. package/src/lib/server/approval-connector-notify.test.ts +253 -0
  135. package/src/lib/server/approvals-auto-approve.test.ts +264 -0
  136. package/src/lib/server/approvals.ts +483 -75
  137. package/src/lib/server/autonomy-runtime.test.ts +341 -0
  138. package/src/lib/server/browser-state.test.ts +118 -0
  139. package/src/lib/server/browser-state.ts +123 -0
  140. package/src/lib/server/build-llm.test.ts +44 -0
  141. package/src/lib/server/build-llm.ts +11 -4
  142. package/src/lib/server/builtin-plugins.ts +34 -0
  143. package/src/lib/server/chat-execution-heartbeat.test.ts +40 -0
  144. package/src/lib/server/chat-execution-tool-events.test.ts +219 -0
  145. package/src/lib/server/chat-execution.ts +402 -125
  146. package/src/lib/server/chatroom-health.test.ts +26 -0
  147. package/src/lib/server/chatroom-health.ts +2 -3
  148. package/src/lib/server/chatroom-helpers.test.ts +74 -2
  149. package/src/lib/server/chatroom-helpers.ts +144 -11
  150. package/src/lib/server/chatroom-session-persistence.test.ts +87 -0
  151. package/src/lib/server/connectors/discord.ts +175 -11
  152. package/src/lib/server/connectors/doctor.test.ts +80 -0
  153. package/src/lib/server/connectors/doctor.ts +116 -0
  154. package/src/lib/server/connectors/manager.ts +994 -130
  155. package/src/lib/server/connectors/policy.test.ts +222 -0
  156. package/src/lib/server/connectors/policy.ts +452 -0
  157. package/src/lib/server/connectors/slack.ts +189 -10
  158. package/src/lib/server/connectors/telegram.ts +65 -15
  159. package/src/lib/server/connectors/thread-context.test.ts +44 -0
  160. package/src/lib/server/connectors/thread-context.ts +72 -0
  161. package/src/lib/server/connectors/types.ts +41 -11
  162. package/src/lib/server/daemon-state.ts +62 -3
  163. package/src/lib/server/data-dir.ts +13 -0
  164. package/src/lib/server/delegation-jobs.test.ts +140 -0
  165. package/src/lib/server/delegation-jobs.ts +248 -0
  166. package/src/lib/server/document-utils.test.ts +47 -0
  167. package/src/lib/server/document-utils.ts +397 -0
  168. package/src/lib/server/eval/agent-regression.test.ts +47 -0
  169. package/src/lib/server/eval/agent-regression.ts +1742 -0
  170. package/src/lib/server/eval/runner.ts +11 -1
  171. package/src/lib/server/eval/store.ts +2 -1
  172. package/src/lib/server/heartbeat-service.ts +23 -43
  173. package/src/lib/server/heartbeat-source.test.ts +22 -0
  174. package/src/lib/server/heartbeat-source.ts +7 -0
  175. package/src/lib/server/identity-continuity.test.ts +77 -0
  176. package/src/lib/server/identity-continuity.ts +127 -0
  177. package/src/lib/server/mailbox-utils.ts +347 -0
  178. package/src/lib/server/main-agent-loop.ts +31 -964
  179. package/src/lib/server/memory-db.ts +4 -6
  180. package/src/lib/server/memory-tiers.ts +40 -0
  181. package/src/lib/server/openclaw-agent-resolver.test.ts +70 -0
  182. package/src/lib/server/openclaw-agent-resolver.ts +128 -0
  183. package/src/lib/server/openclaw-exec-config.ts +6 -5
  184. package/src/lib/server/openclaw-gateway.ts +123 -36
  185. package/src/lib/server/openclaw-skills-normalize.test.ts +56 -0
  186. package/src/lib/server/openclaw-skills-normalize.ts +136 -0
  187. package/src/lib/server/openclaw-sync.ts +3 -2
  188. package/src/lib/server/orchestrator-lg.ts +18 -8
  189. package/src/lib/server/orchestrator.ts +5 -4
  190. package/src/lib/server/playwright-proxy.mjs +27 -3
  191. package/src/lib/server/plugins.test.ts +215 -0
  192. package/src/lib/server/plugins.ts +832 -69
  193. package/src/lib/server/provider-health.ts +33 -3
  194. package/src/lib/server/provider-model-discovery.ts +481 -0
  195. package/src/lib/server/queue.ts +4 -21
  196. package/src/lib/server/runtime-settings.test.ts +119 -0
  197. package/src/lib/server/runtime-settings.ts +12 -92
  198. package/src/lib/server/schedule-normalization.ts +187 -0
  199. package/src/lib/server/scheduler.ts +2 -0
  200. package/src/lib/server/session-archive-memory.test.ts +85 -0
  201. package/src/lib/server/session-archive-memory.ts +230 -0
  202. package/src/lib/server/session-mailbox.ts +8 -18
  203. package/src/lib/server/session-reset-policy.test.ts +99 -0
  204. package/src/lib/server/session-reset-policy.ts +311 -0
  205. package/src/lib/server/session-run-manager.ts +33 -80
  206. package/src/lib/server/session-tools/autonomy-tools.test.ts +128 -0
  207. package/src/lib/server/session-tools/calendar.ts +2 -12
  208. package/src/lib/server/session-tools/connector.ts +109 -8
  209. package/src/lib/server/session-tools/context.ts +14 -2
  210. package/src/lib/server/session-tools/crawl.ts +447 -0
  211. package/src/lib/server/session-tools/crud.ts +96 -34
  212. package/src/lib/server/session-tools/delegate-fallback.test.ts +219 -0
  213. package/src/lib/server/session-tools/delegate.ts +406 -20
  214. package/src/lib/server/session-tools/discovery-approvals.test.ts +170 -0
  215. package/src/lib/server/session-tools/discovery.ts +40 -12
  216. package/src/lib/server/session-tools/document.ts +283 -0
  217. package/src/lib/server/session-tools/email.ts +1 -3
  218. package/src/lib/server/session-tools/extract.ts +137 -0
  219. package/src/lib/server/session-tools/file-normalize.test.ts +98 -0
  220. package/src/lib/server/session-tools/file-send.test.ts +84 -1
  221. package/src/lib/server/session-tools/file.ts +243 -24
  222. package/src/lib/server/session-tools/http.ts +9 -3
  223. package/src/lib/server/session-tools/human-loop.ts +227 -0
  224. package/src/lib/server/session-tools/image-gen.ts +1 -3
  225. package/src/lib/server/session-tools/index.ts +87 -2
  226. package/src/lib/server/session-tools/mailbox.ts +276 -0
  227. package/src/lib/server/session-tools/manage-schedules.test.ts +137 -0
  228. package/src/lib/server/session-tools/memory.ts +35 -3
  229. package/src/lib/server/session-tools/monitor.ts +162 -12
  230. package/src/lib/server/session-tools/normalize-tool-args.ts +17 -14
  231. package/src/lib/server/session-tools/openclaw-nodes.test.ts +111 -0
  232. package/src/lib/server/session-tools/openclaw-nodes.ts +86 -20
  233. package/src/lib/server/session-tools/platform-normalize.test.ts +142 -0
  234. package/src/lib/server/session-tools/platform.ts +142 -4
  235. package/src/lib/server/session-tools/plugin-creator.ts +95 -25
  236. package/src/lib/server/session-tools/primitive-tools.test.ts +257 -0
  237. package/src/lib/server/session-tools/replicate.ts +1 -3
  238. package/src/lib/server/session-tools/sandbox.ts +51 -92
  239. package/src/lib/server/session-tools/schedule.ts +20 -10
  240. package/src/lib/server/session-tools/session-info.ts +58 -4
  241. package/src/lib/server/session-tools/session-tools-wiring.test.ts +54 -17
  242. package/src/lib/server/session-tools/shell.ts +2 -2
  243. package/src/lib/server/session-tools/subagent.ts +195 -27
  244. package/src/lib/server/session-tools/table.ts +587 -0
  245. package/src/lib/server/session-tools/wallet.ts +13 -10
  246. package/src/lib/server/session-tools/web-browser-config.test.ts +39 -0
  247. package/src/lib/server/session-tools/web.ts +947 -108
  248. package/src/lib/server/storage.ts +255 -10
  249. package/src/lib/server/stream-agent-chat.test.ts +61 -0
  250. package/src/lib/server/stream-agent-chat.ts +185 -25
  251. package/src/lib/server/structured-extract.test.ts +72 -0
  252. package/src/lib/server/structured-extract.ts +373 -0
  253. package/src/lib/server/task-mention.test.ts +16 -2
  254. package/src/lib/server/task-mention.ts +61 -11
  255. package/src/lib/server/tool-aliases.ts +80 -12
  256. package/src/lib/server/tool-capability-policy.ts +7 -1
  257. package/src/lib/server/tool-retry.ts +2 -0
  258. package/src/lib/server/watch-jobs.test.ts +173 -0
  259. package/src/lib/server/watch-jobs.ts +532 -0
  260. package/src/lib/server/ws-hub.ts +5 -3
  261. package/src/lib/setup-defaults.ts +352 -11
  262. package/src/lib/tool-definitions.ts +3 -4
  263. package/src/lib/validation/schemas.test.ts +26 -0
  264. package/src/lib/validation/schemas.ts +62 -1
  265. package/src/lib/ws-client.ts +14 -12
  266. package/src/proxy.ts +5 -5
  267. package/src/stores/use-app-store.ts +43 -7
  268. package/src/stores/use-chat-store.ts +31 -2
  269. package/src/stores/use-chatroom-store.ts +153 -26
  270. package/src/types/index.ts +470 -44
  271. package/src/app/api/chats/[id]/main-loop/route.ts +0 -94
  272. package/src/components/chat/new-chat-sheet.tsx +0 -253
  273. package/src/lib/server/main-session.ts +0 -17
  274. package/src/lib/server/session-run-manager.test.ts +0 -26
@@ -0,0 +1,26 @@
1
+ import { describe, it } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import type { Agent } from '@/types'
4
+ import { filterHealthyChatroomAgents } from './chatroom-health'
5
+
6
+ describe('filterHealthyChatroomAgents', () => {
7
+ it('treats providers with default endpoints as healthy without explicit agent endpoints', () => {
8
+ const now = Date.now()
9
+ const agents: Record<string, Agent> = {
10
+ agent_writer: {
11
+ id: 'agent_writer',
12
+ name: 'Writer',
13
+ description: '',
14
+ systemPrompt: '',
15
+ provider: 'ollama',
16
+ model: 'glm-5:cloud',
17
+ createdAt: now,
18
+ updatedAt: now,
19
+ },
20
+ }
21
+
22
+ const result = filterHealthyChatroomAgents(['agent_writer'], agents)
23
+ assert.deepEqual(result.healthyAgentIds, ['agent_writer'])
24
+ assert.deepEqual(result.skipped, [])
25
+ })
26
+ })
@@ -1,6 +1,6 @@
1
1
  import { getProvider } from '@/lib/providers'
2
2
  import type { Agent } from '@/types'
3
- import { resolveApiKey } from './chatroom-helpers'
3
+ import { resolveAgentApiEndpoint, resolveApiKey } from './chatroom-helpers'
4
4
  import { isProviderCoolingDown } from './provider-health'
5
5
 
6
6
  export interface ChatroomAgentHealthSkip {
@@ -47,7 +47,7 @@ export function filterHealthyChatroomAgents(
47
47
  skipped.push({ agentId, reason: 'missing_api_credentials' })
48
48
  continue
49
49
  }
50
- if (providerInfo.requiresEndpoint && !agent.apiEndpoint) {
50
+ if (providerInfo.requiresEndpoint && !resolveAgentApiEndpoint(agent)) {
51
51
  skipped.push({ agentId, reason: 'missing_api_endpoint' })
52
52
  continue
53
53
  }
@@ -57,4 +57,3 @@ export function filterHealthyChatroomAgents(
57
57
 
58
58
  return { healthyAgentIds, skipped }
59
59
  }
60
-
@@ -1,7 +1,15 @@
1
1
  import { describe, it } from 'node:test'
2
2
  import assert from 'node:assert/strict'
3
3
  import type { Agent, Chatroom } from '@/types'
4
- import { parseMentions, compactChatroomMessages, buildHistoryForAgent } from './chatroom-helpers'
4
+ import {
5
+ parseMentions,
6
+ compactChatroomMessages,
7
+ buildHistoryForAgent,
8
+ buildSyntheticSession,
9
+ resolveChatroomWorkspaceDir,
10
+ resolveAgentApiEndpoint,
11
+ resolveReplyTargetAgentId,
12
+ } from './chatroom-helpers'
5
13
 
6
14
  function makeAgents(): Record<string, Agent> {
7
15
  const now = Date.now()
@@ -37,6 +45,48 @@ describe('chatroom-helpers', () => {
37
45
  assert.deepEqual(mentions, ['default', 'agent_analyst'])
38
46
  })
39
47
 
48
+ it('routes reply-only messages back to the replied-to agent', () => {
49
+ const agents = makeAgents()
50
+ const memberIds = ['default', 'agent_analyst']
51
+ const replyTargetAgentId = resolveReplyTargetAgentId('agent-msg', [
52
+ {
53
+ id: 'agent-msg',
54
+ senderId: 'default',
55
+ senderName: 'Assistant',
56
+ role: 'assistant',
57
+ text: 'Here is the previous answer.',
58
+ mentions: [],
59
+ reactions: [],
60
+ time: Date.now(),
61
+ },
62
+ ], memberIds)
63
+ const mentions = parseMentions('Can you expand on that?', agents, memberIds, { replyTargetAgentId })
64
+ assert.deepEqual(mentions, ['default'])
65
+ })
66
+
67
+ it('keeps explicit mentions ahead of reply-based implicit targeting', () => {
68
+ const agents = makeAgents()
69
+ const memberIds = ['default', 'agent_analyst']
70
+ const mentions = parseMentions('Actually @Analyst should take this one.', agents, memberIds, { replyTargetAgentId: 'default' })
71
+ assert.deepEqual(mentions, ['agent_analyst'])
72
+ })
73
+
74
+ it('ignores replies to non-agent messages', () => {
75
+ const replyTargetAgentId = resolveReplyTargetAgentId('user-msg', [
76
+ {
77
+ id: 'user-msg',
78
+ senderId: 'user',
79
+ senderName: 'You',
80
+ role: 'user',
81
+ text: 'Question',
82
+ mentions: [],
83
+ reactions: [],
84
+ time: Date.now(),
85
+ },
86
+ ], ['default', 'agent_analyst'])
87
+ assert.equal(replyTargetAgentId, null)
88
+ })
89
+
40
90
  it('compacts long chatrooms with a persisted summary message', () => {
41
91
  const now = Date.now()
42
92
  const chatroom: Chatroom = {
@@ -90,5 +140,27 @@ describe('chatroom-helpers', () => {
90
140
  const attachmentMarkers = history.filter((msg) => msg.text.includes('[Attached:')).length
91
141
  assert.ok(attachmentMarkers <= 6)
92
142
  })
93
- })
94
143
 
144
+ it('resolves default provider endpoints for chatroom sessions', () => {
145
+ const now = Date.now()
146
+ const agent: Agent = {
147
+ id: 'agent_writer',
148
+ name: 'Writer',
149
+ description: '',
150
+ systemPrompt: '',
151
+ provider: 'ollama',
152
+ model: 'glm-5:cloud',
153
+ createdAt: now,
154
+ updatedAt: now,
155
+ }
156
+
157
+ assert.equal(resolveAgentApiEndpoint(agent), 'http://localhost:11434')
158
+ assert.equal(buildSyntheticSession(agent, 'room-1').apiEndpoint, 'http://localhost:11434')
159
+ })
160
+
161
+ it('keeps chatroom execution inside the workspace instead of the repo root', () => {
162
+ const cwd = buildSyntheticSession(makeAgents().default, 'room-safe').cwd
163
+ assert.equal(cwd, resolveChatroomWorkspaceDir('room-safe'))
164
+ assert.match(cwd, /chatrooms[\/\\]room-safe$/)
165
+ })
166
+ })
@@ -1,8 +1,15 @@
1
+ import fs from 'fs'
1
2
  import os from 'os'
2
- import { loadSettings, loadSkills, loadCredentials, decryptKey } from './storage'
3
+ import path from 'path'
4
+ import { loadSettings, loadSkills, loadCredentials, decryptKey, loadSessions, saveSessions } from './storage'
3
5
  import { buildCurrentDateTimePromptContext } from './prompt-runtime-context'
6
+ import { buildIdentityContinuityContext } from './identity-continuity'
4
7
  import { genId } from '@/lib/id'
5
- import type { Chatroom, ChatroomMember, Agent, Session, Message } from '@/types'
8
+ import { getProvider } from '@/lib/providers'
9
+ import { normalizeProviderEndpoint } from '@/lib/openclaw-endpoint'
10
+ import { WORKSPACE_DIR } from './data-dir'
11
+ import { applyResolvedRoute, resolvePrimaryAgentRoute } from './agent-runtime-config'
12
+ import type { Chatroom, ChatroomMember, Agent, Session, Message, ChatroomMessage } from '@/types'
6
13
 
7
14
  /** Resolve API key from an agent's credentialId */
8
15
  export function resolveApiKey(credentialId: string | null | undefined): string | null {
@@ -34,6 +41,14 @@ export function isMuted(chatroom: Chatroom, agentId: string): boolean {
34
41
  return new Date(member.mutedUntil).getTime() > Date.now()
35
42
  }
36
43
 
44
+ export function resolveAgentApiEndpoint(agent: Agent): string | null {
45
+ const explicit = normalizeProviderEndpoint(agent.provider, agent.apiEndpoint ?? null)
46
+ if (explicit) return explicit
47
+ const provider = getProvider(agent.provider)
48
+ if (!provider?.defaultEndpoint) return null
49
+ return normalizeProviderEndpoint(agent.provider, provider.defaultEndpoint) || provider.defaultEndpoint.replace(/\/+$/, '')
50
+ }
51
+
37
52
  const COMPACTION_PREFIX = '[Conversation summary]'
38
53
 
39
54
  function normalizeMentionToken(raw: string): string {
@@ -53,7 +68,12 @@ function truncateText(text: string, max: number): string {
53
68
  import { isImplicitlyMentioned } from './chatroom-orchestration'
54
69
 
55
70
  /** Parse @mentions from message text, returns matching agentIds */
56
- export function parseMentions(text: string, agents: Record<string, Agent>, memberIds: string[]): string[] {
71
+ export function parseMentions(
72
+ text: string,
73
+ agents: Record<string, Agent>,
74
+ memberIds: string[],
75
+ opts?: { replyTargetAgentId?: string | null },
76
+ ): string[] {
57
77
  if (/@all\b/i.test(text)) return [...memberIds]
58
78
  const mentionPattern = /(?:^|[\s(])@([a-zA-Z0-9._-]+)/g
59
79
  const mentioned: string[] = []
@@ -73,8 +93,17 @@ export function parseMentions(text: string, agents: Record<string, Agent>, membe
73
93
  }
74
94
  }
75
95
 
76
- // 2. Implicit mentions (OpenClaw Style - Reading the room)
77
- // Only if no explicit mentions found yet
96
+ // 2. Reply-based implicit mention
97
+ // Only if no explicit mentions were found.
98
+ if (mentioned.length === 0) {
99
+ const replyTargetAgentId = opts?.replyTargetAgentId
100
+ if (replyTargetAgentId && memberIds.includes(replyTargetAgentId)) {
101
+ mentioned.push(replyTargetAgentId)
102
+ }
103
+ }
104
+
105
+ // 3. Implicit mentions (OpenClaw Style - Reading the room)
106
+ // Only if no explicit mentions were found.
78
107
  if (mentioned.length === 0) {
79
108
  for (const id of memberIds) {
80
109
  const agent = agents[id]
@@ -87,6 +116,19 @@ export function parseMentions(text: string, agents: Record<string, Agent>, membe
87
116
  return mentioned
88
117
  }
89
118
 
119
+ export function resolveReplyTargetAgentId(
120
+ replyToId: string | undefined,
121
+ messages: ChatroomMessage[],
122
+ memberIds: string[],
123
+ ): string | null {
124
+ if (!replyToId) return null
125
+ const replyMsg = messages.find((m) => m.id === replyToId)
126
+ if (!replyMsg) return null
127
+ if (replyMsg.role !== 'assistant') return null
128
+ if (!memberIds.includes(replyMsg.senderId)) return null
129
+ return replyMsg.senderId
130
+ }
131
+
90
132
  /**
91
133
  * Persisted chatroom compaction so long-lived rooms stay inside context budgets.
92
134
  * Returns true when the message list was compacted.
@@ -172,24 +214,113 @@ export function buildChatroomSystemPrompt(chatroom: Chatroom, agents: Record<str
172
214
  }
173
215
 
174
216
  /** Build a synthetic session object for an agent in a chatroom */
175
- export function buildSyntheticSession(agent: Agent, chatroomId: string): Session {
217
+ export function resolveChatroomWorkspaceDir(chatroomId: string): string {
218
+ return path.join(WORKSPACE_DIR, 'chatrooms', chatroomId)
219
+ }
220
+
221
+ export function resolveSyntheticSessionId(chatroomId: string, agentId: string): string {
222
+ return `chatroom-${chatroomId}-${agentId}`
223
+ }
224
+
225
+ function buildEmptyDelegateResumeIds(): NonNullable<Session['delegateResumeIds']> {
176
226
  return {
177
- id: `chatroom-${chatroomId}-${agent.id}`,
227
+ claudeCode: null,
228
+ codex: null,
229
+ opencode: null,
230
+ gemini: null,
231
+ }
232
+ }
233
+
234
+ export function buildSyntheticSession(agent: Agent, chatroomId: string): Session {
235
+ const roomWorkspace = resolveChatroomWorkspaceDir(chatroomId)
236
+ fs.mkdirSync(roomWorkspace, { recursive: true })
237
+ const now = Date.now()
238
+ return applyResolvedRoute({
239
+ id: resolveSyntheticSessionId(chatroomId, agent.id),
178
240
  name: `Chatroom session for ${agent.name}`,
179
- cwd: process.cwd(),
241
+ cwd: roomWorkspace,
180
242
  user: 'chatroom',
181
243
  provider: agent.provider,
182
244
  model: agent.model,
183
245
  credentialId: agent.credentialId ?? null,
184
246
  fallbackCredentialIds: agent.fallbackCredentialIds,
185
- apiEndpoint: agent.apiEndpoint ?? null,
247
+ apiEndpoint: resolveAgentApiEndpoint(agent),
186
248
  claudeSessionId: null,
249
+ codexThreadId: null,
250
+ opencodeSessionId: null,
251
+ delegateResumeIds: buildEmptyDelegateResumeIds(),
187
252
  messages: [],
188
- createdAt: Date.now(),
189
- lastActiveAt: Date.now(),
253
+ createdAt: now,
254
+ lastActiveAt: now,
255
+ sessionType: 'human',
190
256
  plugins: agent.plugins || agent.tools || [],
191
257
  agentId: agent.id,
258
+ }, resolvePrimaryAgentRoute(agent))
259
+ }
260
+
261
+ export function ensureSyntheticSession(agent: Agent, chatroomId: string): Session {
262
+ const roomWorkspace = resolveChatroomWorkspaceDir(chatroomId)
263
+ fs.mkdirSync(roomWorkspace, { recursive: true })
264
+ const sessionId = resolveSyntheticSessionId(chatroomId, agent.id)
265
+ const sessions = loadSessions()
266
+ const now = Date.now()
267
+ const existing = sessions[sessionId]
268
+ const session: Session = existing
269
+ ? applyResolvedRoute({
270
+ ...existing,
271
+ id: sessionId,
272
+ name: `Chatroom session for ${agent.name}`,
273
+ cwd: roomWorkspace,
274
+ user: 'chatroom',
275
+ provider: agent.provider,
276
+ model: agent.model,
277
+ credentialId: agent.credentialId ?? null,
278
+ fallbackCredentialIds: Array.isArray(agent.fallbackCredentialIds) ? [...agent.fallbackCredentialIds] : [],
279
+ apiEndpoint: resolveAgentApiEndpoint(agent),
280
+ sessionType: existing.sessionType || 'human',
281
+ agentId: agent.id,
282
+ plugins: agent.plugins || agent.tools || [],
283
+ tools: agent.plugins || agent.tools || [],
284
+ createdAt: existing.createdAt || now,
285
+ lastActiveAt: now,
286
+ }, resolvePrimaryAgentRoute(agent))
287
+ : applyResolvedRoute({
288
+ ...buildSyntheticSession(agent, chatroomId),
289
+ fallbackCredentialIds: Array.isArray(agent.fallbackCredentialIds) ? [...agent.fallbackCredentialIds] : [],
290
+ lastActiveAt: now,
291
+ tools: agent.plugins || agent.tools || [],
292
+ }, resolvePrimaryAgentRoute(agent))
293
+
294
+ if (!Array.isArray(session.messages)) session.messages = []
295
+ if (!session.delegateResumeIds || typeof session.delegateResumeIds !== 'object') {
296
+ session.delegateResumeIds = buildEmptyDelegateResumeIds()
192
297
  }
298
+ if (session.codexThreadId === undefined) session.codexThreadId = null
299
+ if (session.opencodeSessionId === undefined) session.opencodeSessionId = null
300
+ sessions[sessionId] = session
301
+ saveSessions(sessions)
302
+ return session
303
+ }
304
+
305
+ export function appendSyntheticSessionMessage(
306
+ sessionId: string,
307
+ role: 'user' | 'assistant',
308
+ text: string,
309
+ ): void {
310
+ const trimmed = String(text || '').trim()
311
+ if (!trimmed) return
312
+ const sessions = loadSessions()
313
+ const session = sessions[sessionId]
314
+ if (!session) return
315
+ if (!Array.isArray(session.messages)) session.messages = []
316
+ session.messages.push({
317
+ role,
318
+ text: trimmed,
319
+ time: Date.now(),
320
+ })
321
+ session.lastActiveAt = Date.now()
322
+ sessions[sessionId] = session
323
+ saveSessions(sessions)
193
324
  }
194
325
 
195
326
  /** Build agent's system prompt including skills and identity context */
@@ -207,6 +338,8 @@ export function buildAgentSystemPromptForChatroom(agent: Agent): string {
207
338
  if (agent.description) identityLines.push(`Description: ${agent.description}`)
208
339
  identityLines.push('I should always embody this identity in the group chat. I am a persistent companion with my own soul and presence.')
209
340
  parts.push(identityLines.join('\n'))
341
+ const continuityBlock = buildIdentityContinuityContext(null, agent)
342
+ if (continuityBlock) parts.push(continuityBlock)
210
343
 
211
344
  // 2. Runtime & Capabilities (OpenClaw Style)
212
345
  const runtimeLines = [
@@ -0,0 +1,87 @@
1
+ import assert from 'node:assert/strict'
2
+ import fs from 'node:fs'
3
+ import os from 'node:os'
4
+ import path from 'node:path'
5
+ import { spawnSync } from 'node:child_process'
6
+ import { describe, it } from 'node:test'
7
+
8
+ const repoRoot = path.resolve(path.dirname(new URL(import.meta.url).pathname), '../../..')
9
+
10
+ function runWithTempDataDir(script: string) {
11
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-chatroom-session-'))
12
+ try {
13
+ const result = spawnSync(process.execPath, ['--import', 'tsx', '--input-type=module', '--eval', script], {
14
+ cwd: repoRoot,
15
+ env: {
16
+ ...process.env,
17
+ DATA_DIR: path.join(tempDir, 'data'),
18
+ WORKSPACE_DIR: path.join(tempDir, 'workspace'),
19
+ },
20
+ encoding: 'utf-8',
21
+ })
22
+ assert.equal(result.status, 0, result.stderr || result.stdout || 'subprocess failed')
23
+ const lines = (result.stdout || '')
24
+ .trim()
25
+ .split('\n')
26
+ .map((line) => line.trim())
27
+ .filter(Boolean)
28
+ const jsonLine = [...lines].reverse().find((line) => line.startsWith('{'))
29
+ return JSON.parse(jsonLine || '{}')
30
+ } finally {
31
+ fs.rmSync(tempDir, { recursive: true, force: true })
32
+ }
33
+ }
34
+
35
+ describe('chatroom synthetic session persistence', () => {
36
+ it('reuses stored synthetic sessions and preserves delegate resume state', () => {
37
+ const output = runWithTempDataDir(`
38
+ const helpersMod = await import('./src/lib/server/chatroom-helpers.ts')
39
+ const helpers = helpersMod.default || helpersMod
40
+ const storageMod = await import('./src/lib/server/storage.ts')
41
+ const storage = storageMod.default || storageMod
42
+ const now = Date.now()
43
+ const agent = {
44
+ id: 'default',
45
+ name: 'Molly',
46
+ description: '',
47
+ systemPrompt: '',
48
+ provider: 'openai',
49
+ model: 'gpt-4o',
50
+ createdAt: now,
51
+ updatedAt: now,
52
+ plugins: ['delegate'],
53
+ }
54
+
55
+ const first = helpers.ensureSyntheticSession(agent, 'room-1')
56
+ helpers.appendSyntheticSessionMessage(first.id, 'user', 'first prompt')
57
+
58
+ const sessions = storage.loadSessions()
59
+ sessions[first.id].delegateResumeIds = {
60
+ claudeCode: null,
61
+ codex: 'resume-123',
62
+ opencode: null,
63
+ gemini: null,
64
+ }
65
+ storage.saveSessions(sessions)
66
+
67
+ const second = helpers.ensureSyntheticSession({ ...agent, model: 'gpt-4.1' }, 'room-1')
68
+ console.log(JSON.stringify({
69
+ sessionId: second.id,
70
+ cwd: second.cwd,
71
+ model: second.model,
72
+ messageCount: second.messages.length,
73
+ firstMessage: second.messages[0]?.text || '',
74
+ delegateResumeIds: second.delegateResumeIds,
75
+ plugins: second.plugins || [],
76
+ }))
77
+ `)
78
+
79
+ assert.equal(output.sessionId, 'chatroom-room-1-default')
80
+ assert.match(String(output.cwd), /chatrooms[\/\\]room-1$/)
81
+ assert.equal(output.model, 'gpt-4.1')
82
+ assert.equal(output.messageCount, 1)
83
+ assert.equal(output.firstMessage, 'first prompt')
84
+ assert.equal(output.delegateResumeIds?.codex, 'resume-123')
85
+ assert.deepEqual(output.plugins, ['delegate'])
86
+ })
87
+ })
@@ -2,9 +2,94 @@ import { Client, GatewayIntentBits, Events, Partials, AttachmentBuilder } from '
2
2
  import fs from 'fs'
3
3
  import path from 'path'
4
4
  import type { Connector } from '@/types'
5
- import type { PlatformConnector, ConnectorInstance, InboundMessage } from './types'
5
+ import type { PlatformConnector, ConnectorInstance, InboundMessage, InboundThreadHistoryEntry } from './types'
6
6
  import { downloadInboundMediaToUpload, inferInboundMediaType } from './media'
7
- import { isNoMessage } from './manager'
7
+ import { getConnectorReplySendOptions, isNoMessage, recordConnectorOutboundDelivery } from './manager'
8
+
9
+ function buildDiscordThreadTitle(params: {
10
+ threadName?: string
11
+ channelName?: string
12
+ starterText?: string
13
+ fallbackId: string
14
+ }): string {
15
+ const threadName = String(params.threadName || '').trim()
16
+ if (threadName) return threadName
17
+ const snippet = String(params.starterText || '').replace(/\s+/g, ' ').trim().slice(0, 56)
18
+ if (snippet) return `${params.channelName || 'Discord'} · ${snippet}`
19
+ return `${params.channelName || 'Discord'} thread ${params.fallbackId}`
20
+ }
21
+
22
+ function discordSenderName(message: any): string {
23
+ return message?.member?.displayName
24
+ || message?.author?.globalName
25
+ || message?.author?.displayName
26
+ || message?.author?.username
27
+ || 'Unknown'
28
+ }
29
+
30
+ async function hydrateDiscordThreadContext(message: any, inbound: InboundMessage): Promise<void> {
31
+ const channel = message.channel as any
32
+ const isThread = typeof channel?.isThread === 'function' && channel.isThread()
33
+
34
+ try {
35
+ if (isThread) {
36
+ const starter = typeof channel.fetchStarterMessage === 'function'
37
+ ? await channel.fetchStarterMessage().catch(() => null)
38
+ : null
39
+ const historyCollection = channel?.messages && typeof channel.messages.fetch === 'function'
40
+ ? await channel.messages.fetch({ limit: 8, before: message.id }).catch(() => null)
41
+ : null
42
+ const historyMessages = historyCollection
43
+ ? Array.from(historyCollection.values()).sort((a: any, b: any) => (a.createdTimestamp || 0) - (b.createdTimestamp || 0))
44
+ : []
45
+ const history: InboundThreadHistoryEntry[] = historyMessages
46
+ .filter((item: any) => item?.content?.trim())
47
+ .map((item: any) => ({
48
+ role: (item.author?.bot ? 'assistant' : 'user') as 'assistant' | 'user',
49
+ senderName: discordSenderName(item),
50
+ text: item.content,
51
+ messageId: item.id,
52
+ }))
53
+
54
+ inbound.threadParentChannelId = channel?.parentId || undefined
55
+ inbound.threadParentChannelName = channel?.parent?.name || undefined
56
+ inbound.threadStarterText = starter?.content?.trim() || undefined
57
+ inbound.threadStarterSenderName = starter ? discordSenderName(starter) : undefined
58
+ inbound.threadTitle = buildDiscordThreadTitle({
59
+ threadName: channel?.name,
60
+ channelName: inbound.threadParentChannelName || inbound.channelName,
61
+ starterText: inbound.threadStarterText,
62
+ fallbackId: inbound.threadId || inbound.channelId,
63
+ })
64
+ inbound.threadPersonaLabel = inbound.threadTitle
65
+ inbound.threadHistory = history.length ? history : undefined
66
+ return
67
+ }
68
+
69
+ if (message.reference?.messageId && typeof message.fetchReference === 'function') {
70
+ const starter = await message.fetchReference().catch(() => null)
71
+ if (!starter) return
72
+ inbound.threadStarterText = starter.content?.trim() || undefined
73
+ inbound.threadStarterSenderName = discordSenderName(starter)
74
+ inbound.threadParentChannelId = inbound.channelId
75
+ inbound.threadParentChannelName = inbound.channelName
76
+ inbound.threadTitle = buildDiscordThreadTitle({
77
+ channelName: inbound.channelName,
78
+ starterText: inbound.threadStarterText,
79
+ fallbackId: starter.id || inbound.replyToMessageId || inbound.channelId,
80
+ })
81
+ inbound.threadPersonaLabel = inbound.threadTitle
82
+ inbound.threadHistory = [{
83
+ role: (starter.author?.bot ? 'assistant' : 'user') as 'assistant' | 'user',
84
+ senderName: discordSenderName(starter),
85
+ text: starter.content || '',
86
+ messageId: starter.id,
87
+ }].filter((entry) => entry.text.trim().length > 0)
88
+ }
89
+ } catch (err: unknown) {
90
+ console.warn(`[discord] Thread context bootstrap failed: ${err instanceof Error ? err.message : String(err)}`)
91
+ }
92
+ }
8
93
 
9
94
  const discord: PlatformConnector = {
10
95
  async start(connector, botToken, onMessage): Promise<ConnectorInstance> {
@@ -23,6 +108,23 @@ const discord: PlatformConnector = {
23
108
  ? connector.config.channelIds.split(',').map((s) => s.trim()).filter(Boolean)
24
109
  : null
25
110
 
111
+ async function resolveTextChannel(targetChannelId: string) {
112
+ const channel = await client.channels.fetch(targetChannelId)
113
+ if (!channel || !('send' in channel) || typeof (channel as any).send !== 'function') {
114
+ throw new Error(`Cannot send to channel ${targetChannelId}`)
115
+ }
116
+ return channel as any
117
+ }
118
+
119
+ async function resolveChannelMessage(targetChannelId: string, messageId: string) {
120
+ const channel = await resolveTextChannel(targetChannelId)
121
+ const messages = (channel as any).messages
122
+ if (!messages || typeof messages.fetch !== 'function') {
123
+ throw new Error(`Channel ${targetChannelId} does not support message actions`)
124
+ }
125
+ return await messages.fetch(messageId)
126
+ }
127
+
26
128
  client.on(Events.MessageCreate, async (message) => {
27
129
  console.log(`[discord] Message from ${message.author.username} in ${message.channel.type === 1 ? 'DM' : '#' + ('name' in message.channel ? (message.channel as any).name : message.channelId)}: ${message.content.slice(0, 80)}`)
28
130
  // Ignore bot messages
@@ -71,9 +173,17 @@ const discord: PlatformConnector = {
71
173
  senderId: message.author.id,
72
174
  senderName: message.author.displayName || message.author.username,
73
175
  text: message.content || (media.length > 0 ? '(media message)' : ''),
176
+ isGroup: message.channel.type !== 1,
177
+ messageId: message.id,
178
+ mentionsBot: client.user ? message.mentions.users.has(client.user.id) : false,
74
179
  imageUrl: firstImage?.url,
75
180
  media,
181
+ replyToMessageId: message.reference?.messageId || undefined,
182
+ threadId: typeof (message.channel as any).isThread === 'function' && (message.channel as any).isThread()
183
+ ? message.channelId
184
+ : undefined,
76
185
  }
186
+ await hydrateDiscordThreadContext(message, inbound)
77
187
 
78
188
  try {
79
189
  // Show typing indicator
@@ -82,16 +192,41 @@ const discord: PlatformConnector = {
82
192
 
83
193
  if (isNoMessage(response)) return
84
194
 
195
+ const replyOptions = getConnectorReplySendOptions({ connectorId: connector.id, inbound })
196
+ const targetChannelId = replyOptions.threadId || inbound.channelId
197
+ const sendChunk = async (chunk: string, isFirstChunk: boolean) => {
198
+ const channel = await resolveTextChannel(targetChannelId)
199
+ const payload: Record<string, unknown> = {
200
+ content: chunk,
201
+ allowedMentions: { repliedUser: false },
202
+ }
203
+ if (isFirstChunk && replyOptions.replyToMessageId) {
204
+ payload.reply = {
205
+ messageReference: replyOptions.replyToMessageId,
206
+ failIfNotExists: false,
207
+ }
208
+ }
209
+ const sent = await channel.send(payload)
210
+ return String(sent.id || '')
211
+ }
212
+
213
+ let lastMessageId: string | undefined
85
214
  // Discord has a 2000 char limit per message
86
215
  if (response.length <= 2000) {
87
- await message.channel.send(response)
216
+ lastMessageId = await sendChunk(response, true)
88
217
  } else {
89
218
  // Split into chunks
90
219
  const chunks = response.match(/[\s\S]{1,1990}/g) || [response]
91
- for (const chunk of chunks) {
92
- await message.channel.send(chunk)
220
+ for (let i = 0; i < chunks.length; i += 1) {
221
+ lastMessageId = await sendChunk(chunks[i], i === 0)
93
222
  }
94
223
  }
224
+ await recordConnectorOutboundDelivery({
225
+ connectorId: connector.id,
226
+ inbound,
227
+ messageId: lastMessageId,
228
+ state: 'sent',
229
+ })
95
230
  } catch (err: any) {
96
231
  console.error(`[discord] Error handling message:`, err.message)
97
232
  try {
@@ -109,10 +244,8 @@ const discord: PlatformConnector = {
109
244
  return client.isReady()
110
245
  },
111
246
  async sendMessage(channelId, text, options) {
112
- const channel = await client.channels.fetch(channelId)
113
- if (!channel || !('send' in channel) || typeof (channel as any).send !== 'function') {
114
- throw new Error(`Cannot send to channel ${channelId}`)
115
- }
247
+ const targetChannelId = options?.threadId?.trim() || channelId
248
+ const channel = await resolveTextChannel(targetChannelId)
116
249
 
117
250
  const files: AttachmentBuilder[] = []
118
251
  if (options?.mediaPath) {
@@ -125,12 +258,43 @@ const discord: PlatformConnector = {
125
258
  }
126
259
 
127
260
  const content = options?.caption || text || undefined
128
- const msg = await (channel as any).send({
261
+ const payload: Record<string, unknown> = {
129
262
  content: content || (files.length ? undefined : '(empty)'),
130
263
  files: files.length ? files : undefined,
131
- })
264
+ }
265
+ if (options?.replyToMessageId) {
266
+ payload.reply = {
267
+ messageReference: options.replyToMessageId,
268
+ failIfNotExists: false,
269
+ }
270
+ }
271
+
272
+ const msg = await channel.send(payload)
132
273
  return { messageId: msg.id }
133
274
  },
275
+ async sendReaction(channelId, messageId, emoji) {
276
+ const message = await resolveChannelMessage(channelId, messageId)
277
+ await message.react(emoji)
278
+ },
279
+ async editMessage(channelId, messageId, newText) {
280
+ const message = await resolveChannelMessage(channelId, messageId)
281
+ await message.edit(newText)
282
+ },
283
+ async deleteMessage(channelId, messageId) {
284
+ const message = await resolveChannelMessage(channelId, messageId)
285
+ await message.delete()
286
+ },
287
+ async pinMessage(channelId, messageId) {
288
+ const message = await resolveChannelMessage(channelId, messageId)
289
+ await message.pin()
290
+ },
291
+ async sendTyping(channelId, options) {
292
+ const targetChannelId = options?.threadId?.trim() || channelId
293
+ const channel = await resolveTextChannel(targetChannelId)
294
+ if (typeof channel.sendTyping === 'function') {
295
+ await channel.sendTyping()
296
+ }
297
+ },
134
298
  async stop() {
135
299
  client.destroy()
136
300
  console.log(`[discord] Bot disconnected`)