cync-cli 0.3.0__tar.gz → 0.4.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.3.0 → cync_cli-0.4.0}/.env.example +2 -0
- {cync_cli-0.3.0 → cync_cli-0.4.0}/Dockerfile +2 -1
- {cync_cli-0.3.0 → cync_cli-0.4.0}/PKG-INFO +6 -1
- {cync_cli-0.3.0 → cync_cli-0.4.0}/README.md +5 -0
- {cync_cli-0.3.0 → cync_cli-0.4.0}/pyproject.toml +1 -1
- {cync_cli-0.3.0 → cync_cli-0.4.0}/src/cync/__init__.py +1 -1
- {cync_cli-0.3.0 → cync_cli-0.4.0}/src/cync/client.py +150 -1
- {cync_cli-0.3.0 → cync_cli-0.4.0}/src/cync/config.py +2 -0
- {cync_cli-0.3.0 → cync_cli-0.4.0}/src/cync/server.py +5 -1
- {cync_cli-0.3.0 → cync_cli-0.4.0}/.dockerignore +0 -0
- {cync_cli-0.3.0 → cync_cli-0.4.0}/.github/workflows/publish.yml +0 -0
- {cync_cli-0.3.0 → cync_cli-0.4.0}/.gitignore +0 -0
- {cync_cli-0.3.0 → cync_cli-0.4.0}/LICENSE +0 -0
- {cync_cli-0.3.0 → cync_cli-0.4.0}/PRIVACY.md +0 -0
- {cync_cli-0.3.0 → cync_cli-0.4.0}/client/.env.example +0 -0
- {cync_cli-0.3.0 → cync_cli-0.4.0}/client/.gitignore +0 -0
- {cync_cli-0.3.0 → cync_cli-0.4.0}/client/.npmrc +0 -0
- {cync_cli-0.3.0 → cync_cli-0.4.0}/client/.vscode/extensions.json +0 -0
- {cync_cli-0.3.0 → cync_cli-0.4.0}/client/README.md +0 -0
- {cync_cli-0.3.0 → cync_cli-0.4.0}/client/package.json +0 -0
- {cync_cli-0.3.0 → cync_cli-0.4.0}/client/pnpm-lock.yaml +0 -0
- {cync_cli-0.3.0 → cync_cli-0.4.0}/client/pnpm-workspace.yaml +0 -0
- {cync_cli-0.3.0 → cync_cli-0.4.0}/client/src/app.d.ts +0 -0
- {cync_cli-0.3.0 → cync_cli-0.4.0}/client/src/app.html +0 -0
- {cync_cli-0.3.0 → cync_cli-0.4.0}/client/src/hooks.server.ts +0 -0
- {cync_cli-0.3.0 → cync_cli-0.4.0}/client/src/lib/assets/favicon.svg +0 -0
- {cync_cli-0.3.0 → cync_cli-0.4.0}/client/src/lib/index.ts +0 -0
- {cync_cli-0.3.0 → cync_cli-0.4.0}/client/src/lib/parse.ts +0 -0
- {cync_cli-0.3.0 → cync_cli-0.4.0}/client/src/lib/server/cync.ts +0 -0
- {cync_cli-0.3.0 → cync_cli-0.4.0}/client/src/lib/types.ts +0 -0
- {cync_cli-0.3.0 → cync_cli-0.4.0}/client/src/routes/+layout.server.ts +0 -0
- {cync_cli-0.3.0 → cync_cli-0.4.0}/client/src/routes/+layout.svelte +0 -0
- {cync_cli-0.3.0 → cync_cli-0.4.0}/client/src/routes/+layout.ts +0 -0
- {cync_cli-0.3.0 → cync_cli-0.4.0}/client/src/routes/+page.server.ts +0 -0
- {cync_cli-0.3.0 → cync_cli-0.4.0}/client/src/routes/+page.svelte +0 -0
- {cync_cli-0.3.0 → cync_cli-0.4.0}/client/src/routes/auth/callback/+server.ts +0 -0
- {cync_cli-0.3.0 → cync_cli-0.4.0}/client/src/routes/auth/signout/+server.ts +0 -0
- {cync_cli-0.3.0 → cync_cli-0.4.0}/client/src/routes/login/+page.svelte +0 -0
- {cync_cli-0.3.0 → cync_cli-0.4.0}/client/src/routes/p/[slug]/+page.server.ts +0 -0
- {cync_cli-0.3.0 → cync_cli-0.4.0}/client/src/routes/p/[slug]/+page.svelte +0 -0
- {cync_cli-0.3.0 → cync_cli-0.4.0}/client/src/routes/p/[slug]/c/[id]/+page.server.ts +0 -0
- {cync_cli-0.3.0 → cync_cli-0.4.0}/client/src/routes/p/[slug]/c/[id]/+page.svelte +0 -0
- {cync_cli-0.3.0 → cync_cli-0.4.0}/client/src/routes/privacy/+page.svelte +0 -0
- {cync_cli-0.3.0 → cync_cli-0.4.0}/client/static/robots.txt +0 -0
- {cync_cli-0.3.0 → cync_cli-0.4.0}/client/tsconfig.json +0 -0
- {cync_cli-0.3.0 → cync_cli-0.4.0}/client/vite.config.ts +0 -0
- {cync_cli-0.3.0 → cync_cli-0.4.0}/scripts/migrate_legacy.py +0 -0
- {cync_cli-0.3.0 → cync_cli-0.4.0}/src/cync/auth.py +0 -0
- {cync_cli-0.3.0 → cync_cli-0.4.0}/src/cync/common.py +0 -0
- {cync_cli-0.3.0 → cync_cli-0.4.0}/src/cync/storage.py +0 -0
- {cync_cli-0.3.0 → cync_cli-0.4.0}/src/cync/supabase_store.py +0 -0
- {cync_cli-0.3.0 → cync_cli-0.4.0}/supabase/schema.sql +0 -0
|
@@ -22,6 +22,8 @@ SUPABASE_SERVICE_ROLE_KEY=your_supabase_service_role_key
|
|
|
22
22
|
CYNC_HOST=127.0.0.1
|
|
23
23
|
CYNC_PORT=8787
|
|
24
24
|
# CYNC_MAX_BODY_BYTES=67108864 # 64 MB request-body cap
|
|
25
|
+
# Public web viewer URL, advertised via /config so `cync open` can find it:
|
|
26
|
+
# CYNC_WEB_URL=https://your-cync-web.vercel.app
|
|
25
27
|
|
|
26
28
|
# ---- hosted-service policy (optional) ----
|
|
27
29
|
# Access: restrict to specific GitHub emails/usernames (comma-separated). Empty = open.
|
|
@@ -4,7 +4,8 @@ FROM python:3.12-slim
|
|
|
4
4
|
WORKDIR /app
|
|
5
5
|
|
|
6
6
|
# Install the package + server extras (fastapi, uvicorn, boto3, pydantic).
|
|
7
|
-
|
|
7
|
+
# README.md + LICENSE are needed because pyproject references them (readme/license).
|
|
8
|
+
COPY pyproject.toml README.md LICENSE ./
|
|
8
9
|
COPY src ./src
|
|
9
10
|
RUN pip install --no-cache-dir ".[server]"
|
|
10
11
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cync-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.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
|
|
@@ -142,10 +142,15 @@ The SvelteKit server holds the token (Supabase SSR cookies); browse at `/p/<proj
|
|
|
142
142
|
| `cync init <name>` | create a project + link this directory |
|
|
143
143
|
| `cync link <name>` | link this directory to an existing project |
|
|
144
144
|
| `cync unlink` | remove this directory's link |
|
|
145
|
+
| `cync status` | show login, linked project, and local-vs-server sync state |
|
|
145
146
|
| `cync push [id]` | upload this project's conversation(s) |
|
|
146
147
|
| `cync pull [id]` | download into `~/.claude` (overwrite/add) |
|
|
147
148
|
| `cync list` | list this project's conversations |
|
|
149
|
+
| `cync rm <id>` | delete one conversation from the server (local copy kept) |
|
|
150
|
+
| `cync open` | open the web viewer for this project |
|
|
151
|
+
| `cync install-hooks` | auto-sync via Claude Code hooks (pull on start, push on end) |
|
|
148
152
|
| `cync project list` / `rm <name>` | manage your projects |
|
|
153
|
+
| `cync --version` | print the version |
|
|
149
154
|
|
|
150
155
|
## Migrating from v0.2
|
|
151
156
|
If you have v0.2 (static-token) data still in R2, after `cync login`:
|
|
@@ -88,10 +88,15 @@ The SvelteKit server holds the token (Supabase SSR cookies); browse at `/p/<proj
|
|
|
88
88
|
| `cync init <name>` | create a project + link this directory |
|
|
89
89
|
| `cync link <name>` | link this directory to an existing project |
|
|
90
90
|
| `cync unlink` | remove this directory's link |
|
|
91
|
+
| `cync status` | show login, linked project, and local-vs-server sync state |
|
|
91
92
|
| `cync push [id]` | upload this project's conversation(s) |
|
|
92
93
|
| `cync pull [id]` | download into `~/.claude` (overwrite/add) |
|
|
93
94
|
| `cync list` | list this project's conversations |
|
|
95
|
+
| `cync rm <id>` | delete one conversation from the server (local copy kept) |
|
|
96
|
+
| `cync open` | open the web viewer for this project |
|
|
97
|
+
| `cync install-hooks` | auto-sync via Claude Code hooks (pull on start, push on end) |
|
|
94
98
|
| `cync project list` / `rm <name>` | manage your projects |
|
|
99
|
+
| `cync --version` | print the version |
|
|
95
100
|
|
|
96
101
|
## Migrating from v0.2
|
|
97
102
|
If you have v0.2 (static-token) data still in R2, after `cync login`:
|
|
@@ -16,7 +16,7 @@ import typer
|
|
|
16
16
|
from rich.console import Console
|
|
17
17
|
from rich.table import Table
|
|
18
18
|
|
|
19
|
-
from . import auth, common
|
|
19
|
+
from . import __version__, auth, common
|
|
20
20
|
from .config import ClientConfig, get_link, remove_link, set_link
|
|
21
21
|
|
|
22
22
|
app = typer.Typer(add_completion=False, help="Sync Claude Code conversations across machines.")
|
|
@@ -25,6 +25,22 @@ app.add_typer(project_app, name="project")
|
|
|
25
25
|
console = Console()
|
|
26
26
|
|
|
27
27
|
|
|
28
|
+
def _version_callback(value: bool) -> None:
|
|
29
|
+
if value:
|
|
30
|
+
console.print(f"cync {__version__}")
|
|
31
|
+
raise typer.Exit()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@app.callback()
|
|
35
|
+
def _main(
|
|
36
|
+
version: bool = typer.Option(
|
|
37
|
+
False, "--version", callback=_version_callback, is_eager=True,
|
|
38
|
+
help="Show the cync version and exit.",
|
|
39
|
+
),
|
|
40
|
+
) -> None:
|
|
41
|
+
"""Sync Claude Code conversations across machines."""
|
|
42
|
+
|
|
43
|
+
|
|
28
44
|
def _check(r: httpx.Response) -> None:
|
|
29
45
|
if r.is_success:
|
|
30
46
|
return
|
|
@@ -147,6 +163,41 @@ def whoami() -> None:
|
|
|
147
163
|
console.print(user.get("email") or user.get("id") or "(unknown)")
|
|
148
164
|
|
|
149
165
|
|
|
166
|
+
@app.command()
|
|
167
|
+
def status() -> None:
|
|
168
|
+
"""Show login, the linked project, and local-vs-server sync state."""
|
|
169
|
+
root = _repo_root()
|
|
170
|
+
console.print(f"dir: {root}")
|
|
171
|
+
session = auth.load_session()
|
|
172
|
+
pid = get_link(root)
|
|
173
|
+
if not session:
|
|
174
|
+
console.print("login: [yellow]not logged in[/] (run `cync login`)")
|
|
175
|
+
if not pid:
|
|
176
|
+
console.print("project: [yellow]not linked[/] (run `cync init <name>` or `cync link <name>`)")
|
|
177
|
+
if not session or not pid:
|
|
178
|
+
return
|
|
179
|
+
api = Api()
|
|
180
|
+
user = auth.current_user(session)
|
|
181
|
+
console.print(f"login: {user.get('email') or user.get('id')}")
|
|
182
|
+
r = api.get("/projects")
|
|
183
|
+
_check(r)
|
|
184
|
+
proj = next((p for p in r.json() if p["id"] == pid), None)
|
|
185
|
+
console.print(f"project: {proj['slug'] if proj else '[red]link points to a missing project[/]'}")
|
|
186
|
+
folder = common.claude_projects_dir() / common.encode_project_dir(root)
|
|
187
|
+
local = {f.stem for f in common.list_local_conversations(folder)}
|
|
188
|
+
rr = api.get("/conversations", params={"project_id": pid})
|
|
189
|
+
_check(rr)
|
|
190
|
+
server = {m["id"] for m in rr.json()}
|
|
191
|
+
console.print(
|
|
192
|
+
f"sync: {len(local & server)} synced · "
|
|
193
|
+
f"{len(local - server)} local-only · {len(server - local)} server-only"
|
|
194
|
+
)
|
|
195
|
+
if local - server:
|
|
196
|
+
console.print(f" [yellow]push these:[/] {', '.join(sorted(s[:8] for s in (local - server)))}")
|
|
197
|
+
if server - local:
|
|
198
|
+
console.print(f" [cyan]pull these:[/] {', '.join(sorted(s[:8] for s in (server - local)))}")
|
|
199
|
+
|
|
200
|
+
|
|
150
201
|
# ---- project linking ----
|
|
151
202
|
|
|
152
203
|
|
|
@@ -267,6 +318,104 @@ def list_cmd() -> None:
|
|
|
267
318
|
console.print(table)
|
|
268
319
|
|
|
269
320
|
|
|
321
|
+
@app.command()
|
|
322
|
+
def rm(
|
|
323
|
+
id: str = typer.Argument(..., help="conversation id or prefix to delete from the server"),
|
|
324
|
+
yes: bool = typer.Option(False, "--yes", "-y", help="skip confirmation"),
|
|
325
|
+
) -> None:
|
|
326
|
+
"""Delete a single conversation from the server (your local copy is untouched)."""
|
|
327
|
+
api = Api()
|
|
328
|
+
_root, pid, _folder = _linked()
|
|
329
|
+
r = api.get("/conversations", params={"project_id": pid})
|
|
330
|
+
_check(r)
|
|
331
|
+
matches = [m["id"] for m in r.json() if m["id"].startswith(id)]
|
|
332
|
+
if not matches:
|
|
333
|
+
console.print(f"[yellow]no conversation matching '{id}'[/]")
|
|
334
|
+
raise typer.Exit(1)
|
|
335
|
+
if len(matches) > 1:
|
|
336
|
+
console.print(f"[red]'{id}' is ambiguous[/] ({len(matches)} matches) — use more characters")
|
|
337
|
+
raise typer.Exit(1)
|
|
338
|
+
full = matches[0]
|
|
339
|
+
if not yes:
|
|
340
|
+
typer.confirm(f"delete conversation {full[:8]} from the server?", abort=True)
|
|
341
|
+
rr = api.delete(f"/conversations/{full}", params={"project_id": pid})
|
|
342
|
+
_check(rr)
|
|
343
|
+
console.print(f"[green]deleted[/] {full[:8]} from the server (local copy kept)")
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
@app.command(name="open")
|
|
347
|
+
def open_cmd() -> None:
|
|
348
|
+
"""Open the web viewer for this project in your browser."""
|
|
349
|
+
import webbrowser
|
|
350
|
+
|
|
351
|
+
server = ClientConfig.load().server_url
|
|
352
|
+
web = os.environ.get("CYNC_WEB_URL") or ""
|
|
353
|
+
if not web:
|
|
354
|
+
try:
|
|
355
|
+
web = httpx.get(f"{server}/config", timeout=15).json().get("web_url") or ""
|
|
356
|
+
except Exception:
|
|
357
|
+
web = ""
|
|
358
|
+
web = web.rstrip("/")
|
|
359
|
+
if not web:
|
|
360
|
+
console.print("[yellow]web viewer URL unknown[/] — set CYNC_WEB_URL or configure it on the server")
|
|
361
|
+
raise typer.Exit(1)
|
|
362
|
+
url = web
|
|
363
|
+
pid = get_link(_repo_root())
|
|
364
|
+
if pid and auth.load_session():
|
|
365
|
+
r = Api().get("/projects")
|
|
366
|
+
if r.is_success:
|
|
367
|
+
proj = next((p for p in r.json() if p["id"] == pid), None)
|
|
368
|
+
if proj:
|
|
369
|
+
url = f"{web}/p/{proj['slug']}"
|
|
370
|
+
console.print(f"opening {url}")
|
|
371
|
+
webbrowser.open(url)
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
@app.command(name="install-hooks")
|
|
375
|
+
def install_hooks(
|
|
376
|
+
uninstall: bool = typer.Option(False, "--uninstall", help="remove cync's Claude Code hooks"),
|
|
377
|
+
) -> None:
|
|
378
|
+
"""Auto-sync via Claude Code hooks: pull on session start, push on session end."""
|
|
379
|
+
settings = common.claude_projects_dir().parent / "settings.json" # ~/.claude/settings.json
|
|
380
|
+
settings.parent.mkdir(parents=True, exist_ok=True)
|
|
381
|
+
data: dict = {}
|
|
382
|
+
if settings.exists():
|
|
383
|
+
try:
|
|
384
|
+
data = json.loads(settings.read_text())
|
|
385
|
+
except Exception:
|
|
386
|
+
data = {}
|
|
387
|
+
hooks = data.setdefault("hooks", {})
|
|
388
|
+
|
|
389
|
+
def _strip(event: str) -> None:
|
|
390
|
+
kept = [
|
|
391
|
+
e for e in hooks.get(event, [])
|
|
392
|
+
if not any("cync" in h.get("command", "") for h in e.get("hooks", []))
|
|
393
|
+
]
|
|
394
|
+
if kept:
|
|
395
|
+
hooks[event] = kept
|
|
396
|
+
else:
|
|
397
|
+
hooks.pop(event, None)
|
|
398
|
+
|
|
399
|
+
_strip("SessionStart")
|
|
400
|
+
_strip("SessionEnd")
|
|
401
|
+
if uninstall:
|
|
402
|
+
if not hooks:
|
|
403
|
+
data.pop("hooks", None)
|
|
404
|
+
settings.write_text(json.dumps(data, indent=2))
|
|
405
|
+
console.print("removed cync hooks")
|
|
406
|
+
return
|
|
407
|
+
hooks.setdefault("SessionStart", []).append(
|
|
408
|
+
{"hooks": [{"type": "command", "command": "cync pull >/dev/null 2>&1 || true"}]}
|
|
409
|
+
)
|
|
410
|
+
hooks.setdefault("SessionEnd", []).append(
|
|
411
|
+
{"hooks": [{"type": "command", "command": "cync push >/dev/null 2>&1 || true"}]}
|
|
412
|
+
)
|
|
413
|
+
settings.write_text(json.dumps(data, indent=2))
|
|
414
|
+
console.print(f"[green]installed[/] Claude Code hooks in {settings}")
|
|
415
|
+
console.print(" · pull on session start, push on session end (only for linked dirs)")
|
|
416
|
+
console.print(" · last-writer-wins; run [bold]cync install-hooks --uninstall[/] to remove")
|
|
417
|
+
|
|
418
|
+
|
|
270
419
|
# ---- project management ----
|
|
271
420
|
|
|
272
421
|
|
|
@@ -18,6 +18,7 @@ class ServerConfig:
|
|
|
18
18
|
token: str
|
|
19
19
|
supabase_url: str
|
|
20
20
|
supabase_anon_key: str
|
|
21
|
+
web_url: str
|
|
21
22
|
|
|
22
23
|
@classmethod
|
|
23
24
|
def from_env(cls) -> "ServerConfig":
|
|
@@ -33,6 +34,7 @@ class ServerConfig:
|
|
|
33
34
|
token=os.environ.get("CYNC_TOKEN", ""),
|
|
34
35
|
supabase_url=os.environ.get("SUPABASE_URL", "").rstrip("/"),
|
|
35
36
|
supabase_anon_key=os.environ.get("SUPABASE_ANON_KEY", ""),
|
|
37
|
+
web_url=os.environ.get("CYNC_WEB_URL", "").rstrip("/"),
|
|
36
38
|
)
|
|
37
39
|
|
|
38
40
|
|
|
@@ -132,7 +132,11 @@ def health() -> dict:
|
|
|
132
132
|
@app.get("/config")
|
|
133
133
|
def public_config() -> dict:
|
|
134
134
|
"""Public Supabase params for the CLI/web login flow (the anon key is public)."""
|
|
135
|
-
return {
|
|
135
|
+
return {
|
|
136
|
+
"supabase_url": cfg.supabase_url,
|
|
137
|
+
"supabase_anon_key": cfg.supabase_anon_key,
|
|
138
|
+
"web_url": cfg.web_url,
|
|
139
|
+
}
|
|
136
140
|
|
|
137
141
|
|
|
138
142
|
# ---- projects ----
|
|
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
|