sidekick-shared 0.24.4 → 0.25.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 (98) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/README.md +200 -52
  3. package/dist/accountChangeSignal.d.ts +3 -0
  4. package/dist/accountChangeSignal.js +19 -0
  5. package/dist/accountChanges.d.ts +21 -0
  6. package/dist/accountChanges.js +151 -0
  7. package/dist/accountManager.d.ts +13 -0
  8. package/dist/accountManager.js +60 -32
  9. package/dist/accountRegistry.d.ts +3 -1
  10. package/dist/accountRegistry.js +64 -41
  11. package/dist/accounts.js +40 -19
  12. package/dist/autoSwitch.d.ts +2 -1
  13. package/dist/autoSwitch.js +32 -7
  14. package/dist/browser.d.ts +10 -3
  15. package/dist/browser.js +17 -1
  16. package/dist/codexProfiles.d.ts +14 -0
  17. package/dist/codexProfiles.js +268 -116
  18. package/dist/codexQuotaWatcher.d.ts +6 -0
  19. package/dist/codexQuotaWatcher.js +101 -47
  20. package/dist/context/sessionContext.js +2 -4
  21. package/dist/ensureDefaultAccounts.js +4 -4
  22. package/dist/index.d.ts +32 -15
  23. package/dist/index.js +54 -9
  24. package/dist/modelAliases.d.ts +13 -0
  25. package/dist/modelAliases.js +58 -0
  26. package/dist/modelCatalog.d.ts +32 -0
  27. package/dist/modelCatalog.js +145 -0
  28. package/dist/modelContext.d.ts +36 -3
  29. package/dist/modelContext.js +109 -34
  30. package/dist/modelInfo.d.ts +30 -1
  31. package/dist/modelInfo.js +116 -26
  32. package/dist/multiProviderQuotaService.d.ts +8 -0
  33. package/dist/multiProviderQuotaService.js +64 -3
  34. package/dist/node.d.ts +8 -2
  35. package/dist/node.js +11 -1
  36. package/dist/observedContextWindows.d.ts +13 -0
  37. package/dist/observedContextWindows.js +13 -0
  38. package/dist/observedSessionCollector.d.ts +75 -4
  39. package/dist/observedSessionCollector.js +452 -42
  40. package/dist/parsers/codexHistory.d.ts +32 -0
  41. package/dist/parsers/codexHistory.js +125 -0
  42. package/dist/parsers/mcpToolName.d.ts +26 -0
  43. package/dist/parsers/mcpToolName.js +31 -0
  44. package/dist/parsers/sessionActivityDetector.d.ts +6 -0
  45. package/dist/parsers/sessionActivityDetector.js +17 -0
  46. package/dist/parsers/sessionPathResolver.d.ts +11 -4
  47. package/dist/parsers/sessionPathResolver.js +11 -4
  48. package/dist/paths.d.ts +7 -3
  49. package/dist/paths.js +7 -3
  50. package/dist/providerIds.d.ts +11 -0
  51. package/dist/providerIds.js +9 -0
  52. package/dist/providerQuota.d.ts +3 -1
  53. package/dist/providerQuota.js +3 -0
  54. package/dist/providers/claudeCode.d.ts +11 -1
  55. package/dist/providers/claudeCode.js +222 -39
  56. package/dist/providers/codex.d.ts +39 -1
  57. package/dist/providers/codex.js +326 -39
  58. package/dist/providers/codexDatabase.d.ts +9 -0
  59. package/dist/providers/codexDatabase.js +107 -2
  60. package/dist/providers/diagnostics.d.ts +13 -0
  61. package/dist/providers/diagnostics.js +88 -0
  62. package/dist/providers/factory.d.ts +14 -0
  63. package/dist/providers/factory.js +57 -0
  64. package/dist/providers/openCode.d.ts +20 -1
  65. package/dist/providers/openCode.js +361 -11
  66. package/dist/providers/openCodeDatabase.d.ts +10 -0
  67. package/dist/providers/openCodeDatabase.js +69 -0
  68. package/dist/providers/types.d.ts +57 -2
  69. package/dist/providers/types.js +4 -0
  70. package/dist/quota.d.ts +4 -1
  71. package/dist/quota.js +3 -7
  72. package/dist/quotaHistory.d.ts +3 -2
  73. package/dist/quotaHistory.js +2 -2
  74. package/dist/quotaPoller.d.ts +13 -1
  75. package/dist/quotaPoller.js +65 -3
  76. package/dist/quotaSnapshots.js +8 -77
  77. package/dist/schemas/accountManager.d.ts +2 -2
  78. package/dist/schemas/accountManager.js +2 -4
  79. package/dist/schemas/accountStatus.d.ts +3 -0
  80. package/dist/schemas/accountStatus.js +1 -0
  81. package/dist/schemas/index.d.ts +1 -0
  82. package/dist/schemas/index.js +7 -2
  83. package/dist/schemas/observedSessionV1.d.ts +27 -26
  84. package/dist/schemas/observedSessionV1.js +6 -4
  85. package/dist/schemas/quota.d.ts +2 -2
  86. package/dist/schemas/quota.js +3 -6
  87. package/dist/schemas/quotaHistory.d.ts +2 -2
  88. package/dist/schemas/tokenEstimation.d.ts +1 -1
  89. package/dist/sessionMonitor.d.ts +15 -0
  90. package/dist/sessionMonitor.js +142 -0
  91. package/dist/sessionPreviews.d.ts +85 -0
  92. package/dist/sessionPreviews.js +549 -0
  93. package/dist/sessionTranscripts.js +10 -4
  94. package/dist/types/observedSessionV1.d.ts +6 -2
  95. package/dist/types/observedSessionV1.js +14 -8
  96. package/dist/writers/atomic.d.ts +20 -0
  97. package/dist/writers/atomic.js +190 -19
  98. package/package.json +22 -1
