@swarmclawai/swarmclaw 0.6.7 → 0.7.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 (203) hide show
  1. package/README.md +82 -39
  2. package/next.config.ts +31 -6
  3. package/package.json +3 -2
  4. package/src/app/api/agents/[id]/thread/route.ts +1 -0
  5. package/src/app/api/agents/route.ts +19 -5
  6. package/src/app/api/approvals/route.ts +22 -0
  7. package/src/app/api/chatrooms/[id]/chat/route.ts +4 -0
  8. package/src/app/api/clawhub/install/route.ts +2 -2
  9. package/src/app/api/eval/run/route.ts +37 -0
  10. package/src/app/api/eval/scenarios/route.ts +24 -0
  11. package/src/app/api/eval/suite/route.ts +29 -0
  12. package/src/app/api/mcp-servers/[id]/conformance/route.ts +26 -0
  13. package/src/app/api/mcp-servers/[id]/invoke/route.ts +81 -0
  14. package/src/app/api/memory/graph/route.ts +46 -0
  15. package/src/app/api/memory/route.ts +36 -5
  16. package/src/app/api/notifications/route.ts +3 -0
  17. package/src/app/api/plugins/install/route.ts +57 -5
  18. package/src/app/api/plugins/marketplace/route.ts +73 -22
  19. package/src/app/api/plugins/route.ts +61 -1
  20. package/src/app/api/plugins/ui/route.ts +34 -0
  21. package/src/app/api/sessions/[id]/checkpoints/route.ts +31 -0
  22. package/src/app/api/sessions/[id]/restore/route.ts +36 -0
  23. package/src/app/api/settings/route.ts +62 -0
  24. package/src/app/api/setup/doctor/route.ts +22 -5
  25. package/src/app/api/souls/[id]/route.ts +65 -0
  26. package/src/app/api/souls/route.ts +70 -0
  27. package/src/app/api/tasks/[id]/approve/route.ts +4 -3
  28. package/src/app/api/tasks/[id]/route.ts +16 -3
  29. package/src/app/api/tasks/route.ts +10 -2
  30. package/src/app/api/usage/route.ts +9 -2
  31. package/src/app/globals.css +27 -0
  32. package/src/app/page.tsx +10 -5
  33. package/src/cli/index.js +37 -0
  34. package/src/components/activity/activity-feed.tsx +9 -2
  35. package/src/components/agents/agent-avatar.tsx +5 -1
  36. package/src/components/agents/agent-card.tsx +55 -9
  37. package/src/components/agents/agent-sheet.tsx +112 -34
  38. package/src/components/agents/inspector-panel.tsx +1 -1
  39. package/src/components/agents/soul-library-picker.tsx +84 -13
  40. package/src/components/auth/access-key-gate.tsx +63 -54
  41. package/src/components/auth/user-picker.tsx +37 -32
  42. package/src/components/chat/activity-moment.tsx +2 -0
  43. package/src/components/chat/chat-area.tsx +11 -0
  44. package/src/components/chat/chat-header.tsx +69 -25
  45. package/src/components/chat/chat-tool-toggles.tsx +2 -2
  46. package/src/components/chat/checkpoint-timeline.tsx +112 -0
  47. package/src/components/chat/code-block.tsx +3 -1
  48. package/src/components/chat/exec-approval-card.tsx +8 -1
  49. package/src/components/chat/message-bubble.tsx +164 -4
  50. package/src/components/chat/message-list.tsx +46 -4
  51. package/src/components/chat/session-approval-card.tsx +80 -0
  52. package/src/components/chat/session-debug-panel.tsx +106 -84
  53. package/src/components/chat/streaming-bubble.tsx +6 -5
  54. package/src/components/chat/task-approval-card.tsx +78 -0
  55. package/src/components/chat/thinking-indicator.tsx +48 -12
  56. package/src/components/chat/tool-call-bubble.tsx +3 -0
  57. package/src/components/chat/tool-request-banner.tsx +39 -20
  58. package/src/components/chatrooms/chatroom-list.tsx +11 -4
  59. package/src/components/chatrooms/chatroom-sheet.tsx +7 -2
  60. package/src/components/connectors/connector-list.tsx +33 -11
  61. package/src/components/connectors/connector-sheet.tsx +37 -7
  62. package/src/components/home/home-view.tsx +54 -24
  63. package/src/components/input/chat-input.tsx +22 -1
  64. package/src/components/knowledge/knowledge-list.tsx +17 -18
  65. package/src/components/knowledge/knowledge-sheet.tsx +9 -5
  66. package/src/components/layout/app-layout.tsx +87 -19
  67. package/src/components/mcp-servers/mcp-server-list.tsx +352 -50
  68. package/src/components/mcp-servers/mcp-server-sheet.tsx +25 -9
  69. package/src/components/memory/memory-browser.tsx +73 -45
  70. package/src/components/memory/memory-graph-view.tsx +203 -0
  71. package/src/components/memory/memory-list.tsx +20 -13
  72. package/src/components/plugins/plugin-list.tsx +214 -60
  73. package/src/components/plugins/plugin-sheet.tsx +119 -24
  74. package/src/components/projects/project-list.tsx +17 -9
  75. package/src/components/providers/provider-list.tsx +21 -6
  76. package/src/components/providers/provider-sheet.tsx +42 -25
  77. package/src/components/runs/run-list.tsx +17 -13
  78. package/src/components/schedules/schedule-card.tsx +10 -3
  79. package/src/components/schedules/schedule-list.tsx +2 -2
  80. package/src/components/schedules/schedule-sheet.tsx +28 -9
  81. package/src/components/secrets/secret-sheet.tsx +7 -2
  82. package/src/components/secrets/secrets-list.tsx +18 -5
  83. package/src/components/sessions/new-session-sheet.tsx +183 -376
  84. package/src/components/sessions/session-card.tsx +10 -2
  85. package/src/components/settings/gateway-connection-panel.tsx +9 -8
  86. package/src/components/shared/command-palette.tsx +13 -5
  87. package/src/components/shared/empty-state.tsx +20 -8
  88. package/src/components/shared/hint-tip.tsx +31 -0
  89. package/src/components/shared/notification-center.tsx +134 -86
  90. package/src/components/shared/profile-sheet.tsx +4 -0
  91. package/src/components/shared/settings/plugin-manager.tsx +360 -135
  92. package/src/components/shared/settings/section-capability-policy.tsx +3 -3
  93. package/src/components/shared/settings/section-runtime-loop.tsx +149 -4
  94. package/src/components/skills/clawhub-browser.tsx +1 -0
  95. package/src/components/skills/skill-list.tsx +31 -12
  96. package/src/components/skills/skill-sheet.tsx +20 -7
  97. package/src/components/tasks/approvals-panel.tsx +224 -0
  98. package/src/components/tasks/task-board.tsx +20 -12
  99. package/src/components/tasks/task-card.tsx +21 -7
  100. package/src/components/tasks/task-column.tsx +4 -3
  101. package/src/components/tasks/task-list.tsx +1 -1
  102. package/src/components/tasks/task-sheet.tsx +130 -1
  103. package/src/components/ui/dialog.tsx +1 -0
  104. package/src/components/ui/sheet.tsx +1 -0
  105. package/src/components/usage/metrics-dashboard.tsx +72 -48
  106. package/src/components/wallets/wallet-panel.tsx +65 -41
  107. package/src/components/wallets/wallet-section.tsx +9 -3
  108. package/src/components/webhooks/webhook-list.tsx +21 -12
  109. package/src/components/webhooks/webhook-sheet.tsx +13 -3
  110. package/src/lib/approval-display.test.ts +45 -0
  111. package/src/lib/approval-display.ts +62 -0
  112. package/src/lib/clipboard.ts +38 -0
  113. package/src/lib/memory.ts +8 -0
  114. package/src/lib/providers/claude-cli.ts +5 -3
  115. package/src/lib/providers/index.ts +67 -21
  116. package/src/lib/runtime-loop.ts +3 -2
  117. package/src/lib/server/approvals.ts +150 -0
  118. package/src/lib/server/chat-execution.ts +319 -74
  119. package/src/lib/server/chatroom-helpers.ts +63 -5
  120. package/src/lib/server/chatroom-orchestration.ts +74 -0
  121. package/src/lib/server/clawhub-client.ts +82 -6
  122. package/src/lib/server/connectors/manager.ts +27 -1
  123. package/src/lib/server/context-manager.ts +132 -50
  124. package/src/lib/server/cost.test.ts +73 -0
  125. package/src/lib/server/cost.ts +165 -34
  126. package/src/lib/server/daemon-state.ts +112 -1
  127. package/src/lib/server/data-dir.ts +18 -1
  128. package/src/lib/server/eval/runner.ts +126 -0
  129. package/src/lib/server/eval/scenarios.ts +218 -0
  130. package/src/lib/server/eval/scorer.ts +96 -0
  131. package/src/lib/server/eval/store.ts +37 -0
  132. package/src/lib/server/eval/types.ts +48 -0
  133. package/src/lib/server/execution-log.ts +12 -8
  134. package/src/lib/server/guardian.ts +34 -0
  135. package/src/lib/server/heartbeat-service.ts +53 -1
  136. package/src/lib/server/integrity-monitor.ts +208 -0
  137. package/src/lib/server/langgraph-checkpoint.ts +10 -0
  138. package/src/lib/server/link-understanding.ts +55 -0
  139. package/src/lib/server/llm-response-cache.test.ts +102 -0
  140. package/src/lib/server/llm-response-cache.ts +227 -0
  141. package/src/lib/server/main-agent-loop.ts +115 -16
  142. package/src/lib/server/main-session.ts +6 -3
  143. package/src/lib/server/mcp-conformance.test.ts +18 -0
  144. package/src/lib/server/mcp-conformance.ts +233 -0
  145. package/src/lib/server/memory-db.ts +193 -19
  146. package/src/lib/server/memory-retrieval.test.ts +56 -0
  147. package/src/lib/server/mmr.ts +73 -0
  148. package/src/lib/server/orchestrator-lg.ts +7 -1
  149. package/src/lib/server/orchestrator.ts +4 -3
  150. package/src/lib/server/plugins.ts +662 -132
  151. package/src/lib/server/process-manager.ts +18 -0
  152. package/src/lib/server/query-expansion.ts +57 -0
  153. package/src/lib/server/queue.ts +280 -11
  154. package/src/lib/server/runtime-settings.ts +9 -0
  155. package/src/lib/server/session-run-manager.test.ts +23 -0
  156. package/src/lib/server/session-run-manager.ts +32 -2
  157. package/src/lib/server/session-tools/canvas.ts +85 -50
  158. package/src/lib/server/session-tools/chatroom.ts +130 -127
  159. package/src/lib/server/session-tools/connector.ts +233 -454
  160. package/src/lib/server/session-tools/context-mgmt.ts +87 -105
  161. package/src/lib/server/session-tools/crud.ts +84 -7
  162. package/src/lib/server/session-tools/delegate.ts +351 -752
  163. package/src/lib/server/session-tools/discovery.ts +198 -0
  164. package/src/lib/server/session-tools/edit_file.ts +82 -0
  165. package/src/lib/server/session-tools/file-send.test.ts +39 -0
  166. package/src/lib/server/session-tools/file.ts +257 -425
  167. package/src/lib/server/session-tools/git.ts +87 -47
  168. package/src/lib/server/session-tools/http.ts +95 -33
  169. package/src/lib/server/session-tools/index.ts +217 -138
  170. package/src/lib/server/session-tools/memory.ts +154 -239
  171. package/src/lib/server/session-tools/monitor.ts +126 -0
  172. package/src/lib/server/session-tools/normalize-tool-args.test.ts +61 -0
  173. package/src/lib/server/session-tools/normalize-tool-args.ts +48 -0
  174. package/src/lib/server/session-tools/openclaw-nodes.ts +82 -99
  175. package/src/lib/server/session-tools/openclaw-workspace.ts +103 -93
  176. package/src/lib/server/session-tools/platform.ts +86 -0
  177. package/src/lib/server/session-tools/plugin-creator.ts +239 -0
  178. package/src/lib/server/session-tools/sample-ui.ts +97 -0
  179. package/src/lib/server/session-tools/sandbox.ts +175 -148
  180. package/src/lib/server/session-tools/schedule.ts +78 -0
  181. package/src/lib/server/session-tools/session-info.ts +104 -410
  182. package/src/lib/server/session-tools/shell-normalize.test.ts +43 -0
  183. package/src/lib/server/session-tools/shell.ts +171 -143
  184. package/src/lib/server/session-tools/subagent.ts +77 -77
  185. package/src/lib/server/session-tools/wallet.ts +182 -106
  186. package/src/lib/server/session-tools/web.ts +181 -327
  187. package/src/lib/server/storage.ts +36 -0
  188. package/src/lib/server/stream-agent-chat.ts +348 -242
  189. package/src/lib/server/task-quality-gate.test.ts +44 -0
  190. package/src/lib/server/task-quality-gate.ts +67 -0
  191. package/src/lib/server/task-validation.test.ts +78 -0
  192. package/src/lib/server/task-validation.ts +67 -2
  193. package/src/lib/server/tool-aliases.ts +68 -0
  194. package/src/lib/server/tool-capability-policy.ts +24 -5
  195. package/src/lib/server/tool-retry.ts +62 -0
  196. package/src/lib/server/transcript-repair.ts +72 -0
  197. package/src/lib/setup-defaults.ts +1 -0
  198. package/src/lib/tasks.ts +7 -1
  199. package/src/lib/tool-definitions.ts +24 -23
  200. package/src/lib/validation/schemas.ts +13 -0
  201. package/src/lib/view-routes.ts +2 -23
  202. package/src/stores/use-app-store.ts +23 -1
  203. package/src/types/index.ts +155 -10
