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.
- {cync_cli-0.4.0 → cync_cli-0.5.0}/PKG-INFO +18 -1
- {cync_cli-0.4.0 → cync_cli-0.5.0}/README.md +14 -0
- {cync_cli-0.4.0 → cync_cli-0.5.0}/pyproject.toml +5 -1
- {cync_cli-0.4.0 → cync_cli-0.5.0}/src/cync/__init__.py +1 -1
- {cync_cli-0.4.0 → cync_cli-0.5.0}/src/cync/client.py +13 -0
- cync_cli-0.5.0/src/cync/mcp_server.py +132 -0
- {cync_cli-0.4.0 → cync_cli-0.5.0}/.dockerignore +0 -0
- {cync_cli-0.4.0 → cync_cli-0.5.0}/.env.example +0 -0
- {cync_cli-0.4.0 → cync_cli-0.5.0}/.github/workflows/publish.yml +0 -0
- {cync_cli-0.4.0 → cync_cli-0.5.0}/.gitignore +0 -0
- {cync_cli-0.4.0 → cync_cli-0.5.0}/Dockerfile +0 -0
- {cync_cli-0.4.0 → cync_cli-0.5.0}/LICENSE +0 -0
- {cync_cli-0.4.0 → cync_cli-0.5.0}/PRIVACY.md +0 -0
- {cync_cli-0.4.0 → cync_cli-0.5.0}/client/.env.example +0 -0
- {cync_cli-0.4.0 → cync_cli-0.5.0}/client/.gitignore +0 -0
- {cync_cli-0.4.0 → cync_cli-0.5.0}/client/.npmrc +0 -0
- {cync_cli-0.4.0 → cync_cli-0.5.0}/client/.vscode/extensions.json +0 -0
- {cync_cli-0.4.0 → cync_cli-0.5.0}/client/README.md +0 -0
- {cync_cli-0.4.0 → cync_cli-0.5.0}/client/package.json +0 -0
- {cync_cli-0.4.0 → cync_cli-0.5.0}/client/pnpm-lock.yaml +0 -0
- {cync_cli-0.4.0 → cync_cli-0.5.0}/client/pnpm-workspace.yaml +0 -0
- {cync_cli-0.4.0 → cync_cli-0.5.0}/client/src/app.d.ts +0 -0
- {cync_cli-0.4.0 → cync_cli-0.5.0}/client/src/app.html +0 -0
- {cync_cli-0.4.0 → cync_cli-0.5.0}/client/src/hooks.server.ts +0 -0
- {cync_cli-0.4.0 → cync_cli-0.5.0}/client/src/lib/assets/favicon.svg +0 -0
- {cync_cli-0.4.0 → cync_cli-0.5.0}/client/src/lib/index.ts +0 -0
- {cync_cli-0.4.0 → cync_cli-0.5.0}/client/src/lib/parse.ts +0 -0
- {cync_cli-0.4.0 → cync_cli-0.5.0}/client/src/lib/server/cync.ts +0 -0
- {cync_cli-0.4.0 → cync_cli-0.5.0}/client/src/lib/types.ts +0 -0
- {cync_cli-0.4.0 → cync_cli-0.5.0}/client/src/routes/+layout.server.ts +0 -0
- {cync_cli-0.4.0 → cync_cli-0.5.0}/client/src/routes/+layout.svelte +0 -0
- {cync_cli-0.4.0 → cync_cli-0.5.0}/client/src/routes/+layout.ts +0 -0
- {cync_cli-0.4.0 → cync_cli-0.5.0}/client/src/routes/+page.server.ts +0 -0
- {cync_cli-0.4.0 → cync_cli-0.5.0}/client/src/routes/+page.svelte +0 -0
- {cync_cli-0.4.0 → cync_cli-0.5.0}/client/src/routes/auth/callback/+server.ts +0 -0
- {cync_cli-0.4.0 → cync_cli-0.5.0}/client/src/routes/auth/signout/+server.ts +0 -0
- {cync_cli-0.4.0 → cync_cli-0.5.0}/client/src/routes/login/+page.svelte +0 -0
- {cync_cli-0.4.0 → cync_cli-0.5.0}/client/src/routes/p/[slug]/+page.server.ts +0 -0
- {cync_cli-0.4.0 → cync_cli-0.5.0}/client/src/routes/p/[slug]/+page.svelte +0 -0
- {cync_cli-0.4.0 → cync_cli-0.5.0}/client/src/routes/p/[slug]/c/[id]/+page.server.ts +0 -0
- {cync_cli-0.4.0 → cync_cli-0.5.0}/client/src/routes/p/[slug]/c/[id]/+page.svelte +0 -0
- {cync_cli-0.4.0 → cync_cli-0.5.0}/client/src/routes/privacy/+page.svelte +0 -0
- {cync_cli-0.4.0 → cync_cli-0.5.0}/client/static/robots.txt +0 -0
- {cync_cli-0.4.0 → cync_cli-0.5.0}/client/tsconfig.json +0 -0
- {cync_cli-0.4.0 → cync_cli-0.5.0}/client/vite.config.ts +0 -0
- {cync_cli-0.4.0 → cync_cli-0.5.0}/scripts/migrate_legacy.py +0 -0
- {cync_cli-0.4.0 → cync_cli-0.5.0}/src/cync/auth.py +0 -0
- {cync_cli-0.4.0 → cync_cli-0.5.0}/src/cync/common.py +0 -0
- {cync_cli-0.4.0 → cync_cli-0.5.0}/src/cync/config.py +0 -0
- {cync_cli-0.4.0 → cync_cli-0.5.0}/src/cync/server.py +0 -0
- {cync_cli-0.4.0 → cync_cli-0.5.0}/src/cync/storage.py +0 -0
- {cync_cli-0.4.0 → cync_cli-0.5.0}/src/cync/supabase_store.py +0 -0
- {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.
|
|
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.
|
|
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
|
]
|
|
@@ -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
|
|
File without changes
|