@poolzin/pool-bot 2026.3.23 → 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 (38) hide show
  1. package/CHANGELOG.md +57 -0
  2. package/dist/.buildstamp +1 -1
  3. package/dist/acp/policy.js +52 -0
  4. package/dist/agents/btw.js +280 -0
  5. package/dist/agents/fast-mode.js +24 -0
  6. package/dist/agents/live-model-errors.js +23 -0
  7. package/dist/agents/model-auth-env-vars.js +44 -0
  8. package/dist/agents/model-auth-markers.js +69 -0
  9. package/dist/agents/models-config.providers.discovery.js +180 -0
  10. package/dist/agents/models-config.providers.static.js +480 -0
  11. package/dist/auto-reply/reply/typing-policy.js +15 -0
  12. package/dist/build-info.json +3 -3
  13. package/dist/channels/account-snapshot-fields.js +176 -0
  14. package/dist/channels/draft-stream-controls.js +89 -0
  15. package/dist/channels/inbound-debounce-policy.js +28 -0
  16. package/dist/channels/typing-lifecycle.js +39 -0
  17. package/dist/cli/program/command-registry.js +52 -0
  18. package/dist/commands/agent-binding.js +123 -0
  19. package/dist/commands/agents.commands.bind.js +280 -0
  20. package/dist/commands/backup-shared.js +186 -0
  21. package/dist/commands/backup-verify.js +236 -0
  22. package/dist/commands/backup.js +166 -0
  23. package/dist/commands/channel-account-context.js +15 -0
  24. package/dist/commands/channel-account.js +190 -0
  25. package/dist/commands/gateway-install-token.js +117 -0
  26. package/dist/commands/oauth-tls-preflight.js +121 -0
  27. package/dist/commands/ollama-setup.js +402 -0
  28. package/dist/commands/self-hosted-provider-setup.js +207 -0
  29. package/dist/commands/session-store-targets.js +12 -0
  30. package/dist/commands/sessions-cleanup.js +97 -0
  31. package/dist/cron/heartbeat-policy.js +26 -0
  32. package/dist/gateway/hooks-mapping.js +46 -7
  33. package/dist/hooks/module-loader.js +28 -0
  34. package/dist/infra/agent-command-binding.js +144 -0
  35. package/dist/infra/backup.js +328 -0
  36. package/dist/infra/channel-account-context.js +173 -0
  37. package/dist/infra/session-cleanup.js +143 -0
  38. package/package.json +1 -1
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Session Cleanup Command
3
+ *
4
+ * Clean up old sessions to stay within disk budget.
5
+ */
6
+ import { cleanupSessions, getSessionCleanupStatus } from "../infra/session-cleanup.js";
7
+ function formatBytes(bytes) {
8
+ if (bytes < 1024)
9
+ return `${bytes} B`;
10
+ if (bytes < 1024 * 1024)
11
+ return `${(bytes / 1024).toFixed(2)} KB`;
12
+ if (bytes < 1024 * 1024 * 1024)
13
+ return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
14
+ return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
15
+ }
16
+ export function registerSessionCleanupCommand(program) {
17
+ const cleanupCmd = program
18
+ .command("sessions-cleanup")
19
+ .description("Clean up old sessions to stay within disk budget");
20
+ // Run cleanup
21
+ cleanupCmd
22
+ .command("run")
23
+ .description("Run session cleanup")
24
+ .option("--max-size <MB>", "Maximum disk usage in MB", "1000")
25
+ .option("--min-keep <count>", "Minimum sessions to keep", "5")
26
+ .option("--dry-run", "Show what would be deleted without actually deleting")
27
+ .action(async (options) => {
28
+ console.log("🎱 Pool Bot - Session Cleanup\n");
29
+ const maxDiskUsageMB = parseInt(options.maxSize);
30
+ const minSessionsToKeep = parseInt(options.minKeep);
31
+ // Show current status
32
+ const status = await getSessionCleanupStatus({
33
+ maxDiskUsageMB,
34
+ });
35
+ console.log("📊 Current Status:");
36
+ console.log(` Sessions: ${status.sessionCount}`);
37
+ console.log(` Disk Usage: ${formatBytes(status.currentUsageMB * 1024 * 1024)}`);
38
+ console.log(` Max Allowed: ${formatBytes(status.maxUsageMB * 1024 * 1024)}`);
39
+ console.log(` Usage: ${status.usagePercent.toFixed(1)}%`);
40
+ console.log(` Over Budget: ${status.overBudget ? "Yes ⚠️" : "No ✅"}\n`);
41
+ if (!status.overBudget) {
42
+ console.log("✅ No cleanup needed - under budget\n");
43
+ return;
44
+ }
45
+ // Run cleanup
46
+ const result = await cleanupSessions({
47
+ maxDiskUsageMB,
48
+ minSessionsToKeep,
49
+ dryRun: options.dryRun,
50
+ });
51
+ if (result.success) {
52
+ if (options.dryRun) {
53
+ console.log("🔍 Dry Run - No changes made\n");
54
+ console.log("📊 Would cleanup:");
55
+ console.log(` - Sessions to delete: ${result.sessionsDeleted}`);
56
+ console.log(` - Space to free: ${formatBytes(result.spaceFreedBytes)}`);
57
+ console.log(` - Sessions remaining: ${result.sessionsRemaining}`);
58
+ }
59
+ else {
60
+ console.log("✅ Cleanup completed successfully!\n");
61
+ console.log("📊 Results:");
62
+ console.log(` - Sessions deleted: ${result.sessionsDeleted}`);
63
+ console.log(` - Space freed: ${formatBytes(result.spaceFreedBytes)}`);
64
+ console.log(` - Sessions remaining: ${result.sessionsRemaining}`);
65
+ console.log(` - Current usage: ${formatBytes(result.currentDiskUsageBytes)}`);
66
+ console.log(` - Duration: ${result.duration}ms`);
67
+ }
68
+ }
69
+ else {
70
+ console.error("❌ Cleanup failed!\n");
71
+ console.error(`Error: ${result.error}`);
72
+ process.exit(1);
73
+ }
74
+ });
75
+ // Show status
76
+ cleanupCmd
77
+ .command("status")
78
+ .description("Show session cleanup status")
79
+ .option("--max-size <MB>", "Maximum disk usage in MB", "1000")
80
+ .action(async (options) => {
81
+ console.log("🎱 Pool Bot - Session Status\n");
82
+ const maxDiskUsageMB = parseInt(options.maxSize);
83
+ const status = await getSessionCleanupStatus({
84
+ maxDiskUsageMB,
85
+ });
86
+ console.log("📊 Session Status:");
87
+ console.log(` Sessions: ${status.sessionCount}`);
88
+ console.log(` Disk Usage: ${formatBytes(status.currentUsageMB * 1024 * 1024)}`);
89
+ console.log(` Max Allowed: ${formatBytes(status.maxUsageMB * 1024 * 1024)}`);
90
+ console.log(` Usage: ${status.usagePercent.toFixed(1)}%`);
91
+ console.log(` Over Budget: ${status.overBudget ? "Yes ⚠️" : "No ✅"}\n`);
92
+ if (status.overBudget) {
93
+ console.log('💡 Run "poolbot sessions-cleanup run" to cleanup old sessions\n');
94
+ }
95
+ });
96
+ return cleanupCmd;
97
+ }
@@ -0,0 +1,26 @@
1
+ import { stripHeartbeatToken } from "../auto-reply/heartbeat.js";
2
+ export function shouldSkipHeartbeatOnlyDelivery(payloads, ackMaxChars) {
3
+ if (payloads.length === 0) {
4
+ return true;
5
+ }
6
+ const hasAnyMedia = payloads.some((payload) => (payload.mediaUrls?.length ?? 0) > 0 || Boolean(payload.mediaUrl));
7
+ if (hasAnyMedia) {
8
+ return false;
9
+ }
10
+ return payloads.some((payload) => {
11
+ const result = stripHeartbeatToken(payload.text, {
12
+ mode: "heartbeat",
13
+ maxAckChars: ackMaxChars,
14
+ });
15
+ return result.shouldSkip;
16
+ });
17
+ }
18
+ export function shouldEnqueueCronMainSummary(params) {
19
+ const summaryText = params.summaryText?.trim();
20
+ return Boolean(summaryText &&
21
+ params.isCronSystemEvent(summaryText) &&
22
+ params.deliveryRequested &&
23
+ !params.delivered &&
24
+ params.deliveryAttempted !== true &&
25
+ !params.suppressMainSummary);
26
+ }
@@ -1,6 +1,7 @@
1
+ import fs from "node:fs";
1
2
  import path from "node:path";
