@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.
Files changed (258) hide show
  1. package/CHANGELOG.md +118 -0
  2. package/README-header.png +0 -0
  3. package/dist/agents/bash-tools.exec.js +76 -25
  4. package/dist/agents/cli-runner/helpers.js +9 -11
  5. package/dist/agents/context.js +1 -1
  6. package/dist/agents/identity.js +47 -7
  7. package/dist/agents/memory-search.js +25 -8
  8. package/dist/agents/model-catalog.js +1 -1
  9. package/dist/agents/model-selection.js +21 -0
  10. package/dist/agents/pi-embedded-block-chunker.js +117 -42
  11. package/dist/agents/pi-embedded-helpers/errors.js +183 -78
  12. package/dist/agents/pi-embedded-helpers.js +1 -1
  13. package/dist/agents/pi-embedded-runner/compact.js +8 -10
  14. package/dist/agents/pi-embedded-runner/model.js +62 -3
  15. package/dist/agents/pi-embedded-runner/run/attempt.js +21 -11
  16. package/dist/agents/pi-embedded-runner/run.js +199 -46
  17. package/dist/agents/pi-embedded-runner/system-prompt.js +10 -2
  18. package/dist/agents/pi-embedded-subscribe.js +118 -29
  19. package/dist/agents/pi-tools.js +10 -5
  20. package/dist/agents/poolbot-tools.js +15 -10
  21. package/dist/agents/sandbox-paths.js +31 -0
  22. package/dist/agents/session-tool-result-guard.js +94 -15
  23. package/dist/agents/shell-utils.js +51 -0
  24. package/dist/agents/skills/bundled-context.js +23 -0
  25. package/dist/agents/skills/bundled-dir.js +41 -7
  26. package/dist/agents/skills-install.js +60 -23
  27. package/dist/agents/subagent-announce.js +79 -34
  28. package/dist/agents/tool-policy.conformance.js +14 -0
  29. package/dist/agents/tool-policy.js +24 -0
  30. package/dist/agents/tools/cron-tool.js +166 -19
  31. package/dist/agents/tools/discord-actions-presence.js +78 -0
  32. package/dist/agents/tools/image-tool.js +1 -1
  33. package/dist/agents/tools/message-tool.js +56 -2
  34. package/dist/agents/tools/sessions-history-tool.js +69 -1
  35. package/dist/agents/tools/web-search.js +211 -42
  36. package/dist/agents/usage.js +23 -1
  37. package/dist/agents/workspace-run.js +67 -0
  38. package/dist/agents/workspace-templates.js +44 -0
  39. package/dist/auto-reply/command-auth.js +121 -6
  40. package/dist/auto-reply/envelope.js +74 -82
  41. package/dist/auto-reply/reply/commands-compact.js +1 -0
  42. package/dist/auto-reply/reply/commands-context-report.js +1 -0
  43. package/dist/auto-reply/reply/commands-context.js +1 -0
  44. package/dist/auto-reply/reply/commands-models.js +107 -60
  45. package/dist/auto-reply/reply/commands-ptt.js +171 -0
  46. package/dist/auto-reply/reply/get-reply-run.js +2 -1
  47. package/dist/auto-reply/reply/inbound-context.js +5 -1
  48. package/dist/auto-reply/reply/mentions.js +1 -1
  49. package/dist/auto-reply/reply/model-selection.js +3 -3
  50. package/dist/auto-reply/thinking.js +88 -43
  51. package/dist/browser/bridge-server.js +13 -0
  52. package/dist/browser/cdp.helpers.js +38 -24
  53. package/dist/browser/client-fetch.js +50 -7
  54. package/dist/browser/config.js +1 -10
  55. package/dist/browser/extension-relay.js +101 -40
  56. package/dist/browser/pw-ai.js +1 -1
  57. package/dist/browser/pw-session.js +143 -8
  58. package/dist/browser/pw-tools-core.interactions.js +125 -27
  59. package/dist/browser/pw-tools-core.responses.js +1 -1
  60. package/dist/browser/pw-tools-core.state.js +1 -1
  61. package/dist/browser/routes/agent.act.js +86 -41
  62. package/dist/browser/routes/dispatcher.js +4 -4
  63. package/dist/browser/screenshot.js +1 -1
  64. package/dist/browser/server.js +13 -0
  65. package/dist/build-info.json +3 -3
  66. package/dist/canvas-host/a2ui/index.html +28 -28
  67. package/dist/channels/reply-prefix.js +8 -1
  68. package/dist/cli/cron-cli/register.cron-add.js +61 -40
  69. package/dist/cli/cron-cli/register.cron-edit.js +60 -34
  70. package/dist/cli/cron-cli/shared.js +56 -41
  71. package/dist/cli/dns-cli.js +26 -14
  72. package/dist/cli/gateway-cli/register.js +37 -19
  73. package/dist/cli/memory-cli.js +5 -5
  74. package/dist/cli/parse-bytes.js +37 -0
  75. package/dist/cli/update-cli.js +173 -52
  76. package/dist/commands/agent.js +1 -0
  77. package/dist/commands/auth-choice.apply.oauth.js +1 -1
  78. package/dist/commands/doctor-config-flow.js +61 -5
  79. package/dist/commands/doctor-state-migrations.js +1 -1
  80. package/dist/commands/health.js +1 -1
  81. package/dist/commands/model-allowlist.js +29 -0
  82. package/dist/commands/model-picker.js +2 -1
  83. package/dist/commands/models/list.registry.js +1 -1
  84. package/dist/commands/models/list.status-command.js +43 -23
  85. package/dist/commands/models/shared.js +15 -0
  86. package/dist/commands/onboard-custom.js +384 -0
  87. package/dist/commands/onboard-non-interactive/local/auth-choice-inference.js +35 -0
  88. package/dist/commands/onboard-non-interactive/local/auth-choice.js +6 -3
  89. package/dist/commands/onboard-skills.js +63 -38
  90. package/dist/commands/openai-model-default.js +41 -0
  91. package/dist/compat/legacy-names.js +2 -0
  92. package/dist/config/defaults.js +3 -2
  93. package/dist/config/paths.js +136 -35
  94. package/dist/config/plugin-auto-enable.js +21 -5
  95. package/dist/config/redact-snapshot.js +153 -0
  96. package/dist/config/schema.field-metadata.js +590 -0
  97. package/dist/config/schema.js +2 -2
  98. package/dist/config/sessions/store.js +291 -23
  99. package/dist/config/zod-schema.agent-defaults.js +3 -0
  100. package/dist/config/zod-schema.agent-runtime.js +13 -2
  101. package/dist/config/zod-schema.providers-core.js +142 -0
  102. package/dist/config/zod-schema.session.js +3 -0
  103. package/dist/control-ui/assets/{index-CIRDm-Lu.css → index-CSfXd2LO.css} +1 -1
  104. package/dist/control-ui/assets/{index-CmNMuoem.js → index-HRr1grwl.js} +446 -413
  105. package/dist/control-ui/assets/index-HRr1grwl.js.map +1 -0
  106. package/dist/control-ui/index.html +4 -4
  107. package/dist/cron/delivery.js +57 -0
  108. package/dist/cron/isolated-agent/delivery-target.js +18 -3
  109. package/dist/cron/isolated-agent/helpers.js +22 -5
  110. package/dist/cron/isolated-agent/run.js +172 -63
  111. package/dist/cron/isolated-agent/session.js +2 -0
  112. package/dist/cron/normalize.js +356 -28
  113. package/dist/cron/parse.js +10 -5
  114. package/dist/cron/run-log.js +35 -10
  115. package/dist/cron/schedule.js +41 -6
  116. package/dist/cron/service/jobs.js +208 -35
  117. package/dist/cron/service/ops.js +72 -16
  118. package/dist/cron/service/state.js +2 -0
  119. package/dist/cron/service/store.js +386 -14
  120. package/dist/cron/service/timer.js +390 -147
  121. package/dist/cron/session-reaper.js +86 -0
  122. package/dist/cron/store.js +23 -8
  123. package/dist/cron/validate-timestamp.js +43 -0
  124. package/dist/discord/monitor/agent-components.js +438 -0
  125. package/dist/discord/monitor/allow-list.js +28 -5
  126. package/dist/discord/monitor/gateway-registry.js +29 -0
  127. package/dist/discord/monitor/native-command.js +44 -23
  128. package/dist/discord/monitor/sender-identity.js +45 -0
  129. package/dist/discord/pluralkit.js +27 -0
  130. package/dist/discord/send.outbound.js +92 -5
  131. package/dist/discord/send.shared.js +60 -23
  132. package/dist/discord/targets.js +84 -1
  133. package/dist/entry.js +15 -9
  134. package/dist/extensionAPI.js +8 -0
  135. package/dist/gateway/control-ui.js +8 -1
  136. package/dist/gateway/hooks-mapping.js +3 -0
  137. package/dist/gateway/hooks.js +65 -0
  138. package/dist/gateway/net.js +96 -31
  139. package/dist/gateway/node-command-policy.js +50 -15
  140. package/dist/gateway/origin-check.js +56 -0
  141. package/dist/gateway/protocol/client-info.js +9 -0
  142. package/dist/gateway/protocol/index.js +9 -2
  143. package/dist/gateway/protocol/schema/agents-models-skills.js +71 -1
  144. package/dist/gateway/protocol/schema/cron.js +22 -10
  145. package/dist/gateway/protocol/schema/protocol-schemas.js +16 -2
  146. package/dist/gateway/protocol/schema/sessions.js +12 -0
  147. package/dist/gateway/server/hooks.js +1 -1
  148. package/dist/gateway/server-broadcast.js +26 -9
  149. package/dist/gateway/server-chat.js +112 -23
  150. package/dist/gateway/server-discovery-runtime.js +10 -2
  151. package/dist/gateway/server-http.js +109 -11
  152. package/dist/gateway/server-methods/agent-timestamp.js +60 -0
  153. package/dist/gateway/server-methods/agents.js +321 -2
  154. package/dist/gateway/server-methods/usage.js +559 -16
  155. package/dist/gateway/server-runtime-state.js +22 -8
  156. package/dist/gateway/server-startup-memory.js +16 -0
  157. package/dist/gateway/server.impl.js +5 -1
  158. package/dist/gateway/session-utils.fs.js +23 -25
  159. package/dist/gateway/session-utils.js +20 -10
  160. package/dist/gateway/sessions-patch.js +7 -22
  161. package/dist/gateway/test-helpers.mocks.js +11 -7
  162. package/dist/gateway/test-helpers.server.js +35 -2
  163. package/dist/imessage/constants.js +2 -0
  164. package/dist/imessage/monitor/deliver.js +4 -1
  165. package/dist/imessage/monitor/monitor-provider.js +51 -1
  166. package/dist/infra/bonjour-discovery.js +131 -70
  167. package/dist/infra/control-ui-assets.js +134 -12
  168. package/dist/infra/errors.js +12 -0
  169. package/dist/infra/exec-approvals.js +266 -57
  170. package/dist/infra/format-time/format-datetime.js +79 -0
  171. package/dist/infra/format-time/format-duration.js +81 -0
  172. package/dist/infra/format-time/format-relative.js +80 -0
  173. package/dist/infra/heartbeat-runner.js +140 -49
  174. package/dist/infra/home-dir.js +54 -0
  175. package/dist/infra/net/fetch-guard.js +122 -0
  176. package/dist/infra/net/ssrf.js +65 -29
  177. package/dist/infra/outbound/abort.js +14 -0
  178. package/dist/infra/outbound/message-action-runner.js +77 -13
  179. package/dist/infra/outbound/outbound-session.js +143 -37
  180. package/dist/infra/poolbot-root.js +43 -1
  181. package/dist/infra/session-cost-usage.js +631 -41
  182. package/dist/infra/state-migrations.js +317 -47
  183. package/dist/infra/update-global.js +35 -0
  184. package/dist/infra/update-runner.js +149 -43
  185. package/dist/infra/warning-filter.js +65 -0
  186. package/dist/infra/widearea-dns.js +30 -9
  187. package/dist/logging/redact-identifier.js +12 -0
  188. package/dist/media/fetch.js +81 -58
  189. package/dist/media/store.js +2 -0
  190. package/dist/media-understanding/apply.js +403 -3
  191. package/dist/media-understanding/attachments.js +38 -27
  192. package/dist/media-understanding/defaults.js +16 -0
  193. package/dist/media-understanding/providers/deepgram/audio.js +22 -14
  194. package/dist/media-understanding/providers/google/audio.js +24 -17
  195. package/dist/media-understanding/providers/google/video.js +24 -17
  196. package/dist/media-understanding/providers/image.js +3 -3
  197. package/dist/media-understanding/providers/index.js +4 -1
  198. package/dist/media-understanding/providers/openai/audio.js +22 -14
  199. package/dist/media-understanding/providers/shared.js +16 -11
  200. package/dist/media-understanding/providers/zai/index.js +6 -0
  201. package/dist/media-understanding/runner.js +158 -90
  202. package/dist/memory/batch-voyage.js +277 -0
  203. package/dist/memory/embeddings-voyage.js +75 -0
  204. package/dist/memory/embeddings.js +28 -16
  205. package/dist/memory/internal.js +101 -18
  206. package/dist/memory/manager.js +154 -48
  207. package/dist/memory/search-manager.js +173 -0
  208. package/dist/memory/session-files.js +9 -3
  209. package/dist/node-host/runner.js +34 -24
  210. package/dist/node-host/with-timeout.js +27 -0
  211. package/dist/plugins/commands.js +5 -1
  212. package/dist/plugins/config-state.js +86 -7
  213. package/dist/plugins/source-display.js +51 -0
  214. package/dist/process/exec.js +20 -2
  215. package/dist/routing/resolve-route.js +12 -0
  216. package/dist/routing/session-key.js +15 -0
  217. package/dist/runtime.js +2 -0
  218. package/dist/security/audit-extra.async.js +601 -0
  219. package/dist/security/audit-extra.js +2 -830
  220. package/dist/security/audit-extra.sync.js +505 -0
  221. package/dist/security/channel-metadata.js +34 -0
  222. package/dist/security/external-content.js +88 -6
  223. package/dist/security/skill-scanner.js +330 -0
  224. package/dist/sessions/session-key-utils.js +7 -0
  225. package/dist/signal/monitor/event-handler.js +80 -1
  226. package/dist/slack/monitor/media.js +85 -15
  227. package/dist/tailscale/detect.js +1 -2
  228. package/dist/telegram/bot/helpers.js +109 -28
  229. package/dist/telegram/bot-handlers.js +144 -3
  230. package/dist/telegram/bot-message-context.js +37 -10
  231. package/dist/telegram/bot-message-dispatch.js +54 -17
  232. package/dist/telegram/bot-native-commands.js +86 -29
  233. package/dist/telegram/bot.js +30 -29
  234. package/dist/telegram/model-buttons.js +163 -0
  235. package/dist/telegram/monitor.js +110 -85
  236. package/dist/telegram/send.js +129 -47
  237. package/dist/terminal/restore.js +45 -0
  238. package/dist/test-helpers/state-dir-env.js +16 -0
  239. package/dist/tts/tts.js +12 -6
  240. package/dist/tui/tui-session-actions.js +166 -54
  241. package/dist/utils/fetch-timeout.js +20 -0
  242. package/dist/utils/normalize-secret-input.js +19 -0
  243. package/dist/utils/transcript-tools.js +58 -0
  244. package/dist/utils.js +45 -14
  245. package/dist/version.js +42 -5
  246. package/dist/wizard/clack-prompter.js +9 -6
  247. package/extensions/googlechat/node_modules/.bin/poolbot +21 -0
  248. package/extensions/googlechat/package.json +2 -2
  249. package/extensions/line/node_modules/.bin/poolbot +21 -0
  250. package/extensions/line/package.json +1 -1
  251. package/extensions/matrix/node_modules/.bin/poolbot +21 -0
  252. package/extensions/matrix/package.json +1 -1
  253. package/extensions/memory-core/node_modules/.bin/poolbot +21 -0
  254. package/extensions/memory-core/package.json +4 -1
  255. package/extensions/twitch/node_modules/.bin/poolbot +21 -0
  256. package/extensions/twitch/package.json +1 -1
  257. package/package.json +183 -24
  258. 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
