@timefly/opencode-plugin 0.2.3 → 0.2.7

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. |
@@ -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,CAwDd,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
  };
@@ -33,16 +34,30 @@ export const handleMessageUpdated = (eventProperties, tracker, publish) => {
33
34
  return Promise.resolve();
34
35
  }
35
36
  tracker.markMessageProcessed(assistantMessage.id);
36
- const eventsToPublish = [mapAssistantTurnCompleteInput(assistantMessage), mapAssistantLlmResponseInput(assistantMessage)].filter((eventInput) => eventInput !== undefined);
37
+ const durationMs = assistantMessage.time.completed - assistantMessage.time.created;
38
+ const tokenMetrics = buildTokenMetrics(assistantMessage.tokens, durationMs);
39
+ const sessionStats = tracker.getSessionStats(assistantMessage.sessionID);
40
+ const compactionDelta = tracker.recordTurnTokens(assistantMessage.sessionID, {
41
+ contextTokens: tokenMetrics.inputTokens,
42
+ billedInputTokens: tokenMetrics.inputTokens,
43
+ outputTokens: tokenMetrics.outputTokens,
44
+ totalTokens: tokenMetrics.totalTokens,
45
+ cacheReadTokens: tokenMetrics.cacheReadTokens,
46
+ cacheWriteTokens: tokenMetrics.cacheWriteTokens,
47
+ reasoningTokens: tokenMetrics.reasoningTokens
48
+ });
49
+ tracker.recordSessionStats(assistantMessage.sessionID, {
50
+ aiGenerationMs: durationMs,
51
+ primaryProviderId: assistantMessage.providerID
52
+ });
53
+ const turnCompleteInput = mapAssistantTurnCompleteInput(assistantMessage, {
54
+ providerSource: sessionStats.primaryProviderSource || undefined,
55
+ compactionDelta
56
+ });
57
+ const eventsToPublish = [turnCompleteInput, mapAssistantLlmResponseInput(assistantMessage)].filter((eventInput) => eventInput !== undefined);
37
58
  if (!eventsToPublish.length) {
38
59
  return Promise.resolve();
39
60
  }
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
61
  return publish(eventsToPublish);
47
62
  }
48
63
  const userMessage = readUserMessage(eventProperties.info);
@@ -71,6 +86,7 @@ export const handleMessagePartUpdated = (eventProperties, tracker, publish) => {
71
86
  return Promise.resolve();
72
87
  }
73
88
  tracker.markPartProcessed(retryPart.id);
89
+ tracker.recordSessionStats(retryPart.sessionID, { retryCount: 1 });
74
90
  return publish([mapRetryPartInput(retryPart)]);
75
91
  }
76
92
  if (compactionPart) {
@@ -78,18 +94,24 @@ export const handleMessagePartUpdated = (eventProperties, tracker, publish) => {
78
94
  return Promise.resolve();
79
95
  }
80
96
  tracker.markPartProcessed(compactionPart.id);
81
- return publish([mapCompactionPartInput(compactionPart)]);
97
+ const contextBeforeTokens = tracker.getSessionStats(compactionPart.sessionID).latestContextTokens;
98
+ tracker.recordCompactionPending(compactionPart.sessionID, contextBeforeTokens);
99
+ tracker.recordSessionStats(compactionPart.sessionID, { compactionCount: 1 });
100
+ return publish([mapCompactionPartInput(compactionPart, contextBeforeTokens)]);
82
101
  }
83
102
  return Promise.resolve();
84
103
  };
