@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.
Files changed (128) hide show
  1. package/CHANGELOG.md +35 -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 +10 -0
  97. package/extensions/matrix/package.json +1 -1
  98. package/extensions/mattermost/package.json +1 -1
  99. package/extensions/mavalie/README.md +97 -0
  100. package/extensions/mavalie/package.json +15 -0
  101. package/extensions/mavalie/src/index.ts +62 -0
  102. package/extensions/mcp-server/index.ts +14 -0
  103. package/extensions/mcp-server/package.json +11 -0
  104. package/extensions/mcp-server/src/service.ts +540 -0
  105. package/extensions/memory-core/package.json +1 -1
  106. package/extensions/memory-lancedb/package.json +1 -1
  107. package/extensions/minimax-portal-auth/package.json +1 -1
  108. package/extensions/msteams/CHANGELOG.md +10 -0
  109. package/extensions/msteams/package.json +1 -1
  110. package/extensions/nextcloud-talk/package.json +1 -1
  111. package/extensions/nostr/CHANGELOG.md +10 -0
  112. package/extensions/nostr/package.json +1 -1
  113. package/extensions/open-prose/package.json +1 -1
  114. package/extensions/openai-codex-auth/package.json +1 -1
  115. package/extensions/signal/package.json +1 -1
  116. package/extensions/slack/package.json +1 -1
  117. package/extensions/telegram/package.json +1 -1
  118. package/extensions/tlon/package.json +1 -1
  119. package/extensions/twitch/CHANGELOG.md +10 -0
  120. package/extensions/twitch/package.json +1 -1
  121. package/extensions/voice-call/CHANGELOG.md +10 -0
  122. package/extensions/voice-call/package.json +1 -1
  123. package/extensions/whatsapp/package.json +1 -1
  124. package/extensions/zalo/CHANGELOG.md +10 -0
  125. package/extensions/zalo/package.json +1 -1
  126. package/extensions/zalouser/CHANGELOG.md +10 -0
  127. package/extensions/zalouser/package.json +1 -1
  128. package/package.json +8 -1
