sidekick-shared 0.18.4 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/README.md +23 -2
  2. package/dist/accounts.d.ts +1 -0
  3. package/dist/aggregation/EventAggregator.js +26 -11
  4. package/dist/browser.d.ts +2 -0
  5. package/dist/browser.js +5 -1
  6. package/dist/codexProfiles.d.ts +3 -2
  7. package/dist/codexProfiles.js +382 -52
  8. package/dist/context/sessionContext.d.ts +99 -0
  9. package/dist/context/sessionContext.js +523 -0
  10. package/dist/ensureDefaultAccounts.js +6 -0
  11. package/dist/index.d.ts +3 -1
  12. package/dist/index.js +9 -3
  13. package/dist/modelContext.js +3 -1
  14. package/dist/modelInfo.js +69 -9
  15. package/dist/parsers/codexParser.d.ts +2 -0
  16. package/dist/parsers/codexParser.js +129 -63
  17. package/dist/providers/claudeCode.d.ts +2 -0
  18. package/dist/providers/claudeCode.js +4 -0
  19. package/dist/providers/codex.d.ts +2 -0
  20. package/dist/providers/codex.js +125 -91
  21. package/dist/providers/openCode.d.ts +2 -0
  22. package/dist/providers/openCode.js +4 -0
  23. package/dist/providers/types.d.ts +4 -0
  24. package/dist/report/htmlReportGenerator.js +3 -0
  25. package/dist/report/index.d.ts +1 -1
  26. package/dist/report/index.js +2 -1
  27. package/dist/report/transcriptParser.d.ts +3 -0
  28. package/dist/report/transcriptParser.js +25 -3
  29. package/dist/report/types.d.ts +2 -0
  30. package/dist/schemas/sessionEvent.d.ts +15 -0
  31. package/dist/schemas/sessionEvent.js +14 -1
  32. package/dist/types/sessionEvent.d.ts +16 -1
  33. package/dist/watchers/eventBridge.js +24 -0
  34. package/dist/watchers/factory.d.ts +2 -2
  35. package/dist/watchers/factory.js +7 -5
  36. package/dist/watchers/providerReaderWatcher.d.ts +27 -0
  37. package/dist/watchers/providerReaderWatcher.js +148 -0
  38. package/package.json +1 -1
@@ -46,6 +46,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
46
46
  exports.CodexProvider = void 0;
47
47
  const fs = __importStar(require("fs"));
48
48
  const path = __importStar(require("path"));
49
+ const EventAggregator_1 = require("../aggregation/EventAggregator");
50
+ const sessionContext_1 = require("../context/sessionContext");
49
51
  const codexParser_1 = require("../parsers/codexParser");
50
52
  const codexDatabase_1 = require("./codexDatabase");
51
53
  const codexProfiles_1 = require("../codexProfiles");
