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.
- cync_cli-0.6.0/.github/workflows/release.yml +139 -0
- {cync_cli-0.4.0 → cync_cli-0.6.0}/PKG-INFO +33 -3
- {cync_cli-0.4.0 → cync_cli-0.6.0}/README.md +29 -2
- {cync_cli-0.4.0 → cync_cli-0.6.0}/client/src/routes/+layout.svelte +2 -1
- cync_cli-0.6.0/client/src/routes/docs/+page.svelte +92 -0
- cync_cli-0.6.0/packaging/HOMEBREW.md +76 -0
- cync_cli-0.6.0/packaging/cync.py +9 -0
- cync_cli-0.6.0/packaging/gen_formula.sh +63 -0
- {cync_cli-0.4.0 → cync_cli-0.6.0}/pyproject.toml +5 -1
- {cync_cli-0.4.0 → cync_cli-0.6.0}/src/cync/__init__.py +1 -1
- {cync_cli-0.4.0 → cync_cli-0.6.0}/src/cync/client.py +13 -0
- cync_cli-0.6.0/src/cync/mcp_server.py +132 -0
- {cync_cli-0.4.0 → cync_cli-0.6.0}/.dockerignore +0 -0
- {cync_cli-0.4.0 → cync_cli-0.6.0}/.env.example +0 -0
- {cync_cli-0.4.0 → cync_cli-0.6.0}/.github/workflows/publish.yml +0 -0
- {cync_cli-0.4.0 → cync_cli-0.6.0}/.gitignore +0 -0
- {cync_cli-0.4.0 → cync_cli-0.6.0}/Dockerfile +0 -0
- {cync_cli-0.4.0 → cync_cli-0.6.0}/LICENSE +0 -0
- {cync_cli-0.4.0 → cync_cli-0.6.0}/PRIVACY.md +0 -0
- {cync_cli-0.4.0 → cync_cli-0.6.0}/client/.env.example +0 -0
- {cync_cli-0.4.0 → cync_cli-0.6.0}/client/.gitignore +0 -0
- {cync_cli-0.4.0 → cync_cli-0.6.0}/client/.npmrc +0 -0
- {cync_cli-0.4.0 → cync_cli-0.6.0}/client/.vscode/extensions.json +0 -0
- {cync_cli-0.4.0 → cync_cli-0.6.0}/client/README.md +0 -0
- {cync_cli-0.4.0 → cync_cli-0.6.0}/client/package.json +0 -0
- {cync_cli-0.4.0 → cync_cli-0.6.0}/client/pnpm-lock.yaml +0 -0
- {cync_cli-0.4.0 → cync_cli-0.6.0}/client/pnpm-workspace.yaml +0 -0
- {cync_cli-0.4.0 → cync_cli-0.6.0}/client/src/app.d.ts +0 -0
- {cync_cli-0.4.0 → cync_cli-0.6.0}/client/src/app.html +0 -0
- {cync_cli-0.4.0 → cync_cli-0.6.0}/client/src/hooks.server.ts +0 -0
- {cync_cli-0.4.0 → cync_cli-0.6.0}/client/src/lib/assets/favicon.svg +0 -0
- {cync_cli-0.4.0 → cync_cli-0.6.0}/client/src/lib/index.ts +0 -0
- {cync_cli-0.4.0 → cync_cli-0.6.0}/client/src/lib/parse.ts +0 -0
- {cync_cli-0.4.0 → cync_cli-0.6.0}/client/src/lib/server/cync.ts +0 -0
- {cync_cli-0.4.0 → cync_cli-0.6.0}/client/src/lib/types.ts +0 -0
- {cync_cli-0.4.0 → cync_cli-0.6.0}/client/src/routes/+layout.server.ts +0 -0
- {cync_cli-0.4.0 → cync_cli-0.6.0}/client/src/routes/+layout.ts +0 -0
- {cync_cli-0.4.0 → cync_cli-0.6.0}/client/src/routes/+page.server.ts +0 -0
- {cync_cli-0.4.0 → cync_cli-0.6.0}/client/src/routes/+page.svelte +0 -0
- {cync_cli-0.4.0 → cync_cli-0.6.0}/client/src/routes/auth/callback/+server.ts +0 -0
- {cync_cli-0.4.0 → cync_cli-0.6.0}/client/src/routes/auth/signout/+server.ts +0 -0
- {cync_cli-0.4.0 → cync_cli-0.6.0}/client/src/routes/login/+page.svelte +0 -0
- {cync_cli-0.4.0 → cync_cli-0.6.0}/client/src/routes/p/[slug]/+page.server.ts +0 -0
- {cync_cli-0.4.0 → cync_cli-0.6.0}/client/src/routes/p/[slug]/+page.svelte +0 -0
- {cync_cli-0.4.0 → cync_cli-0.6.0}/client/src/routes/p/[slug]/c/[id]/+page.server.ts +0 -0
- {cync_cli-0.4.0 → cync_cli-0.6.0}/client/src/routes/p/[slug]/c/[id]/+page.svelte +0 -0
- {cync_cli-0.4.0 → cync_cli-0.6.0}/client/src/routes/privacy/+page.svelte +0 -0
- {cync_cli-0.4.0 → cync_cli-0.6.0}/client/static/robots.txt +0 -0
- {cync_cli-0.4.0 → cync_cli-0.6.0}/client/tsconfig.json +0 -0
- {cync_cli-0.4.0 → cync_cli-0.6.0}/client/vite.config.ts +0 -0
- {cync_cli-0.4.0 → cync_cli-0.6.0}/scripts/migrate_legacy.py +0 -0
- {cync_cli-0.4.0 → cync_cli-0.6.0}/src/cync/auth.py +0 -0
- {cync_cli-0.4.0 → cync_cli-0.6.0}/src/cync/common.py +0 -0
- {cync_cli-0.4.0 → cync_cli-0.6.0}/src/cync/config.py +0 -0
- {cync_cli-0.4.0 → cync_cli-0.6.0}/src/cync/server.py +0 -0
- {cync_cli-0.4.0 → cync_cli-0.6.0}/src/cync/storage.py +0 -0
- {cync_cli-0.4.0 → cync_cli-0.6.0}/src/cync/supabase_store.py +0 -0
- {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.
|
|
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
|
|
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
|
|
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="/
|
|
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 <name></code></td><td>create a project + link this directory</td></tr>
|
|
28
|
+
<tr><td><code>cync link <name></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 <id></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 <token></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/{id}/conversations</code></td><td>conversations in a project</td></tr>
|
|
64
|
+
<tr><td><code>GET /projects/{id}/conversations/{cid}</code></td><td>one conversation (base64 transcript)</td></tr>
|
|
65
|
+
<tr><td><code>PUT /conversations/{cid}</code></td><td>upsert a conversation</td></tr>
|
|
66
|
+
<tr><td><code>DELETE /conversations/{cid}</code> · <code>/projects/{id}</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.
|
|
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
|
]
|
|
@@ -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
|
|
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
|