Skip to content

Commit 03b2467

Browse files
committed
Fix session tokens --project filter for absolute paths
The filter compared Claude's on-disk project slug (e.g. "-workspaces-projects-CodeForge") to the raw --project path (e.g. "/workspaces/projects/CodeForge"), which never matched — so passing an absolute path per the session-search convention returned zero sessions. Encode absolute and ./..-relative inputs to slug form (replace / and . with -) before comparing. Plain substrings without separators still pass through unchanged, so --project CodeForge keeps working. Bump CLI to 0.2.1 and add tokens.test.ts covering the transform.
1 parent c01fde2 commit 03b2467

4 files changed

Lines changed: 82 additions & 5 deletions

File tree

cli/CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# CodeForge CLI Changelog
22

3+
## v0.2.1 — 2026-04-16
4+
5+
### Fixes
6+
7+
- **`session tokens --project` now works with absolute paths** — the filter was comparing an on-disk slug (e.g. `-workspaces-projects-CodeForge`) to the raw `--project` path (e.g. `/workspaces/projects/CodeForge`), which never matched. Absolute paths and paths starting with `./` / `../` are now encoded to Claude's slug form (replacing `/` and `.` with `-`) before matching. Plain substrings without separators still pass through unchanged, so `--project CodeForge` keeps working.
8+
39
## v0.2.0 — 2026-04-16
410

511
### New Command

cli/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"publishConfig": {
44
"access": "public"
55
},
6-
"version": "0.2.0",
6+
"version": "0.2.1",
77
"description": "CLI for CodeForge development workflows",
88
"keywords": [
99
"codeforge",

cli/src/commands/session/tokens.ts

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import chalk from "chalk";
22
import type { Command } from "commander";
3-
import { basename } from "path";
3+
import { basename, isAbsolute, resolve } from "path";
44
import { readLines } from "../../search/engine.js";
55
import { discoverSessionFiles } from "../../utils/glob.js";
66
import { parseRelativeTime, parseTime } from "../../utils/time.js";
@@ -80,6 +80,30 @@ function extractProjectFromPath(filePath: string): string | undefined {
8080
return undefined;
8181
}
8282

83+
/**
84+
* Normalize a user-provided --project value for matching against Claude's
85+
* on-disk project slugs.
86+
*
87+
* Claude encodes project cwds by replacing `/` and `.` with `-`, e.g.
88+
* /workspaces/projects/CodeForge -> -workspaces-projects-CodeForge
89+
* /workspaces/.devcontainer -> -workspaces--devcontainer
90+
*
91+
* Behavior:
92+
* - Absolute paths are converted to slug form.
93+
* - Relative paths beginning with `./` or `../` are resolved against cwd first.
94+
* - Plain substrings without separators pass through unchanged so users can
95+
* filter with `--project CodeForge` against the slug (backwards-compatible).
96+
*/
97+
export function pathToProjectSlug(input: string): string {
98+
const looksLikePath =
99+
isAbsolute(input) || input.startsWith("./") || input.startsWith("../");
100+
if (looksLikePath) {
101+
const abs = isAbsolute(input) ? input : resolve(input);
102+
return abs.replace(/\/+$/, "").replace(/[./]/g, "-");
103+
}
104+
return input;
105+
}
106+
83107
function isSubagentPath(filePath: string): boolean {
84108
return filePath.includes("/subagents/");
85109
}
@@ -304,11 +328,15 @@ async function analyzeTokens(options: {
304328
const mainSessions: SessionTokenStats[] = [];
305329
const subagentSessions: SessionTokenStats[] = [];
306330

331+
const projectNeedle = options.project
332+
? pathToProjectSlug(options.project)
333+
: undefined;
334+
307335
for (const filePath of files) {
308-
// Filter by project
309-
if (options.project) {
336+
// Filter by project (slug-to-slug match; absolute paths are encoded).
337+
if (projectNeedle) {
310338
const project = extractProjectFromPath(filePath);
311-
if (!project?.includes(options.project)) continue;
339+
if (!project?.includes(projectNeedle)) continue;
312340
}
313341

314342
const stats = await analyzeSessionTokens(filePath);

cli/tests/tokens.test.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { describe, expect, test } from "bun:test";
2+
import { pathToProjectSlug } from "../src/commands/session/tokens.js";
3+
4+
describe("pathToProjectSlug", () => {
5+
test("encodes absolute paths to claude's slug form", () => {
6+
expect(pathToProjectSlug("/workspaces/projects/CodeForge")).toBe(
7+
"-workspaces-projects-CodeForge",
8+
);
9+
});
10+
11+
test("encodes dotfiles with double dashes like claude does", () => {
12+
// /workspaces/.devcontainer -> /workspaces/-devcontainer (slash -> dash,
13+
// then the leading dot on the next segment becomes another dash)
14+
expect(pathToProjectSlug("/workspaces/.devcontainer")).toBe(
15+
"-workspaces--devcontainer",
16+
);
17+
});
18+
19+
test("strips trailing slashes before encoding", () => {
20+
expect(pathToProjectSlug("/workspaces/projects/CodeForge/")).toBe(
21+
"-workspaces-projects-CodeForge",
22+
);
23+
expect(pathToProjectSlug("/workspaces/projects/CodeForge///")).toBe(
24+
"-workspaces-projects-CodeForge",
25+
);
26+
});
27+
28+
test("passes plain substrings through unchanged for backwards compat", () => {
29+
// No separator -> user wants substring match against a slug
30+
expect(pathToProjectSlug("CodeForge")).toBe("CodeForge");
31+
expect(pathToProjectSlug("projects-CodeForge")).toBe("projects-CodeForge");
32+
});
33+
34+
test("resolves relative ./ and ../ paths before encoding", () => {
35+
const abs = pathToProjectSlug("./foo");
36+
// Resolved path always ends with /foo; after encoding trailing segment is -foo
37+
expect(abs.endsWith("-foo")).toBe(true);
38+
expect(abs.startsWith("-")).toBe(true);
39+
40+
const abs2 = pathToProjectSlug("../bar");
41+
expect(abs2.endsWith("-bar")).toBe(true);
42+
});
43+
});

0 commit comments

Comments
 (0)