@swarmclawai/swarmclaw 0.6.7 → 0.7.0
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 +82 -39
- package/next.config.ts +31 -6
- package/package.json +3 -2
- package/src/app/api/agents/[id]/thread/route.ts +1 -0
- package/src/app/api/agents/route.ts +19 -5
- package/src/app/api/approvals/route.ts +22 -0
- package/src/app/api/chatrooms/[id]/chat/route.ts +4 -0
- package/src/app/api/clawhub/install/route.ts +2 -2
- package/src/app/api/eval/run/route.ts +37 -0
- package/src/app/api/eval/scenarios/route.ts +24 -0
- package/src/app/api/eval/suite/route.ts +29 -0
- package/src/app/api/mcp-servers/[id]/conformance/route.ts +26 -0
- package/src/app/api/mcp-servers/[id]/invoke/route.ts +81 -0
- package/src/app/api/memory/graph/route.ts +46 -0
- package/src/app/api/memory/route.ts +36 -5
- package/src/app/api/notifications/route.ts +3 -0
- package/src/app/api/plugins/install/route.ts +57 -5
- package/src/app/api/plugins/marketplace/route.ts +73 -22
- package/src/app/api/plugins/route.ts +61 -1
- package/src/app/api/plugins/ui/route.ts +34 -0
- package/src/app/api/sessions/[id]/checkpoints/route.ts +31 -0
- package/src/app/api/sessions/[id]/restore/route.ts +36 -0
- package/src/app/api/settings/route.ts +62 -0
- package/src/app/api/setup/doctor/route.ts +22 -5
- package/src/app/api/souls/[id]/route.ts +65 -0
- package/src/app/api/souls/route.ts +70 -0
- package/src/app/api/tasks/[id]/approve/route.ts +4 -3
- package/src/app/api/tasks/[id]/route.ts +16 -3
- package/src/app/api/tasks/route.ts +10 -2
- package/src/app/api/usage/route.ts +9 -2
- package/src/app/globals.css +27 -0
- package/src/app/page.tsx +10 -5
- package/src/cli/index.js +37 -0
- package/src/components/activity/activity-feed.tsx +9 -2
- package/src/components/agents/agent-avatar.tsx +5 -1
- package/src/components/agents/agent-card.tsx +55 -9
- package/src/components/agents/agent-sheet.tsx +112 -34
- package/src/components/agents/inspector-panel.tsx +1 -1
- package/src/components/agents/soul-library-picker.tsx +84 -13
- package/src/components/auth/access-key-gate.tsx +63 -54
- package/src/components/auth/user-picker.tsx +37 -32
- package/src/components/chat/activity-moment.tsx +2 -0
- package/src/components/chat/chat-area.tsx +11 -0
- package/src/components/chat/chat-header.tsx +69 -25
- package/src/components/chat/chat-tool-toggles.tsx +2 -2
- package/src/components/chat/checkpoint-timeline.tsx +112 -0
- package/src/components/chat/code-block.tsx +3 -1
- package/src/components/chat/exec-approval-card.tsx +8 -1
- package/src/components/chat/message-bubble.tsx +164 -4
- package/src/components/chat/message-list.tsx +46 -4
- package/src/components/chat/session-approval-card.tsx +80 -0
- package/src/components/chat/session-debug-panel.tsx +106 -84
- package/src/components/chat/streaming-bubble.tsx +6 -5
- package/src/components/chat/task-approval-card.tsx +78 -0
- package/src/components/chat/thinking-indicator.tsx +48 -12
- package/src/components/chat/tool-call-bubble.tsx +3 -0
- package/src/components/chat/tool-request-banner.tsx +39 -20
- package/src/components/chatrooms/chatroom-list.tsx +11 -4
- package/src/components/chatrooms/chatroom-sheet.tsx +7 -2
- package/src/components/connectors/connector-list.tsx +33 -11
- package/src/components/connectors/connector-sheet.tsx +37 -7
- package/src/components/home/home-view.tsx +54 -24
- package/src/components/input/chat-input.tsx +22 -1
- package/src/components/knowledge/knowledge-list.tsx +17 -18
- package/src/components/knowledge/knowledge-sheet.tsx +9 -5
- package/src/components/layout/app-layout.tsx +87 -19
- package/src/components/mcp-servers/mcp-server-list.tsx +352 -50
- package/src/components/mcp-servers/mcp-server-sheet.tsx +25 -9
- package/src/components/memory/memory-browser.tsx +73 -45
- package/src/components/memory/memory-graph-view.tsx +203 -0
- package/src/components/memory/memory-list.tsx +20 -13
- package/src/components/plugins/plugin-list.tsx +214 -60
- package/src/components/plugins/plugin-sheet.tsx +119 -24
- package/src/components/projects/project-list.tsx +17 -9
- package/src/components/providers/provider-list.tsx +21 -6
- package/src/components/providers/provider-sheet.tsx +42 -25
- package/src/components/runs/run-list.tsx +17 -13
- package/src/components/schedules/schedule-card.tsx +10 -3
- package/src/components/schedules/schedule-list.tsx +2 -2
- package/src/components/schedules/schedule-sheet.tsx +28 -9
- package/src/components/secrets/secret-sheet.tsx +7 -2
- package/src/components/secrets/secrets-list.tsx +18 -5
- package/src/components/sessions/new-session-sheet.tsx +183 -376
- package/src/components/sessions/session-card.tsx +10 -2
- package/src/components/settings/gateway-connection-panel.tsx +9 -8
- package/src/components/shared/command-palette.tsx +13 -5
- package/src/components/shared/empty-state.tsx +20 -8
- package/src/components/shared/hint-tip.tsx +31 -0
- package/src/components/shared/notification-center.tsx +134 -86
- package/src/components/shared/profile-sheet.tsx +4 -0
- package/src/components/shared/settings/plugin-manager.tsx +360 -135
- package/src/components/shared/settings/section-capability-policy.tsx +3 -3
- package/src/components/shared/settings/section-runtime-loop.tsx +149 -4
- package/src/components/skills/clawhub-browser.tsx +1 -0
- package/src/components/skills/skill-list.tsx +31 -12
- package/src/components/skills/skill-sheet.tsx +20 -7
- package/src/components/tasks/approvals-panel.tsx +224 -0
- package/src/components/tasks/task-board.tsx +20 -12
- package/src/components/tasks/task-card.tsx +21 -7
- package/src/components/tasks/task-column.tsx +4 -3
- package/src/components/tasks/task-list.tsx +1 -1
- package/src/components/tasks/task-sheet.tsx +130 -1
- package/src/components/ui/dialog.tsx +1 -0
- package/src/components/ui/sheet.tsx +1 -0
- package/src/components/usage/metrics-dashboard.tsx +72 -48
- package/src/components/wallets/wallet-panel.tsx +65 -41
- package/src/components/wallets/wallet-section.tsx +9 -3
- package/src/components/webhooks/webhook-list.tsx +21 -12
- package/src/components/webhooks/webhook-sheet.tsx +13 -3
- package/src/lib/approval-display.test.ts +45 -0
- package/src/lib/approval-display.ts +62 -0
- package/src/lib/clipboard.ts +38 -0
- package/src/lib/memory.ts +8 -0
- package/src/lib/providers/claude-cli.ts +5 -3
- package/src/lib/providers/index.ts +67 -21
- package/src/lib/runtime-loop.ts +3 -2
- package/src/lib/server/approvals.ts +150 -0
- package/src/lib/server/chat-execution.ts +319 -74
- package/src/lib/server/chatroom-helpers.ts +63 -5
- package/src/lib/server/chatroom-orchestration.ts +74 -0
- package/src/lib/server/clawhub-client.ts +82 -6
- package/src/lib/server/connectors/manager.ts +27 -1
- package/src/lib/server/context-manager.ts +132 -50
- package/src/lib/server/cost.test.ts +73 -0
- package/src/lib/server/cost.ts +165 -34
- package/src/lib/server/daemon-state.ts +112 -1
- package/src/lib/server/data-dir.ts +18 -1
- package/src/lib/server/eval/runner.ts +126 -0
- package/src/lib/server/eval/scenarios.ts +218 -0
- package/src/lib/server/eval/scorer.ts +96 -0
- package/src/lib/server/eval/store.ts +37 -0
- package/src/lib/server/eval/types.ts +48 -0
- package/src/lib/server/execution-log.ts +12 -8
- package/src/lib/server/guardian.ts +34 -0
- package/src/lib/server/heartbeat-service.ts +53 -1
- package/src/lib/server/integrity-monitor.ts +208 -0
- package/src/lib/server/langgraph-checkpoint.ts +10 -0
- package/src/lib/server/link-understanding.ts +55 -0
- package/src/lib/server/llm-response-cache.test.ts +102 -0
- package/src/lib/server/llm-response-cache.ts +227 -0
- package/src/lib/server/main-agent-loop.ts +115 -16
- package/src/lib/server/main-session.ts +6 -3
- package/src/lib/server/mcp-conformance.test.ts +18 -0
- package/src/lib/server/mcp-conformance.ts +233 -0
- package/src/lib/server/memory-db.ts +193 -19
- package/src/lib/server/memory-retrieval.test.ts +56 -0
- package/src/lib/server/mmr.ts +73 -0
- package/src/lib/server/orchestrator-lg.ts +7 -1
- package/src/lib/server/orchestrator.ts +4 -3
- package/src/lib/server/plugins.ts +662 -132
- package/src/lib/server/process-manager.ts +18 -0
- package/src/lib/server/query-expansion.ts +57 -0
- package/src/lib/server/queue.ts +280 -11
- package/src/lib/server/runtime-settings.ts +9 -0
- package/src/lib/server/session-run-manager.test.ts +23 -0
- package/src/lib/server/session-run-manager.ts +32 -2
- package/src/lib/server/session-tools/canvas.ts +85 -50
- package/src/lib/server/session-tools/chatroom.ts +130 -127
- package/src/lib/server/session-tools/connector.ts +233 -454
- package/src/lib/server/session-tools/context-mgmt.ts +87 -105
- package/src/lib/server/session-tools/crud.ts +84 -7
- package/src/lib/server/session-tools/delegate.ts +351 -752
- package/src/lib/server/session-tools/discovery.ts +198 -0
- package/src/lib/server/session-tools/edit_file.ts +82 -0
- package/src/lib/server/session-tools/file-send.test.ts +39 -0
- package/src/lib/server/session-tools/file.ts +257 -425
- package/src/lib/server/session-tools/git.ts +87 -47
- package/src/lib/server/session-tools/http.ts +95 -33
- package/src/lib/server/session-tools/index.ts +217 -138
- package/src/lib/server/session-tools/memory.ts +154 -239
- package/src/lib/server/session-tools/monitor.ts +126 -0
- package/src/lib/server/session-tools/normalize-tool-args.test.ts +61 -0
- package/src/lib/server/session-tools/normalize-tool-args.ts +48 -0
- package/src/lib/server/session-tools/openclaw-nodes.ts +82 -99
- package/src/lib/server/session-tools/openclaw-workspace.ts +103 -93
- package/src/lib/server/session-tools/platform.ts +86 -0
- package/src/lib/server/session-tools/plugin-creator.ts +239 -0
- package/src/lib/server/session-tools/sample-ui.ts +97 -0
- package/src/lib/server/session-tools/sandbox.ts +175 -148
- package/src/lib/server/session-tools/schedule.ts +78 -0
- package/src/lib/server/session-tools/session-info.ts +104 -410
- package/src/lib/server/session-tools/shell-normalize.test.ts +43 -0
- package/src/lib/server/session-tools/shell.ts +171 -143
- package/src/lib/server/session-tools/subagent.ts +77 -77
- package/src/lib/server/session-tools/wallet.ts +182 -106
- package/src/lib/server/session-tools/web.ts +181 -327
- package/src/lib/server/storage.ts +36 -0
- package/src/lib/server/stream-agent-chat.ts +348 -242
- package/src/lib/server/task-quality-gate.test.ts +44 -0
- package/src/lib/server/task-quality-gate.ts +67 -0
- package/src/lib/server/task-validation.test.ts +78 -0
- package/src/lib/server/task-validation.ts +67 -2
- package/src/lib/server/tool-aliases.ts +68 -0
- package/src/lib/server/tool-capability-policy.ts +24 -5
- package/src/lib/server/tool-retry.ts +62 -0
- package/src/lib/server/transcript-repair.ts +72 -0
- package/src/lib/setup-defaults.ts +1 -0
- package/src/lib/tasks.ts +7 -1
- package/src/lib/tool-definitions.ts +24 -23
- package/src/lib/validation/schemas.ts +13 -0
- package/src/lib/view-routes.ts +2 -23
- package/src/stores/use-app-store.ts +23 -1
- package/src/types/index.ts +155 -10
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import { tool, type StructuredToolInterface } from '@langchain/core/tools'
|
|
3
|
+
import type { ToolBuildContext } from './context'
|
|
4
|
+
import { getPluginManager } from '../plugins'
|
|
5
|
+
import type { Plugin, PluginHooks, ClawHubSkill } from '@/types'
|
|
6
|
+
import { searchClawHub } from '../clawhub-client'
|
|
7
|
+
import { normalizeToolInputArgs } from './normalize-tool-args'
|
|
8
|
+
import { toolIdMatches } from '../tool-aliases'
|
|
9
|
+
import { loadSessions } from '../storage'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Unified Discovery Logic
|
|
13
|
+
*/
|
|
14
|
+
async function executeDiscoveryAction(args: Record<string, unknown>, bctx?: ToolBuildContext) {
|
|
15
|
+
const normalized = normalizeToolInputArgs(args)
|
|
16
|
+
const action = normalized.action
|
|
17
|
+
const approved = normalized.approved
|
|
18
|
+
const pluginId = typeof normalized.pluginId === 'string'
|
|
19
|
+
? normalized.pluginId.trim()
|
|
20
|
+
: typeof normalized.plugin_id === 'string'
|
|
21
|
+
? normalized.plugin_id.trim()
|
|
22
|
+
: undefined
|
|
23
|
+
const url = typeof normalized.url === 'string' ? normalized.url.trim() : undefined
|
|
24
|
+
const reason = normalized.reason as string | undefined
|
|
25
|
+
const manager = getPluginManager()
|
|
26
|
+
const q = typeof normalized.query === 'string' ? normalized.query : ''
|
|
27
|
+
|
|
28
|
+
console.log('[discovery] Executing action:', action, { query: q, pluginId })
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
switch (action) {
|
|
32
|
+
case 'list':
|
|
33
|
+
case 'discover': {
|
|
34
|
+
const plugins = manager.listPlugins()
|
|
35
|
+
return JSON.stringify(plugins.map(p => ({
|
|
36
|
+
id: p.filename,
|
|
37
|
+
name: p.name,
|
|
38
|
+
description: p.description,
|
|
39
|
+
enabled: p.enabled,
|
|
40
|
+
isBuiltin: !p.filename.endsWith('.js') && !p.filename.endsWith('.mjs')
|
|
41
|
+
})), null, 2)
|
|
42
|
+
}
|
|
43
|
+
case 'search_marketplace': {
|
|
44
|
+
const results: Record<string, unknown>[] = []
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
console.log('[discovery] Searching ClawHub...')
|
|
48
|
+
const hubResults = await searchClawHub(q)
|
|
49
|
+
if (hubResults && hubResults.skills) {
|
|
50
|
+
results.push(...hubResults.skills.map((s) => ({
|
|
51
|
+
id: s.id,
|
|
52
|
+
name: s.name,
|
|
53
|
+
description: s.description,
|
|
54
|
+
author: s.author,
|
|
55
|
+
source: 'clawhub',
|
|
56
|
+
url: (s as ClawHubSkill & { rawUrl?: string }).rawUrl ?? s.url
|
|
57
|
+
})))
|
|
58
|
+
}
|
|
59
|
+
} catch (err: unknown) {
|
|
60
|
+
console.error('[discovery] ClawHub search failed:', err instanceof Error ? err.message : String(err))
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
console.log('[discovery] Searching SwarmClaw registry...')
|
|
65
|
+
const scRes = await fetch('https://swarmclaw.ai/registry/plugins.json', { signal: AbortSignal.timeout(5000) })
|
|
66
|
+
if (scRes.ok) {
|
|
67
|
+
const scPlugins = await scRes.json()
|
|
68
|
+
const filtered = (scPlugins as Record<string, unknown>[]).filter((p: Record<string, unknown>) =>
|
|
69
|
+
!q || (String(p.name || '')).toLowerCase().includes(q.toLowerCase()) || (String(p.description || '')).toLowerCase().includes(q.toLowerCase())
|
|
70
|
+
)
|
|
71
|
+
results.push(...filtered.map(p => ({ ...p, source: 'swarmclaw' })))
|
|
72
|
+
}
|
|
73
|
+
} catch (err: unknown) {
|
|
74
|
+
console.error('[discovery] SC Registry search failed:', err instanceof Error ? err.message : String(err))
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (results.length === 0) {
|
|
78
|
+
return 'No marketplace plugins found for your query.'
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return JSON.stringify(results, null, 2)
|
|
82
|
+
}
|
|
83
|
+
case 'request_access': {
|
|
84
|
+
if (!pluginId) {
|
|
85
|
+
return JSON.stringify({ error: 'pluginId is required for request_access. Use "discover" first to find the plugin filename.' })
|
|
86
|
+
}
|
|
87
|
+
// Check if the agent already has access via alias expansion (e.g. manage_platform includes manage_secrets)
|
|
88
|
+
if (bctx?.ctx?.sessionId) {
|
|
89
|
+
const allSessions = loadSessions()
|
|
90
|
+
const currentSession = allSessions[bctx.ctx.sessionId]
|
|
91
|
+
if (currentSession && toolIdMatches(currentSession.tools, pluginId)) {
|
|
92
|
+
return JSON.stringify({
|
|
93
|
+
alreadyGranted: true,
|
|
94
|
+
pluginId,
|
|
95
|
+
message: `You already have access to "${pluginId}" — proceed to use it directly.`,
|
|
96
|
+
})
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
const { requestApproval } = await import('../approvals')
|
|
100
|
+
requestApproval({
|
|
101
|
+
category: 'tool_access',
|
|
102
|
+
title: `Enable Plugin: ${pluginId}`,
|
|
103
|
+
description: reason || `Agent is requesting access to the "${pluginId}" plugin.`,
|
|
104
|
+
data: { toolId: pluginId, pluginId, requestedBy: bctx?.ctx?.agentId || 'unknown', reason: reason || '' },
|
|
105
|
+
agentId: bctx?.ctx?.agentId,
|
|
106
|
+
sessionId: bctx?.ctx?.sessionId,
|
|
107
|
+
})
|
|
108
|
+
return JSON.stringify({
|
|
109
|
+
type: 'plugin_request',
|
|
110
|
+
pluginId,
|
|
111
|
+
toolId: pluginId,
|
|
112
|
+
reason,
|
|
113
|
+
message: `Plugin access request sent to user for "${pluginId}". Once granted, I'll automatically continue.`,
|
|
114
|
+
})
|
|
115
|
+
}
|
|
116
|
+
case 'install_request': {
|
|
117
|
+
if (!url) {
|
|
118
|
+
return JSON.stringify({ error: 'url is required for install_request.' })
|
|
119
|
+
}
|
|
120
|
+
if (approved !== true) {
|
|
121
|
+
const { requestApproval } = await import('../approvals')
|
|
122
|
+
requestApproval({
|
|
123
|
+
category: 'plugin_install',
|
|
124
|
+
title: `Install Plugin${pluginId ? `: ${pluginId}` : ' from URL'}`,
|
|
125
|
+
description: reason || `Agent wants to install a plugin${url ? ` from ${url}` : ''}.`,
|
|
126
|
+
data: { url, pluginId: pluginId || '', requestedBy: bctx?.ctx?.agentId || 'unknown', reason: reason || '' },
|
|
127
|
+
agentId: bctx?.ctx?.agentId,
|
|
128
|
+
sessionId: bctx?.ctx?.sessionId,
|
|
129
|
+
})
|
|
130
|
+
return JSON.stringify({
|
|
131
|
+
type: 'plugin_install_request',
|
|
132
|
+
url,
|
|
133
|
+
pluginId,
|
|
134
|
+
reason,
|
|
135
|
+
message: `I'm requesting to install a new plugin from ${url}. This will add new capabilities to the platform.`
|
|
136
|
+
})
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return `Installation approved. Please go to the Plugins manager and install from: ${url}`
|
|
140
|
+
}
|
|
141
|
+
default:
|
|
142
|
+
return `Error: Unknown action "${action}"`
|
|
143
|
+
}
|
|
144
|
+
} catch (err: unknown) {
|
|
145
|
+
const msg = err instanceof Error ? err.message : String(err)
|
|
146
|
+
console.error('[discovery] executeDiscoveryAction failed:', msg)
|
|
147
|
+
return `Error: ${msg}`
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Register as a Built-in Plugin
|
|
153
|
+
*/
|
|
154
|
+
const DiscoveryPlugin: Plugin = {
|
|
155
|
+
name: 'Core Discovery',
|
|
156
|
+
description: 'Discover available plugins, search marketplaces, request access, or suggest new installs.',
|
|
157
|
+
hooks: {} as PluginHooks,
|
|
158
|
+
tools: [
|
|
159
|
+
{
|
|
160
|
+
name: 'manage_capabilities',
|
|
161
|
+
description: 'Search for available plugins locally or in external marketplaces.',
|
|
162
|
+
parameters: {
|
|
163
|
+
type: 'object',
|
|
164
|
+
properties: {
|
|
165
|
+
action: { type: 'string', enum: ['discover', 'search_marketplace', 'request_access', 'install_request'] },
|
|
166
|
+
query: { type: 'string', description: 'Search term for marketplace' },
|
|
167
|
+
pluginId: { type: 'string', description: 'The ID or filename of the plugin' },
|
|
168
|
+
url: { type: 'string', description: 'URL for new plugin install request' },
|
|
169
|
+
reason: { type: 'string', description: 'Why you need this capability' }
|
|
170
|
+
},
|
|
171
|
+
required: ['action', 'reason']
|
|
172
|
+
},
|
|
173
|
+
execute: async (args) => executeDiscoveryAction(args)
|
|
174
|
+
}
|
|
175
|
+
]
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
getPluginManager().registerBuiltin('discovery', DiscoveryPlugin)
|
|
179
|
+
|
|
180
|
+
export function buildDiscoveryTools(bctx: ToolBuildContext): StructuredToolInterface[] {
|
|
181
|
+
// Always allow agents to discover what they can do
|
|
182
|
+
return [
|
|
183
|
+
tool(
|
|
184
|
+
async (args) => executeDiscoveryAction(args, bctx),
|
|
185
|
+
{
|
|
186
|
+
name: 'manage_capabilities',
|
|
187
|
+
description: DiscoveryPlugin.tools![0].description,
|
|
188
|
+
schema: z.object({
|
|
189
|
+
action: z.enum(['discover', 'search_marketplace', 'request_access', 'install_request']).describe('The discovery action to perform'),
|
|
190
|
+
query: z.string().optional().describe('The search query for marketplace actions'),
|
|
191
|
+
pluginId: z.string().optional(),
|
|
192
|
+
url: z.string().optional(),
|
|
193
|
+
reason: z.string().describe('Why you need to perform this discovery action')
|
|
194
|
+
})
|
|
195
|
+
}
|
|
196
|
+
)
|
|
197
|
+
]
|
|
198
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import { tool, type StructuredToolInterface } from '@langchain/core/tools'
|
|
3
|
+
import fs from 'fs'
|
|
4
|
+
import type { ToolBuildContext } from './context'
|
|
5
|
+
import { safePath } from './context'
|
|
6
|
+
import type { Plugin, PluginHooks } from '@/types'
|
|
7
|
+
import { getPluginManager } from '../plugins'
|
|
8
|
+
import { normalizeToolInputArgs } from './normalize-tool-args'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Core Edit File Execution Logic (Surgical Search and Replace)
|
|
12
|
+
*/
|
|
13
|
+
async function executeEditFile(args: { filePath: string; oldString: string; newString: string }, context: { cwd: string }) {
|
|
14
|
+
const normalized = normalizeToolInputArgs((args ?? {}) as Record<string, unknown>)
|
|
15
|
+
const filePath = (normalized.filePath ?? normalized.path) as string
|
|
16
|
+
const oldString = normalized.oldString as string
|
|
17
|
+
const newString = normalized.newString as string
|
|
18
|
+
try {
|
|
19
|
+
const resolved = safePath(context.cwd, filePath)
|
|
20
|
+
if (!fs.existsSync(resolved)) return `Error: File not found: ${filePath}`
|
|
21
|
+
|
|
22
|
+
const content = fs.readFileSync(resolved, 'utf-8')
|
|
23
|
+
const count = content.split(oldString).length - 1
|
|
24
|
+
|
|
25
|
+
if (count === 0) {
|
|
26
|
+
return `Error: Exact match for 'oldString' not found in ${filePath}. Use 'files' with action='read' to check content.`
|
|
27
|
+
}
|
|
28
|
+
if (count > 1) {
|
|
29
|
+
return `Error: Multiple matches (${count}) found for 'oldString'. Please provide more context to ensure a surgical replacement.`
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const updated = content.replace(oldString, newString)
|
|
33
|
+
fs.writeFileSync(resolved, updated, 'utf-8')
|
|
34
|
+
return `Successfully updated ${filePath} (1 replacement made).`
|
|
35
|
+
} catch (err: any) {
|
|
36
|
+
return `Error: ${err.message}`
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Register as a Built-in Plugin
|
|
42
|
+
*/
|
|
43
|
+
const EditFilePlugin: Plugin = {
|
|
44
|
+
name: 'Core Edit File',
|
|
45
|
+
description: 'Surgical search-and-replace within existing files.',
|
|
46
|
+
hooks: {} as PluginHooks,
|
|
47
|
+
tools: [
|
|
48
|
+
{
|
|
49
|
+
name: 'edit_file',
|
|
50
|
+
description: 'Surgically replace a specific string within a file.',
|
|
51
|
+
parameters: {
|
|
52
|
+
type: 'object',
|
|
53
|
+
properties: {
|
|
54
|
+
filePath: { type: 'string' },
|
|
55
|
+
oldString: { type: 'string', description: 'The exact literal text to replace' },
|
|
56
|
+
newString: { type: 'string', description: 'The replacement text' }
|
|
57
|
+
},
|
|
58
|
+
required: ['filePath', 'oldString', 'newString']
|
|
59
|
+
},
|
|
60
|
+
execute: async (args, context) => executeEditFile(args as any, { cwd: context.session.cwd || process.cwd() })
|
|
61
|
+
}
|
|
62
|
+
]
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
getPluginManager().registerBuiltin('edit_file', EditFilePlugin)
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Legacy Bridge
|
|
69
|
+
*/
|
|
70
|
+
export function buildEditFileTools(bctx: ToolBuildContext): StructuredToolInterface[] {
|
|
71
|
+
if (!bctx.hasTool('edit_file')) return []
|
|
72
|
+
return [
|
|
73
|
+
tool(
|
|
74
|
+
async (args) => executeEditFile(args as any, { cwd: bctx.cwd }),
|
|
75
|
+
{
|
|
76
|
+
name: 'edit_file',
|
|
77
|
+
description: EditFilePlugin.tools![0].description,
|
|
78
|
+
schema: z.object({}).passthrough()
|
|
79
|
+
}
|
|
80
|
+
)
|
|
81
|
+
]
|
|
82
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { describe, it } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import { normalizeSendFilePaths } from './file'
|
|
4
|
+
|
|
5
|
+
describe('normalizeSendFilePaths', () => {
|
|
6
|
+
it('reads top-level filePath', () => {
|
|
7
|
+
const out = normalizeSendFilePaths({ filePath: 'foo.png' })
|
|
8
|
+
assert.deepEqual(out, ['foo.png'])
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
it('reads nested input.files string array payload', () => {
|
|
12
|
+
const out = normalizeSendFilePaths({
|
|
13
|
+
input: {
|
|
14
|
+
files: ['a.png', 'b.png'],
|
|
15
|
+
},
|
|
16
|
+
})
|
|
17
|
+
assert.deepEqual(out, ['a.png', 'b.png'])
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('reads stringified input payload', () => {
|
|
21
|
+
const out = normalizeSendFilePaths({
|
|
22
|
+
input: JSON.stringify({
|
|
23
|
+
files: ['a.png'],
|
|
24
|
+
}),
|
|
25
|
+
})
|
|
26
|
+
assert.deepEqual(out, ['a.png'])
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('reads files object entries with path/filePath and dedupes', () => {
|
|
30
|
+
const out = normalizeSendFilePaths({
|
|
31
|
+
files: [
|
|
32
|
+
{ path: 'a.png' },
|
|
33
|
+
{ filePath: 'b.png' },
|
|
34
|
+
{ path: 'a.png' },
|
|
35
|
+
],
|
|
36
|
+
})
|
|
37
|
+
assert.deepEqual(out, ['a.png', 'b.png'])
|
|
38
|
+
})
|
|
39
|
+
})
|