sidekick-shared 0.24.3 → 0.24.5

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 (52) hide show
  1. package/CHANGELOG.md +436 -0
  2. package/LICENSE +21 -0
  3. package/README.md +172 -60
  4. package/dist/accountManager.d.ts +13 -0
  5. package/dist/accountManager.js +60 -32
  6. package/dist/accountRegistry.js +59 -41
  7. package/dist/accounts.js +40 -19
  8. package/dist/autoSwitch.d.ts +2 -1
  9. package/dist/autoSwitch.js +32 -7
  10. package/dist/browser.d.ts +2 -0
  11. package/dist/browser.js +3 -1
  12. package/dist/codexProfiles.d.ts +14 -0
  13. package/dist/codexProfiles.js +268 -116
  14. package/dist/context/sessionContext.js +2 -4
  15. package/dist/ensureDefaultAccounts.js +4 -4
  16. package/dist/index.d.ts +14 -7
  17. package/dist/index.js +33 -7
  18. package/dist/modelInfo.js +11 -2
  19. package/dist/node.d.ts +6 -0
  20. package/dist/node.js +8 -1
  21. package/dist/observedContextWindows.d.ts +20 -4
  22. package/dist/observedContextWindows.js +47 -7
  23. package/dist/parsers/codexHistory.d.ts +30 -0
  24. package/dist/parsers/codexHistory.js +118 -0
  25. package/dist/parsers/mcpToolName.d.ts +26 -0
  26. package/dist/parsers/mcpToolName.js +31 -0
  27. package/dist/parsers/sessionPathResolver.d.ts +11 -4
  28. package/dist/parsers/sessionPathResolver.js +11 -4
  29. package/dist/paths.d.ts +43 -4
  30. package/dist/paths.js +66 -6
  31. package/dist/peakHours.d.ts +16 -2
  32. package/dist/peakHours.js +18 -3
  33. package/dist/pricingCatalog.d.ts +7 -3
  34. package/dist/pricingCatalog.js +3 -2
  35. package/dist/providers/claudeCode.d.ts +2 -1
  36. package/dist/providers/claudeCode.js +18 -0
  37. package/dist/providers/codex.d.ts +22 -1
  38. package/dist/providers/codex.js +58 -0
  39. package/dist/providers/types.d.ts +5 -0
  40. package/dist/quotaHistory.d.ts +1 -1
  41. package/dist/quotaHistory.js +2 -2
  42. package/dist/quotaSnapshots.js +8 -77
  43. package/dist/sessionPreviews.d.ts +60 -0
  44. package/dist/sessionPreviews.js +234 -0
  45. package/dist/sessionTranscripts.js +10 -4
  46. package/dist/writers/atomic.d.ts +20 -0
  47. package/dist/writers/atomic.js +190 -19
  48. package/dist/zaiQuota.d.ts +87 -0
  49. package/dist/zaiQuota.js +49 -0
  50. package/dist/zaiQuotaWatcher.d.ts +7 -0
  51. package/dist/zaiQuotaWatcher.js +3 -0
  52. package/package.json +60 -2
@@ -0,0 +1,31 @@
1
+ "use strict";
2
+ /**
3
+ * Claude Code exposes MCP tools under composite identifiers of the form
4
+ * `mcp__<server>__<tool>`. Splitting them correctly is subtle enough that
5
+ * consumers keep re-implementing it by hand (usually with a plain
6
+ * `split('__')`, which mangles server names containing single underscores).
7
+ * This module is the canonical splitter. Browser-safe: no Node imports.
8
+ *
9
+ * @module mcpToolName
10
+ */
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.parseMcpToolName = parseMcpToolName;
13
+ const MCP_TOOL_NAME_PATTERN = /^mcp__([^_]+(?:_[^_]+)*)__(.+)$/;
14
+ /**
15
+ * Splits a Claude Code MCP tool identifier `mcp__<server>__<tool>` into parts.
16
+ *
17
+ * The server name ends at the first double underscore after the prefix, so
18
+ * single underscores stay inside it (`mcp__plugin_context7_context7__query-docs`
19
+ * → server `plugin_context7_context7`) and the tool keeps any further double
20
+ * underscores (`mcp__a__b__c` → tool `b__c`).
21
+ *
22
+ * Returns `null` for anything that is not a well-formed MCP tool id: a
23
+ * non-`mcp__` name, a missing or empty server or tool part, or a
24
+ * case-mismatched prefix. No trimming is applied.
25
+ */
26
+ function parseMcpToolName(name) {
27
+ const match = MCP_TOOL_NAME_PATTERN.exec(name);
28
+ if (!match)
29
+ return null;
30
+ return { serverName: match[1], toolName: match[2] };
31
+ }
@@ -11,10 +11,17 @@
11
11
  */
