@timefly/opencode-plugin 0.2.7 → 0.2.9

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
@@ -104,26 +104,24 @@ OpenCode plugins run inside the OpenCode process. There is no built-in TimeFly U
104
104
 
105
105
  ## How sync works
106
106
 
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.
107
+ The SDK batches OpenCode events before sending them to TimeFly. This keeps VPS load predictable as usage grows.
108
108
 
109
- ### Timing when does data leave your machine?
109
+ | Setting | Default | Behavior |
110
+ |---------|---------|----------|
111
+ | Debounce | 5s | Flush pending events 5 seconds after the last event in a burst |
112
+ | Max batch | 50 events | Flush immediately when the in-memory buffer is full |
113
+ | Immediate flush | `session_end` | Session totals are sent right away |
110
114
 
111
- | Trigger | What happens |
112
- |---------|----------------|
113
- | LLM request (`chat.params`) | Immediate sync attempt |
114
- | Assistant turn completes | Immediate sync attempt |
115
- | Tool call / result | Immediate sync attempt |
116
- | Session start / end | Immediate sync attempt |
117
- | Next event after failure | Retries queued events first, then sends new ones |
115
+ Typical flow during a coding session:
118
116
 
119
- **Typical flow during a coding session:**
117
+ 1. You send a prompt → `llm_request` is queued
118
+ 2. Model responds → `turn_complete`, tool events, etc. join the same batch
119
+ 3. After ~5s idle (or 50 events), one gzip `POST /ai/sync` sends the batch
120
+ 4. `session_end` flushes immediately so session totals are not delayed
120
121
 
121
- 1. You send a prompt `llm_request` syncs (~instant)
122
- 2. Model responds → `turn_complete` + `llm_response` sync (~instant when turn finishes)
123
- 3. Agent runs tools → each `tool_call` / `tool_result` syncs
124
- 4. Session goes idle → `session_end` with session totals syncs
122
+ Compared with the old per-event sync model, one active agent turn usually produces **1–2 HTTP requests** instead of **10–15+**.
125
123
 
126
- So sync frequency = **how active OpenCode is**, not a fixed cron. A busy session with 10 tool calls and 3 LLM turns can produce 20+ HTTP requests in a few minutes.
124
+ Unlike the VS Code extension (timer every ~2 minutes), OpenCode still syncs in near real time just batched within each active burst.
127
125
 
128
126
  ### What each sync request does
129
127
 
