Skip to content

Commit c6337cf

Browse files
ryoppippiclaude
andauthored
fix(config): don't require .claude directory for non-Claude agents (#1017)
* fix(config): don't require .claude directory for non-Claude agents ccusage now supports codex, opencode, amp, and pi-agent in addition to Claude Code. Running it on a machine without a Claude data directory crashed with "No valid Claude data directories found" because getConfigSearchPaths() called the throwing getClaudePaths() while discovering ccusage.json locations. Treat Claude config dirs as best-effort: swallow the error and fall back to the local .ccusage directory so users of other agents (or no agent yet) can run ccusage without setting CLAUDE_CONFIG_DIR. Fixes #1014 * refactor(claude): split getClaudePaths into safe/required variants Previously getClaudePaths() threw when no Claude data directory was found, which forced callers that don't actually need a Claude dir (like config discovery in getConfigSearchPaths) to wrap the call in try/catch or Result.try. That asymmetry is what made #1014 easy to hit in the first place. Make getClaudePaths() the unsurprising "give me what you find, possibly nothing" variant, and add requireClaudePaths() for explicit Claude-only entry points (claude:* command loaders, statusline session lookup) that want to surface the helpful setup hint when no data exists. debug.ts already had its own length check so it keeps using getClaudePaths(). The config layer now just calls getClaudePaths() directly — no wrapping needed — restoring the consistency with the other agents' detect functions and getClaudeProjectPaths(). No behavior change for users: - `ccusage` on a machine without .claude still runs and reports "None" - `ccusage claude daily` without .claude still surfaces the original "No valid Claude data directories found" error --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 4103209 commit c6337cf

2 files changed

Lines changed: 60 additions & 24 deletions

File tree

apps/ccusage/src/adapter/claude/data-loader.ts

Lines changed: 38 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -247,13 +247,17 @@ function getTimestampFromLine(line: string): Date | null {
247247
}
248248

249249
/**
250-
* Get Claude data directories to search for usage data
250+
* Get Claude data directories that actually contain a `projects/` subdirectory.
251251
* When CLAUDE_CONFIG_DIR is set: uses only those paths
252252
* When not set: uses default paths (~/.config/claude and ~/.claude)
253-
* @returns Array of valid Claude data directory paths
253+
*
254+
* Returns an empty array when nothing is found. Callers that require at least
255+
* one valid Claude path (e.g. explicit `ccusage claude …` commands) should use
256+
* {@link requireClaudePaths} instead so users get a helpful error message.
257+
* @returns Array of valid Claude data directory paths (possibly empty)
254258
*/
255259
export function getClaudePaths(): string[] {
256-
const paths = [];
260+
const paths: string[] = [];
257261
const normalizedPaths = new Set<string>();
258262

259263
// Check environment variable first (supports comma-separated paths)
@@ -276,15 +280,8 @@ export function getClaudePaths(): string[] {
276280
}
277281
}
278282
}
279-
// If environment variable is set, return only those paths (or error if none valid)
280-
if (paths.length > 0) {
281-
return paths;
282-
}
283-
// If environment variable is set but no valid paths found, throw error
284-
throw new Error(
285-
`No valid Claude data directories found in CLAUDE_CONFIG_DIR. Please ensure the following exists:
286-
- ${envPaths}/${CLAUDE_PROJECTS_DIR_NAME}`.trim(),
287-
);
283+
// If environment variable is set, return only those paths
284+
return paths;
288285
}
289286

290287
// Only check default paths if no environment variable is set
@@ -307,16 +304,35 @@ export function getClaudePaths(): string[] {
307304
}
308305
}
309306

310-
if (paths.length === 0) {
307+
return paths;
308+
}
309+
310+
/**
311+
* Like {@link getClaudePaths} but throws a user-friendly error when no Claude
312+
* data directory exists. Use this from explicit Claude-only entry points (the
313+
* `claude:*` commands and `debug` utilities) where surfacing the setup hint to
314+
* the user is the right behavior.
315+
*/
316+
export function requireClaudePaths(): string[] {
317+
const paths = getClaudePaths();
318+
if (paths.length > 0) {
319+
return paths;
320+
}
321+
322+
const envPaths = (process.env[CLAUDE_CONFIG_DIR_ENV] ?? '').trim();
323+
if (envPaths !== '') {
311324
throw new Error(
312-
`No valid Claude data directories found. Please ensure at least one of the following exists:
313-
- ${path.join(DEFAULT_CLAUDE_CONFIG_PATH, CLAUDE_PROJECTS_DIR_NAME)}
314-
- ${path.join(USER_HOME_DIR, DEFAULT_CLAUDE_CODE_PATH, CLAUDE_PROJECTS_DIR_NAME)}
315-
- Or set ${CLAUDE_CONFIG_DIR_ENV} environment variable to valid directory path(s) containing a '${CLAUDE_PROJECTS_DIR_NAME}' subdirectory`.trim(),
325+
`No valid Claude data directories found in CLAUDE_CONFIG_DIR. Please ensure the following exists:
326+
- ${envPaths}/${CLAUDE_PROJECTS_DIR_NAME}`.trim(),
316327
);
317328
}
318329

319-
return paths;
330+
throw new Error(
331+
`No valid Claude data directories found. Please ensure at least one of the following exists:
332+
- ${path.join(DEFAULT_CLAUDE_CONFIG_PATH, CLAUDE_PROJECTS_DIR_NAME)}
333+
- ${path.join(USER_HOME_DIR, DEFAULT_CLAUDE_CODE_PATH, CLAUDE_PROJECTS_DIR_NAME)}
334+
- Or set ${CLAUDE_CONFIG_DIR_ENV} environment variable to valid directory path(s) containing a '${CLAUDE_PROJECTS_DIR_NAME}' subdirectory`.trim(),
335+
);
320336
}
321337

