cync-cli 0.4.0__tar.gz → 0.5.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 (53) hide show
  1. {cync_cli-0.4.0 → cync_cli-0.5.0}/PKG-INFO +18 -1
  2. {cync_cli-0.4.0 → cync_cli-0.5.0}/README.md +14 -0
  3. {cync_cli-0.4.0 → cync_cli-0.5.0}/pyproject.toml +5 -1
  4. {cync_cli-0.4.0 → cync_cli-0.5.0}/src/cync/__init__.py +1 -1
  5. {cync_cli-0.4.0 → cync_cli-0.5.0}/src/cync/client.py +13 -0
  6. cync_cli-0.5.0/src/cync/mcp_server.py +132 -0
  7. {cync_cli-0.4.0 → cync_cli-0.5.0}/.dockerignore +0 -0
  8. {cync_cli-0.4.0 → cync_cli-0.5.0}/.env.example +0 -0
  9. {cync_cli-0.4.0 → cync_cli-0.5.0}/.github/workflows/publish.yml +0 -0
  10. {cync_cli-0.4.0 → cync_cli-0.5.0}/.gitignore +0 -0
  11. {cync_cli-0.4.0 → cync_cli-0.5.0}/Dockerfile +0 -0
  12. {cync_cli-0.4.0 → cync_cli-0.5.0}/LICENSE +0 -0
  13. {cync_cli-0.4.0 → cync_cli-0.5.0}/PRIVACY.md +0 -0
  14. {cync_cli-0.4.0 → cync_cli-0.5.0}/client/.env.example +0 -0
  15. {cync_cli-0.4.0 → cync_cli-0.5.0}/client/.gitignore +0 -0
  16. {cync_cli-0.4.0 → cync_cli-0.5.0}/client/.npmrc +0 -0
  17. {cync_cli-0.4.0 → cync_cli-0.5.0}/client/.vscode/extensions.json +0 -0
  18. {cync_cli-0.4.0 → cync_cli-0.5.0}/client/README.md +0 -0
  19. {cync_cli-0.4.0 → cync_cli-0.5.0}/client/package.json +0 -0
  20. {cync_cli-0.4.0 → cync_cli-0.5.0}/client/pnpm-lock.yaml +0 -0
  21. {cync_cli-0.4.0 → cync_cli-0.5.0}/client/pnpm-workspace.yaml +0 -0
  22. {cync_cli-0.4.0 → cync_cli-0.5.0}/client/src/app.d.ts +0 -0
  23. {cync_cli-0.4.0 → cync_cli-0.5.0}/client/src/app.html +0 -0
  24. {cync_cli-0.4.0 → cync_cli-0.5.0}/client/src/hooks.server.ts +0 -0
  25. {cync_cli-0.4.0 → cync_cli-0.5.0}/client/src/lib/assets/favicon.svg +0 -0
  26. {cync_cli-0.4.0 → cync_cli-0.5.0}/client/src/lib/index.ts +0 -0
  27. {cync_cli-0.4.0 → cync_cli-0.5.0}/client/src/lib/parse.ts +0 -0
  28. {cync_cli-0.4.0 → cync_cli-0.5.0}/client/src/lib/server/cync.ts +0 -0
  29. {cync_cli-0.4.0 → cync_cli-0.5.0}/client/src/lib/types.ts +0 -0
  30. {cync_cli-0.4.0 → cync_cli-0.5.0}/client/src/routes/+layout.server.ts +0 -0
  31. {cync_cli-0.4.0 → cync_cli-0.5.0}/client/src/routes/+layout.svelte +0 -0
  32. {cync_cli-0.4.0 → cync_cli-0.5.0}/client/src/routes/+layout.ts +0 -0
  33. {cync_cli-0.4.0 → cync_cli-0.5.0}/client/src/routes/+page.server.ts +0 -0
  34. {cync_cli-0.4.0 → cync_cli-0.5.0}/client/src/routes/+page.svelte +0 -0
  35. {cync_cli-0.4.0 → cync_cli-0.5.0}/client/src/routes/auth/callback/+server.ts +0 -0
  36. {cync_cli-0.4.0 → cync_cli-0.5.0}/client/src/routes/auth/signout/+server.ts +0 -0
  37. {cync_cli-0.4.0 → cync_cli-0.5.0}/client/src/routes/login/+page.svelte +0 -0
  38. {cync_cli-0.4.0 → cync_cli-0.5.0}/client/src/routes/p/[slug]/+page.server.ts +0 -0
  39. {cync_cli-0.4.0 → cync_cli-0.5.0}/client/src/routes/p/[slug]/+page.svelte +0 -0
  40. {cync_cli-0.4.0 → cync_cli-0.5.0}/client/src/routes/p/[slug]/c/[id]/+page.server.ts +0 -0
  41. {cync_cli-0.4.0 → cync_cli-0.5.0}/client/src/routes/p/[slug]/c/[id]/+page.svelte +0 -0
  42. {cync_cli-0.4.0 → cync_cli-0.5.0}/client/src/routes/privacy/+page.svelte +0 -0
  43. {cync_cli-0.4.0 → cync_cli-0.5.0}/client/static/robots.txt +0 -0
  44. {cync_cli-0.4.0 → cync_cli-0.5.0}/client/tsconfig.json +0 -0
  45. {cync_cli-0.4.0 → cync_cli-0.5.0}/client/vite.config.ts +0 -0
  46. {cync_cli-0.4.0 → cync_cli-0.5.0}/scripts/migrate_legacy.py +0 -0
  47. {cync_cli-0.4.0 → cync_cli-0.5.0}/src/cync/auth.py +0 -0
  48. {cync_cli-0.4.0 → cync_cli-0.5.0}/src/cync/common.py +0 -0
  49. {cync_cli-0.4.0 → cync_cli-0.5.0}/src/cync/config.py +0 -0
  50. {cync_cli-0.4.0 → cync_cli-0.5.0}/src/cync/server.py +0 -0
  51. {cync_cli-0.4.0 → cync_cli-0.5.0}/src/cync/storage.py +0 -0
  52. {cync_cli-0.4.0 → cync_cli-0.5.0}/src/cync/supabase_store.py +0 -0
  53. {cync_cli-0.4.0 → cync_cli-0.5.0}/supabase/schema.sql +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cync-cli
