@swarmclawai/swarmclaw 0.6.4 → 0.6.7

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 (143) hide show
  1. package/README.md +62 -30
  2. package/package.json +10 -1
  3. package/src/app/api/agents/[id]/clone/route.ts +40 -0
  4. package/src/app/api/agents/route.ts +39 -14
  5. package/src/app/api/chatrooms/[id]/chat/route.ts +58 -3
  6. package/src/app/api/chatrooms/[id]/moderate/route.ts +150 -0
  7. package/src/app/api/chatrooms/[id]/route.ts +34 -2
  8. package/src/app/api/chatrooms/route.ts +26 -3
  9. package/src/app/api/connectors/[id]/health/route.ts +64 -0
  10. package/src/app/api/connectors/route.ts +17 -2
  11. package/src/app/api/knowledge/route.ts +6 -1
  12. package/src/app/api/openclaw/doctor/route.ts +17 -0
  13. package/src/app/api/schedules/[id]/run/route.ts +3 -0
  14. package/src/app/api/sessions/[id]/chat/route.ts +5 -1
  15. package/src/app/api/sessions/route.ts +11 -2
  16. package/src/app/api/tasks/[id]/route.ts +18 -13
  17. package/src/app/api/tasks/route.ts +44 -1
  18. package/src/app/api/usage/route.ts +16 -7
  19. package/src/app/api/wallets/[id]/approve/route.ts +62 -0
  20. package/src/app/api/wallets/[id]/balance-history/route.ts +18 -0
  21. package/src/app/api/wallets/[id]/route.ts +118 -0
  22. package/src/app/api/wallets/[id]/send/route.ts +118 -0
  23. package/src/app/api/wallets/[id]/transactions/route.ts +18 -0
  24. package/src/app/api/wallets/route.ts +74 -0
  25. package/src/app/globals.css +8 -0
  26. package/src/cli/index.js +20 -0
  27. package/src/cli/index.ts +223 -39
  28. package/src/cli/spec.js +14 -0
  29. package/src/components/agents/agent-avatar.tsx +15 -1
  30. package/src/components/agents/agent-card.tsx +38 -6
  31. package/src/components/agents/agent-chat-list.tsx +79 -3
  32. package/src/components/agents/agent-sheet.tsx +191 -26
  33. package/src/components/auth/setup-wizard.tsx +268 -353
  34. package/src/components/chat/chat-area.tsx +24 -9
  35. package/src/components/chat/chat-header.tsx +48 -19
  36. package/src/components/chat/chat-tool-toggles.tsx +1 -1
  37. package/src/components/chat/delegation-banner.test.ts +27 -0
  38. package/src/components/chat/delegation-banner.tsx +109 -23
  39. package/src/components/chat/message-bubble.tsx +17 -16
  40. package/src/components/chat/message-list.tsx +6 -5
  41. package/src/components/chat/streaming-bubble.tsx +3 -2
  42. package/src/components/chat/thinking-indicator.tsx +3 -2
  43. package/src/components/chat/transfer-agent-picker.tsx +1 -1
  44. package/src/components/chatrooms/agent-hover-card.tsx +1 -1
  45. package/src/components/chatrooms/chatroom-input.tsx +1 -1
  46. package/src/components/chatrooms/chatroom-message.tsx +165 -23
  47. package/src/components/chatrooms/chatroom-sheet.tsx +289 -4
  48. package/src/components/chatrooms/chatroom-typing-bar.tsx +1 -1
  49. package/src/components/chatrooms/chatroom-view.tsx +62 -17
  50. package/src/components/connectors/connector-health.tsx +120 -0
  51. package/src/components/connectors/connector-list.tsx +1 -1
  52. package/src/components/connectors/connector-sheet.tsx +9 -0
  53. package/src/components/home/home-view.tsx +25 -3
  54. package/src/components/input/chat-input.tsx +8 -1
  55. package/src/components/knowledge/knowledge-list.tsx +1 -1
  56. package/src/components/knowledge/knowledge-sheet.tsx +1 -1
  57. package/src/components/layout/app-layout.tsx +35 -4
  58. package/src/components/memory/memory-agent-list.tsx +1 -1
  59. package/src/components/memory/memory-browser.tsx +1 -0
  60. package/src/components/memory/memory-card.tsx +3 -2
  61. package/src/components/memory/memory-detail.tsx +3 -3
  62. package/src/components/memory/memory-sheet.tsx +2 -2
  63. package/src/components/projects/project-detail.tsx +4 -4
  64. package/src/components/schedules/schedule-list.tsx +55 -9
  65. package/src/components/schedules/schedule-sheet.tsx +134 -23
  66. package/src/components/secrets/secret-sheet.tsx +1 -1
  67. package/src/components/secrets/secrets-list.tsx +1 -1
  68. package/src/components/sessions/session-card.tsx +1 -1
  69. package/src/components/shared/agent-picker-list.tsx +1 -1
  70. package/src/components/shared/agent-switch-dialog.tsx +1 -1
  71. package/src/components/shared/command-palette.tsx +237 -0
  72. package/src/components/shared/connector-platform-icon.tsx +1 -0
  73. package/src/components/shared/settings/section-user-preferences.tsx +4 -4
  74. package/src/components/skills/skill-list.tsx +1 -1
  75. package/src/components/skills/skill-sheet.tsx +1 -1
  76. package/src/components/tasks/task-board.tsx +3 -3
  77. package/src/components/tasks/task-card.tsx +22 -2
  78. package/src/components/tasks/task-sheet.tsx +112 -17
  79. package/src/components/usage/metrics-dashboard.tsx +13 -25
  80. package/src/components/wallets/wallet-approval-dialog.tsx +99 -0
  81. package/src/components/wallets/wallet-panel.tsx +616 -0
  82. package/src/components/wallets/wallet-section.tsx +100 -0
  83. package/src/hooks/use-swipe.ts +49 -0
  84. package/src/lib/providers/anthropic.ts +16 -2
  85. package/src/lib/providers/claude-cli.ts +7 -1
  86. package/src/lib/providers/index.ts +7 -0
  87. package/src/lib/providers/ollama.ts +16 -2
  88. package/src/lib/providers/openai.ts +7 -2
  89. package/src/lib/providers/openclaw.ts +6 -1
  90. package/src/lib/providers/provider-defaults.ts +7 -0
  91. package/src/lib/schedule-templates.ts +115 -0
  92. package/src/lib/server/agent-registry.ts +2 -2
  93. package/src/lib/server/alert-dispatch.ts +64 -0
  94. package/src/lib/server/chat-execution.ts +76 -4
  95. package/src/lib/server/chatroom-health.ts +60 -0
  96. package/src/lib/server/chatroom-helpers.test.ts +94 -0
  97. package/src/lib/server/chatroom-helpers.ts +86 -12
  98. package/src/lib/server/chatroom-routing.ts +65 -0
  99. package/src/lib/server/connectors/discord.ts +3 -0
  100. package/src/lib/server/connectors/email.ts +267 -0
  101. package/src/lib/server/connectors/inbound-audio-transcription.test.ts +191 -0
  102. package/src/lib/server/connectors/inbound-audio-transcription.ts +261 -0
  103. package/src/lib/server/connectors/manager.ts +239 -5
  104. package/src/lib/server/connectors/openclaw.ts +3 -0
  105. package/src/lib/server/connectors/slack.ts +6 -0
  106. package/src/lib/server/connectors/telegram.ts +18 -0
  107. package/src/lib/server/connectors/types.ts +2 -0
  108. package/src/lib/server/connectors/whatsapp-text.test.ts +29 -0
  109. package/src/lib/server/connectors/whatsapp-text.ts +26 -0
  110. package/src/lib/server/connectors/whatsapp.ts +17 -5
  111. package/src/lib/server/cost.ts +70 -0
  112. package/src/lib/server/create-notification.ts +2 -0
  113. package/src/lib/server/daemon-state.ts +124 -0
  114. package/src/lib/server/dag-validation.ts +115 -0
  115. package/src/lib/server/memory-db.ts +12 -7
  116. package/src/lib/server/openclaw-doctor.ts +48 -0
  117. package/src/lib/server/orchestrator-lg.ts +12 -2
  118. package/src/lib/server/orchestrator.ts +6 -1
  119. package/src/lib/server/queue-followups.test.ts +224 -0
  120. package/src/lib/server/queue.ts +238 -24
  121. package/src/lib/server/scheduler.ts +3 -0
  122. package/src/lib/server/session-run-manager.ts +22 -1
  123. package/src/lib/server/session-tools/chatroom.ts +11 -2
  124. package/src/lib/server/session-tools/context-mgmt.ts +2 -2
  125. package/src/lib/server/session-tools/index.ts +8 -2
  126. package/src/lib/server/session-tools/memory.ts +23 -4
  127. package/src/lib/server/session-tools/openclaw-workspace.ts +132 -0
  128. package/src/lib/server/session-tools/shell.ts +1 -1
  129. package/src/lib/server/session-tools/wallet.ts +124 -0
  130. package/src/lib/server/session-tools/web.ts +2 -2
  131. package/src/lib/server/solana.ts +122 -0
  132. package/src/lib/server/storage.ts +158 -6
  133. package/src/lib/server/stream-agent-chat.ts +126 -63
  134. package/src/lib/server/task-mention.test.ts +41 -0
  135. package/src/lib/server/task-mention.ts +3 -2
  136. package/src/lib/setup-defaults.ts +277 -0
  137. package/src/lib/tool-definitions.ts +1 -0
  138. package/src/lib/validation/schemas.ts +69 -0
  139. package/src/lib/view-routes.ts +1 -0
  140. package/src/stores/use-app-store.ts +15 -3
  141. package/src/stores/use-chatroom-store.ts +52 -2
  142. package/src/types/index.ts +98 -2
  143. package/tsconfig.json +2 -1