@@ -1,3 +1,4 @@
1
+ import os from 'os'
1
2
  import { loadSettings, loadSkills, loadCredentials, decryptKey } from './storage'
2
3
  import { buildCurrentDateTimePromptContext } from './prompt-runtime-context'
3
4
  import { genId } from '@/lib/id'
@@ -49,11 +50,15 @@ function truncateText(text: string, max: number): string {
49
50
  return `${compact.slice(0, Math.max(0, max - 3))}...`
50
51
  }
51
52
 
53
+ import { isImplicitlyMentioned } from './chatroom-orchestration'
54
+
52
55
  /** Parse @mentions from message text, returns matching agentIds */
53
56
  export function parseMentions(text: string, agents: Record<string, Agent>, memberIds: string[]): string[] {
54
57
  if (/@all\b/i.test(text)) return [...memberIds]
55
58
  const mentionPattern = /(?:^|[\s(])@([a-zA-Z0-9._-]+)/g
56
59
  const mentioned: string[] = []
60
+
61
+ // 1. Explicit @mentions
57
62
  let match: RegExpExecArray | null
58
63
  while ((match = mentionPattern.exec(text)) !== null) {
59
64
  const token = normalizeMentionToken(match[1] || '')
@@ -67,6 +72,18 @@ export function parseMentions(text: string, agents: Record<string, Agent>, membe
67
72
  }
68
73
  }
69
74
  }
75
+
76
+ // 2. Implicit mentions (OpenClaw Style - Reading the room)
77
+ // Only if no explicit mentions found yet
78
+ if (mentioned.length === 0) {
79
+ for (const id of memberIds) {
80
+ const agent = agents[id]
81
+ if (agent && isImplicitlyMentioned(text, agent)) {
82
+ mentioned.push(id)
83
+ }
84
+ }
85
+ }
86
+
70
87
  return mentioned
71
88
  }