@@ -49,6 +49,7 @@ const path = __importStar(require("path"));
49
49
  const string_decoder_1 = require("string_decoder");
50
50
  const sessionContext_1 = require("../context/sessionContext");
51
51
  const jsonl_1 = require("../parsers/jsonl");
52
+ const diagnostics_1 = require("./diagnostics");
52
53
  const sessionPathResolver_1 = require("../parsers/sessionPathResolver");
53
54
  const subagentScanner_1 = require("../parsers/subagentScanner");
54
55
  const modelContext_1 = require("../modelContext");
@@ -88,6 +89,42 @@ function extractSearchableText(event) {
88
89
  }
89
90
  return '';
90
91
  }
92
+ function extractClaudeLabelFromPrefix(chunk) {
93
+ for (const line of chunk.split('\n')) {
94
+ const trimmed = line.trim();
95
+ if (!trimmed)
96
+ continue;
97
+ try {
98
+ const event = JSON.parse(trimmed);
99
+ if (event.type !== 'user')
100
+ continue;
101
+ const content = event.message?.content;
102
+ if (!content)
103
+ continue;
104
+ let text = null;
105
+ if (typeof content === 'string') {
106
+ text = content.trim();
107
+ }
108
+ else if (Array.isArray(content)) {
109
+ const textBlock = content.find((block) => isTypedBlock(block) &&
110
+ block.type === 'text' &&
111
+ typeof block.text === 'string' &&
112
+ block.text.trim().length > 0);
113
+ if (textBlock && isTypedBlock(textBlock) && typeof textBlock.text === 'string') {
114
+ text = textBlock.text.trim();
115
+ }
116
+ }
117
+ if (text) {
118
+ const compact = text.replace(/\s+/g, ' ');
119
+ return compact.length > 60 ? compact.substring(0, 57) + '...' : compact;
120
+ }
121
+ }
122
+ catch {
123
+ // Skip malformed lines.
124
+ }
125
+ }
126
+ return null;
127
+ }
91
128
  /**
92
129
  * Incremental JSONL reader for Claude Code session files.
93
130
  *
@@ -234,8 +271,12 @@ function normalizeClaudeUsage(event) {
234
271
  class ClaudeCodeProvider {
235
272
  id = 'claude-code';
236
273
  displayName = 'Claude Code';
274
+ diagnostics;
237
275
  /** Runtime-reported context window limit (overrides static map when set). */
