@timefly/opencode-plugin 0.2.2 → 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.
@@ -180,7 +173,7 @@ OpenCode hooks → @timefly/opencode-plugin → @timefly/ai-sdk
180
173
  → POST /ai/sync (gzip JSON, Bearer token)
181
174
  → Gateway auth + Supporter check
182
175
  → Ingest queue (Redis)
183
- → Worker (every 5s) → ClickHouse ai_usage_events
176
+ → Worker (every 5s) → ClickHouse activity_events (ai.* activities)
184
177
  → Dashboard GET /analytics/ai-usage
185
178
  ```
186
179
 
@@ -213,27 +206,85 @@ 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
 
217
+ Based on OpenCode source (`packages/opencode`, `@opencode-ai/plugin` hooks, and `@opencode-ai/sdk` event types). We only emit metadata — never prompts, tool args, or file contents.
218
+
219
+ ### Plugin hooks we use
220
+
221
+ | Hook | When it fires (OpenCode source) | TimeFly event |
222
+ |------|----------------------------------|---------------|
223
+ | `event` | Bus events forwarded from `EventV2Bridge` to all plugins | see bus table below |
224
+ | `chat.params` | Before every LLM call (`LLMRequestPrep.prepare`) | `llm_request` |
225
+ | `tool.execute.before` | Before built-in, MCP, and registry tools run (`SessionTools.resolve`) | `tool_call` |
226
+ | `tool.execute.after` | After tool completes (same path) | `tool_result` |
227
+ | `experimental.session.compacting` | Before compaction LLM call (`SessionCompaction.process`) | `compaction` |
228
+
229
+ ### Bus events we handle (`event` hook)
230
+
231
+ | OpenCode event | Status | TimeFly event | Data captured |
232
+ |----------------|--------|---------------|---------------|
233
+ | `session.created` | Active | `session_start` | title, project, directory |
234
+ | `session.status` (`type: idle`) | Preferred | `session_end` | session token/tool/request totals |
235
+ | `session.idle` | Deprecated alias | `session_end` | same as above |
236
+ | `message.updated` (assistant, completed) | Active | `turn_complete` + `llm_response` | tokens, cost, duration, finish reason |
237
+ | `message.updated` (user) | Active | `llm_request` | model, agent (metadata only) |
238
+ | `message.part.updated` (`step-finish`) | Active | `llm_response` | per-step tokens, cost |
239
+ | `message.part.updated` (`retry`) | Active | `error` | retry attempt |
240
+ | `message.part.updated` (`compaction`) | Active | `compaction` | auto/manual flag |
241
+ | `session.compacted` | Active | `compaction` | session id |
242
+ | `session.error` | Active | `error` | error name/message |
243
+ | `command.executed` | Active | `tool_call` | command name (as `command:*`) |
244
+
245
+ ### Bus events we intentionally skip
246
+
247
+ | OpenCode event | Why not tracked |
248
+ |----------------|-----------------|
249
+ | `message.part.updated` (`text`, `reasoning`, `tool`, …) | Would expose prompt/response/tool args |
250
+ | `message.removed`, `message.part.removed` | Deletion only — no usage signal |
251
+ | `session.updated`, `session.deleted`, `session.diff` | Metadata or file diffs — not AI usage |
252
+ | `file.edited`, `file.watcher.updated` | File paths/content |
253
+ | `permission.asked`, `permission.replied` | No token/model signal |
254
+ | `todo.updated` | Task list text |
255
+ | `lsp.*`, `pty.*`, `tui.*`, `installation.*`, `server.*` | IDE/infra — not LLM usage |
256
+
257
+ ### Hooks we do not use (available in OpenCode, not needed for usage telemetry)
258
+
259
+ | Hook | Reason |
260
+ |------|--------|
261
+ | `chat.message` | Full user message + parts — privacy |
262
+ | `chat.headers` | Auth headers — security |
263
+ | `shell.env` | Environment variables — secrets risk |
264
+ | `permission.ask` | Could track denials; not implemented yet |
265
+ | `command.execute.before` | Parts contain prompt fragments |
266
+ | `experimental.chat.messages.transform` | Full message history |
267
+ | `experimental.compaction.autocontinue` | No extra telemetry beyond compaction events |
268
+ | `tool` (custom tools) | Execution still flows through `tool.execute.*` |
269
+
270
+ ### Known limitations
271
+
272
+ | Topic | Detail |
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. |
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. |
276
+ | **Token counts** | Require provider to report usage. If `message.updated` lacks token fields, turn events are skipped (no crash). |
277
+ | **Provider-side tools** | Tools executed inside the provider (`providerExecuted`) may not hit `tool.execute.*` — no tool events for those. |
278
+ | **Multi-step turns** | `step-finish` parts can emit extra `llm_response` events; message-level `turn_complete` deduplicates by message id. |
279
+ | **`session.idle` vs `session.status`** | OpenCode marks `session.idle` deprecated; we handle both. |
280
+ | **v2 event system** | OpenCode is migrating to `session.next.*` internally; plugins still receive v1 bus types above via the bridge. |
281
+
282
+ ### Quick reference (captured signals)
283
+
233
284
  | OpenCode signal | TimeFly `eventType` | Data |
234
285
  |-----------------|---------------------|------|
235
286
  | `session.created` | `session_start` | title, project, directory |
236
- | `session.idle` | `session_end` | session token/tool/request totals |
287
+ | `session.status` / `session.idle` | `session_end` | session token/tool/request totals |
237
288
  | `chat.params` | `llm_request` | model, provider, agent, temperature |
238
289
  | `message.updated` (assistant, completed) | `turn_complete` + `llm_response` | tokens, duration, tokens/s, cost |
239
290
  | `message.part.updated` (step-finish) | `llm_response` | per-step tokens |
@@ -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,CAyBd,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,14 @@ 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);
153
+ case 'session.status': {
154
+ const statusRecord = isRecord(eventProperties.status) ? eventProperties.status : undefined;
155
+ if (statusRecord?.type !== 'idle') {
156
+ return Promise.resolve();
157
+ }
158
+ return handleSessionIdle(eventProperties, tracker, publish);
159
+ }
130
160
  case 'session.idle':
131
161
  return handleSessionIdle(eventProperties, tracker, publish);
132
162
  case 'message.updated':
@@ -134,9 +164,9 @@ export const handleBusEvent = (event, tracker, publish) => {
134
164
  case 'message.part.updated':
135
165
  return handleMessagePartUpdated(eventProperties, tracker, publish);
136
166
  case 'session.compacted':
137
- return handleSessionCompacted(eventProperties, publish);
167
+ return handleSessionCompacted(eventProperties, tracker, publish);
138
168
  case 'session.error':
139
- return handleSessionError(eventProperties, publish);
169
+ return handleSessionError(eventProperties, tracker, publish);
140
170
  case 'command.executed':
141
171
  return handleCommandExecuted(eventProperties, publish);
142
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":"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"}
@@ -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;AAWjD,eAAO,MAAM,qBAAqB,EAAE,MAyDnC,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
@@ -5,6 +5,9 @@ import { createEventPublisher } from './publish-events.js';
5
5
  import { resolveChatParams } from './read-chat-params.js';
6
6
  import packageJson from '../package.json' with { type: 'json' };
7
7
  const PLUGIN_VERSION = packageJson.version;
8
+ const runHookSafely = (run) => Promise.resolve()
9
+ .then(run)
10
+ .catch(() => undefined);
8
11
  export const timeflyOpenCodePlugin = ({ client }) => {
9
12
  const tracker = createEventTracker();
10
13
  const publisher = createEventPublisher(client, PLUGIN_VERSION);
@@ -20,10 +23,11 @@ export const timeflyOpenCodePlugin = ({ client }) => {
20
23
  .then(() => undefined)
21
24
  .catch(() => undefined)
22
25
  .then(() => Promise.resolve({
23
- event: (input) => handleBusEvent(input.event, tracker, publisher.publish),
24
- 'chat.params': (input, output) => {
26
+ event: (input) => runHookSafely(() => handleBusEvent(input.event, tracker, publisher.publish)),
27
+ 'chat.params': (input, output) => runHookSafely(() => {
25
28
  tracker.recordSessionStats(input.sessionID, { requestCount: 1 });
26
29
  const resolvedParams = resolveChatParams(input, output);
30
+ tracker.recordProviderConnection(resolvedParams.sessionID, resolvedParams.providerId, resolvedParams.providerSource);
27
31
  return publisher.publish([
28
32
  mapLlmRequestInput({
29
33
  sessionID: resolvedParams.sessionID,
@@ -36,21 +40,24 @@ export const timeflyOpenCodePlugin = ({ client }) => {
36
40
  maxOutputTokens: resolvedParams.maxOutputTokens
37
41
  })
38
42
  ]);
39
- },
40
- 'tool.execute.before': (input) => {
43
+ }),
44
+ 'tool.execute.before': (input) => runHookSafely(() => {
41
45
  tracker.recordSessionStats(input.sessionID, { toolCallCount: 1 });
42
46
  return publisher.publish([mapToolCallInput(input)]);
43
- },
44
- 'tool.execute.after': (input, output) => publisher.publish([
45
- mapToolResultInput({
46
- sessionID: input.sessionID,
47
- tool: input.tool,
48
- callID: input.callID,
49
- hasOutput: Boolean(output.output),
50
- outputLength: output.output.length
51
- })
52
- ]),
53
- 'experimental.session.compacting': (input) => publisher.publish([mapCompactionInput(input.sessionID)])
47
+ }),
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
+ }),
60
+ 'experimental.session.compacting': (input) => runHookSafely(() => publisher.publish([mapCompactionInput(input.sessionID)]))
54
61
  }));
55
62
  };
56
63
  export default timeflyOpenCodePlugin;
@@ -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
  };
@@ -1,16 +1,37 @@
1
+ /**
2
+ * OpenCode runtime passes flat `Provider.Info` to `chat.params`:
3
+ * `packages/opencode/src/session/llm/request.ts` → `provider: input.provider`
4
+ *
5
+ * Shape: `{ id, name, source, env, options, models }` — id is at the top level.
6
+ *
7
+ * `@opencode-ai/plugin` types document `ProviderContext` (`{ source, info, options }`)
8
+ * which does not match current runtime (anomalyco/opencode#20562). We accept both.
9
+ *
10
+ * Model shape: `Provider.Model` → `{ id, providerID, … }`.
11
+ */
12
+ export type OpenCodeProviderInfo = {
13
+ id: string;
14
+ name?: string;
15
+ source?: 'env' | 'config' | 'custom' | 'api' | string;
16
+ options?: Record<string, unknown>;
17
+ };
18
+ export type OpenCodeProviderContext = {
19
+ source?: 'env' | 'config' | 'custom' | 'api' | string;
20
+ info?: {
21
+ id?: string;
22
+ name?: string;
23
+ };
24
+ options?: Record<string, unknown>;
25
+ };
26
+ export type OpenCodeModel = {
27
+ id: string;
28
+ providerID: string;
29
+ };
1
30
  export type ChatParamsInput = {
2
31
  sessionID: string;
3
32
  agent: string;
4
- model?: {
5
- id?: string;
6
- providerID?: string;
7
- };
8
- provider?: {
9
- source?: string;
10
- info?: {
11
- id?: string;
12
- };
13
- };
33
+ model?: OpenCodeModel;
34
+ provider?: OpenCodeProviderInfo | OpenCodeProviderContext;
14
35
  };
15
36
  export type ChatParamsOutput = {
16
37
  temperature: number;
@@ -1 +1 @@
1
- {"version":3,"file":"read-chat-params.d.ts","sourceRoot":"","sources":["../src/read-chat-params.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,eAAe,GAAG;IAC7B,SAAS,EAAE,MAAM,CAAA;IACjB,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE;QACP,EAAE,CAAC,EAAE,MAAM,CAAA;QACX,UAAU,CAAC,EAAE,MAAM,CAAA;KACnB,CAAA;IACD,QAAQ,CAAC,EAAE;QACV,MAAM,CAAC,EAAE,MAAM,CAAA;QACf,IAAI,CAAC,EAAE;YACN,EAAE,CAAC,EAAE,MAAM,CAAA;SACX,CAAA;KACD,CAAA;CACD,CAAA;AAED,MAAM,MAAM,gBAAgB,GAAG;IAC9B,WAAW,EAAE,MAAM,CAAA;IACnB,IAAI,EAAE,MAAM,CAAA;IACZ,eAAe,CAAC,EAAE,MAAM,CAAA;CACxB,CAAA;AAED,MAAM,MAAM,kBAAkB,GAAG;IAChC,SAAS,EAAE,MAAM,CAAA;IACjB,KAAK,EAAE,MAAM,CAAA;IACb,UAAU,EAAE,MAAM,CAAA;IAClB,OAAO,EAAE,MAAM,CAAA;IACf,cAAc,EAAE,MAAM,CAAA;IACtB,WAAW,EAAE,MAAM,CAAA;IACnB,IAAI,EAAE,MAAM,CAAA;IACZ,eAAe,CAAC,EAAE,MAAM,CAAA;CACxB,CAAA;AAED,eAAO,MAAM,iBAAiB,GAAI,OAAO,eAAe,EAAE,QAAQ,gBAAgB,KAAG,kBASnF,CAAA"}
1
+ {"version":3,"file":"read-chat-params.d.ts","sourceRoot":"","sources":["../src/read-chat-params.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,MAAM,MAAM,oBAAoB,GAAG;IAClC,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,MAAM,CAAC,EAAE,KAAK,GAAG,QAAQ,GAAG,QAAQ,GAAG,KAAK,GAAG,MAAM,CAAA;IACrD,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CACjC,CAAA;AAED,MAAM,MAAM,uBAAuB,GAAG;IACrC,MAAM,CAAC,EAAE,KAAK,GAAG,QAAQ,GAAG,QAAQ,GAAG,KAAK,GAAG,MAAM,CAAA;IACrD,IAAI,CAAC,EAAE;QACN,EAAE,CAAC,EAAE,MAAM,CAAA;QACX,IAAI,CAAC,EAAE,MAAM,CAAA;KACb,CAAA;IACD,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CACjC,CAAA;AAED,MAAM,MAAM,aAAa,GAAG;IAC3B,EAAE,EAAE,MAAM,CAAA;IACV,UAAU,EAAE,MAAM,CAAA;CAClB,CAAA;AAED,MAAM,MAAM,eAAe,GAAG;IAC7B,SAAS,EAAE,MAAM,CAAA;IACjB,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,aAAa,CAAA;IACrB,QAAQ,CAAC,EAAE,oBAAoB,GAAG,uBAAuB,CAAA;CACzD,CAAA;AAED,MAAM,MAAM,gBAAgB,GAAG;IAC9B,WAAW,EAAE,MAAM,CAAA;IACnB,IAAI,EAAE,MAAM,CAAA;IACZ,eAAe,CAAC,EAAE,MAAM,CAAA;CACxB,CAAA;AAED,MAAM,MAAM,kBAAkB,GAAG;IAChC,SAAS,EAAE,MAAM,CAAA;IACjB,KAAK,EAAE,MAAM,CAAA;IACb,UAAU,EAAE,MAAM,CAAA;IAClB,OAAO,EAAE,MAAM,CAAA;IACf,cAAc,EAAE,MAAM,CAAA;IACtB,WAAW,EAAE,MAAM,CAAA;IACnB,IAAI,EAAE,MAAM,CAAA;IACZ,eAAe,CAAC,EAAE,MAAM,CAAA;CACxB,CAAA;AAiBD,eAAO,MAAM,iBAAiB,GAAI,OAAO,eAAe,EAAE,QAAQ,gBAAgB,KAAG,kBASnF,CAAA"}
@@ -1,9 +1,20 @@
1
+ const readProviderId = (provider, model) => {
2
+ if (provider && 'id' in provider && typeof provider.id === 'string' && provider.id.length > 0) {
3
+ return provider.id;
4
+ }
5
+ const nestedProviderId = provider && 'info' in provider ? provider.info?.id : undefined;
6
+ if (typeof nestedProviderId === 'string' && nestedProviderId.length > 0) {
7
+ return nestedProviderId;
8
+ }
9
+ return model?.providerID ?? 'unknown';
10
+ };
11
+ const readProviderSource = (provider) => provider?.source ?? 'unknown';
1
12
  export const resolveChatParams = (input, output) => ({
2
13
  sessionID: input.sessionID,
3
14
  agent: input.agent,
4
- providerId: input.provider?.info?.id ?? input.model?.providerID ?? 'unknown',
15
+ providerId: readProviderId(input.provider, input.model),
5
16
  modelId: input.model?.id ?? 'unknown',
6
- providerSource: input.provider?.source ?? 'unknown',
17
+ providerSource: readProviderSource(input.provider),
7
18
  temperature: output.temperature,
8
19
  topP: output.topP,
9
20
  maxOutputTokens: output.maxOutputTokens
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@timefly/opencode-plugin",
3
- "version": "0.2.2",
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
  }