@poolzin/pool-bot 2026.4.43 → 2026.4.45
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/dist/agents/model-catalog-gemma4.d.ts +6 -0
- package/dist/agents/model-catalog-gemma4.d.ts.map +1 -0
- package/dist/agents/model-catalog-gemma4.js +17 -0
- package/dist/agents/model-catalog.d.ts.map +1 -1
- package/dist/agents/model-catalog.js +7 -0
- package/dist/agents/tools/music-edit-tool.d.ts +9 -0
- package/dist/agents/tools/music-edit-tool.d.ts.map +1 -0
- package/dist/agents/tools/music-edit-tool.js +46 -0
- package/dist/agents/tools/video-edit-tool.d.ts +9 -0
- package/dist/agents/tools/video-edit-tool.d.ts.map +1 -0
- package/dist/agents/tools/video-edit-tool.js +46 -0
- package/dist/build-info.json +3 -3
- package/dist/cli/completion.d.ts +15 -0
- package/dist/cli/completion.d.ts.map +1 -0
- package/dist/cli/completion.js +183 -0
- package/dist/cli/onboard-cli.d.ts +20 -0
- package/dist/cli/onboard-cli.d.ts.map +1 -0
- package/dist/cli/onboard-cli.js +443 -0
- package/dist/cli/plugins-config-tui.d.ts.map +1 -1
- package/dist/cli/plugins-config-tui.js +1 -1
- package/dist/cli/program/register.subclis.d.ts.map +1 -1
- package/dist/cli/program/register.subclis.js +18 -0
- package/dist/cli/sessions-checkpoints-cli.d.ts +6 -0
- package/dist/cli/sessions-checkpoints-cli.d.ts.map +1 -0
- package/dist/cli/sessions-checkpoints-cli.js +117 -0
- package/dist/cli/webhooks-cli.d.ts +13 -0
- package/dist/cli/webhooks-cli.d.ts.map +1 -1
- package/dist/cli/webhooks-cli.js +323 -130
- package/dist/errors/user-friendly-errors.d.ts +23 -0
- package/dist/errors/user-friendly-errors.d.ts.map +1 -0
- package/dist/errors/user-friendly-errors.js +182 -0
- package/dist/infra/net/fetch-guard.d.ts.map +1 -1
- package/dist/memory/wiki/store.d.ts +53 -0
- package/dist/memory/wiki/store.d.ts.map +1 -0
- package/dist/memory/wiki/store.js +222 -0
- package/dist/memory/wiki/types.d.ts +57 -0
- package/dist/memory/wiki/types.d.ts.map +1 -0
- package/dist/memory/wiki/types.js +6 -0
- package/dist/plugins/webhook-ingress.d.ts +104 -0
- package/dist/plugins/webhook-ingress.d.ts.map +1 -0
- package/dist/plugins/webhook-ingress.js +287 -0
- package/dist/providers/ollama-vision.d.ts +30 -0
- package/dist/providers/ollama-vision.d.ts.map +1 -0
- package/dist/providers/ollama-vision.js +62 -0
- package/dist/sessions/checkpoints.d.ts +76 -0
- package/dist/sessions/checkpoints.d.ts.map +1 -0
- package/dist/sessions/checkpoints.js +162 -0
- package/dist/slack/channel.d.ts.map +1 -1
- package/dist/slack/channel.js +13 -2
- 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,
|
|
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"}
|
package/dist/slack/channel.js
CHANGED
|
@@ -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
|
-
|
|
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 }) => {
|