@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.
- package/README.md +155 -150
- package/package.json +1 -1
- package/src/app/api/agents/[id]/route.ts +26 -0
- package/src/app/api/agents/[id]/thread/route.ts +37 -9
- package/src/app/api/agents/route.ts +13 -2
- package/src/app/api/auth/route.ts +76 -7
- package/src/app/api/chatrooms/[id]/chat/route.ts +7 -2
- package/src/app/api/{sessions → chats}/[id]/browser/route.ts +5 -1
- package/src/app/api/{sessions → chats}/[id]/chat/route.ts +7 -3
- package/src/app/api/{sessions → chats}/[id]/checkpoints/route.ts +1 -1
- package/src/app/api/chats/[id]/main-loop/route.ts +13 -0
- package/src/app/api/{sessions → chats}/[id]/messages/route.ts +19 -13
- package/src/app/api/{sessions → chats}/[id]/restore/route.ts +1 -1
- package/src/app/api/{sessions → chats}/[id]/route.ts +22 -52
- package/src/app/api/{sessions → chats}/[id]/stop/route.ts +6 -1
- package/src/app/api/{sessions → chats}/route.ts +21 -7
- package/src/app/api/connectors/[id]/doctor/route.ts +26 -0
- package/src/app/api/connectors/doctor/route.ts +13 -0
- package/src/app/api/files/open/route.ts +16 -14
- package/src/app/api/memory/maintenance/route.ts +11 -1
- package/src/app/api/openclaw/agent-files/route.ts +27 -4
- package/src/app/api/openclaw/skills/route.ts +11 -3
- package/src/app/api/plugins/dependencies/route.ts +24 -0
- package/src/app/api/plugins/install/route.ts +15 -92
- package/src/app/api/plugins/route.ts +6 -26
- package/src/app/api/plugins/settings/route.ts +40 -0
- package/src/app/api/plugins/ui/route.ts +1 -0
- package/src/app/api/settings/route.ts +49 -7
- package/src/app/api/tasks/[id]/route.ts +15 -6
- package/src/app/api/tasks/bulk/route.ts +2 -2
- package/src/app/api/tasks/route.ts +9 -4
- package/src/app/api/usage/route.ts +30 -0
- package/src/app/api/webhooks/[id]/route.ts +8 -1
- package/src/app/page.tsx +9 -2
- package/src/cli/index.js +39 -33
- package/src/cli/index.ts +43 -49
- package/src/cli/spec.js +29 -27
- package/src/components/agents/agent-card.tsx +16 -13
- package/src/components/agents/agent-chat-list.tsx +104 -4
- package/src/components/agents/agent-list.tsx +54 -22
- package/src/components/agents/agent-sheet.tsx +209 -18
- package/src/components/agents/cron-job-form.tsx +3 -3
- package/src/components/agents/inspector-panel.tsx +110 -50
- package/src/components/auth/access-key-gate.tsx +36 -97
- package/src/components/auth/setup-wizard.tsx +5 -38
- package/src/components/chat/chat-area.tsx +39 -27
- package/src/components/{sessions/session-card.tsx → chat/chat-card.tsx} +7 -23
- package/src/components/chat/chat-header.tsx +299 -314
- package/src/components/{sessions/session-list.tsx → chat/chat-list.tsx} +11 -14
- package/src/components/chat/chat-tool-toggles.tsx +26 -17
- package/src/components/chat/checkpoint-timeline.tsx +4 -4
- package/src/components/chat/message-bubble.tsx +4 -1
- package/src/components/chat/message-list.tsx +5 -3
- package/src/components/chat/session-debug-panel.tsx +1 -1
- package/src/components/chat/tool-request-banner.tsx +3 -3
- package/src/components/chatrooms/agent-hover-card.tsx +3 -3
- package/src/components/chatrooms/chatroom-tool-request-banner.tsx +2 -2
- package/src/components/chatrooms/chatroom-view.tsx +347 -205
- package/src/components/connectors/connector-list.tsx +265 -127
- package/src/components/connectors/connector-sheet.tsx +218 -1
- package/src/components/home/home-view.tsx +129 -5
- package/src/components/layout/app-layout.tsx +392 -182
- package/src/components/layout/mobile-header.tsx +26 -8
- package/src/components/plugins/plugin-list.tsx +487 -254
- package/src/components/plugins/plugin-sheet.tsx +236 -13
- package/src/components/projects/project-detail.tsx +183 -0
- package/src/components/settings/gateway-connection-panel.tsx +1 -1
- package/src/components/shared/agent-picker-list.tsx +2 -2
- package/src/components/shared/command-palette.tsx +111 -25
- package/src/components/shared/settings/plugin-manager.tsx +20 -4
- package/src/components/shared/settings/section-capability-policy.tsx +105 -0
- package/src/components/shared/settings/section-heartbeat.tsx +78 -1
- package/src/components/shared/settings/section-orchestrator.tsx +3 -3
- package/src/components/shared/settings/section-providers.tsx +1 -1
- package/src/components/shared/settings/section-runtime-loop.tsx +5 -5
- package/src/components/shared/settings/section-secrets.tsx +6 -6
- package/src/components/shared/settings/section-user-preferences.tsx +1 -1
- package/src/components/shared/settings/section-voice.tsx +5 -1
- package/src/components/shared/settings/section-web-search.tsx +10 -2
- package/src/components/shared/settings/settings-page.tsx +244 -56
- package/src/components/tasks/approvals-panel.tsx +205 -18
- package/src/components/tasks/task-board.tsx +242 -46
- package/src/components/usage/metrics-dashboard.tsx +147 -1
- package/src/components/wallets/wallet-panel.tsx +17 -5
- package/src/components/webhooks/webhook-sheet.tsx +8 -8
- package/src/lib/auth.ts +17 -0
- package/src/lib/chat-streaming-state.test.ts +108 -0
- package/src/lib/chat-streaming-state.ts +108 -0
- package/src/lib/chat.ts +1 -1
- package/src/lib/{sessions.ts → chats.ts} +28 -18
- package/src/lib/openclaw-agent-id.test.ts +14 -0
- package/src/lib/openclaw-agent-id.ts +31 -0
- package/src/lib/providers/claude-cli.ts +1 -1
- package/src/lib/server/agent-assignment.test.ts +112 -0
- package/src/lib/server/agent-assignment.ts +169 -0
- package/src/lib/server/approval-connector-notify.test.ts +253 -0
- package/src/lib/server/approvals-auto-approve.test.ts +205 -0
- package/src/lib/server/approvals.ts +483 -75
- package/src/lib/server/autonomy-runtime.test.ts +341 -0
- package/src/lib/server/browser-state.test.ts +118 -0
- package/src/lib/server/browser-state.ts +123 -0
- package/src/lib/server/build-llm.test.ts +36 -0
- package/src/lib/server/build-llm.ts +11 -4
- package/src/lib/server/builtin-plugins.ts +34 -0
- package/src/lib/server/capability-router.ts +10 -8
- package/src/lib/server/chat-execution-heartbeat.test.ts +40 -0
- package/src/lib/server/chat-execution-tool-events.test.ts +134 -0
- package/src/lib/server/chat-execution.ts +285 -165
- package/src/lib/server/chatroom-health.test.ts +26 -0
- package/src/lib/server/chatroom-health.ts +2 -3
- package/src/lib/server/chatroom-helpers.test.ts +67 -2
- package/src/lib/server/chatroom-helpers.ts +48 -8
- package/src/lib/server/connectors/discord.ts +175 -11
- package/src/lib/server/connectors/doctor.test.ts +80 -0
- package/src/lib/server/connectors/doctor.ts +116 -0
- package/src/lib/server/connectors/manager.ts +948 -112
- package/src/lib/server/connectors/policy.test.ts +222 -0
- package/src/lib/server/connectors/policy.ts +452 -0
- package/src/lib/server/connectors/slack.ts +188 -9
- package/src/lib/server/connectors/telegram.ts +65 -15
- package/src/lib/server/connectors/thread-context.test.ts +44 -0
- package/src/lib/server/connectors/thread-context.ts +72 -0
- package/src/lib/server/connectors/types.ts +41 -11
- package/src/lib/server/cost.ts +34 -1
- package/src/lib/server/daemon-state.ts +61 -3
- package/src/lib/server/data-dir.ts +13 -0
- package/src/lib/server/delegation-jobs.test.ts +140 -0
- package/src/lib/server/delegation-jobs.ts +248 -0
- package/src/lib/server/document-utils.test.ts +47 -0
- package/src/lib/server/document-utils.ts +397 -0
- package/src/lib/server/heartbeat-service.ts +14 -40
- package/src/lib/server/heartbeat-source.test.ts +22 -0
- package/src/lib/server/heartbeat-source.ts +7 -0
- package/src/lib/server/identity-continuity.test.ts +77 -0
- package/src/lib/server/identity-continuity.ts +127 -0
- package/src/lib/server/mailbox-utils.ts +347 -0
- package/src/lib/server/main-agent-loop.ts +28 -1103
- package/src/lib/server/memory-db.ts +4 -6
- package/src/lib/server/memory-tiers.ts +40 -0
- package/src/lib/server/openclaw-agent-resolver.test.ts +70 -0
- package/src/lib/server/openclaw-agent-resolver.ts +128 -0
- package/src/lib/server/openclaw-exec-config.ts +5 -6
- package/src/lib/server/openclaw-skills-normalize.test.ts +56 -0
- package/src/lib/server/openclaw-skills-normalize.ts +136 -0
- package/src/lib/server/openclaw-sync.ts +3 -2
- package/src/lib/server/orchestrator-lg.ts +20 -9
- package/src/lib/server/orchestrator.ts +7 -7
- package/src/lib/server/playwright-proxy.mjs +27 -3
- package/src/lib/server/plugins.test.ts +207 -0
- package/src/lib/server/plugins.ts +927 -66
- package/src/lib/server/provider-health.ts +38 -6
- package/src/lib/server/queue.ts +13 -28
- package/src/lib/server/scheduler.ts +2 -0
- package/src/lib/server/session-archive-memory.test.ts +85 -0
- package/src/lib/server/session-archive-memory.ts +230 -0
- package/src/lib/server/session-mailbox.ts +8 -18
- package/src/lib/server/session-reset-policy.test.ts +99 -0
- package/src/lib/server/session-reset-policy.ts +311 -0
- package/src/lib/server/session-run-manager.ts +33 -82
- package/src/lib/server/session-tools/autonomy-tools.test.ts +105 -0
- package/src/lib/server/session-tools/calendar.ts +366 -0
- package/src/lib/server/session-tools/canvas.ts +1 -1
- package/src/lib/server/session-tools/chatroom.ts +4 -2
- package/src/lib/server/session-tools/connector.ts +114 -10
- package/src/lib/server/session-tools/context.ts +21 -5
- package/src/lib/server/session-tools/crawl.ts +447 -0
- package/src/lib/server/session-tools/crud.ts +74 -28
- package/src/lib/server/session-tools/delegate-fallback.test.ts +219 -0
- package/src/lib/server/session-tools/delegate.ts +497 -24
- package/src/lib/server/session-tools/discovery.ts +24 -6
- package/src/lib/server/session-tools/document.ts +283 -0
- package/src/lib/server/session-tools/edit_file.ts +4 -2
- package/src/lib/server/session-tools/email.ts +320 -0
- package/src/lib/server/session-tools/extract.ts +137 -0
- package/src/lib/server/session-tools/file-normalize.test.ts +93 -0
- package/src/lib/server/session-tools/file-send.test.ts +84 -1
- package/src/lib/server/session-tools/file.ts +241 -25
- package/src/lib/server/session-tools/git.ts +1 -1
- package/src/lib/server/session-tools/http.ts +1 -1
- package/src/lib/server/session-tools/human-loop.ts +227 -0
- package/src/lib/server/session-tools/image-gen.ts +380 -0
- package/src/lib/server/session-tools/index.ts +130 -50
- package/src/lib/server/session-tools/mailbox.ts +276 -0
- package/src/lib/server/session-tools/memory.ts +172 -3
- package/src/lib/server/session-tools/monitor.ts +151 -8
- package/src/lib/server/session-tools/normalize-tool-args.ts +17 -14
- package/src/lib/server/session-tools/openclaw-nodes.ts +1 -1
- package/src/lib/server/session-tools/openclaw-workspace.ts +1 -1
- package/src/lib/server/session-tools/platform-normalize.test.ts +142 -0
- package/src/lib/server/session-tools/platform.ts +148 -7
- package/src/lib/server/session-tools/plugin-creator.ts +89 -26
- package/src/lib/server/session-tools/primitive-tools.test.ts +257 -0
- package/src/lib/server/session-tools/replicate.ts +301 -0
- package/src/lib/server/session-tools/sample-ui.ts +1 -1
- package/src/lib/server/session-tools/sandbox.ts +4 -2
- package/src/lib/server/session-tools/schedule.ts +24 -12
- package/src/lib/server/session-tools/session-info.ts +43 -7
- package/src/lib/server/session-tools/session-tools-wiring.test.ts +31 -17
- package/src/lib/server/session-tools/shell.ts +5 -2
- package/src/lib/server/session-tools/subagent.ts +194 -28
- package/src/lib/server/session-tools/table.ts +587 -0
- package/src/lib/server/session-tools/wallet.ts +42 -12
- package/src/lib/server/session-tools/web-browser-config.test.ts +39 -0
- package/src/lib/server/session-tools/web.ts +926 -91
- package/src/lib/server/storage.ts +255 -16
- package/src/lib/server/stream-agent-chat.ts +116 -268
- package/src/lib/server/structured-extract.test.ts +72 -0
- package/src/lib/server/structured-extract.ts +373 -0
- package/src/lib/server/task-mention.test.ts +16 -2
- package/src/lib/server/task-mention.ts +61 -10
- package/src/lib/server/tool-aliases.ts +66 -18
- package/src/lib/server/tool-capability-policy.test.ts +9 -9
- package/src/lib/server/tool-capability-policy.ts +38 -27
- package/src/lib/server/tool-retry.ts +2 -0
- package/src/lib/server/watch-jobs.test.ts +173 -0
- package/src/lib/server/watch-jobs.ts +532 -0
- package/src/lib/server/ws-hub.ts +5 -3
- package/src/lib/tool-definitions.ts +4 -0
- package/src/lib/validation/schemas.test.ts +26 -0
- package/src/lib/validation/schemas.ts +10 -1
- package/src/lib/ws-client.ts +14 -12
- package/src/proxy.ts +5 -5
- package/src/stores/use-app-store.ts +5 -11
- package/src/stores/use-chat-store.ts +38 -9
- package/src/types/index.ts +352 -47
- package/src/app/api/sessions/[id]/main-loop/route.ts +0 -94
- package/src/components/sessions/new-session-sheet.tsx +0 -253
- package/src/lib/server/main-session.ts +0 -24
- package/src/lib/server/session-run-manager.test.ts +0 -23
- /package/src/app/api/{sessions → chats}/[id]/clear/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/deploy/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/devserver/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/edit-resend/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/fork/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/mailbox/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/retry/route.ts +0 -0
- /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 {
|
|
67
|
-
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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: {
|
|
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: '
|
|
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.
|
|
212
|
+
if (!bctx.hasPlugin('wallet')) return []
|
|
183
213
|
return [
|
|
184
214
|
tool(
|
|
185
215
|
async (args) => executeWalletAction(args, { agentId: bctx.ctx?.agentId }),
|