cync-cli 0.4.0__tar.gz → 0.6.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 (58) hide show
  1. cync_cli-0.6.0/.github/workflows/release.yml +139 -0
  2. {cync_cli-0.4.0 → cync_cli-0.6.0}/PKG-INFO +33 -3
  3. {cync_cli-0.4.0 → cync_cli-0.6.0}/README.md +29 -2
  4. {cync_cli-0.4.0 → cync_cli-0.6.0}/client/src/routes/+layout.svelte +2 -1
  5. cync_cli-0.6.0/client/src/routes/docs/+page.svelte +92 -0
  6. cync_cli-0.6.0/packaging/HOMEBREW.md +76 -0
  7. cync_cli-0.6.0/packaging/cync.py +9 -0
  8. cync_cli-0.6.0/packaging/gen_formula.sh +63 -0
  9. {cync_cli-0.4.0 → cync_cli-0.6.0}/pyproject.toml +5 -1
  10. {cync_cli-0.4.0 → cync_cli-0.6.0}/src/cync/__init__.py +1 -1
  11. {cync_cli-0.4.0 → cync_cli-0.6.0}/src/cync/client.py +13 -0
  12. cync_cli-0.6.0/src/cync/mcp_server.py +132 -0
  13. {cync_cli-0.4.0 → cync_cli-0.6.0}/.dockerignore +0 -0
  14. {cync_cli-0.4.0 → cync_cli-0.6.0}/.env.example +0 -0
  15. {cync_cli-0.4.0 → cync_cli-0.6.0}/.github/workflows/publish.yml +0 -0
  16. {cync_cli-0.4.0 → cync_cli-0.6.0}/.gitignore +0 -0
  17. {cync_cli-0.4.0 → cync_cli-0.6.0}/Dockerfile +0 -0
  18. {cync_cli-0.4.0 → cync_cli-0.6.0}/LICENSE +0 -0
  19. {cync_cli-0.4.0 → cync_cli-0.6.0}/PRIVACY.md +0 -0
  20. {cync_cli-0.4.0 → cync_cli-0.6.0}/client/.env.example +0 -0
  21. {cync_cli-0.4.0 → cync_cli-0.6.0}/client/.gitignore +0 -0
  22. {cync_cli-0.4.0 → cync_cli-0.6.0}/client/.npmrc +0 -0
  23. {cync_cli-0.4.0 → cync_cli-0.6.0}/client/.vscode/extensions.json +0 -0
  24. {cync_cli-0.4.0 → cync_cli-0.6.0}/client/README.md +0 -0
  25. {cync_cli-0.4.0 → cync_cli-0.6.0}/client/package.json +0 -0
  26. {cync_cli-0.4.0 → cync_cli-0.6.0}/client/pnpm-lock.yaml +0 -0
  27. {cync_cli-0.4.0 → cync_cli-0.6.0}/client/pnpm-workspace.yaml +0 -0
  28. {cync_cli-0.4.0 → cync_cli-0.6.0}/client/src/app.d.ts +0 -0
  29. {cync_cli-0.4.0 → cync_cli-0.6.0}/client/src/app.html +0 -0
  30. {cync_cli-0.4.0 → cync_cli-0.6.0}/client/src/hooks.server.ts +0 -0
  31. {cync_cli-0.4.0 → cync_cli-0.6.0}/client/src/lib/assets/favicon.svg +0 -0
  32. {cync_cli-0.4.0 → cync_cli-0.6.0}/client/src/lib/index.ts +0 -0
  33. {cync_cli-0.4.0 → cync_cli-0.6.0}/client/src/lib/parse.ts +0 -0
  34. {cync_cli-0.4.0 → cync_cli-0.6.0}/client/src/lib/server/cync.ts +0 -0
  35. {cync_cli-0.4.0 → cync_cli-0.6.0}/client/src/lib/types.ts +0 -0
  36. {cync_cli-0.4.0 → cync_cli-0.6.0}/client/src/routes/+layout.server.ts +0 -0
  37. {cync_cli-0.4.0 → cync_cli-0.6.0}/client/src/routes/+layout.ts +0 -0
  38. {cync_cli-0.4.0 → cync_cli-0.6.0}/client/src/routes/+page.server.ts +0 -0
  39. {cync_cli-0.4.0 → cync_cli-0.6.0}/client/src/routes/+page.svelte +0 -0
  40. {cync_cli-0.4.0 → cync_cli-0.6.0}/client/src/routes/auth/callback/+server.ts +0 -0
  41. {cync_cli-0.4.0 → cync_cli-0.6.0}/client/src/routes/auth/signout/+server.ts +0 -0
  42. {cync_cli-0.4.0 → cync_cli-0.6.0}/client/src/routes/login/+page.svelte +0 -0
  43. {cync_cli-0.4.0 → cync_cli-0.6.0}/client/src/routes/p/[slug]/+page.server.ts +0 -0
  44. {cync_cli-0.4.0 → cync_cli-0.6.0}/client/src/routes/p/[slug]/+page.svelte +0 -0
  45. {cync_cli-0.4.0 → cync_cli-0.6.0}/client/src/routes/p/[slug]/c/[id]/+page.server.ts +0 -0
  46. {cync_cli-0.4.0 → cync_cli-0.6.0}/client/src/routes/p/[slug]/c/[id]/+page.svelte +0 -0
  47. {cync_cli-0.4.0 → cync_cli-0.6.0}/client/src/routes/privacy/+page.svelte +0 -0
  48. {cync_cli-0.4.0 → cync_cli-0.6.0}/client/static/robots.txt +0 -0
  49. {cync_cli-0.4.0 → cync_cli-0.6.0}/client/tsconfig.json +0 -0
  50. {cync_cli-0.4.0 → cync_cli-0.6.0}/client/vite.config.ts +0 -0
  51. {cync_cli-0.4.0 → cync_cli-0.6.0}/scripts/migrate_legacy.py +0 -0
  52. {cync_cli-0.4.0 → cync_cli-0.6.0}/src/cync/auth.py +0 -0
  53. {cync_cli-0.4.0 → cync_cli-0.6.0}/src/cync/common.py +0 -0
  54. {cync_cli-0.4.0 → cync_cli-0.6.0}/src/cync/config.py +0 -0
  55. {cync_cli-0.4.0 → cync_cli-0.6.0}/src/cync/server.py +0 -0
  56. {cync_cli-0.4.0 → cync_cli-0.6.0}/src/cync/storage.py +0 -0
  57. {cync_cli-0.4.0 → cync_cli-0.6.0}/src/cync/supabase_store.py +0 -0
  58. {cync_cli-0.4.0 → cync_cli-0.6.0}/supabase/schema.sql +0 -0