322338
/**
@@ -2271,7 +2287,7 @@ async function collectBlockFileResult(
22712287
*/
22722288
export async function loadDailyUsageData(options?: LoadOptions): Promise<DailyUsage[]> {
22732289
// Get all Claude paths or use the specific one from options
2274-
const claudePaths = toArray(options?.claudePath ?? getClaudePaths());
2290+
const claudePaths = toArray(options?.claudePath ?? requireClaudePaths());
22752291

22762292
// Collect files from all paths in parallel
22772293
const allFiles = await globUsageFiles(claudePaths);
@@ -2407,7 +2423,7 @@ export async function loadDailyUsageData(options?: LoadOptions): Promise<DailyUs
24072423
*/
24082424
export async function loadSessionData(options?: LoadOptions): Promise<SessionUsage[]> {
24092425
// Get all Claude paths or use the specific one from options
2410-
const claudePaths = toArray(options?.claudePath ?? getClaudePaths());
2426+
const claudePaths = toArray(options?.claudePath ?? requireClaudePaths());
24112427

24122428
// Collect files from all paths with their base directories in parallel
24132429
const filesWithBase = await globUsageFiles(claudePaths);
@@ -2596,7 +2612,7 @@ export async function loadSessionUsageById(
25962612
sessionId: string,
25972613
options?: { mode?: CostMode; offline?: boolean },
25982614
): Promise<{ totalCost: number; entries: UsageData[] } | null> {
2599-
const claudePaths = getClaudePaths();
2615+
const claudePaths = requireClaudePaths();
26002616

26012617
const targetFile = `${sessionId}.jsonl`;
26022618
let file: string | undefined;
@@ -2846,7 +2862,7 @@ function compareBlockFileResults(
28462862
*/
28472863
export async function loadSessionBlockData(options?: LoadOptions): Promise<SessionBlock[]> {
28482864
// Get all Claude paths or use the specific one from options
2849-
const claudePaths = toArray(options?.claudePath ?? getClaudePaths());
2865+
const claudePaths = toArray(options?.claudePath ?? requireClaudePaths());
28502866

28512867
// Collect files from all paths
28522868
const allFiles = (await globUsageFiles(claudePaths)).map((item) => item.file);

apps/ccusage/src/config-loader-tokens.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,10 +72,13 @@ export type ConfigData = {
7272
* Get configuration file search paths in priority order (highest to lowest)
7373
* 1. Local .ccusage/ccusage.json
7474
* 2. User config directories from getClaudePaths() + ccusage.json
75+
*
76+
* Claude paths are best-effort: ccusage supports non-Claude agents too, so a
77+
* missing Claude data directory simply contributes no extra search paths.
7578
*/
7679
function getConfigSearchPaths(): string[] {
77-
const claudeConfigDirs = [join(process.cwd(), '.ccusage'), ...toArray(getClaudePaths())];
78-
return claudeConfigDirs.map((dir) => join(dir, CONFIG_FILE_NAME));
80+
const dirs = [join(process.cwd(), '.ccusage'), ...toArray(getClaudePaths())];
81+
return dirs.map((dir) => join(dir, CONFIG_FILE_NAME));
7982
}
8083

8184
/**
@@ -622,6 +625,23 @@ if (import.meta.vitest != null) {
622625
expect(config?.defaults?.json).toBe(true);
623626
});
624627

628+
it('does not surface getClaudePaths errors when no Claude data exists (issue #1014)', async () => {
629+
await using fixture = await createFixture({
630+
'empty-dir': '',
631+
});
632+
633+
// Point CLAUDE_CONFIG_DIR at a directory without a projects/ subfolder so
634+
// getClaudePaths() throws; loadConfig() must still return undefined
635+
// instead of propagating the Claude-specific error.
636+
vi.stubEnv('CLAUDE_CONFIG_DIR', fixture.getPath('empty-dir'));
637+
vi.spyOn(process, 'cwd').mockReturnValue(fixture.getPath());
638+
639+
expect(() => loadConfig()).not.toThrow();
640+
expect(loadConfig()).toBeUndefined();
641+
642+
vi.unstubAllEnvs();
643+
});
644+
625645
it('should handle empty configuration file', async () => {
626646
await using fixture = await createFixture({
627647
'.ccusage/ccusage.json': '{}',

0 commit comments

Comments
 (0)