@poolzin/pool-bot 2026.2.0 → 2026.2.2
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/CHANGELOG.md +118 -0
- package/README-header.png +0 -0
- package/dist/agents/bash-tools.exec.js +76 -25
- package/dist/agents/cli-runner/helpers.js +9 -11
- package/dist/agents/context.js +1 -1
- package/dist/agents/identity.js +47 -7
- package/dist/agents/memory-search.js +25 -8
- package/dist/agents/model-catalog.js +1 -1
- package/dist/agents/model-selection.js +21 -0
- package/dist/agents/pi-embedded-block-chunker.js +117 -42
- package/dist/agents/pi-embedded-helpers/errors.js +183 -78
- package/dist/agents/pi-embedded-helpers.js +1 -1
- package/dist/agents/pi-embedded-runner/compact.js +8 -10
- package/dist/agents/pi-embedded-runner/model.js +62 -3
- package/dist/agents/pi-embedded-runner/run/attempt.js +21 -11
- package/dist/agents/pi-embedded-runner/run.js +199 -46
- package/dist/agents/pi-embedded-runner/system-prompt.js +10 -2
- package/dist/agents/pi-embedded-subscribe.js +118 -29
- package/dist/agents/pi-tools.js +10 -5
- package/dist/agents/poolbot-tools.js +15 -10
- package/dist/agents/sandbox-paths.js +31 -0
- package/dist/agents/session-tool-result-guard.js +94 -15
- package/dist/agents/shell-utils.js +51 -0
- package/dist/agents/skills/bundled-context.js +23 -0
- package/dist/agents/skills/bundled-dir.js +41 -7
- package/dist/agents/skills-install.js +60 -23
- package/dist/agents/subagent-announce.js +79 -34
- package/dist/agents/tool-policy.conformance.js +14 -0
- package/dist/agents/tool-policy.js +24 -0
- package/dist/agents/tools/cron-tool.js +166 -19
- package/dist/agents/tools/discord-actions-presence.js +78 -0
- package/dist/agents/tools/image-tool.js +1 -1
- package/dist/agents/tools/message-tool.js +56 -2
- package/dist/agents/tools/sessions-history-tool.js +69 -1
- package/dist/agents/tools/web-search.js +211 -42
- package/dist/agents/usage.js +23 -1
- package/dist/agents/workspace-run.js +67 -0
- package/dist/agents/workspace-templates.js +44 -0
- package/dist/auto-reply/command-auth.js +121 -6
- package/dist/auto-reply/envelope.js +74 -82
- package/dist/auto-reply/reply/commands-compact.js +1 -0
- package/dist/auto-reply/reply/commands-context-report.js +1 -0
- package/dist/auto-reply/reply/commands-context.js +1 -0
- package/dist/auto-reply/reply/commands-models.js +107 -60
- package/dist/auto-reply/reply/commands-ptt.js +171 -0
- package/dist/auto-reply/reply/get-reply-run.js +2 -1
- package/dist/auto-reply/reply/inbound-context.js +5 -1
- package/dist/auto-reply/reply/mentions.js +1 -1
- package/dist/auto-reply/reply/model-selection.js +3 -3
- package/dist/auto-reply/thinking.js +88 -43
- package/dist/browser/bridge-server.js +13 -0
- package/dist/browser/cdp.helpers.js +38 -24
- package/dist/browser/client-fetch.js +50 -7
- package/dist/browser/config.js +1 -10
- package/dist/browser/extension-relay.js +101 -40
- package/dist/browser/pw-ai.js +1 -1
- package/dist/browser/pw-session.js +143 -8
- package/dist/browser/pw-tools-core.interactions.js +125 -27
- package/dist/browser/pw-tools-core.responses.js +1 -1
- package/dist/browser/pw-tools-core.state.js +1 -1
- package/dist/browser/routes/agent.act.js +86 -41
- package/dist/browser/routes/dispatcher.js +4 -4
- package/dist/browser/screenshot.js +1 -1
- package/dist/browser/server.js +13 -0
- package/dist/build-info.json +3 -3
- package/dist/canvas-host/a2ui/index.html +28 -28
- package/dist/channels/reply-prefix.js +8 -1
- package/dist/cli/cron-cli/register.cron-add.js +61 -40
- package/dist/cli/cron-cli/register.cron-edit.js +60 -34
- package/dist/cli/cron-cli/shared.js +56 -41
- package/dist/cli/dns-cli.js +26 -14
- package/dist/cli/gateway-cli/register.js +37 -19
- package/dist/cli/memory-cli.js +5 -5
- package/dist/cli/parse-bytes.js +37 -0
- package/dist/cli/update-cli.js +173 -52
- package/dist/commands/agent.js +1 -0
- package/dist/commands/auth-choice.apply.oauth.js +1 -1
- package/dist/commands/doctor-config-flow.js +61 -5
- package/dist/commands/doctor-state-migrations.js +1 -1
- package/dist/commands/health.js +1 -1
- package/dist/commands/model-allowlist.js +29 -0
- package/dist/commands/model-picker.js +2 -1
- package/dist/commands/models/list.registry.js +1 -1
- package/dist/commands/models/list.status-command.js +43 -23
- package/dist/commands/models/shared.js +15 -0
- package/dist/commands/onboard-custom.js +384 -0
- package/dist/commands/onboard-non-interactive/local/auth-choice-inference.js +35 -0
- package/dist/commands/onboard-non-interactive/local/auth-choice.js +6 -3
- package/dist/commands/onboard-skills.js +63 -38
- package/dist/commands/openai-model-default.js +41 -0
- package/dist/compat/legacy-names.js +2 -0
- package/dist/config/defaults.js +3 -2
- package/dist/config/paths.js +136 -35
- package/dist/config/plugin-auto-enable.js +21 -5
- package/dist/config/redact-snapshot.js +153 -0
- package/dist/config/schema.field-metadata.js +590 -0
- package/dist/config/schema.js +2 -2
- package/dist/config/sessions/store.js +291 -23
- package/dist/config/zod-schema.agent-defaults.js +3 -0
- package/dist/config/zod-schema.agent-runtime.js +13 -2
- package/dist/config/zod-schema.providers-core.js +142 -0
- package/dist/config/zod-schema.session.js +3 -0
- package/dist/control-ui/assets/{index-CIRDm-Lu.css → index-CSfXd2LO.css} +1 -1
- package/dist/control-ui/assets/{index-CmNMuoem.js → index-HRr1grwl.js} +446 -413
- package/dist/control-ui/assets/index-HRr1grwl.js.map +1 -0
- package/dist/control-ui/index.html +4 -4
- package/dist/cron/delivery.js +57 -0
- package/dist/cron/isolated-agent/delivery-target.js +18 -3
- package/dist/cron/isolated-agent/helpers.js +22 -5
- package/dist/cron/isolated-agent/run.js +172 -63
- package/dist/cron/isolated-agent/session.js +2 -0
- package/dist/cron/normalize.js +356 -28
- package/dist/cron/parse.js +10 -5
- package/dist/cron/run-log.js +35 -10
- package/dist/cron/schedule.js +41 -6
- package/dist/cron/service/jobs.js +208 -35
- package/dist/cron/service/ops.js +72 -16
- package/dist/cron/service/state.js +2 -0
- package/dist/cron/service/store.js +386 -14
- package/dist/cron/service/timer.js +390 -147
- package/dist/cron/session-reaper.js +86 -0
- package/dist/cron/store.js +23 -8
- package/dist/cron/validate-timestamp.js +43 -0
- package/dist/discord/monitor/agent-components.js +438 -0
- package/dist/discord/monitor/allow-list.js +28 -5
- package/dist/discord/monitor/gateway-registry.js +29 -0
- package/dist/discord/monitor/native-command.js +44 -23
- package/dist/discord/monitor/sender-identity.js +45 -0
- package/dist/discord/pluralkit.js +27 -0
- package/dist/discord/send.outbound.js +92 -5
- package/dist/discord/send.shared.js +60 -23
- package/dist/discord/targets.js +84 -1
- package/dist/entry.js +15 -9
- package/dist/extensionAPI.js +8 -0
- package/dist/gateway/control-ui.js +8 -1
- package/dist/gateway/hooks-mapping.js +3 -0
- package/dist/gateway/hooks.js +65 -0
- package/dist/gateway/net.js +96 -31
- package/dist/gateway/node-command-policy.js +50 -15
- package/dist/gateway/origin-check.js +56 -0
- package/dist/gateway/protocol/client-info.js +9 -0
- package/dist/gateway/protocol/index.js +9 -2
- package/dist/gateway/protocol/schema/agents-models-skills.js +71 -1
- package/dist/gateway/protocol/schema/cron.js +22 -10
- package/dist/gateway/protocol/schema/protocol-schemas.js +16 -2
- package/dist/gateway/protocol/schema/sessions.js +12 -0
- package/dist/gateway/server/hooks.js +1 -1
- package/dist/gateway/server-broadcast.js +26 -9
- package/dist/gateway/server-chat.js +112 -23
- package/dist/gateway/server-discovery-runtime.js +10 -2
- package/dist/gateway/server-http.js +109 -11
- package/dist/gateway/server-methods/agent-timestamp.js +60 -0
- package/dist/gateway/server-methods/agents.js +321 -2
- package/dist/gateway/server-methods/usage.js +559 -16
- package/dist/gateway/server-runtime-state.js +22 -8
- package/dist/gateway/server-startup-memory.js +16 -0
- package/dist/gateway/server.impl.js +5 -1
- package/dist/gateway/session-utils.fs.js +23 -25
- package/dist/gateway/session-utils.js +20 -10
- package/dist/gateway/sessions-patch.js +7 -22
- package/dist/gateway/test-helpers.mocks.js +11 -7
- package/dist/gateway/test-helpers.server.js +35 -2
- package/dist/imessage/constants.js +2 -0
- package/dist/imessage/monitor/deliver.js +4 -1
- package/dist/imessage/monitor/monitor-provider.js +51 -1
- package/dist/infra/bonjour-discovery.js +131 -70
- package/dist/infra/control-ui-assets.js +134 -12
- package/dist/infra/errors.js +12 -0
- package/dist/infra/exec-approvals.js +266 -57
- package/dist/infra/format-time/format-datetime.js +79 -0
- package/dist/infra/format-time/format-duration.js +81 -0
- package/dist/infra/format-time/format-relative.js +80 -0
- package/dist/infra/heartbeat-runner.js +140 -49
- package/dist/infra/home-dir.js +54 -0
- package/dist/infra/net/fetch-guard.js +122 -0
- package/dist/infra/net/ssrf.js +65 -29
- package/dist/infra/outbound/abort.js +14 -0
- package/dist/infra/outbound/message-action-runner.js +77 -13
- package/dist/infra/outbound/outbound-session.js +143 -37
- package/dist/infra/poolbot-root.js +43 -1
- package/dist/infra/session-cost-usage.js +631 -41
- package/dist/infra/state-migrations.js +317 -47
- package/dist/infra/update-global.js +35 -0
- package/dist/infra/update-runner.js +149 -43
- package/dist/infra/warning-filter.js +65 -0
- package/dist/infra/widearea-dns.js +30 -9
- package/dist/logging/redact-identifier.js +12 -0
- package/dist/media/fetch.js +81 -58
- package/dist/media/store.js +2 -0
- package/dist/media-understanding/apply.js +403 -3
- package/dist/media-understanding/attachments.js +38 -27
- package/dist/media-understanding/defaults.js +16 -0
- package/dist/media-understanding/providers/deepgram/audio.js +22 -14
- package/dist/media-understanding/providers/google/audio.js +24 -17
- package/dist/media-understanding/providers/google/video.js +24 -17
- package/dist/media-understanding/providers/image.js +3 -3
- package/dist/media-understanding/providers/index.js +4 -1
- package/dist/media-understanding/providers/openai/audio.js +22 -14
- package/dist/media-understanding/providers/shared.js +16 -11
- package/dist/media-understanding/providers/zai/index.js +6 -0
- package/dist/media-understanding/runner.js +158 -90
- package/dist/memory/batch-voyage.js +277 -0
- package/dist/memory/embeddings-voyage.js +75 -0
- package/dist/memory/embeddings.js +28 -16
- package/dist/memory/internal.js +101 -18
- package/dist/memory/manager.js +154 -48
- package/dist/memory/search-manager.js +173 -0
- package/dist/memory/session-files.js +9 -3
- package/dist/node-host/runner.js +34 -24
- package/dist/node-host/with-timeout.js +27 -0
- package/dist/plugins/commands.js +5 -1
- package/dist/plugins/config-state.js +86 -7
- package/dist/plugins/source-display.js +51 -0
- package/dist/process/exec.js +20 -2
- package/dist/routing/resolve-route.js +12 -0
- package/dist/routing/session-key.js +15 -0
- package/dist/runtime.js +2 -0
- package/dist/security/audit-extra.async.js +601 -0
- package/dist/security/audit-extra.js +2 -830
- package/dist/security/audit-extra.sync.js +505 -0
- package/dist/security/channel-metadata.js +34 -0
- package/dist/security/external-content.js +88 -6
- package/dist/security/skill-scanner.js +330 -0
- package/dist/sessions/session-key-utils.js +7 -0
- package/dist/signal/monitor/event-handler.js +80 -1
- package/dist/slack/monitor/media.js +85 -15
- package/dist/tailscale/detect.js +1 -2
- package/dist/telegram/bot/helpers.js +109 -28
- package/dist/telegram/bot-handlers.js +144 -3
- package/dist/telegram/bot-message-context.js +37 -10
- package/dist/telegram/bot-message-dispatch.js +54 -17
- package/dist/telegram/bot-native-commands.js +86 -29
- package/dist/telegram/bot.js +30 -29
- package/dist/telegram/model-buttons.js +163 -0
- package/dist/telegram/monitor.js +110 -85
- package/dist/telegram/send.js +129 -47
- package/dist/terminal/restore.js +45 -0
- package/dist/test-helpers/state-dir-env.js +16 -0
- package/dist/tts/tts.js +12 -6
- package/dist/tui/tui-session-actions.js +166 -54
- package/dist/utils/fetch-timeout.js +20 -0
- package/dist/utils/normalize-secret-input.js +19 -0
- package/dist/utils/transcript-tools.js +58 -0
- package/dist/utils.js +45 -14
- package/dist/version.js +42 -5
- package/dist/wizard/clack-prompter.js +9 -6
- package/extensions/googlechat/node_modules/.bin/poolbot +21 -0
- package/extensions/googlechat/package.json +2 -2
- package/extensions/line/node_modules/.bin/poolbot +21 -0
- package/extensions/line/package.json +1 -1
- package/extensions/matrix/node_modules/.bin/poolbot +21 -0
- package/extensions/matrix/package.json +1 -1
- package/extensions/memory-core/node_modules/.bin/poolbot +21 -0
- package/extensions/memory-core/package.json +4 -1
- package/extensions/twitch/node_modules/.bin/poolbot +21 -0
- package/extensions/twitch/package.json +1 -1
- package/package.json +183 -24
- package/dist/control-ui/assets/index-CmNMuoem.js.map +0 -1
|
@@ -2,56 +2,67 @@ import fs from "node:fs";
|
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { resolveDefaultAgentId } from "../agents/agent-scope.js";
|
|
5
|
-
import { resolveOAuthDir, resolveStateDir } from "../config/paths.js";
|
|
5
|
+
import { resolveLegacyStateDirs, resolveNewStateDir, resolveOAuthDir, resolveStateDir, } from "../config/paths.js";
|
|
6
6
|
import { saveSessionStore } from "../config/sessions.js";
|
|
7
|
+
import { canonicalizeMainSessionAlias } from "../config/sessions/main-session.js";
|
|
7
8
|
import { createSubsystemLogger } from "../logging/subsystem.js";
|
|
8
9
|
import { buildAgentMainSessionKey, DEFAULT_ACCOUNT_ID, DEFAULT_MAIN_KEY, normalizeAgentId, } from "../routing/session-key.js";
|
|
9
|
-
import { canonicalizeMainSessionAlias } from "../config/sessions/main-session.js";
|
|
10
10
|
import { ensureDir, existsDir, fileExists, isLegacyWhatsAppAuthFile, readSessionStoreJson5, safeReadDir, } from "./state-migrations.fs.js";
|
|
11
11
|
let autoMigrateChecked = false;
|
|
12
|
+
let autoMigrateStateDirChecked = false;
|
|
12
13
|
function isSurfaceGroupKey(key) {
|
|
13
14
|
return key.includes(":group:") || key.includes(":channel:");
|
|
14
15
|
}
|
|
15
16
|
function isLegacyGroupKey(key) {
|
|
16
17
|
const trimmed = key.trim();
|
|
17
|
-
if (!trimmed)
|
|
18
|
+
if (!trimmed) {
|
|
18
19
|
return false;
|
|
19
|
-
|
|
20
|
+
}
|
|
21
|
+
if (trimmed.startsWith("group:")) {
|
|
20
22
|
return true;
|
|
23
|
+
}
|
|
21
24
|
const lower = trimmed.toLowerCase();
|
|
22
|
-
if (!lower.includes("@g.us"))
|
|
25
|
+
if (!lower.includes("@g.us")) {
|
|
23
26
|
return false;
|
|
27
|
+
}
|
|
24
28
|
// Legacy WhatsApp group keys: bare JID or "whatsapp:<jid>" without explicit ":group:" kind.
|
|
25
|
-
if (!trimmed.includes(":"))
|
|
29
|
+
if (!trimmed.includes(":")) {
|
|
26
30
|
return true;
|
|
27
|
-
|
|
31
|
+
}
|
|
32
|
+
if (lower.startsWith("whatsapp:") && !trimmed.includes(":group:")) {
|
|
28
33
|
return true;
|
|
34
|
+
}
|
|
29
35
|
return false;
|
|
30
36
|
}
|
|
31
37
|
function canonicalizeSessionKeyForAgent(params) {
|
|
32
38
|
const agentId = normalizeAgentId(params.agentId);
|
|
33
39
|
const raw = params.key.trim();
|
|
34
|
-
if (!raw)
|
|
40
|
+
if (!raw) {
|
|
35
41
|
return raw;
|
|
36
|
-
|
|
42
|
+
}
|
|
43
|
+
if (raw.toLowerCase() === "global" || raw.toLowerCase() === "unknown") {
|
|
37
44
|
return raw.toLowerCase();
|
|
45
|
+
}
|
|
38
46
|
const canonicalMain = canonicalizeMainSessionAlias({
|
|
39
47
|
cfg: { session: { scope: params.scope, mainKey: params.mainKey } },
|
|
40
48
|
agentId,
|
|
41
49
|
sessionKey: raw,
|
|
42
50
|
});
|
|
43
|
-
if (canonicalMain !== raw)
|
|
51
|
+
if (canonicalMain !== raw) {
|
|
44
52
|
return canonicalMain.toLowerCase();
|
|
45
|
-
|
|
53
|
+
}
|
|
54
|
+
if (raw.toLowerCase().startsWith("agent:")) {
|
|
46
55
|
return raw.toLowerCase();
|
|
56
|
+
}
|
|
47
57
|
if (raw.toLowerCase().startsWith("subagent:")) {
|
|
48
58
|
const rest = raw.slice("subagent:".length);
|
|
49
59
|
return `agent:${agentId}:subagent:${rest}`.toLowerCase();
|
|
50
60
|
}
|
|
51
61
|
if (raw.startsWith("group:")) {
|
|
52
62
|
const id = raw.slice("group:".length).trim();
|
|
53
|
-
if (!id)
|
|
63
|
+
if (!id) {
|
|
54
64
|
return raw;
|
|
65
|
+
}
|
|
55
66
|
const channel = id.toLowerCase().includes("@g.us") ? "whatsapp" : "unknown";
|
|
56
67
|
return `agent:${agentId}:${channel}:group:${id}`.toLowerCase();
|
|
57
68
|
}
|
|
@@ -74,19 +85,25 @@ function pickLatestLegacyDirectEntry(store) {
|
|
|
74
85
|
let best = null;
|
|
75
86
|
let bestUpdated = -1;
|
|
76
87
|
for (const [key, entry] of Object.entries(store)) {
|
|
77
|
-
if (!entry || typeof entry !== "object")
|
|
88
|
+
if (!entry || typeof entry !== "object") {
|
|
78
89
|
continue;
|
|
90
|
+
}
|
|
79
91
|
const normalized = key.trim();
|
|
80
|
-
if (!normalized)
|
|
92
|
+
if (!normalized) {
|
|
81
93
|
continue;
|
|
82
|
-
|
|
94
|
+
}
|
|
95
|
+
if (normalized === "global") {
|
|
83
96
|
continue;
|
|
84
|
-
|
|
97
|
+
}
|
|
98
|
+
if (normalized.startsWith("agent:")) {
|
|
85
99
|
continue;
|
|
86
|
-
|
|
100
|
+
}
|
|
101
|
+
if (normalized.toLowerCase().startsWith("subagent:")) {
|
|
87
102
|
continue;
|
|
88
|
-
|
|
103
|
+
}
|
|
104
|
+
if (isLegacyGroupKey(normalized) || isSurfaceGroupKey(normalized)) {
|
|
89
105
|
continue;
|
|
106
|
+
}
|
|
90
107
|
const updatedAt = typeof entry.updatedAt === "number" ? entry.updatedAt : 0;
|
|
91
108
|
if (updatedAt > bestUpdated) {
|
|
92
109
|
bestUpdated = updatedAt;
|
|
@@ -97,8 +114,9 @@ function pickLatestLegacyDirectEntry(store) {
|
|
|
97
114
|
}
|
|
98
115
|
function normalizeSessionEntry(entry) {
|
|
99
116
|
const sessionId = typeof entry.sessionId === "string" ? entry.sessionId : null;
|
|
100
|
-
if (!sessionId)
|
|
117
|
+
if (!sessionId) {
|
|
101
118
|
return null;
|
|
119
|
+
}
|
|
102
120
|
const updatedAt = typeof entry.updatedAt === "number" && Number.isFinite(entry.updatedAt)
|
|
103
121
|
? entry.updatedAt
|
|
104
122
|
: Date.now();
|
|
@@ -116,14 +134,17 @@ function resolveUpdatedAt(entry) {
|
|
|
116
134
|
: 0;
|
|
117
135
|
}
|
|
118
136
|
function mergeSessionEntry(params) {
|
|
119
|
-
if (!params.existing)
|
|
137
|
+
if (!params.existing) {
|
|
120
138
|
return params.incoming;
|
|
139
|
+
}
|
|
121
140
|
const existingUpdated = resolveUpdatedAt(params.existing);
|
|
122
141
|
const incomingUpdated = resolveUpdatedAt(params.incoming);
|
|
123
|
-
if (incomingUpdated > existingUpdated)
|
|
142
|
+
if (incomingUpdated > existingUpdated) {
|
|
124
143
|
return params.incoming;
|
|
125
|
-
|
|
144
|
+
}
|
|
145
|
+
if (incomingUpdated < existingUpdated) {
|
|
126
146
|
return params.existing;
|
|
147
|
+
}
|
|
127
148
|
return params.preferIncomingOnTie ? params.incoming : params.existing;
|
|
128
149
|
}
|
|
129
150
|
function canonicalizeSessionStore(params) {
|
|
@@ -131,8 +152,9 @@ function canonicalizeSessionStore(params) {
|
|
|
131
152
|
const meta = new Map();
|
|
132
153
|
const legacyKeys = [];
|
|
133
154
|
for (const [key, entry] of Object.entries(params.store)) {
|
|
134
|
-
if (!entry || typeof entry !== "object")
|
|
155
|
+
if (!entry || typeof entry !== "object") {
|
|
135
156
|
continue;
|
|
157
|
+
}
|
|
136
158
|
const canonicalKey = canonicalizeSessionKeyForAgent({
|
|
137
159
|
key,
|
|
138
160
|
agentId: params.agentId,
|
|
@@ -140,8 +162,9 @@ function canonicalizeSessionStore(params) {
|
|
|
140
162
|
scope: params.scope,
|
|
141
163
|
});
|
|
142
164
|
const isCanonical = canonicalKey === key;
|
|
143
|
-
if (!isCanonical)
|
|
165
|
+
if (!isCanonical) {
|
|
144
166
|
legacyKeys.push(key);
|
|
167
|
+
}
|
|
145
168
|
const existing = canonical[canonicalKey];
|
|
146
169
|
if (!existing) {
|
|
147
170
|
canonical[canonicalKey] = entry;
|
|
@@ -156,10 +179,12 @@ function canonicalizeSessionStore(params) {
|
|
|
156
179
|
meta.set(canonicalKey, { isCanonical, updatedAt: incomingUpdated });
|
|
157
180
|
continue;
|
|
158
181
|
}
|
|
159
|
-
if (incomingUpdated < existingUpdated)
|
|
182
|
+
if (incomingUpdated < existingUpdated) {
|
|
160
183
|
continue;
|
|
161
|
-
|
|
184
|
+
}
|
|
185
|
+
if (existingMeta?.isCanonical && !isCanonical) {
|
|
162
186
|
continue;
|
|
187
|
+
}
|
|
163
188
|
if (!existingMeta?.isCanonical && isCanonical) {
|
|
164
189
|
canonical[canonicalKey] = entry;
|
|
165
190
|
meta.set(canonicalKey, { isCanonical, updatedAt: incomingUpdated });
|
|
@@ -177,21 +202,25 @@ function listLegacySessionKeys(params) {
|
|
|
177
202
|
mainKey: params.mainKey,
|
|
178
203
|
scope: params.scope,
|
|
179
204
|
});
|
|
180
|
-
if (canonical !== key)
|
|
205
|
+
if (canonical !== key) {
|
|
181
206
|
legacy.push(key);
|
|
207
|
+
}
|
|
182
208
|
}
|
|
183
209
|
return legacy;
|
|
184
210
|
}
|
|
185
211
|
function emptyDirOrMissing(dir) {
|
|
186
|
-
if (!existsDir(dir))
|
|
212
|
+
if (!existsDir(dir)) {
|
|
187
213
|
return true;
|
|
214
|
+
}
|
|
188
215
|
return safeReadDir(dir).length === 0;
|
|
189
216
|
}
|
|
190
217
|
function removeDirIfEmpty(dir) {
|
|
191
|
-
if (!existsDir(dir))
|
|
218
|
+
if (!existsDir(dir)) {
|
|
192
219
|
return;
|
|
193
|
-
|
|
220
|
+
}
|
|
221
|
+
if (!emptyDirOrMissing(dir)) {
|
|
194
222
|
return;
|
|
223
|
+
}
|
|
195
224
|
try {
|
|
196
225
|
fs.rmdirSync(dir);
|
|
197
226
|
}
|
|
@@ -205,6 +234,218 @@ export function resetAutoMigrateLegacyStateForTest() {
|
|
|
205
234
|
export function resetAutoMigrateLegacyAgentDirForTest() {
|
|
206
235
|
resetAutoMigrateLegacyStateForTest();
|
|
207
236
|
}
|
|
237
|
+
export function resetAutoMigrateLegacyStateDirForTest() {
|
|
238
|
+
autoMigrateStateDirChecked = false;
|
|
239
|
+
}
|
|
240
|
+
function resolveSymlinkTarget(linkPath) {
|
|
241
|
+
try {
|
|
242
|
+
const target = fs.readlinkSync(linkPath);
|
|
243
|
+
return path.resolve(path.dirname(linkPath), target);
|
|
244
|
+
}
|
|
245
|
+
catch {
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
function formatStateDirMigration(legacyDir, targetDir) {
|
|
250
|
+
return `State dir: ${legacyDir} → ${targetDir} (legacy path now symlinked)`;
|
|
251
|
+
}
|
|
252
|
+
function isDirPath(filePath) {
|
|
253
|
+
try {
|
|
254
|
+
return fs.statSync(filePath).isDirectory();
|
|
255
|
+
}
|
|
256
|
+
catch {
|
|
257
|
+
return false;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
function isWithinDir(targetPath, rootDir) {
|
|
261
|
+
const relative = path.relative(path.resolve(rootDir), path.resolve(targetPath));
|
|
262
|
+
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
|
|
263
|
+
}
|
|
264
|
+
function isLegacyTreeSymlinkMirror(currentDir, realTargetDir) {
|
|
265
|
+
let entries;
|
|
266
|
+
try {
|
|
267
|
+
entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
|
268
|
+
}
|
|
269
|
+
catch {
|
|
270
|
+
return false;
|
|
271
|
+
}
|
|
272
|
+
if (entries.length === 0) {
|
|
273
|
+
return false;
|
|
274
|
+
}
|
|
275
|
+
for (const entry of entries) {
|
|
276
|
+
const entryPath = path.join(currentDir, entry.name);
|
|
277
|
+
let stat;
|
|
278
|
+
try {
|
|
279
|
+
stat = fs.lstatSync(entryPath);
|
|
280
|
+
}
|
|
281
|
+
catch {
|
|
282
|
+
return false;
|
|
283
|
+
}
|
|
284
|
+
if (stat.isSymbolicLink()) {
|
|
285
|
+
const resolvedTarget = resolveSymlinkTarget(entryPath);
|
|
286
|
+
if (!resolvedTarget) {
|
|
287
|
+
return false;
|
|
288
|
+
}
|
|
289
|
+
let resolvedRealTarget;
|
|
290
|
+
try {
|
|
291
|
+
resolvedRealTarget = fs.realpathSync(resolvedTarget);
|
|
292
|
+
}
|
|
293
|
+
catch {
|
|
294
|
+
return false;
|
|
295
|
+
}
|
|
296
|
+
if (!isWithinDir(resolvedRealTarget, realTargetDir)) {
|
|
297
|
+
return false;
|
|
298
|
+
}
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
if (stat.isDirectory()) {
|
|
302
|
+
if (!isLegacyTreeSymlinkMirror(entryPath, realTargetDir)) {
|
|
303
|
+
return false;
|
|
304
|
+
}
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
return false;
|
|
308
|
+
}
|
|
309
|
+
return true;
|
|
310
|
+
}
|
|
311
|
+
function isLegacyDirSymlinkMirror(legacyDir, targetDir) {
|
|
312
|
+
let realTargetDir;
|
|
313
|
+
try {
|
|
314
|
+
realTargetDir = fs.realpathSync(targetDir);
|
|
315
|
+
}
|
|
316
|
+
catch {
|
|
317
|
+
return false;
|
|
318
|
+
}
|
|
319
|
+
return isLegacyTreeSymlinkMirror(legacyDir, realTargetDir);
|
|
320
|
+
}
|
|
321
|
+
export async function autoMigrateLegacyStateDir(params) {
|
|
322
|
+
if (autoMigrateStateDirChecked) {
|
|
323
|
+
return { migrated: false, skipped: true, changes: [], warnings: [] };
|
|
324
|
+
}
|
|
325
|
+
autoMigrateStateDirChecked = true;
|
|
326
|
+
const env = params.env ?? process.env;
|
|
327
|
+
if (env.CLAWDBOT_STATE_DIR?.trim()) {
|
|
328
|
+
return { migrated: false, skipped: true, changes: [], warnings: [] };
|
|
329
|
+
}
|
|
330
|
+
const homedir = params.homedir ?? os.homedir;
|
|
331
|
+
const targetDir = resolveNewStateDir(homedir);
|
|
332
|
+
const legacyDirs = resolveLegacyStateDirs(homedir);
|
|
333
|
+
let legacyDir = legacyDirs.find((dir) => {
|
|
334
|
+
try {
|
|
335
|
+
return fs.existsSync(dir);
|
|
336
|
+
}
|
|
337
|
+
catch {
|
|
338
|
+
return false;
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
const warnings = [];
|
|
342
|
+
const changes = [];
|
|
343
|
+
let legacyStat = null;
|
|
344
|
+
try {
|
|
345
|
+
legacyStat = legacyDir ? fs.lstatSync(legacyDir) : null;
|
|
346
|
+
}
|
|
347
|
+
catch {
|
|
348
|
+
legacyStat = null;
|
|
349
|
+
}
|
|
350
|
+
if (!legacyStat) {
|
|
351
|
+
return { migrated: false, skipped: false, changes, warnings };
|
|
352
|
+
}
|
|
353
|
+
if (!legacyStat.isDirectory() && !legacyStat.isSymbolicLink()) {
|
|
354
|
+
warnings.push(`Legacy state path is not a directory: ${legacyDir}`);
|
|
355
|
+
return { migrated: false, skipped: false, changes, warnings };
|
|
356
|
+
}
|
|
357
|
+
let symlinkDepth = 0;
|
|
358
|
+
while (legacyStat.isSymbolicLink()) {
|
|
359
|
+
const legacyTarget = legacyDir ? resolveSymlinkTarget(legacyDir) : null;
|
|
360
|
+
if (!legacyTarget) {
|
|
361
|
+
warnings.push(`Legacy state dir is a symlink (${legacyDir ?? "unknown"}); could not resolve target.`);
|
|
362
|
+
return { migrated: false, skipped: false, changes, warnings };
|
|
363
|
+
}
|
|
364
|
+
if (path.resolve(legacyTarget) === path.resolve(targetDir)) {
|
|
365
|
+
return { migrated: false, skipped: false, changes, warnings };
|
|
366
|
+
}
|
|
367
|
+
if (legacyDirs.some((dir) => path.resolve(dir) === path.resolve(legacyTarget))) {
|
|
368
|
+
legacyDir = legacyTarget;
|
|
369
|
+
try {
|
|
370
|
+
legacyStat = fs.lstatSync(legacyDir);
|
|
371
|
+
}
|
|
372
|
+
catch {
|
|
373
|
+
legacyStat = null;
|
|
374
|
+
}
|
|
375
|
+
if (!legacyStat) {
|
|
376
|
+
warnings.push(`Legacy state dir missing after symlink resolution: ${legacyDir}`);
|
|
377
|
+
return { migrated: false, skipped: false, changes, warnings };
|
|
378
|
+
}
|
|
379
|
+
if (!legacyStat.isDirectory() && !legacyStat.isSymbolicLink()) {
|
|
380
|
+
warnings.push(`Legacy state path is not a directory: ${legacyDir}`);
|
|
381
|
+
return { migrated: false, skipped: false, changes, warnings };
|
|
382
|
+
}
|
|
383
|
+
symlinkDepth += 1;
|
|
384
|
+
if (symlinkDepth > 2) {
|
|
385
|
+
warnings.push(`Legacy state dir symlink chain too deep: ${legacyDir}`);
|
|
386
|
+
return { migrated: false, skipped: false, changes, warnings };
|
|
387
|
+
}
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
warnings.push(`Legacy state dir is a symlink (${legacyDir ?? "unknown"} → ${legacyTarget}); skipping auto-migration.`);
|
|
391
|
+
return { migrated: false, skipped: false, changes, warnings };
|
|
392
|
+
}
|
|
393
|
+
if (isDirPath(targetDir)) {
|
|
394
|
+
if (legacyDir && isLegacyDirSymlinkMirror(legacyDir, targetDir)) {
|
|
395
|
+
return { migrated: false, skipped: false, changes, warnings };
|
|
396
|
+
}
|
|
397
|
+
warnings.push(`State dir migration skipped: target already exists (${targetDir}). Remove or merge manually.`);
|
|
398
|
+
return { migrated: false, skipped: false, changes, warnings };
|
|
399
|
+
}
|
|
400
|
+
try {
|
|
401
|
+
if (!legacyDir) {
|
|
402
|
+
throw new Error("Legacy state dir not found");
|
|
403
|
+
}
|
|
404
|
+
fs.renameSync(legacyDir, targetDir);
|
|
405
|
+
}
|
|
406
|
+
catch (err) {
|
|
407
|
+
warnings.push(`Failed to move legacy state dir (${legacyDir ?? "unknown"} → ${targetDir}): ${String(err)}`);
|
|
408
|
+
return { migrated: false, skipped: false, changes, warnings };
|
|
409
|
+
}
|
|
410
|
+
try {
|
|
411
|
+
if (!legacyDir) {
|
|
412
|
+
throw new Error("Legacy state dir not found");
|
|
413
|
+
}
|
|
414
|
+
fs.symlinkSync(targetDir, legacyDir, "dir");
|
|
415
|
+
changes.push(formatStateDirMigration(legacyDir, targetDir));
|
|
416
|
+
}
|
|
417
|
+
catch (err) {
|
|
418
|
+
try {
|
|
419
|
+
if (process.platform === "win32") {
|
|
420
|
+
if (!legacyDir) {
|
|
421
|
+
throw new Error("Legacy state dir not found", { cause: err });
|
|
422
|
+
}
|
|
423
|
+
fs.symlinkSync(targetDir, legacyDir, "junction");
|
|
424
|
+
changes.push(formatStateDirMigration(legacyDir, targetDir));
|
|
425
|
+
}
|
|
426
|
+
else {
|
|
427
|
+
throw err;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
catch (fallbackErr) {
|
|
431
|
+
try {
|
|
432
|
+
if (!legacyDir) {
|
|
433
|
+
// oxlint-disable-next-line preserve-caught-error
|
|
434
|
+
throw new Error("Legacy state dir not found", { cause: fallbackErr });
|
|
435
|
+
}
|
|
436
|
+
fs.renameSync(targetDir, legacyDir);
|
|
437
|
+
warnings.push(`State dir migration rolled back (failed to link legacy path): ${String(fallbackErr)}`);
|
|
438
|
+
return { migrated: false, skipped: false, changes: [], warnings };
|
|
439
|
+
}
|
|
440
|
+
catch (rollbackErr) {
|
|
441
|
+
warnings.push(`State dir moved but failed to link legacy path (${legacyDir ?? "unknown"} → ${targetDir}): ${String(fallbackErr)}`);
|
|
442
|
+
warnings.push(`Rollback failed; set CLAWDBOT_STATE_DIR=${targetDir} to avoid split state: ${String(rollbackErr)}`);
|
|
443
|
+
changes.push(`State dir: ${legacyDir ?? "unknown"} → ${targetDir}`);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
return { migrated: changes.length > 0, skipped: false, changes, warnings };
|
|
448
|
+
}
|
|
208
449
|
export async function detectLegacyStateMigrations(params) {
|
|
209
450
|
const env = params.env ?? process.env;
|
|
210
451
|
const homedir = params.homedir ?? os.homedir;
|
|
@@ -283,8 +524,9 @@ export async function detectLegacyStateMigrations(params) {
|
|
|
283
524
|
async function migrateLegacySessions(detected, now) {
|
|
284
525
|
const changes = [];
|
|
285
526
|
const warnings = [];
|
|
286
|
-
if (!detected.sessions.hasLegacy)
|
|
527
|
+
if (!detected.sessions.hasLegacy) {
|
|
287
528
|
return { changes, warnings };
|
|
529
|
+
}
|
|
288
530
|
ensureDir(detected.sessions.targetDir);
|
|
289
531
|
const legacyParsed = fileExists(detected.sessions.legacyStorePath)
|
|
290
532
|
? readSessionStoreJson5(detected.sessions.legacyStorePath)
|
|
@@ -333,11 +575,14 @@ async function migrateLegacySessions(detected, now) {
|
|
|
333
575
|
const normalized = {};
|
|
334
576
|
for (const [key, entry] of Object.entries(merged)) {
|
|
335
577
|
const normalizedEntry = normalizeSessionEntry(entry);
|
|
336
|
-
if (!normalizedEntry)
|
|
578
|
+
if (!normalizedEntry) {
|
|
337
579
|
continue;
|
|
580
|
+
}
|
|
338
581
|
normalized[key] = normalizedEntry;
|
|
339
582
|
}
|
|
340
|
-
await saveSessionStore(detected.sessions.targetStorePath, normalized
|
|
583
|
+
await saveSessionStore(detected.sessions.targetStorePath, normalized, {
|
|
584
|
+
skipMaintenance: true,
|
|
585
|
+
});
|
|
341
586
|
changes.push(`Merged sessions store → ${detected.sessions.targetStorePath}`);
|
|
342
587
|
if (canonicalizedTarget.legacyKeys.length > 0) {
|
|
343
588
|
changes.push(`Canonicalized ${canonicalizedTarget.legacyKeys.length} legacy session key(s)`);
|
|
@@ -345,14 +590,17 @@ async function migrateLegacySessions(detected, now) {
|
|
|
345
590
|
}
|
|
346
591
|
const entries = safeReadDir(detected.sessions.legacyDir);
|
|
347
592
|
for (const entry of entries) {
|
|
348
|
-
if (!entry.isFile())
|
|
593
|
+
if (!entry.isFile()) {
|
|
349
594
|
continue;
|
|
350
|
-
|
|
595
|
+
}
|
|
596
|
+
if (entry.name === "sessions.json") {
|
|
351
597
|
continue;
|
|
598
|
+
}
|
|
352
599
|
const from = path.join(detected.sessions.legacyDir, entry.name);
|
|
353
600
|
const to = path.join(detected.sessions.targetDir, entry.name);
|
|
354
|
-
if (fileExists(to))
|
|
601
|
+
if (fileExists(to)) {
|
|
355
602
|
continue;
|
|
603
|
+
}
|
|
356
604
|
try {
|
|
357
605
|
fs.renameSync(from, to);
|
|
358
606
|
changes.push(`Moved ${entry.name} → agents/${detected.targetAgentId}/sessions`);
|
|
@@ -388,15 +636,17 @@ async function migrateLegacySessions(detected, now) {
|
|
|
388
636
|
export async function migrateLegacyAgentDir(detected, now) {
|
|
389
637
|
const changes = [];
|
|
390
638
|
const warnings = [];
|
|
391
|
-
if (!detected.agentDir.hasLegacy)
|
|
639
|
+
if (!detected.agentDir.hasLegacy) {
|
|
392
640
|
return { changes, warnings };
|
|
641
|
+
}
|
|
393
642
|
ensureDir(detected.agentDir.targetDir);
|
|
394
643
|
const entries = safeReadDir(detected.agentDir.legacyDir);
|
|
395
644
|
for (const entry of entries) {
|
|
396
645
|
const from = path.join(detected.agentDir.legacyDir, entry.name);
|
|
397
646
|
const to = path.join(detected.agentDir.targetDir, entry.name);
|
|
398
|
-
if (fs.existsSync(to))
|
|
647
|
+
if (fs.existsSync(to)) {
|
|
399
648
|
continue;
|
|
649
|
+
}
|
|
400
650
|
try {
|
|
401
651
|
fs.renameSync(from, to);
|
|
402
652
|
changes.push(`Moved agent file ${entry.name} → agents/${detected.targetAgentId}/agent`);
|
|
@@ -421,21 +671,26 @@ export async function migrateLegacyAgentDir(detected, now) {
|
|
|
421
671
|
async function migrateLegacyWhatsAppAuth(detected) {
|
|
422
672
|
const changes = [];
|
|
423
673
|
const warnings = [];
|
|
424
|
-
if (!detected.whatsappAuth.hasLegacy)
|
|
674
|
+
if (!detected.whatsappAuth.hasLegacy) {
|
|
425
675
|
return { changes, warnings };
|
|
676
|
+
}
|
|
426
677
|
ensureDir(detected.whatsappAuth.targetDir);
|
|
427
678
|
const entries = safeReadDir(detected.whatsappAuth.legacyDir);
|
|
428
679
|
for (const entry of entries) {
|
|
429
|
-
if (!entry.isFile())
|
|
680
|
+
if (!entry.isFile()) {
|
|
430
681
|
continue;
|
|
431
|
-
|
|
682
|
+
}
|
|
683
|
+
if (entry.name === "oauth.json") {
|
|
432
684
|
continue;
|
|
433
|
-
|
|
685
|
+
}
|
|
686
|
+
if (!isLegacyWhatsAppAuthFile(entry.name)) {
|
|
434
687
|
continue;
|
|
688
|
+
}
|
|
435
689
|
const from = path.join(detected.whatsappAuth.legacyDir, entry.name);
|
|
436
690
|
const to = path.join(detected.whatsappAuth.targetDir, entry.name);
|
|
437
|
-
if (fileExists(to))
|
|
691
|
+
if (fileExists(to)) {
|
|
438
692
|
continue;
|
|
693
|
+
}
|
|
439
694
|
try {
|
|
440
695
|
fs.renameSync(from, to);
|
|
441
696
|
changes.push(`Moved WhatsApp auth ${entry.name} → whatsapp/default`);
|
|
@@ -466,8 +721,18 @@ export async function autoMigrateLegacyState(params) {
|
|
|
466
721
|
}
|
|
467
722
|
autoMigrateChecked = true;
|
|
468
723
|
const env = params.env ?? process.env;
|
|
724
|
+
const stateDirResult = await autoMigrateLegacyStateDir({
|
|
725
|
+
env,
|
|
726
|
+
homedir: params.homedir,
|
|
727
|
+
log: params.log,
|
|
728
|
+
});
|
|
469
729
|
if (env.CLAWDBOT_AGENT_DIR?.trim() || env.PI_CODING_AGENT_DIR?.trim()) {
|
|
470
|
-
return {
|
|
730
|
+
return {
|
|
731
|
+
migrated: stateDirResult.migrated,
|
|
732
|
+
skipped: true,
|
|
733
|
+
changes: stateDirResult.changes,
|
|
734
|
+
warnings: stateDirResult.warnings,
|
|
735
|
+
};
|
|
471
736
|
}
|
|
472
737
|
const detected = await detectLegacyStateMigrations({
|
|
473
738
|
cfg: params.cfg,
|
|
@@ -475,13 +740,18 @@ export async function autoMigrateLegacyState(params) {
|
|
|
475
740
|
homedir: params.homedir,
|
|
476
741
|
});
|
|
477
742
|
if (!detected.sessions.hasLegacy && !detected.agentDir.hasLegacy) {
|
|
478
|
-
return {
|
|
743
|
+
return {
|
|
744
|
+
migrated: stateDirResult.migrated,
|
|
745
|
+
skipped: false,
|
|
746
|
+
changes: stateDirResult.changes,
|
|
747
|
+
warnings: stateDirResult.warnings,
|
|
748
|
+
};
|
|
479
749
|
}
|
|
480
750
|
const now = params.now ?? (() => Date.now());
|
|
481
751
|
const sessions = await migrateLegacySessions(detected, now);
|
|
482
752
|
const agentDir = await migrateLegacyAgentDir(detected, now);
|
|
483
|
-
const changes = [...sessions.changes, ...agentDir.changes];
|
|
484
|
-
const warnings = [...sessions.warnings, ...agentDir.warnings];
|
|
753
|
+
const changes = [...stateDirResult.changes, ...sessions.changes, ...agentDir.changes];
|
|
754
|
+
const warnings = [...stateDirResult.warnings, ...sessions.warnings, ...agentDir.warnings];
|
|
485
755
|
const logger = params.log ?? createSubsystemLogger("state-migrations");
|
|
486
756
|
if (changes.length > 0) {
|
|
487
757
|
logger.info(`Auto-migrated legacy state:\n${changes.map((entry) => `- ${entry}`).join("\n")}`);
|
|
@@ -83,3 +83,38 @@ export function globalInstallArgs(manager, spec) {
|
|
|
83
83
|
return ["bun", "add", "-g", spec];
|
|
84
84
|
return ["npm", "i", "-g", spec];
|
|
85
85
|
}
|
|
86
|
+
const GLOBAL_RENAME_PREFIX = ".";
|
|
87
|
+
export async function cleanupGlobalRenameDirs(params) {
|
|
88
|
+
const removed = [];
|
|
89
|
+
const root = params.globalRoot.trim();
|
|
90
|
+
const name = params.packageName.trim();
|
|
91
|
+
if (!root || !name) {
|
|
92
|
+
return { removed };
|
|
93
|
+
}
|
|
94
|
+
const prefix = `${GLOBAL_RENAME_PREFIX}${name}-`;
|
|
95
|
+
let entries = [];
|
|
96
|
+
try {
|
|
97
|
+
entries = await fs.readdir(root);
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return { removed };
|
|
101
|
+
}
|
|
102
|
+
for (const entry of entries) {
|
|
103
|
+
if (!entry.startsWith(prefix)) {
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
const target = path.join(root, entry);
|
|
107
|
+
try {
|
|
108
|
+
const stat = await fs.lstat(target);
|
|
109
|
+
if (!stat.isDirectory()) {
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
await fs.rm(target, { recursive: true, force: true });
|
|
113
|
+
removed.push(entry);
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
// ignore cleanup failures
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return { removed };
|
|
120
|
+
}
|