Skip to content

Commit a069bd4

Browse files
jakeefrclaude
andcommitted
fix: resolve Windows project paths in --project flag
Accept real Windows paths (D:\jarvis\space), forward-slash paths (D:/jarvis/space), and table display names (D//jarvis/space) in the --project flag for both `advise` and `analyze` commands. Adds project_path_to_encoded_name() to parser.py and updates _resolve_projects() with a two-strategy lookup: direct directory first, then encoded name under ~/.claude/projects/. Also adds exists=False to advise_cmd's --project option. Bumps version to 0.1.2. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent e0b9b09 commit a069bd4

5 files changed

Lines changed: 122 additions & 12 deletions

File tree

prism/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
"""PRISM — session intelligence for Claude Code."""
22

3-
__version__ = "0.1.1"
3+
__version__ = "0.1.2"

prism/cli.py

Lines changed: 53 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,13 @@
1818

1919
from prism import __version__
2020
from prism.analyzer import analyze_project, score_to_grade
21-
from prism.parser import CLAUDE_PROJECTS_DIR, ProjectInfo, discover_projects, parse_session_file
21+
from prism.parser import (
22+
CLAUDE_PROJECTS_DIR,
23+
ProjectInfo,
24+
discover_projects,
25+
parse_session_file,
26+
project_path_to_encoded_name,
27+
)
2228

2329
# Force UTF-8 output on Windows so Unicode symbols render correctly
2430
if hasattr(sys.stdout, "reconfigure"):
@@ -227,6 +233,7 @@ def advise_cmd(
227233
"--project",
228234
"-p",
229235
help="Path to a specific project directory.",
236+
exists=False,
230237
),
231238
apply: bool = typer.Option(
232239
False,
@@ -393,22 +400,58 @@ def _resolve_projects(
393400
project_path: Path | None,
394401
base_dir: Path | None,
395402
) -> list[ProjectInfo]:
396-
"""Return a list of ProjectInfo objects based on CLI options."""
403+
"""Return a list of ProjectInfo objects based on CLI options.
404+
405+
The ``--project`` flag accepts any of:
406+
407+
* The actual path to a Claude Code project directory (inside
408+
``~/.claude/projects/``), e.g. ``~/.claude/projects/D--jarvis-space``.
409+
* The real absolute path of the user's workspace on any OS, e.g.
410+
``D:\\jarvis\\space`` or ``/home/user/proj``. The path is encoded
411+
to the Claude Code convention and looked up under *effective_base*.
412+
* The display name shown in the projects table, e.g. ``D//jarvis/space``
413+
or ``/home/user/proj``. Forward-slash normalisation by ``pathlib``
414+
means this is equivalent to passing the real path on most systems.
415+
"""
397416
effective_base = base_dir or CLAUDE_PROJECTS_DIR
398417

399418
if project_path is not None:
400-
# User pointed at a specific project directory
419+
# Strategy 1: the argument is already a Claude Code project directory
420+
# that contains JSONL session files — use it directly.
401421
if project_path.is_dir():
402-
from prism.parser import ProjectInfo
403-
sessions = sorted(project_path.glob("*.jsonl"), key=lambda p: p.stat().st_mtime, reverse=True)
422+
sessions = sorted(
423+
project_path.glob("*.jsonl"),
424+
key=lambda p: p.stat().st_mtime,
425+
reverse=True,
426+
)
427+
if sessions:
428+
return [ProjectInfo(
429+
encoded_name=project_path.name,
430+
project_dir=project_path,
431+
session_files=sessions,
432+
)]
433+
434+
# Strategy 2: interpret the argument as a real path or display name
435+
# and look up the corresponding encoded directory inside effective_base.
436+
encoded = project_path_to_encoded_name(str(project_path))
437+
candidate = effective_base / encoded
438+
if candidate.is_dir():
439+
sessions = sorted(
440+
candidate.glob("*.jsonl"),
441+
key=lambda p: p.stat().st_mtime,
442+
reverse=True,
443+
)
404444
return [ProjectInfo(
405-
encoded_name=project_path.name,
406-
project_dir=project_path,
445+
encoded_name=encoded,
446+
project_dir=candidate,
407447
session_files=sessions,
408448
)]
409-
else:
410-
err_console.print(f"[red]Project path is not a directory: {project_path}[/red]")
411-
return []
449+
450+
err_console.print(f"[red]Project not found: {project_path}[/red]")
451+
err_console.print(
452+
f"[dim]Tried encoded name '{encoded}' in {effective_base}[/dim]"
453+
)
454+
return []
412455

413456
return discover_projects(effective_base)
414457

prism/parser.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,32 @@ def last_active(self) -> str | None:
259259
return newest.stat().st_mtime.__class__.__name__ # return mtime float str
260260

261261

262+
def project_path_to_encoded_name(path_str: str) -> str:
263+
"""Convert a path string to a Claude Code encoded project directory name.
264+
265+
Claude Code stores project sessions under ``~/.claude/projects/`` using
266+
directory names that are the absolute project path with every path
267+
separator and colon replaced by a hyphen. This function performs that
268+
same normalisation so callers can look up a project by its real path
269+
*or* by the display name shown in the projects table.
270+
271+
Examples::
272+
273+
"D:\\\\jarvis\\\\space" -> "D--jarvis-space" # native Windows path
274+
"D:/jarvis/space" -> "D--jarvis-space" # forward-slash Windows
275+
"D//jarvis/space" -> "D--jarvis-space" # display name from table
276+
"/home/user/proj" -> "-home-user-proj" # Unix path
277+
278+
Args:
279+
path_str: A real absolute path (any OS) or a display name from the
280+
projects table.
281+
282+
Returns:
283+
The encoded directory name used by Claude Code.
284+
"""
285+
return path_str.replace("\\", "-").replace("/", "-").replace(":", "-")
286+
287+
262288
def discover_projects(base_dir: Path | None = None) -> list[ProjectInfo]:
263289
"""Discover all Claude Code projects under base_dir (default: ~/.claude/projects).
264290

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "prism-cc"
7-
version = "0.1.1"
7+
version = "0.1.2"
88
description = "Session intelligence for Claude Code — find why your sessions fail and fix them"
99
readme = "README.md"
1010
license = { text = "MIT" }

tests/test_parser.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,13 @@
1111
AssistantRecord,
1212
ContentBlock,
1313
ParseResult,
14+
ProjectInfo,
1415
SystemRecord,
1516
UserRecord,
1617
discover_projects,
1718
parse_record,
1819
parse_session_file,
20+
project_path_to_encoded_name,
1921
)
2022

2123
FIXTURES = Path(__file__).parent / "fixtures"
@@ -352,3 +354,42 @@ def test_non_jsonl_files_ignored(self, tmp_path):
352354
projects = discover_projects(tmp_path)
353355
assert len(projects[0].session_files) == 1
354356
assert projects[0].session_files[0].suffix == ".jsonl"
357+
358+
359+
# ---------------------------------------------------------------------------
360+
# project_path_to_encoded_name tests
361+
# ---------------------------------------------------------------------------
362+
363+
class TestProjectPathToEncodedName:
364+
"""Verify encoding round-trips for Windows paths, Unix paths, and display names."""
365+
366+
def test_unix_absolute_path(self):
367+
assert project_path_to_encoded_name("/home/user/myproject") == "-home-user-myproject"
368+
369+
def test_unix_nested_path(self):
370+
assert project_path_to_encoded_name("/home/alice/work/proj") == "-home-alice-work-proj"
371+
372+
def test_windows_backslash_path(self):
373+
# Native Windows path with backslash separators
374+
assert project_path_to_encoded_name("D:\\jarvis\\space") == "D--jarvis-space"
375+
376+
def test_windows_forward_slash_path(self):
377+
# Windows path written with forward slashes
378+
assert project_path_to_encoded_name("D:/jarvis/space") == "D--jarvis-space"
379+
380+
def test_windows_display_name(self):
381+
# Display name as shown in the projects table (D:\ → D-- after decode)
382+
assert project_path_to_encoded_name("D//jarvis/space") == "D--jarvis-space"
383+
384+
def test_already_encoded_name_unchanged(self):
385+
# An already-encoded name has no path chars — passes through as-is
386+
assert project_path_to_encoded_name("D--jarvis-space") == "D--jarvis-space"
387+
388+
def test_display_name_roundtrip(self):
389+
"""display_name -> encode -> same encoded dir name."""
390+
encoded = "D--jarvis-space"
391+
info = ProjectInfo(encoded_name=encoded, project_dir=Path("."), session_files=[])
392+
# display_name replaces "-" with "/" giving "D//jarvis/space"
393+
display = info.display_name
394+
# re-encoding the display should yield the original encoded name
395+
assert project_path_to_encoded_name(display) == encoded

0 commit comments

Comments
 (0)