@poolzin/pool-bot 2026.3.7 → 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 (150) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/README.md +147 -69
  3. package/dist/.buildstamp +1 -1
  4. package/dist/agents/error-classifier.js +251 -0
  5. package/dist/agents/skills/security.js +211 -0
  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/lazy-commands.example.js +113 -0
  11. package/dist/cli/lazy-commands.js +329 -0
  12. package/dist/cli/program/command-registry.js +26 -0
  13. package/dist/cli/program/register.maintenance.js +21 -0
  14. package/dist/cli/program/register.skills.js +4 -0
  15. package/dist/cli/program/register.subclis.js +9 -0
  16. package/dist/cli/swarm-cli/register.js +8 -0
  17. package/dist/cli/swarm-cli/register.swarm-status.js +488 -0
  18. package/dist/cli/telemetry-cli/register.js +10 -0
  19. package/dist/cli/telemetry-cli/register.telemetry-alerts.js +176 -0
  20. package/dist/cli/telemetry-cli/register.telemetry-metrics.js +323 -0
  21. package/dist/cli/telemetry-cli/register.telemetry-status.js +179 -0
  22. package/dist/commands/doctor-checks.js +498 -0
  23. package/dist/config/config.js +1 -0
  24. package/dist/config/secrets-integration.js +88 -0
  25. package/dist/context-engine/index.js +33 -0
  26. package/dist/context-engine/legacy.js +179 -0
  27. package/dist/context-engine/registry.js +86 -0
  28. package/dist/context-engine/summarizing.js +290 -0
  29. package/dist/context-engine/types.js +7 -0
  30. package/dist/cron/service/timer.js +18 -0
  31. package/dist/gateway/protocol/index.js +5 -2
  32. package/dist/gateway/protocol/schema/error-codes.js +1 -0
  33. package/dist/gateway/protocol/schema/swarm.js +80 -0
  34. package/dist/gateway/protocol/schema.js +1 -0
  35. package/dist/gateway/server-close.js +4 -0
  36. package/dist/gateway/server-constants.js +1 -0
  37. package/dist/gateway/server-cron.js +29 -0
  38. package/dist/gateway/server-maintenance.js +35 -2
  39. package/dist/gateway/server-methods/swarm.js +58 -0
  40. package/dist/gateway/server-methods/telemetry.js +71 -0
  41. package/dist/gateway/server-methods-list.js +8 -0
  42. package/dist/gateway/server-methods.js +9 -2
  43. package/dist/gateway/server.impl.js +33 -16
  44. package/dist/infra/abort-pattern.js +106 -0
  45. package/dist/infra/retry.js +96 -0
  46. package/dist/secrets/index.js +28 -0
  47. package/dist/secrets/resolver.js +185 -0
  48. package/dist/secrets/runtime.js +142 -0
  49. package/dist/secrets/types.js +11 -0
  50. package/dist/security/dangerous-tools.js +80 -0
  51. package/dist/security/types.js +12 -0
  52. package/dist/skills/commands.js +333 -0
  53. package/dist/skills/index.js +164 -0
  54. package/dist/skills/loader.js +282 -0
  55. package/dist/skills/parser.js +446 -0
  56. package/dist/skills/registry.js +394 -0
  57. package/dist/skills/security.js +312 -0
  58. package/dist/skills/types.js +21 -0
  59. package/dist/swarm/service.js +247 -0
  60. package/dist/telemetry/alert-engine.js +258 -0
  61. package/dist/telemetry/cron-instrumentation.js +49 -0
  62. package/dist/telemetry/gateway-instrumentation.js +80 -0
  63. package/dist/telemetry/instrumentation.js +66 -0
  64. package/dist/telemetry/service.js +345 -0
  65. package/dist/test-utils/index.js +219 -0
  66. package/dist/tui/components/assistant-message.js +6 -2
  67. package/dist/tui/components/hyperlink-markdown.js +32 -0
  68. package/dist/tui/components/searchable-select-list.js +12 -1
  69. package/dist/tui/components/user-message.js +6 -2
  70. package/dist/tui/index.js +611 -0
  71. package/dist/tui/theme/theme-detection.js +226 -0
  72. package/dist/tui/tui-command-handlers.js +20 -0
  73. package/dist/tui/tui-formatters.js +4 -3
  74. package/dist/tui/utils/ctrl-c-handler.js +67 -0
  75. package/dist/tui/utils/osc8-hyperlinks.js +208 -0
  76. package/dist/tui/utils/safe-stop.js +180 -0
  77. package/dist/tui/utils/session-key-utils.js +81 -0
  78. package/dist/tui/utils/text-sanitization.js +284 -0
  79. package/dist/utils/lru-cache.js +116 -0
  80. package/dist/utils/performance.js +199 -0
  81. package/dist/utils/retry.js +240 -0
  82. package/docs/INTEGRATION_PLAN.md +475 -0
  83. package/docs/INTEGRATION_SUMMARY.md +215 -0
  84. package/docs/MELHORIAS_IMPLEMENTADAS.md +228 -0
  85. package/docs/MELHORIAS_PROFISSIONAIS.md +282 -0
  86. package/docs/PLANO_ACAO_TUI.md +357 -0
  87. package/docs/PROGRESSO_TUI.md +66 -0
  88. package/docs/RELATORIO_FINAL.md +217 -0
  89. package/docs/diagnostico-shell-completion.md +265 -0
  90. package/docs/features/advanced-memory.md +585 -0
  91. package/docs/features/discord-components-v2.md +277 -0
  92. package/docs/features/swarm.md +100 -0
  93. package/docs/features/telemetry.md +284 -0
  94. package/docs/integrations/HEXSTRIKE_PLAN.md +796 -0
  95. package/docs/integrations/INTEGRATION_PLAN.md +744 -0
  96. package/docs/integrations/PAGE_AGENT_PLAN.md +370 -0
  97. package/docs/integrations/XYOPS_PLAN.md +978 -0
  98. package/docs/models/provider-infrastructure.md +400 -0
  99. package/docs/security/exec-approvals.md +294 -0
  100. package/docs/skills/IMPLEMENTATION_SUMMARY.md +145 -0
  101. package/docs/skills/SKILL.md +524 -0
  102. package/docs/skills.md +405 -0
  103. package/extensions/bluebubbles/package.json +1 -1
  104. package/extensions/copilot-proxy/package.json +1 -1
  105. package/extensions/diagnostics-otel/package.json +1 -1
  106. package/extensions/discord/package.json +1 -1
  107. package/extensions/feishu/package.json +1 -1
  108. package/extensions/google-antigravity-auth/package.json +1 -1
  109. package/extensions/google-gemini-cli-auth/package.json +1 -1
  110. package/extensions/googlechat/package.json +1 -1
  111. package/extensions/hexstrike-bridge/README.md +119 -0
  112. package/extensions/hexstrike-bridge/index.test.ts +247 -0
  113. package/extensions/hexstrike-bridge/index.ts +487 -0
  114. package/extensions/hexstrike-bridge/package.json +17 -0
  115. package/extensions/imessage/package.json +1 -1
  116. package/extensions/irc/package.json +1 -1
  117. package/extensions/line/package.json +1 -1
  118. package/extensions/llm-task/package.json +1 -1
  119. package/extensions/lobster/package.json +1 -1
  120. package/extensions/matrix/CHANGELOG.md +5 -0
  121. package/extensions/matrix/package.json +1 -1
  122. package/extensions/mattermost/package.json +1 -1
  123. package/extensions/mcp-server/index.ts +14 -0
  124. package/extensions/mcp-server/package.json +11 -0
  125. package/extensions/mcp-server/src/service.ts +540 -0
  126. package/extensions/memory-core/package.json +1 -1
  127. package/extensions/memory-lancedb/package.json +1 -1
  128. package/extensions/minimax-portal-auth/package.json +1 -1
  129. package/extensions/msteams/CHANGELOG.md +5 -0
  130. package/extensions/msteams/package.json +1 -1
  131. package/extensions/nextcloud-talk/package.json +1 -1
  132. package/extensions/nostr/CHANGELOG.md +5 -0
  133. package/extensions/nostr/package.json +1 -1
  134. package/extensions/open-prose/package.json +1 -1
  135. package/extensions/openai-codex-auth/package.json +1 -1
  136. package/extensions/signal/package.json +1 -1
  137. package/extensions/slack/package.json +1 -1
  138. package/extensions/telegram/package.json +1 -1
  139. package/extensions/tlon/package.json +1 -1
  140. package/extensions/twitch/CHANGELOG.md +5 -0
  141. package/extensions/twitch/package.json +1 -1
  142. package/extensions/voice-call/CHANGELOG.md +5 -0
  143. package/extensions/voice-call/package.json +1 -1
  144. package/extensions/whatsapp/package.json +1 -1
  145. package/extensions/zalo/CHANGELOG.md +5 -0
  146. package/extensions/zalo/package.json +1 -1
  147. package/extensions/zalouser/CHANGELOG.md +5 -0
  148. package/extensions/zalouser/package.json +1 -1
  149. package/package.json +8 -1
  150. package/skills/example-skill/SKILL.md +195 -0
