@swarmclawai/swarmclaw 0.7.2 → 0.7.4
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 +116 -50
- package/bin/package-manager.js +157 -0
- package/bin/package-manager.test.js +90 -0
- package/bin/server-cmd.js +38 -7
- package/bin/swarmclaw.js +54 -4
- package/bin/update-cmd.js +48 -10
- package/bin/update-cmd.test.js +55 -0
- package/package.json +8 -3
- package/scripts/postinstall.mjs +26 -0
- package/src/app/api/agents/[id]/route.ts +43 -0
- package/src/app/api/agents/[id]/thread/route.ts +39 -8
- package/src/app/api/agents/route.ts +35 -2
- package/src/app/api/auth/route.ts +77 -8
- package/src/app/api/chatrooms/[id]/chat/route.ts +22 -6
- package/src/app/api/chatrooms/[id]/pins/route.ts +2 -1
- package/src/app/api/chatrooms/[id]/reactions/route.ts +2 -1
- package/src/app/api/chatrooms/[id]/route.ts +6 -0
- package/src/app/api/chats/[id]/browser/route.ts +5 -1
- package/src/app/api/chats/[id]/chat/route.ts +7 -3
- package/src/app/api/chats/[id]/messages/route.ts +19 -13
- package/src/app/api/chats/[id]/route.ts +30 -0
- package/src/app/api/chats/[id]/stop/route.ts +6 -1
- package/src/app/api/chats/heartbeat/route.ts +2 -1
- package/src/app/api/chats/route.ts +23 -1
- 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/external-agents/[id]/heartbeat/route.ts +33 -0
- package/src/app/api/external-agents/[id]/route.ts +31 -0
- package/src/app/api/external-agents/register/route.ts +3 -0
- package/src/app/api/external-agents/route.ts +66 -0
- package/src/app/api/files/open/route.ts +16 -14
- package/src/app/api/gateways/[id]/health/route.ts +28 -0
- package/src/app/api/gateways/[id]/route.ts +79 -0
- package/src/app/api/gateways/route.ts +57 -0
- 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/gateway/route.ts +10 -7
- package/src/app/api/openclaw/skills/route.ts +12 -4
- 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 +3 -26
- package/src/app/api/plugins/settings/route.ts +17 -12
- package/src/app/api/plugins/ui/route.ts +1 -0
- package/src/app/api/providers/[id]/discover-models/route.ts +27 -0
- package/src/app/api/schedules/[id]/route.ts +38 -9
- package/src/app/api/schedules/route.ts +51 -28
- package/src/app/api/settings/route.ts +55 -17
- package/src/app/api/setup/doctor/route.ts +6 -4
- package/src/app/api/tasks/[id]/route.ts +16 -6
- package/src/app/api/tasks/bulk/route.ts +3 -3
- package/src/app/api/tasks/route.ts +9 -4
- package/src/app/api/webhooks/[id]/route.ts +8 -1
- package/src/app/page.tsx +135 -17
- package/src/cli/binary.test.js +142 -0
- package/src/cli/index.js +38 -11
- package/src/cli/index.test.js +195 -0
- package/src/cli/index.ts +21 -12
- package/src/cli/server-cmd.test.js +59 -0
- package/src/cli/spec.js +20 -2
- package/src/components/agents/agent-card.tsx +15 -12
- package/src/components/agents/agent-chat-list.tsx +101 -1
- package/src/components/agents/agent-list.tsx +46 -9
- package/src/components/agents/agent-sheet.tsx +456 -23
- package/src/components/agents/inspector-panel.tsx +110 -49
- package/src/components/agents/sandbox-env-panel.tsx +4 -1
- package/src/components/auth/access-key-gate.tsx +36 -97
- package/src/components/auth/setup-wizard.tsx +970 -275
- package/src/components/chat/chat-area.tsx +70 -27
- package/src/components/chat/chat-card.tsx +6 -21
- package/src/components/chat/chat-header.tsx +263 -366
- package/src/components/chat/chat-list.tsx +62 -26
- package/src/components/chat/checkpoint-timeline.tsx +1 -1
- package/src/components/chat/message-list.tsx +145 -19
- package/src/components/chatrooms/chatroom-input.tsx +96 -33
- package/src/components/chatrooms/chatroom-list.tsx +141 -72
- package/src/components/chatrooms/chatroom-message.tsx +7 -6
- package/src/components/chatrooms/chatroom-sheet.tsx +13 -1
- package/src/components/chatrooms/chatroom-tool-request-banner.tsx +5 -2
- package/src/components/chatrooms/chatroom-view.tsx +422 -209
- package/src/components/chatrooms/reaction-picker.tsx +38 -33
- package/src/components/connectors/connector-list.tsx +265 -127
- package/src/components/connectors/connector-sheet.tsx +217 -0
- package/src/components/gateways/gateway-sheet.tsx +567 -0
- package/src/components/home/home-view.tsx +128 -4
- package/src/components/input/chat-input.tsx +135 -86
- package/src/components/layout/app-layout.tsx +385 -194
- package/src/components/layout/mobile-header.tsx +26 -8
- package/src/components/memory/memory-browser.tsx +71 -6
- package/src/components/memory/memory-card.tsx +18 -0
- package/src/components/memory/memory-detail.tsx +58 -31
- package/src/components/memory/memory-sheet.tsx +32 -4
- package/src/components/plugins/plugin-list.tsx +15 -3
- package/src/components/plugins/plugin-sheet.tsx +118 -9
- package/src/components/projects/project-detail.tsx +189 -1
- package/src/components/providers/provider-list.tsx +158 -2
- package/src/components/providers/provider-sheet.tsx +81 -70
- package/src/components/shared/agent-picker-list.tsx +2 -2
- package/src/components/shared/bottom-sheet.tsx +31 -15
- package/src/components/shared/command-palette.tsx +111 -24
- package/src/components/shared/confirm-dialog.tsx +45 -30
- package/src/components/shared/model-combobox.tsx +90 -8
- 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 +88 -6
- package/src/components/shared/settings/section-orchestrator.tsx +6 -3
- 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 +248 -47
- package/src/components/tasks/approvals-panel.tsx +211 -18
- package/src/components/tasks/task-board.tsx +242 -46
- package/src/components/ui/dialog.tsx +2 -2
- package/src/components/usage/metrics-dashboard.tsx +74 -1
- package/src/components/wallets/wallet-approval-dialog.tsx +59 -54
- package/src/components/wallets/wallet-panel.tsx +17 -5
- package/src/components/webhooks/webhook-sheet.tsx +7 -7
- 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/heartbeat-defaults.ts +48 -0
- package/src/lib/memory-presentation.ts +59 -0
- package/src/lib/openclaw-agent-id.test.ts +14 -0
- package/src/lib/openclaw-agent-id.ts +31 -0
- package/src/lib/provider-model-discovery-client.ts +29 -0
- package/src/lib/providers/index.ts +12 -5
- package/src/lib/runtime-loop.ts +105 -3
- package/src/lib/safe-storage.ts +6 -1
- package/src/lib/server/agent-assignment.test.ts +112 -0
- package/src/lib/server/agent-assignment.ts +169 -0
- package/src/lib/server/agent-runtime-config.test.ts +141 -0
- package/src/lib/server/agent-runtime-config.ts +277 -0
- package/src/lib/server/approval-connector-notify.test.ts +253 -0
- package/src/lib/server/approvals-auto-approve.test.ts +264 -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 +44 -0
- package/src/lib/server/build-llm.ts +11 -4
- package/src/lib/server/builtin-plugins.ts +34 -0
- package/src/lib/server/chat-execution-heartbeat.test.ts +40 -0
- package/src/lib/server/chat-execution-tool-events.test.ts +219 -0
- package/src/lib/server/chat-execution.ts +402 -125
- 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 +74 -2
- package/src/lib/server/chatroom-helpers.ts +144 -11
- package/src/lib/server/chatroom-session-persistence.test.ts +87 -0
- 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 +994 -130
- 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 +189 -10
- 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/daemon-state.ts +62 -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/eval/agent-regression.test.ts +47 -0
- package/src/lib/server/eval/agent-regression.ts +1742 -0
- package/src/lib/server/eval/runner.ts +11 -1
- package/src/lib/server/eval/store.ts +2 -1
- package/src/lib/server/heartbeat-service.ts +23 -43
- 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 +31 -964
- 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 +6 -5
- package/src/lib/server/openclaw-gateway.ts +123 -36
- 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 +18 -8
- package/src/lib/server/orchestrator.ts +5 -4
- package/src/lib/server/playwright-proxy.mjs +27 -3
- package/src/lib/server/plugins.test.ts +215 -0
- package/src/lib/server/plugins.ts +832 -69
- package/src/lib/server/provider-health.ts +33 -3
- package/src/lib/server/provider-model-discovery.ts +481 -0
- package/src/lib/server/queue.ts +4 -21
- package/src/lib/server/runtime-settings.test.ts +119 -0
- package/src/lib/server/runtime-settings.ts +12 -92
- package/src/lib/server/schedule-normalization.ts +187 -0
- 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 -80
- package/src/lib/server/session-tools/autonomy-tools.test.ts +128 -0
- package/src/lib/server/session-tools/calendar.ts +2 -12
- package/src/lib/server/session-tools/connector.ts +109 -8
- package/src/lib/server/session-tools/context.ts +14 -2
- package/src/lib/server/session-tools/crawl.ts +447 -0
- package/src/lib/server/session-tools/crud.ts +96 -34
- package/src/lib/server/session-tools/delegate-fallback.test.ts +219 -0
- package/src/lib/server/session-tools/delegate.ts +406 -20
- package/src/lib/server/session-tools/discovery-approvals.test.ts +170 -0
- package/src/lib/server/session-tools/discovery.ts +40 -12
- package/src/lib/server/session-tools/document.ts +283 -0
- package/src/lib/server/session-tools/email.ts +1 -3
- package/src/lib/server/session-tools/extract.ts +137 -0
- package/src/lib/server/session-tools/file-normalize.test.ts +98 -0
- package/src/lib/server/session-tools/file-send.test.ts +84 -1
- package/src/lib/server/session-tools/file.ts +243 -24
- package/src/lib/server/session-tools/http.ts +9 -3
- package/src/lib/server/session-tools/human-loop.ts +227 -0
- package/src/lib/server/session-tools/image-gen.ts +1 -3
- package/src/lib/server/session-tools/index.ts +87 -2
- package/src/lib/server/session-tools/mailbox.ts +276 -0
- package/src/lib/server/session-tools/manage-schedules.test.ts +137 -0
- package/src/lib/server/session-tools/memory.ts +35 -3
- package/src/lib/server/session-tools/monitor.ts +162 -12
- package/src/lib/server/session-tools/normalize-tool-args.ts +17 -14
- package/src/lib/server/session-tools/openclaw-nodes.test.ts +111 -0
- package/src/lib/server/session-tools/openclaw-nodes.ts +86 -20
- package/src/lib/server/session-tools/platform-normalize.test.ts +142 -0
- package/src/lib/server/session-tools/platform.ts +142 -4
- package/src/lib/server/session-tools/plugin-creator.ts +95 -25
- package/src/lib/server/session-tools/primitive-tools.test.ts +257 -0
- package/src/lib/server/session-tools/replicate.ts +1 -3
- package/src/lib/server/session-tools/sandbox.ts +51 -92
- package/src/lib/server/session-tools/schedule.ts +20 -10
- package/src/lib/server/session-tools/session-info.ts +58 -4
- package/src/lib/server/session-tools/session-tools-wiring.test.ts +54 -17
- package/src/lib/server/session-tools/shell.ts +2 -2
- package/src/lib/server/session-tools/subagent.ts +195 -27
- package/src/lib/server/session-tools/table.ts +587 -0
- package/src/lib/server/session-tools/wallet.ts +13 -10
- package/src/lib/server/session-tools/web-browser-config.test.ts +39 -0
- package/src/lib/server/session-tools/web.ts +947 -108
- package/src/lib/server/storage.ts +255 -10
- package/src/lib/server/stream-agent-chat.test.ts +61 -0
- package/src/lib/server/stream-agent-chat.ts +185 -25
- 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 -11
- package/src/lib/server/tool-aliases.ts +80 -12
- package/src/lib/server/tool-capability-policy.ts +7 -1
- 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/setup-defaults.ts +352 -11
- package/src/lib/tool-definitions.ts +3 -4
- package/src/lib/validation/schemas.test.ts +26 -0
- package/src/lib/validation/schemas.ts +62 -1
- package/src/lib/ws-client.ts +14 -12
- package/src/proxy.ts +5 -5
- package/src/stores/use-app-store.ts +43 -7
- package/src/stores/use-chat-store.ts +31 -2
- package/src/stores/use-chatroom-store.ts +153 -26
- package/src/types/index.ts +470 -44
- package/src/app/api/chats/[id]/main-loop/route.ts +0 -94
- package/src/components/chat/new-chat-sheet.tsx +0 -253
- package/src/lib/server/main-session.ts +0 -17
- package/src/lib/server/session-run-manager.test.ts +0 -26
|
@@ -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')
|
|
@@ -175,7 +178,7 @@ const WalletPlugin: Plugin = {
|
|
|
175
178
|
headerWidgets: [
|
|
176
179
|
{
|
|
177
180
|
id: 'wallet-status',
|
|
178
|
-
label: '
|
|
181
|
+
label: 'Wallet'
|
|
179
182
|
}
|
|
180
183
|
]
|
|
181
184
|
},
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { describe, it } from 'node:test'
|
|
3
|
+
|
|
4
|
+
import { buildBrowserConnectionOptions, buildBrowserStdioServerParams, sanitizePlaywrightMcpEnv } from './web'
|
|
5
|
+
|
|
6
|
+
describe('browser tool connection config', () => {
|
|
7
|
+
it('does not opt into Playwright shared browser contexts', () => {
|
|
8
|
+
const config = buildBrowserConnectionOptions('/tmp/swarmclaw-browser-profile')
|
|
9
|
+
|
|
10
|
+
assert.equal(config.sharedBrowserContext, false)
|
|
11
|
+
assert.equal(config.browser.userDataDir, '/tmp/swarmclaw-browser-profile')
|
|
12
|
+
assert.deepEqual(config.browser.contextOptions.viewport, { width: 1440, height: 900 })
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
it('spawns a dedicated stdio MCP server with an isolated profile directory', () => {
|
|
16
|
+
const params = buildBrowserStdioServerParams('/tmp/swarmclaw-browser-profile')
|
|
17
|
+
|
|
18
|
+
assert.equal(params.command, process.execPath)
|
|
19
|
+
assert.equal(params.args.includes('--headless'), true)
|
|
20
|
+
assert.equal(params.args.includes('--shared-browser-context'), false)
|
|
21
|
+
assert.equal(params.args.includes('/tmp/swarmclaw-browser-profile'), true)
|
|
22
|
+
assert.equal(params.env.PLAYWRIGHT_MCP_USER_DATA_DIR, '/tmp/swarmclaw-browser-profile')
|
|
23
|
+
assert.equal(params.env.PLAYWRIGHT_MCP_OUTPUT_MODE, 'file')
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('strips host Playwright MCP env overrides before applying the local browser config', () => {
|
|
27
|
+
const env = sanitizePlaywrightMcpEnv({
|
|
28
|
+
PLAYWRIGHT_MCP_CONFIG: '/tmp/evil-config.json',
|
|
29
|
+
PLAYWRIGHT_MCP_SHARED_BROWSER_CONTEXT: '1',
|
|
30
|
+
PLAYWRIGHT_MCP_TIMEOUT_ACTION: '999999',
|
|
31
|
+
OTHER_ENV: 'keep-me',
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
assert.equal(env.PLAYWRIGHT_MCP_CONFIG, undefined)
|
|
35
|
+
assert.equal(env.PLAYWRIGHT_MCP_SHARED_BROWSER_CONTEXT, undefined)
|
|
36
|
+
assert.equal(env.PLAYWRIGHT_MCP_TIMEOUT_ACTION, undefined)
|
|
37
|
+
assert.equal(env.OTHER_ENV, 'keep-me')
|
|
38
|
+
})
|
|
39
|
+
})
|