@poolzin/pool-bot 2026.3.9 → 2026.3.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (125) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +147 -69
  3. package/dist/.buildstamp +1 -1
  4. package/dist/agents/error-classifier.js +26 -77
  5. package/dist/agents/skills/security.js +1 -7
  6. package/dist/build-info.json +3 -3
  7. package/dist/cli/cron-cli/register.cron-dashboard.js +339 -0
  8. package/dist/cli/cron-cli/register.js +2 -0
  9. package/dist/cli/errors.js +187 -0
  10. package/dist/cli/program/command-registry.js +13 -0
  11. package/dist/cli/program/register.maintenance.js +21 -0
  12. package/dist/cli/program/register.subclis.js +9 -0
  13. package/dist/cli/swarm-cli/register.js +8 -0
  14. package/dist/cli/swarm-cli/register.swarm-status.js +488 -0
  15. package/dist/cli/telemetry-cli/register.js +10 -0
  16. package/dist/cli/telemetry-cli/register.telemetry-alerts.js +176 -0
  17. package/dist/cli/telemetry-cli/register.telemetry-metrics.js +323 -0
  18. package/dist/cli/telemetry-cli/register.telemetry-status.js +179 -0
  19. package/dist/commands/doctor-checks.js +498 -0
  20. package/dist/context-engine/index.js +1 -1
  21. package/dist/context-engine/legacy.js +1 -3
  22. package/dist/context-engine/summarizing.js +5 -8
  23. package/dist/cron/service/timer.js +18 -0
  24. package/dist/gateway/protocol/index.js +5 -2
  25. package/dist/gateway/protocol/schema/error-codes.js +1 -0
  26. package/dist/gateway/protocol/schema/swarm.js +80 -0
  27. package/dist/gateway/protocol/schema.js +1 -0
  28. package/dist/gateway/server-close.js +4 -0
  29. package/dist/gateway/server-constants.js +1 -0
  30. package/dist/gateway/server-cron.js +29 -0
  31. package/dist/gateway/server-maintenance.js +35 -2
  32. package/dist/gateway/server-methods/swarm.js +58 -0
  33. package/dist/gateway/server-methods/telemetry.js +71 -0
  34. package/dist/gateway/server-methods-list.js +8 -0
  35. package/dist/gateway/server-methods.js +9 -2
  36. package/dist/gateway/server.impl.js +33 -16
  37. package/dist/infra/abort-pattern.js +4 -4
  38. package/dist/infra/retry.js +3 -1
  39. package/dist/skills/commands.js +7 -25
  40. package/dist/skills/index.js +14 -17
  41. package/dist/skills/parser.js +12 -27
  42. package/dist/skills/registry.js +3 -6
  43. package/dist/skills/security.js +2 -8
  44. package/dist/swarm/service.js +247 -0
  45. package/dist/telemetry/alert-engine.js +258 -0
  46. package/dist/telemetry/cron-instrumentation.js +49 -0
  47. package/dist/telemetry/gateway-instrumentation.js +80 -0
  48. package/dist/telemetry/instrumentation.js +66 -0
  49. package/dist/telemetry/service.js +345 -0
  50. package/dist/tui/components/assistant-message.js +6 -2
  51. package/dist/tui/components/hyperlink-markdown.js +32 -0
  52. package/dist/tui/components/searchable-select-list.js +12 -1
  53. package/dist/tui/components/user-message.js +6 -2
  54. package/dist/tui/index.js +22 -6
  55. package/dist/tui/theme/theme-detection.js +226 -0
  56. package/dist/tui/tui-command-handlers.js +20 -0
  57. package/dist/tui/tui-formatters.js +4 -3
  58. package/dist/tui/utils/ctrl-c-handler.js +67 -0
  59. package/dist/tui/utils/osc8-hyperlinks.js +208 -0
  60. package/dist/tui/utils/safe-stop.js +180 -0
  61. package/dist/tui/utils/session-key-utils.js +81 -0
  62. package/dist/tui/utils/text-sanitization.js +284 -0
  63. package/dist/utils/lru-cache.js +116 -0
  64. package/dist/utils/performance.js +199 -0
  65. package/dist/utils/retry.js +240 -0
  66. package/docs/MELHORIAS_IMPLEMENTADAS.md +228 -0
  67. package/docs/MELHORIAS_PROFISSIONAIS.md +282 -0
  68. package/docs/PLANO_ACAO_TUI.md +357 -0
  69. package/docs/PROGRESSO_TUI.md +66 -0
  70. package/docs/RELATORIO_FINAL.md +217 -0
  71. package/docs/diagnostico-shell-completion.md +265 -0
  72. package/docs/features/advanced-memory.md +585 -0
  73. package/docs/features/discord-components-v2.md +277 -0
  74. package/docs/features/swarm.md +100 -0
  75. package/docs/features/telemetry.md +284 -0
  76. package/docs/integrations/INTEGRATION_PLAN.md +665 -345
  77. package/docs/models/provider-infrastructure.md +400 -0
  78. package/docs/security/exec-approvals.md +294 -0
  79. package/extensions/bluebubbles/package.json +1 -1
  80. package/extensions/copilot-proxy/package.json +1 -1
  81. package/extensions/diagnostics-otel/package.json +1 -1
  82. package/extensions/discord/package.json +1 -1
  83. package/extensions/feishu/package.json +1 -1
  84. package/extensions/google-antigravity-auth/package.json +1 -1
  85. package/extensions/google-gemini-cli-auth/package.json +1 -1
  86. package/extensions/googlechat/package.json +1 -1
  87. package/extensions/hexstrike-bridge/README.md +119 -0
  88. package/extensions/hexstrike-bridge/index.test.ts +247 -0
  89. package/extensions/hexstrike-bridge/index.ts +487 -0
  90. package/extensions/hexstrike-bridge/package.json +17 -0
  91. package/extensions/imessage/package.json +1 -1
  92. package/extensions/irc/package.json +1 -1
  93. package/extensions/line/package.json +1 -1
  94. package/extensions/llm-task/package.json +1 -1
  95. package/extensions/lobster/package.json +1 -1
  96. package/extensions/matrix/CHANGELOG.md +5 -0
  97. package/extensions/matrix/package.json +1 -1
  98. package/extensions/mattermost/package.json +1 -1
  99. package/extensions/mcp-server/index.ts +14 -0
  100. package/extensions/mcp-server/package.json +11 -0
  101. package/extensions/mcp-server/src/service.ts +540 -0
  102. package/extensions/memory-core/package.json +1 -1
  103. package/extensions/memory-lancedb/package.json +1 -1
  104. package/extensions/minimax-portal-auth/package.json +1 -1
  105. package/extensions/msteams/CHANGELOG.md +5 -0
  106. package/extensions/msteams/package.json +1 -1
  107. package/extensions/nextcloud-talk/package.json +1 -1
  108. package/extensions/nostr/CHANGELOG.md +5 -0
  109. package/extensions/nostr/package.json +1 -1
  110. package/extensions/open-prose/package.json +1 -1
  111. package/extensions/openai-codex-auth/package.json +1 -1
  112. package/extensions/signal/package.json +1 -1
  113. package/extensions/slack/package.json +1 -1
  114. package/extensions/telegram/package.json +1 -1
  115. package/extensions/tlon/package.json +1 -1
  116. package/extensions/twitch/CHANGELOG.md +5 -0
  117. package/extensions/twitch/package.json +1 -1
  118. package/extensions/voice-call/CHANGELOG.md +5 -0
  119. package/extensions/voice-call/package.json +1 -1
  120. package/extensions/whatsapp/package.json +1 -1
  121. package/extensions/zalo/CHANGELOG.md +5 -0
  122. package/extensions/zalo/package.json +1 -1
  123. package/extensions/zalouser/CHANGELOG.md +5 -0
  124. package/extensions/zalouser/package.json +1 -1
  125. 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
+ }