@swarmclawai/swarmclaw 0.6.3 → 0.6.6

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 (106) hide show
  1. package/README.md +5 -3
  2. package/package.json +5 -1
  3. package/src/app/api/chatrooms/[id]/chat/route.ts +41 -2
  4. package/src/app/api/chatrooms/[id]/route.ts +15 -1
  5. package/src/app/api/chatrooms/route.ts +15 -2
  6. package/src/app/api/schedules/[id]/run/route.ts +3 -0
  7. package/src/app/api/tasks/route.ts +24 -0
  8. package/src/app/api/wallets/[id]/approve/route.ts +62 -0
  9. package/src/app/api/wallets/[id]/balance-history/route.ts +18 -0
  10. package/src/app/api/wallets/[id]/route.ts +118 -0
  11. package/src/app/api/wallets/[id]/send/route.ts +118 -0
  12. package/src/app/api/wallets/[id]/transactions/route.ts +18 -0
  13. package/src/app/api/wallets/route.ts +74 -0
  14. package/src/app/globals.css +8 -0
  15. package/src/app/page.tsx +7 -3
  16. package/src/cli/index.js +15 -0
  17. package/src/cli/spec.js +14 -0
  18. package/src/components/agents/agent-avatar.tsx +15 -1
  19. package/src/components/agents/agent-card.tsx +1 -0
  20. package/src/components/agents/agent-chat-list.tsx +1 -1
  21. package/src/components/agents/agent-sheet.tsx +112 -26
  22. package/src/components/auth/access-key-gate.tsx +22 -11
  23. package/src/components/chat/chat-area.tsx +2 -2
  24. package/src/components/chat/chat-header.tsx +48 -19
  25. package/src/components/chat/chat-tool-toggles.tsx +1 -1
  26. package/src/components/chat/delegation-banner.test.ts +27 -0
  27. package/src/components/chat/delegation-banner.tsx +109 -23
  28. package/src/components/chat/message-bubble.tsx +14 -3
  29. package/src/components/chat/message-list.tsx +5 -4
  30. package/src/components/chat/streaming-bubble.tsx +3 -2
  31. package/src/components/chat/thinking-indicator.tsx +3 -2
  32. package/src/components/chat/tool-call-bubble.test.ts +28 -0
  33. package/src/components/chat/tool-call-bubble.tsx +13 -1
  34. package/src/components/chat/transfer-agent-picker.tsx +1 -1
  35. package/src/components/chatrooms/agent-hover-card.tsx +1 -1
  36. package/src/components/chatrooms/chatroom-input.tsx +7 -6
  37. package/src/components/chatrooms/chatroom-message.tsx +1 -1
  38. package/src/components/chatrooms/chatroom-sheet.tsx +1 -1
  39. package/src/components/chatrooms/chatroom-typing-bar.tsx +1 -1
  40. package/src/components/chatrooms/chatroom-view.tsx +1 -1
  41. package/src/components/connectors/connector-list.tsx +1 -1
  42. package/src/components/home/home-view.tsx +2 -1
  43. package/src/components/input/chat-input.tsx +5 -4
  44. package/src/components/knowledge/knowledge-list.tsx +1 -1
  45. package/src/components/knowledge/knowledge-sheet.tsx +1 -1
  46. package/src/components/layout/app-layout.tsx +23 -9
  47. package/src/components/logs/log-list.tsx +7 -7
  48. package/src/components/memory/memory-agent-list.tsx +1 -1
  49. package/src/components/memory/memory-browser.tsx +1 -0
  50. package/src/components/memory/memory-card.tsx +3 -2
  51. package/src/components/memory/memory-detail.tsx +3 -3
  52. package/src/components/memory/memory-sheet.tsx +2 -2
  53. package/src/components/projects/project-detail.tsx +4 -4
  54. package/src/components/secrets/secret-sheet.tsx +1 -1
  55. package/src/components/secrets/secrets-list.tsx +1 -1
  56. package/src/components/sessions/new-session-sheet.tsx +4 -3
  57. package/src/components/sessions/session-card.tsx +1 -1
  58. package/src/components/shared/agent-picker-list.tsx +1 -1
  59. package/src/components/shared/agent-switch-dialog.tsx +1 -1
  60. package/src/components/shared/settings/section-user-preferences.tsx +4 -4
  61. package/src/components/skills/skill-list.tsx +1 -1
  62. package/src/components/skills/skill-sheet.tsx +1 -1
  63. package/src/components/tasks/task-board.tsx +3 -3
  64. package/src/components/tasks/task-sheet.tsx +21 -1
  65. package/src/components/wallets/wallet-approval-dialog.tsx +99 -0
  66. package/src/components/wallets/wallet-panel.tsx +616 -0
  67. package/src/components/wallets/wallet-section.tsx +100 -0
  68. package/src/hooks/use-media-query.ts +30 -4
  69. package/src/lib/api-client.ts +6 -18
  70. package/src/lib/fetch-timeout.ts +17 -0
  71. package/src/lib/notification-sounds.ts +4 -4
  72. package/src/lib/safe-storage.ts +42 -0
  73. package/src/lib/server/agent-registry.ts +2 -2
  74. package/src/lib/server/chat-execution.ts +35 -3
  75. package/src/lib/server/chatroom-health.ts +60 -0
  76. package/src/lib/server/chatroom-helpers.test.ts +94 -0
  77. package/src/lib/server/chatroom-helpers.ts +64 -11
  78. package/src/lib/server/connectors/inbound-audio-transcription.test.ts +191 -0
  79. package/src/lib/server/connectors/inbound-audio-transcription.ts +261 -0
  80. package/src/lib/server/connectors/manager.ts +80 -2
  81. package/src/lib/server/connectors/whatsapp-text.test.ts +29 -0
  82. package/src/lib/server/connectors/whatsapp-text.ts +26 -0
  83. package/src/lib/server/connectors/whatsapp.ts +8 -5
  84. package/src/lib/server/orchestrator-lg.ts +12 -2
  85. package/src/lib/server/orchestrator.ts +6 -1
  86. package/src/lib/server/queue-followups.test.ts +224 -0
  87. package/src/lib/server/queue.ts +226 -24
  88. package/src/lib/server/scheduler.ts +3 -0
  89. package/src/lib/server/session-tools/chatroom.ts +11 -2
  90. package/src/lib/server/session-tools/context-mgmt.ts +2 -2
  91. package/src/lib/server/session-tools/index.ts +6 -2
  92. package/src/lib/server/session-tools/memory.ts +1 -1
  93. package/src/lib/server/session-tools/shell.ts +1 -1
  94. package/src/lib/server/session-tools/wallet.ts +124 -0
  95. package/src/lib/server/session-tools/web-output.test.ts +29 -0
  96. package/src/lib/server/session-tools/web-output.ts +16 -0
  97. package/src/lib/server/session-tools/web.ts +7 -3
  98. package/src/lib/server/solana.ts +122 -0
  99. package/src/lib/server/storage.ts +38 -0
  100. package/src/lib/server/stream-agent-chat.ts +126 -63
  101. package/src/lib/server/task-mention.test.ts +41 -0
  102. package/src/lib/server/task-mention.ts +3 -2
  103. package/src/lib/tool-definitions.ts +1 -0
  104. package/src/lib/view-routes.ts +6 -1
  105. package/src/stores/use-app-store.ts +17 -11
  106. package/src/types/index.ts +60 -1