12
12
  import type { ProjectFolderInfo } from '../providers/types';
13
13
  /**
14
- * Encodes a workspace path to Claude Code's directory naming scheme.
15
- *
16
- * Claude Code replaces path separators, colons, and underscores with hyphens
17
- * to create a flat directory structure.
14
+ * Encodes a workspace path to Claude Code's on-disk directory naming scheme
15
+ * under `~/.claude/projects/` — every non-ASCII-alphanumeric character
16
+ * (separators, colons, underscores, dots, spaces) becomes a hyphen, and case
17
+ * is preserved. Exported from the package root as `encodeClaudeWorkspacePath`.
18
+ *
19
+ * Not to be confused with the top-level `encodeWorkspacePath` export from
20
+ * `paths.ts`: that one is Sidekick's own config-store slug (it keeps dots) and
21
+ * does NOT match Claude Code's scheme — using it to build a
22
+ * `~/.claude/projects/` path silently misses for any workspace containing a
23
+ * dot or space. For lookups, prefer `getClaudeSessionDirectory` or the
24
+ * mismatch-tolerant `discoverSessionDirectory` over raw string encoding.
18
25
  *
19
26
  * @param workspacePath - Absolute path to workspace directory
20
27
  * @returns Encoded path string (e.g., "-home-user-code-project")
@@ -61,10 +61,17 @@ const fs = __importStar(require("fs"));
61
61
  const path = __importStar(require("path"));
62
62
  const os = __importStar(require("os"));
63
63
  /**
64
- * Encodes a workspace path to Claude Code's directory naming scheme.
65
- *
66
- * Claude Code replaces path separators, colons, and underscores with hyphens
67
- * to create a flat directory structure.
64
+ * Encodes a workspace path to Claude Code's on-disk directory naming scheme
65
+ * under `~/.claude/projects/` — every non-ASCII-alphanumeric character
66
+ * (separators, colons, underscores, dots, spaces) becomes a hyphen, and case
67
+ * is preserved. Exported from the package root as `encodeClaudeWorkspacePath`.
68
+ *
69
+ * Not to be confused with the top-level `encodeWorkspacePath` export from
70
+ * `paths.ts`: that one is Sidekick's own config-store slug (it keeps dots) and
71
+ * does NOT match Claude Code's scheme — using it to build a
72
+ * `~/.claude/projects/` path silently misses for any workspace containing a
73
+ * dot or space. For lookups, prefer `getClaudeSessionDirectory` or the
74
+ * mismatch-tolerant `discoverSessionDirectory` over raw string encoding.
68
75
  *
69
76
  * @param workspacePath - Absolute path to workspace directory
70
77
  * @returns Encoded path string (e.g., "-home-user-code-project")
package/dist/paths.d.ts CHANGED
@@ -3,9 +3,44 @@
3
3
  * Mirrors patterns from sidekick-vscode/src/services/SessionPathResolver.ts
4
4
  * and TaskPersistenceService.ts
5
5
  */
