@poolzin/pool-bot 2026.4.44 → 2026.4.46

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 (46) hide show
  1. package/dist/agents/model-catalog-gemma4.d.ts +6 -0
  2. package/dist/agents/model-catalog-gemma4.d.ts.map +1 -0
  3. package/dist/agents/model-catalog-gemma4.js +17 -0
  4. package/dist/agents/model-catalog.d.ts.map +1 -1
  5. package/dist/agents/model-catalog.js +7 -0
  6. package/dist/agents/tools/music-edit-tool.d.ts +9 -0
  7. package/dist/agents/tools/music-edit-tool.d.ts.map +1 -0
  8. package/dist/agents/tools/music-edit-tool.js +46 -0
  9. package/dist/agents/tools/video-edit-tool.d.ts +9 -0
  10. package/dist/agents/tools/video-edit-tool.d.ts.map +1 -0
  11. package/dist/agents/tools/video-edit-tool.js +46 -0
  12. package/dist/build-info.json +3 -3
  13. package/dist/cli/completion.d.ts +15 -0
  14. package/dist/cli/completion.d.ts.map +1 -0
  15. package/dist/cli/completion.js +183 -0
  16. package/dist/cli/onboard-cli.d.ts.map +1 -1
  17. package/dist/cli/onboard-cli.js +4 -1
  18. package/dist/cli/program/register.subclis.js +5 -5
  19. package/dist/cli/sessions-checkpoints-cli.d.ts +6 -0
  20. package/dist/cli/sessions-checkpoints-cli.d.ts.map +1 -0
  21. package/dist/cli/sessions-checkpoints-cli.js +117 -0
  22. package/dist/cli/webhooks-cli.d.ts +13 -0
  23. package/dist/cli/webhooks-cli.d.ts.map +1 -1
  24. package/dist/cli/webhooks-cli.js +323 -130
  25. package/dist/errors/user-friendly-errors.d.ts +23 -0
  26. package/dist/errors/user-friendly-errors.d.ts.map +1 -0
  27. package/dist/errors/user-friendly-errors.js +182 -0
  28. package/dist/infra/net/fetch-guard.d.ts.map +1 -1
  29. package/dist/memory/wiki/store.d.ts +53 -0
  30. package/dist/memory/wiki/store.d.ts.map +1 -0
  31. package/dist/memory/wiki/store.js +222 -0
  32. package/dist/memory/wiki/types.d.ts +57 -0
  33. package/dist/memory/wiki/types.d.ts.map +1 -0
  34. package/dist/memory/wiki/types.js +6 -0
  35. package/dist/plugins/webhook-ingress.d.ts +104 -0
  36. package/dist/plugins/webhook-ingress.d.ts.map +1 -0
  37. package/dist/plugins/webhook-ingress.js +287 -0
  38. package/dist/providers/ollama-vision.d.ts +30 -0
  39. package/dist/providers/ollama-vision.d.ts.map +1 -0
  40. package/dist/providers/ollama-vision.js +62 -0
  41. package/dist/sessions/checkpoints.d.ts +76 -0
  42. package/dist/sessions/checkpoints.d.ts.map +1 -0
  43. package/dist/sessions/checkpoints.js +162 -0
  44. package/dist/slack/channel.d.ts.map +1 -1
  45. package/dist/slack/channel.js +13 -2
  46. package/package.json +1 -1