2
- import { pathToFileURL } from "node:url";
3
3
  import { CONFIG_PATH } from "../config/config.js";
4
+ import { importFileModule, resolveFunctionModuleExport } from "../hooks/module-loader.js";
4
5
  const hookPresetMappings = {
5
6
  gmail: [
6
7
  {
@@ -204,15 +205,18 @@ async function loadTransform(transform) {
204
205
  if (cached) {
205
206
  return cached;
206
207
  }
207
- const url = pathToFileURL(transform.modulePath).href;
208
- const mod = (await import(url));
208
+ const mod = await importFileModule({ modulePath: transform.modulePath });
209
209
  const fn = resolveTransformFn(mod, transform.exportName);
210
210
  transformCache.set(cacheKey, fn);
211
211
  return fn;
212
212
  }
213
213
  function resolveTransformFn(mod, exportName) {
214
- const candidate = exportName ? mod[exportName] : (mod.default ?? mod.transform);
215
- if (typeof candidate !== "function") {
214
+ const candidate = resolveFunctionModuleExport({
215
+ mod,
216
+ exportName,
217
+ fallbackExportNames: ["default", "transform"],
218
+ });
219
+ if (!candidate) {
216
220
  throw new Error("hook transform module must export a function");
217
221
  }
218
222
  return candidate;
@@ -223,6 +227,32 @@ function resolvePath(baseDir, target) {
223
227
  }
224
228
  return path.isAbsolute(target) ? path.resolve(target) : path.resolve(baseDir, target);
225
229
  }
230
+ function escapesBase(baseDir, candidate) {
231
+ const relative = path.relative(baseDir, candidate);
232
+ return relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative);
233
+ }
234
+ function safeRealpathSync(candidate) {
235
+ try {
236
+ const nativeRealpath = fs.realpathSync.native;
237
+ return nativeRealpath ? nativeRealpath(candidate) : fs.realpathSync(candidate);
238
+ }
239
+ catch {
240
+ return null;
241
+ }
242
+ }
243
+ function resolveExistingAncestor(candidate) {
244
+ let current = path.resolve(candidate);
245
+ while (true) {
246
+ if (fs.existsSync(current)) {
247
+ return current;
248
+ }
249
+ const parent = path.dirname(current);
250
+ if (parent === current) {
251
+ return null;
252
+ }
253
+ current = parent;
254
+ }
255
+ }
226
256
  function resolveContainedPath(baseDir, target, label) {
227
257
  const base = path.resolve(baseDir);
228
258
  const trimmed = target?.trim();
@@ -230,8 +260,17 @@ function resolveContainedPath(baseDir, target, label) {
230
260
  throw new Error(`${label} module path is required`);
231
261
  }
232
262
  const resolved = resolvePath(base, trimmed);
233
- const relative = path.relative(base, resolved);
234
- if (relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
263
+ if (escapesBase(base, resolved)) {
264
+ throw new Error(`${label} module path must be within ${base}: ${target}`);
265
+ }
266
+ // Block symlink escapes for existing path segments while preserving current
267
+ // behavior for not-yet-created files.
268
+ const baseRealpath = safeRealpathSync(base);
269
+ const existingAncestor = resolveExistingAncestor(resolved);
270
+ const existingAncestorRealpath = existingAncestor ? safeRealpathSync(existingAncestor) : null;
271
+ if (baseRealpath &&
272
+ existingAncestorRealpath &&
273
+ escapesBase(baseRealpath, existingAncestorRealpath)) {
235
274
  throw new Error(`${label} module path must be within ${base}: ${target}`);
236
275
  }
237
276
  return resolved;
@@ -0,0 +1,28 @@
1
+ import { pathToFileURL } from "node:url";
2
+ export function resolveFileModuleUrl(params) {
3
+ const url = pathToFileURL(params.modulePath).href;
4
+ if (!params.cacheBust) {
5
+ return url;
6
+ }
7
+ const ts = params.nowMs ?? Date.now();
8
+ return `${url}?t=${ts}`;
9
+ }
10
+ export async function importFileModule(params) {
11
+ const specifier = resolveFileModuleUrl(params);
12
+ return (await import(specifier));
13
+ }
14
+ export function resolveFunctionModuleExport(params) {
15
+ const explicitExport = params.exportName?.trim();
16
+ if (explicitExport) {
17
+ const candidate = params.mod[explicitExport];
18
+ return typeof candidate === "function" ? candidate : undefined;
19
+ }
20
+ const fallbacks = params.fallbackExportNames ?? ["default"];
21
+ for (const exportName of fallbacks) {
22
+ const candidate = params.mod[exportName];
23
+ if (typeof candidate === "function") {
24
+ return candidate;
25
+ }
26
+ }
27
+ return undefined;
28
+ }
@@ -0,0 +1,144 @@
1
+ /**
2
+ * Agent Command Binding System
3
+ *
4
+ * Allows binding specific commands to agents.
5
+ * Implemented from scratch for Pool Bot architecture.
6
+ */
7
+ import fs from "node:fs/promises";
8
+ import path from "node:path";
9
+ const BINDINGS_FILE = path.join(process.cwd(), ".poolbot", "agent-command-bindings.json");
10
+ /**
11
+ * Load command bindings from file
12
+ */
13
+ export async function loadCommandBindings() {
14
+ try {
15
+ const data = await fs.readFile(BINDINGS_FILE, "utf-8");
16
+ return JSON.parse(data);
17
+ }
18
+ catch (error) {
19
+ if (error.code === "ENOENT") {
20
+ return { bindings: {} };
21
+ }
22
+ throw error;
23
+ }
24
+ }
25
+ /**
26
+ * Save command bindings to file
27
+ */
28
+ export async function saveCommandBindings(store) {
29
+ await fs.mkdir(path.dirname(BINDINGS_FILE), { recursive: true });
30
+ await fs.writeFile(BINDINGS_FILE, JSON.stringify(store, null, 2));
31
+ }
32
+ /**
33
+ * Bind commands to an agent
34
+ */
35
+ export async function bindCommandsToAgent(params) {
36
+ const { agentId, commands, allowlist = true } = params;
37
+ const now = Date.now();
38
+ const store = await loadCommandBindings();
39
+ const existing = store.bindings[agentId];
40
+ const binding = {
41
+ agentId,
42
+ commands: [...new Set(commands)], // Remove duplicates
43
+ allowlist,
44
+ createdAt: existing?.createdAt || now,
45
+ updatedAt: now,
46
+ };
47
+ store.bindings[agentId] = binding;
48
+ await saveCommandBindings(store);
49
+ return binding;
50
+ }
51
+ /**
52
+ * Unbind commands from an agent
53
+ */
54
+ export async function unbindCommandsFromAgent(agentId) {
55
+ const store = await loadCommandBindings();
56
+ if (!store.bindings[agentId]) {
57
+ return false;
58
+ }
59
+ delete store.bindings[agentId];
60
+ await saveCommandBindings(store);
61
+ return true;
62
+ }
63
+ /**
64
+ * Get command bindings for an agent
65
+ */
66
+ export async function getAgentBindings(agentId) {
67
+ const store = await loadCommandBindings();
68
+ return store.bindings[agentId];
69
+ }
70
+ /**
71
+ * Get all command bindings
72
+ */
73
+ export async function getAllBindings() {
74
+ const store = await loadCommandBindings();
75
+ return Object.values(store.bindings);
76
+ }
77
+ /**
78
+ * Check if a command is allowed for an agent
79
+ */
80
+ export async function isCommandAllowedForAgent(params) {
81
+ const { agentId, command } = params;
82
+ const binding = await getAgentBindings(agentId);
83
+ // No binding = all commands allowed
84
+ if (!binding) {
85
+ return { allowed: true, reason: "No binding - all commands allowed" };
86
+ }
87
+ const commandExists = binding.commands.includes(command);
88
+ if (binding.allowlist) {
89
+ // Allowlist mode: only these commands are allowed
90
+ if (commandExists) {
91
+ return { allowed: true, reason: "Command in allowlist" };
92
+ }
93
+ else {
94
+ return { allowed: false, reason: "Command not in allowlist" };
95
+ }
96
+ }
97
+ else {
98
+ // Denylist mode: all commands allowed except these
99
+ if (commandExists) {
100
+ return { allowed: false, reason: "Command in denylist" };
101
+ }
102
+ else {
103
+ return { allowed: true, reason: "Command not in denylist" };
104
+ }
105
+ }
106
+ }
107
+ /**
108
+ * Add commands to an agent's binding
109
+ */
110
+ export async function addCommandsToAgent(params) {
111
+ const { agentId, commands } = params;
112
+ const binding = await getAgentBindings(agentId);
113
+ if (!binding) {
114
+ return bindCommandsToAgent({ agentId, commands });
115
+ }
116
+ const updatedCommands = [...new Set([...binding.commands, ...commands])];
117
+ return bindCommandsToAgent({ agentId, commands: updatedCommands, allowlist: binding.allowlist });
118
+ }
119
+ /**
120
+ * Remove commands from an agent's binding
121
+ */
122
+ export async function removeCommandsFromAgent(params) {
123
+ const { agentId, commands } = params;
124
+ const binding = await getAgentBindings(agentId);
125
+ if (!binding) {
126
+ throw new Error(`No binding found for agent ${agentId}`);
127
+ }
128
+ const updatedCommands = binding.commands.filter((cmd) => !commands.includes(cmd));
129
+ return bindCommandsToAgent({ agentId, commands: updatedCommands, allowlist: binding.allowlist });
130
+ }
131
+ /**
132
+ * Toggle binding mode (allowlist/denylist)
133
+ */
134
+ export async function toggleBindingMode(agentId) {
135
+ const binding = await getAgentBindings(agentId);
136
+ if (!binding) {
137
+ throw new Error(`No binding found for agent ${agentId}`);
138
+ }
139
+ return bindCommandsToAgent({
140
+ agentId,
141
+ commands: binding.commands,
142
+ allowlist: !binding.allowlist,
143
+ });
144
+ }