@swarmclawai/swarmclaw 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (209) hide show
  1. package/README.md +21 -4
  2. package/bin/server-cmd.js +28 -19
  3. package/next.config.ts +13 -0
  4. package/package.json +3 -1
  5. package/src/app/api/agents/[id]/route.ts +39 -22
  6. package/src/app/api/agents/[id]/thread/route.ts +2 -2
  7. package/src/app/api/agents/route.ts +3 -2
  8. package/src/app/api/agents/trash/route.ts +44 -0
  9. package/src/app/api/clawhub/install/route.ts +2 -2
  10. package/src/app/api/connectors/[id]/route.ts +17 -7
  11. package/src/app/api/connectors/[id]/webhook/route.ts +103 -0
  12. package/src/app/api/connectors/route.ts +6 -3
  13. package/src/app/api/credentials/[id]/route.ts +2 -1
  14. package/src/app/api/credentials/route.ts +2 -2
  15. package/src/app/api/documents/route.ts +2 -2
  16. package/src/app/api/files/serve/route.ts +8 -0
  17. package/src/app/api/knowledge/[id]/route.ts +5 -4
  18. package/src/app/api/knowledge/upload/route.ts +2 -2
  19. package/src/app/api/mcp-servers/[id]/route.ts +11 -14
  20. package/src/app/api/mcp-servers/[id]/test/route.ts +2 -1
  21. package/src/app/api/mcp-servers/[id]/tools/route.ts +2 -1
  22. package/src/app/api/mcp-servers/route.ts +2 -2
  23. package/src/app/api/memory/[id]/route.ts +9 -8
  24. package/src/app/api/memory/route.ts +2 -2
  25. package/src/app/api/memory-images/[filename]/route.ts +2 -1
  26. package/src/app/api/openclaw/agent-files/route.ts +57 -0
  27. package/src/app/api/openclaw/approvals/route.ts +46 -0
  28. package/src/app/api/openclaw/config-sync/route.ts +33 -0
  29. package/src/app/api/openclaw/cron/route.ts +52 -0
  30. package/src/app/api/openclaw/directory/route.ts +27 -0
  31. package/src/app/api/openclaw/discover/route.ts +62 -0
  32. package/src/app/api/openclaw/dotenv-keys/route.ts +18 -0
  33. package/src/app/api/openclaw/exec-config/route.ts +41 -0
  34. package/src/app/api/openclaw/gateway/route.ts +72 -0
  35. package/src/app/api/openclaw/history/route.ts +109 -0
  36. package/src/app/api/openclaw/media/route.ts +53 -0
  37. package/src/app/api/openclaw/models/route.ts +12 -0
  38. package/src/app/api/openclaw/permissions/route.ts +39 -0
  39. package/src/app/api/openclaw/sandbox-env/route.ts +69 -0
  40. package/src/app/api/openclaw/skills/install/route.ts +32 -0
  41. package/src/app/api/openclaw/skills/remove/route.ts +24 -0
  42. package/src/app/api/openclaw/skills/route.ts +82 -0
  43. package/src/app/api/openclaw/sync/route.ts +31 -0
  44. package/src/app/api/orchestrator/run/route.ts +2 -2
  45. package/src/app/api/projects/[id]/route.ts +55 -0
  46. package/src/app/api/projects/route.ts +27 -0
  47. package/src/app/api/providers/[id]/models/route.ts +2 -1
  48. package/src/app/api/providers/[id]/route.ts +13 -15
  49. package/src/app/api/providers/route.ts +2 -2
  50. package/src/app/api/schedules/[id]/route.ts +16 -18
  51. package/src/app/api/schedules/[id]/run/route.ts +4 -3
  52. package/src/app/api/schedules/route.ts +2 -2
  53. package/src/app/api/secrets/[id]/route.ts +16 -17
  54. package/src/app/api/secrets/route.ts +2 -2
  55. package/src/app/api/sessions/[id]/clear/route.ts +2 -1
  56. package/src/app/api/sessions/[id]/deploy/route.ts +2 -1
  57. package/src/app/api/sessions/[id]/devserver/route.ts +2 -1
  58. package/src/app/api/sessions/[id]/edit-resend/route.ts +22 -0
  59. package/src/app/api/sessions/[id]/fork/route.ts +44 -0
  60. package/src/app/api/sessions/[id]/messages/route.ts +20 -2
  61. package/src/app/api/sessions/[id]/retry/route.ts +2 -1
  62. package/src/app/api/sessions/[id]/route.ts +14 -4
  63. package/src/app/api/sessions/route.ts +8 -4
  64. package/src/app/api/skills/[id]/route.ts +23 -21
  65. package/src/app/api/skills/import/route.ts +2 -2
  66. package/src/app/api/skills/route.ts +2 -2
  67. package/src/app/api/tasks/[id]/approve/route.ts +2 -1
  68. package/src/app/api/tasks/[id]/route.ts +6 -5
  69. package/src/app/api/tasks/route.ts +2 -2
  70. package/src/app/api/tts/stream/route.ts +48 -0
  71. package/src/app/api/upload/route.ts +2 -2
  72. package/src/app/api/uploads/[filename]/route.ts +4 -1
  73. package/src/app/api/webhooks/[id]/route.ts +29 -31
  74. package/src/app/api/webhooks/route.ts +2 -2
  75. package/src/app/globals.css +14 -0
  76. package/src/app/layout.tsx +5 -20
  77. package/src/app/page.tsx +3 -24
  78. package/src/cli/index.js +60 -0
  79. package/src/cli/index.ts +1 -1
  80. package/src/cli/spec.js +42 -0
  81. package/src/components/agents/agent-avatar.tsx +45 -0
  82. package/src/components/agents/agent-card.tsx +19 -5
  83. package/src/components/agents/agent-chat-list.tsx +31 -24
  84. package/src/components/agents/agent-files-editor.tsx +185 -0
  85. package/src/components/agents/agent-list.tsx +84 -3
  86. package/src/components/agents/agent-sheet.tsx +147 -14
  87. package/src/components/agents/cron-job-form.tsx +137 -0
  88. package/src/components/agents/exec-config-panel.tsx +147 -0
  89. package/src/components/agents/inspector-panel.tsx +310 -0
  90. package/src/components/agents/openclaw-skills-panel.tsx +230 -0
  91. package/src/components/agents/permission-preset-selector.tsx +79 -0
  92. package/src/components/agents/personality-builder.tsx +111 -0
  93. package/src/components/agents/sandbox-env-panel.tsx +72 -0
  94. package/src/components/agents/skill-install-dialog.tsx +102 -0
  95. package/src/components/agents/trash-list.tsx +109 -0
  96. package/src/components/chat/chat-area.tsx +41 -6
  97. package/src/components/chat/chat-header.tsx +305 -29
  98. package/src/components/chat/chat-preview-panel.tsx +113 -0
  99. package/src/components/chat/exec-approval-card.tsx +89 -0
  100. package/src/components/chat/message-bubble.tsx +218 -36
  101. package/src/components/chat/message-list.tsx +135 -31
  102. package/src/components/chat/streaming-bubble.tsx +59 -10
  103. package/src/components/chat/suggestions-bar.tsx +74 -0
  104. package/src/components/chat/thinking-indicator.tsx +20 -6
  105. package/src/components/chat/tool-call-bubble.tsx +98 -19
  106. package/src/components/chat/tool-request-banner.tsx +20 -2
  107. package/src/components/chat/trace-block.tsx +103 -0
  108. package/src/components/chat/voice-overlay.tsx +80 -0
  109. package/src/components/connectors/connector-list.tsx +6 -2
  110. package/src/components/connectors/connector-sheet.tsx +31 -7
  111. package/src/components/layout/app-layout.tsx +47 -25
  112. package/src/components/projects/project-list.tsx +123 -0
  113. package/src/components/projects/project-sheet.tsx +135 -0
  114. package/src/components/schedules/schedule-list.tsx +3 -1
  115. package/src/components/sessions/new-session-sheet.tsx +6 -6
  116. package/src/components/sessions/session-card.tsx +1 -1
  117. package/src/components/sessions/session-list.tsx +7 -7
  118. package/src/components/settings/gateway-connection-panel.tsx +278 -0
  119. package/src/components/shared/avatar.tsx +13 -2
  120. package/src/components/shared/connector-platform-icon.tsx +4 -0
  121. package/src/components/shared/settings/section-heartbeat.tsx +1 -1
  122. package/src/components/shared/settings/section-orchestrator.tsx +1 -2
  123. package/src/components/shared/settings/section-web-search.tsx +56 -0
  124. package/src/components/shared/settings/settings-page.tsx +74 -0
  125. package/src/components/skills/skill-list.tsx +2 -1
  126. package/src/components/tasks/task-board.tsx +1 -1
  127. package/src/components/tasks/task-list.tsx +5 -2
  128. package/src/components/tasks/task-sheet.tsx +12 -12
  129. package/src/hooks/use-continuous-speech.ts +181 -0
  130. package/src/hooks/use-openclaw-gateway.ts +63 -0
  131. package/src/hooks/use-view-router.ts +52 -0
  132. package/src/hooks/use-voice-conversation.ts +80 -0
  133. package/src/lib/id.ts +6 -0
  134. package/src/lib/notification-sounds.ts +58 -0
  135. package/src/lib/personality-parser.ts +97 -0
  136. package/src/lib/projects.ts +13 -0
  137. package/src/lib/provider-sets.ts +5 -0
  138. package/src/lib/providers/anthropic.ts +14 -1
  139. package/src/lib/providers/index.ts +6 -0
  140. package/src/lib/providers/ollama.ts +9 -1
  141. package/src/lib/providers/openai.ts +9 -1
  142. package/src/lib/providers/openclaw.ts +28 -2
  143. package/src/lib/runtime-loop.ts +2 -2
  144. package/src/lib/server/api-routes.test.ts +5 -6
  145. package/src/lib/server/build-llm.ts +17 -4
  146. package/src/lib/server/chat-execution.ts +82 -6
  147. package/src/lib/server/collection-helpers.ts +54 -0
  148. package/src/lib/server/connectors/bluebubbles.test.ts +217 -0
  149. package/src/lib/server/connectors/bluebubbles.ts +360 -0
  150. package/src/lib/server/connectors/connector-routing.test.ts +1 -1
  151. package/src/lib/server/connectors/googlechat.ts +51 -8
  152. package/src/lib/server/connectors/manager.ts +424 -13
  153. package/src/lib/server/connectors/media.ts +2 -2
  154. package/src/lib/server/connectors/openclaw.ts +65 -0
  155. package/src/lib/server/connectors/pairing.test.ts +99 -0
  156. package/src/lib/server/connectors/pairing.ts +256 -0
  157. package/src/lib/server/connectors/signal.ts +1 -0
  158. package/src/lib/server/connectors/teams.ts +5 -5
  159. package/src/lib/server/connectors/types.ts +10 -0
  160. package/src/lib/server/daemon-state.ts +11 -0
  161. package/src/lib/server/execution-log.ts +3 -3
  162. package/src/lib/server/heartbeat-service.ts +1 -1
  163. package/src/lib/server/knowledge-db.test.ts +2 -33
  164. package/src/lib/server/main-agent-loop.ts +8 -9
  165. package/src/lib/server/main-session.ts +21 -0
  166. package/src/lib/server/memory-db.ts +6 -6
  167. package/src/lib/server/openclaw-approvals.ts +105 -0
  168. package/src/lib/server/openclaw-config-sync.ts +107 -0
  169. package/src/lib/server/openclaw-exec-config.ts +52 -0
  170. package/src/lib/server/openclaw-gateway.ts +291 -0
  171. package/src/lib/server/openclaw-history-merge.ts +36 -0
  172. package/src/lib/server/openclaw-models.ts +56 -0
  173. package/src/lib/server/openclaw-permission-presets.ts +64 -0
  174. package/src/lib/server/openclaw-sync.ts +497 -0
  175. package/src/lib/server/orchestrator-lg.ts +30 -9
  176. package/src/lib/server/orchestrator.ts +4 -4
  177. package/src/lib/server/process-manager.ts +2 -2
  178. package/src/lib/server/queue.ts +24 -11
  179. package/src/lib/server/scheduler.ts +2 -2
  180. package/src/lib/server/session-mailbox.ts +2 -2
  181. package/src/lib/server/session-run-manager.ts +2 -2
  182. package/src/lib/server/session-tools/connector.ts +53 -6
  183. package/src/lib/server/session-tools/crud.ts +3 -3
  184. package/src/lib/server/session-tools/delegate.ts +22 -6
  185. package/src/lib/server/session-tools/file.ts +192 -19
  186. package/src/lib/server/session-tools/index.ts +4 -2
  187. package/src/lib/server/session-tools/memory.ts +2 -2
  188. package/src/lib/server/session-tools/openclaw-nodes.ts +112 -0
  189. package/src/lib/server/session-tools/sandbox.ts +33 -0
  190. package/src/lib/server/session-tools/search-providers.ts +277 -0
  191. package/src/lib/server/session-tools/session-info.ts +2 -2
  192. package/src/lib/server/session-tools/session-tools-wiring.test.ts +2 -2
  193. package/src/lib/server/session-tools/shell.ts +1 -1
  194. package/src/lib/server/session-tools/web.ts +53 -72
  195. package/src/lib/server/storage.ts +74 -11
  196. package/src/lib/server/stream-agent-chat.ts +53 -4
  197. package/src/lib/server/suggestions.ts +20 -0
  198. package/src/lib/server/task-result.test.ts +44 -0
  199. package/src/lib/server/task-result.ts +14 -0
  200. package/src/lib/server/ws-hub.ts +14 -0
  201. package/src/lib/tool-definitions.ts +5 -3
  202. package/src/lib/tts-stream.ts +130 -0
  203. package/src/lib/view-routes.ts +28 -0
  204. package/src/proxy.ts +3 -0
  205. package/src/stores/use-app-store.ts +80 -1
  206. package/src/stores/use-approval-store.ts +78 -0
  207. package/src/stores/use-chat-store.ts +162 -6
  208. package/src/types/index.ts +154 -3
  209. package/tsconfig.json +13 -4
