@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,226 @@
1
+ /**
2
+ * Theme detection and light/dark mode utilities
3
+ *
4
+ * Features:
5
+ * - Automatic light/dark detection via environment variables
6
+ * - WCAG contrast ratio calculation
7
+ * - COLORFGBG parsing
8
+ * - Manual theme override via POOLBOT_THEME
9
+ */
10
+ import { createSubsystemLogger } from "../../logging/subsystem.js";
11
+ const themeLog = createSubsystemLogger("theme");
12
+ /**
13
+ * Detect theme mode from environment
14
+ *
15
+ * Detection order:
16
+ * 1. POOLBOT_THEME environment variable
17
+ * 2. COLORFGBG environment variable (terminal background color)
18
+ * 3. Default to "dark"
19
+ */
20
+ export function detectThemeMode() {
21
+ // Check for explicit theme override
22
+ const envTheme = process.env.POOLBOT_THEME?.toLowerCase();
23
+ if (envTheme === "light" || envTheme === "dark") {
24
+ themeLog.debug(`Theme set via POOLBOT_THEME: ${envTheme}`);
25
+ return envTheme;
26
+ }
27
+ // Try to detect from COLORFGBG
28
+ const colorFgbg = process.env.COLORFGBG;
29
+ if (colorFgbg) {
30
+ const detected = parseColorFgbg(colorFgbg);
31
+ if (detected) {
32
+ themeLog.debug(`Theme detected from COLORFGBG: ${detected}`);
33
+ return detected;
34
+ }
35
+ }
36
+ // Default to dark mode
37
+ themeLog.debug("Theme detection: defaulting to dark");
38
+ return "dark";
39
+ }
40
+ /**
41
+ * Parse COLORFGBG environment variable
42
+ *
43
+ * Format: "fg;bg" or "fg:bg" where colors are 0-15 or RGB hex
44
+ * Examples:
45
+ * "0;15" - black on white (light mode)
46
+ * "15;0" - white on black (dark mode)
47
+ * "rgb:0000/0000/0000;rgb:ffff/ffff/ffff" - RGB values
48
+ */
49
+ export function parseColorFgbg(colorFgbg) {
50
+ try {
51
+ // Split by semicolon or colon
52
+ const parts = colorFgbg.split(/[;:]/);
53
+ if (parts.length < 2)
54
+ return null;
55
+ const bgPart = parts[1] ?? "";
56
+ // Check for RGB format: rgb:R/G/B
57
+ if (bgPart.startsWith("rgb:")) {
58
+ const rgb = parseRgbHex(bgPart);
59
+ if (rgb) {
60
+ return isLightColor(rgb) ? "light" : "dark";
61
+ }
62
+ return null;
63
+ }
64
+ // Check for numeric color (0-15)
65
+ const colorNum = parseInt(bgPart, 10);
66
+ if (!isNaN(colorNum)) {
67
+ // Standard 16-color palette:
68
+ // 0-7: dark colors, 8-15: light colors
69
+ if (colorNum >= 0 && colorNum <= 7) {
70
+ return "dark";
71
+ }
72
+ if (colorNum >= 8 && colorNum <= 15) {
73
+ return "light";
74
+ }
75
+ }
76
+ return null;
77
+ }
78
+ catch {
79
+ return null;
80
+ }
81
+ }
82
+ /**
83
+ * Parse RGB hex format: rgb:RRRR/GGGG/BBBB or rgb:RR/GG/BB
84
+ */
85
+ export function parseRgbHex(rgbStr) {
86
+ try {
87
+ // Remove "rgb:" prefix
88
+ const parts = rgbStr.replace("rgb:", "").split("/");
89
+ if (parts.length !== 3)
90
+ return null;
91
+ // Parse each component
92
+ const parseComponent = (hex) => {
93
+ const len = hex.length;
94
+ if (len === 2) {
95
+ // 8-bit color
96
+ return parseInt(hex, 16);
97
+ }
98
+ if (len === 4) {
99
+ // 16-bit color, take high byte
100
+ return parseInt(hex.slice(0, 2), 16);
101
+ }
102
+ return 0;
103
+ };
104
+ return {
105
+ r: parseComponent(parts[0] ?? "0"),
106
+ g: parseComponent(parts[1] ?? "0"),
107
+ b: parseComponent(parts[2] ?? "0"),
108
+ };
109
+ }
110
+ catch {
111
+ return null;
112
+ }
113
+ }
114
+ /**
115
+ * Calculate relative luminance of an RGB color (WCAG formula)
116
+ * https://www.w3.org/TR/WCAG20/#relativeluminancedef
117
+ */
118
+ export function relativeLuminance(rgb) {
119
+ // Normalize to 0-1
120
+ const rsRGB = rgb.r / 255;
121
+ const gsRGB = rgb.g / 255;
122
+ const bsRGB = rgb.b / 255;
123
+ // Apply gamma correction
124
+ const r = rsRGB <= 0.03928 ? rsRGB / 12.92 : Math.pow((rsRGB + 0.055) / 1.055, 2.4);
125
+ const g = gsRGB <= 0.03928 ? gsRGB / 12.92 : Math.pow((gsRGB + 0.055) / 1.055, 2.4);
126
+ const b = bsRGB <= 0.03928 ? bsRGB / 12.92 : Math.pow((bsRGB + 0.055) / 1.055, 2.4);
127
+ return 0.2126 * r + 0.7152 * g + 0.0722 * b;
128
+ }
129
+ /**
130
+ * Calculate contrast ratio between two colors (WCAG formula)
131
+ * https://www.w3.org/TR/WCAG20/#contrast-ratiodef
132
+ */
133
+ export function contrastRatio(color1, color2) {
134
+ const lum1 = relativeLuminance(color1);
135
+ const lum2 = relativeLuminance(color2);
136
+ const lighter = Math.max(lum1, lum2);
137
+ const darker = Math.min(lum1, lum2);
138
+ return (lighter + 0.05) / (darker + 0.05);
139
+ }
140
+ /**
141
+ * Determine if a color is "light" based on its luminance
142
+ * Uses threshold of 0.5 (midpoint between black and white)
143
+ */
144
+ export function isLightColor(rgb) {
145
+ const luminance = relativeLuminance(rgb);
146
+ return luminance > 0.5;
147
+ }
148
+ /**
149
+ * Check if contrast ratio meets WCAG AA standards
150
+ * AA requires 4.5:1 for normal text, 3:1 for large text
151
+ */
152
+ export function meetsContrastStandard(fg, bg, level = "AA", largeText = false) {
153
+ const ratio = contrastRatio(fg, bg);
154
+ if (level === "AAA") {
155
+ return largeText ? ratio >= 4.5 : ratio >= 7;
156
+ }
157
+ // AA level
158
+ return largeText ? ratio >= 3 : ratio >= 4.5;
159
+ }
160
+ /**
161
+ * Light theme color palette
162
+ */
163
+ export const lightThemePalette = {
164
+ // Backgrounds
165
+ bg: { r: 250, g: 250, b: 250 }, // Very light gray
166
+ userBg: { r: 230, g: 245, b: 255 }, // Light blue
167
+ dimBg: { r: 240, g: 240, b: 240 }, // Light gray
168
+ // Foregrounds
169
+ fg: { r: 30, g: 30, b: 30 }, // Near black
170
+ dim: { r: 100, g: 100, b: 100 }, // Gray
171
+ muted: { r: 120, g: 120, b: 120 }, // Medium gray
172
+ // Accents
173
+ accent: { r: 0, g: 100, b: 200 }, // Blue
174
+ accentText: { r: 0, g: 80, b: 160 }, // Darker blue
175
+ userText: { r: 0, g: 60, b: 120 }, // Dark blue
176
+ // Semantic
177
+ error: { r: 200, g: 0, b: 0 }, // Red
178
+ success: { r: 0, g: 150, b: 0 }, // Green
179
+ warning: { r: 200, g: 150, b: 0 }, // Orange
180
+ // Assistant text (uses terminal default)
181
+ assistantText: { r: 30, g: 30, b: 30 },
182
+ };
183
+ /**
184
+ * Dark theme color palette (current default)
185
+ */
186
+ export const darkThemePalette = {
187
+ // Backgrounds
188
+ bg: { r: 20, g: 20, b: 20 }, // Very dark gray
189
+ userBg: { r: 40, g: 60, b: 80 }, // Dark blue
190
+ dimBg: { r: 40, g: 40, b: 40 }, // Dark gray
191
+ // Foregrounds
192
+ fg: { r: 230, g: 230, b: 230 }, // Near white
193
+ dim: { r: 150, g: 150, b: 150 }, // Gray
194
+ muted: { r: 120, g: 120, b: 120 }, // Medium gray
195
+ // Accents
196
+ accent: { r: 100, g: 180, b: 255 }, // Light blue
197
+ accentText: { r: 100, g: 200, b: 255 }, // Cyan
198
+ userText: { r: 150, g: 220, b: 255 }, // Light cyan
199
+ // Semantic
200
+ error: { r: 255, g: 100, b: 100 }, // Light red
201
+ success: { r: 100, g: 255, b: 100 }, // Light green
202
+ warning: { r: 255, g: 200, b: 100 }, // Light orange
203
+ // Assistant text (uses terminal default)
204
+ assistantText: { r: 230, g: 230, b: 230 },
205
+ };
206
+ /**
207
+ * Get the appropriate color palette for the current theme
208
+ */
209
+ export function getThemePalette(mode = detectThemeMode()) {
210
+ return mode === "light" ? lightThemePalette : darkThemePalette;
211
+ }
212
+ /**
213
+ * Convert RGB to ANSI color code (approximate)
214
+ * This is a simplified version - for exact colors, terminals support true color
215
+ */
216
+ export function rgbToAnsi(rgb, foreground = true) {
217
+ // Use 24-bit true color if terminal supports it
218
+ const prefix = foreground ? "38" : "48";
219
+ return `\x1b[${prefix};2;${rgb.r};${rgb.g};${rgb.b}m`;
220
+ }
221
+ /**
222
+ * Reset ANSI colors
223
+ */
224
+ export function resetAnsi() {
225
+ return "\x1b[0m";
226
+ }
@@ -1,3 +1,4 @@
1
+ import { randomUUID } from "node:crypto";
1
2
  import { formatThinkingLevels, normalizeUsageDisplay, resolveResponseUsageMode, } from "../auto-reply/thinking.js";