@@ -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,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
+ {"version":3,"file":"event-handlers.d.ts","sourceRoot":"","sources":["../src/event-handlers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAA;AAiBvD,OAAO,EASN,KAAK,gBAAgB,EACrB,MAAM,uBAAuB,CAAA;AAC9B,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAA;AAG/D,KAAK,cAAc,GAAG,UAAU,CAAC,OAAO,oBAAoB,CAAC,CAAC,SAAS,CAAC,CAAA;AAKxE,eAAO,MAAM,oBAAoB,GAChC,iBAAiB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACxC,SAAS,UAAU,CAAC,OAAO,kBAAkB,CAAC,EAC9C,SAAS,cAAc,KACrB,OAAO,CAAC,IAAI,CAed,CAAA;AAED,eAAO,MAAM,iBAAiB,GAC7B,iBAAiB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACxC,SAAS,UAAU,CAAC,OAAO,kBAAkB,CAAC,EAC9C,SAAS,cAAc,KACrB,OAAO,CAAC,IAAI,CAad,CAAA;AAED,eAAO,MAAM,oBAAoB,GAChC,iBAAiB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACxC,SAAS,UAAU,CAAC,OAAO,kBAAkB,CAAC,EAC9C,SAAS,cAAc,KACrB,OAAO,CAAC,IAAI,CA0Dd,CAAA;AAED,eAAO,MAAM,wBAAwB,GACpC,iBAAiB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACxC,SAAS,UAAU,CAAC,OAAO,kBAAkB,CAAC,EAC9C,SAAS,cAAc,KACrB,OAAO,CAAC,IAAI,CAqCd,CAAA;AAED,eAAO,MAAM,sBAAsB,GAClC,iBAAiB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACxC,SAAS,UAAU,CAAC,OAAO,kBAAkB,CAAC,EAC9C,SAAS,cAAc,KACrB,OAAO,CAAC,IAAI,CAWd,CAAA;AAED,eAAO,MAAM,kBAAkB,GAC9B,iBAAiB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACxC,SAAS,UAAU,CAAC,OAAO,kBAAkB,CAAC,EAC9C,SAAS,cAAc,KACrB,OAAO,CAAC,IAAI,CAqBd,CAAA;AAED,eAAO,MAAM,qBAAqB,GACjC,iBAAiB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACxC,SAAS,cAAc,KACrB,OAAO,CAAC,IAAI,CAad,CAAA;AAED,eAAO,MAAM,cAAc,GAC1B,OAAO,gBAAgB,EACvB,SAAS,UAAU,CAAC,OAAO,kBAAkB,CAAC,EAC9C,SAAS,cAAc,KACrB,OAAO,CAAC,IAAI,CAkCd,CAAA"}
@@ -33,8 +33,9 @@ export const handleMessageUpdated = (eventProperties, tracker, publish) => {
33
33
  if (assistantMessage.time.completed === undefined || tracker.hasProcessedMessage(assistantMessage.id)) {
34
34
  return Promise.resolve();
35
35
  }
36
+ tracker.recordSessionStart(assistantMessage.sessionID, assistantMessage.time.created);
36
37
  tracker.markMessageProcessed(assistantMessage.id);
37
- const durationMs = assistantMessage.time.completed - assistantMessage.time.created;
38
+ const durationMs = Math.max(0, assistantMessage.time.completed - assistantMessage.time.created);
38
39
  const tokenMetrics = buildTokenMetrics(assistantMessage.tokens, durationMs);
39
40
  const sessionStats = tracker.getSessionStats(assistantMessage.sessionID);
40
41
  const compactionDelta = tracker.recordTurnTokens(assistantMessage.sessionID, {
@@ -65,6 +66,7 @@ export const handleMessageUpdated = (eventProperties, tracker, publish) => {
65
66
  if (tracker.hasProcessedUserMessage(userMessage.id)) {
66
67
  return Promise.resolve();
67
68
  }
69
+ tracker.recordSessionStart(userMessage.sessionID);
68
70
  tracker.markUserMessageProcessed(userMessage.id);
69
71
  return publish([mapUserMessageInput(userMessage)]);
70
72
  }
@@ -124,7 +126,7 @@ export const handleSessionError = (eventProperties, tracker, publish) => {
124
126
  return publish([
125
127
  mapErrorInput(sessionId, {
126
128
  error_name: errorName,
127
- error_message: errorMessage,
129
+ error_message_length: errorMessage.length,
128
130
  error_scope: 'session'
129
131
  })
130
132
  ]);
@@ -1 +1 @@
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
+ {"version":3,"file":"event-tracker.d.ts","sourceRoot":"","sources":["../src/event-tracker.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,eAAe,GAAG;IAC7B,mBAAmB,EAAE,MAAM,CAAA;IAC3B,kBAAkB,EAAE,MAAM,CAAA;IAC1B,WAAW,EAAE,MAAM,CAAA;CACnB,CAAA;AAED,MAAM,MAAM,YAAY,GAAG;IAC1B,iBAAiB,EAAE,MAAM,CAAA;IACzB,YAAY,EAAE,MAAM,CAAA;IACpB,WAAW,EAAE,MAAM,CAAA;IACnB,SAAS,EAAE,MAAM,CAAA;IACjB,aAAa,EAAE,MAAM,CAAA;IACrB,YAAY,EAAE,MAAM,CAAA;IACpB,SAAS,EAAE,MAAM,CAAA;IACjB,eAAe,EAAE,MAAM,CAAA;IACvB,UAAU,EAAE,MAAM,CAAA;IAClB,UAAU,EAAE,MAAM,CAAA;IAClB,eAAe,EAAE,MAAM,CAAA;IACvB,gBAAgB,EAAE,MAAM,CAAA;IACxB,eAAe,EAAE,MAAM,CAAA;IACvB,iBAAiB,EAAE,MAAM,CAAA;IACzB,mBAAmB,EAAE,MAAM,CAAA;IAC3B,oBAAoB,EAAE,MAAM,CAAA;IAC5B,qBAAqB,EAAE,MAAM,CAAA;IAC7B,oBAAoB,EAAE,MAAM,CAAA;IAC5B,iBAAiB,EAAE,MAAM,CAAA;IACzB,qBAAqB,EAAE,MAAM,CAAA;IAC7B,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,cAAc,EAAE,MAAM,CAAA;CACtB,CAAA;AAkCD,MAAM,MAAM,aAAa,GAAG;IAC3B,aAAa,EAAE,MAAM,CAAA;IACrB,cAAc,EAAE,MAAM,CAAA;IACtB,UAAU,EAAE,MAAM,CAAA;CAClB,CAAA;AAED,MAAM,MAAM,iBAAiB,GAAG;IAC/B,aAAa,EAAE,MAAM,CAAA;IACrB,eAAe,EAAE,MAAM,CAAA;IACvB,gBAAgB,EAAE,MAAM,CAAA;IACxB,eAAe,EAAE,MAAM,CAAA;IACvB,iBAAiB,EAAE,MAAM,CAAA;IACzB,YAAY,EAAE,MAAM,CAAA;IACpB,WAAW,EAAE,MAAM,CAAA;CACnB,CAAA;AAED,MAAM,MAAM,YAAY,GAAG;IAC1B,mBAAmB,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,OAAO,CAAA;IACnD,oBAAoB,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAA;IACjD,uBAAuB,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,OAAO,CAAA;IACvD,wBAAwB,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAA;IACrD,gBAAgB,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAA;IAC7C,iBAAiB,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAA;IAC3C,kBAAkB,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,KAAK,IAAI,CAAA;IACrE,kBAAkB,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,CAAC,YAAY,CAAC,KAAK,IAAI,CAAA;IAC7E,wBAAwB,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,KAAK,IAAI,CAAA;IACjG,uBAAuB,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,mBAAmB,EAAE,MAAM,KAAK,IAAI,CAAA;IACjF,qBAAqB,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,KAAK,IAAI,CAAA;IACvE,gBAAgB,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,iBAAiB,KAAK,eAAe,GAAG,SAAS,CAAA;IACnG,eAAe,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,YAAY,CAAA;IACpD,gBAAgB,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,KAAK,aAAa,CAAA;CAC1E,CAAA;AAgCD,eAAO,MAAM,kBAAkB,QAAO,YA6GrC,CAAA"}
@@ -21,6 +21,11 @@ const emptySessionStats = () => ({
21
21
  primaryProviderSource: '',
22
22
  aiGenerationMs: 0
23
23
  });
24
+ const INVALID_ID_VALUES = new Set(['', 'unknown', 'undefined', 'null']);
25
+ const cleanIdentity = (value) => {
26
+ const cleanedValue = value.trim();
27
+ return INVALID_ID_VALUES.has(cleanedValue.toLowerCase()) ? '' : cleanedValue;
28
+ };
24
29
  const mergeSessionStats = (currentStats, delta) => ({
25
30
  billedInputTokens: currentStats.billedInputTokens + (delta.billedInputTokens ?? 0),
26
31
  outputTokens: currentStats.outputTokens + (delta.outputTokens ?? 0),
@@ -42,8 +47,8 @@ const mergeSessionStats = (currentStats, delta) => ({
42
47
  : (delta.contextAtStartTokens ?? currentStats.contextAtStartTokens),
43
48
  compactionTokensSaved: currentStats.compactionTokensSaved + (delta.compactionTokensSaved ?? 0),
44
49
  totalToolOutputChars: currentStats.totalToolOutputChars + (delta.totalToolOutputChars ?? 0),
45
- primaryProviderId: delta.primaryProviderId ?? currentStats.primaryProviderId,
46
- primaryProviderSource: delta.primaryProviderSource ?? currentStats.primaryProviderSource,
50
+ primaryProviderId: delta.primaryProviderId !== undefined ? cleanIdentity(delta.primaryProviderId) : currentStats.primaryProviderId,
51
+ primaryProviderSource: delta.primaryProviderSource !== undefined ? cleanIdentity(delta.primaryProviderSource) : currentStats.primaryProviderSource,
47
52
  startedAtMs: currentStats.startedAtMs,
48
53
  aiGenerationMs: currentStats.aiGenerationMs + (delta.aiGenerationMs ?? 0)
49
54
  });
@@ -84,8 +89,8 @@ export const createEventTracker = () => {
84
89
  const currentStats = sessionStatsById.get(sessionId) ?? emptySessionStats();
85
90
  sessionStatsById.set(sessionId, {
86
91
  ...currentStats,
87
- primaryProviderId: providerId,
88
- primaryProviderSource: providerSource
92
+ primaryProviderId: cleanIdentity(providerId),
93
+ primaryProviderSource: cleanIdentity(providerSource)
89
94
  });
90
95
  },
91
96
  recordCompactionPending: (sessionId, contextBeforeTokens) => {
@@ -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,MAsEnC,CAAA;AAED,eAAe,qBAAqB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAA;AAgBjD,eAAO,MAAM,qBAAqB,EAAE,MAuEnC,CAAA;AAED,eAAe,qBAAqB,CAAA"}
package/dist/index.js CHANGED
@@ -25,6 +25,7 @@ export const timeflyOpenCodePlugin = ({ client }) => {
25
25
  .then(() => Promise.resolve({
26
26
  event: (input) => runHookSafely(() => handleBusEvent(input.event, tracker, publisher.publish)),
27
27
  'chat.params': (input, output) => runHookSafely(() => {
28
+ tracker.recordSessionStart(input.sessionID);
28
29
  tracker.recordSessionStats(input.sessionID, { requestCount: 1 });
29
30
  const resolvedParams = resolveChatParams(input, output);
30
31
  tracker.recordProviderConnection(resolvedParams.sessionID, resolvedParams.providerId, resolvedParams.providerSource);
@@ -1 +1 @@
1
- {"version":3,"file":"map-opencode-event.d.ts","sourceRoot":"","sources":["../src/map-opencode-event.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"map-opencode-event.d.ts","sourceRoot":"","sources":["../src/map-opencode-event.ts"],"names":[],"mappings":"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"}
@@ -2,7 +2,38 @@ import { buildTokenMetadata, buildTokenMetrics } from './token-usage.js';
2
2
  export const readSessionIdOverride = (sessionId) => typeof process !== 'undefined' && process.env.TIMEFLY_OPENCODE_SESSION_ID
3
3
  ? process.env.TIMEFLY_OPENCODE_SESSION_ID
4
4
  : sessionId;
5
- export const buildModelId = (providerId, modelId) => `${providerId}/${modelId}`;
5
+ const INVALID_ID_VALUES = new Set(['', 'unknown', 'undefined', 'null']);
6
+ const cleanText = (value) => value.trim();
7
+ const cleanIdentity = (value) => {
8
+ const cleanedValue = cleanText(value);
9
+ return INVALID_ID_VALUES.has(cleanedValue.toLowerCase()) ? '' : cleanedValue;
10
+ };
11
+ const cleanNumber = (value) => Number.isFinite(value) ? value : 0;
12
+ const cleanMetadata = (metadata) => Object.fromEntries(Object.entries(metadata).filter(([, metadataValue]) => {
13
+ if (metadataValue === undefined) {
14
+ return false;
15
+ }
16
+ if (typeof metadataValue === 'number') {
17
+ return Number.isFinite(metadataValue);
18
+ }
19
+ if (typeof metadataValue === 'string') {
20
+ return cleanText(metadataValue).length > 0;
21
+ }
22
+ return true;
23
+ }));
24
+ const identityMetadata = (providerId, modelId) => {
25
+ const cleanProviderId = cleanIdentity(providerId);
26
+ const cleanModelId = cleanIdentity(modelId);
27
+ return {
28
+ ...(cleanProviderId ? { provider_id: cleanProviderId } : {}),
29
+ ...(cleanModelId ? { model_id: cleanModelId } : {})
30
+ };
31
+ };
32
+ export const buildModelId = (providerId, modelId) => {
33
+ const cleanProviderId = cleanIdentity(providerId);
34
+ const cleanModelId = cleanIdentity(modelId);
35
+ return cleanProviderId && cleanModelId ? `${cleanProviderId}/${cleanModelId}` : cleanModelId;
36
+ };
6
37
  export const mapSessionStartInput = (sessionInfo) => ({
7
38
  sessionId: readSessionIdOverride(sessionInfo.id),
8
39
  metadata: {
@@ -41,43 +72,46 @@ export const mapSessionEndInput = (sessionId, sessionStats, sessionTiming) => ({
41
72
  user_wait_ms: sessionTiming.userWaitMs
42
73
  }
43
74
  });
44
- export const mapLlmRequestInput = (input) => ({
45
- sessionId: readSessionIdOverride(input.sessionID),
46
- eventType: 'llm_request',
47
- modelId: buildModelId(input.providerId, input.modelId),
48
- planMode: input.agent,
49
- metadata: {
50
- provider_id: input.providerId,
51
- model_id: input.modelId,
52
- provider_source: input.providerSource,
53
- agent: input.agent,
54
- temperature: input.temperature,
55
- top_p: input.topP,
56
- ...(input.maxOutputTokens !== undefined ? { max_output_tokens: input.maxOutputTokens } : {})
57
- }
58
- });
75
+ export const mapLlmRequestInput = (input) => {
76
+ const modelId = buildModelId(input.providerId, input.modelId);
77
+ return {
78
+ sessionId: readSessionIdOverride(input.sessionID),
79
+ eventType: 'llm_request',
80
+ ...(modelId ? { modelId } : {}),
81
+ planMode: input.agent,
82
+ metadata: cleanMetadata({
83
+ ...identityMetadata(input.providerId, input.modelId),
84
+ provider_source: cleanIdentity(input.providerSource),
85
+ agent: input.agent,
86
+ temperature: cleanNumber(input.temperature),
87
+ top_p: cleanNumber(input.topP),
88
+ ...(input.maxOutputTokens !== undefined ? { max_output_tokens: cleanNumber(input.maxOutputTokens) } : {})
89
+ })
90
+ };
91
+ };
59
92
  export const mapAssistantTurnCompleteInput = (message, options) => {
60
93
  if (message.time.completed === undefined) {
61
94
  return undefined;
62
95
  }
63
- const durationMs = message.time.completed - message.time.created;
96
+ const durationMs = Math.max(0, message.time.completed - message.time.created);
64
97
  const tokenMetrics = buildTokenMetrics(message.tokens, durationMs);
98
+ const modelId = buildModelId(message.providerID, message.modelID);
65
99
  return {
66
100
  sessionId: readSessionIdOverride(message.sessionID),
67
101
  eventType: 'turn_complete',
68
- modelId: buildModelId(message.providerID, message.modelID),
102
+ ...(modelId ? { modelId } : {}),
69
103
  planMode: message.mode,
70
104
  inputTokens: tokenMetrics.inputTokens,
71
105
  outputTokens: tokenMetrics.outputTokens,
72
106
  totalTokens: tokenMetrics.totalTokens,
73
107
  durationMs,
74
- metadata: buildTokenMetadata(tokenMetrics, {
108
+ eventAtUtc: new Date(message.time.completed),
109
+ metadata: cleanMetadata(buildTokenMetadata(tokenMetrics, {
75
110
  message_id: message.id,
76
- provider_id: message.providerID,
77
- model_id: message.modelID,
78
- ...(options?.providerSource ? { provider_source: options.providerSource } : {}),
111
+ ...identityMetadata(message.providerID, message.modelID),
112
+ ...(options?.providerSource ? { provider_source: cleanIdentity(options.providerSource) } : {}),
79
113
  context_tokens: tokenMetrics.inputTokens,
80
- cost: message.cost,
114
+ cost: cleanNumber(message.cost),
81
115
  ...(message.finish ? { finish_reason: message.finish } : {}),
82
116
  has_error: Boolean(message.error),
83
117
  ...(message.error ? { error_name: message.error.name } : {}),
@@ -88,31 +122,32 @@ export const mapAssistantTurnCompleteInput = (message, options) => {
88
122
  compaction_tokens_saved: options.compactionDelta.tokensSaved
89
123
  }
90
124
  : {})
91
- })
125
+ }))
92
126
  };
93
127
  };
94
128
  export const mapAssistantLlmResponseInput = (message) => {
95
129
  if (message.time.completed === undefined) {
96
130
  return undefined;
97
131
  }
98
- const durationMs = message.time.completed - message.time.created;
132
+ const durationMs = Math.max(0, message.time.completed - message.time.created);
99
133
  const tokenMetrics = buildTokenMetrics(message.tokens, durationMs);
134
+ const modelId = buildModelId(message.providerID, message.modelID);
100
135
  return {
101
136
  sessionId: readSessionIdOverride(message.sessionID),
102
137
  eventType: 'llm_response',
103
- modelId: buildModelId(message.providerID, message.modelID),
138
+ ...(modelId ? { modelId } : {}),
104
139
  planMode: message.mode,
105
140
  inputTokens: tokenMetrics.inputTokens,
106
141
  outputTokens: tokenMetrics.outputTokens,
107
142
  totalTokens: tokenMetrics.totalTokens,
108
143
  durationMs,
109
- metadata: buildTokenMetadata(tokenMetrics, {
144
+ eventAtUtc: new Date(message.time.completed),
145
+ metadata: cleanMetadata(buildTokenMetadata(tokenMetrics, {
110
146
  message_id: message.id,
111
- provider_id: message.providerID,
112
- model_id: message.modelID,
113
- cost: message.cost,
147
+ ...identityMetadata(message.providerID, message.modelID),
148
+ cost: cleanNumber(message.cost),
114
149
  response_scope: 'message'
115
- })
150
+ }))
116
151
  };
117
152
  };
118
153
  export const mapStepFinishInput = (part) => {
@@ -123,28 +158,30 @@ export const mapStepFinishInput = (part) => {
123
158
  inputTokens: tokenMetrics.inputTokens,
124
159
  outputTokens: tokenMetrics.outputTokens,
125
160
  totalTokens: tokenMetrics.totalTokens,
126
- metadata: buildTokenMetadata(tokenMetrics, {
161
+ metadata: cleanMetadata(buildTokenMetadata(tokenMetrics, {
127
162
  message_id: part.messageID,
128
163
  part_id: part.id,
129
164
  step_reason: part.reason,
130
- cost: part.cost,
165
+ cost: cleanNumber(part.cost),
131
166
  response_scope: 'step'
167
+ }))
168
+ };
169
+ };
170
+ export const mapUserMessageInput = (message) => {
171
+ const modelId = buildModelId(message.model.providerID, message.model.modelID);
172
+ return {
173
+ sessionId: readSessionIdOverride(message.sessionID),
174
+ eventType: 'llm_request',
175
+ ...(modelId ? { modelId } : {}),
176
+ planMode: message.agent,
177
+ metadata: cleanMetadata({
178
+ message_id: message.id,
179
+ ...identityMetadata(message.model.providerID, message.model.modelID),
180
+ agent: message.agent,
181
+ request_scope: 'user_message'
132
182
  })
133
183
  };
134
184
  };
135
- export const mapUserMessageInput = (message) => ({
136
- sessionId: readSessionIdOverride(message.sessionID),
137
- eventType: 'llm_request',
138
- modelId: buildModelId(message.model.providerID, message.model.modelID),
139
- planMode: message.agent,
140
- metadata: {
141
- message_id: message.id,
142
- provider_id: message.model.providerID,
143
- model_id: message.model.modelID,
144
- agent: message.agent,
145
- request_scope: 'user_message'
146
- }
147
- });
148
185
  export const mapCompactionInput = (sessionId, contextBeforeTokens, auto) => ({
149
186
  sessionId: readSessionIdOverride(sessionId),
150
187
  eventType: 'compaction',
@@ -184,7 +221,7 @@ export const mapCommandExecutedInput = (input) => ({
184
221
  toolName: `command:${input.name}`,
185
222
  metadata: {
186
223
  command_name: input.name,
187
- command_arguments: input.arguments,
224
+ command_arguments_length: input.arguments.length,
188
225
  message_id: input.messageID
189
226
  }
190
227
  });
@@ -1 +1 @@
1
- {"version":3,"file":"opencode-readers.d.ts","sourceRoot":"","sources":["../src/opencode-readers.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"opencode-readers.d.ts","sourceRoot":"","sources":["../src/opencode-readers.ts"],"names":[],"mappings":"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"}
@@ -1,23 +1,30 @@
1
1
  const isRecord = (value) => typeof value === 'object' && value !== null;
2
+ const isFiniteNumber = (value) => typeof value === 'number' && Number.isFinite(value);
3
+ const readTelemetryNumber = (value) => typeof value === 'number' ? (Number.isFinite(value) ? value : 0) : undefined;
2
4
  const readTokenUsage = (value) => {
3
5
  if (!isRecord(value)) {
4
6
  return undefined;
5
7
  }
6
8
  const cacheRecord = isRecord(value.cache) ? value.cache : undefined;
7
- if (typeof value.input !== 'number' ||
8
- typeof value.output !== 'number' ||
9
- typeof value.reasoning !== 'number' ||
10
- typeof cacheRecord?.read !== 'number' ||
11
- typeof cacheRecord?.write !== 'number') {
9
+ const inputTokens = readTelemetryNumber(value.input);
10
+ const outputTokens = readTelemetryNumber(value.output);
11
+ const reasoningTokens = readTelemetryNumber(value.reasoning);
12
+ const cacheReadTokens = readTelemetryNumber(cacheRecord?.read);
13
+ const cacheWriteTokens = readTelemetryNumber(cacheRecord?.write);
14
+ if (inputTokens === undefined ||
15
+ outputTokens === undefined ||
16
+ reasoningTokens === undefined ||
17
+ cacheReadTokens === undefined ||
18
+ cacheWriteTokens === undefined) {
12
19
  return undefined;
13
20
  }
14
21
  return {
15
- input: value.input,
16
- output: value.output,
17
- reasoning: value.reasoning,
22
+ input: inputTokens,
23
+ output: outputTokens,
24
+ reasoning: reasoningTokens,
18
25
  cache: {
19
- read: cacheRecord.read,
20
- write: cacheRecord.write
26
+ read: cacheReadTokens,
27
+ write: cacheWriteTokens
21
28
  }
22
29
  };
23
30
  };
@@ -34,13 +41,14 @@ export const readAssistantMessage = (message) => {
34
41
  }
35
42
  const tokenUsage = readTokenUsage(message.tokens);
36
43
  const timeRecord = isRecord(message.time) ? message.time : undefined;
44
+ const cost = readTelemetryNumber(message.cost);
37
45
  if (typeof message.id !== 'string' ||
38
46
  typeof message.sessionID !== 'string' ||
39
47
  typeof message.modelID !== 'string' ||
40
48
  typeof message.providerID !== 'string' ||
41
49
  typeof message.mode !== 'string' ||
42
- typeof message.cost !== 'number' ||
43
- typeof timeRecord?.created !== 'number' ||
50
+ cost === undefined ||
51
+ !isFiniteNumber(timeRecord?.created) ||
44
52
  !tokenUsage) {
45
53
  return undefined;
46
54
  }
@@ -50,12 +58,12 @@ export const readAssistantMessage = (message) => {
50
58
  role: 'assistant',
51
59
  time: {
52
60
  created: timeRecord.created,
53
- completed: typeof timeRecord.completed === 'number' ? timeRecord.completed : undefined
61
+ completed: isFiniteNumber(timeRecord.completed) ? timeRecord.completed : undefined
54
62
  },
55
63
  modelID: message.modelID,
56
64
  providerID: message.providerID,
57
65
  mode: message.mode,
58
- cost: message.cost,
66
+ cost,
59
67
  tokens: tokenUsage,
60
68
  finish: typeof message.finish === 'string' ? message.finish : undefined,
61
69
  error: isRecord(message.error) && typeof message.error.name === 'string' ? { name: message.error.name } : undefined
@@ -100,11 +108,12 @@ export const readStepFinishPart = (part) => {
100
108
  return undefined;
101
109
  }
102
110
  const tokenUsage = readTokenUsage(part.tokens);
111
+ const cost = readTelemetryNumber(part.cost);
103
112
  if (typeof part.id !== 'string' ||
104
113
  typeof part.sessionID !== 'string' ||
105
114
  typeof part.messageID !== 'string' ||
106
115
  typeof part.reason !== 'string' ||
107
- typeof part.cost !== 'number' ||
116
+ cost === undefined ||
108
117
  !tokenUsage) {
109
118
  return undefined;
110
119
  }
@@ -114,7 +123,7 @@ export const readStepFinishPart = (part) => {
114
123
  messageID: part.messageID,
115
124
  type: 'step-finish',
116
125
  reason: part.reason,
117
- cost: part.cost,
126
+ cost,
118
127
  tokens: tokenUsage
119
128
  };
120
129
  };
@@ -1 +1 @@
1
- {"version":3,"file":"token-usage.d.ts","sourceRoot":"","sources":["../src/token-usage.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,kBAAkB,GAAG;IAChC,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;IACd,SAAS,EAAE,MAAM,CAAA;IACjB,KAAK,EAAE;QACN,IAAI,EAAE,MAAM,CAAA;QACZ,KAAK,EAAE,MAAM,CAAA;KACb,CAAA;CACD,CAAA;AAED,MAAM,MAAM,YAAY,GAAG;IAC1B,WAAW,EAAE,MAAM,CAAA;IACnB,YAAY,EAAE,MAAM,CAAA;IACpB,WAAW,EAAE,MAAM,CAAA;IACnB,eAAe,EAAE,MAAM,CAAA;IACvB,eAAe,EAAE,MAAM,CAAA;IACvB,gBAAgB,EAAE,MAAM,CAAA;IACxB,qBAAqB,CAAC,EAAE,MAAM,CAAA;IAC9B,oBAAoB,CAAC,EAAE,MAAM,CAAA;CAC7B,CAAA;AAED,eAAO,MAAM,aAAa,GAAI,YAAY,kBAAkB,KAAG,MACH,CAAA;AAE5D,eAAO,MAAM,iBAAiB,GAAI,YAAY,kBAAkB,EAAE,aAAa,MAAM,KAAG,YAmBvF,CAAA;AAED,eAAO,MAAM,kBAAkB,GAAI,cAAc,YAAY,EAAE,QAAO,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAM,KAAG,MAAM,CAC5H,MAAM,EACN,MAAM,GAAG,MAAM,GAAG,OAAO,CAUxB,CAAA"}
1
+ {"version":3,"file":"token-usage.d.ts","sourceRoot":"","sources":["../src/token-usage.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,kBAAkB,GAAG;IAChC,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;IACd,SAAS,EAAE,MAAM,CAAA;IACjB,KAAK,EAAE;QACN,IAAI,EAAE,MAAM,CAAA;QACZ,KAAK,EAAE,MAAM,CAAA;KACb,CAAA;CACD,CAAA;AAED,MAAM,MAAM,YAAY,GAAG;IAC1B,WAAW,EAAE,MAAM,CAAA;IACnB,YAAY,EAAE,MAAM,CAAA;IACpB,WAAW,EAAE,MAAM,CAAA;IACnB,eAAe,EAAE,MAAM,CAAA;IACvB,eAAe,EAAE,MAAM,CAAA;IACvB,gBAAgB,EAAE,MAAM,CAAA;IACxB,qBAAqB,CAAC,EAAE,MAAM,CAAA;IAC9B,oBAAoB,CAAC,EAAE,MAAM,CAAA;CAC7B,CAAA;AAKD,eAAO,MAAM,aAAa,GAAI,YAAY,kBAAkB,KAAG,MACyD,CAAA;AAExH,eAAO,MAAM,iBAAiB,GAAI,YAAY,kBAAkB,EAAE,aAAa,MAAM,KAAG,YAmBvF,CAAA;AAED,eAAO,MAAM,kBAAkB,GAAI,cAAc,YAAY,EAAE,QAAO,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAM,KAAG,MAAM,CAC5H,MAAM,EACN,MAAM,GAAG,MAAM,GAAG,OAAO,CAUxB,CAAA"}
@@ -1,17 +1,18 @@
1
- export const sumTokenUsage = (tokenUsage) => tokenUsage.input + tokenUsage.output + tokenUsage.reasoning;
1
+ const sanitizeTokenCount = (value) => Number.isFinite(value) && value > 0 ? Math.floor(value) : 0;
2
+ export const sumTokenUsage = (tokenUsage) => sanitizeTokenCount(tokenUsage.input) + sanitizeTokenCount(tokenUsage.output) + sanitizeTokenCount(tokenUsage.reasoning);
2
3
  export const buildTokenMetrics = (tokenUsage, durationMs) => {
3
- const inputTokens = tokenUsage.input;
4
- const outputTokens = tokenUsage.output;
5
- const reasoningTokens = tokenUsage.reasoning;
4
+ const inputTokens = sanitizeTokenCount(tokenUsage.input);
5
+ const outputTokens = sanitizeTokenCount(tokenUsage.output);
6
+ const reasoningTokens = sanitizeTokenCount(tokenUsage.reasoning);
6
7
  const totalTokens = sumTokenUsage(tokenUsage);
7
- const durationSeconds = durationMs && durationMs > 0 ? durationMs / 1000 : undefined;
8
+ const durationSeconds = durationMs && Number.isFinite(durationMs) && durationMs > 0 ? durationMs / 1000 : undefined;
8
9
  return {
9
10
  inputTokens,
10
11
  outputTokens,
11
12
  totalTokens,
12
13
  reasoningTokens,
13
- cacheReadTokens: tokenUsage.cache.read,
14
- cacheWriteTokens: tokenUsage.cache.write,
14
+ cacheReadTokens: sanitizeTokenCount(tokenUsage.cache.read),
15
+ cacheWriteTokens: sanitizeTokenCount(tokenUsage.cache.write),
15
16
  outputTokensPerSecond: durationSeconds !== undefined ? Math.round((outputTokens / durationSeconds) * 100) / 100 : undefined,
16
17
  totalTokensPerSecond: durationSeconds !== undefined ? Math.round((totalTokens / durationSeconds) * 100) / 100 : undefined
17
18
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@timefly/opencode-plugin",
3
- "version": "0.2.7",
3
+ "version": "0.2.9",
4
4
  "description": "TimeFly telemetry plugin for OpenCode — sessions, tokens, models, and tools",
5
5
  "type": "module",
6
6
  "bin": "./dist/cli.js",
@@ -50,7 +50,7 @@
50
50
  },
51
51
  "license": "MIT",
52
52
  "dependencies": {
53
- "@timefly/ai-sdk": "^0.2.0"
53
+ "@timefly/ai-sdk": "^0.2.2"
54
54
  },
55
55
  "peerDependencies": {
56
56
  "@opencode-ai/plugin": ">=1.0.0"