@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.
- package/README.md +62 -30
- package/package.json +10 -1
- package/src/app/api/agents/[id]/clone/route.ts +40 -0
- package/src/app/api/agents/route.ts +39 -14
- package/src/app/api/chatrooms/[id]/chat/route.ts +58 -3
- package/src/app/api/chatrooms/[id]/moderate/route.ts +150 -0
- package/src/app/api/chatrooms/[id]/route.ts +34 -2
- package/src/app/api/chatrooms/route.ts +26 -3
- package/src/app/api/connectors/[id]/health/route.ts +64 -0
- package/src/app/api/connectors/route.ts +17 -2
- package/src/app/api/knowledge/route.ts +6 -1
- package/src/app/api/openclaw/doctor/route.ts +17 -0
- package/src/app/api/schedules/[id]/run/route.ts +3 -0
- package/src/app/api/sessions/[id]/chat/route.ts +5 -1
- package/src/app/api/sessions/route.ts +11 -2
- package/src/app/api/tasks/[id]/route.ts +18 -13
- package/src/app/api/tasks/route.ts +44 -1
- package/src/app/api/usage/route.ts +16 -7
- package/src/app/api/wallets/[id]/approve/route.ts +62 -0
- package/src/app/api/wallets/[id]/balance-history/route.ts +18 -0
- package/src/app/api/wallets/[id]/route.ts +118 -0
- package/src/app/api/wallets/[id]/send/route.ts +118 -0
- package/src/app/api/wallets/[id]/transactions/route.ts +18 -0
- package/src/app/api/wallets/route.ts +74 -0
- package/src/app/globals.css +8 -0
- package/src/cli/index.js +20 -0
- package/src/cli/index.ts +223 -39
- package/src/cli/spec.js +14 -0
- package/src/components/agents/agent-avatar.tsx +15 -1
- package/src/components/agents/agent-card.tsx +38 -6
- package/src/components/agents/agent-chat-list.tsx +79 -3
- package/src/components/agents/agent-sheet.tsx +191 -26
- package/src/components/auth/setup-wizard.tsx +268 -353
- package/src/components/chat/chat-area.tsx +24 -9
- package/src/components/chat/chat-header.tsx +48 -19
- package/src/components/chat/chat-tool-toggles.tsx +1 -1
- package/src/components/chat/delegation-banner.test.ts +27 -0
- package/src/components/chat/delegation-banner.tsx +109 -23
- package/src/components/chat/message-bubble.tsx +17 -16
- package/src/components/chat/message-list.tsx +6 -5
- package/src/components/chat/streaming-bubble.tsx +3 -2
- package/src/components/chat/thinking-indicator.tsx +3 -2
- package/src/components/chat/transfer-agent-picker.tsx +1 -1
- package/src/components/chatrooms/agent-hover-card.tsx +1 -1
- package/src/components/chatrooms/chatroom-input.tsx +1 -1
- package/src/components/chatrooms/chatroom-message.tsx +165 -23
- package/src/components/chatrooms/chatroom-sheet.tsx +289 -4
- package/src/components/chatrooms/chatroom-typing-bar.tsx +1 -1
- package/src/components/chatrooms/chatroom-view.tsx +62 -17
- package/src/components/connectors/connector-health.tsx +120 -0
- package/src/components/connectors/connector-list.tsx +1 -1
- package/src/components/connectors/connector-sheet.tsx +9 -0
- package/src/components/home/home-view.tsx +25 -3
- package/src/components/input/chat-input.tsx +8 -1
- package/src/components/knowledge/knowledge-list.tsx +1 -1
- package/src/components/knowledge/knowledge-sheet.tsx +1 -1
- package/src/components/layout/app-layout.tsx +35 -4
- package/src/components/memory/memory-agent-list.tsx +1 -1
- package/src/components/memory/memory-browser.tsx +1 -0
- package/src/components/memory/memory-card.tsx +3 -2
- package/src/components/memory/memory-detail.tsx +3 -3
- package/src/components/memory/memory-sheet.tsx +2 -2
- package/src/components/projects/project-detail.tsx +4 -4
- package/src/components/schedules/schedule-list.tsx +55 -9
- package/src/components/schedules/schedule-sheet.tsx +134 -23
- package/src/components/secrets/secret-sheet.tsx +1 -1
- package/src/components/secrets/secrets-list.tsx +1 -1
- package/src/components/sessions/session-card.tsx +1 -1
- package/src/components/shared/agent-picker-list.tsx +1 -1
- package/src/components/shared/agent-switch-dialog.tsx +1 -1
- package/src/components/shared/command-palette.tsx +237 -0
- package/src/components/shared/connector-platform-icon.tsx +1 -0
- package/src/components/shared/settings/section-user-preferences.tsx +4 -4
- package/src/components/skills/skill-list.tsx +1 -1
- package/src/components/skills/skill-sheet.tsx +1 -1
- package/src/components/tasks/task-board.tsx +3 -3
- package/src/components/tasks/task-card.tsx +22 -2
- package/src/components/tasks/task-sheet.tsx +112 -17
- package/src/components/usage/metrics-dashboard.tsx +13 -25
- package/src/components/wallets/wallet-approval-dialog.tsx +99 -0
- package/src/components/wallets/wallet-panel.tsx +616 -0
- package/src/components/wallets/wallet-section.tsx +100 -0
- package/src/hooks/use-swipe.ts +49 -0
- package/src/lib/providers/anthropic.ts +16 -2
- package/src/lib/providers/claude-cli.ts +7 -1
- package/src/lib/providers/index.ts +7 -0
- package/src/lib/providers/ollama.ts +16 -2
- package/src/lib/providers/openai.ts +7 -2
- package/src/lib/providers/openclaw.ts +6 -1
- package/src/lib/providers/provider-defaults.ts +7 -0
- package/src/lib/schedule-templates.ts +115 -0
- package/src/lib/server/agent-registry.ts +2 -2
- package/src/lib/server/alert-dispatch.ts +64 -0
- package/src/lib/server/chat-execution.ts +76 -4
- package/src/lib/server/chatroom-health.ts +60 -0
- package/src/lib/server/chatroom-helpers.test.ts +94 -0
- package/src/lib/server/chatroom-helpers.ts +86 -12
- package/src/lib/server/chatroom-routing.ts +65 -0
- package/src/lib/server/connectors/discord.ts +3 -0
- package/src/lib/server/connectors/email.ts +267 -0
- package/src/lib/server/connectors/inbound-audio-transcription.test.ts +191 -0
- package/src/lib/server/connectors/inbound-audio-transcription.ts +261 -0
- package/src/lib/server/connectors/manager.ts +239 -5
- package/src/lib/server/connectors/openclaw.ts +3 -0
- package/src/lib/server/connectors/slack.ts +6 -0
- package/src/lib/server/connectors/telegram.ts +18 -0
- package/src/lib/server/connectors/types.ts +2 -0
- package/src/lib/server/connectors/whatsapp-text.test.ts +29 -0
- package/src/lib/server/connectors/whatsapp-text.ts +26 -0
- package/src/lib/server/connectors/whatsapp.ts +17 -5
- package/src/lib/server/cost.ts +70 -0
- package/src/lib/server/create-notification.ts +2 -0
- package/src/lib/server/daemon-state.ts +124 -0
- package/src/lib/server/dag-validation.ts +115 -0
- package/src/lib/server/memory-db.ts +12 -7
- package/src/lib/server/openclaw-doctor.ts +48 -0
- package/src/lib/server/orchestrator-lg.ts +12 -2
- package/src/lib/server/orchestrator.ts +6 -1
- package/src/lib/server/queue-followups.test.ts +224 -0
- package/src/lib/server/queue.ts +238 -24
- package/src/lib/server/scheduler.ts +3 -0
- package/src/lib/server/session-run-manager.ts +22 -1
- package/src/lib/server/session-tools/chatroom.ts +11 -2
- package/src/lib/server/session-tools/context-mgmt.ts +2 -2
- package/src/lib/server/session-tools/index.ts +8 -2
- package/src/lib/server/session-tools/memory.ts +23 -4
- package/src/lib/server/session-tools/openclaw-workspace.ts +132 -0
- package/src/lib/server/session-tools/shell.ts +1 -1
- package/src/lib/server/session-tools/wallet.ts +124 -0
- package/src/lib/server/session-tools/web.ts +2 -2
- package/src/lib/server/solana.ts +122 -0
- package/src/lib/server/storage.ts +158 -6
- package/src/lib/server/stream-agent-chat.ts +126 -63
- package/src/lib/server/task-mention.test.ts +41 -0
- package/src/lib/server/task-mention.ts +3 -2
- package/src/lib/setup-defaults.ts +277 -0
- package/src/lib/tool-definitions.ts +1 -0
- package/src/lib/validation/schemas.ts +69 -0
- package/src/lib/view-routes.ts +1 -0
- package/src/stores/use-app-store.ts +15 -3
- package/src/stores/use-chatroom-store.ts +52 -2
- package/src/types/index.ts +98 -2
- 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
|
|
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
|
|
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: '
|
|
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: '
|
|
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 =>
|
|
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:
|
|
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: '
|
|
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: '
|
|
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
|
|
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
|
|
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
|
+
}
|