@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.
- package/CHANGELOG.md +40 -0
- package/README.md +147 -69
- package/dist/.buildstamp +1 -1
- package/dist/agents/error-classifier.js +251 -0
- package/dist/agents/skills/security.js +211 -0
- package/dist/build-info.json +3 -3
- package/dist/cli/cron-cli/register.cron-dashboard.js +339 -0
- package/dist/cli/cron-cli/register.js +2 -0
- package/dist/cli/errors.js +187 -0
- package/dist/cli/lazy-commands.example.js +113 -0
- package/dist/cli/lazy-commands.js +329 -0
- package/dist/cli/program/command-registry.js +26 -0
- package/dist/cli/program/register.maintenance.js +21 -0
- package/dist/cli/program/register.skills.js +4 -0
- package/dist/cli/program/register.subclis.js +9 -0
- package/dist/cli/swarm-cli/register.js +8 -0
- package/dist/cli/swarm-cli/register.swarm-status.js +488 -0
- package/dist/cli/telemetry-cli/register.js +10 -0
- package/dist/cli/telemetry-cli/register.telemetry-alerts.js +176 -0
- package/dist/cli/telemetry-cli/register.telemetry-metrics.js +323 -0
- package/dist/cli/telemetry-cli/register.telemetry-status.js +179 -0
- package/dist/commands/doctor-checks.js +498 -0
- package/dist/config/config.js +1 -0
- package/dist/config/secrets-integration.js +88 -0
- package/dist/context-engine/index.js +33 -0
- package/dist/context-engine/legacy.js +179 -0
- package/dist/context-engine/registry.js +86 -0
- package/dist/context-engine/summarizing.js +290 -0
- package/dist/context-engine/types.js +7 -0
- package/dist/cron/service/timer.js +18 -0
- package/dist/gateway/protocol/index.js +5 -2
- package/dist/gateway/protocol/schema/error-codes.js +1 -0
- package/dist/gateway/protocol/schema/swarm.js +80 -0
- package/dist/gateway/protocol/schema.js +1 -0
- package/dist/gateway/server-close.js +4 -0
- package/dist/gateway/server-constants.js +1 -0
- package/dist/gateway/server-cron.js +29 -0
- package/dist/gateway/server-maintenance.js +35 -2
- package/dist/gateway/server-methods/swarm.js +58 -0
- package/dist/gateway/server-methods/telemetry.js +71 -0
- package/dist/gateway/server-methods-list.js +8 -0
- package/dist/gateway/server-methods.js +9 -2
- package/dist/gateway/server.impl.js +33 -16
- package/dist/infra/abort-pattern.js +106 -0
- package/dist/infra/retry.js +96 -0
- package/dist/secrets/index.js +28 -0
- package/dist/secrets/resolver.js +185 -0
- package/dist/secrets/runtime.js +142 -0
- package/dist/secrets/types.js +11 -0
- package/dist/security/dangerous-tools.js +80 -0
- package/dist/security/types.js +12 -0
- package/dist/skills/commands.js +333 -0
- package/dist/skills/index.js +164 -0
- package/dist/skills/loader.js +282 -0
- package/dist/skills/parser.js +446 -0
- package/dist/skills/registry.js +394 -0
- package/dist/skills/security.js +312 -0
- package/dist/skills/types.js +21 -0
- package/dist/swarm/service.js +247 -0
- package/dist/telemetry/alert-engine.js +258 -0
- package/dist/telemetry/cron-instrumentation.js +49 -0
- package/dist/telemetry/gateway-instrumentation.js +80 -0
- package/dist/telemetry/instrumentation.js +66 -0
- package/dist/telemetry/service.js +345 -0
- package/dist/test-utils/index.js +219 -0
- package/dist/tui/components/assistant-message.js +6 -2
- package/dist/tui/components/hyperlink-markdown.js +32 -0
- package/dist/tui/components/searchable-select-list.js +12 -1
- package/dist/tui/components/user-message.js +6 -2
- package/dist/tui/index.js +611 -0
- package/dist/tui/theme/theme-detection.js +226 -0
- package/dist/tui/tui-command-handlers.js +20 -0
- package/dist/tui/tui-formatters.js +4 -3
- package/dist/tui/utils/ctrl-c-handler.js +67 -0
- package/dist/tui/utils/osc8-hyperlinks.js +208 -0
- package/dist/tui/utils/safe-stop.js +180 -0
- package/dist/tui/utils/session-key-utils.js +81 -0
- package/dist/tui/utils/text-sanitization.js +284 -0
- package/dist/utils/lru-cache.js +116 -0
- package/dist/utils/performance.js +199 -0
- package/dist/utils/retry.js +240 -0
- package/docs/INTEGRATION_PLAN.md +475 -0
- package/docs/INTEGRATION_SUMMARY.md +215 -0
- package/docs/MELHORIAS_IMPLEMENTADAS.md +228 -0
- package/docs/MELHORIAS_PROFISSIONAIS.md +282 -0
- package/docs/PLANO_ACAO_TUI.md +357 -0
- package/docs/PROGRESSO_TUI.md +66 -0
- package/docs/RELATORIO_FINAL.md +217 -0
- package/docs/diagnostico-shell-completion.md +265 -0
- package/docs/features/advanced-memory.md +585 -0
- package/docs/features/discord-components-v2.md +277 -0
- package/docs/features/swarm.md +100 -0
- package/docs/features/telemetry.md +284 -0
- package/docs/integrations/HEXSTRIKE_PLAN.md +796 -0
- package/docs/integrations/INTEGRATION_PLAN.md +744 -0
- package/docs/integrations/PAGE_AGENT_PLAN.md +370 -0
- package/docs/integrations/XYOPS_PLAN.md +978 -0
- package/docs/models/provider-infrastructure.md +400 -0
- package/docs/security/exec-approvals.md +294 -0
- package/docs/skills/IMPLEMENTATION_SUMMARY.md +145 -0
- package/docs/skills/SKILL.md +524 -0
- package/docs/skills.md +405 -0
- package/extensions/bluebubbles/package.json +1 -1
- package/extensions/copilot-proxy/package.json +1 -1
- package/extensions/diagnostics-otel/package.json +1 -1
- package/extensions/discord/package.json +1 -1
- package/extensions/feishu/package.json +1 -1
- package/extensions/google-antigravity-auth/package.json +1 -1
- package/extensions/google-gemini-cli-auth/package.json +1 -1
- package/extensions/googlechat/package.json +1 -1
- package/extensions/hexstrike-bridge/README.md +119 -0
- package/extensions/hexstrike-bridge/index.test.ts +247 -0
- package/extensions/hexstrike-bridge/index.ts +487 -0
- package/extensions/hexstrike-bridge/package.json +17 -0
- package/extensions/imessage/package.json +1 -1
- package/extensions/irc/package.json +1 -1
- package/extensions/line/package.json +1 -1
- package/extensions/llm-task/package.json +1 -1
- package/extensions/lobster/package.json +1 -1
- package/extensions/matrix/CHANGELOG.md +5 -0
- package/extensions/matrix/package.json +1 -1
- package/extensions/mattermost/package.json +1 -1
- package/extensions/mcp-server/index.ts +14 -0
- package/extensions/mcp-server/package.json +11 -0
- package/extensions/mcp-server/src/service.ts +540 -0
- package/extensions/memory-core/package.json +1 -1
- package/extensions/memory-lancedb/package.json +1 -1
- package/extensions/minimax-portal-auth/package.json +1 -1
- package/extensions/msteams/CHANGELOG.md +5 -0
- package/extensions/msteams/package.json +1 -1
- package/extensions/nextcloud-talk/package.json +1 -1
- package/extensions/nostr/CHANGELOG.md +5 -0
- package/extensions/nostr/package.json +1 -1
- package/extensions/open-prose/package.json +1 -1
- package/extensions/openai-codex-auth/package.json +1 -1
- package/extensions/signal/package.json +1 -1
- package/extensions/slack/package.json +1 -1
- package/extensions/telegram/package.json +1 -1
- package/extensions/tlon/package.json +1 -1
- package/extensions/twitch/CHANGELOG.md +5 -0
- package/extensions/twitch/package.json +1 -1
- package/extensions/voice-call/CHANGELOG.md +5 -0
- package/extensions/voice-call/package.json +1 -1
- package/extensions/whatsapp/package.json +1 -1
- package/extensions/zalo/CHANGELOG.md +5 -0
- package/extensions/zalo/package.json +1 -1
- package/extensions/zalouser/CHANGELOG.md +5 -0
- package/extensions/zalouser/package.json +1 -1
- package/package.json +8 -1
- 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
|
-
|
|
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
|
-
|
|
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
|
+
}
|