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.
Files changed (24) hide show
  1. {nmem_cli-0.8.1 → nmem_cli-0.8.4}/PKG-INFO +1 -1
  2. {nmem_cli-0.8.1 → nmem_cli-0.8.4}/pyproject.toml +1 -1
  3. {nmem_cli-0.8.1 → nmem_cli-0.8.4}/src/nmem_cli/__init__.py +1 -1
  4. nmem_cli-0.8.4/src/nmem_cli/claude_paths.py +63 -0
  5. {nmem_cli-0.8.1 → nmem_cli-0.8.4}/src/nmem_cli/cli.py +1 -1
  6. {nmem_cli-0.8.1 → nmem_cli-0.8.4}/src/nmem_cli/session_import.py +75 -68
  7. {nmem_cli-0.8.1 → nmem_cli-0.8.4}/src/nmem_cli/tui/screens/settings.py +1 -1
  8. {nmem_cli-0.8.1 → nmem_cli-0.8.4}/.gitignore +0 -0
  9. {nmem_cli-0.8.1 → nmem_cli-0.8.4}/README.md +0 -0
  10. {nmem_cli-0.8.1 → nmem_cli-0.8.4}/src/nmem_cli/license_payload.py +0 -0
  11. {nmem_cli-0.8.1 → nmem_cli-0.8.4}/src/nmem_cli/py.typed +0 -0
  12. {nmem_cli-0.8.1 → nmem_cli-0.8.4}/src/nmem_cli/tui/__init__.py +0 -0
  13. {nmem_cli-0.8.1 → nmem_cli-0.8.4}/src/nmem_cli/tui/__main__.py +0 -0
  14. {nmem_cli-0.8.1 → nmem_cli-0.8.4}/src/nmem_cli/tui/api_client.py +0 -0
  15. {nmem_cli-0.8.1 → nmem_cli-0.8.4}/src/nmem_cli/tui/app.py +0 -0
  16. {nmem_cli-0.8.1 → nmem_cli-0.8.4}/src/nmem_cli/tui/screens/__init__.py +0 -0
  17. {nmem_cli-0.8.1 → nmem_cli-0.8.4}/src/nmem_cli/tui/screens/dashboard.py +0 -0
  18. {nmem_cli-0.8.1 → nmem_cli-0.8.4}/src/nmem_cli/tui/screens/graph.py +0 -0
  19. {nmem_cli-0.8.1 → nmem_cli-0.8.4}/src/nmem_cli/tui/screens/help.py +0 -0
  20. {nmem_cli-0.8.1 → nmem_cli-0.8.4}/src/nmem_cli/tui/screens/memories.py +0 -0
  21. {nmem_cli-0.8.1 → nmem_cli-0.8.4}/src/nmem_cli/tui/screens/memory_detail.py +0 -0
  22. {nmem_cli-0.8.1 → nmem_cli-0.8.4}/src/nmem_cli/tui/screens/thread_detail.py +0 -0
  23. {nmem_cli-0.8.1 → nmem_cli-0.8.4}/src/nmem_cli/tui/screens/threads.py +0 -0
  24. {nmem_cli-0.8.1 → nmem_cli-0.8.4}/src/nmem_cli/tui/widgets/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nmem-cli
3
- Version: 0.8.1
3
+ Version: 0.8.4
4
4
  Summary: CLI and TUI for Nowledge Mem - AI memory management
5
5
  Project-URL: Homepage, https://mem.nowledge.co/
6
6
  Project-URL: Repository, https://github.com/nowledge-co/
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "nmem-cli"
3
- version = "0.8.1"
3
+ version = "0.8.4"
4
4
  description = "CLI and TUI for Nowledge Mem - AI memory management"
5
5
  authors = [
6
6
  {name = "Nowledge Labs"}
@@ -20,7 +20,7 @@ Environment (overrides config file):
20
20
  NMEM_API_KEY Optional API key (Bearer auth)
21
21
  """
22
22
 
23
- __version__ = "0.8.1"
23
+ __version__ = "0.8.4"
24
24
  __author__ = "Nowledge Labs"
25
25
 
26
26
  from .cli import main
@@ -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-chat", "api_base": "https://api.deepseek.com/v1"},
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
- filename = session_file.stem
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 role == "user" and not first_user_content:
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 role == "user" and not first_user_content:
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 role == "user" and not first_user_content:
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 role == "user" and not first_user_content:
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 role == "user" and not first_user_content:
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 = _find_valid_path("/", segments)
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 = _find_valid_path_windows(base, segments)
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-chat", "api_base": "https://api.deepseek.com/v1"},
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