claude-code-session-sync 0.1.0__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 (64) hide show
  1. claude_code_session_sync-0.1.0/CHANGELOG.md +28 -0
  2. claude_code_session_sync-0.1.0/LICENSE +21 -0
  3. claude_code_session_sync-0.1.0/MANIFEST.in +3 -0
  4. claude_code_session_sync-0.1.0/PKG-INFO +87 -0
  5. claude_code_session_sync-0.1.0/README.md +62 -0
  6. claude_code_session_sync-0.1.0/claude_code_session_sync.egg-info/PKG-INFO +87 -0
  7. claude_code_session_sync-0.1.0/claude_code_session_sync.egg-info/SOURCES.txt +62 -0
  8. claude_code_session_sync-0.1.0/claude_code_session_sync.egg-info/dependency_links.txt +1 -0
  9. claude_code_session_sync-0.1.0/claude_code_session_sync.egg-info/entry_points.txt +2 -0
  10. claude_code_session_sync-0.1.0/claude_code_session_sync.egg-info/top_level.txt +1 -0
  11. claude_code_session_sync-0.1.0/claude_session_sync/__init__.py +11 -0
  12. claude_code_session_sync-0.1.0/claude_session_sync/acks.py +279 -0
  13. claude_code_session_sync-0.1.0/claude_session_sync/anomaly.py +161 -0
  14. claude_code_session_sync-0.1.0/claude_session_sync/apply.py +874 -0
  15. claude_code_session_sync-0.1.0/claude_session_sync/atomicio.py +621 -0
  16. claude_code_session_sync-0.1.0/claude_session_sync/bootstrap.py +370 -0
  17. claude_code_session_sync-0.1.0/claude_session_sync/canonical.py +185 -0
  18. claude_code_session_sync-0.1.0/claude_session_sync/classify.py +133 -0
  19. claude_code_session_sync-0.1.0/claude_session_sync/cli.py +1065 -0
  20. claude_code_session_sync-0.1.0/claude_session_sync/config.py +128 -0
  21. claude_code_session_sync-0.1.0/claude_session_sync/doctor.py +351 -0
  22. claude_code_session_sync-0.1.0/claude_session_sync/fuzzy.py +136 -0
  23. claude_code_session_sync-0.1.0/claude_session_sync/lineset.py +143 -0
  24. claude_code_session_sync-0.1.0/claude_session_sync/memory.py +953 -0
  25. claude_code_session_sync-0.1.0/claude_session_sync/merge.py +836 -0
  26. claude_code_session_sync-0.1.0/claude_session_sync/pathsafe.py +91 -0
  27. claude_code_session_sync-0.1.0/claude_session_sync/py.typed +0 -0
  28. claude_code_session_sync-0.1.0/claude_session_sync/resolve.py +226 -0
  29. claude_code_session_sync-0.1.0/claude_session_sync/scan.py +485 -0
  30. claude_code_session_sync-0.1.0/claude_session_sync/session_merge.py +214 -0
  31. claude_code_session_sync-0.1.0/claude_session_sync/sidecar.py +238 -0
  32. claude_code_session_sync-0.1.0/claude_session_sync/snapshot.py +136 -0
  33. claude_code_session_sync-0.1.0/claude_session_sync/state.py +240 -0
  34. claude_code_session_sync-0.1.0/claude_session_sync/tombstone.py +330 -0
  35. claude_code_session_sync-0.1.0/claude_session_sync/transfer.py +462 -0
  36. claude_code_session_sync-0.1.0/pyproject.toml +41 -0
  37. claude_code_session_sync-0.1.0/setup.cfg +4 -0
  38. claude_code_session_sync-0.1.0/tests/test_acks.py +579 -0
  39. claude_code_session_sync-0.1.0/tests/test_anomaly.py +147 -0
  40. claude_code_session_sync-0.1.0/tests/test_apply.py +623 -0
  41. claude_code_session_sync-0.1.0/tests/test_apply_memory.py +766 -0
  42. claude_code_session_sync-0.1.0/tests/test_atomicio.py +439 -0
  43. claude_code_session_sync-0.1.0/tests/test_bootstrap.py +474 -0
  44. claude_code_session_sync-0.1.0/tests/test_canonical.py +107 -0
  45. claude_code_session_sync-0.1.0/tests/test_classify.py +122 -0
  46. claude_code_session_sync-0.1.0/tests/test_cli.py +292 -0
  47. claude_code_session_sync-0.1.0/tests/test_config.py +101 -0
  48. claude_code_session_sync-0.1.0/tests/test_doctor.py +287 -0
  49. claude_code_session_sync-0.1.0/tests/test_fuzzy.py +626 -0
  50. claude_code_session_sync-0.1.0/tests/test_lineset.py +65 -0
  51. claude_code_session_sync-0.1.0/tests/test_memory.py +508 -0
  52. claude_code_session_sync-0.1.0/tests/test_memory_classify.py +641 -0
  53. claude_code_session_sync-0.1.0/tests/test_memory_index.py +382 -0
  54. claude_code_session_sync-0.1.0/tests/test_memory_merge_from.py +275 -0
  55. claude_code_session_sync-0.1.0/tests/test_merge.py +710 -0
  56. claude_code_session_sync-0.1.0/tests/test_nudge.py +357 -0
  57. claude_code_session_sync-0.1.0/tests/test_resolve.py +241 -0
  58. claude_code_session_sync-0.1.0/tests/test_scan.py +457 -0
  59. claude_code_session_sync-0.1.0/tests/test_session_merge.py +244 -0
  60. claude_code_session_sync-0.1.0/tests/test_sidecar.py +176 -0
  61. claude_code_session_sync-0.1.0/tests/test_snapshot.py +127 -0
  62. claude_code_session_sync-0.1.0/tests/test_state.py +170 -0
  63. claude_code_session_sync-0.1.0/tests/test_tombstone.py +199 -0
  64. claude_code_session_sync-0.1.0/tests/test_transfer.py +380 -0