- if (trimmed.startsWith("group:"))
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
- if (lower.startsWith("whatsapp:") && !trimmed.includes(":group:"))
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
- if (raw.toLowerCase() === "global" || raw.toLowerCase() === "unknown")
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
- if (raw.toLowerCase().startsWith("agent:"))
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
- if (normalized === "global")
94
+ }
95
+ if (normalized === "global") {
83
96
  continue;
84
- if (normalized.startsWith("agent:"))
97
+ }
98
+ if (normalized.startsWith("agent:")) {
85
99
  continue;
86
- if (normalized.toLowerCase().startsWith("subagent:"))
100
+ }
101
+ if (normalized.toLowerCase().startsWith("subagent:")) {
87
102
  continue;
88
- if (isLegacyGroupKey(normalized) || isSurfaceGroupKey(normalized))
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
- if (incomingUpdated < existingUpdated)
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
- if (existingMeta?.isCanonical && !isCanonical)
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
- if (!emptyDirOrMissing(dir))
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
- if (entry.name === "sessions.json")
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
- if (entry.name === "oauth.json")
682
+ }
683
+ if (entry.name === "oauth.json") {
432
684
  continue;
433
- if (!isLegacyWhatsAppAuthFile(entry.name))
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 { migrated: false, skipped: true, changes: [], warnings: [] };
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 { migrated: false, skipped: false, changes: [], warnings: [] };
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
+ }