@swarmclawai/swarmclaw 1.2.6 → 1.2.9
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 +54 -23
- package/next.config.ts +1 -0
- package/package.json +4 -3
- package/scripts/easy-setup.mjs +1 -1
- package/scripts/postinstall.mjs +1 -1
- package/skills/swarmclaw.md +115 -0
- package/skills/tools/browser.md +131 -0
- package/skills/tools/execute.md +98 -0
- package/skills/tools/files.md +98 -0
- package/skills/tools/memory.md +104 -0
- package/skills/tools/platform.md +144 -0
- package/skills/tools/skills.md +83 -0
- package/src/app/agents/[id]/page.tsx +1 -18
- package/src/app/api/agents/thread-route.test.ts +0 -1
- package/src/app/api/approvals/route.test.ts +6 -22
- package/src/app/api/chats/[id]/messages/route.ts +23 -19
- package/src/app/api/chats/messages-route.test.ts +105 -51
- package/src/app/api/connectors/route.ts +2 -2
- package/src/app/api/mcp-servers/[id]/test/route.ts +3 -2
- package/src/app/api/openclaw/deploy/route.ts +2 -0
- package/src/app/api/portability/export/route.ts +8 -0
- package/src/app/api/portability/import/route.test.ts +80 -0
- package/src/app/api/portability/import/route.ts +28 -0
- package/src/app/api/settings/route.ts +0 -2
- package/src/app/api/setup/doctor/route.ts +4 -4
- package/src/app/api/wallets/[id]/route.ts +15 -157
- package/src/app/api/wallets/generate/route.ts +22 -0
- package/src/app/api/wallets/route.test.ts +147 -0
- package/src/app/api/wallets/route.ts +13 -95
- package/src/app/autonomy/page.tsx +2 -57
- package/src/app/protocols/page.tsx +2 -21
- package/src/app/settings/page.tsx +0 -9
- package/src/app/wallets/page.tsx +105 -5
- package/src/cli/index.js +21 -33
- package/src/cli/spec.js +19 -30
- package/src/components/agents/agent-chat-list.tsx +23 -1
- package/src/components/agents/agent-sheet.tsx +2 -40
- package/src/components/agents/inspector-panel.tsx +165 -131
- package/src/components/chat/chat-area.tsx +38 -9
- package/src/components/chat/chat-card.tsx +0 -31
- package/src/components/chat/message-bubble.tsx +1 -108
- package/src/components/chat/message-list.tsx +33 -19
- package/src/components/connectors/connector-sheet.tsx +25 -1
- package/src/components/gateways/gateway-sheet.tsx +5 -2
- package/src/components/layout/sidebar-rail.tsx +6 -10
- package/src/components/projects/project-detail.tsx +3 -35
- package/src/components/projects/tabs/overview-tab.tsx +3 -59
- package/src/components/projects/tabs/work-tab.tsx +7 -77
- package/src/components/protocols/structured-session-launcher.tsx +1 -22
- package/src/components/shared/connector-platform-icon.tsx +1 -0
- package/src/components/tasks/task-card.tsx +4 -34
- package/src/components/tasks/task-sheet.tsx +6 -36
- package/src/components/wallets/wallet-list.tsx +150 -0
- package/src/lib/agent-execute-defaults.test.ts +24 -0
- package/src/lib/agent-execute-defaults.ts +62 -0
- package/src/lib/app/navigation.test.ts +0 -13
- package/src/lib/app/navigation.ts +2 -7
- package/src/lib/app/view-constants.ts +14 -19
- package/src/lib/chat/queued-message-queue.test.ts +134 -1
- package/src/lib/chat/queued-message-queue.ts +77 -2
- package/src/lib/server/agents/agent-service.ts +5 -0
- package/src/lib/server/agents/agent-thread-session.ts +0 -1
- package/src/lib/server/agents/delegation-advisory.test.ts +0 -1
- package/src/lib/server/agents/delegation-jobs.test.ts +0 -69
- package/src/lib/server/agents/delegation-jobs.ts +0 -25
- package/src/lib/server/agents/main-agent-loop.ts +1 -49
- package/src/lib/server/agents/subagent-runtime.ts +0 -1
- package/src/lib/server/approval-match.ts +0 -85
- package/src/lib/server/approvals.test.ts +6 -6
- package/src/lib/server/approvals.ts +0 -6
- package/src/lib/server/autonomy/supervisor-reflection.test.ts +0 -1
- package/src/lib/server/builtin-extensions.ts +1 -2
- package/src/lib/server/capability-router.test.ts +0 -2
- package/src/lib/server/chat-execution/chat-execution-advanced.test.ts +1 -1
- package/src/lib/server/chat-execution/chat-execution-tool-events.test.ts +15 -14
- package/src/lib/server/chat-execution/chat-execution-types.ts +0 -2
- package/src/lib/server/chat-execution/chat-execution-utils.ts +2 -4
- package/src/lib/server/chat-execution/chat-streaming-utils.ts +2 -30
- package/src/lib/server/chat-execution/chat-turn-finalization.ts +1 -36
- package/src/lib/server/chat-execution/chat-turn-preparation.ts +81 -64
- package/src/lib/server/chat-execution/chat-turn-stream-execution.ts +4 -0
- package/src/lib/server/chat-execution/continuation-evaluator.ts +8 -0
- package/src/lib/server/chat-execution/iteration-event-handler.ts +0 -24
- package/src/lib/server/chat-execution/memory-mutation-tools.ts +1 -1
- package/src/lib/server/chat-execution/message-classifier.test.ts +0 -45
- package/src/lib/server/chat-execution/message-classifier.ts +11 -16
- package/src/lib/server/chat-execution/prompt-builder.test.ts +27 -0
- package/src/lib/server/chat-execution/prompt-builder.ts +14 -31
- package/src/lib/server/chat-execution/prompt-mode.test.ts +24 -0
- package/src/lib/server/chat-execution/prompt-mode.ts +5 -1
- package/src/lib/server/chat-execution/prompt-sections.ts +0 -1
- package/src/lib/server/chat-execution/situational-awareness.test.ts +2 -73
- package/src/lib/server/chat-execution/situational-awareness.ts +4 -38
- package/src/lib/server/chat-execution/stream-agent-chat.test.ts +13 -126
- package/src/lib/server/chat-execution/stream-agent-chat.ts +46 -21
- package/src/lib/server/chat-execution/stream-continuation.test.ts +4 -52
- package/src/lib/server/chat-execution/stream-continuation.ts +6 -48
- package/src/lib/server/chatrooms/chatroom-routing.test.ts +4 -0
- package/src/lib/server/chatrooms/session-mailbox.ts +0 -10
- package/src/lib/server/chats/chat-session-service.ts +3 -5
- package/src/lib/server/connectors/connector-inbound.ts +0 -1
- package/src/lib/server/connectors/connector-lifecycle.ts +19 -3
- package/src/lib/server/connectors/connector-service.ts +39 -9
- package/src/lib/server/connectors/discord.ts +2 -2
- package/src/lib/server/connectors/matrix.ts +3 -2
- package/src/lib/server/connectors/signal.ts +5 -4
- package/src/lib/server/connectors/slack.ts +10 -9
- package/src/lib/server/connectors/swarmdock-bidding.ts +74 -0
- package/src/lib/server/connectors/swarmdock-payloads.test.ts +85 -0
- package/src/lib/server/connectors/swarmdock-secret.test.ts +128 -0
- package/src/lib/server/connectors/swarmdock-secret.ts +152 -0
- package/src/lib/server/connectors/swarmdock-tasks.ts +119 -0
- package/src/lib/server/connectors/swarmdock.ts +255 -0
- package/src/lib/server/connectors/teams.ts +3 -2
- package/src/lib/server/connectors/telegram.ts +4 -4
- package/src/lib/server/connectors/whatsapp.ts +2 -2
- package/src/lib/server/daemon/controller.ts +7 -0
- package/src/lib/server/execution-brief.test.ts +2 -25
- package/src/lib/server/execution-brief.ts +12 -35
- package/src/lib/server/execution-engine/task-attempt.ts +0 -1
- package/src/lib/server/gateways/gateway-profile-service.ts +19 -1
- package/src/lib/server/messages/message-repository.test.ts +70 -0
- package/src/lib/server/messages/message-repository.ts +11 -6
- package/src/lib/server/openclaw/deploy.ts +32 -2
- package/src/lib/server/persistence/storage-context.ts +0 -5
- package/src/lib/server/plugins-advanced.test.ts +1 -2
- package/src/lib/server/portability/export.ts +109 -0
- package/src/lib/server/portability/import.ts +159 -0
- package/src/lib/server/protocols/protocol-normalization.ts +0 -4
- package/src/lib/server/protocols/protocol-queries.ts +0 -6
- package/src/lib/server/protocols/protocol-run-lifecycle.ts +4 -32
- package/src/lib/server/protocols/protocol-service.ts +0 -1
- package/src/lib/server/protocols/protocol-step-helpers.ts +0 -4
- package/src/lib/server/protocols/protocol-step-processors.ts +0 -6
- package/src/lib/server/protocols/protocol-swarm.ts +0 -2
- package/src/lib/server/protocols/protocol-types.ts +0 -2
- package/src/lib/server/provider-health.ts +1 -10
- package/src/lib/server/runtime/daemon-state/core.ts +0 -9
- package/src/lib/server/runtime/daemon-state.test.ts +0 -35
- package/src/lib/server/runtime/heartbeat-service.ts +3 -23
- package/src/lib/server/runtime/process-manager.ts +13 -9
- package/src/lib/server/runtime/queue/core.ts +11 -33
- package/src/lib/server/runtime/runtime-storage-write-paths.test.ts +6 -6
- package/src/lib/server/runtime/scheduler.ts +0 -13
- package/src/lib/server/runtime/session-run-manager/drain.ts +0 -24
- package/src/lib/server/runtime/session-run-manager/enqueue.ts +0 -1
- package/src/lib/server/runtime/session-run-manager/queries.ts +15 -1
- package/src/lib/server/runtime/session-run-manager/recovery.ts +0 -1
- package/src/lib/server/runtime/session-run-manager.test.ts +58 -28
- package/src/lib/server/sandbox/session-runtime.test.ts +18 -1
- package/src/lib/server/sandbox/session-runtime.ts +40 -28
- package/src/lib/server/session-tools/autonomy-tools.test.ts +7 -9
- package/src/lib/server/session-tools/context.ts +1 -1
- package/src/lib/server/session-tools/credential-env.ts +109 -0
- package/src/lib/server/session-tools/crud.ts +3 -17
- package/src/lib/server/session-tools/delegate.ts +0 -4
- package/src/lib/server/session-tools/edit_file.ts +3 -2
- package/src/lib/server/session-tools/execute.test.ts +58 -0
- package/src/lib/server/session-tools/execute.ts +334 -0
- package/src/lib/server/session-tools/files-tool.ts +635 -0
- package/src/lib/server/session-tools/index.ts +14 -8
- package/src/lib/server/session-tools/memory-tool.ts +242 -0
- package/src/lib/server/session-tools/memory.ts +1 -1
- package/src/lib/server/session-tools/openclaw-nodes.ts +3 -2
- package/src/lib/server/session-tools/openclaw-workspace.ts +3 -2
- package/src/lib/server/session-tools/platform-tool.ts +617 -0
- package/src/lib/server/session-tools/session-info.ts +3 -2
- package/src/lib/server/session-tools/session-tools-wiring.test.ts +3 -4
- package/src/lib/server/session-tools/shell.ts +7 -122
- package/src/lib/server/session-tools/skills-tool.ts +396 -0
- package/src/lib/server/session-tools/team-context.ts +0 -3
- package/src/lib/server/session-tools/web.ts +2 -2
- package/src/lib/server/storage-normalization.ts +10 -0
- package/src/lib/server/storage.ts +18 -45
- package/src/lib/server/tasks/task-checkout.ts +59 -0
- package/src/lib/server/tasks/task-lifecycle.ts +2 -0
- package/src/lib/server/tasks/task-route-service.ts +4 -26
- package/src/lib/server/tasks/task-service.ts +0 -7
- package/src/lib/server/tool-aliases.ts +2 -2
- package/src/lib/server/tool-capability-policy-advanced.test.ts +13 -6
- package/src/lib/server/tool-capability-policy.test.ts +2 -1
- package/src/lib/server/tool-capability-policy.ts +60 -35
- package/src/lib/server/tool-planning.ts +11 -12
- package/src/lib/server/universal-tool-access.ts +0 -1
- package/src/lib/server/wallets/wallet-crypto.ts +33 -0
- package/src/lib/server/wallets/wallet-repository.ts +24 -0
- package/src/lib/server/wallets/wallet-service.ts +119 -0
- package/src/lib/server/working-state/extraction.ts +8 -42
- package/src/lib/server/working-state/normalization.ts +10 -103
- package/src/lib/server/working-state/service.ts +12 -21
- package/src/lib/setup-defaults.ts +5 -0
- package/src/lib/strip-internal-metadata.test.ts +1 -1
- package/src/lib/strip-internal-metadata.ts +1 -1
- package/src/lib/tool-definitions.ts +1 -1
- package/src/lib/validation/schemas.test.ts +16 -0
- package/src/lib/validation/schemas.ts +49 -2
- package/src/stores/slices/data-slice.ts +5 -1
- package/src/stores/slices/ui-slice.ts +0 -4
- package/src/stores/use-chat-store.test.ts +231 -0
- package/src/stores/use-chat-store.ts +62 -13
- package/src/types/agent.ts +264 -0
- package/src/types/app-settings.ts +173 -0
- package/src/types/approval.ts +25 -0
- package/src/types/connector.ts +188 -0
- package/src/types/extension.ts +386 -0
- package/src/types/index.ts +16 -3555
- package/src/types/message.ts +56 -0
- package/src/types/misc.ts +737 -0
- package/src/types/protocol.ts +420 -0
- package/src/types/provider.ts +52 -0
- package/src/types/run.ts +180 -0
- package/src/types/schedule.ts +59 -0
- package/src/types/session.ts +215 -0
- package/src/types/skill.ts +157 -0
- package/src/types/swarmdock.ts +29 -0
- package/src/types/task.ts +144 -0
- package/src/types/working-state.ts +204 -0
- package/src/views/settings/section-heartbeat.tsx +2 -2
- package/src/views/settings/section-runtime-loop.tsx +0 -14
- package/src/app/api/canvas/[sessionId]/route.ts +0 -35
- package/src/app/api/missions/[id]/actions/route.ts +0 -31
- package/src/app/api/missions/[id]/events/route.ts +0 -14
- package/src/app/api/missions/[id]/route.ts +0 -10
- package/src/app/api/missions/route.test.ts +0 -244
- package/src/app/api/missions/route.ts +0 -57
- package/src/app/api/wallets/[id]/approve/route.ts +0 -79
- package/src/app/api/wallets/[id]/balance-history/route.ts +0 -18
- package/src/app/api/wallets/[id]/send/route.ts +0 -113
- package/src/app/api/wallets/[id]/transactions/route.ts +0 -18
- package/src/app/missions/[id]/page.tsx +0 -3
- package/src/app/missions/page.tsx +0 -685
- package/src/components/canvas/canvas-panel.tsx +0 -267
- package/src/components/wallets/wallet-approval-dialog.tsx +0 -107
- package/src/components/wallets/wallet-panel.tsx +0 -1010
- package/src/components/wallets/wallet-section.tsx +0 -260
- package/src/features/missions/queries.ts +0 -23
- package/src/lib/canvas-content.test.ts +0 -360
- package/src/lib/canvas-content.ts +0 -198
- package/src/lib/server/canvas-content.test.ts +0 -32
- package/src/lib/server/canvas-content.ts +0 -6
- package/src/lib/server/ethereum.ts +0 -591
- package/src/lib/server/evm-swap.ts +0 -476
- package/src/lib/server/missions/mission-intent.test.ts +0 -63
- package/src/lib/server/missions/mission-intent.ts +0 -569
- package/src/lib/server/missions/mission-repository.ts +0 -74
- package/src/lib/server/missions/mission-service/actions.ts +0 -6
- package/src/lib/server/missions/mission-service/bindings.ts +0 -9
- package/src/lib/server/missions/mission-service/context.ts +0 -4
- package/src/lib/server/missions/mission-service/core.ts +0 -2271
- package/src/lib/server/missions/mission-service/queries.ts +0 -12
- package/src/lib/server/missions/mission-service/recovery.ts +0 -5
- package/src/lib/server/missions/mission-service/ticks.ts +0 -9
- package/src/lib/server/missions/mission-service.test.ts +0 -888
- package/src/lib/server/missions/mission-service.ts +0 -6
- package/src/lib/server/session-tools/canvas.ts +0 -105
- package/src/lib/server/session-tools/sandbox.ts +0 -281
- package/src/lib/server/session-tools/wallet-tool.test.ts +0 -150
- package/src/lib/server/session-tools/wallet.ts +0 -1287
- package/src/lib/server/solana.ts +0 -327
- package/src/lib/server/wallet/wallet-execution.test.ts +0 -198
- package/src/lib/server/wallet/wallet-portfolio.test.ts +0 -98
- package/src/lib/server/wallet/wallet-portfolio.ts +0 -772
- package/src/lib/server/wallet/wallet-service.test.ts +0 -81
- package/src/lib/server/wallet/wallet-service.ts +0 -225
- package/src/lib/wallet/wallet-transactions.test.ts +0 -75
- package/src/lib/wallet/wallet-transactions.ts +0 -43
- package/src/lib/wallet/wallet.test.ts +0 -333
- package/src/lib/wallet/wallet.ts +0 -183
- package/src/views/settings/section-wallets.tsx +0 -35
|
@@ -0,0 +1,635 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* files-tool — Consolidated file operations tool.
|
|
3
|
+
*
|
|
4
|
+
* Merges the capabilities of file.ts (read/write/list/copy/move/delete)
|
|
5
|
+
* and edit_file.ts (surgical string replacement) into a single tool
|
|
6
|
+
* with an `action` discriminator, plus a new `search` action.
|
|
7
|
+
*
|
|
8
|
+
* Actions:
|
|
9
|
+
* read — Read file contents (optional offset/limit for line ranges)
|
|
10
|
+
* write — Write/overwrite a file (supports bulk via files[])
|
|
11
|
+
* edit — Surgical old_string -> new_string replacement
|
|
12
|
+
* list — List directory contents (with depth control)
|
|
13
|
+
* search — Search file contents (grep-like, with include glob filter)
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { z } from 'zod'
|
|
17
|
+
import { tool } from '@langchain/core/tools'
|
|
18
|
+
import fs from 'fs'
|
|
19
|
+
import path from 'path'
|
|
20
|
+
import type { Extension, ExtensionHooks } from '@/types'
|
|
21
|
+
import { registerNativeCapability } from '../native-capabilities'
|
|
22
|
+
import { normalizeToolInputArgs } from './normalize-tool-args'
|
|
23
|
+
import { checkFileAccess } from './file-access-policy'
|
|
24
|
+
import { errorMessage } from '@/lib/shared-utils'
|
|
25
|
+
import { log } from '../logger'
|
|
26
|
+
import type { ToolBuildContext } from './context'
|
|
27
|
+
import { safePath, truncate, listDirRecursive, MAX_FILE, MAX_OUTPUT } from './context'
|
|
28
|
+
|
|
29
|
+
const TAG = 'files-tool'
|
|
30
|
+
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
// Types
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
type FilesAction = 'read' | 'write' | 'edit' | 'list' | 'search'
|
|
36
|
+
|
|
37
|
+
interface FilesToolContext {
|
|
38
|
+
cwd: string
|
|
39
|
+
filesystemScope?: 'workspace' | 'machine'
|
|
40
|
+
fileAccessPolicy?: { allowedPaths?: string[]; blockedPaths?: string[] } | null
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface NormalizedFilesArgs {
|
|
44
|
+
action: FilesAction | undefined
|
|
45
|
+
path: string | undefined
|
|
46
|
+
content: string | undefined
|
|
47
|
+
encoding: string | undefined
|
|
48
|
+
// read
|
|
49
|
+
offset: number | undefined
|
|
50
|
+
limit: number | undefined
|
|
51
|
+
// edit
|
|
52
|
+
oldString: string | undefined
|
|
53
|
+
newString: string | undefined
|
|
54
|
+
// list
|
|
55
|
+
depth: number | undefined
|
|
56
|
+
// search
|
|
57
|
+
query: string | undefined
|
|
58
|
+
include: string | undefined
|
|
59
|
+
// write bulk
|
|
60
|
+
files: Array<Record<string, unknown>> | undefined
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
// Arg normalization helpers
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
|
|
67
|
+
function pickString(...values: unknown[]): string | undefined {
|
|
68
|
+
for (const v of values) {
|
|
69
|
+
if (typeof v === 'string') {
|
|
70
|
+
const trimmed = v.trim()
|
|
71
|
+
if (trimmed) return trimmed
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return undefined
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function pickNumber(...values: unknown[]): number | undefined {
|
|
78
|
+
for (const v of values) {
|
|
79
|
+
if (typeof v === 'number' && Number.isFinite(v)) return v
|
|
80
|
+
if (typeof v === 'string') {
|
|
81
|
+
const n = Number(v)
|
|
82
|
+
if (Number.isFinite(n)) return n
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return undefined
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function pickStringRaw(...values: unknown[]): string | undefined {
|
|
89
|
+
for (const v of values) {
|
|
90
|
+
if (typeof v === 'string') return v
|
|
91
|
+
}
|
|
92
|
+
return undefined
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function parseFileEntries(value: unknown): Array<Record<string, unknown>> | undefined {
|
|
96
|
+
const candidates = [value]
|
|
97
|
+
if (typeof value === 'string') {
|
|
98
|
+
const trimmed = value.trim()
|
|
99
|
+
if (trimmed.startsWith('[')) {
|
|
100
|
+
try {
|
|
101
|
+
candidates.unshift(JSON.parse(trimmed))
|
|
102
|
+
} catch {
|
|
103
|
+
// ignore malformed JSON
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
for (const candidate of candidates) {
|
|
108
|
+
if (!Array.isArray(candidate)) continue
|
|
109
|
+
return candidate.filter(
|
|
110
|
+
(entry): entry is Record<string, unknown> =>
|
|
111
|
+
!!entry && typeof entry === 'object' && !Array.isArray(entry),
|
|
112
|
+
)
|
|
113
|
+
}
|
|
114
|
+
return undefined
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function getEntryPath(entry: Record<string, unknown> | undefined): string | undefined {
|
|
118
|
+
if (!entry) return undefined
|
|
119
|
+
return pickString(
|
|
120
|
+
entry.path,
|
|
121
|
+
entry.filePath,
|
|
122
|
+
entry.filename,
|
|
123
|
+
entry.fileName,
|
|
124
|
+
entry.name,
|
|
125
|
+
entry.targetPath,
|
|
126
|
+
entry.target,
|
|
127
|
+
)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function getEntryContent(entry: Record<string, unknown> | undefined): string | undefined {
|
|
131
|
+
if (!entry) return undefined
|
|
132
|
+
const raw = entry.content ?? entry.text ?? entry.contents ?? entry.value ?? entry.body
|
|
133
|
+
if (raw === undefined || raw === null) return undefined
|
|
134
|
+
return typeof raw === 'string' ? raw : JSON.stringify(raw)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Infer the action when the LLM doesn't provide one explicitly.
|
|
139
|
+
*/
|
|
140
|
+
function inferAction(
|
|
141
|
+
normalized: Record<string, unknown>,
|
|
142
|
+
files: Array<Record<string, unknown>> | undefined,
|
|
143
|
+
filePath: string | undefined,
|
|
144
|
+
): FilesAction {
|
|
145
|
+
// If old_string/oldString is present, it's an edit
|
|
146
|
+
if (normalized.oldString !== undefined || normalized.old_string !== undefined) return 'edit'
|
|
147
|
+
// If query/search/pattern is present, it's a search
|
|
148
|
+
if (normalized.query !== undefined || normalized.search !== undefined || normalized.pattern !== undefined) return 'search'
|
|
149
|
+
// If files array has content, it's a write
|
|
150
|
+
if (Array.isArray(files) && files.some((e) => getEntryContent(e) !== undefined)) return 'write'
|
|
151
|
+
// If content is present, it's a write
|
|
152
|
+
if (normalized.content !== undefined || normalized.text !== undefined || normalized.body !== undefined) return 'write'
|
|
153
|
+
// If depth is present or path looks like a directory, it's a list
|
|
154
|
+
if (normalized.depth !== undefined) return 'list'
|
|
155
|
+
if (normalized.dirPath !== undefined || normalized.directory !== undefined || normalized.dir !== undefined) return 'list'
|
|
156
|
+
if (filePath && filePath.endsWith('/')) return 'list'
|
|
157
|
+
// Default: if we have a path, read it; otherwise list cwd
|
|
158
|
+
return filePath ? 'read' : 'list'
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Normalize the chaotic LLM arg shapes into a clean internal structure.
|
|
163
|
+
*/
|
|
164
|
+
function normalizeArgs(rawArgs: Record<string, unknown>): NormalizedFilesArgs {
|
|
165
|
+
const n = normalizeToolInputArgs(rawArgs)
|
|
166
|
+
|
|
167
|
+
// Some LLMs nest the payload under the action key: { read: { path: "..." } }
|
|
168
|
+
const actionPayload = (['read', 'write', 'edit', 'list', 'search'] as const)
|
|
169
|
+
.map((candidate) => {
|
|
170
|
+
const value = n[candidate]
|
|
171
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
172
|
+
? { action: candidate, value: value as Record<string, unknown> }
|
|
173
|
+
: null
|
|
174
|
+
})
|
|
175
|
+
.find(Boolean)
|
|
176
|
+
|
|
177
|
+
const merged: Record<string, unknown> = { ...n, ...(actionPayload?.value ?? {}) }
|
|
178
|
+
const files = parseFileEntries(merged.files)
|
|
179
|
+
|
|
180
|
+
const filePath = pickString(
|
|
181
|
+
merged.filePath, merged.filepath, merged.path, merged.file,
|
|
182
|
+
merged.filename, merged.fileName, merged.name, merged.targetPath,
|
|
183
|
+
merged.target, merged.dirPath, merged.directory, merged.directoryPath,
|
|
184
|
+
merged.dir, merged.folder,
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
let action = pickString(n.action, actionPayload?.action) as FilesAction | undefined
|
|
188
|
+
if (!action && Array.isArray(files) && files.length > 0) {
|
|
189
|
+
action = pickString(files[0].action) as FilesAction | undefined
|
|
190
|
+
}
|
|
191
|
+
if (!action) {
|
|
192
|
+
action = inferAction(merged, files, filePath)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
action,
|
|
197
|
+
path: filePath,
|
|
198
|
+
content: pickStringRaw(merged.content, merged.text, merged.contents, merged.value, merged.body),
|
|
199
|
+
encoding: pickString(merged.encoding),
|
|
200
|
+
offset: pickNumber(merged.offset, merged.startLine, merged.start_line, merged.from_line),
|
|
201
|
+
limit: pickNumber(merged.limit, merged.lineCount, merged.line_count, merged.maxLines, merged.max_lines, merged.lines),
|
|
202
|
+
oldString: pickStringRaw(merged.oldString, merged.old_string, merged.oldText, merged.old_text, merged.find, merged.search_string),
|
|
203
|
+
newString: pickStringRaw(merged.newString, merged.new_string, merged.newText, merged.new_text, merged.replace, merged.replacement),
|
|
204
|
+
depth: pickNumber(merged.depth, merged.maxDepth, merged.max_depth),
|
|
205
|
+
query: pickString(merged.query, merged.search, merged.pattern, merged.grep, merged.regex),
|
|
206
|
+
include: pickString(merged.include, merged.glob, merged.filePattern, merged.file_pattern),
|
|
207
|
+
files,
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// ---------------------------------------------------------------------------
|
|
212
|
+
// Path resolution + access policy enforcement
|
|
213
|
+
// ---------------------------------------------------------------------------
|
|
214
|
+
|
|
215
|
+
function resolveFilePath(cwd: string, target: string, scope?: 'workspace' | 'machine'): string {
|
|
216
|
+
try {
|
|
217
|
+
return safePath(cwd, target, scope)
|
|
218
|
+
} catch (err: unknown) {
|
|
219
|
+
// For absolute paths, try resolving against process.cwd() as a fallback
|
|
220
|
+
if (!path.isAbsolute(target)) throw err
|
|
221
|
+
return safePath(process.cwd(), target, scope)
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function enforceAccess(
|
|
226
|
+
filePath: string,
|
|
227
|
+
cwd: string,
|
|
228
|
+
policy: FilesToolContext['fileAccessPolicy'],
|
|
229
|
+
): string | null {
|
|
230
|
+
if (!policy) return null
|
|
231
|
+
const result = checkFileAccess(filePath, cwd, policy)
|
|
232
|
+
if (!result.allowed) return result.reason ?? 'File access denied by policy'
|
|
233
|
+
return null
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// ---------------------------------------------------------------------------
|
|
237
|
+
// Binary file detection
|
|
238
|
+
// ---------------------------------------------------------------------------
|
|
239
|
+
|
|
240
|
+
const BINARY_EXTENSIONS = new Set([
|
|
241
|
+
'.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp', '.ico', '.svg', '.pdf',
|
|
242
|
+
'.zip', '.gz', '.tar', '.tgz', '.7z', '.rar',
|
|
243
|
+
'.mp3', '.wav', '.ogg', '.m4a', '.mp4', '.mov', '.avi', '.webm',
|
|
244
|
+
'.woff', '.woff2', '.ttf', '.otf',
|
|
245
|
+
'.exe', '.dll', '.so', '.dylib', '.bin',
|
|
246
|
+
])
|
|
247
|
+
|
|
248
|
+
function isLikelyBinary(resolvedPath: string, data: Buffer): boolean {
|
|
249
|
+
const ext = path.extname(resolvedPath).toLowerCase()
|
|
250
|
+
if (BINARY_EXTENSIONS.has(ext)) return true
|
|
251
|
+
const sample = data.subarray(0, Math.min(data.length, 512))
|
|
252
|
+
for (const byte of sample) {
|
|
253
|
+
if (byte === 0) return true
|
|
254
|
+
}
|
|
255
|
+
return false
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// ---------------------------------------------------------------------------
|
|
259
|
+
// Action implementations
|
|
260
|
+
// ---------------------------------------------------------------------------
|
|
261
|
+
|
|
262
|
+
function actionRead(args: NormalizedFilesArgs, ctx: FilesToolContext): string {
|
|
263
|
+
const target = args.path
|
|
264
|
+
if (!target) return 'Error: path is required for read action.'
|
|
265
|
+
|
|
266
|
+
const blocked = enforceAccess(target, ctx.cwd, ctx.fileAccessPolicy)
|
|
267
|
+
if (blocked) return `Error: ${blocked}`
|
|
268
|
+
|
|
269
|
+
const resolved = resolveFilePath(ctx.cwd, target, ctx.filesystemScope)
|
|
270
|
+
const data = fs.readFileSync(resolved)
|
|
271
|
+
|
|
272
|
+
if (isLikelyBinary(resolved, data)) {
|
|
273
|
+
return `Binary file: ${target} (${data.byteLength} bytes). Contents not displayed.`
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
let text = data.toString('utf-8')
|
|
277
|
+
|
|
278
|
+
// Apply line-range slicing if offset/limit provided
|
|
279
|
+
if (args.offset !== undefined || args.limit !== undefined) {
|
|
280
|
+
const lines = text.split('\n')
|
|
281
|
+
const start = Math.max(0, (args.offset ?? 1) - 1) // 1-based to 0-based
|
|
282
|
+
const count = args.limit ?? lines.length
|
|
283
|
+
const sliced = lines.slice(start, start + count)
|
|
284
|
+
// Prefix with line numbers for context
|
|
285
|
+
text = sliced
|
|
286
|
+
.map((line, i) => `${start + i + 1}: ${line}`)
|
|
287
|
+
.join('\n')
|
|
288
|
+
if (start + count < lines.length) {
|
|
289
|
+
text += `\n... (${lines.length - start - count} more lines)`
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return truncate(text, MAX_FILE)
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function actionWrite(args: NormalizedFilesArgs, ctx: FilesToolContext): string {
|
|
297
|
+
const filesToWrite: Array<Record<string, unknown>> = Array.isArray(args.files)
|
|
298
|
+
? args.files
|
|
299
|
+
: [{ path: args.path, content: args.content }]
|
|
300
|
+
|
|
301
|
+
const results: string[] = []
|
|
302
|
+
|
|
303
|
+
for (const file of filesToWrite) {
|
|
304
|
+
const targetPath = getEntryPath(file)
|
|
305
|
+
if (!targetPath) continue
|
|
306
|
+
|
|
307
|
+
const blocked = enforceAccess(targetPath, ctx.cwd, ctx.fileAccessPolicy)
|
|
308
|
+
if (blocked) {
|
|
309
|
+
results.push(`Error (${targetPath}): ${blocked}`)
|
|
310
|
+
continue
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const fileContent = getEntryContent(file) ?? ''
|
|
314
|
+
|
|
315
|
+
// Directory creation: paths ending with / or \
|
|
316
|
+
if (/[\\/]$/.test(targetPath)) {
|
|
317
|
+
const resolvedDir = resolveFilePath(ctx.cwd, targetPath, ctx.filesystemScope)
|
|
318
|
+
fs.mkdirSync(resolvedDir, { recursive: true })
|
|
319
|
+
results.push(`Created directory ${targetPath}`)
|
|
320
|
+
continue
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const resolved = resolveFilePath(ctx.cwd, targetPath, ctx.filesystemScope)
|
|
324
|
+
fs.mkdirSync(path.dirname(resolved), { recursive: true })
|
|
325
|
+
|
|
326
|
+
if (args.encoding === 'base64' && typeof fileContent === 'string') {
|
|
327
|
+
const buf = Buffer.from(fileContent, 'base64')
|
|
328
|
+
fs.writeFileSync(resolved, buf)
|
|
329
|
+
results.push(`Written ${targetPath} (${buf.length} bytes, binary)`)
|
|
330
|
+
} else {
|
|
331
|
+
fs.writeFileSync(resolved, fileContent, 'utf-8')
|
|
332
|
+
results.push(`Written ${targetPath} (${fileContent.length} bytes)`)
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
return results.join('\n') || 'Error: no files to write.'
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function actionEdit(args: NormalizedFilesArgs, ctx: FilesToolContext): string {
|
|
340
|
+
const target = args.path
|
|
341
|
+
if (!target) return 'Error: path is required for edit action.'
|
|
342
|
+
|
|
343
|
+
const blocked = enforceAccess(target, ctx.cwd, ctx.fileAccessPolicy)
|
|
344
|
+
if (blocked) return `Error: ${blocked}`
|
|
345
|
+
|
|
346
|
+
if (args.oldString === undefined) return 'Error: old_string is required for edit action.'
|
|
347
|
+
if (args.newString === undefined) return 'Error: new_string is required for edit action.'
|
|
348
|
+
|
|
349
|
+
const resolved = resolveFilePath(ctx.cwd, target, ctx.filesystemScope)
|
|
350
|
+
if (!fs.existsSync(resolved)) return `Error: File not found: ${target}`
|
|
351
|
+
|
|
352
|
+
const content = fs.readFileSync(resolved, 'utf-8')
|
|
353
|
+
const count = content.split(args.oldString).length - 1
|
|
354
|
+
|
|
355
|
+
if (count === 0) {
|
|
356
|
+
return `Error: Exact match for old_string not found in ${target}. Use action="read" to check current content.`
|
|
357
|
+
}
|
|
358
|
+
if (count > 1) {
|
|
359
|
+
return `Error: Multiple matches (${count}) found for old_string. Provide more surrounding context for a unique match.`
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const updated = content.replace(args.oldString, args.newString)
|
|
363
|
+
fs.writeFileSync(resolved, updated, 'utf-8')
|
|
364
|
+
return `Successfully updated ${target} (1 replacement made).`
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function actionList(args: NormalizedFilesArgs, ctx: FilesToolContext): string {
|
|
368
|
+
const target = args.path || '.'
|
|
369
|
+
const maxDepth = Math.min(Math.max(args.depth ?? 3, 1), 10)
|
|
370
|
+
|
|
371
|
+
const blocked = enforceAccess(target, ctx.cwd, ctx.fileAccessPolicy)
|
|
372
|
+
if (blocked) return `Error: ${blocked}`
|
|
373
|
+
|
|
374
|
+
const resolved = resolveFilePath(ctx.cwd, target, ctx.filesystemScope)
|
|
375
|
+
const tree = listDirRecursive(resolved, 0, maxDepth)
|
|
376
|
+
return tree.length ? tree.join('\n') : '(empty directory)'
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function actionSearch(args: NormalizedFilesArgs, ctx: FilesToolContext): string {
|
|
380
|
+
const query = args.query
|
|
381
|
+
if (!query) return 'Error: query is required for search action.'
|
|
382
|
+
|
|
383
|
+
const target = args.path || '.'
|
|
384
|
+
const blocked = enforceAccess(target, ctx.cwd, ctx.fileAccessPolicy)
|
|
385
|
+
if (blocked) return `Error: ${blocked}`
|
|
386
|
+
|
|
387
|
+
const resolved = resolveFilePath(ctx.cwd, target, ctx.filesystemScope)
|
|
388
|
+
|
|
389
|
+
let regex: RegExp
|
|
390
|
+
try {
|
|
391
|
+
regex = new RegExp(query, 'i')
|
|
392
|
+
} catch {
|
|
393
|
+
// Fall back to literal search if the query is not a valid regex
|
|
394
|
+
regex = new RegExp(query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'i')
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const includeGlob = args.include
|
|
398
|
+
const results: string[] = []
|
|
399
|
+
const maxResults = 200
|
|
400
|
+
|
|
401
|
+
searchDir(resolved, resolved, regex, includeGlob, results, maxResults, 0, 10)
|
|
402
|
+
|
|
403
|
+
if (results.length === 0) return `No matches found for "${query}" in ${target}`
|
|
404
|
+
const output = results.join('\n')
|
|
405
|
+
return truncate(output, MAX_OUTPUT)
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Recursively search directory for files matching the query.
|
|
410
|
+
*/
|
|
411
|
+
function searchDir(
|
|
412
|
+
root: string,
|
|
413
|
+
dir: string,
|
|
414
|
+
regex: RegExp,
|
|
415
|
+
includeGlob: string | undefined,
|
|
416
|
+
results: string[],
|
|
417
|
+
maxResults: number,
|
|
418
|
+
depth: number,
|
|
419
|
+
maxDepth: number,
|
|
420
|
+
): void {
|
|
421
|
+
if (depth > maxDepth || results.length >= maxResults) return
|
|
422
|
+
|
|
423
|
+
let entries: fs.Dirent[]
|
|
424
|
+
try {
|
|
425
|
+
entries = fs.readdirSync(dir, { withFileTypes: true })
|
|
426
|
+
} catch {
|
|
427
|
+
return
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
for (const entry of entries) {
|
|
431
|
+
if (results.length >= maxResults) return
|
|
432
|
+
if (entry.name.startsWith('.') || entry.name === 'node_modules') continue
|
|
433
|
+
|
|
434
|
+
const fullPath = path.join(dir, entry.name)
|
|
435
|
+
|
|
436
|
+
if (entry.isDirectory()) {
|
|
437
|
+
searchDir(root, fullPath, regex, includeGlob, results, maxResults, depth + 1, maxDepth)
|
|
438
|
+
continue
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
if (!entry.isFile()) continue
|
|
442
|
+
|
|
443
|
+
// Apply include glob filter (simple extension/suffix matching)
|
|
444
|
+
if (includeGlob && !matchSimpleGlob(entry.name, includeGlob)) continue
|
|
445
|
+
|
|
446
|
+
// Skip binary files
|
|
447
|
+
const ext = path.extname(entry.name).toLowerCase()
|
|
448
|
+
if (BINARY_EXTENSIONS.has(ext)) continue
|
|
449
|
+
|
|
450
|
+
let fileContent: string
|
|
451
|
+
try {
|
|
452
|
+
const buf = fs.readFileSync(fullPath)
|
|
453
|
+
// Quick binary check on first 512 bytes
|
|
454
|
+
const sample = buf.subarray(0, Math.min(buf.length, 512))
|
|
455
|
+
let isBinary = false
|
|
456
|
+
for (const byte of sample) {
|
|
457
|
+
if (byte === 0) { isBinary = true; break }
|
|
458
|
+
}
|
|
459
|
+
if (isBinary) continue
|
|
460
|
+
fileContent = buf.toString('utf-8')
|
|
461
|
+
} catch {
|
|
462
|
+
continue
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
const relativePath = path.relative(root, fullPath)
|
|
466
|
+
const lines = fileContent.split('\n')
|
|
467
|
+
for (let i = 0; i < lines.length; i++) {
|
|
468
|
+
if (results.length >= maxResults) return
|
|
469
|
+
if (regex.test(lines[i])) {
|
|
470
|
+
results.push(`${relativePath}:${i + 1}: ${lines[i].trimEnd()}`)
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Simple glob matching for include filters.
|
|
478
|
+
* Supports: "*.ts", "*.{ts,tsx}", "test_*", exact names.
|
|
479
|
+
*/
|
|
480
|
+
function matchSimpleGlob(filename: string, glob: string): boolean {
|
|
481
|
+
// Handle brace expansion: *.{ts,tsx} -> check each extension
|
|
482
|
+
const braceMatch = glob.match(/^(.+)\.\{([^}]+)\}$/)
|
|
483
|
+
if (braceMatch) {
|
|
484
|
+
const prefix = braceMatch[1]
|
|
485
|
+
const extensions = braceMatch[2].split(',').map((e) => e.trim())
|
|
486
|
+
for (const ext of extensions) {
|
|
487
|
+
if (matchSimpleGlob(filename, `${prefix}.${ext}`)) return true
|
|
488
|
+
}
|
|
489
|
+
return false
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// Convert simple glob to regex
|
|
493
|
+
const escaped = glob
|
|
494
|
+
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
495
|
+
.replace(/\*/g, '.*')
|
|
496
|
+
.replace(/\?/g, '.')
|
|
497
|
+
try {
|
|
498
|
+
return new RegExp(`^${escaped}$`, 'i').test(filename)
|
|
499
|
+
} catch {
|
|
500
|
+
return filename === glob
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// ---------------------------------------------------------------------------
|
|
505
|
+
// Main dispatch
|
|
506
|
+
// ---------------------------------------------------------------------------
|
|
507
|
+
|
|
508
|
+
async function executeFilesAction(
|
|
509
|
+
rawArgs: Record<string, unknown>,
|
|
510
|
+
ctx: FilesToolContext,
|
|
511
|
+
): Promise<string> {
|
|
512
|
+
const args = normalizeArgs(rawArgs)
|
|
513
|
+
|
|
514
|
+
log.info(TAG, `action=${args.action ?? 'inferred'} path=${args.path ?? '(none)'}`)
|
|
515
|
+
|
|
516
|
+
try {
|
|
517
|
+
switch (args.action) {
|
|
518
|
+
case 'read':
|
|
519
|
+
return actionRead(args, ctx)
|
|
520
|
+
case 'write':
|
|
521
|
+
return actionWrite(args, ctx)
|
|
522
|
+
case 'edit':
|
|
523
|
+
return actionEdit(args, ctx)
|
|
524
|
+
case 'list':
|
|
525
|
+
return actionList(args, ctx)
|
|
526
|
+
case 'search':
|
|
527
|
+
return actionSearch(args, ctx)
|
|
528
|
+
default:
|
|
529
|
+
return `Error: Unknown action "${String(args.action)}". Valid actions: read, write, edit, list, search.`
|
|
530
|
+
}
|
|
531
|
+
} catch (err: unknown) {
|
|
532
|
+
const msg = errorMessage(err)
|
|
533
|
+
if (msg === 'Path traversal not allowed') {
|
|
534
|
+
if (ctx.filesystemScope === 'workspace') {
|
|
535
|
+
return 'Error: target path is outside the session workspace. Use a relative path (e.g., "src/app/globals.css" instead of "/projectname/src/app/globals.css"). The files tool only accesses paths under the workspace root.'
|
|
536
|
+
}
|
|
537
|
+
return 'Error: target path is blocked by the current filesystem policy.'
|
|
538
|
+
}
|
|
539
|
+
return `Error: ${msg}`
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// ---------------------------------------------------------------------------
|
|
544
|
+
// Extension registration
|
|
545
|
+
// ---------------------------------------------------------------------------
|
|
546
|
+
|
|
547
|
+
const FilesExtension: Extension = {
|
|
548
|
+
name: 'Core Files',
|
|
549
|
+
description: 'Consolidated file operations: read, write, edit, list, and search.',
|
|
550
|
+
hooks: {
|
|
551
|
+
getCapabilityDescription: () =>
|
|
552
|
+
'I can manage files with the `files` tool. ' +
|
|
553
|
+
'Actions: `read` (view contents with optional line range), ' +
|
|
554
|
+
'`write` (create/overwrite files), ' +
|
|
555
|
+
'`edit` (surgical find-and-replace), ' +
|
|
556
|
+
'`list` (directory tree), ' +
|
|
557
|
+
'`search` (grep-like content search).',
|
|
558
|
+
getOperatingGuidance: () => [
|
|
559
|
+
'Use `{"action":"list","path":"."}` to inspect the workspace structure.',
|
|
560
|
+
'Use `{"action":"read","path":"src/index.ts"}` to read a file. Add `offset` and `limit` for large files.',
|
|
561
|
+
'Use `{"action":"write","path":"output.txt","content":"..."}` to create or overwrite a file.',
|
|
562
|
+
'Use `{"action":"edit","path":"src/index.ts","old_string":"foo","new_string":"bar"}` for surgical edits without rewriting the whole file.',
|
|
563
|
+
'Use `{"action":"search","path":"src/","query":"TODO","include":"*.ts"}` to find patterns across files.',
|
|
564
|
+
'If a call fails, correct the arguments and retry. Do not conclude the workspace is inaccessible until an explicit attempt fails.',
|
|
565
|
+
],
|
|
566
|
+
} as ExtensionHooks,
|
|
567
|
+
tools: [
|
|
568
|
+
{
|
|
569
|
+
name: 'files',
|
|
570
|
+
description:
|
|
571
|
+
'Consolidated file operations tool. ' +
|
|
572
|
+
'Actions: read (view file, optional offset/limit for line ranges), ' +
|
|
573
|
+
'write (create/overwrite, supports bulk via files[]), ' +
|
|
574
|
+
'edit (surgical old_string->new_string replacement), ' +
|
|
575
|
+
'list (directory tree with depth control), ' +
|
|
576
|
+
'search (grep-like content search with include glob filter).',
|
|
577
|
+
parameters: {
|
|
578
|
+
type: 'object',
|
|
579
|
+
properties: {
|
|
580
|
+
action: { type: 'string', enum: ['read', 'write', 'edit', 'list', 'search'] },
|
|
581
|
+
path: { type: 'string', description: 'Target file or directory path' },
|
|
582
|
+
content: { type: 'string', description: 'File content (write action)' },
|
|
583
|
+
old_string: { type: 'string', description: 'Exact text to find (edit action)' },
|
|
584
|
+
new_string: { type: 'string', description: 'Replacement text (edit action)' },
|
|
585
|
+
offset: { type: 'number', description: 'Start line number, 1-based (read action)' },
|
|
586
|
+
limit: { type: 'number', description: 'Max lines to return (read action)' },
|
|
587
|
+
depth: { type: 'number', description: 'Max directory depth (list action, default 3)' },
|
|
588
|
+
query: { type: 'string', description: 'Search pattern/regex (search action)' },
|
|
589
|
+
include: { type: 'string', description: 'File glob filter, e.g. "*.ts" (search action)' },
|
|
590
|
+
encoding: { type: 'string', enum: ['utf-8', 'base64'] },
|
|
591
|
+
files: {
|
|
592
|
+
type: 'array',
|
|
593
|
+
items: {
|
|
594
|
+
type: 'object',
|
|
595
|
+
properties: { path: { type: 'string' }, content: { type: 'string' } },
|
|
596
|
+
},
|
|
597
|
+
description: 'Bulk file writes',
|
|
598
|
+
},
|
|
599
|
+
},
|
|
600
|
+
required: ['action'],
|
|
601
|
+
},
|
|
602
|
+
execute: async (args, context) =>
|
|
603
|
+
executeFilesAction(
|
|
604
|
+
args as Record<string, unknown>,
|
|
605
|
+
{ cwd: context.session?.cwd || process.cwd() },
|
|
606
|
+
),
|
|
607
|
+
},
|
|
608
|
+
],
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
registerNativeCapability('files', FilesExtension)
|
|
612
|
+
|
|
613
|
+
// ---------------------------------------------------------------------------
|
|
614
|
+
// Tool builder (called from session-tools/index.ts)
|
|
615
|
+
// ---------------------------------------------------------------------------
|
|
616
|
+
|
|
617
|
+
export function buildFilesTools(bctx: ToolBuildContext) {
|
|
618
|
+
if (!bctx.hasExtension('files')) return []
|
|
619
|
+
|
|
620
|
+
return [
|
|
621
|
+
tool(
|
|
622
|
+
async (args) =>
|
|
623
|
+
executeFilesAction(args, {
|
|
624
|
+
cwd: bctx.cwd,
|
|
625
|
+
filesystemScope: bctx.filesystemScope,
|
|
626
|
+
fileAccessPolicy: bctx.fileAccessPolicy,
|
|
627
|
+
}),
|
|
628
|
+
{
|
|
629
|
+
name: 'files',
|
|
630
|
+
description: FilesExtension.tools![0].description,
|
|
631
|
+
schema: z.object({}).passthrough(),
|
|
632
|
+
},
|
|
633
|
+
),
|
|
634
|
+
]
|
|
635
|
+
}
|
|
@@ -19,8 +19,6 @@ import { buildMemoryTools } from './memory'
|
|
|
19
19
|
import { buildChatroomTools } from './chatroom'
|
|
20
20
|
import { buildProtocolTools } from './protocol'
|
|
21
21
|
import { buildSubagentTools } from './subagent'
|
|
22
|
-
import { buildCanvasTools } from './canvas'
|
|
23
|
-
import { buildWalletTools } from './wallet'
|
|
24
22
|
import { buildOpenClawWorkspaceTools } from './openclaw-workspace'
|
|
25
23
|
import { buildScheduleTools } from './schedule'
|
|
26
24
|
import { buildPlatformTools } from './platform'
|
|
@@ -41,6 +39,11 @@ import { buildSkillRuntimeTools } from './skill-runtime'
|
|
|
41
39
|
import { buildConnectorTools } from './connector'
|
|
42
40
|
import { buildPeerQueryTools } from './peer-query'
|
|
43
41
|
import { buildTeamContextTools } from './team-context'
|
|
42
|
+
import { buildExecuteTools } from './execute'
|
|
43
|
+
import { buildSkillsTools } from './skills-tool'
|
|
44
|
+
import { buildFilesTools } from './files-tool'
|
|
45
|
+
import { buildMemoryTool } from './memory-tool'
|
|
46
|
+
import { buildPlatformV2Tools } from './platform-tool'
|
|
44
47
|
import './connector'
|
|
45
48
|
import { normalizeToolInputArgs } from './normalize-tool-args'
|
|
46
49
|
import { enforceFileAccessPolicy } from './file-access-policy'
|
|
@@ -182,8 +185,6 @@ export async function buildSessionTools(cwd: string, enabledExtensions: string[]
|
|
|
182
185
|
['manage_chatrooms', buildChatroomTools],
|
|
183
186
|
['manage_protocols', buildProtocolTools],
|
|
184
187
|
['spawn_subagent', buildSubagentTools],
|
|
185
|
-
['canvas', buildCanvasTools],
|
|
186
|
-
['wallet', buildWalletTools],
|
|
187
188
|
['openclaw_workspace', buildOpenClawWorkspaceTools],
|
|
188
189
|
['schedule', buildScheduleTools],
|
|
189
190
|
['manage_sessions', buildSessionInfoTools],
|
|
@@ -202,6 +203,11 @@ export async function buildSessionTools(cwd: string, enabledExtensions: string[]
|
|
|
202
203
|
['ask_human', buildHumanLoopTools],
|
|
203
204
|
['peer_query', buildPeerQueryTools],
|
|
204
205
|
['team_context', buildTeamContextTools],
|
|
206
|
+
['execute', buildExecuteTools],
|
|
207
|
+
['skills', buildSkillsTools],
|
|
208
|
+
['files_v2', buildFilesTools],
|
|
209
|
+
['memory_v2', buildMemoryTool],
|
|
210
|
+
['platform_v2', buildPlatformV2Tools],
|
|
205
211
|
]
|
|
206
212
|
|
|
207
213
|
for (const [extensionId, builder] of nativeBuilders) {
|
|
@@ -288,8 +294,8 @@ export async function buildSessionTools(cwd: string, enabledExtensions: string[]
|
|
|
288
294
|
tools.push(t)
|
|
289
295
|
}
|
|
290
296
|
}
|
|
291
|
-
} catch (err:
|
|
292
|
-
log.warn('session-tools', `Failed to connect MCP server "${config.name}"`, { serverId, error: err
|
|
297
|
+
} catch (err: unknown) {
|
|
298
|
+
log.warn('session-tools', `Failed to connect MCP server "${config.name}"`, { serverId, error: errorMessage(err) })
|
|
293
299
|
}
|
|
294
300
|
}
|
|
295
301
|
cleanupFns.push(async () => {
|
|
@@ -456,8 +462,8 @@ export async function buildSessionTools(cwd: string, enabledExtensions: string[]
|
|
|
456
462
|
toolToExtensionMap,
|
|
457
463
|
abortSignalRef,
|
|
458
464
|
}
|
|
459
|
-
} catch (err:
|
|
460
|
-
log.error(TAG, 'buildSessionTools critical failure:', err
|
|
465
|
+
} catch (err: unknown) {
|
|
466
|
+
log.error(TAG, 'buildSessionTools critical failure:', errorMessage(err))
|
|
461
467
|
throw err
|
|
462
468
|
}
|
|
463
469
|
}
|