85
- export const handleSessionCompacted = (eventProperties, publish) => {
104
+ export const handleSessionCompacted = (eventProperties, tracker, publish) => {
86
105
  const sessionId = readSessionIdFromProperties(eventProperties);
87
106
  if (!sessionId) {
88
107
  return Promise.resolve();
89
108
  }
90
- return publish([mapCompactionInput(sessionId)]);
109
+ tracker.recordSessionStats(sessionId, { compactionCount: 1 });
110
+ const contextBeforeTokens = tracker.getSessionStats(sessionId).latestContextTokens;
111
+ tracker.recordCompactionPending(sessionId, contextBeforeTokens);
112
+ return publish([mapCompactionInput(sessionId, contextBeforeTokens)]);
91
113
  };
92
- export const handleSessionError = (eventProperties, publish) => {
114
+ export const handleSessionError = (eventProperties, tracker, publish) => {
93
115
  const sessionId = readSessionIdFromProperties(eventProperties);
94
116
  const errorRecord = isRecord(eventProperties.error) ? eventProperties.error : undefined;
95
117
  const errorName = errorRecord && typeof errorRecord.name === 'string' ? errorRecord.name : 'unknown';
@@ -98,6 +120,7 @@ export const handleSessionError = (eventProperties, publish) => {
98
120
  if (!sessionId) {
99
121
  return Promise.resolve();
100
122
  }
123
+ tracker.recordSessionStats(sessionId, { errorCount: 1 });
101
124
  return publish([
102
125
  mapErrorInput(sessionId, {
103
126
  error_name: errorName,
@@ -126,7 +149,7 @@ export const handleBusEvent = (event, tracker, publish) => {
126
149
  }
127
150
  switch (event.type) {
128
151
  case 'session.created':
129
- return handleSessionCreated(eventProperties, publish);
152
+ return handleSessionCreated(eventProperties, tracker, publish);
130
153
  case 'session.status': {
131
154
  const statusRecord = isRecord(eventProperties.status) ? eventProperties.status : undefined;
132
155
  if (statusRecord?.type !== 'idle') {
@@ -141,9 +164,9 @@ export const handleBusEvent = (event, tracker, publish) => {
141
164
  case 'message.part.updated':
142
165
  return handleMessagePartUpdated(eventProperties, tracker, publish);
143
166
  case 'session.compacted':
144
- return handleSessionCompacted(eventProperties, publish);
167
+ return handleSessionCompacted(eventProperties, tracker, publish);
145
168
  case 'session.error':
146
- return handleSessionError(eventProperties, publish);
169
+ return handleSessionError(eventProperties, tracker, publish);
147
170
  case 'command.executed':
148
171
  return handleCommandExecuted(eventProperties, publish);
149
172
  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;AA0BD,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;AA8BD,eAAO,MAAM,kBAAkB,QAAO,YA6GrC,CAAA"}
@@ -1,17 +1,58 @@
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 mergeSessionStats = (currentStats, delta) => ({
25
+ billedInputTokens: currentStats.billedInputTokens + (delta.billedInputTokens ?? 0),
26
+ outputTokens: currentStats.outputTokens + (delta.outputTokens ?? 0),
27
+ totalTokens: currentStats.totalTokens + (delta.totalTokens ?? 0),
28
+ turnCount: currentStats.turnCount + (delta.turnCount ?? 0),
29
+ toolCallCount: currentStats.toolCallCount + (delta.toolCallCount ?? 0),
30
+ requestCount: currentStats.requestCount + (delta.requestCount ?? 0),
31
+ stepCount: currentStats.stepCount + (delta.stepCount ?? 0),
32
+ compactionCount: currentStats.compactionCount + (delta.compactionCount ?? 0),
33
+ retryCount: currentStats.retryCount + (delta.retryCount ?? 0),
34
+ errorCount: currentStats.errorCount + (delta.errorCount ?? 0),
35
+ cacheReadTokens: currentStats.cacheReadTokens + (delta.cacheReadTokens ?? 0),
36
+ cacheWriteTokens: currentStats.cacheWriteTokens + (delta.cacheWriteTokens ?? 0),
37
+ reasoningTokens: currentStats.reasoningTokens + (delta.reasoningTokens ?? 0),
38
+ peakContextTokens: Math.max(currentStats.peakContextTokens, delta.peakContextTokens ?? 0),
39
+ latestContextTokens: delta.latestContextTokens ?? currentStats.latestContextTokens,
40
+ contextAtStartTokens: currentStats.contextAtStartTokens > 0
41
+ ? currentStats.contextAtStartTokens
42
+ : (delta.contextAtStartTokens ?? currentStats.contextAtStartTokens),
43
+ compactionTokensSaved: currentStats.compactionTokensSaved + (delta.compactionTokensSaved ?? 0),
44
+ totalToolOutputChars: currentStats.totalToolOutputChars + (delta.totalToolOutputChars ?? 0),
45
+ primaryProviderId: delta.primaryProviderId ?? currentStats.primaryProviderId,
46
+ primaryProviderSource: delta.primaryProviderSource ?? currentStats.primaryProviderSource,
47
+ startedAtMs: currentStats.startedAtMs,
48
+ aiGenerationMs: currentStats.aiGenerationMs + (delta.aiGenerationMs ?? 0)
9
49
  });
10
50
  export const createEventTracker = () => {
11
51
  const processedMessageIds = new Set();
12
52
  const processedUserMessageIds = new Set();
13
53
  const processedPartIds = new Set();
14
54
  const sessionStatsById = new Map();
55
+ const pendingCompactionContextBySession = new Map();
15
56
  return {
16
57
  hasProcessedMessage: (messageId) => processedMessageIds.has(messageId),
17
58
  markMessageProcessed: (messageId) => {
@@ -25,18 +66,80 @@ export const createEventTracker = () => {
25
66
  markPartProcessed: (partId) => {
26
67
  processedPartIds.add(partId);
27
68
  },
69
+ recordSessionStart: (sessionId, startedAtMs = Date.now()) => {
70
+ const currentStats = sessionStatsById.get(sessionId) ?? emptySessionStats();
71
+ if (currentStats.startedAtMs !== undefined) {
72
+ return;
73
+ }
74
+ sessionStatsById.set(sessionId, {
75
+ ...currentStats,
76
+ startedAtMs
77
+ });
78
+ },
28
79
  recordSessionStats: (sessionId, delta) => {
80
+ const currentStats = sessionStatsById.get(sessionId) ?? emptySessionStats();
81
+ sessionStatsById.set(sessionId, mergeSessionStats(currentStats, delta));
82
+ },
83
+ recordProviderConnection: (sessionId, providerId, providerSource) => {
29
84
  const currentStats = sessionStatsById.get(sessionId) ?? emptySessionStats();
30
85
  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)
86
+ ...currentStats,
87
+ primaryProviderId: providerId,
88
+ primaryProviderSource: providerSource
38
89
  });
39
90
  },
40
- getSessionStats: (sessionId) => sessionStatsById.get(sessionId) ?? emptySessionStats()
91
+ recordCompactionPending: (sessionId, contextBeforeTokens) => {
92
+ if (contextBeforeTokens > 0) {
93
+ pendingCompactionContextBySession.set(sessionId, contextBeforeTokens);
94
+ }
95
+ },
96
+ recordToolOutputChars: (sessionId, outputChars) => {
97
+ const currentStats = sessionStatsById.get(sessionId) ?? emptySessionStats();
98
+ sessionStatsById.set(sessionId, {
99
+ ...currentStats,
100
+ totalToolOutputChars: currentStats.totalToolOutputChars + Math.max(0, outputChars)
101
+ });
102
+ },
103
+ recordTurnTokens: (sessionId, turnTokens) => {
104
+ const currentStats = sessionStatsById.get(sessionId) ?? emptySessionStats();
105
+ const isFirstTurn = currentStats.turnCount === 0;
106
+ const contextBeforeCompaction = pendingCompactionContextBySession.get(sessionId);
107
+ const compactionDelta = contextBeforeCompaction !== undefined && contextBeforeCompaction > turnTokens.contextTokens
108
+ ? {
109
+ contextBeforeTokens: contextBeforeCompaction,
110
+ contextAfterTokens: turnTokens.contextTokens,
111
+ tokensSaved: contextBeforeCompaction - turnTokens.contextTokens
112
+ }
113
+ : undefined;
114
+ if (compactionDelta) {
115
+ pendingCompactionContextBySession.delete(sessionId);
116
+ }
117
+ sessionStatsById.set(sessionId, mergeSessionStats(currentStats, {
118
+ billedInputTokens: turnTokens.billedInputTokens,
119
+ outputTokens: turnTokens.outputTokens,
120
+ totalTokens: turnTokens.totalTokens,
121
+ cacheReadTokens: turnTokens.cacheReadTokens,
122
+ cacheWriteTokens: turnTokens.cacheWriteTokens,
123
+ reasoningTokens: turnTokens.reasoningTokens,
124
+ turnCount: 1,
125
+ peakContextTokens: turnTokens.contextTokens,
126
+ latestContextTokens: turnTokens.contextTokens,
127
+ contextAtStartTokens: isFirstTurn ? turnTokens.contextTokens : currentStats.contextAtStartTokens,
128
+ ...(compactionDelta ? { compactionTokensSaved: compactionDelta.tokensSaved } : {})
129
+ }));
130
+ return compactionDelta;
131
+ },
132
+ getSessionStats: (sessionId) => sessionStatsById.get(sessionId) ?? emptySessionStats(),
133
+ getSessionTiming: (sessionId, endedAtMs = Date.now()) => {
134
+ const currentStats = sessionStatsById.get(sessionId) ?? emptySessionStats();
135
+ const sessionWallMs = currentStats.startedAtMs !== undefined ? Math.max(0, endedAtMs - currentStats.startedAtMs) : 0;
136
+ const aiGenerationMs = currentStats.aiGenerationMs;
137
+ const userWaitMs = Math.max(0, sessionWallMs - aiGenerationMs);
138
+ return {
139
+ sessionWallMs,
140
+ aiGenerationMs,
141
+ userWaitMs
142
+ };
143
+ }
41
144
  };
42
145
  };
@@ -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,MAsEnC,CAAA;AAED,eAAe,qBAAqB,CAAA"}
package/dist/index.js CHANGED
@@ -27,6 +27,7 @@ export const timeflyOpenCodePlugin = ({ client }) => {
27
27
  'chat.params': (input, output) => runHookSafely(() => {
28
28
  tracker.recordSessionStats(input.sessionID, { requestCount: 1 });
29
29
  const resolvedParams = resolveChatParams(input, output);
30
+ tracker.recordProviderConnection(resolvedParams.sessionID, resolvedParams.providerId, resolvedParams.providerSource);
30
31
  return publisher.publish([
31
32
  mapLlmRequestInput({
32
33
  sessionID: resolvedParams.sessionID,
@@ -44,15 +45,18 @@ export const timeflyOpenCodePlugin = ({ client }) => {
44
45
  tracker.recordSessionStats(input.sessionID, { toolCallCount: 1 });
45
46
  return publisher.publish([mapToolCallInput(input)]);
46
47
  }),
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
- ])),
48
+ 'tool.execute.after': (input, output) => runHookSafely(() => {
49
+ tracker.recordToolOutputChars(input.sessionID, output.output.length);
50
+ return publisher.publish([
51
+ mapToolResultInput({
52
+ sessionID: input.sessionID,
53
+ tool: input.tool,
54
+ callID: input.callID,
55
+ hasOutput: Boolean(output.output),
56
+ outputLength: output.output.length
57
+ })
58
+ ]);
59
+ }),
56
60
  'experimental.session.compacting': (input) => runHookSafely(() => publisher.publish([mapCompactionInput(input.sessionID)]))
57
61
  }));
58
62
  };
@@ -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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"}
@@ -11,16 +11,34 @@ export const mapSessionStartInput = (sessionInfo) => ({
11
11
  directory: sessionInfo.directory
12
12
  }
13
13
  });
14
- export const mapSessionEndInput = (sessionId, sessionStats) => ({
14
+ export const mapSessionEndInput = (sessionId, sessionStats, sessionTiming) => ({
15
15
  sessionId: readSessionIdOverride(sessionId),
16
+ durationMs: sessionTiming.sessionWallMs,
16
17
  metadata: {
17
- session_input_tokens: sessionStats.inputTokens,
18
+ session_input_tokens: sessionStats.billedInputTokens,
19
+ session_billed_input_tokens: sessionStats.billedInputTokens,
18
20
  session_output_tokens: sessionStats.outputTokens,
19
21
  session_total_tokens: sessionStats.totalTokens,
20
22
  session_turn_count: sessionStats.turnCount,
21
23
  session_tool_call_count: sessionStats.toolCallCount,
22
24
  session_request_count: sessionStats.requestCount,
23
- session_step_count: sessionStats.stepCount
25
+ session_step_count: sessionStats.stepCount,
26
+ session_compaction_count: sessionStats.compactionCount,
27
+ session_retry_count: sessionStats.retryCount,
28
+ session_error_count: sessionStats.errorCount,
29
+ session_cache_read_tokens: sessionStats.cacheReadTokens,
30
+ session_cache_write_tokens: sessionStats.cacheWriteTokens,
31
+ session_reasoning_tokens: sessionStats.reasoningTokens,
32
+ session_compaction_tokens_saved: sessionStats.compactionTokensSaved,
33
+ session_tool_output_chars: sessionStats.totalToolOutputChars,
34
+ primary_provider_id: sessionStats.primaryProviderId,
35
+ primary_provider_source: sessionStats.primaryProviderSource,
36
+ peak_context_tokens: sessionStats.peakContextTokens,
37
+ latest_context_tokens: sessionStats.latestContextTokens,
38
+ context_at_start_tokens: sessionStats.contextAtStartTokens,
39
+ session_wall_ms: sessionTiming.sessionWallMs,
40
+ ai_generation_ms: sessionTiming.aiGenerationMs,
41
+ user_wait_ms: sessionTiming.userWaitMs
24
42
  }
25
43
  });
26
44
  export const mapLlmRequestInput = (input) => ({
@@ -38,7 +56,7 @@ export const mapLlmRequestInput = (input) => ({
38
56
  ...(input.maxOutputTokens !== undefined ? { max_output_tokens: input.maxOutputTokens } : {})
39
57
  }
40
58
  });
41
- export const mapAssistantTurnCompleteInput = (message) => {
59
+ export const mapAssistantTurnCompleteInput = (message, options) => {
42
60
  if (message.time.completed === undefined) {
43
61
  return undefined;
44
62
  }
@@ -57,10 +75,19 @@ export const mapAssistantTurnCompleteInput = (message) => {
57
75
  message_id: message.id,
58
76
  provider_id: message.providerID,
59
77
  model_id: message.modelID,
78
+ ...(options?.providerSource ? { provider_source: options.providerSource } : {}),
79
+ context_tokens: tokenMetrics.inputTokens,
60
80
  cost: message.cost,
61
81
  ...(message.finish ? { finish_reason: message.finish } : {}),
62
82
  has_error: Boolean(message.error),
63
- ...(message.error ? { error_name: message.error.name } : {})
83
+ ...(message.error ? { error_name: message.error.name } : {}),
84
+ ...(options?.compactionDelta
85
+ ? {
86
+ context_before_compaction: options.compactionDelta.contextBeforeTokens,
87
+ context_after_compaction: options.compactionDelta.contextAfterTokens,
88
+ compaction_tokens_saved: options.compactionDelta.tokensSaved
89
+ }
90
+ : {})
64
91
  })
65
92
  };
66
93
  };
@@ -118,10 +145,13 @@ export const mapUserMessageInput = (message) => ({
118
145
  request_scope: 'user_message'
119
146
  }
120
147
  });
121
- export const mapCompactionInput = (sessionId, auto) => ({
148
+ export const mapCompactionInput = (sessionId, contextBeforeTokens, auto) => ({
122
149
  sessionId: readSessionIdOverride(sessionId),
123
150
  eventType: 'compaction',
124
151
  metadata: {
152
+ ...(contextBeforeTokens !== undefined && contextBeforeTokens > 0
153
+ ? { context_before_compaction: contextBeforeTokens }
154
+ : {}),
125
155
  ...(auto !== undefined ? { auto_compaction: auto } : {})
126
156
  }
127
157
  });
@@ -168,12 +198,15 @@ export const mapRetryPartInput = (part) => ({
168
198
  error_scope: 'retry'
169
199
  }
170
200
  });
171
- export const mapCompactionPartInput = (part) => ({
201
+ export const mapCompactionPartInput = (part, contextBeforeTokens) => ({
172
202
  sessionId: readSessionIdOverride(part.sessionID),
173
203
  eventType: 'compaction',
174
204
  metadata: {
175
205
  message_id: part.messageID,
176
206
  part_id: part.id,
177
- auto_compaction: part.auto
207
+ auto_compaction: part.auto,
208
+ ...(contextBeforeTokens !== undefined && contextBeforeTokens > 0
209
+ ? { context_before_compaction: contextBeforeTokens }
210
+ : {})
178
211
  }
179
212
  });
@@ -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
  };
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.7",
4
4
  "description": "TimeFly telemetry plugin for OpenCode — sessions, tokens, models, and tools",
5
5
  "type": "module",
6
6
  "bin": "./dist/cli.js",
@@ -63,6 +63,6 @@
63
63
  "typescript": "^5.9.3"
64
64
  },
65
65
  "engines": {
66
- "node": ">=18"
66
+ "bun": ">=1.3.0"
67
67
  }
68
68
  }