package/README.md CHANGED
@@ -118,20 +118,20 @@ Notes:
118
118
  - **Orchestration** — Multi-agent workflows powered by LangGraph with automatic sub-agent routing, checkpointed execution, and rich delegation cards that link to sub-agent chat threads
119
119
  - **Agentic Execution Policy** — Tool-first autonomous action loop with progress updates, evidence-driven answers, and better use of platform tools for long-lived work
120
120
  - **Runtime Date/Time Grounding** — Session, orchestrator, chatroom, and connector prompts include authoritative current timestamp context to reduce stale-date behavior
121
- - **Task Board** — Queue and track agent tasks with status, comments, results, and archiving. Strict capability policy pauses tasks for human approval before tool execution
121
+ - **Task Board** — Queue and track agent tasks with status, comments, structured result artifacts (`outputFiles`, uploads), completion reports, and archiving. Strict capability policy pauses tasks for human approval before tool execution
122
122
  - **Task Metrics API** — Built-in analytics endpoint for WIP, cycle times, throughput velocity, completion/failure by agent, and priority distribution
123
123
  - **Background Daemon** — Auto-processes queued tasks and scheduled jobs with a 30s heartbeat plus recurring health monitoring
124
124
  - **Scheduling** — Cron-based agent scheduling with human-friendly presets
125
125
  - **Loop Runtime Controls** — Switch between bounded and ongoing loops with configurable step caps, runtime guards, heartbeat cadence, and timeout budgets
126
126
  - **Session Run Queue** — Per-session queued runs with followup/steer/collect modes, collect coalescing for bursty inputs, and run-state APIs
127
127
  - **Chat Iteration Workflow** — Edit-and-resend user turns, fork a new session from any message, bookmark key messages, use contextual follow-up suggestion chips, and auto-continue after tool access grants
128
- - **Agent Chatrooms** — Multi-agent room conversations with `@mention` routing, chained agent replies, reactions, and file/image-aware chat context
128
+ - **Agent Chatrooms** — Multi-agent room conversations with `@mention` routing, chained agent replies, reactions, file/image-aware context, health-aware member filtering, and persistent context compaction for long-lived rooms
129
129
  - **Live Chat Telemetry** — Thinking/tool/responding stream phases, live main-loop status badges, connector activity presence, tone indicator, and optional sound notifications
130
130
  - **Global Search Palette** — `Cmd/Ctrl+K` search across agents, tasks, sessions, schedules, webhooks, and skills from anywhere in the app
131
131
  - **Notification Center** — Real-time in-app notifications for task/schedule/daemon events with unread tracking, mark-all/clear-read controls, and optional action links
132
132
  - **Preview-Rich Chat UI** — Side preview panel for tool outputs (image/browser/html/code), inline code/PDF previews for attachments, and image lightbox support
133
133
  - **Voice Settings** — Per-instance ElevenLabs API key + voice ID for TTS replies, plus configurable speech recognition language for chat input