6
+ /** Environment variable that redirects the Sidekick config root. */
7
+ export declare const CONFIG_DIR_ENV_VAR = "SIDEKICK_CONFIG_DIR";
8
+ /**
9
+ * The platform default config directory, ignoring both {@link setConfigDir}
10
+ * and `SIDEKICK_CONFIG_DIR`.
11
+ *
12
+ * `~/.config/sidekick` on Unix, `%APPDATA%/sidekick` on Windows.
13
+ */
14
+ export declare function getDefaultConfigDir(): string;
15
+ /**
16
+ * Redirect the Sidekick config root for this process.
17
+ *
18
+ * Takes precedence over `SIDEKICK_CONFIG_DIR`. Pass `null` (or an empty
19
+ * string) to clear it. Every reader and writer in the package resolves the
20
+ * root lazily through {@link getConfigDir}, so this takes effect immediately
21
+ * for all of them — including transitive callers of `getProjectDataPath`,
22
+ * `getGlobalDataPath`, and the account registry.
23
+ *
24
+ * Prefer `SIDEKICK_CONFIG_DIR` when the value is known before the process
25
+ * starts: it is read fresh on every call, so it stays correct even if a
26
+ * bundler ends up with more than one copy of this module.
27
+ *
28
+ * @example
29
+ * setConfigDir(path.join(os.tmpdir(), 'sidekick-fixture'));
30
+ * try {
31
+ * await addTask(project, { subject: 'isolated' });
32
+ * } finally {
33
+ * setConfigDir(null);
34
+ * }
35
+ */
36
+ export declare function setConfigDir(dir: string | null | undefined): void;
37
+ /** The active {@link setConfigDir} override, or `null` when unset. */
38
+ export declare function getConfigDirOverride(): string | null;
6
39
  /**
7
40
  * Gets the Sidekick config directory.
8
- * ~/.config/sidekick on Unix, %APPDATA%/sidekick on Windows.
41
+ *
42
+ * Resolution order: {@link setConfigDir} override → `SIDEKICK_CONFIG_DIR`
43
+ * → {@link getDefaultConfigDir}.
9
44
  */
10
45
  export declare function getConfigDir(): string;
11
46
  /**
@@ -19,10 +54,14 @@ export declare function getProjectDataPath(slug: string, subdomain: string): str
19
54
  */
20
55
  export declare function getGlobalDataPath(filename: string): string;
21
56
  /**
22
- * Encodes a workspace path to Claude Code's directory naming scheme.
23
- * Replaces path separators, colons, and underscores with hyphens.
57
+ * Encodes a workspace path into Sidekick's own config-store slug — path
58
+ * separators, colons, and underscores become hyphens, dots are kept.
24
59
  *
25
- * From: sidekick-vscode/src/services/SessionPathResolver.ts:35-43
60
+ * This is NOT Claude Code's `~/.claude/projects/` naming scheme, despite the
61
+ * shared name: Claude Code also replaces dots, spaces, and every other
62
+ * non-alphanumeric character. To locate Claude Code session directories use
63
+ * `encodeClaudeWorkspacePath` / `getClaudeSessionDirectory` from
64
+ * `parsers/sessionPathResolver` instead.
26
65
  */
27
66
  export declare function encodeWorkspacePath(workspacePath: string): string;
28
67
  /**
package/dist/paths.js CHANGED
@@ -38,6 +38,10 @@ var __importStar = (this && this.__importStar) || (function () {
38
38
  };
39
39
  })();
40
40
  Object.defineProperty(exports, "__esModule", { value: true });
41
+ exports.CONFIG_DIR_ENV_VAR = void 0;
42
+ exports.getDefaultConfigDir = getDefaultConfigDir;
43
+ exports.setConfigDir = setConfigDir;
44
+ exports.getConfigDirOverride = getConfigDirOverride;
41
45
  exports.getConfigDir = getConfigDir;
42
46
  exports.getProjectDataPath = getProjectDataPath;
43
47
  exports.getGlobalDataPath = getGlobalDataPath;
@@ -48,16 +52,68 @@ exports.getProjectSlugRaw = getProjectSlugRaw;
48
52
  const fs = __importStar(require("fs"));
49
53
  const path = __importStar(require("path"));
50
54
  const os = __importStar(require("os"));
55
+ /** Environment variable that redirects the Sidekick config root. */
56
+ exports.CONFIG_DIR_ENV_VAR = 'SIDEKICK_CONFIG_DIR';
57
+ let configDirOverride = null;
51
58
  /**
52
- * Gets the Sidekick config directory.
53
- * ~/.config/sidekick on Unix, %APPDATA%/sidekick on Windows.
59
+ * The platform default config directory, ignoring both {@link setConfigDir}
60
+ * and `SIDEKICK_CONFIG_DIR`.
61
+ *
62
+ * `~/.config/sidekick` on Unix, `%APPDATA%/sidekick` on Windows.
54
63
  */