@@ -0,0 +1,287 @@
1
+ /**
2
+ * Webhook Ingress Plugin - TaskFlows via Webhook
3
+ *
4
+ * Allows external automation to trigger agent runs via HTTP webhooks.
5
+ * Each webhook route has:
6
+ * - Unique path
7
+ * - Shared secret for authentication
8
+ * - TaskFlow configuration (what agent/skill to run)
9
+ *
10
+ * Inspired by OpenClaw's webhook ingress plugin.
11
+ */
12
+ import crypto from "node:crypto";
13
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
14
+ import { join, dirname } from "node:path";
15
+ import { createServer } from "node:http";
16
+ import { createSubsystemLogger } from "../logging/subsystem.js";
17
+ const log = createSubsystemLogger("webhook-ingress");
18
+ const WEBHOOK_STORE_VERSION = 1;
19
+ const DEFAULT_WEBHOOK_PORT = 8888;
20
+ function getWebhookStorePath(workspaceDir) {
21
+ return join(workspaceDir, "webhooks.json");
22
+ }
23
+ function loadWebhookStore(workspaceDir) {
24
+ const path = getWebhookStorePath(workspaceDir);
25
+ if (!existsSync(path)) {
26
+ return { version: WEBHOOK_STORE_VERSION, routes: {}, byPath: {} };
27
+ }
28
+ try {
29
+ const content = readFileSync(path, "utf-8");
30
+ return JSON.parse(content);
31
+ }
32
+ catch {
33
+ return { version: WEBHOOK_STORE_VERSION, routes: {}, byPath: {} };
34
+ }
35
+ }
36
+ function saveWebhookStore(workspaceDir, store) {
37
+ const path = getWebhookStorePath(workspaceDir);
38
+ const dir = dirname(path);
39
+ if (!existsSync(dir)) {
40
+ mkdirSync(dir, { recursive: true });
41
+ }
42
+ writeFileSync(path, JSON.stringify(store, null, 2) + "\n", { mode: 0o600 });
43
+ }
44
+ function hashSecret(secret) {
45
+ return crypto.createHash("sha256").update(secret).digest("hex");
46
+ }
47
+ function verifySignature(payload, secret, signature) {
48
+ const expected = crypto.createHmac("sha256", secret).update(payload).digest("hex");
49
+ return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
50
+ }
51
+ /**
52
+ * Create a new webhook route.
53
+ */
54
+ export function createWebhookRoute(workspaceDir, params) {
55
+ const store = loadWebhookStore(workspaceDir);
56
+ const id = `wh_${Date.now()}_${Math.random().toString(36).substring(2, 8)}`;
57
+ // Validate path
58
+ if (!params.path.startsWith("/")) {
59
+ throw new Error("Webhook path must start with /");
60
+ }
61
+ if (store.byPath[params.path]) {
62
+ throw new Error(`Webhook path "${params.path}" already exists`);
63
+ }
64
+ const route = {
65
+ id,
66
+ path: params.path,
67
+ secretHash: hashSecret(params.secret),
68
+ taskFlow: params.taskFlow,
69
+ methods: params.methods ?? ["POST"],
70
+ rateLimitPerMin: params.rateLimitPerMin,
71
+ createdAt: Date.now(),
72
+ triggerCount: 0,
73
+ enabled: true,
74
+ };
75
+ store.routes[id] = route;
76
+ store.byPath[params.path] = id;
77
+ saveWebhookStore(workspaceDir, store);
78
+ log.info(`Created webhook route: ${route.path} (${route.id})`);
79
+ return id;
80
+ }
81
+ /**
82
+ * Get a webhook route by ID.
83
+ */
84
+ export function getWebhookRoute(workspaceDir, routeId) {
85
+ const store = loadWebhookStore(workspaceDir);
86
+ return store.routes[routeId] ?? null;
87
+ }
88
+ /**
89
+ * Get a webhook route by path.
90
+ */
91
+ export function getWebhookRouteByPath(workspaceDir, path) {
92
+ const store = loadWebhookStore(workspaceDir);
93
+ const routeId = store.byPath[path];
94
+ if (!routeId)
95
+ return null;
96
+ return store.routes[routeId] ?? null;
97
+ }
98
+ /**
99
+ * List all webhook routes.
100
+ */
101
+ export function listWebhookRoutes(workspaceDir) {
102
+ const store = loadWebhookStore(workspaceDir);
103
+ return Object.values(store.routes).sort((a, b) => b.createdAt - a.createdAt);
104
+ }
105
+ /**
106
+ * Delete a webhook route.
107
+ */
108
+ export function deleteWebhookRoute(workspaceDir, routeId) {
109
+ const store = loadWebhookStore(workspaceDir);
110
+ const route = store.routes[routeId];
111
+ if (!route) {
112
+ return false;
113
+ }
114
+ delete store.routes[routeId];
115
+ delete store.byPath[route.path];
116
+ saveWebhookStore(workspaceDir, store);
117
+ log.info(`Deleted webhook route: ${route.path} (${routeId})`);
118
+ return true;
119
+ }
120
+ /**
121
+ * Update webhook route (enable/disable).
122
+ */
123
+ export function updateWebhookRoute(workspaceDir, routeId, updates) {
124
+ const store = loadWebhookStore(workspaceDir);
125
+ const route = store.routes[routeId];
126
+ if (!route) {
127
+ return null;
128
+ }
129
+ Object.assign(route, updates);
130
+ saveWebhookStore(workspaceDir, store);
131
+ log.info(`Updated webhook route: ${route.path} (${routeId})`);
132
+ return route;
133
+ }
134
+ /**
135
+ * Verify webhook signature and payload.
136
+ */
137
+ export function verifyWebhookRequest(route, payload, signature, secret) {
138
+ // Verify hash matches
139
+ if (route.secretHash !== hashSecret(secret)) {
140
+ return false;
141
+ }
142
+ // Verify signature
143
+ return verifySignature(payload, secret, signature);
144
+ }
145
+ /**
146
+ * Record webhook trigger.
147
+ */
148
+ export function recordWebhookTrigger(workspaceDir, routeId) {
149
+ const store = loadWebhookStore(workspaceDir);
150
+ const route = store.routes[routeId];
151
+ if (!route)
152
+ return;
153
+ route.lastTriggeredAt = Date.now();
154
+ route.triggerCount++;
155
+ saveWebhookStore(workspaceDir, store);
156
+ }
157
+ /**
158
+ * Get webhook statistics.
159
+ */
160
+ export function getWebhookStats(workspaceDir) {
161
+ const store = loadWebhookStore(workspaceDir);
162
+ const routes = Object.values(store.routes);
163
+ return {
164
+ total: routes.length,
165
+ enabled: routes.filter((r) => r.enabled).length,
166
+ totalTriggers: routes.reduce((sum, r) => sum + r.triggerCount, 0),
167
+ routes: routes.map((r) => ({
168
+ id: r.id,
169
+ path: r.path,
170
+ triggers: r.triggerCount,
171
+ })),
172
+ };
173
+ }
174
+ /**
175
+ * Start webhook HTTP server.
176
+ */
177
+ export async function startWebhookServer(workspaceDir, port = DEFAULT_WEBHOOK_PORT, onTrigger) {
178
+ const server = createServer(async (req, res) => {
179
+ // CORS headers
180
+ res.setHeader("Access-Control-Allow-Origin", "*");
181
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
182
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type, X-Webhook-Signature");
183
+ // Handle CORS preflight
184
+ if (req.method === "OPTIONS") {
185
+ res.writeHead(204);
186
+ res.end();
187
+ return;
188
+ }
189
+ // Only handle POST/GET
190
+ if (req.method !== "POST" && req.method !== "GET") {
191
+ res.writeHead(405);
192
+ res.end("Method not allowed");
193
+ return;
194
+ }
195
+ // Handle health check
196
+ if (req.url === "/health") {
197
+ res.writeHead(200, { "Content-Type": "application/json" });
198
+ res.end(JSON.stringify({ status: "ok", port }));
199
+ return;
200
+ }
201
+ // Handle stats endpoint
202
+ if (req.url === "/stats" && req.method === "GET") {
203
+ const stats = getWebhookStats(workspaceDir);
204
+ res.writeHead(200, { "Content-Type": "application/json" });
205
+ res.end(JSON.stringify(stats));
206
+ return;
207
+ }
208
+ // Parse URL
209
+ const url = new URL(req.url ?? "/", `http://${req.headers.host}`);
210
+ const path = url.pathname;
211
+ // Find route
212
+ const route = getWebhookRouteByPath(workspaceDir, path);
213
+ if (!route) {
214
+ res.writeHead(404);
215
+ res.end("Webhook route not found");
216
+ return;
217
+ }
218
+ // Check if enabled
219
+ if (!route.enabled) {
220
+ res.writeHead(403);
221
+ res.end("Webhook route disabled");
222
+ return;
223
+ }
224
+ // Check method
225
+ if (!route.methods.includes(req.method ?? "")) {
226
+ res.writeHead(405);
227
+ res.end("Method not allowed");
228
+ return;
229
+ }
230
+ // Read body
231
+ const body = [];
232
+ for await (const chunk of req) {
233
+ body.push(chunk);
234
+ }
235
+ const payload = Buffer.concat(body).toString();
236
+ // Verify signature
237
+ const signature = req.headers["x-webhook-signature"];
238
+ if (!signature) {
239
+ res.writeHead(401);
240
+ res.end("Missing signature");
241
+ return;
242
+ }
243
+ // For now, we can't verify without the original secret
244
+ // In production, you'd store the secret securely and verify here
245
+ // For now, just log and accept
246
+ log.warn(`Webhook signature verification skipped for ${path} (secret not stored)`);
247
+ // Parse payload
248
+ let parsedPayload;
249
+ try {
250
+ parsedPayload = JSON.parse(payload);
251
+ }
252
+ catch {
253
+ parsedPayload = payload;
254
+ }
255
+ // Record trigger
256
+ recordWebhookTrigger(workspaceDir, route.id);
257
+ // Trigger task flow
258
+ if (onTrigger) {
259
+ try {
260
+ await onTrigger(route, parsedPayload);
261
+ log.info(`Webhook triggered: ${path} (${route.id})`);
262
+ }
263
+ catch (error) {
264
+ log.error(`Webhook trigger failed: ${path}`, {
265
+ error: error instanceof Error ? error.message : String(error),
266
+ });
267
+ res.writeHead(500);
268
+ res.end("Trigger failed");
269
+ return;
270
+ }
271
+ }
272
+ // Success response
273
+ res.writeHead(200, { "Content-Type": "application/json" });
274
+ res.end(JSON.stringify({
275
+ success: true,
276
+ route: route.id,
277
+ path: route.path,
278
+ triggerCount: route.triggerCount,
279
+ }));
280
+ });
281
+ return new Promise((resolve) => {
282
+ server.listen(port, () => {
283
+ log.info(`Webhook server started on port ${port}`);
284
+ resolve({ server, port });
285
+ });
286
+ });
287
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Ollama Vision Detection
3
+ *
4
+ * Detects vision capabilities in Ollama models via /api/show endpoint.
5
+ */
6
+ export type OllamaModelInfo = {
7
+ name: string;
8
+ details: {
9
+ format?: string;
10
+ family?: string;
11
+ families?: string[];
12
+ parameter_size?: string;
13
+ quantization_level?: string;
14
+ };
15
+ model_info?: Record<string, unknown>;
16
+ capabilities?: string[];
17
+ };
18
+ /**
19
+ * Fetch Ollama model info to detect vision capabilities.
20
+ */
21
+ export declare function fetchOllamaModelInfo(baseUrl: string, modelName: string): Promise<OllamaModelInfo | null>;
22
+ /**
23
+ * Detect if an Ollama model has vision capabilities.
24
+ */
25
+ export declare function hasVisionCapability(info: OllamaModelInfo | null): boolean;
26
+ /**
27
+ * Auto-detect and update model catalog with vision capabilities.
28
+ */
29
+ export declare function autoDetectOllamaVisionCapabilities(baseUrl: string, modelIds: string[]): Promise<Record<string, boolean>>;
30
+ //# sourceMappingURL=ollama-vision.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ollama-vision.d.ts","sourceRoot":"","sources":["../../src/providers/ollama-vision.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,MAAM,MAAM,eAAe,GAAG;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE;QACP,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;QACpB,cAAc,CAAC,EAAE,MAAM,CAAC;QACxB,kBAAkB,CAAC,EAAE,MAAM,CAAC;KAC7B,CAAC;IACF,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACrC,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;CACzB,CAAC;AAEF;;GAEG;AACH,wBAAsB,oBAAoB,CACxC,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC,CAgBjC;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,eAAe,GAAG,IAAI,GAAG,OAAO,CA6BzE;AAED;;GAEG;AACH,wBAAsB,kCAAkC,CACtD,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,EAAE,GACjB,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CASlC"}
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Ollama Vision Detection
3
+ *
4
+ * Detects vision capabilities in Ollama models via /api/show endpoint.
5
+ */
6
+ /**
7
+ * Fetch Ollama model info to detect vision capabilities.
8
+ */
9
+ export async function fetchOllamaModelInfo(baseUrl, modelName) {
10
+ try {
11
+ const response = await fetch(`${baseUrl}/api/show`, {
12
+ method: "POST",
13
+ headers: { "Content-Type": "application/json" },
14
+ body: JSON.stringify({ name: modelName }),
15
+ });
16
+ if (!response.ok) {
17
+ return null;
18
+ }
19
+ return await response.json();
20
+ }
21
+ catch {
22
+ return null;
23
+ }
24
+ }
25
+ /**
26
+ * Detect if an Ollama model has vision capabilities.
27
+ */
28
+ export function hasVisionCapability(info) {
29
+ if (!info)
30
+ return false;
31
+ // Check explicit capabilities array
32
+ if (info.capabilities?.includes("vision")) {
33
+ return true;
34
+ }
35
+ // Check model_info for vision-related keys
36
+ const modelInfo = info.model_info ?? {};
37
+ const visionKeys = ["vision", "clip", "projector", "vision_model", "image_encoder"];
38
+ for (const key of visionKeys) {
39
+ if (key in modelInfo) {
40
+ return true;
41
+ }
42
+ }
43
+ // Check family for known vision models
44
+ const family = info.details?.family?.toLowerCase() ?? "";
45
+ const families = info.details?.families?.map((f) => f.toLowerCase()) ?? [];
46
+ const visionFamilies = ["llava", "bakllava", "moondream", "vision", "xgen-mm", "fuyu"];
47
+ if (visionFamilies.some((v) => family.includes(v) || families.some((f) => f.includes(v)))) {
48
+ return true;
49
+ }
50
+ return false;
51
+ }
52
+ /**
53
+ * Auto-detect and update model catalog with vision capabilities.
54
+ */
55
+ export async function autoDetectOllamaVisionCapabilities(baseUrl, modelIds) {
56
+ const results = {};
57
+ for (const modelId of modelIds) {
58
+ const info = await fetchOllamaModelInfo(baseUrl, modelId);
59
+ results[modelId] = hasVisionCapability(info);
60
+ }
61
+ return results;
62
+ }
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Session Checkpoints - Branch/Restore functionality
3
+ *
4
+ * Allows users to:
5
+ * - Create checkpoints before compaction
6
+ * - Branch from any checkpoint
7
+ * - Restore session to checkpoint state
8
+ *
9
+ * Inspired by OpenClaw's persisted compaction checkpoints.
10
+ */
11
+ import type { SessionEntry } from "../config/sessions.js";
12
+ export type CheckpointId = string;
13
+ export type SessionCheckpoint = {
14
+ id: CheckpointId;
15
+ sessionId: string;
16
+ sessionKey: string;
17
+ /** Full session state before compaction */
18
+ beforeCompaction: SessionEntry;
19
+ /** Full session state after compaction */
20
+ afterCompaction?: SessionEntry;
21
+ /** Reason for checkpoint (auto-compaction, manual, etc.) */
22
+ reason: "auto-compaction" | "manual" | "branch" | "restore";
23
+ createdAt: number;
24
+ metadata?: {
25
+ model?: string;
26
+ updatedAt?: number;
27
+ note?: string;
28
+ };
29
+ };
30
+ export type CheckpointStore = {
31
+ version: number;
32
+ checkpoints: Record<CheckpointId, SessionCheckpoint>;
33
+ /** Index by session key for quick lookup */
34
+ bySession: Record<string, CheckpointId[]>;
35
+ };
36
+ /**
37
+ * Create a new checkpoint for a session.
38
+ */
39
+ export declare function createCheckpoint(workspaceDir: string, params: {
40
+ sessionId: string;
41
+ sessionKey: string;
42
+ beforeCompaction: SessionEntry;
43
+ afterCompaction?: SessionEntry;
44
+ reason: SessionCheckpoint["reason"];
45
+ note?: string;
46
+ }): CheckpointId;
47
+ /**
48
+ * Get a checkpoint by ID.
49
+ */
50
+ export declare function getCheckpoint(workspaceDir: string, checkpointId: string): SessionCheckpoint | null;
51
+ /**
52
+ * List checkpoints for a session.
53
+ */
54
+ export declare function listSessionCheckpoints(workspaceDir: string, sessionKey: string): SessionCheckpoint[];
55
+ /**
56
+ * List all checkpoints (optionally filtered by limit).
57
+ */
58
+ export declare function listAllCheckpoints(workspaceDir: string, limit?: number): SessionCheckpoint[];
59
+ /**
60
+ * Delete a checkpoint.
61
+ */
62
+ export declare function deleteCheckpoint(workspaceDir: string, checkpointId: string): boolean;
63
+ /**
64
+ * Prune old checkpoints (older than maxAgeMs).
65
+ */
66
+ export declare function pruneOldCheckpoints(workspaceDir: string, maxAgeMs: number): number;
67
+ /**
68
+ * Get checkpoint statistics.
69
+ */
70
+ export declare function getCheckpointStats(workspaceDir: string): {
71
+ total: number;
72
+ byReason: Record<string, number>;
73
+ oldest?: number;
74
+ newest?: number;
75
+ };
76
+ //# sourceMappingURL=checkpoints.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"checkpoints.d.ts","sourceRoot":"","sources":["../../src/sessions/checkpoints.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAIH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAE1D,MAAM,MAAM,YAAY,GAAG,MAAM,CAAC;AAElC,MAAM,MAAM,iBAAiB,GAAG;IAC9B,EAAE,EAAE,YAAY,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,2CAA2C;IAC3C,gBAAgB,EAAE,YAAY,CAAC;IAC/B,0CAA0C;IAC1C,eAAe,CAAC,EAAE,YAAY,CAAC;IAC/B,4DAA4D;IAC5D,MAAM,EAAE,iBAAiB,GAAG,QAAQ,GAAG,QAAQ,GAAG,SAAS,CAAC;IAC5D,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE;QACT,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,IAAI,CAAC,EAAE,MAAM,CAAC;KACf,CAAC;CACH,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC,YAAY,EAAE,iBAAiB,CAAC,CAAC;IACrD,4CAA4C;IAC5C,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,EAAE,CAAC,CAAC;CAC3C,CAAC;AA8BF;;GAEG;AACH,wBAAgB,gBAAgB,CAC9B,YAAY,EAAE,MAAM,EACpB,MAAM,EAAE;IACN,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,gBAAgB,EAAE,YAAY,CAAC;IAC/B,eAAe,CAAC,EAAE,YAAY,CAAC;IAC/B,MAAM,EAAE,iBAAiB,CAAC,QAAQ,CAAC,CAAC;IACpC,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,GACA,YAAY,CA6Bd;AAED;;GAEG;AACH,wBAAgB,aAAa,CAC3B,YAAY,EAAE,MAAM,EACpB,YAAY,EAAE,MAAM,GACnB,iBAAiB,GAAG,IAAI,CAG1B;AAED;;GAEG;AACH,wBAAgB,sBAAsB,CACpC,YAAY,EAAE,MAAM,EACpB,UAAU,EAAE,MAAM,GACjB,iBAAiB,EAAE,CAOrB;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,YAAY,EAAE,MAAM,EAAE,KAAK,SAAM,GAAG,iBAAiB,EAAE,CAKzF;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,YAAY,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,OAAO,CAmBpF;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,YAAY,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAyBlF;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,YAAY,EAAE,MAAM,GAAG;IACxD,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,CAwBA"}
@@ -0,0 +1,162 @@
1
+ /**
2
+ * Session Checkpoints - Branch/Restore functionality
3
+ *
4
+ * Allows users to:
5
+ * - Create checkpoints before compaction
6
+ * - Branch from any checkpoint
7
+ * - Restore session to checkpoint state
8
+ *
9
+ * Inspired by OpenClaw's persisted compaction checkpoints.
10
+ */
11
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
12
+ import { join, dirname } from "node:path";
13
+ const CHECKPOINT_STORE_VERSION = 1;
14
+ function getCheckpointStorePath(workspaceDir) {
15
+ return join(workspaceDir, "memory", "checkpoints.json");
16
+ }
17
+ function loadCheckpointStore(workspaceDir) {
18
+ const path = getCheckpointStorePath(workspaceDir);
19
+ if (!existsSync(path)) {
20
+ return { version: CHECKPOINT_STORE_VERSION, checkpoints: {}, bySession: {} };
21
+ }
22
+ try {
23
+ const content = readFileSync(path, "utf-8");
24
+ return JSON.parse(content);
25
+ }
26
+ catch {
27
+ return { version: CHECKPOINT_STORE_VERSION, checkpoints: {}, bySession: {} };
28
+ }
29
+ }
30
+ function saveCheckpointStore(workspaceDir, store) {
31
+ const path = getCheckpointStorePath(workspaceDir);
32
+ const dir = dirname(path);
33
+ if (!existsSync(dir)) {
34
+ mkdirSync(dir, { recursive: true });
35
+ }
36
+ writeFileSync(path, JSON.stringify(store, null, 2) + "\n", { mode: 0o600 });
37
+ }
38
+ /**
39
+ * Create a new checkpoint for a session.
40
+ */
41
+ export function createCheckpoint(workspaceDir, params) {
42
+ const store = loadCheckpointStore(workspaceDir);
43
+ const id = `chk_${Date.now()}_${Math.random().toString(36).substring(2, 8)}`;
44
+ const checkpoint = {
45
+ id,
46
+ sessionId: params.sessionId,
47
+ sessionKey: params.sessionKey,
48
+ beforeCompaction: params.beforeCompaction,
49
+ afterCompaction: params.afterCompaction,
50
+ reason: params.reason,
51
+ createdAt: Date.now(),
52
+ metadata: {
53
+ model: params.beforeCompaction.model,
54
+ updatedAt: params.beforeCompaction.updatedAt,
55
+ note: params.note,
56
+ },
57
+ };
58
+ store.checkpoints[id] = checkpoint;
59
+ if (!store.bySession[params.sessionKey]) {
60
+ store.bySession[params.sessionKey] = [];
61
+ }
62
+ store.bySession[params.sessionKey].push(id);
63
+ saveCheckpointStore(workspaceDir, store);
64
+ return id;
65
+ }
66
+ /**
67
+ * Get a checkpoint by ID.
68
+ */
69
+ export function getCheckpoint(workspaceDir, checkpointId) {
70
+ const store = loadCheckpointStore(workspaceDir);
71
+ return store.checkpoints[checkpointId] ?? null;
72
+ }
73
+ /**
74
+ * List checkpoints for a session.
75
+ */
76
+ export function listSessionCheckpoints(workspaceDir, sessionKey) {
77
+ const store = loadCheckpointStore(workspaceDir);
78
+ const checkpointIds = store.bySession[sessionKey] ?? [];
79
+ return checkpointIds
80
+ .map((id) => store.checkpoints[id])
81
+ .filter((c) => c !== undefined)
82
+ .sort((a, b) => b.createdAt - a.createdAt);
83
+ }
84
+ /**
85
+ * List all checkpoints (optionally filtered by limit).
86
+ */
87
+ export function listAllCheckpoints(workspaceDir, limit = 100) {
88
+ const store = loadCheckpointStore(workspaceDir);
89
+ return Object.values(store.checkpoints)
90
+ .sort((a, b) => b.createdAt - a.createdAt)
91
+ .slice(0, limit);
92
+ }
93
+ /**
94
+ * Delete a checkpoint.
95
+ */
96
+ export function deleteCheckpoint(workspaceDir, checkpointId) {
97
+ const store = loadCheckpointStore(workspaceDir);
98
+ const checkpoint = store.checkpoints[checkpointId];
99
+ if (!checkpoint) {
100
+ return false;
101
+ }
102
+ delete store.checkpoints[checkpointId];
103
+ const sessionCheckpoints = store.bySession[checkpoint.sessionKey];
104
+ if (sessionCheckpoints) {
105
+ store.bySession[checkpoint.sessionKey] = sessionCheckpoints.filter((id) => id !== checkpointId);
106
+ if (store.bySession[checkpoint.sessionKey].length === 0) {
107
+ delete store.bySession[checkpoint.sessionKey];
108
+ }
109
+ }
110
+ saveCheckpointStore(workspaceDir, store);
111
+ return true;
112
+ }
113
+ /**
114
+ * Prune old checkpoints (older than maxAgeMs).
115
+ */
116
+ export function pruneOldCheckpoints(workspaceDir, maxAgeMs) {
117
+ const store = loadCheckpointStore(workspaceDir);
118
+ const now = Date.now();
119
+ let pruned = 0;
120
+ for (const [id, checkpoint] of Object.entries(store.checkpoints)) {
121
+ if (now - checkpoint.createdAt > maxAgeMs) {
122
+ delete store.checkpoints[id];
123
+ pruned++;
124
+ const sessionCheckpoints = store.bySession[checkpoint.sessionKey];
125
+ if (sessionCheckpoints) {
126
+ store.bySession[checkpoint.sessionKey] = sessionCheckpoints.filter((cid) => cid !== id);
127
+ if (store.bySession[checkpoint.sessionKey].length === 0) {
128
+ delete store.bySession[checkpoint.sessionKey];
129
+ }
130
+ }
131
+ }
132
+ }
133
+ if (pruned > 0) {
134
+ saveCheckpointStore(workspaceDir, store);
135
+ }
136
+ return pruned;
137
+ }
138
+ /**
139
+ * Get checkpoint statistics.
140
+ */
141
+ export function getCheckpointStats(workspaceDir) {
142
+ const store = loadCheckpointStore(workspaceDir);
143
+ const checkpoints = Object.values(store.checkpoints);
144
+ const byReason = {};
145
+ let oldest;
146
+ let newest;
147
+ for (const checkpoint of checkpoints) {
148
+ byReason[checkpoint.reason] = (byReason[checkpoint.reason] ?? 0) + 1;
149
+ if (oldest === undefined || checkpoint.createdAt < oldest) {
150
+ oldest = checkpoint.createdAt;
151
+ }
152
+ if (newest === undefined || checkpoint.createdAt > newest) {
153
+ newest = checkpoint.createdAt;
154
+ }
155
+ }
156
+ return {
157
+ total: checkpoints.length,
158
+ byReason,
159
+ oldest,
160
+ newest,
161
+ };
162
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"channel.d.ts","sourceRoot":"","sources":["../../src/slack/channel.ts"],"names":[],"mappings":"AAQA,OAAO,EA6BL,KAAK,aAAa,EAClB,KAAK,oBAAoB,EAC1B,MAAM,6CAA6C,CAAC;AAwErD,eAAO,MAAM,WAAW,EAAE,aAAa,CAAC,oBAAoB,CAuX3D,CAAC"}
1
+ {"version":3,"file":"channel.d.ts","sourceRoot":"","sources":["../../src/slack/channel.ts"],"names":[],"mappings":"AAQA,OAAO,EA6BL,KAAK,aAAa,EAClB,KAAK,oBAAoB,EAC1B,MAAM,6CAA6C,CAAC;AAwErD,eAAO,MAAM,WAAW,EAAE,aAAa,CAAC,oBAAoB,CA0Y3D,CAAC"}
@@ -131,8 +131,14 @@ export const slackPlugin = {
131
131
  });
