synapse-mcp 1.0.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 +607 -0
- package/dist/constants.d.ts +23 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +58 -0
- package/dist/constants.js.map +1 -0
- package/dist/formatters/index.d.ts +275 -0
- package/dist/formatters/index.d.ts.map +1 -0
- package/dist/formatters/index.js +461 -0
- package/dist/formatters/index.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +178 -0
- package/dist/index.js.map +1 -0
- package/dist/schemas/common.d.ts +48 -0
- package/dist/schemas/common.d.ts.map +1 -0
- package/dist/schemas/common.js +69 -0
- package/dist/schemas/common.js.map +1 -0
- package/dist/schemas/discriminator.d.ts +20 -0
- package/dist/schemas/discriminator.d.ts.map +1 -0
- package/dist/schemas/discriminator.js +25 -0
- package/dist/schemas/discriminator.js.map +1 -0
- package/dist/schemas/flux/compose.d.ts +93 -0
- package/dist/schemas/flux/compose.d.ts.map +1 -0
- package/dist/schemas/flux/compose.js +112 -0
- package/dist/schemas/flux/compose.js.map +1 -0
- package/dist/schemas/flux/container.d.ts +144 -0
- package/dist/schemas/flux/container.d.ts.map +1 -0
- package/dist/schemas/flux/container.js +163 -0
- package/dist/schemas/flux/container.js.map +1 -0
- package/dist/schemas/flux/docker.d.ts +91 -0
- package/dist/schemas/flux/docker.d.ts.map +1 -0
- package/dist/schemas/flux/docker.js +101 -0
- package/dist/schemas/flux/docker.js.map +1 -0
- package/dist/schemas/flux/host.d.ts +61 -0
- package/dist/schemas/flux/host.d.ts.map +1 -0
- package/dist/schemas/flux/host.js +72 -0
- package/dist/schemas/flux/host.js.map +1 -0
- package/dist/schemas/flux/index.d.ts +20 -0
- package/dist/schemas/flux/index.d.ts.map +1 -0
- package/dist/schemas/flux/index.js +88 -0
- package/dist/schemas/flux/index.js.map +1 -0
- package/dist/schemas/index.d.ts +11 -0
- package/dist/schemas/index.d.ts.map +1 -0
- package/dist/schemas/index.js +11 -0
- package/dist/schemas/index.js.map +1 -0
- package/dist/schemas/scout/index.d.ts +151 -0
- package/dist/schemas/scout/index.d.ts.map +1 -0
- package/dist/schemas/scout/index.js +41 -0
- package/dist/schemas/scout/index.js.map +1 -0
- package/dist/schemas/scout/logs.d.ts +48 -0
- package/dist/schemas/scout/logs.d.ts.map +1 -0
- package/dist/schemas/scout/logs.js +47 -0
- package/dist/schemas/scout/logs.js.map +1 -0
- package/dist/schemas/scout/simple.d.ts +68 -0
- package/dist/schemas/scout/simple.d.ts.map +1 -0
- package/dist/schemas/scout/simple.js +75 -0
- package/dist/schemas/scout/simple.js.map +1 -0
- package/dist/schemas/scout/zfs.d.ts +37 -0
- package/dist/schemas/scout/zfs.d.ts.map +1 -0
- package/dist/schemas/scout/zfs.js +36 -0
- package/dist/schemas/scout/zfs.js.map +1 -0
- package/dist/schemas/unified.d.ts +674 -0
- package/dist/schemas/unified.d.ts.map +1 -0
- package/dist/schemas/unified.js +453 -0
- package/dist/schemas/unified.js.map +1 -0
- package/dist/services/compose.d.ts +107 -0
- package/dist/services/compose.d.ts.map +1 -0
- package/dist/services/compose.js +308 -0
- package/dist/services/compose.js.map +1 -0
- package/dist/services/container.d.ts +69 -0
- package/dist/services/container.d.ts.map +1 -0
- package/dist/services/container.js +111 -0
- package/dist/services/container.js.map +1 -0
- package/dist/services/docker.d.ts +243 -0
- package/dist/services/docker.d.ts.map +1 -0
- package/dist/services/docker.js +812 -0
- package/dist/services/docker.js.map +1 -0
- package/dist/services/file-service.d.ts +79 -0
- package/dist/services/file-service.d.ts.map +1 -0
- package/dist/services/file-service.js +226 -0
- package/dist/services/file-service.js.map +1 -0
- package/dist/services/interfaces.d.ts +537 -0
- package/dist/services/interfaces.d.ts.map +1 -0
- package/dist/services/interfaces.js +2 -0
- package/dist/services/interfaces.js.map +1 -0
- package/dist/services/ssh-pool-exec.d.ts +10 -0
- package/dist/services/ssh-pool-exec.d.ts.map +1 -0
- package/dist/services/ssh-pool-exec.js +10 -0
- package/dist/services/ssh-pool-exec.js.map +1 -0
- package/dist/services/ssh-pool.d.ts +66 -0
- package/dist/services/ssh-pool.d.ts.map +1 -0
- package/dist/services/ssh-pool.js +253 -0
- package/dist/services/ssh-pool.js.map +1 -0
- package/dist/services/ssh-service.d.ts +39 -0
- package/dist/services/ssh-service.d.ts.map +1 -0
- package/dist/services/ssh-service.js +143 -0
- package/dist/services/ssh-service.js.map +1 -0
- package/dist/services/ssh.d.ts +37 -0
- package/dist/services/ssh.d.ts.map +1 -0
- package/dist/services/ssh.js +50 -0
- package/dist/services/ssh.js.map +1 -0
- package/dist/tools/flux.d.ts +14 -0
- package/dist/tools/flux.d.ts.map +1 -0
- package/dist/tools/flux.js +86 -0
- package/dist/tools/flux.js.map +1 -0
- package/dist/tools/index.d.ts +7 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +43 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/scout.d.ts +14 -0
- package/dist/tools/scout.d.ts.map +1 -0
- package/dist/tools/scout.js +96 -0
- package/dist/tools/scout.js.map +1 -0
- package/dist/tools/unified.d.ts +7 -0
- package/dist/tools/unified.d.ts.map +1 -0
- package/dist/tools/unified.js +827 -0
- package/dist/tools/unified.js.map +1 -0
- package/dist/types.d.ts +93 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +7 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/errors.d.ts +60 -0
- package/dist/utils/errors.d.ts.map +1 -0
- package/dist/utils/errors.js +131 -0
- package/dist/utils/errors.js.map +1 -0
- package/dist/utils/help.d.ts +69 -0
- package/dist/utils/help.d.ts.map +1 -0
- package/dist/utils/help.js +259 -0
- package/dist/utils/help.js.map +1 -0
- package/dist/utils/index.d.ts +4 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +4 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/path-security.d.ts +64 -0
- package/dist/utils/path-security.d.ts.map +1 -0
- package/dist/utils/path-security.js +138 -0
- package/dist/utils/path-security.js.map +1 -0
- package/package.json +85 -0
|
@@ -0,0 +1,827 @@
|
|
|
1
|
+
import { UnifiedHomelabSchema } from "../schemas/unified.js";
|
|
2
|
+
import { loadHostConfigs } from "../services/docker.js";
|
|
3
|
+
import { ResponseFormat } from "../types.js";
|
|
4
|
+
import { truncateIfNeeded, formatContainersMarkdown, formatLogsMarkdown, formatStatsMarkdown, formatMultiStatsMarkdown, formatInspectMarkdown, formatInspectSummaryMarkdown, formatHostStatusMarkdown, formatSearchResultsMarkdown, formatDockerInfoMarkdown, formatDockerDfMarkdown, formatPruneMarkdown, formatHostResourcesMarkdown, formatImagesMarkdown, formatComposeListMarkdown, formatComposeStatusMarkdown, formatScoutReadMarkdown, formatScoutListMarkdown, formatScoutTreeMarkdown, formatScoutExecMarkdown, formatScoutFindMarkdown, formatScoutTransferMarkdown, formatScoutDiffMarkdown } from "../formatters/index.js";
|
|
5
|
+
import { logError, HostOperationError } from "../utils/errors.js";
|
|
6
|
+
/**
|
|
7
|
+
* Collect container stats in parallel across hosts and containers
|
|
8
|
+
*
|
|
9
|
+
* Performance characteristics:
|
|
10
|
+
* - Hosts: Parallel execution via Promise.allSettled
|
|
11
|
+
* - Containers per host: Parallel execution via Promise.allSettled
|
|
12
|
+
* - Complexity: O(max(container_latency)) instead of O(hosts × containers)
|
|
13
|
+
* - Speedup: ~20x for 10 hosts × 20 containers (100s → 5s)
|
|
14
|
+
*
|
|
15
|
+
* Error handling:
|
|
16
|
+
* - Host failures: Logged to console.error, operation continues
|
|
17
|
+
* - Container failures: Skipped silently, partial results returned
|
|
18
|
+
* - Network timeouts: Handled by dockerode timeout config
|
|
19
|
+
*
|
|
20
|
+
* @param targetHosts - Hosts to collect stats from
|
|
21
|
+
* @param dockerService - Docker service instance for operations
|
|
22
|
+
* @param maxContainersPerHost - Maximum containers to query per host (default: 20)
|
|
23
|
+
* @returns Array of stats with host information (partial results on failures)
|
|
24
|
+
*/
|
|
25
|
+
async function collectStatsParallel(targetHosts, dockerService, maxContainersPerHost = 20) {
|
|
26
|
+
// Parallel collection across hosts
|
|
27
|
+
const hostResults = await Promise.allSettled(targetHosts.map(async (host) => {
|
|
28
|
+
try {
|
|
29
|
+
// Get running containers for this host
|
|
30
|
+
const containers = await dockerService.listContainers([host], { state: "running" });
|
|
31
|
+
// Limit to maxContainersPerHost
|
|
32
|
+
const limitedContainers = containers.slice(0, maxContainersPerHost);
|
|
33
|
+
// Parallel collection across containers for this host
|
|
34
|
+
const containerResults = await Promise.allSettled(limitedContainers.map(async (container) => {
|
|
35
|
+
const stats = await dockerService.getContainerStats(container.id, host);
|
|
36
|
+
return { stats, host: host.name };
|
|
37
|
+
}));
|
|
38
|
+
// Filter successful container stat collections
|
|
39
|
+
return containerResults
|
|
40
|
+
.filter((result) => result.status === "fulfilled")
|
|
41
|
+
.map((result) => result.value);
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
logError(new HostOperationError("Failed to collect stats from host", host.name, "collectStatsParallel", error), {
|
|
45
|
+
metadata: {
|
|
46
|
+
maxContainersPerHost,
|
|
47
|
+
timestamp: new Date().toISOString()
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
return [];
|
|
51
|
+
}
|
|
52
|
+
}));
|
|
53
|
+
// Flatten results from all hosts
|
|
54
|
+
const allStats = [];
|
|
55
|
+
for (const result of hostResults) {
|
|
56
|
+
if (result.status === "fulfilled") {
|
|
57
|
+
allStats.push(...result.value);
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
console.error("Host stats collection failed:", result.reason);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return allStats;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Register the unified homelab tool
|
|
67
|
+
*/
|
|
68
|
+
export function registerUnifiedTool(server, container) {
|
|
69
|
+
const hosts = loadHostConfigs();
|
|
70
|
+
const TOOL_DESCRIPTION = `Unified homelab Docker management tool.
|
|
71
|
+
|
|
72
|
+
ACTIONS:
|
|
73
|
+
container <subaction> - Container operations
|
|
74
|
+
list - List containers with filters
|
|
75
|
+
start/stop/restart - Control container state
|
|
76
|
+
pause/unpause - Pause/unpause container
|
|
77
|
+
logs - Get container logs
|
|
78
|
+
stats - Get resource usage stats
|
|
79
|
+
inspect - Get detailed container info
|
|
80
|
+
search - Search containers by query
|
|
81
|
+
pull - Pull latest image for container
|
|
82
|
+
recreate - Recreate container with latest image
|
|
83
|
+
|
|
84
|
+
compose <subaction> - Docker Compose operations
|
|
85
|
+
list - List compose projects
|
|
86
|
+
status - Get project status
|
|
87
|
+
up/down/restart - Control project state
|
|
88
|
+
logs - Get project logs
|
|
89
|
+
build - Build project images
|
|
90
|
+
pull - Pull project images
|
|
91
|
+
recreate - Force recreate containers
|
|
92
|
+
|
|
93
|
+
host <subaction> - Host operations
|
|
94
|
+
status - Check host connectivity
|
|
95
|
+
resources - Get CPU/memory/disk via SSH
|
|
96
|
+
|
|
97
|
+
docker <subaction> - Docker daemon operations (host parameter required)
|
|
98
|
+
info - Get Docker system info
|
|
99
|
+
df - Get disk usage
|
|
100
|
+
prune - Remove unused resources
|
|
101
|
+
|
|
102
|
+
image <subaction> - Image operations
|
|
103
|
+
list - List images
|
|
104
|
+
pull - Pull an image
|
|
105
|
+
build - Build from Dockerfile
|
|
106
|
+
remove - Remove an image
|
|
107
|
+
|
|
108
|
+
scout <subaction> - Remote file operations via SSH
|
|
109
|
+
read - Read file content
|
|
110
|
+
list - List directory contents
|
|
111
|
+
tree - Show directory tree
|
|
112
|
+
exec - Execute command
|
|
113
|
+
find - Find files by pattern
|
|
114
|
+
transfer - Transfer file between hosts
|
|
115
|
+
diff - Diff files across hosts
|
|
116
|
+
|
|
117
|
+
EXAMPLES:
|
|
118
|
+
{ action: "container", subaction: "list", state: "running" }
|
|
119
|
+
{ action: "container", subaction: "restart", container_id: "plex" }
|
|
120
|
+
{ action: "compose", subaction: "up", host: "tootie", project: "plex" }
|
|
121
|
+
{ action: "host", subaction: "resources", host: "tootie" }
|
|
122
|
+
{ action: "docker", subaction: "info", host: "tootie" }
|
|
123
|
+
{ action: "docker", subaction: "df", host: "tootie" }
|
|
124
|
+
{ action: "docker", subaction: "prune", host: "tootie", prune_target: "images", force: true }
|
|
125
|
+
{ action: "image", subaction: "pull", host: "tootie", image: "nginx:latest" }
|
|
126
|
+
{ action: "scout", subaction: "read", host: "tootie", path: "/etc/hosts" }
|
|
127
|
+
{ action: "scout", subaction: "list", host: "tootie", path: "/var/log" }
|
|
128
|
+
{ action: "scout", subaction: "exec", host: "tootie", path: "/tmp", command: "ls -la" }`;
|
|
129
|
+
server.registerTool("homelab", {
|
|
130
|
+
title: "Homelab Manager",
|
|
131
|
+
description: TOOL_DESCRIPTION,
|
|
132
|
+
inputSchema: UnifiedHomelabSchema,
|
|
133
|
+
annotations: {
|
|
134
|
+
readOnlyHint: false,
|
|
135
|
+
destructiveHint: false,
|
|
136
|
+
idempotentHint: false,
|
|
137
|
+
openWorldHint: true
|
|
138
|
+
}
|
|
139
|
+
}, async (params) => {
|
|
140
|
+
try {
|
|
141
|
+
// Validate and parse input with Zod
|
|
142
|
+
const validated = UnifiedHomelabSchema.parse(params);
|
|
143
|
+
return await routeAction(validated, hosts, container);
|
|
144
|
+
}
|
|
145
|
+
catch (error) {
|
|
146
|
+
return {
|
|
147
|
+
isError: true,
|
|
148
|
+
content: [
|
|
149
|
+
{
|
|
150
|
+
type: "text",
|
|
151
|
+
text: `Error: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
152
|
+
}
|
|
153
|
+
]
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Route action to appropriate handler
|
|
160
|
+
*/
|
|
161
|
+
async function routeAction(params, hosts, container) {
|
|
162
|
+
const { action } = params;
|
|
163
|
+
switch (action) {
|
|
164
|
+
case "container":
|
|
165
|
+
return handleContainerAction(params, hosts, container);
|
|
166
|
+
case "compose":
|
|
167
|
+
return handleComposeAction(params, hosts, container);
|
|
168
|
+
case "host":
|
|
169
|
+
return handleHostAction(params, hosts, container);
|
|
170
|
+
case "docker":
|
|
171
|
+
return handleDockerAction(params, hosts, container);
|
|
172
|
+
case "image":
|
|
173
|
+
return handleImageAction(params, hosts, container);
|
|
174
|
+
case "scout":
|
|
175
|
+
return handleScoutAction(params, hosts, container);
|
|
176
|
+
default:
|
|
177
|
+
throw new Error(`Unknown action: ${action}`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
// ===== Container Action Handlers =====
|
|
181
|
+
async function handleContainerAction(params, hosts, container) {
|
|
182
|
+
if (params.action !== "container")
|
|
183
|
+
throw new Error("Invalid action");
|
|
184
|
+
const { subaction } = params;
|
|
185
|
+
const dockerService = container.getDockerService();
|
|
186
|
+
switch (subaction) {
|
|
187
|
+
case "list": {
|
|
188
|
+
const targetHosts = params.host ? hosts.filter((h) => h.name === params.host) : hosts;
|
|
189
|
+
if (params.host && targetHosts.length === 0) {
|
|
190
|
+
return errorResponse(`Host '${params.host}' not found. Available: ${hosts.map((h) => h.name).join(", ")}`);
|
|
191
|
+
}
|
|
192
|
+
const containers = await dockerService.listContainers(targetHosts, {
|
|
193
|
+
state: params.state,
|
|
194
|
+
nameFilter: params.name_filter,
|
|
195
|
+
imageFilter: params.image_filter,
|
|
196
|
+
labelFilter: params.label_filter
|
|
197
|
+
});
|
|
198
|
+
const total = containers.length;
|
|
199
|
+
const paginated = containers.slice(params.offset, params.offset + params.limit);
|
|
200
|
+
const hasMore = total > params.offset + params.limit;
|
|
201
|
+
const output = {
|
|
202
|
+
total,
|
|
203
|
+
count: paginated.length,
|
|
204
|
+
offset: params.offset,
|
|
205
|
+
containers: paginated,
|
|
206
|
+
has_more: hasMore
|
|
207
|
+
};
|
|
208
|
+
const text = params.response_format === ResponseFormat.JSON
|
|
209
|
+
? JSON.stringify(output, null, 2)
|
|
210
|
+
: formatContainersMarkdown(paginated, total, params.offset, hasMore);
|
|
211
|
+
return successResponse(text, output);
|
|
212
|
+
}
|
|
213
|
+
case "start":
|
|
214
|
+
case "stop":
|
|
215
|
+
case "restart":
|
|
216
|
+
case "pause":
|
|
217
|
+
case "unpause": {
|
|
218
|
+
const targetHost = await resolveContainerHost(params.container_id, params.host, hosts, dockerService);
|
|
219
|
+
if (!targetHost) {
|
|
220
|
+
return errorResponse(`Container '${params.container_id}' not found.`);
|
|
221
|
+
}
|
|
222
|
+
await dockerService.containerAction(params.container_id, subaction, targetHost);
|
|
223
|
+
return successResponse(`✓ Successfully performed '${subaction}' on container '${params.container_id}' (host: ${targetHost.name})`);
|
|
224
|
+
}
|
|
225
|
+
case "logs": {
|
|
226
|
+
const targetHost = await resolveContainerHost(params.container_id, params.host, hosts, dockerService);
|
|
227
|
+
if (!targetHost) {
|
|
228
|
+
return errorResponse(`Container '${params.container_id}' not found.`);
|
|
229
|
+
}
|
|
230
|
+
let logs = await dockerService.getContainerLogs(params.container_id, targetHost, {
|
|
231
|
+
lines: params.lines,
|
|
232
|
+
since: params.since,
|
|
233
|
+
until: params.until,
|
|
234
|
+
stream: params.stream
|
|
235
|
+
});
|
|
236
|
+
if (params.grep) {
|
|
237
|
+
const grepLower = params.grep.toLowerCase();
|
|
238
|
+
logs = logs.filter((l) => l.message.toLowerCase().includes(grepLower));
|
|
239
|
+
}
|
|
240
|
+
const output = {
|
|
241
|
+
container: params.container_id,
|
|
242
|
+
host: targetHost.name,
|
|
243
|
+
count: logs.length,
|
|
244
|
+
logs
|
|
245
|
+
};
|
|
246
|
+
const text = params.response_format === ResponseFormat.JSON
|
|
247
|
+
? JSON.stringify(output, null, 2)
|
|
248
|
+
: formatLogsMarkdown(logs, params.container_id, targetHost.name);
|
|
249
|
+
return successResponse(text, output);
|
|
250
|
+
}
|
|
251
|
+
case "stats": {
|
|
252
|
+
if (params.container_id) {
|
|
253
|
+
const targetHost = await resolveContainerHost(params.container_id, params.host, hosts, dockerService);
|
|
254
|
+
if (!targetHost) {
|
|
255
|
+
return errorResponse(`Container '${params.container_id}' not found.`);
|
|
256
|
+
}
|
|
257
|
+
const stats = await dockerService.getContainerStats(params.container_id, targetHost);
|
|
258
|
+
const output = { ...stats, host: targetHost.name };
|
|
259
|
+
const text = params.response_format === ResponseFormat.JSON
|
|
260
|
+
? JSON.stringify(output, null, 2)
|
|
261
|
+
: formatStatsMarkdown([stats], targetHost.name);
|
|
262
|
+
return successResponse(text, output);
|
|
263
|
+
}
|
|
264
|
+
else {
|
|
265
|
+
const targetHosts = params.host ? hosts.filter((h) => h.name === params.host) : hosts;
|
|
266
|
+
// Collect stats in parallel across all hosts and containers
|
|
267
|
+
const allStats = await collectStatsParallel(targetHosts, dockerService, 20);
|
|
268
|
+
const output = { stats: allStats.map((s) => ({ ...s.stats, host: s.host })) };
|
|
269
|
+
const text = params.response_format === ResponseFormat.JSON
|
|
270
|
+
? JSON.stringify(output, null, 2)
|
|
271
|
+
: formatMultiStatsMarkdown(allStats);
|
|
272
|
+
return successResponse(text, output);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
case "inspect": {
|
|
276
|
+
const targetHost = await resolveContainerHost(params.container_id, params.host, hosts, dockerService);
|
|
277
|
+
if (!targetHost) {
|
|
278
|
+
return errorResponse(`Container '${params.container_id}' not found.`);
|
|
279
|
+
}
|
|
280
|
+
const info = await dockerService.inspectContainer(params.container_id, targetHost);
|
|
281
|
+
// Summary mode returns condensed output to save tokens
|
|
282
|
+
if (params.summary) {
|
|
283
|
+
const summary = {
|
|
284
|
+
id: info.Id?.slice(0, 12),
|
|
285
|
+
name: info.Name?.replace(/^\//, ""),
|
|
286
|
+
image: info.Config?.Image,
|
|
287
|
+
state: info.State?.Status,
|
|
288
|
+
created: info.Created,
|
|
289
|
+
started: info.State?.StartedAt,
|
|
290
|
+
restartCount: info.RestartCount,
|
|
291
|
+
ports: Object.keys(info.NetworkSettings?.Ports || {}).filter((p) => info.NetworkSettings?.Ports?.[p]),
|
|
292
|
+
mounts: (info.Mounts || []).map((m) => ({
|
|
293
|
+
src: m.Source,
|
|
294
|
+
dst: m.Destination,
|
|
295
|
+
type: m.Type
|
|
296
|
+
})),
|
|
297
|
+
networks: Object.keys(info.NetworkSettings?.Networks || {}),
|
|
298
|
+
env_count: (info.Config?.Env || []).length,
|
|
299
|
+
labels_count: Object.keys(info.Config?.Labels || {}).length,
|
|
300
|
+
host: targetHost.name
|
|
301
|
+
};
|
|
302
|
+
const text = params.response_format === ResponseFormat.JSON
|
|
303
|
+
? JSON.stringify(summary, null, 2)
|
|
304
|
+
: formatInspectSummaryMarkdown(summary);
|
|
305
|
+
return successResponse(text, summary);
|
|
306
|
+
}
|
|
307
|
+
// Full mode returns complete inspect output
|
|
308
|
+
const output = { ...info, _host: targetHost.name };
|
|
309
|
+
const text = params.response_format === ResponseFormat.JSON
|
|
310
|
+
? JSON.stringify(output, null, 2)
|
|
311
|
+
: formatInspectMarkdown(info, targetHost.name);
|
|
312
|
+
return successResponse(text, output);
|
|
313
|
+
}
|
|
314
|
+
case "search": {
|
|
315
|
+
const targetHosts = params.host ? hosts.filter((h) => h.name === params.host) : hosts;
|
|
316
|
+
const allContainers = await dockerService.listContainers(targetHosts, {});
|
|
317
|
+
const query = params.query.toLowerCase();
|
|
318
|
+
const matches = allContainers.filter((c) => {
|
|
319
|
+
const searchText = [c.name, c.image, ...Object.keys(c.labels), ...Object.values(c.labels)]
|
|
320
|
+
.join(" ")
|
|
321
|
+
.toLowerCase();
|
|
322
|
+
return searchText.includes(query);
|
|
323
|
+
});
|
|
324
|
+
const total = matches.length;
|
|
325
|
+
const paginated = matches.slice(params.offset, params.offset + params.limit);
|
|
326
|
+
const hasMore = total > params.offset + params.limit;
|
|
327
|
+
const output = {
|
|
328
|
+
query: params.query,
|
|
329
|
+
total,
|
|
330
|
+
count: paginated.length,
|
|
331
|
+
containers: paginated,
|
|
332
|
+
has_more: hasMore
|
|
333
|
+
};
|
|
334
|
+
const text = params.response_format === ResponseFormat.JSON
|
|
335
|
+
? JSON.stringify(output, null, 2)
|
|
336
|
+
: formatSearchResultsMarkdown(paginated, params.query, total);
|
|
337
|
+
return successResponse(text, output);
|
|
338
|
+
}
|
|
339
|
+
case "pull": {
|
|
340
|
+
const targetHost = await resolveContainerHost(params.container_id, params.host, hosts, dockerService);
|
|
341
|
+
if (!targetHost) {
|
|
342
|
+
return errorResponse(`Container '${params.container_id}' not found.`);
|
|
343
|
+
}
|
|
344
|
+
const info = await dockerService.inspectContainer(params.container_id, targetHost);
|
|
345
|
+
const imageName = info.Config.Image;
|
|
346
|
+
await dockerService.pullImage(imageName, targetHost);
|
|
347
|
+
return successResponse(`✓ Successfully pulled latest image '${imageName}' for container '${params.container_id}'`);
|
|
348
|
+
}
|
|
349
|
+
case "recreate": {
|
|
350
|
+
const targetHost = await resolveContainerHost(params.container_id, params.host, hosts, dockerService);
|
|
351
|
+
if (!targetHost) {
|
|
352
|
+
return errorResponse(`Container '${params.container_id}' not found.`);
|
|
353
|
+
}
|
|
354
|
+
const result = await dockerService.recreateContainer(params.container_id, targetHost, {
|
|
355
|
+
pull: params.pull
|
|
356
|
+
});
|
|
357
|
+
return successResponse(`✓ ${result.status}. New container ID: ${result.containerId.slice(0, 12)}`);
|
|
358
|
+
}
|
|
359
|
+
default:
|
|
360
|
+
throw new Error(`Unknown container subaction: ${subaction}`);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
// ===== Compose Action Handlers =====
|
|
364
|
+
async function handleComposeAction(params, hosts, container) {
|
|
365
|
+
if (params.action !== "compose")
|
|
366
|
+
throw new Error("Invalid action");
|
|
367
|
+
const { subaction } = params;
|
|
368
|
+
const composeService = container.getComposeService();
|
|
369
|
+
const targetHost = hosts.find((h) => h.name === params.host);
|
|
370
|
+
if (!targetHost) {
|
|
371
|
+
return errorResponse(`Host '${params.host}' not found.`);
|
|
372
|
+
}
|
|
373
|
+
switch (subaction) {
|
|
374
|
+
case "list": {
|
|
375
|
+
let projects = await composeService.listComposeProjects(targetHost);
|
|
376
|
+
// Apply name filter if provided
|
|
377
|
+
if (params.name_filter) {
|
|
378
|
+
const filter = params.name_filter.toLowerCase();
|
|
379
|
+
projects = projects.filter((p) => p.name.toLowerCase().includes(filter));
|
|
380
|
+
}
|
|
381
|
+
const total = projects.length;
|
|
382
|
+
const paginated = projects.slice(params.offset, params.offset + params.limit);
|
|
383
|
+
const hasMore = total > params.offset + params.limit;
|
|
384
|
+
const output = {
|
|
385
|
+
host: params.host,
|
|
386
|
+
total,
|
|
387
|
+
count: paginated.length,
|
|
388
|
+
offset: params.offset,
|
|
389
|
+
projects: paginated,
|
|
390
|
+
has_more: hasMore
|
|
391
|
+
};
|
|
392
|
+
const text = params.response_format === ResponseFormat.JSON
|
|
393
|
+
? JSON.stringify(output, null, 2)
|
|
394
|
+
: formatComposeListMarkdown(paginated, params.host, total, params.offset, hasMore);
|
|
395
|
+
return successResponse(text, output);
|
|
396
|
+
}
|
|
397
|
+
case "status": {
|
|
398
|
+
let status = await composeService.getComposeStatus(targetHost, params.project);
|
|
399
|
+
// Apply service filter if provided
|
|
400
|
+
if (params.service_filter) {
|
|
401
|
+
const filter = params.service_filter.toLowerCase();
|
|
402
|
+
status = {
|
|
403
|
+
...status,
|
|
404
|
+
services: status.services.filter((s) => s.name.toLowerCase().includes(filter))
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
const totalServices = status.services.length;
|
|
408
|
+
const paginatedServices = status.services.slice(params.offset, params.offset + params.limit);
|
|
409
|
+
const hasMore = totalServices > params.offset + params.limit;
|
|
410
|
+
const paginatedStatus = { ...status, services: paginatedServices };
|
|
411
|
+
const output = {
|
|
412
|
+
project: params.project,
|
|
413
|
+
host: params.host,
|
|
414
|
+
total_services: totalServices,
|
|
415
|
+
count: paginatedServices.length,
|
|
416
|
+
offset: params.offset,
|
|
417
|
+
has_more: hasMore,
|
|
418
|
+
status: paginatedStatus
|
|
419
|
+
};
|
|
420
|
+
const text = params.response_format === ResponseFormat.JSON
|
|
421
|
+
? JSON.stringify(output, null, 2)
|
|
422
|
+
: formatComposeStatusMarkdown(paginatedStatus, totalServices, params.offset, hasMore);
|
|
423
|
+
return successResponse(text, output);
|
|
424
|
+
}
|
|
425
|
+
case "up": {
|
|
426
|
+
await composeService.composeUp(targetHost, params.project, params.detach);
|
|
427
|
+
const status = await composeService.getComposeStatus(targetHost, params.project);
|
|
428
|
+
const text = `✓ Started project '${params.project}'\n\n${formatComposeStatusMarkdown(status)}`;
|
|
429
|
+
return successResponse(text, { project: params.project, status });
|
|
430
|
+
}
|
|
431
|
+
case "down": {
|
|
432
|
+
await composeService.composeDown(targetHost, params.project, params.remove_volumes);
|
|
433
|
+
return successResponse(`✓ Stopped project '${params.project}'`);
|
|
434
|
+
}
|
|
435
|
+
case "restart": {
|
|
436
|
+
await composeService.composeRestart(targetHost, params.project);
|
|
437
|
+
const status = await composeService.getComposeStatus(targetHost, params.project);
|
|
438
|
+
const text = `✓ Restarted project '${params.project}'\n\n${formatComposeStatusMarkdown(status)}`;
|
|
439
|
+
return successResponse(text, { project: params.project, status });
|
|
440
|
+
}
|
|
441
|
+
case "logs": {
|
|
442
|
+
const logs = await composeService.composeLogs(targetHost, params.project, {
|
|
443
|
+
tail: params.lines,
|
|
444
|
+
services: params.service ? [params.service] : undefined
|
|
445
|
+
});
|
|
446
|
+
const title = params.service
|
|
447
|
+
? `## Logs: ${params.project}/${params.service}`
|
|
448
|
+
: `## Logs: ${params.project}`;
|
|
449
|
+
const output = {
|
|
450
|
+
project: params.project,
|
|
451
|
+
host: params.host,
|
|
452
|
+
service: params.service || "all",
|
|
453
|
+
logs
|
|
454
|
+
};
|
|
455
|
+
const text = params.response_format === ResponseFormat.JSON
|
|
456
|
+
? JSON.stringify(output, null, 2)
|
|
457
|
+
: `${title}\n\n\`\`\`\n${logs}\n\`\`\``;
|
|
458
|
+
return successResponse(text, output);
|
|
459
|
+
}
|
|
460
|
+
case "build": {
|
|
461
|
+
await composeService.composeBuild(targetHost, params.project, {
|
|
462
|
+
service: params.service,
|
|
463
|
+
noCache: params.no_cache
|
|
464
|
+
});
|
|
465
|
+
return successResponse(`✓ Built images for project '${params.project}'${params.service ? ` (service: ${params.service})` : ""}`);
|
|
466
|
+
}
|
|
467
|
+
case "pull": {
|
|
468
|
+
await composeService.composePull(targetHost, params.project, { service: params.service });
|
|
469
|
+
return successResponse(`✓ Pulled images for project '${params.project}'${params.service ? ` (service: ${params.service})` : ""}`);
|
|
470
|
+
}
|
|
471
|
+
case "recreate": {
|
|
472
|
+
await composeService.composeRecreate(targetHost, params.project, { service: params.service });
|
|
473
|
+
const status = await composeService.getComposeStatus(targetHost, params.project);
|
|
474
|
+
const text = `✓ Recreated project '${params.project}'${params.service ? ` (service: ${params.service})` : ""}\n\n${formatComposeStatusMarkdown(status)}`;
|
|
475
|
+
return successResponse(text, { project: params.project, status });
|
|
476
|
+
}
|
|
477
|
+
default:
|
|
478
|
+
throw new Error(`Unknown compose subaction: ${subaction}`);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
// ===== Host Action Handlers =====
|
|
482
|
+
async function handleHostAction(params, hosts, container) {
|
|
483
|
+
if (params.action !== "host")
|
|
484
|
+
throw new Error("Invalid action");
|
|
485
|
+
const { subaction } = params;
|
|
486
|
+
const dockerService = container.getDockerService();
|
|
487
|
+
const sshService = container.getSSHService();
|
|
488
|
+
switch (subaction) {
|
|
489
|
+
case "status": {
|
|
490
|
+
const targetHosts = params.host ? hosts.filter((h) => h.name === params.host) : hosts;
|
|
491
|
+
if (params.host && targetHosts.length === 0) {
|
|
492
|
+
return errorResponse(`Host '${params.host}' not found.`);
|
|
493
|
+
}
|
|
494
|
+
const status = await dockerService.getHostStatus(targetHosts);
|
|
495
|
+
const output = { hosts: status };
|
|
496
|
+
const text = params.response_format === ResponseFormat.JSON
|
|
497
|
+
? JSON.stringify(output, null, 2)
|
|
498
|
+
: formatHostStatusMarkdown(status);
|
|
499
|
+
return successResponse(text, output);
|
|
500
|
+
}
|
|
501
|
+
case "resources": {
|
|
502
|
+
const targetHosts = params.host ? hosts.filter((h) => h.name === params.host) : hosts;
|
|
503
|
+
if (params.host && targetHosts.length === 0) {
|
|
504
|
+
return errorResponse(`Host '${params.host}' not found.`);
|
|
505
|
+
}
|
|
506
|
+
const results = await Promise.all(targetHosts.map(async (host) => {
|
|
507
|
+
if (host.host.startsWith("/")) {
|
|
508
|
+
return { host: host.name, resources: null, error: "Local socket - SSH not available" };
|
|
509
|
+
}
|
|
510
|
+
try {
|
|
511
|
+
const resources = await sshService.getHostResources(host);
|
|
512
|
+
return { host: host.name, resources };
|
|
513
|
+
}
|
|
514
|
+
catch (error) {
|
|
515
|
+
logError(new HostOperationError("Failed to get host resources", host.name, "getHostResources", error), { operation: "handleHostAction:resources" });
|
|
516
|
+
return {
|
|
517
|
+
host: host.name,
|
|
518
|
+
resources: null,
|
|
519
|
+
error: error instanceof Error ? error.message : "SSH failed"
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
}));
|
|
523
|
+
const output = { hosts: results };
|
|
524
|
+
const text = params.response_format === ResponseFormat.JSON
|
|
525
|
+
? JSON.stringify(output, null, 2)
|
|
526
|
+
: formatHostResourcesMarkdown(results);
|
|
527
|
+
return successResponse(text, output);
|
|
528
|
+
}
|
|
529
|
+
default:
|
|
530
|
+
throw new Error(`Unknown host subaction: ${subaction}`);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
// ===== Docker Action Handlers =====
|
|
534
|
+
async function handleDockerAction(params, hosts, container) {
|
|
535
|
+
if (params.action !== "docker")
|
|
536
|
+
throw new Error("Invalid action");
|
|
537
|
+
const { subaction } = params;
|
|
538
|
+
const dockerService = container.getDockerService();
|
|
539
|
+
switch (subaction) {
|
|
540
|
+
case "info": {
|
|
541
|
+
const targetHost = hosts.find((h) => h.name === params.host);
|
|
542
|
+
if (!targetHost) {
|
|
543
|
+
return errorResponse(`Host '${params.host}' not found.`);
|
|
544
|
+
}
|
|
545
|
+
try {
|
|
546
|
+
const info = await dockerService.getDockerInfo(targetHost);
|
|
547
|
+
const results = [{ host: targetHost.name, info }];
|
|
548
|
+
const output = { hosts: results };
|
|
549
|
+
const text = params.response_format === ResponseFormat.JSON
|
|
550
|
+
? JSON.stringify(output, null, 2)
|
|
551
|
+
: formatDockerInfoMarkdown(results);
|
|
552
|
+
return successResponse(text, output);
|
|
553
|
+
}
|
|
554
|
+
catch (error) {
|
|
555
|
+
return errorResponse(`Failed to get Docker info from ${targetHost.name}: ${error instanceof Error ? error.message : "Connection failed"}`);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
case "df": {
|
|
559
|
+
const targetHost = hosts.find((h) => h.name === params.host);
|
|
560
|
+
if (!targetHost) {
|
|
561
|
+
return errorResponse(`Host '${params.host}' not found.`);
|
|
562
|
+
}
|
|
563
|
+
try {
|
|
564
|
+
const usage = await dockerService.getDockerDiskUsage(targetHost);
|
|
565
|
+
const results = [{ host: targetHost.name, usage }];
|
|
566
|
+
const output = { hosts: results };
|
|
567
|
+
const text = params.response_format === ResponseFormat.JSON
|
|
568
|
+
? JSON.stringify(output, null, 2)
|
|
569
|
+
: formatDockerDfMarkdown(results);
|
|
570
|
+
return successResponse(text, output);
|
|
571
|
+
}
|
|
572
|
+
catch (error) {
|
|
573
|
+
return errorResponse(`Failed to get disk usage from ${targetHost.name}: ${error instanceof Error ? error.message : "Connection failed"}`);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
case "prune": {
|
|
577
|
+
if (!params.force) {
|
|
578
|
+
return errorResponse("⚠️ This is a destructive operation. Set force=true to confirm.");
|
|
579
|
+
}
|
|
580
|
+
const targetHost = hosts.find((h) => h.name === params.host);
|
|
581
|
+
if (!targetHost) {
|
|
582
|
+
return errorResponse(`Host '${params.host}' not found.`);
|
|
583
|
+
}
|
|
584
|
+
try {
|
|
585
|
+
const results = await dockerService.pruneDocker(targetHost, params.prune_target);
|
|
586
|
+
const allResults = [{ host: targetHost.name, results }];
|
|
587
|
+
const output = { hosts: allResults };
|
|
588
|
+
const text = formatPruneMarkdown(allResults);
|
|
589
|
+
return successResponse(text, output);
|
|
590
|
+
}
|
|
591
|
+
catch (error) {
|
|
592
|
+
return errorResponse(`Failed to prune on ${targetHost.name}: ${error instanceof Error ? error.message : "Connection failed"}`);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
default:
|
|
596
|
+
throw new Error(`Unknown docker subaction: ${subaction}`);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
// ===== Image Action Handlers =====
|
|
600
|
+
async function handleImageAction(params, hosts, container) {
|
|
601
|
+
if (params.action !== "image")
|
|
602
|
+
throw new Error("Invalid action");
|
|
603
|
+
const { subaction } = params;
|
|
604
|
+
const dockerService = container.getDockerService();
|
|
605
|
+
switch (subaction) {
|
|
606
|
+
case "list": {
|
|
607
|
+
const targetHosts = params.host ? hosts.filter((h) => h.name === params.host) : hosts;
|
|
608
|
+
if (params.host && targetHosts.length === 0) {
|
|
609
|
+
return errorResponse(`Host '${params.host}' not found.`);
|
|
610
|
+
}
|
|
611
|
+
const images = await dockerService.listImages(targetHosts, {
|
|
612
|
+
danglingOnly: params.dangling_only
|
|
613
|
+
});
|
|
614
|
+
const paginated = images.slice(params.offset, params.offset + params.limit);
|
|
615
|
+
const output = {
|
|
616
|
+
images: paginated,
|
|
617
|
+
pagination: {
|
|
618
|
+
total: images.length,
|
|
619
|
+
count: paginated.length,
|
|
620
|
+
offset: params.offset,
|
|
621
|
+
hasMore: params.offset + params.limit < images.length
|
|
622
|
+
}
|
|
623
|
+
};
|
|
624
|
+
const text = params.response_format === ResponseFormat.JSON
|
|
625
|
+
? JSON.stringify(output, null, 2)
|
|
626
|
+
: formatImagesMarkdown(paginated, images.length, params.offset);
|
|
627
|
+
return successResponse(text, output);
|
|
628
|
+
}
|
|
629
|
+
case "pull": {
|
|
630
|
+
const targetHost = hosts.find((h) => h.name === params.host);
|
|
631
|
+
if (!targetHost) {
|
|
632
|
+
return errorResponse(`Host '${params.host}' not found.`);
|
|
633
|
+
}
|
|
634
|
+
await dockerService.pullImage(params.image, targetHost);
|
|
635
|
+
return successResponse(`✓ Successfully pulled image '${params.image}' on ${params.host}`);
|
|
636
|
+
}
|
|
637
|
+
case "build": {
|
|
638
|
+
const targetHost = hosts.find((h) => h.name === params.host);
|
|
639
|
+
if (!targetHost) {
|
|
640
|
+
return errorResponse(`Host '${params.host}' not found.`);
|
|
641
|
+
}
|
|
642
|
+
await dockerService.buildImage(targetHost, {
|
|
643
|
+
context: params.context,
|
|
644
|
+
tag: params.tag,
|
|
645
|
+
dockerfile: params.dockerfile,
|
|
646
|
+
noCache: params.no_cache
|
|
647
|
+
});
|
|
648
|
+
return successResponse(`✓ Successfully built image '${params.tag}' on ${params.host}`);
|
|
649
|
+
}
|
|
650
|
+
case "remove": {
|
|
651
|
+
const targetHost = hosts.find((h) => h.name === params.host);
|
|
652
|
+
if (!targetHost) {
|
|
653
|
+
return errorResponse(`Host '${params.host}' not found.`);
|
|
654
|
+
}
|
|
655
|
+
await dockerService.removeImage(params.image, targetHost, { force: params.force });
|
|
656
|
+
return successResponse(`✓ Successfully removed image '${params.image}' from ${params.host}`);
|
|
657
|
+
}
|
|
658
|
+
default:
|
|
659
|
+
throw new Error(`Unknown image subaction: ${subaction}`);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
// ===== Scout Action Handlers =====
|
|
663
|
+
async function handleScoutAction(params, hosts, container) {
|
|
664
|
+
if (params.action !== "scout")
|
|
665
|
+
throw new Error("Invalid action");
|
|
666
|
+
const { subaction } = params;
|
|
667
|
+
const fileService = container.getFileService();
|
|
668
|
+
switch (subaction) {
|
|
669
|
+
case "read": {
|
|
670
|
+
const targetHost = hosts.find((h) => h.name === params.host);
|
|
671
|
+
if (!targetHost) {
|
|
672
|
+
return errorResponse(`Host '${params.host}' not found.`);
|
|
673
|
+
}
|
|
674
|
+
const result = await fileService.readFile(targetHost, params.path, params.max_size);
|
|
675
|
+
const output = {
|
|
676
|
+
host: params.host,
|
|
677
|
+
path: params.path,
|
|
678
|
+
content: result.content,
|
|
679
|
+
size: result.size,
|
|
680
|
+
truncated: result.truncated
|
|
681
|
+
};
|
|
682
|
+
const text = params.response_format === ResponseFormat.JSON
|
|
683
|
+
? JSON.stringify(output, null, 2)
|
|
684
|
+
: formatScoutReadMarkdown(params.host, params.path, result.content, result.size, result.truncated);
|
|
685
|
+
return successResponse(text, output);
|
|
686
|
+
}
|
|
687
|
+
case "list": {
|
|
688
|
+
const targetHost = hosts.find((h) => h.name === params.host);
|
|
689
|
+
if (!targetHost) {
|
|
690
|
+
return errorResponse(`Host '${params.host}' not found.`);
|
|
691
|
+
}
|
|
692
|
+
const listing = await fileService.listDirectory(targetHost, params.path, params.all);
|
|
693
|
+
const output = {
|
|
694
|
+
host: params.host,
|
|
695
|
+
path: params.path,
|
|
696
|
+
listing
|
|
697
|
+
};
|
|
698
|
+
const text = params.response_format === ResponseFormat.JSON
|
|
699
|
+
? JSON.stringify(output, null, 2)
|
|
700
|
+
: formatScoutListMarkdown(params.host, params.path, listing);
|
|
701
|
+
return successResponse(text, output);
|
|
702
|
+
}
|
|
703
|
+
case "tree": {
|
|
704
|
+
const targetHost = hosts.find((h) => h.name === params.host);
|
|
705
|
+
if (!targetHost) {
|
|
706
|
+
return errorResponse(`Host '${params.host}' not found.`);
|
|
707
|
+
}
|
|
708
|
+
const tree = await fileService.treeDirectory(targetHost, params.path, params.depth);
|
|
709
|
+
const output = {
|
|
710
|
+
host: params.host,
|
|
711
|
+
path: params.path,
|
|
712
|
+
depth: params.depth,
|
|
713
|
+
tree
|
|
714
|
+
};
|
|
715
|
+
const text = params.response_format === ResponseFormat.JSON
|
|
716
|
+
? JSON.stringify(output, null, 2)
|
|
717
|
+
: formatScoutTreeMarkdown(params.host, params.path, tree, params.depth);
|
|
718
|
+
return successResponse(text, output);
|
|
719
|
+
}
|
|
720
|
+
case "exec": {
|
|
721
|
+
const targetHost = hosts.find((h) => h.name === params.host);
|
|
722
|
+
if (!targetHost) {
|
|
723
|
+
return errorResponse(`Host '${params.host}' not found.`);
|
|
724
|
+
}
|
|
725
|
+
const result = await fileService.executeCommand(targetHost, params.path, params.command, params.timeout);
|
|
726
|
+
const output = {
|
|
727
|
+
host: params.host,
|
|
728
|
+
path: params.path,
|
|
729
|
+
command: params.command,
|
|
730
|
+
stdout: result.stdout,
|
|
731
|
+
exitCode: result.exitCode
|
|
732
|
+
};
|
|
733
|
+
const text = params.response_format === ResponseFormat.JSON
|
|
734
|
+
? JSON.stringify(output, null, 2)
|
|
735
|
+
: formatScoutExecMarkdown(params.host, params.path, params.command, result.stdout, result.exitCode);
|
|
736
|
+
return successResponse(text, output);
|
|
737
|
+
}
|
|
738
|
+
case "find": {
|
|
739
|
+
const targetHost = hosts.find((h) => h.name === params.host);
|
|
740
|
+
if (!targetHost) {
|
|
741
|
+
return errorResponse(`Host '${params.host}' not found.`);
|
|
742
|
+
}
|
|
743
|
+
const results = await fileService.findFiles(targetHost, params.path, params.pattern, {
|
|
744
|
+
type: params.type,
|
|
745
|
+
maxDepth: params.max_depth,
|
|
746
|
+
limit: params.limit
|
|
747
|
+
});
|
|
748
|
+
const output = {
|
|
749
|
+
host: params.host,
|
|
750
|
+
path: params.path,
|
|
751
|
+
pattern: params.pattern,
|
|
752
|
+
results
|
|
753
|
+
};
|
|
754
|
+
const text = params.response_format === ResponseFormat.JSON
|
|
755
|
+
? JSON.stringify(output, null, 2)
|
|
756
|
+
: formatScoutFindMarkdown(params.host, params.path, params.pattern, results);
|
|
757
|
+
return successResponse(text, output);
|
|
758
|
+
}
|
|
759
|
+
case "transfer": {
|
|
760
|
+
const sourceHost = hosts.find((h) => h.name === params.source_host);
|
|
761
|
+
const targetHost = hosts.find((h) => h.name === params.target_host);
|
|
762
|
+
if (!sourceHost) {
|
|
763
|
+
return errorResponse(`Source host '${params.source_host}' not found.`);
|
|
764
|
+
}
|
|
765
|
+
if (!targetHost) {
|
|
766
|
+
return errorResponse(`Target host '${params.target_host}' not found.`);
|
|
767
|
+
}
|
|
768
|
+
const result = await fileService.transferFile(sourceHost, params.source_path, targetHost, params.target_path);
|
|
769
|
+
const output = {
|
|
770
|
+
source_host: params.source_host,
|
|
771
|
+
source_path: params.source_path,
|
|
772
|
+
target_host: params.target_host,
|
|
773
|
+
target_path: params.target_path,
|
|
774
|
+
bytes_transferred: result.bytesTransferred,
|
|
775
|
+
warning: result.warning
|
|
776
|
+
};
|
|
777
|
+
const text = formatScoutTransferMarkdown(params.source_host, params.source_path, params.target_host, params.target_path, result.bytesTransferred, result.warning);
|
|
778
|
+
return successResponse(text, output);
|
|
779
|
+
}
|
|
780
|
+
case "diff": {
|
|
781
|
+
const host1 = hosts.find((h) => h.name === params.host1);
|
|
782
|
+
const host2 = hosts.find((h) => h.name === params.host2);
|
|
783
|
+
if (!host1) {
|
|
784
|
+
return errorResponse(`Host '${params.host1}' not found.`);
|
|
785
|
+
}
|
|
786
|
+
if (!host2) {
|
|
787
|
+
return errorResponse(`Host '${params.host2}' not found.`);
|
|
788
|
+
}
|
|
789
|
+
const diff = await fileService.diffFiles(host1, params.path1, host2, params.path2, params.context_lines);
|
|
790
|
+
const output = {
|
|
791
|
+
host1: params.host1,
|
|
792
|
+
path1: params.path1,
|
|
793
|
+
host2: params.host2,
|
|
794
|
+
path2: params.path2,
|
|
795
|
+
diff
|
|
796
|
+
};
|
|
797
|
+
const text = params.response_format === ResponseFormat.JSON
|
|
798
|
+
? JSON.stringify(output, null, 2)
|
|
799
|
+
: formatScoutDiffMarkdown(params.host1, params.path1, params.host2, params.path2, diff);
|
|
800
|
+
return successResponse(text, output);
|
|
801
|
+
}
|
|
802
|
+
default:
|
|
803
|
+
throw new Error(`Unknown scout subaction: ${subaction}`);
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
// ===== Helper Functions =====
|
|
807
|
+
async function resolveContainerHost(containerId, hostName, hosts, dockerService) {
|
|
808
|
+
if (hostName) {
|
|
809
|
+
const found = hosts.find((h) => h.name === hostName);
|
|
810
|
+
return found || null;
|
|
811
|
+
}
|
|
812
|
+
const result = await dockerService.findContainerHost(containerId, hosts);
|
|
813
|
+
return result?.host || null;
|
|
814
|
+
}
|
|
815
|
+
function successResponse(text, structuredContent) {
|
|
816
|
+
return {
|
|
817
|
+
content: [{ type: "text", text: truncateIfNeeded(text) }],
|
|
818
|
+
...(structuredContent ? { structuredContent } : {})
|
|
819
|
+
};
|
|
820
|
+
}
|
|
821
|
+
function errorResponse(message) {
|
|
822
|
+
return {
|
|
823
|
+
isError: true,
|
|
824
|
+
content: [{ type: "text", text: message }]
|
|
825
|
+
};
|
|
826
|
+
}
|
|
827
|
+
//# sourceMappingURL=unified.js.map
|