@clinebot/core 0.0.5 → 0.0.6

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 (42) hide show
  1. package/dist/index.d.ts +4 -1
  2. package/dist/index.node.d.ts +1 -0
  3. package/dist/index.node.js +134 -107
  4. package/dist/runtime/session-runtime.d.ts +3 -1
  5. package/dist/session/default-session-manager.d.ts +4 -0
  6. package/dist/session/session-host.d.ts +2 -0
  7. package/dist/session/session-manager.d.ts +1 -0
  8. package/dist/telemetry/ITelemetryAdapter.d.ts +54 -0
  9. package/dist/telemetry/LoggerTelemetryAdapter.d.ts +21 -0
  10. package/dist/telemetry/OpenTelemetryAdapter.d.ts +43 -0
  11. package/dist/telemetry/OpenTelemetryProvider.d.ts +41 -0
  12. package/dist/telemetry/TelemetryService.d.ts +34 -0
  13. package/dist/telemetry/opentelemetry.d.ts +3 -0
  14. package/dist/telemetry/opentelemetry.js +27 -0
  15. package/dist/tools/schemas.d.ts +6 -0
  16. package/dist/types/config.d.ts +2 -1
  17. package/package.json +16 -3
  18. package/src/agents/hooks-config-loader.ts +19 -1
  19. package/src/index.node.ts +3 -0
  20. package/src/index.ts +16 -0
  21. package/src/runtime/hook-file-hooks.test.ts +47 -0
  22. package/src/runtime/hook-file-hooks.ts +3 -0
  23. package/src/runtime/runtime-builder.test.ts +20 -0
  24. package/src/runtime/runtime-builder.ts +1 -0
  25. package/src/runtime/session-runtime.ts +3 -1
  26. package/src/session/default-session-manager.test.ts +72 -0
  27. package/src/session/default-session-manager.ts +59 -1
  28. package/src/session/session-host.ts +6 -1
  29. package/src/session/session-manager.ts +1 -0
  30. package/src/telemetry/ITelemetryAdapter.ts +94 -0
  31. package/src/telemetry/LoggerTelemetryAdapter.test.ts +42 -0
  32. package/src/telemetry/LoggerTelemetryAdapter.ts +114 -0
  33. package/src/telemetry/OpenTelemetryAdapter.test.ts +157 -0
  34. package/src/telemetry/OpenTelemetryAdapter.ts +348 -0
  35. package/src/telemetry/OpenTelemetryProvider.test.ts +113 -0
  36. package/src/telemetry/OpenTelemetryProvider.ts +322 -0
  37. package/src/telemetry/TelemetryService.test.ts +134 -0
  38. package/src/telemetry/TelemetryService.ts +141 -0
  39. package/src/telemetry/opentelemetry.ts +20 -0
  40. package/src/tools/definitions.ts +35 -28
  41. package/src/tools/schemas.ts +9 -0
  42. package/src/types/config.ts +2 -0
