nmem-cli 0.10.7__tar.gz → 0.10.8__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.10.7 → nmem_cli-0.10.8}/Cargo.lock +13 -13
- {nmem_cli-0.10.7 → nmem_cli-0.10.8}/Cargo.toml +1 -1
- {nmem_cli-0.10.7 → nmem_cli-0.10.8}/PKG-INFO +1 -1
- {nmem_cli-0.10.7 → nmem_cli-0.10.8}/crates/nmem-cli/src/commands/session_import.rs +172 -4
- {nmem_cli-0.10.7 → nmem_cli-0.10.8}/crates/nmem-cli/src/commands/threads.rs +44 -3
- {nmem_cli-0.10.7 → nmem_cli-0.10.8}/pyproject.toml +1 -1
- {nmem_cli-0.10.7 → nmem_cli-0.10.8}/README.md +0 -0
- {nmem_cli-0.10.7 → nmem_cli-0.10.8}/crates/nmem-cli/Cargo.toml +0 -0
- {nmem_cli-0.10.7 → nmem_cli-0.10.8}/crates/nmem-cli/README.md +0 -0
- {nmem_cli-0.10.7 → nmem_cli-0.10.8}/crates/nmem-cli/src/cli.rs +0 -0
- {nmem_cli-0.10.7 → nmem_cli-0.10.8}/crates/nmem-cli/src/client.rs +0 -0
- {nmem_cli-0.10.7 → nmem_cli-0.10.8}/crates/nmem-cli/src/commands/agents.rs +0 -0
- {nmem_cli-0.10.7 → nmem_cli-0.10.8}/crates/nmem-cli/src/commands/communities.rs +0 -0
- {nmem_cli-0.10.7 → nmem_cli-0.10.8}/crates/nmem-cli/src/commands/data_transfer.rs +0 -0
- {nmem_cli-0.10.7 → nmem_cli-0.10.8}/crates/nmem-cli/src/commands/feed.rs +0 -0
- {nmem_cli-0.10.7 → nmem_cli-0.10.8}/crates/nmem-cli/src/commands/fs.rs +0 -0
- {nmem_cli-0.10.7 → nmem_cli-0.10.8}/crates/nmem-cli/src/commands/graph.rs +0 -0
- {nmem_cli-0.10.7 → nmem_cli-0.10.8}/crates/nmem-cli/src/commands/library.rs +0 -0
- {nmem_cli-0.10.7 → nmem_cli-0.10.8}/crates/nmem-cli/src/commands/license.rs +0 -0
- {nmem_cli-0.10.7 → nmem_cli-0.10.8}/crates/nmem-cli/src/commands/memories.rs +0 -0
- {nmem_cli-0.10.7 → nmem_cli-0.10.8}/crates/nmem-cli/src/commands/memory_relations.rs +0 -0
- {nmem_cli-0.10.7 → nmem_cli-0.10.8}/crates/nmem-cli/src/commands/mod.rs +0 -0
- {nmem_cli-0.10.7 → nmem_cli-0.10.8}/crates/nmem-cli/src/commands/models.rs +0 -0
- {nmem_cli-0.10.7 → nmem_cli-0.10.8}/crates/nmem-cli/src/commands/plugins.rs +0 -0
- {nmem_cli-0.10.7 → nmem_cli-0.10.8}/crates/nmem-cli/src/commands/provider.rs +0 -0
- {nmem_cli-0.10.7 → nmem_cli-0.10.8}/crates/nmem-cli/src/commands/rules.rs +0 -0
- {nmem_cli-0.10.7 → nmem_cli-0.10.8}/crates/nmem-cli/src/commands/service.rs +0 -0
- {nmem_cli-0.10.7 → nmem_cli-0.10.8}/crates/nmem-cli/src/commands/skills.rs +0 -0
- {nmem_cli-0.10.7 → nmem_cli-0.10.8}/crates/nmem-cli/src/commands/spaces.rs +0 -0
- {nmem_cli-0.10.7 → nmem_cli-0.10.8}/crates/nmem-cli/src/commands/stubs.rs +0 -0
- {nmem_cli-0.10.7 → nmem_cli-0.10.8}/crates/nmem-cli/src/commands/system.rs +0 -0
- {nmem_cli-0.10.7 → nmem_cli-0.10.8}/crates/nmem-cli/src/commands/wiki.rs +0 -0
- {nmem_cli-0.10.7 → nmem_cli-0.10.8}/crates/nmem-cli/src/commands/working_memory.rs +0 -0
- {nmem_cli-0.10.7 → nmem_cli-0.10.8}/crates/nmem-cli/src/config.rs +0 -0
- {nmem_cli-0.10.7 → nmem_cli-0.10.8}/crates/nmem-cli/src/format.rs +0 -0
- {nmem_cli-0.10.7 → nmem_cli-0.10.8}/crates/nmem-cli/src/main.rs +0 -0
- {nmem_cli-0.10.7 → nmem_cli-0.10.8}/crates/nmem-cli/src/style.rs +0 -0
|
@@ -5206,7 +5206,7 @@ dependencies = [
|
|
|
5206
5206
|
|
|
5207
5207
|
[[package]]
|
|
5208
5208
|
name = "nmem-cli"
|
|
5209
|
-
version = "0.10.
|
|
5209
|
+
version = "0.10.8"
|
|
5210
5210
|
dependencies = [
|
|
5211
5211
|
"anstyle",
|
|
5212
5212
|
"anyhow",
|
|
@@ -5223,7 +5223,7 @@ dependencies = [
|
|
|
5223
5223
|
|
|
5224
5224
|
[[package]]
|
|
5225
5225
|
name = "nmem-content"
|
|
5226
|
-
version = "0.10.
|
|
5226
|
+
version = "0.10.8"
|
|
5227
5227
|
dependencies = [
|
|
5228
5228
|
"anyhow",
|
|
5229
5229
|
"md5",
|
|
@@ -5235,7 +5235,7 @@ dependencies = [
|
|
|
5235
5235
|
|
|
5236
5236
|
[[package]]
|
|
5237
5237
|
name = "nmem-core"
|
|
5238
|
-
version = "0.10.
|
|
5238
|
+
version = "0.10.8"
|
|
5239
5239
|
dependencies = [
|
|
5240
5240
|
"serde",
|
|
5241
5241
|
"serde_json",
|
|
@@ -5245,7 +5245,7 @@ dependencies = [
|
|
|
5245
5245
|
|
|
5246
5246
|
[[package]]
|
|
5247
5247
|
name = "nmem-docs"
|
|
5248
|
-
version = "0.10.
|
|
5248
|
+
version = "0.10.8"
|
|
5249
5249
|
dependencies = [
|
|
5250
5250
|
"criterion",
|
|
5251
5251
|
"scraper",
|
|
@@ -5255,7 +5255,7 @@ dependencies = [
|
|
|
5255
5255
|
|
|
5256
5256
|
[[package]]
|
|
5257
5257
|
name = "nmem-embed"
|
|
5258
|
-
version = "0.10.
|
|
5258
|
+
version = "0.10.8"
|
|
5259
5259
|
dependencies = [
|
|
5260
5260
|
"anyhow",
|
|
5261
5261
|
"arrow-array",
|
|
@@ -5275,7 +5275,7 @@ dependencies = [
|
|
|
5275
5275
|
|
|
5276
5276
|
[[package]]
|
|
5277
5277
|
name = "nmem-feed"
|
|
5278
|
-
version = "0.10.
|
|
5278
|
+
version = "0.10.8"
|
|
5279
5279
|
dependencies = [
|
|
5280
5280
|
"anyhow",
|
|
5281
5281
|
"serde_json",
|
|
@@ -5284,7 +5284,7 @@ dependencies = [
|
|
|
5284
5284
|
|
|
5285
5285
|
[[package]]
|
|
5286
5286
|
name = "nmem-graph"
|
|
5287
|
-
version = "0.10.
|
|
5287
|
+
version = "0.10.8"
|
|
5288
5288
|
dependencies = [
|
|
5289
5289
|
"anyhow",
|
|
5290
5290
|
"caseless",
|
|
@@ -5303,7 +5303,7 @@ dependencies = [
|
|
|
5303
5303
|
|
|
5304
5304
|
[[package]]
|
|
5305
5305
|
name = "nmem-harness"
|
|
5306
|
-
version = "0.10.
|
|
5306
|
+
version = "0.10.8"
|
|
5307
5307
|
dependencies = [
|
|
5308
5308
|
"anyhow",
|
|
5309
5309
|
"futures",
|
|
@@ -5323,7 +5323,7 @@ dependencies = [
|
|
|
5323
5323
|
|
|
5324
5324
|
[[package]]
|
|
5325
5325
|
name = "nmem-model"
|
|
5326
|
-
version = "0.10.
|
|
5326
|
+
version = "0.10.8"
|
|
5327
5327
|
dependencies = [
|
|
5328
5328
|
"anyhow",
|
|
5329
5329
|
"encoding_rs",
|
|
@@ -5352,7 +5352,7 @@ dependencies = [
|
|
|
5352
5352
|
|
|
5353
5353
|
[[package]]
|
|
5354
5354
|
name = "nmem-search"
|
|
5355
|
-
version = "0.10.
|
|
5355
|
+
version = "0.10.8"
|
|
5356
5356
|
dependencies = [
|
|
5357
5357
|
"anyhow",
|
|
5358
5358
|
"arrow-array",
|
|
@@ -5374,7 +5374,7 @@ dependencies = [
|
|
|
5374
5374
|
|
|
5375
5375
|
[[package]]
|
|
5376
5376
|
name = "nmem-server"
|
|
5377
|
-
version = "0.10.
|
|
5377
|
+
version = "0.10.8"
|
|
5378
5378
|
dependencies = [
|
|
5379
5379
|
"aes-gcm",
|
|
5380
5380
|
"anyhow",
|
|
@@ -5430,7 +5430,7 @@ dependencies = [
|
|
|
5430
5430
|
|
|
5431
5431
|
[[package]]
|
|
5432
5432
|
name = "nmem-tui"
|
|
5433
|
-
version = "0.10.
|
|
5433
|
+
version = "0.10.8"
|
|
5434
5434
|
dependencies = [
|
|
5435
5435
|
"anyhow",
|
|
5436
5436
|
"arboard",
|
|
@@ -5444,7 +5444,7 @@ dependencies = [
|
|
|
5444
5444
|
|
|
5445
5445
|
[[package]]
|
|
5446
5446
|
name = "nmem-websearch"
|
|
5447
|
-
version = "0.10.
|
|
5447
|
+
version = "0.10.8"
|
|
5448
5448
|
dependencies = [
|
|
5449
5449
|
"anyhow",
|
|
5450
5450
|
"dom_smoothie",
|
|
@@ -884,9 +884,62 @@ fn encode_claude_segment(segment: &str, dot_mode: &str) -> String {
|
|
|
884
884
|
}
|
|
885
885
|
}
|
|
886
886
|
|
|
887
|
+
/// True for a Windows drive-letter absolute path (`C:\...`, `d:/...`).
|
|
888
|
+
fn is_windows_drive_path(path: &str) -> bool {
|
|
889
|
+
let bytes = path.as_bytes();
|
|
890
|
+
bytes.len() >= 2 && bytes[0].is_ascii_alphabetic() && bytes[1] == b':'
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
/// Claude Code's Windows directory-naming rule (observed on Windows): every
|
|
894
|
+
/// special character in the absolute path is replaced with a single `-`, with
|
|
895
|
+
/// NO leading `-` (the path already starts with a drive letter). The drive
|
|
896
|
+
/// colon and the trailing separator each map to their own `-`, hence the
|
|
897
|
+
/// characteristic double dash after the drive letter:
|
|
898
|
+
/// `D:\` -> `D--` (`:` -> `-`, `\` -> `-`)
|
|
899
|
+
/// `D:\server-prod-env-setting` -> `D--server-prod-env-setting`
|
|
900
|
+
/// `D:\a\b` -> `D--a-b`
|
|
901
|
+
/// See `.context/attachments/.../claude-code-nmem-stop-hook-bug.md`.
|
|
902
|
+
fn encode_claude_windows_project_path(project_path: &str) -> String {
|
|
903
|
+
project_path
|
|
904
|
+
.chars()
|
|
905
|
+
.map(|c| match c {
|
|
906
|
+
'\\' | '/' | ':' | '.' | '_' => '-',
|
|
907
|
+
other => other,
|
|
908
|
+
})
|
|
909
|
+
.collect()
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
/// Historical single-dash Windows slug: collapse each RUN of special chars to
|
|
913
|
+
/// one `-` (so the drive `:` + `\` becomes a single dash). Used only as a
|
|
914
|
+
/// migration fallback when the correct double-dash dir is absent:
|
|
915
|
+
/// `D:\server-prod-env-setting` -> `D-server-prod-env-setting`
|
|
916
|
+
fn encode_claude_windows_project_path_single_dash(project_path: &str) -> String {
|
|
917
|
+
let mut out = String::with_capacity(project_path.len());
|
|
918
|
+
let mut prev_dash = false;
|
|
919
|
+
for c in project_path.chars() {
|
|
920
|
+
if matches!(c, '\\' | '/' | ':' | '.' | '_') {
|
|
921
|
+
if !prev_dash {
|
|
922
|
+
out.push('-');
|
|
923
|
+
prev_dash = true;
|
|
924
|
+
}
|
|
925
|
+
} else {
|
|
926
|
+
out.push(c);
|
|
927
|
+
prev_dash = false;
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
out
|
|
931
|
+
}
|
|
932
|
+
|
|
887
933
|
fn encode_claude_project_path(project_path: &str, dot_mode: &str) -> String {
|
|
888
|
-
// Windows drive
|
|
889
|
-
// the
|
|
934
|
+
// Windows drive paths (`C:\...`) follow Claude Code's own slug rule, which
|
|
935
|
+
// differs from POSIX: no leading `-`, and the drive `:` becomes its own `-`
|
|
936
|
+
// (producing the `D--...` double-dash form). The POSIX segment logic below
|
|
937
|
+
// can't reproduce that because it always prepends a leading `-` and never
|
|
938
|
+
// touches the drive colon. `dot_mode` is irrelevant for the drive form:
|
|
939
|
+
// Claude Code maps every special char (including `.`/`_`) to one `-`.
|
|
940
|
+
if is_windows_drive_path(project_path) {
|
|
941
|
+
return encode_claude_windows_project_path(project_path);
|
|
942
|
+
}
|
|
890
943
|
let segments: Vec<String> = project_path
|
|
891
944
|
.replace('\\', "/")
|
|
892
945
|
.split('/')
|
|
@@ -993,11 +1046,19 @@ fn resolve_claude_project_dir(project_path: &str) -> Option<PathBuf> {
|
|
|
993
1046
|
let variants = project_path_variants(project_path);
|
|
994
1047
|
let mut candidate_dirs: Vec<PathBuf> = Vec::new();
|
|
995
1048
|
for variant in &variants {
|
|
996
|
-
|
|
1049
|
+
let mut encodings = vec![
|
|
997
1050
|
encode_claude_project_path(variant, "all_as_hyphen"),
|
|
998
1051
|
encode_claude_project_path(variant, "hidden_only"),
|
|
999
1052
|
encode_claude_project_path(variant, "preserve"),
|
|
1000
|
-
]
|
|
1053
|
+
];
|
|
1054
|
+
// Historical single-dash Windows slug (`D-server-...`): older components
|
|
1055
|
+
// collapsed the drive `:` + separator run into one `-`, so a user
|
|
1056
|
+
// mid-migration may still have transcripts under the single-dash dir.
|
|
1057
|
+
// The double-dash form is primary; this only adds a fallback candidate.
|
|
1058
|
+
if is_windows_drive_path(variant) {
|
|
1059
|
+
encodings.push(encode_claude_windows_project_path_single_dash(variant));
|
|
1060
|
+
}
|
|
1061
|
+
for encoded in encodings {
|
|
1001
1062
|
let candidate = claude_projects_dir.join(&encoded);
|
|
1002
1063
|
if !candidate_dirs.contains(&candidate) {
|
|
1003
1064
|
candidate_dirs.push(candidate);
|
|
@@ -1632,9 +1693,46 @@ fn get_cursor_projects_dir() -> PathBuf {
|
|
|
1632
1693
|
|
|
1633
1694
|
fn encode_cursor_project_path(project_path: &str) -> String {
|
|
1634
1695
|
let normalized = normpath(&expanduser(project_path).to_string_lossy());
|
|
1696
|
+
// Windows drive paths (`C:\...`, `C:/...`) follow Cursor's own dir-naming
|
|
1697
|
+
// rule, which differs from POSIX: the drive `:` + separator collapses to a
|
|
1698
|
+
// double dash and remaining separators each become one `-`, with no leading
|
|
1699
|
+
// `-` (the encoded name starts with the drive letter). Mirrors the Python
|
|
1700
|
+
// `_encode_cursor_project_path` win32 branch (`":\\" -> "--"`, `"\\" -> "-"`):
|
|
1701
|
+
// C:\Users\me\proj -> C--Users-me-proj
|
|
1702
|
+
// C:/a/b -> C--a-b
|
|
1703
|
+
// `normpath` above only collapses `/`, so a Windows path is untouched by it;
|
|
1704
|
+
// detect the drive form here and slug both separators. Require a real
|
|
1705
|
+
// separator after the drive colon (mirrors the Python `":\\" in ...` guard),
|
|
1706
|
+
// so a bare `C:` never produces a stray trailing dash.
|
|
1707
|
+
if is_windows_drive_path(&normalized)
|
|
1708
|
+
&& matches!(normalized.as_bytes().get(2), Some(b'\\') | Some(b'/'))
|
|
1709
|
+
{
|
|
1710
|
+
return encode_cursor_windows_project_path(&normalized);
|
|
1711
|
+
}
|
|
1635
1712
|
normalized.trim_start_matches('/').replace('/', "-")
|
|
1636
1713
|
}
|
|
1637
1714
|
|
|
1715
|
+
/// Cursor's Windows dir-naming rule: `<drive>:<sep><rest>` becomes
|
|
1716
|
+
/// `<drive>--<rest with each `\`/`/` mapped to `-`>`. Pure/cross-platform so it
|
|
1717
|
+
/// unit-tests on any host. Mirrors the Python win32 branch exactly.
|
|
1718
|
+
fn encode_cursor_windows_project_path(normalized: &str) -> String {
|
|
1719
|
+
// `is_windows_drive_path` guarantees bytes[0] is the drive letter and
|
|
1720
|
+
// bytes[1] == ':'. Everything after the colon is the rest (which begins with
|
|
1721
|
+
// a separator for an absolute drive path). Map the leading `:`+sep to `--`
|
|
1722
|
+
// and any further separator to a single `-`.
|
|
1723
|
+
let drive = &normalized[..1];
|
|
1724
|
+
let after_colon = &normalized[2..]; // skip "<drive>:"
|
|
1725
|
+
let rest: String = after_colon
|
|
1726
|
+
.chars()
|
|
1727
|
+
.map(|c| if c == '\\' || c == '/' { '-' } else { c })
|
|
1728
|
+
.collect();
|
|
1729
|
+
// `after_colon` starts with a separator for `C:\...`, which the map turns
|
|
1730
|
+
// into a leading `-`; combined with the `-` we prepend, that yields the
|
|
1731
|
+
// characteristic `--` after the drive letter. For the bare drive `C:` (no
|
|
1732
|
+
// trailing separator) there is no leading separator, so emit a single `-`.
|
|
1733
|
+
format!("{drive}-{rest}")
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1638
1736
|
fn cursor_project_name(project_encoded: Option<&str>) -> Option<String> {
|
|
1639
1737
|
let enc = project_encoded?;
|
|
1640
1738
|
let parts: Vec<&str> = enc.split('-').filter(|p| !p.is_empty()).collect();
|
|
@@ -4848,6 +4946,76 @@ mod tests {
|
|
|
4848
4946
|
);
|
|
4849
4947
|
}
|
|
4850
4948
|
|
|
4949
|
+
#[test]
|
|
4950
|
+
fn claude_encode_windows_drive_double_dash() {
|
|
4951
|
+
// Claude Code's real Windows dir rule: every special char -> one `-`,
|
|
4952
|
+
// no leading `-`. The drive `:` + separator produce the double dash.
|
|
4953
|
+
// dot_mode must be irrelevant for the drive form.
|
|
4954
|
+
for mode in ["all_as_hyphen", "hidden_only", "preserve"] {
|
|
4955
|
+
assert_eq!(encode_claude_project_path("D:\\", mode), "D--", "mode={mode}");
|
|
4956
|
+
assert_eq!(
|
|
4957
|
+
encode_claude_project_path("D:\\server-prod-env-setting", mode),
|
|
4958
|
+
"D--server-prod-env-setting",
|
|
4959
|
+
"mode={mode}"
|
|
4960
|
+
);
|
|
4961
|
+
assert_eq!(encode_claude_project_path("D:\\a\\b", mode), "D--a-b", "mode={mode}");
|
|
4962
|
+
// Forward-slash drive form is equivalent.
|
|
4963
|
+
assert_eq!(encode_claude_project_path("C:/a/b", mode), "C--a-b", "mode={mode}");
|
|
4964
|
+
}
|
|
4965
|
+
// POSIX path must keep the leading `-` and existing dot handling.
|
|
4966
|
+
assert_eq!(
|
|
4967
|
+
encode_claude_project_path("/Users/x/proj", "all_as_hyphen"),
|
|
4968
|
+
"-Users-x-proj"
|
|
4969
|
+
);
|
|
4970
|
+
}
|
|
4971
|
+
|
|
4972
|
+
#[test]
|
|
4973
|
+
fn claude_windows_single_dash_migration_variant() {
|
|
4974
|
+
assert_eq!(
|
|
4975
|
+
encode_claude_windows_project_path_single_dash("D:\\server-prod-env-setting"),
|
|
4976
|
+
"D-server-prod-env-setting"
|
|
4977
|
+
);
|
|
4978
|
+
assert_eq!(
|
|
4979
|
+
encode_claude_windows_project_path_single_dash("D:\\"),
|
|
4980
|
+
"D-"
|
|
4981
|
+
);
|
|
4982
|
+
assert_eq!(
|
|
4983
|
+
encode_claude_windows_project_path_single_dash("D:\\a\\b"),
|
|
4984
|
+
"D-a-b"
|
|
4985
|
+
);
|
|
4986
|
+
}
|
|
4987
|
+
|
|
4988
|
+
#[test]
|
|
4989
|
+
fn cursor_encode_windows_drive_double_dash() {
|
|
4990
|
+
// Cursor's real Windows dir rule (Python `_encode_cursor_project_path`
|
|
4991
|
+
// win32 branch): drive `:`+sep -> `--`, remaining sep -> `-`, no leading
|
|
4992
|
+
// `-`. Pure transform, so it must produce the right slug on any host.
|
|
4993
|
+
assert_eq!(
|
|
4994
|
+
encode_cursor_windows_project_path("C:\\Users\\me\\proj"),
|
|
4995
|
+
"C--Users-me-proj"
|
|
4996
|
+
);
|
|
4997
|
+
assert_eq!(encode_cursor_windows_project_path("D:\\a\\b"), "D--a-b");
|
|
4998
|
+
// Forward-slash drive form is equivalent (normpath leaves `\` alone but
|
|
4999
|
+
// a `/`-style Windows input can still reach here).
|
|
5000
|
+
assert_eq!(encode_cursor_windows_project_path("C:/a/b"), "C--a-b");
|
|
5001
|
+
// The public entrypoint routes drive paths through the Windows branch
|
|
5002
|
+
// and POSIX paths through the original slug (leading `-` stripped).
|
|
5003
|
+
assert_eq!(
|
|
5004
|
+
encode_cursor_project_path("C:\\Users\\me\\proj"),
|
|
5005
|
+
"C--Users-me-proj"
|
|
5006
|
+
);
|
|
5007
|
+
assert_eq!(encode_cursor_project_path("/Users/me/proj"), "Users-me-proj");
|
|
5008
|
+
}
|
|
5009
|
+
|
|
5010
|
+
#[test]
|
|
5011
|
+
fn detects_windows_drive_paths() {
|
|
5012
|
+
assert!(is_windows_drive_path("D:\\"));
|
|
5013
|
+
assert!(is_windows_drive_path("c:/x"));
|
|
5014
|
+
assert!(!is_windows_drive_path("/Users/x"));
|
|
5015
|
+
assert!(!is_windows_drive_path("relative/path"));
|
|
5016
|
+
assert!(!is_windows_drive_path("."));
|
|
5017
|
+
}
|
|
5018
|
+
|
|
4851
5019
|
#[test]
|
|
4852
5020
|
fn sanitize_lowercases_and_replaces() {
|
|
4853
5021
|
assert_eq!(sanitize_session_id("Sess_ABC 123!"), "sess_abc-123-");
|
|
@@ -1048,7 +1048,35 @@ fn abs_lexical(project: &str) -> String {
|
|
|
1048
1048
|
.unwrap_or_default()
|
|
1049
1049
|
.join(expanded)
|
|
1050
1050
|
};
|
|
1051
|
-
|
|
1051
|
+
// `PathBuf::join(".")` does not normalize, so `abs_lexical(".")` on Windows
|
|
1052
|
+
// yields `D:\.` (a trailing `.` component). That trailing dot then slugs
|
|
1053
|
+
// wrong (an extra separator in the Claude Code dir name). Strip a trailing
|
|
1054
|
+
// `.` component lexically, honoring both separators. `current_dir()` is
|
|
1055
|
+
// already canonical, so this only removes the artifact introduced by joining
|
|
1056
|
+
// the literal default ".".
|
|
1057
|
+
let s = abs.to_string_lossy().to_string();
|
|
1058
|
+
strip_trailing_dot_component(&s)
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
/// Remove a trailing `\.` or `/.` (a no-op `.` path component) without touching
|
|
1062
|
+
/// the rest of the string. `D:\.` -> `D:\`, `/a/b/.` -> `/a/b`, `D:\` -> `D:\`.
|
|
1063
|
+
fn strip_trailing_dot_component(path: &str) -> String {
|
|
1064
|
+
if let Some(stripped) = path.strip_suffix('.') {
|
|
1065
|
+
// Only strip if the `.` is its own component (preceded by a separator),
|
|
1066
|
+
// never a real name like `foo.` or the parent of `..`.
|
|
1067
|
+
if stripped.ends_with(['/', '\\']) {
|
|
1068
|
+
let trimmed = stripped.trim_end_matches(['/', '\\']);
|
|
1069
|
+
// Keep the root separator: `/.` -> `/`, `D:\.` -> `D:\`.
|
|
1070
|
+
if trimmed.is_empty() {
|
|
1071
|
+
return "/".to_string();
|
|
1072
|
+
}
|
|
1073
|
+
if trimmed.ends_with(':') {
|
|
1074
|
+
return format!("{trimmed}\\");
|
|
1075
|
+
}
|
|
1076
|
+
return trimmed.to_string();
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
path.to_string()
|
|
1052
1080
|
}
|
|
1053
1081
|
|
|
1054
1082
|
/// `os.path.basename(path.rstrip('/'))`.
|
|
@@ -2034,8 +2062,8 @@ fn urlencode(s: &str) -> String {
|
|
|
2034
2062
|
#[cfg(test)]
|
|
2035
2063
|
mod tests {
|
|
2036
2064
|
use super::{
|
|
2037
|
-
is_simple_message_list,
|
|
2038
|
-
with_stable_external_ids,
|
|
2065
|
+
is_simple_message_list, strip_trailing_dot_component, sync_persistence_source,
|
|
2066
|
+
thread_authorship_metadata, urlencode, with_stable_external_ids,
|
|
2039
2067
|
};
|
|
2040
2068
|
use serde_json::json;
|
|
2041
2069
|
|
|
@@ -2045,6 +2073,19 @@ mod tests {
|
|
|
2045
2073
|
assert_eq!(urlencode("codex-019e95ac"), "codex-019e95ac");
|
|
2046
2074
|
}
|
|
2047
2075
|
|
|
2076
|
+
#[test]
|
|
2077
|
+
fn strip_trailing_dot_component_removes_join_artifact() {
|
|
2078
|
+
// `abs_lexical(".")` -> `cwd + "\."` on Windows; the trailing `.` must go.
|
|
2079
|
+
assert_eq!(strip_trailing_dot_component("D:\\."), "D:\\");
|
|
2080
|
+
assert_eq!(strip_trailing_dot_component("D:\\proj\\."), "D:\\proj");
|
|
2081
|
+
assert_eq!(strip_trailing_dot_component("/a/b/."), "/a/b");
|
|
2082
|
+
assert_eq!(strip_trailing_dot_component("/."), "/");
|
|
2083
|
+
// Never touch real names ending in a dot or `..`.
|
|
2084
|
+
assert_eq!(strip_trailing_dot_component("D:\\foo."), "D:\\foo.");
|
|
2085
|
+
assert_eq!(strip_trailing_dot_component("D:\\proj"), "D:\\proj");
|
|
2086
|
+
assert_eq!(strip_trailing_dot_component("/a/b"), "/a/b");
|
|
2087
|
+
}
|
|
2088
|
+
|
|
2048
2089
|
#[test]
|
|
2049
2090
|
fn authorship_explicit_wins_and_source_app_is_provenance() {
|
|
2050
2091
|
// Use a source_app that is also a valid identity-looking string to prove
|
|
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
|
|
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
|