Skip to content

Commit 199e1ac

Browse files
authored
Merge pull request #451 from rajbos/crush-support
Adding support for the "Crush" editor (split from OpenCode)
2 parents 3648e92 + cd118d1 commit 199e1ac

8 files changed

Lines changed: 680 additions & 25 deletions

File tree

cli/src/helpers.ts

Lines changed: 68 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import * as os from 'os';
88
import chalk from 'chalk';
99
import { SessionDiscovery } from '../../src/sessionDiscovery';
1010
import { OpenCodeDataAccess } from '../../src/opencode';
11+
import { CrushDataAccess } from '../../src/crush';
12+
import { ContinueDataAccess } from '../../src/continue';
1113
import { parseSessionFileContent } from '../../src/sessionParser';
1214
import { estimateTokensFromText, getModelFromRequest, isJsonlContent, estimateTokensFromJsonlSession, calculateEstimatedCost, getModelTier } from '../../src/tokenEstimation';
1315
import type { DetailedStats, PeriodStats, ModelUsage, EditorUsage, SessionFileCache, UsageAnalysisStats, UsageAnalysisPeriod } from '../../src/types';
@@ -40,10 +42,25 @@ function createOpenCode(): OpenCodeDataAccess {
4042
return new OpenCodeDataAccess(fakeUri as any);
4143
}
4244

45+
/** Create Crush data access instance for CLI */
46+
function createCrush(): CrushDataAccess {
47+
const fakeUri = vscodeStub.Uri.file(__dirname);
48+
return new CrushDataAccess(fakeUri as any);
49+
}
50+
51+
/** Create Continue data access instance for CLI */
52+
function createContinue(): ContinueDataAccess {
53+
return new ContinueDataAccess();
54+
}
55+
56+
// Module-level singletons so sql.js WASM is only initialised once across all session files
57+
const _openCodeInstance = createOpenCode();
58+
const _crushInstance = createCrush();
59+
const _continueInstance = createContinue();
60+
4361
/** Create session discovery instance for CLI */
4462
function createSessionDiscovery(): SessionDiscovery {
45-
const openCode = createOpenCode();
46-
return new SessionDiscovery({ log, warn, error, openCode });
63+
return new SessionDiscovery({ log, warn, error, openCode: _openCodeInstance, crush: _crushInstance, continue_: _continueInstance });
4764
}
4865

4966
/** Discover all session files on this machine */
@@ -80,10 +97,20 @@ function isOpenCodeDbSession(filePath: string): boolean {
8097
}
8198

8299
/**
83-
* Stat a session file, handling OpenCode DB virtual paths.
84-
* Virtual DB paths (opencode.db#ses_<id>) are resolved to the actual DB file.
100+
* Check if a session file path is a Crush DB virtual path.
101+
*/
102+
function isCrushSessionFile(filePath: string): boolean {
103+
return _crushInstance.isCrushSessionFile(filePath);
104+
}
105+
106+
/**
107+
* Stat a session file, handling DB virtual paths (OpenCode and Crush).
108+
* Virtual DB paths are resolved to the actual DB file.
85109
*/
86110
async function statSessionFile(filePath: string): Promise<fs.Stats> {
111+
if (isCrushSessionFile(filePath)) {
112+
return _crushInstance.statSessionFile(filePath);
113+
}
87114
if (isOpenCodeDbSession(filePath)) {
88115
const dbPath = filePath.split('#')[0];
89116
return fs.promises.stat(dbPath);
@@ -99,6 +126,7 @@ function getEditorSourceFromPath(filePath: string): string {
99126
if (normalized.includes('/code - exploration/')) { return 'vscode-exploration'; }
100127
if (normalized.includes('/vscodium/')) { return 'vscodium'; }
101128
if (normalized.includes('/.copilot/')) { return 'copilot-cli'; }
129+
if (normalized.includes('/.crush/crush.db#')) { return 'crush'; }
102130
if (normalized.includes('/opencode/')) { return 'opencode'; }
103131
if (normalized.includes('.vscode-server')) { return 'vscode-remote'; }
104132
return 'vscode';
@@ -120,9 +148,40 @@ export interface SessionData {
120148
export async function processSessionFile(filePath: string): Promise<SessionData | null> {
121149
try {
122150
const stats = await statSessionFile(filePath);
123-
const content = isOpenCodeDbSession(filePath)
124-
? '' // OpenCode DB sessions are handled by the opencode module
125-
: await fs.promises.readFile(filePath, 'utf-8');
151+
152+
// Handle Crush DB virtual paths directly via the crush module
153+
if (isCrushSessionFile(filePath)) {
154+
const result = await _crushInstance.getTokensFromCrushSession(filePath);
155+
const interactions = await _crushInstance.countCrushInteractions(filePath);
156+
const modelUsage = await _crushInstance.getCrushModelUsage(filePath);
157+
return {
158+
file: filePath,
159+
tokens: result.tokens,
160+
thinkingTokens: result.thinkingTokens,
161+
interactions,
162+
modelUsage,
163+
lastModified: stats.mtime,
164+
editorSource: getEditorSourceFromPath(filePath),
165+
};
166+
}
167+
168+
// Handle OpenCode DB virtual paths directly via the opencode module
169+
if (isOpenCodeDbSession(filePath)) {
170+
const result = await _openCodeInstance.getTokensFromOpenCodeSession(filePath);
171+
const interactions = await _openCodeInstance.countOpenCodeInteractions(filePath);
172+
const modelUsage = await _openCodeInstance.getOpenCodeModelUsage(filePath);
173+
return {
174+
file: filePath,
175+
tokens: result.tokens,
176+
thinkingTokens: result.thinkingTokens,
177+
interactions,
178+
modelUsage,
179+
lastModified: stats.mtime,
180+
editorSource: getEditorSourceFromPath(filePath),
181+
};
182+
}
183+
184+
const content = await fs.promises.readFile(filePath, 'utf-8');
126185

127186
if (!content.trim()) {
128187
return null;
@@ -306,11 +365,10 @@ function aggregateIntoPeriod(period: PeriodStats, data: SessionData): void {
306365
* This is a simplified version that uses the shared usageAnalysis module.
307366
*/
308367
export async function calculateUsageAnalysisStats(sessionFiles: string[]): Promise<UsageAnalysisStats> {
309-
const openCode = createOpenCode();
310-
311368
const deps = {
312369
warn,
313-
openCode,
370+
openCode: _openCodeInstance,
371+
crush: _crushInstance,
314372
tokenEstimators,
315373
modelPricing,
316374
toolNameMap,

0 commit comments

Comments
 (0)