@poolzin/pool-bot 2026.3.22 → 2026.3.24

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 (159) hide show
  1. package/CHANGELOG.md +111 -0
  2. package/dist/.buildstamp +1 -1
  3. package/dist/acp/bindings-store.js +209 -0
  4. package/dist/acp/control-plane/runtime-cache.js +54 -0
  5. package/dist/acp/control-plane/runtime-options.js +215 -0
  6. package/dist/acp/control-plane/session-actor-queue.js +36 -0
  7. package/dist/acp/policy.js +52 -0
  8. package/dist/acp/runtime/errors.js +47 -0
  9. package/dist/acp/runtime/registry.js +86 -0
  10. package/dist/acp/runtime/types.js +1 -0
  11. package/dist/acp/translator.js +97 -0
  12. package/dist/agents/btw.js +280 -0
  13. package/dist/agents/failover-error.js +145 -47
  14. package/dist/agents/fast-mode.js +24 -0
  15. package/dist/agents/live-model-errors.js +23 -0
  16. package/dist/agents/model-auth-env-vars.js +44 -0
  17. package/dist/agents/model-auth-markers.js +69 -0
  18. package/dist/agents/models-config.providers.discovery.js +180 -0
  19. package/dist/agents/models-config.providers.static.js +480 -0
  20. package/dist/auto-reply/reply/typing-policy.js +15 -0
  21. package/dist/browser/browser-profile-manager.js +319 -0
  22. package/dist/browser/cdp-proxy-bypass.js +129 -0
  23. package/dist/browser/cdp-timeouts.js +41 -0
  24. package/dist/browser/chrome-extension-validator.js +406 -0
  25. package/dist/browser/chrome-mcp-snapshot.js +222 -0
  26. package/dist/browser/chrome-mcp.js +421 -0
  27. package/dist/browser/chrome-mcp.snapshot.js +133 -0
  28. package/dist/browser/errors.js +67 -0
  29. package/dist/browser/form-fields.js +22 -0
  30. package/dist/browser/output-atomic.js +44 -0
  31. package/dist/browser/profile-capabilities.js +47 -0
  32. package/dist/browser/safe-filename.js +25 -0
  33. package/dist/browser/snapshot-roles.js +60 -0
  34. package/dist/build-info.json +3 -3
  35. package/dist/channels/account-snapshot-fields.js +176 -0
  36. package/dist/channels/draft-stream-controls.js +89 -0
  37. package/dist/channels/inbound-debounce-policy.js +28 -0
  38. package/dist/channels/typing-lifecycle.js +39 -0
  39. package/dist/cli/program/command-registry.js +52 -0
  40. package/dist/commands/agent-binding.js +123 -0
  41. package/dist/commands/agents.commands.bind.js +280 -0
  42. package/dist/commands/backup-shared.js +186 -0
  43. package/dist/commands/backup-verify.js +236 -0
  44. package/dist/commands/backup.js +166 -0
  45. package/dist/commands/channel-account-context.js +15 -0
  46. package/dist/commands/channel-account.js +190 -0
  47. package/dist/commands/gateway-install-token.js +117 -0
  48. package/dist/commands/oauth-tls-preflight.js +121 -0
  49. package/dist/commands/ollama-setup.js +402 -0
  50. package/dist/commands/security-owner-only.js +86 -0
  51. package/dist/commands/self-hosted-provider-setup.js +207 -0
  52. package/dist/commands/session-store-targets.js +12 -0
  53. package/dist/commands/sessions-cleanup.js +97 -0
  54. package/dist/control-ui/assets/{index-Dvkl4Xlx.js → index-D7shnQwQ.js} +404 -388
  55. package/dist/control-ui/assets/index-D7shnQwQ.js.map +1 -0
  56. package/dist/control-ui/index.html +1 -1
  57. package/dist/cron/cron-filters.js +150 -0
  58. package/dist/cron/heartbeat-policy.js +26 -0
  59. package/dist/gateway/device-pairing-security.js +197 -0
  60. package/dist/gateway/event-deduplication.js +167 -0
  61. package/dist/gateway/hooks-mapping.js +46 -7
  62. package/dist/gateway/run-tracker.js +253 -0
  63. package/dist/gateway/server-methods/nodes.js +14 -0
  64. package/dist/gateway/websocket-preauth-security.js +188 -0
  65. package/dist/hooks/module-loader.js +28 -0
  66. package/dist/infra/agent-command-binding.js +144 -0
  67. package/dist/infra/backup.js +328 -0
  68. package/dist/infra/channel-account-context.js +173 -0
  69. package/dist/infra/errors.js +53 -13
  70. package/dist/infra/exec-approvals-security.js +217 -0
  71. package/dist/infra/security/command-analyzer.js +257 -0
  72. package/dist/infra/session-cleanup.js +143 -0
  73. package/dist/plugins/loader.js +16 -8
  74. package/dist/security/external-content.js +51 -1
  75. package/dist/sessions/session-costs.js +228 -0
  76. package/dist/shared/param-key.js +16 -0
  77. package/dist/shared/poll-params.js +58 -0
  78. package/dist/shared/polls.js +55 -0
  79. package/docs/DASHBOARD-GAP-ANALYSIS-AND-PLAN.md +430 -0
  80. package/docs/FEATURES.md +523 -0
  81. package/docs/FINAL-IMPLEMENTATION-REVIEW.md +274 -0
  82. package/docs/FINAL-IMPLEMENTATION-SUMMARY.md +356 -0
  83. package/docs/FINAL-PROFESSIONAL-EVALUATION.md +312 -0
  84. package/docs/IMPLEMENTATION-PRIORITY-EVALUATION.md +298 -0
  85. package/docs/IMPLEMENTATION-PROGRESS.md +237 -0
  86. package/docs/IMPLEMENTATION-REVIEW-PHASE1-2.md +381 -0
  87. package/docs/IMPLEMENTATION-REVIEW-PHASE4.md +389 -0
  88. package/docs/IMPLEMENTATION-REVIEW-PHASE5.md +420 -0
  89. package/docs/IMPLEMENTATION-REVIEW-PHASE6.md +422 -0
  90. package/docs/IMPLEMENTATION-REVIEW-PHASE7-FINAL.md +184 -0
  91. package/docs/MIKRODASH-ANALYSIS.md +412 -0
  92. package/docs/OPENCLAW-GAP-ANALYSIS-FINAL.md +431 -0
  93. package/docs/OPENCLAW-VS-POOLBOT-ANALYSIS.md +351 -0
  94. package/docs/PHASE-7-SUMMARY.md +144 -0
  95. package/docs/POOLBOT-OFFICE-PLAN.md +697 -0
  96. package/docs/PROJECT-FINAL-STATUS.md +237 -0
  97. package/docs/README.md +116 -0
  98. package/docs/REAL-IMPROVEMENTS-EVALUATION.md +477 -0
  99. package/docs/SECURITY-HARDENING-IMPLEMENTATION.md +161 -0
  100. package/docs/channels/googlechat.md +235 -206
  101. package/docs/channels/irc.md +332 -0
  102. package/docs/channels/nostr.md +255 -168
  103. package/docs/components/command-palette.md +166 -0
  104. package/docs/components/login-gate.md +219 -0
  105. package/docs/getting-started/installation.md +191 -0
  106. package/docs/getting-started/introduction.md +120 -0
  107. package/docs/improvements/USAGE-GUIDE.md +359 -0
  108. package/docs/plans/2026-03-15-openclaw-features-implementation.md +1632 -0
  109. package/docs/reference/deadcode-detection.md +72 -0
  110. package/extensions/acpx/node_modules/.bin/acpx +21 -0
  111. package/extensions/agency-agents/node_modules/.bin/vite +4 -4
  112. package/extensions/agency-agents/node_modules/.bin/vitest +2 -2
  113. package/extensions/googlechat/node_modules/.bin/tsc +21 -0
  114. package/extensions/googlechat/node_modules/.bin/tsserver +21 -0
  115. package/extensions/googlechat/node_modules/.bin/vitest +21 -0
  116. package/extensions/googlechat/package.json +11 -28
  117. package/extensions/googlechat/src/googlechat-channel.test.ts +60 -0
  118. package/extensions/googlechat/src/googlechat-channel.ts +120 -0
  119. package/extensions/googlechat/src/index.ts +14 -0
  120. package/extensions/irc/node_modules/.bin/tsc +21 -0
  121. package/extensions/irc/node_modules/.bin/tsserver +21 -0
  122. package/extensions/irc/node_modules/.bin/vitest +21 -0
  123. package/extensions/irc/package.json +16 -8
  124. package/extensions/irc/src/index.ts +14 -0
  125. package/extensions/irc/src/irc-channel.test.ts +43 -0
  126. package/extensions/irc/src/irc-channel.ts +191 -0
  127. package/extensions/keyed-async-queue/node_modules/.bin/tsc +21 -0
  128. package/extensions/keyed-async-queue/node_modules/.bin/tsserver +21 -0
  129. package/extensions/keyed-async-queue/node_modules/.bin/vitest +21 -0
  130. package/extensions/keyed-async-queue/package.json +20 -0
  131. package/extensions/keyed-async-queue/src/index.ts +14 -0
  132. package/extensions/keyed-async-queue/src/queue.test.ts +135 -0
  133. package/extensions/keyed-async-queue/src/queue.ts +200 -0
  134. package/extensions/memory-core/node_modules/.bin/tsc +21 -0
  135. package/extensions/memory-core/node_modules/.bin/tsserver +21 -0
  136. package/extensions/memory-core/node_modules/.bin/vitest +21 -0
  137. package/extensions/memory-core/package.json +11 -8
  138. package/extensions/memory-core/src/index.ts +14 -0
  139. package/extensions/memory-core/src/memory-manager.test.ts +124 -0
  140. package/extensions/memory-core/src/memory-manager.ts +186 -0
  141. package/extensions/nostr/node_modules/.bin/tsc +2 -2
  142. package/extensions/nostr/node_modules/.bin/tsserver +2 -2
  143. package/extensions/nostr/node_modules/.bin/vitest +21 -0
  144. package/extensions/nostr/package.json +15 -24
  145. package/extensions/nostr/src/index.ts +14 -0
  146. package/extensions/nostr/src/nostr-channel.test.ts +55 -0
  147. package/extensions/nostr/src/nostr-channel.ts +228 -0
  148. package/extensions/page-agent/node_modules/.bin/vitest +2 -2
  149. package/extensions/test-utils/node_modules/.bin/jiti +21 -0
  150. package/extensions/test-utils/node_modules/.bin/playwright +21 -0
  151. package/extensions/test-utils/node_modules/.bin/tsx +21 -0
  152. package/extensions/test-utils/node_modules/.bin/vite +21 -0
  153. package/extensions/test-utils/node_modules/.bin/vitest +21 -0
  154. package/extensions/test-utils/node_modules/.bin/yaml +21 -0
  155. package/extensions/xyops/node_modules/.bin/vitest +2 -2
  156. package/package.json +2 -1
  157. package/dist/control-ui/assets/index-Dvkl4Xlx.js.map +0 -1
  158. package/extensions/googlechat/node_modules/.bin/poolbot +0 -21
  159. package/extensions/memory-core/node_modules/.bin/poolbot +0 -21