238
276
  dynamicContextWindowLimit = null;
277
+ constructor(options = {}) {
278
+ this.diagnostics = new diagnostics_1.ProviderDiagnosticTracker(this.id, options);
279
+ }
239
280
  // --- Path resolution ---
240
281
  getSessionDirectory(workspacePath) {
241
282
  return (0, sessionPathResolver_1.getSessionDirectory)(workspacePath);
@@ -250,6 +291,43 @@ class ClaudeCodeProvider {
250
291
  findAllSessions(workspacePath) {
251
292
  return (0, sessionPathResolver_1.findAllSessions)(workspacePath);
252
293
  }
294
+ findSessionById(workspacePath, sessionId) {
295
+ const normalizedId = sessionId.trim();
296
+ if (!normalizedId || path.basename(normalizedId) !== normalizedId)
297
+ return null;
298
+ try {
299
+ const directories = new Set();
300
+ directories.add(this.getSessionDirectory(workspacePath));
301
+ const discovered = this.discoverSessionDirectory(workspacePath);
302
+ if (discovered)
303
+ directories.add(discovered);
304
+ for (const folder of this.getAllProjectFolders(workspacePath))
305
+ directories.add(folder.dir);
306
+ for (const directory of directories) {
307
+ const direct = path.join(directory, `${normalizedId}.jsonl`);
308
+ try {
309
+ if (fs.statSync(direct).isFile()) {
310
+ this.diagnostics.available('findSessionById');
311
+ return direct;
312
+ }
313
+ }
314
+ catch {
315
+ // Continue with directory-shaped lookup for worktree layouts.
316
+ }
317
+ const match = this.findSessionsInDirectory(directory).find((candidate) => this.getSessionId(candidate) === normalizedId);
318
+ if (match) {
319
+ this.diagnostics.available('findSessionById');
320
+ return match;
321
+ }
322
+ }
323
+ this.recordHomeStatus('findSessionById');
324
+ return null;
325
+ }
326
+ catch {
327
+ this.recordHomeStatus('findSessionById');
328
+ return null;
329
+ }
330
+ }
253
331
  /** Backward-compatible alias for findAllSessions. */
254
332
  findSessionFiles(workspacePath) {
255
333
  return this.findAllSessions(workspacePath);
@@ -260,6 +338,97 @@ class ClaudeCodeProvider {
260
338
  getAllProjectFolders(workspacePath) {
261
339
  return (0, sessionPathResolver_1.getAllProjectFolders)(workspacePath);
262
340
  }
341
+ listAllSessionFiles() {
342
+ const results = [];
343
+ const seen = new Set();
344
+ for (const folder of this.getAllProjectFolders()) {
345
+ for (const sessionPath of this.findSessionsInDirectory(folder.dir)) {
346
+ if (seen.has(sessionPath))
347
+ continue;
348
+ seen.add(sessionPath);
349
+ try {
350
+ results.push({ path: sessionPath, mtime: fs.statSync(sessionPath).mtime });
351
+ }
352
+ catch {
353
+ // Skip files that vanish between listing and stat.
354
+ }
355
+ }
356
+ }
357
+ this.recordHomeStatus('listAllSessionFiles');
358
+ return results;
359
+ }
360
+ async listSessionFilesAsync(workspacePath) {
361
+ const projectsRoot = this.getProjectsBaseDir();
362
+ let projectEntries = [];
363
+ try {
364
+ projectEntries = await fs.promises.readdir(projectsRoot, { withFileTypes: true });
365
+ }
366
+ catch {
367
+ this.recordHomeStatus('listSessionFilesAsync');
368
+ return [];
369
+ }
370
+ const encodedWorkspace = workspacePath ? (0, sessionPathResolver_1.encodeWorkspacePath)(workspacePath).toLowerCase() : null;
371
+ const workspaceBasename = workspacePath
372
+ ? path
373
+ .basename(workspacePath)
374
+ .replace(/[^a-zA-Z0-9]/g, '-')
375
+ .toLowerCase()
376
+ : null;
377
+ const directories = [];
378
+ for (const entry of projectEntries) {
379
+ const directory = path.join(projectsRoot, entry.name);
380
+ try {
381
+ const stat = entry.isDirectory() ? null : await fs.promises.stat(directory);
382
+ if (!entry.isDirectory() && !stat?.isDirectory())
383
+ continue;
384
+ }
385
+ catch {
386
+ continue;
387
+ }
388
+ const name = entry.name.toLowerCase();
389
+ if (encodedWorkspace &&
390
+ name !== encodedWorkspace &&
391
+ !name.startsWith(`${encodedWorkspace}-`) &&
392
+ name !== workspaceBasename &&
393
+ !name.endsWith(`-${workspaceBasename}`)) {
394
+ continue;
395
+ }
396
+ directories.push(directory);
397
+ await new Promise((resolve) => setImmediate(resolve));
398
+ }
399
+ const results = [];
400
+ for (const directory of directories) {
401
+ let entries = [];
402
+ try {
403
+ entries = await fs.promises.readdir(directory, { withFileTypes: true });
404
+ }
405
+ catch {
406
+ continue;
407
+ }
408
+ for (const entry of entries) {
409
+ if (!entry.isFile() || !this.isSessionFile(entry.name))
410
+ continue;
411
+ const sessionPath = path.join(directory, entry.name);
412
+ try {
413
+ const stat = await fs.promises.stat(sessionPath);
414
+ if (stat.size > 0) {
415
+ results.push({
416
+ path: sessionPath,
417
+ mtime: stat.mtime,
418
+ sizeBytes: stat.size,
419
+ sessionId: this.getSessionId(sessionPath),
420
+ });
421
+ }
422
+ }
423
+ catch {
424
+ // Skip files that vanish between discovery and stat.
425
+ }
426
+ await new Promise((resolve) => setImmediate(resolve));
427
+ }
428
+ }
429
+ this.recordHomeStatus('listSessionFilesAsync');
430
+ return results.sort((left, right) => right.mtime.getTime() - left.mtime.getTime());
431
+ }
263
432
  // --- File identification ---
264
433
  isSessionFile(filename) {
265
434
  return filename.endsWith('.jsonl');
@@ -278,50 +447,32 @@ class ClaudeCodeProvider {
278
447
  fs.closeSync(fd);
279
448
  if (bytesRead === 0)
280
449
  return null;
281
- const chunk = buffer.toString('utf-8', 0, bytesRead);
282
- const lines = chunk.split('\n');
283
- for (const line of lines) {
284
- const trimmed = line.trim();
285
- if (!trimmed)
286
- continue;
287
- try {
288
- const event = JSON.parse(trimmed);
289
- if (event.type !== 'user')
290
- continue;
291
- const content = event.message?.content;
292
- if (!content)
293
- continue;
294
- let text = null;
295
- if (typeof content === 'string') {
296
- text = content.trim();
297
- }
298
- else if (Array.isArray(content)) {
299
- const textBlock = content.find((block) => isTypedBlock(block) &&
300
- block.type === 'text' &&
301
- typeof block.text === 'string' &&
302
- block.text.trim().length > 0);
303
- if (textBlock && isTypedBlock(textBlock) && typeof textBlock.text === 'string') {
304
- text = textBlock.text.trim();
305
- }
306
- }
307
- if (text && text.length > 0) {
308
- text = text.replace(/\s+/g, ' ');
309
- if (text.length > 60) {
310
- text = text.substring(0, 57) + '...';
311
- }
312
- return text;
313
- }
314
- }
315
- catch {
316
- // Skip malformed lines
317
- }
318
- }
319
- return null;
450
+ return extractClaudeLabelFromPrefix(buffer.toString('utf-8', 0, bytesRead));
320
451
  }
321
452
  catch {
322
453
  return null;
323
454
  }
324
455
  }
456
+ async extractSessionLabelsAsync(sessionPaths) {
457
+ const results = new Map();
458
+ for (const sessionPath of sessionPaths) {
459
+ let handle = null;
460
+ try {
461
+ handle = await fs.promises.open(sessionPath, 'r');
462
+ const buffer = Buffer.alloc(8192);
463
+ const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);
464
+ results.set(sessionPath, extractClaudeLabelFromPrefix(buffer.toString('utf8', 0, bytesRead)));
465
+ }
466
+ catch {
467
+ results.set(sessionPath, null);
468
+ }
469
+ finally {
470
+ await handle?.close().catch(() => undefined);
471
+ }
472
+ await new Promise((resolve) => setImmediate(resolve));
473
+ }
474
+ return results;
475
+ }
325
476
  // --- Data reading ---
326
477
  createReader(sessionPath) {
327
478
  return new ClaudeCodeReader(sessionPath);
@@ -379,6 +530,9 @@ class ClaudeCodeProvider {
379
530
  getProjectsBaseDir() {
380
531
  return path.join(os.homedir(), '.claude', 'projects');
381
532
  }
533
+ getWatchRoots() {
534
+ return [this.getProjectsBaseDir()];
535
+ }
382
536
  // --- Stats ---
383
537
  readSessionStats(sessionPath) {
384
538
  const sessionId = path.basename(sessionPath, '.jsonl');
@@ -473,6 +627,22 @@ class ClaudeCodeProvider {
473
627
  return (0, sessionContext_1.readSessionContextSnapshot)(this, sessionPath, options);
474
628
  }
475
629
  // --- Optional methods ---
630
+ getRuntimeStatus() {
631
+ try {
632
+ fs.accessSync(this.getProjectsBaseDir(), fs.constants.R_OK);
633
+ return { available: true, kind: 'available' };
634
+ }
635
+ catch {
636
+ return {
637
+ available: false,
638
+ kind: 'home_unavailable',
639
+ message: 'Claude Code session home is unavailable.',
640
+ };
641
+ }
642
+ }
643
+ getLastOperationStatus() {
644
+ return this.diagnostics.getLastOperationStatus();
645
+ }
476
646
  getContextWindowLimit(modelId) {
477
647
  if (this.dynamicContextWindowLimit)
478
648
  return this.dynamicContextWindowLimit;
@@ -534,5 +704,18 @@ class ClaudeCodeProvider {
534
704
  dispose() {
535
705
  this.dynamicContextWindowLimit = null;
536
706
  }
707
+ recordHomeStatus(operation) {
708
+ const status = this.getRuntimeStatus();
709
+ if (status.available) {
710
+ this.diagnostics.available(operation);
711
+ return;
712
+ }
713
+ this.diagnostics.degraded(operation, status, {
714
+ kind: 'home_unavailable',
715
+ severity: 'info',
716
+ phase: 'enumerate',
717
+ message: status.message ?? 'Claude Code session home is unavailable.',
718
+ });
719
+ }
537
720
  }
538
721
  exports.ClaudeCodeProvider = ClaudeCodeProvider;
@@ -9,9 +9,29 @@
9
9
  * Ported from sidekick-vscode/src/services/providers/CodexSessionProvider.ts
10
10
  */
11
11
  import type { ReadSessionContextSnapshotOptions, SessionContextSnapshot } from '../context/sessionContext';
12
- import type { SessionProviderBase, SessionReader, ProjectFolderInfo, SearchHit, SessionFileStats, ProviderId } from './types';
12
+ import type { SessionProviderBase, SessionReader, ProjectFolderInfo, SearchHit, SessionFileInfo, SessionFileStats, ProviderId, ProviderOperationStatus, ProviderRuntimeStatus, SessionProviderOptions } from './types';
13
13
  import type { SubagentStats, TokenUsage } from '../types/sessionEvent';
14
14
  import type { CodexRateLimits } from '../types/codex';
15
+ export interface FindCodexRolloutFileOptions {
16
+ /**
17
+ * Codex home whose `sessions/` subtree to search. Defaults to every
18
+ * monitored home: `$CODEX_HOME` if set, else `~/.codex`, plus managed
19
+ * profile homes that recorded sessions.
20
+ */
21
+ codexHome?: string;
22
+ }
23
+ /**
24
+ * Locates the rollout file for a Codex session id by walking
25
+ * `<home>/sessions/YYYY/MM/DD/rollout-<timestamp>-<id>.jsonl`.
26
+ *
27
+ * Filename-only match, case-insensitive on the id — no file contents are read
28
+ * and files are only stat'ed once their name matches, so the walk stays cheap
29
+ * even over a large history. When the same id exists in more than one home,
30
+ * the most recently modified copy wins. Returns `null` for an unknown, empty,
31
+ * or whitespace id. Consumers who want the database's answer instead can query
32
+ * the already-public `CodexDatabase` (requires the external `sqlite3` binary).
33
+ */
34
+ export declare function findCodexRolloutFile(sessionId: string, options?: FindCodexRolloutFileOptions): string | null;
15
35
  /**
16
36
  * Session provider for Codex CLI.
17
37
  *
@@ -23,23 +43,39 @@ export declare class CodexProvider implements SessionProviderBase {
23
43
  readonly displayName = "Codex CLI";
24
44
  private db;
25
45
  private dbInitialized;
46
+ private dbStatus;
47
+ private readonly diagnostics;
26
48
  private dynamicContextWindowLimit;
27
49
  private lastRateLimitsData;
28
50
  private lastTokenUsageData;
51
+ constructor(options?: SessionProviderOptions);
29
52
  /** Lazy-initialize database connection. */
30
53
  private ensureDb;
54
+ private ensureDbAsync;
55
+ /**
56
+ * Cache an async-created database only after an operation proved it usable.
57
+ * Sync paths treat a cached `db` as "open() succeeded", so adopting an
58
+ * unproven instance would silently disable their file fallbacks.
59
+ */
60
+ private adoptDbIfAvailable;
61
+ getRuntimeStatus(): ProviderRuntimeStatus;
62
+ getLastOperationStatus(): ProviderOperationStatus;
31
63
  getSessionDirectory(workspacePath: string): string;
32
64
  discoverSessionDirectory(workspacePath: string): string | null;
33
65
  findActiveSession(workspacePath: string): string | null;
34
66
  findAllSessions(workspacePath: string): string[];
67
+ findSessionById(_workspacePath: string, sessionId: string): string | null;
35
68
  /** Backward-compatible alias for findAllSessions. */
36
69
  findSessionFiles(workspacePath: string): string[];
37
70
  findSessionsInDirectory(dir: string): string[];
71
+ listAllSessionFiles(): SessionFileInfo[];
72
+ listSessionFilesAsync(workspacePath?: string): Promise<SessionFileInfo[]>;
38
73
  getAllProjectFolders(workspacePath?: string): ProjectFolderInfo[];
39
74
  isSessionFile(filename: string): boolean;
40
75
  getSessionId(sessionPath: string): string;
41
76
  encodeWorkspacePath(workspacePath: string): string;
42
77
  extractSessionLabel(sessionPath: string): string | null;
78
+ extractSessionLabelsAsync(sessionPaths: readonly string[]): Promise<Map<string, string | null>>;
43
79
  createReader(sessionPath: string): SessionReader;
44
80
  scanSubagents(_sessionDir: string, sessionId: string): SubagentStats[];
45
81
  /**
@@ -48,6 +84,7 @@ export declare class CodexProvider implements SessionProviderBase {
48
84
  private buildSubagentStats;
49
85
  searchInSession(sessionPath: string, query: string, maxResults: number): SearchHit[];
50
86
  getProjectsBaseDir(): string;
87
+ getWatchRoots(): string[];
51
88
  readSessionStats(sessionPath: string): SessionFileStats;
52
89
  readSessionContextSnapshot(sessionPath: string, options?: ReadSessionContextSnapshotOptions): SessionContextSnapshot;
53
90
  getContextWindowLimit(modelId?: string): number;
@@ -56,4 +93,5 @@ export declare class CodexProvider implements SessionProviderBase {
56
93
  /** Get the last rate limits data received from token_count events. */
57
94
  getLastRateLimits(): CodexRateLimits | null;
58
95
  dispose(): void;
96
+ private recordDatabaseFallback;
59
97
  }