72
89
 
@@ -144,6 +161,8 @@ export function buildChatroomSystemPrompt(chatroom: Chatroom, agents: Record<str
144
161
  '- **Handle greetings like a human.** For "hello", "how are you", or light check-ins, give a normal conversational reply instead of tool/process commentary.',
145
162
  '- **Keep responses short** unless depth is needed. A few sentences is usually enough. This is a chat, not an essay.',
146
163
  '- **@mention teammates** only when you genuinely need their specific expertise. Don\'t tag people just to be polite.',
164
+ '- **Use Reactions**: To acknowledge a message, agree with a plan, or signal progress without sending a full text reply, use this format at the end of your message: [REACTION]{"emoji": "👍", "to": "message_id"}.',
165
+ '- **Implicit Mentions**: If someone uses your name, creature, or vibe in a message but doesn\'t @tag you, they are still "reading the room" and you may respond if it\'s relevant to you.',
147
166
  '- **Don\'t narrate your capabilities** unless asked. Just demonstrate them by doing things.',
148
167
  '- **Read the room.** Look at recent messages to understand context. Don\'t repeat what others already said.',
149
168
  '',
@@ -173,14 +192,39 @@ export function buildSyntheticSession(agent: Agent, chatroomId: string): Session
173
192
  }
174
193
  }
175
194
 