2
3
  import { formatRelativeTimestamp } from "../infra/format-time/format-relative.js";
3
4
  import { normalizeAgentId } from "../routing/session-key.js";
@@ -364,6 +365,25 @@ export function createCommandHandlers(context) {
364
365
  }
365
366
  break;
366
367
  case "new":
368
+ try {
369
+ // Create a new isolated session with unique key
370
+ const newSessionKey = `tui-${randomUUID()}`;
371
+ // Clear token counts
372
+ state.sessionInfo.inputTokens = null;
373
+ state.sessionInfo.outputTokens = null;
374
+ state.sessionInfo.totalTokens = null;
375
+ state.sessionInfo.updatedAt = null; // Reset freshness timestamp
376
+ tui.requestRender();
377
+ // Switch to new session
378
+ await setSession(newSessionKey);
379
+ chatLog.addSystem(`created new session: ${newSessionKey}`);
380
+ chatLog.clear(); // Clear chat log for fresh session
381
+ await loadHistory();
382
+ }
383
+ catch (err) {
384
+ chatLog.addSystem(`failed to create new session: ${String(err)}`);
385
+ }
386
+ break;
367
387
  case "reset":
368
388
  try {
369
389
  // Clear token counts immediately to avoid stale display (#1523)
@@ -1,5 +1,6 @@
1
1
  import { formatTokenCount } from "../utils/usage-format.js";
2
2
  import { formatRawAssistantErrorForUi } from "../agents/pi-embedded-helpers.js";
3
+ import { sanitizeRenderableText } from "./utils/text-sanitization.js";
3
4
  export function resolveFinalAssistantText(params) {
4
5
  const finalText = params.finalText ?? "";
5
6
  if (finalText.trim())
@@ -61,7 +62,7 @@ export function extractContentFromMessage(message) {
61
62
  const stopReason = typeof record.stopReason === "string" ? record.stopReason : "";
62
63
  if (stopReason === "error") {
63
64
  const errorMessage = typeof record.errorMessage === "string" ? record.errorMessage : "";
64
- return formatRawAssistantErrorForUi(errorMessage);
65
+ return sanitizeRenderableText(formatRawAssistantErrorForUi(errorMessage));
65
66
  }
66
67
  return "";
67
68
  }
@@ -79,7 +80,7 @@ export function extractContentFromMessage(message) {
79
80
  const stopReason = typeof record.stopReason === "string" ? record.stopReason : "";
80
81
  if (stopReason === "error") {
81
82
  const errorMessage = typeof record.errorMessage === "string" ? record.errorMessage : "";
82
- return formatRawAssistantErrorForUi(errorMessage);
83
+ return sanitizeRenderableText(formatRawAssistantErrorForUi(errorMessage));
83
84
  }
84
85
  }
85
86
  return parts.join("\n").trim();
@@ -121,7 +122,7 @@ export function extractTextFromMessage(message, opts) {
121
122
  if (stopReason !== "error")
122
123
  return "";
123
124
  const errorMessage = typeof record.errorMessage === "string" ? record.errorMessage : "";
124
- return formatRawAssistantErrorForUi(errorMessage);
125
+ return sanitizeRenderableText(formatRawAssistantErrorForUi(errorMessage));
125
126
  }
126
127
  export function isCommandMessage(message) {
127
128
  if (!message || typeof message !== "object")
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Ctrl+C handling utilities with state machine
3
+ *
4
+ * States:
5
+ * - "clear": Clear input if text present
6
+ * - "warn": Show warning on first Ctrl+C with empty input
7
+ * - "exit": Exit on second Ctrl+C within timeout
8
+ */
9
+ /**
10
+ * Create a new Ctrl+C state machine
11
+ */
12
+ export function createCtrlCStateMachine(timeoutMs = 1000) {
13
+ return {
14
+ state: "clear",
15
+ lastCtrlCAt: 0,
16
+ timeoutMs,
17
+ };
18
+ }
19
+ /**
20
+ * Process Ctrl+C event and determine action
21
+ *
22
+ * @param machine - State machine instance
23
+ * @param hasInput - Whether there's text in the input
24
+ * @returns The action to take: "clear", "warn", or "exit"
25
+ */
26
+ export function processCtrlC(machine, hasInput) {
27
+ const now = Date.now();
28
+ // If has input, always clear
29
+ if (hasInput) {
30
+ machine.state = "clear";
31
+ machine.lastCtrlCAt = now;
32
+ return "clear";
33
+ }
34
+ // Check if within double-press timeout
35
+ const withinTimeout = now - machine.lastCtrlCAt < machine.timeoutMs;
36
+ if (machine.state === "warn" && withinTimeout) {
37
+ // Second press within timeout -> exit
38
+ machine.state = "exit";
39
+ return "exit";
40
+ }
41
+ // First press or timeout expired -> warn
42
+ machine.state = "warn";
43
+ machine.lastCtrlCAt = now;
44
+ return "warn";
45
+ }
46
+ /**
47
+ * Format status message based on Ctrl+C state
48
+ */
49
+ export function getCtrlCStatusMessage(state, timeoutMs) {
50
+ switch (state) {
51
+ case "clear":
52
+ return "cleared input";
53
+ case "warn":
54
+ return `press ctrl+c again within ${Math.ceil(timeoutMs / 1000)}s to exit`;
55
+ case "exit":
56
+ return "exiting...";
57
+ default:
58
+ return "";
59
+ }
60
+ }
61
+ /**
62
+ * Reset state machine
63
+ */
64
+ export function resetCtrlCState(machine) {
65
+ machine.state = "clear";
66
+ machine.lastCtrlCAt = 0;
67
+ }
@@ -0,0 +1,208 @@
1
+ /**
2
+ * OSC 8 Hyperlink utilities for clickable terminal URLs
3
+ *
4
+ * OSC 8 is a terminal escape sequence that makes URLs clickable.
5
+ * Supported terminals: iTerm2, Windows Terminal, GNOME Terminal,
6
+ * VS Code terminal, Hyper, and most modern terminals.
7
+ *
8
+ * Format: \e]8;;URL\e\\TEXT\e]8;;\e\\
9
+ * ^^^^^ start ^^^^^^ end
10
+ *
11
+ * Features:
12
+ * - Automatic URL detection in text
13
+ * - OSC 8 hyperlink generation
14
+ * - Terminal capability detection
15
+ * - Graceful fallback for unsupported terminals
16
+ */
17
+ /**
18
+ * OSC 8 escape sequences
19
+ */
20
+ const OSC8_START = "\x1b]8;;";
21
+ const OSC8_END = "\x1b\\";
22
+ const OSC8_ST = "\x07"; // Alternative terminator (Bell)
23
+ /**
24
+ * URL pattern for detection
25
+ * Matches http://, https://, file://, and other common schemes
26
+ */
27
+ const URL_REGEX = /(https?:\/\/|file:\/\/|ftp:\/\/|mailto:|ssh:|sftp:)[^\s<>"{}|\\^`[\]]+/gi;
28
+ /**
29
+ * Check if terminal supports OSC 8 hyperlinks
30
+ *
31
+ * Detection methods:
32
+ * 1. Check for known supporting terminals via TERM or TERM_PROGRAM
33
+ * 2. Check for explicit HYPERLINK_SUPPORT env var
34
+ * 3. Check for VTE_VERSION (VTE-based terminals)
35
+ */
36
+ export function isOsc8Supported() {
37
+ const env = process.env;
38
+ // Explicit opt-in/opt-out
39
+ if (env.HYPERLINK_SUPPORT === "1" || env.HYPERLINK_SUPPORT === "true") {
40
+ return true;
41
+ }
42
+ if (env.HYPERLINK_SUPPORT === "0" || env.HYPERLINK_SUPPORT === "false") {
43
+ return false;
44
+ }
45
+ const termProgram = (env.TERM_PROGRAM ?? "").toLowerCase();
46
+ const term = (env.TERM ?? "").toLowerCase();
47
+ // Known supporting terminals
48
+ const supportingPrograms = [
49
+ "iterm",
50
+ "iterm2",
51
+ "vscode",
52
+ "code",
53
+ "hyper",
54
+ "alacritty",
55
+ "wezterm",
56
+ "tabby",
57
+ "warp",
58
+ "rio",
59
+ "rio-terminal",
60
+ ];
61
+ for (const program of supportingPrograms) {
62
+ if (termProgram.includes(program)) {
63
+ return true;
64
+ }
65
+ }
66
+ // VTE-based terminals (GNOME Terminal, Tilix, etc.)
67
+ if (env.VTE_VERSION) {
68
+ return true;
69
+ }
70
+ // Windows Terminal (new)
71
+ if (env.WT_SESSION || env.WT_PROFILE_ID) {
72
+ return true;
73
+ }
74
+ // Konsole (KDE terminal)
75
+ if (env.KONSOLE_VERSION) {
76
+ return true;
77
+ }
78
+ // Screen/Tmux might support it if the outer terminal does
79
+ if (term.includes("screen") || term.includes("tmux")) {
80
+ // Check if we're in a supported outer terminal
81
+ if (env.TMUX || env.STY) {
82
+ // Conservative: assume supported if in tmux/screen
83
+ return true;
84
+ }
85
+ }
86
+ // Default: assume modern terminals support it
87
+ // This can be overridden with HYPERLINK_SUPPORT env var
88
+ return true;
89
+ }
90
+ /**
91
+ * Create an OSC 8 hyperlink
92
+ *
93
+ * @param url - The URL to link to
94
+ * @param text - The visible text
95
+ * @param params - Optional parameters (id, etc.)
96
+ * @returns Formatted hyperlink string
97
+ */
98
+ export function createHyperlink(url, text, params) {
99
+ if (!isOsc8Supported()) {
100
+ return text;
101
+ }
102
+ // Validate URL
103
+ try {
104
+ new URL(url);
105
+ }
106
+ catch {
107
+ // Invalid URL, return plain text
108
+ return text;
109
+ }
110
+ const terminator = params?.useSt ? OSC8_ST : OSC8_END;
111
+ const idPart = params?.id ? `id=${params.id}:` : "";
112
+ return `${OSC8_START}${idPart}${url}${terminator}${text}${OSC8_START}${terminator}`;
113
+ }
114
+ /**
115
+ * Convert plain URLs in text to OSC 8 hyperlinks
116
+ *
117
+ * @param text - Input text containing URLs
118
+ * @returns Text with URLs converted to hyperlinks
119
+ */
120
+ export function hyperlinkUrls(text) {
121
+ if (!isOsc8Supported()) {
122
+ return text;
123
+ }
124
+ return text.replace(URL_REGEX, (url) => {
125
+ return createHyperlink(url, url);
126
+ });
127
+ }
128
+ /**
129
+ * Strip OSC 8 sequences from text
130
+ * Useful for getting plain text or for length calculations
131
+ *
132
+ * @param text - Text that may contain OSC 8 sequences
133
+ * @returns Text with OSC 8 sequences removed
134
+ */
135
+ export function stripOsc8(text) {
136
+ // Match OSC 8 start sequences: ESC]8;;params:urlESC\ or ESC]8;;params:urlBEL
137
+ // Match OSC 8 end sequences: ESC]8;;ESC\ or ESC]8;;BEL
138
+ // eslint-disable-next-line no-control-regex
139
+ const osc8Regex = /\x1b\]8;;[^\x07\x1b]*(?:\x1b\\|\x07)/g;
140
+ return text.replace(osc8Regex, "");
141
+ }
142
+ /**
143
+ * Get the visible length of text (excluding OSC 8 sequences)
144
+ *
145
+ * @param text - Text that may contain OSC 8 sequences
146
+ * @returns Visible character count
147
+ */
148
+ export function getVisibleLength(text) {
149
+ return stripOsc8(text).length;
150
+ }
151
+ /**
152
+ * Truncate text with OSC 8 support
153
+ * Preserves hyperlinks when truncating
154
+ *
155
+ * @param text - Text to truncate
156
+ * @param maxLength - Maximum visible length
157
+ * @returns Truncated text
158
+ */
159
+ export function truncateWithHyperlinks(text, maxLength) {
160
+ if (getVisibleLength(text) <= maxLength) {
161
+ return text;
162
+ }
163
+ // Simple approach: strip OSC 8, truncate, return plain text
164
+ // A more sophisticated approach would preserve partial hyperlinks
165
+ const plain = stripOsc8(text);
166
+ return plain.slice(0, maxLength - 3) + "...";
167
+ }
168
+ /**
169
+ * Detect URLs in text and return their positions
170
+ *
171
+ * @param text - Input text
172
+ * @returns Array of URL matches with positions
173
+ */
174
+ export function detectUrls(text) {
175
+ const matches = [];
176
+ let match;
177
+ // Reset regex lastIndex
178
+ URL_REGEX.lastIndex = 0;
179
+ while ((match = URL_REGEX.exec(text)) !== null) {
180
+ matches.push({
181
+ url: match[0],
182
+ start: match.index,
183
+ end: match.index + match[0].length,
184
+ });
185
+ }
186
+ return matches;
187
+ }
188
+ /**
189
+ * Create a hyperlink with display text different from URL
190
+ * Example: createNamedHyperlink("https://example.com", "Click here")
191
+ *
192
+ * @param url - The target URL
193
+ * @param displayText - The visible text
194
+ * @returns Hyperlink string
195
+ */
196
+ export function createNamedHyperlink(url, displayText) {
197
+ return createHyperlink(url, displayText);
198
+ }
199
+ /**
200
+ * Wrap a URL in OSC 8 sequence
201
+ * Shorthand for createHyperlink(url, url)
202
+ *
203
+ * @param url - URL to wrap
204
+ * @returns Hyperlink string
205
+ */
206
+ export function wrapUrl(url) {
207
+ return createHyperlink(url, url);
208
+ }