@timefly/opencode-plugin 0.2.3 → 0.2.8

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/README.md CHANGED
@@ -76,7 +76,7 @@ Example result:
76
76
  ```json
77
77
  {
78
78
  "$schema": "https://opencode.ai/config.json",
79
- "plugin": ["@timefly/opencode-plugin"]
79
+ "plugin": ["@timefly/opencode-plugin@latest"]
80
80
  }
81
81
  ```
82
82
 
@@ -97,18 +97,11 @@ OpenCode plugins run inside the OpenCode process. There is no built-in TimeFly U
97
97
  | Step | What happens |
98
98
  |------|----------------|
99
99
  | `login` | Opens browser → Google OAuth → saves tokens to `~/.config/opencode/timefly-auth.json` |
100
- | Plugin startup | Reads auth file (or `TIMEFLY_ACCESS_TOKEN` env override) |
100
+ | Plugin startup | Reads auth file from `login` |
101
101
  | Each sync | Sends `Authorization: Bearer <accessToken>` to `POST /ai/sync` |
102
102
  | Token expiry | SDK auto-refreshes using `refreshToken` (~30 days) and updates the auth file |
103
103
  | No auth | Events queue locally in `.timefly-ai-queue.json` (project cwd) |
104
104
 
105
- Manual env override (not recommended — access tokens expire in ~15 minutes):
106
-
107
- ```bash
108
- export TIMEFLY_ACCESS_TOKEN="..."
109
- export TIMEFLY_API_BASE_URL="https://api.timefly.dev"
110
- ```
111
-
112
105
  ## How sync works
113
106
 
114
107
  Unlike the VS Code extension (which syncs on a timer every ~2 minutes), the OpenCode plugin syncs **on every event** — there is no background interval.
@@ -213,20 +206,11 @@ Add to `opencode.json` or `~/.config/opencode/opencode.json`:
213
206
  ```json
214
207
  {
215
208
  "$schema": "https://opencode.ai/config.json",
216
- "plugin": ["@timefly/opencode-plugin"]
209
+ "plugin": ["@timefly/opencode-plugin@latest"]
217
210
  }
218
211
  ```
219
212
 
220
- OpenCode installs npm plugins automatically at startup via Bun.
221
-
222
- ## Environment
223
-
224
- | Variable | Required | Default |
225
- |----------|----------|---------|
226
- | Auth file from `login` | Recommended | `~/.config/opencode/timefly-auth.json` |
227
- | `TIMEFLY_ACCESS_TOKEN` | Optional override | — |
228
- | `TIMEFLY_API_BASE_URL` | No | `https://api.timefly.dev` |
229
- | `TIMEFLY_OPENCODE_SESSION_ID` | No | OpenCode session ID |
213
+ OpenCode installs npm plugins automatically at startup via Bun. Re-run `setup-opencode` to refresh `@latest` and clear the plugin cache.
230
214
 
231
215
  ## Events captured
232
216
 