176
- /** Build agent's system prompt including skills */
195
+ /** Build agent's system prompt including skills and identity context */
177
196
  export function buildAgentSystemPromptForChatroom(agent: Agent): string {
178
197
  const settings = loadSettings()
179
198
  const parts: string[] = []
180
- if (settings.userPrompt) parts.push(settings.userPrompt)
199
+
200
+ // 1. Identity & Persona (Grounded OpenClaw Style)
201
+ const identityLines = [`## My Identity`]
202
+ identityLines.push(`Name: ${agent.name}`)
203
+ if (agent.emoji) identityLines.push(`Emoji: ${agent.emoji}`)
204
+ if (agent.creature) identityLines.push(`Creature: ${agent.creature}`)
205
+ if (agent.vibe) identityLines.push(`Vibe: ${agent.vibe}`)
206
+ if (agent.theme) identityLines.push(`Theme: ${agent.theme}`)
207
+ if (agent.description) identityLines.push(`Description: ${agent.description}`)
208
+ identityLines.push('I should always embody this identity in the group chat. I am a persistent companion with my own soul and presence.')
209
+ parts.push(identityLines.join('\n'))
210
+
211
+ // 2. Runtime & Capabilities (OpenClaw Style)
212
+ const runtimeLines = [
213
+ '## Runtime',
214
+ `os=${process.platform} | host=${os.hostname()} | agent=${agent.id} | provider=${agent.provider} | model=${agent.model}`,
215
+ `capabilities=tools,multi_agent_chatroom,collaborative_reasoning`,
216
+ ]
217
+ parts.push(runtimeLines.join('\n'))
218
+
219
+ // 3. User & DateTime Context
220
+ if (settings.userPrompt) parts.push(`## User Instructions\n${settings.userPrompt}`)
181
221
  parts.push(buildCurrentDateTimePromptContext())
182
- if (agent.soul) parts.push(agent.soul)
183
- if (agent.systemPrompt) parts.push(agent.systemPrompt)
222
+
223
+ // 4. Soul & Core Instructions
224
+ if (agent.soul) parts.push(`## Soul\n${agent.soul}`)
225
+ if (agent.systemPrompt) parts.push(`## System Prompt\n${agent.systemPrompt}`)
226
+
227
+ // 5. Skills (SwarmClaw Core)
184
228
  if (agent.skillIds?.length) {
185
229
  const allSkills = loadSkills()
186
230
  for (const skillId of agent.skillIds) {
@@ -188,6 +232,16 @@ export function buildAgentSystemPromptForChatroom(agent: Agent): string {
188
232
  if (skill?.content) parts.push(`## Skill: ${skill.name}\n${skill.content}`)
189
233
  }
190
234
  }
235
+
236
+ // 6. Thinking & Output Format (OpenClaw Style)
237
+ const thinkingHint = [
238
+ '## Output Format',
239
+ 'If your model supports internal reasoning/thinking, put all internal analysis inside <think>...</think> tags.',
240
+ 'Your final response to the chatroom should be clear and concise.',
241
+ 'When you have nothing to say, respond with ONLY: NO_MESSAGE',
242
+ ]
243
+ parts.push(thinkingHint.join('\n'))
244
+
191
245
  return parts.join('\n\n')
192
246
  }
193
247
 
@@ -196,7 +250,11 @@ export function buildHistoryForAgent(chatroom: Chatroom, agentId: string, imageP
196
250
  const recentMessages = chatroom.messages.slice(-24)
197
251
  const includeAttachmentsFrom = Math.max(0, recentMessages.length - 6)
198
252
  const history = recentMessages.map((m, idx) => {
199
- let msgText = `[${m.senderName}]: ${m.text}`
253
+ let msgText = `[${m.senderName}] (id: ${m.id}): ${m.text}`
254
+ if (m.reactions?.length) {
255
+ const reactionSummary = m.reactions.map(r => `${r.emoji} by ${r.reactorId}`).join(', ')
256
+ msgText += `\n[Reactions: ${reactionSummary}]`
257
+ }
200
258
  const includeAttachments = idx >= includeAttachmentsFrom
201
259
  if (includeAttachments && m.attachedFiles?.length) {
202
260
  const names = m.attachedFiles.map((f) => f.split('/').pop()).join(', ')
@@ -0,0 +1,74 @@
1
+ import type { Chatroom, Agent } from '@/types'
2
+ import { loadChatrooms, saveChatrooms } from './storage'
3
+ import { notify } from './ws-hub'
4
+
5
+ /**
6
+ * Normalizes text for comparison (lowercase, alphanumeric only)
7
+ */
8
+ function normalizeForMatch(text: string): string {
9
+ return text.toLowerCase().replace(/[^a-z0-9]/g, '')
10
+ }
11
+
12
+ /**
13
+ * Determines if an agent was implicitly mentioned in a message.
14
+ * Matches against name, creature, and vibe.
15
+ */
16
+ export function isImplicitlyMentioned(text: string, agent: Agent): boolean {
17
+ const normText = normalizeForMatch(text)
18
+ const normName = normalizeForMatch(agent.name)
19
+ const normCreature = agent.creature ? normalizeForMatch(agent.creature) : null
20
+ const normVibe = agent.vibe ? normalizeForMatch(agent.vibe) : null
21
+
22
+ if (normText.includes(normName)) return true
23
+ if (normCreature && normText.includes(normCreature)) return true
24
+
25
+ // Vibe match: only if the vibe is a distinct single word like "skeptic" or "helper"
26
+ if (normVibe && normVibe.length > 3 && normVibe.split(' ').length === 1) {
27
+ if (normText.includes(normVibe)) return true
28
+ }
29
+
30
+ return false
31
+ }
32
+
33
+ /**
34
+ * Adds an "ack" reaction to a chatroom message on behalf of an agent.
35
+ * Useful for acknowledging tasks or agreeing with teammates.
36
+ */
37
+ export function addAgentReaction(chatroomId: string, messageId: string, agentId: string, emoji: string) {
38
+ const chatrooms = loadChatrooms()
39
+ const chatroom = chatrooms[chatroomId] as Chatroom | undefined
40
+ if (!chatroom) return
41
+
42
+ const message = chatroom.messages.find(m => m.id === messageId)
43
+ if (!message) return
44
+
45
+ // Prevent duplicate reactions from the same agent
46
+ if (message.reactions.some(r => r.reactorId === agentId && r.emoji === emoji)) return
47
+
48
+ message.reactions.push({
49
+ emoji,
50
+ reactorId: agentId,
51
+ time: Date.now()
52
+ })
53
+
54
+ chatrooms[chatroomId] = chatroom
55
+ saveChatrooms(chatrooms)
56
+ notify(`chatroom:${chatroomId}`)
57
+ }
58
+
59
+ /**
60
+ * Parses [REACTION] tokens from agent output and applies them.
61
+ * Format: [REACTION]{"emoji": "👍", "to": "msg_id"}
62
+ */
63
+ export function applyAgentReactionsFromText(text: string, chatroomId: string, agentId: string) {
64
+ const reactionRegex = /\[REACTION\]\s*(\{.*?\})/g
65
+ let match
66
+ while ((match = reactionRegex.exec(text)) !== null) {
67
+ try {
68
+ const data = JSON.parse(match[1])
69
+ if (data.emoji && data.to) {
70
+ addAgentReaction(chatroomId, data.to, agentId, data.emoji)
71
+ }
72
+ } catch { /* ignore invalid JSON */ }
73
+ }
74
+ }
@@ -4,23 +4,99 @@ export interface ClawHubSearchResult {
4
4
  skills: ClawHubSkill[]
5
5
  total: number
6
6
  page: number
7
+ nextCursor?: string | null
7
8
  }
8
9
 
9
- const CLAWHUB_BASE_URL = process.env.CLAWHUB_API_URL || 'https://clawhub.openclaw.dev/api'
10
+ const CLAWHUB_BASE_URL = process.env.CLAWHUB_API_URL || 'https://clawhub.ai/api/v1'
11
+
12
+ /**
13
+ * Raw shape returned by the ClawHub `/skills` endpoint.
14
+ * Fields are mapped to our internal `ClawHubSkill` type.
15
+ */
16
+ interface ClawHubRawItem {
17
+ slug: string
18
+ displayName?: string
19
+ name?: string
20
+ summary?: string
21
+ description?: string
22
+ author?: string | { name?: string }
23
+ tags?: Record<string, string> | string[]
24
+ stats?: { downloads?: number; installsAllTime?: number; stars?: number }
25
+ latestVersion?: { version?: string; changelog?: string }
26
+ metadata?: Record<string, unknown> | null
27
+ url?: string
28
+ createdAt?: number
29
+ updatedAt?: number
30
+ }
31
+
32
+ function mapRawToSkill(raw: ClawHubRawItem): ClawHubSkill {
33
+ const name = raw.displayName || raw.name || raw.slug
34
+ const description = raw.summary || raw.description || ''
35
+ const author = typeof raw.author === 'string'
36
+ ? raw.author
37
+ : raw.author?.name || 'community'
38
+ const tags = Array.isArray(raw.tags)
39
+ ? raw.tags
40
+ : raw.tags ? Object.keys(raw.tags) : []
41
+ const downloads = raw.stats?.installsAllTime ?? raw.stats?.downloads ?? 0
42
+ const version = raw.latestVersion?.version || '1.0.0'
43
+ return {
44
+ id: raw.slug,
45
+ name,
46
+ description,
47
+ author,
48
+ tags,
49
+ downloads,
50
+ url: raw.url || `https://clawhub.ai/skills/${raw.slug}`,
51
+ version,
52
+ }
53
+ }
10
54
 
11
55
  export async function searchClawHub(query: string, page = 1, limit = 20): Promise<ClawHubSearchResult> {
12
56
  try {
13
- const url = `${CLAWHUB_BASE_URL}/skills?q=${encodeURIComponent(query)}&page=${page}&limit=${limit}`
14
- const res = await fetch(url)
57
+ const params = new URLSearchParams({ limit: String(limit) })
58
+ if (query) params.set('q', query)
59
+ if (page > 1) params.set('page', String(page))
60
+
61
+ const url = `${CLAWHUB_BASE_URL}/skills?${params}`
62
+ const res = await fetch(url, { signal: AbortSignal.timeout(8000) })
15
63
  if (!res.ok) throw new Error(`ClawHub responded with ${res.status}`)
16
- return await res.json()
17
- } catch {
64
+
65
+ const data = await res.json() as { items?: ClawHubRawItem[]; skills?: ClawHubRawItem[]; nextCursor?: string | null; total?: number }
66
+
67
+ // ClawHub v1 returns { items, nextCursor }; fall back to { skills, total } for compat
68
+ const rawItems = data.items || data.skills || []
69
+ const skills = rawItems.map(mapRawToSkill)
70
+ const total = data.total ?? (data.nextCursor ? skills.length + 1 : skills.length)
71
+
72
+ return { skills, total, page, nextCursor: data.nextCursor }
73
+ } catch (err: unknown) {
74
+ console.warn('[clawhub] search failed:', err instanceof Error ? err.message : String(err))
18
75
  return { skills: [], total: 0, page }
19
76
  }
20
77
  }
21
78
 
22
79
  export async function fetchSkillContent(rawUrl: string): Promise<string> {
23
- const res = await fetch(rawUrl)
80
+ // ClawHub skill pages are at /skills/<slug> — try raw content endpoint first
81
+ let contentUrl = rawUrl
82
+ if (contentUrl.startsWith('https://clawhub.ai/skills/') && !contentUrl.includes('/raw')) {
83
+ const slug = contentUrl.replace('https://clawhub.ai/skills/', '').replace(/\/$/, '')
84
+ // Try the raw content API first
85
+ const rawApiUrl = `${CLAWHUB_BASE_URL}/skills/${slug}/content`
86
+ try {
87
+ const res = await fetch(rawApiUrl, { signal: AbortSignal.timeout(8000) })
88
+ if (res.ok) {
89
+ const data = await res.json() as { content?: string }
90
+ if (data.content) return data.content
91
+ }
92
+ } catch {
93
+ // Fall through to direct fetch
94
+ }
95
+ // Try the raw endpoint pattern
96
+ contentUrl = `https://clawhub.ai/skills/${slug}/raw`
97
+ }
98
+
99
+ const res = await fetch(contentUrl, { signal: AbortSignal.timeout(10000) })
24
100
  if (!res.ok) throw new Error(`Failed to fetch skill content: ${res.status}`)
25
101
  return res.text()
26
102
  }
@@ -335,6 +335,7 @@ export function isCurrentGeneration(connectorId: string, gen: number): boolean {
335
335
 
336
336
  /** Get platform implementation lazily */
337
337
  export async function getPlatform(platform: string) {
338
+ // 1. Check Built-ins
338
339
  switch (platform) {
339
340
  case 'discord': return (await import('./discord')).default
340
341
  case 'telegram': return (await import('./telegram')).default
@@ -347,8 +348,33 @@ export async function getPlatform(platform: string) {
347
348
  case 'googlechat': return (await import('./googlechat')).default
348
349
  case 'matrix': return (await import('./matrix')).default
349
350
  case 'email': return (await import('./email')).default
350
- default: throw new Error(`Unknown platform: ${platform}`)
351
351
  }
352
+
353
+ // 2. Check Plugin-provided connectors
354
+ try {
355
+ const { getPluginManager } = await import('../plugins')
356
+ const manager = getPluginManager()
357
+ const pluginConnectors = manager.getConnectors()
358
+ const found = pluginConnectors.find(c => c.id === platform)
359
+
360
+ if (found) {
361
+ return {
362
+ start: async (connector: Connector, token: string, onMessage: (msg: InboundMessage) => Promise<string>) => {
363
+ const stop = found.startListener ? await found.startListener(onMessage) : () => {}
364
+ return {
365
+ connector,
366
+ stop: async () => { if (stop) await stop() },
367
+ sendMessage: found.sendMessage,
368
+ authenticated: true,
369
+ }
370
+ }
371
+ }
372
+ }
373
+ } catch (err: unknown) {
374
+ console.warn(`[connector] Failed to check plugins for platform "${platform}":`, err instanceof Error ? err.message : String(err))
375
+ }
376
+
377
+ throw new Error(`Unknown platform: ${platform}`)
352
378
  }
353
379
 
354
380
  export function formatMediaLine(media: InboundMedia): string {
@@ -1,14 +1,25 @@
1
1
  import type { Message } from '@/types'
2
2
  import { getMemoryDb } from './memory-db'
3
3
 
4
+ import { repairTranscriptConsistency } from './transcript-repair'
5
+
4
6
  // --- LLM compaction constants ---
5
7
 
6
- const COMPACTION_CHUNK_BUDGET_RATIO = 0.4 // 40% of context per summarization chunk
7
- const COMPACTION_SAFETY_MARGIN = 1.2 // 20% buffer for token underestimation
8
- const COMPACTION_OVERHEAD_TOKENS = 4096 // reserved for summarization prompt + response
8
+ const BASE_CHUNK_RATIO = 0.4
9
+ const MIN_CHUNK_RATIO = 0.15
10
+ const COMPACTION_SAFETY_MARGIN = 1.2
11
+ const COMPACTION_OVERHEAD_TOKENS = 4096
9
12
  const MAX_TOOL_FAILURES = 8
10
13
  const MAX_FAILURE_CHARS = 240
11
14
 
15
+ const MERGE_SUMMARIES_INSTRUCTIONS =
16
+ 'Merge these partial summaries into a single cohesive summary. Preserve decisions,' +
17
+ ' TODOs, open questions, and any constraints.'
18
+
19
+ const IDENTIFIER_PRESERVATION_INSTRUCTIONS =
20
+ 'Preserve all opaque identifiers exactly as written (no shortening or reconstruction), ' +
21
+ 'including UUIDs, hashes, IDs, tokens, API keys, hostnames, IPs, ports, URLs, and file names.'
22
+
12
23
  /** Callback that sends a prompt to an LLM and returns response text */
13
24
  export type LLMSummarizer = (prompt: string) => Promise<string>
14
25
 
@@ -132,6 +143,44 @@ export function getContextStatus(
132
143
  }
133
144
  }
134
145
 
146
+ // --- Context degradation warnings ---
147
+
148
+ /** Returns a warning string when context usage exceeds thresholds, or null if within safe bounds. */
149
+ export function getContextDegradationWarning(
150
+ messages: Message[],
151
+ systemPromptTokens: number,
152
+ provider: string,
153
+ model: string,
154
+ ): string | null {
155
+ const status = getContextStatus(messages, systemPromptTokens, provider, model)
156
+ const pct = status.percentUsed
157
+ const remaining = status.contextWindow - status.estimatedTokens
158
+ const estTurnsLeft = Math.max(0, Math.floor(remaining / 2000))
159
+
160
+ if (pct >= 85) {
161
+ return [
162
+ `[CONTEXT_WARNING] Context window is ${pct}% full (${status.estimatedTokens.toLocaleString()} / ${status.contextWindow.toLocaleString()} tokens).`,
163
+ `Estimated remaining capacity: ~${estTurnsLeft} turns.`,
164
+ 'CRITICAL: Save essential state to memory immediately. Summarize key findings, decisions, and next steps.',
165
+ 'Consider completing the current subtask and storing a checkpoint before context is exhausted.',
166
+ ].join(' ')
167
+ }
168
+ if (pct >= 70) {
169
+ return [
170
+ `[CONTEXT_WARNING] Context window is ${pct}% full.`,
171
+ `Estimated remaining capacity: ~${estTurnsLeft} turns.`,
172
+ 'Recommended: Store important progress notes to memory. Prioritize completing high-value subtasks.',
173
+ ].join(' ')
174
+ }
175
+ if (pct >= 60) {
176
+ return [
177
+ `[CONTEXT_WARNING] Context window is ${pct}% full (~${estTurnsLeft} turns remaining).`,
178
+ 'Consider saving intermediate state to memory for continuity.',
179
+ ].join(' ')
180
+ }
181
+ return null
182
+ }
183
+
135
184
  // --- Memory consolidation ---
136
185
 
137
186
  /** Extract important facts from old messages before pruning */
@@ -240,6 +289,54 @@ export function splitMessagesByTokenBudget(messages: Message[], budgetPerChunk:
240
289
  return chunks
241
290
  }
242
291
 
292
+ /** Compute adaptive chunk ratio based on average message size. */
293
+ export function computeAdaptiveChunkRatio(messages: Message[], contextWindow: number): number {
294
+ if (messages.length === 0) return BASE_CHUNK_RATIO
295
+ const totalTokens = estimateMessagesTokens(messages)
296
+ const avgTokens = totalTokens / messages.length
297
+ const safeAvgTokens = avgTokens * COMPACTION_SAFETY_MARGIN
298
+ const avgRatio = safeAvgTokens / contextWindow
299
+
300
+ if (avgRatio > 0.1) {
301
+ const reduction = Math.min(avgRatio * 2, BASE_CHUNK_RATIO - MIN_CHUNK_RATIO)
302
+ return Math.max(MIN_CHUNK_RATIO, BASE_CHUNK_RATIO - reduction)
303
+ }
304
+ return BASE_CHUNK_RATIO
305
+ }
306
+
307
+ /** Summarize in hierarchical stages if context is very large */
308
+ export async function summarizeInStages(opts: {
309
+ messages: Message[]
310
+ contextWindow: number
311
+ summarize: LLMSummarizer
312
+ maxChunkTokens: number
313
+ }): Promise<string> {
314
+ const { messages, summarize, maxChunkTokens } = opts
315
+ const totalTokens = estimateMessagesTokens(messages)
316
+
317
+ if (totalTokens <= maxChunkTokens || messages.length < 4) {
318
+ return summarize(buildSummarizationPrompt(messages))
319
+ }
320
+
321
+ const chunks = splitMessagesByTokenBudget(messages, maxChunkTokens)
322
+ if (chunks.length <= 1) {
323
+ return summarize(buildSummarizationPrompt(messages))
324
+ }
325
+
326
+ const partialSummaries: string[] = []
327
+ for (const chunk of chunks) {
328
+ try {
329
+ const partial = await summarize(buildSummarizationPrompt(chunk))
330
+ if (partial?.trim()) partialSummaries.push(partial.trim())
331
+ } catch { /* skip failed chunk */ }
332
+ }
333
+
334
+ if (partialSummaries.length === 0) return 'Summary unavailable.'
335
+ if (partialSummaries.length === 1) return partialSummaries[0]
336
+
337
+ return summarize(buildMergePrompt(partialSummaries))
338
+ }
339
+
243
340
  /** Build an OpenClaw-aligned summarization prompt for a batch of messages */
244
341
  function buildSummarizationPrompt(messages: Message[]): string {
245
342
  const transcript = messages.map((m) => {
@@ -258,13 +355,13 @@ function buildSummarizationPrompt(messages: Message[]): string {
258
355
  'Summarize the following conversation transcript into structured notes.',
259
356
  '',
260
357
  'Rules:',
261
- '- Preserve all decisions, TODOs, open questions, and constraints',
262
- '- Preserve all opaque identifiers exactly as they appear (UUIDs, hashes, IDs, URLs, file paths, API keys, variable names)',
263
- '- Note errors encountered and their resolutions',
264
- '- Keep technical details needed to continue work (versions, configs, commands)',
265
- '- Aim for 20-40% of original length',
266
- '- Use structured notes with bullet points, not narrative prose',
267
- '- Group by topic/theme when possible',
358
+ '- Preserve all decisions, TODOs, open questions, and any constraints.',
359
+ `- ${IDENTIFIER_PRESERVATION_INSTRUCTIONS}`,
360
+ '- Note errors encountered and their resolutions.',
361
+ '- Keep technical details needed to continue work (versions, configs, commands).',
362
+ '- Aim for 20-40% of original length.',
363
+ '- Use structured notes with bullet points, not narrative prose.',
364
+ '- Group by topic/theme when possible.',
268
365
  '',
269
366
  '---TRANSCRIPT---',
270
367
  transcript,
@@ -280,11 +377,12 @@ function buildMergePrompt(partialSummaries: string[]): string {
280
377
  'Merge the following partial conversation summaries into a single cohesive summary.',
281
378
  '',
282
379
  'Rules:',
283
- '- Remove redundancy across parts while preserving all important details',
284
- '- Preserve all opaque identifiers exactly (UUIDs, hashes, IDs, URLs, file paths)',
285
- '- Keep decisions, TODOs, open questions, constraints, and error resolutions',
286
- '- Use structured notes with bullet points',
287
- '- The result should be shorter than the combined input',
380
+ '- Remove redundancy across parts while preserving all important details.',
381
+ `- ${MERGE_SUMMARIES_INSTRUCTIONS}`,
382
+ `- ${IDENTIFIER_PRESERVATION_INSTRUCTIONS}`,
383
+ '- Keep decisions, TODOs, open questions, constraints, and error resolutions.',
384
+ '- Use structured notes with bullet points.',
385
+ '- The result should be shorter than the combined input.',
288
386
  '',
289
387
  numbered,
290
388
  ].join('\n')
@@ -324,62 +422,46 @@ export async function llmCompact(opts: {
324
422
  return { messages, prunedCount: 0, memoriesStored: 0, summaryAdded: false }
325
423
  }
326
424
 
327
- const oldMessages = messages.slice(0, -keepLastN)
328
- const recentMessages = messages.slice(-keepLastN)
425
+ const repaired = repairTranscriptConsistency(messages)
426
+ const oldMessages = repaired.slice(0, -keepLastN)
427
+ const recentMessages = repaired.slice(-keepLastN)
329
428
 
330
- // 1. Consolidate important info to memory (existing regex extraction)
429
+ // 1. Consolidate important info to memory
331
430
  const memoriesStored = consolidateToMemory(oldMessages, agentId, sessionId)
332
431
 
333
- // 2. Extract metadata from old messages
432
+ // 2. Extract metadata
334
433
  const toolFailures = extractToolFailures(oldMessages)
335
434
  const fileOps = extractFileOperations(oldMessages)
336
435
 
337
- // 3. Compute chunk budget
436
+ // 3. Compute adaptive budget
338
437
  const contextWindow = getContextWindowSize(provider, model)
339
- const chunkBudget = Math.floor((contextWindow / COMPACTION_SAFETY_MARGIN) * COMPACTION_CHUNK_BUDGET_RATIO) - COMPACTION_OVERHEAD_TOKENS
438
+ const ratio = computeAdaptiveChunkRatio(oldMessages, contextWindow)
439
+ const chunkBudget = Math.floor((contextWindow / COMPACTION_SAFETY_MARGIN) * ratio) - COMPACTION_OVERHEAD_TOKENS
340
440
 
341
- // 4. Split old messages into chunks
342
- const chunks = splitMessagesByTokenBudget(oldMessages, Math.max(chunkBudget, 2000))
343
-
344
- // 5. Summarize chunks (progressive fallback on failure)
441
+ // 4. Hierarchical summarization
345
442
  let finalSummary: string | null = null
346
443
  try {
347
- if (chunks.length === 1) {
348
- finalSummary = await summarize(buildSummarizationPrompt(chunks[0]))
349
- } else {
350
- // Multi-chunk: summarize each, then merge
351
- const partialSummaries: string[] = []
352
- for (const chunk of chunks) {
353
- try {
354
- const partial = await summarize(buildSummarizationPrompt(chunk))
355
- if (partial?.trim()) partialSummaries.push(partial.trim())
356
- } catch {
357
- // Skip failed chunks — progressive fallback
358
- }
359
- }
360
- if (partialSummaries.length === 0) {
361
- finalSummary = null // all chunks failed
362
- } else if (partialSummaries.length === 1) {
363
- finalSummary = partialSummaries[0]
364
- } else {
365
- finalSummary = await summarize(buildMergePrompt(partialSummaries))
366
- }
367
- }
444
+ finalSummary = await summarizeInStages({
445
+ messages: oldMessages,
446
+ contextWindow,
447
+ summarize,
448
+ maxChunkTokens: Math.max(chunkBudget, 2000),
449
+ })
368
450
  } catch {
369
451
  finalSummary = null
370
452
  }
371
453
 
372
- // 6. Fall back to sliding window if LLM summarization failed entirely
454
+ // 5. Fall back to sliding window if LLM summarization failed entirely
373
455
  if (!finalSummary?.trim()) {
374
456
  return {
375
- messages: slidingWindowCompact(messages, keepLastN),
457
+ messages: slidingWindowCompact(repaired, keepLastN),
376
458
  prunedCount: oldMessages.length,
377
459
  memoriesStored,
378
460
  summaryAdded: false,
379
461
  }
380
462
  }
381
463
 
382
- // 7. Append metadata sections
464
+ // 6. Append metadata sections
383
465
  const metaSections: string[] = [finalSummary.trim()]
384
466
 
385
467
  if (toolFailures.length > 0) {
@@ -392,7 +474,7 @@ export async function llmCompact(opts: {
392
474
  metaSections.push('\n## File Operations\n' + parts.join('\n'))
393
475
  }
394
476
 
395
- // 8. Build context summary message
477
+ // 7. Build context summary message
396
478
  const summaryMessage: Message = {
397
479
  role: 'assistant',
398
480
  text: `[Context Summary]\n${metaSections.join('\n')}`,
@@ -0,0 +1,73 @@
1
+ import assert from 'node:assert/strict'
2
+ import { test } from 'node:test'
3
+ import type { Agent } from '@/types'
4
+ import { checkAgentBudgetLimits, getAgentSpendWindows } from './cost.ts'
5
+
6
+ function buildNowTs(): number {
7
+ const d = new Date()
8
+ d.setFullYear(2026, 2, 15)
9
+ d.setHours(12, 0, 0, 0)
10
+ return d.getTime()
11
+ }
12
+
13
+ test('getAgentSpendWindows aggregates hourly/daily/monthly windows', () => {
14
+ const now = buildNowTs()
15
+ const previousMonth = new Date(2026, 1, 20, 12, 0, 0, 0).getTime()
16
+
17
+ const sessions = {
18
+ s1: { agentId: 'agent-a' },
19
+ s2: { agentId: 'agent-b' },
20
+ }
21
+ const usage = {
22
+ s1: [
23
+ { timestamp: now - 20 * 60_000, estimatedCost: 1.25 }, // within hour
24
+ { timestamp: now - 3 * 60 * 60_000, estimatedCost: 0.5 }, // today
25
+ { timestamp: now - 26 * 60 * 60_000, estimatedCost: 2.0 }, // yesterday
26
+ { timestamp: previousMonth, estimatedCost: 4.0 }, // previous month
27
+ ],
28
+ s2: [
29
+ { timestamp: now - 5 * 60_000, estimatedCost: 99 }, // different agent
30
+ ],
31
+ }
32
+
33
+ const spend = getAgentSpendWindows('agent-a', now, { sessions, usage })
34
+ assert.equal(spend.hourly, 1.25)
35
+ assert.equal(spend.daily, 1.75)
36
+ assert.equal(spend.monthly, 3.75)
37
+ })
38
+
39
+ test('checkAgentBudgetLimits reports exceeded and warning windows', () => {
40
+ const now = buildNowTs()
41
+ const sessions = { s1: { agentId: 'agent-a' } }
42
+ const usage = {
43
+ s1: [
44
+ { timestamp: now - 15 * 60_000, estimatedCost: 1.25 }, // hourly over
45
+ { timestamp: now - 4 * 60 * 60_000, estimatedCost: 0.5 }, // daily near
46
+ { timestamp: now - 26 * 60 * 60_000, estimatedCost: 2.0 }, // monthly near
47
+ ],
48
+ }
49
+ const agent = {
50
+ id: 'agent-a',
51
+ name: 'Agent A',
52
+ hourlyBudget: 1.0,
53
+ dailyBudget: 2.0,
54
+ monthlyBudget: 4.0,
55
+ } as Agent
56
+
57
+ const result = checkAgentBudgetLimits(agent, now, { sessions, usage })
58
+ assert.equal(result.ok, false)
59
+ assert.deepEqual(result.exceeded.map((x) => x.window), ['hourly'])
60
+ assert.deepEqual(result.warnings.map((x) => x.window), ['daily', 'monthly'])
61
+ })
62
+
63
+ test('checkAgentBudgetLimits is ok when no caps are configured', () => {
64
+ const now = buildNowTs()
65
+ const sessions = { s1: { agentId: 'agent-a' } }
66
+ const usage = { s1: [{ timestamp: now - 10 * 60_000, estimatedCost: 10 }] }
67
+ const agent = { id: 'agent-a', name: 'Agent A' } as Agent
68
+
69
+ const result = checkAgentBudgetLimits(agent, now, { sessions, usage })
70
+ assert.equal(result.ok, true)
71
+ assert.equal(result.exceeded.length, 0)
72
+ assert.equal(result.warnings.length, 0)
73
+ })