@@ -0,0 +1,99 @@
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 { test } from 'node:test'
6
+ import {
7
+ addAllowedSender,
8
+ approvePairingCode,
9
+ clearConnectorPairingState,
10
+ createOrTouchPairingRequest,
11
+ isSenderAllowed,
12
+ listPendingPairingRequests,
13
+ listStoredAllowedSenders,
14
+ parseAllowFromCsv,
15
+ parsePairingPolicy,
16
+ } from './pairing.ts'
17
+
18
+ function withTempDataDir<T>(fn: (dir: string) => T): T {
19
+ const original = process.env.DATA_DIR
20
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-pairing-test-'))
21
+ process.env.DATA_DIR = tempDir
22
+ try {
23
+ return fn(tempDir)
24
+ } finally {
25
+ if (typeof original === 'string') process.env.DATA_DIR = original
26
+ else delete process.env.DATA_DIR
27
+ fs.rmSync(tempDir, { recursive: true, force: true })
28
+ }
29
+ }
30
+
31
+ test('pairing store creates request, approves code, and persists allowlist', () => {
32
+ withTempDataDir(() => {
33
+ const connectorId = 'pair-test-1'
34
+
35
+ const first = createOrTouchPairingRequest({
36
+ connectorId,
37
+ senderId: '+15551234567',
38
+ senderName: 'Alice',
39
+ channelId: 'chat:1',
40
+ })
41
+ assert.equal(first.created, true)
42
+ assert.equal(first.code.length, 8)
43
+
44
+ const second = createOrTouchPairingRequest({
45
+ connectorId,
46
+ senderId: '+15551234567',
47
+ senderName: 'Alice',
48
+ channelId: 'chat:1',
49
+ })
50
+ assert.equal(second.created, false)
51
+ assert.equal(second.code, first.code)
52
+
53
+ const pendingBefore = listPendingPairingRequests(connectorId)
54
+ assert.equal(pendingBefore.length, 1)
55
+ assert.equal(pendingBefore[0].senderId, '+15551234567')
56
+
57
+ const bad = approvePairingCode(connectorId, 'INVALID')
58
+ assert.equal(bad.ok, false)
59
+
60
+ const approved = approvePairingCode(connectorId, first.code)
61
+ assert.equal(approved.ok, true)
62
+ assert.equal(approved.senderId, '+15551234567')
63
+
64
+ const pendingAfter = listPendingPairingRequests(connectorId)
65
+ assert.equal(pendingAfter.length, 0)
66
+
67
+ const stored = listStoredAllowedSenders(connectorId)
68
+ assert.deepEqual(stored, ['+15551234567'])
69
+
70
+ assert.equal(isSenderAllowed({ connectorId, senderId: '+15551234567' }), true)
71
+ assert.equal(isSenderAllowed({ connectorId, senderId: '+16667778888' }), false)
72
+
73
+ clearConnectorPairingState(connectorId)
74
+ assert.deepEqual(listStoredAllowedSenders(connectorId), [])
75
+ })
76
+ })
77
+
78
+ test('pairing helpers normalize policy and allowFrom csv entries', () => {
79
+ assert.equal(parsePairingPolicy('PAIRING'), 'pairing')
80
+ assert.equal(parsePairingPolicy('allowlist'), 'allowlist')
81
+ assert.equal(parsePairingPolicy('unknown', 'open'), 'open')
82
+
83
+ const list = parseAllowFromCsv(' +1555,TEST@example.com,+1555 , ')
84
+ assert.deepEqual(list, ['+1555', 'test@example.com'])
85
+ })
86
+
87
+ test('addAllowedSender deduplicates and normalizes sender ids', () => {
88
+ withTempDataDir(() => {
89
+ const connectorId = 'pair-test-2'
90
+ const first = addAllowedSender(connectorId, ' TEST@Example.com ')
91
+ assert.equal(first.added, true)
92
+ assert.equal(first.normalized, 'test@example.com')
93
+
94
+ const second = addAllowedSender(connectorId, 'test@example.com')
95
+ assert.equal(second.added, false)
96
+
97
+ assert.deepEqual(listStoredAllowedSenders(connectorId), ['test@example.com'])
98
+ })
99
+ })
@@ -0,0 +1,256 @@
1
+ import crypto from 'node:crypto'
2
+ import fs from 'node:fs'
3
+ import path from 'node:path'
4
+
5
+ const DEFAULT_DATA_DIR = path.join(process.cwd(), 'data')
6
+ const STORE_VERSION = 1
7
+ const PENDING_TTL_MS = 24 * 60 * 60 * 1000
8
+ const MAX_PENDING_PER_CONNECTOR = 100
9
+ const PAIR_CODE_LENGTH = 8
10
+ const PAIR_CODE_ALPHABET = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'
11
+
12
+ function resolveStorePath(): string {
13
+ const dataDir = process.env.DATA_DIR || DEFAULT_DATA_DIR
14
+ return path.join(dataDir, 'connectors', 'pairing-store.json')
15
+ }
16
+
17
+ export type PairingPolicy = 'open' | 'allowlist' | 'pairing' | 'disabled'
18
+
19
+ export interface PairingRequest {
20
+ code: string
21
+ senderId: string
22
+ senderName?: string
23
+ channelId?: string
24
+ createdAt: number
25
+ updatedAt: number
26
+ }
27
+
28
+ interface ConnectorPairingState {
29
+ allowedSenderIds: string[]
30
+ pending: PairingRequest[]
31
+ }
32
+
33
+ interface PairingStore {
34
+ version: number
35
+ connectors: Record<string, ConnectorPairingState>
36
+ }
37
+
38
+ function normalizeSenderId(value: string): string {
39
+ return value.trim().toLowerCase()
40
+ }
41
+
42
+ function dedupe(items: string[]): string[] {
43
+ const seen = new Set<string>()
44
+ const out: string[] = []
45
+ for (const item of items) {
46
+ const normalized = normalizeSenderId(item)
47
+ if (!normalized || seen.has(normalized)) continue
48
+ seen.add(normalized)
49
+ out.push(normalized)
50
+ }
51
+ return out
52
+ }
53
+
54
+ function prunePending(entries: PairingRequest[]): PairingRequest[] {
55
+ const now = Date.now()
56
+ return entries.filter((entry) => {
57
+ if (!entry?.code || !entry?.senderId) return false
58
+ if (!Number.isFinite(entry.createdAt) || !Number.isFinite(entry.updatedAt)) return false
59
+ return (now - entry.updatedAt) <= PENDING_TTL_MS
60
+ }).slice(-MAX_PENDING_PER_CONNECTOR)
61
+ }
62
+
63
+ function emptyStore(): PairingStore {
64
+ return { version: STORE_VERSION, connectors: {} }
65
+ }
66
+
67
+ function loadStore(): PairingStore {
68
+ const storePath = resolveStorePath()
69
+ try {
70
+ if (!fs.existsSync(storePath)) return emptyStore()
71
+ const raw = fs.readFileSync(storePath, 'utf8')
72
+ const parsed = JSON.parse(raw) as PairingStore
73
+ if (!parsed || typeof parsed !== 'object' || typeof parsed.connectors !== 'object') {
74
+ return emptyStore()
75
+ }
76
+
77
+ const normalized: PairingStore = emptyStore()
78
+ for (const [connectorId, value] of Object.entries(parsed.connectors || {})) {
79
+ const state = value as Partial<ConnectorPairingState>
80
+ const allowedSenderIds = dedupe(Array.isArray(state.allowedSenderIds) ? state.allowedSenderIds.map(String) : [])
81
+ const pending = prunePending(Array.isArray(state.pending) ? state.pending as PairingRequest[] : [])
82
+ normalized.connectors[connectorId] = { allowedSenderIds, pending }
83
+ }
84
+ return normalized
85
+ } catch {
86
+ return emptyStore()
87
+ }
88
+ }
89
+
90
+ function saveStore(store: PairingStore): void {
91
+ const storePath = resolveStorePath()
92
+ fs.mkdirSync(path.dirname(storePath), { recursive: true })
93
+ fs.writeFileSync(storePath, `${JSON.stringify(store, null, 2)}\n`)
94
+ }
95
+
96
+ function ensureConnectorState(store: PairingStore, connectorId: string): ConnectorPairingState {
97
+ const existing = store.connectors[connectorId]
98
+ if (existing) {
99
+ existing.allowedSenderIds = dedupe(existing.allowedSenderIds || [])
100
+ existing.pending = prunePending(existing.pending || [])
101
+ return existing
102
+ }
103
+ const created: ConnectorPairingState = {
104
+ allowedSenderIds: [],
105
+ pending: [],
106
+ }
107
+ store.connectors[connectorId] = created
108
+ return created
109
+ }
110
+
111
+ function randomPairCode(existing: Set<string>): string {
112
+ for (let i = 0; i < 256; i++) {
113
+ const bytes = crypto.randomBytes(PAIR_CODE_LENGTH)
114
+ let out = ''
115
+ for (let j = 0; j < PAIR_CODE_LENGTH; j++) {
116
+ out += PAIR_CODE_ALPHABET[bytes[j] % PAIR_CODE_ALPHABET.length]
117
+ }
118
+ if (!existing.has(out)) return out
119
+ }
120
+ throw new Error('Unable to generate unique pairing code')
121
+ }
122
+
123
+ export function parsePairingPolicy(value: unknown, fallback: PairingPolicy = 'open'): PairingPolicy {
124
+ if (typeof value !== 'string') return fallback
125
+ const normalized = value.trim().toLowerCase()
126
+ if (normalized === 'open' || normalized === 'allowlist' || normalized === 'pairing' || normalized === 'disabled') {
127
+ return normalized
128
+ }
129
+ return fallback
130
+ }
131
+
132
+ export function parseAllowFromCsv(value: unknown): string[] {
133
+ if (typeof value !== 'string') return []
134
+ return dedupe(value.split(',').map((item) => item.trim()).filter(Boolean))
135
+ }
136
+
137
+ export function listStoredAllowedSenders(connectorId: string): string[] {
138
+ const store = loadStore()
139
+ const state = ensureConnectorState(store, connectorId)
140
+ return state.allowedSenderIds.slice()
141
+ }
142
+
143
+ export function listPendingPairingRequests(connectorId: string): PairingRequest[] {
144
+ const store = loadStore()
145
+ const state = ensureConnectorState(store, connectorId)
146
+ return state.pending.slice().sort((a, b) => b.updatedAt - a.updatedAt)
147
+ }
148
+
149
+ export function addAllowedSender(connectorId: string, senderId: string): { added: boolean; normalized: string } {
150
+ const normalized = normalizeSenderId(senderId)
151
+ if (!normalized) return { added: false, normalized }
152
+
153
+ const store = loadStore()
154
+ const state = ensureConnectorState(store, connectorId)
155
+ const hasExisting = state.allowedSenderIds.includes(normalized)
156
+ if (!hasExisting) {
157
+ state.allowedSenderIds.push(normalized)
158
+ }
159
+
160
+ // Remove any pending requests for the same sender after approval.
161
+ state.pending = state.pending.filter((entry) => normalizeSenderId(entry.senderId) !== normalized)
162
+
163
+ saveStore(store)
164
+ return { added: !hasExisting, normalized }
165
+ }
166
+
167
+ export function createOrTouchPairingRequest(params: {
168
+ connectorId: string
169
+ senderId: string
170
+ senderName?: string
171
+ channelId?: string
172
+ }): { code: string; created: boolean } {
173
+ const normalized = normalizeSenderId(params.senderId)
174
+ if (!normalized) throw new Error('senderId is required')
175
+
176
+ const store = loadStore()
177
+ const state = ensureConnectorState(store, params.connectorId)
178
+ const now = Date.now()
179
+
180
+ const existing = state.pending.find((entry) => normalizeSenderId(entry.senderId) === normalized)
181
+ if (existing) {
182
+ existing.updatedAt = now
183
+ existing.senderName = params.senderName || existing.senderName
184
+ existing.channelId = params.channelId || existing.channelId
185
+ saveStore(store)
186
+ return { code: existing.code, created: false }
187
+ }
188
+
189
+ const existingCodes = new Set(state.pending.map((entry) => entry.code.toUpperCase()))
190
+ const code = randomPairCode(existingCodes)
191
+ state.pending.push({
192
+ code,
193
+ senderId: normalized,
194
+ senderName: params.senderName,
195
+ channelId: params.channelId,
196
+ createdAt: now,
197
+ updatedAt: now,
198
+ })
199
+ state.pending = prunePending(state.pending)
200
+ saveStore(store)
201
+ return { code, created: true }
202
+ }
203
+
204
+ export function approvePairingCode(connectorId: string, codeRaw: string): {
205
+ ok: boolean
206
+ senderId?: string
207
+ senderName?: string
208
+ reason?: string
209
+ } {
210
+ const code = codeRaw.trim().toUpperCase()
211
+ if (!code) return { ok: false, reason: 'Missing code' }
212
+
213
+ const store = loadStore()
214
+ const state = ensureConnectorState(store, connectorId)
215
+ const idx = state.pending.findIndex((entry) => entry.code.toUpperCase() === code)
216
+ if (idx < 0) return { ok: false, reason: 'Code not found or expired' }
217
+
218
+ const pending = state.pending[idx]
219
+ state.pending.splice(idx, 1)
220
+
221
+ const normalizedSender = normalizeSenderId(pending.senderId)
222
+ if (!state.allowedSenderIds.includes(normalizedSender)) {
223
+ state.allowedSenderIds.push(normalizedSender)
224
+ state.allowedSenderIds = dedupe(state.allowedSenderIds)
225
+ }
226
+
227
+ saveStore(store)
228
+ return {
229
+ ok: true,
230
+ senderId: normalizedSender,
231
+ senderName: pending.senderName,
232
+ }
233
+ }
234
+
235
+ export function isSenderAllowed(params: {
236
+ connectorId: string
237
+ senderId: string
238
+ configAllowFrom?: string[]
239
+ }): boolean {
240
+ const normalized = normalizeSenderId(params.senderId)
241
+ if (!normalized) return false
242
+
243
+ const configSet = new Set((params.configAllowFrom || []).map((item) => normalizeSenderId(item)).filter(Boolean))
244
+ if (configSet.has(normalized)) return true
245
+
246
+ const store = loadStore()
247
+ const state = ensureConnectorState(store, params.connectorId)
248
+ return state.allowedSenderIds.includes(normalized)
249
+ }
250
+
251
+ export function clearConnectorPairingState(connectorId: string): void {
252
+ const store = loadStore()
253
+ if (!store.connectors[connectorId]) return
254
+ delete store.connectors[connectorId]
255
+ saveStore(store)
256
+ }
@@ -147,6 +147,7 @@ export async function handleSignalEvent(
147
147
  senderId: sender,
148
148
  senderName: envelope.sourceName || sender,
149
149
  text,
150
+ isGroup: !!groupId,
150
151
  }
