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.
Files changed (52) hide show
  1. {cync_cli-0.3.0 → cync_cli-0.4.0}/.env.example +2 -0
  2. {cync_cli-0.3.0 → cync_cli-0.4.0}/Dockerfile +2 -1
  3. {cync_cli-0.3.0 → cync_cli-0.4.0}/PKG-INFO +6 -1
  4. {cync_cli-0.3.0 → cync_cli-0.4.0}/README.md +5 -0
  5. {cync_cli-0.3.0 → cync_cli-0.4.0}/pyproject.toml +1 -1
  6. {cync_cli-0.3.0 → cync_cli-0.4.0}/src/cync/__init__.py +1 -1
  7. {cync_cli-0.3.0 → cync_cli-0.4.0}/src/cync/client.py +150 -1
  8. {cync_cli-0.3.0 → cync_cli-0.4.0}/src/cync/config.py +2 -0
  9. {cync_cli-0.3.0 → cync_cli-0.4.0}/src/cync/server.py +5 -1
  10. {cync_cli-0.3.0 → cync_cli-0.4.0}/.dockerignore +0 -0
  11. {cync_cli-0.3.0 → cync_cli-0.4.0}/.github/workflows/publish.yml +0 -0
  12. {cync_cli-0.3.0 → cync_cli-0.4.0}/.gitignore +0 -0
  13. {cync_cli-0.3.0 → cync_cli-0.4.0}/LICENSE +0 -0
  14. {cync_cli-0.3.0 → cync_cli-0.4.0}/PRIVACY.md +0 -0
  15. {cync_cli-0.3.0 → cync_cli-0.4.0}/client/.env.example +0 -0
  16. {cync_cli-0.3.0 → cync_cli-0.4.0}/client/.gitignore +0 -0
  17. {cync_cli-0.3.0 → cync_cli-0.4.0}/client/.npmrc +0 -0
  18. {cync_cli-0.3.0 → cync_cli-0.4.0}/client/.vscode/extensions.json +0 -0
  19. {cync_cli-0.3.0 → cync_cli-0.4.0}/client/README.md +0 -0
  20. {cync_cli-0.3.0 → cync_cli-0.4.0}/client/package.json +0 -0
  21. {cync_cli-0.3.0 → cync_cli-0.4.0}/client/pnpm-lock.yaml +0 -0
  22. {cync_cli-0.3.0 → cync_cli-0.4.0}/client/pnpm-workspace.yaml +0 -0
  23. {cync_cli-0.3.0 → cync_cli-0.4.0}/client/src/app.d.ts +0 -0
  24. {cync_cli-0.3.0 → cync_cli-0.4.0}/client/src/app.html +0 -0
  25. {cync_cli-0.3.0 → cync_cli-0.4.0}/client/src/hooks.server.ts +0 -0
  26. {cync_cli-0.3.0 → cync_cli-0.4.0}/client/src/lib/assets/favicon.svg +0 -0
  27. {cync_cli-0.3.0 → cync_cli-0.4.0}/client/src/lib/index.ts +0 -0
  28. {cync_cli-0.3.0 → cync_cli-0.4.0}/client/src/lib/parse.ts +0 -0
  29. {cync_cli-0.3.0 → cync_cli-0.4.0}/client/src/lib/server/cync.ts +0 -0
  30. {cync_cli-0.3.0 → cync_cli-0.4.0}/client/src/lib/types.ts +0 -0
  31. {cync_cli-0.3.0 → cync_cli-0.4.0}/client/src/routes/+layout.server.ts +0 -0
  32. {cync_cli-0.3.0 → cync_cli-0.4.0}/client/src/routes/+layout.svelte +0 -0
  33. {cync_cli-0.3.0 → cync_cli-0.4.0}/client/src/routes/+layout.ts +0 -0
  34. {cync_cli-0.3.0 → cync_cli-0.4.0}/client/src/routes/+page.server.ts +0 -0
  35. {cync_cli-0.3.0 → cync_cli-0.4.0}/client/src/routes/+page.svelte +0 -0
  36. {cync_cli-0.3.0 → cync_cli-0.4.0}/client/src/routes/auth/callback/+server.ts +0 -0
  37. {cync_cli-0.3.0 → cync_cli-0.4.0}/client/src/routes/auth/signout/+server.ts +0 -0
  38. {cync_cli-0.3.0 → cync_cli-0.4.0}/client/src/routes/login/+page.svelte +0 -0
  39. {cync_cli-0.3.0 → cync_cli-0.4.0}/client/src/routes/p/[slug]/+page.server.ts +0 -0
  40. {cync_cli-0.3.0 → cync_cli-0.4.0}/client/src/routes/p/[slug]/+page.svelte +0 -0
  41. {cync_cli-0.3.0 → cync_cli-0.4.0}/client/src/routes/p/[slug]/c/[id]/+page.server.ts +0 -0
  42. {cync_cli-0.3.0 → cync_cli-0.4.0}/client/src/routes/p/[slug]/c/[id]/+page.svelte +0 -0
  43. {cync_cli-0.3.0 → cync_cli-0.4.0}/client/src/routes/privacy/+page.svelte +0 -0
  44. {cync_cli-0.3.0 → cync_cli-0.4.0}/client/static/robots.txt +0 -0
  45. {cync_cli-0.3.0 → cync_cli-0.4.0}/client/tsconfig.json +0 -0
  46. {cync_cli-0.3.0 → cync_cli-0.4.0}/client/vite.config.ts +0 -0
  47. {cync_cli-0.3.0 → cync_cli-0.4.0}/scripts/migrate_legacy.py +0 -0
  48. {cync_cli-0.3.0 → cync_cli-0.4.0}/src/cync/auth.py +0 -0
  49. {cync_cli-0.3.0 → cync_cli-0.4.0}/src/cync/common.py +0 -0
  50. {cync_cli-0.3.0 → cync_cli-0.4.0}/src/cync/storage.py +0 -0
  51. {cync_cli-0.3.0 → cync_cli-0.4.0}/src/cync/supabase_store.py +0 -0
  52. {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
- COPY pyproject.toml ./
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.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`:
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "cync-cli"
3
- version = "0.3.0"
3
+ version = "0.4.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"
@@ -1,3 +1,3 @@
1
1
  """cync — sync Claude Code conversations across machines."""
2
2
 
3
- __version__ = "0.3.0"
3
+ __version__ = "0.4.0"
@@ -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 {"supabase_url": cfg.supabase_url, "supabase_anon_key": cfg.supabase_anon_key}
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