@@ -0,0 +1,66 @@
1
+ import { getGlobalTelemetryService } from "../telemetry/service.js";
2
+ export function withInstrumentation(options, fn) {
3
+ const telemetry = getGlobalTelemetryService();
4
+ if (!telemetry?.isEnabled()) {
5
+ return Promise.resolve(fn());
6
+ }
7
+ const spanName = `${options.component}.${options.operation}`;
8
+ const attributes = {
9
+ component: options.component,
10
+ operation: options.operation,
11
+ ...options.attributes,
12
+ };
13
+ telemetry.recordCounter(`poolbot.${options.component}.operations`, 1, {
14
+ operation: options.operation,
15
+ });
16
+ const startTime = Date.now();
17
+ return telemetry.withSpan(spanName, async () => {
18
+ try {
19
+ const result = await fn();
20
+ const duration = Date.now() - startTime;
21
+ telemetry.recordHistogram(`poolbot.${options.component}.duration_ms`, duration, {
22
+ operation: options.operation,
23
+ });
24
+ telemetry.recordCounter(`poolbot.${options.component}.success`, 1, {
25
+ operation: options.operation,
26
+ });
27
+ return result;
28
+ }
29
+ catch (error) {
30
+ const duration = Date.now() - startTime;
31
+ telemetry.recordHistogram(`poolbot.${options.component}.duration_ms`, duration, {
32
+ operation: options.operation,
33
+ error: "true",
34
+ });
35
+ telemetry.recordCounter(`poolbot.${options.component}.errors`, 1, {
36
+ operation: options.operation,
37
+ error_type: error instanceof Error ? error.name : "unknown",
38
+ });
39
+ throw error;
40
+ }
41
+ }, attributes);
42
+ }
43
+ export function recordMetric(name, value, attributes) {
44
+ const telemetry = getGlobalTelemetryService();
45
+ if (!telemetry?.isEnabled())
46
+ return;
47
+ telemetry.recordHistogram(name, value, attributes);
48
+ }
49
+ export function recordCounter(name, value = 1, attributes) {
50
+ const telemetry = getGlobalTelemetryService();
51
+ if (!telemetry?.isEnabled())
52
+ return;
53
+ telemetry.recordCounter(name, value, attributes);
54
+ }
55
+ export function recordGauge(name, value, attributes) {
56
+ const telemetry = getGlobalTelemetryService();
57
+ if (!telemetry?.isEnabled())
58
+ return;
59
+ telemetry.recordGauge(name, value, attributes);
60
+ }
61
+ export function recordHistogram(name, value, attributes) {
62
+ const telemetry = getGlobalTelemetryService();
63
+ if (!telemetry?.isEnabled())
64
+ return;
65
+ telemetry.recordHistogram(name, value, attributes);
66
+ }
@@ -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
+ }
@@ -0,0 +1,219 @@
1
+ /**
2
+ * Test Utilities for PoolBot
3
+ *
4
+ * Shared testing helpers and fixtures for consistent testing.
5
+ */
6
+ import { afterEach } from "vitest";
7
+ /**
8
+ * Frozen time utility for deterministic time-based tests
9
+ */
10
+ export class FrozenTime {
11
+ originalDateNow;
12
+ currentTime;
13
+ constructor(startTime = 1700000000000) {
14
+ this.originalDateNow = Date.now;
15
+ this.currentTime = startTime;
16
+ }
17
+ /**
18
+ * Freeze time at a specific timestamp
19
+ */
20
+ freeze(timestamp) {
21
+ if (timestamp !== undefined) {
22
+ this.currentTime = timestamp;
23
+ }
24
+ Date.now = () => this.currentTime;
25
+ }
26
+ /**
27
+ * Advance time by milliseconds
28
+ */
29
+ advance(ms) {
30
+ this.currentTime += ms;
31
+ }
32
+ /**
33
+ * Get current frozen time
34
+ */
35
+ now() {
36
+ return this.currentTime;
37
+ }
38
+ /**
39
+ * Restore original Date.now
40
+ */
41
+ restore() {
42
+ Date.now = this.originalDateNow;
43
+ }
44
+ /**
45
+ * Create ISO timestamp from current frozen time
46
+ */
47
+ toISOString() {
48
+ return new Date(this.currentTime).toISOString();
49
+ }
50
+ }
51
+ /**
52
+ * Create a test fixture with isolated state
53
+ */
54
+ export function createFixture(factory, options = {}) {
55
+ let instance = null;
56
+ const get = () => {
57
+ if (!instance) {
58
+ instance = factory();
59
+ }
60
+ return instance;
61
+ };
62
+ const reset = () => {
63
+ instance = null;
64
+ };
65
+ if (options.autoCleanup !== false) {
66
+ afterEach(reset);
67
+ }
68
+ return { get, reset };
69
+ }
70
+ /**
71
+ * Type-safe test case builder
72
+ */
73
+ export function typedCases(cases) {
74
+ return cases;
75
+ }
76
+ /**
77
+ * Temporary directory helper
78
+ */
79
+ export class TempDir {
80
+ dirs = [];
81
+ /**
82
+ * Create a temporary directory
83
+ */
84
+ create(prefix = "poolbot-test-") {
85
+ const dir = `${prefix}${Date.now()}-${Math.random().toString(36).slice(2)}`;
86
+ this.dirs.push(dir);
87
+ return dir;
88
+ }
89
+ /**
90
+ * Clean up all created directories
91
+ */
92
+ cleanup() {
93
+ this.dirs = [];
94
+ }
95
+ }
96
+ /**
97
+ * Mock utilities for common dependencies
98
+ */
99
+ export class MockFactory {
100
+ /**
101
+ * Create a mock function with tracking
102
+ */
103
+ static fn(implementation) {
104
+ const calls = [];
105
+ const mockFn = ((...args) => {
106
+ const result = implementation ? implementation(...args) : undefined;
107
+ calls.push({ args, result });
108
+ return result;
109
+ });
110
+ mockFn.calls = calls;
111
+ mockFn.mockClear = () => {
112
+ calls.length = 0;
113
+ };
114
+ return mockFn;
115
+ }
116
+ /**
117
+ * Create a mock logger
118
+ */
119
+ static logger() {
120
+ return {
121
+ info: MockFactory.fn(),
122
+ warn: MockFactory.fn(),
123
+ error: MockFactory.fn(),
124
+ debug: MockFactory.fn(),
125
+ logs: [],
126
+ };
127
+ }
128
+ }
129
+ /**
130
+ * Assertion helpers
131
+ */
132
+ export class Assert {
133
+ /**
134
+ * Assert that a promise resolves within a timeout
135
+ */
136
+ static async resolvesWithin(promise, timeoutMs, message) {
137
+ const timeout = new Promise((_, reject) => {
138
+ setTimeout(() => reject(new Error(message ?? `Promise did not resolve within ${timeoutMs}ms`)), timeoutMs);
139
+ });
140
+ return Promise.race([promise, timeout]);
141
+ }
142
+ /**
143
+ * Assert that an error is thrown
144
+ */
145
+ static async throws(fn, expectedMessage) {
146
+ try {
147
+ await fn();
148
+ throw new Error("Expected function to throw but it did not");
149
+ }
150
+ catch (error) {
151
+ if (error instanceof Error) {
152
+ if (expectedMessage) {
153
+ if (expectedMessage instanceof RegExp) {
154
+ if (!expectedMessage.test(error.message)) {
155
+ throw new Error(`Expected error message to match ${expectedMessage}, got: ${error.message}`);
156
+ }
157
+ }
158
+ else if (!error.message.includes(expectedMessage)) {
159
+ throw new Error(`Expected error message to include "${expectedMessage}", got: ${error.message}`);
160
+ }
161
+ }
162
+ return error;
163
+ }
164
+ throw error;
165
+ }
166
+ }
167
+ }
168
+ /**
169
+ * Data generators for tests
170
+ */
171
+ export class Generators {
172
+ /**
173
+ * Generate a UUID v4
174
+ */
175
+ static uuid() {
176
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
177
+ const r = (Math.random() * 16) | 0;
178
+ const v = c === "x" ? r : (r & 0x3) | 0x8;
179
+ return v.toString(16);
180
+ });
181
+ }
182
+ /**
183
+ * Generate a random session ID
184
+ */
185
+ static sessionId() {
186
+ return `session-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
187
+ }
188
+ /**
189
+ * Generate a timestamp with optional offset
190
+ */
191
+ static timestamp(offsetMs = 0) {
192
+ return Date.now() + offsetMs;
193
+ }
194
+ /**
195
+ * Generate random text of specified length
196
+ */
197
+ static text(length = 100) {
198
+ const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789 ";
199
+ let result = "";
200
+ for (let i = 0; i < length; i++) {
201
+ result += chars.charAt(Math.floor(Math.random() * chars.length));
202
+ }
203
+ return result;
204
+ }
205
+ }
206
+ /**
207
+ * Setup helper for common test patterns
208
+ */
209
+ export function setupTestEnvironment() {
210
+ const frozenTime = new FrozenTime();
211
+ const tempDir = new TempDir();
212
+ frozenTime.freeze();
213
+ const cleanup = () => {
214
+ frozenTime.restore();
215
+ tempDir.cleanup();
216
+ };
217
+ afterEach(cleanup);
218
+ return { frozenTime, tempDir, cleanup };
219
+ }
@@ -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
+ }