151
152
 
152
153
  try {
@@ -22,11 +22,11 @@ const teams: PlatformConnector = {
22
22
  const conversationReferences = new Map<string, any>()
23
23
  let stopped = false
24
24
 
25
- // Process incoming activities — called from the webhook endpoint
26
- // POST /api/connectors/[id]/webhook should pipe req/res through this
27
- const processActivity = async (req: any, res: any) => {
25
+ // Process incoming activities — called from the webhook endpoint.
26
+ // We use processActivityDirect so this works from Next.js route handlers.
27
+ const processActivity = async (activity: any) => {
28
28
  if (stopped) return
29
- await adapter.processActivity(req, res, async (context: any) => {
29
+ await adapter.processActivityDirect(activity, async (context: any) => {
30
30
  if (context.activity.type !== 'message') return
31
31
  if (!context.activity.text) return
32
32
 
@@ -57,7 +57,7 @@ const teams: PlatformConnector = {
57
57
  })
58
58
  }
59
59
 
60
- // Store processActivity on globalThis so the webhook route can access it
60
+ // Store processActivity on globalThis so the webhook route can access it.
61
61
  const handlerKey = `__swarmclaw_teams_handler_${connector.id}__`
62
62
  ;(globalThis as any)[handlerKey] = processActivity
63
63
 
@@ -21,9 +21,11 @@ export interface InboundMessage {
21
21
  senderId: string // platform-specific user ID
22
22
  senderName: string // display name
23
23
  text: string
24
+ isGroup?: boolean
24
25
  imageUrl?: string
25
26
  media?: InboundMedia[]
26
27
  replyToMessageId?: string
28
+ agentIdOverride?: string
27
29
  }
28
30
 
29
31
  /** A running connector instance */
@@ -50,6 +52,14 @@ export interface ConnectorInstance {
50
52
  authenticated?: boolean
51
53
  /** Whether the connector has existing saved credentials (WhatsApp only) */
52
54
  hasCredentials?: boolean
55
+ /** Rich messaging: send a reaction emoji to a message */
56
+ sendReaction?: (channelId: string, messageId: string, emoji: string) => Promise<void>
57
+ /** Rich messaging: edit a previously sent message */
58
+ editMessage?: (channelId: string, messageId: string, newText: string) => Promise<void>
59
+ /** Rich messaging: delete a message */
60
+ deleteMessage?: (channelId: string, messageId: string) => Promise<void>
61
+ /** Rich messaging: pin a message */
62
+ pinMessage?: (channelId: string, messageId: string) => Promise<void>
53
63
  }
54
64
 
55
65
  /** Platform-specific connector implementation */
@@ -12,6 +12,7 @@ import {
12
12
  getConnectorStatus,
13
13
  } from './connectors/manager'
14
14
  import { startHeartbeatService, stopHeartbeatService, getHeartbeatServiceStatus } from './heartbeat-service'
15
+ import { hasOpenClawAgents, ensureGatewayConnected, disconnectGateway, getGateway } from './openclaw-gateway'
15
16
 
16
17
  const QUEUE_CHECK_INTERVAL = 30_000 // 30 seconds
17
18
  const BROWSER_SWEEP_INTERVAL = 60_000 // 60 seconds
@@ -179,6 +180,16 @@ function startQueueProcessor() {
179
180
  await processNext()
180
181
  ds.lastProcessedAt = Date.now()
181
182
  }
183
+ // OpenClaw gateway lifecycle: lazy connect when openclaw agents exist, disconnect when none remain
184
+ try {
185
+ if (hasOpenClawAgents()) {
186
+ if (!getGateway()?.connected) {
187
+ await ensureGatewayConnected()
188
+ }
189
+ } else if (getGateway()?.connected) {
190
+ disconnectGateway()
191
+ }
192
+ } catch { /* gateway errors are non-fatal */ }
182
193
  }, QUEUE_CHECK_INTERVAL)
183
194
  }
184
195
 
@@ -1,7 +1,7 @@
1
1
  import fs from 'fs'
2
2
  import path from 'path'
3
- import crypto from 'crypto'
4
3
  import Database from 'better-sqlite3'
4
+ import { genId } from '@/lib/id'
5
5
 
6
6
  // ---------------------------------------------------------------------------
7
7
  // Types
@@ -94,7 +94,7 @@ export function logExecution(
94
94
  detail?: Record<string, unknown>
95
95
  },
96
96
  ): string {
97
- const id = crypto.randomBytes(8).toString('hex')
97
+ const id = genId(8)
98
98
  const ts = Date.now()
99
99
  try {
100
100
  insertStmt().run(
@@ -133,7 +133,7 @@ export function logExecutionBatch(
133
133
  const tx = db.transaction(() => {
134
134
  for (const e of entries) {
135
135
  stmt.run(
136
- crypto.randomBytes(8).toString('hex'),
136
+ genId(8),
137
137
  e.sessionId,
138
138
  e.runId ?? null,
139
139
  e.agentId ?? null,
@@ -158,6 +158,7 @@ function buildAgentHeartbeatPrompt(session: any, agent: any, fallbackPrompt: str
158
158
  soul ? `Persona: ${soul.slice(0, 300)}` : '',
159
159
  heartbeatFileContent ? `\nHEARTBEAT.md contents:\n${heartbeatFileContent.slice(0, 2000)}` : '',
160
160
  recentContext ? `Recent conversation:\n${recentContext}` : '',
161
+ fallbackPrompt !== DEFAULT_HEARTBEAT_PROMPT ? `\nAgent instructions:\n${fallbackPrompt}` : '',
161
162
  '',
162
163
  'You are running an autonomous heartbeat tick. Review your goal and recent context.',
163
164
  'If there is meaningful work to do toward your goal, use your tools and take action.',
@@ -167,7 +168,6 @@ function buildAgentHeartbeatPrompt(session: any, agent: any, fallbackPrompt: str
167
168
  'To update your goal or plan, include this line in your response:',
168
169
  '[AGENT_HEARTBEAT_META]{"goal": "your evolved goal", "status": "progress", "next_action": "what you plan to do next"}',
169
170
  'You can evolve your goal as you learn more. Set status to "progress" while working, "ok" when done, "idle" when waiting.',
170
- fallbackPrompt !== DEFAULT_HEARTBEAT_PROMPT ? `\nAdditional instructions: ${fallbackPrompt}` : '',
171
171
  ].filter(Boolean).join('\n')
172
172
  }
173
173
 
@@ -104,39 +104,8 @@ function rowToEntry(row: Record<string, unknown>): MemoryEntry {
104
104
  }
105
105
  }
106
106
 
107
- // ---- Knowledge helpers (mirrors memory-db.ts exported functions) ----
108
-
109
- const MEMORY_FTS_STOP_WORDS = new Set([
110
- 'a', 'an', 'and', 'are', 'as', 'at', 'be', 'by', 'for', 'from', 'how',
111
- 'i', 'if', 'in', 'is', 'it', 'of', 'on', 'or', 'that', 'the', 'this',
112
- 'to', 'was', 'we', 'were', 'what', 'when', 'where', 'which', 'who', 'with',
113
- 'you', 'your',
114
- ])
115
- const MAX_FTS_QUERY_TERMS = 6
116
- const MAX_FTS_TERM_LENGTH = 48
117
-
118
- function buildFtsQuery(input: string): string {
119
- const tokens = String(input || '')
120
- .toLowerCase()
121
- .match(/[a-z0-9][a-z0-9._:/-]*/g) || []
122
- if (!tokens.length) return ''
123
- const unique: string[] = []
124
- const seen = new Set<string>()
125
- for (const token of tokens) {
126
- const term = token.slice(0, MAX_FTS_TERM_LENGTH)
127
- if (term.length < 3) continue
128
- if (MEMORY_FTS_STOP_WORDS.has(term)) continue
129
- if (seen.has(term)) continue
130
- seen.add(term)
131
- unique.push(term)
132
- if (unique.length >= MAX_FTS_QUERY_TERMS) break
133
- }
134
- if (unique.length === 1) {
135
- return unique[0].length >= 5 ? `"${unique[0].replace(/"/g, '')}"` : ''
136
- }
137
- const selected = unique.slice(0, Math.min(4, MAX_FTS_QUERY_TERMS))
138
- return selected.map((term) => `"${term.replace(/"/g, '')}"`).join(' AND ')
139
- }
107
+ // ---- Knowledge helpers (re-exported from memory-db.ts) ----
108
+ import { buildFtsQuery } from './memory-db'
140
109
 
141
110
  function addRawMemory(data: {
142
111
  agentId?: string | null
@@ -1,17 +1,16 @@
1
- import crypto from 'crypto'
1
+ import { genId } from '@/lib/id'
2
2
  import { z } from 'zod'
3
3
  import type { GoalContract, MessageToolEvent } from '@/types'
4
4
  import { loadSessions, saveSessions, loadAgents, saveAgents, loadTasks, saveTasks } from './storage'
5
5
  import { log } from './logger'
6
6
  import { getMemoryDb } from './memory-db'
7
+ import { isProtectedMainSession } from './main-session'
7
8
  import {
8
9
  mergeGoalContracts,
9
10
  parseGoalContractFromText,
10
11
  parseMainLoopPlan,
11
12
  parseMainLoopReview,
12
13
  } from './autonomy-contract'
13
-
14
- const MAIN_SESSION_NAME = '__main__'
15
14
  const MAX_PENDING_EVENTS = 40
16
15
  const MAX_TIMELINE_EVENTS = 80
17
16
  const EVENT_TTL_MS = 7 * 24 * 60 * 60 * 1000
@@ -151,7 +150,7 @@ function appendTimeline(
151
150
  const recent = state.timeline.at(-1)
152
151
  if (recent && recent.source === source && recent.note === normalizedNote && now - recent.at < 45_000) return
153
152
  state.timeline.push({
154
- id: `tl_${crypto.randomBytes(4).toString('hex')}`,
153
+ id: `tl_${genId()}`,
155
154
  at: now,
156
155
  source,
157
156
  note: normalizedNote,
@@ -226,7 +225,7 @@ function normalizeState(raw: any, now = Date.now()): MainLoopState {
226
225
  const text = toOneLine(typeof e?.text === 'string' ? e.text : '')
227
226
  if (!text) return null
228
227
  return {
229
- id: typeof e?.id === 'string' && e.id.trim() ? e.id.trim() : `evt_${crypto.randomBytes(3).toString('hex')}`,
228
+ id: typeof e?.id === 'string' && e.id.trim() ? e.id.trim() : `evt_${genId(3)}`,
230
229
  type: typeof e?.type === 'string' && e.type.trim() ? e.type.trim() : 'event',
231
230
  text,
232
231
  createdAt: typeof e?.createdAt === 'number' ? e.createdAt : now,
@@ -246,7 +245,7 @@ function normalizeState(raw: any, now = Date.now()): MainLoopState {
246
245
  ? entry.status
247
246
  : undefined
248
247
  return {
249
- id: typeof entry?.id === 'string' && entry.id.trim() ? entry.id.trim() : `tl_${crypto.randomBytes(3).toString('hex')}`,
248
+ id: typeof entry?.id === 'string' && entry.id.trim() ? entry.id.trim() : `tl_${genId(3)}`,
250
249
  at: typeof entry?.at === 'number' ? entry.at : now,
251
250
  source: typeof entry?.source === 'string' && entry.source.trim() ? entry.source.trim() : 'event',
252
251
  note,
@@ -303,7 +302,7 @@ function appendEvent(state: MainLoopState, type: string, text: string, now = Dat
303
302
  return false
304
303
  }
305
304
  state.pendingEvents.push({
306
- id: `evt_${crypto.randomBytes(4).toString('hex')}`,
305
+ id: `evt_${genId()}`,
307
306
  type,
308
307
  text: normalizedText,
309
308
  createdAt: now,
@@ -517,7 +516,7 @@ function upsertMissionTask(session: any, state: MainLoopState, now: number): str
517
516
  ].filter(Boolean).join('\n')
518
517
 
519
518
  if (!task) {
520
- const id = crypto.randomBytes(4).toString('hex')
519
+ const id = genId()
521
520
  task = {
522
521
  id,
523
522
  title,
@@ -669,7 +668,7 @@ function buildFollowupPrompt(state: MainLoopState, opts?: { hasMemoryTool?: bool
669
668
  }
670
669
 
671
670
  export function isMainSession(session: any): boolean {
672
- return session?.name === MAIN_SESSION_NAME
671
+ return isProtectedMainSession(session)
673
672
  }
674
673
 
675
674
  export function buildMainLoopHeartbeatPrompt(session: any, fallbackPrompt: string): string {
@@ -0,0 +1,21 @@
1
+ const MAIN_SESSION_NAME = '__main__'
2
+
3
+ export function isProtectedMainSession(session: any): boolean {
4
+ if (!session || typeof session !== 'object') return false
5
+ if (session.mainSession === true) return true
6
+
7
+ const name = typeof session.name === 'string' ? session.name.trim() : ''
8
+ if (name === MAIN_SESSION_NAME) return true
9
+
10
+ const id = typeof session.id === 'string' ? session.id.trim() : ''
11
+ if (id.startsWith('main-')) return true
12
+
13
+ return false
14
+ }
15
+
16
+ export function ensureMainSessionFlag(session: any): void {
17
+ if (!session || typeof session !== 'object') return
18
+ if (isProtectedMainSession(session)) {
19
+ session.mainSession = true
20
+ }
21
+ }
@@ -1,7 +1,7 @@
1
1
  import Database from 'better-sqlite3'
2
2
  import path from 'path'
3
- import crypto from 'crypto'
4
3
  import fs from 'fs'
4
+ import { genId } from '@/lib/id'
5
5
  import type { MemoryEntry, FileReference, MemoryImage, MemoryReference } from '@/types'
6
6
  import { getEmbedding, cosineSimilarity, serializeEmbedding, deserializeEmbedding } from './embeddings'
7
7
  import { loadSettings } from './storage'
@@ -20,12 +20,12 @@ const IMAGES_DIR = path.join(DATA_DIR, 'memory-images')
20
20
 
21
21
  const MAX_IMAGE_INPUT_BYTES = 10 * 1024 * 1024 // 10MB
22
22
  const IMAGE_EXT_WHITELIST = new Set(['.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp', '.tiff'])
23
- const MAX_FTS_QUERY_TERMS = 6
24
- const MAX_FTS_TERM_LENGTH = 48
23
+ export const MAX_FTS_QUERY_TERMS = 6
24
+ export const MAX_FTS_TERM_LENGTH = 48
25
25
  const MAX_FTS_RESULT_ROWS = 30
26
26
  const MAX_MERGED_RESULTS = 50
27
27
 
28
- const MEMORY_FTS_STOP_WORDS = new Set([
28
+ export const MEMORY_FTS_STOP_WORDS = new Set([
29
29
  'a', 'an', 'and', 'are', 'as', 'at', 'be', 'by', 'for', 'from', 'how',
30
30
  'i', 'if', 'in', 'is', 'it', 'of', 'on', 'or', 'that', 'the', 'this',
31
31
  'to', 'was', 'we', 'were', 'what', 'when', 'where', 'which', 'who', 'with',
@@ -184,7 +184,7 @@ function canonicalText(value: unknown): string {
184
184
  .trim()
185
185
  }
186
186
 
187
- function buildFtsQuery(input: string): string {
187
+ export function buildFtsQuery(input: string): string {
188
188
  const tokens = String(input || '')
189
189
  .toLowerCase()
190
190
  .match(/[a-z0-9][a-z0-9._:/-]*/g) || []
@@ -546,7 +546,7 @@ function initDb() {
546
546
 
547
547
  return {
548
548
  add(data: Omit<MemoryEntry, 'id' | 'createdAt' | 'updatedAt'>): MemoryEntry {
549
- const id = crypto.randomBytes(6).toString('hex')
549
+ const id = genId(6)
550
550
  const now = Date.now()
551
551
  const references = normalizeReferences(data.references, data.filePaths)
552
552
  const legacyFilePaths = referencesToLegacyFilePaths(references)