@@ -287,6 +271,7 @@ Based on OpenCode source (`packages/opencode`, `@opencode-ai/plugin` hooks, and
287
271
 
288
272
  | Topic | Detail |
289
273
  |-------|--------|
274
+ | **Plugin cache** | OpenCode caches plugins under `~/.cache/opencode/packages/`. If you upgraded but still see old errors, delete `@timefly` there and restart OpenCode. |
290
275
  | **Custom providers** (e.g. `nan`) | OpenCode runtime passes flat `Provider.Info` (`provider.id`) via `LLMRequestPrep.prepare` — not `provider.info.id`. Plugin reads both runtime and typed `ProviderContext` shapes. |
291
276
  | **Token counts** | Require provider to report usage. If `message.updated` lacks token fields, turn events are skipped (no crash). |
292
277
  | **Provider-side tools** | Tools executed inside the provider (`providerExecuted`) may not hit `tool.execute.*` — no tool events for those. |
package/dist/cli.js CHANGED
File without changes
@@ -2,12 +2,12 @@ import { createEventTracker } from './event-tracker.js';
2
2
  import { type OpenCodeBusEvent } from './opencode-readers.js';
3
3
  import type { createEventPublisher } from './publish-events.js';
4
4
  type EventPublisher = ReturnType<typeof createEventPublisher>['publish'];
5
- export declare const handleSessionCreated: (eventProperties: Record<string, unknown>, publish: EventPublisher) => Promise<void>;
5
+ export declare const handleSessionCreated: (eventProperties: Record<string, unknown>, tracker: ReturnType<typeof createEventTracker>, publish: EventPublisher) => Promise<void>;
6
6
  export declare const handleSessionIdle: (eventProperties: Record<string, unknown>, tracker: ReturnType<typeof createEventTracker>, publish: EventPublisher) => Promise<void>;
7
7
  export declare const handleMessageUpdated: (eventProperties: Record<string, unknown>, tracker: ReturnType<typeof createEventTracker>, publish: EventPublisher) => Promise<void>;
8
8
  export declare const handleMessagePartUpdated: (eventProperties: Record<string, unknown>, tracker: ReturnType<typeof createEventTracker>, publish: EventPublisher) => Promise<void>;
9
- export declare const handleSessionCompacted: (eventProperties: Record<string, unknown>, publish: EventPublisher) => Promise<void>;
10
- export declare const handleSessionError: (eventProperties: Record<string, unknown>, publish: EventPublisher) => Promise<void>;
9
+ export declare const handleSessionCompacted: (eventProperties: Record<string, unknown>, tracker: ReturnType<typeof createEventTracker>, publish: EventPublisher) => Promise<void>;
10
+ export declare const handleSessionError: (eventProperties: Record<string, unknown>, tracker: ReturnType<typeof createEventTracker>, publish: EventPublisher) => Promise<void>;
11
11
  export declare const handleCommandExecuted: (eventProperties: Record<string, unknown>, publish: EventPublisher) => Promise<void>;
12
12
  export declare const handleBusEvent: (event: OpenCodeBusEvent, tracker: ReturnType<typeof createEventTracker>, publish: EventPublisher) => Promise<void>;
13
13
  export {};
@@ -1 +1 @@
1
- {"version":3,"file":"event-handlers.d.ts","sourceRoot":"","sources":["../src/event-handlers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAA;AAiBvD,OAAO,EASN,KAAK,gBAAgB,EACrB,MAAM,uBAAuB,CAAA;AAC9B,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAA;AAG/D,KAAK,cAAc,GAAG,UAAU,CAAC,OAAO,oBAAoB,CAAC,CAAC,SAAS,CAAC,CAAA;AAKxE,eAAO,MAAM,oBAAoB,GAAI,iBAAiB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,SAAS,cAAc,KAAG,OAAO,CAAC,IAAI,CAapH,CAAA;AAED,eAAO,MAAM,iBAAiB,GAC7B,iBAAiB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACxC,SAAS,UAAU,CAAC,OAAO,kBAAkB,CAAC,EAC9C,SAAS,cAAc,KACrB,OAAO,CAAC,IAAI,CAad,CAAA;AAED,eAAO,MAAM,oBAAoB,GAChC,iBAAiB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACxC,SAAS,UAAU,CAAC,OAAO,kBAAkB,CAAC,EAC9C,SAAS,cAAc,KACrB,OAAO,CAAC,IAAI,CAwCd,CAAA;AAED,eAAO,MAAM,wBAAwB,GACpC,iBAAiB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACxC,SAAS,UAAU,CAAC,OAAO,kBAAkB,CAAC,EAC9C,SAAS,cAAc,KACrB,OAAO,CAAC,IAAI,CAiCd,CAAA;AAED,eAAO,MAAM,sBAAsB,GAClC,iBAAiB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACxC,SAAS,cAAc,KACrB,OAAO,CAAC,IAAI,CAQd,CAAA;AAED,eAAO,MAAM,kBAAkB,GAC9B,iBAAiB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACxC,SAAS,cAAc,KACrB,OAAO,CAAC,IAAI,CAmBd,CAAA;AAED,eAAO,MAAM,qBAAqB,GACjC,iBAAiB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACxC,SAAS,cAAc,KACrB,OAAO,CAAC,IAAI,CAad,CAAA;AAED,eAAO,MAAM,cAAc,GAC1B,OAAO,gBAAgB,EACvB,SAAS,UAAU,CAAC,OAAO,kBAAkB,CAAC,EAC9C,SAAS,cAAc,KACrB,OAAO,CAAC,IAAI,CAkCd,CAAA"}
1
+ {"version":3,"file":"event-handlers.d.ts","sourceRoot":"","sources":["../src/event-handlers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAA;AAiBvD,OAAO,EASN,KAAK,gBAAgB,EACrB,MAAM,uBAAuB,CAAA;AAC9B,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAA;AAG/D,KAAK,cAAc,GAAG,UAAU,CAAC,OAAO,oBAAoB,CAAC,CAAC,SAAS,CAAC,CAAA;AAKxE,eAAO,MAAM,oBAAoB,GAChC,iBAAiB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACxC,SAAS,UAAU,CAAC,OAAO,kBAAkB,CAAC,EAC9C,SAAS,cAAc,KACrB,OAAO,CAAC,IAAI,CAed,CAAA;AAED,eAAO,MAAM,iBAAiB,GAC7B,iBAAiB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACxC,SAAS,UAAU,CAAC,OAAO,kBAAkB,CAAC,EAC9C,SAAS,cAAc,KACrB,OAAO,CAAC,IAAI,CAad,CAAA;AAED,eAAO,MAAM,oBAAoB,GAChC,iBAAiB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACxC,SAAS,UAAU,CAAC,OAAO,kBAAkB,CAAC,EAC9C,SAAS,cAAc,KACrB,OAAO,CAAC,IAAI,CA0Dd,CAAA;AAED,eAAO,MAAM,wBAAwB,GACpC,iBAAiB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACxC,SAAS,UAAU,CAAC,OAAO,kBAAkB,CAAC,EAC9C,SAAS,cAAc,KACrB,OAAO,CAAC,IAAI,CAqCd,CAAA;AAED,eAAO,MAAM,sBAAsB,GAClC,iBAAiB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACxC,SAAS,UAAU,CAAC,OAAO,kBAAkB,CAAC,EAC9C,SAAS,cAAc,KACrB,OAAO,CAAC,IAAI,CAWd,CAAA;AAED,eAAO,MAAM,kBAAkB,GAC9B,iBAAiB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACxC,SAAS,UAAU,CAAC,OAAO,kBAAkB,CAAC,EAC9C,SAAS,cAAc,KACrB,OAAO,CAAC,IAAI,CAqBd,CAAA;AAED,eAAO,MAAM,qBAAqB,GACjC,iBAAiB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACxC,SAAS,cAAc,KACrB,OAAO,CAAC,IAAI,CAad,CAAA;AAED,eAAO,MAAM,cAAc,GAC1B,OAAO,gBAAgB,EACvB,SAAS,UAAU,CAAC,OAAO,kBAAkB,CAAC,EAC9C,SAAS,cAAc,KACrB,OAAO,CAAC,IAAI,CAkCd,CAAA"}
@@ -1,12 +1,13 @@
1
1
  import { mapAssistantLlmResponseInput, mapAssistantTurnCompleteInput, mapCommandExecutedInput, mapCompactionInput, mapCompactionPartInput, mapErrorInput, mapRetryPartInput, mapSessionEndInput, mapSessionStartInput, mapStepFinishInput, mapUserMessageInput } from './map-opencode-event.js';
2
2
  import { readAssistantMessage, readCompactionPart, readEventProperties, readRetryPart, readSessionIdFromProperties, readSessionInfo, readStepFinishPart, readUserMessage } from './opencode-readers.js';
3
- import { sumTokenUsage } from './token-usage.js';
3
+ import { buildTokenMetrics } from './token-usage.js';
4
4
  const isRecord = (value) => typeof value === 'object' && value !== null;
5
- export const handleSessionCreated = (eventProperties, publish) => {
5
+ export const handleSessionCreated = (eventProperties, tracker, publish) => {
6
6
  const sessionInfo = readSessionInfo(eventProperties.info);
7
7
  if (!sessionInfo) {
8
8
  return Promise.resolve();
9
9
  }
10
+ tracker.recordSessionStart(sessionInfo.id);
10
11
  return publish([
11
12
  {
12
13
  eventType: 'session_start',
@@ -22,7 +23,7 @@ export const handleSessionIdle = (eventProperties, tracker, publish) => {
22
23
  return publish([
23
24
  {
24
25
  eventType: 'session_end',
25
- ...mapSessionEndInput(sessionId, tracker.getSessionStats(sessionId))
26
+ ...mapSessionEndInput(sessionId, tracker.getSessionStats(sessionId), tracker.getSessionTiming(sessionId))
26
27
  }
27
28
  ]);
28
29
  };
@@ -32,17 +33,32 @@ export const handleMessageUpdated = (eventProperties, tracker, publish) => {
32
33
  if (assistantMessage.time.completed === undefined || tracker.hasProcessedMessage(assistantMessage.id)) {
33
34
  return Promise.resolve();
34
35
  }
36
+ tracker.recordSessionStart(assistantMessage.sessionID, assistantMessage.time.created);
35
37
  tracker.markMessageProcessed(assistantMessage.id);
36
- const eventsToPublish = [mapAssistantTurnCompleteInput(assistantMessage), mapAssistantLlmResponseInput(assistantMessage)].filter((eventInput) => eventInput !== undefined);
38
+ const durationMs = Math.max(0, assistantMessage.time.completed - assistantMessage.time.created);
39
+ const tokenMetrics = buildTokenMetrics(assistantMessage.tokens, durationMs);
40
+ const sessionStats = tracker.getSessionStats(assistantMessage.sessionID);
41
+ const compactionDelta = tracker.recordTurnTokens(assistantMessage.sessionID, {
42
+ contextTokens: tokenMetrics.inputTokens,
43
+ billedInputTokens: tokenMetrics.inputTokens,
44
+ outputTokens: tokenMetrics.outputTokens,
45
+ totalTokens: tokenMetrics.totalTokens,
46
+ cacheReadTokens: tokenMetrics.cacheReadTokens,
47
+ cacheWriteTokens: tokenMetrics.cacheWriteTokens,
48
+ reasoningTokens: tokenMetrics.reasoningTokens
49
+ });
50
+ tracker.recordSessionStats(assistantMessage.sessionID, {
51
+ aiGenerationMs: durationMs,
52
+ primaryProviderId: assistantMessage.providerID
53
+ });
54
+ const turnCompleteInput = mapAssistantTurnCompleteInput(assistantMessage, {
55
+ providerSource: sessionStats.primaryProviderSource || undefined,
56
+ compactionDelta
57
+ });
58
+ const eventsToPublish = [turnCompleteInput, mapAssistantLlmResponseInput(assistantMessage)].filter((eventInput) => eventInput !== undefined);
37
59
  if (!eventsToPublish.length) {
38
60
  return Promise.resolve();
39
61
  }
40
- tracker.recordSessionStats(assistantMessage.sessionID, {
41
- inputTokens: assistantMessage.tokens.input,
42
- outputTokens: assistantMessage.tokens.output,
43
- totalTokens: sumTokenUsage(assistantMessage.tokens),
44
- turnCount: 1
45
- });
46
62
  return publish(eventsToPublish);
47
63
  }
48
64
  const userMessage = readUserMessage(eventProperties.info);
@@ -50,6 +66,7 @@ export const handleMessageUpdated = (eventProperties, tracker, publish) => {
50
66
  if (tracker.hasProcessedUserMessage(userMessage.id)) {
51
67
  return Promise.resolve();
52
68
  }
69
+ tracker.recordSessionStart(userMessage.sessionID);
53
70
  tracker.markUserMessageProcessed(userMessage.id);
54
71
  return publish([mapUserMessageInput(userMessage)]);
55
72
  }
@@ -71,6 +88,7 @@ export const handleMessagePartUpdated = (eventProperties, tracker, publish) => {
71
88
  return Promise.resolve();
72
89
  }
73
90
  tracker.markPartProcessed(retryPart.id);
91
+ tracker.recordSessionStats(retryPart.sessionID, { retryCount: 1 });
74
92
  return publish([mapRetryPartInput(retryPart)]);
75
93
  }
76
94
  if (compactionPart) {
@@ -78,18 +96,24 @@ export const handleMessagePartUpdated = (eventProperties, tracker, publish) => {
78
96
  return Promise.resolve();
79
97
  }
80
98
  tracker.markPartProcessed(compactionPart.id);
81
- return publish([mapCompactionPartInput(compactionPart)]);
99
+ const contextBeforeTokens = tracker.getSessionStats(compactionPart.sessionID).latestContextTokens;
100
+ tracker.recordCompactionPending(compactionPart.sessionID, contextBeforeTokens);
101
+ tracker.recordSessionStats(compactionPart.sessionID, { compactionCount: 1 });
102
+ return publish([mapCompactionPartInput(compactionPart, contextBeforeTokens)]);
82
103
  }
83
104
  return Promise.resolve();
84
105
  };
85
- export const handleSessionCompacted = (eventProperties, publish) => {
106
+ export const handleSessionCompacted = (eventProperties, tracker, publish) => {
86
107
  const sessionId = readSessionIdFromProperties(eventProperties);
87
108
  if (!sessionId) {
88
109
  return Promise.resolve();
89
110
  }
90
- return publish([mapCompactionInput(sessionId)]);
111
+ tracker.recordSessionStats(sessionId, { compactionCount: 1 });
112
+ const contextBeforeTokens = tracker.getSessionStats(sessionId).latestContextTokens;
113
+ tracker.recordCompactionPending(sessionId, contextBeforeTokens);
114
+ return publish([mapCompactionInput(sessionId, contextBeforeTokens)]);
91
115
  };
92
- export const handleSessionError = (eventProperties, publish) => {
116
+ export const handleSessionError = (eventProperties, tracker, publish) => {
93
117
  const sessionId = readSessionIdFromProperties(eventProperties);
94
118
  const errorRecord = isRecord(eventProperties.error) ? eventProperties.error : undefined;
95
119
  const errorName = errorRecord && typeof errorRecord.name === 'string' ? errorRecord.name : 'unknown';
@@ -98,10 +122,11 @@ export const handleSessionError = (eventProperties, publish) => {
98
122
  if (!sessionId) {
99
123
  return Promise.resolve();
100
124
  }
125
+ tracker.recordSessionStats(sessionId, { errorCount: 1 });
101
126
  return publish([
102
127
  mapErrorInput(sessionId, {
103
128
  error_name: errorName,
104
- error_message: errorMessage,
129
+ error_message_length: errorMessage.length,
105
130
  error_scope: 'session'
106
131
  })
107
132
  ]);
@@ -126,7 +151,7 @@ export const handleBusEvent = (event, tracker, publish) => {
126
151
  }
127
152
  switch (event.type) {
128
153
  case 'session.created':
129
- return handleSessionCreated(eventProperties, publish);
154
+ return handleSessionCreated(eventProperties, tracker, publish);
130
155
  case 'session.status': {
131
156
  const statusRecord = isRecord(eventProperties.status) ? eventProperties.status : undefined;
132
157
  if (statusRecord?.type !== 'idle') {
@@ -141,9 +166,9 @@ export const handleBusEvent = (event, tracker, publish) => {
141
166
  case 'message.part.updated':
142
167
  return handleMessagePartUpdated(eventProperties, tracker, publish);
143
168
  case 'session.compacted':
144
- return handleSessionCompacted(eventProperties, publish);
169
+ return handleSessionCompacted(eventProperties, tracker, publish);
145
170
  case 'session.error':
146
- return handleSessionError(eventProperties, publish);
171
+ return handleSessionError(eventProperties, tracker, publish);
147
172
  case 'command.executed':
148
173
  return handleCommandExecuted(eventProperties, publish);
149
174
  default:
@@ -1,11 +1,45 @@
1
+ export type CompactionDelta = {
2
+ contextBeforeTokens: number;
3
+ contextAfterTokens: number;
4
+ tokensSaved: number;
5
+ };
1
6
  export type SessionStats = {
2
- inputTokens: number;
7
+ billedInputTokens: number;
3
8
  outputTokens: number;
4
9
  totalTokens: number;
5
10
  turnCount: number;
6
11
  toolCallCount: number;
7
12
  requestCount: number;
8
13
  stepCount: number;
14
+ compactionCount: number;
15
+ retryCount: number;
16
+ errorCount: number;
17
+ cacheReadTokens: number;
18
+ cacheWriteTokens: number;
19
+ reasoningTokens: number;
20
+ peakContextTokens: number;
21
+ latestContextTokens: number;
22
+ contextAtStartTokens: number;
23
+ compactionTokensSaved: number;
24
+ totalToolOutputChars: number;
25
+ primaryProviderId: string;
26
+ primaryProviderSource: string;
27
+ startedAtMs?: number;
28
+ aiGenerationMs: number;
29
+ };
30
+ export type SessionTiming = {
31
+ sessionWallMs: number;
32
+ aiGenerationMs: number;
33
+ userWaitMs: number;
34
+ };
35
+ export type TurnTokenSnapshot = {
36
+ contextTokens: number;
37
+ cacheReadTokens: number;
38
+ cacheWriteTokens: number;
39
+ reasoningTokens: number;
40
+ billedInputTokens: number;
41
+ outputTokens: number;
42
+ totalTokens: number;
9
43
  };
10
44
  export type EventTracker = {
11
45
  hasProcessedMessage: (messageId: string) => boolean;
@@ -14,8 +48,14 @@ export type EventTracker = {
14
48
  markUserMessageProcessed: (messageId: string) => void;
15
49
  hasProcessedPart: (partId: string) => boolean;
16
50
  markPartProcessed: (partId: string) => void;
51
+ recordSessionStart: (sessionId: string, startedAtMs?: number) => void;
17
52
  recordSessionStats: (sessionId: string, delta: Partial<SessionStats>) => void;
53
+ recordProviderConnection: (sessionId: string, providerId: string, providerSource: string) => void;
54
+ recordCompactionPending: (sessionId: string, contextBeforeTokens: number) => void;
55
+ recordToolOutputChars: (sessionId: string, outputChars: number) => void;
56
+ recordTurnTokens: (sessionId: string, turnTokens: TurnTokenSnapshot) => CompactionDelta | undefined;
18
57
  getSessionStats: (sessionId: string) => SessionStats;
58
+ getSessionTiming: (sessionId: string, endedAtMs?: number) => SessionTiming;
19
59
  };
20
60
  export declare const createEventTracker: () => EventTracker;
21
61
  //# sourceMappingURL=event-tracker.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"event-tracker.d.ts","sourceRoot":"","sources":["../src/event-tracker.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,YAAY,GAAG;IAC1B,WAAW,EAAE,MAAM,CAAA;IACnB,YAAY,EAAE,MAAM,CAAA;IACpB,WAAW,EAAE,MAAM,CAAA;IACnB,SAAS,EAAE,MAAM,CAAA;IACjB,aAAa,EAAE,MAAM,CAAA;IACrB,YAAY,EAAE,MAAM,CAAA;IACpB,SAAS,EAAE,MAAM,CAAA;CACjB,CAAA;AAYD,MAAM,MAAM,YAAY,GAAG;IAC1B,mBAAmB,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,OAAO,CAAA;IACnD,oBAAoB,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAA;IACjD,uBAAuB,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,OAAO,CAAA;IACvD,wBAAwB,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAA;IACrD,gBAAgB,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAA;IAC7C,iBAAiB,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAA;IAC3C,kBAAkB,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,CAAC,YAAY,CAAC,KAAK,IAAI,CAAA;IAC7E,eAAe,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,YAAY,CAAA;CACpD,CAAA;AAED,eAAO,MAAM,kBAAkB,QAAO,YAiCrC,CAAA"}
1
+ {"version":3,"file":"event-tracker.d.ts","sourceRoot":"","sources":["../src/event-tracker.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,eAAe,GAAG;IAC7B,mBAAmB,EAAE,MAAM,CAAA;IAC3B,kBAAkB,EAAE,MAAM,CAAA;IAC1B,WAAW,EAAE,MAAM,CAAA;CACnB,CAAA;AAED,MAAM,MAAM,YAAY,GAAG;IAC1B,iBAAiB,EAAE,MAAM,CAAA;IACzB,YAAY,EAAE,MAAM,CAAA;IACpB,WAAW,EAAE,MAAM,CAAA;IACnB,SAAS,EAAE,MAAM,CAAA;IACjB,aAAa,EAAE,MAAM,CAAA;IACrB,YAAY,EAAE,MAAM,CAAA;IACpB,SAAS,EAAE,MAAM,CAAA;IACjB,eAAe,EAAE,MAAM,CAAA;IACvB,UAAU,EAAE,MAAM,CAAA;IAClB,UAAU,EAAE,MAAM,CAAA;IAClB,eAAe,EAAE,MAAM,CAAA;IACvB,gBAAgB,EAAE,MAAM,CAAA;IACxB,eAAe,EAAE,MAAM,CAAA;IACvB,iBAAiB,EAAE,MAAM,CAAA;IACzB,mBAAmB,EAAE,MAAM,CAAA;IAC3B,oBAAoB,EAAE,MAAM,CAAA;IAC5B,qBAAqB,EAAE,MAAM,CAAA;IAC7B,oBAAoB,EAAE,MAAM,CAAA;IAC5B,iBAAiB,EAAE,MAAM,CAAA;IACzB,qBAAqB,EAAE,MAAM,CAAA;IAC7B,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,cAAc,EAAE,MAAM,CAAA;CACtB,CAAA;AAkCD,MAAM,MAAM,aAAa,GAAG;IAC3B,aAAa,EAAE,MAAM,CAAA;IACrB,cAAc,EAAE,MAAM,CAAA;IACtB,UAAU,EAAE,MAAM,CAAA;CAClB,CAAA;AAED,MAAM,MAAM,iBAAiB,GAAG;IAC/B,aAAa,EAAE,MAAM,CAAA;IACrB,eAAe,EAAE,MAAM,CAAA;IACvB,gBAAgB,EAAE,MAAM,CAAA;IACxB,eAAe,EAAE,MAAM,CAAA;IACvB,iBAAiB,EAAE,MAAM,CAAA;IACzB,YAAY,EAAE,MAAM,CAAA;IACpB,WAAW,EAAE,MAAM,CAAA;CACnB,CAAA;AAED,MAAM,MAAM,YAAY,GAAG;IAC1B,mBAAmB,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,OAAO,CAAA;IACnD,oBAAoB,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAA;IACjD,uBAAuB,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,OAAO,CAAA;IACvD,wBAAwB,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAA;IACrD,gBAAgB,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAA;IAC7C,iBAAiB,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAA;IAC3C,kBAAkB,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,KAAK,IAAI,CAAA;IACrE,kBAAkB,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,CAAC,YAAY,CAAC,KAAK,IAAI,CAAA;IAC7E,wBAAwB,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,KAAK,IAAI,CAAA;IACjG,uBAAuB,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,mBAAmB,EAAE,MAAM,KAAK,IAAI,CAAA;IACjF,qBAAqB,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,KAAK,IAAI,CAAA;IACvE,gBAAgB,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,iBAAiB,KAAK,eAAe,GAAG,SAAS,CAAA;IACnG,eAAe,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,YAAY,CAAA;IACpD,gBAAgB,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,KAAK,aAAa,CAAA;CAC1E,CAAA;AAgCD,eAAO,MAAM,kBAAkB,QAAO,YA6GrC,CAAA"}
@@ -1,17 +1,63 @@
1
1
  const emptySessionStats = () => ({
2
- inputTokens: 0,
2
+ billedInputTokens: 0,
3
3
  outputTokens: 0,
4
4
  totalTokens: 0,
5
5
  turnCount: 0,
6
6
  toolCallCount: 0,
7
7
  requestCount: 0,
8
- stepCount: 0
8
+ stepCount: 0,
9
+ compactionCount: 0,
10
+ retryCount: 0,
11
+ errorCount: 0,
12
+ cacheReadTokens: 0,
13
+ cacheWriteTokens: 0,
14
+ reasoningTokens: 0,
15
+ peakContextTokens: 0,
16
+ latestContextTokens: 0,
17
+ contextAtStartTokens: 0,
18
+ compactionTokensSaved: 0,
19
+ totalToolOutputChars: 0,
20
+ primaryProviderId: '',
21
+ primaryProviderSource: '',
22
+ aiGenerationMs: 0
23
+ });
24
+ const INVALID_ID_VALUES = new Set(['', 'unknown', 'undefined', 'null']);
25
+ const cleanIdentity = (value) => {
26
+ const cleanedValue = value.trim();
27
+ return INVALID_ID_VALUES.has(cleanedValue.toLowerCase()) ? '' : cleanedValue;
28
+ };
29
+ const mergeSessionStats = (currentStats, delta) => ({
30
+ billedInputTokens: currentStats.billedInputTokens + (delta.billedInputTokens ?? 0),
31
+ outputTokens: currentStats.outputTokens + (delta.outputTokens ?? 0),
32
+ totalTokens: currentStats.totalTokens + (delta.totalTokens ?? 0),
33
+ turnCount: currentStats.turnCount + (delta.turnCount ?? 0),
34
+ toolCallCount: currentStats.toolCallCount + (delta.toolCallCount ?? 0),
35
+ requestCount: currentStats.requestCount + (delta.requestCount ?? 0),
36
+ stepCount: currentStats.stepCount + (delta.stepCount ?? 0),
37
+ compactionCount: currentStats.compactionCount + (delta.compactionCount ?? 0),
38
+ retryCount: currentStats.retryCount + (delta.retryCount ?? 0),
39
+ errorCount: currentStats.errorCount + (delta.errorCount ?? 0),
40
+ cacheReadTokens: currentStats.cacheReadTokens + (delta.cacheReadTokens ?? 0),
41
+ cacheWriteTokens: currentStats.cacheWriteTokens + (delta.cacheWriteTokens ?? 0),
42
+ reasoningTokens: currentStats.reasoningTokens + (delta.reasoningTokens ?? 0),
43
+ peakContextTokens: Math.max(currentStats.peakContextTokens, delta.peakContextTokens ?? 0),
44
+ latestContextTokens: delta.latestContextTokens ?? currentStats.latestContextTokens,
45
+ contextAtStartTokens: currentStats.contextAtStartTokens > 0
46
+ ? currentStats.contextAtStartTokens
47
+ : (delta.contextAtStartTokens ?? currentStats.contextAtStartTokens),
48
+ compactionTokensSaved: currentStats.compactionTokensSaved + (delta.compactionTokensSaved ?? 0),
49
+ totalToolOutputChars: currentStats.totalToolOutputChars + (delta.totalToolOutputChars ?? 0),
50
+ primaryProviderId: delta.primaryProviderId !== undefined ? cleanIdentity(delta.primaryProviderId) : currentStats.primaryProviderId,
51
+ primaryProviderSource: delta.primaryProviderSource !== undefined ? cleanIdentity(delta.primaryProviderSource) : currentStats.primaryProviderSource,
52
+ startedAtMs: currentStats.startedAtMs,
53
+ aiGenerationMs: currentStats.aiGenerationMs + (delta.aiGenerationMs ?? 0)
9
54
  });
10
55
  export const createEventTracker = () => {
11
56
  const processedMessageIds = new Set();
12
57
  const processedUserMessageIds = new Set();
13
58
  const processedPartIds = new Set();
14
59
  const sessionStatsById = new Map();
60
+ const pendingCompactionContextBySession = new Map();
15
61
  return {
16
62
  hasProcessedMessage: (messageId) => processedMessageIds.has(messageId),
17
63
  markMessageProcessed: (messageId) => {
@@ -25,18 +71,80 @@ export const createEventTracker = () => {
25
71
  markPartProcessed: (partId) => {
26
72
  processedPartIds.add(partId);
27
73
  },
74
+ recordSessionStart: (sessionId, startedAtMs = Date.now()) => {
75
+ const currentStats = sessionStatsById.get(sessionId) ?? emptySessionStats();
76
+ if (currentStats.startedAtMs !== undefined) {
77
+ return;
78
+ }
79
+ sessionStatsById.set(sessionId, {
80
+ ...currentStats,
81
+ startedAtMs
82
+ });
83
+ },
28
84
  recordSessionStats: (sessionId, delta) => {
85
+ const currentStats = sessionStatsById.get(sessionId) ?? emptySessionStats();
86
+ sessionStatsById.set(sessionId, mergeSessionStats(currentStats, delta));
87
+ },
88
+ recordProviderConnection: (sessionId, providerId, providerSource) => {
29
89
  const currentStats = sessionStatsById.get(sessionId) ?? emptySessionStats();
30
90
  sessionStatsById.set(sessionId, {
31
- inputTokens: currentStats.inputTokens + (delta.inputTokens ?? 0),
32
- outputTokens: currentStats.outputTokens + (delta.outputTokens ?? 0),
33
- totalTokens: currentStats.totalTokens + (delta.totalTokens ?? 0),
34
- turnCount: currentStats.turnCount + (delta.turnCount ?? 0),
35
- toolCallCount: currentStats.toolCallCount + (delta.toolCallCount ?? 0),
36
- requestCount: currentStats.requestCount + (delta.requestCount ?? 0),
37
- stepCount: currentStats.stepCount + (delta.stepCount ?? 0)
91
+ ...currentStats,
92
+ primaryProviderId: cleanIdentity(providerId),
93
+ primaryProviderSource: cleanIdentity(providerSource)
38
94
  });
39
95
  },
40
- getSessionStats: (sessionId) => sessionStatsById.get(sessionId) ?? emptySessionStats()
96
+ recordCompactionPending: (sessionId, contextBeforeTokens) => {
97
+ if (contextBeforeTokens > 0) {
98
+ pendingCompactionContextBySession.set(sessionId, contextBeforeTokens);
99
+ }
100
+ },
101
+ recordToolOutputChars: (sessionId, outputChars) => {
102
+ const currentStats = sessionStatsById.get(sessionId) ?? emptySessionStats();
103
+ sessionStatsById.set(sessionId, {
104
+ ...currentStats,
105
+ totalToolOutputChars: currentStats.totalToolOutputChars + Math.max(0, outputChars)
106
+ });
107
+ },
108
+ recordTurnTokens: (sessionId, turnTokens) => {
109
+ const currentStats = sessionStatsById.get(sessionId) ?? emptySessionStats();
110
+ const isFirstTurn = currentStats.turnCount === 0;
111
+ const contextBeforeCompaction = pendingCompactionContextBySession.get(sessionId);
112
+ const compactionDelta = contextBeforeCompaction !== undefined && contextBeforeCompaction > turnTokens.contextTokens
113
+ ? {
114
+ contextBeforeTokens: contextBeforeCompaction,
115
+ contextAfterTokens: turnTokens.contextTokens,
116
+ tokensSaved: contextBeforeCompaction - turnTokens.contextTokens
117
+ }
118
+ : undefined;
119
+ if (compactionDelta) {
120
+ pendingCompactionContextBySession.delete(sessionId);
121
+ }
122
+ sessionStatsById.set(sessionId, mergeSessionStats(currentStats, {
123
+ billedInputTokens: turnTokens.billedInputTokens,
124
+ outputTokens: turnTokens.outputTokens,
125
+ totalTokens: turnTokens.totalTokens,
126
+ cacheReadTokens: turnTokens.cacheReadTokens,
127
+ cacheWriteTokens: turnTokens.cacheWriteTokens,
128
+ reasoningTokens: turnTokens.reasoningTokens,
129
+ turnCount: 1,
130
+ peakContextTokens: turnTokens.contextTokens,
131
+ latestContextTokens: turnTokens.contextTokens,
132
+ contextAtStartTokens: isFirstTurn ? turnTokens.contextTokens : currentStats.contextAtStartTokens,
133
+ ...(compactionDelta ? { compactionTokensSaved: compactionDelta.tokensSaved } : {})
134
+ }));
135
+ return compactionDelta;
136
+ },
137
+ getSessionStats: (sessionId) => sessionStatsById.get(sessionId) ?? emptySessionStats(),
138
+ getSessionTiming: (sessionId, endedAtMs = Date.now()) => {
139
+ const currentStats = sessionStatsById.get(sessionId) ?? emptySessionStats();
140
+ const sessionWallMs = currentStats.startedAtMs !== undefined ? Math.max(0, endedAtMs - currentStats.startedAtMs) : 0;
141
+ const aiGenerationMs = currentStats.aiGenerationMs;
142
+ const userWaitMs = Math.max(0, sessionWallMs - aiGenerationMs);
143
+ return {
144
+ sessionWallMs,
145
+ aiGenerationMs,
146
+ userWaitMs
147
+ };
148
+ }
41
149
  };
42
150
  };
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAA;AAgBjD,eAAO,MAAM,qBAAqB,EAAE,MA+DnC,CAAA;AAED,eAAe,qBAAqB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAA;AAgBjD,eAAO,MAAM,qBAAqB,EAAE,MAuEnC,CAAA;AAED,eAAe,qBAAqB,CAAA"}
package/dist/index.js CHANGED
@@ -25,8 +25,10 @@ export const timeflyOpenCodePlugin = ({ client }) => {
25
25
  .then(() => Promise.resolve({
26
26
  event: (input) => runHookSafely(() => handleBusEvent(input.event, tracker, publisher.publish)),
27
27
  'chat.params': (input, output) => runHookSafely(() => {
28
+ tracker.recordSessionStart(input.sessionID);
28
29
  tracker.recordSessionStats(input.sessionID, { requestCount: 1 });
29
30
  const resolvedParams = resolveChatParams(input, output);
31
+ tracker.recordProviderConnection(resolvedParams.sessionID, resolvedParams.providerId, resolvedParams.providerSource);
30
32
  return publisher.publish([
31
33
  mapLlmRequestInput({
32
34
  sessionID: resolvedParams.sessionID,
@@ -44,15 +46,18 @@ export const timeflyOpenCodePlugin = ({ client }) => {
44
46
  tracker.recordSessionStats(input.sessionID, { toolCallCount: 1 });
45
47
  return publisher.publish([mapToolCallInput(input)]);
46
48
  }),
47
- 'tool.execute.after': (input, output) => runHookSafely(() => publisher.publish([
48
- mapToolResultInput({
49
- sessionID: input.sessionID,
50
- tool: input.tool,
51
- callID: input.callID,
52
- hasOutput: Boolean(output.output),
53
- outputLength: output.output.length
54
- })
55
- ])),
49
+ 'tool.execute.after': (input, output) => runHookSafely(() => {
50
+ tracker.recordToolOutputChars(input.sessionID, output.output.length);
51
+ return publisher.publish([
52
+ mapToolResultInput({
53
+ sessionID: input.sessionID,
54
+ tool: input.tool,
55
+ callID: input.callID,
56
+ hasOutput: Boolean(output.output),
57
+ outputLength: output.output.length
58
+ })
59
+ ]);
60
+ }),
56
61
  'experimental.session.compacting': (input) => runHookSafely(() => publisher.publish([mapCompactionInput(input.sessionID)]))
57
62
  }));
58
63
  };
@@ -1 +1 @@
1
- {"version":3,"file":"install.d.ts","sourceRoot":"","sources":["../src/install.ts"],"names":[],"mappings":";AA8HA,eAAO,MAAM,UAAU,GAAI,eAAe,MAAM,EAAE,KAAG,OAAO,CAAC,IAAI,CAGhE,CAAA"}
1
+ {"version":3,"file":"install.d.ts","sourceRoot":"","sources":["../src/install.ts"],"names":[],"mappings":";AAuJA,eAAO,MAAM,UAAU,GAAI,eAAe,MAAM,EAAE,KAAG,OAAO,CAAC,IAAI,CAGhE,CAAA"}
package/dist/install.js CHANGED
@@ -1,9 +1,10 @@
1
1
  #!/usr/bin/env bun
2
- import { mkdir, readFile, writeFile } from 'node:fs/promises';
2
+ import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
3
3
  import path from 'node:path';
4
4
  import { fileURLToPath } from 'node:url';
5
5
  import { resolveDefaultAuthFilePath } from '@timefly/ai-sdk';
6
- const PLUGIN_PACKAGE_NAME = '@timefly/opencode-plugin';
6
+ const PLUGIN_PACKAGE_LATEST = '@timefly/opencode-plugin@latest';
7
+ const TIMEFLY_PLUGIN_PATTERN = /^@timefly\/opencode-plugin(?:@|$)/;
7
8
  const PRICING_URL = 'https://timefly.dev/pricing';
8
9
  const currentDirectory = path.dirname(fileURLToPath(import.meta.url));
9
10
  const packageRoot = path.resolve(currentDirectory, '..');
@@ -36,6 +37,22 @@ const resolveConfigDestination = (options) => {
36
37
  }
37
38
  return path.join(homeDirectory, '.config', 'opencode', 'opencode.json');
38
39
  };
40
+ const resolveOpenCodePluginCachePath = () => {
41
+ const homeDirectory = process.env.HOME ?? process.env.USERPROFILE;
42
+ if (!homeDirectory) {
43
+ return undefined;
44
+ }
45
+ return path.join(homeDirectory, '.cache', 'opencode', 'packages', '@timefly');
46
+ };
47
+ const clearOpenCodePluginCache = () => {
48
+ const cachePath = resolveOpenCodePluginCachePath();
49
+ if (!cachePath) {
50
+ return Promise.resolve();
51
+ }
52
+ return rm(cachePath, { recursive: true, force: true })
53
+ .then(() => undefined)
54
+ .catch(() => undefined);
55
+ };
39
56
  const readConfigFile = (configPath) => readFile(configPath, 'utf8')
40
57
  .then((configContent) => JSON.parse(configContent))
41
58
  .catch((error) => {
@@ -44,16 +61,17 @@ const readConfigFile = (configPath) => readFile(configPath, 'utf8')
44
61
  }
45
62
  throw error;
46
63
  });
47
- const resolvePluginEntry = (options) => options.useLocalPath ? localPluginPath.replaceAll('\\', '/') : PLUGIN_PACKAGE_NAME;
48
- const pluginEntryExists = (pluginList, pluginEntry) => (pluginList ?? []).some((entry) => (Array.isArray(entry) ? entry[0] : entry) === pluginEntry);
64
+ const resolvePluginEntry = (options) => options.useLocalPath ? localPluginPath.replaceAll('\\', '/') : PLUGIN_PACKAGE_LATEST;
65
+ const pluginEntryName = (entry) => Array.isArray(entry) ? entry[0] : entry;
66
+ const pluginEntryExists = (pluginList, pluginEntry) => (pluginList ?? []).some((entry) => pluginEntryName(entry) === pluginEntry);
49
67
  const mergePluginConfig = (config, pluginEntry) => {
50
- if (pluginEntryExists(config.plugin, pluginEntry)) {
51
- return config;
52
- }
68
+ const pluginListWithoutStaleTimefly = (config.plugin ?? []).filter((entry) => !TIMEFLY_PLUGIN_PATTERN.test(pluginEntryName(entry)));
53
69
  return {
54
70
  ...config,
55
71
  $schema: config.$schema ?? 'https://opencode.ai/config.json',
56
- plugin: [...(config.plugin ?? []), pluginEntry]
72
+ plugin: pluginEntryExists(pluginListWithoutStaleTimefly, pluginEntry)
73
+ ? pluginListWithoutStaleTimefly
74
+ : [...pluginListWithoutStaleTimefly, pluginEntry]
57
75
  };
58
76
  };
59
77
  const printPostInstallInstructions = (configPath) => {
@@ -69,9 +87,7 @@ const printPostInstallInstructions = (configPath) => {
69
87
  console.log(` Upgrade: ${PRICING_URL}`);
70
88
  console.log(' 3. Restart OpenCode.');
71
89
  console.log('');
72
- console.log('Optional env overrides:');
73
- console.log(' TIMEFLY_API_BASE_URL=https://api.timefly.dev');
74
- console.log(' TIMEFLY_ACCESS_TOKEN=... (manual token, expires in ~15 min without refresh file)');
90
+ console.log('To update later, run setup-opencode again — it refreshes @latest and clears the plugin cache.');
75
91
  console.log('');
76
92
  console.log(`Auth file: ${authFilePath}`);
77
93
  };
@@ -79,7 +95,8 @@ const installPlugin = (options) => {
79
95
  const configPath = resolveConfigDestination(options);
80
96
  const configDirectory = path.dirname(configPath);
81
97
  const pluginEntry = resolvePluginEntry(options);
82
- return mkdir(configDirectory, { recursive: true })
98
+ return clearOpenCodePluginCache()
99
+ .then(() => mkdir(configDirectory, { recursive: true }))
83
100
  .then(() => readConfigFile(configPath))
84
101
  .then((config) => mergePluginConfig(config, pluginEntry))
85
102
  .then((mergedConfig) => writeFile(configPath, `${JSON.stringify(mergedConfig, null, 2)}\n`, 'utf8'))
@@ -1,19 +1,12 @@
1
1
  import type { CreateAiUsageEventInput } from '@timefly/ai-sdk';
2
2
  import type { OpenCodeAssistantMessage, OpenCodeCompactionPart, OpenCodeRetryPart, OpenCodeSessionInfo, OpenCodeStepFinishPart, OpenCodeUserMessage } from './opencode-readers.js';
3
+ import type { SessionStats, SessionTiming, CompactionDelta } from './event-tracker.js';
3
4
  export type { OpenCodeAssistantMessage, OpenCodeCompactionPart, OpenCodeRetryPart, OpenCodeSessionInfo, OpenCodeStepFinishPart, OpenCodeUserMessage } from './opencode-readers.js';
4
5
  export type { OpenCodeTokenUsage } from './token-usage.js';
5
6
  export declare const readSessionIdOverride: (sessionId: string) => string;
6
7
  export declare const buildModelId: (providerId: string, modelId: string) => string;
7
8
  export declare const mapSessionStartInput: (sessionInfo: OpenCodeSessionInfo) => Pick<CreateAiUsageEventInput, "sessionId" | "metadata">;
8
- export declare const mapSessionEndInput: (sessionId: string, sessionStats: {
9
- inputTokens: number;
10
- outputTokens: number;
11
- totalTokens: number;
12
- turnCount: number;
13
- toolCallCount: number;
14
- requestCount: number;
15
- stepCount: number;
16
- }) => Pick<CreateAiUsageEventInput, "sessionId" | "metadata">;
9
+ export declare const mapSessionEndInput: (sessionId: string, sessionStats: SessionStats, sessionTiming: SessionTiming) => Pick<CreateAiUsageEventInput, "sessionId" | "metadata" | "durationMs">;
17
10
  export declare const mapLlmRequestInput: (input: {
18
11
  sessionID: string;
19
12
  agent: string;
@@ -24,11 +17,14 @@ export declare const mapLlmRequestInput: (input: {
24
17
  topP: number;
25
18
  maxOutputTokens?: number;
26
19
  }) => CreateAiUsageEventInput;
27
- export declare const mapAssistantTurnCompleteInput: (message: OpenCodeAssistantMessage) => CreateAiUsageEventInput | undefined;
20
+ export declare const mapAssistantTurnCompleteInput: (message: OpenCodeAssistantMessage, options?: {
21
+ providerSource?: string;
22
+ compactionDelta?: CompactionDelta;
23
+ }) => CreateAiUsageEventInput | undefined;
28
24
  export declare const mapAssistantLlmResponseInput: (message: OpenCodeAssistantMessage) => CreateAiUsageEventInput | undefined;
29
25
  export declare const mapStepFinishInput: (part: OpenCodeStepFinishPart) => CreateAiUsageEventInput;
30
26
  export declare const mapUserMessageInput: (message: OpenCodeUserMessage) => CreateAiUsageEventInput;
31
- export declare const mapCompactionInput: (sessionId: string, auto?: boolean) => CreateAiUsageEventInput;
27
+ export declare const mapCompactionInput: (sessionId: string, contextBeforeTokens?: number, auto?: boolean) => CreateAiUsageEventInput;
32
28
  export declare const mapErrorInput: (sessionId: string, errorMetadata: Record<string, string | number | boolean>) => CreateAiUsageEventInput;
33
29
  export declare const mapToolCallInput: (input: {
34
30
  sessionID: string;
@@ -49,5 +45,5 @@ export declare const mapCommandExecutedInput: (input: {
49
45
  messageID: string;
50
46
  }) => CreateAiUsageEventInput;
51
47
  export declare const mapRetryPartInput: (part: OpenCodeRetryPart) => CreateAiUsageEventInput;
52
- export declare const mapCompactionPartInput: (part: OpenCodeCompactionPart) => CreateAiUsageEventInput;
48
+ export declare const mapCompactionPartInput: (part: OpenCodeCompactionPart, contextBeforeTokens?: number) => CreateAiUsageEventInput;
53
49
  //# sourceMappingURL=map-opencode-event.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"map-opencode-event.d.ts","sourceRoot":"","sources":["../src/map-opencode-event.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"map-opencode-event.d.ts","sourceRoot":"","sources":["../src/map-opencode-event.ts"],"names":[],"mappings":"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"}
@@ -2,7 +2,38 @@ import { buildTokenMetadata, buildTokenMetrics } from './token-usage.js';
2
2
  export const readSessionIdOverride = (sessionId) => typeof process !== 'undefined' && process.env.TIMEFLY_OPENCODE_SESSION_ID
3
3
  ? process.env.TIMEFLY_OPENCODE_SESSION_ID
4
4
  : sessionId;
5
- export const buildModelId = (providerId, modelId) => `${providerId}/${modelId}`;
5
+ const INVALID_ID_VALUES = new Set(['', 'unknown', 'undefined', 'null']);
6
+ const cleanText = (value) => value.trim();
7
+ const cleanIdentity = (value) => {
8
+ const cleanedValue = cleanText(value);
9
+ return INVALID_ID_VALUES.has(cleanedValue.toLowerCase()) ? '' : cleanedValue;
10
+ };
11
+ const cleanNumber = (value) => Number.isFinite(value) ? value : 0;
12
+ const cleanMetadata = (metadata) => Object.fromEntries(Object.entries(metadata).filter(([, metadataValue]) => {
13
+ if (metadataValue === undefined) {
14
+ return false;
15
+ }
16
+ if (typeof metadataValue === 'number') {
17
+ return Number.isFinite(metadataValue);
18
+ }
19
+ if (typeof metadataValue === 'string') {
20
+ return cleanText(metadataValue).length > 0;
21
+ }
22
+ return true;
23
+ }));
24
+ const identityMetadata = (providerId, modelId) => {
25
+ const cleanProviderId = cleanIdentity(providerId);
26
+ const cleanModelId = cleanIdentity(modelId);
27
+ return {
28
+ ...(cleanProviderId ? { provider_id: cleanProviderId } : {}),
29
+ ...(cleanModelId ? { model_id: cleanModelId } : {})
30
+ };
31
+ };
32
+ export const buildModelId = (providerId, modelId) => {
33
+ const cleanProviderId = cleanIdentity(providerId);
34
+ const cleanModelId = cleanIdentity(modelId);
35
+ return cleanProviderId && cleanModelId ? `${cleanProviderId}/${cleanModelId}` : cleanModelId;
36
+ };
6
37
  export const mapSessionStartInput = (sessionInfo) => ({
7
38
  sessionId: readSessionIdOverride(sessionInfo.id),
8
39
  metadata: {
@@ -11,81 +42,112 @@ export const mapSessionStartInput = (sessionInfo) => ({
11
42
  directory: sessionInfo.directory
12
43
  }
13
44
  });
14
- export const mapSessionEndInput = (sessionId, sessionStats) => ({
45
+ export const mapSessionEndInput = (sessionId, sessionStats, sessionTiming) => ({
15
46
  sessionId: readSessionIdOverride(sessionId),
47
+ durationMs: sessionTiming.sessionWallMs,
16
48
  metadata: {
17
- session_input_tokens: sessionStats.inputTokens,
49
+ session_input_tokens: sessionStats.billedInputTokens,
50
+ session_billed_input_tokens: sessionStats.billedInputTokens,
18
51
  session_output_tokens: sessionStats.outputTokens,
19
52
  session_total_tokens: sessionStats.totalTokens,
20
53
  session_turn_count: sessionStats.turnCount,
21
54
  session_tool_call_count: sessionStats.toolCallCount,
22
55
  session_request_count: sessionStats.requestCount,
23
- session_step_count: sessionStats.stepCount
24
- }
25
- });
26
- export const mapLlmRequestInput = (input) => ({
27
- sessionId: readSessionIdOverride(input.sessionID),
28
- eventType: 'llm_request',
29
- modelId: buildModelId(input.providerId, input.modelId),
30
- planMode: input.agent,
31
- metadata: {
32
- provider_id: input.providerId,
33
- model_id: input.modelId,
34
- provider_source: input.providerSource,
35
- agent: input.agent,
36
- temperature: input.temperature,
37
- top_p: input.topP,
38
- ...(input.maxOutputTokens !== undefined ? { max_output_tokens: input.maxOutputTokens } : {})
56
+ session_step_count: sessionStats.stepCount,
57
+ session_compaction_count: sessionStats.compactionCount,
58
+ session_retry_count: sessionStats.retryCount,
59
+ session_error_count: sessionStats.errorCount,
60
+ session_cache_read_tokens: sessionStats.cacheReadTokens,
61
+ session_cache_write_tokens: sessionStats.cacheWriteTokens,
62
+ session_reasoning_tokens: sessionStats.reasoningTokens,
63
+ session_compaction_tokens_saved: sessionStats.compactionTokensSaved,
64
+ session_tool_output_chars: sessionStats.totalToolOutputChars,
65
+ primary_provider_id: sessionStats.primaryProviderId,
66
+ primary_provider_source: sessionStats.primaryProviderSource,
67
+ peak_context_tokens: sessionStats.peakContextTokens,
68
+ latest_context_tokens: sessionStats.latestContextTokens,
69
+ context_at_start_tokens: sessionStats.contextAtStartTokens,
70
+ session_wall_ms: sessionTiming.sessionWallMs,
71
+ ai_generation_ms: sessionTiming.aiGenerationMs,
72
+ user_wait_ms: sessionTiming.userWaitMs
39
73
  }
40
74
  });
41
- export const mapAssistantTurnCompleteInput = (message) => {
75
+ export const mapLlmRequestInput = (input) => {
76
+ const modelId = buildModelId(input.providerId, input.modelId);
77
+ return {
78
+ sessionId: readSessionIdOverride(input.sessionID),
79
+ eventType: 'llm_request',
80
+ ...(modelId ? { modelId } : {}),
81
+ planMode: input.agent,
82
+ metadata: cleanMetadata({
83
+ ...identityMetadata(input.providerId, input.modelId),
84
+ provider_source: cleanIdentity(input.providerSource),
85
+ agent: input.agent,
86
+ temperature: cleanNumber(input.temperature),
87
+ top_p: cleanNumber(input.topP),
88
+ ...(input.maxOutputTokens !== undefined ? { max_output_tokens: cleanNumber(input.maxOutputTokens) } : {})
89
+ })
90
+ };
91
+ };
92
+ export const mapAssistantTurnCompleteInput = (message, options) => {
42
93
  if (message.time.completed === undefined) {
43
94
  return undefined;
44
95
  }
45
- const durationMs = message.time.completed - message.time.created;
96
+ const durationMs = Math.max(0, message.time.completed - message.time.created);
46
97
  const tokenMetrics = buildTokenMetrics(message.tokens, durationMs);
98
+ const modelId = buildModelId(message.providerID, message.modelID);
47
99
  return {
48
100
  sessionId: readSessionIdOverride(message.sessionID),
49
101
  eventType: 'turn_complete',
50
- modelId: buildModelId(message.providerID, message.modelID),
102
+ ...(modelId ? { modelId } : {}),
51
103
  planMode: message.mode,
52
104
  inputTokens: tokenMetrics.inputTokens,
53
105
  outputTokens: tokenMetrics.outputTokens,
54
106
  totalTokens: tokenMetrics.totalTokens,
55
107
  durationMs,
56
- metadata: buildTokenMetadata(tokenMetrics, {
108
+ eventAtUtc: new Date(message.time.completed),
109
+ metadata: cleanMetadata(buildTokenMetadata(tokenMetrics, {
57
110
  message_id: message.id,
58
- provider_id: message.providerID,
59
- model_id: message.modelID,
60
- cost: message.cost,
111
+ ...identityMetadata(message.providerID, message.modelID),
112
+ ...(options?.providerSource ? { provider_source: cleanIdentity(options.providerSource) } : {}),
113
+ context_tokens: tokenMetrics.inputTokens,
114
+ cost: cleanNumber(message.cost),
61
115
  ...(message.finish ? { finish_reason: message.finish } : {}),
62
116
  has_error: Boolean(message.error),
63
- ...(message.error ? { error_name: message.error.name } : {})
64
- })
117
+ ...(message.error ? { error_name: message.error.name } : {}),
118
+ ...(options?.compactionDelta
119
+ ? {
120
+ context_before_compaction: options.compactionDelta.contextBeforeTokens,
121
+ context_after_compaction: options.compactionDelta.contextAfterTokens,
122
+ compaction_tokens_saved: options.compactionDelta.tokensSaved
123
+ }
124
+ : {})
125
+ }))
65
126
  };
66
127
  };
67
128
  export const mapAssistantLlmResponseInput = (message) => {
68
129
  if (message.time.completed === undefined) {
69
130
  return undefined;
70
131
  }
71
- const durationMs = message.time.completed - message.time.created;
132
+ const durationMs = Math.max(0, message.time.completed - message.time.created);
72
133
  const tokenMetrics = buildTokenMetrics(message.tokens, durationMs);
134
+ const modelId = buildModelId(message.providerID, message.modelID);
73
135
  return {
74
136
  sessionId: readSessionIdOverride(message.sessionID),
75
137
  eventType: 'llm_response',
76
- modelId: buildModelId(message.providerID, message.modelID),
138
+ ...(modelId ? { modelId } : {}),
77
139
  planMode: message.mode,
78
140
  inputTokens: tokenMetrics.inputTokens,
79
141
  outputTokens: tokenMetrics.outputTokens,
80
142
  totalTokens: tokenMetrics.totalTokens,
81
143
  durationMs,
82
- metadata: buildTokenMetadata(tokenMetrics, {
144
+ eventAtUtc: new Date(message.time.completed),
145
+ metadata: cleanMetadata(buildTokenMetadata(tokenMetrics, {
83
146
  message_id: message.id,
84
- provider_id: message.providerID,
85
- model_id: message.modelID,
86
- cost: message.cost,
147
+ ...identityMetadata(message.providerID, message.modelID),
148
+ cost: cleanNumber(message.cost),
87
149
  response_scope: 'message'
88
- })
150
+ }))
89
151
  };
90
152
  };
91
153
  export const mapStepFinishInput = (part) => {
@@ -96,32 +158,37 @@ export const mapStepFinishInput = (part) => {
96
158
  inputTokens: tokenMetrics.inputTokens,
97
159
  outputTokens: tokenMetrics.outputTokens,
98
160
  totalTokens: tokenMetrics.totalTokens,
99
- metadata: buildTokenMetadata(tokenMetrics, {
161
+ metadata: cleanMetadata(buildTokenMetadata(tokenMetrics, {
100
162
  message_id: part.messageID,
101
163
  part_id: part.id,
102
164
  step_reason: part.reason,
103
- cost: part.cost,
165
+ cost: cleanNumber(part.cost),
104
166
  response_scope: 'step'
167
+ }))
168
+ };
169
+ };
170
+ export const mapUserMessageInput = (message) => {
171
+ const modelId = buildModelId(message.model.providerID, message.model.modelID);
172
+ return {
173
+ sessionId: readSessionIdOverride(message.sessionID),
174
+ eventType: 'llm_request',
175
+ ...(modelId ? { modelId } : {}),
176
+ planMode: message.agent,
177
+ metadata: cleanMetadata({
178
+ message_id: message.id,
179
+ ...identityMetadata(message.model.providerID, message.model.modelID),
180
+ agent: message.agent,
181
+ request_scope: 'user_message'
105
182
  })
106
183
  };
107
184
  };
108
- export const mapUserMessageInput = (message) => ({
109
- sessionId: readSessionIdOverride(message.sessionID),
110
- eventType: 'llm_request',
111
- modelId: buildModelId(message.model.providerID, message.model.modelID),
112
- planMode: message.agent,
113
- metadata: {
114
- message_id: message.id,
115
- provider_id: message.model.providerID,
116
- model_id: message.model.modelID,
117
- agent: message.agent,
118
- request_scope: 'user_message'
119
- }
120
- });
121
- export const mapCompactionInput = (sessionId, auto) => ({
185
+ export const mapCompactionInput = (sessionId, contextBeforeTokens, auto) => ({
122
186
  sessionId: readSessionIdOverride(sessionId),
123
187
  eventType: 'compaction',
124
188
  metadata: {
189
+ ...(contextBeforeTokens !== undefined && contextBeforeTokens > 0
190
+ ? { context_before_compaction: contextBeforeTokens }
191
+ : {}),
125
192
  ...(auto !== undefined ? { auto_compaction: auto } : {})
126
193
  }
127
194
  });
@@ -154,7 +221,7 @@ export const mapCommandExecutedInput = (input) => ({
154
221
  toolName: `command:${input.name}`,
155
222
  metadata: {
156
223
  command_name: input.name,
157
- command_arguments: input.arguments,
224
+ command_arguments_length: input.arguments.length,
158
225
  message_id: input.messageID
159
226
  }
160
227
  });
@@ -168,12 +235,15 @@ export const mapRetryPartInput = (part) => ({
168
235
  error_scope: 'retry'
169
236
  }
170
237
  });
171
- export const mapCompactionPartInput = (part) => ({
238
+ export const mapCompactionPartInput = (part, contextBeforeTokens) => ({
172
239
  sessionId: readSessionIdOverride(part.sessionID),
173
240
  eventType: 'compaction',
174
241
  metadata: {
175
242
  message_id: part.messageID,
176
243
  part_id: part.id,
177
- auto_compaction: part.auto
244
+ auto_compaction: part.auto,
245
+ ...(contextBeforeTokens !== undefined && contextBeforeTokens > 0
246
+ ? { context_before_compaction: contextBeforeTokens }
247
+ : {})
178
248
  }
179
249
  });
@@ -1 +1 @@
1
- {"version":3,"file":"opencode-readers.d.ts","sourceRoot":"","sources":["../src/opencode-readers.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"opencode-readers.d.ts","sourceRoot":"","sources":["../src/opencode-readers.ts"],"names":[],"mappings":"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"}
@@ -1,23 +1,30 @@
1
1
  const isRecord = (value) => typeof value === 'object' && value !== null;
2
+ const isFiniteNumber = (value) => typeof value === 'number' && Number.isFinite(value);
3
+ const readTelemetryNumber = (value) => typeof value === 'number' ? (Number.isFinite(value) ? value : 0) : undefined;
2
4
  const readTokenUsage = (value) => {
3
5
  if (!isRecord(value)) {
4
6
  return undefined;
5
7
  }
6
8
  const cacheRecord = isRecord(value.cache) ? value.cache : undefined;
7
- if (typeof value.input !== 'number' ||
8
- typeof value.output !== 'number' ||
9
- typeof value.reasoning !== 'number' ||
10
- typeof cacheRecord?.read !== 'number' ||
11
- typeof cacheRecord?.write !== 'number') {
9
+ const inputTokens = readTelemetryNumber(value.input);
10
+ const outputTokens = readTelemetryNumber(value.output);
11
+ const reasoningTokens = readTelemetryNumber(value.reasoning);
12
+ const cacheReadTokens = readTelemetryNumber(cacheRecord?.read);
13
+ const cacheWriteTokens = readTelemetryNumber(cacheRecord?.write);
14
+ if (inputTokens === undefined ||
15
+ outputTokens === undefined ||
16
+ reasoningTokens === undefined ||
17
+ cacheReadTokens === undefined ||
18
+ cacheWriteTokens === undefined) {
12
19
  return undefined;
13
20
  }
14
21
  return {
15
- input: value.input,
16
- output: value.output,
17
- reasoning: value.reasoning,
22
+ input: inputTokens,
23
+ output: outputTokens,
24
+ reasoning: reasoningTokens,
18
25
  cache: {
19
- read: cacheRecord.read,
20
- write: cacheRecord.write
26
+ read: cacheReadTokens,
27
+ write: cacheWriteTokens
21
28
  }
22
29
  };
23
30
  };
@@ -34,13 +41,14 @@ export const readAssistantMessage = (message) => {
34
41
  }
35
42
  const tokenUsage = readTokenUsage(message.tokens);
36
43
  const timeRecord = isRecord(message.time) ? message.time : undefined;
44
+ const cost = readTelemetryNumber(message.cost);
37
45
  if (typeof message.id !== 'string' ||
38
46
  typeof message.sessionID !== 'string' ||
39
47
  typeof message.modelID !== 'string' ||
40
48
  typeof message.providerID !== 'string' ||
41
49
  typeof message.mode !== 'string' ||
42
- typeof message.cost !== 'number' ||
43
- typeof timeRecord?.created !== 'number' ||
50
+ cost === undefined ||
51
+ !isFiniteNumber(timeRecord?.created) ||
44
52
  !tokenUsage) {
45
53
  return undefined;
46
54
  }
@@ -50,12 +58,12 @@ export const readAssistantMessage = (message) => {
50
58
  role: 'assistant',
51
59
  time: {
52
60
  created: timeRecord.created,
53
- completed: typeof timeRecord.completed === 'number' ? timeRecord.completed : undefined
61
+ completed: isFiniteNumber(timeRecord.completed) ? timeRecord.completed : undefined
54
62
  },
55
63
  modelID: message.modelID,
56
64
  providerID: message.providerID,
57
65
  mode: message.mode,
58
- cost: message.cost,
66
+ cost,
59
67
  tokens: tokenUsage,
60
68
  finish: typeof message.finish === 'string' ? message.finish : undefined,
61
69
  error: isRecord(message.error) && typeof message.error.name === 'string' ? { name: message.error.name } : undefined
@@ -100,11 +108,12 @@ export const readStepFinishPart = (part) => {
100
108
  return undefined;
101
109
  }
102
110
  const tokenUsage = readTokenUsage(part.tokens);
111
+ const cost = readTelemetryNumber(part.cost);
103
112
  if (typeof part.id !== 'string' ||
104
113
  typeof part.sessionID !== 'string' ||
105
114
  typeof part.messageID !== 'string' ||
106
115
  typeof part.reason !== 'string' ||
107
- typeof part.cost !== 'number' ||
116
+ cost === undefined ||
108
117
  !tokenUsage) {
109
118
  return undefined;
110
119
  }
@@ -114,7 +123,7 @@ export const readStepFinishPart = (part) => {
114
123
  messageID: part.messageID,
115
124
  type: 'step-finish',
116
125
  reason: part.reason,
117
- cost: part.cost,
126
+ cost,
118
127
  tokens: tokenUsage
119
128
  };
120
129
  };
@@ -5,7 +5,7 @@ const buildSyncFailureMessage = (error) => {
5
5
  return `TimeFly sync blocked: Supporter plan required. Upgrade at ${PRICING_URL}`;
6
6
  }
7
7
  if (error.isUnauthorized) {
8
- return 'TimeFly sync blocked: not signed in. Run `bunx @timefly/opencode-plugin login` or set TIMEFLY_ACCESS_TOKEN.';
8
+ return 'TimeFly sync blocked: not signed in. Run `bunx @timefly/opencode-plugin login`.';
9
9
  }
10
10
  return `TimeFly sync failed (${error.statusCode}): ${error.message}`;
11
11
  };
@@ -1 +1 @@
1
- {"version":3,"file":"token-usage.d.ts","sourceRoot":"","sources":["../src/token-usage.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,kBAAkB,GAAG;IAChC,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;IACd,SAAS,EAAE,MAAM,CAAA;IACjB,KAAK,EAAE;QACN,IAAI,EAAE,MAAM,CAAA;QACZ,KAAK,EAAE,MAAM,CAAA;KACb,CAAA;CACD,CAAA;AAED,MAAM,MAAM,YAAY,GAAG;IAC1B,WAAW,EAAE,MAAM,CAAA;IACnB,YAAY,EAAE,MAAM,CAAA;IACpB,WAAW,EAAE,MAAM,CAAA;IACnB,eAAe,EAAE,MAAM,CAAA;IACvB,eAAe,EAAE,MAAM,CAAA;IACvB,gBAAgB,EAAE,MAAM,CAAA;IACxB,qBAAqB,CAAC,EAAE,MAAM,CAAA;IAC9B,oBAAoB,CAAC,EAAE,MAAM,CAAA;CAC7B,CAAA;AAED,eAAO,MAAM,aAAa,GAAI,YAAY,kBAAkB,KAAG,MACH,CAAA;AAE5D,eAAO,MAAM,iBAAiB,GAAI,YAAY,kBAAkB,EAAE,aAAa,MAAM,KAAG,YAmBvF,CAAA;AAED,eAAO,MAAM,kBAAkB,GAAI,cAAc,YAAY,EAAE,QAAO,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAM,KAAG,MAAM,CAC5H,MAAM,EACN,MAAM,GAAG,MAAM,GAAG,OAAO,CAUxB,CAAA"}
1
+ {"version":3,"file":"token-usage.d.ts","sourceRoot":"","sources":["../src/token-usage.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,kBAAkB,GAAG;IAChC,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;IACd,SAAS,EAAE,MAAM,CAAA;IACjB,KAAK,EAAE;QACN,IAAI,EAAE,MAAM,CAAA;QACZ,KAAK,EAAE,MAAM,CAAA;KACb,CAAA;CACD,CAAA;AAED,MAAM,MAAM,YAAY,GAAG;IAC1B,WAAW,EAAE,MAAM,CAAA;IACnB,YAAY,EAAE,MAAM,CAAA;IACpB,WAAW,EAAE,MAAM,CAAA;IACnB,eAAe,EAAE,MAAM,CAAA;IACvB,eAAe,EAAE,MAAM,CAAA;IACvB,gBAAgB,EAAE,MAAM,CAAA;IACxB,qBAAqB,CAAC,EAAE,MAAM,CAAA;IAC9B,oBAAoB,CAAC,EAAE,MAAM,CAAA;CAC7B,CAAA;AAKD,eAAO,MAAM,aAAa,GAAI,YAAY,kBAAkB,KAAG,MACyD,CAAA;AAExH,eAAO,MAAM,iBAAiB,GAAI,YAAY,kBAAkB,EAAE,aAAa,MAAM,KAAG,YAmBvF,CAAA;AAED,eAAO,MAAM,kBAAkB,GAAI,cAAc,YAAY,EAAE,QAAO,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAM,KAAG,MAAM,CAC5H,MAAM,EACN,MAAM,GAAG,MAAM,GAAG,OAAO,CAUxB,CAAA"}
@@ -1,17 +1,18 @@
1
- export const sumTokenUsage = (tokenUsage) => tokenUsage.input + tokenUsage.output + tokenUsage.reasoning;
1
+ const sanitizeTokenCount = (value) => Number.isFinite(value) && value > 0 ? Math.floor(value) : 0;
2
+ export const sumTokenUsage = (tokenUsage) => sanitizeTokenCount(tokenUsage.input) + sanitizeTokenCount(tokenUsage.output) + sanitizeTokenCount(tokenUsage.reasoning);
2
3
  export const buildTokenMetrics = (tokenUsage, durationMs) => {
3
- const inputTokens = tokenUsage.input;
4
- const outputTokens = tokenUsage.output;
5
- const reasoningTokens = tokenUsage.reasoning;
4
+ const inputTokens = sanitizeTokenCount(tokenUsage.input);
5
+ const outputTokens = sanitizeTokenCount(tokenUsage.output);
6
+ const reasoningTokens = sanitizeTokenCount(tokenUsage.reasoning);
6
7
  const totalTokens = sumTokenUsage(tokenUsage);
7
- const durationSeconds = durationMs && durationMs > 0 ? durationMs / 1000 : undefined;
8
+ const durationSeconds = durationMs && Number.isFinite(durationMs) && durationMs > 0 ? durationMs / 1000 : undefined;
8
9
  return {
9
10
  inputTokens,
10
11
  outputTokens,
11
12
  totalTokens,
12
13
  reasoningTokens,
13
- cacheReadTokens: tokenUsage.cache.read,
14
- cacheWriteTokens: tokenUsage.cache.write,
14
+ cacheReadTokens: sanitizeTokenCount(tokenUsage.cache.read),
15
+ cacheWriteTokens: sanitizeTokenCount(tokenUsage.cache.write),
15
16
  outputTokensPerSecond: durationSeconds !== undefined ? Math.round((outputTokens / durationSeconds) * 100) / 100 : undefined,
16
17
  totalTokensPerSecond: durationSeconds !== undefined ? Math.round((totalTokens / durationSeconds) * 100) / 100 : undefined
17
18
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@timefly/opencode-plugin",
3
- "version": "0.2.3",
3
+ "version": "0.2.8",
4
4
  "description": "TimeFly telemetry plugin for OpenCode — sessions, tokens, models, and tools",
5
5
  "type": "module",
6
6
  "bin": "./dist/cli.js",
@@ -50,19 +50,19 @@
50
50
  },
51
51
  "license": "MIT",
52
52
  "dependencies": {
53
- "@timefly/ai-sdk": "^0.2.0"
53
+ "@timefly/ai-sdk": "^0.2.1"
54
54
  },
55
55
  "peerDependencies": {
56
56
  "@opencode-ai/plugin": ">=1.0.0"
57
57
  },
58
58
  "devDependencies": {
59
59
  "@opencode-ai/plugin": "^1.17.7",
60
- "@timefly/ai-sdk": "workspace:*",
60
+ "@timefly/ai-sdk": "0.2.1",
61
61
  "@types/bun": "^1.3.14",
62
62
  "@types/node": "^22.15.21",
63
63
  "typescript": "^5.9.3"
64
64
  },
65
65
  "engines": {
66
- "node": ">=18"
66
+ "bun": ">=1.3.0"
67
67
  }
68
68
  }