@@ -13,7 +13,7 @@ export function buildChatroomTools(bctx: ToolBuildContext): StructuredToolInterf
13
13
  if (hasTool('manage_chatrooms')) {
14
14
  tools.push(
15
15
  tool(
16
- async ({ action, chatroomId, name, description, agentIds, agentId, message }) => {
16
+ async ({ action, chatroomId, name, description, agentIds, agentId, message, chatMode, autoAddress }) => {
17
17
  try {
18
18
  const chatrooms = loadChatrooms() as Record<string, Chatroom>
19
19
 
@@ -31,13 +31,20 @@ export function buildChatroomTools(bctx: ToolBuildContext): StructuredToolInterf
31
31
  if (action === 'create_chatroom') {
32
32
  const id = genId()
33
33
  const agents = loadAgents()
34
- const validAgentIds = (agentIds || []).filter((aid: string) => agents[aid])
34
+ const requestedAgentIds = agentIds || []
35
+ const invalidAgentIds = requestedAgentIds.filter((aid: string) => !agents[aid])
36
+ if (invalidAgentIds.length > 0) {
37
+ return `Error: unknown agent IDs: ${invalidAgentIds.join(', ')}`
38
+ }
39
+ const validAgentIds = requestedAgentIds
35
40
  const chatroom: Chatroom = {
36
41
  id,
37
42
  name: name || 'New Chatroom',
38
43
  description: description || '',
39
44
  agentIds: validAgentIds,
40
45
  messages: [],
46
+ chatMode: chatMode === 'parallel' ? 'parallel' : 'sequential',
47
+ autoAddress: Boolean(autoAddress),
41
48
  createdAt: Date.now(),
42
49
  updatedAt: Date.now(),
43
50
  }
@@ -124,6 +131,8 @@ export function buildChatroomTools(bctx: ToolBuildContext): StructuredToolInterf
124
131
  name: z.string().optional().describe('Chatroom name (for create_chatroom)'),
125
132
  description: z.string().optional().describe('Chatroom description (for create_chatroom)'),
126
133
  agentIds: z.array(z.string()).optional().describe('Initial agent IDs (for create_chatroom)'),
134
+ chatMode: z.enum(['sequential', 'parallel']).optional().describe('Optional orchestration mode for create_chatroom'),
135
+ autoAddress: z.boolean().optional().describe('Whether to auto-address all members when no @mention is present'),
127
136
  agentId: z.string().optional().describe('Agent ID (for add_agent/remove_agent)'),
128
137
  message: z.string().optional().describe('Message text (for send_message)'),
129
138
  }),
@@ -28,7 +28,7 @@ export function buildContextTools(bctx: ToolBuildContext): StructuredToolInterfa
28
28
  },
29
29
  {
30
30
  name: 'context_status',
31
- description: 'Check current context window usage for this session. Returns estimated tokens used, provider context limit, percentage used, and compaction strategy recommendation.',
31
+ description: 'Check how much of my context window I\'ve used. Returns my token usage, the model\'s limit, percentage used, and whether I should compact.',
32
32
  schema: z.object({}),
33
33
  },
34
34
  ),
@@ -108,7 +108,7 @@ export function buildContextTools(bctx: ToolBuildContext): StructuredToolInterfa
108
108
  },
109
109
  {
110
110
  name: 'context_summarize',
111
- description: 'Summarize and compact the conversation history to free context window space. Old messages are consolidated to memory (preserving decisions, key facts, results) and replaced with a summary. Use context_status first to check if compaction is needed.',
111
+ description: 'Compact my conversation history to free up context space. I\'ll save important decisions, facts, and results to memory, then replace older messages with a summary. I should check context_status first to see if this is needed.',
112
112
  schema: z.object({
113
113
  keepLastN: z.number().optional().describe('Number of recent messages to keep (default 10, min 2).'),
114
114
  }),
@@ -21,6 +21,8 @@ import { buildSubagentTools } from './subagent'
21
21
  import { buildCanvasTools } from './canvas'
22
22
  import { buildHttpTools } from './http'
23
23
  import { buildGitTools } from './git'
24
+ import { buildWalletTools } from './wallet'
25
+ import { buildOpenClawWorkspaceTools } from './openclaw-workspace'
24
26
 
25
27
  export type { ToolContext, SessionToolsResult }
26
28
  export { sweepOrphanedBrowsers, cleanupSessionBrowser, getActiveBrowserCount, hasActiveBrowser }
@@ -34,7 +36,9 @@ export async function buildSessionTools(cwd: string, enabledTools: string[], ctx
34
36
  const cliProcessTimeoutMs = runtime.cliProcessTimeoutMs
35
37
  const appSettings = loadSettings()
36
38
  const toolPolicy = resolveSessionToolPolicy(enabledTools, appSettings)
37
- const activeTools = toolPolicy.enabledTools
39
+ const activeTools = toolPolicy.enabledTools.includes('shell') && !toolPolicy.enabledTools.includes('process')
40
+ ? [...toolPolicy.enabledTools, 'process']
41
+ : toolPolicy.enabledTools
38
42
  const hasTool = (toolName: string) => activeTools.includes(toolName)
39
43
 
40
44
  if (toolPolicy.blockedTools.length > 0) {
@@ -107,6 +111,8 @@ export async function buildSessionTools(cwd: string, enabledTools: string[], ctx
107
111
  ...buildCanvasTools(bctx),
108
112
  ...buildHttpTools(bctx),
109
113
  ...buildGitTools(bctx),
114
+ ...buildWalletTools(bctx),
115
+ ...buildOpenClawWorkspaceTools(bctx),
110
116
  )
111
117
 
112
118
  // ---------------------------------------------------------------------------
@@ -158,7 +164,7 @@ export async function buildSessionTools(cwd: string, enabledTools: string[], ctx
158
164
  },
159
165
  {
160
166
  name: 'request_tool_access',
161
- description: 'Request access to a tool that is currently disabled. The user will be prompted to grant access, and a follow-up "Continue" message will be sent automatically once granted. End your current response after calling this — do NOT tell the user to "let you know" or ask them to confirm; the continuation is automatic.',
167
+ description: 'Ask the user for access to a tool I don\'t currently have. They\'ll get a prompt to grant it, and once they do, I\'ll automatically continue where I left off. I should end my current response after calling this — no need to ask the user to confirm, it happens on its own.',
162
168
  schema: z.object({
163
169
  toolId: z.string().describe('The tool ID to request access for (e.g. manage_tasks, shell, claude_code)'),
164
170
  reason: z.string().describe('Brief explanation of why you need this tool'),
@@ -165,12 +165,16 @@ export function buildMemoryTools(bctx: ToolBuildContext): StructuredToolInterfac
165
165
  if (action === 'knowledge_store') {
166
166
  const { addKnowledge } = await import('../memory-db')
167
167
  if (!value) return 'Error: value (content) is required for knowledge_store'
168
+ const source = (input as Record<string, unknown>).source as string | undefined
169
+ const sourceUrl = (input as Record<string, unknown>).sourceUrl as string | undefined
168
170
  const entry = addKnowledge({
169
171
  title: key || 'Untitled',
170
172
  content: value,
171
173
  tags: tags,
172
174
  createdByAgentId: ctx?.agentId || null,
173
175
  createdBySessionId: ctx?.sessionId || null,
176
+ source: source || undefined,
177
+ sourceUrl: sourceUrl || undefined,
174
178
  })
175
179
  return `Knowledge stored: "${entry.title}" (id: ${entry.id})`
176
180
  }
@@ -178,16 +182,29 @@ export function buildMemoryTools(bctx: ToolBuildContext): StructuredToolInterfac
178
182
  const { searchKnowledge } = await import('../memory-db')
179
183
  const results = searchKnowledge(query || key || '', tags, 10)
180
184
  if (!results.length) return 'No knowledge entries found.'
181
- return results.map(r => `[${r.id}] ${r.title}: ${r.content.slice(0, 200)}`).join('\n---\n')
185
+ return results.map(r => {
186
+ const meta = r.metadata as Record<string, unknown> | undefined
187
+ const src = meta?.source as string | undefined
188
+ const srcUrl = meta?.sourceUrl as string | undefined
189
+ let line = `[${r.id}] ${r.title}: ${r.content.slice(0, 200)}`
190
+ if (src && srcUrl) {
191
+ line += ` [${src}](${srcUrl})`
192
+ } else if (src) {
193
+ line += ` (source: ${src})`
194
+ } else if (srcUrl) {
195
+ line += ` (${srcUrl})`
196
+ }
197
+ return line
198
+ }).join('\n---\n')
182
199
  }
183
200
  return `Unknown action "${action}". Use: store, get, search, list, delete, link, unlink, knowledge_store, or knowledge_search.`
184
- } catch (err: any) {
185
- return `Error: ${err.message}`
201
+ } catch (err: unknown) {
202
+ return `Error: ${err instanceof Error ? err.message : String(err)}`
186
203
  }
187
204
  },
188
205
  {
189
206
  name: 'memory_tool',
190
- description: 'Store and retrieve long-term memories that persist across sessions. Memories can be shared or agent-scoped. Supports file references, image attachments, and linking memories together with depth traversal. Also supports a cross-agent knowledge base via "knowledge_store" and "knowledge_search". Use "store", "get", "search", "list", "delete", "link", "unlink", "knowledge_store", or "knowledge_search".',
207
+ description: 'My long-term memory things I remember across conversations. I can store personal notes, recall past context, and build up knowledge over time. Memories can be private to me or shared with other agents. I can also attach files, link related memories, and contribute to a shared knowledge base. Actions: store, get, search, list, delete, link, unlink, knowledge_store, knowledge_search.',
191
208
  schema: z.object({
192
209
  action: z.enum(['store', 'get', 'search', 'list', 'delete', 'link', 'unlink', 'knowledge_store', 'knowledge_search']).describe('The action to perform'),
193
210
  key: z.string().describe('For store: memory title. For get/delete/link/unlink: memory ID. For search: optional query fallback.'),
@@ -224,6 +241,8 @@ export function buildMemoryTools(bctx: ToolBuildContext): StructuredToolInterfac
224
241
  linkedLimit: z.number().optional().describe('Max linked memories expanded during traversal. Respects configured server cap.'),
225
242
  targetIds: z.array(z.string()).optional().describe('Memory IDs to link/unlink (for link/unlink actions)'),
226
243
  tags: z.array(z.string()).optional().describe('Tags for categorizing knowledge entries'),
244
+ source: z.string().optional().describe("Source of the knowledge, e.g. 'user', 'web', 'document'"),
245
+ sourceUrl: z.string().optional().describe('URL where the knowledge was sourced from'),
227
246
  pinned: z.boolean().optional().describe('Mark memory as pinned (always preloaded in agent context). For store action.'),
228
247
  sharedWith: z.array(z.string()).optional().describe('Agent IDs to share this memory with (for store action). They can read it in their context.'),
229
248
  }),
@@ -0,0 +1,132 @@
1
+ import { z } from 'zod'
2
+ import { tool, type StructuredToolInterface } from '@langchain/core/tools'
3
+ import { execFile } from 'child_process'
4
+ import { promisify } from 'util'
5
+ import * as path from 'path'
6
+ import * as os from 'os'
7
+ import { loadSettings } from '../storage'
8
+ import type { ToolBuildContext } from './context'
9
+ import { MAX_OUTPUT } from './context'
10
+
11
+ const execFileAsync = promisify(execFile)
12
+
13
+ function resolveWorkspacePath(): string {
14
+ const settings = loadSettings()
15
+ if (typeof settings.openclawWorkspacePath === 'string' && settings.openclawWorkspacePath.trim()) {
16
+ return settings.openclawWorkspacePath.trim()
17
+ }
18
+ return path.join(os.homedir(), '.openclaw', 'workspace')
19
+ }
20
+
21
+ async function gitInWorkspace(args: string[], timeoutMs = 15_000): Promise<{ stdout: string; stderr: string }> {
22
+ const cwd = resolveWorkspacePath()
23
+ return execFileAsync('git', args, {
24
+ cwd,
25
+ timeout: timeoutMs,
26
+ maxBuffer: MAX_OUTPUT,
27
+ env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
28
+ })
29
+ }
30
+
31
+ export function buildOpenClawWorkspaceTools(bctx: ToolBuildContext): StructuredToolInterface[] {
32
+ if (!bctx.hasTool('openclaw_workspace')) return []
33
+
34
+ return [
35
+ tool(
36
+ async ({ message }) => {
37
+ try {
38
+ const workspace = resolveWorkspacePath()
39
+ // Verify it's a git repo
40
+ await execFileAsync('git', ['rev-parse', '--is-inside-work-tree'], {
41
+ cwd: workspace,
42
+ timeout: 5000,
43
+ })
44
+
45
+ const label = message || new Date().toISOString().replace(/[:.]/g, '-')
46
+ await gitInWorkspace(['add', '-A'])
47
+
48
+ // Check if there's anything to commit
49
+ const status = await gitInWorkspace(['status', '--porcelain'])
50
+ if (!status.stdout.trim()) {
51
+ return JSON.stringify({ ok: true, commitHash: null, message: 'Nothing to commit — workspace is clean.' })
52
+ }
53
+
54
+ await gitInWorkspace(['commit', '-m', `backup: ${label}`])
55
+ const { stdout } = await gitInWorkspace(['rev-parse', 'HEAD'])
56
+ return JSON.stringify({ ok: true, commitHash: stdout.trim() })
57
+ } catch (err: unknown) {
58
+ const execErr = err as { stderr?: string; message?: string }
59
+ return JSON.stringify({ ok: false, error: execErr.stderr || execErr.message || String(err) })
60
+ }
61
+ },
62
+ {
63
+ name: 'openclaw_workspace_backup',
64
+ description: 'Create a git backup of the OpenClaw workspace. Stages all changes and commits.',
65
+ schema: z.object({
66
+ message: z.string().optional().describe('Optional backup message (defaults to timestamp)'),
67
+ }),
68
+ },
69
+ ),
70
+ tool(
71
+ async ({ commitHash }) => {
72
+ try {
73
+ if (!commitHash) {
74
+ // Find first stable commit (skip auto-generated ones)
75
+ const { stdout } = await gitInWorkspace(['log', '--oneline', '--format=%H %s', '-50'])
76
+ const lines = stdout.trim().split('\n').filter(Boolean)
77
+ const autoPattern = /^(rollback|daily-backup|auto-backup|guardian-auto|backup:)/i
78
+ const stable = lines.find((line) => {
79
+ const msg = line.substring(41) // after hash + space
80
+ return !autoPattern.test(msg)
81
+ })
82
+ if (!stable) {
83
+ return JSON.stringify({ ok: false, error: 'No stable commit found to roll back to.' })
84
+ }
85
+ commitHash = stable.substring(0, 40)
86
+ }
87
+
88
+ const { stdout: prevHead } = await gitInWorkspace(['rev-parse', 'HEAD'])
89
+ await gitInWorkspace(['reset', '--hard', commitHash])
90
+ return JSON.stringify({ ok: true, rolledBackTo: commitHash, previousHead: prevHead.trim() })
91
+ } catch (err: unknown) {
92
+ const execErr = err as { stderr?: string; message?: string }
93
+ return JSON.stringify({ ok: false, error: execErr.stderr || execErr.message || String(err) })
94
+ }
95
+ },
96
+ {
97
+ name: 'openclaw_workspace_rollback',
98
+ description: 'Roll back the OpenClaw workspace to a specific commit, or automatically find the last stable (non-auto-generated) commit.',
99
+ schema: z.object({
100
+ commitHash: z.string().optional().describe('Target commit hash (if omitted, uses the last stable commit)'),
101
+ }),
102
+ },
103
+ ),
104
+ tool(
105
+ async ({ limit }) => {
106
+ try {
107
+ const count = Math.max(1, Math.min(limit ?? 20, 100))
108
+ const { stdout } = await gitInWorkspace(['log', '--oneline', `--format=%H %s`, `-${count}`])
109
+ const commits = stdout
110
+ .trim()
111
+ .split('\n')
112
+ .filter(Boolean)
113
+ .map((line) => ({
114
+ hash: line.substring(0, 40),
115
+ message: line.substring(41),
116
+ }))
117
+ return JSON.stringify({ commits })
118
+ } catch (err: unknown) {
119
+ const execErr = err as { stderr?: string; message?: string }
120
+ return JSON.stringify({ ok: false, error: execErr.stderr || execErr.message || String(err) })
121
+ }
122
+ },
123
+ {
124
+ name: 'openclaw_workspace_history',
125
+ description: 'List recent git commits in the OpenClaw workspace.',
126
+ schema: z.object({
127
+ limit: z.number().optional().describe('Number of commits to return (default 20, max 100)'),
128
+ }),
129
+ },
130
+ ),
131
+ ]
132
+ }
@@ -49,7 +49,7 @@ export function buildShellTools(bctx: ToolBuildContext): StructuredToolInterface
49
49
  },
50
50
  {
51
51
  name: 'execute_command',
52
- description: 'Execute a shell command in the session working directory. This is the PRIMARY tool for running servers, dev servers, installing packages, running scripts, git operations, and any command the user wants to run or test. Use background=true for long-running processes like servers. Supports timeout/yield controls.',
52
+ description: 'Run a shell command in my working directory. This is how I run servers, install packages, execute scripts, use git, and do anything hands-on. Use background=true for long-running processes like dev servers. Supports timeout/yield controls.',
53
53
  schema: z.object({
54
54
  command: z.string().describe('The shell command to execute'),
55
55
  background: z.boolean().optional().describe('If true, start command in background immediately'),
@@ -0,0 +1,124 @@
1
+ import { z } from 'zod'
2
+ import { tool, type StructuredToolInterface } from '@langchain/core/tools'
3
+ import type { ToolBuildContext } from './context'
4
+ import { loadWallets, loadWalletTransactions } from '../storage'
5
+ import type { AgentWallet, WalletTransaction } from '@/types'
6
+
7
+ export function buildWalletTools(bctx: ToolBuildContext): StructuredToolInterface[] {
8
+ if (!bctx.hasTool('wallet')) return []
9
+
10
+ const agentId = bctx.ctx?.agentId
11
+
12
+ function getAgentWallet(): AgentWallet | null {
13
+ if (!agentId) return null
14
+ const wallets = loadWallets() as Record<string, AgentWallet>
15
+ return Object.values(wallets).find((w) => w.agentId === agentId) ?? null
16
+ }
17
+
18
+ return [
19
+ tool(
20
+ async ({ action, toAddress, amountSol, memo, limit }) => {
21
+ const wallet = getAgentWallet()
22
+ if (!wallet) {
23
+ return JSON.stringify({ error: 'No wallet linked to this agent. Ask the user to create one in the Wallets section.' })
24
+ }
25
+
26
+ switch (action) {
27
+ case 'balance': {
28
+ try {
29
+ const { getBalance, lamportsToSol } = await import('../solana')
30
+ const balanceLamports = await getBalance(wallet.publicKey)
31
+ return JSON.stringify({
32
+ address: wallet.publicKey,
33
+ chain: wallet.chain,
34
+ balanceLamports,
35
+ balanceSol: lamportsToSol(balanceLamports),
36
+ })
37
+ } catch (err: unknown) {
38
+ return JSON.stringify({ error: `Failed to fetch balance: ${err instanceof Error ? err.message : String(err)}` })
39
+ }
40
+ }
41
+
42
+ case 'address': {
43
+ return JSON.stringify({
44
+ address: wallet.publicKey,
45
+ chain: wallet.chain,
46
+ })
47
+ }
48
+
49
+ case 'send': {
50
+ if (!toAddress) return JSON.stringify({ error: 'toAddress is required for send action' })
51
+ if (!amountSol || amountSol <= 0) return JSON.stringify({ error: 'amountSol must be positive' })
52
+
53
+ const { isValidSolanaAddress, solToLamports, lamportsToSol } = await import('../solana')
54
+ if (!isValidSolanaAddress(toAddress)) {
55
+ return JSON.stringify({ error: 'Invalid Solana address' })
56
+ }
57
+
58
+ const amountLamports = solToLamports(amountSol)
59
+
60
+ // Check per-tx limit
61
+ const perTxLimit = wallet.spendingLimitLamports ?? 100_000_000
62
+ if (amountLamports > perTxLimit) {
63
+ return JSON.stringify({
64
+ error: `Amount ${amountSol} SOL exceeds per-transaction limit of ${lamportsToSol(perTxLimit)} SOL`,
65
+ })
66
+ }
67
+
68
+ // Send via API to enforce all limits and approval flow
69
+ try {
70
+ const baseUrl = process.env.NEXTAUTH_URL || `http://localhost:${process.env.PORT || 3456}`
71
+ const res = await fetch(`${baseUrl}/api/wallets/${wallet.id}/send`, {
72
+ method: 'POST',
73
+ headers: {
74
+ 'Content-Type': 'application/json',
75
+ 'X-Access-Key': process.env.ACCESS_KEY || '',
76
+ },
77
+ body: JSON.stringify({ toAddress, amountLamports, memo }),
78
+ })
79
+ const result = await res.json()
80
+ return JSON.stringify(result)
81
+ } catch (err: unknown) {
82
+ return JSON.stringify({ error: `Send failed: ${err instanceof Error ? err.message : String(err)}` })
83
+ }
84
+ }
85
+
86
+ case 'transactions': {
87
+ const allTxs = loadWalletTransactions() as Record<string, WalletTransaction>
88
+ const walletTxs = Object.values(allTxs)
89
+ .filter((tx) => tx.walletId === wallet.id)
90
+ .sort((a, b) => b.timestamp - a.timestamp)
91
+ .slice(0, limit ?? 10)
92
+ .map((tx) => ({
93
+ id: tx.id,
94
+ type: tx.type,
95
+ status: tx.status,
96
+ amountLamports: tx.amountLamports,
97
+ toAddress: tx.toAddress,
98
+ fromAddress: tx.fromAddress,
99
+ signature: tx.signature || undefined,
100
+ memo: tx.memo,
101
+ timestamp: tx.timestamp,
102
+ }))
103
+
104
+ return JSON.stringify({ transactions: walletTxs, count: walletTxs.length })
105
+ }
106
+
107
+ default:
108
+ return JSON.stringify({ error: `Unknown action: ${action}. Use balance, address, send, or transactions.` })
109
+ }
110
+ },
111
+ {
112
+ name: 'wallet_tool',
113
+ description: 'Manage your own crypto wallet. Actions: balance (check your SOL balance), address (get your wallet address), send (send SOL from your wallet — subject to your spending limits and user approval), transactions (view your recent transaction history).',
114
+ schema: z.object({
115
+ action: z.enum(['balance', 'address', 'send', 'transactions']).describe('Wallet action to perform'),
116
+ toAddress: z.string().optional().describe('Recipient Solana address (required for send)'),
117
+ amountSol: z.number().optional().describe('Amount in SOL to send (required for send)'),
118
+ memo: z.string().optional().describe('Reason or memo for the transaction'),
119
+ limit: z.number().optional().describe('Number of transactions to return (default 10, for transactions action)'),
120
+ }),
121
+ },
122
+ ),
123
+ ]
124
+ }
@@ -141,7 +141,7 @@ export function buildWebTools(bctx: ToolBuildContext): StructuredToolInterface[]
141
141
  },
142
142
  {
143
143
  name: 'web_search',
144
- description: 'Search the web. Returns an array of results with title, url, and snippet.',
144
+ description: 'Search the web for information. Returns results with title, url, and snippet.',
145
145
  schema: z.object({
146
146
  query: z.string().describe('Search query'),
147
147
  maxResults: z.number().optional().describe('Maximum results to return (default 5, max 10)'),
@@ -179,7 +179,7 @@ export function buildWebTools(bctx: ToolBuildContext): StructuredToolInterface[]
179
179
  },
180
180
  {
181
181
  name: 'web_fetch',
182
- description: 'Fetch a URL and return its text content (HTML stripped). Useful for reading web pages.',
182
+ description: 'Fetch a URL and read its content (HTML stripped to text). How I read web pages and pull in external information.',
183
183
  schema: z.object({
184
184
  url: z.string().describe('The URL to fetch'),
185
185
  }),
@@ -0,0 +1,122 @@
1
+ import { Keypair, Connection, PublicKey, SystemProgram, Transaction, LAMPORTS_PER_SOL, sendAndConfirmTransaction } from '@solana/web3.js'
2
+ import bs58 from 'bs58'
3
+ import { encryptKey, decryptKey } from './storage'
4
+
5
+ const DEFAULT_RPC_URL = process.env.SOLANA_RPC_URL || 'https://api.mainnet-beta.solana.com'
6
+
7
+ // ---------------------------------------------------------------------------
8
+ // Keypair generation & encryption
9
+ // ---------------------------------------------------------------------------
10
+
11
+ export function generateSolanaKeypair(): { publicKey: string; encryptedPrivateKey: string } {
12
+ const keypair = Keypair.generate()
13
+ const secretKeyBase58 = bs58.encode(keypair.secretKey)
14
+ return {
15
+ publicKey: keypair.publicKey.toBase58(),
16
+ encryptedPrivateKey: encryptKey(secretKeyBase58),
17
+ }
18
+ }
19
+
20
+ export function getKeypairFromEncrypted(encryptedPrivateKey: string): Keypair {
21
+ const secretKeyBase58 = decryptKey(encryptedPrivateKey)
22
+ const secretKey = bs58.decode(secretKeyBase58)
23
+ return Keypair.fromSecretKey(secretKey)
24
+ }
25
+
26
+ // ---------------------------------------------------------------------------
27
+ // Connection
28
+ // ---------------------------------------------------------------------------
29
+
30
+ export function getConnection(rpcUrl?: string): Connection {
31
+ return new Connection(rpcUrl || DEFAULT_RPC_URL, 'confirmed')
32
+ }
33
+
34
+ // ---------------------------------------------------------------------------
35
+ // Balance
36
+ // ---------------------------------------------------------------------------
37
+
38
+ export async function getBalance(publicKey: string, rpcUrl?: string): Promise<number> {
39
+ const connection = getConnection(rpcUrl)
40
+ const pk = new PublicKey(publicKey)
41
+ return connection.getBalance(pk)
42
+ }
43
+
44
+ // ---------------------------------------------------------------------------
45
+ // Send SOL
46
+ // ---------------------------------------------------------------------------
47
+
48
+ export async function sendSol(
49
+ encryptedPrivateKey: string,
50
+ toAddress: string,
51
+ lamports: number,
52
+ rpcUrl?: string,
53
+ ): Promise<{ signature: string; fee: number }> {
54
+ const connection = getConnection(rpcUrl)
55
+ const fromKeypair = getKeypairFromEncrypted(encryptedPrivateKey)
56
+ const toPublicKey = new PublicKey(toAddress)
57
+
58
+ const transaction = new Transaction().add(
59
+ SystemProgram.transfer({
60
+ fromPubkey: fromKeypair.publicKey,
61
+ toPubkey: toPublicKey,
62
+ lamports,
63
+ }),
64
+ )
65
+
66
+ const signature = await sendAndConfirmTransaction(connection, transaction, [fromKeypair])
67
+
68
+ // Fetch fee from confirmed tx
69
+ let fee = 5000 // default fee estimate
70
+ try {
71
+ const txInfo = await connection.getTransaction(signature, { commitment: 'confirmed' })
72
+ if (txInfo?.meta?.fee) fee = txInfo.meta.fee
73
+ } catch {
74
+ // use default
75
+ }
76
+
77
+ return { signature, fee }
78
+ }
79
+
80
+ // ---------------------------------------------------------------------------
81
+ // Recent transactions
82
+ // ---------------------------------------------------------------------------
83
+
84
+ export async function getRecentTransactions(
85
+ publicKey: string,
86
+ limit = 20,
87
+ rpcUrl?: string,
88
+ ): Promise<Array<{ signature: string; blockTime: number | null; err: unknown }>> {
89
+ const connection = getConnection(rpcUrl)
90
+ const pk = new PublicKey(publicKey)
91
+ const signatures = await connection.getSignaturesForAddress(pk, { limit })
92
+ return signatures.map((s) => ({
93
+ signature: s.signature,
94
+ blockTime: s.blockTime ?? null,
95
+ err: s.err,
96
+ }))
97
+ }
98
+
99
+ // ---------------------------------------------------------------------------
100
+ // Validate address
101
+ // ---------------------------------------------------------------------------
102
+
103
+ export function isValidSolanaAddress(address: string): boolean {
104
+ try {
105
+ new PublicKey(address)
106
+ return true
107
+ } catch {
108
+ return false
109
+ }
110
+ }
111
+
112
+ // ---------------------------------------------------------------------------
113
+ // Helpers
114
+ // ---------------------------------------------------------------------------
115
+
116
+ export function lamportsToSol(lamports: number): number {
117
+ return lamports / LAMPORTS_PER_SOL
118
+ }
119
+
120
+ export function solToLamports(sol: number): number {
121
+ return Math.round(sol * LAMPORTS_PER_SOL)
122
+ }