sidekick-shared 0.27.6 → 0.28.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.
- package/CHANGELOG.md +28 -0
- package/README.md +58 -18
- package/dist/browser.d.ts +13 -2
- package/dist/browser.js +30 -2
- package/dist/claudePaths.d.ts +11 -0
- package/dist/claudePaths.js +61 -0
- package/dist/claudeProfiles.d.ts +2 -9
- package/dist/claudeProfiles.js +10 -25
- package/dist/codexPaths.d.ts +22 -0
- package/dist/codexPaths.js +67 -0
- package/dist/codexProfiles.d.ts +1 -4
- package/dist/codexProfiles.js +5 -43
- package/dist/doctor.js +2 -2
- package/dist/esm/accountChangeSignal.js +15 -0
- package/dist/esm/accountChanges.js +152 -0
- package/dist/esm/accountHealth.js +288 -0
- package/dist/esm/accountLaunch.js +145 -0
- package/dist/esm/accountLease.js +60 -0
- package/dist/esm/accountManager.js +606 -0
- package/dist/esm/accountRegistry.js +214 -0
- package/dist/esm/accountStatus.js +51 -0
- package/dist/esm/accountSwitch.js +74 -0
- package/dist/esm/accountSync.js +156 -0
- package/dist/esm/accountSyncTypes.js +4 -0
- package/dist/esm/accounts.js +1291 -0
- package/dist/esm/aggregation/EventAggregator.js +2034 -0
- package/dist/esm/aggregation/FrequencyTracker.js +69 -0
- package/dist/esm/aggregation/HeatmapTracker.js +89 -0
- package/dist/esm/aggregation/PatternExtractor.js +168 -0
- package/dist/esm/aggregation/costProvenance.js +31 -0
- package/dist/esm/aggregation/snapshot.js +161 -0
- package/dist/esm/aggregation/types.js +5 -0
- package/dist/esm/analytics/codeImpact.js +18 -0
- package/dist/esm/analytics/compactionLedger.js +37 -0
- package/dist/esm/analytics/qualityScore.js +112 -0
- package/dist/esm/autoSwitch.js +185 -0
- package/dist/esm/browser.js +34 -0
- package/dist/esm/claudeCredentialDoctor.js +239 -0
- package/dist/esm/claudeCredentialLedger.js +91 -0
- package/dist/esm/claudeCredentials.js +111 -0
- package/dist/esm/claudeLiveGuard.js +43 -0
- package/dist/esm/claudePaths.js +22 -0
- package/dist/esm/claudeProfiles.js +142 -0
- package/dist/esm/claudeRefreshLock.js +106 -0
- package/dist/esm/codexAuth.js +101 -0
- package/dist/esm/codexCredentialDoctor.js +265 -0
- package/dist/esm/codexCredentialStore.js +164 -0
- package/dist/esm/codexKeepAlive.js +112 -0
- package/dist/esm/codexPaths.js +172 -0
- package/dist/esm/codexProfiles.js +1251 -0
- package/dist/esm/codexQuota.js +564 -0
- package/dist/esm/codexQuotaWatcher.js +363 -0
- package/dist/esm/context/composer.js +71 -0
- package/dist/esm/context/sessionContext.js +546 -0
- package/dist/esm/credentialIO.js +283 -0
- package/dist/esm/credentials.js +49 -0
- package/dist/esm/doctor.js +305 -0
- package/dist/esm/ensureDefaultAccounts.js +62 -0
- package/dist/esm/errorHistory.js +56 -0
- package/dist/esm/extractors/errorTaxonomy.js +43 -0
- package/dist/esm/extractors/gatherAssets.js +32 -0
- package/dist/esm/extractors/sessionAssets.js +176 -0
- package/dist/esm/extractors/sources/claudeAssets.js +158 -0
- package/dist/esm/extractors/sources/codexAssets.js +234 -0
- package/dist/esm/extractors/tokenUsage.js +41 -0
- package/dist/esm/extractors/toolCall.js +142 -0
- package/dist/esm/failingTools.js +65 -0
- package/dist/esm/fileHead.js +27 -0
- package/dist/esm/formatters/eventHighlighter.js +332 -0
- package/dist/esm/formatters/noiseClassifier.js +209 -0
- package/dist/esm/formatters/sessionDump.js +328 -0
- package/dist/esm/formatters/toolSummary.js +226 -0
- package/dist/esm/formatting.js +82 -0
- package/dist/esm/handoffUrl.js +23 -0
- package/dist/esm/historicalStore.js +305 -0
- package/dist/esm/humanPrompt.js +88 -0
- package/dist/esm/index.js +213 -0
- package/dist/esm/keepAlive.js +189 -0
- package/dist/esm/liveCredentialGuard.js +33 -0
- package/dist/esm/modelAliases.js +51 -0
- package/dist/esm/modelCatalog.js +139 -0
- package/dist/esm/modelContext.js +280 -0
- package/dist/esm/modelInfo.js +832 -0
- package/dist/esm/multiProviderQuotaService.js +450 -0
- package/dist/esm/node.js +46 -0
- package/dist/esm/observedContextWindows.js +165 -0
- package/dist/esm/observedSessionCollector.js +939 -0
- package/dist/esm/parsers/changelogParser.js +71 -0
- package/dist/esm/parsers/codexHistory.js +89 -0
- package/dist/esm/parsers/codexParser.js +906 -0
- package/dist/esm/parsers/debugLogParser.js +126 -0
- package/dist/esm/parsers/jsonl.js +66 -0
- package/dist/esm/parsers/mcpToolName.js +28 -0
- package/dist/esm/parsers/openCodeParser.js +625 -0
- package/dist/esm/parsers/planExtractor.js +335 -0
- package/dist/esm/parsers/sessionActivityDetector.js +246 -0
- package/dist/esm/parsers/sessionPathResolver.js +713 -0
- package/dist/esm/parsers/subagentScanner.js +360 -0
- package/dist/esm/parsers/subagentStatsCache.js +45 -0
- package/dist/esm/parsers/subagentTraceParser.js +325 -0
- package/dist/esm/paths.js +134 -0
- package/dist/esm/peakHours.js +194 -0
- package/dist/esm/phrases.js +1330 -0
- package/dist/esm/pricingCatalog.js +247 -0
- package/dist/esm/processDetection.js +200 -0
- package/dist/esm/projectMigration.js +59 -0
- package/dist/esm/promptHistory.js +1221 -0
- package/dist/esm/providerFailure.js +362 -0
- package/dist/esm/providerIds.js +6 -0
- package/dist/esm/providerQuota.js +1 -0
- package/dist/esm/providerServiceStatus.js +145 -0
- package/dist/esm/providerServiceStatusTypes.js +1 -0
- package/dist/esm/providerStatus.js +117 -0
- package/dist/esm/providers/claudeCode.js +575 -0
- package/dist/esm/providers/claudeListing.js +121 -0
- package/dist/esm/providers/codex.js +1034 -0
- package/dist/esm/providers/codexDatabase.js +297 -0
- package/dist/esm/providers/codexRollouts.js +215 -0
- package/dist/esm/providers/detect.js +185 -0
- package/dist/esm/providers/diagnostics.js +83 -0
- package/dist/esm/providers/directoryListingCache.js +206 -0
- package/dist/esm/providers/factory.js +54 -0
- package/dist/esm/providers/openCode.js +2008 -0
- package/dist/esm/providers/openCodeDatabase.js +419 -0
- package/dist/esm/providers/rolloutWalker.js +252 -0
- package/dist/esm/providers/types.js +12 -0
- package/dist/esm/quota.js +182 -0
- package/dist/esm/quotaHistory.js +364 -0
- package/dist/esm/quotaPoller.js +230 -0
- package/dist/esm/quotaPresentation.js +97 -0
- package/dist/esm/quotaResolver.js +231 -0
- package/dist/esm/quotaSnapshots.js +130 -0
- package/dist/esm/quotaThresholds.js +85 -0
- package/dist/esm/readers/decisions.js +24 -0
- package/dist/esm/readers/handoff.js +20 -0
- package/dist/esm/readers/helpers.js +27 -0
- package/dist/esm/readers/history.js +9 -0
- package/dist/esm/readers/notes.js +48 -0
- package/dist/esm/readers/plans.js +225 -0
- package/dist/esm/readers/tasks.js +19 -0
- package/dist/esm/report/htmlHelpers.js +280 -0
- package/dist/esm/report/htmlReportGenerator.js +704 -0
- package/dist/esm/report/index.js +7 -0
- package/dist/esm/report/logo.js +2 -0
- package/dist/esm/report/openBrowser.js +19 -0
- package/dist/esm/report/sessionReportInputs.js +33 -0
- package/dist/esm/report/transcriptParser.js +83 -0
- package/dist/esm/report/types.js +4 -0
- package/dist/esm/schemas/accountManager.js +204 -0
- package/dist/esm/schemas/accountStatus.js +25 -0
- package/dist/esm/schemas/assistantTurn.js +74 -0
- package/dist/esm/schemas/index.js +25 -0
- package/dist/esm/schemas/observedSessionV1.js +77 -0
- package/dist/esm/schemas/providerFailure.js +37 -0
- package/dist/esm/schemas/quota.js +132 -0
- package/dist/esm/schemas/quotaHistory.js +40 -0
- package/dist/esm/schemas/sessionEvent.js +216 -0
- package/dist/esm/schemas/stateFile.js +75 -0
- package/dist/esm/schemas/tokenEstimation.js +7 -0
- package/dist/esm/schemas/transcript.js +86 -0
- package/dist/esm/schemas/usageNormalization.js +38 -0
- package/dist/esm/search/advancedFilter.js +197 -0
- package/dist/esm/search/sessionSearch.js +66 -0
- package/dist/esm/sessionFingerprint.js +40 -0
- package/dist/esm/sessionMonitor.js +198 -0
- package/dist/esm/sessionPreviews.js +522 -0
- package/dist/esm/sessionPromptHistory.js +254 -0
- package/dist/esm/sessionPromptSignals.js +364 -0
- package/dist/esm/sessionStats.js +160 -0
- package/dist/esm/sessionTokenTotals.js +98 -0
- package/dist/esm/sessionTranscripts.js +49 -0
- package/dist/esm/stateFile.js +119 -0
- package/dist/esm/statusline/BurnRateCalculator.js +40 -0
- package/dist/esm/statusline/claudeStatuslinePayload.js +148 -0
- package/dist/esm/statusline/formatter.js +122 -0
- package/dist/esm/statusline/index.js +8 -0
- package/dist/esm/terminalSync.js +95 -0
- package/dist/esm/testing/credentialGuard.js +197 -0
- package/dist/esm/testing/index.js +12 -0
- package/dist/esm/testing/sessionLogFixtures.js +128 -0
- package/dist/esm/tokenEstimation.js +67 -0
- package/dist/esm/tokenSummary.js +113 -0
- package/dist/esm/toolCategory.js +82 -0
- package/dist/esm/transcript.js +257 -0
- package/dist/esm/turns/assistantTurn.js +373 -0
- package/dist/esm/types/codex.js +23 -0
- package/dist/esm/types/decisionLog.js +5 -0
- package/dist/esm/types/historicalData.js +47 -0
- package/dist/esm/types/knowledgeNote.js +15 -0
- package/dist/esm/types/observedSessionV1.js +257 -0
- package/dist/esm/types/opencode.js +12 -0
- package/dist/esm/types/plan.js +7 -0
- package/dist/esm/types/sessionEvent.js +10 -0
- package/dist/esm/types/taskPersistence.js +12 -0
- package/dist/esm/usage/billingBlocks.js +148 -0
- package/dist/esm/usage/claudeUsageDedupe.js +189 -0
- package/dist/esm/usage/importSessionHistory.js +84 -0
- package/dist/esm/usage/subagentUsage.js +18 -0
- package/dist/esm/usage/usageEvents.js +402 -0
- package/dist/esm/usage/usageReports.js +202 -0
- package/dist/esm/usageNormalization.js +95 -0
- package/dist/esm/watchers/eventBridge.js +219 -0
- package/dist/esm/watchers/factory.js +63 -0
- package/dist/esm/watchers/jsonlTail.js +289 -0
- package/dist/esm/watchers/jsonlWatcher.js +435 -0
- package/dist/esm/watchers/providerReaderWatcher.js +118 -0
- package/dist/esm/watchers/sqliteWatcher.js +285 -0
- package/dist/esm/watchers/types.js +4 -0
- package/dist/esm/workspaceRoots.js +56 -0
- package/dist/esm/writers/atomic.js +375 -0
- package/dist/esm/writers/decisions.js +51 -0
- package/dist/esm/writers/notes.js +46 -0
- package/dist/esm/writers/tasks.js +63 -0
- package/dist/esm/zaiQuota.js +323 -0
- package/dist/esm/zaiQuotaApi.js +271 -0
- package/dist/esm/zaiQuotaWatcher.js +265 -0
- package/dist/fileHead.d.ts +22 -0
- package/dist/fileHead.js +33 -0
- package/dist/index.d.ts +16 -7
- package/dist/index.js +49 -15
- package/dist/node.d.ts +13 -1
- package/dist/node.js +43 -2
- package/dist/parsers/codexHistory.js +2 -2
- package/dist/parsers/codexParser.d.ts +12 -0
- package/dist/parsers/codexParser.js +34 -12
- package/dist/parsers/debugLogParser.js +2 -2
- package/dist/parsers/sessionPathResolver.js +5 -4
- package/dist/promptHistory.d.ts +32 -0
- package/dist/promptHistory.js +115 -59
- package/dist/providerFailure.d.ts +12 -6
- package/dist/providerFailure.js +45 -0
- package/dist/providers/claudeCode.js +7 -99
- package/dist/providers/claudeListing.d.ts +22 -0
- package/dist/providers/claudeListing.js +158 -0
- package/dist/providers/codex.d.ts +3 -1
- package/dist/providers/codex.js +26 -227
- package/dist/providers/codexRollouts.d.ts +44 -0
- package/dist/providers/codexRollouts.js +261 -0
- package/dist/providers/detect.js +4 -4
- package/dist/readers/plans.js +3 -3
- package/dist/schemas/index.d.ts +2 -0
- package/dist/schemas/index.js +7 -2
- package/dist/schemas/providerFailure.d.ts +154 -0
- package/dist/schemas/providerFailure.js +40 -0
- package/dist/sessionPromptHistory.d.ts +12 -7
- package/dist/sessionPromptHistory.js +15 -3
- package/dist/sessionPromptSignals.d.ts +9 -0
- package/dist/sessionPromptSignals.js +13 -0
- package/dist/testing/credentialGuard.d.ts +23 -0
- package/dist/testing/credentialGuard.js +233 -0
- package/dist/testing/index.d.ts +14 -0
- package/dist/testing/index.js +22 -0
- package/dist/testing/sessionLogFixtures.d.ts +28 -0
- package/dist/testing/sessionLogFixtures.js +169 -0
- package/dist/tokenEstimation.d.ts +2 -0
- package/dist/tokenEstimation.js +21 -11
- package/dist/toolCategory.d.ts +18 -0
- package/dist/toolCategory.js +86 -0
- package/dist/types/codex.d.ts +4 -0
- package/dist/usage/usageEvents.d.ts +16 -0
- package/dist/usage/usageEvents.js +53 -9
- package/dist/watchers/jsonlTail.d.ts +26 -1
- package/dist/watchers/jsonlTail.js +136 -28
- package/dist/watchers/jsonlWatcher.js +7 -0
- package/dist/watchers/providerReaderWatcher.js +1 -0
- package/dist/watchers/types.d.ts +6 -0
- package/dist/workspaceRoots.d.ts +23 -0
- package/dist/workspaceRoots.js +93 -0
- package/package.json +22 -2
|
@@ -0,0 +1,713 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Session path resolution for Claude Code projects.
|
|
3
|
+
*
|
|
4
|
+
* This module provides utilities for locating Claude Code session files
|
|
5
|
+
* in ~/.claude/projects/. Claude Code encodes workspace paths by replacing
|
|
6
|
+
* slashes with hyphens, e.g., /home/user/code/project -> home-user-code-project.
|
|
7
|
+
*
|
|
8
|
+
* Session files are stored as [session-uuid].jsonl in the encoded directory.
|
|
9
|
+
*
|
|
10
|
+
* @module sidekick-shared/parsers/sessionPathResolver
|
|
11
|
+
*/
|
|
12
|
+
import * as fs from 'fs';
|
|
13
|
+
import * as path from 'path';
|
|
14
|
+
import * as os from 'os';
|
|
15
|
+
import { getClaudeProjectsDir } from '../claudePaths';
|
|
16
|
+
/**
|
|
17
|
+
* Encodes a workspace path to Claude Code's on-disk directory naming scheme
|
|
18
|
+
* under `~/.claude/projects/` — every non-ASCII-alphanumeric character
|
|
19
|
+
* (separators, colons, underscores, dots, spaces) becomes a hyphen, and case
|
|
20
|
+
* is preserved. Exported from the package root as `encodeClaudeWorkspacePath`.
|
|
21
|
+
*
|
|
22
|
+
* Not to be confused with the top-level `encodeWorkspacePath` export from
|
|
23
|
+
* `paths.ts`: that one is Sidekick's own config-store slug (it keeps dots) and
|
|
24
|
+
* does NOT match Claude Code's scheme — using it to build a
|
|
25
|
+
* `~/.claude/projects/` path silently misses for any workspace containing a
|
|
26
|
+
* dot or space. For lookups, prefer `getClaudeSessionDirectory` or the
|
|
27
|
+
* mismatch-tolerant `discoverSessionDirectory` over raw string encoding.
|
|
28
|
+
*
|
|
29
|
+
* @param workspacePath - Absolute path to workspace directory
|
|
30
|
+
* @returns Encoded path string (e.g., "-home-user-code-project")
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```typescript
|
|
34
|
+
* encodeWorkspacePath('/home/user/code/my_project');
|
|
35
|
+
* // => "-home-user-code-my-project"
|
|
36
|
+
*
|
|
37
|
+
* encodeWorkspacePath('C:\\Users\\user\\code\\my_project'); // Windows
|
|
38
|
+
* // => "C--Users-user-code-my-project"
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
export function encodeWorkspacePath(workspacePath) {
|
|
42
|
+
// Normalize path separators to forward slash
|
|
43
|
+
const normalized = workspacePath.replace(/\\/g, '/');
|
|
44
|
+
// Claude Code replaces every non-alphanumeric path character, including
|
|
45
|
+
// dots and spaces, with a hyphen.
|
|
46
|
+
return normalized.replace(/[^a-zA-Z0-9]/g, '-');
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Gets the session directory path for a workspace.
|
|
50
|
+
*
|
|
51
|
+
* Returns the directory where Claude Code stores session files
|
|
52
|
+
* for the given workspace, even if the directory doesn't exist.
|
|
53
|
+
*
|
|
54
|
+
* @param workspacePath - Absolute path to workspace directory
|
|
55
|
+
* @returns Absolute path to session directory
|
|
56
|
+
*
|
|
57
|
+
* @example
|
|
58
|
+
* ```typescript
|
|
59
|
+
* getSessionDirectory('/home/user/code/project');
|
|
60
|
+
* // => "/home/user/.claude/projects/-home-user-code-project"
|
|
61
|
+
* ```
|
|
62
|
+
*/
|
|
63
|
+
export function getSessionDirectory(workspacePath) {
|
|
64
|
+
const encoded = encodeWorkspacePath(workspacePath);
|
|
65
|
+
return path.join(getClaudeProjectsDir(), encoded);
|
|
66
|
+
}
|
|
67
|
+
/** How recently a file must be modified to be considered "active" (5 minutes) */
|
|
68
|
+
const ACTIVE_SESSION_THRESHOLD_MS = 5 * 60 * 1000;
|
|
69
|
+
/**
|
|
70
|
+
* Finds session directories for Claude Code sessions started from subdirectories
|
|
71
|
+
* of the given workspace path.
|
|
72
|
+
*
|
|
73
|
+
* When VS Code workspace is `/home/user/project` but Claude Code starts from
|
|
74
|
+
* a subdirectory like `/home/user/project/packages/app`, the session goes to
|
|
75
|
+
* `~/.claude/projects/-home-user-project-packages-app/` which we need to find.
|
|
76
|
+
*
|
|
77
|
+
* @param workspacePath - Absolute path to workspace directory
|
|
78
|
+
* @returns Array of matching session directory paths
|
|
79
|
+
*
|
|
80
|
+
* @example
|
|
81
|
+
* ```typescript
|
|
82
|
+
* // Workspace: /home/user/project
|
|
83
|
+
* // Claude started from: /home/user/project/packages/app
|
|
84
|
+
* findSubdirectorySessionDirs('/home/user/project');
|
|
85
|
+
* // => ["/home/user/.claude/projects/-home-user-project-packages-app"]
|
|
86
|
+
* ```
|
|
87
|
+
*/
|
|
88
|
+
export function findSubdirectorySessionDirs(workspacePath) {
|
|
89
|
+
const projectsDir = getClaudeProjectsDir();
|
|
90
|
+
try {
|
|
91
|
+
if (!fs.existsSync(projectsDir)) {
|
|
92
|
+
return [];
|
|
93
|
+
}
|
|
94
|
+
// Encode workspace path to get the prefix to match
|
|
95
|
+
const encodedPrefix = encodeWorkspacePath(workspacePath).toLowerCase();
|
|
96
|
+
// Get all directories in projects folder
|
|
97
|
+
const allDirs = fs.readdirSync(projectsDir).filter((name) => {
|
|
98
|
+
const fullPath = path.join(projectsDir, name);
|
|
99
|
+
try {
|
|
100
|
+
return fs.statSync(fullPath).isDirectory();
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
// Find directories that start with the encoded workspace prefix followed by a hyphen
|
|
107
|
+
// The hyphen requirement prevents /project matching /project-v2
|
|
108
|
+
const matches = [];
|
|
109
|
+
for (const dir of allDirs) {
|
|
110
|
+
const dirLower = dir.toLowerCase();
|
|
111
|
+
// Must start with prefix AND have a hyphen after (indicating subdirectory)
|
|
112
|
+
if (dirLower.startsWith(encodedPrefix + '-')) {
|
|
113
|
+
matches.push(path.join(projectsDir, dir));
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return matches;
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
return [];
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Gets the most recently active session directory from a list of directories.
|
|
124
|
+
*
|
|
125
|
+
* For each directory, finds the most recent .jsonl file by modification time,
|
|
126
|
+
* then returns the directory containing the overall most recent session.
|
|
127
|
+
*
|
|
128
|
+
* @param sessionDirs - Array of session directory paths to check
|
|
129
|
+
* @returns Path to most recently active directory, or null if none have sessions
|
|
130
|
+
*
|
|
131
|
+
* @example
|
|
132
|
+
* ```typescript
|
|
133
|
+
* getMostRecentlyActiveSessionDir([
|
|
134
|
+
* '/home/user/.claude/projects/-home-user-project-packages-app',
|
|
135
|
+
* '/home/user/.claude/projects/-home-user-project-packages-lib'
|
|
136
|
+
* ]);
|
|
137
|
+
* // => "/home/user/.claude/projects/-home-user-project-packages-app"
|
|
138
|
+
* ```
|
|
139
|
+
*/
|
|
140
|
+
export function getMostRecentlyActiveSessionDir(sessionDirs) {
|
|
141
|
+
let mostRecentDir = null;
|
|
142
|
+
let mostRecentMtime = 0;
|
|
143
|
+
for (const dir of sessionDirs) {
|
|
144
|
+
try {
|
|
145
|
+
// Find most recent .jsonl file in this directory
|
|
146
|
+
const files = fs.readdirSync(dir).filter((file) => file.endsWith('.jsonl'));
|
|
147
|
+
for (const file of files) {
|
|
148
|
+
try {
|
|
149
|
+
const fullPath = path.join(dir, file);
|
|
150
|
+
const stats = fs.statSync(fullPath);
|
|
151
|
+
// Only consider non-empty files
|
|
152
|
+
if (stats.size > 0 && stats.mtime.getTime() > mostRecentMtime) {
|
|
153
|
+
mostRecentMtime = stats.mtime.getTime();
|
|
154
|
+
mostRecentDir = dir;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
// Skip files we can't stat
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
// Skip directories we can't read
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return mostRecentDir;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Discovers the session directory for a workspace by trying multiple strategies.
|
|
170
|
+
*
|
|
171
|
+
* Strategy order:
|
|
172
|
+
* 1. Try the computed encoded path (fast, works if our encoding matches Claude Code's)
|
|
173
|
+
* 2. Scan ~/.claude/projects/ for directories matching the workspace name
|
|
174
|
+
* 3. Scan temp directory for Claude scratchpad directories to find actual encoding
|
|
175
|
+
*
|
|
176
|
+
* @param workspacePath - Absolute path to workspace directory
|
|
177
|
+
* @returns Absolute path to session directory, or null if not found
|
|
178
|
+
*/
|
|
179
|
+
export function discoverSessionDirectory(workspacePath) {
|
|
180
|
+
const projectsDir = getClaudeProjectsDir();
|
|
181
|
+
// Strategy 1: Try computed encoded path (exact match)
|
|
182
|
+
const computedDir = getSessionDirectory(workspacePath);
|
|
183
|
+
if (fs.existsSync(computedDir)) {
|
|
184
|
+
return computedDir;
|
|
185
|
+
}
|
|
186
|
+
// Strategy 1.5: Check for subdirectory sessions
|
|
187
|
+
// When Claude Code starts from a subdirectory of the workspace, the session
|
|
188
|
+
// is stored in a directory matching the subdirectory path. We find the most
|
|
189
|
+
// recently active one.
|
|
190
|
+
const subdirMatches = findSubdirectorySessionDirs(workspacePath);
|
|
191
|
+
if (subdirMatches.length > 0) {
|
|
192
|
+
const mostRecent = getMostRecentlyActiveSessionDir(subdirMatches);
|
|
193
|
+
if (mostRecent) {
|
|
194
|
+
return mostRecent;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
// Strategy 2: Scan ~/.claude/projects/ for matching directories
|
|
198
|
+
try {
|
|
199
|
+
if (fs.existsSync(projectsDir)) {
|
|
200
|
+
const existingDirs = fs.readdirSync(projectsDir).filter((name) => {
|
|
201
|
+
const fullPath = path.join(projectsDir, name);
|
|
202
|
+
try {
|
|
203
|
+
return fs.statSync(fullPath).isDirectory();
|
|
204
|
+
}
|
|
205
|
+
catch {
|
|
206
|
+
return false;
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
// Try to match by workspace path components
|
|
210
|
+
// Normalize workspace path for comparison
|
|
211
|
+
const normalizedWorkspace = encodeWorkspacePath(workspacePath).toLowerCase();
|
|
212
|
+
for (const dir of existingDirs) {
|
|
213
|
+
// Check if the directory name matches (case-insensitive)
|
|
214
|
+
if (dir.toLowerCase() === normalizedWorkspace) {
|
|
215
|
+
return path.join(projectsDir, dir);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
// Fallback: match by final path component (project name)
|
|
219
|
+
const workspaceBasename = path
|
|
220
|
+
.basename(workspacePath)
|
|
221
|
+
.replace(/[^a-zA-Z0-9]/g, '-')
|
|
222
|
+
.toLowerCase();
|
|
223
|
+
for (const dir of existingDirs) {
|
|
224
|
+
const dirLower = dir.toLowerCase();
|
|
225
|
+
// Check if dir ends with the project name
|
|
226
|
+
if (dirLower.endsWith('-' + workspaceBasename) || dirLower === workspaceBasename) {
|
|
227
|
+
return path.join(projectsDir, dir);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
catch {
|
|
233
|
+
// Ignore errors during discovery
|
|
234
|
+
}
|
|
235
|
+
// Strategy 3: Check temp directory for Claude scratchpad directories
|
|
236
|
+
// Claude creates: <tmpdir>/claude/<encoded-workspace>/<session-uuid>/scratchpad
|
|
237
|
+
try {
|
|
238
|
+
const claudeTempDir = path.join(os.tmpdir(), 'claude');
|
|
239
|
+
if (fs.existsSync(claudeTempDir)) {
|
|
240
|
+
const tempDirs = fs.readdirSync(claudeTempDir).filter((name) => {
|
|
241
|
+
const fullPath = path.join(claudeTempDir, name);
|
|
242
|
+
try {
|
|
243
|
+
return fs.statSync(fullPath).isDirectory();
|
|
244
|
+
}
|
|
245
|
+
catch {
|
|
246
|
+
return false;
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
// Match by workspace basename
|
|
250
|
+
const workspaceBasename = path.basename(workspacePath).replace(/_/g, '-').toLowerCase();
|
|
251
|
+
for (const encodedDir of tempDirs) {
|
|
252
|
+
const encodedLower = encodedDir.toLowerCase();
|
|
253
|
+
if (encodedLower.endsWith('-' + workspaceBasename) || encodedLower === workspaceBasename) {
|
|
254
|
+
// Found a match in temp - use this encoding for the session directory
|
|
255
|
+
const sessionDir = path.join(projectsDir, encodedDir);
|
|
256
|
+
if (fs.existsSync(sessionDir)) {
|
|
257
|
+
return sessionDir;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
catch {
|
|
264
|
+
// Ignore errors during temp directory scan
|
|
265
|
+
}
|
|
266
|
+
return null;
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Finds the most recently modified session file for a workspace.
|
|
270
|
+
*
|
|
271
|
+
* Prioritizes "active" sessions (modified within last 5 minutes) over
|
|
272
|
+
* stale ones. This helps select the right session when multiple exist.
|
|
273
|
+
*
|
|
274
|
+
* @param workspacePath - Absolute path to workspace directory
|
|
275
|
+
* @returns Path to active session file, or null if none exists
|
|
276
|
+
*
|
|
277
|
+
* @example
|
|
278
|
+
* ```typescript
|
|
279
|
+
* const sessionPath = findActiveSession('/home/user/code/project');
|
|
280
|
+
* if (sessionPath) {
|
|
281
|
+
* console.log('Active session:', sessionPath);
|
|
282
|
+
* } else {
|
|
283
|
+
* console.log('No active Claude Code session for this workspace');
|
|
284
|
+
* }
|
|
285
|
+
* ```
|
|
286
|
+
*/
|
|
287
|
+
export function findActiveSession(workspacePath) {
|
|
288
|
+
// Use discovery to find the session directory (handles encoding differences)
|
|
289
|
+
const sessionDir = discoverSessionDirectory(workspacePath);
|
|
290
|
+
try {
|
|
291
|
+
// Check if directory was found
|
|
292
|
+
if (!sessionDir) {
|
|
293
|
+
return null;
|
|
294
|
+
}
|
|
295
|
+
const now = Date.now();
|
|
296
|
+
// Find all .jsonl files with stats
|
|
297
|
+
const files = fs
|
|
298
|
+
.readdirSync(sessionDir)
|
|
299
|
+
.filter((file) => file.endsWith('.jsonl'))
|
|
300
|
+
.map((file) => {
|
|
301
|
+
const fullPath = path.join(sessionDir, file);
|
|
302
|
+
const stats = fs.statSync(fullPath);
|
|
303
|
+
const mtime = stats.mtime.getTime();
|
|
304
|
+
return {
|
|
305
|
+
path: fullPath,
|
|
306
|
+
mtime,
|
|
307
|
+
size: stats.size,
|
|
308
|
+
isActive: now - mtime < ACTIVE_SESSION_THRESHOLD_MS,
|
|
309
|
+
};
|
|
310
|
+
})
|
|
311
|
+
// Filter out empty files
|
|
312
|
+
.filter((file) => file.size > 0);
|
|
313
|
+
// Return null if no session files
|
|
314
|
+
if (files.length === 0) {
|
|
315
|
+
return null;
|
|
316
|
+
}
|
|
317
|
+
// Prefer active sessions, then sort by modification time
|
|
318
|
+
files.sort((a, b) => {
|
|
319
|
+
// Active sessions first
|
|
320
|
+
if (a.isActive && !b.isActive)
|
|
321
|
+
return -1;
|
|
322
|
+
if (!a.isActive && b.isActive)
|
|
323
|
+
return 1;
|
|
324
|
+
// Then by modification time (most recent first)
|
|
325
|
+
return b.mtime - a.mtime;
|
|
326
|
+
});
|
|
327
|
+
return files[0].path;
|
|
328
|
+
}
|
|
329
|
+
catch (error) {
|
|
330
|
+
// Handle errors gracefully - missing directory, permission issues, etc.
|
|
331
|
+
console.error('Error finding active session:', error);
|
|
332
|
+
return null;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Finds all session files for a workspace.
|
|
337
|
+
*
|
|
338
|
+
* Returns paths to all .jsonl session files for the workspace,
|
|
339
|
+
* sorted by modification time (most recent first). Useful for
|
|
340
|
+
* session history features.
|
|
341
|
+
*
|
|
342
|
+
* @param workspacePath - Absolute path to workspace directory
|
|
343
|
+
* @returns Array of session file paths (empty if none exist)
|
|
344
|
+
*
|
|
345
|
+
* @example
|
|
346
|
+
* ```typescript
|
|
347
|
+
* const sessions = findAllSessions('/home/user/code/project');
|
|
348
|
+
* console.log(`Found ${sessions.length} session(s)`);
|
|
349
|
+
* sessions.forEach(session => console.log(session));
|
|
350
|
+
* ```
|
|
351
|
+
*/
|
|
352
|
+
export function findAllSessions(workspacePath) {
|
|
353
|
+
// Use discovery to find the session directory (handles encoding differences)
|
|
354
|
+
const sessionDir = discoverSessionDirectory(workspacePath);
|
|
355
|
+
try {
|
|
356
|
+
// Check if directory was found
|
|
357
|
+
if (!sessionDir) {
|
|
358
|
+
return [];
|
|
359
|
+
}
|
|
360
|
+
// Find and sort all .jsonl files
|
|
361
|
+
const files = fs
|
|
362
|
+
.readdirSync(sessionDir)
|
|
363
|
+
.filter((file) => file.endsWith('.jsonl'))
|
|
364
|
+
.map((file) => {
|
|
365
|
+
const fullPath = path.join(sessionDir, file);
|
|
366
|
+
try {
|
|
367
|
+
const stats = fs.statSync(fullPath);
|
|
368
|
+
return {
|
|
369
|
+
path: fullPath,
|
|
370
|
+
mtime: stats.mtime.getTime(),
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
catch {
|
|
374
|
+
return null;
|
|
375
|
+
}
|
|
376
|
+
})
|
|
377
|
+
.filter((file) => file !== null)
|
|
378
|
+
.sort((a, b) => b.mtime - a.mtime)
|
|
379
|
+
.map((file) => file.path);
|
|
380
|
+
return files;
|
|
381
|
+
}
|
|
382
|
+
catch (error) {
|
|
383
|
+
// Handle errors gracefully
|
|
384
|
+
console.error('Error finding sessions:', error);
|
|
385
|
+
return [];
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* Decodes an encoded directory name back to a human-readable path.
|
|
390
|
+
*
|
|
391
|
+
* Claude Code encodes paths by replacing slashes, colons, and underscores with hyphens.
|
|
392
|
+
* This function attempts to reverse that encoding. Note that some ambiguity exists
|
|
393
|
+
* (e.g., was it a slash or underscore?), so we default to slashes for path separators.
|
|
394
|
+
*
|
|
395
|
+
* @param encoded - Encoded directory name (e.g., "-home-user-project")
|
|
396
|
+
* @returns Decoded path (e.g., "/home/user/project")
|
|
397
|
+
*
|
|
398
|
+
* @example
|
|
399
|
+
* ```typescript
|
|
400
|
+
* decodeEncodedPath('-home-user-code-project');
|
|
401
|
+
* // => "/home/user/code/project"
|
|
402
|
+
*
|
|
403
|
+
* decodeEncodedPath('C--Users-user-code');
|
|
404
|
+
* // => "C:/Users/user/code"
|
|
405
|
+
* ```
|
|
406
|
+
*/
|
|
407
|
+
export function decodeEncodedPath(encoded) {
|
|
408
|
+
// Handle Windows paths (start with drive letter like "C-")
|
|
409
|
+
const windowsDriveMatch = encoded.match(/^([A-Za-z])--(.*)/);
|
|
410
|
+
if (windowsDriveMatch) {
|
|
411
|
+
const drive = windowsDriveMatch[1];
|
|
412
|
+
const rest = windowsDriveMatch[2];
|
|
413
|
+
// Replace single hyphens with slashes (path separators)
|
|
414
|
+
const decoded = rest.replace(/-/g, '/');
|
|
415
|
+
return `${drive}:/${decoded}`;
|
|
416
|
+
}
|
|
417
|
+
// Handle Unix paths (start with hyphen representing root /)
|
|
418
|
+
if (encoded.startsWith('-')) {
|
|
419
|
+
// Remove leading hyphen, replace remaining hyphens with slashes
|
|
420
|
+
return '/' + encoded.substring(1).replace(/-/g, '/');
|
|
421
|
+
}
|
|
422
|
+
// Fallback: just replace hyphens with slashes
|
|
423
|
+
return encoded.replace(/-/g, '/');
|
|
424
|
+
}
|
|
425
|
+
/**
|
|
426
|
+
* Gets all project folders from ~/.claude/projects/.
|
|
427
|
+
*
|
|
428
|
+
* Returns information about every project directory Claude Code has created,
|
|
429
|
+
* sorted by priority:
|
|
430
|
+
* 1. Exact workspace match (if workspacePath provided)
|
|
431
|
+
* 2. Subdirectories of workspace (if workspacePath provided)
|
|
432
|
+
* 3. Most recently active (based on session file modification times)
|
|
433
|
+
*
|
|
434
|
+
* @param workspacePath - Optional workspace path to prioritize in sorting
|
|
435
|
+
* @returns Array of project folder info, sorted by priority
|
|
436
|
+
*
|
|
437
|
+
* @example
|
|
438
|
+
* ```typescript
|
|
439
|
+
* const folders = getAllProjectFolders('/home/user/project');
|
|
440
|
+
* // => [
|
|
441
|
+
* // { dir: '/home/user/.claude/projects/-home-user-project', // exact match first
|
|
442
|
+
* // encodedName: '-home-user-project',
|
|
443
|
+
* // name: '/home/user/project',
|
|
444
|
+
* // sessionCount: 3,
|
|
445
|
+
* // lastModified: Date },
|
|
446
|
+
* // { dir: '/home/user/.claude/projects/-home-user-project-subdir', // subdirs next
|
|
447
|
+
* // ... },
|
|
448
|
+
* // ... // then others by recency
|
|
449
|
+
* // ]
|
|
450
|
+
* ```
|
|
451
|
+
*/
|
|
452
|
+
export function getAllProjectFolders(workspacePath) {
|
|
453
|
+
const projectsDir = getClaudeProjectsDir();
|
|
454
|
+
const folders = [];
|
|
455
|
+
try {
|
|
456
|
+
if (!fs.existsSync(projectsDir)) {
|
|
457
|
+
return [];
|
|
458
|
+
}
|
|
459
|
+
const entries = fs.readdirSync(projectsDir);
|
|
460
|
+
for (const entry of entries) {
|
|
461
|
+
const fullPath = path.join(projectsDir, entry);
|
|
462
|
+
try {
|
|
463
|
+
const stats = fs.statSync(fullPath);
|
|
464
|
+
if (!stats.isDirectory()) {
|
|
465
|
+
continue;
|
|
466
|
+
}
|
|
467
|
+
// Count session files and find most recent modification
|
|
468
|
+
const sessionFiles = fs.readdirSync(fullPath).filter((f) => f.endsWith('.jsonl'));
|
|
469
|
+
let lastModified = stats.mtime;
|
|
470
|
+
let sessionCount = 0;
|
|
471
|
+
for (const sessionFile of sessionFiles) {
|
|
472
|
+
try {
|
|
473
|
+
const sessionPath = path.join(fullPath, sessionFile);
|
|
474
|
+
const sessionStats = fs.statSync(sessionPath);
|
|
475
|
+
if (sessionStats.size > 0) {
|
|
476
|
+
sessionCount++;
|
|
477
|
+
if (sessionStats.mtime > lastModified) {
|
|
478
|
+
lastModified = sessionStats.mtime;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
catch {
|
|
483
|
+
// Skip files we can't stat
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
folders.push({
|
|
487
|
+
dir: fullPath,
|
|
488
|
+
encodedName: entry,
|
|
489
|
+
name: decodeEncodedPath(entry),
|
|
490
|
+
sessionCount,
|
|
491
|
+
lastModified,
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
catch {
|
|
495
|
+
// Skip directories we can't read
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
// Sort with workspace prioritization
|
|
499
|
+
// Use encoded names for comparison (decoded paths are lossy — hyphens in
|
|
500
|
+
// directory names become indistinguishable from path separators)
|
|
501
|
+
folders.sort((a, b) => {
|
|
502
|
+
if (workspacePath) {
|
|
503
|
+
const encodedWorkspace = encodeWorkspacePath(workspacePath).toLowerCase();
|
|
504
|
+
const aEncoded = a.encodedName.toLowerCase();
|
|
505
|
+
const bEncoded = b.encodedName.toLowerCase();
|
|
506
|
+
// Priority 1: Exact workspace match comes first
|
|
507
|
+
const aIsExact = aEncoded === encodedWorkspace;
|
|
508
|
+
const bIsExact = bEncoded === encodedWorkspace;
|
|
509
|
+
if (aIsExact && !bIsExact)
|
|
510
|
+
return -1;
|
|
511
|
+
if (!aIsExact && bIsExact)
|
|
512
|
+
return 1;
|
|
513
|
+
// Priority 2: Subdirectories of workspace come next
|
|
514
|
+
const aIsSubdir = aEncoded.startsWith(encodedWorkspace + '-');
|
|
515
|
+
const bIsSubdir = bEncoded.startsWith(encodedWorkspace + '-');
|
|
516
|
+
if (aIsSubdir && !bIsSubdir)
|
|
517
|
+
return -1;
|
|
518
|
+
if (!aIsSubdir && bIsSubdir)
|
|
519
|
+
return 1;
|
|
520
|
+
}
|
|
521
|
+
// Default: sort by most recently active
|
|
522
|
+
return b.lastModified.getTime() - a.lastModified.getTime();
|
|
523
|
+
});
|
|
524
|
+
return folders;
|
|
525
|
+
}
|
|
526
|
+
catch {
|
|
527
|
+
return [];
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
/**
|
|
531
|
+
* Finds all session files in a specific directory.
|
|
532
|
+
*
|
|
533
|
+
* Unlike findAllSessions which uses workspace-based discovery, this function
|
|
534
|
+
* accepts a direct path to a session directory and returns all .jsonl files.
|
|
535
|
+
*
|
|
536
|
+
* @param sessionDir - Absolute path to a session directory
|
|
537
|
+
* @returns Array of session file paths, sorted by modification time (most recent first)
|
|
538
|
+
*
|
|
539
|
+
* @example
|
|
540
|
+
* ```typescript
|
|
541
|
+
* findSessionsInDirectory('/home/user/.claude/projects/-home-user-project');
|
|
542
|
+
* // => ['/home/user/.claude/projects/-home-user-project/abc123.jsonl', ...]
|
|
543
|
+
* ```
|
|
544
|
+
*/
|
|
545
|
+
export function findSessionsInDirectory(sessionDir) {
|
|
546
|
+
return findSessionFilesWithStats(sessionDir).map((file) => file.path);
|
|
547
|
+
}
|
|
548
|
+
/**
|
|
549
|
+
* The non-empty `.jsonl` files in a session directory with the stat each one
|
|
550
|
+
* already cost, newest first, so callers that need the mtime do not stat the
|
|
551
|
+
* same files a second time.
|
|
552
|
+
*/
|
|
553
|
+
export function findSessionFilesWithStats(sessionDir) {
|
|
554
|
+
try {
|
|
555
|
+
if (!fs.existsSync(sessionDir)) {
|
|
556
|
+
return [];
|
|
557
|
+
}
|
|
558
|
+
return fs
|
|
559
|
+
.readdirSync(sessionDir)
|
|
560
|
+
.filter((file) => file.endsWith('.jsonl'))
|
|
561
|
+
.map((file) => {
|
|
562
|
+
const fullPath = path.join(sessionDir, file);
|
|
563
|
+
try {
|
|
564
|
+
const stats = fs.statSync(fullPath);
|
|
565
|
+
return { path: fullPath, mtime: stats.mtime, sizeBytes: stats.size };
|
|
566
|
+
}
|
|
567
|
+
catch {
|
|
568
|
+
return null;
|
|
569
|
+
}
|
|
570
|
+
})
|
|
571
|
+
.filter((f) => f !== null && f.sizeBytes > 0)
|
|
572
|
+
.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
|
|
573
|
+
}
|
|
574
|
+
catch {
|
|
575
|
+
return [];
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
579
|
+
// Multi-worktree session discovery
|
|
580
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
581
|
+
/**
|
|
582
|
+
* Detects if a workspace is a git worktree and resolves the main repo path.
|
|
583
|
+
*
|
|
584
|
+
* Git worktrees have a .git file (not directory) containing a gitdir: pointer.
|
|
585
|
+
* This function reads that file and resolves the main repo path.
|
|
586
|
+
*
|
|
587
|
+
* @param workspacePath - Absolute path to workspace directory
|
|
588
|
+
* @returns Main repository path, or null if not a worktree
|
|
589
|
+
*/
|
|
590
|
+
export function resolveWorktreeMainRepo(workspacePath) {
|
|
591
|
+
const gitPath = path.join(workspacePath, '.git');
|
|
592
|
+
try {
|
|
593
|
+
const stat = fs.statSync(gitPath);
|
|
594
|
+
if (stat.isDirectory()) {
|
|
595
|
+
// Regular repo, not a worktree
|
|
596
|
+
return null;
|
|
597
|
+
}
|
|
598
|
+
// It's a file — this is a worktree
|
|
599
|
+
const content = fs.readFileSync(gitPath, 'utf-8').trim();
|
|
600
|
+
const match = content.match(/^gitdir:\s*(.+)$/m);
|
|
601
|
+
if (!match)
|
|
602
|
+
return null;
|
|
603
|
+
const gitdir = match[1].trim();
|
|
604
|
+
// gitdir points to something like /main-repo/.git/worktrees/<name>
|
|
605
|
+
// Resolve to absolute path
|
|
606
|
+
const resolvedGitdir = path.isAbsolute(gitdir) ? gitdir : path.resolve(workspacePath, gitdir);
|
|
607
|
+
// Navigate up from .git/worktrees/<name> to the main repo
|
|
608
|
+
const worktreesDir = path.dirname(resolvedGitdir);
|
|
609
|
+
if (path.basename(worktreesDir) !== 'worktrees')
|
|
610
|
+
return null;
|
|
611
|
+
const dotGit = path.dirname(worktreesDir);
|
|
612
|
+
if (path.basename(dotGit) !== '.git')
|
|
613
|
+
return null;
|
|
614
|
+
return path.dirname(dotGit);
|
|
615
|
+
}
|
|
616
|
+
catch {
|
|
617
|
+
return null;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
/**
|
|
621
|
+
* Discovers all worktree sibling paths for a main repository.
|
|
622
|
+
*
|
|
623
|
+
* Reads .git/worktrees/{name}/gitdir to find all worktree paths.
|
|
624
|
+
*
|
|
625
|
+
* @param mainRepoPath - Absolute path to the main repository
|
|
626
|
+
* @returns Array of absolute paths to worktree working directories
|
|
627
|
+
*/
|
|
628
|
+
export function discoverWorktreeSiblings(mainRepoPath) {
|
|
629
|
+
const worktreesDir = path.join(mainRepoPath, '.git', 'worktrees');
|
|
630
|
+
const siblings = [];
|
|
631
|
+
try {
|
|
632
|
+
if (!fs.existsSync(worktreesDir))
|
|
633
|
+
return siblings;
|
|
634
|
+
const entries = fs.readdirSync(worktreesDir);
|
|
635
|
+
for (const entry of entries) {
|
|
636
|
+
const gitdirFile = path.join(worktreesDir, entry, 'gitdir');
|
|
637
|
+
try {
|
|
638
|
+
const content = fs.readFileSync(gitdirFile, 'utf-8').trim();
|
|
639
|
+
// gitdir file contains path to the worktree's .git file
|
|
640
|
+
// which is the worktree working directory + /.git
|
|
641
|
+
const worktreeGit = path.isAbsolute(content)
|
|
642
|
+
? content
|
|
643
|
+
: path.resolve(worktreesDir, entry, content);
|
|
644
|
+
const worktreeDir = path.dirname(worktreeGit);
|
|
645
|
+
if (fs.existsSync(worktreeDir)) {
|
|
646
|
+
siblings.push(worktreeDir);
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
catch {
|
|
650
|
+
// Skip entries without gitdir
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
catch {
|
|
655
|
+
// worktrees dir doesn't exist or isn't readable
|
|
656
|
+
}
|
|
657
|
+
return siblings;
|
|
658
|
+
}
|
|
659
|
+
/**
|
|
660
|
+
* Finds all sessions across the main repo and all its worktrees.
|
|
661
|
+
*
|
|
662
|
+
* When monitoring from a worktree, sessions started from the main repo
|
|
663
|
+
* or other worktrees are normally invisible. This function consolidates
|
|
664
|
+
* sessions from all related workspaces.
|
|
665
|
+
*
|
|
666
|
+
* @param workspacePath - Absolute path to workspace directory (may be worktree or main repo)
|
|
667
|
+
* @returns Sorted array of session file paths from all related workspaces
|
|
668
|
+
*/
|
|
669
|
+
export function findAllSessionsWithWorktrees(workspacePath) {
|
|
670
|
+
const allPaths = new Set();
|
|
671
|
+
// Start with sessions from the given workspace
|
|
672
|
+
for (const s of findAllSessions(workspacePath)) {
|
|
673
|
+
allPaths.add(s);
|
|
674
|
+
}
|
|
675
|
+
// Check if this is a worktree
|
|
676
|
+
const mainRepo = resolveWorktreeMainRepo(workspacePath);
|
|
677
|
+
if (mainRepo) {
|
|
678
|
+
// Add sessions from the main repo
|
|
679
|
+
for (const s of findAllSessions(mainRepo)) {
|
|
680
|
+
allPaths.add(s);
|
|
681
|
+
}
|
|
682
|
+
// Add sessions from sibling worktrees
|
|
683
|
+
for (const sibling of discoverWorktreeSiblings(mainRepo)) {
|
|
684
|
+
if (sibling !== workspacePath) {
|
|
685
|
+
for (const s of findAllSessions(sibling)) {
|
|
686
|
+
allPaths.add(s);
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
else {
|
|
692
|
+
// This might be the main repo — check for worktrees
|
|
693
|
+
const siblings = discoverWorktreeSiblings(workspacePath);
|
|
694
|
+
for (const sibling of siblings) {
|
|
695
|
+
for (const s of findAllSessions(sibling)) {
|
|
696
|
+
allPaths.add(s);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
// Sort by mtime (most recent first)
|
|
701
|
+
return Array.from(allPaths)
|
|
702
|
+
.map((sessionPath) => {
|
|
703
|
+
try {
|
|
704
|
+
const stat = fs.statSync(sessionPath);
|
|
705
|
+
return { path: sessionPath, mtime: stat.mtime.getTime() };
|
|
706
|
+
}
|
|
707
|
+
catch {
|
|
708
|
+
return { path: sessionPath, mtime: 0 };
|
|
709
|
+
}
|
|
710
|
+
})
|
|
711
|
+
.sort((a, b) => b.mtime - a.mtime)
|
|
712
|
+
.map((f) => f.path);
|
|
713
|
+
}
|