3
- Version: 0.4.0
3
+ Version: 0.5.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'
@@ -152,6 +155,20 @@ The SvelteKit server holds the token (Supabase SSR cookies); browse at `/p/<proj
152
155
  | `cync project list` / `rm <name>` | manage your projects |
153
156
  | `cync --version` | print the version |
154
157
 
158
+ ## MCP server (optional)
159
+ Let Claude (or any MCP host) read your synced conversations *as context* — pull a
160
+ past chat into the model mid-conversation, not just sync files.
161
+
162
+ ```bash
163
+ pipx install 'cync-cli[mcp]' # the mcp extra
164
+ cync login # the server uses your cync session
165
+ ```
166
+ Add it to your MCP host (Claude Code `.mcp.json`, Claude Desktop config, etc.):
167
+ ```json
168
+ { "mcpServers": { "cync": { "command": "cync", "args": ["mcp"] } } }
169
+ ```
170
+ Tools: `list_projects`, `list_conversations(project)`, `get_conversation(project, id)`.
171
+
155
172
  ## Migrating from v0.2
156
173
  If you have v0.2 (static-token) data still in R2, after `cync login`:
157
174
  ```bash
@@ -98,6 +98,20 @@ The SvelteKit server holds the token (Supabase SSR cookies); browse at `/p/<proj
98
98
  | `cync project list` / `rm <name>` | manage your projects |
99
99
  | `cync --version` | print the version |
100
100
 
101
+ ## MCP server (optional)
102
+ Let Claude (or any MCP host) read your synced conversations *as context* — pull a
103
+ past chat into the model mid-conversation, not just sync files.
104
+
105
+ ```bash
106
+ pipx install 'cync-cli[mcp]' # the mcp extra
107
+ cync login # the server uses your cync session
108
+ ```
109
+ Add it to your MCP host (Claude Code `.mcp.json`, Claude Desktop config, etc.):
110
+ ```json
111
+ { "mcpServers": { "cync": { "command": "cync", "args": ["mcp"] } } }
112
+ ```
113
+ Tools: `list_projects`, `list_conversations(project)`, `get_conversation(project, id)`.
114
+
101
115
  ## Migrating from v0.2
102
116
  If you have v0.2 (static-token) data still in R2, after `cync login`:
103
117
  ```bash
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "cync-cli"
3
- version = "0.4.0"
3
+ version = "0.5.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.5.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