nmem-cli 0.8.0__tar.gz → 0.8.3__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.0 → nmem_cli-0.8.3}/PKG-INFO +1 -1
- {nmem_cli-0.8.0 → nmem_cli-0.8.3}/pyproject.toml +1 -1
- {nmem_cli-0.8.0 → nmem_cli-0.8.3}/src/nmem_cli/__init__.py +1 -1
- nmem_cli-0.8.3/src/nmem_cli/claude_paths.py +63 -0
- {nmem_cli-0.8.0 → nmem_cli-0.8.3}/src/nmem_cli/cli.py +105 -11
- {nmem_cli-0.8.0 → nmem_cli-0.8.3}/src/nmem_cli/session_import.py +75 -68
- {nmem_cli-0.8.0 → nmem_cli-0.8.3}/src/nmem_cli/tui/screens/settings.py +1 -1
- {nmem_cli-0.8.0 → nmem_cli-0.8.3}/.gitignore +0 -0
- {nmem_cli-0.8.0 → nmem_cli-0.8.3}/README.md +0 -0
- {nmem_cli-0.8.0 → nmem_cli-0.8.3}/src/nmem_cli/license_payload.py +0 -0
- {nmem_cli-0.8.0 → nmem_cli-0.8.3}/src/nmem_cli/py.typed +0 -0
- {nmem_cli-0.8.0 → nmem_cli-0.8.3}/src/nmem_cli/tui/__init__.py +0 -0
- {nmem_cli-0.8.0 → nmem_cli-0.8.3}/src/nmem_cli/tui/__main__.py +0 -0
- {nmem_cli-0.8.0 → nmem_cli-0.8.3}/src/nmem_cli/tui/api_client.py +0 -0
- {nmem_cli-0.8.0 → nmem_cli-0.8.3}/src/nmem_cli/tui/app.py +0 -0
- {nmem_cli-0.8.0 → nmem_cli-0.8.3}/src/nmem_cli/tui/screens/__init__.py +0 -0
- {nmem_cli-0.8.0 → nmem_cli-0.8.3}/src/nmem_cli/tui/screens/dashboard.py +0 -0
- {nmem_cli-0.8.0 → nmem_cli-0.8.3}/src/nmem_cli/tui/screens/graph.py +0 -0
- {nmem_cli-0.8.0 → nmem_cli-0.8.3}/src/nmem_cli/tui/screens/help.py +0 -0
- {nmem_cli-0.8.0 → nmem_cli-0.8.3}/src/nmem_cli/tui/screens/memories.py +0 -0
- {nmem_cli-0.8.0 → nmem_cli-0.8.3}/src/nmem_cli/tui/screens/memory_detail.py +0 -0
- {nmem_cli-0.8.0 → nmem_cli-0.8.3}/src/nmem_cli/tui/screens/thread_detail.py +0 -0
- {nmem_cli-0.8.0 → nmem_cli-0.8.3}/src/nmem_cli/tui/screens/threads.py +0 -0
- {nmem_cli-0.8.0 → nmem_cli-0.8.3}/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"},
|
|
@@ -1378,6 +1378,88 @@ def _is_wsl_environment() -> bool:
|
|
|
1378
1378
|
return False
|
|
1379
1379
|
|
|
1380
1380
|
|
|
1381
|
+
class _CliInstallContext(NamedTuple):
|
|
1382
|
+
source: str
|
|
1383
|
+
command_path: str | None
|
|
1384
|
+
python_executable: str
|
|
1385
|
+
package_path: str
|
|
1386
|
+
|
|
1387
|
+
|
|
1388
|
+
def _detect_cli_install_context() -> _CliInstallContext:
|
|
1389
|
+
command_path = shutil.which("nmem")
|
|
1390
|
+
python_executable = str(Path(sys.executable).resolve())
|
|
1391
|
+
package_path = str(Path(__file__).resolve())
|
|
1392
|
+
haystack = " ".join(
|
|
1393
|
+
item.lower()
|
|
1394
|
+
for item in [command_path or "", python_executable, package_path]
|
|
1395
|
+
)
|
|
1396
|
+
|
|
1397
|
+
if "python-standalone" in haystack or "nowledge mem.app" in haystack:
|
|
1398
|
+
source = "desktop-bundled"
|
|
1399
|
+
elif "/usr/lib/nowledge-mem" in haystack or "/usr/lib/nowledge mem" in haystack:
|
|
1400
|
+
source = "desktop-bundled"
|
|
1401
|
+
elif "\\nowledge mem\\" in haystack or "/.local/share/nowledge-mem/" in haystack:
|
|
1402
|
+
source = "desktop-bundled"
|
|
1403
|
+
elif "pipx/venvs/nmem-cli" in haystack or ".local/pipx/venvs/nmem-cli" in haystack:
|
|
1404
|
+
source = "pipx"
|
|
1405
|
+
elif "/uv/tools/nmem-cli" in haystack or "\\uv\\tools\\nmem-cli" in haystack:
|
|
1406
|
+
source = "uv"
|
|
1407
|
+
elif "site-packages/nmem_cli" in haystack:
|
|
1408
|
+
source = "python-package"
|
|
1409
|
+
else:
|
|
1410
|
+
source = "unknown"
|
|
1411
|
+
|
|
1412
|
+
return _CliInstallContext(
|
|
1413
|
+
source=source,
|
|
1414
|
+
command_path=command_path,
|
|
1415
|
+
python_executable=python_executable,
|
|
1416
|
+
package_path=package_path,
|
|
1417
|
+
)
|
|
1418
|
+
|
|
1419
|
+
|
|
1420
|
+
def _format_cli_upgrade_hint(*, context: _CliInstallContext, server_newer: bool) -> str:
|
|
1421
|
+
command_detail = (
|
|
1422
|
+
f" first on PATH ({context.command_path})" if context.command_path else ""
|
|
1423
|
+
)
|
|
1424
|
+
if server_newer:
|
|
1425
|
+
if context.source == "desktop-bundled":
|
|
1426
|
+
return (
|
|
1427
|
+
f"The CLI{command_detail} is older than the connected server. "
|
|
1428
|
+
"In the desktop app, open Settings > Preferences > Developer Tools > "
|
|
1429
|
+
"Install CLI, then restart the terminal. If this terminal is using a "
|
|
1430
|
+
"PyPI/pipx install instead, run "
|
|
1431
|
+
"`python -m pip install --upgrade nmem-cli` or `pipx upgrade nmem-cli`."
|
|
1432
|
+
)
|
|
1433
|
+
if context.source == "pipx":
|
|
1434
|
+
return (
|
|
1435
|
+
f"The CLI{command_detail} is older than the connected server. "
|
|
1436
|
+
"Run `pipx upgrade nmem-cli`, or use "
|
|
1437
|
+
"`uvx --from nmem-cli nmem status` for a fresh one-shot CLI."
|
|
1438
|
+
)
|
|
1439
|
+
if context.source == "uv":
|
|
1440
|
+
return (
|
|
1441
|
+
f"The CLI{command_detail} is older than the connected server. "
|
|
1442
|
+
"Use `uvx --from nmem-cli nmem status` for the latest published CLI, "
|
|
1443
|
+
"or reinstall the uv tool."
|
|
1444
|
+
)
|
|
1445
|
+
if context.source == "python-package":
|
|
1446
|
+
return (
|
|
1447
|
+
f"The CLI{command_detail} is older than the connected server. "
|
|
1448
|
+
"Run `python -m pip install --upgrade nmem-cli`, or "
|
|
1449
|
+
"`uvx --from nmem-cli nmem status` without installing."
|
|
1450
|
+
)
|
|
1451
|
+
return (
|
|
1452
|
+
f"The CLI{command_detail} is older than the connected server. "
|
|
1453
|
+
"Upgrade the `nmem-cli` package, or reinstall the desktop CLI from "
|
|
1454
|
+
"Settings > Preferences > Developer Tools."
|
|
1455
|
+
)
|
|
1456
|
+
|
|
1457
|
+
return (
|
|
1458
|
+
"The CLI is newer than the connected server. Update or restart "
|
|
1459
|
+
"Nowledge Mem, or point NMEM_API_URL at the server you intended to use."
|
|
1460
|
+
)
|
|
1461
|
+
|
|
1462
|
+
|
|
1381
1463
|
def _status_version_warning(
|
|
1382
1464
|
*,
|
|
1383
1465
|
cli_version: str,
|
|
@@ -1389,21 +1471,26 @@ def _status_version_warning(
|
|
|
1389
1471
|
if not normalized_server or normalized_server == normalized_cli:
|
|
1390
1472
|
return None
|
|
1391
1473
|
|
|
1392
|
-
message = (
|
|
1393
|
-
|
|
1474
|
+
message = "CLI is v{}, but the server at {} reports v{}.".format(
|
|
1475
|
+
normalized_cli,
|
|
1476
|
+
api_url,
|
|
1477
|
+
normalized_server,
|
|
1394
1478
|
)
|
|
1395
1479
|
|
|
1396
1480
|
parsed = urlsplit(api_url)
|
|
1397
1481
|
if _is_wsl_environment() and _is_loopback_hostname(parsed.hostname):
|
|
1398
1482
|
return (
|
|
1399
1483
|
message,
|
|
1400
|
-
"WSL localhost may be reaching the Windows desktop app instead of
|
|
1401
|
-
"If you expected the WSL server, stop the
|
|
1484
|
+
"WSL localhost may be reaching the Windows desktop app instead of "
|
|
1485
|
+
"the Linux service. If you expected the WSL server, stop the "
|
|
1486
|
+
"Windows app or point NMEM_API_URL at the Linux service explicitly.",
|
|
1402
1487
|
)
|
|
1403
1488
|
|
|
1489
|
+
context = _detect_cli_install_context()
|
|
1490
|
+
server_newer = _compare_versions(normalized_cli, normalized_server)
|
|
1404
1491
|
return (
|
|
1405
1492
|
message,
|
|
1406
|
-
|
|
1493
|
+
_format_cli_upgrade_hint(context=context, server_newer=server_newer),
|
|
1407
1494
|
)
|
|
1408
1495
|
|
|
1409
1496
|
|
|
@@ -1487,6 +1574,18 @@ def cmd_status() -> None:
|
|
|
1487
1574
|
except Exception:
|
|
1488
1575
|
search_index = None
|
|
1489
1576
|
|
|
1577
|
+
version_warning = _status_version_warning(
|
|
1578
|
+
cli_version=result["cli_version"],
|
|
1579
|
+
server_version=result["server_version"],
|
|
1580
|
+
api_url=result["api_url"],
|
|
1581
|
+
)
|
|
1582
|
+
if version_warning:
|
|
1583
|
+
warning_message, warning_hint = version_warning
|
|
1584
|
+
result["version_mismatch"] = {
|
|
1585
|
+
"message": warning_message,
|
|
1586
|
+
"hint": warning_hint,
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1490
1589
|
if is_json_mode():
|
|
1491
1590
|
if agent_data:
|
|
1492
1591
|
result["agent"] = {
|
|
@@ -1535,11 +1634,6 @@ def cmd_status() -> None:
|
|
|
1535
1634
|
console.print(f" search {state_label}")
|
|
1536
1635
|
if search_index.get("message"):
|
|
1537
1636
|
console.print(f" [dim]{search_index['message']}[/dim]")
|
|
1538
|
-
version_warning = _status_version_warning(
|
|
1539
|
-
cli_version=result["cli_version"],
|
|
1540
|
-
server_version=result["server_version"],
|
|
1541
|
-
api_url=result["api_url"],
|
|
1542
|
-
)
|
|
1543
1637
|
if version_warning:
|
|
1544
1638
|
warning_message, warning_hint = version_warning
|
|
1545
1639
|
console.print(f" [bold yellow]! Version mismatch[/bold yellow] {warning_message}")
|
|
@@ -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
|