@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
@@ -23,6 +23,12 @@ export declare const ReadFilesInputUnionSchema: z.ZodUnion<readonly [z.ZodObject
23
23
  export declare const SearchCodebaseInputSchema: z.ZodObject<{
24
24
  queries: z.ZodArray<z.ZodString>;
25
25
  }, z.core.$strip>;
26
+ /**
27
+ * Union schema for search_codebase tool input, allowing either a single string, an array of strings, or the full object schema
28
+ */
29
+ export declare const SearchCodebaseUnionInputSchema: z.ZodUnion<readonly [z.ZodObject<{
30
+ queries: z.ZodArray<z.ZodString>;
31
+ }, z.core.$strip>, z.ZodArray<z.ZodString>, z.ZodString]>;
26
32
  /**
27
33
  * Schema for run_commands tool input
28
34
  */
@@ -1,6 +1,6 @@
1
1
  import type { AgentConfig, AgentHooks, ConsecutiveMistakeLimitContext, ConsecutiveMistakeLimitDecision, HookErrorMode, TeamEvent, Tool } from "@clinebot/agents";
2
2
  import type { providers as LlmsProviders } from "@clinebot/llms";
3
- import type { AgentMode, BasicLogger, SessionExecutionConfig, SessionPromptConfig, SessionWorkspaceConfig } from "@clinebot/shared";
3
+ import type { AgentMode, BasicLogger, ITelemetryService, SessionExecutionConfig, SessionPromptConfig, SessionWorkspaceConfig } from "@clinebot/shared";
4
4
  import type { ToolRoutingRule } from "../tools/model-tool-routing.js";
5
5
  export type CoreAgentMode = AgentMode;
6
6
  export interface CoreModelConfig {
@@ -35,6 +35,7 @@ export interface CoreSessionConfig extends CoreModelConfig, CoreRuntimeFeatures,
35
35
  hooks?: AgentHooks;
36
36
  hookErrorMode?: HookErrorMode;
37
37
  logger?: BasicLogger;
38
+ telemetry?: ITelemetryService;
38
39
  extraTools?: Tool[];
39
40
  pluginPaths?: string[];
40
41
  extensions?: AgentConfig["extensions"];
package/package.json CHANGED
@@ -1,10 +1,18 @@
1
1
  {
2
2
  "name": "@clinebot/core",
3
- "version": "0.0.5",
3
+ "version": "0.0.6",
4
4
  "main": "./dist/index.node.js",
5
5
  "dependencies": {
6
- "@clinebot/agents": "0.0.5",
7
- "@clinebot/llms": "0.0.5",
6
+ "@clinebot/agents": "0.0.6",
7
+ "@clinebot/llms": "0.0.6",
8
+ "@opentelemetry/api": "^1.9.0",
9
+ "@opentelemetry/api-logs": "^0.56.0",
10
+ "@opentelemetry/exporter-logs-otlp-http": "^0.56.0",
11
+ "@opentelemetry/exporter-metrics-otlp-http": "^0.56.0",
12
+ "@opentelemetry/resources": "^1.30.1",
13
+ "@opentelemetry/sdk-logs": "^0.56.0",
14
+ "@opentelemetry/sdk-metrics": "^1.30.1",
15
+ "@opentelemetry/semantic-conventions": "^1.37.0",
8
16
  "better-sqlite3": "^11.10.0",
9
17
  "nanoid": "^5.1.7",
10
18
  "simple-git": "^3.32.3",
@@ -21,6 +29,11 @@
21
29
  "development": "./src/index.node.ts",
22
30
  "types": "./dist/index.node.d.ts",
23
31
  "import": "./dist/index.node.js"
32
+ },
33
+ "./telemetry/opentelemetry": {
34
+ "development": "./src/telemetry/opentelemetry.ts",
35
+ "types": "./dist/telemetry/opentelemetry.d.ts",
36
+ "import": "./dist/telemetry/opentelemetry.js"
24
37
  }
25
38
  },
26
39
  "description": "State-aware orchestration for Cline Agent runtimes",
@@ -45,10 +45,28 @@ const HOOK_CONFIG_FILE_LOOKUP = new Map<string, HookConfigFileName>(
45
45
  Object.values(HookConfigFileName).map((name) => [name.toLowerCase(), name]),
46
46
  );
47
47
 
48
+ const SUPPORTED_HOOK_FILE_EXTENSIONS = new Set([
49
+ "",
50
+ ".sh",
51
+ ".bash",
52
+ ".zsh",
53
+ ".js",
54
+ ".mjs",
55
+ ".cjs",
56
+ ".ts",
57
+ ".mts",
58
+ ".cts",
59
+ ".py",
60
+ ]);
61
+
48
62
  export function toHookConfigFileName(
49
63
  fileName: string,
50
64
  ): HookConfigFileName | undefined {
51
- const key = basename(fileName, extname(fileName)).trim().toLowerCase();
65
+ const extension = extname(fileName).toLowerCase();
66
+ if (!SUPPORTED_HOOK_FILE_EXTENSIONS.has(extension)) {
67
+ return undefined;
68
+ }
69
+ const key = basename(fileName, extension).trim().toLowerCase();
52
70
  return HOOK_CONFIG_FILE_LOOKUP.get(key);
53
71
  }
54
72
 
package/src/index.node.ts CHANGED
@@ -100,6 +100,9 @@ export {
100
100
  OCI_HEADER_OPC_REQUEST_ID,
101
101
  refreshOcaToken,
102
102
  } from "./auth/oca";
103
+ export async function loadOpenTelemetryAdapter() {
104
+ return import("./telemetry/opentelemetry.js");
105
+ }
103
106
  export { startLocalOAuthServer } from "./auth/server";
104
107
  export type {
105
108
  OAuthCredentials,
package/src/index.ts CHANGED
@@ -19,6 +19,7 @@ export type {
19
19
  BasicLogger,
20
20
  ConnectorHookEvent,
21
21
  HookSessionContext,
22
+ ITelemetryService,
22
23
  RpcAddProviderActionRequest,
23
24
  RpcChatMessage,
24
25
  RpcChatRunTurnRequest,
@@ -38,6 +39,12 @@ export type {
38
39
  RpcSaveProviderSettingsActionRequest,
39
40
  SessionLineage,
40
41
  TeamProgressProjectionEvent,
42
+ TelemetryArray,
43
+ TelemetryMetadata,
44
+ TelemetryObject,
45
+ TelemetryPrimitive,
46
+ TelemetryProperties,
47
+ TelemetryValue,
41
48
  ToolPolicy,
42
49
  } from "@clinebot/shared";
43
50
  export {
@@ -116,6 +123,15 @@ export {
116
123
  buildTeamProgressSummary,
117
124
  toTeamProgressLifecycleEvent,
118
125
  } from "./team";
126
+ export type { ITelemetryAdapter } from "./telemetry/ITelemetryAdapter";
127
+ export {
128
+ LoggerTelemetryAdapter,
129
+ type LoggerTelemetryAdapterOptions,
130
+ } from "./telemetry/LoggerTelemetryAdapter";
131
+ export {
132
+ TelemetryService,
133
+ type TelemetryServiceOptions,
134
+ } from "./telemetry/TelemetryService";
119
135
  export {
120
136
  ALL_DEFAULT_TOOL_NAMES,
121
137
  type AskQuestionExecutor,
@@ -17,6 +17,22 @@ async function createWorkspaceWithHook(
17
17
  }
18
18
 
19
19
  describe("createHookConfigFileHooks", () => {
20
+ it("ignores example hook files", async () => {
21
+ const { workspace } = await createWorkspaceWithHook(
22
+ "PreToolUse.example",
23
+ 'echo \'HOOK_CONTROL\t{"cancel":true,"context":"should-not-run"}\'\n',
24
+ );
25
+ try {
26
+ const hooks = createHookConfigFileHooks({
27
+ cwd: workspace,
28
+ workspacePath: workspace,
29
+ });
30
+ expect(hooks).toBeUndefined();
31
+ } finally {
32
+ await rm(workspace, { recursive: true, force: true });
33
+ }
34
+ });
35
+
20
36
  it("executes extensionless legacy hook files via bash fallback", async () => {
21
37
  const { workspace } = await createWorkspaceWithHook(
22
38
  "PreToolUse",
@@ -103,4 +119,35 @@ describe("createHookConfigFileHooks", () => {
103
119
  await rm(workspace, { recursive: true, force: true });
104
120
  }
105
121
  });
122
+
123
+ it("executes python hook files", async () => {
124
+ const { workspace } = await createWorkspaceWithHook(
125
+ "PreToolUse.py",
126
+ 'print(\'HOOK_CONTROL\\t{"cancel": false, "context": "python-ok"}\')\n',
127
+ );
128
+ try {
129
+ const hooks = createHookConfigFileHooks({
130
+ cwd: workspace,
131
+ workspacePath: workspace,
132
+ });
133
+ expect(hooks?.onToolCallStart).toBeTypeOf("function");
134
+ const control = await hooks?.onToolCallStart?.({
135
+ agentId: "agent_1",
136
+ conversationId: "conv_1",
137
+ parentAgentId: null,
138
+ iteration: 1,
139
+ call: {
140
+ id: "call_1",
141
+ name: "read_file",
142
+ input: { path: "README.md" },
143
+ },
144
+ });
145
+ expect(control).toMatchObject({
146
+ cancel: false,
147
+ context: "python-ok",
148
+ });
149
+ } finally {
150
+ await rm(workspace, { recursive: true, force: true });
151
+ }
152
+ });
106
153
  });
@@ -323,6 +323,9 @@ function inferHookCommand(path: string): string[] {
323
323
  ) {
324
324
  return ["bun", "run", path];
325
325
  }
326
+ if (lowered.endsWith(".py")) {
327
+ return ["python3", path];
328
+ }
326
329
  // Default to bash for legacy hook files with no extension/shebang.
327
330
  return ["/bin/bash", path];
328
331
  }
@@ -3,6 +3,7 @@ import { tmpdir } from "node:os";
3
3
  import { join } from "node:path";
4
4
  import type { Tool } from "@clinebot/agents";
5
5
  import { describe, expect, it } from "vitest";
6
+ import { TelemetryService } from "../telemetry/TelemetryService";
6
7
  import { DefaultRuntimeBuilder } from "./runtime-builder";
7
8
 
8
9
  function makeSpawnTool(): Tool {
@@ -55,6 +56,25 @@ describe("DefaultRuntimeBuilder", () => {
55
56
  expect(runtime.logger).toBe(logger);
56
57
  });
57
58
 
59
+ it("forwards telemetry for downstream runtime consumers", () => {
60
+ const telemetry = new TelemetryService();
61
+ const runtime = new DefaultRuntimeBuilder().build({
62
+ config: {
63
+ providerId: "anthropic",
64
+ modelId: "claude-sonnet-4-6",
65
+ apiKey: "key",
66
+ systemPrompt: "test",
67
+ cwd: process.cwd(),
68
+ enableTools: false,
69
+ enableSpawnAgent: false,
70
+ enableAgentTeams: false,
71
+ telemetry,
72
+ },
73
+ });
74
+
75
+ expect(runtime.telemetry).toBe(telemetry);
76
+ });
77
+
58
78
  it("uses readonly preset in plan mode", () => {
59
79
  const runtime = new DefaultRuntimeBuilder().build({
60
80
  config: {
@@ -522,6 +522,7 @@ export class DefaultRuntimeBuilder implements RuntimeBuilder {
522
522
  return {
523
523
  tools,
524
524
  logger: logger ?? config.logger,
525
+ telemetry: input.telemetry ?? config.telemetry,
525
526
  teamRuntime,
526
527
  completionGuard,
527
528
  shutdown: (reason: string) => {
@@ -5,7 +5,7 @@ import type {
5
5
  AgentTeamsRuntime,
6
6
  Tool,
7
7
  } from "@clinebot/agents";
8
- import type { BasicLogger } from "@clinebot/shared";
8
+ import type { BasicLogger, ITelemetryService } from "@clinebot/shared";
9
9
  import type { UserInstructionConfigWatcher } from "../agents";
10
10
  import type { ToolExecutors } from "../tools";
11
11
  import type { CoreSessionConfig } from "../types/config";
@@ -14,6 +14,7 @@ export interface BuiltRuntime {
14
14
  tools: Tool[];
15
15
  hooks?: AgentHooks;
16
16
  logger?: BasicLogger;
17
+ telemetry?: ITelemetryService;
17
18
  teamRuntime?: AgentTeamsRuntime;
18
19
  completionGuard?: () => string | undefined;
19
20
  shutdown: (reason: string) => Promise<void> | void;
@@ -29,6 +30,7 @@ export interface RuntimeBuilderInput {
29
30
  userInstructionWatcher?: UserInstructionConfigWatcher;
30
31
  defaultToolExecutors?: Partial<ToolExecutors>;
31
32
  logger?: BasicLogger;
33
+ telemetry?: ITelemetryService;
32
34
  }
33
35
 
34
36
  export interface RuntimeBuilder {
@@ -3,6 +3,7 @@ import { tmpdir } from "node:os";
3
3
  import { join } from "node:path";
4
4
  import type { AgentResult } from "@clinebot/agents";
5
5
  import { describe, expect, it, vi } from "vitest";
6
+ import { TelemetryService } from "../telemetry/TelemetryService";
6
7
  import { SessionSource } from "../types/common";
7
8
  import type { CoreSessionConfig } from "../types/config";
8
9
  import { DefaultSessionManager } from "./default-session-manager";
@@ -70,6 +71,77 @@ function createConfig(
70
71
  }
71
72
 
72
73
  describe("DefaultSessionManager", () => {
74
+ it("emits session lifecycle telemetry when configured", async () => {
75
+ const sessionId = "sess-telemetry";
76
+ const manifest = createManifest(sessionId);
77
+ const adapter = {
78
+ name: "test",
79
+ emit: vi.fn(),
80
+ emitRequired: vi.fn(),
81
+ recordCounter: vi.fn(),
82
+ recordHistogram: vi.fn(),
83
+ recordGauge: vi.fn(),
84
+ isEnabled: vi.fn(() => true),
85
+ flush: vi.fn().mockResolvedValue(undefined),
86
+ dispose: vi.fn().mockResolvedValue(undefined),
87
+ };
88
+ const telemetry = new TelemetryService({
89
+ adapters: [adapter],
90
+ distinctId: distinctId,
91
+ });
92
+ const sessionService = {
93
+ ensureSessionsDir: vi.fn().mockReturnValue("/tmp/sessions"),
94
+ createRootSessionWithArtifacts: vi.fn().mockResolvedValue({
95
+ manifestPath: "/tmp/manifest.json",
96
+ transcriptPath: "/tmp/transcript.log",
97
+ hookPath: "/tmp/hook.log",
98
+ messagesPath: "/tmp/messages.json",
99
+ manifest,
100
+ }),
101
+ persistSessionMessages: vi.fn(),
102
+ updateSessionStatus: vi.fn().mockResolvedValue({
103
+ updated: true,
104
+ endedAt: "2026-01-01T00:00:05.000Z",
105
+ }),
106
+ writeSessionManifest: vi.fn(),
107
+ listSessions: vi.fn().mockResolvedValue([]),
108
+ deleteSession: vi.fn().mockResolvedValue({ deleted: true }),
109
+ };
110
+ const runtimeBuilder = {
111
+ build: vi.fn().mockReturnValue({
112
+ tools: [],
113
+ shutdown: vi.fn(),
114
+ }),
115
+ };
116
+ const agent = {
117
+ run: vi.fn().mockResolvedValue(createResult()),
118
+ continue: vi.fn().mockResolvedValue(createResult()),
119
+ getMessages: vi.fn().mockReturnValue([]),
120
+ abort: vi.fn(),
121
+ shutdown: vi.fn().mockResolvedValue(undefined),
122
+ };
123
+ const manager = new DefaultSessionManager({
124
+ distinctId,
125
+ sessionService: sessionService as never,
126
+ runtimeBuilder: runtimeBuilder as never,
127
+ createAgent: () => agent as never,
128
+ telemetry,
129
+ });
130
+
131
+ await manager.start({
132
+ config: createConfig({ telemetry, sessionId }),
133
+ prompt: "hello",
134
+ });
135
+
136
+ expect(adapter.emit).toHaveBeenCalledWith(
137
+ "session.started",
138
+ expect.objectContaining({
139
+ sessionId,
140
+ distinct_id: distinctId,
141
+ }),
142
+ );
143
+ });
144
+
73
145
  it("runs a non-interactive prompt and persists messages/status", async () => {
74
146
  const sessionId = "sess-1";
75
147
  const manifest = createManifest(sessionId);
@@ -14,7 +14,11 @@ import {
14
14
  type ToolApprovalResult,
15
15
  } from "@clinebot/agents";
16
16
  import type { providers as LlmsProviders } from "@clinebot/llms";
17
- import { formatUserInputBlock, normalizeUserInput } from "@clinebot/shared";
17
+ import {
18
+ formatUserInputBlock,
19
+ type ITelemetryService,
20
+ normalizeUserInput,
21
+ } from "@clinebot/shared";
18
22
  import { setHomeDirIfUnset } from "@clinebot/shared/storage";
19
23
  import { nanoid } from "nanoid";
20
24
  import { resolveAndLoadAgentPlugins } from "../agents/plugin-config-loader";
@@ -89,6 +93,7 @@ export interface DefaultSessionManagerOptions {
89
93
  toolPolicies?: AgentConfig["toolPolicies"];
90
94
  providerSettingsManager?: ProviderSettingsManager;
91
95
  oauthTokenManager?: RuntimeOAuthTokenManager;
96
+ telemetry?: ITelemetryService;
92
97
  requestToolApproval?: (
93
98
  request: ToolApprovalRequest,
94
99
  ) => Promise<ToolApprovalResult>;
@@ -120,6 +125,7 @@ export class DefaultSessionManager implements SessionManager {
120
125
  private readonly defaultToolPolicies?: AgentConfig["toolPolicies"];
121
126
  private readonly providerSettingsManager: ProviderSettingsManager;
122
127
  private readonly oauthTokenManager: RuntimeOAuthTokenManager;
128
+ private readonly defaultTelemetry?: ITelemetryService;
123
129
  private readonly defaultRequestToolApproval?: (
124
130
  request: ToolApprovalRequest,
125
131
  ) => Promise<ToolApprovalResult>;
@@ -143,6 +149,7 @@ export class DefaultSessionManager implements SessionManager {
143
149
  new RuntimeOAuthTokenManager({
144
150
  providerSettingsManager: this.providerSettingsManager,
145
151
  });
152
+ this.defaultTelemetry = options.telemetry;
146
153
  this.defaultRequestToolApproval = options.requestToolApproval;
147
154
  }
148
155
 
@@ -263,6 +270,7 @@ export class DefaultSessionManager implements SessionManager {
263
270
  ...input.config,
264
271
  hooks: effectiveHooks,
265
272
  extensions: effectiveExtensions,
273
+ telemetry: input.config.telemetry ?? this.defaultTelemetry,
266
274
  };
267
275
  const providerConfig =
268
276
  this.buildResolvedProviderConfig(effectiveConfigBase);
@@ -276,6 +284,7 @@ export class DefaultSessionManager implements SessionManager {
276
284
  hooks: effectiveHooks,
277
285
  extensions: effectiveExtensions,
278
286
  logger: effectiveConfig.logger,
287
+ telemetry: effectiveConfig.telemetry,
279
288
  onTeamEvent: (event: TeamEvent) => {
280
289
  void this.handleTeamEvent(sessionId, event);
281
290
  effectiveConfig.onTeamEvent?.(event);
@@ -287,6 +296,18 @@ export class DefaultSessionManager implements SessionManager {
287
296
  input.defaultToolExecutors ?? this.defaultToolExecutors,
288
297
  });
289
298
  const tools = [...runtime.tools, ...(effectiveConfig.extraTools ?? [])];
299
+ effectiveConfig.telemetry?.capture({
300
+ event: "session.started",
301
+ properties: {
302
+ sessionId,
303
+ source,
304
+ providerId: effectiveConfig.providerId,
305
+ modelId: effectiveConfig.modelId,
306
+ enableTools: effectiveConfig.enableTools,
307
+ enableSpawnAgent: effectiveConfig.enableSpawnAgent,
308
+ enableAgentTeams: effectiveConfig.enableAgentTeams,
309
+ },
310
+ });
290
311
  const agent = this.createAgentInstance({
291
312
  providerId: providerConfig.providerId,
292
313
  modelId: providerConfig.modelId,
@@ -326,6 +347,14 @@ export class DefaultSessionManager implements SessionManager {
326
347
  }),
327
348
  );
328
349
  }
350
+ if (event.type === "iteration_end") {
351
+ void this.invoke<void>(
352
+ "persistSessionMessages",
353
+ sessionId,
354
+ liveSession?.agent.getMessages() ?? [],
355
+ liveSession?.config.systemPrompt,
356
+ );
357
+ }
329
358
  this.emit({
330
359
  type: "agent_event",
331
360
  payload: {
@@ -397,6 +426,15 @@ export class DefaultSessionManager implements SessionManager {
397
426
  if (!session) {
398
427
  throw new Error(`session not found: ${input.sessionId}`);
399
428
  }
429
+ session.config.telemetry?.capture({
430
+ event: "session.input_sent",
431
+ properties: {
432
+ sessionId: input.sessionId,
433
+ promptLength: input.prompt.length,
434
+ userImageCount: input.userImages?.length ?? 0,
435
+ userFileCount: input.userFiles?.length ?? 0,
436
+ },
437
+ });
400
438
  try {
401
439
  const result = await this.runTurn(session, {
402
440
  prompt: input.prompt,
@@ -428,6 +466,10 @@ export class DefaultSessionManager implements SessionManager {
428
466
  if (!session) {
429
467
  return;
430
468
  }
469
+ session.config.telemetry?.capture({
470
+ event: "session.aborted",
471
+ properties: { sessionId },
472
+ });
431
473
  session.aborting = true;
432
474
  session.agent.abort();
433
475
  }
@@ -437,6 +479,10 @@ export class DefaultSessionManager implements SessionManager {
437
479
  if (!session) {
438
480
  return;
439
481
  }
482
+ session.config.telemetry?.capture({
483
+ event: "session.stopped",
484
+ properties: { sessionId },
485
+ });
440
486
  await this.shutdownSession(session, {
441
487
  status: "cancelled",
442
488
  exitCode: null,
@@ -1200,4 +1246,16 @@ export class DefaultSessionManager implements SessionManager {
1200
1246
  apiKey: resolved.apiKey,
1201
1247
  });
1202
1248
  }
1249
+
1250
+ async updateSessionModel(sessionId: string, modelId: string): Promise<void> {
1251
+ const session = this.sessions.get(sessionId);
1252
+ if (!session) {
1253
+ throw new Error(`session not found: ${sessionId}`);
1254
+ }
1255
+ session.config.modelId = modelId;
1256
+ const agentWithConnection = session.agent as Agent & {
1257
+ updateConnection?: (overrides: { modelId?: string }) => void;
1258
+ };
1259
+ agentWithConnection.updateConnection?.({ modelId });
1260
+ }
1203
1261
  }
@@ -7,6 +7,7 @@ import type {
7
7
  ToolApprovalResult,
8
8
  } from "@clinebot/agents";
9
9
  import { getRpcServerDefaultAddress, getRpcServerHealth } from "@clinebot/rpc";
10
+ import type { ITelemetryService } from "@clinebot/shared";
10
11
  import { resolveSessionDataDir } from "@clinebot/shared/storage";
11
12
  import { nanoid } from "nanoid";
12
13
  import { SqliteSessionStore } from "../storage/sqlite-session-store";
@@ -33,6 +34,7 @@ export interface CreateSessionHostOptions {
33
34
  rpcConnectAttempts?: number;
34
35
  rpcConnectDelayMs?: number;
35
36
  defaultToolExecutors?: Partial<ToolExecutors>;
37
+ telemetry?: ITelemetryService;
36
38
  toolPolicies?: AgentConfig["toolPolicies"];
37
39
  requestToolApproval?: (
38
40
  request: ToolApprovalRequest,
@@ -190,13 +192,16 @@ export async function resolveSessionBackend(
190
192
  export async function createSessionHost(
191
193
  options: CreateSessionHostOptions,
192
194
  ): Promise<SessionHost> {
195
+ const distinctId = resolveHostDistinctId(options.distinctId);
196
+ options.telemetry?.setDistinctId(distinctId);
193
197
  const backend =
194
198
  options.sessionService ?? (await resolveSessionBackend(options));
195
199
  return new DefaultSessionManager({
196
200
  sessionService: backend,
197
201
  defaultToolExecutors: options.defaultToolExecutors,
202
+ telemetry: options.telemetry,
198
203
  toolPolicies: options.toolPolicies,
199
204
  requestToolApproval: options.requestToolApproval,
200
- distinctId: resolveHostDistinctId(options.distinctId),
205
+ distinctId,
201
206
  });
202
207
  }
@@ -64,4 +64,5 @@ export interface SessionManager {
64
64
  readTranscript(sessionId: string, maxChars?: number): Promise<string>;
65
65
  readHooks(sessionId: string, limit?: number): Promise<unknown[]>;
66
66
  subscribe(listener: (event: CoreSessionEvent) => void): () => void;
67
+ updateSessionModel?(sessionId: string, modelId: string): Promise<void>;
67
68
  }
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Telemetry adapter interface for the @clinebot/core SDK.
3
+ *
4
+ * This is the SDK-side counterpart to the extension's ITelemetryProvider.
5
+ * It is intentionally free of VS Code / host-provider dependencies so that
6
+ * any consumer (CLI, tests, third-party integrations) can plug in their own
7
+ * backend without pulling in the full extension runtime.
8
+ */
9
+
10
+ import type { TelemetryProperties } from "@clinebot/shared";
11
+
12
+ export type {
13
+ TelemetryArray,
14
+ TelemetryMetadata,
15
+ TelemetryObject,
16
+ TelemetryPrimitive,
17
+ TelemetryProperties,
18
+ TelemetryValue,
19
+ } from "@clinebot/shared";
20
+
21
+ // ---------------------------------------------------------------------------
22
+ // Adapter interface
23
+ // ---------------------------------------------------------------------------
24
+
25
+ /**
26
+ * Telemetry adapter that an SDK consumer implements (or uses via the
27
+ * provided {@link OpenTelemetryAdapter}) to receive Cline telemetry events.
28
+ *
29
+ * The interface intentionally mirrors ITelemetryProvider from the extension
30
+ * so that shared logic can be re-used or compared easily.
31
+ */
32
+ export interface ITelemetryAdapter {
33
+ /** Human-readable adapter name used for logging / diagnostics. */
34
+ readonly name: string;
35
+
36
+ /**
37
+ * Emit a standard telemetry event.
38
+ * Implementations may silently drop events when telemetry is disabled.
39
+ */
40
+ emit(event: string, properties?: TelemetryProperties): void;
41
+
42
+ /**
43
+ * Emit a *required* telemetry event that must not be suppressed by
44
+ * user opt-out settings (e.g. final opt-out confirmation events).
45
+ */
46
+ emitRequired(event: string, properties?: TelemetryProperties): void;
47
+
48
+ /**
49
+ * Record a monotonically-increasing counter metric.
50
+ * Implementations that do not support metrics may treat this as a no-op.
51
+ */
52
+ recordCounter(
53
+ name: string,
54
+ value: number,
55
+ attributes?: TelemetryProperties,
56
+ description?: string,
57
+ required?: boolean,
58
+ ): void;
59
+
60
+ /**
61
+ * Record a histogram (distribution) metric.
62
+ * Implementations that do not support metrics may treat this as a no-op.
63
+ */
64
+ recordHistogram(
65
+ name: string,
66
+ value: number,
67
+ attributes?: TelemetryProperties,
68
+ description?: string,
69
+ required?: boolean,
70
+ ): void;
71
+
72
+ /**
73
+ * Record a gauge (point-in-time) metric.
74
+ * Pass `null` as `value` to retire the series identified by
75
+ * `name + attributes` and prevent stale gauge entries.
76
+ * Implementations that do not support metrics may treat this as a no-op.
77
+ */
78
+ recordGauge(
79
+ name: string,
80
+ value: number | null,
81
+ attributes?: TelemetryProperties,
82
+ description?: string,
83
+ required?: boolean,
84
+ ): void;
85
+
86
+ /** Returns whether the adapter is currently accepting events. */
87
+ isEnabled(): boolean;
88
+
89
+ /** Flush any buffered events/metrics to the backend. */
90
+ flush(): Promise<void>;
91
+
92
+ /** Release all resources held by the adapter. */
93
+ dispose(): Promise<void>;
94
+ }
@@ -0,0 +1,42 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import { LoggerTelemetryAdapter } from "./LoggerTelemetryAdapter";
3
+
4
+ describe("LoggerTelemetryAdapter", () => {
5
+ it("logs events and metrics through the provided logger", async () => {
6
+ const logger = {
7
+ debug: vi.fn(),
8
+ info: vi.fn(),
9
+ warn: vi.fn(),
10
+ };
11
+ const adapter = new LoggerTelemetryAdapter({ logger });
12
+
13
+ adapter.emit("session.started", { sessionId: "s1" });
14
+ adapter.emitRequired("user.opt_out", { reason: "manual" });
15
+ adapter.recordCounter("cline.session.starts.total", 1, {
16
+ sessionId: "s1",
17
+ });
18
+
19
+ expect(logger.info).toHaveBeenCalledWith("telemetry.event", {
20
+ adapter: "LoggerTelemetryAdapter",
21
+ event: "session.started",
22
+ properties: { sessionId: "s1" },
23
+ });
24
+ expect(logger.warn).toHaveBeenCalledWith("telemetry.required_event", {
25
+ adapter: "LoggerTelemetryAdapter",
26
+ event: "user.opt_out",
27
+ properties: { reason: "manual" },
28
+ });
29
+ expect(logger.debug).toHaveBeenCalledWith("telemetry.metric", {
30
+ adapter: "LoggerTelemetryAdapter",
31
+ instrument: "counter",
32
+ name: "cline.session.starts.total",
33
+ value: 1,
34
+ attributes: { sessionId: "s1" },
35
+ description: undefined,
36
+ required: false,
37
+ });
38
+
39
+ await adapter.flush();
40
+ await adapter.dispose();
41
+ });
42
+ });