@poolzin/pool-bot 2026.3.4 → 2026.3.7
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 +10 -0
- package/assets/pool-bot-icon-dark.png +0 -0
- package/assets/pool-bot-logo-1.png +0 -0
- package/assets/pool-bot-mascot.png +0 -0
- package/dist/agents/pi-embedded-runner/tool-result-truncation.js +62 -7
- package/dist/agents/pi-tools.js +32 -2
- package/dist/agents/poolbot-tools.js +12 -0
- package/dist/agents/session-write-lock.js +93 -8
- package/dist/agents/tools/pdf-native-providers.js +102 -0
- package/dist/agents/tools/pdf-tool.helpers.js +86 -0
- package/dist/agents/tools/pdf-tool.js +508 -0
- package/dist/auto-reply/reply/get-reply.js +6 -0
- package/dist/auto-reply/reply/message-preprocess-hooks.js +17 -0
- package/dist/build-info.json +3 -3
- package/dist/cli/banner.js +20 -1
- package/dist/cli/security-cli.js +211 -2
- package/dist/cli/tagline.js +7 -0
- package/dist/config/types.cli.js +1 -0
- package/dist/config/types.security.js +33 -0
- package/dist/config/zod-schema.js +15 -0
- package/dist/config/zod-schema.providers-core.js +1 -0
- package/dist/config/zod-schema.security.js +113 -0
- package/dist/cron/normalize.js +3 -0
- package/dist/cron/service/jobs.js +48 -0
- package/dist/discord/monitor/message-handler.preflight.js +11 -2
- package/dist/gateway/http-common.js +6 -1
- package/dist/gateway/protocol/schema/cron.js +3 -0
- package/dist/gateway/server-channels.js +99 -14
- package/dist/gateway/server-cron.js +89 -0
- package/dist/gateway/server-health-probes.js +55 -0
- package/dist/gateway/server-http.js +5 -0
- package/dist/hooks/bundled/session-memory/handler.js +8 -2
- package/dist/hooks/fire-and-forget.js +6 -0
- package/dist/hooks/internal-hooks.js +64 -19
- package/dist/hooks/message-hook-mappers.js +179 -0
- package/dist/infra/abort-signal.js +12 -0
- package/dist/infra/boundary-file-read.js +118 -0
- package/dist/infra/boundary-path.js +594 -0
- package/dist/infra/file-identity.js +12 -0
- package/dist/infra/fs-safe.js +377 -12
- package/dist/infra/hardlink-guards.js +30 -0
- package/dist/infra/json-utf8-bytes.js +8 -0
- package/dist/infra/net/fetch-guard.js +63 -13
- package/dist/infra/net/proxy-env.js +17 -0
- package/dist/infra/net/ssrf.js +74 -272
- package/dist/infra/path-alias-guards.js +21 -0
- package/dist/infra/path-guards.js +13 -1
- package/dist/infra/ports-probe.js +19 -0
- package/dist/infra/prototype-keys.js +4 -0
- package/dist/infra/restart-stale-pids.js +254 -0
- package/dist/infra/safe-open-sync.js +71 -0
- package/dist/infra/secure-random.js +7 -0
- package/dist/media/ffmpeg-limits.js +4 -0
- package/dist/media/input-files.js +6 -2
- package/dist/media/temp-files.js +12 -0
- package/dist/memory/embedding-chunk-limits.js +5 -2
- package/dist/memory/embeddings-ollama.js +91 -138
- package/dist/memory/embeddings-remote-fetch.js +11 -10
- package/dist/memory/embeddings.js +25 -9
- package/dist/memory/manager-embedding-ops.js +1 -1
- package/dist/memory/post-json.js +23 -0
- package/dist/memory/qmd-manager.js +272 -77
- package/dist/memory/remote-http.js +33 -0
- package/dist/plugin-sdk/windows-spawn.js +214 -0
- package/dist/security/capability-guards.js +89 -0
- package/dist/security/capability-manager.js +76 -0
- package/dist/security/capability.js +147 -0
- package/dist/security/index.js +7 -0
- package/dist/security/middleware.js +105 -0
- package/dist/shared/net/ip-test-fixtures.js +1 -0
- package/dist/shared/net/ip.js +303 -0
- package/dist/shared/net/ipv4.js +8 -11
- package/dist/shared/pid-alive.js +59 -2
- package/dist/slack/monitor/context.js +1 -0
- package/dist/slack/monitor/message-handler/dispatch.js +14 -1
- package/dist/slack/monitor/provider.js +2 -0
- package/dist/test-helpers/ssrf.js +13 -0
- package/dist/tui/tui.js +9 -4
- package/dist/utils/fetch-timeout.js +12 -1
- package/docs/adr/003-feature-gap-analysis.md +112 -0
- package/package.json +10 -4
|
@@ -0,0 +1,508 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { complete } from "@mariozechner/pi-ai";
|
|
3
|
+
import { Type } from "@sinclair/typebox";
|
|
4
|
+
import { resolveUserPath } from "../../utils.js";
|
|
5
|
+
import { getDefaultLocalRoots, loadWebMedia } from "../../web/media.js";
|
|
6
|
+
import { ensureAuthProfileStore, listProfilesForProvider } from "../auth-profiles.js";
|
|
7
|
+
import { DEFAULT_MODEL, DEFAULT_PROVIDER } from "../defaults.js";
|
|
8
|
+
import { getApiKeyForModel, requireApiKey, resolveEnvApiKey } from "../model-auth.js";
|
|
9
|
+
import { runWithImageModelFallback } from "../model-fallback.js";
|
|
10
|
+
import { resolveConfiguredModelRef } from "../model-selection.js";
|
|
11
|
+
import { ensurePoolbotModelsJson } from "../models-config.js";
|
|
12
|
+
import { discoverAuthStorage, discoverModels } from "../pi-model-discovery.js";
|
|
13
|
+
import { normalizeWorkspaceDir } from "../workspace-dir.js";
|
|
14
|
+
import { resolveProviderVisionModelFromConfig } from "./image-tool.helpers.js";
|
|
15
|
+
import { anthropicAnalyzePdf, geminiAnalyzePdf } from "./pdf-native-providers.js";
|
|
16
|
+
import { extractPdfContent, normalizeMimeType, resolveInputFileLimits, } from "../../media/input-files.js";
|
|
17
|
+
import { coercePdfAssistantText, coercePdfModelConfig, parsePageRange, providerSupportsNativePdf, resolvePdfToolMaxTokens, } from "./pdf-tool.helpers.js";
|
|
18
|
+
const DEFAULT_PROMPT = "Analyze the PDF document.";
|
|
19
|
+
const ANTHROPIC_PDF_PRIMARY = "anthropic/claude-opus-4-6";
|
|
20
|
+
const ANTHROPIC_PDF_FALLBACK = "anthropic/claude-opus-4-5";
|
|
21
|
+
const DEFAULT_MAX_PDFS = 10;
|
|
22
|
+
export const __testing = {
|
|
23
|
+
coercePdfAssistantText,
|
|
24
|
+
resolvePdfToolMaxTokens,
|
|
25
|
+
};
|
|
26
|
+
function resolveDefaultModelRef(cfg) {
|
|
27
|
+
if (cfg) {
|
|
28
|
+
const resolved = resolveConfiguredModelRef({
|
|
29
|
+
cfg,
|
|
30
|
+
defaultProvider: DEFAULT_PROVIDER,
|
|
31
|
+
defaultModel: DEFAULT_MODEL,
|
|
32
|
+
});
|
|
33
|
+
return { provider: resolved.provider, model: resolved.model };
|
|
34
|
+
}
|
|
35
|
+
return { provider: DEFAULT_PROVIDER, model: DEFAULT_MODEL };
|
|
36
|
+
}
|
|
37
|
+
function hasAuthForProvider(params) {
|
|
38
|
+
if (resolveEnvApiKey(params.provider)?.apiKey) {
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
const store = ensureAuthProfileStore(params.agentDir, {
|
|
42
|
+
allowKeychainPrompt: false,
|
|
43
|
+
});
|
|
44
|
+
return listProfilesForProvider(store, params.provider).length > 0;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Resolve the effective PDF model config for the `pdf` tool.
|
|
48
|
+
*
|
|
49
|
+
* - Prefer explicit config (`agents.defaults.pdfModel`).
|
|
50
|
+
* - Otherwise, try to "pair" the primary model with a PDF-capable model:
|
|
51
|
+
* - same provider (best effort)
|
|
52
|
+
* - fall back to OpenAI/Anthropic when available
|
|
53
|
+
*/
|
|
54
|
+
export function resolvePdfModelConfigForTool(params) {
|
|
55
|
+
const explicit = coercePdfModelConfig(params.cfg);
|
|
56
|
+
if (explicit.primary?.trim() || (explicit.fallbacks?.length ?? 0) > 0) {
|
|
57
|
+
return explicit;
|
|
58
|
+
}
|
|
59
|
+
const primary = resolveDefaultModelRef(params.cfg);
|
|
60
|
+
const openaiOk = hasAuthForProvider({
|
|
61
|
+
provider: "openai",
|
|
62
|
+
agentDir: params.agentDir,
|
|
63
|
+
});
|
|
64
|
+
const anthropicOk = hasAuthForProvider({
|
|
65
|
+
provider: "anthropic",
|
|
66
|
+
agentDir: params.agentDir,
|
|
67
|
+
});
|
|
68
|
+
const fallbacks = [];
|
|
69
|
+
const addFallback = (modelRef) => {
|
|
70
|
+
const ref = (modelRef ?? "").trim();
|
|
71
|
+
if (!ref) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (fallbacks.includes(ref)) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
fallbacks.push(ref);
|
|
78
|
+
};
|
|
79
|
+
const providerVisionFromConfig = resolveProviderVisionModelFromConfig({
|
|
80
|
+
cfg: params.cfg,
|
|
81
|
+
provider: primary.provider,
|
|
82
|
+
});
|
|
83
|
+
const providerOk = hasAuthForProvider({
|
|
84
|
+
provider: primary.provider,
|
|
85
|
+
agentDir: params.agentDir,
|
|
86
|
+
});
|
|
87
|
+
let preferred = null;
|
|
88
|
+
if (providerOk && providerVisionFromConfig) {
|
|
89
|
+
preferred = providerVisionFromConfig;
|
|
90
|
+
}
|
|
91
|
+
else if (primary.provider === "openai" && openaiOk) {
|
|
92
|
+
preferred = "openai/gpt-5-mini";
|
|
93
|
+
}
|
|
94
|
+
else if (primary.provider === "anthropic" && anthropicOk) {
|
|
95
|
+
preferred = ANTHROPIC_PDF_PRIMARY;
|
|
96
|
+
}
|
|
97
|
+
if (preferred?.trim()) {
|
|
98
|
+
if (openaiOk) {
|
|
99
|
+
addFallback("openai/gpt-5-mini");
|
|
100
|
+
}
|
|
101
|
+
if (anthropicOk) {
|
|
102
|
+
addFallback(ANTHROPIC_PDF_FALLBACK);
|
|
103
|
+
}
|
|
104
|
+
// Don't duplicate primary in fallbacks.
|
|
105
|
+
const pruned = fallbacks.filter((ref) => ref !== preferred);
|
|
106
|
+
return {
|
|
107
|
+
primary: preferred,
|
|
108
|
+
...(pruned.length > 0 ? { fallbacks: pruned } : {}),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
// Cross-provider fallback when we can't pair with the primary provider.
|
|
112
|
+
if (openaiOk) {
|
|
113
|
+
if (anthropicOk) {
|
|
114
|
+
addFallback(ANTHROPIC_PDF_FALLBACK);
|
|
115
|
+
}
|
|
116
|
+
return {
|
|
117
|
+
primary: "openai/gpt-5-mini",
|
|
118
|
+
...(fallbacks.length ? { fallbacks } : {}),
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
if (anthropicOk) {
|
|
122
|
+
return {
|
|
123
|
+
primary: ANTHROPIC_PDF_PRIMARY,
|
|
124
|
+
fallbacks: [ANTHROPIC_PDF_FALLBACK],
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
function pickMaxBytes(cfg, maxBytesMb) {
|
|
130
|
+
if (typeof maxBytesMb === "number" && Number.isFinite(maxBytesMb) && maxBytesMb > 0) {
|
|
131
|
+
return Math.floor(maxBytesMb * 1024 * 1024);
|
|
132
|
+
}
|
|
133
|
+
const configured = cfg?.agents?.defaults?.mediaMaxMb;
|
|
134
|
+
if (typeof configured === "number" && Number.isFinite(configured) && configured > 0) {
|
|
135
|
+
return Math.floor(configured * 1024 * 1024);
|
|
136
|
+
}
|
|
137
|
+
return undefined;
|
|
138
|
+
}
|
|
139
|
+
function buildPdfContext(prompt, extractedText, images) {
|
|
140
|
+
const content = [];
|
|
141
|
+
// Build text block: prompt + extracted text
|
|
142
|
+
const textBlock = extractedText.trim()
|
|
143
|
+
? `${prompt}\n\n--- Extracted PDF Text ---\n${extractedText}\n--- End Extracted PDF Text ---`
|
|
144
|
+
: prompt;
|
|
145
|
+
content.push({ type: "text", text: textBlock });
|
|
146
|
+
// Add rendered page images
|
|
147
|
+
for (const img of images) {
|
|
148
|
+
content.push({ type: "image", data: img.data, mimeType: img.mimeType });
|
|
149
|
+
}
|
|
150
|
+
return {
|
|
151
|
+
messages: [
|
|
152
|
+
{
|
|
153
|
+
role: "user",
|
|
154
|
+
content,
|
|
155
|
+
timestamp: Date.now(),
|
|
156
|
+
},
|
|
157
|
+
],
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
async function resolveSandboxedPdfPath(params) {
|
|
161
|
+
const normalize = (p) => (p.startsWith("file://") ? p.slice("file://".length) : p);
|
|
162
|
+
const filePath = normalize(params.pdfPath);
|
|
163
|
+
try {
|
|
164
|
+
const resolved = params.sandbox.bridge.resolvePath({
|
|
165
|
+
filePath,
|
|
166
|
+
cwd: params.sandbox.root,
|
|
167
|
+
});
|
|
168
|
+
return { resolved: resolved.hostPath };
|
|
169
|
+
}
|
|
170
|
+
catch (err) {
|
|
171
|
+
const name = path.basename(filePath);
|
|
172
|
+
const candidateRel = path.join("media", "inbound", name);
|
|
173
|
+
try {
|
|
174
|
+
const stat = await params.sandbox.bridge.stat({
|
|
175
|
+
filePath: candidateRel,
|
|
176
|
+
cwd: params.sandbox.root,
|
|
177
|
+
});
|
|
178
|
+
if (!stat) {
|
|
179
|
+
throw err;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
throw err;
|
|
184
|
+
}
|
|
185
|
+
const out = params.sandbox.bridge.resolvePath({
|
|
186
|
+
filePath: candidateRel,
|
|
187
|
+
cwd: params.sandbox.root,
|
|
188
|
+
});
|
|
189
|
+
return { resolved: out.hostPath, rewrittenFrom: filePath };
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
async function runPdfPrompt(params) {
|
|
193
|
+
// Overlay pdfModelConfig onto cfg.agents.defaults.imageModel so
|
|
194
|
+
// runWithImageModelFallback reads our PDF-specific model cascade.
|
|
195
|
+
const effectiveCfg = params.cfg
|
|
196
|
+
? {
|
|
197
|
+
...params.cfg,
|
|
198
|
+
agents: {
|
|
199
|
+
...params.cfg.agents,
|
|
200
|
+
defaults: {
|
|
201
|
+
...params.cfg.agents?.defaults,
|
|
202
|
+
imageModel: params.pdfModelConfig,
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
}
|
|
206
|
+
: undefined;
|
|
207
|
+
await ensurePoolbotModelsJson(effectiveCfg, params.agentDir);
|
|
208
|
+
const authStorage = discoverAuthStorage(params.agentDir);
|
|
209
|
+
const modelRegistry = discoverModels(authStorage, params.agentDir);
|
|
210
|
+
const result = await runWithImageModelFallback({
|
|
211
|
+
cfg: effectiveCfg,
|
|
212
|
+
modelOverride: params.modelOverride,
|
|
213
|
+
run: async (provider, modelId) => {
|
|
214
|
+
const model = modelRegistry.find(provider, modelId);
|
|
215
|
+
if (!model) {
|
|
216
|
+
throw new Error(`Unknown model: ${provider}/${modelId}`);
|
|
217
|
+
}
|
|
218
|
+
const apiKeyInfo = await getApiKeyForModel({
|
|
219
|
+
model,
|
|
220
|
+
cfg: effectiveCfg,
|
|
221
|
+
agentDir: params.agentDir,
|
|
222
|
+
});
|
|
223
|
+
const apiKey = requireApiKey(apiKeyInfo, model.provider);
|
|
224
|
+
authStorage.setRuntimeApiKey(model.provider, apiKey);
|
|
225
|
+
// Native path: send raw PDF bytes to providers with native document support.
|
|
226
|
+
if (providerSupportsNativePdf(model.provider)) {
|
|
227
|
+
const pdfs = params.pdfBuffers.map((buf) => ({
|
|
228
|
+
base64: buf.toString("base64"),
|
|
229
|
+
}));
|
|
230
|
+
if (model.provider === "anthropic") {
|
|
231
|
+
const text = await anthropicAnalyzePdf({
|
|
232
|
+
apiKey,
|
|
233
|
+
modelId: model.id,
|
|
234
|
+
prompt: params.prompt,
|
|
235
|
+
pdfs,
|
|
236
|
+
maxTokens: resolvePdfToolMaxTokens(model.maxTokens),
|
|
237
|
+
baseUrl: model.baseUrl,
|
|
238
|
+
});
|
|
239
|
+
return { text, provider: model.provider, model: model.id };
|
|
240
|
+
}
|
|
241
|
+
if (model.provider === "google") {
|
|
242
|
+
const text = await geminiAnalyzePdf({
|
|
243
|
+
apiKey,
|
|
244
|
+
modelId: model.id,
|
|
245
|
+
prompt: params.prompt,
|
|
246
|
+
pdfs,
|
|
247
|
+
baseUrl: model.baseUrl,
|
|
248
|
+
});
|
|
249
|
+
return { text, provider: model.provider, model: model.id };
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
// Non-native fallback: extract text + optional page images, then send
|
|
253
|
+
// via pi-ai complete().
|
|
254
|
+
const limits = resolveInputFileLimits();
|
|
255
|
+
let allText = "";
|
|
256
|
+
let allImages = [];
|
|
257
|
+
for (const buf of params.pdfBuffers) {
|
|
258
|
+
const extracted = await extractPdfContent({
|
|
259
|
+
buffer: buf,
|
|
260
|
+
limits,
|
|
261
|
+
pageNumbers: params.pageNumbers,
|
|
262
|
+
});
|
|
263
|
+
if (extracted.text) {
|
|
264
|
+
allText += (allText ? "\n\n" : "") + extracted.text;
|
|
265
|
+
}
|
|
266
|
+
allImages.push(...extracted.images);
|
|
267
|
+
}
|
|
268
|
+
// If model doesn't support images, proceed text-only (don't error).
|
|
269
|
+
if (!model.input?.includes("image")) {
|
|
270
|
+
allImages = [];
|
|
271
|
+
}
|
|
272
|
+
const context = buildPdfContext(params.prompt, allText, allImages);
|
|
273
|
+
const message = await complete(model, context, {
|
|
274
|
+
apiKey,
|
|
275
|
+
maxTokens: resolvePdfToolMaxTokens(model.maxTokens),
|
|
276
|
+
});
|
|
277
|
+
const text = coercePdfAssistantText({
|
|
278
|
+
message,
|
|
279
|
+
provider: model.provider,
|
|
280
|
+
model: model.id,
|
|
281
|
+
});
|
|
282
|
+
return { text, provider: model.provider, model: model.id };
|
|
283
|
+
},
|
|
284
|
+
});
|
|
285
|
+
return {
|
|
286
|
+
text: result.result.text,
|
|
287
|
+
provider: result.result.provider,
|
|
288
|
+
model: result.result.model,
|
|
289
|
+
attempts: result.attempts.map((attempt) => ({
|
|
290
|
+
provider: attempt.provider,
|
|
291
|
+
model: attempt.model,
|
|
292
|
+
error: attempt.error,
|
|
293
|
+
})),
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
export function createPdfTool(options) {
|
|
297
|
+
const agentDir = options?.agentDir?.trim();
|
|
298
|
+
if (!agentDir) {
|
|
299
|
+
const explicit = coercePdfModelConfig(options?.config);
|
|
300
|
+
if (explicit.primary?.trim() || (explicit.fallbacks?.length ?? 0) > 0) {
|
|
301
|
+
throw new Error("createPdfTool requires agentDir when enabled");
|
|
302
|
+
}
|
|
303
|
+
return null;
|
|
304
|
+
}
|
|
305
|
+
const pdfModelConfig = resolvePdfModelConfigForTool({
|
|
306
|
+
cfg: options?.config,
|
|
307
|
+
agentDir,
|
|
308
|
+
});
|
|
309
|
+
if (!pdfModelConfig) {
|
|
310
|
+
return null;
|
|
311
|
+
}
|
|
312
|
+
const description = "Analyze one or more PDF documents. Use pdf for a single path/URL, or pdfs for multiple (up to 10). Optionally specify pages to limit analysis to specific page ranges.";
|
|
313
|
+
const localRoots = (() => {
|
|
314
|
+
const roots = getDefaultLocalRoots();
|
|
315
|
+
const workspaceDir = normalizeWorkspaceDir(options?.workspaceDir);
|
|
316
|
+
if (!workspaceDir) {
|
|
317
|
+
return roots;
|
|
318
|
+
}
|
|
319
|
+
return Array.from(new Set([...roots, workspaceDir]));
|
|
320
|
+
})();
|
|
321
|
+
return {
|
|
322
|
+
label: "PDF",
|
|
323
|
+
name: "pdf",
|
|
324
|
+
description,
|
|
325
|
+
parameters: Type.Object({
|
|
326
|
+
prompt: Type.Optional(Type.String()),
|
|
327
|
+
pdf: Type.Optional(Type.String({ description: "Single PDF path or URL." })),
|
|
328
|
+
pdfs: Type.Optional(Type.Array(Type.String(), {
|
|
329
|
+
description: "Multiple PDF paths or URLs (up to 10).",
|
|
330
|
+
})),
|
|
331
|
+
pages: Type.Optional(Type.String({
|
|
332
|
+
description: 'Page range to analyze, e.g. "1-5", "3", "1-3,7-9". Omit to analyze all pages.',
|
|
333
|
+
})),
|
|
334
|
+
model: Type.Optional(Type.String()),
|
|
335
|
+
maxBytesMb: Type.Optional(Type.Number()),
|
|
336
|
+
}),
|
|
337
|
+
execute: async (_toolCallId, args) => {
|
|
338
|
+
const record = args && typeof args === "object" ? args : {};
|
|
339
|
+
// MARK: - Normalize pdf + pdfs input and dedupe while preserving order
|
|
340
|
+
const pdfCandidates = [];
|
|
341
|
+
if (typeof record.pdf === "string") {
|
|
342
|
+
pdfCandidates.push(record.pdf);
|
|
343
|
+
}
|
|
344
|
+
if (Array.isArray(record.pdfs)) {
|
|
345
|
+
pdfCandidates.push(...record.pdfs.filter((v) => typeof v === "string"));
|
|
346
|
+
}
|
|
347
|
+
const seenPdfs = new Set();
|
|
348
|
+
const pdfInputs = [];
|
|
349
|
+
for (const candidate of pdfCandidates) {
|
|
350
|
+
const trimmedCandidate = candidate.trim();
|
|
351
|
+
const normalizedForDedupe = trimmedCandidate.startsWith("@")
|
|
352
|
+
? trimmedCandidate.slice(1).trim()
|
|
353
|
+
: trimmedCandidate;
|
|
354
|
+
if (!normalizedForDedupe || seenPdfs.has(normalizedForDedupe)) {
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
seenPdfs.add(normalizedForDedupe);
|
|
358
|
+
pdfInputs.push(trimmedCandidate);
|
|
359
|
+
}
|
|
360
|
+
if (pdfInputs.length === 0) {
|
|
361
|
+
throw new Error("pdf required");
|
|
362
|
+
}
|
|
363
|
+
// MARK: - Enforce max PDFs cap (hardcoded)
|
|
364
|
+
if (pdfInputs.length > DEFAULT_MAX_PDFS) {
|
|
365
|
+
return {
|
|
366
|
+
content: [
|
|
367
|
+
{
|
|
368
|
+
type: "text",
|
|
369
|
+
text: `Too many PDFs: ${pdfInputs.length} provided, maximum is ${DEFAULT_MAX_PDFS}. Please reduce the number of PDFs.`,
|
|
370
|
+
},
|
|
371
|
+
],
|
|
372
|
+
details: { error: "too_many_pdfs", count: pdfInputs.length, max: DEFAULT_MAX_PDFS },
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
// MARK: - Parse pages param
|
|
376
|
+
const pagesRaw = typeof record.pages === "string" && record.pages.trim() ? record.pages.trim() : undefined;
|
|
377
|
+
const pageNumbers = pagesRaw ? parsePageRange(pagesRaw, 9999) : undefined;
|
|
378
|
+
const promptRaw = typeof record.prompt === "string" && record.prompt.trim()
|
|
379
|
+
? record.prompt.trim()
|
|
380
|
+
: DEFAULT_PROMPT;
|
|
381
|
+
const modelOverride = typeof record.model === "string" && record.model.trim() ? record.model.trim() : undefined;
|
|
382
|
+
const maxBytesMb = typeof record.maxBytesMb === "number" ? record.maxBytesMb : undefined;
|
|
383
|
+
const maxBytes = pickMaxBytes(options?.config, maxBytesMb);
|
|
384
|
+
const sandboxConfig = options?.sandbox && options?.sandbox.root.trim()
|
|
385
|
+
? { root: options.sandbox.root.trim(), bridge: options.sandbox.bridge }
|
|
386
|
+
: null;
|
|
387
|
+
// MARK: - Load and resolve each PDF
|
|
388
|
+
const loadedPdfs = [];
|
|
389
|
+
for (const pdfRawInput of pdfInputs) {
|
|
390
|
+
const trimmed = pdfRawInput.trim();
|
|
391
|
+
const pdfRaw = trimmed.startsWith("@") ? trimmed.slice(1).trim() : trimmed;
|
|
392
|
+
if (!pdfRaw) {
|
|
393
|
+
throw new Error("pdf required (empty string in array)");
|
|
394
|
+
}
|
|
395
|
+
// Validate scheme: allow file://, http(s)://, and bare paths.
|
|
396
|
+
// Reject data: URLs (too large for PDFs) and unsupported schemes.
|
|
397
|
+
const looksLikeWindowsDrivePath = /^[a-zA-Z]:[\\/]/.test(pdfRaw);
|
|
398
|
+
const hasScheme = /^[a-z][a-z0-9+.-]*:/i.test(pdfRaw);
|
|
399
|
+
const isFileUrl = /^file:/i.test(pdfRaw);
|
|
400
|
+
const isHttpUrl = /^https?:\/\//i.test(pdfRaw);
|
|
401
|
+
const isDataUrl = /^data:/i.test(pdfRaw);
|
|
402
|
+
if (isDataUrl) {
|
|
403
|
+
return {
|
|
404
|
+
content: [
|
|
405
|
+
{
|
|
406
|
+
type: "text",
|
|
407
|
+
text: `Data URLs are not supported for PDFs. Use a file path or an http(s) URL instead.`,
|
|
408
|
+
},
|
|
409
|
+
],
|
|
410
|
+
details: { error: "unsupported_pdf_reference", pdf: pdfRawInput },
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
if (hasScheme && !looksLikeWindowsDrivePath && !isFileUrl && !isHttpUrl) {
|
|
414
|
+
return {
|
|
415
|
+
content: [
|
|
416
|
+
{
|
|
417
|
+
type: "text",
|
|
418
|
+
text: `Unsupported PDF reference: ${pdfRawInput}. Use a file path, a file:// URL, or an http(s) URL.`,
|
|
419
|
+
},
|
|
420
|
+
],
|
|
421
|
+
details: {
|
|
422
|
+
error: "unsupported_pdf_reference",
|
|
423
|
+
pdf: pdfRawInput,
|
|
424
|
+
},
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
if (sandboxConfig && isHttpUrl) {
|
|
428
|
+
throw new Error("Sandboxed PDF tool does not allow remote URLs.");
|
|
429
|
+
}
|
|
430
|
+
const resolvedPdf = (() => {
|
|
431
|
+
if (sandboxConfig) {
|
|
432
|
+
return pdfRaw;
|
|
433
|
+
}
|
|
434
|
+
if (pdfRaw.startsWith("~")) {
|
|
435
|
+
return resolveUserPath(pdfRaw);
|
|
436
|
+
}
|
|
437
|
+
return pdfRaw;
|
|
438
|
+
})();
|
|
439
|
+
const resolvedPathInfo = sandboxConfig
|
|
440
|
+
? await resolveSandboxedPdfPath({
|
|
441
|
+
sandbox: sandboxConfig,
|
|
442
|
+
pdfPath: resolvedPdf,
|
|
443
|
+
})
|
|
444
|
+
: {
|
|
445
|
+
resolved: resolvedPdf.startsWith("file://")
|
|
446
|
+
? resolvedPdf.slice("file://".length)
|
|
447
|
+
: resolvedPdf,
|
|
448
|
+
};
|
|
449
|
+
const resolvedPath = resolvedPathInfo.resolved;
|
|
450
|
+
const media = sandboxConfig
|
|
451
|
+
? await loadWebMedia(resolvedPath ?? resolvedPdf, {
|
|
452
|
+
maxBytes,
|
|
453
|
+
sandboxValidated: true,
|
|
454
|
+
optimizeImages: false,
|
|
455
|
+
readFile: (filePath) => sandboxConfig.bridge.readFile({ filePath, cwd: sandboxConfig.root }),
|
|
456
|
+
})
|
|
457
|
+
: await loadWebMedia(resolvedPath ?? resolvedPdf, {
|
|
458
|
+
maxBytes,
|
|
459
|
+
localRoots,
|
|
460
|
+
optimizeImages: false,
|
|
461
|
+
});
|
|
462
|
+
const mime = normalizeMimeType(("contentType" in media && media.contentType) || "");
|
|
463
|
+
if (mime !== "application/pdf") {
|
|
464
|
+
throw new Error(`Expected PDF, got: ${("contentType" in media && media.contentType) || "unknown"}`);
|
|
465
|
+
}
|
|
466
|
+
loadedPdfs.push({
|
|
467
|
+
buffer: media.buffer,
|
|
468
|
+
resolvedPdf,
|
|
469
|
+
...(resolvedPathInfo.rewrittenFrom
|
|
470
|
+
? { rewrittenFrom: resolvedPathInfo.rewrittenFrom }
|
|
471
|
+
: {}),
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
// MARK: - Run PDF prompt with all loaded PDFs
|
|
475
|
+
const result = await runPdfPrompt({
|
|
476
|
+
cfg: options?.config,
|
|
477
|
+
agentDir,
|
|
478
|
+
pdfModelConfig,
|
|
479
|
+
modelOverride,
|
|
480
|
+
prompt: promptRaw,
|
|
481
|
+
pdfBuffers: loadedPdfs.map((p) => p.buffer),
|
|
482
|
+
pageNumbers,
|
|
483
|
+
});
|
|
484
|
+
const pdfDetails = loadedPdfs.length === 1
|
|
485
|
+
? {
|
|
486
|
+
pdf: loadedPdfs[0].resolvedPdf,
|
|
487
|
+
...(loadedPdfs[0].rewrittenFrom
|
|
488
|
+
? { rewrittenFrom: loadedPdfs[0].rewrittenFrom }
|
|
489
|
+
: {}),
|
|
490
|
+
}
|
|
491
|
+
: {
|
|
492
|
+
pdfs: loadedPdfs.map((p) => ({
|
|
493
|
+
pdf: p.resolvedPdf,
|
|
494
|
+
...(p.rewrittenFrom ? { rewrittenFrom: p.rewrittenFrom } : {}),
|
|
495
|
+
})),
|
|
496
|
+
};
|
|
497
|
+
return {
|
|
498
|
+
content: [{ type: "text", text: result.text }],
|
|
499
|
+
details: {
|
|
500
|
+
model: `${result.provider}/${result.model}`,
|
|
501
|
+
...pdfDetails,
|
|
502
|
+
...(pageNumbers ? { pages: pageNumbers } : {}),
|
|
503
|
+
attempts: result.attempts,
|
|
504
|
+
},
|
|
505
|
+
};
|
|
506
|
+
},
|
|
507
|
+
};
|
|
508
|
+
}
|
|
@@ -14,6 +14,7 @@ import { resolveReplyDirectives } from "./get-reply-directives.js";
|
|
|
14
14
|
import { handleInlineActions } from "./get-reply-inline-actions.js";
|
|
15
15
|
import { runPreparedReply } from "./get-reply-run.js";
|
|
16
16
|
import { finalizeInboundContext } from "./inbound-context.js";
|
|
17
|
+
import { emitPreAgentMessageHooks } from "./message-preprocess-hooks.js";
|
|
17
18
|
import { applyResetModelOverride } from "./session-reset-model.js";
|
|
18
19
|
import { initSessionState } from "./session.js";
|
|
19
20
|
import { stageSandboxMedia } from "./stage-sandbox-media.js";
|
|
@@ -110,6 +111,11 @@ export async function getReplyFromConfig(ctx, opts, configOverride) {
|
|
|
110
111
|
cfg,
|
|
111
112
|
});
|
|
112
113
|
}
|
|
114
|
+
emitPreAgentMessageHooks({
|
|
115
|
+
ctx: finalized,
|
|
116
|
+
cfg,
|
|
117
|
+
isFastTestEnv,
|
|
118
|
+
});
|
|
113
119
|
const commandAuthorized = finalized.CommandAuthorized;
|
|
114
120
|
resolveCommandAuthorization({
|
|
115
121
|
ctx: finalized,
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { fireAndForgetHook } from "../../hooks/fire-and-forget.js";
|
|
2
|
+
import { createInternalHookEvent, triggerInternalHook } from "../../hooks/internal-hooks.js";
|
|
3
|
+
import { deriveInboundMessageHookContext, toInternalMessagePreprocessedContext, toInternalMessageTranscribedContext, } from "../../hooks/message-hook-mappers.js";
|
|
4
|
+
export function emitPreAgentMessageHooks(params) {
|
|
5
|
+
if (params.isFastTestEnv) {
|
|
6
|
+
return;
|
|
7
|
+
}
|
|
8
|
+
const sessionKey = params.ctx.SessionKey?.trim();
|
|
9
|
+
if (!sessionKey) {
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
const canonical = deriveInboundMessageHookContext(params.ctx);
|
|
13
|
+
if (canonical.transcript) {
|
|
14
|
+
fireAndForgetHook(triggerInternalHook(createInternalHookEvent("message", "transcribed", sessionKey, toInternalMessageTranscribedContext(canonical, params.cfg))), "get-reply: message:transcribed internal hook failed");
|
|
15
|
+
}
|
|
16
|
+
fireAndForgetHook(triggerInternalHook(createInternalHookEvent("message", "preprocessed", sessionKey, toInternalMessagePreprocessedContext(canonical, params.cfg))), "get-reply: message:preprocessed internal hook failed");
|
|
17
|
+
}
|
package/dist/build-info.json
CHANGED
package/dist/cli/banner.js
CHANGED
|
@@ -1,9 +1,28 @@
|
|
|
1
|
+
import { loadConfig } from "../config/config.js";
|
|
1
2
|
import { resolveCommitHash } from "../infra/git-commit.js";
|
|
2
3
|
import { visibleWidth } from "../terminal/ansi.js";
|
|
3
4
|
import { isRich, theme } from "../terminal/theme.js";
|
|
4
5
|
import { hasRootVersionAlias } from "./argv.js";
|
|
5
6
|
import { pickTagline } from "./tagline.js";
|
|
6
7
|
import { resolveCliName } from "./cli-name.js";
|
|
8
|
+
function parseTaglineMode(value) {
|
|
9
|
+
if (value === "random" || value === "default" || value === "off") {
|
|
10
|
+
return value;
|
|
11
|
+
}
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
14
|
+
function resolveTaglineMode(options) {
|
|
15
|
+
const explicit = parseTaglineMode(options.mode);
|
|
16
|
+
if (explicit) {
|
|
17
|
+
return explicit;
|
|
18
|
+
}
|
|
19
|
+
try {
|
|
20
|
+
return parseTaglineMode(loadConfig().cli?.banner?.taglineMode);
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
7
26
|
let bannerEmitted = false;
|
|
8
27
|
const graphemeSegmenter = typeof Intl !== "undefined" && "Segmenter" in Intl
|
|
9
28
|
? new Intl.Segmenter(undefined, { granularity: "grapheme" })
|
|
@@ -23,7 +42,7 @@ const hasVersionFlag = (argv) => argv.some((arg) => arg === "--version" || arg =
|
|
|
23
42
|
export function formatCliBannerLine(version, options = {}) {
|
|
24
43
|
const commit = options.commit ?? resolveCommitHash({ env: options.env });
|
|
25
44
|
const commitLabel = commit ?? "unknown";
|
|
26
|
-
const tagline = pickTagline(options);
|
|
45
|
+
const tagline = pickTagline({ ...options, mode: resolveTaglineMode(options) });
|
|
27
46
|
const rich = options.richTty ?? isRich();
|
|
28
47
|
const cliName = resolveCliName(options.argv ?? process.argv);
|
|
29
48
|
const title = cliName === "poolbot" ? "🎱 Pool Bot" : "🎱 Pool Bot";
|