132
132
  },
133
133
  collectWarnings: ({ account, cfg }) => {
134
+ const warnings = [];
134
135
  const channelAllowlistConfigured = Boolean(account.config.channels) && Object.keys(account.config.channels ?? {}).length > 0;
135
- return collectOpenProviderGroupPolicyWarnings({
136
+ // Validate appToken format
137
+ const appToken = account.appToken?.trim() ?? "";
138
+ if (appToken && !appToken.startsWith("xapp-")) {
139
+ warnings.push(`- Slack appToken is not an app-level token (should start with 'xapp-').`, ` Current token starts with: ${appToken.substring(0, 6)}...`, ` Fix: Get app-level token from https://api.slack.com/apps > Your App > Basic Information > App-Level Tokens`);
140
+ }
141
+ warnings.push(...collectOpenProviderGroupPolicyWarnings({
136
142
  cfg,
137
143
  providerConfigPresent: cfg.channels?.slack !== undefined,
138
144
  configuredGroupPolicy: account.config.groupPolicy,
@@ -151,7 +157,8 @@ export const slackPlugin = {
151
157
  remediation: 'Set channels.slack.groupPolicy="allowlist" and configure channels.slack.channels',
152
158
  },
153
159
  }),
154
- });
160
+ }));
161
+ return warnings;
155
162
  },
156
163
  },
157
164
  groups: {
@@ -234,6 +241,10 @@ export const slackPlugin = {
234
241
  if (!input.useEnv && (!input.botToken || !input.appToken)) {
235
242
  return "Slack requires --bot-token and --app-token (or --use-env).";
236
243
  }
244
+ // Validate appToken format (must be app-level token starting with xapp-)
245
+ if (!input.useEnv && input.appToken && !input.appToken.startsWith("xapp-")) {
246
+ return "Slack appToken must be an app-level token starting with 'xapp-'. Bot tokens (xoxb-) won't work. Create an app at https://api.slack.com/apps and get the app-level token from 'Basic Information' > 'App-Level Tokens'.";
247
+ }
237
248
  return null;
238
249
  },
239
250
  applyAccountConfig: ({ cfg, accountId, input }) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@poolzin/pool-bot",
3
- "version": "2026.4.44",
3
+ "version": "2026.4.46",
4
4
  "description": "🎱 Pool Bot - AI assistant with PLCODE integrations",
5
5
  "keywords": [],
6
6
  "license": "MIT",