55
- function getConfigDir() {
64
+ function getDefaultConfigDir() {
56
65
  if (process.platform === 'win32') {
57
66
  return path.join(process.env.APPDATA || os.homedir(), 'sidekick');
58
67
  }
59
68
  return path.join(os.homedir(), '.config', 'sidekick');
60
69
  }
70
+ /**
71
+ * Redirect the Sidekick config root for this process.
72
+ *
73
+ * Takes precedence over `SIDEKICK_CONFIG_DIR`. Pass `null` (or an empty
74
+ * string) to clear it. Every reader and writer in the package resolves the
75
+ * root lazily through {@link getConfigDir}, so this takes effect immediately
76
+ * for all of them — including transitive callers of `getProjectDataPath`,
77
+ * `getGlobalDataPath`, and the account registry.
78
+ *
79
+ * Prefer `SIDEKICK_CONFIG_DIR` when the value is known before the process
80
+ * starts: it is read fresh on every call, so it stays correct even if a
81
+ * bundler ends up with more than one copy of this module.
82
+ *
83
+ * @example
84
+ * setConfigDir(path.join(os.tmpdir(), 'sidekick-fixture'));
85
+ * try {
86
+ * await addTask(project, { subject: 'isolated' });
87
+ * } finally {
88
+ * setConfigDir(null);
89
+ * }
90
+ */
91
+ function setConfigDir(dir) {
92
+ // Resolve the trimmed value, not the original: `path.resolve(' /tmp/x ')`
93
+ // treats the leading spaces as a relative segment and lands under cwd. The
94
+ // `SIDEKICK_CONFIG_DIR` branch in getConfigDir() already trims, and the two
95
+ // routes are documented as interchangeable.
96
+ const trimmed = dir?.trim();
97
+ configDirOverride = trimmed ? path.resolve(trimmed) : null;
98
+ }
99
+ /** The active {@link setConfigDir} override, or `null` when unset. */
100
+ function getConfigDirOverride() {
101
+ return configDirOverride;
102
+ }
103
+ /**
104
+ * Gets the Sidekick config directory.
105
+ *
106
+ * Resolution order: {@link setConfigDir} override → `SIDEKICK_CONFIG_DIR`
107
+ * → {@link getDefaultConfigDir}.
108
+ */
109
+ function getConfigDir() {
110
+ if (configDirOverride)
111
+ return configDirOverride;
112
+ const fromEnv = process.env[exports.CONFIG_DIR_ENV_VAR]?.trim();
113
+ if (fromEnv)
114
+ return path.resolve(fromEnv);
115
+ return getDefaultConfigDir();
116
+ }
61
117
  /**
62
118
  * Gets the path to a project-specific data file.
63
119
  * e.g., ~/.config/sidekick/tasks/my-project.json
@@ -73,10 +129,14 @@ function getGlobalDataPath(filename) {
73
129
  return path.join(getConfigDir(), filename);
74
130
  }
75
131
  /**
76
- * Encodes a workspace path to Claude Code's directory naming scheme.
77
- * Replaces path separators, colons, and underscores with hyphens.
132
+ * Encodes a workspace path into Sidekick's own config-store slug — path
133
+ * separators, colons, and underscores become hyphens, dots are kept.
78
134
  *
79
- * From: sidekick-vscode/src/services/SessionPathResolver.ts:35-43
135
+ * This is NOT Claude Code's `~/.claude/projects/` naming scheme, despite the
136
+ * shared name: Claude Code also replaces dots, spaces, and every other
137
+ * non-alphanumeric character. To locate Claude Code session directories use
138
+ * `encodeClaudeWorkspacePath` / `getClaudeSessionDirectory` from
139
+ * `parsers/sessionPathResolver` instead.
80
140
  */
81
141
  function encodeWorkspacePath(workspacePath) {
82
142
  const normalized = workspacePath.replace(/\\/g, '/');
@@ -23,6 +23,16 @@ export interface PeakHoursState {
23
23
  unavailable: boolean;
24
24
  notApplicable?: boolean;
25
25
  }
26
+ /** Default budget for the promoclock.co request, in milliseconds. */
27
+ export declare const DEFAULT_PEAK_HOURS_TIMEOUT_MS = 10000;
28
+ /** Options for {@link fetchPeakHoursStatus}. */
29
+ export interface FetchPeakHoursOptions {
30
+ /**
31
+ * Abort the request after this many milliseconds.
32
+ * Defaults to {@link DEFAULT_PEAK_HOURS_TIMEOUT_MS}.
33
+ */
34
+ timeoutMs?: number;
35
+ }
26
36
  export declare function isClaudeCodeSessionProvider(providerId: ProviderId): boolean;
27
37
  export declare function createPeakHoursNotApplicableState(providerId: ProviderId): PeakHoursState;
28
38
  export declare function scopePeakHoursToSessionProvider(providerId: ProviderId, status: PeakHoursState | null | undefined): PeakHoursState | null;
@@ -30,6 +40,10 @@ export declare function scopePeakHoursToSessionProvider(providerId: ProviderId,
30
40
  * Fetch current Claude peak-hours state from promoclock.co.
31
41
  *
32
42
  * Single-shot — caller wraps in polling loop, EventEmitter, or interval.
33
- * Returns `unavailable: true` on any network or parse error.
43
+ * Returns `unavailable: true` on any network, timeout, or parse error.
44
+ *
45
+ * promoclock.co is a third-party host, so the request is always bounded by
46
+ * an abort timeout. Without one a hung host would stall the caller — and
47
+ * this is polled, so the stall would repeat.
34
48
  */
35
- export declare function fetchPeakHoursStatus(): Promise<PeakHoursState>;
49
+ export declare function fetchPeakHoursStatus(options?: FetchPeakHoursOptions): Promise<PeakHoursState>;
package/dist/peakHours.js CHANGED
@@ -11,11 +11,14 @@
11
11
  * Polling / eventing is the caller's responsibility.
12
12
  */
13
13
  Object.defineProperty(exports, "__esModule", { value: true });
14
+ exports.DEFAULT_PEAK_HOURS_TIMEOUT_MS = void 0;
14
15
  exports.isClaudeCodeSessionProvider = isClaudeCodeSessionProvider;
15
16
  exports.createPeakHoursNotApplicableState = createPeakHoursNotApplicableState;
16
17
  exports.scopePeakHoursToSessionProvider = scopePeakHoursToSessionProvider;
17
18
  exports.fetchPeakHoursStatus = fetchPeakHoursStatus;
18
19
  const PROMOCLOCK_ENDPOINT = 'https://promoclock.co/api/status';
20
+ /** Default budget for the promoclock.co request, in milliseconds. */
21
+ exports.DEFAULT_PEAK_HOURS_TIMEOUT_MS = 10_000;
19
22
  function unavailableState() {
20
23
  return {
21
24
  status: 'unknown',
@@ -73,11 +76,19 @@ function normalizeSpeed(raw) {
73
76
  * Fetch current Claude peak-hours state from promoclock.co.
74
77
  *
75
78
  * Single-shot — caller wraps in polling loop, EventEmitter, or interval.
76
- * Returns `unavailable: true` on any network or parse error.
79
+ * Returns `unavailable: true` on any network, timeout, or parse error.
80
+ *
81
+ * promoclock.co is a third-party host, so the request is always bounded by
82
+ * an abort timeout. Without one a hung host would stall the caller — and
83
+ * this is polled, so the stall would repeat.
77
84
  */
78
- async function fetchPeakHoursStatus() {
85
+ async function fetchPeakHoursStatus(options = {}) {
86
+ const timeoutMs = options.timeoutMs ?? exports.DEFAULT_PEAK_HOURS_TIMEOUT_MS;
87
+ const controller = new AbortController();
88
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
89
+ timeout.unref();
79
90
  try {
80
- const res = await fetch(PROMOCLOCK_ENDPOINT);
91
+ const res = await fetch(PROMOCLOCK_ENDPOINT, { signal: controller.signal });
81
92
  if (!res.ok)
82
93
  return unavailableState();
83
94
  const data = await res.json();
@@ -98,6 +109,10 @@ async function fetchPeakHoursStatus() {
98
109
  };
99
110
  }
100
111
  catch {
112
+ // Covers AbortError from the timeout above alongside network/parse failures.
101
113
  return unavailableState();
102
114
  }
115
+ finally {
116
+ clearTimeout(timeout);
117
+ }
103
118
  }
@@ -18,8 +18,12 @@
18
18
  */
19
19
  import { type ModelPricing } from './modelInfo';
20
20
  export interface HydrateOptions {
21
- /** Directory for the on-disk cache (e.g. ~/.config/sidekick). */
22
- cacheDir: string;
21
+ /**
22
+ * Directory for the on-disk cache. Defaults to the Sidekick config dir,
23
+ * which honors `SIDEKICK_CONFIG_DIR` and `setConfigDir()` — pass this only
24
+ * to place the cache somewhere outside the config root.
25
+ */
26
+ cacheDir?: string;
23
27
  /** Inject a fetch implementation (tests, custom agents). Defaults to `globalThis.fetch`. */
24
28
  fetchImpl?: typeof fetch;
25
29
  /** Cache TTL in ms. Default 24h. */
@@ -52,7 +56,7 @@ export declare const LITELLM_CATALOG_URL = "https://raw.githubusercontent.com/Be
52
56
  *
53
57
  * Always resolves — never throws. Offline-safe.
54
58
  */
55
- export declare function hydratePricingCatalog(options: HydrateOptions): Promise<HydrateResult>;
59
+ export declare function hydratePricingCatalog(options?: HydrateOptions): Promise<HydrateResult>;
56
60
  /**
57
61
  * Convert a LiteLLM catalog payload to our `ModelPricing` override map.
58
62
  * Exported for tests; rarely called directly.
@@ -59,6 +59,7 @@ const node_fs_1 = require("node:fs");
59
59
  const path = __importStar(require("node:path"));
60
60
  const modelContext_1 = require("./modelContext");
61
61
  const modelInfo_1 = require("./modelInfo");
62
+ const paths_1 = require("./paths");
62
63
  // ── Constants ──
63
64
  exports.LITELLM_CATALOG_URL = 'https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json';
64
65
  const CACHE_FILE_NAME = 'pricing-catalog.json';
@@ -75,8 +76,8 @@ const DEFAULT_TIMEOUT_MS = 3_000;
75
76
  *
76
77
  * Always resolves — never throws. Offline-safe.
77
78
  */
78
- async function hydratePricingCatalog(options) {
79
- const { cacheDir, fetchImpl = globalThis.fetch, ttlMs = DEFAULT_TTL_MS, timeoutMs = DEFAULT_TIMEOUT_MS, url = exports.LITELLM_CATALOG_URL, logger, } = options;
79
+ async function hydratePricingCatalog(options = {}) {
80
+ const { cacheDir = (0, paths_1.getConfigDir)(), fetchImpl = globalThis.fetch, ttlMs = DEFAULT_TTL_MS, timeoutMs = DEFAULT_TIMEOUT_MS, url = exports.LITELLM_CATALOG_URL, logger, } = options;
80
81
  const cachePath = path.join(cacheDir, CACHE_FILE_NAME);
81
82
  const now = Date.now();
82
83
  // 1. Fresh cache?
@@ -9,7 +9,7 @@
9
9
  */
10
10
  import type { ReadSessionContextSnapshotOptions, SessionContextSnapshot } from '../context/sessionContext';
11
11
  import type { SubagentStats, TokenUsage } from '../types/sessionEvent';
12
- import type { SessionProviderBase, SessionReader, SessionFileStats, SearchHit, ProjectFolderInfo, ProviderId } from './types';
12
+ import type { SessionProviderBase, SessionReader, SessionFileInfo, SessionFileStats, SearchHit, ProjectFolderInfo, ProviderId } from './types';
13
13
  /**
14
14
  * Session provider for Claude Code CLI.
15
15
  *
@@ -30,6 +30,7 @@ export declare class ClaudeCodeProvider implements SessionProviderBase {
30
30
  findSessionFiles(workspacePath: string): string[];
31
31
  findSessionsInDirectory(dir: string): string[];
32
32
  getAllProjectFolders(workspacePath?: string): ProjectFolderInfo[];
33
+ listAllSessionFiles(): SessionFileInfo[];
33
34
  isSessionFile(filename: string): boolean;
34
35
  getSessionId(sessionPath: string): string;
35
36
  encodeWorkspacePath(workspacePath: string): string;
@@ -260,6 +260,24 @@ class ClaudeCodeProvider {
260
260
  getAllProjectFolders(workspacePath) {
261
261
  return (0, sessionPathResolver_1.getAllProjectFolders)(workspacePath);
262
262
  }
263
+ listAllSessionFiles() {
264
+ const results = [];
265
+ const seen = new Set();
266
+ for (const folder of this.getAllProjectFolders()) {
267
+ for (const sessionPath of this.findSessionsInDirectory(folder.dir)) {
268
+ if (seen.has(sessionPath))
269
+ continue;
270
+ seen.add(sessionPath);
271
+ try {
272
+ results.push({ path: sessionPath, mtime: fs.statSync(sessionPath).mtime });
273
+ }
274
+ catch {
275
+ // Skip files that vanish between listing and stat.
276
+ }
277
+ }
278
+ }
279
+ return results;
280
+ }
263
281
  // --- File identification ---
264
282
  isSessionFile(filename) {
265
283
  return filename.endsWith('.jsonl');
@@ -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 } 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
  *
@@ -35,6 +55,7 @@ export declare class CodexProvider implements SessionProviderBase {
35
55
  /** Backward-compatible alias for findAllSessions. */
36
56
  findSessionFiles(workspacePath: string): string[];
37
57
  findSessionsInDirectory(dir: string): string[];
58
+ listAllSessionFiles(): SessionFileInfo[];
38
59
  getAllProjectFolders(workspacePath?: string): ProjectFolderInfo[];
39
60
  isSessionFile(filename: string): boolean;
40
61
  getSessionId(sessionPath: string): string;
@@ -44,6 +44,7 @@ var __importStar = (this && this.__importStar) || (function () {
44
44
  })();
45
45
  Object.defineProperty(exports, "__esModule", { value: true });
46
46
  exports.CodexProvider = void 0;
47
+ exports.findCodexRolloutFile = findCodexRolloutFile;
47
48
  const fs = __importStar(require("fs"));
48
49
  const path = __importStar(require("path"));
49
50
  const string_decoder_1 = require("string_decoder");
@@ -164,6 +165,60 @@ function findRolloutFilesInConfiguredHomes() {
164
165
  }
165
166
  return results;
166
167
  }
168
+ /**
169
+ * Locates the rollout file for a Codex session id by walking
170
+ * `<home>/sessions/YYYY/MM/DD/rollout-<timestamp>-<id>.jsonl`.
171
+ *
172
+ * Filename-only match, case-insensitive on the id — no file contents are read
173
+ * and files are only stat'ed once their name matches, so the walk stays cheap
174
+ * even over a large history. When the same id exists in more than one home,
175
+ * the most recently modified copy wins. Returns `null` for an unknown, empty,
176
+ * or whitespace id. Consumers who want the database's answer instead can query
177
+ * the already-public `CodexDatabase` (requires the external `sqlite3` binary).
178
+ */
179
+ function findCodexRolloutFile(sessionId, options = {}) {
180
+ const normalizedId = sessionId.trim().toLowerCase();
181
+ if (!normalizedId)
182
+ return null;
183
+ const sessionsDirs = options.codexHome
184
+ ? [path.join(options.codexHome, 'sessions')]
185
+ : getSessionsDirs();
186
+ let best = null;
187
+ for (const dir of sessionsDirs) {
188
+ for (const candidate of findRolloutFilesById(dir, normalizedId)) {
189
+ if (!best || candidate.mtimeMs > best.mtimeMs)
190
+ best = candidate;
191
+ }
192
+ }
193
+ return best?.path ?? null;
194
+ }
195
+ function findRolloutFilesById(dir, normalizedId) {
196
+ const results = [];
197
+ try {
198
+ if (!fs.existsSync(dir))
199
+ return results;
200
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
201
+ const fullPath = path.join(dir, entry.name);
202
+ if (entry.isDirectory()) {
203
+ results.push(...findRolloutFilesById(fullPath, normalizedId));
204
+ }
205
+ else if (entry.isFile() &&
206
+ isRolloutFile(entry.name) &&
207
+ extractSessionId(entry.name).toLowerCase() === normalizedId) {
208
+ try {
209
+ results.push({ path: fullPath, mtimeMs: fs.statSync(fullPath).mtimeMs });
210
+ }
211
+ catch {
212
+ // Skip files that vanish between readdir and stat.
213
+ }
214
+ }
215
+ }
216
+ }
217
+ catch {
218
+ // Skip inaccessible directories.
219
+ }
220
+ return results;
221
+ }
167
222
  /** Pick the best sessions directory for UI/search entrypoints. */
168
223
  function getSessionsDir() {
169
224
  const dirs = getSessionsDirs();
@@ -711,6 +766,9 @@ class CodexProvider {
711
766
  files.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
712
767
  return files.map((f) => f.path);
713
768
  }
769
+ listAllSessionFiles() {
770
+ return findRolloutFilesInConfiguredHomes();
771
+ }
714
772
  getAllProjectFolders(workspacePath) {
715
773
  const folders = [];
716
774
  const seenCwds = new Map();
@@ -143,6 +143,11 @@ export interface SessionProviderBase {
143
143
  readSessionContextSnapshot?(sessionPath: string, options?: ReadSessionContextSnapshotOptions): SessionContextSnapshot;
144
144
  /** Reports provider runtime readiness for DB-backed providers. */
145
145
  getRuntimeStatus?(): ProviderRuntimeStatus;
146
+ /**
147
+ * Lists every session file this provider knows about, across all workspaces.
148
+ * Stat-only — implementations must not read file contents.
149
+ */
150
+ listAllSessionFiles?(): SessionFileInfo[];
146
151
  /** Tests whether a provider can monitor a directory path, including synthetic DB-backed paths. */
147
152
  canMonitorDirectory?(dir: string): boolean;
148
153
  /** Release resources. */
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Sibling to `quotaSnapshots.ts`. Where snapshots persist a single most-recent sample per
5
5
  * (provider, account), this module accumulates time-series samples so consumers (the VS Code
6
- * dashboard, the `sidekick quota history` CLI, contextful_desktop) can render heatmaps and
6
+ * dashboard, the `sidekick quota history` CLI, external consumers) can render heatmaps and
7
7
  * trend visualisations over a 13-week window.
8
8
  */
9
9
  export type QuotaHistoryRuntimeProvider = 'claude' | 'codex' | 'zai';
@@ -4,7 +4,7 @@
4
4
  *
5
5
  * Sibling to `quotaSnapshots.ts`. Where snapshots persist a single most-recent sample per
6
6
  * (provider, account), this module accumulates time-series samples so consumers (the VS Code
7
- * dashboard, the `sidekick quota history` CLI, contextful_desktop) can render heatmaps and
7
+ * dashboard, the `sidekick quota history` CLI, external consumers) can render heatmaps and
8
8
  * trend visualisations over a 13-week window.
9
9
  */
10
10
  var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
@@ -258,7 +258,7 @@ async function runAppend(sample, filePath, options) {
258
258
  // ignore
259
259
  }
260
260
  // Backwards-compat: keep the latest-snapshot store hot so existing callers (DashboardViewProvider,
261
- // codex session provider, contextful_desktop) don't have to query history for "latest".
261
+ // codex session provider, external consumers) don't have to query history for "latest".
262
262
  try {
263
263
  const providerId = runtimeProviderToSnapshotProvider(sample.runtimeProvider);
264
264
  (0, quotaSnapshots_1.writeQuotaSnapshot)(providerId, sample.providerId, sampleToQuotaState(sample));