@@ -0,0 +1,345 @@
1
+ import { context, metrics, trace } from "@opentelemetry/api";
2
+ import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-http";
3
+ import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
4
+ import { resourceFromAttributes } from "@opentelemetry/resources";
5
+ import { ConsoleMetricExporter, InMemoryMetricExporter, MeterProvider, PeriodicExportingMetricReader, } from "@opentelemetry/sdk-metrics";
6
+ import { BatchSpanProcessor, ConsoleSpanExporter, InMemorySpanExporter, NodeTracerProvider, SimpleSpanProcessor, } from "@opentelemetry/sdk-trace-node";
7
+ import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from "@opentelemetry/semantic-conventions";
8
+ import { createSubsystemLogger } from "../logging/subsystem.js";
9
+ const log = createSubsystemLogger("telemetry");
10
+ export const defaultTelemetryConfig = {
11
+ enabled: true,
12
+ serviceName: "poolbot",
13
+ serviceVersion: process.env.npm_package_version ?? "unknown",
14
+ tracing: {
15
+ enabled: true,
16
+ exporter: "console",
17
+ sampleRate: 1.0,
18
+ },
19
+ metrics: {
20
+ enabled: true,
21
+ exporter: "console",
22
+ exportIntervalMs: 60000,
23
+ aggregationTemporality: 1, // CUMULATIVE
24
+ },
25
+ };
26
+ export class TelemetryService {
27
+ config;
28
+ tracerProvider;
29
+ meterProvider;
30
+ tracer;
31
+ meter;
32
+ inMemorySpanExporter;
33
+ inMemoryMetricExporter;
34
+ counters = new Map();
35
+ histograms = new Map();
36
+ gauges = new Map();
37
+ gaugeValues = new Map();
38
+ activeSpans = new Map();
39
+ isStarted = false;
40
+ alertEngine;
41
+ onAlertCallback;
42
+ constructor(config = {}) {
43
+ this.config = { ...defaultTelemetryConfig, ...config };
44
+ if (config.tracing) {
45
+ this.config.tracing = { ...defaultTelemetryConfig.tracing, ...config.tracing };
46
+ }
47
+ if (config.metrics) {
48
+ this.config.metrics = { ...defaultTelemetryConfig.metrics, ...config.metrics };
49
+ }
50
+ }
51
+ async start() {
52
+ if (this.isStarted || !this.config.enabled) {
53
+ return;
54
+ }
55
+ log.info("Starting telemetry service...");
56
+ const resource = resourceFromAttributes({
57
+ [ATTR_SERVICE_NAME]: this.config.serviceName,
58
+ [ATTR_SERVICE_VERSION]: this.config.serviceVersion,
59
+ ...this.config.attributes,
60
+ });
61
+ if (this.config.tracing.enabled) {
62
+ this.setupTracing(resource);
63
+ }
64
+ if (this.config.metrics.enabled) {
65
+ this.setupMetrics(resource);
66
+ }
67
+ this.isStarted = true;
68
+ log.info("Telemetry service started");
69
+ }
70
+ stop() {
71
+ if (!this.isStarted) {
72
+ return;
73
+ }
74
+ log.info("Stopping telemetry service...");
75
+ void this.tracerProvider?.shutdown();
76
+ void this.meterProvider?.shutdown();
77
+ this.isStarted = false;
78
+ log.info("Telemetry service stopped");
79
+ }
80
+ setupTracing(resource) {
81
+ const spanProcessors = [];
82
+ switch (this.config.tracing.exporter) {
83
+ case "console":
84
+ spanProcessors.push(new SimpleSpanProcessor(new ConsoleSpanExporter()));
85
+ break;
86
+ case "memory":
87
+ this.inMemorySpanExporter = new InMemorySpanExporter();
88
+ spanProcessors.push(new SimpleSpanProcessor(this.inMemorySpanExporter));
89
+ break;
90
+ case "otlp": {
91
+ const endpoint = this.config.tracing.otlpEndpoint ?? "http://localhost:4318/v1/traces";
92
+ spanProcessors.push(new BatchSpanProcessor(new OTLPTraceExporter({ url: endpoint })));
93
+ break;
94
+ }
95
+ case "none":
96
+ default:
97
+ break;
98
+ }
99
+ this.tracerProvider = new NodeTracerProvider({
100
+ resource,
101
+ spanProcessors,
102
+ });
103
+ this.tracerProvider.register();
104
+ this.tracer = trace.getTracer(this.config.serviceName, this.config.serviceVersion);
105
+ }
106
+ setupMetrics(resource) {
107
+ const readers = [];
108
+ switch (this.config.metrics.exporter) {
109
+ case "console":
110
+ readers.push(new PeriodicExportingMetricReader({
111
+ exporter: new ConsoleMetricExporter(),
112
+ exportIntervalMillis: this.config.metrics.exportIntervalMs,
113
+ }));
114
+ break;
115
+ case "memory":
116
+ this.inMemoryMetricExporter = new InMemoryMetricExporter(this.config.metrics.aggregationTemporality);
117
+ readers.push(new PeriodicExportingMetricReader({
118
+ exporter: this.inMemoryMetricExporter,
119
+ exportIntervalMillis: this.config.metrics.exportIntervalMs,
120
+ }));
121
+ break;
122
+ case "otlp": {
123
+ const endpoint = this.config.metrics.otlpEndpoint ?? "http://localhost:4318/v1/metrics";
124
+ readers.push(new PeriodicExportingMetricReader({
125
+ exporter: new OTLPMetricExporter({ url: endpoint }),
126
+ exportIntervalMillis: this.config.metrics.exportIntervalMs,
127
+ }));
128
+ break;
129
+ }
130
+ case "none":
131
+ default:
132
+ break;
133
+ }
134
+ this.meterProvider = new MeterProvider({
135
+ resource,
136
+ readers,
137
+ });
138
+ metrics.setGlobalMeterProvider(this.meterProvider);
139
+ this.meter = metrics.getMeter(this.config.serviceName, this.config.serviceVersion);
140
+ }
141
+ isEnabled() {
142
+ return this.isStarted && this.config.enabled;
143
+ }
144
+ createSpan(name, attributes) {
145
+ if (!this.tracer)
146
+ return undefined;
147
+ const span = this.tracer.startSpan(name, { attributes });
148
+ const spanId = span.spanContext().spanId;
149
+ this.activeSpans.set(spanId, span);
150
+ return span;
151
+ }
152
+ endSpan(span, status) {
153
+ if (status) {
154
+ const statusCode = status.code === "ok" ? 1 : 2;
155
+ span.setStatus({ code: statusCode, message: status.message });
156
+ }
157
+ span.end();
158
+ this.activeSpans.delete(span.spanContext().spanId);
159
+ }
160
+ withSpan(name, fn, attributes) {
161
+ if (!this.tracer) {
162
+ return Promise.resolve(fn());
163
+ }
164
+ return this.tracer.startActiveSpan(name, { attributes }, async (span) => {
165
+ try {
166
+ const result = await fn();
167
+ span.setStatus({ code: 1 }); // OK
168
+ return result;
169
+ }
170
+ catch (error) {
171
+ span.recordException(error);
172
+ span.setStatus({ code: 2, message: String(error) }); // ERROR
173
+ throw error;
174
+ }
175
+ finally {
176
+ span.end();
177
+ }
178
+ });
179
+ }
180
+ recordCounter(name, value = 1, attributes) {
181
+ if (!this.meter)
182
+ return;
183
+ let counter = this.counters.get(name);
184
+ if (!counter) {
185
+ counter = this.meter.createCounter(name);
186
+ this.counters.set(name, counter);
187
+ }
188
+ counter.add(value, attributes);
189
+ }
190
+ recordHistogram(name, value, attributes, options) {
191
+ if (!this.meter)
192
+ return;
193
+ let histogram = this.histograms.get(name);
194
+ if (!histogram) {
195
+ histogram = this.meter.createHistogram(name, options);
196
+ this.histograms.set(name, histogram);
197
+ }
198
+ histogram.record(value, attributes);
199
+ }
200
+ recordGauge(name, value, _attributes) {
201
+ if (!this.meter)
202
+ return;
203
+ const existingGauge = this.gauges.get(name);
204
+ if (!existingGauge) {
205
+ const newGauge = this.meter.createObservableGauge(name, {
206
+ valueType: 1, // DOUBLE
207
+ });
208
+ this.gauges.set(name, newGauge);
209
+ }
210
+ this.gaugeValues.set(name, value);
211
+ }
212
+ addEvent(span, name, attributes) {
213
+ span.addEvent(name, attributes);
214
+ }
215
+ getCurrentSpan() {
216
+ return trace.getSpan(context.active()) ?? undefined;
217
+ }
218
+ getSnapshot() {
219
+ if (!this.inMemorySpanExporter || !this.inMemoryMetricExporter) {
220
+ return undefined;
221
+ }
222
+ const spans = this.inMemorySpanExporter.getFinishedSpans();
223
+ const exportedMetrics = this.inMemoryMetricExporter.getMetrics();
224
+ const spanInfos = spans.map((span) => {
225
+ const spanContext = span.spanContext();
226
+ const startTime = span.startTime;
227
+ const endTime = span.endTime;
228
+ return {
229
+ traceId: spanContext.traceId,
230
+ spanId: spanContext.spanId,
231
+ parentSpanId: span.parentSpanId,
232
+ name: span.name,
233
+ startTime: startTime[0] * 1000000000 + startTime[1],
234
+ endTime: endTime ? endTime[0] * 1000000000 + endTime[1] : undefined,
235
+ status: span.status.code === 0 ? "unset" : span.status.code === 1 ? "ok" : "error",
236
+ attributes: Object.fromEntries(Object.entries(span.attributes)),
237
+ events: span.events.map((e) => {
238
+ const time = e.time;
239
+ return {
240
+ name: e.name,
241
+ timestamp: time[0] * 1000000000 + time[1],
242
+ attributes: Object.fromEntries(Object.entries(e.attributes ?? {})),
243
+ };
244
+ }),
245
+ };
246
+ });
247
+ const metricValues = [];
248
+ for (const metric of exportedMetrics) {
249
+ for (const scopeMetric of metric.scopeMetrics) {
250
+ for (const m of scopeMetric.metrics) {
251
+ const descriptor = m.descriptor;
252
+ const dataPoints = m.dataPoints;
253
+ if (dataPoints) {
254
+ for (const dp of dataPoints) {
255
+ metricValues.push({
256
+ name: descriptor.name,
257
+ value: typeof dp.value === "number" ? dp.value : 0,
258
+ attributes: dp.attributes,
259
+ timestamp: Date.now(),
260
+ });
261
+ }
262
+ }
263
+ }
264
+ }
265
+ }
266
+ return {
267
+ timestamp: Date.now(),
268
+ spans: spanInfos,
269
+ metrics: metricValues,
270
+ };
271
+ }
272
+ clearSnapshot() {
273
+ this.inMemorySpanExporter?.reset();
274
+ this.inMemoryMetricExporter?.reset();
275
+ }
276
+ getConfig() {
277
+ return { ...this.config };
278
+ }
279
+ async shutdown() {
280
+ log.debug("telemetry: shutting down");
281
+ await this.tracerProvider?.shutdown();
282
+ await this.meterProvider?.shutdown();
283
+ }
284
+ setAlertEngine(engine) {
285
+ this.alertEngine = engine;
286
+ engine.onAlert((alert) => {
287
+ this.onAlertCallback?.(alert);
288
+ });
289
+ log.info(`Alert engine configured with ${engine.getRules().length} rules`);
290
+ }
291
+ onAlert(callback) {
292
+ this.onAlertCallback = callback;
293
+ }
294
+ evaluateAlerts(snapshot) {
295
+ if (!this.alertEngine)
296
+ return [];
297
+ const snap = snapshot ?? this.getSnapshot();
298
+ if (!snap)
299
+ return [];
300
+ return this.alertEngine.evaluate(snap);
301
+ }
302
+ }
303
+ let globalTelemetryService;
304
+ export function getGlobalTelemetryService() {
305
+ return globalTelemetryService;
306
+ }
307
+ export function setGlobalTelemetryService(service) {
308
+ globalTelemetryService = service;
309
+ }
310
+ export function createTelemetryService(config) {
311
+ return new TelemetryService(config);
312
+ }
313
+ /**
314
+ * Convert OTel config from zod schema format to TelemetryService config format.
315
+ */
316
+ export function telemetryConfigFromOtelConfig(otel, opts) {
317
+ if (!otel?.enabled) {
318
+ return {
319
+ ...defaultTelemetryConfig,
320
+ enabled: false,
321
+ tracing: { ...defaultTelemetryConfig.tracing, enabled: false },
322
+ metrics: { ...defaultTelemetryConfig.metrics, enabled: false },
323
+ };
324
+ }
325
+ const exporterType = otel.endpoint ? "otlp" : "console";
326
+ return {
327
+ enabled: true,
328
+ serviceName: otel.serviceName ?? opts?.defaultServiceName ?? "poolbot",
329
+ serviceVersion: opts?.serviceVersion ?? process.env.npm_package_version ?? "unknown",
330
+ tracing: {
331
+ enabled: otel.traces ?? true,
332
+ exporter: exporterType,
333
+ otlpEndpoint: otel.endpoint,
334
+ sampleRate: otel.sampleRate ?? 1.0,
335
+ },
336
+ metrics: {
337
+ enabled: otel.metrics ?? true,
338
+ exporter: exporterType,
339
+ otlpEndpoint: otel.endpoint,
340
+ exportIntervalMs: otel.flushIntervalMs ?? 60000,
341
+ aggregationTemporality: 1, // CUMULATIVE
342
+ },
343
+ attributes: otel.headers,
344
+ };
345
+ }
@@ -1,16 +1,20 @@
1
1
  import { Container, Markdown, Spacer } from "@mariozechner/pi-tui";
