|
1 | | -import fs from "node:fs"; |
2 | | -import path from "node:path"; |
| 1 | +import { findOverlappingWorkspaceAgentIds } from "../agents/agent-delete-safety.js"; |
3 | 2 | import { resolveAgentDir, resolveAgentWorkspaceDir } from "../agents/agent-scope.js"; |
4 | 3 | import { replaceConfigFile } from "../config/config.js"; |
5 | 4 | import { logConfigUpdated } from "../config/logging.js"; |
6 | | -import { resolveSessionTranscriptsDirForAgent } from "../config/sessions.js"; |
7 | | -import type { OpenClawConfig } from "../config/types.openclaw.js"; |
| 5 | +import { |
| 6 | + purgeAgentSessionStoreEntries, |
| 7 | + resolveSessionTranscriptsDirForAgent, |
| 8 | +} from "../config/sessions.js"; |
| 9 | +import { callGateway, isGatewayTransportError } from "../gateway/call.js"; |
8 | 10 | import { DEFAULT_AGENT_ID, normalizeAgentId } from "../routing/session-key.js"; |
9 | 11 | import { type RuntimeEnv, writeRuntimeJson } from "../runtime.js"; |
10 | 12 | import { defaultRuntime } from "../runtime.js"; |
11 | | -import { lowercasePreservingWhitespace } from "../shared/string-coerce.js"; |
| 13 | +import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../utils/message-channel.js"; |
12 | 14 | import { createClackPrompter } from "../wizard/clack-prompter.js"; |
13 | | -import { |
14 | | - createQuietRuntime, |
15 | | - purgeAgentSessionStoreEntries, |
16 | | - requireValidConfigFileSnapshot, |
17 | | -} from "./agents.command-shared.js"; |
| 15 | +import { createQuietRuntime, requireValidConfigFileSnapshot } from "./agents.command-shared.js"; |
18 | 16 | import { findAgentEntryIndex, listAgentEntries, pruneAgentConfig } from "./agents.config.js"; |
19 | 17 | import { moveToTrash } from "./onboard-helpers.js"; |
20 | 18 |
|
21 | | -function normalizeWorkspacePathForComparison(input: string): string { |
22 | | - const resolved = path.resolve(input.replaceAll("\0", "")); |
23 | | - let normalized = resolved; |
24 | | - try { |
25 | | - normalized = fs.realpathSync.native(resolved); |
26 | | - } catch { |
27 | | - // Keep lexical path for non-existent directories. |
28 | | - } |
29 | | - if (process.platform === "win32") { |
30 | | - return lowercasePreservingWhitespace(normalized); |
31 | | - } |
32 | | - return normalized; |
33 | | -} |
34 | | - |
35 | | -function isPathWithinRoot(candidatePath: string, rootPath: string): boolean { |
36 | | - const relative = path.relative(rootPath, candidatePath); |
37 | | - return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative)); |
38 | | -} |
39 | | - |
40 | | -function workspacePathsOverlap(left: string, right: string): boolean { |
41 | | - const normalizedLeft = normalizeWorkspacePathForComparison(left); |
42 | | - const normalizedRight = normalizeWorkspacePathForComparison(right); |
43 | | - return ( |
44 | | - isPathWithinRoot(normalizedLeft, normalizedRight) || |
45 | | - isPathWithinRoot(normalizedRight, normalizedLeft) |
46 | | - ); |
47 | | -} |
48 | | - |
49 | | -function findOverlappingWorkspaceAgentIds( |
50 | | - cfg: OpenClawConfig, |
51 | | - agentId: string, |
52 | | - workspaceDir: string, |
53 | | -): string[] { |
54 | | - const entries = listAgentEntries(cfg); |
55 | | - const normalizedAgentId = normalizeAgentId(agentId); |
56 | | - const overlappingAgentIds: string[] = []; |
57 | | - for (const entry of entries) { |
58 | | - const otherAgentId = normalizeAgentId(entry.id); |
59 | | - if (otherAgentId === normalizedAgentId) { |
60 | | - continue; |
61 | | - } |
62 | | - const otherWorkspace = resolveAgentWorkspaceDir(cfg, otherAgentId); |
63 | | - if (workspacePathsOverlap(workspaceDir, otherWorkspace)) { |
64 | | - overlappingAgentIds.push(otherAgentId); |
65 | | - } |
66 | | - } |
67 | | - return overlappingAgentIds; |
68 | | -} |
69 | | - |
70 | 19 | type AgentsDeleteOptions = { |
71 | 20 | id: string; |
72 | 21 | force?: boolean; |
73 | 22 | json?: boolean; |
74 | 23 | }; |
75 | 24 |
|
| 25 | +type AgentsDeleteGatewayResult = { |
| 26 | + ok: true; |
| 27 | + agentId: string; |
| 28 | + removedBindings: number; |
| 29 | +}; |
| 30 | + |
| 31 | +async function maybeDeleteAgentThroughGateway(params: { |
| 32 | + agentId: string; |
| 33 | + deleteFiles: boolean; |
| 34 | +}): Promise<AgentsDeleteGatewayResult | null> { |
| 35 | + try { |
| 36 | + return await callGateway<AgentsDeleteGatewayResult>({ |
| 37 | + method: "agents.delete", |
| 38 | + params: { |
| 39 | + agentId: params.agentId, |
| 40 | + deleteFiles: params.deleteFiles, |
| 41 | + }, |
| 42 | + mode: GATEWAY_CLIENT_MODES.CLI, |
| 43 | + clientName: GATEWAY_CLIENT_NAMES.CLI, |
| 44 | + requiredMethods: ["agents.delete"], |
| 45 | + }); |
| 46 | + } catch (error) { |
| 47 | + if (isGatewayTransportError(error)) { |
| 48 | + return null; |
| 49 | + } |
| 50 | + throw error; |
| 51 | + } |
| 52 | +} |
| 53 | + |
76 | 54 | export async function agentsDeleteCommand( |
77 | 55 | opts: AgentsDeleteOptions, |
78 | 56 | runtime: RuntimeEnv = defaultRuntime, |
@@ -127,8 +105,34 @@ export async function agentsDeleteCommand( |
127 | 105 | const workspaceDir = resolveAgentWorkspaceDir(cfg, agentId); |
128 | 106 | const agentDir = resolveAgentDir(cfg, agentId); |
129 | 107 | const sessionsDir = resolveSessionTranscriptsDirForAgent(agentId); |
130 | | - |
131 | 108 | const result = pruneAgentConfig(cfg, agentId); |
| 109 | + |
| 110 | + const gatewayResult = await maybeDeleteAgentThroughGateway({ |
| 111 | + agentId, |
| 112 | + deleteFiles: true, |
| 113 | + }); |
| 114 | + if (gatewayResult) { |
| 115 | + const workspaceSharedWith = findOverlappingWorkspaceAgentIds(cfg, agentId, workspaceDir); |
| 116 | + const workspaceRetained = workspaceSharedWith.length > 0; |
| 117 | + if (opts.json) { |
| 118 | + writeRuntimeJson(runtime, { |
| 119 | + agentId, |
| 120 | + workspace: workspaceDir, |
| 121 | + workspaceRetained: workspaceRetained || undefined, |
| 122 | + workspaceRetainedReason: workspaceRetained ? "shared" : undefined, |
| 123 | + workspaceSharedWith: workspaceRetained ? workspaceSharedWith : undefined, |
| 124 | + agentDir, |
| 125 | + sessionsDir, |
| 126 | + removedBindings: gatewayResult.removedBindings, |
| 127 | + removedAllow: result.removedAllow, |
| 128 | + transport: "gateway", |
| 129 | + }); |
| 130 | + } else { |
| 131 | + runtime.log(`Deleted agent: ${agentId}`); |
| 132 | + } |
| 133 | + return; |
| 134 | + } |
| 135 | + |
132 | 136 | await replaceConfigFile({ |
133 | 137 | nextConfig: result.config, |
134 | 138 | ...(baseHash !== undefined ? { baseHash } : {}), |
|
0 commit comments