@@ -0,0 +1,28 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here.
4
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
5
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## [0.1.0] — 2026-07-05
8
+
9
+ First public release.
10
+
11
+ ### Added
12
+ - Offline cross-machine sync for Claude Code sessions (JSONL) and memory (.md)
13
+ over an external / network drive — no forced cloud or git.
14
+ - Same-group two-way sync via a shared hub; cross-group explicit, selectable
15
+ `pull` / `push` of specific sessions.
16
+ - Safe writes throughout: read-verify-write + file lock + tombstone + keep-both.
17
+ Never silently loses data (mechanical work to Python, semantic calls to the human).
18
+ - Commands: `bootstrap`, `status`, `sync` (`--apply` / `--interactive`),
19
+ `pull` / `push`, `remote`, `doctor` (`--rebuild-state` / `--break-lock` /
20
+ `--ack-all` / `--unack-all` / `--show-acked`), `memory-merge`
21
+ (incl. cross-group `--from` and advisory `--fuzzy` / `--stage` / `--interactive`),
22
+ and a read-only SessionEnd `nudge` hook.
23
+ - Memory union + tombstone + `MEMORY.md` index rebuild; AI-assisted memory merge
24
+ that always preserves both versions and never auto-merges.
25
+ - Cross-platform: Linux + Windows CI on Python 3.11 / 3.13; zero third-party
26
+ dependencies (standard library only).
27
+
28
+ [0.1.0]: https://github.com/weilung/claude-session-sync/releases/tag/v0.1.0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 weilung
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,3 @@
1
+ # sdist 額外收錄(LICENSE 由 pyproject license-files、README 由 readme 已自動收;
2
+ # CHANGELOG 無對應 pyproject 欄位,須在此明列才會進原始碼發佈包)。
3
+ include CHANGELOG.md
@@ -0,0 +1,87 @@
1
+ Metadata-Version: 2.4
2
+ Name: claude-code-session-sync
3
+ Version: 0.1.0
4
+ Summary: Offline cross-machine sync for Claude Code sessions (JSONL) and memory (.md).
5
+ Author: weilung
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/weilung/claude-session-sync
8
+ Project-URL: Repository, https://github.com/weilung/claude-session-sync
9
+ Project-URL: Issues, https://github.com/weilung/claude-session-sync/issues
10
+ Project-URL: Changelog, https://github.com/weilung/claude-session-sync/blob/main/CHANGELOG.md
11
+ Keywords: claude,claude-code,sync,sessions,memory,jsonl,offline
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Utilities
21
+ Requires-Python: >=3.11
22
+ Description-Content-Type: text/markdown
23
+ License-File: LICENSE
24
+ Dynamic: license-file
25
+
26
+ # claude-session-sync
27
+
28
+ 讓 Claude Code 的**對話 session(JSONL)與 memory(.md)**透過**外接 / 網路硬碟**在多台機器間離線同步的 CLI 工具。
29
+
30
+ - 同群組多台共用一個 hub 目錄**雙向同步**;跨群組**明確、可挑選**地引入/送回特定 session。
31
+ - 核心原則:**機械的事交給 Python,語意的事交給 AI;永不靜默丟資料。**
32
+ - 與現成工具的差異:離線(不強制上雲/git)+可挑選+AI 輔助 memory 合併+多寫入者安全。
33
+
34
+ ## 現況
35
+
36
+ **核心功能已實作並收斂**(跨模型 code review + 逐塊 fresh-gate;跨平台,Windows 綠)。
37
+
38
+ - 已完成:唯讀掃描/分類 → `sync` 雙向同步(安全寫入:read-verify-write + lock + tombstone + keep-both)、
39
+ 跨群 `pull`/`push`、`bootstrap` 基線、`doctor` 診斷/rebuild-state/break-lock/ack、
40
+ memory union + tombstone + `MEMORY.md` 索引重建、`memory-merge`(含跨群 `--from` 與模糊近似 `--fuzzy`)、SessionEnd `nudge` hook。
41
+ - memory「同事實不同檔名」的**模糊近似比對**(P2 最後一項、最高風險)已完成——刻意只做**唯讀建議**(`memory-merge --fuzzy` 列候選、由你逐對放行才保留兩版,**絕不自動合併**)。
42
+ - 首個公開版 **0.1.0(beta)**;尚未到 1.0 穩定版,介面/行為仍可能調整。
43
+
44
+ ## 安裝
45
+
46
+ ```bash
47
+ # 從 PyPI(推薦)— 套件名 claude-code-session-sync,安裝後指令仍是 claude-session-sync
48
+ pipx install claude-code-session-sync # 或: pip install claude-code-session-sync
49
+
50
+ # 或從原始碼安裝最新版
51
+ pipx install "git+https://github.com/weilung/claude-session-sync.git"
52
+ ```
53
+
54
+ 需 Python ≥ 3.11、零第三方相依(標準庫)。安裝後即有 `claude-session-sync` 指令。
55
+
56
+ ## 快速開始
57
+
58
+ ```bash
59
+ # 1) 設定自己群組的 hub(編輯設定檔;Windows 路徑用單引號)
60
+ # Windows: %APPDATA%\claude-session-sync\config.toml
61
+ # POSIX: ~/.config/claude-session-sync/config.toml
62
+ # own_hub = 'D:\SyncDrive\HomeJSONL'
63
+
64
+ # 2) 第一次先建基線
65
+ claude-session-sync bootstrap --map "本機專案夾=hub專案夾" --yes
66
+
67
+ # 3) 日常同步(先預覽,再 --apply 落地)
68
+ claude-session-sync status # 看差異(純唯讀)
69
+ claude-session-sync sync # 預覽
70
+ claude-session-sync sync --apply # 落地
71
+ ```
72
+
73
+ (開發環境未安裝時,等價寫法為 `python -m claude_session_sync.cli <子指令>`。)
74
+
75
+ ## 文件
76
+
77
+ - **[`docs/`](docs/README.md) — 使用者指南(白話版)**:不需要懂程式,講清楚「做什麼、怎麼安全、怎麼用」。
78
+ - [概念與運作](docs/01-概念與運作.md) | [安全機制白話](docs/02-安全機制白話.md) | [指令手冊](docs/03-指令手冊.md)
79
+ - [情境劇本](docs/04-情境劇本.md) | [Windows 與已知限制](docs/05-windows-與已知限制.md) | [名詞對照](docs/06-名詞對照.md)
80
+
81
+ ## 開發
82
+
83
+ ```bash
84
+ python -m unittest discover -t . -s tests
85
+ ```
86
+
87
+ 純 Python、零第三方相依(標準庫)。跨 OS CI 於 Ubuntu + Windows 跑 py3.11/3.13。
@@ -0,0 +1,62 @@
1
+ # claude-session-sync
2
+
3
+ 讓 Claude Code 的**對話 session(JSONL)與 memory(.md)**透過**外接 / 網路硬碟**在多台機器間離線同步的 CLI 工具。
4
+
5
+ - 同群組多台共用一個 hub 目錄**雙向同步**;跨群組**明確、可挑選**地引入/送回特定 session。
6
+ - 核心原則:**機械的事交給 Python,語意的事交給 AI;永不靜默丟資料。**
7
+ - 與現成工具的差異:離線(不強制上雲/git)+可挑選+AI 輔助 memory 合併+多寫入者安全。
8
+
9
+ ## 現況
10
+
11
+ **核心功能已實作並收斂**(跨模型 code review + 逐塊 fresh-gate;跨平台,Windows 綠)。
12
+
13
+ - 已完成:唯讀掃描/分類 → `sync` 雙向同步(安全寫入:read-verify-write + lock + tombstone + keep-both)、
14
+ 跨群 `pull`/`push`、`bootstrap` 基線、`doctor` 診斷/rebuild-state/break-lock/ack、
15
+ memory union + tombstone + `MEMORY.md` 索引重建、`memory-merge`(含跨群 `--from` 與模糊近似 `--fuzzy`)、SessionEnd `nudge` hook。
16
+ - memory「同事實不同檔名」的**模糊近似比對**(P2 最後一項、最高風險)已完成——刻意只做**唯讀建議**(`memory-merge --fuzzy` 列候選、由你逐對放行才保留兩版,**絕不自動合併**)。
17
+ - 首個公開版 **0.1.0(beta)**;尚未到 1.0 穩定版,介面/行為仍可能調整。
18
+
19
+ ## 安裝
20
+
21
+ ```bash
22
+ # 從 PyPI(推薦)— 套件名 claude-code-session-sync,安裝後指令仍是 claude-session-sync
23
+ pipx install claude-code-session-sync # 或: pip install claude-code-session-sync
24
+
25
+ # 或從原始碼安裝最新版
26
+ pipx install "git+https://github.com/weilung/claude-session-sync.git"
27
+ ```
28
+
29
+ 需 Python ≥ 3.11、零第三方相依(標準庫)。安裝後即有 `claude-session-sync` 指令。
30
+
31
+ ## 快速開始
32
+
33
+ ```bash
34
+ # 1) 設定自己群組的 hub(編輯設定檔;Windows 路徑用單引號)
35
+ # Windows: %APPDATA%\claude-session-sync\config.toml
36
+ # POSIX: ~/.config/claude-session-sync/config.toml
37
+ # own_hub = 'D:\SyncDrive\HomeJSONL'
38
+
39
+ # 2) 第一次先建基線
40
+ claude-session-sync bootstrap --map "本機專案夾=hub專案夾" --yes
41
+
42
+ # 3) 日常同步(先預覽,再 --apply 落地)
43
+ claude-session-sync status # 看差異(純唯讀)
44
+ claude-session-sync sync # 預覽
45
+ claude-session-sync sync --apply # 落地
46
+ ```
47
+
48
+ (開發環境未安裝時,等價寫法為 `python -m claude_session_sync.cli <子指令>`。)
49
+
50
+ ## 文件
51
+
52
+ - **[`docs/`](docs/README.md) — 使用者指南(白話版)**:不需要懂程式,講清楚「做什麼、怎麼安全、怎麼用」。
53
+ - [概念與運作](docs/01-概念與運作.md) | [安全機制白話](docs/02-安全機制白話.md) | [指令手冊](docs/03-指令手冊.md)
54
+ - [情境劇本](docs/04-情境劇本.md) | [Windows 與已知限制](docs/05-windows-與已知限制.md) | [名詞對照](docs/06-名詞對照.md)
55
+
56
+ ## 開發
57
+
58
+ ```bash
59
+ python -m unittest discover -t . -s tests
60
+ ```
61
+
62
+ 純 Python、零第三方相依(標準庫)。跨 OS CI 於 Ubuntu + Windows 跑 py3.11/3.13。
@@ -0,0 +1,87 @@
1
+ Metadata-Version: 2.4
2
+ Name: claude-code-session-sync
3
+ Version: 0.1.0
4
+ Summary: Offline cross-machine sync for Claude Code sessions (JSONL) and memory (.md).
5
+ Author: weilung
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/weilung/claude-session-sync
8
+ Project-URL: Repository, https://github.com/weilung/claude-session-sync
9
+ Project-URL: Issues, https://github.com/weilung/claude-session-sync/issues
10
+ Project-URL: Changelog, https://github.com/weilung/claude-session-sync/blob/main/CHANGELOG.md
11
+ Keywords: claude,claude-code,sync,sessions,memory,jsonl,offline
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Utilities
21
+ Requires-Python: >=3.11
22
+ Description-Content-Type: text/markdown
23
+ License-File: LICENSE
24
+ Dynamic: license-file
25
+
26
+ # claude-session-sync
27
+
28
+ 讓 Claude Code 的**對話 session(JSONL)與 memory(.md)**透過**外接 / 網路硬碟**在多台機器間離線同步的 CLI 工具。
29
+
30
+ - 同群組多台共用一個 hub 目錄**雙向同步**;跨群組**明確、可挑選**地引入/送回特定 session。
31
+ - 核心原則:**機械的事交給 Python,語意的事交給 AI;永不靜默丟資料。**
32
+ - 與現成工具的差異:離線(不強制上雲/git)+可挑選+AI 輔助 memory 合併+多寫入者安全。
33
+
34
+ ## 現況
35
+
36
+ **核心功能已實作並收斂**(跨模型 code review + 逐塊 fresh-gate;跨平台,Windows 綠)。
37
+
38
+ - 已完成:唯讀掃描/分類 → `sync` 雙向同步(安全寫入:read-verify-write + lock + tombstone + keep-both)、
39
+ 跨群 `pull`/`push`、`bootstrap` 基線、`doctor` 診斷/rebuild-state/break-lock/ack、
40
+ memory union + tombstone + `MEMORY.md` 索引重建、`memory-merge`(含跨群 `--from` 與模糊近似 `--fuzzy`)、SessionEnd `nudge` hook。
41
+ - memory「同事實不同檔名」的**模糊近似比對**(P2 最後一項、最高風險)已完成——刻意只做**唯讀建議**(`memory-merge --fuzzy` 列候選、由你逐對放行才保留兩版,**絕不自動合併**)。
42
+ - 首個公開版 **0.1.0(beta)**;尚未到 1.0 穩定版,介面/行為仍可能調整。
43
+
44
+ ## 安裝
45
+
46
+ ```bash
47
+ # 從 PyPI(推薦)— 套件名 claude-code-session-sync,安裝後指令仍是 claude-session-sync
48
+ pipx install claude-code-session-sync # 或: pip install claude-code-session-sync
49
+
50
+ # 或從原始碼安裝最新版
51
+ pipx install "git+https://github.com/weilung/claude-session-sync.git"
52
+ ```
53
+
54
+ 需 Python ≥ 3.11、零第三方相依(標準庫)。安裝後即有 `claude-session-sync` 指令。
55
+
56
+ ## 快速開始
57
+
58
+ ```bash
59
+ # 1) 設定自己群組的 hub(編輯設定檔;Windows 路徑用單引號)
60
+ # Windows: %APPDATA%\claude-session-sync\config.toml
61
+ # POSIX: ~/.config/claude-session-sync/config.toml
62
+ # own_hub = 'D:\SyncDrive\HomeJSONL'
63
+
64
+ # 2) 第一次先建基線
65
+ claude-session-sync bootstrap --map "本機專案夾=hub專案夾" --yes
66
+
67
+ # 3) 日常同步(先預覽,再 --apply 落地)
68
+ claude-session-sync status # 看差異(純唯讀)
69
+ claude-session-sync sync # 預覽
70
+ claude-session-sync sync --apply # 落地
71
+ ```
72
+
73
+ (開發環境未安裝時,等價寫法為 `python -m claude_session_sync.cli <子指令>`。)
74
+
75
+ ## 文件
76
+
77
+ - **[`docs/`](docs/README.md) — 使用者指南(白話版)**:不需要懂程式,講清楚「做什麼、怎麼安全、怎麼用」。
78
+ - [概念與運作](docs/01-概念與運作.md) | [安全機制白話](docs/02-安全機制白話.md) | [指令手冊](docs/03-指令手冊.md)
79
+ - [情境劇本](docs/04-情境劇本.md) | [Windows 與已知限制](docs/05-windows-與已知限制.md) | [名詞對照](docs/06-名詞對照.md)
80
+
81
+ ## 開發
82
+
83
+ ```bash
84
+ python -m unittest discover -t . -s tests
85
+ ```
86
+
87
+ 純 Python、零第三方相依(標準庫)。跨 OS CI 於 Ubuntu + Windows 跑 py3.11/3.13。
@@ -0,0 +1,62 @@
1
+ CHANGELOG.md
2
+ LICENSE
3
+ MANIFEST.in
4
+ README.md
5
+ pyproject.toml
6
+ claude_code_session_sync.egg-info/PKG-INFO
7
+ claude_code_session_sync.egg-info/SOURCES.txt
8
+ claude_code_session_sync.egg-info/dependency_links.txt
9
+ claude_code_session_sync.egg-info/entry_points.txt
10
+ claude_code_session_sync.egg-info/top_level.txt
11
+ claude_session_sync/__init__.py
12
+ claude_session_sync/acks.py
13
+ claude_session_sync/anomaly.py
14
+ claude_session_sync/apply.py
15
+ claude_session_sync/atomicio.py
16
+ claude_session_sync/bootstrap.py
17
+ claude_session_sync/canonical.py
18
+ claude_session_sync/classify.py
19
+ claude_session_sync/cli.py
20
+ claude_session_sync/config.py
21
+ claude_session_sync/doctor.py
22
+ claude_session_sync/fuzzy.py
23
+ claude_session_sync/lineset.py
24
+ claude_session_sync/memory.py
25
+ claude_session_sync/merge.py
26
+ claude_session_sync/pathsafe.py
27
+ claude_session_sync/py.typed
28
+ claude_session_sync/resolve.py
29
+ claude_session_sync/scan.py
30
+ claude_session_sync/session_merge.py
31
+ claude_session_sync/sidecar.py
32
+ claude_session_sync/snapshot.py
33
+ claude_session_sync/state.py
34
+ claude_session_sync/tombstone.py
35
+ claude_session_sync/transfer.py
36
+ tests/test_acks.py
37
+ tests/test_anomaly.py
38
+ tests/test_apply.py
39
+ tests/test_apply_memory.py
40
+ tests/test_atomicio.py
41
+ tests/test_bootstrap.py
42
+ tests/test_canonical.py
43
+ tests/test_classify.py
44
+ tests/test_cli.py
45
+ tests/test_config.py
46
+ tests/test_doctor.py
47
+ tests/test_fuzzy.py
48
+ tests/test_lineset.py
49
+ tests/test_memory.py
50
+ tests/test_memory_classify.py
51
+ tests/test_memory_index.py
52
+ tests/test_memory_merge_from.py
53
+ tests/test_merge.py
54
+ tests/test_nudge.py
55
+ tests/test_resolve.py
56
+ tests/test_scan.py
57
+ tests/test_session_merge.py
58
+ tests/test_sidecar.py
59
+ tests/test_snapshot.py
60
+ tests/test_state.py
61
+ tests/test_tombstone.py
62
+ tests/test_transfer.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ claude-session-sync = claude_session_sync.cli:main
@@ -0,0 +1,11 @@
1
+ """claude-session-sync — 跨機同步 Claude Code 的 session(JSONL) 與 memory(.md)。
2
+
3
+ P1a 唯讀核心(已實作):
4
+ - canonical:編碼吸收 + canonical hash + 行解析(三態:ok / zero-byte / blank / decode-error)
5
+ - lineset:行身分、root-set、genuine leaf、active-tip
6
+ - classify:§4.1 分類表 + 安全閘 + main-root 相容性 + active-tip 交叉驗
7
+
8
+ 依據 DESIGN.md v0.4 附錄 B(P0 spike 定案)與 PLAN-P1.md v0.3。
9
+ """
10
+
11
+ __version__ = "0.1.0"
@@ -0,0 +1,279 @@
1
+ """ack 帳本:對「工具永遠無法自動解決」的 blocked 項(damaged / casefold-collision / identity-collision)
2
+ 記錄「已審閱」,讓 doctor/sync 不再反覆回報(DESIGN 附錄 A15「blocked 收斂出口」)。
3
+
4
+ **安全鐵則**:
5
+ 1. **純呈現層**:ack 只抑制「回報」,**絕不改變分類**——acked 的 damaged 仍永不同步、acked 的 collision 仍永不
6
+ 自動合併。本帳本從不進 `build_plan`/`classify`/`apply`,故結構上不可能把 blocked 變成 auto-apply。呈現層
7
+ (`format_plan` / `doctor.diagnose`)以 `AckView` 隱藏/降級 acked 項,分類與寫入路徑一律看不到本模組。
8
+ 2. **fingerprint 綁定**:ack 記 `(kind, identity, fingerprint)`。現況 fingerprint 不符(damaged 檔內容改、
9
+ 撞名集合變)→ 視為**未** ack、照常重報——不遮蓋新的/變動過的問題。
10
+ 3. **fail-closed 讀**:帳本缺 → 無 ack(全部照常回報,`ok=True`);壞/讀不到 → 忽略整本(`ok=False`,呼叫端
11
+ 警告,仍全部照常回報);壞條目 → 跳過該條、不毒化整本。`.tombstones/` 為 symlink/逃逸 → **不信任、不
12
+ suppress**(回空、`ok=True`)。任一路徑都**只會少 suppress、不會多 suppress**。
13
+ 4. **A3 不丟**:ack 只寫一個 hub 側 per-project JSON(`<proj>/.tombstones/acks.json`),**絕不動 session/
14
+ memory 檔**。落點在 `.tombstones/`(已被 scan 排除、不會被當 session/專案);寫走 atomicio 原子寫 + 專屬鎖。
15
+
16
+ 範圍(v1,session 側):casefold-collision(A9 檔名撞名,兩份都是真 session)、damaged / blocked-damaged-source
17
+ (壞 JSONL)、identity-collision(同 uuid 異 hash 的內容身分衝突)。memory 側對應項留 follow-on。
18
+ """
19
+ from __future__ import annotations
20
+
21
+ import hashlib
22
+ import json
23
+ from dataclasses import dataclass, field
24
+ from pathlib import Path
25
+
26
+ from . import atomicio, pathsafe, scan, tombstone
27
+
28
+ SCHEMA_VERSION = 1
29
+ ACKS_FILE = "acks.json"
30
+
31
+ # ackable 的 plan action → ack kind。casefold-collision 以 casefold key 為身分(一項含整組 sid);
32
+ # damaged / identity-collision 以 sid 為身分(指紋看兩側檔 bytes)。
33
+ _ACTION_KIND: dict[str, str] = {
34
+ "blocked-casefold-collision": "casefold-collision",
35
+ "blocked-damaged-source": "damaged",
36
+ "damaged": "damaged",
37
+ "identity-collision": "identity-collision",
38
+ }
39
+ _KINDS = frozenset(_ACTION_KIND.values())
40
+ # 可 ack 的 plan action 字串(供 apply.format_report 呈現層過濾)。**單一真相源在 `scan.ACKABLE_ACTIONS`**(scan 被
41
+ # acks 依賴、不可反向 import);`_ACTION_KIND` 的鍵須與之一致(test_acks 有漂移守衛)。
42
+ ACKABLE_ACTIONS = scan.ACKABLE_ACTIONS
43
+
44
+
45
+ class UnsafeAcksDir(OSError):
46
+ """`<proj>/.tombstones` 是 symlink 或逃逸專案夾(指界外)→ 拒寫 acks(否則寫到信任根外)。"""
47
+
48
+
49
+ @dataclass(frozen=True)
50
+ class AckItem:
51
+ """一個可 ack 的 blocked 項(doctor / format_plan / ack 寫入的**單一真相源**,由 `ackable_from_plan` 產)。"""
52
+ project: str # hub 專案夾名(pk)
53
+ hub_dir: str # hub 專案夾路徑;ledger 落點 = <hub_dir>/.tombstones/acks.json
54
+ kind: str # casefold-collision | damaged | identity-collision
55
+ identity: str # collision: casefold key;damaged/identity: sid
56
+ fingerprint: str | None # 現況指紋(見 fingerprint_*);None=不可綁定內容(讀不到)→ 不可 ack、不被隱藏(g6)
57
+ session_ids: tuple[str, ...] # 此項涵蓋的 sid(collision=整組;其餘=單一)——供呈現層隱藏對應行
58
+ label: str # 顯示用短標籤
59
+
60
+
61
+ @dataclass
62
+ class Ledger:
63
+ """載入後的 acks.json。`by_key` 以 **(kind, identity, fingerprint) 三元組**為鍵(同一 (kind,identity) 可有多個
64
+ fingerprint 並存,g2);`ok=False` 表帳本損壞/讀不到(已忽略,呼叫端警告)。"""
65
+ by_key: dict[tuple[str, str, str], dict] = field(default_factory=dict)
66
+ ok: bool = True
67
+ path: Path | None = None
68
+
69
+
70
+ # ── fingerprint ─────────────────────────────────────────────────────────────
71
+
72
+ def fingerprint_collision(names, local_files=None, hub_files=None) -> str | None:
73
+ """撞名項指紋 = 撞名集(排序拼法)+**各撞名檔兩側 raw bytes digest**。新拼法加入/移除 → 集變 → 指紋變;
74
+ 某撞名檔內容改(變 damaged/換成別的 session)→ digest 變 → 指紋變 → 重報。**為何也綁內容**(g4):
75
+ `classify_session` 的撞名閘**先於** damaged 閘 → 撞名檔之一變壞仍分類為 collision,若 fp 只看名稱,collision ack
76
+ 會遮蓋「撞名檔變 damaged」這個新情況;綁內容使「撞名檔變了」重新提示。**回 `None`** 若某撞名檔 present 但讀不到
77
+ (不可綁定內容 → 不列為 ackable,fail-closed,g5 Medium)。`*_files` 由 `scan._session_files` 提供(已排除 symlink);
78
+ 未給(如非比對用的直接呼叫)→ 視為兩側皆無檔。"""
79
+ lf, hf = local_files or {}, hub_files or {}
80
+ parts = ["\n".join(sorted(names))]
81
+ for n in sorted(names):
82
+ fpf = fingerprint_files(lf.get(n), hf.get(n))
83
+ if fpf is None:
84
+ return None # 某撞名檔讀不到 → 不可綁定 → 不可 ack(fail-closed)
85
+ parts.append(fpf)
86
+ return "cf:" + hashlib.sha256("|".join(parts).encode("utf-8")).hexdigest()
87
+
88
+
89
+ def fingerprint_files(*paths: Path | None) -> str | None:
90
+ """damaged / identity-collision 指紋 = 兩側檔 raw bytes digest 的組合。任一側內容改/消失 → 指紋變 → 重報。
91
+ **回 `None`** 若某 present 檔讀不到(unreadable,`raw_file_digest`→None)→ 呼叫端(ackable_from_plan)視為
92
+ **不可綁定內容 → 不列為 ackable、永遠照報**(fail-closed,g5 Medium:不可綁定則 fp 無法反映內容變動,read-denied
93
+ 檔內容變會被舊 ack 遮蓋)。缺檔(None path)→ "-"(可綁定為「該側無此檔」)。path 僅由 `scan._session_files`
94
+ 提供(已排除 symlink),故 `raw_file_digest`(會跟隨 symlink)不會讀到界外檔。"""
95
+ parts: list[str] = []
96
+ for p in paths:
97
+ if p is None:
98
+ parts.append("-") # 該側無此檔(可綁定)
99
+ else:
100
+ d = tombstone.raw_file_digest(p)
101
+ if d is None:
102
+ return None # present 但讀不到 → 不可綁定內容(fail-closed)
103
+ parts.append(d)
104
+ return "fs:" + hashlib.sha256("|".join(parts).encode("utf-8")).hexdigest()
105
+
106
+
107
+ # ── 從 plan 抽 ackable 項(單一真相源)─────────────────────────────────────────
108
+
109
+ def ackable_from_plan(plan) -> list[AckItem]:
110
+ """從 `SyncPlan` 抽出所有可 ack 的 session 側 blocked 項。**只含有 hub_dir 的專案**(ack 帳本 hub 側;
111
+ local-only 專案尚未上 hub、還不是同步問題,不納入)。撞名依 casefold key 併組(整組一項)。"""
112
+ out: list[AckItem] = []
113
+ for pp in plan.projects:
114
+ if not pp.hub_dir:
115
+ continue
116
+ pk = Path(pp.hub_dir).name
117
+ coll: dict[str, set[str]] = {}
118
+ filebased: list[tuple[str, str]] = [] # (sid, kind) for damaged / identity-collision
119
+ for s in pp.sessions:
120
+ kind = _ACTION_KIND.get(s.action)
121
+ if kind is None:
122
+ continue
123
+ if kind == "casefold-collision":
124
+ coll.setdefault(s.session_id.casefold(), set()).add(s.session_id)
125
+ else:
126
+ filebased.append((s.session_id, kind))
127
+ if not coll and not filebased:
128
+ continue
129
+ # collision 與 damaged/identity 皆需讀撞名/壞檔內容算指紋(g4:collision fp 亦綁內容)→ 有 ackable 項就讀。
130
+ # 用 `_session_files`(已排除 symlink)取真實檔路徑,確保指紋只看工具本就會處理的實體檔。
131
+ local_files = scan._session_files(Path(pp.local_dir)) if pp.local_dir else {}
132
+ hub_files = scan._session_files(Path(pp.hub_dir))
133
+ # 不可綁定(讀不到內容)→ fp=None:**仍列出**(不 skip)。fingerprint=None → is_acked 恆 False → compute_ack_view
134
+ # 不會隱藏該 (pk,sid)(fail-closed,g6:若整個 skip 掉,同 hub 另一 view 對同 sid 的 ack 會讓此 sid 看似「所有涵蓋
135
+ # 項都已 ack」而誤藏這個不可綁定行)。`_doctor_ack` 另會濾掉 fp=None 者(不可 ack、無從綁定內容變動)。
136
+ for cf, sids in sorted(coll.items()):
137
+ names = sorted(sids)
138
+ out.append(AckItem(pk, pp.hub_dir, "casefold-collision", cf,
139
+ fingerprint_collision(names, local_files, hub_files), tuple(names),
140
+ "/".join(n[:8] for n in names)))
141
+ for sid, kind in sorted(filebased):
142
+ out.append(AckItem(pk, pp.hub_dir, kind, sid,
143
+ fingerprint_files(local_files.get(sid), hub_files.get(sid)), (sid,), sid[:8]))
144
+ return out
145
+
146
+
147
+ # ── 帳本讀(lock-free、fail-closed)────────────────────────────────────────────
148
+
149
+ def load_ledger(hub_project_dir) -> Ledger:
150
+ """讀 `<hub_project_dir>/.tombstones/acks.json`。fail-closed:缺 → 空 `ok=True`;壞/讀不到 → 空 `ok=False`;
151
+ `.tombstones` 不安全 → 空 `ok=True`(不信任、不 suppress)。**任一情況都只會少 suppress,不會多 suppress**。"""
152
+ tdir = tombstone.tombstones_dir(hub_project_dir)
153
+ path = tdir / ACKS_FILE
154
+ if not tombstone._tombstones_ok(hub_project_dir):
155
+ return Ledger({}, True, path) # symlink/逃逸 .tombstones → 不信任其內容、不 suppress
156
+ if pathsafe.is_reparse(path):
157
+ # `.tombstones` 是真夾但 `acks.json` **本身**是 symlink/reparse → `read_bytes()` 會跟隨讀界外/被植入的帳本
158
+ # → 不信任、不 suppress(fail-closed,g1 High;leaf 防線,補 `_tombstones_ok` 只管父夾)。
159
+ return Ledger({}, True, path)
160
+ try:
161
+ raw = path.read_bytes()
162
+ except FileNotFoundError:
163
+ return Ledger({}, True, path) # 尚無帳本 → 無 ack(正常)
164
+ except OSError:
165
+ return Ledger({}, False, path) # 讀不到(權限等)→ fail-closed 忽略
166
+ try:
167
+ obj = json.loads(raw.decode("utf-8"))
168
+ except (ValueError, UnicodeDecodeError):
169
+ return Ledger({}, False, path) # 壞 JSON → 忽略整本
170
+ version = obj.get("version") if isinstance(obj, dict) else None
171
+ # `type(version) is int`:**拒 bool/float**——Python 中 `True == 1`、`1.0 == 1` 皆真,若只寫 `version == 1`
172
+ # 則 `{"version": true}` / `{"version": 1.0}` 會被當合法版本放行 → 壞帳本可 suppress 真問題(R1 High#1,
173
+ # 對稱 tombstone `_valid_tombstone` 的型別 fail-closed)。
174
+ if not (type(version) is int and version == SCHEMA_VERSION and isinstance(obj.get("acks"), list)):
175
+ return Ledger({}, False, path) # 版本不符/型別錯/結構壞 → 忽略整本
176
+ by_key: dict[tuple[str, str, str], dict] = {}
177
+ for rec in obj["acks"]:
178
+ if not isinstance(rec, dict):
179
+ continue # 壞條目跳過(不毒化整本)
180
+ k, i, fp = rec.get("kind"), rec.get("identity"), rec.get("fingerprint")
181
+ if isinstance(k, str) and isinstance(i, str) and isinstance(fp, str) and k in _KINDS:
182
+ by_key[(k, i, fp)] = rec # 以 **(kind,identity,fingerprint) 三元組**為鍵:同一 (kind,identity)
183
+ # 可有多個 fp 並存(同 hub 專案被多個 local 夾映射時 damaged 的內容 fp 不同,g2)——不互蓋。
184
+ return Ledger(by_key, True, path)
185
+
186
+
187
+ def is_acked(ledger: Ledger, kind: str, identity: str, fingerprint: str | None) -> bool:
188
+ """該 (kind, identity, fingerprint) 三元組是否在帳本內(**fp-exact**:指紋不符=內容/撞名集已變 → 未 ack)。
189
+ `fingerprint=None`(不可綁定內容)→ **恆 False**(fail-closed:讀不到內容者不可能『已 ack』、也不該被隱藏,g6)。"""
190
+ return fingerprint is not None and (kind, identity, fingerprint) in ledger.by_key
191
+
192
+
193
+ # ── ack view(呈現層過濾:format_plan / doctor 共用)────────────────────────────
194
+
195
+ @dataclass
196
+ class AckView:
197
+ """呈現層過濾視圖。`hidden[pk]` = 該專案要隱藏的 session_id 集(見 compute_ack_view 的 fail-safe 規則);
198
+ `corrupt_projects` = 帳本損壞的專案(呼叫端警告)。隱藏行數由各 renderer 自行計。"""
199
+ hidden: dict[str, set[str]] = field(default_factory=dict)
200
+ corrupt_projects: list[str] = field(default_factory=list)
201
+
202
+
203
+ def compute_ack_view(plan) -> AckView:
204
+ """對 plan 的每個 ackable 項查對應專案帳本(每專案讀一次),算出要隱藏的 session_id。純讀、lock-free。
205
+
206
+ **fail-safe 隱藏**:某 `(pk, sid)` 只在「**涵蓋它的所有 ackable 項都 fp-exact 已 ack**」時才隱藏——否則不藏。
207
+ 因同一 hub 專案可被多個 local 夾映射(兩 cwd 綁定/兩 clone),同一 sid 的 damaged 內容 fp 可不同 → 兩個
208
+ AckItem 共用 `(pk, sid)`;若只 ack 其一就用 `(pk,sid)` 藏,會誤藏另一個未 ack 的(遮蓋真問題,g2 High)。
209
+ 寧可多顯示已 ack 項,也不誤藏未 ack 的同 sid 項。(collision 的 fp 只看名稱集、跨 view 相同,本規則對它無副作用。)"""
210
+ view = AckView()
211
+ by_dir: dict[str, list[AckItem]] = {}
212
+ for it in ackable_from_plan(plan):
213
+ by_dir.setdefault(it.hub_dir, []).append(it)
214
+ for hub_dir, its in by_dir.items():
215
+ led = load_ledger(hub_dir)
216
+ pk = its[0].project
217
+ if not led.ok:
218
+ view.corrupt_projects.append(pk)
219
+ sid_all_acked: dict[str, bool] = {} # sid → 涵蓋它的**所有**項是否都已 ack(AND)
220
+ for it in its:
221
+ acked = is_acked(led, it.kind, it.identity, it.fingerprint)
222
+ for sid in it.session_ids:
223
+ sid_all_acked[sid] = sid_all_acked.get(sid, True) and acked
224
+ hide = {sid for sid, ok in sid_all_acked.items() if ok}
225
+ if hide:
226
+ view.hidden.setdefault(pk, set()).update(hide)
227
+ return view
228
+
229
+
230
+ # ── 帳本寫(加鎖 read-modify-write、atomic)─────────────────────────────────────
231
+
232
+ @dataclass
233
+ class UpdateResult:
234
+ added: list[str] = field(default_factory=list) # 新 ack / 更新指紋的 label
235
+ removed: list[str] = field(default_factory=list) # 取消 ack 的 label
236
+ unchanged: list[str] = field(default_factory=list) # 已 ack 且指紋相符(無變更)
237
+ replaced_corrupt: bool = False # 原帳本損壞、已以新內容取代(呼叫端告知)
238
+
239
+
240
+ def update_ledger(hub_project_dir, *, add=(), remove=(), lock_timeout_s: float = 5.0) -> UpdateResult:
241
+ """加鎖 read-modify-write 一個專案帳本。`add`:`list[AckItem]`(以 (kind,identity,fingerprint) 三元組為鍵——
242
+ 同 (kind,identity) 不同 fp **並存不互蓋**,g2);`remove`:`list[(kind, identity, fingerprint)]`(load_ledger 暴露
243
+ 的三元組鍵)。鎖內重讀(並發 ack 合併、不互蓋);原子寫。`.tombstones` 不安全 → raise。"""
244
+ tdir = tombstone.tombstones_dir(hub_project_dir)
245
+ if not tombstone._tombstones_ok(hub_project_dir):
246
+ raise UnsafeAcksDir(f".tombstones 為 symlink 或逃逸專案夾,拒絕寫入 acks:{tdir}")
247
+ path = tdir / ACKS_FILE
248
+ res = UpdateResult()
249
+ lock = atomicio.FileLock(path).acquire_blocking(timeout_s=lock_timeout_s)
250
+ try:
251
+ led = load_ledger(hub_project_dir) # 鎖內重讀
252
+ res.replaced_corrupt = not led.ok # 損壞帳本會被本次寫入取代(原本就已被忽略、不 suppress 任何項)
253
+ by_key = dict(led.by_key)
254
+ for it in add:
255
+ key = (it.kind, it.identity, it.fingerprint)
256
+ if key in by_key:
257
+ res.unchanged.append(it.label)
258
+ continue
259
+ by_key[key] = {
260
+ "kind": it.kind, "identity": it.identity, "fingerprint": it.fingerprint,
261
+ "label": it.label, "acked_at": tombstone.now_iso(),
262
+ "acked_by": tombstone.local_machine_id(),
263
+ }
264
+ res.added.append(it.label)
265
+ for key in remove:
266
+ rec = by_key.pop(key, None)
267
+ if rec is not None:
268
+ res.removed.append(rec.get("label") or key[1])
269
+ _write_ledger(path, by_key)
270
+ return res
271
+ finally:
272
+ lock.release()
273
+
274
+
275
+ def _write_ledger(path: Path, by_key: dict[tuple[str, str, str], dict]) -> None:
276
+ acks = sorted(by_key.values(),
277
+ key=lambda r: (r.get("kind", ""), r.get("identity", ""), r.get("fingerprint", "")))
278
+ obj = {"version": SCHEMA_VERSION, "acks": acks}
279
+ atomicio.atomic_write_text(path, json.dumps(obj, ensure_ascii=False, indent=2))