@@ -0,0 +1,328 @@
1
+ /**
2
+ * Backup System for Pool Bot
3
+ *
4
+ * Creates and restores backups of sessions, config, and credentials.
5
+ * Implemented from scratch for Pool Bot architecture.
6
+ */
7
+ import fs from "node:fs/promises";
8
+ import path from "node:path";
9
+ const BACKUP_DIR = path.join(process.cwd(), ".poolbot", "backups");
10
+ const CONFIG_DIR = path.join(process.cwd(), ".poolbot");
11
+ const SESSIONS_DIR = path.join(CONFIG_DIR, "sessions");
12
+ const CREDENTIALS_DIR = path.join(CONFIG_DIR, "credentials");
13
+ /**
14
+ * Create a backup of Pool Bot data
15
+ */
16
+ export async function createBackup(options = {}) {
17
+ const startTime = Date.now();
18
+ const { includeSessions = true, includeConfig = true, includeCredentials = true, destinationDir = BACKUP_DIR, } = options;
19
+ try {
20
+ // Ensure backup directory exists
21
+ await fs.mkdir(destinationDir, { recursive: true });
22
+ // Create backup metadata
23
+ const metadata = {
24
+ version: "1.0",
25
+ createdAt: startTime,
26
+ poolBotVersion: process.env.npm_package_version || "unknown",
27
+ includesSessions: includeSessions,
28
+ includesConfig: includeConfig,
29
+ includesCredentials: includeCredentials,
30
+ };
31
+ // Create temporary directory for backup contents
32
+ const tempDir = path.join(destinationDir, `backup-${Date.now()}-temp`);
33
+ await fs.mkdir(tempDir, { recursive: true });
34
+ let totalSize = 0;
35
+ let sessionsBackedUp = 0;
36
+ // Backup sessions
37
+ if (includeSessions) {
38
+ const sessionsBackupDir = path.join(tempDir, "sessions");
39
+ await fs.mkdir(sessionsBackupDir, { recursive: true });
40
+ try {
41
+ const sessionFiles = await fs.readdir(SESSIONS_DIR);
42
+ for (const file of sessionFiles) {
43
+ if (file.endsWith(".json")) {
44
+ const srcPath = path.join(SESSIONS_DIR, file);
45
+ const destPath = path.join(sessionsBackupDir, file);
46
+ await fs.copyFile(srcPath, destPath);
47
+ sessionsBackedUp++;
48
+ }
49
+ }
50
+ metadata.sessionCount = sessionsBackedUp;
51
+ }
52
+ catch (error) {
53
+ if (error.code !== "ENOENT") {
54
+ throw error;
55
+ }
56
+ // Sessions directory doesn't exist yet - that's ok
57
+ metadata.sessionCount = 0;
58
+ }
59
+ }
60
+ // Backup config
61
+ if (includeConfig) {
62
+ const configFiles = ["config.json", "poolbot.json"];
63
+ const configBackupDir = path.join(tempDir, "config");
64
+ await fs.mkdir(configBackupDir, { recursive: true });
65
+ for (const configFile of configFiles) {
66
+ const srcPath = path.join(CONFIG_DIR, configFile);
67
+ const destPath = path.join(configBackupDir, configFile);
68
+ try {
69
+ await fs.copyFile(srcPath, destPath);
70
+ const stats = await fs.stat(destPath);
71
+ totalSize += stats.size;
72
+ metadata.configSize = stats.size;
73
+ }
74
+ catch (error) {
75
+ if (error.code !== "ENOENT") {
76
+ throw error;
77
+ }
78
+ }
79
+ }
80
+ }
81
+ // Backup credentials
82
+ if (includeCredentials) {
83
+ const credentialsBackupDir = path.join(tempDir, "credentials");
84
+ await fs.mkdir(credentialsBackupDir, { recursive: true });
85
+ try {
86
+ const credFiles = await fs.readdir(CREDENTIALS_DIR);
87
+ for (const file of credFiles) {
88
+ const srcPath = path.join(CREDENTIALS_DIR, file);
89
+ const destPath = path.join(credentialsBackupDir, file);
90
+ await fs.copyFile(srcPath, destPath);
91
+ const stats = await fs.stat(destPath);
92
+ totalSize += stats.size;
93
+ metadata.credentialsSize = (metadata.credentialsSize || 0) + stats.size;
94
+ }
95
+ }
96
+ catch (error) {
97
+ if (error.code !== "ENOENT") {
98
+ throw error;
99
+ }
100
+ // Credentials directory doesn't exist yet - that's ok
101
+ }
102
+ }
103
+ // Write metadata
104
+ const metadataPath = path.join(tempDir, "metadata.json");
105
+ await fs.writeFile(metadataPath, JSON.stringify(metadata, null, 2));
106
+ const metadataStats = await fs.stat(metadataPath);
107
+ totalSize += metadataStats.size;
108
+ // Create compressed archive
109
+ const timestamp = new Date(startTime).toISOString().replace(/[:.]/g, "-");
110
+ const backupFileName = `poolbot-backup-${timestamp}.tar.gz`;
111
+ const backupPath = path.join(destinationDir, backupFileName);
112
+ // For now, just copy the temp directory contents (simplified - not actually tar.gz)
113
+ // In production, you'd use a proper tar library
114
+ await fs.rename(tempDir, backupPath.replace(".tar.gz", ""));
115
+ const duration = Date.now() - startTime;
116
+ return {
117
+ success: true,
118
+ backupPath: backupPath.replace(".tar.gz", ""),
119
+ metadata,
120
+ sessionsBackedUp,
121
+ configBackedUp: includeConfig,
122
+ credentialsBackedUp: includeCredentials,
123
+ totalSize,
124
+ duration,
125
+ };
126
+ }
127
+ catch (error) {
128
+ const duration = Date.now() - startTime;
129
+ return {
130
+ success: false,
131
+ backupPath: "",
132
+ metadata: {
133
+ version: "1.0",
134
+ createdAt: startTime,
135
+ poolBotVersion: process.env.npm_package_version || "unknown",
136
+ includesSessions: false,
137
+ includesConfig: false,
138
+ includesCredentials: false,
139
+ },
140
+ totalSize: 0,
141
+ duration,
142
+ error: error.message,
143
+ };
144
+ }
145
+ }
146
+ /**
147
+ * Restore from a backup
148
+ */
149
+ export async function restoreBackup(options) {
150
+ const startTime = Date.now();
151
+ const { backupPath, restoreSessions = true, restoreConfig = true, restoreCredentials = true, dryRun = false, } = options;
152
+ try {
153
+ // Verify backup exists
154
+ const backupDir = backupPath.endsWith(".tar.gz")
155
+ ? backupPath.replace(".tar.gz", "")
156
+ : backupPath;
157
+ const metadataPath = path.join(backupDir, "metadata.json");
158
+ try {
159
+ await fs.access(metadataPath);
160
+ }
161
+ catch {
162
+ throw new Error(`Invalid backup: metadata.json not found at ${metadataPath}`);
163
+ }
164
+ // Read metadata
165
+ const metadataContent = await fs.readFile(metadataPath, "utf-8");
166
+ const metadata = JSON.parse(metadataContent);
167
+ const report = {
168
+ success: true,
169
+ backupPath,
170
+ dryRun,
171
+ duration: 0,
172
+ sessionsRestored: 0,
173
+ configRestored: false,
174
+ credentialsRestored: false,
175
+ };
176
+ if (dryRun) {
177
+ // Just report what would be restored
178
+ if (restoreSessions && metadata.includesSessions) {
179
+ report.sessionsRestored = metadata.sessionCount || 0;
180
+ }
181
+ if (restoreConfig && metadata.includesConfig) {
182
+ report.configRestored = true;
183
+ }
184
+ if (restoreCredentials && metadata.includesCredentials) {
185
+ report.credentialsRestored = true;
186
+ }
187
+ report.duration = Date.now() - startTime;
188
+ return report;
189
+ }
190
+ // Restore sessions
191
+ if (restoreSessions && metadata.includesSessions) {
192
+ const sessionsBackupDir = path.join(backupDir, "sessions");
193
+ try {
194
+ await fs.mkdir(SESSIONS_DIR, { recursive: true });
195
+ const sessionFiles = await fs.readdir(sessionsBackupDir);
196
+ for (const file of sessionFiles) {
197
+ if (file.endsWith(".json")) {
198
+ const srcPath = path.join(sessionsBackupDir, file);
199
+ const destPath = path.join(SESSIONS_DIR, file);
200
+ await fs.copyFile(srcPath, destPath);
201
+ report.sessionsRestored = (report.sessionsRestored || 0) + 1;
202
+ }
203
+ }
204
+ }
205
+ catch (error) {
206
+ if (error.code !== "ENOENT") {
207
+ throw error;
208
+ }
209
+ }
210
+ }
211
+ // Restore config
212
+ if (restoreConfig && metadata.includesConfig) {
213
+ const configBackupDir = path.join(backupDir, "config");
214
+ try {
215
+ const configFiles = await fs.readdir(configBackupDir);
216
+ for (const file of configFiles) {
217
+ const srcPath = path.join(configBackupDir, file);
218
+ const destPath = path.join(CONFIG_DIR, file);
219
+ await fs.copyFile(srcPath, destPath);
220
+ report.configRestored = true;
221
+ }
222
+ }
223
+ catch (error) {
224
+ if (error.code !== "ENOENT") {
225
+ throw error;
226
+ }
227
+ }
228
+ }
229
+ // Restore credentials
230
+ if (restoreCredentials && metadata.includesCredentials) {
231
+ const credentialsBackupDir = path.join(backupDir, "credentials");
232
+ try {
233
+ await fs.mkdir(CREDENTIALS_DIR, { recursive: true });
234
+ const credFiles = await fs.readdir(credentialsBackupDir);
235
+ for (const file of credFiles) {
236
+ const srcPath = path.join(credentialsBackupDir, file);
237
+ const destPath = path.join(CREDENTIALS_DIR, file);
238
+ await fs.copyFile(srcPath, destPath);
239
+ report.credentialsRestored = true;
240
+ }
241
+ }
242
+ catch (error) {
243
+ if (error.code !== "ENOENT") {
244
+ throw error;
245
+ }
246
+ }
247
+ }
248
+ report.duration = Date.now() - startTime;
249
+ return report;
250
+ }
251
+ catch (error) {
252
+ const duration = Date.now() - startTime;
253
+ return {
254
+ success: false,
255
+ backupPath,
256
+ dryRun,
257
+ duration,
258
+ error: error.message,
259
+ };
260
+ }
261
+ }
262
+ /**
263
+ * List available backups
264
+ */
265
+ export async function listBackups(backupDir = BACKUP_DIR) {
266
+ try {
267
+ await fs.mkdir(backupDir, { recursive: true });
268
+ const entries = await fs.readdir(backupDir, { withFileTypes: true });
269
+ const backups = [];
270
+ for (const entry of entries) {
271
+ if (entry.isDirectory() && entry.name.startsWith("poolbot-backup-")) {
272
+ const metadataPath = path.join(backupDir, entry.name, "metadata.json");
273
+ try {
274
+ const metadataContent = await fs.readFile(metadataPath, "utf-8");
275
+ const metadata = JSON.parse(metadataContent);
276
+ // Calculate total size
277
+ let totalSize = 0;
278
+ const backupPath = path.join(backupDir, entry.name);
279
+ const files = await fs.readdir(backupPath, { recursive: true });
280
+ for (const file of files) {
281
+ const filePath = path.join(backupPath, file);
282
+ try {
283
+ const stats = await fs.stat(filePath);
284
+ totalSize += stats.size;
285
+ }
286
+ catch {
287
+ // File might have been deleted
288
+ }
289
+ }
290
+ backups.push({
291
+ path: backupPath,
292
+ metadata,
293
+ size: totalSize,
294
+ createdAt: metadata.createdAt,
295
+ });
296
+ }
297
+ catch {
298
+ // Skip invalid backups
299
+ }
300
+ }
301
+ }
302
+ // Sort by creation date (newest first)
303
+ return backups.sort((a, b) => b.createdAt - a.createdAt);
304
+ }
305
+ catch {
306
+ return [];
307
+ }
308
+ }
309
+ /**
310
+ * Delete a backup
311
+ */
312
+ export async function deleteBackup(backupPath) {
313
+ try {
314
+ // Check if path exists first
315
+ await fs.access(backupPath);
316
+ await fs.rm(backupPath, { recursive: true, force: true });
317
+ return true;
318
+ }
319
+ catch {
320
+ return false;
321
+ }
322
+ }
323
+ /**
324
+ * Get backup directory
325
+ */
326
+ export function getBackupDir() {
327
+ return BACKUP_DIR;
328
+ }
@@ -0,0 +1,173 @@
1
+ /**
2
+ * Channel Account Context System
3
+ *
4
+ * Manage multiple accounts per channel with context switching.
5
+ * Implemented from scratch for Pool Bot architecture.
6
+ */
7
+ import fs from "node:fs/promises";
8
+ import path from "node:path";
9
+ const ACCOUNTS_FILE = path.join(process.cwd(), ".poolbot", "channel-accounts.json");
10
+ /**
11
+ * Load channel accounts from file
12
+ */
13
+ export async function loadChannelAccounts() {
14
+ try {
15
+ const data = await fs.readFile(ACCOUNTS_FILE, "utf-8");
16
+ return JSON.parse(data);
17
+ }
18
+ catch (error) {
19
+ if (error.code === "ENOENT") {
20
+ return { accounts: {}, activeAccounts: {} };
21
+ }
22
+ throw error;
23
+ }
24
+ }
25
+ /**
26
+ * Save channel accounts to file
27
+ */
28
+ export async function saveChannelAccounts(store) {
29
+ await fs.mkdir(path.dirname(ACCOUNTS_FILE), { recursive: true });
30
+ await fs.writeFile(ACCOUNTS_FILE, JSON.stringify(store, null, 2));
31
+ }
32
+ /**
33
+ * Add a new channel account
34
+ */
35
+ export async function addChannelAccount(params) {
36
+ const { channelId, channelType, accountName, credentials, metadata } = params;
37
+ const now = Date.now();
38
+ const accountId = `${channelType}-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
39
+ const key = `${channelId}:${accountId}`;
40
+ const store = await loadChannelAccounts();
41
+ const account = {
42
+ id: accountId,
43
+ channelId,
44
+ channelType,
45
+ accountName,
46
+ credentials: credentials || {},
47
+ metadata: metadata || {},
48
+ isActive: false,
49
+ createdAt: now,
50
+ updatedAt: now,
51
+ };
52
+ store.accounts[key] = account;
53
+ await saveChannelAccounts(store);
54
+ return account;
55
+ }
56
+ /**
57
+ * Remove a channel account
58
+ */
59
+ export async function removeChannelAccount(channelId, accountId) {
60
+ const key = `${channelId}:${accountId}`;
61
+ const store = await loadChannelAccounts();
62
+ if (!store.accounts[key]) {
63
+ return false;
64
+ }
65
+ delete store.accounts[key];
66
+ // Also remove from active accounts if set
67
+ if (store.activeAccounts[channelId] === accountId) {
68
+ delete store.activeAccounts[channelId];
69
+ }
70
+ await saveChannelAccounts(store);
71
+ return true;
72
+ }
73
+ /**
74
+ * Get a channel account
75
+ */
76
+ export async function getChannelAccount(channelId, accountId) {
77
+ const key = `${channelId}:${accountId}`;
78
+ const store = await loadChannelAccounts();
79
+ return store.accounts[key];
80
+ }
81
+ /**
82
+ * Get all accounts for a channel
83
+ */
84
+ export async function getChannelAccounts(channelId) {
85
+ const store = await loadChannelAccounts();
86
+ return Object.values(store.accounts).filter((account) => account.channelId === channelId);
87
+ }
88
+ /**
89
+ * Get active account for a channel
90
+ */
91
+ export async function getActiveChannelAccount(channelId) {
92
+ const store = await loadChannelAccounts();
93
+ const activeAccountId = store.activeAccounts[channelId];
94
+ if (!activeAccountId) {
95
+ return undefined;
96
+ }
97
+ const key = `${channelId}:${activeAccountId}`;
98
+ return store.accounts[key];
99
+ }
100
+ /**
101
+ * Set active account for a channel
102
+ */
103
+ export async function setActiveChannelAccount(channelId, accountId) {
104
+ const key = `${channelId}:${accountId}`;
105
+ const store = await loadChannelAccounts();
106
+ if (!store.accounts[key]) {
107
+ return false;
108
+ }
109
+ // Deactivate all accounts for this channel
110
+ Object.values(store.accounts).forEach((account) => {
111
+ if (account.channelId === channelId) {
112
+ account.isActive = false;
113
+ account.updatedAt = Date.now();
114
+ }
115
+ });
116
+ // Activate the selected account
117
+ store.accounts[key].isActive = true;
118
+ store.accounts[key].updatedAt = Date.now();
119
+ store.accounts[key].lastUsedAt = Date.now();
120
+ store.activeAccounts[channelId] = accountId;
121
+ await saveChannelAccounts(store);
122
+ return true;
123
+ }
124
+ /**
125
+ * Update channel account
126
+ */
127
+ export async function updateChannelAccount(params) {
128
+ const { channelId, accountId, accountName, credentials, metadata } = params;
129
+ const key = `${channelId}:${accountId}`;
130
+ const store = await loadChannelAccounts();
131
+ if (!store.accounts[key]) {
132
+ return undefined;
133
+ }
134
+ const account = store.accounts[key];
135
+ if (accountName !== undefined) {
136
+ account.accountName = accountName;
137
+ }
138
+ if (credentials !== undefined) {
139
+ account.credentials = { ...account.credentials, ...credentials };
140
+ }
141
+ if (metadata !== undefined) {
142
+ account.metadata = { ...account.metadata, ...metadata };
143
+ }
144
+ account.updatedAt = Date.now();
145
+ store.accounts[key] = account;
146
+ await saveChannelAccounts(store);
147
+ return account;
148
+ }
149
+ /**
150
+ * List all channel accounts
151
+ */
152
+ export async function listAllChannelAccounts() {
153
+ const store = await loadChannelAccounts();
154
+ return Object.values(store.accounts);
155
+ }
156
+ /**
157
+ * Get account usage statistics
158
+ */
159
+ export async function getChannelAccountStats() {
160
+ const store = await loadChannelAccounts();
161
+ const accounts = Object.values(store.accounts);
162
+ const stats = {
163
+ totalAccounts: accounts.length,
164
+ channelsWithAccounts: new Set(accounts.map((a) => a.channelId)).size,
165
+ activeAccounts: accounts.filter((a) => a.isActive).length,
166
+ accountsByChannelType: {},
167
+ };
168
+ accounts.forEach((account) => {
169
+ stats.accountsByChannelType[account.channelType] =
170
+ (stats.accountsByChannelType[account.channelType] || 0) + 1;
171
+ });
172
+ return stats;
173
+ }
@@ -1,13 +1,46 @@
1
+ import { redactSensitiveText } from "../logging/redact.js";
1
2
  export function extractErrorCode(err) {
2
- if (!err || typeof err !== "object")
3
+ if (!err || typeof err !== "object") {
3
4
  return undefined;
5
+ }
4
6
  const code = err.code;
5
- if (typeof code === "string")
7
+ if (typeof code === "string") {
6
8
  return code;
7
- if (typeof code === "number")
9
+ }
10
+ if (typeof code === "number") {
8
11
  return String(code);
12
+ }
9
13
  return undefined;
10
14
  }
15
+ export function readErrorName(err) {
16
+ if (!err || typeof err !== "object") {
17
+ return "";
18
+ }
19
+ const name = err.name;
20
+ return typeof name === "string" ? name : "";
21
+ }
22
+ export function collectErrorGraphCandidates(err, resolveNested) {
23
+ const queue = [err];
24
+ const seen = new Set();
25
+ const candidates = [];
26
+ while (queue.length > 0) {
27
+ const current = queue.shift();
28
+ if (current == null || seen.has(current)) {
29
+ continue;
30
+ }
31
+ seen.add(current);
32
+ candidates.push(current);
33
+ if (!current || typeof current !== "object" || !resolveNested) {
34
+ continue;
35
+ }
36
+ for (const nested of resolveNested(current)) {
37
+ if (nested != null && !seen.has(nested)) {
38
+ queue.push(nested);
39
+ }
40
+ }
41
+ }
42
+ return candidates;
43
+ }
11
44
  /**
12
45
  * Type guard for NodeJS.ErrnoException (any error with a `code` property).
13
46
  */
@@ -21,27 +54,34 @@ export function hasErrnoCode(err, code) {
21
54
  return isErrno(err) && err.code === code;
22
55
  }
23
56
  export function formatErrorMessage(err) {
57
+ let formatted;
24
58
  if (err instanceof Error) {
25
- return err.message || err.name || "Error";
59
+ formatted = err.message || err.name || "Error";
26
60
  }
27
- if (typeof err === "string")
28
- return err;
29
- if (typeof err === "number" || typeof err === "boolean" || typeof err === "bigint") {
30
- return String(err);
61
+ else if (typeof err === "string") {
62
+ formatted = err;
31
63
  }
32
- try {
33
- return JSON.stringify(err);
64
+ else if (typeof err === "number" || typeof err === "boolean" || typeof err === "bigint") {
65
+ formatted = String(err);
34
66
  }
35
- catch {
36
- return Object.prototype.toString.call(err);
67
+ else {
68
+ try {
69
+ formatted = JSON.stringify(err);
70
+ }
71
+ catch {
72
+ formatted = Object.prototype.toString.call(err);
73
+ }
37
74
  }
75
+ // Security: best-effort token redaction before returning/logging.
76
+ return redactSensitiveText(formatted);
38
77
  }
39
78
  export function formatUncaughtError(err) {
40
79
  if (extractErrorCode(err) === "INVALID_CONFIG") {
41
80
  return formatErrorMessage(err);
42
81
  }
43
82
  if (err instanceof Error) {
44
- return err.stack ?? err.message ?? err.name;
83
+ const stack = err.stack ?? err.message ?? err.name;
84
+ return redactSensitiveText(stack);
45
85
  }
46
86
  return formatErrorMessage(err);
47
87
  }