nmem-cli 0.8.1__tar.gz → 0.8.4__tar.gz
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.
- {nmem_cli-0.8.1 → nmem_cli-0.8.4}/PKG-INFO +1 -1
- {nmem_cli-0.8.1 → nmem_cli-0.8.4}/pyproject.toml +1 -1
- {nmem_cli-0.8.1 → nmem_cli-0.8.4}/src/nmem_cli/__init__.py +1 -1
- nmem_cli-0.8.4/src/nmem_cli/claude_paths.py +63 -0
- {nmem_cli-0.8.1 → nmem_cli-0.8.4}/src/nmem_cli/cli.py +1 -1
- {nmem_cli-0.8.1 → nmem_cli-0.8.4}/src/nmem_cli/session_import.py +75 -68
- {nmem_cli-0.8.1 → nmem_cli-0.8.4}/src/nmem_cli/tui/screens/settings.py +1 -1
- {nmem_cli-0.8.1 → nmem_cli-0.8.4}/.gitignore +0 -0
- {nmem_cli-0.8.1 → nmem_cli-0.8.4}/README.md +0 -0
- {nmem_cli-0.8.1 → nmem_cli-0.8.4}/src/nmem_cli/license_payload.py +0 -0
- {nmem_cli-0.8.1 → nmem_cli-0.8.4}/src/nmem_cli/py.typed +0 -0
- {nmem_cli-0.8.1 → nmem_cli-0.8.4}/src/nmem_cli/tui/__init__.py +0 -0
- {nmem_cli-0.8.1 → nmem_cli-0.8.4}/src/nmem_cli/tui/__main__.py +0 -0
- {nmem_cli-0.8.1 → nmem_cli-0.8.4}/src/nmem_cli/tui/api_client.py +0 -0
- {nmem_cli-0.8.1 → nmem_cli-0.8.4}/src/nmem_cli/tui/app.py +0 -0
- {nmem_cli-0.8.1 → nmem_cli-0.8.4}/src/nmem_cli/tui/screens/__init__.py +0 -0
- {nmem_cli-0.8.1 → nmem_cli-0.8.4}/src/nmem_cli/tui/screens/dashboard.py +0 -0
- {nmem_cli-0.8.1 → nmem_cli-0.8.4}/src/nmem_cli/tui/screens/graph.py +0 -0
- {nmem_cli-0.8.1 → nmem_cli-0.8.4}/src/nmem_cli/tui/screens/help.py +0 -0
- {nmem_cli-0.8.1 → nmem_cli-0.8.4}/src/nmem_cli/tui/screens/memories.py +0 -0
- {nmem_cli-0.8.1 → nmem_cli-0.8.4}/src/nmem_cli/tui/screens/memory_detail.py +0 -0
- {nmem_cli-0.8.1 → nmem_cli-0.8.4}/src/nmem_cli/tui/screens/thread_detail.py +0 -0
- {nmem_cli-0.8.1 → nmem_cli-0.8.4}/src/nmem_cli/tui/screens/threads.py +0 -0
- {nmem_cli-0.8.1 → nmem_cli-0.8.4}/src/nmem_cli/tui/widgets/__init__.py +0 -0
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""Claude Code project-directory path decoding helpers."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def find_valid_claude_path(base: str, remaining: list[str]) -> str | None:
|
|
7
|
+
"""Find a valid path by matching Claude-encoded segments to real dirs."""
|
|
8
|
+
seen: set[tuple[str, tuple[str, ...]]] = set()
|
|
9
|
+
|
|
10
|
+
def _walk(current_base: str, parts: tuple[str, ...]) -> str | None:
|
|
11
|
+
state = (os.path.normpath(current_base), parts)
|
|
12
|
+
if state in seen:
|
|
13
|
+
return None
|
|
14
|
+
seen.add(state)
|
|
15
|
+
|
|
16
|
+
if not parts:
|
|
17
|
+
return current_base if os.path.exists(current_base) else None
|
|
18
|
+
|
|
19
|
+
try:
|
|
20
|
+
entries = list(os.scandir(current_base))
|
|
21
|
+
except OSError:
|
|
22
|
+
return None
|
|
23
|
+
|
|
24
|
+
matches: list[tuple[int, str]] = []
|
|
25
|
+
for entry in entries:
|
|
26
|
+
try:
|
|
27
|
+
if not entry.is_dir():
|
|
28
|
+
continue
|
|
29
|
+
except OSError:
|
|
30
|
+
continue
|
|
31
|
+
|
|
32
|
+
for encoded_parts in _encoded_segment_variants(entry.name):
|
|
33
|
+
length = len(encoded_parts)
|
|
34
|
+
if length <= len(parts) and parts[:length] == encoded_parts:
|
|
35
|
+
matches.append((length, entry.path))
|
|
36
|
+
break
|
|
37
|
+
|
|
38
|
+
for consumed, next_base in sorted(
|
|
39
|
+
matches,
|
|
40
|
+
key=lambda candidate: candidate[0],
|
|
41
|
+
reverse=True,
|
|
42
|
+
):
|
|
43
|
+
result = _walk(next_base, parts[consumed:])
|
|
44
|
+
if result:
|
|
45
|
+
return result
|
|
46
|
+
return None
|
|
47
|
+
|
|
48
|
+
return _walk(base, tuple(remaining))
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _encoded_segment_variants(segment: str) -> list[tuple[str, ...]]:
|
|
52
|
+
variants: list[tuple[str, ...]] = []
|
|
53
|
+
|
|
54
|
+
def _add(encoded: str) -> None:
|
|
55
|
+
parts = tuple(encoded.split("-"))
|
|
56
|
+
if parts not in variants:
|
|
57
|
+
variants.append(parts)
|
|
58
|
+
|
|
59
|
+
_add(segment.replace(".", "-").replace("_", "-"))
|
|
60
|
+
if segment.startswith(".") and len(segment) > 1:
|
|
61
|
+
_add("-" + segment[1:])
|
|
62
|
+
_add(segment)
|
|
63
|
+
return variants
|
|
@@ -88,7 +88,7 @@ PROVIDER_DEFAULTS: dict[str, dict[str, str]] = {
|
|
|
88
88
|
"anthropic": {"model": "claude-sonnet-4-20250514", "api_base": "https://api.anthropic.com"},
|
|
89
89
|
"gemini": {"model": "gemini-flash-latest", "api_base": "https://generativelanguage.googleapis.com"},
|
|
90
90
|
"xai": {"model": "grok-3", "api_base": "https://api.x.ai/v1"},
|
|
91
|
-
"deepseek": {"model": "deepseek-
|
|
91
|
+
"deepseek": {"model": "deepseek-v4-flash", "api_base": "https://api.deepseek.com/v1"},
|
|
92
92
|
"openrouter": {"model": "anthropic/claude-sonnet-4", "api_base": "https://openrouter.ai/api/v1"},
|
|
93
93
|
"github_copilot": {"model": "gpt-5-mini"},
|
|
94
94
|
"openai_codex": {"model": "gpt-5.2-codex"},
|
|
@@ -25,9 +25,55 @@ from datetime import datetime
|
|
|
25
25
|
from pathlib import Path
|
|
26
26
|
from typing import Any, Optional
|
|
27
27
|
|
|
28
|
+
from .claude_paths import find_valid_claude_path
|
|
29
|
+
|
|
28
30
|
|
|
29
31
|
logger = logging.getLogger(__name__)
|
|
30
32
|
|
|
33
|
+
_KNOWN_TITLE_SCAFFOLD_PREFIXES = (
|
|
34
|
+
"system:",
|
|
35
|
+
"developer:",
|
|
36
|
+
"you are ",
|
|
37
|
+
"you are codex",
|
|
38
|
+
"you are claude code",
|
|
39
|
+
"you are opencode",
|
|
40
|
+
"you are a coding assistant",
|
|
41
|
+
"you are an ai coding assistant",
|
|
42
|
+
"act as ",
|
|
43
|
+
"# ",
|
|
44
|
+
"## ",
|
|
45
|
+
"### ",
|
|
46
|
+
"instructions:",
|
|
47
|
+
"rules:",
|
|
48
|
+
"constraints:",
|
|
49
|
+
"codebase and user instructions",
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
_KNOWN_HOST_SCAFFOLD_TAG_PREFIXES = (
|
|
53
|
+
"<environment_context>",
|
|
54
|
+
"<permissions instructions>",
|
|
55
|
+
"<apps_instructions>",
|
|
56
|
+
"<skills_instructions>",
|
|
57
|
+
"<plugins_instructions>",
|
|
58
|
+
"<system_instruction>",
|
|
59
|
+
"<system>",
|
|
60
|
+
"<system-prompt>",
|
|
61
|
+
"<instructions>",
|
|
62
|
+
"<context>",
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _is_poor_title_source(content: str) -> bool:
|
|
67
|
+
stripped = content.strip()
|
|
68
|
+
if not stripped:
|
|
69
|
+
return True
|
|
70
|
+
lower = stripped.lower()
|
|
71
|
+
if any(lower.startswith(prefix) for prefix in _KNOWN_TITLE_SCAFFOLD_PREFIXES):
|
|
72
|
+
return True
|
|
73
|
+
if any(lower.startswith(prefix) for prefix in _KNOWN_HOST_SCAFFOLD_TAG_PREFIXES):
|
|
74
|
+
return True
|
|
75
|
+
return len(stripped) > 500
|
|
76
|
+
|
|
31
77
|
|
|
32
78
|
def _project_path_variants(project_path: str) -> list[str]:
|
|
33
79
|
variants: list[str] = []
|
|
@@ -312,9 +358,7 @@ def _discover_codex_sessions(
|
|
|
312
358
|
if session_cwd != project_path:
|
|
313
359
|
continue
|
|
314
360
|
|
|
315
|
-
|
|
316
|
-
parts = filename.rsplit("-", 1)
|
|
317
|
-
file_session_id = parts[1] if len(parts) > 1 else filename
|
|
361
|
+
file_session_id = str(payload.get("id") or session_file.stem)
|
|
318
362
|
if target_session_id and file_session_id != target_session_id:
|
|
319
363
|
continue
|
|
320
364
|
|
|
@@ -706,7 +750,11 @@ def parse_codex_session_streaming(
|
|
|
706
750
|
continue
|
|
707
751
|
seen_message_hashes.add(message_hash)
|
|
708
752
|
|
|
709
|
-
if
|
|
753
|
+
if (
|
|
754
|
+
role == "user"
|
|
755
|
+
and not first_user_content
|
|
756
|
+
and not _is_poor_title_source(content_text)
|
|
757
|
+
):
|
|
710
758
|
first_user_content = content_text
|
|
711
759
|
|
|
712
760
|
messages.append(
|
|
@@ -788,7 +836,11 @@ def parse_gemini_session(file_path: Path) -> dict[str, Any]:
|
|
|
788
836
|
if not content_text.strip():
|
|
789
837
|
continue
|
|
790
838
|
|
|
791
|
-
if
|
|
839
|
+
if (
|
|
840
|
+
role == "user"
|
|
841
|
+
and not first_user_content
|
|
842
|
+
and not _is_poor_title_source(content_text)
|
|
843
|
+
):
|
|
792
844
|
first_user_content = content_text
|
|
793
845
|
|
|
794
846
|
messages.append(
|
|
@@ -923,7 +975,11 @@ def _parse_opencode_sqlite_session(db_path: Path, session_id: str) -> dict[str,
|
|
|
923
975
|
if not content:
|
|
924
976
|
continue
|
|
925
977
|
|
|
926
|
-
if
|
|
978
|
+
if (
|
|
979
|
+
role == "user"
|
|
980
|
+
and not first_user_content
|
|
981
|
+
and not _is_poor_title_source(content)
|
|
982
|
+
):
|
|
927
983
|
first_user_content = content
|
|
928
984
|
|
|
929
985
|
messages.append(
|
|
@@ -1011,7 +1067,11 @@ def _parse_opencode_json_session(session_path: Path) -> dict[str, Any]:
|
|
|
1011
1067
|
if not content:
|
|
1012
1068
|
continue
|
|
1013
1069
|
|
|
1014
|
-
if
|
|
1070
|
+
if (
|
|
1071
|
+
role == "user"
|
|
1072
|
+
and not first_user_content
|
|
1073
|
+
and not _is_poor_title_source(content)
|
|
1074
|
+
):
|
|
1015
1075
|
first_user_content = content
|
|
1016
1076
|
|
|
1017
1077
|
messages.append(
|
|
@@ -1318,7 +1378,11 @@ def parse_claude_code_session_streaming(
|
|
|
1318
1378
|
i = max(i + 1, j)
|
|
1319
1379
|
continue
|
|
1320
1380
|
|
|
1321
|
-
if
|
|
1381
|
+
if (
|
|
1382
|
+
role == "user"
|
|
1383
|
+
and not first_user_content
|
|
1384
|
+
and not _is_poor_title_source(content)
|
|
1385
|
+
):
|
|
1322
1386
|
first_user_content = content
|
|
1323
1387
|
|
|
1324
1388
|
messages.append(
|
|
@@ -1413,40 +1477,13 @@ def _decode_project_path_enhanced(encoded: str) -> Optional[str]:
|
|
|
1413
1477
|
return simple
|
|
1414
1478
|
|
|
1415
1479
|
segments = encoded[1:].split("-")
|
|
1416
|
-
result =
|
|
1480
|
+
result = find_valid_claude_path("/", segments)
|
|
1417
1481
|
if result:
|
|
1418
1482
|
return result
|
|
1419
1483
|
|
|
1420
1484
|
return simple
|
|
1421
1485
|
|
|
1422
1486
|
|
|
1423
|
-
def _find_valid_path(base: str, remaining: list[str]) -> Optional[str]:
|
|
1424
|
-
if not remaining:
|
|
1425
|
-
return base if os.path.exists(base) else None
|
|
1426
|
-
|
|
1427
|
-
if remaining[0] == "" and len(remaining) >= 2 and remaining[1]:
|
|
1428
|
-
for prefix in (".", "-"):
|
|
1429
|
-
result = _find_valid_path(base, [prefix + remaining[1], *remaining[2:]])
|
|
1430
|
-
if result:
|
|
1431
|
-
return result
|
|
1432
|
-
|
|
1433
|
-
next_segment = remaining[0]
|
|
1434
|
-
with_slash = os.path.join(base, next_segment)
|
|
1435
|
-
if os.path.exists(with_slash):
|
|
1436
|
-
result = _find_valid_path(with_slash, remaining[1:])
|
|
1437
|
-
if result:
|
|
1438
|
-
return result
|
|
1439
|
-
|
|
1440
|
-
if len(remaining) >= 2:
|
|
1441
|
-
for delimiter in ("-", "."):
|
|
1442
|
-
combined = remaining[0] + delimiter + remaining[1]
|
|
1443
|
-
result = _find_valid_path(base, [combined] + remaining[2:])
|
|
1444
|
-
if result:
|
|
1445
|
-
return result
|
|
1446
|
-
|
|
1447
|
-
return None
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
1487
|
def _decode_project_path_windows(encoded: str) -> Optional[str]:
|
|
1451
1488
|
drive_match = re.match(r"^([A-Za-z])-(.*)$", encoded)
|
|
1452
1489
|
if not drive_match:
|
|
@@ -1461,43 +1498,13 @@ def _decode_project_path_windows(encoded: str) -> Optional[str]:
|
|
|
1461
1498
|
|
|
1462
1499
|
segments = rest.split("-")
|
|
1463
1500
|
base = f"{drive_letter}:\\"
|
|
1464
|
-
result =
|
|
1501
|
+
result = find_valid_claude_path(base, segments)
|
|
1465
1502
|
if result:
|
|
1466
1503
|
return result
|
|
1467
1504
|
|
|
1468
1505
|
return simple
|
|
1469
1506
|
|
|
1470
1507
|
|
|
1471
|
-
def _find_valid_path_windows(base: str, remaining: list[str]) -> Optional[str]:
|
|
1472
|
-
if not remaining:
|
|
1473
|
-
return base if os.path.exists(base) else None
|
|
1474
|
-
|
|
1475
|
-
if remaining[0] == "" and len(remaining) >= 2 and remaining[1]:
|
|
1476
|
-
for prefix in (".", "-"):
|
|
1477
|
-
result = _find_valid_path_windows(
|
|
1478
|
-
base,
|
|
1479
|
-
[prefix + remaining[1], *remaining[2:]],
|
|
1480
|
-
)
|
|
1481
|
-
if result:
|
|
1482
|
-
return result
|
|
1483
|
-
|
|
1484
|
-
next_segment = remaining[0]
|
|
1485
|
-
with_slash = os.path.join(base, next_segment)
|
|
1486
|
-
if os.path.exists(with_slash):
|
|
1487
|
-
result = _find_valid_path_windows(with_slash, remaining[1:])
|
|
1488
|
-
if result:
|
|
1489
|
-
return result
|
|
1490
|
-
|
|
1491
|
-
if len(remaining) >= 2:
|
|
1492
|
-
for delimiter in ("-", "."):
|
|
1493
|
-
combined = remaining[0] + delimiter + remaining[1]
|
|
1494
|
-
result = _find_valid_path_windows(base, [combined] + remaining[2:])
|
|
1495
|
-
if result:
|
|
1496
|
-
return result
|
|
1497
|
-
|
|
1498
|
-
return None
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
1508
|
def _encode_claude_project_path_variant(
|
|
1502
1509
|
project_path: str, *, dot_mode: str
|
|
1503
1510
|
) -> str:
|
|
@@ -1527,7 +1534,7 @@ def _encode_claude_path_segment(
|
|
|
1527
1534
|
segment: str, *, dot_mode: str
|
|
1528
1535
|
) -> str:
|
|
1529
1536
|
if dot_mode == "all_as_hyphen":
|
|
1530
|
-
return segment.replace(".", "-")
|
|
1537
|
+
return segment.replace(".", "-").replace("_", "-")
|
|
1531
1538
|
if dot_mode == "hidden_only" and segment.startswith(".") and len(segment) > 1:
|
|
1532
1539
|
return "-" + segment[1:]
|
|
1533
1540
|
return segment
|
|
@@ -54,7 +54,7 @@ PROVIDER_DEFAULTS: dict[str, dict[str, str]] = {
|
|
|
54
54
|
"anthropic": {"model": "claude-sonnet-4-20250514", "api_base": "https://api.anthropic.com"},
|
|
55
55
|
"gemini": {"model": "gemini-flash-latest", "api_base": "https://generativelanguage.googleapis.com"},
|
|
56
56
|
"xai": {"model": "grok-3", "api_base": "https://api.x.ai/v1"},
|
|
57
|
-
"deepseek": {"model": "deepseek-
|
|
57
|
+
"deepseek": {"model": "deepseek-v4-flash", "api_base": "https://api.deepseek.com/v1"},
|
|
58
58
|
"openrouter": {"model": "anthropic/claude-sonnet-4", "api_base": "https://openrouter.ai/api/v1"},
|
|
59
59
|
"github_copilot": {"model": "gpt-5-mini"},
|
|
60
60
|
"openai_codex": {"model": "gpt-5.2-codex"},
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|