@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.
- package/CHANGELOG.md +57 -0
- package/dist/.buildstamp +1 -1
- package/dist/acp/policy.js +52 -0
- package/dist/agents/btw.js +280 -0
- package/dist/agents/fast-mode.js +24 -0
- package/dist/agents/live-model-errors.js +23 -0
- package/dist/agents/model-auth-env-vars.js +44 -0
- package/dist/agents/model-auth-markers.js +69 -0
- package/dist/agents/models-config.providers.discovery.js +180 -0
- package/dist/agents/models-config.providers.static.js +480 -0
- package/dist/auto-reply/reply/typing-policy.js +15 -0
- package/dist/build-info.json +3 -3
- package/dist/channels/account-snapshot-fields.js +176 -0
- package/dist/channels/draft-stream-controls.js +89 -0
- package/dist/channels/inbound-debounce-policy.js +28 -0
- package/dist/channels/typing-lifecycle.js +39 -0
- package/dist/cli/program/command-registry.js +52 -0
- package/dist/commands/agent-binding.js +123 -0
- package/dist/commands/agents.commands.bind.js +280 -0
- package/dist/commands/backup-shared.js +186 -0
- package/dist/commands/backup-verify.js +236 -0
- package/dist/commands/backup.js +166 -0
- package/dist/commands/channel-account-context.js +15 -0
- package/dist/commands/channel-account.js +190 -0
- package/dist/commands/gateway-install-token.js +117 -0
- package/dist/commands/oauth-tls-preflight.js +121 -0
- package/dist/commands/ollama-setup.js +402 -0
- package/dist/commands/self-hosted-provider-setup.js +207 -0
- package/dist/commands/session-store-targets.js +12 -0
- package/dist/commands/sessions-cleanup.js +97 -0
- package/dist/cron/heartbeat-policy.js +26 -0
- package/dist/gateway/hooks-mapping.js +46 -7
- package/dist/hooks/module-loader.js +28 -0
- package/dist/infra/agent-command-binding.js +144 -0
- package/dist/infra/backup.js +328 -0
- package/dist/infra/channel-account-context.js +173 -0
- package/dist/infra/session-cleanup.js +143 -0
- 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
|
|
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 =
|
|
215
|
-
|
|
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
|
-
|
|
234
|
-
|
|
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
|
+
}
|