@swarmclawai/swarmclaw 0.7.1 → 0.7.3

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 (237) hide show
  1. package/README.md +155 -150
  2. package/package.json +1 -1
  3. package/src/app/api/agents/[id]/route.ts +26 -0
  4. package/src/app/api/agents/[id]/thread/route.ts +37 -9
  5. package/src/app/api/agents/route.ts +13 -2
  6. package/src/app/api/auth/route.ts +76 -7
  7. package/src/app/api/chatrooms/[id]/chat/route.ts +7 -2
  8. package/src/app/api/{sessions → chats}/[id]/browser/route.ts +5 -1
  9. package/src/app/api/{sessions → chats}/[id]/chat/route.ts +7 -3
  10. package/src/app/api/{sessions → chats}/[id]/checkpoints/route.ts +1 -1
  11. package/src/app/api/chats/[id]/main-loop/route.ts +13 -0
  12. package/src/app/api/{sessions → chats}/[id]/messages/route.ts +19 -13
  13. package/src/app/api/{sessions → chats}/[id]/restore/route.ts +1 -1
  14. package/src/app/api/{sessions → chats}/[id]/route.ts +22 -52
  15. package/src/app/api/{sessions → chats}/[id]/stop/route.ts +6 -1
  16. package/src/app/api/{sessions → chats}/route.ts +21 -7
  17. package/src/app/api/connectors/[id]/doctor/route.ts +26 -0
  18. package/src/app/api/connectors/doctor/route.ts +13 -0
  19. package/src/app/api/files/open/route.ts +16 -14
  20. package/src/app/api/memory/maintenance/route.ts +11 -1
  21. package/src/app/api/openclaw/agent-files/route.ts +27 -4
  22. package/src/app/api/openclaw/skills/route.ts +11 -3
  23. package/src/app/api/plugins/dependencies/route.ts +24 -0
  24. package/src/app/api/plugins/install/route.ts +15 -92
  25. package/src/app/api/plugins/route.ts +6 -26
  26. package/src/app/api/plugins/settings/route.ts +40 -0
  27. package/src/app/api/plugins/ui/route.ts +1 -0
  28. package/src/app/api/settings/route.ts +49 -7
  29. package/src/app/api/tasks/[id]/route.ts +15 -6
  30. package/src/app/api/tasks/bulk/route.ts +2 -2
  31. package/src/app/api/tasks/route.ts +9 -4
  32. package/src/app/api/usage/route.ts +30 -0
  33. package/src/app/api/webhooks/[id]/route.ts +8 -1
  34. package/src/app/page.tsx +9 -2
  35. package/src/cli/index.js +39 -33
  36. package/src/cli/index.ts +43 -49
  37. package/src/cli/spec.js +29 -27
  38. package/src/components/agents/agent-card.tsx +16 -13
  39. package/src/components/agents/agent-chat-list.tsx +104 -4
  40. package/src/components/agents/agent-list.tsx +54 -22
  41. package/src/components/agents/agent-sheet.tsx +209 -18
  42. package/src/components/agents/cron-job-form.tsx +3 -3
  43. package/src/components/agents/inspector-panel.tsx +110 -50
  44. package/src/components/auth/access-key-gate.tsx +36 -97
  45. package/src/components/auth/setup-wizard.tsx +5 -38
  46. package/src/components/chat/chat-area.tsx +39 -27
  47. package/src/components/{sessions/session-card.tsx → chat/chat-card.tsx} +7 -23
  48. package/src/components/chat/chat-header.tsx +299 -314
  49. package/src/components/{sessions/session-list.tsx → chat/chat-list.tsx} +11 -14
  50. package/src/components/chat/chat-tool-toggles.tsx +26 -17
  51. package/src/components/chat/checkpoint-timeline.tsx +4 -4
  52. package/src/components/chat/message-bubble.tsx +4 -1
  53. package/src/components/chat/message-list.tsx +5 -3
  54. package/src/components/chat/session-debug-panel.tsx +1 -1
  55. package/src/components/chat/tool-request-banner.tsx +3 -3
  56. package/src/components/chatrooms/agent-hover-card.tsx +3 -3
  57. package/src/components/chatrooms/chatroom-tool-request-banner.tsx +2 -2
  58. package/src/components/chatrooms/chatroom-view.tsx +347 -205
  59. package/src/components/connectors/connector-list.tsx +265 -127
  60. package/src/components/connectors/connector-sheet.tsx +218 -1
  61. package/src/components/home/home-view.tsx +129 -5
  62. package/src/components/layout/app-layout.tsx +392 -182
  63. package/src/components/layout/mobile-header.tsx +26 -8
  64. package/src/components/plugins/plugin-list.tsx +487 -254
  65. package/src/components/plugins/plugin-sheet.tsx +236 -13
  66. package/src/components/projects/project-detail.tsx +183 -0
  67. package/src/components/settings/gateway-connection-panel.tsx +1 -1
  68. package/src/components/shared/agent-picker-list.tsx +2 -2
  69. package/src/components/shared/command-palette.tsx +111 -25
  70. package/src/components/shared/settings/plugin-manager.tsx +20 -4
  71. package/src/components/shared/settings/section-capability-policy.tsx +105 -0
  72. package/src/components/shared/settings/section-heartbeat.tsx +78 -1
  73. package/src/components/shared/settings/section-orchestrator.tsx +3 -3
  74. package/src/components/shared/settings/section-providers.tsx +1 -1
  75. package/src/components/shared/settings/section-runtime-loop.tsx +5 -5
  76. package/src/components/shared/settings/section-secrets.tsx +6 -6
  77. package/src/components/shared/settings/section-user-preferences.tsx +1 -1
  78. package/src/components/shared/settings/section-voice.tsx +5 -1
  79. package/src/components/shared/settings/section-web-search.tsx +10 -2
  80. package/src/components/shared/settings/settings-page.tsx +244 -56
  81. package/src/components/tasks/approvals-panel.tsx +205 -18
  82. package/src/components/tasks/task-board.tsx +242 -46
  83. package/src/components/usage/metrics-dashboard.tsx +147 -1
  84. package/src/components/wallets/wallet-panel.tsx +17 -5
  85. package/src/components/webhooks/webhook-sheet.tsx +8 -8
  86. package/src/lib/auth.ts +17 -0
  87. package/src/lib/chat-streaming-state.test.ts +108 -0
  88. package/src/lib/chat-streaming-state.ts +108 -0
  89. package/src/lib/chat.ts +1 -1
  90. package/src/lib/{sessions.ts → chats.ts} +28 -18
  91. package/src/lib/openclaw-agent-id.test.ts +14 -0
  92. package/src/lib/openclaw-agent-id.ts +31 -0
  93. package/src/lib/providers/claude-cli.ts +1 -1
  94. package/src/lib/server/agent-assignment.test.ts +112 -0
  95. package/src/lib/server/agent-assignment.ts +169 -0
  96. package/src/lib/server/approval-connector-notify.test.ts +253 -0
  97. package/src/lib/server/approvals-auto-approve.test.ts +205 -0
  98. package/src/lib/server/approvals.ts +483 -75
  99. package/src/lib/server/autonomy-runtime.test.ts +341 -0
  100. package/src/lib/server/browser-state.test.ts +118 -0
  101. package/src/lib/server/browser-state.ts +123 -0
  102. package/src/lib/server/build-llm.test.ts +36 -0
  103. package/src/lib/server/build-llm.ts +11 -4
  104. package/src/lib/server/builtin-plugins.ts +34 -0
  105. package/src/lib/server/capability-router.ts +10 -8
  106. package/src/lib/server/chat-execution-heartbeat.test.ts +40 -0
  107. package/src/lib/server/chat-execution-tool-events.test.ts +134 -0
  108. package/src/lib/server/chat-execution.ts +285 -165
  109. package/src/lib/server/chatroom-health.test.ts +26 -0
  110. package/src/lib/server/chatroom-health.ts +2 -3
  111. package/src/lib/server/chatroom-helpers.test.ts +67 -2
  112. package/src/lib/server/chatroom-helpers.ts +48 -8
  113. package/src/lib/server/connectors/discord.ts +175 -11
  114. package/src/lib/server/connectors/doctor.test.ts +80 -0
  115. package/src/lib/server/connectors/doctor.ts +116 -0
  116. package/src/lib/server/connectors/manager.ts +948 -112
  117. package/src/lib/server/connectors/policy.test.ts +222 -0
  118. package/src/lib/server/connectors/policy.ts +452 -0
  119. package/src/lib/server/connectors/slack.ts +188 -9
  120. package/src/lib/server/connectors/telegram.ts +65 -15
  121. package/src/lib/server/connectors/thread-context.test.ts +44 -0
  122. package/src/lib/server/connectors/thread-context.ts +72 -0
  123. package/src/lib/server/connectors/types.ts +41 -11
  124. package/src/lib/server/cost.ts +34 -1
  125. package/src/lib/server/daemon-state.ts +61 -3
  126. package/src/lib/server/data-dir.ts +13 -0
  127. package/src/lib/server/delegation-jobs.test.ts +140 -0
  128. package/src/lib/server/delegation-jobs.ts +248 -0
  129. package/src/lib/server/document-utils.test.ts +47 -0
  130. package/src/lib/server/document-utils.ts +397 -0
  131. package/src/lib/server/heartbeat-service.ts +14 -40
  132. package/src/lib/server/heartbeat-source.test.ts +22 -0
  133. package/src/lib/server/heartbeat-source.ts +7 -0
  134. package/src/lib/server/identity-continuity.test.ts +77 -0
  135. package/src/lib/server/identity-continuity.ts +127 -0
  136. package/src/lib/server/mailbox-utils.ts +347 -0
  137. package/src/lib/server/main-agent-loop.ts +28 -1103
  138. package/src/lib/server/memory-db.ts +4 -6
  139. package/src/lib/server/memory-tiers.ts +40 -0
  140. package/src/lib/server/openclaw-agent-resolver.test.ts +70 -0
  141. package/src/lib/server/openclaw-agent-resolver.ts +128 -0
  142. package/src/lib/server/openclaw-exec-config.ts +5 -6
  143. package/src/lib/server/openclaw-skills-normalize.test.ts +56 -0
  144. package/src/lib/server/openclaw-skills-normalize.ts +136 -0
  145. package/src/lib/server/openclaw-sync.ts +3 -2
  146. package/src/lib/server/orchestrator-lg.ts +20 -9
  147. package/src/lib/server/orchestrator.ts +7 -7
  148. package/src/lib/server/playwright-proxy.mjs +27 -3
  149. package/src/lib/server/plugins.test.ts +207 -0
  150. package/src/lib/server/plugins.ts +927 -66
  151. package/src/lib/server/provider-health.ts +38 -6
  152. package/src/lib/server/queue.ts +13 -28
  153. package/src/lib/server/scheduler.ts +2 -0
  154. package/src/lib/server/session-archive-memory.test.ts +85 -0
  155. package/src/lib/server/session-archive-memory.ts +230 -0
  156. package/src/lib/server/session-mailbox.ts +8 -18
  157. package/src/lib/server/session-reset-policy.test.ts +99 -0
  158. package/src/lib/server/session-reset-policy.ts +311 -0
  159. package/src/lib/server/session-run-manager.ts +33 -82
  160. package/src/lib/server/session-tools/autonomy-tools.test.ts +105 -0
  161. package/src/lib/server/session-tools/calendar.ts +366 -0
  162. package/src/lib/server/session-tools/canvas.ts +1 -1
  163. package/src/lib/server/session-tools/chatroom.ts +4 -2
  164. package/src/lib/server/session-tools/connector.ts +114 -10
  165. package/src/lib/server/session-tools/context.ts +21 -5
  166. package/src/lib/server/session-tools/crawl.ts +447 -0
  167. package/src/lib/server/session-tools/crud.ts +74 -28
  168. package/src/lib/server/session-tools/delegate-fallback.test.ts +219 -0
  169. package/src/lib/server/session-tools/delegate.ts +497 -24
  170. package/src/lib/server/session-tools/discovery.ts +24 -6
  171. package/src/lib/server/session-tools/document.ts +283 -0
  172. package/src/lib/server/session-tools/edit_file.ts +4 -2
  173. package/src/lib/server/session-tools/email.ts +320 -0
  174. package/src/lib/server/session-tools/extract.ts +137 -0
  175. package/src/lib/server/session-tools/file-normalize.test.ts +93 -0
  176. package/src/lib/server/session-tools/file-send.test.ts +84 -1
  177. package/src/lib/server/session-tools/file.ts +241 -25
  178. package/src/lib/server/session-tools/git.ts +1 -1
  179. package/src/lib/server/session-tools/http.ts +1 -1
  180. package/src/lib/server/session-tools/human-loop.ts +227 -0
  181. package/src/lib/server/session-tools/image-gen.ts +380 -0
  182. package/src/lib/server/session-tools/index.ts +130 -50
  183. package/src/lib/server/session-tools/mailbox.ts +276 -0
  184. package/src/lib/server/session-tools/memory.ts +172 -3
  185. package/src/lib/server/session-tools/monitor.ts +151 -8
  186. package/src/lib/server/session-tools/normalize-tool-args.ts +17 -14
  187. package/src/lib/server/session-tools/openclaw-nodes.ts +1 -1
  188. package/src/lib/server/session-tools/openclaw-workspace.ts +1 -1
  189. package/src/lib/server/session-tools/platform-normalize.test.ts +142 -0
  190. package/src/lib/server/session-tools/platform.ts +148 -7
  191. package/src/lib/server/session-tools/plugin-creator.ts +89 -26
  192. package/src/lib/server/session-tools/primitive-tools.test.ts +257 -0
  193. package/src/lib/server/session-tools/replicate.ts +301 -0
  194. package/src/lib/server/session-tools/sample-ui.ts +1 -1
  195. package/src/lib/server/session-tools/sandbox.ts +4 -2
  196. package/src/lib/server/session-tools/schedule.ts +24 -12
  197. package/src/lib/server/session-tools/session-info.ts +43 -7
  198. package/src/lib/server/session-tools/session-tools-wiring.test.ts +31 -17
  199. package/src/lib/server/session-tools/shell.ts +5 -2
  200. package/src/lib/server/session-tools/subagent.ts +194 -28
  201. package/src/lib/server/session-tools/table.ts +587 -0
  202. package/src/lib/server/session-tools/wallet.ts +42 -12
  203. package/src/lib/server/session-tools/web-browser-config.test.ts +39 -0
  204. package/src/lib/server/session-tools/web.ts +926 -91
  205. package/src/lib/server/storage.ts +255 -16
  206. package/src/lib/server/stream-agent-chat.ts +116 -268
  207. package/src/lib/server/structured-extract.test.ts +72 -0
  208. package/src/lib/server/structured-extract.ts +373 -0
  209. package/src/lib/server/task-mention.test.ts +16 -2
  210. package/src/lib/server/task-mention.ts +61 -10
  211. package/src/lib/server/tool-aliases.ts +66 -18
  212. package/src/lib/server/tool-capability-policy.test.ts +9 -9
  213. package/src/lib/server/tool-capability-policy.ts +38 -27
  214. package/src/lib/server/tool-retry.ts +2 -0
  215. package/src/lib/server/watch-jobs.test.ts +173 -0
  216. package/src/lib/server/watch-jobs.ts +532 -0
  217. package/src/lib/server/ws-hub.ts +5 -3
  218. package/src/lib/tool-definitions.ts +4 -0
  219. package/src/lib/validation/schemas.test.ts +26 -0
  220. package/src/lib/validation/schemas.ts +10 -1
  221. package/src/lib/ws-client.ts +14 -12
  222. package/src/proxy.ts +5 -5
  223. package/src/stores/use-app-store.ts +5 -11
  224. package/src/stores/use-chat-store.ts +38 -9
  225. package/src/types/index.ts +352 -47
  226. package/src/app/api/sessions/[id]/main-loop/route.ts +0 -94
  227. package/src/components/sessions/new-session-sheet.tsx +0 -253
  228. package/src/lib/server/main-session.ts +0 -24
  229. package/src/lib/server/session-run-manager.test.ts +0 -23
  230. /package/src/app/api/{sessions → chats}/[id]/clear/route.ts +0 -0
  231. /package/src/app/api/{sessions → chats}/[id]/deploy/route.ts +0 -0
  232. /package/src/app/api/{sessions → chats}/[id]/devserver/route.ts +0 -0
  233. /package/src/app/api/{sessions → chats}/[id]/edit-resend/route.ts +0 -0
  234. /package/src/app/api/{sessions → chats}/[id]/fork/route.ts +0 -0
  235. /package/src/app/api/{sessions → chats}/[id]/mailbox/route.ts +0 -0
  236. /package/src/app/api/{sessions → chats}/[id]/retry/route.ts +0 -0
  237. /package/src/app/api/{sessions → chats}/heartbeat/route.ts +0 -0
