@poolzin/pool-bot 2026.3.9 → 2026.3.11
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 +35 -0
- package/README.md +147 -69
- package/dist/.buildstamp +1 -1
- package/dist/agents/error-classifier.js +26 -77
- package/dist/agents/skills/security.js +1 -7
- package/dist/build-info.json +3 -3
- package/dist/cli/cron-cli/register.cron-dashboard.js +339 -0
- package/dist/cli/cron-cli/register.js +2 -0
- package/dist/cli/errors.js +187 -0
- package/dist/cli/program/command-registry.js +13 -0
- package/dist/cli/program/register.maintenance.js +21 -0
- package/dist/cli/program/register.subclis.js +9 -0
- package/dist/cli/swarm-cli/register.js +8 -0
- package/dist/cli/swarm-cli/register.swarm-status.js +488 -0
- package/dist/cli/telemetry-cli/register.js +10 -0
- package/dist/cli/telemetry-cli/register.telemetry-alerts.js +176 -0
- package/dist/cli/telemetry-cli/register.telemetry-metrics.js +323 -0
- package/dist/cli/telemetry-cli/register.telemetry-status.js +179 -0
- package/dist/commands/doctor-checks.js +498 -0
- package/dist/context-engine/index.js +1 -1
- package/dist/context-engine/legacy.js +1 -3
- package/dist/context-engine/summarizing.js +5 -8
- package/dist/cron/service/timer.js +18 -0
- package/dist/gateway/protocol/index.js +5 -2
- package/dist/gateway/protocol/schema/error-codes.js +1 -0
- package/dist/gateway/protocol/schema/swarm.js +80 -0
- package/dist/gateway/protocol/schema.js +1 -0
- package/dist/gateway/server-close.js +4 -0
- package/dist/gateway/server-constants.js +1 -0
- package/dist/gateway/server-cron.js +29 -0
- package/dist/gateway/server-maintenance.js +35 -2
- package/dist/gateway/server-methods/swarm.js +58 -0
- package/dist/gateway/server-methods/telemetry.js +71 -0
- package/dist/gateway/server-methods-list.js +8 -0
- package/dist/gateway/server-methods.js +9 -2
- package/dist/gateway/server.impl.js +33 -16
- package/dist/infra/abort-pattern.js +4 -4
- package/dist/infra/retry.js +3 -1
- package/dist/skills/commands.js +7 -25
- package/dist/skills/index.js +14 -17
- package/dist/skills/parser.js +12 -27
- package/dist/skills/registry.js +3 -6
- package/dist/skills/security.js +2 -8
- package/dist/swarm/service.js +247 -0
- package/dist/telemetry/alert-engine.js +258 -0
- package/dist/telemetry/cron-instrumentation.js +49 -0
- package/dist/telemetry/gateway-instrumentation.js +80 -0
- package/dist/telemetry/instrumentation.js +66 -0
- package/dist/telemetry/service.js +345 -0
- package/dist/tui/components/assistant-message.js +6 -2
- package/dist/tui/components/hyperlink-markdown.js +32 -0
- package/dist/tui/components/searchable-select-list.js +12 -1
- package/dist/tui/components/user-message.js +6 -2
- package/dist/tui/index.js +22 -6
- package/dist/tui/theme/theme-detection.js +226 -0
- package/dist/tui/tui-command-handlers.js +20 -0
- package/dist/tui/tui-formatters.js +4 -3
- package/dist/tui/utils/ctrl-c-handler.js +67 -0
- package/dist/tui/utils/osc8-hyperlinks.js +208 -0
- package/dist/tui/utils/safe-stop.js +180 -0
- package/dist/tui/utils/session-key-utils.js +81 -0
- package/dist/tui/utils/text-sanitization.js +284 -0
- package/dist/utils/lru-cache.js +116 -0
- package/dist/utils/performance.js +199 -0
- package/dist/utils/retry.js +240 -0
- package/docs/MELHORIAS_IMPLEMENTADAS.md +228 -0
- package/docs/MELHORIAS_PROFISSIONAIS.md +282 -0
- package/docs/PLANO_ACAO_TUI.md +357 -0
- package/docs/PROGRESSO_TUI.md +66 -0
- package/docs/RELATORIO_FINAL.md +217 -0
- package/docs/diagnostico-shell-completion.md +265 -0
- package/docs/features/advanced-memory.md +585 -0
- package/docs/features/discord-components-v2.md +277 -0
- package/docs/features/swarm.md +100 -0
- package/docs/features/telemetry.md +284 -0
- package/docs/integrations/INTEGRATION_PLAN.md +665 -345
- package/docs/models/provider-infrastructure.md +400 -0
- package/docs/security/exec-approvals.md +294 -0
- package/extensions/bluebubbles/package.json +1 -1
- package/extensions/copilot-proxy/package.json +1 -1
- package/extensions/diagnostics-otel/package.json +1 -1
- package/extensions/discord/package.json +1 -1
- package/extensions/feishu/package.json +1 -1
- package/extensions/google-antigravity-auth/package.json +1 -1
- package/extensions/google-gemini-cli-auth/package.json +1 -1
- package/extensions/googlechat/package.json +1 -1
- package/extensions/hexstrike-bridge/README.md +119 -0
- package/extensions/hexstrike-bridge/index.test.ts +247 -0
- package/extensions/hexstrike-bridge/index.ts +487 -0
- package/extensions/hexstrike-bridge/package.json +17 -0
- package/extensions/imessage/package.json +1 -1
- package/extensions/irc/package.json +1 -1
- package/extensions/line/package.json +1 -1
- package/extensions/llm-task/package.json +1 -1
- package/extensions/lobster/package.json +1 -1
- package/extensions/matrix/CHANGELOG.md +10 -0
- package/extensions/matrix/package.json +1 -1
- package/extensions/mattermost/package.json +1 -1
- package/extensions/mavalie/README.md +97 -0
- package/extensions/mavalie/package.json +15 -0
- package/extensions/mavalie/src/index.ts +62 -0
- package/extensions/mcp-server/index.ts +14 -0
- package/extensions/mcp-server/package.json +11 -0
- package/extensions/mcp-server/src/service.ts +540 -0
- package/extensions/memory-core/package.json +1 -1
- package/extensions/memory-lancedb/package.json +1 -1
- package/extensions/minimax-portal-auth/package.json +1 -1
- package/extensions/msteams/CHANGELOG.md +10 -0
- package/extensions/msteams/package.json +1 -1
- package/extensions/nextcloud-talk/package.json +1 -1
- package/extensions/nostr/CHANGELOG.md +10 -0
- package/extensions/nostr/package.json +1 -1
- package/extensions/open-prose/package.json +1 -1
- package/extensions/openai-codex-auth/package.json +1 -1
- package/extensions/signal/package.json +1 -1
- package/extensions/slack/package.json +1 -1
- package/extensions/telegram/package.json +1 -1
- package/extensions/tlon/package.json +1 -1
- package/extensions/twitch/CHANGELOG.md +10 -0
- package/extensions/twitch/package.json +1 -1
- package/extensions/voice-call/CHANGELOG.md +10 -0
- package/extensions/voice-call/package.json +1 -1
- package/extensions/whatsapp/package.json +1 -1
- package/extensions/zalo/CHANGELOG.md +10 -0
- package/extensions/zalo/package.json +1 -1
- package/extensions/zalouser/CHANGELOG.md +10 -0
- package/extensions/zalouser/package.json +1 -1
- package/package.json +8 -1
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Safe TUI stop utilities
|
|
3
|
+
*
|
|
4
|
+
* Features:
|
|
5
|
+
* - Graceful TUI shutdown with error handling
|
|
6
|
+
* - EBADF error detection and handling
|
|
7
|
+
* - Ignorable error filtering
|
|
8
|
+
* - Signal handler management
|
|
9
|
+
*/
|
|
10
|
+
import { createSubsystemLogger } from "../../logging/subsystem.js";
|
|
11
|
+
const tuiLog = createSubsystemLogger("tui");
|
|
12
|
+
/**
|
|
13
|
+
* Errors that can be safely ignored during TUI stop
|
|
14
|
+
* These typically occur when the terminal is already closed/detached
|
|
15
|
+
*/
|
|
16
|
+
const IGNORABLE_STOP_ERRORS = new Set([
|
|
17
|
+
"EBADF", // Bad file descriptor - terminal already closed
|
|
18
|
+
"EIO", // I/O error - terminal disconnected
|
|
19
|
+
"EPIPE", // Broken pipe
|
|
20
|
+
"ENOTTY", // Not a TTY
|
|
21
|
+
"ENODEV", // No such device
|
|
22
|
+
]);
|
|
23
|
+
/**
|
|
24
|
+
* Check if an error during TUI stop can be safely ignored
|
|
25
|
+
*/
|
|
26
|
+
export function isIgnorableTuiStopError(error) {
|
|
27
|
+
if (!error || typeof error !== "object") {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
const err = error;
|
|
31
|
+
// Check error code
|
|
32
|
+
if (err.code && IGNORABLE_STOP_ERRORS.has(err.code)) {
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
// Check error message for known patterns
|
|
36
|
+
const message = err.message ?? "";
|
|
37
|
+
const ignorablePatterns = [
|
|
38
|
+
/bad file descriptor/i,
|
|
39
|
+
/not a tty/i,
|
|
40
|
+
/input\/output error/i,
|
|
41
|
+
/broken pipe/i,
|
|
42
|
+
];
|
|
43
|
+
for (const pattern of ignorablePatterns) {
|
|
44
|
+
if (pattern.test(message)) {
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Safely stop the TUI with proper error handling
|
|
52
|
+
*
|
|
53
|
+
* @param tui - The TUI instance to stop
|
|
54
|
+
* @param options - Stop options
|
|
55
|
+
*/
|
|
56
|
+
export function stopTuiSafely(tui, options = {}) {
|
|
57
|
+
const { logErrors = true, exitCode = 0, shouldExit = false } = options;
|
|
58
|
+
try {
|
|
59
|
+
tui.stop();
|
|
60
|
+
tuiLog.debug("TUI stopped gracefully");
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
if (isIgnorableTuiStopError(error)) {
|
|
64
|
+
// These errors are expected when terminal is already closed
|
|
65
|
+
tuiLog.debug("TUI stop error (ignorable)", { error: String(error) });
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
if (logErrors) {
|
|
69
|
+
tuiLog.error("Error stopping TUI:", { error: String(error) });
|
|
70
|
+
}
|
|
71
|
+
// Re-throw non-ignorable errors
|
|
72
|
+
throw error;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (shouldExit) {
|
|
76
|
+
process.exit(exitCode);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Active signal handlers for cleanup
|
|
81
|
+
*/
|
|
82
|
+
const activeSignalHandlers = new Map();
|
|
83
|
+
/**
|
|
84
|
+
* Register a signal handler with tracking for cleanup
|
|
85
|
+
*
|
|
86
|
+
* @param signal - Signal name (e.g., 'SIGINT', 'SIGTERM')
|
|
87
|
+
* @param handler - Handler function
|
|
88
|
+
* @param options - Registration options
|
|
89
|
+
* @returns Cleanup function to remove the handler
|
|
90
|
+
*/
|
|
91
|
+
export function registerSignalHandler(signal, handler, options = {}) {
|
|
92
|
+
const { once = false } = options;
|
|
93
|
+
// Wrap handler to track execution
|
|
94
|
+
const wrappedHandler = () => {
|
|
95
|
+
tuiLog.debug(`Signal ${signal} received`);
|
|
96
|
+
handler();
|
|
97
|
+
};
|
|
98
|
+
if (once) {
|
|
99
|
+
process.once(signal, wrappedHandler);
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
process.on(signal, wrappedHandler);
|
|
103
|
+
}
|
|
104
|
+
// Store for potential cleanup
|
|
105
|
+
const key = `${signal}_${Date.now()}`;
|
|
106
|
+
activeSignalHandlers.set(key, handler);
|
|
107
|
+
// Return cleanup function
|
|
108
|
+
return () => {
|
|
109
|
+
process.removeListener(signal, wrappedHandler);
|
|
110
|
+
activeSignalHandlers.delete(key);
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Remove all registered signal handlers
|
|
115
|
+
*/
|
|
116
|
+
export function cleanupAllSignalHandlers() {
|
|
117
|
+
activeSignalHandlers.clear();
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Create SIGINT handler with proper cleanup
|
|
121
|
+
*
|
|
122
|
+
* @param cleanup - Cleanup function to run before exit
|
|
123
|
+
* @returns Signal handler function
|
|
124
|
+
*/
|
|
125
|
+
export function createSigintHandler(cleanup) {
|
|
126
|
+
return () => {
|
|
127
|
+
tuiLog.debug("SIGINT received, cleaning up...");
|
|
128
|
+
try {
|
|
129
|
+
cleanup?.();
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
tuiLog.error("Error during SIGINT cleanup:", { error: String(error) });
|
|
133
|
+
}
|
|
134
|
+
// Force exit after cleanup
|
|
135
|
+
process.exit(0);
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Create SIGTERM handler with proper cleanup
|
|
140
|
+
*
|
|
141
|
+
* @param cleanup - Cleanup function to run before exit
|
|
142
|
+
* @returns Signal handler function
|
|
143
|
+
*/
|
|
144
|
+
export function createSigtermHandler(cleanup) {
|
|
145
|
+
return () => {
|
|
146
|
+
tuiLog.debug("SIGTERM received, cleaning up...");
|
|
147
|
+
try {
|
|
148
|
+
cleanup?.();
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
tuiLog.error("Error during SIGTERM cleanup:", { error: String(error) });
|
|
152
|
+
}
|
|
153
|
+
process.exit(0);
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Request graceful exit with proper cleanup sequence
|
|
158
|
+
*
|
|
159
|
+
* @param tui - TUI instance
|
|
160
|
+
* @param client - Optional client to stop
|
|
161
|
+
* @param options - Exit options
|
|
162
|
+
*/
|
|
163
|
+
export function requestExit(tui, client, options = {}) {
|
|
164
|
+
const { exitCode = 0, reason = "user requested" } = options;
|
|
165
|
+
tuiLog.info(`Exit requested: ${reason}`);
|
|
166
|
+
// Stop client first if available
|
|
167
|
+
if (client) {
|
|
168
|
+
try {
|
|
169
|
+
client.stop();
|
|
170
|
+
tuiLog.debug("Client stopped");
|
|
171
|
+
}
|
|
172
|
+
catch (error) {
|
|
173
|
+
tuiLog.warn("Error stopping client:", { error: String(error) });
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
// Stop TUI safely
|
|
177
|
+
stopTuiSafely(tui, { logErrors: true, shouldExit: false });
|
|
178
|
+
// Exit process
|
|
179
|
+
process.exit(exitCode);
|
|
180
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session key normalization and comparison utilities
|
|
3
|
+
*
|
|
4
|
+
* Ensures consistent session key handling across the application
|
|
5
|
+
*/
|
|
6
|
+
import { parseAgentSessionKey, normalizeAgentId, normalizeMainKey, } from "../../routing/session-key.js";
|
|
7
|
+
/**
|
|
8
|
+
* Parse and normalize a session key
|
|
9
|
+
*
|
|
10
|
+
* @param key - Raw session key
|
|
11
|
+
* @returns Normalized session key or null if invalid
|
|
12
|
+
*/
|
|
13
|
+
export function normalizeSessionKey(key) {
|
|
14
|
+
try {
|
|
15
|
+
const parsed = parseAgentSessionKey(key);
|
|
16
|
+
if (!parsed) {
|
|
17
|
+
// Try to normalize as plain key
|
|
18
|
+
return {
|
|
19
|
+
agentId: "",
|
|
20
|
+
mainKey: normalizeMainKey(key),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
return {
|
|
24
|
+
agentId: normalizeAgentId(parsed.agentId),
|
|
25
|
+
mainKey: normalizeMainKey(parsed.rest),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Compare two session keys for equality
|
|
34
|
+
*
|
|
35
|
+
* Handles:
|
|
36
|
+
* - Case insensitivity
|
|
37
|
+
* - Different formats (agent:key vs key)
|
|
38
|
+
* - Normalization differences
|
|
39
|
+
*
|
|
40
|
+
* @param key1 - First session key
|
|
41
|
+
* @param key2 - Second session key
|
|
42
|
+
* @returns True if keys represent the same session
|
|
43
|
+
*/
|
|
44
|
+
export function isSameSessionKey(key1, key2) {
|
|
45
|
+
if (key1 === key2)
|
|
46
|
+
return true;
|
|
47
|
+
const normalized1 = normalizeSessionKey(key1);
|
|
48
|
+
const normalized2 = normalizeSessionKey(key2);
|
|
49
|
+
if (!normalized1 || !normalized2) {
|
|
50
|
+
// If normalization fails, do case-insensitive comparison
|
|
51
|
+
return key1.toLowerCase() === key2.toLowerCase();
|
|
52
|
+
}
|
|
53
|
+
return (normalized1.agentId.toLowerCase() === normalized2.agentId.toLowerCase() &&
|
|
54
|
+
normalized1.mainKey.toLowerCase() === normalized2.mainKey.toLowerCase());
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Format session key for display
|
|
58
|
+
*
|
|
59
|
+
* @param key - Session key
|
|
60
|
+
* @param maxLength - Maximum length (default: 40)
|
|
61
|
+
* @returns Formatted key
|
|
62
|
+
*/
|
|
63
|
+
export function formatSessionKeyForDisplay(key, maxLength = 40) {
|
|
64
|
+
if (key.length <= maxLength)
|
|
65
|
+
return key;
|
|
66
|
+
// Show start and end with ellipsis
|
|
67
|
+
const start = key.slice(0, maxLength / 2 - 2);
|
|
68
|
+
const end = key.slice(-(maxLength / 2 - 2));
|
|
69
|
+
return `${start}...${end}`;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Extract session identifier for logging
|
|
73
|
+
* Removes sensitive or verbose parts
|
|
74
|
+
*/
|
|
75
|
+
export function getSessionIdentifier(key) {
|
|
76
|
+
const normalized = normalizeSessionKey(key);
|
|
77
|
+
if (!normalized)
|
|
78
|
+
return key.slice(0, 20);
|
|
79
|
+
// Return main key which is usually the user-facing identifier
|
|
80
|
+
return normalized.mainKey || normalized.agentId || key.slice(0, 20);
|
|
81
|
+
}
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Text sanitization utilities for safe terminal rendering
|
|
3
|
+
*
|
|
4
|
+
* Features:
|
|
5
|
+
* - Control character stripping
|
|
6
|
+
* - ANSI sequence handling
|
|
7
|
+
* - Path and URL preservation
|
|
8
|
+
* - RTL text isolation
|
|
9
|
+
* - Binary data redaction
|
|
10
|
+
* - Safe truncation
|
|
11
|
+
*/
|
|
12
|
+
import { createSubsystemLogger } from "../../logging/subsystem.js";
|
|
13
|
+
const sanitizeLog = createSubsystemLogger("sanitize");
|
|
14
|
+
/**
|
|
15
|
+
* Maximum safe length for a single token (word)
|
|
16
|
+
* Tokens longer than this will be chunked
|
|
17
|
+
*/
|
|
18
|
+
const MAX_TOKEN_LENGTH = 4096;
|
|
19
|
+
/**
|
|
20
|
+
* Characters to strip (control characters except common whitespace)
|
|
21
|
+
*/
|
|
22
|
+
const CONTROL_CHARS_TO_STRIP = new Set([
|
|
23
|
+
0x00, // NUL - null
|
|
24
|
+
0x01, // SOH - start of heading
|
|
25
|
+
0x02, // STX - start of text
|
|
26
|
+
0x03, // ETX - end of text
|
|
27
|
+
0x04, // EOT - end of transmission
|
|
28
|
+
0x05, // ENQ - enquiry
|
|
29
|
+
0x06, // ACK - acknowledge
|
|
30
|
+
0x07, // BEL - bell
|
|
31
|
+
0x08, // BS - backspace
|
|
32
|
+
0x0b, // VT - vertical tab
|
|
33
|
+
0x0c, // FF - form feed
|
|
34
|
+
0x0e, // SO - shift out
|
|
35
|
+
0x0f, // SI - shift in
|
|
36
|
+
0x10, // DLE - data link escape
|
|
37
|
+
0x11, // DC1 - device control 1
|
|
38
|
+
0x12, // DC2 - device control 2
|
|
39
|
+
0x13, // DC3 - device control 3
|
|
40
|
+
0x14, // DC4 - device control 4
|
|
41
|
+
0x15, // NAK - negative acknowledge
|
|
42
|
+
0x16, // SYN - synchronous idle
|
|
43
|
+
0x17, // ETB - end of transmission block
|
|
44
|
+
0x18, // CAN - cancel
|
|
45
|
+
0x19, // EM - end of medium
|
|
46
|
+
0x1a, // SUB - substitute
|
|
47
|
+
0x1b, // ESC - escape (handled separately)
|
|
48
|
+
0x1c, // FS - file separator
|
|
49
|
+
0x1d, // GS - group separator
|
|
50
|
+
0x1e, // RS - record separator
|
|
51
|
+
0x1f, // US - unit separator
|
|
52
|
+
0x7f, // DEL - delete
|
|
53
|
+
]);
|
|
54
|
+
/**
|
|
55
|
+
* Check if a character is a control character that should be stripped
|
|
56
|
+
*/
|
|
57
|
+
function shouldStripControlChar(codePoint) {
|
|
58
|
+
return CONTROL_CHARS_TO_STRIP.has(codePoint);
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Check if text contains binary/null bytes
|
|
62
|
+
*/
|
|
63
|
+
function containsBinaryData(text) {
|
|
64
|
+
for (let i = 0; i < Math.min(text.length, 1024); i++) {
|
|
65
|
+
const code = text.charCodeAt(i);
|
|
66
|
+
if (code === 0x00 || (code < 0x20 && !isCommonWhitespace(code))) {
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Check if character is common whitespace (preserved)
|
|
74
|
+
*/
|
|
75
|
+
function isCommonWhitespace(codePoint) {
|
|
76
|
+
return (codePoint === 0x09 || // TAB
|
|
77
|
+
codePoint === 0x0a || // LF
|
|
78
|
+
codePoint === 0x0d || // CR
|
|
79
|
+
codePoint === 0x20); // SPACE
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Check if character is RTL (right-to-left)
|
|
83
|
+
*/
|
|
84
|
+
function isRtlChar(codePoint) {
|
|
85
|
+
// Hebrew: U+0590 - U+05FF
|
|
86
|
+
if (codePoint >= 0x0590 && codePoint <= 0x05ff)
|
|
87
|
+
return true;
|
|
88
|
+
// Arabic: U+0600 - U+06FF
|
|
89
|
+
if (codePoint >= 0x0600 && codePoint <= 0x06ff)
|
|
90
|
+
return true;
|
|
91
|
+
// Arabic Supplement: U+0750 - U+077F
|
|
92
|
+
if (codePoint >= 0x0750 && codePoint <= 0x077f)
|
|
93
|
+
return true;
|
|
94
|
+
// RTL marks
|
|
95
|
+
if (codePoint === 0x200f || codePoint === 0x202e)
|
|
96
|
+
return true;
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Check if text contains RTL characters
|
|
101
|
+
*/
|
|
102
|
+
function containsRtl(text) {
|
|
103
|
+
for (const char of text) {
|
|
104
|
+
if (isRtlChar(char.codePointAt(0) ?? 0)) {
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* URL pattern for detection
|
|
112
|
+
*/
|
|
113
|
+
const URL_PATTERN = /https?:\/\/[^\s<>"{}|\\^`[\]]+/gi;
|
|
114
|
+
/**
|
|
115
|
+
* Path pattern for detection (Unix and Windows)
|
|
116
|
+
*/
|
|
117
|
+
const PATH_PATTERN = /(?:~\/|\/|[a-zA-Z]:\\)[^\s<>"{}|\\^`[\]]*/g;
|
|
118
|
+
/**
|
|
119
|
+
* Truncate a long token while preserving meaning
|
|
120
|
+
*/
|
|
121
|
+
function truncateToken(token, maxLength) {
|
|
122
|
+
if (token.length <= maxLength)
|
|
123
|
+
return token;
|
|
124
|
+
// For URLs, try to preserve the domain and endpoint
|
|
125
|
+
if (token.startsWith("http")) {
|
|
126
|
+
try {
|
|
127
|
+
const url = new URL(token);
|
|
128
|
+
const domain = url.hostname;
|
|
129
|
+
const path = url.pathname;
|
|
130
|
+
const search = url.search;
|
|
131
|
+
// Calculate available space for path
|
|
132
|
+
const prefix = `${url.protocol}//${domain}`;
|
|
133
|
+
const suffix = "…";
|
|
134
|
+
const available = maxLength - prefix.length - suffix.length;
|
|
135
|
+
if (available > 10) {
|
|
136
|
+
const truncatedPath = path.slice(0, Math.max(0, available - search.length)) + (search ? "?…" : "") + suffix;
|
|
137
|
+
return prefix + truncatedPath;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
// Not a valid URL, fall through to default truncation
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
// Default truncation
|
|
145
|
+
return token.slice(0, maxLength - 1) + "…";
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Sanitize text for safe terminal rendering
|
|
149
|
+
*
|
|
150
|
+
* @param text - Raw text to sanitize
|
|
151
|
+
* @param options - Sanitization options
|
|
152
|
+
* @returns Sanitized text safe for terminal rendering
|
|
153
|
+
*/
|
|
154
|
+
export function sanitizeRenderableText(text, options = {}) {
|
|
155
|
+
const { maxTokenLength = MAX_TOKEN_LENGTH, preserveUrls = true, isolateRtl = true, redactBinary = true, maxTotalLength, } = options;
|
|
156
|
+
if (!text || typeof text !== "string") {
|
|
157
|
+
return "";
|
|
158
|
+
}
|
|
159
|
+
let result = text;
|
|
160
|
+
// Check for binary data
|
|
161
|
+
if (redactBinary && containsBinaryData(result)) {
|
|
162
|
+
sanitizeLog.debug("Redacting binary data from text");
|
|
163
|
+
return "[binary data redacted]";
|
|
164
|
+
}
|
|
165
|
+
// Strip control characters
|
|
166
|
+
result = result
|
|
167
|
+
.split("")
|
|
168
|
+
.map((char) => {
|
|
169
|
+
const code = char.codePointAt(0) ?? 0;
|
|
170
|
+
if (shouldStripControlChar(code)) {
|
|
171
|
+
return "";
|
|
172
|
+
}
|
|
173
|
+
return char;
|
|
174
|
+
})
|
|
175
|
+
.join("");
|
|
176
|
+
// Handle very long tokens (preserve URLs/paths)
|
|
177
|
+
if (preserveUrls) {
|
|
178
|
+
// First identify URLs and paths to preserve
|
|
179
|
+
const preserved = [];
|
|
180
|
+
let preservedIndex = 0;
|
|
181
|
+
result = result.replace(URL_PATTERN, (url) => {
|
|
182
|
+
if (url.length > maxTokenLength) {
|
|
183
|
+
url = truncateToken(url, maxTokenLength);
|
|
184
|
+
}
|
|
185
|
+
const placeholder = `\x00PRESERVED_${preservedIndex++}\x00`;
|
|
186
|
+
preserved.push(url);
|
|
187
|
+
return placeholder;
|
|
188
|
+
});
|
|
189
|
+
result = result.replace(PATH_PATTERN, (path) => {
|
|
190
|
+
if (path.length > maxTokenLength) {
|
|
191
|
+
path = truncateToken(path, maxTokenLength);
|
|
192
|
+
}
|
|
193
|
+
const placeholder = `\x00PRESERVED_${preservedIndex++}\x00`;
|
|
194
|
+
preserved.push(path);
|
|
195
|
+
return placeholder;
|
|
196
|
+
});
|
|
197
|
+
// Truncate remaining long tokens
|
|
198
|
+
result = result
|
|
199
|
+
.split(/\s+/)
|
|
200
|
+
.map((token) => {
|
|
201
|
+
if (token.length > maxTokenLength) {
|
|
202
|
+
return truncateToken(token, maxTokenLength);
|
|
203
|
+
}
|
|
204
|
+
return token;
|
|
205
|
+
})
|
|
206
|
+
.join(" ");
|
|
207
|
+
// Restore preserved URLs/paths
|
|
208
|
+
for (let i = 0; i < preserved.length; i++) {
|
|
209
|
+
result = result.replace(`\x00PRESERVED_${i}\x00`, preserved[i]);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
// Simple token truncation
|
|
214
|
+
result = result
|
|
215
|
+
.split(/\s+/)
|
|
216
|
+
.map((token) => {
|
|
217
|
+
if (token.length > maxTokenLength) {
|
|
218
|
+
return truncateToken(token, maxTokenLength);
|
|
219
|
+
}
|
|
220
|
+
return token;
|
|
221
|
+
})
|
|
222
|
+
.join(" ");
|
|
223
|
+
}
|
|
224
|
+
// Isolate RTL text with Unicode marks
|
|
225
|
+
if (isolateRtl && containsRtl(result)) {
|
|
226
|
+
// Add LTR mark at start and end to ensure proper display
|
|
227
|
+
result = "\u200e" + result + "\u200e";
|
|
228
|
+
}
|
|
229
|
+
// Apply total length limit
|
|
230
|
+
if (maxTotalLength && result.length > maxTotalLength) {
|
|
231
|
+
result = result.slice(0, maxTotalLength - 3) + "...";
|
|
232
|
+
}
|
|
233
|
+
return result;
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Sanitize error message for display
|
|
237
|
+
*/
|
|
238
|
+
export function sanitizeErrorMessage(error) {
|
|
239
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
240
|
+
return sanitizeRenderableText(message, {
|
|
241
|
+
maxTokenLength: 1024,
|
|
242
|
+
maxTotalLength: 8192,
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Strip ANSI escape sequences from text
|
|
247
|
+
*/
|
|
248
|
+
export function stripAnsi(text) {
|
|
249
|
+
// eslint-disable-next-line no-control-regex
|
|
250
|
+
return text.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "");
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Check if text is safe to render (no sanitization needed)
|
|
254
|
+
*/
|
|
255
|
+
export function isSafeToRender(text) {
|
|
256
|
+
if (!text || text.length === 0)
|
|
257
|
+
return true;
|
|
258
|
+
// Check length
|
|
259
|
+
if (text.length > MAX_TOKEN_LENGTH)
|
|
260
|
+
return false;
|
|
261
|
+
// Check for control characters
|
|
262
|
+
for (let i = 0; i < text.length; i++) {
|
|
263
|
+
const code = text.charCodeAt(i);
|
|
264
|
+
if (shouldStripControlChar(code)) {
|
|
265
|
+
return false;
|
|
266
|
+
}
|
|
267
|
+
if (code === 0x00)
|
|
268
|
+
return false; // Null bytes
|
|
269
|
+
}
|
|
270
|
+
return true;
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Create a sanitized preview of text (for logging/debugging)
|
|
274
|
+
*/
|
|
275
|
+
export function createTextPreview(text, maxLength = 200) {
|
|
276
|
+
const sanitized = sanitizeRenderableText(text, {
|
|
277
|
+
maxTokenLength: maxLength,
|
|
278
|
+
maxTotalLength: maxLength,
|
|
279
|
+
});
|
|
280
|
+
if (text.length > maxLength) {
|
|
281
|
+
return sanitized + ` [+${text.length - maxLength} chars]`;
|
|
282
|
+
}
|
|
283
|
+
return sanitized;
|
|
284
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LRU (Least Recently Used) Cache implementation
|
|
3
|
+
*
|
|
4
|
+
* Features:
|
|
5
|
+
* - Fixed maximum size
|
|
6
|
+
* - Automatic eviction of least recently used items
|
|
7
|
+
* - O(1) get and set operations
|
|
8
|
+
* - Type-safe with TypeScript generics
|
|
9
|
+
*/
|
|
10
|
+
export class LRUCache {
|
|
11
|
+
cache;
|
|
12
|
+
maxSize;
|
|
13
|
+
onEvict;
|
|
14
|
+
constructor(options) {
|
|
15
|
+
this.cache = new Map();
|
|
16
|
+
this.maxSize = options.maxSize;
|
|
17
|
+
this.onEvict = options.onEvict;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Get a value from the cache
|
|
21
|
+
* Updates recency of the accessed item
|
|
22
|
+
*/
|
|
23
|
+
get(key) {
|
|
24
|
+
const value = this.cache.get(key);
|
|
25
|
+
if (value !== undefined) {
|
|
26
|
+
// Move to end (most recently used)
|
|
27
|
+
this.cache.delete(key);
|
|
28
|
+
this.cache.set(key, value);
|
|
29
|
+
}
|
|
30
|
+
return value;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Set a value in the cache
|
|
34
|
+
* Evicts least recently used item if at capacity
|
|
35
|
+
*/
|
|
36
|
+
set(key, value) {
|
|
37
|
+
if (this.cache.has(key)) {
|
|
38
|
+
// Update existing key
|
|
39
|
+
this.cache.delete(key);
|
|
40
|
+
}
|
|
41
|
+
else if (this.cache.size >= this.maxSize) {
|
|
42
|
+
// Evict least recently used (first item)
|
|
43
|
+
const firstKey = this.cache.keys().next().value;
|
|
44
|
+
if (firstKey !== undefined) {
|
|
45
|
+
const evictedValue = this.cache.get(firstKey);
|
|
46
|
+
this.cache.delete(firstKey);
|
|
47
|
+
this.onEvict?.(firstKey, evictedValue);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
this.cache.set(key, value);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Check if a key exists in the cache
|
|
54
|
+
*/
|
|
55
|
+
has(key) {
|
|
56
|
+
return this.cache.has(key);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Delete a key from the cache
|
|
60
|
+
*/
|
|
61
|
+
delete(key) {
|
|
62
|
+
return this.cache.delete(key);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Clear all items from the cache
|
|
66
|
+
*/
|
|
67
|
+
clear() {
|
|
68
|
+
this.cache.clear();
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Get the current size of the cache
|
|
72
|
+
*/
|
|
73
|
+
get size() {
|
|
74
|
+
return this.cache.size;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Get all keys in the cache (in LRU order)
|
|
78
|
+
*/
|
|
79
|
+
keys() {
|
|
80
|
+
return this.cache.keys();
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Get all values in the cache (in LRU order)
|
|
84
|
+
*/
|
|
85
|
+
values() {
|
|
86
|
+
return this.cache.values();
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Get cache statistics
|
|
90
|
+
*/
|
|
91
|
+
getStats() {
|
|
92
|
+
return {
|
|
93
|
+
size: this.cache.size,
|
|
94
|
+
maxSize: this.maxSize,
|
|
95
|
+
utilization: this.cache.size / this.maxSize,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Create a cached version of a function with LRU eviction
|
|
101
|
+
*/
|
|
102
|
+
export function memoizeWithLRU(fn, options) {
|
|
103
|
+
const cache = new LRUCache({ maxSize: options.maxSize });
|
|
104
|
+
const defaultKeyFn = (...args) => JSON.stringify(args);
|
|
105
|
+
const keyFn = options.keyFn ?? defaultKeyFn;
|
|
106
|
+
return (...args) => {
|
|
107
|
+
const key = keyFn(...args);
|
|
108
|
+
const cached = cache.get(key);
|
|
109
|
+
if (cached !== undefined) {
|
|
110
|
+
return cached;
|
|
111
|
+
}
|
|
112
|
+
const result = fn(...args);
|
|
113
|
+
cache.set(key, result);
|
|
114
|
+
return result;
|
|
115
|
+
};
|
|
116
|
+
}
|