@@ -0,0 +1,114 @@
1
+ import type { BasicLogger } from "@clinebot/shared";
2
+ import type {
3
+ ITelemetryAdapter,
4
+ TelemetryProperties,
5
+ } from "./ITelemetryAdapter";
6
+
7
+ export interface LoggerTelemetryAdapterOptions {
8
+ logger?: BasicLogger;
9
+ name?: string;
10
+ enabled?: boolean | (() => boolean);
11
+ }
12
+
13
+ export class LoggerTelemetryAdapter implements ITelemetryAdapter {
14
+ readonly name: string;
15
+
16
+ private readonly logger?: BasicLogger;
17
+ private readonly enabled: boolean | (() => boolean);
18
+
19
+ constructor(options: LoggerTelemetryAdapterOptions = {}) {
20
+ this.name = options.name ?? "LoggerTelemetryAdapter";
21
+ this.logger = options.logger;
22
+ this.enabled = options.enabled ?? true;
23
+ }
24
+
25
+ emit(event: string, properties?: TelemetryProperties): void {
26
+ if (!this.isEnabled()) {
27
+ return;
28
+ }
29
+ this.logger?.info?.("telemetry.event", {
30
+ adapter: this.name,
31
+ event,
32
+ properties,
33
+ });
34
+ }
35
+
36
+ emitRequired(event: string, properties?: TelemetryProperties): void {
37
+ this.logger?.warn?.("telemetry.required_event", {
38
+ adapter: this.name,
39
+ event,
40
+ properties,
41
+ });
42
+ }
43
+
44
+ recordCounter(
45
+ name: string,
46
+ value: number,
47
+ attributes?: TelemetryProperties,
48
+ description?: string,
49
+ required?: boolean,
50
+ ): void {
51
+ if (!required && !this.isEnabled()) {
52
+ return;
53
+ }
54
+ this.logger?.debug?.("telemetry.metric", {
55
+ adapter: this.name,
56
+ instrument: "counter",
57
+ name,
58
+ value,
59
+ attributes,
60
+ description,
61
+ required: required === true,
62
+ });
63
+ }
64
+
65
+ recordHistogram(
66
+ name: string,
67
+ value: number,
68
+ attributes?: TelemetryProperties,
69
+ description?: string,
70
+ required?: boolean,
71
+ ): void {
72
+ if (!required && !this.isEnabled()) {
73
+ return;
74
+ }
75
+ this.logger?.debug?.("telemetry.metric", {
76
+ adapter: this.name,
77
+ instrument: "histogram",
78
+ name,
79
+ value,
80
+ attributes,
81
+ description,
82
+ required: required === true,
83
+ });
84
+ }
85
+
86
+ recordGauge(
87
+ name: string,
88
+ value: number | null,
89
+ attributes?: TelemetryProperties,
90
+ description?: string,
91
+ required?: boolean,
92
+ ): void {
93
+ if (!required && !this.isEnabled()) {
94
+ return;
95
+ }
96
+ this.logger?.debug?.("telemetry.metric", {
97
+ adapter: this.name,
98
+ instrument: "gauge",
99
+ name,
100
+ value,
101
+ attributes,
102
+ description,
103
+ required: required === true,
104
+ });
105
+ }
106
+
107
+ isEnabled(): boolean {
108
+ return typeof this.enabled === "function" ? this.enabled() : this.enabled;
109
+ }
110
+
111
+ async flush(): Promise<void> {}
112
+
113
+ async dispose(): Promise<void> {}
114
+ }
@@ -0,0 +1,157 @@
1
+ import type { LoggerProvider } from "@opentelemetry/sdk-logs";
2
+ import type { MeterProvider } from "@opentelemetry/sdk-metrics";
3
+ import { describe, expect, it, vi } from "vitest";
4
+ import { OpenTelemetryAdapter } from "./OpenTelemetryAdapter";
5
+
6
+ describe("OpenTelemetryAdapter", () => {
7
+ it("emits events in the telemetry service log format", () => {
8
+ const emit = vi.fn();
9
+ const adapter = new OpenTelemetryAdapter({
10
+ metadata: makeMetadata(),
11
+ distinctId: "user-123",
12
+ commonProperties: {
13
+ organization_id: "org-1",
14
+ },
15
+ loggerProvider: {
16
+ getLogger: () => ({ emit }),
17
+ } as unknown as LoggerProvider,
18
+ });
19
+
20
+ adapter.emit("task.created", {
21
+ ulid: "01HXYZ",
22
+ nested: {
23
+ mode: "act",
24
+ },
25
+ items: ["a", "b"],
26
+ nullable: null,
27
+ });
28
+
29
+ expect(emit).toHaveBeenCalledWith({
30
+ severityText: "INFO",
31
+ body: "task.created",
32
+ attributes: expect.objectContaining({
33
+ ulid: "01HXYZ",
34
+ "nested.mode": "act",
35
+ items: JSON.stringify(["a", "b"]),
36
+ nullable: "null",
37
+ distinct_id: "user-123",
38
+ organization_id: "org-1",
39
+ extension_version: "1.2.3",
40
+ cline_type: "cli",
41
+ platform: "terminal",
42
+ }),
43
+ });
44
+ });
45
+
46
+ it("marks required events with the expected flag", () => {
47
+ const emit = vi.fn();
48
+ const adapter = new OpenTelemetryAdapter({
49
+ metadata: makeMetadata(),
50
+ loggerProvider: {
51
+ getLogger: () => ({ emit }),
52
+ } as unknown as LoggerProvider,
53
+ enabled: false,
54
+ });
55
+
56
+ adapter.emitRequired("user.opt_out");
57
+
58
+ expect(emit).toHaveBeenCalledWith({
59
+ severityText: "INFO",
60
+ body: "user.opt_out",
61
+ attributes: expect.objectContaining({
62
+ _required: true,
63
+ }),
64
+ });
65
+ });
66
+
67
+ it("records metrics with merged telemetry attributes and retires gauge series", () => {
68
+ const counterAdd = vi.fn();
69
+ const histogramRecord = vi.fn();
70
+ let gaugeCallback:
71
+ | ((result: {
72
+ observe: (
73
+ value: number,
74
+ attributes?: Record<string, string | number | boolean>,
75
+ ) => void;
76
+ }) => void)
77
+ | undefined;
78
+
79
+ const forceFlush = vi.fn().mockResolvedValue(undefined);
80
+ const shutdown = vi.fn().mockResolvedValue(undefined);
81
+
82
+ const adapter = new OpenTelemetryAdapter({
83
+ metadata: makeMetadata(),
84
+ distinctId: "user-123",
85
+ meterProvider: {
86
+ getMeter: () =>
87
+ ({
88
+ createCounter: () => ({ add: counterAdd }),
89
+ createHistogram: () => ({ record: histogramRecord }),
90
+ createObservableGauge: () => ({
91
+ addCallback: (callback: typeof gaugeCallback) => {
92
+ gaugeCallback = callback;
93
+ },
94
+ }),
95
+ }) as never,
96
+ forceFlush,
97
+ shutdown,
98
+ } as unknown as MeterProvider,
99
+ });
100
+
101
+ adapter.recordCounter("cline.turns.total", 2, { ulid: "01HXYZ" });
102
+ adapter.recordHistogram("cline.api.duration.seconds", 1.5, {
103
+ ulid: "01HXYZ",
104
+ });
105
+ adapter.recordGauge("cline.workspace.active_roots", 3, { workspace: "a" });
106
+
107
+ expect(counterAdd).toHaveBeenCalledWith(
108
+ 2,
109
+ expect.objectContaining({
110
+ ulid: "01HXYZ",
111
+ distinct_id: "user-123",
112
+ extension_version: "1.2.3",
113
+ }),
114
+ );
115
+ expect(histogramRecord).toHaveBeenCalledWith(
116
+ 1.5,
117
+ expect.objectContaining({
118
+ ulid: "01HXYZ",
119
+ distinct_id: "user-123",
120
+ }),
121
+ );
122
+
123
+ const observe = vi.fn();
124
+ gaugeCallback?.({ observe });
125
+ expect(observe).toHaveBeenCalledWith(
126
+ 3,
127
+ expect.objectContaining({
128
+ workspace: "a",
129
+ distinct_id: "user-123",
130
+ }),
131
+ );
132
+
133
+ adapter.recordGauge("cline.workspace.active_roots", null, {
134
+ workspace: "a",
135
+ });
136
+ const observeAfterRetire = vi.fn();
137
+ gaugeCallback?.({ observe: observeAfterRetire });
138
+ expect(observeAfterRetire).not.toHaveBeenCalled();
139
+
140
+ return Promise.all([adapter.flush(), adapter.dispose()]).then(() => {
141
+ expect(forceFlush).toHaveBeenCalledTimes(1);
142
+ expect(shutdown).toHaveBeenCalledTimes(1);
143
+ });
144
+ });
145
+ });
146
+
147
+ function makeMetadata() {
148
+ return {
149
+ extension_version: "1.2.3",
150
+ cline_type: "cli",
151
+ platform: "terminal",
152
+ platform_version: "1.0.0",
153
+ os_type: "darwin",
154
+ os_version: "24.0.0",
155
+ is_dev: "true",
156
+ };
157
+ }
@@ -0,0 +1,348 @@
1
+ import type { Meter } from "@opentelemetry/api";
2
+ import type { Logger as OpenTelemetryLogger } from "@opentelemetry/api-logs";
3
+ import type { LoggerProvider } from "@opentelemetry/sdk-logs";
4
+ import type { MeterProvider } from "@opentelemetry/sdk-metrics";
5
+ import type {
6
+ ITelemetryAdapter,
7
+ TelemetryMetadata,
8
+ TelemetryPrimitive,
9
+ TelemetryProperties,
10
+ } from "./ITelemetryAdapter";
11
+
12
+ type FlatTelemetryAttributes = Record<string, string | number | boolean>;
13
+
14
+ export interface OpenTelemetryAdapterOptions {
15
+ readonly metadata: TelemetryMetadata;
16
+ readonly meterProvider?: MeterProvider | null;
17
+ readonly loggerProvider?: LoggerProvider | null;
18
+ readonly name?: string;
19
+ readonly enabled?: boolean | (() => boolean);
20
+ readonly distinctId?: string;
21
+ readonly commonProperties?: TelemetryProperties;
22
+ }
23
+
24
+ export class OpenTelemetryAdapter implements ITelemetryAdapter {
25
+ readonly name: string;
26
+
27
+ private readonly metadata: TelemetryMetadata;
28
+ private readonly meter: Meter | null;
29
+ private readonly logger: OpenTelemetryLogger | null;
30
+ private readonly enabled: boolean | (() => boolean);
31
+ private distinctId?: string;
32
+ private commonProperties: TelemetryProperties;
33
+ private counters = new Map<string, ReturnType<Meter["createCounter"]>>();
34
+ private histograms = new Map<string, ReturnType<Meter["createHistogram"]>>();
35
+ private gauges = new Map<
36
+ string,
37
+ ReturnType<Meter["createObservableGauge"]>
38
+ >();
39
+ private gaugeValues = new Map<
40
+ string,
41
+ Map<string, { value: number; attributes?: TelemetryProperties }>
42
+ >();
43
+ private readonly meterProvider?: MeterProvider | null;
44
+ private readonly loggerProvider?: LoggerProvider | null;
45
+
46
+ constructor(options: OpenTelemetryAdapterOptions) {
47
+ this.name = options.name ?? "OpenTelemetryAdapter";
48
+ this.metadata = { ...options.metadata };
49
+ this.meterProvider = options.meterProvider;
50
+ this.loggerProvider = options.loggerProvider;
51
+ this.meter = options.meterProvider?.getMeter("cline") ?? null;
52
+ this.logger = options.loggerProvider?.getLogger("cline") ?? null;
53
+ this.enabled = options.enabled ?? true;
54
+ this.distinctId = options.distinctId;
55
+ this.commonProperties = options.commonProperties
56
+ ? { ...options.commonProperties }
57
+ : {};
58
+ }
59
+
60
+ emit(event: string, properties?: TelemetryProperties): void {
61
+ if (!this.isEnabled()) {
62
+ return;
63
+ }
64
+ this.emitLog(event, properties, false);
65
+ }
66
+
67
+ emitRequired(event: string, properties?: TelemetryProperties): void {
68
+ this.emitLog(event, properties, true);
69
+ }
70
+
71
+ recordCounter(
72
+ name: string,
73
+ value: number,
74
+ attributes?: TelemetryProperties,
75
+ description?: string,
76
+ required = false,
77
+ ): void {
78
+ if (!this.meter || (!required && !this.isEnabled())) {
79
+ return;
80
+ }
81
+
82
+ let counter = this.counters.get(name);
83
+ if (!counter) {
84
+ counter = this.meter.createCounter(
85
+ name,
86
+ description ? { description } : undefined,
87
+ );
88
+ this.counters.set(name, counter);
89
+ }
90
+
91
+ counter.add(
92
+ value,
93
+ this.flattenProperties(this.buildAttributes(attributes)),
94
+ );
95
+ }
96
+
97
+ recordHistogram(
98
+ name: string,
99
+ value: number,
100
+ attributes?: TelemetryProperties,
101
+ description?: string,
102
+ required = false,
103
+ ): void {
104
+ if (!this.meter || (!required && !this.isEnabled())) {
105
+ return;
106
+ }
107
+
108
+ let histogram = this.histograms.get(name);
109
+ if (!histogram) {
110
+ histogram = this.meter.createHistogram(
111
+ name,
112
+ description ? { description } : undefined,
113
+ );
114
+ this.histograms.set(name, histogram);
115
+ }
116
+
117
+ histogram.record(
118
+ value,
119
+ this.flattenProperties(this.buildAttributes(attributes)),
120
+ );
121
+ }
122
+
123
+ recordGauge(
124
+ name: string,
125
+ value: number | null,
126
+ attributes?: TelemetryProperties,
127
+ description?: string,
128
+ required = false,
129
+ ): void {
130
+ if (!this.meter || (!required && !this.isEnabled())) {
131
+ return;
132
+ }
133
+
134
+ const mergedAttributes = this.buildAttributes(attributes);
135
+ const attrKey = JSON.stringify(mergedAttributes);
136
+ const existingSeries = this.gaugeValues.get(name);
137
+
138
+ if (value === null) {
139
+ if (existingSeries) {
140
+ existingSeries.delete(attrKey);
141
+ if (existingSeries.size === 0) {
142
+ this.gaugeValues.delete(name);
143
+ this.gauges.delete(name);
144
+ }
145
+ }
146
+ return;
147
+ }
148
+
149
+ let series = existingSeries;
150
+ if (!series) {
151
+ series = new Map();
152
+ this.gaugeValues.set(name, series);
153
+ }
154
+
155
+ if (!this.gauges.has(name)) {
156
+ const gauge = this.meter.createObservableGauge(
157
+ name,
158
+ description ? { description } : undefined,
159
+ );
160
+ gauge.addCallback((observableResult) => {
161
+ for (const data of this.snapshotGaugeSeries(name)) {
162
+ observableResult.observe(
163
+ data.value,
164
+ this.flattenProperties(data.attributes),
165
+ );
166
+ }
167
+ });
168
+ this.gauges.set(name, gauge);
169
+ }
170
+
171
+ series.set(attrKey, { value, attributes: mergedAttributes });
172
+ }
173
+
174
+ isEnabled(): boolean {
175
+ return typeof this.enabled === "function" ? this.enabled() : this.enabled;
176
+ }
177
+
178
+ setDistinctId(distinctId?: string): void {
179
+ this.distinctId = distinctId;
180
+ }
181
+
182
+ setCommonProperties(properties: TelemetryProperties): void {
183
+ this.commonProperties = { ...properties };
184
+ }
185
+
186
+ updateCommonProperties(properties: TelemetryProperties): void {
187
+ this.commonProperties = {
188
+ ...this.commonProperties,
189
+ ...properties,
190
+ };
191
+ }
192
+
193
+ async flush(): Promise<void> {
194
+ await Promise.all([
195
+ this.meterProvider?.forceFlush?.(),
196
+ this.loggerProvider?.forceFlush?.(),
197
+ ]);
198
+ }
199
+
200
+ async dispose(): Promise<void> {
201
+ await Promise.all([
202
+ this.meterProvider?.shutdown?.(),
203
+ this.loggerProvider?.shutdown?.(),
204
+ ]);
205
+ }
206
+
207
+ private emitLog(
208
+ event: string,
209
+ properties: TelemetryProperties | undefined,
210
+ required: boolean,
211
+ ): void {
212
+ if (!this.logger) {
213
+ return;
214
+ }
215
+
216
+ const attributes = this.flattenProperties(
217
+ this.buildAttributes(properties, required),
218
+ );
219
+ this.logger.emit({
220
+ severityText: "INFO",
221
+ body: event,
222
+ attributes,
223
+ });
224
+ }
225
+
226
+ private buildAttributes(
227
+ properties?: TelemetryProperties,
228
+ required = false,
229
+ ): TelemetryProperties {
230
+ return {
231
+ ...this.commonProperties,
232
+ ...properties,
233
+ ...this.metadata,
234
+ ...(this.distinctId ? { distinct_id: this.distinctId } : {}),
235
+ ...(required ? { _required: true } : {}),
236
+ };
237
+ }
238
+
239
+ private snapshotGaugeSeries(
240
+ name: string,
241
+ ): Array<{ value: number; attributes?: TelemetryProperties }> {
242
+ const series = this.gaugeValues.get(name);
243
+ if (!series) {
244
+ return [];
245
+ }
246
+ return Array.from(series.values(), (entry) => ({
247
+ value: entry.value,
248
+ attributes: entry.attributes ? { ...entry.attributes } : undefined,
249
+ }));
250
+ }
251
+
252
+ private flattenProperties(
253
+ properties?: TelemetryProperties,
254
+ prefix = "",
255
+ seen: WeakSet<object> = new WeakSet(),
256
+ depth = 0,
257
+ ): FlatTelemetryAttributes {
258
+ if (!properties) {
259
+ return {};
260
+ }
261
+
262
+ const flattened: FlatTelemetryAttributes = {};
263
+ const maxArraySize = 100;
264
+ const maxDepth = 10;
265
+
266
+ for (const [key, value] of Object.entries(properties)) {
267
+ if (key === "__proto__" || key === "constructor" || key === "prototype") {
268
+ continue;
269
+ }
270
+
271
+ const fullKey = prefix ? `${prefix}.${key}` : key;
272
+
273
+ if (value === null || value === undefined) {
274
+ flattened[fullKey] = String(value);
275
+ continue;
276
+ }
277
+
278
+ if (Array.isArray(value)) {
279
+ const limited =
280
+ value.length > maxArraySize ? value.slice(0, maxArraySize) : value;
281
+ try {
282
+ flattened[fullKey] = JSON.stringify(limited);
283
+ } catch {
284
+ flattened[fullKey] = "[UnserializableArray]";
285
+ }
286
+ if (value.length > maxArraySize) {
287
+ flattened[`${fullKey}_truncated`] = true;
288
+ flattened[`${fullKey}_original_length`] = value.length;
289
+ }
290
+ continue;
291
+ }
292
+
293
+ if (typeof value === "object") {
294
+ if (value instanceof Date) {
295
+ flattened[fullKey] = value.toISOString();
296
+ continue;
297
+ }
298
+ if (value instanceof Error) {
299
+ flattened[fullKey] = value.message;
300
+ continue;
301
+ }
302
+ if (seen.has(value)) {
303
+ flattened[fullKey] = "[Circular]";
304
+ continue;
305
+ }
306
+ if (depth >= maxDepth) {
307
+ flattened[fullKey] = "[MaxDepthExceeded]";
308
+ continue;
309
+ }
310
+
311
+ seen.add(value);
312
+ Object.assign(
313
+ flattened,
314
+ this.flattenProperties(
315
+ value as TelemetryProperties,
316
+ fullKey,
317
+ seen,
318
+ depth + 1,
319
+ ),
320
+ );
321
+ continue;
322
+ }
323
+
324
+ if (isTelemetryPrimitive(value)) {
325
+ flattened[fullKey] = value;
326
+ continue;
327
+ }
328
+
329
+ try {
330
+ flattened[fullKey] = JSON.stringify(value);
331
+ } catch {
332
+ flattened[fullKey] = String(value);
333
+ }
334
+ }
335
+
336
+ return flattened;
337
+ }
338
+ }
339
+
340
+ function isTelemetryPrimitive(
341
+ value: unknown,
342
+ ): value is Exclude<TelemetryPrimitive, null | undefined> {
343
+ return (
344
+ typeof value === "string" ||
345
+ typeof value === "number" ||
346
+ typeof value === "boolean"
347
+ );
348
+ }