@@ -0,0 +1,587 @@
1
+ import path from 'path'
2
+ import { z } from 'zod'
3
+ import { tool, type StructuredToolInterface } from '@langchain/core/tools'
4
+ import type { Plugin, PluginHooks } from '@/types'
5
+ import { getPluginManager } from '../plugins'
6
+ import {
7
+ loadTabularFile,
8
+ normalizeInlineRows,
9
+ writeStructuredTable,
10
+ type StructuredTable,
11
+ } from '../document-utils'
12
+ import type { ToolBuildContext } from './context'
13
+ import { safePath } from './context'
14
+ import { normalizeToolInputArgs } from './normalize-tool-args'
15
+
16
+ interface TableCondition {
17
+ column: string
18
+ op: string
19
+ value?: unknown
20
+ }
21
+
22
+ interface SortSpec {
23
+ column: string
24
+ direction: 'asc' | 'desc'
25
+ }
26
+
27
+ interface GroupMetric {
28
+ op: 'count' | 'sum' | 'avg' | 'min' | 'max' | 'values'
29
+ column?: string
30
+ as?: string
31
+ }
32
+
33
+ function parseJsonValue<T>(value: unknown): T | null {
34
+ if (value === undefined || value === null) return null
35
+ if (typeof value === 'string') {
36
+ const trimmed = value.trim()
37
+ if (!trimmed) return null
38
+ try {
39
+ return JSON.parse(trimmed) as T
40
+ } catch {
41
+ return null
42
+ }
43
+ }
44
+ return value as T
45
+ }
46
+
47
+ function resolveTablePath(cwd: string, value: unknown): string {
48
+ if (typeof value !== 'string' || !value.trim()) throw new Error('filePath is required.')
49
+ return path.isAbsolute(value) ? path.resolve(value) : safePath(cwd, value)
50
+ }
51
+
52
+ async function loadPrimaryTable(normalized: Record<string, unknown>, cwd: string): Promise<StructuredTable> {
53
+ if (normalized.rows !== undefined) {
54
+ const parsed = parseJsonValue<unknown[]>(normalized.rows) ?? normalized.rows
55
+ return normalizeInlineRows(parsed)
56
+ }
57
+ const filePath = normalized.filePath ?? normalized.path
58
+ return loadTabularFile(resolveTablePath(cwd, filePath), {
59
+ sheetName: typeof normalized.sheetName === 'string' ? normalized.sheetName : undefined,
60
+ })
61
+ }
62
+
63
+ async function loadJoinTable(
64
+ normalized: Record<string, unknown>,
65
+ cwd: string,
66
+ side: 'left' | 'right',
67
+ ): Promise<StructuredTable> {
68
+ const rowsKey = side === 'left' ? 'leftRows' : 'rightRows'
69
+ const fileKey = side === 'left' ? 'leftFilePath' : 'rightFilePath'
70
+ const sheetKey = side === 'left' ? 'leftSheetName' : 'rightSheetName'
71
+ const rowSource = normalized[rowsKey] !== undefined
72
+ ? normalized[rowsKey]
73
+ : side === 'left'
74
+ ? normalized.rows
75
+ : undefined
76
+ if (rowSource !== undefined) {
77
+ const parsed = parseJsonValue<unknown[]>(rowSource) ?? rowSource
78
+ return normalizeInlineRows(parsed)
79
+ }
80
+ const fileSource = normalized[fileKey] !== undefined
81
+ ? normalized[fileKey]
82
+ : side === 'left'
83
+ ? normalized.filePath ?? normalized.path
84
+ : undefined
85
+ return loadTabularFile(resolveTablePath(cwd, fileSource), {
86
+ sheetName: typeof normalized[sheetKey] === 'string'
87
+ ? normalized[sheetKey] as string
88
+ : side === 'left' && typeof normalized.sheetName === 'string'
89
+ ? normalized.sheetName
90
+ : undefined,
91
+ })
92
+ }
93
+
94
+ function previewTable(table: StructuredTable, sample = 50) {
95
+ return {
96
+ name: table.name,
97
+ headers: table.headers,
98
+ rowCount: table.rowCount,
99
+ rows: table.rows.slice(0, sample),
100
+ truncated: table.rowCount > sample,
101
+ }
102
+ }
103
+
104
+ function scalarToString(value: unknown): string {
105
+ if (value === null || value === undefined) return ''
106
+ if (typeof value === 'string') return value
107
+ if (typeof value === 'number' || typeof value === 'boolean') return String(value)
108
+ return JSON.stringify(value)
109
+ }
110
+
111
+ function numericValue(value: unknown): number | null {
112
+ if (typeof value === 'number' && Number.isFinite(value)) return value
113
+ if (typeof value === 'string' && value.trim()) {
114
+ const parsed = Number(value)
115
+ if (Number.isFinite(parsed)) return parsed
116
+ }
117
+ return null
118
+ }
119
+
120
+ function compareValues(left: unknown, right: unknown): number {
121
+ const leftNumber = numericValue(left)
122
+ const rightNumber = numericValue(right)
123
+ if (leftNumber !== null && rightNumber !== null) return leftNumber - rightNumber
124
+ return scalarToString(left).localeCompare(scalarToString(right), undefined, { numeric: true, sensitivity: 'base' })
125
+ }
126
+
127
+ function normalizeConditions(normalized: Record<string, unknown>): TableCondition[] {
128
+ const where = parseJsonValue<TableCondition[]>(normalized.where) ?? (Array.isArray(normalized.where) ? normalized.where as TableCondition[] : [])
129
+ if (where.length > 0) {
130
+ return where
131
+ .filter((entry) => entry && typeof entry === 'object')
132
+ .map((entry) => ({
133
+ column: typeof entry.column === 'string' ? entry.column : '',
134
+ op: typeof entry.op === 'string' ? entry.op.toLowerCase() : 'eq',
135
+ value: entry.value,
136
+ }))
137
+ .filter((entry) => entry.column)
138
+ }
139
+
140
+ if (normalized.filters && typeof normalized.filters === 'object' && !Array.isArray(normalized.filters)) {
141
+ return Object.entries(normalized.filters as Record<string, unknown>).map(([column, value]) => ({
142
+ column,
143
+ op: 'eq',
144
+ value,
145
+ }))
146
+ }
147
+
148
+ if (typeof normalized.column === 'string' && normalized.column.trim()) {
149
+ if (normalized.greaterThan !== undefined) return [{ column: normalized.column, op: 'gt', value: normalized.greaterThan }]
150
+ if (normalized.greaterThanOrEqual !== undefined) return [{ column: normalized.column, op: 'gte', value: normalized.greaterThanOrEqual }]
151
+ if (normalized.lessThan !== undefined) return [{ column: normalized.column, op: 'lt', value: normalized.lessThan }]
152
+ if (normalized.lessThanOrEqual !== undefined) return [{ column: normalized.column, op: 'lte', value: normalized.lessThanOrEqual }]
153
+ if (normalized.contains !== undefined) return [{ column: normalized.column, op: 'contains', value: normalized.contains }]
154
+ if (normalized.equals !== undefined) return [{ column: normalized.column, op: 'eq', value: normalized.equals }]
155
+ }
156
+
157
+ return []
158
+ }
159
+
160
+ function rowMatchesConditions(row: Record<string, unknown>, conditions: TableCondition[]): boolean {
161
+ return conditions.every((condition) => {
162
+ const actual = row[condition.column]
163
+ const actualText = scalarToString(actual).toLowerCase()
164
+ const expectedText = scalarToString(condition.value).toLowerCase()
165
+
166
+ switch (condition.op) {
167
+ case 'eq':
168
+ return compareValues(actual, condition.value) === 0
169
+ case 'neq':
170
+ return compareValues(actual, condition.value) !== 0
171
+ case 'gt':
172
+ return compareValues(actual, condition.value) > 0
173
+ case 'gte':
174
+ return compareValues(actual, condition.value) >= 0
175
+ case 'lt':
176
+ return compareValues(actual, condition.value) < 0
177
+ case 'lte':
178
+ return compareValues(actual, condition.value) <= 0
179
+ case 'contains':
180
+ return actualText.includes(expectedText)
181
+ case 'regex':
182
+ if (typeof condition.value !== 'string' || !condition.value.trim()) return false
183
+ try {
184
+ return new RegExp(condition.value, 'i').test(actualText)
185
+ } catch {
186
+ return false
187
+ }
188
+ case 'in': {
189
+ const values = Array.isArray(condition.value) ? condition.value : [condition.value]
190
+ return values.some((entry) => compareValues(actual, entry) === 0)
191
+ }
192
+ case 'exists':
193
+ return actual !== null && actual !== undefined && scalarToString(actual).trim().length > 0
194
+ case 'not_empty':
195
+ return scalarToString(actual).trim().length > 0
196
+ default:
197
+ return compareValues(actual, condition.value) === 0
198
+ }
199
+ })
200
+ }
201
+
202
+ function normalizeSortSpecs(normalized: Record<string, unknown>): SortSpec[] {
203
+ const sort = parseJsonValue<SortSpec[]>(normalized.sort) ?? (Array.isArray(normalized.sort) ? normalized.sort as SortSpec[] : [])
204
+ if (sort.length > 0) {
205
+ return sort
206
+ .map((entry) => ({
207
+ column: typeof entry.column === 'string' ? entry.column : '',
208
+ direction: (entry.direction === 'desc' ? 'desc' : 'asc') as 'asc' | 'desc',
209
+ }))
210
+ .filter((entry) => entry.column)
211
+ }
212
+ if (typeof normalized.sortBy === 'string' && normalized.sortBy.trim()) {
213
+ return [{
214
+ column: normalized.sortBy,
215
+ direction: (normalized.direction === 'desc' ? 'desc' : 'asc') as 'asc' | 'desc',
216
+ }]
217
+ }
218
+ return []
219
+ }
220
+
221
+ function applySort(table: StructuredTable, specs: SortSpec[]): StructuredTable {
222
+ if (specs.length === 0) return table
223
+ const rows = [...table.rows].sort((left, right) => {
224
+ for (const spec of specs) {
225
+ const result = compareValues(left[spec.column], right[spec.column])
226
+ if (result !== 0) return spec.direction === 'desc' ? -result : result
227
+ }
228
+ return 0
229
+ })
230
+ return { ...table, rows, rowCount: rows.length }
231
+ }
232
+
233
+ function normalizeGroupMetrics(normalized: Record<string, unknown>): GroupMetric[] {
234
+ const metrics = parseJsonValue<GroupMetric[]>(normalized.metrics) ?? (Array.isArray(normalized.metrics) ? normalized.metrics as GroupMetric[] : [])
235
+ if (metrics.length > 0) {
236
+ return metrics.map((metric) => ({
237
+ op: ['count', 'sum', 'avg', 'min', 'max', 'values'].includes(metric.op) ? metric.op : 'count',
238
+ column: typeof metric.column === 'string' ? metric.column : undefined,
239
+ as: typeof metric.as === 'string' ? metric.as : undefined,
240
+ }))
241
+ }
242
+ return [{ op: 'count', as: 'count' }]
243
+ }
244
+
245
+ function groupTable(table: StructuredTable, by: string[], metrics: GroupMetric[]): StructuredTable {
246
+ const groups = new Map<string, Record<string, unknown>[]>()
247
+ for (const row of table.rows) {
248
+ const key = JSON.stringify(by.map((column) => row[column] ?? null))
249
+ const current = groups.get(key) || []
250
+ current.push(row)
251
+ groups.set(key, current)
252
+ }
253
+
254
+ const outRows = Array.from(groups.entries()).map(([key, rows]) => {
255
+ const groupValues = JSON.parse(key) as unknown[]
256
+ const next: Record<string, unknown> = {}
257
+ by.forEach((column, index) => {
258
+ next[column] = groupValues[index] ?? null
259
+ })
260
+ for (const metric of metrics) {
261
+ const name = metric.as || (metric.column ? `${metric.op}_${metric.column}` : metric.op)
262
+ const values = metric.column ? rows.map((row) => row[metric.column!]) : []
263
+ const numeric = values.map((value) => numericValue(value)).filter((value): value is number => value !== null)
264
+ switch (metric.op) {
265
+ case 'count':
266
+ next[name] = rows.length
267
+ break
268
+ case 'sum':
269
+ next[name] = numeric.reduce((total, value) => total + value, 0)
270
+ break
271
+ case 'avg':
272
+ next[name] = numeric.length ? numeric.reduce((total, value) => total + value, 0) / numeric.length : null
273
+ break
274
+ case 'min':
275
+ next[name] = numeric.length ? Math.min(...numeric) : null
276
+ break
277
+ case 'max':
278
+ next[name] = numeric.length ? Math.max(...numeric) : null
279
+ break
280
+ case 'values':
281
+ next[name] = Array.from(new Set(values.map((value) => scalarToString(value)).filter(Boolean)))
282
+ break
283
+ }
284
+ }
285
+ return next
286
+ })
287
+
288
+ const headers = Array.from(new Set([...by, ...metrics.map((metric) => metric.as || (metric.column ? `${metric.op}_${metric.column}` : metric.op))]))
289
+ return {
290
+ name: `${table.name || 'table'}_grouped`,
291
+ headers,
292
+ rows: outRows,
293
+ rowCount: outRows.length,
294
+ }
295
+ }
296
+
297
+ function dedupeTable(table: StructuredTable, keys: string[], keep: 'first' | 'last'): StructuredTable {
298
+ const seen = new Map<string, Record<string, unknown>>()
299
+ for (const row of table.rows) {
300
+ const key = JSON.stringify(keys.map((column) => row[column] ?? null))
301
+ if (keep === 'last' || !seen.has(key)) seen.set(key, row)
302
+ }
303
+ const rows = Array.from(seen.values())
304
+ return { ...table, rows, rowCount: rows.length }
305
+ }
306
+
307
+ function joinTables(
308
+ left: StructuredTable,
309
+ right: StructuredTable,
310
+ keys: string[],
311
+ joinType: 'inner' | 'left',
312
+ rightPrefix = 'right_',
313
+ ): StructuredTable {
314
+ const rightGroups = new Map<string, Record<string, unknown>[]>()
315
+ for (const row of right.rows) {
316
+ const key = JSON.stringify(keys.map((column) => row[column] ?? null))
317
+ const current = rightGroups.get(key) || []
318
+ current.push(row)
319
+ rightGroups.set(key, current)
320
+ }
321
+
322
+ const rightHeaders = right.headers.map((header) => (keys.includes(header) ? null : left.headers.includes(header) ? `${rightPrefix}${header}` : header)).filter((header): header is string => !!header)
323
+ const rows: Array<Record<string, unknown>> = []
324
+
325
+ for (const leftRow of left.rows) {
326
+ const key = JSON.stringify(keys.map((column) => leftRow[column] ?? null))
327
+ const matches = rightGroups.get(key) || []
328
+ if (matches.length === 0) {
329
+ if (joinType === 'left') rows.push({ ...leftRow })
330
+ continue
331
+ }
332
+ for (const rightRow of matches) {
333
+ const merged: Record<string, unknown> = { ...leftRow }
334
+ for (const header of right.headers) {
335
+ if (keys.includes(header)) continue
336
+ const target = left.headers.includes(header) ? `${rightPrefix}${header}` : header
337
+ merged[target] = rightRow[header]
338
+ }
339
+ rows.push(merged)
340
+ }
341
+ }
342
+
343
+ return {
344
+ name: `${left.name || 'left'}_joined_${right.name || 'right'}`,
345
+ headers: Array.from(new Set([...left.headers, ...rightHeaders])),
346
+ rows,
347
+ rowCount: rows.length,
348
+ }
349
+ }
350
+
351
+ function pivotTable(
352
+ table: StructuredTable,
353
+ indexColumns: string[],
354
+ columnField: string,
355
+ valueField: string,
356
+ aggregate: 'count' | 'sum' | 'first',
357
+ ): StructuredTable {
358
+ const columnValues = Array.from(new Set(table.rows.map((row) => scalarToString(row[columnField])).filter(Boolean)))
359
+ const grouped = new Map<string, Record<string, unknown>[]>()
360
+ for (const row of table.rows) {
361
+ const key = JSON.stringify(indexColumns.map((column) => row[column] ?? null))
362
+ const current = grouped.get(key) || []
363
+ current.push(row)
364
+ grouped.set(key, current)
365
+ }
366
+
367
+ const rows = Array.from(grouped.entries()).map(([key, groupRows]) => {
368
+ const base: Record<string, unknown> = {}
369
+ const indexValues = JSON.parse(key) as unknown[]
370
+ indexColumns.forEach((column, index) => {
371
+ base[column] = indexValues[index] ?? null
372
+ })
373
+ for (const columnValue of columnValues) {
374
+ const matches = groupRows.filter((row) => scalarToString(row[columnField]) === columnValue)
375
+ if (aggregate === 'count') {
376
+ base[columnValue] = matches.length
377
+ } else if (aggregate === 'sum') {
378
+ base[columnValue] = matches
379
+ .map((row) => numericValue(row[valueField]))
380
+ .filter((value): value is number => value !== null)
381
+ .reduce((total, value) => total + value, 0)
382
+ } else {
383
+ base[columnValue] = matches[0]?.[valueField] ?? null
384
+ }
385
+ }
386
+ return base
387
+ })
388
+
389
+ return {
390
+ name: `${table.name || 'table'}_pivot`,
391
+ headers: [...indexColumns, ...columnValues],
392
+ rows,
393
+ rowCount: rows.length,
394
+ }
395
+ }
396
+
397
+ async function maybePersistOutput(normalized: Record<string, unknown>, cwd: string, table: StructuredTable) {
398
+ const outputPath = typeof normalized.outputPath === 'string'
399
+ ? normalized.outputPath
400
+ : typeof normalized.saveTo === 'string'
401
+ ? normalized.saveTo
402
+ : typeof normalized.outputFilePath === 'string'
403
+ ? normalized.outputFilePath
404
+ : null
405
+ if (!outputPath) return null
406
+ const resolved = path.isAbsolute(outputPath) ? path.resolve(outputPath) : safePath(cwd, outputPath)
407
+ return writeStructuredTable(resolved, table)
408
+ }
409
+
410
+ async function executeTableAction(args: Record<string, unknown>, bctx: { cwd: string }) {
411
+ const normalized = normalizeToolInputArgs(args)
412
+ const action = String(normalized.action || 'read').trim().toLowerCase()
413
+
414
+ try {
415
+ if (action === 'status') {
416
+ return JSON.stringify({
417
+ supports: ['read', 'load_csv', 'load_xlsx', 'summarize', 'filter', 'sort', 'group', 'pivot', 'dedupe', 'join', 'write'],
418
+ })
419
+ }
420
+
421
+ if (action === 'join') {
422
+ const left = await loadJoinTable(normalized, bctx.cwd, 'left')
423
+ const right = await loadJoinTable(normalized, bctx.cwd, 'right')
424
+ const keys = Array.isArray(normalized.on)
425
+ ? normalized.on.filter((entry): entry is string => typeof entry === 'string' && entry.trim().length > 0)
426
+ : typeof normalized.on === 'string'
427
+ ? [normalized.on]
428
+ : []
429
+ if (keys.length === 0) return 'Error: on is required for join.'
430
+ const joined = joinTables(
431
+ left,
432
+ right,
433
+ keys,
434
+ normalized.joinType === 'left' ? 'left' : 'inner',
435
+ typeof normalized.rightPrefix === 'string' && normalized.rightPrefix.trim() ? normalized.rightPrefix : 'right_',
436
+ )
437
+ const persisted = await maybePersistOutput(normalized, bctx.cwd, joined)
438
+ return JSON.stringify({ action, ...previewTable(joined), output: persisted })
439
+ }
440
+
441
+ let table = await loadPrimaryTable(normalized, bctx.cwd)
442
+
443
+ if (action === 'read' || action === 'load_csv' || action === 'load_xlsx') {
444
+ return JSON.stringify({ action: 'read', ...previewTable(table) })
445
+ }
446
+
447
+ if (action === 'summarize') {
448
+ const nonEmptyCounts = Object.fromEntries(table.headers.map((header) => [
449
+ header,
450
+ table.rows.filter((row) => scalarToString(row[header]).trim().length > 0).length,
451
+ ]))
452
+ return JSON.stringify({
453
+ name: table.name,
454
+ headers: table.headers,
455
+ rowCount: table.rowCount,
456
+ nonEmptyCounts,
457
+ sample: table.rows.slice(0, 10),
458
+ })
459
+ }
460
+
461
+ if (action === 'filter') {
462
+ const conditions = normalizeConditions(normalized)
463
+ if (conditions.length === 0) return 'Error: where or filters is required for filter.'
464
+ const rows = table.rows.filter((row) => rowMatchesConditions(row, conditions))
465
+ table = { ...table, rows, rowCount: rows.length }
466
+ } else if (action === 'sort') {
467
+ table = applySort(table, normalizeSortSpecs(normalized))
468
+ } else if (action === 'group') {
469
+ const by = Array.isArray(normalized.by)
470
+ ? normalized.by.filter((entry): entry is string => typeof entry === 'string' && entry.trim().length > 0)
471
+ : typeof normalized.by === 'string'
472
+ ? [normalized.by]
473
+ : []
474
+ if (by.length === 0) return 'Error: by is required for group.'
475
+ table = groupTable(table, by, normalizeGroupMetrics(normalized))
476
+ } else if (action === 'pivot') {
477
+ const indexColumns = Array.isArray(normalized.index)
478
+ ? normalized.index.filter((entry): entry is string => typeof entry === 'string' && entry.trim().length > 0)
479
+ : typeof normalized.index === 'string'
480
+ ? [normalized.index]
481
+ : []
482
+ const columnField = typeof normalized.columns === 'string' ? normalized.columns : typeof normalized.column === 'string' ? normalized.column : ''
483
+ const valueField = typeof normalized.value === 'string' ? normalized.value : ''
484
+ if (indexColumns.length === 0 || !columnField || !valueField) {
485
+ return 'Error: index, columns, and value are required for pivot.'
486
+ }
487
+ const aggregate = normalized.aggregate === 'sum' || normalized.aggregate === 'count' ? normalized.aggregate : 'first'
488
+ table = pivotTable(table, indexColumns, columnField, valueField, aggregate)
489
+ } else if (action === 'dedupe') {
490
+ const keys = Array.isArray(normalized.on)
491
+ ? normalized.on.filter((entry): entry is string => typeof entry === 'string' && entry.trim().length > 0)
492
+ : typeof normalized.on === 'string'
493
+ ? [normalized.on]
494
+ : Array.isArray(normalized.columns)
495
+ ? normalized.columns.filter((entry): entry is string => typeof entry === 'string' && entry.trim().length > 0)
496
+ : table.headers
497
+ table = dedupeTable(table, keys, normalized.keep === 'last' ? 'last' : 'first')
498
+ } else if (action === 'write') {
499
+ const persisted = await maybePersistOutput(normalized, bctx.cwd, table)
500
+ if (!persisted) return 'Error: outputPath or saveTo is required for write.'
501
+ return JSON.stringify({ action: 'write', output: persisted, ...previewTable(table) })
502
+ } else {
503
+ return `Error: Unknown action "${action}".`
504
+ }
505
+
506
+ const persisted = await maybePersistOutput(normalized, bctx.cwd, table)
507
+ return JSON.stringify({ action, ...previewTable(table), output: persisted })
508
+ } catch (err: unknown) {
509
+ return `Error: ${err instanceof Error ? err.message : String(err)}`
510
+ }
511
+ }
512
+
513
+ const TablePlugin: Plugin = {
514
+ name: 'Table',
515
+ enabledByDefault: false,
516
+ description: 'Load, transform, join, pivot, and export CSV/XLSX/JSON tables without dropping to shell scripts.',
517
+ hooks: {
518
+ getCapabilityDescription: () =>
519
+ 'I can load and transform tabular data with `table`, including filtering, sorting, grouping, pivoting, deduping, joining, summarizing, and exporting results.',
520
+ } as PluginHooks,
521
+ tools: [
522
+ {
523
+ name: 'table',
524
+ description: 'Tabular data tool. Actions: read, load_csv, load_xlsx, summarize, filter, sort, group, pivot, dedupe, join, write, status.',
525
+ parameters: {
526
+ type: 'object',
527
+ properties: {
528
+ action: {
529
+ type: 'string',
530
+ enum: ['read', 'load_csv', 'load_xlsx', 'summarize', 'filter', 'sort', 'group', 'pivot', 'dedupe', 'join', 'write', 'status'],
531
+ },
532
+ filePath: { type: 'string' },
533
+ rows: {},
534
+ where: {},
535
+ filters: {},
536
+ sort: {},
537
+ sortBy: { type: 'string' },
538
+ direction: { type: 'string', enum: ['asc', 'desc'] },
539
+ by: {},
540
+ metrics: {},
541
+ index: {},
542
+ columns: { type: 'string' },
543
+ value: { type: 'string' },
544
+ aggregate: { type: 'string', enum: ['first', 'count', 'sum'] },
545
+ on: {},
546
+ keep: { type: 'string', enum: ['first', 'last'] },
547
+ leftFilePath: { type: 'string' },
548
+ rightFilePath: { type: 'string' },
549
+ leftRows: {},
550
+ rightRows: {},
551
+ joinType: { type: 'string', enum: ['inner', 'left'] },
552
+ rightPrefix: { type: 'string' },
553
+ outputPath: { type: 'string' },
554
+ outputFilePath: { type: 'string' },
555
+ saveTo: { type: 'string' },
556
+ greaterThan: {},
557
+ greaterThanOrEqual: {},
558
+ lessThan: {},
559
+ lessThanOrEqual: {},
560
+ equals: {},
561
+ contains: {},
562
+ sheetName: { type: 'string' },
563
+ leftSheetName: { type: 'string' },
564
+ rightSheetName: { type: 'string' },
565
+ },
566
+ required: ['action'],
567
+ },
568
+ execute: async (args, context) => executeTableAction(args, { cwd: context.session.cwd || process.cwd() }),
569
+ },
570
+ ],
571
+ }
572
+
573
+ getPluginManager().registerBuiltin('table', TablePlugin)
574
+
575
+ export function buildTableTools(bctx: ToolBuildContext): StructuredToolInterface[] {
576
+ if (!bctx.hasPlugin('table')) return []
577
+ return [
578
+ tool(
579
+ async (args) => executeTableAction(args, { cwd: bctx.cwd }),
580
+ {
581
+ name: 'table',
582
+ description: TablePlugin.tools![0].description,
583
+ schema: z.object({}).passthrough(),
584
+ },
585
+ ),
586
+ ]
587
+ }
@@ -63,21 +63,24 @@ async function executeWalletAction(args: any, context: { agentId?: string | null
63
63
  if (!amountSol || amountSol <= 0) return JSON.stringify({ error: 'amountSol must be positive' })
64
64
 
65
65
  if (normalized.approved !== true) {
66
- const { requestApproval } = await import('../approvals')
67
- requestApproval({
66
+ const { requestApprovalMaybeAutoApprove } = await import('../approvals')
67
+ const approval = await requestApprovalMaybeAutoApprove({
68
68
  category: 'wallet_transfer',
69
69
  title: `Send ${amountSol} SOL`,
70
70
  description: `Transfer to ${toAddress}. Memo: ${memo || 'none'}`,
71
71
  data: { toAddress, amountSol, memo },
72
72
  agentId: context.agentId,
73
73
  })
74
- return JSON.stringify({
75
- type: 'plugin_wallet_transfer_request',
76
- amountSol,
77
- toAddress,
78
- memo,
79
- message: `I'm requesting to send ${amountSol} SOL to ${toAddress}. Please approve this transaction.`
80
- })
74
+ if (approval.status !== 'approved') {
75
+ return JSON.stringify({
76
+ type: 'plugin_wallet_transfer_request',
77
+ amountSol,
78
+ toAddress,
79
+ memo,
80
+ message: `I'm requesting to send ${amountSol} SOL to ${toAddress}. Please approve this transaction.`
81
+ })
82
+ }
83
+ normalized.approved = true
81
84
  }
82
85
 
83
86
  const { isValidSolanaAddress, solToLamports, lamportsToSol } = await import('../solana')
@@ -135,7 +138,34 @@ async function executeWalletAction(args: any, context: { agentId?: string | null
135
138
  const WalletPlugin: Plugin = {
136
139
  name: 'Core Wallet',
137
140
  description: 'Manage agent crypto wallet: check balance, send SOL, view transactions.',
138
- hooks: {} as PluginHooks,
141
+ hooks: {
142
+ getAgentContext: async (ctx) => {
143
+ const agentId = ctx.session.agentId
144
+ if (!agentId) return null
145
+ const { loadWallets } = await import('../storage')
146
+ const wallets = loadWallets() as Record<string, { agentId: string; publicKey: string; chain: string; spendingLimitLamports?: number; dailyLimitLamports?: number; requireApproval: boolean }>
147
+ const wallet = Object.values(wallets).find((w) => w.agentId === agentId)
148
+ if (!wallet) return null
149
+ const { getBalance, lamportsToSol } = await import('../solana')
150
+ let balanceSol = 0
151
+ try {
152
+ const lamports = await getBalance(wallet.publicKey)
153
+ balanceSol = lamportsToSol(lamports)
154
+ } catch { /* RPC failure — show 0 */ }
155
+ const perTxLimit = lamportsToSol(wallet.spendingLimitLamports ?? 100_000_000)
156
+ const dailyLimit = lamportsToSol(wallet.dailyLimitLamports ?? 1_000_000_000)
157
+ return [
158
+ '## Your Wallet',
159
+ `You own a ${wallet.chain} wallet — this is your wallet, your funds, your address. Speak about it in the first person ("my wallet", "my balance", "I have X SOL"). You can spend from it freely within your limits${wallet.requireApproval ? ', though the user needs to approve each send before it goes through' : ''}.`,
160
+ `- Address: ${wallet.publicKey}`,
161
+ `- Balance: ${balanceSol} SOL`,
162
+ `- Per-transaction limit: ${perTxLimit} SOL`,
163
+ `- Daily limit: ${dailyLimit} SOL`,
164
+ 'Use the `wallet_tool` to check your balance, send SOL, or view your transaction history.',
165
+ ].join('\n')
166
+ },
167
+ getCapabilityDescription: () => 'I have my own crypto wallet (`wallet_tool`) — I can check my balance, send SOL, and review my transaction history.',
168
+ } as PluginHooks,
139
169
  ui: {
140
170
  sidebarItems: [
141
171
  {
@@ -148,7 +178,7 @@ const WalletPlugin: Plugin = {
148
178
  headerWidgets: [
149
179
  {
150
180
  id: 'wallet-status',
151
- label: '💎 Wallet Active'
181
+ label: 'Wallet'
152
182
  }
153
183
  ]
154
184
  },
@@ -179,7 +209,7 @@ getPluginManager().registerBuiltin('wallet', WalletPlugin)
179
209
  * Legacy Bridge
180
210
  */
181
211
  export function buildWalletTools(bctx: ToolBuildContext): StructuredToolInterface[] {
182
- if (!bctx.hasTool('wallet')) return []
212
+ if (!bctx.hasPlugin('wallet')) return []
183
213
  return [
184
214
  tool(
185
215
  async (args) => executeWalletAction(args, { agentId: bctx.ctx?.agentId }),