@@ -284,8 +286,8 @@ function extractFirstUserMessage(rolloutPath) {
284
286
  // Check event_msg with user_message
285
287
  if (parsed.type === 'event_msg') {
286
288
  const payload = parsed.payload;
287
- if (payload.event?.type === 'user_message' && payload.event.message?.trim()) {
288
- return truncate(payload.event.message, 60);
289
+ if (payload.type === 'user_message' && payload.message?.trim()) {
290
+ return truncate(payload.message, 60);
289
291
  }
290
292
  }
291
293
  }
@@ -301,47 +303,106 @@ function extractFirstUserMessage(rolloutPath) {
301
303
  }
302
304
  /** Extract searchable text from a rollout line. */
303
305
  function extractSearchableText(line) {
306
+ const parts = [];
304
307
  switch (line.type) {
305
- case 'response_item': {
308
+ case 'session_meta': {
306
309
  const payload = line.payload;
307
- const item = payload.item;
308
- if (!item)
309
- return '';
310
+ if (payload.cwd)
311
+ parts.push(payload.cwd);
312
+ if (payload.source)
313
+ parts.push(payload.source);
314
+ if (payload.base_instructions?.text)
315
+ parts.push(payload.base_instructions.text);
316
+ if (payload.git?.branch)
317
+ parts.push(payload.git.branch);
318
+ if (payload.git?.commit)
319
+ parts.push(payload.git.commit);
320
+ return parts.join(' ');
321
+ }
322
+ case 'response_item': {
323
+ const item = line.payload;
310
324
  if (item.type === 'message') {
311
- if (typeof item.content === 'string')
312
- return item.content;
313
- if (Array.isArray(item.content)) {
314
- return item.content
315
- .map((p) => p.text || '')
316
- .filter(Boolean)
317
- .join(' ');
325
+ parts.push(extractTextFromUnknownContent(item.content));
326
+ }
327
+ if (item.type === 'reasoning') {
328
+ const summary = item.summary;
329
+ if (Array.isArray(summary)) {
330
+ for (const entry of summary) {
331
+ if (typeof entry.text === 'string')
332
+ parts.push(entry.text);
333
+ }
318
334
  }
319
335
  }
320
- if (item.type === 'function_call' && item.arguments)
321
- return item.arguments;
322
- if (item.type === 'function_call_output' && item.output)
323
- return item.output;
324
- return '';
336
+ if (item.type === 'function_call') {
337
+ if (typeof item.name === 'string')
338
+ parts.push(item.name);
339
+ if (typeof item.arguments === 'string')
340
+ parts.push(item.arguments);
341
+ }
342
+ if (item.type === 'function_call_output' && typeof item.output === 'string') {
343
+ parts.push(item.output);
344
+ }
345
+ if (item.type === 'local_shell_call') {
346
+ const action = item.action;
347
+ const command = action?.command;
348
+ if (Array.isArray(command))
349
+ parts.push(command.map(String).join(' '));
350
+ if (typeof action?.workdir === 'string')
351
+ parts.push(action.workdir);
352
+ }
353
+ if (item.type === 'custom_tool_call') {
354
+ if (typeof item.name === 'string')
355
+ parts.push(item.name);
356
+ if (typeof item.input === 'string')
357
+ parts.push(item.input);
358
+ }
359
+ if (item.type === 'custom_tool_call_output' && typeof item.output === 'string') {
360
+ parts.push(item.output);
361
+ }
362
+ return parts.filter(Boolean).join(' ');
325
363
  }
326
364
  case 'event_msg': {
327
- const payload = line.payload;
328
- const event = payload.event;
329
- if (!event)
330
- return '';
331
- if (event.message)
332
- return event.message;
333
- if (event.result)
334
- return event.result;
335
- return '';
365
+ const event = line.payload;
366
+ for (const key of ['type', 'message', 'result', 'stdout', 'stderr', 'summary', 'error', 'code', 'tool_name', 'server_name']) {
367
+ if (typeof event[key] === 'string')
368
+ parts.push(event[key]);
369
+ }
370
+ const command = event.command;
371
+ if (Array.isArray(command))
372
+ parts.push(command.map(String).join(' '));
373
+ if (event.arguments && typeof event.arguments === 'object')
374
+ parts.push(JSON.stringify(event.arguments));
375
+ return parts.filter(Boolean).join(' ');
336
376
  }
337
377
  case 'compacted': {
338
378
  const payload = line.payload;
339
379
  return payload.summary || '';
340
380
  }
381
+ case 'turn_context': {
382
+ const payload = line.payload;
383
+ for (const key of ['model', 'cwd', 'approval_policy', 'sandbox_policy', 'effort']) {
384
+ if (typeof payload[key] === 'string')
385
+ parts.push(payload[key]);
386
+ }
387
+ return parts.join(' ');
388
+ }
341
389
  default:
342
390
  return '';
343
391
  }
344
392
  }
393
+ function extractTextFromUnknownContent(content) {
394
+ if (typeof content === 'string')
395
+ return content;
396
+ if (!Array.isArray(content))
397
+ return '';
398
+ return content
399
+ .map(part => {
400
+ const p = part;
401
+ return typeof p.text === 'string' ? p.text : '';
402
+ })
403
+ .filter(Boolean)
404
+ .join(' ');
405
+ }
345
406
  // ---------------------------------------------------------------------------
346
407
  // CodexReader — incremental JSONL reader wrapping CodexRolloutParser
347
408
  // ---------------------------------------------------------------------------
@@ -827,82 +888,55 @@ class CodexProvider {
827
888
  // --- Stats ---
828
889
  readSessionStats(sessionPath) {
829
890
  const sessionId = extractSessionId(path.basename(sessionPath));
830
- let messageCount = 0;
831
- let startTime = '';
832
- let endTime = '';
833
- const tokens = { input: 0, output: 0, cacheWrite: 0, cacheRead: 0 };
834
- const modelUsage = {};
835
- const toolUsage = {};
836
- let compactionEstimate = 0;
837
- let currentModel = 'unknown';
891
+ const aggregator = new EventAggregator_1.EventAggregator({
892
+ providerId: 'codex',
893
+ computeContextSize: usage => usage.inputTokens,
894
+ });
838
895
  try {
839
- const content = fs.readFileSync(sessionPath, 'utf8');
840
- const lines = content.split('\n');
841
- for (const line of lines) {
842
- const trimmed = line.trim();
843
- if (!trimmed || !trimmed.startsWith('{'))
844
- continue;
845
- try {
846
- const parsed = JSON.parse(trimmed);
847
- if (!startTime && parsed.timestamp)
848
- startTime = parsed.timestamp;
849
- if (parsed.timestamp)
850
- endTime = parsed.timestamp;
851
- if (parsed.type === 'turn_context' && parsed.payload?.model)
852
- currentModel = parsed.payload.model;
853
- if (parsed.type === 'compacted')
854
- compactionEstimate++;
855
- if (parsed.type === 'response_item') {
856
- const p = parsed.payload;
857
- if (p?.role === 'user' || p?.role === 'assistant')
858
- messageCount++;
859
- if (p?.type === 'function_call') {
860
- const name = p.name || 'unknown';
861
- toolUsage[name] = (toolUsage[name] || 0) + 1;
862
- }
863
- if (p?.type === 'local_shell_call')
864
- toolUsage['Bash'] = (toolUsage['Bash'] || 0) + 1;
865
- if (p?.type === 'custom_tool_call') {
866
- const name = p.name || 'unknown';
867
- toolUsage[name] = (toolUsage[name] || 0) + 1;
868
- }
869
- }
870
- if (parsed.type === 'event_msg') {
871
- const evt = parsed.payload;
872
- if (evt?.type === 'token_count') {
873
- const usage = evt.info?.last_token_usage || evt.info?.total_token_usage;
874
- if (usage) {
875
- tokens.input = usage.input_tokens || 0;
876
- tokens.output = usage.output_tokens || 0;
877
- tokens.cacheRead = usage.cached_input_tokens || 0;
878
- if (!modelUsage[currentModel])
879
- modelUsage[currentModel] = { calls: 0, tokens: 0 };
880
- modelUsage[currentModel].calls++;
881
- modelUsage[currentModel].tokens = (usage.input_tokens || 0) + (usage.output_tokens || 0);
882
- }
883
- }
884
- }
885
- }
886
- catch { /* skip */ }
896
+ const reader = this.createReader(sessionPath);
897
+ for (const event of reader.readAll()) {
898
+ aggregator.processEvent(event);
887
899
  }
888
900
  }
889
- catch { /* skip */ }
901
+ catch (err) {
902
+ // Return an empty stats shell below. Surface the cause under DEBUG so a
903
+ // malformed rollout doesn't fail silently during dashboard refreshes.
904
+ if (process.env.DEBUG)
905
+ console.error(`CodexProvider.readSessionStats: ${err}`);
906
+ }
907
+ const metrics = aggregator.getMetrics();
908
+ const modelUsage = {};
909
+ for (const model of metrics.modelStats) {
910
+ modelUsage[model.model] = { calls: model.calls, tokens: model.tokens };
911
+ }
912
+ const toolUsage = {};
913
+ for (const tool of metrics.toolStats) {
914
+ toolUsage[tool.name] = tool.successCount + tool.failureCount + tool.pendingCount;
915
+ }
890
916
  return {
891
917
  providerId: 'codex',
892
918
  sessionId,
893
919
  filePath: sessionPath,
894
920
  label: this.extractSessionLabel(sessionPath),
895
- startTime,
896
- endTime,
897
- messageCount,
898
- tokens,
921
+ startTime: metrics.sessionStartTime || '',
922
+ endTime: metrics.lastEventTime || '',
923
+ messageCount: metrics.messageCount,
924
+ tokens: {
925
+ input: metrics.tokens.inputTokens,
926
+ output: metrics.tokens.outputTokens,
927
+ cacheWrite: metrics.tokens.cacheWriteTokens,
928
+ cacheRead: metrics.tokens.cacheReadTokens,
929
+ },
899
930
  modelUsage,
900
931
  toolUsage,
901
- compactionEstimate,
902
- truncationCount: 0,
903
- reportedCost: 0,
932
+ compactionEstimate: metrics.compactionCount,
933
+ truncationCount: metrics.truncationCount,
934
+ reportedCost: metrics.tokens.reportedCost,
904
935
  };
905
936
  }
937
+ readSessionContextSnapshot(sessionPath, options = {}) {
938
+ return (0, sessionContext_1.readSessionContextSnapshot)(this, sessionPath, options);
939
+ }
906
940
  // --- Optional methods ---
907
941
  getContextWindowLimit(modelId) {
908
942
  // Prefer actual model_context_window reported by token_count events
@@ -15,6 +15,7 @@
15
15
  *
16
16
  * @module providers/openCode
17
17
  */
18
+ import type { ReadSessionContextSnapshotOptions, SessionContextSnapshot } from '../context/sessionContext';
18
19
  import type { SessionProviderBase, SessionReader, ProjectFolderInfo, SearchHit, SessionFileStats, ProviderId, ProviderRuntimeStatus } from './types';
19
20
  import type { TokenUsage, SubagentStats, ContextAttribution } from '../types/sessionEvent';
20
21
  /**
@@ -61,6 +62,7 @@ export declare class OpenCodeProvider implements SessionProviderBase {
61
62
  private searchInSessionFromFiles;
62
63
  getProjectsBaseDir(): string;
63
64
  readSessionStats(sessionPath: string): SessionFileStats;
65
+ readSessionContextSnapshot(sessionPath: string, options?: ReadSessionContextSnapshotOptions): SessionContextSnapshot;
64
66
  getSessionMetadata(sessionPath: string): {
65
67
  mtime: Date;
66
68
  } | null;
@@ -55,6 +55,7 @@ const fs = __importStar(require("fs"));
55
55
  const os = __importStar(require("os"));
56
56
  const path = __importStar(require("path"));
57
57
  const child_process_1 = require("child_process");
58
+ const sessionContext_1 = require("../context/sessionContext");
58
59
  const openCodeParser_1 = require("../parsers/openCodeParser");
59
60
  const openCodeDatabase_1 = require("./openCodeDatabase");
60
61
  const modelContext_1 = require("../modelContext");
@@ -1367,6 +1368,9 @@ class OpenCodeProvider {
1367
1368
  reportedCost,
1368
1369
  };
1369
1370
  }
1371
+ readSessionContextSnapshot(sessionPath, options = {}) {
1372
+ return (0, sessionContext_1.readSessionContextSnapshot)(this, sessionPath, options);
1373
+ }
1370
1374
  // --- Optional methods ---
1371
1375
  getSessionMetadata(sessionPath) {
1372
1376
  // Check cache first
@@ -8,6 +8,7 @@
8
8
  * SessionProvider is the original simplified interface maintained for backward
9
9
  * compatibility with existing CLI code.
10
10
  */
11
+ import type { ReadSessionContextSnapshotOptions, SessionContextSnapshot } from '../context/sessionContext';
11
12
  import type { SessionEvent, SubagentStats, TokenUsage, ContextAttribution } from '../types/sessionEvent';
12
13
  export type ProviderId = 'claude-code' | 'opencode' | 'codex';
13
14
  export interface SearchHit {
@@ -138,6 +139,8 @@ export interface SessionProviderBase {
138
139
  getCurrentUsageSnapshot?(sessionPath: string): TokenUsage | null;
139
140
  /** Gets context attribution breakdown from provider data, if available. */
140
141
  getContextAttribution?(sessionPath: string): ContextAttribution | null;
142
+ /** Reads a provider-neutral context evidence snapshot for a session. */
143
+ readSessionContextSnapshot?(sessionPath: string, options?: ReadSessionContextSnapshotOptions): SessionContextSnapshot;
141
144
  /** Reports provider runtime readiness for DB-backed providers. */
142
145
  getRuntimeStatus?(): ProviderRuntimeStatus;
143
146
  /** Tests whether a provider can monitor a directory path, including synthetic DB-backed paths. */
@@ -156,6 +159,7 @@ export interface SessionProvider {
156
159
  findAllSessions(workspacePath: string): string[];
157
160
  getProjectsBaseDir(): string;
158
161
  readSessionStats(sessionPath: string): SessionFileStats;
162
+ readSessionContextSnapshot?(sessionPath: string, options?: ReadSessionContextSnapshotOptions): SessionContextSnapshot;
159
163
  extractSessionLabel(sessionPath: string): string | null;
160
164
  searchInSession(sessionPath: string, query: string, maxResults: number): SearchHit[];
161
165
  getAllProjectFolders(workspacePath?: string): ProjectFolderInfo[];
@@ -217,6 +217,7 @@ tr:hover td { background: rgba(155,109,255,0.05); }
217
217
  .role-badge.summary { background: rgba(107,95,128,0.2); color: var(--text-muted); }
218
218
  .message-ts { color: var(--text-muted); margin-left: auto; }
219
219
  .message-model { color: var(--text-secondary); font-family: monospace; font-size: 11px; }
220
+ .message-source { color: var(--text-muted); font-family: monospace; font-size: 11px; font-style: italic; }
220
221
  .message-tokens { color: var(--text-muted); font-size: 11px; }
221
222
 
222
223
  .message-body { padding: 12px 14px; }
@@ -498,6 +499,7 @@ function generateTranscript(transcript, includeThinking, includeToolDetail) {
498
499
  function renderMessage(entry, includeThinking, includeToolDetail) {
499
500
  const ts = entry.timestamp ? (0, htmlHelpers_1.formatTimestamp)(entry.timestamp) : '';
500
501
  const roleBadge = `<span class="role-badge ${entry.type}">${entry.type}</span>`;
502
+ const sourceStr = entry.sourceLabel ? `<span class="message-source">${(0, htmlHelpers_1.escapeHtml)(entry.sourceLabel)}</span>` : '';
501
503
  const modelStr = entry.model ? `<span class="message-model">${(0, htmlHelpers_1.escapeHtml)(entry.model)}</span>` : '';
502
504
  const tokenStr = entry.usage
503
505
  ? `<span class="message-tokens">${(0, htmlHelpers_1.fmtTokens)(entry.usage.input_tokens + entry.usage.output_tokens)} tokens</span>`
@@ -512,6 +514,7 @@ function renderMessage(entry, includeThinking, includeToolDetail) {
512
514
  return `<div class="message ${entry.type}" data-type="${entry.type}">
513
515
  <div class="message-header">
514
516
  ${roleBadge}
517
+ ${sourceStr}
515
518
  ${modelStr}
516
519
  ${tokenStr}
517
520
  <span class="message-ts">${ts}</span>
@@ -2,7 +2,7 @@
2
2
  * Public API for the report module.
3
3
  */
4
4
  export type { TranscriptContentBlock, TranscriptEntry, HtmlReportOptions } from './types';
5
- export { parseTranscript } from './transcriptParser';
5
+ export { parseTranscript, parseTranscriptFromEvents } from './transcriptParser';
6
6
  export { generateHtmlReport } from './htmlReportGenerator';
7
7
  export { openInBrowser } from './openBrowser';
8
8
  export { escapeHtml, simpleMarkdownToHtml, highlightCodeBlock } from './htmlHelpers';
@@ -3,9 +3,10 @@
3
3
  * Public API for the report module.
4
4
  */
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.highlightCodeBlock = exports.simpleMarkdownToHtml = exports.escapeHtml = exports.openInBrowser = exports.generateHtmlReport = exports.parseTranscript = void 0;
6
+ exports.highlightCodeBlock = exports.simpleMarkdownToHtml = exports.escapeHtml = exports.openInBrowser = exports.generateHtmlReport = exports.parseTranscriptFromEvents = exports.parseTranscript = void 0;
7
7
  var transcriptParser_1 = require("./transcriptParser");
8
8
  Object.defineProperty(exports, "parseTranscript", { enumerable: true, get: function () { return transcriptParser_1.parseTranscript; } });
9
+ Object.defineProperty(exports, "parseTranscriptFromEvents", { enumerable: true, get: function () { return transcriptParser_1.parseTranscriptFromEvents; } });
9
10
  var htmlReportGenerator_1 = require("./htmlReportGenerator");
10
11
  Object.defineProperty(exports, "generateHtmlReport", { enumerable: true, get: function () { return htmlReportGenerator_1.generateHtmlReport; } });
11
12
  var openBrowser_1 = require("./openBrowser");
@@ -2,6 +2,7 @@
2
2
  * Parse Claude Code JSONL session files into typed transcript entries
3
3
  * with full, untruncated content suitable for HTML report rendering.
4
4
  */
5
+ import type { SessionEvent } from '../types/sessionEvent';
5
6
  import type { TranscriptEntry } from './types';
6
7
  /**
7
8
  * Parse a JSONL session file into an array of TranscriptEntry objects.
@@ -10,3 +11,5 @@ import type { TranscriptEntry } from './types';
10
11
  * this preserves full message content for HTML report rendering.
11
12
  */
12
13
  export declare function parseTranscript(sessionPath: string): TranscriptEntry[];
14
+ /** Parse canonical provider events into transcript entries. */
15
+ export declare function parseTranscriptFromEvents(events: SessionEvent[]): TranscriptEntry[];
@@ -38,6 +38,7 @@ var __importStar = (this && this.__importStar) || (function () {
38
38
  })();
39
39
  Object.defineProperty(exports, "__esModule", { value: true });
40
40
  exports.parseTranscript = parseTranscript;
41
+ exports.parseTranscriptFromEvents = parseTranscriptFromEvents;
41
42
  const fs = __importStar(require("fs"));
42
43
  const jsonl_1 = require("../parsers/jsonl");
43
44
  /**
@@ -69,11 +70,23 @@ function parseTranscript(sessionPath) {
69
70
  }
70
71
  return entries;
71
72
  }
73
+ /** Parse canonical provider events into transcript entries. */
74
+ function parseTranscriptFromEvents(events) {
75
+ const entries = [];
76
+ for (const event of events) {
77
+ const entry = eventToTranscriptEntry(event);
78
+ if (entry) {
79
+ entries.push(entry);
80
+ }
81
+ }
82
+ return entries;
83
+ }
72
84
  function eventToTranscriptEntry(event) {
73
85
  if (!event.type || !event.message)
74
86
  return null;
75
87
  const role = event.message.role;
76
88
  const timestamp = event.timestamp || '';
89
+ const sourceLabel = event.message.sourceLabel;
77
90
  const model = event.message.model;
78
91
  const usage = event.message.usage
79
92
  ? {
@@ -85,13 +98,22 @@ function eventToTranscriptEntry(event) {
85
98
  : undefined;
86
99
  const content = extractContentBlocks(event.message.content);
87
100
  // Skip empty entries (warmup messages, etc.)
88
- if (content.length === 0)
89
- return null;
101
+ if (content.length === 0) {
102
+ if (usage || sourceLabel) {
103
+ content.push({ type: 'text', text: sourceLabel ? `${sourceLabel} updated` : 'Usage updated' });
104
+ }
105
+ else {
106
+ return null;
107
+ }
108
+ }
90
109
  let type;
91
110
  // Check event.type first — summary events have role 'assistant' but should be typed as 'summary'
92
111
  if (event.type === 'summary') {
93
112
  type = 'summary';
94
113
  }
114
+ else if (event.type === 'system') {
115
+ type = 'system';
116
+ }
95
117
  else if (role === 'user') {
96
118
  type = 'user';
97
119
  }
@@ -101,7 +123,7 @@ function eventToTranscriptEntry(event) {
101
123
  else {
102
124
  type = 'system';
103
125
  }
104
- return { type, timestamp, model, usage, content };
126
+ return { type, timestamp, sourceLabel, model, usage, content };
105
127
  }
106
128
  function extractContentBlocks(content) {
107
129
  if (!content)
@@ -21,6 +21,8 @@ export interface TranscriptContentBlock {
21
21
  export interface TranscriptEntry {
22
22
  type: 'user' | 'assistant' | 'system' | 'summary';
23
23
  timestamp: string;
24
+ /** Optional human-readable source label (e.g. "developer", "base instructions"). */
25
+ sourceLabel?: string;
24
26
  model?: string;
25
27
  usage?: {
26
28
  input_tokens: number;
@@ -18,6 +18,7 @@ export declare const messageUsageSchema: z.ZodObject<{
18
18
  export declare const sessionMessageSchema: z.ZodObject<{
19
19
  role: z.ZodString;
20
20
  id: z.ZodOptional<z.ZodString>;
21
+ sourceLabel: z.ZodOptional<z.ZodString>;
21
22
  model: z.ZodOptional<z.ZodString>;
22
23
  usage: z.ZodOptional<z.ZodObject<{
23
24
  input_tokens: z.ZodNumber;
@@ -42,10 +43,12 @@ export declare const sessionEventSchema: z.ZodObject<{
42
43
  tool_use: "tool_use";
43
44
  tool_result: "tool_result";
44
45
  summary: "summary";
46
+ system: "system";
45
47
  }>;
46
48
  message: z.ZodObject<{
47
49
  role: z.ZodString;
48
50
  id: z.ZodOptional<z.ZodString>;
51
+ sourceLabel: z.ZodOptional<z.ZodString>;
49
52
  model: z.ZodOptional<z.ZodString>;
50
53
  usage: z.ZodOptional<z.ZodObject<{
51
54
  input_tokens: z.ZodNumber;
@@ -65,6 +68,18 @@ export declare const sessionEventSchema: z.ZodObject<{
65
68
  bypassPermissions: "bypassPermissions";
66
69
  plan: "plan";
67
70
  }>>;
71
+ rateLimits: z.ZodOptional<z.ZodObject<{
72
+ primary: z.ZodOptional<z.ZodObject<{
73
+ usedPercent: z.ZodNumber;
74
+ windowMinutes: z.ZodNumber;
75
+ resetsAt: z.ZodNumber;
76
+ }, z.core.$strip>>;
77
+ secondary: z.ZodOptional<z.ZodObject<{
78
+ usedPercent: z.ZodNumber;
79
+ windowMinutes: z.ZodNumber;
80
+ resetsAt: z.ZodNumber;
81
+ }, z.core.$strip>>;
82
+ }, z.core.$strip>>;
68
83
  tool: z.ZodOptional<z.ZodObject<{
69
84
  name: z.ZodString;
70
85
  input: z.ZodRecord<z.ZodString, z.ZodUnknown>;
@@ -23,6 +23,7 @@ exports.messageUsageSchema = zod_1.z.object({
23
23
  exports.sessionMessageSchema = zod_1.z.object({
24
24
  role: zod_1.z.string(),
25
25
  id: zod_1.z.string().optional(),
26
+ sourceLabel: zod_1.z.string().optional(),
26
27
  model: zod_1.z.string().optional(),
27
28
  usage: exports.messageUsageSchema.optional(),
28
29
  content: zod_1.z.unknown().optional(),
@@ -36,11 +37,23 @@ exports.permissionModeSchema = zod_1.z.enum([
36
37
  ]);
37
38
  // ── SessionEvent ──
38
39
  exports.sessionEventSchema = zod_1.z.object({
39
- type: zod_1.z.enum(['user', 'assistant', 'tool_use', 'tool_result', 'summary']),
40
+ type: zod_1.z.enum(['user', 'assistant', 'tool_use', 'tool_result', 'summary', 'system']),
40
41
  message: exports.sessionMessageSchema,
41
42
  timestamp: zod_1.z.string(),
42
43
  isSidechain: zod_1.z.boolean().optional(),
43
44
  permissionMode: exports.permissionModeSchema.optional(),
45
+ rateLimits: zod_1.z.object({
46
+ primary: zod_1.z.object({
47
+ usedPercent: zod_1.z.number(),
48
+ windowMinutes: zod_1.z.number(),
49
+ resetsAt: zod_1.z.number(),
50
+ }).optional(),
51
+ secondary: zod_1.z.object({
52
+ usedPercent: zod_1.z.number(),
53
+ windowMinutes: zod_1.z.number(),
54
+ resetsAt: zod_1.z.number(),
55
+ }).optional(),
56
+ }).optional(),
44
57
  tool: zod_1.z.object({
45
58
  name: zod_1.z.string(),
46
59
  input: zod_1.z.record(zod_1.z.string(), zod_1.z.unknown()),
@@ -37,6 +37,8 @@ export interface SessionMessage {
37
37
  role: string;
38
38
  /** Optional message identifier (for deduplication) */
39
39
  id?: string;
40
+ /** Optional human-readable source label (e.g. "developer", "base instructions"). */
41
+ sourceLabel?: string;
40
42
  /** Model identifier (e.g., "claude-opus-4-20250514") */
41
43
  model?: string;
42
44
  /** Token usage statistics (only present in assistant messages) */
@@ -52,7 +54,7 @@ export interface SessionMessage {
52
54
  */
53
55
  export interface SessionEvent {
54
56
  /** Event type discriminator */
55
- type: 'user' | 'assistant' | 'tool_use' | 'tool_result' | 'summary';
57
+ type: 'user' | 'assistant' | 'tool_use' | 'tool_result' | 'summary' | 'system';
56
58
  /** Message data containing role, model, usage */
57
59
  message: SessionMessage;
58
60
  /** ISO 8601 timestamp of event */
@@ -61,6 +63,19 @@ export interface SessionEvent {
61
63
  isSidechain?: boolean;
62
64
  /** Permission mode active when this event occurred */
63
65
  permissionMode?: 'default' | 'acceptEdits' | 'bypassPermissions' | 'plan';
66
+ /** Subscription rate limits, when provided by the session source. */
67
+ rateLimits?: {
68
+ primary?: {
69
+ usedPercent: number;
70
+ windowMinutes: number;
71
+ resetsAt: number;
72
+ };
73
+ secondary?: {
74
+ usedPercent: number;
75
+ windowMinutes: number;
76
+ resetsAt: number;
77
+ };
78
+ };
64
79
  /** Tool use details (when type is 'tool_use') */
65
80
  tool?: {
66
81
  name: string;
@@ -30,6 +30,7 @@ function toFollowEvents(event, providerId) {
30
30
  : undefined;
31
31
  const cost = usage?.reported_cost;
32
32
  const model = event.message?.model;
33
+ const rateLimits = event.rateLimits;
33
34
  switch (event.type) {
34
35
  case 'user': {
35
36
  const content = event.message?.content;
@@ -118,6 +119,24 @@ function toFollowEvents(event, providerId) {
118
119
  });
119
120
  break;
120
121
  }
122
+ case 'system': {
123
+ const text = extractTextContent(event.message?.content);
124
+ const label = event.message?.sourceLabel || event.message?.role || 'system';
125
+ const summary = text || (usage
126
+ ? `Tokens: ${usage.input_tokens || 0} in / ${usage.output_tokens || 0} out`
127
+ : label);
128
+ events.push({
129
+ providerId, type: 'system', timestamp: ts,
130
+ summary: label && text ? `${label}: ${text}` : summary,
131
+ tokens,
132
+ cacheTokens,
133
+ cost,
134
+ model,
135
+ rateLimits,
136
+ raw: event,
137
+ });
138
+ break;
139
+ }
121
140
  default: {
122
141
  // Handle 'result' and other types as system events
123
142
  const evtType = event.type;
@@ -136,6 +155,11 @@ function toFollowEvents(event, providerId) {
136
155
  e.permissionMode = permissionMode;
137
156
  }
138
157
  }
158
+ if (rateLimits) {
159
+ for (const e of events) {
160
+ e.rateLimits = e.rateLimits ?? rateLimits;
161
+ }
162
+ }
139
163
  return events;
140
164
  }
141
165
  // ── Helpers ──
@@ -1,10 +1,10 @@
1
1
  /**
2
2
  * Factory for creating the correct session watcher by provider.
3
3
  */
4
- import type { SessionProvider } from '../providers/types';
4
+ import type { SessionProviderBase } from '../providers/types';
5
5
  import type { SessionWatcher, SessionWatcherCallbacks } from './types';
6
6
  export interface CreateWatcherOptions {
7
- provider: SessionProvider;
7
+ provider: SessionProviderBase;
8
8
  workspacePath: string;
9
9
  sessionId?: string;
10
10
  callbacks: SessionWatcherCallbacks;