134
- - **Chat Connectors** — Bridge agents to Discord, Slack, Telegram, WhatsApp, BlueBubbles (iMessage), Signal, Microsoft Teams, Google Chat, Matrix, and OpenClaw with media-aware inbound handling
134
+ - **Chat Connectors** — Bridge agents to Discord, Slack, Telegram, WhatsApp, BlueBubbles (iMessage), Signal, Microsoft Teams, Google Chat, Matrix, and OpenClaw with media-aware inbound handling, inbound voice-note transcription (ElevenLabs/OpenAI fallback), and WhatsApp-friendly plain-text formatting
135
135
  - **Skills System** — Discover local skills, import skills from URL, and load OpenClaw `SKILL.md` files (frontmatter-compatible)
136
136
  - **Execution Logging** — Structured audit trail for triggers, tool calls, file ops, commits, and errors in a dedicated `logs.db`
137
137
  - **Context Management** — Auto-compaction of conversation history when approaching context limits, with manual `context_status` and `context_summarize` tools for agents
@@ -157,6 +157,7 @@ CREDENTIAL_SECRET=<auto-generated> # AES-256 encryption key for stored credentia
157
157
  ```
158
158
 
159
159
  Data is stored in `data/swarmclaw.db` (SQLite with WAL mode), `data/memory.db` (agent memory with FTS5 + vector embeddings), `data/logs.db` (execution audit trail), and `data/langgraph-checkpoints.db` (orchestrator checkpoints). Back the `data/` directory up if you care about your sessions, agents, and credentials. Existing JSON file data is auto-migrated to SQLite on first run.
160
+ Agent wallet private keys are stored encrypted (AES-256 via `CREDENTIAL_SECRET`) in `data/swarmclaw.db` and are never returned by wallet API responses; keep `data/` out of version control.
160
161
 
161
162
  The app listens on two ports: `PORT` (default 3456) for the HTTP/SSE API, and `PORT + 1` (default 3457) for WebSocket push notifications. The WS port can be customized with `--ws-port`.
162
163
 
@@ -288,6 +289,7 @@ Agents can use the following tools when enabled:
288
289
  | HTTP Request | Make direct API calls with method, headers, body, redirect control, and timeout |
289
290
  | Git | Run structured git subcommands (`status`, `diff`, `log`, `add`, `commit`, `push`, etc.) with repo safety checks |
290
291
  | Memory | Store and retrieve long-term memories with FTS5 + vector search, file references, image attachments, and linked memory graph traversal |
292
+ | Wallet | Manage an agent-linked Solana wallet (`wallet_tool`) to check balance/address, send SOL (limits + approval), and review transaction history |
291
293
  | Sandbox | Run JS/TS (Deno) or Python code in an isolated sandbox. Created files are returned as downloadable artifacts |
292
294
  | MCP Servers | Connect to external Model Context Protocol servers. Tools from MCP servers are injected as first-class agent tools |
293
295
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swarmclawai/swarmclaw",
3
- "version": "0.6.3",
3
+ "version": "0.6.6",
4
4
  "description": "Self-hosted AI agent orchestration dashboard — manage LLM providers, orchestrate agent swarms, schedule tasks, and bridge agents to chat platforms.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -45,6 +45,7 @@
45
45
  "build:ci": "NEXT_DISABLE_ESLINT=1 next build",
46
46
  "start": "next start",
47
47
  "start:standalone": "node .next/standalone/server.js",
48
+ "benchmark:autonomy": "node ./scripts/benchmark-autonomy-harness.mjs",
48
49
  "lint": "eslint",
49
50
  "lint:fix": "eslint --fix",
50
51
  "lint:baseline": "node ./scripts/lint-baseline.mjs check",
@@ -63,8 +64,10 @@
63
64
  "@multiavatar/multiavatar": "^1.0.7",
64
65
  "@playwright/mcp": "^0.0.68",
65
66
  "@slack/bolt": "^4.6.0",
67
+ "@solana/web3.js": "^1.98.4",
66
68
  "@whiskeysockets/baileys": "^7.0.0-rc.9",
67
69
  "better-sqlite3": "^12.6.2",
70
+ "bs58": "^5.0.0",
68
71
  "cheerio": "^1.2.0",
69
72
  "class-variance-authority": "^0.7.1",
70
73
  "clsx": "^2.1.1",
@@ -90,6 +93,7 @@
90
93
  "recharts": "^3.7.0",
91
94
  "rehype-highlight": "^7.0.2",
92
95
  "remark-gfm": "^4.0.1",
96
+ "remove-markdown": "^0.6.3",
93
97
  "sonner": "^2.0.7",
94
98
  "tailwind-merge": "^3.4.1",
95
99
  "ws": "^8.19.0",
@@ -8,11 +8,14 @@ import { getProvider } from '@/lib/providers'
8
8
  import {
9
9
  resolveApiKey,
10
10
  parseMentions,
11
+ compactChatroomMessages,
11
12
  buildChatroomSystemPrompt,
12
13
  buildSyntheticSession,
13
14
  buildAgentSystemPromptForChatroom,
14
15
  buildHistoryForAgent,
15
16
  } from '@/lib/server/chatroom-helpers'
17
+ import { filterHealthyChatroomAgents } from '@/lib/server/chatroom-health'
18
+ import { markProviderFailure, markProviderSuccess } from '@/lib/server/provider-health'
16
19
  import type { Chatroom, ChatroomMessage, Agent } from '@/types'
17
20
 
18
21
  export const dynamic = 'force-dynamic'
@@ -49,6 +52,8 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
49
52
  if (chatroom.autoAddress && mentions.length === 0) {
50
53
  mentions = [...chatroom.agentIds]
51
54
  }
55
+ const mentionHealth = filterHealthyChatroomAgents(mentions, agents)
56
+ mentions = mentionHealth.healthyAgentIds
52
57
  const userMessage: ChatroomMessage = {
53
58
  id: genId(),
54
59
  senderId,
@@ -63,6 +68,7 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
63
68
  ...(replyToId ? { replyToId } : {}),
64
69
  }
65
70
  chatroom.messages.push(userMessage)
71
+ compactChatroomMessages(chatroom)
66
72
  chatroom.updatedAt = Date.now()
67
73
  chatrooms[id] = chatroom
68
74
  saveChatrooms(chatrooms)
@@ -94,6 +100,22 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
94
100
  }
95
101
 
96
102
  const processAgents = async () => {
103
+ if (mentionHealth.skipped.length > 0) {
104
+ const detail = mentionHealth.skipped
105
+ .map((row) => `${agents[row.agentId]?.name || row.agentId}: ${row.reason}`)
106
+ .join(', ')
107
+ writeEvent({ t: 'err', text: `Skipped agents: ${detail}` })
108
+ }
109
+ if (mentions.length === 0) {
110
+ writeEvent({ t: 'err', text: 'No healthy agents available in this chatroom. Check provider credentials/endpoints and retry.' })
111
+ writeEvent({ t: 'done' })
112
+ if (!closed) {
113
+ try { controller.close() } catch { /* already closed */ }
114
+ closed = true
115
+ }
116
+ return
117
+ }
118
+
97
119
  // Build agent queue: start with mentioned agents, then chain
98
120
  const initialQueue: Array<{ agentId: string; depth: number; contextMessage?: string }> = mentions.map((aid) => ({ agentId: aid, depth: 0 }))
99
121
  const processed = new Set<string>()
@@ -128,6 +150,11 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
128
150
  try {
129
151
  const freshChatrooms = loadChatrooms()
130
152
  const freshChatroom = freshChatrooms[id] as Chatroom
153
+ if (compactChatroomMessages(freshChatroom)) {
154
+ freshChatrooms[id] = freshChatroom
155
+ saveChatrooms(freshChatrooms)
156
+ notify(`chatroom:${id}`)
157
+ }
131
158
 
132
159
  const syntheticSession = buildSyntheticSession(agent, id)
133
160
  const agentSystemPrompt = buildAgentSystemPromptForChatroom(agent)
@@ -170,16 +197,25 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
170
197
  history,
171
198
  })
172
199
 
173
- const responseText = result.fullText || fullText
200
+ const responseText = result.finalResponse || result.fullText || fullText
174
201
 
175
202
  // Don't persist empty or error-only messages — they pollute chat history
176
203
  if (!responseText.trim() && agentError) {
204
+ markProviderFailure(agent.provider, agentError)
177
205
  writeEvent({ t: 'cr_agent_done', agentId: agent.id, agentName: agent.name })
178
206
  return []
179
207
  }
180
208
 
181
209
  if (responseText.trim()) {
182
- const newMentions = parseMentions(responseText, agents, freshChatroom.agentIds)
210
+ const parsedMentions = parseMentions(responseText, agents, freshChatroom.agentIds)
211
+ const chainedHealth = filterHealthyChatroomAgents(parsedMentions, agents)
212
+ const newMentions = chainedHealth.healthyAgentIds
213
+ if (chainedHealth.skipped.length > 0) {
214
+ const detail = chainedHealth.skipped
215
+ .map((row) => `${agents[row.agentId]?.name || row.agentId}: ${row.reason}`)
216
+ .join(', ')
217
+ writeEvent({ t: 'err', text: `Mentioned agents skipped: ${detail}`, agentId: agent.id, agentName: agent.name })
218
+ }
183
219
  const agentMessage: ChatroomMessage = {
184
220
  id: genId(),
185
221
  senderId: agent.id,
@@ -198,16 +234,19 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
198
234
  saveChatrooms(latestChatrooms)
199
235
  notify(`chatroom:${id}`)
200
236
 
237
+ markProviderSuccess(agent.provider)
201
238
  writeEvent({ t: 'cr_agent_done', agentId: agent.id, agentName: agent.name })
202
239
 
203
240
  // Return chained agent IDs — enriched context is built below when queuing
204
241
  return newMentions.filter((mid) => !processed.has(mid) && freshChatroom.agentIds.includes(mid))
205
242
  }
206
243
 
244
+ markProviderSuccess(agent.provider)
207
245
  writeEvent({ t: 'cr_agent_done', agentId: agent.id, agentName: agent.name })
208
246
  return []
209
247
  } catch (err: unknown) {
210
248
  const msg = err instanceof Error ? err.message : String(err)
249
+ markProviderFailure(agent.provider, msg)
211
250
  writeEvent({ t: 'err', text: `Agent ${agent.name} error: ${msg}`, agentId: agent.id })
212
251
  writeEvent({ t: 'cr_agent_done', agentId: agent.id, agentName: agent.name })
213
252
  return []
@@ -21,16 +21,30 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
21
21
 
22
22
  if (body.name !== undefined) chatroom.name = body.name
23
23
  if (body.description !== undefined) chatroom.description = body.description
24
+ if (body.chatMode !== undefined) {
25
+ chatroom.chatMode = body.chatMode === 'parallel' ? 'parallel' : 'sequential'
26
+ }
27
+ if (body.autoAddress !== undefined) {
28
+ chatroom.autoAddress = Boolean(body.autoAddress)
29
+ }
24
30
 
25
31
  // Diff agentIds and inject join/leave system messages
26
32
  if (Array.isArray(body.agentIds)) {
33
+ const agents = loadAgents()
34
+ const invalidAgentIds = (body.agentIds as string[]).filter((agentId) => !agents[agentId])
35
+ if (invalidAgentIds.length > 0) {
36
+ return NextResponse.json(
37
+ { error: `Unknown chatroom member(s): ${invalidAgentIds.join(', ')}` },
38
+ { status: 400 },
39
+ )
40
+ }
41
+
27
42
  const oldIds = new Set(chatroom.agentIds)
28
43
  const newIds = new Set(body.agentIds as string[])
29
44
  const added = (body.agentIds as string[]).filter((aid: string) => !oldIds.has(aid))
30
45
  const removed = chatroom.agentIds.filter((aid: string) => !newIds.has(aid))
31
46
 
32
47
  if (added.length > 0 || removed.length > 0) {
33
- const agents = loadAgents()
34
48
  if (!Array.isArray(chatroom.messages)) chatroom.messages = []
35
49
  const now = Date.now()
36
50
  let offset = 0
@@ -16,11 +16,22 @@ export async function POST(req: Request) {
16
16
  const chatrooms = loadChatrooms()
17
17
  const id = genId()
18
18
 
19
- const agentIds: string[] = Array.isArray(body.agentIds) ? body.agentIds : []
19
+ const requestedAgentIds: string[] = Array.isArray(body.agentIds) ? body.agentIds : []
20
+ const knownAgents = loadAgents()
21
+ const invalidAgentIds = requestedAgentIds.filter((agentId) => !knownAgents[agentId])
22
+ if (invalidAgentIds.length > 0) {
23
+ return NextResponse.json(
24
+ { error: `Unknown chatroom member(s): ${invalidAgentIds.join(', ')}` },
25
+ { status: 400 },
26
+ )
27
+ }
28
+ const agentIds: string[] = requestedAgentIds
29
+ const chatMode = body.chatMode === 'parallel' ? 'parallel' : 'sequential'
30
+ const autoAddress = Boolean(body.autoAddress)
20
31
  const now = Date.now()
21
32
 
22
33
  // Generate join messages for initial agents
23
- const agents = agentIds.length > 0 ? loadAgents() : {}
34
+ const agents = agentIds.length > 0 ? knownAgents : {}
24
35
  const joinMessages: ChatroomMessage[] = agentIds.map((agentId: string, i: number) => ({
25
36
  id: genId(),
26
37
  senderId: 'system',
@@ -38,6 +49,8 @@ export async function POST(req: Request) {
38
49
  description: body.description || '',
39
50
  agentIds,
40
51
  messages: joinMessages,
52
+ chatMode,
53
+ autoAddress,
41
54
  createdAt: now,
42
55
  updatedAt: now,
43
56
  }
@@ -53,7 +53,10 @@ export async function POST(_req: Request, { params }: { params: Promise<{ id: st
53
53
  existingTask.title = `[Sched] ${schedule.name} (run #${schedule.runNumber})`
54
54
  existingTask.result = null
55
55
  existingTask.error = null
56
+ existingTask.outputFiles = []
57
+ existingTask.artifacts = []
56
58
  existingTask.sessionId = null
59
+ existingTask.completionReportPath = null
57
60
  existingTask.updatedAt = now
58
61
  existingTask.queuedAt = null
59
62
  existingTask.startedAt = null
@@ -84,6 +84,30 @@ export async function POST(req: Request) {
84
84
  sessionId: typeof body.sessionId === 'string' ? body.sessionId : null,
85
85
  result: typeof body.result === 'string' ? body.result : null,
86
86
  error: typeof body.error === 'string' ? body.error : null,
87
+ outputFiles: Array.isArray(body.outputFiles)
88
+ ? body.outputFiles.filter((entry: unknown) => typeof entry === 'string').slice(0, 24)
89
+ : [],
90
+ artifacts: Array.isArray(body.artifacts)
91
+ ? body.artifacts
92
+ .filter((artifact: unknown) => artifact && typeof artifact === 'object')
93
+ .map((artifact: unknown) => {
94
+ const row = artifact as {
95
+ url?: unknown
96
+ type?: unknown
97
+ filename?: unknown
98
+ }
99
+ const normalizedType = String(row.type || '')
100
+ return {
101
+ url: String(row.url || ''),
102
+ type: ['image', 'video', 'pdf', 'file'].includes(normalizedType)
103
+ ? (normalizedType as 'image' | 'video' | 'pdf' | 'file')
104
+ : 'file',
105
+ filename: String(row.filename || ''),
106
+ }
107
+ })
108
+ .filter((artifact: { url: string; filename: string }) => artifact.url && artifact.filename)
109
+ .slice(0, 24)
110
+ : [],
87
111
  createdAt: now,
88
112
  updatedAt: now,
89
113
  queuedAt: null,
@@ -0,0 +1,62 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { loadWallets, loadWalletTransactions, upsertWalletTransaction } from '@/lib/server/storage'
3
+ import { sendSol } from '@/lib/server/solana'
4
+ import { notify } from '@/lib/server/ws-hub'
5
+ import type { AgentWallet, WalletTransaction } from '@/types'
6
+ export const dynamic = 'force-dynamic'
7
+
8
+ export async function POST(req: Request, { params }: { params: Promise<{ id: string }> }) {
9
+ const { id } = await params
10
+ const wallets = loadWallets() as Record<string, AgentWallet>
11
+ const wallet = wallets[id]
12
+ if (!wallet) return NextResponse.json({ error: 'Wallet not found' }, { status: 404 })
13
+
14
+ const body = await req.json()
15
+ const transactionId = typeof body.transactionId === 'string' ? body.transactionId.trim() : ''
16
+ const decision = body.decision as 'approve' | 'deny'
17
+
18
+ if (!transactionId) {
19
+ return NextResponse.json({ error: 'transactionId is required' }, { status: 400 })
20
+ }
21
+ if (decision !== 'approve' && decision !== 'deny') {
22
+ return NextResponse.json({ error: 'decision must be "approve" or "deny"' }, { status: 400 })
23
+ }
24
+
25
+ const allTxs = loadWalletTransactions() as Record<string, WalletTransaction>
26
+ const tx = allTxs[transactionId]
27
+ if (!tx || tx.walletId !== id) {
28
+ return NextResponse.json({ error: 'Transaction not found' }, { status: 404 })
29
+ }
30
+ if (tx.status !== 'pending_approval') {
31
+ return NextResponse.json({ error: `Transaction is already ${tx.status}` }, { status: 409 })
32
+ }
33
+
34
+ if (decision === 'deny') {
35
+ tx.status = 'denied'
36
+ tx.approvedBy = 'user'
37
+ upsertWalletTransaction(transactionId, tx)
38
+ notify('wallets')
39
+ return NextResponse.json({ status: 'denied', transactionId })
40
+ }
41
+
42
+ // Approve — sign and submit
43
+ try {
44
+ const { signature, fee } = await sendSol(wallet.encryptedPrivateKey, tx.toAddress, tx.amountLamports)
45
+ tx.status = 'confirmed'
46
+ tx.signature = signature
47
+ tx.feeLamports = fee
48
+ tx.approvedBy = 'user'
49
+ upsertWalletTransaction(transactionId, tx)
50
+ notify('wallets')
51
+ return NextResponse.json({ status: 'confirmed', transactionId, signature })
52
+ } catch (err: unknown) {
53
+ tx.status = 'failed'
54
+ upsertWalletTransaction(transactionId, tx)
55
+ notify('wallets')
56
+ return NextResponse.json({
57
+ error: err instanceof Error ? err.message : String(err),
58
+ transactionId,
59
+ status: 'failed',
60
+ }, { status: 500 })
61
+ }
62
+ }
@@ -0,0 +1,18 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { loadWallets, loadWalletBalanceHistory } from '@/lib/server/storage'
3
+ import type { AgentWallet, WalletBalanceSnapshot } from '@/types'
4
+ export const dynamic = 'force-dynamic'
5
+
6
+ export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
7
+ const { id } = await params
8
+ const wallets = loadWallets() as Record<string, AgentWallet>
9
+ const wallet = wallets[id]
10
+ if (!wallet) return NextResponse.json({ error: 'Wallet not found' }, { status: 404 })
11
+
12
+ const allSnapshots = loadWalletBalanceHistory() as Record<string, WalletBalanceSnapshot>
13
+ const walletSnapshots = Object.values(allSnapshots)
14
+ .filter((s) => s.walletId === id)
15
+ .sort((a, b) => a.timestamp - b.timestamp)
16
+
17
+ return NextResponse.json(walletSnapshots)
18
+ }
@@ -0,0 +1,118 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { loadWallets, upsertWallet, deleteWallet as deleteWalletFromStore, loadAgents, saveAgents } from '@/lib/server/storage'
3
+ import { getBalance, lamportsToSol } from '@/lib/server/solana'
4
+ import { notify } from '@/lib/server/ws-hub'
5
+ import type { AgentWallet } from '@/types'
6
+ export const dynamic = 'force-dynamic'
7
+
8
+ function stripPrivateKey(wallet: Record<string, unknown>): Record<string, unknown> {
9
+ return Object.fromEntries(Object.entries(wallet).filter(([k]) => k !== 'encryptedPrivateKey'))
10
+ }
11
+
12
+ export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
13
+ const { id } = await params
14
+ const wallets = loadWallets() as Record<string, AgentWallet>
15
+ const wallet = wallets[id]
16
+ if (!wallet) return NextResponse.json({ error: 'Wallet not found' }, { status: 404 })
17
+
18
+ // Fetch live on-chain balance
19
+ let balanceLamports = 0
20
+ let balanceSol = 0
21
+ try {
22
+ balanceLamports = await getBalance(wallet.publicKey)
23
+ balanceSol = lamportsToSol(balanceLamports)
24
+ } catch {
25
+ // RPC failure — return 0
26
+ }
27
+
28
+ return NextResponse.json({
29
+ ...stripPrivateKey(wallet as unknown as Record<string, unknown>),
30
+ balanceLamports,
31
+ balanceSol,
32
+ })
33
+ }
34
+
35
+ export async function PATCH(req: Request, { params }: { params: Promise<{ id: string }> }) {
36
+ const { id } = await params
37
+ const wallets = loadWallets() as Record<string, AgentWallet>
38
+ const wallet = wallets[id]
39
+ if (!wallet) return NextResponse.json({ error: 'Wallet not found' }, { status: 404 })
40
+
41
+ const body = await req.json()
42
+
43
+ // Reassign wallet to a different agent
44
+ if (typeof body.agentId === 'string' && body.agentId !== wallet.agentId) {
45
+ const agents = loadAgents()
46
+ const newAgent = agents[body.agentId]
47
+ if (!newAgent) return NextResponse.json({ error: 'Agent not found' }, { status: 404 })
48
+
49
+ // Check new agent doesn't already have a wallet
50
+ const allWallets = loadWallets() as Record<string, AgentWallet>
51
+ const conflict = Object.values(allWallets).find((w) => w.agentId === body.agentId && w.id !== id)
52
+ if (conflict) return NextResponse.json({ error: 'Target agent already has a wallet' }, { status: 409 })
53
+
54
+ // Unlink old agent
55
+ const oldAgent = agents[wallet.agentId]
56
+ if (oldAgent) {
57
+ oldAgent.walletId = null
58
+ oldAgent.updatedAt = Date.now()
59
+ agents[wallet.agentId] = oldAgent
60
+ }
61
+
62
+ // Link new agent
63
+ newAgent.walletId = id
64
+ newAgent.updatedAt = Date.now()
65
+ agents[body.agentId] = newAgent
66
+ saveAgents(agents)
67
+ notify('agents')
68
+
69
+ wallet.agentId = body.agentId
70
+ }
71
+
72
+ if (body.label !== undefined) wallet.label = body.label
73
+ if (typeof body.spendingLimitLamports === 'number') wallet.spendingLimitLamports = body.spendingLimitLamports
74
+ if (typeof body.dailyLimitLamports === 'number') wallet.dailyLimitLamports = body.dailyLimitLamports
75
+ if (typeof body.requireApproval === 'boolean') wallet.requireApproval = body.requireApproval
76
+ wallet.updatedAt = Date.now()
77
+
78
+ upsertWallet(id, wallet)
79
+ notify('wallets')
80
+
81
+ return NextResponse.json(stripPrivateKey(wallet as unknown as Record<string, unknown>))
82
+ }
83
+
84
+ export async function DELETE(_req: Request, { params }: { params: Promise<{ id: string }> }) {
85
+ const { id } = await params
86
+ const wallets = loadWallets() as Record<string, AgentWallet>
87
+ const wallet = wallets[id]
88
+ if (!wallet) return NextResponse.json({ error: 'Wallet not found' }, { status: 404 })
89
+
90
+ // Check if balance > 0 and warn
91
+ let balanceLamports = 0
92
+ try {
93
+ balanceLamports = await getBalance(wallet.publicKey)
94
+ } catch { /* ignore */ }
95
+
96
+ if (balanceLamports > 0) {
97
+ // Still delete, but include warning
98
+ }
99
+
100
+ // Unlink from agent
101
+ const agents = loadAgents()
102
+ const agent = agents[wallet.agentId]
103
+ if (agent) {
104
+ agent.walletId = null
105
+ agent.updatedAt = Date.now()
106
+ agents[wallet.agentId] = agent
107
+ saveAgents(agents)
108
+ notify('agents')
109
+ }
110
+
111
+ deleteWalletFromStore(id)
112
+ notify('wallets')
113
+
114
+ return NextResponse.json({
115
+ ok: true,
116
+ warning: balanceLamports > 0 ? `Wallet had ${lamportsToSol(balanceLamports)} SOL remaining` : undefined,
117
+ })
118
+ }
@@ -0,0 +1,118 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { genId } from '@/lib/id'
3
+ import { loadWallets, loadWalletTransactions, upsertWalletTransaction } from '@/lib/server/storage'
4
+ import { sendSol, isValidSolanaAddress, lamportsToSol } from '@/lib/server/solana'
5
+ import { notify } from '@/lib/server/ws-hub'
6
+ import type { AgentWallet, WalletTransaction } from '@/types'
7
+ export const dynamic = 'force-dynamic'
8
+
9
+ export async function POST(req: Request, { params }: { params: Promise<{ id: string }> }) {
10
+ const { id } = await params
11
+ const wallets = loadWallets() as Record<string, AgentWallet>
12
+ const wallet = wallets[id]
13
+ if (!wallet) return NextResponse.json({ error: 'Wallet not found' }, { status: 404 })
14
+
15
+ const body = await req.json()
16
+ const toAddress = typeof body.toAddress === 'string' ? body.toAddress.trim() : ''
17
+ const amountLamports = typeof body.amountLamports === 'number' ? Math.floor(body.amountLamports) : 0
18
+ const memo = typeof body.memo === 'string' ? body.memo.slice(0, 500) : undefined
19
+
20
+ if (!toAddress || !isValidSolanaAddress(toAddress)) {
21
+ return NextResponse.json({ error: 'Invalid recipient address' }, { status: 400 })
22
+ }
23
+ if (amountLamports <= 0) {
24
+ return NextResponse.json({ error: 'Amount must be positive' }, { status: 400 })
25
+ }
26
+
27
+ // Per-tx spending limit
28
+ const perTxLimit = wallet.spendingLimitLamports ?? 100_000_000
29
+ if (amountLamports > perTxLimit) {
30
+ return NextResponse.json({
31
+ error: `Amount ${lamportsToSol(amountLamports)} SOL exceeds per-transaction limit of ${lamportsToSol(perTxLimit)} SOL`,
32
+ }, { status: 403 })
33
+ }
34
+
35
+ // 24h rolling daily limit
36
+ const dailyLimit = wallet.dailyLimitLamports ?? 1_000_000_000
37
+ const oneDayAgo = Date.now() - 24 * 60 * 60 * 1000
38
+ const allTxs = loadWalletTransactions() as Record<string, WalletTransaction>
39
+ const recentSends = Object.values(allTxs).filter(
40
+ (tx) => tx.walletId === id && tx.type === 'send' && tx.status === 'confirmed' && tx.timestamp > oneDayAgo,
41
+ )
42
+ const dailySpent = recentSends.reduce((sum, tx) => sum + tx.amountLamports, 0)
43
+ if (dailySpent + amountLamports > dailyLimit) {
44
+ return NextResponse.json({
45
+ error: `Daily limit exceeded. Spent ${lamportsToSol(dailySpent)} SOL in last 24h, limit is ${lamportsToSol(dailyLimit)} SOL`,
46
+ }, { status: 403 })
47
+ }
48
+
49
+ const txId = genId(8)
50
+ const now = Date.now()
51
+
52
+ // If requireApproval, create pending tx and return it
53
+ if (wallet.requireApproval) {
54
+ const pendingTx: WalletTransaction = {
55
+ id: txId,
56
+ walletId: id,
57
+ agentId: wallet.agentId,
58
+ chain: wallet.chain,
59
+ type: 'send',
60
+ signature: '',
61
+ fromAddress: wallet.publicKey,
62
+ toAddress,
63
+ amountLamports,
64
+ status: 'pending_approval',
65
+ memo,
66
+ timestamp: now,
67
+ }
68
+ upsertWalletTransaction(txId, pendingTx)
69
+ notify('wallets')
70
+ return NextResponse.json({ status: 'pending_approval', transactionId: txId, message: 'Transaction requires user approval' })
71
+ }
72
+
73
+ // Auto-approved — sign and submit
74
+ try {
75
+ const { signature, fee } = await sendSol(wallet.encryptedPrivateKey, toAddress, amountLamports)
76
+ const confirmedTx: WalletTransaction = {
77
+ id: txId,
78
+ walletId: id,
79
+ agentId: wallet.agentId,
80
+ chain: wallet.chain,
81
+ type: 'send',
82
+ signature,
83
+ fromAddress: wallet.publicKey,
84
+ toAddress,
85
+ amountLamports,
86
+ feeLamports: fee,
87
+ status: 'confirmed',
88
+ memo,
89
+ approvedBy: 'auto',
90
+ timestamp: now,
91
+ }
92
+ upsertWalletTransaction(txId, confirmedTx)
93
+ notify('wallets')
94
+ return NextResponse.json({ status: 'confirmed', transactionId: txId, signature })
95
+ } catch (err: unknown) {
96
+ const failedTx: WalletTransaction = {
97
+ id: txId,
98
+ walletId: id,
99
+ agentId: wallet.agentId,
100
+ chain: wallet.chain,
101
+ type: 'send',
102
+ signature: '',
103
+ fromAddress: wallet.publicKey,
104
+ toAddress,
105
+ amountLamports,
106
+ status: 'failed',
107
+ memo,
108
+ timestamp: now,
109
+ }
110
+ upsertWalletTransaction(txId, failedTx)
111
+ notify('wallets')
112
+ return NextResponse.json({
113
+ error: err instanceof Error ? err.message : String(err),
114
+ transactionId: txId,
115
+ status: 'failed',
116
+ }, { status: 500 })
117
+ }
118
+ }
@@ -0,0 +1,18 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { loadWallets, loadWalletTransactions } from '@/lib/server/storage'
3
+ import type { AgentWallet, WalletTransaction } from '@/types'
4
+ export const dynamic = 'force-dynamic'
5
+
6
+ export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
7
+ const { id } = await params
8
+ const wallets = loadWallets() as Record<string, AgentWallet>
9
+ const wallet = wallets[id]
10
+ if (!wallet) return NextResponse.json({ error: 'Wallet not found' }, { status: 404 })
11
+
12
+ const allTxs = loadWalletTransactions() as Record<string, WalletTransaction>
13
+ const walletTxs = Object.values(allTxs)
14
+ .filter((tx) => tx.walletId === id)
15
+ .sort((a, b) => b.timestamp - a.timestamp)
16
+
17
+ return NextResponse.json(walletTxs)
18
+ }