@@ -0,0 +1,139 @@
1
+ name: release
2
+
3
+ # Build standalone `cync` binaries with PyInstaller and publish them — plus an
4
+ # updated Homebrew formula — to the PUBLIC tap repo. This source repo stays
5
+ # private; only the compiled binaries + formula become public, so
6
+ # `brew install 03hgryan/tap/cync` works for anyone.
7
+ #
8
+ # ── One-time setup ────────────────────────────────────────────────────────────
9
+ # 1. Create a PUBLIC repo `03hgryan/homebrew-tap` (tick "Add a README" so it
10
+ # has an initial commit / default branch).
11
+ # 2. Create a fine-grained PAT scoped to ONLY that repo, permission
12
+ # "Contents: Read and write". Add it to THIS repo as an Actions secret
13
+ # named `HOMEBREW_TAP_TOKEN` (Settings → Secrets and variables → Actions).
14
+ # After that, every `git tag vX.Y.Z && git push --tags` ships binaries + formula.
15
+
16
+ on:
17
+ push:
18
+ tags: ["v*"]
19
+
20
+ permissions:
21
+ contents: read
22
+
23
+ jobs:
24
+ build:
25
+ strategy:
26
+ fail-fast: false
27
+ matrix:
28
+ include:
29
+ - os: macos-14 # Apple Silicon
30
+ target: macos-arm64
31
+ - os: macos-13 # Intel
32
+ target: macos-x86_64
33
+ - os: ubuntu-latest
34
+ target: linux-x86_64
35
+ runs-on: ${{ matrix.os }}
36
+ steps:
37
+ - uses: actions/checkout@v4
38
+ - uses: actions/setup-python@v5
39
+ with:
40
+ python-version: "3.12"
41
+ - name: Install build deps
42
+ run: |
43
+ python -m pip install --upgrade pip
44
+ pip install ".[mcp]" pyinstaller
45
+ - name: Build binary
46
+ run: |
47
+ pyinstaller --onefile --name cync \
48
+ --collect-all mcp \
49
+ --collect-all pydantic \
50
+ --collect-submodules anyio \
51
+ --hidden-import anyio._backends._asyncio \
52
+ --copy-metadata mcp \
53
+ --copy-metadata pydantic \
54
+ packaging/cync.py
55
+ - name: Smoke test
56
+ run: |
57
+ ./dist/cync --version
58
+ ./dist/cync --help >/dev/null
59
+ # MCP is bundled — prove it starts & imports cleanly (feed EOF so it exits).
60
+ ./dist/cync mcp </dev/null 2>mcp_err.txt &
61
+ pid=$!
62
+ sleep 5
63
+ rc=started
64
+ if ! kill -0 "$pid" 2>/dev/null; then
65
+ rc=0; wait "$pid" || rc=$? # exited on its own: EOF→0 is fine, non-zero = crash
66
+ else
67
+ kill "$pid" 2>/dev/null || true; wait "$pid" 2>/dev/null || true
68
+ fi
69
+ if grep -qiE 'traceback|modulenotfound|importerror' mcp_err.txt; then
70
+ echo "::error::MCP failed to import in the bundled binary"; cat mcp_err.txt; exit 1
71
+ fi
72
+ if [ "$rc" != "started" ] && [ "$rc" != "0" ]; then
73
+ echo "::error::cync mcp exited early (rc=$rc) — likely a startup crash"; cat mcp_err.txt; exit 1
74
+ fi
75
+ echo "smoke ok (mcp rc=$rc)"
76
+ - name: Package tarball
77
+ run: tar -C dist -czf "cync-${{ matrix.target }}.tar.gz" cync
78
+ - uses: actions/upload-artifact@v4
79
+ with:
80
+ name: cync-${{ matrix.target }}
81
+ path: cync-${{ matrix.target }}.tar.gz
82
+
83
+ publish-tap:
84
+ needs: build
85
+ runs-on: ubuntu-latest
86
+ steps:
87
+ - uses: actions/checkout@v4 # for packaging/gen_formula.sh
88
+ - uses: actions/download-artifact@v4
89
+ with:
90
+ path: dist
91
+ merge-multiple: true
92
+ - name: Compute checksums
93
+ working-directory: dist
94
+ run: sha256sum *.tar.gz | tee SHA256SUMS
95
+ - name: Check tap token
96
+ env:
97
+ TAP_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }}
98
+ run: |
99
+ if [ -z "$TAP_TOKEN" ]; then
100
+ echo "::error::HOMEBREW_TAP_TOKEN not set — see the header of .github/workflows/release.yml"
101
+ exit 1
102
+ fi
103
+ # Fail fast on an expired/revoked/wrong-scoped token BEFORE creating the
104
+ # release — otherwise a bad token could upload assets, then 403 the
105
+ # formula push, leaving the tap with binaries but a stale formula.
106
+ code=$(curl -sS -o /dev/null -w '%{http_code}' \
107
+ -H "Authorization: Bearer $TAP_TOKEN" \
108
+ -H "Accept: application/vnd.github+json" \
109
+ https://api.github.com/repos/03hgryan/homebrew-tap)
110
+ if [ "$code" != "200" ]; then
111
+ echo "::error::HOMEBREW_TAP_TOKEN cannot reach 03hgryan/homebrew-tap (HTTP $code) — check it hasn't expired and has Contents: Read and write"
112
+ exit 1
113
+ fi
114
+ echo "tap token OK (repo reachable)"
115
+ - name: Publish binaries to the tap release
116
+ uses: softprops/action-gh-release@v2
117
+ with:
118
+ repository: 03hgryan/homebrew-tap
119
+ token: ${{ secrets.HOMEBREW_TAP_TOKEN }}
120
+ tag_name: ${{ github.ref_name }}
121
+ files: |
122
+ dist/*.tar.gz
123
+ dist/SHA256SUMS
124
+ - name: Update the Homebrew formula
125
+ env:
126
+ TAP_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }}
127
+ REF_NAME: ${{ github.ref_name }}
128
+ run: |
129
+ VER="${REF_NAME#v}"
130
+ bash packaging/gen_formula.sh "$VER" dist/SHA256SUMS > /tmp/cync.rb
131
+ git clone "https://x-access-token:${TAP_TOKEN}@github.com/03hgryan/homebrew-tap.git" tap
132
+ mkdir -p tap/Formula
133
+ cp /tmp/cync.rb tap/Formula/cync.rb
134
+ cd tap
135
+ git config user.name "github-actions[bot]"
136
+ git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
137
+ git add Formula/cync.rb
138
+ git commit -m "cync ${VER}" || { echo "formula unchanged"; exit 0; }
139
+ git push
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cync-cli
3
- Version: 0.4.0
3
+ Version: 0.6.0
4
4
  Summary: Sync Claude Code conversations across machines — GitHub-authed CLI + server (Postgres metadata, R2 blobs).
5
5
  Project-URL: Homepage, https://github.com/03hgryan/cync
6
6
  Project-URL: Repository, https://github.com/03hgryan/cync
@@ -41,10 +41,13 @@ Requires-Dist: typer<1,>=0.12
41
41
  Provides-Extra: dev
42
42
  Requires-Dist: boto3<2,>=1.34; extra == 'dev'
43
43
  Requires-Dist: fastapi<1,>=0.110; extra == 'dev'
44
+ Requires-Dist: mcp>=1.0; extra == 'dev'
44
45
  Requires-Dist: pydantic<3,>=2; extra == 'dev'
45
46
  Requires-Dist: pytest<9,>=8; extra == 'dev'
46
47
  Requires-Dist: ruff>=0.5; extra == 'dev'
47
48
  Requires-Dist: uvicorn[standard]<1,>=0.29; extra == 'dev'
49
+ Provides-Extra: mcp
50
+ Requires-Dist: mcp>=1.0; extra == 'mcp'
48
51
  Provides-Extra: server
49
52
  Requires-Dist: boto3<2,>=1.34; extra == 'server'
50
53
  Requires-Dist: fastapi<1,>=0.110; extra == 'server'
@@ -78,17 +81,30 @@ Browser / CLI ── GitHub login (Supabase Auth) ──┐
78
81
 
79
82
  ## Install & use (hosted)
80
83
 
81
- The CLI is on PyPI as **`cync-cli`** (the command is `cync`). No server setup
82
- it points at the hosted cync by default:
84
+ The command is `cync`. No server setup it points at the hosted cync by default.
83
85
 
86
+ **Homebrew (macOS/Linux):**
87
+ ```bash
88
+ brew install 03hgryan/tap/cync
89
+ ```
90
+
91
+ **PyPI** (published as `cync-cli`):
84
92
  ```bash
85
93
  pipx install cync-cli # or: uv tool install cync-cli
94
+ ```
95
+
96
+ Then:
97
+ ```bash
86
98
  cync login # sign in with GitHub
87
99
  cd ~/path/to/your/project
88
100
  cync init myproject && cync push # machine A
89
101
  cync link myproject && cync pull # machine B → claude --resume
90
102
  ```
91
103
 
104
+ > The Homebrew binary is a self-contained PyInstaller build (MCP included) served
105
+ > from the public [`homebrew-tap`](https://github.com/03hgryan/homebrew-tap) repo;
106
+ > see [`packaging/HOMEBREW.md`](packaging/HOMEBREW.md) for how releases are cut.
107
+
92
108
  ## Self-hosting
93
109
 
94
110
  Prefer your own stack? Point the CLI at your deployment via `CYNC_SERVER_URL`
@@ -152,6 +168,20 @@ The SvelteKit server holds the token (Supabase SSR cookies); browse at `/p/<proj
152
168
  | `cync project list` / `rm <name>` | manage your projects |
153
169
  | `cync --version` | print the version |
154
170
 
171
+ ## MCP server (optional)
172
+ Let Claude (or any MCP host) read your synced conversations *as context* — pull a
173
+ past chat into the model mid-conversation, not just sync files.
174
+
175
+ ```bash
176
+ pipx install 'cync-cli[mcp]' # the mcp extra
177
+ cync login # the server uses your cync session
178
+ ```
179
+ Add it to your MCP host (Claude Code `.mcp.json`, Claude Desktop config, etc.):
180
+ ```json
181
+ { "mcpServers": { "cync": { "command": "cync", "args": ["mcp"] } } }
182
+ ```
183
+ Tools: `list_projects`, `list_conversations(project)`, `get_conversation(project, id)`.
184
+
155
185
  ## Migrating from v0.2
156
186
  If you have v0.2 (static-token) data still in R2, after `cync login`:
157
187
  ```bash
@@ -24,17 +24,30 @@ Browser / CLI ── GitHub login (Supabase Auth) ──┐
24
24
 
25
25
  ## Install & use (hosted)
26
26
 
27
- The CLI is on PyPI as **`cync-cli`** (the command is `cync`). No server setup
28
- it points at the hosted cync by default:
27
+ The command is `cync`. No server setup it points at the hosted cync by default.
29
28
 
29
+ **Homebrew (macOS/Linux):**
30
+ ```bash
31
+ brew install 03hgryan/tap/cync
32
+ ```
33
+
34
+ **PyPI** (published as `cync-cli`):
30
35
  ```bash
31
36
  pipx install cync-cli # or: uv tool install cync-cli
37
+ ```
38
+
39
+ Then:
40
+ ```bash
32
41
  cync login # sign in with GitHub
33
42
  cd ~/path/to/your/project
34
43
  cync init myproject && cync push # machine A
35
44
  cync link myproject && cync pull # machine B → claude --resume
36
45
  ```
37
46
 
47
+ > The Homebrew binary is a self-contained PyInstaller build (MCP included) served
48
+ > from the public [`homebrew-tap`](https://github.com/03hgryan/homebrew-tap) repo;
49
+ > see [`packaging/HOMEBREW.md`](packaging/HOMEBREW.md) for how releases are cut.
50
+
38
51
  ## Self-hosting
39
52
 
40
53
  Prefer your own stack? Point the CLI at your deployment via `CYNC_SERVER_URL`
@@ -98,6 +111,20 @@ The SvelteKit server holds the token (Supabase SSR cookies); browse at `/p/<proj
98
111
  | `cync project list` / `rm <name>` | manage your projects |
99
112
  | `cync --version` | print the version |
100
113
 
114
+ ## MCP server (optional)
115
+ Let Claude (or any MCP host) read your synced conversations *as context* — pull a
116
+ past chat into the model mid-conversation, not just sync files.
117
+
118
+ ```bash
119
+ pipx install 'cync-cli[mcp]' # the mcp extra
120
+ cync login # the server uses your cync session
121
+ ```
122
+ Add it to your MCP host (Claude Code `.mcp.json`, Claude Desktop config, etc.):
123
+ ```json
124
+ { "mcpServers": { "cync": { "command": "cync", "args": ["mcp"] } } }
125
+ ```
126
+ Tools: `list_projects`, `list_conversations(project)`, `get_conversation(project, id)`.
127
+
101
128
  ## Migrating from v0.2
102
129
  If you have v0.2 (static-token) data still in R2, after `cync login`:
103
130
  ```bash
@@ -35,7 +35,8 @@
35
35
  <footer
36
36
  style="max-width:880px;margin:0 auto;padding:16px 20px;color:#888;font-size:12px;border-top:1px solid #1d2027"
37
37
  >
38
- <a href="/privacy">Privacy</a> · <a href="https://github.com/03hgryan/cync">GitHub</a>
38
+ <a href="/docs">Docs</a> · <a href="/privacy">Privacy</a> ·
39
+ <a href="https://github.com/03hgryan/cync">GitHub</a>
39
40
  </footer>
40
41
 
41
42
  <style>
@@ -0,0 +1,92 @@
1
+ <h1>cync docs</h1>
2
+ <p class="muted">
3
+ Sync your Claude Code conversations across machines, signed in with GitHub. CLI +
4
+ hosted server; your metadata lives in Postgres, transcripts in R2.
5
+ </p>
6
+
7
+ <h2>Install</h2>
8
+ <pre class="code">brew install 03hgryan/tap/cync # macOS/Linux (Homebrew)
9
+ # or
10
+ pipx install cync-cli # or: uv tool install cync-cli
11
+
12
+ cync login # sign in with GitHub</pre>
13
+
14
+ <h2>Quickstart</h2>
15
+ <pre class="code"># machine A
16
+ cync init myproject # create a project + link this directory
17
+ cync push # upload this repo's conversations
18
+
19
+ # machine B
20
+ cync link myproject
21
+ cync pull # then: claude --resume</pre>
22
+
23
+ <h2>Commands</h2>
24
+ <table>
25
+ <tbody>
26
+ <tr><td><code>cync login</code> / <code>logout</code> / <code>whoami</code></td><td>GitHub auth session</td></tr>
27
+ <tr><td><code>cync init &lt;name&gt;</code></td><td>create a project + link this directory</td></tr>
28
+ <tr><td><code>cync link &lt;name&gt;</code></td><td>link this directory to an existing project</td></tr>
29
+ <tr><td><code>cync unlink</code></td><td>remove this directory's link</td></tr>
30
+ <tr><td><code>cync status</code></td><td>login, linked project, and local-vs-server sync state</td></tr>
31
+ <tr><td><code>cync push [id]</code></td><td>upload conversation(s)</td></tr>
32
+ <tr><td><code>cync pull [id]</code></td><td>download into <code>~/.claude</code></td></tr>
33
+ <tr><td><code>cync list</code></td><td>list this project's conversations</td></tr>
34
+ <tr><td><code>cync rm &lt;id&gt;</code></td><td>delete one server conversation (local copy kept)</td></tr>
35
+ <tr><td><code>cync open</code></td><td>open this project in the web viewer</td></tr>
36
+ <tr><td><code>cync install-hooks</code></td><td>auto-sync via Claude Code hooks</td></tr>
37
+ <tr><td><code>cync project list</code> / <code>rm</code></td><td>manage projects</td></tr>
38
+ </tbody>
39
+ </table>
40
+
41
+ <h2>Auto-sync (hooks)</h2>
42
+ <p>Wire Claude Code to pull on session start and push on session end:</p>
43
+ <pre class="code">cync install-hooks # cync install-hooks --uninstall to remove</pre>
44
+
45
+ <h2>MCP server</h2>
46
+ <p>
47
+ Let Claude (or any MCP host) read your synced conversations <em>as context</em> —
48
+ pull a past chat into the model mid-conversation.
49
+ </p>
50
+ <pre class="code">pipx install 'cync-cli[mcp]'
51
+ cync login</pre>
52
+ <p>Add it to your MCP host (Claude Code <code>.mcp.json</code>, Claude Desktop config, …):</p>
53
+ <pre class="code">{'{'} "mcpServers": {'{'} "cync": {'{'} "command": "cync", "args": ["mcp"] {'}'} {'}'} {'}'}</pre>
54
+ <p class="muted">Tools: <code>list_projects</code>, <code>list_conversations(project)</code>, <code>get_conversation(project, id)</code>.</p>
55
+
56
+ <h2>API (developers)</h2>
57
+ <p>All data endpoints require a Supabase JWT (<code>Authorization: Bearer &lt;token&gt;</code>) and are scoped to your user.</p>
58
+ <table>
59
+ <tbody>
60
+ <tr><td><code>GET /health</code> · <code>/config</code></td><td>liveness · public Supabase/web params</td></tr>
61
+ <tr><td><code>POST /projects</code></td><td>create a project <code>{'{'} name {'}'}</code></td></tr>
62
+ <tr><td><code>GET /projects</code></td><td>your projects</td></tr>
63
+ <tr><td><code>GET /projects/&#123;id&#125;/conversations</code></td><td>conversations in a project</td></tr>
64
+ <tr><td><code>GET /projects/&#123;id&#125;/conversations/&#123;cid&#125;</code></td><td>one conversation (base64 transcript)</td></tr>
65
+ <tr><td><code>PUT /conversations/&#123;cid&#125;</code></td><td>upsert a conversation</td></tr>
66
+ <tr><td><code>DELETE /conversations/&#123;cid&#125;</code> · <code>/projects/&#123;id&#125;</code></td><td>delete</td></tr>
67
+ </tbody>
68
+ </table>
69
+
70
+ <h2>Self-hosting</h2>
71
+ <p>
72
+ Prefer your own stack? Deploy the server + your own Supabase + R2 and point the CLI
73
+ at it with <code>CYNC_SERVER_URL</code>. See the
74
+ <a href="https://github.com/03hgryan/cync">GitHub repo</a>.
75
+ </p>
76
+
77
+ <style>
78
+ h2 { margin-top: 28px; font-size: 16px; }
79
+ .code {
80
+ background: #15191f;
81
+ border: 1px solid #1d2027;
82
+ border-radius: 8px;
83
+ padding: 12px 14px;
84
+ overflow-x: auto;
85
+ font: 13px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace;
86
+ white-space: pre;
87
+ }
88
+ table { border-collapse: collapse; width: 100%; }
89
+ td { border-bottom: 1px solid #1d2027; padding: 7px 8px; vertical-align: top; font-size: 13px; }
90
+ td:first-child { white-space: nowrap; padding-right: 16px; }
91
+ p { max-width: 680px; }
92
+ </style>
@@ -0,0 +1,76 @@
1
+ # Homebrew distribution
2
+
3
+ `cync` ships to Homebrew as **standalone binaries** (PyInstaller `--onefile`,
4
+ MCP bundled) so users don't need Python. Because this source repo is **private**
5
+ and `brew` downloads release assets over unauthenticated HTTPS, the binaries and
6
+ the formula live on a separate **public** repo: [`03hgryan/homebrew-tap`].
7
+
8
+ ```
9
+ tag v0.6.0 on cync (private)
10
+
11
+ ▼ .github/workflows/release.yml
12
+ build macos-arm64 / macos-x86_64 / linux-x86_64 (PyInstaller)
13
+
14
+ ▼ softprops/action-gh-release (via HOMEBREW_TAP_TOKEN)
15
+ 03hgryan/homebrew-tap ── releases/v0.6.0/cync-*.tar.gz + SHA256SUMS
16
+ └─ Formula/cync.rb (regenerated, points at those assets)
17
+
18
+
19
+ brew install 03hgryan/tap/cync
20
+ ```
21
+
22
+ ## One-time setup
23
+
24
+ 1. **Create the tap repo.** New **public** repo `03hgryan/homebrew-tap`, tick
25
+ *"Add a README"* so it has an initial commit (the release step needs a
26
+ default branch to exist).
27
+
28
+ 2. **Create a scoped token.** GitHub → Settings → Developer settings →
29
+ **Fine-grained tokens**. Repository access: **only** `homebrew-tap`.
30
+ Permissions: **Contents → Read and write**. Copy the token.
31
+
32
+ 3. **Add it to the cync repo.** cync → Settings → Secrets and variables →
33
+ Actions → **New repository secret**, name `HOMEBREW_TAP_TOKEN`, paste the
34
+ token.
35
+
36
+ ## Cutting a release
37
+
38
+ ```bash
39
+ # bump version in pyproject.toml + src/cync/__init__.py first, commit, then:
40
+ git tag v0.6.0
41
+ git push origin v0.6.0
42
+ ```
43
+
44
+ That fires two workflows in parallel:
45
+ - **publish.yml** → PyPI (`cync-cli`)
46
+ - **release.yml** → builds binaries, uploads them + `SHA256SUMS` to the tap's
47
+ release, regenerates `Formula/cync.rb`, and pushes it to the tap.
48
+
49
+ Verify:
50
+ ```bash
51
+ brew update
52
+ brew install 03hgryan/tap/cync # or: brew upgrade cync
53
+ cync --version
54
+ ```
55
+
56
+ ## Regenerating the formula by hand
57
+
58
+ If you ever need to rebuild the formula outside CI (e.g. the tap push failed),
59
+ download the release's `SHA256SUMS` from the tap and run:
60
+
61
+ ```bash
62
+ bash packaging/gen_formula.sh 0.6.0 SHA256SUMS > Formula/cync.rb
63
+ ```
64
+
65
+ ## Notes / caveats
66
+
67
+ - **Unsigned binaries.** The PyInstaller output isn't code-signed or notarized.
68
+ Homebrew installs via `curl` (no quarantine xattr), so it runs — but if a user
69
+ ever sees a Gatekeeper block, `xattr -dr com.apple.quarantine $(brew --prefix)/bin/cync`
70
+ clears it. Notarization is a future improvement.
71
+ - **Startup cost.** `--onefile` unpacks to a temp dir on first run each launch;
72
+ fine for a CLI, slightly slower than a native binary.
73
+ - **Platforms.** macOS arm64/x86_64 and Linux x86_64. Add `ubuntu-24.04-arm`
74
+ to the matrix + an `on_linux/on_arm` block in `gen_formula.sh` for Linux ARM.
75
+
76
+ [`03hgryan/homebrew-tap`]: https://github.com/03hgryan/homebrew-tap
@@ -0,0 +1,9 @@
1
+ """PyInstaller entry point for the standalone `cync` binary.
2
+
3
+ PyInstaller needs a script path (not a module), so this thin wrapper calls the
4
+ same entry point as the `cync` console script (`cync.client:main`).
5
+ """
6
+ from cync.client import main
7
+
8
+ if __name__ == "__main__":
9
+ main()
@@ -0,0 +1,63 @@
1
+ #!/usr/bin/env bash
2
+ # Generate the Homebrew formula for cync from a SHA256SUMS file.
3
+ #
4
+ # gen_formula.sh <version-without-v> [SHA256SUMS-path] > cync.rb
5
+ #
6
+ # Binaries live on the PUBLIC tap repo's releases (this source repo is private),
7
+ # so the formula's download URLs point there, not at the cync repo.
8
+ set -euo pipefail
9
+
10
+ VERSION="${1:?usage: gen_formula.sh <version> [sums-file]}"
11
+ SUMS="${2:-SHA256SUMS}"
12
+ TAP_OWNER="${TAP_OWNER:-03hgryan}"
13
+ TAP_REPO="${TAP_REPO:-homebrew-tap}"
14
+ BASE="https://github.com/${TAP_OWNER}/${TAP_REPO}/releases/download/v${VERSION}"
15
+
16
+ # Pull one checksum out of the `sha256sum` output (format: "<hash> <file>").
17
+ sha() {
18
+ local line
19
+ line=$(grep " cync-$1\.tar\.gz\$" "$SUMS") || {
20
+ echo "gen_formula: no checksum for cync-$1.tar.gz in $SUMS" >&2
21
+ exit 1
22
+ }
23
+ echo "${line%% *}"
24
+ }
25
+
26
+ ARM=$(sha macos-arm64)
27
+ INTEL=$(sha macos-x86_64)
28
+ LINUX=$(sha linux-x86_64)
29
+
30
+ cat <<EOF
31
+ class Cync < Formula
32
+ desc "Sync Claude Code conversations across machines"
33
+ homepage "https://cync-topaz.vercel.app/docs"
34
+ version "${VERSION}"
35
+ license "MIT"
36
+
37
+ on_macos do
38
+ on_arm do
39
+ url "${BASE}/cync-macos-arm64.tar.gz"
40
+ sha256 "${ARM}"
41
+ end
42
+ on_intel do
43
+ url "${BASE}/cync-macos-x86_64.tar.gz"
44
+ sha256 "${INTEL}"
45
+ end
46
+ end
47
+
48
+ on_linux do
49
+ on_intel do
50
+ url "${BASE}/cync-linux-x86_64.tar.gz"
51
+ sha256 "${LINUX}"
52
+ end
53
+ end
54
+
55
+ def install
56
+ bin.install "cync"
57
+ end
58
+
59
+ test do
60
+ assert_match version.to_s, shell_output("#{bin}/cync --version")
61
+ end
62
+ end
63
+ EOF
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "cync-cli"
3
- version = "0.4.0"
3
+ version = "0.6.0"
4
4
  description = "Sync Claude Code conversations across machines — GitHub-authed CLI + server (Postgres metadata, R2 blobs)."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -34,8 +34,12 @@ server = [
34
34
  "boto3>=1.34,<2",
35
35
  "pydantic>=2,<3",
36
36
  ]
37
+ mcp = [
38
+ "mcp>=1.0",
39
+ ]
37
40
  dev = [
38
41
  "cync-cli[server]",
42
+ "cync-cli[mcp]",
39
43
  "pytest>=8,<9",
40
44
  "ruff>=0.5",
41
45
  ]
@@ -1,3 +1,3 @@
1
1
  """cync — sync Claude Code conversations across machines."""
2
2
 
3
- __version__ = "0.4.0"
3
+ __version__ = "0.6.0"
@@ -416,6 +416,19 @@ def install_hooks(
416
416
  console.print(" · last-writer-wins; run [bold]cync install-hooks --uninstall[/] to remove")
417
417
 
418
418
 
419
+ @app.command(name="mcp")
420
+ def mcp_cmd() -> None:
421
+ """Run the cync MCP server (stdio) so MCP hosts can read your conversations."""
422
+ import sys
423
+
424
+ try:
425
+ from .mcp_server import run
426
+ except ImportError:
427
+ print("MCP support not installed — run: pipx install 'cync-cli[mcp]'", file=sys.stderr)
428
+ raise typer.Exit(1)
429
+ run()
430
+
431
+
419
432
  # ---- project management ----
420
433
 
421
434
 
@@ -0,0 +1,132 @@
1
+ """cync MCP server — expose your synced conversations to MCP hosts.
2
+
3
+ Run via `cync mcp` (stdio). Authenticates with your cync session (from
4
+ `cync login`) and calls the cync API, so an MCP host (Claude Desktop, Claude
5
+ Code, etc.) can read your conversations across machines as retrievable context.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import base64
10
+ import json
11
+
12
+ import httpx
13
+ from mcp.server.fastmcp import FastMCP
14
+
15
+ from . import auth, common
16
+ from .config import ClientConfig
17
+
18
+ mcp = FastMCP("cync")
19
+
20
+ _MAX_CHARS = 30000 # cap transcript size returned to the model
21
+
22
+
23
+ def _request(method: str, path: str, **kw) -> httpx.Response:
24
+ session = auth.load_session()
25
+ if not session:
26
+ raise RuntimeError("not signed in — run `cync login` in a terminal first")
27
+ server = ClientConfig.load().server_url
28
+ headers = {"Authorization": f"Bearer {session.access_token}"}
29
+ r = httpx.request(method, server + path, headers=headers, timeout=60.0, **kw)
30
+ if r.status_code == 401:
31
+ session = auth.refresh(session)
32
+ headers["Authorization"] = f"Bearer {session.access_token}"
33
+ r = httpx.request(method, server + path, headers=headers, timeout=60.0, **kw)
34
+ r.raise_for_status()
35
+ return r
36
+
37
+
38
+ def _resolve_project(project: str) -> dict:
39
+ slug = common.slugify_project(project)
40
+ for p in _request("GET", "/projects").json():
41
+ if p.get("slug") == slug:
42
+ return p
43
+ raise ValueError(f"no project '{project}' — call list_projects() to see what's available")
44
+
45
+
46
+ def _render(jsonl_text: str) -> str:
47
+ out: list[str] = []
48
+ for line in jsonl_text.splitlines():
49
+ line = line.strip()
50
+ if not line:
51
+ continue
52
+ try:
53
+ o = json.loads(line)
54
+ except Exception:
55
+ continue
56
+ typ = o.get("type")
57
+ if typ == "summary" and o.get("summary"):
58
+ out.append(f"[summary] {o['summary']}")
59
+ continue
60
+ if typ not in ("user", "assistant"):
61
+ continue
62
+ content = (o.get("message") or {}).get("content")
63
+ text = ""
64
+ if isinstance(content, str):
65
+ text = content
66
+ elif isinstance(content, list):
67
+ parts = []
68
+ for p in content:
69
+ if not isinstance(p, dict):
70
+ continue
71
+ if p.get("type") == "text":
72
+ parts.append(p.get("text", ""))
73
+ elif p.get("type") == "tool_use":
74
+ parts.append(f"[tool: {p.get('name', '')}]")
75
+ elif p.get("type") == "tool_result":
76
+ parts.append("[tool result]")
77
+ text = "\n".join(x for x in parts if x)
78
+ if text.strip() and not text.startswith("<"):
79
+ out.append(f"{typ}: {text}")
80
+ return "\n\n".join(out)
81
+
82
+
83
+ @mcp.tool()
84
+ def list_projects() -> list[dict]:
85
+ """List your cync projects (name, slug, and conversation count)."""
86
+ return [
87
+ {"slug": p["slug"], "name": p.get("name", ""), "conversations": p.get("count", 0)}
88
+ for p in _request("GET", "/projects").json()
89
+ ]
90
+
91
+
92
+ @mcp.tool()
93
+ def list_conversations(project: str) -> list[dict]:
94
+ """List conversations in a project (by slug): id, title, and updated time."""
95
+ proj = _resolve_project(project)
96
+ convs = _request("GET", f"/projects/{proj['id']}/conversations").json()
97
+ return [
98
+ {"id": c["id"], "title": c.get("title", ""), "updated": c.get("updated_at", "")}
99
+ for c in convs
100
+ ]
101
+
102
+
103
+ @mcp.tool()
104
+ def get_conversation(project: str, conversation_id: str) -> str:
105
+ """Fetch a past conversation's transcript as readable text, to use as context.
106
+
107
+ `project` is a slug; `conversation_id` is a full id or an unambiguous prefix.
108
+ """
109
+ proj = _resolve_project(project)
110
+ convs = _request("GET", f"/projects/{proj['id']}/conversations").json()
111
+ matches = [c["id"] for c in convs if c["id"].startswith(conversation_id)]
112
+ if not matches:
113
+ raise ValueError(f"no conversation matching '{conversation_id}'")
114
+ if len(matches) > 1:
115
+ raise ValueError(f"'{conversation_id}' is ambiguous ({len(matches)} matches)")
116
+ d = _request("GET", f"/projects/{proj['id']}/conversations/{matches[0]}").json()
117
+ text = _render(base64.b64decode(d["content_b64"]).decode("utf-8", "ignore"))
118
+ if len(text) > _MAX_CHARS:
119
+ text = "…(earlier messages truncated)…\n\n" + text[-_MAX_CHARS:]
120
+ return text or "(no renderable content)"
121
+
122
+
123
+ def run() -> None:
124
+ """Run the stdio MCP server (used by `cync mcp`).
125
+
126
+ Silence per-request httpx logs — in stdio mode stdout must be a clean
127
+ JSON-RPC stream (the mcp SDK sends its own logs to stderr).
128
+ """
129
+ import logging
130
+
131
+ logging.getLogger("httpx").setLevel(logging.WARNING)
132
+ mcp.run()
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