2
2
  import { markdownTheme, theme } from "../theme/theme.js";
3
+ import { sanitizeRenderableText } from "../utils/text-sanitization.js";
3
4
  export class AssistantMessageComponent extends Container {
4
5
  body;
5
6
  constructor(text) {
6
7
  super();
7
- this.body = new Markdown(text, 1, 0, markdownTheme, {
8
+ // Sanitize text before rendering to prevent terminal corruption
9
+ const sanitizedText = sanitizeRenderableText(text);
10
+ this.body = new Markdown(sanitizedText, 1, 0, markdownTheme, {
8
11
  color: (line) => theme.fg(line),
9
12
  });
10
13
  this.addChild(new Spacer(1));
11
14
  this.addChild(this.body);
12
15
  }
13
16
  setText(text) {
14
- this.body.setText(text);
17
+ // Sanitize text before updating
18
+ this.body.setText(sanitizeRenderableText(text));
15
19
  }
16
20
  }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Hyperlink Markdown Component
3
+ *
4
+ * Extends Markdown component with OSC 8 hyperlink support.
5
+ * Automatically detects and converts URLs to clickable hyperlinks.
6
+ */
7
+ import { Markdown } from "@mariozechner/pi-tui";
8
+ import { hyperlinkUrls } from "../utils/osc8-hyperlinks.js";
9
+ /**
10
+ * Markdown component with automatic hyperlink detection
11
+ */
12
+ export class HyperlinkMarkdown extends Markdown {
13
+ constructor(text, padLeft, padRight, theme, options) {
14
+ // Convert URLs to hyperlinks before passing to parent
15
+ const textWithHyperlinks = hyperlinkUrls(text);
16
+ super(textWithHyperlinks, padLeft, padRight, theme, options);
17
+ }
18
+ /**
19
+ * Update text with hyperlink conversion
20
+ */
21
+ setText(text) {
22
+ const textWithHyperlinks = hyperlinkUrls(text);
23
+ super.setText(textWithHyperlinks);
24
+ }
25
+ /**
26
+ * Set raw text without hyperlink conversion
27
+ * Use this if you need to bypass automatic URL detection
28
+ */
29
+ setRawText(text) {
30
+ super.setText(text);
31
+ }
32
+ }
@@ -1,8 +1,18 @@
1
1
  import { getEditorKeybindings, Input, isKeyRelease, matchesKey, truncateToWidth, } from "@mariozechner/pi-tui";
2
+ import { LRUCache } from "../../utils/lru-cache.js";
2
3
  import { visibleWidth } from "../../terminal/ansi.js";
3
4
  import { findWordBoundaryIndex, fuzzyFilterLower, prepareSearchItems } from "./fuzzy-filter.js";
5
+ /**
6
+ * Maximum number of cached regex patterns to prevent memory leaks
7
+ */
8
+ const MAX_REGEX_CACHE_SIZE = 100;
4
9
  /**
5
10
  * A select list with a search input at the top for fuzzy filtering.
11
+ *
12
+ * Features:
13
+ * - Smart tiered filtering (exact > word-boundary > description > fuzzy)
14
+ * - LRU-cached regex patterns for performance
15
+ * - Keyboard navigation with vim-style bindings
6
16
  */
7
17
  export class SearchableSelectList {
8
18
  items;
@@ -11,7 +21,7 @@ export class SearchableSelectList {
11
21
  maxVisible;
12
22
  theme;
13
23
  searchInput;
14
- regexCache = new Map();
24
+ regexCache;
15
25
  onSelect;
16
26
  onCancel;
17
27
  onSelectionChange;
@@ -21,6 +31,7 @@ export class SearchableSelectList {
21
31
  this.maxVisible = maxVisible;
22
32
  this.theme = theme;
23
33
  this.searchInput = new Input();
34
+ this.regexCache = new LRUCache({ maxSize: MAX_REGEX_CACHE_SIZE });
24
35
  }
25
36
  getCachedRegex(pattern) {
26
37
  let regex = this.regexCache.get(pattern);
@@ -1,10 +1,13 @@
1
1
  import { Container, Markdown, Spacer } from "@mariozechner/pi-tui";
2
2
  import { markdownTheme, theme } from "../theme/theme.js";
3
+ import { sanitizeRenderableText } from "../utils/text-sanitization.js";
3
4
  export class UserMessageComponent extends Container {
4
5
  body;
5
6
  constructor(text) {
6
7
  super();
7
- this.body = new Markdown(text, 1, 1, markdownTheme, {
8
+ // Sanitize text before rendering to prevent terminal corruption
9
+ const sanitizedText = sanitizeRenderableText(text);
10
+ this.body = new Markdown(sanitizedText, 1, 1, markdownTheme, {
8
11
  bgColor: (line) => theme.userBg(line),
9
12
  color: (line) => theme.userText(line),
10
13
  });
@@ -12,6 +15,7 @@ export class UserMessageComponent extends Container {
12
15
  this.addChild(this.body);
13
16
  }
14
17
  setText(text) {
15
- this.body.setText(text);
18
+ // Sanitize text before updating
19
+ this.body.setText(sanitizeRenderableText(text));
16
20
  }
17
21
  }
package/dist/tui/index.js CHANGED
@@ -88,7 +88,18 @@ export const SpinnerChars = {
88
88
  dots: ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"],
89
89
  line: ["-", "\\", "|", "/"],
90
90
  arrow: ["←", "↖", "↑", "↗", "→", "↘", "↓", "↙"],
91
- bounce: ["( ● )", "( ● )", "( ● )", "( ● )", "( ●)", "( ● )", "( ● )", "( ● )", "( ● )", "(● )"],
91
+ bounce: [
92
+ "( ● )",
93
+ "( ● )",
94
+ "( ● )",
95
+ "( ● )",
96
+ "( ●)",
97
+ "( ● )",
98
+ "( ● )",
99
+ "( ● )",
100
+ "( ● )",
101
+ "(● )",
102
+ ],
92
103
  pulse: ["█", "▉", "▊", "▋", "▌", "▍", "▎", "▏"],
93
104
  };
94
105
  /**
@@ -386,7 +397,7 @@ export class Table {
386
397
  });
387
398
  }
388
399
  renderHorizontalLine(left, right, horizontal, cross, widths) {
389
- const segments = widths.map(w => horizontal.repeat(w));
400
+ const segments = widths.map((w) => horizontal.repeat(w));
390
401
  return left + segments.join(cross) + right;
391
402
  }
392
403
  renderRow(cells, vertical) {
@@ -466,7 +477,7 @@ export class Prompts {
466
477
  * Select from list prompt
467
478
  */
468
479
  static async select(options) {
469
- const available = options.choices.filter(c => !c.disabled);
480
+ const available = options.choices.filter((c) => !c.disabled);
470
481
  console.log(options.message);
471
482
  available.forEach((choice, i) => {
472
483
  const marker = choice.value === options.default ? colorize("›", Colors.cyan) : " ";
@@ -482,7 +493,7 @@ export class Prompts {
482
493
  return available[num - 1].value;
483
494
  }
484
495
  // Try matching by value
485
- const match = available.find(c => c.value.toLowerCase() === answer.trim().toLowerCase());
496
+ const match = available.find((c) => c.value.toLowerCase() === answer.trim().toLowerCase());
486
497
  if (match)
487
498
  return match.value;
488
499
  console.log(colorize("Invalid selection", Colors.red));
@@ -515,7 +526,7 @@ export function logStatus(status, message) {
515
526
  export function box(content, options = {}) {
516
527
  const caps = detectCapabilities();
517
528
  const lines = content.split("\n");
518
- const maxLineLen = Math.max(...lines.map(l => l.length));
529
+ const maxLineLen = Math.max(...lines.map((l) => l.length));
519
530
  const width = options.width ?? maxLineLen + 4;
520
531
  const padding = options.padding ?? 1;
521
532
  const borders = {
@@ -533,7 +544,12 @@ export function box(content, options = {}) {
533
544
  : options.title;
534
545
  const titleWithPadding = ` ${title} `;
535
546
  const before = Math.floor((width - titleWithPadding.length) / 2);
536
- topBorder = borders.tl + borders.h.repeat(before) + titleWithPadding + borders.h.repeat(width - before - titleWithPadding.length - 2) + borders.tr;
547
+ topBorder =
548
+ borders.tl +
549
+ borders.h.repeat(before) +
550
+ titleWithPadding +
551
+ borders.h.repeat(width - before - titleWithPadding.length - 2) +
552
+ borders.tr;
537
553
  }
538
554
  result.push(topBorder);
539
555
  // Padding top