token-cut-cli 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
@@ -0,0 +1,4 @@
1
+ from token_cut_cli.main import main
2
+
3
+ if __name__ == "__main__":
4
+ raise SystemExit(main())
@@ -0,0 +1,82 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from typing import Any
5
+
6
+ import httpx
7
+
8
+ DEFAULT_API_URL = "https://www.token-cut.com"
9
+ # Public OSS repo used only for `token-cut verify` smoke test (no customer code).
10
+ DEFAULT_REPO_URL = "https://github.com/fastapi/fastapi"
11
+ DEFAULT_TIMEOUT = 120.0
12
+
13
+
14
+ def api_base() -> str:
15
+ raw = (
16
+ os.getenv("TOKEN_CUT_API_URL")
17
+ or os.getenv("CONTEXT_CUT_API_URL")
18
+ or DEFAULT_API_URL
19
+ ).strip().rstrip("/")
20
+ return raw
21
+
22
+
23
+ def api_key() -> str:
24
+ key = (
25
+ os.getenv("TOKEN_CUT_API_KEY")
26
+ or os.getenv("CONTEXT_CUT_API_KEY")
27
+ or ""
28
+ ).strip()
29
+ return key
30
+
31
+
32
+ class TokenCutClient:
33
+ def __init__(
34
+ self,
35
+ base_url: str | None = None,
36
+ key: str | None = None,
37
+ timeout: float = DEFAULT_TIMEOUT,
38
+ ) -> None:
39
+ self.base = (base_url or api_base()).rstrip("/")
40
+ self.key = (key or api_key()).strip()
41
+ self.timeout = timeout
42
+
43
+ def _headers(self) -> dict[str, str]:
44
+ if not self.key:
45
+ raise ValueError("Missing API key")
46
+ return {"x-api-key": self.key, "Content-Type": "application/json"}
47
+
48
+ def health(self) -> dict[str, Any]:
49
+ with httpx.Client(timeout=30.0) as client:
50
+ r = client.get(f"{self.base}/health")
51
+ r.raise_for_status()
52
+ return r.json()
53
+
54
+ def optimize(
55
+ self,
56
+ prompt: str,
57
+ *,
58
+ repo_url: str | None = None,
59
+ repo_path: str | None = None,
60
+ use_graph: bool = True,
61
+ ) -> dict[str, Any]:
62
+ body: dict[str, Any] = {
63
+ "prompt": prompt,
64
+ "use_graph": use_graph,
65
+ "auto_build_graph": True,
66
+ }
67
+ if repo_url:
68
+ body["repo_url"] = repo_url
69
+ if repo_path:
70
+ body["repo_path"] = repo_path
71
+ with httpx.Client(timeout=self.timeout) as client:
72
+ r = client.post(
73
+ f"{self.base}/v1/optimize",
74
+ headers=self._headers(),
75
+ json=body,
76
+ )
77
+ if r.status_code == 401:
78
+ raise PermissionError("Invalid or expired API key (401)")
79
+ if r.status_code == 429:
80
+ raise PermissionError("Plan limit reached (429)")
81
+ r.raise_for_status()
82
+ return r.json()
token_cut_cli/main.py ADDED
@@ -0,0 +1,116 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import json
5
+ import sys
6
+
7
+ from token_cut_cli import __version__
8
+ from token_cut_cli.client import DEFAULT_API_URL, DEFAULT_REPO_URL, TokenCutClient
9
+ from token_cut_cli.verify import run_verify
10
+
11
+
12
+ def main(argv: list[str] | None = None) -> int:
13
+ parser = argparse.ArgumentParser(
14
+ prog="token-cut",
15
+ description="Token-Cut CLI — verify integration without cloning the repo",
16
+ )
17
+ parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
18
+ sub = parser.add_subparsers(dest="command", required=True)
19
+
20
+ v = sub.add_parser(
21
+ "verify",
22
+ help="Check API key, optimize, and Graphify on a public repo (~1 min)",
23
+ )
24
+ v.add_argument(
25
+ "--api-url",
26
+ default=None,
27
+ help=f"API base URL (default: {DEFAULT_API_URL} or TOKEN_CUT_API_URL)",
28
+ )
29
+ v.add_argument(
30
+ "--key",
31
+ default=None,
32
+ help="cc_live_… key (default: TOKEN_CUT_API_KEY env)",
33
+ )
34
+ v.add_argument(
35
+ "--repo-url",
36
+ default=DEFAULT_REPO_URL,
37
+ help="Public GitHub/GitLab URL for graph test",
38
+ )
39
+ v.add_argument(
40
+ "--repo-path",
41
+ default=None,
42
+ help="Local repo path instead of --repo-url (optional)",
43
+ )
44
+ v.add_argument("-q", "--quiet", action="store_true", help="Minimal output")
45
+
46
+ o = sub.add_parser("optimize", help="Run one optimize call and print JSON")
47
+ o.add_argument("prompt", help="Prompt text")
48
+ o.add_argument("--api-url", default=None)
49
+ o.add_argument("--key", default=None)
50
+ o.add_argument("--repo-url", default=None)
51
+ o.add_argument("--repo-path", default=None)
52
+
53
+ m = sub.add_parser(
54
+ "mcp-config",
55
+ help="Print MCP JSON for Cursor / Claude (uses pip package, no git clone)",
56
+ )
57
+ m.add_argument("--api-url", default=None)
58
+ m.add_argument("--key", default=None)
59
+ m.add_argument("--repo-path", default=None, help="Default TOKEN_CUT_REPO_PATH in env")
60
+
61
+ args = parser.parse_args(argv)
62
+
63
+ try:
64
+ client = TokenCutClient(base_url=args.api_url, key=args.key)
65
+ except ValueError:
66
+ print(
67
+ "error: set TOKEN_CUT_API_KEY or pass --key cc_live_…",
68
+ file=sys.stderr,
69
+ )
70
+ return 2
71
+
72
+ if args.command == "verify":
73
+ return run_verify(
74
+ client,
75
+ repo_url=args.repo_url,
76
+ repo_path=args.repo_path,
77
+ quiet=args.quiet,
78
+ )
79
+
80
+ if args.command == "optimize":
81
+ data = client.optimize(
82
+ args.prompt,
83
+ repo_url=args.repo_url,
84
+ repo_path=args.repo_path,
85
+ )
86
+ print(json.dumps(data, indent=2))
87
+ return 0
88
+
89
+ if args.command == "mcp-config":
90
+ env: dict[str, str] = {
91
+ "TOKEN_CUT_API_URL": client.base,
92
+ "TOKEN_CUT_API_KEY": client.key,
93
+ }
94
+ if getattr(args, "repo_path", None):
95
+ env["TOKEN_CUT_REPO_PATH"] = args.repo_path
96
+ cfg = {
97
+ "mcpServers": {
98
+ "token-cut": {
99
+ "command": sys.executable,
100
+ "args": ["-m", "token_cut_cli.mcp_server"],
101
+ "env": env,
102
+ }
103
+ }
104
+ }
105
+ print(json.dumps(cfg, indent=2))
106
+ print(
107
+ "\n# Paste into ~/.cursor/mcp.json or Claude Desktop config, then restart IDE.",
108
+ file=sys.stderr,
109
+ )
110
+ return 0
111
+
112
+ return 1
113
+
114
+
115
+ if __name__ == "__main__":
116
+ sys.exit(main())
@@ -0,0 +1,170 @@
1
+ """Token-Cut MCP server — shipped in token-cut-cli (calls your cloud API with cc_live_ key)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ import sys
8
+
9
+ from token_cut_cli import __version__
10
+ from token_cut_cli.client import DEFAULT_API_URL, api_base, api_key
11
+
12
+
13
+ def _post(path: str, body: dict) -> dict:
14
+ import urllib.request
15
+
16
+ base = api_base().rstrip("/")
17
+ key = api_key()
18
+ if not key:
19
+ raise RuntimeError("Set TOKEN_CUT_API_KEY=cc_live_…")
20
+
21
+ req = urllib.request.Request(
22
+ f"{base}{path}",
23
+ data=json.dumps(body).encode(),
24
+ headers={"Content-Type": "application/json", "x-api-key": key},
25
+ method="POST",
26
+ )
27
+ timeout = int(os.getenv("TOKEN_CUT_HTTP_TIMEOUT", "360"))
28
+ with urllib.request.urlopen(req, timeout=timeout) as resp:
29
+ return json.loads(resp.read().decode())
30
+
31
+
32
+ def _respond(obj: dict) -> None:
33
+ print(json.dumps(obj), flush=True)
34
+
35
+
36
+ def _tool_list() -> dict:
37
+ return {
38
+ "tools": [
39
+ {
40
+ "name": "optimize_prompt",
41
+ "description": (
42
+ "Optimize a prompt (Token-Cut cloud). Auto-builds graph.json when missing. "
43
+ "Requires cc_live_ API key. IDE still runs the LLM on your plan."
44
+ ),
45
+ "inputSchema": {
46
+ "type": "object",
47
+ "properties": {
48
+ "prompt": {"type": "string"},
49
+ "repo_path": {"type": "string"},
50
+ "repo_url": {"type": "string"},
51
+ "auto_build_graph": {"type": "boolean"},
52
+ },
53
+ "required": ["prompt"],
54
+ },
55
+ },
56
+ {
57
+ "name": "build_repo_graph",
58
+ "description": "Build Graphify graph for a repo (Token-Cut cloud).",
59
+ "inputSchema": {
60
+ "type": "object",
61
+ "properties": {
62
+ "repo_path": {"type": "string"},
63
+ "repo_url": {"type": "string"},
64
+ },
65
+ },
66
+ },
67
+ ]
68
+ }
69
+
70
+
71
+ def _env_repo_path() -> str | None:
72
+ return os.getenv("TOKEN_CUT_REPO_PATH") or os.getenv("CONTEXT_CUT_REPO_PATH")
73
+
74
+
75
+ def _env_repo_url() -> str | None:
76
+ return os.getenv("TOKEN_CUT_REPO_URL") or os.getenv("CONTEXT_CUT_REPO_URL")
77
+
78
+
79
+ def _handle_tools_call(msg: dict) -> None:
80
+ params = msg.get("params", {})
81
+ name = params.get("name")
82
+ args = params.get("arguments", {})
83
+ try:
84
+ if name == "optimize_prompt":
85
+ data = _post(
86
+ "/v1/optimize",
87
+ {
88
+ "prompt": args["prompt"],
89
+ "repo_path": args.get("repo_path") or _env_repo_path(),
90
+ "repo_url": args.get("repo_url") or _env_repo_url(),
91
+ "model": "gpt-4o",
92
+ "use_graph": True,
93
+ "auto_build_graph": args.get("auto_build_graph", True),
94
+ },
95
+ )
96
+ graph_line = ""
97
+ if data.get("graph_ready"):
98
+ built = " (built now)" if data.get("graph_built") else " (cached)"
99
+ graph_line = f"Graph: {data.get('graph_nodes', 0)} nodes{built}\n"
100
+ text = (
101
+ f"Reduction: {data['reduction_factor']}×\n"
102
+ f"Tokens: {data['original_tokens']} → {data['optimized_tokens']}\n"
103
+ f"Saved: ${data['cost_saved_usd']}\n"
104
+ f"{graph_line}\n"
105
+ f"{data['optimized_prompt'][:8000]}"
106
+ )
107
+ elif name == "build_repo_graph":
108
+ data = _post(
109
+ "/v1/graph/build",
110
+ {
111
+ "repo_path": args.get("repo_path"),
112
+ "repo_url": args.get("repo_url"),
113
+ },
114
+ )
115
+ text = json.dumps(data, indent=2)
116
+ else:
117
+ raise ValueError(f"Unknown tool: {name}")
118
+ _respond(
119
+ {
120
+ "jsonrpc": "2.0",
121
+ "id": msg.get("id"),
122
+ "result": {"content": [{"type": "text", "text": text}]},
123
+ }
124
+ )
125
+ except Exception as e:
126
+ _respond(
127
+ {
128
+ "jsonrpc": "2.0",
129
+ "id": msg.get("id"),
130
+ "error": {"code": -32000, "message": str(e)},
131
+ },
132
+ )
133
+
134
+
135
+ def main() -> None:
136
+ for line in sys.stdin:
137
+ line = line.strip()
138
+ if not line:
139
+ continue
140
+ msg = json.loads(line)
141
+ method = msg.get("method")
142
+ if method == "initialize":
143
+ _respond(
144
+ {
145
+ "jsonrpc": "2.0",
146
+ "id": msg.get("id"),
147
+ "result": {
148
+ "protocolVersion": "2024-11-05",
149
+ "capabilities": {"tools": {}},
150
+ "serverInfo": {
151
+ "name": "token-cut",
152
+ "version": __version__,
153
+ },
154
+ },
155
+ }
156
+ )
157
+ elif method == "notifications/initialized":
158
+ continue
159
+ elif method == "tools/list":
160
+ _respond(
161
+ {"jsonrpc": "2.0", "id": msg.get("id"), "result": _tool_list()}
162
+ )
163
+ elif method == "tools/call":
164
+ _handle_tools_call(msg)
165
+ elif "id" in msg:
166
+ _respond({"jsonrpc": "2.0", "id": msg["id"], "result": {}})
167
+
168
+
169
+ if __name__ == "__main__":
170
+ main()
@@ -0,0 +1,93 @@
1
+ from __future__ import annotations
2
+
3
+ import sys
4
+ from typing import Any
5
+
6
+ from token_cut_cli.client import DEFAULT_REPO_URL, TokenCutClient
7
+
8
+ VERIFY_PROMPT = (
9
+ "Explain how authentication and the optimize pipeline work in this repository. "
10
+ "Focus on api/app/ and the main entrypoints."
11
+ )
12
+
13
+
14
+ def _ok(msg: str) -> None:
15
+ print(f" ✓ {msg}")
16
+
17
+
18
+ def _fail(msg: str) -> None:
19
+ print(f" ✗ {msg}", file=sys.stderr)
20
+
21
+
22
+ def run_verify(
23
+ client: TokenCutClient,
24
+ *,
25
+ repo_url: str = DEFAULT_REPO_URL,
26
+ repo_path: str | None = None,
27
+ quiet: bool = False,
28
+ ) -> int:
29
+ if not quiet:
30
+ print(f"Token-Cut verify → {client.base}")
31
+ print(f" repo: {repo_path or repo_url}")
32
+ print()
33
+
34
+ # 1. Health (non-fatal — some deployments only expose /v1/*)
35
+ try:
36
+ health = client.health()
37
+ version = health.get("version", "?")
38
+ _ok(f"API reachable (version {version})")
39
+ except Exception as e:
40
+ if not quiet:
41
+ print(f" … health check skipped ({e})")
42
+
43
+ # 2. Auth + optimize + graph
44
+ try:
45
+ data = client.optimize(
46
+ VERIFY_PROMPT,
47
+ repo_url=repo_url if not repo_path else None,
48
+ repo_path=repo_path,
49
+ use_graph=True,
50
+ )
51
+ except PermissionError as e:
52
+ _fail(str(e))
53
+ print(
54
+ "\nGet a key: https://www.token-cut.com → Connect & Security",
55
+ file=sys.stderr,
56
+ )
57
+ return 2
58
+ except Exception as e:
59
+ _fail(f"Optimize failed: {e}")
60
+ return 3
61
+
62
+ original = int(data.get("original_tokens") or 0)
63
+ optimized = int(data.get("optimized_tokens") or 0)
64
+ graph_ready = bool(data.get("graph_ready"))
65
+ graph_built = bool(data.get("graph_built"))
66
+ nodes = int(data.get("graph_nodes") or 0)
67
+
68
+ _ok(f"API key accepted")
69
+ _ok(f"Optimize: {original} → {optimized} tokens")
70
+
71
+ if graph_ready:
72
+ cache = "built now" if graph_built else "cached"
73
+ _ok(f"Graphify: {nodes:,} nodes ({cache})")
74
+ else:
75
+ _fail("Graph not ready — check repo URL or Graphify on server")
76
+ return 4
77
+
78
+ if not quiet:
79
+ print()
80
+ if optimized < original:
81
+ pct = round(100 * (1 - optimized / max(original, 1)))
82
+ print(f" Token reduction on prompt alone: ~{pct}%")
83
+ print(" Next: token-cut mcp-config → paste into Cursor / Claude MCP settings")
84
+ print(" Your IDE subscription still runs the LLM.")
85
+
86
+ return 0
87
+
88
+
89
+ def format_result_summary(data: dict[str, Any]) -> str:
90
+ return (
91
+ f"tokens={data.get('original_tokens')}→{data.get('optimized_tokens')} "
92
+ f"graph_nodes={data.get('graph_nodes')}"
93
+ )
@@ -0,0 +1,78 @@
1
+ Metadata-Version: 2.4
2
+ Name: token-cut-cli
3
+ Version: 0.1.0
4
+ Summary: Verify Token-Cut API integration from the terminal (no repo clone required)
5
+ Project-URL: Homepage, https://www.token-cut.com
6
+ Project-URL: Documentation, https://www.token-cut.com
7
+ Author: Token-Cut
8
+ License: MIT
9
+ Keywords: claude,copilot,cursor,llm,optimization,token-cut
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Environment :: Console
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Requires-Python: >=3.10
18
+ Requires-Dist: httpx>=0.27.0
19
+ Description-Content-Type: text/markdown
20
+
21
+ # token-cut-cli
22
+
23
+ Public **client** for [Token-Cut](https://www.token-cut.com). Your proprietary API stays private; customers only get this thin CLI + MCP shim.
24
+
25
+ ## Business model
26
+
27
+ | What | Public? | Paid? |
28
+ |------|---------|-------|
29
+ | This PyPI package (`token-cut-cli`) | Yes | Free to install |
30
+ | `cc_live_…` API key | — | Yes (your Stripe plans) |
31
+ | Optimization + Graphify on **your** API | Private server | Metered per key |
32
+
33
+ The CLI cannot optimize without a valid key against **your** deployment.
34
+
35
+ ## Install
36
+
37
+ ```bash
38
+ pip install token-cut-cli
39
+ ```
40
+
41
+ Not on PyPI yet? Maintainer runs `./scripts/publish-cli.sh` from the repo root.
42
+ Until then:
43
+
44
+ ```bash
45
+ cd cli && pip install .
46
+ ```
47
+
48
+ ## Verify integration (~1 min)
49
+
50
+ ```bash
51
+ export TOKEN_CUT_API_KEY=cc_live_your_key
52
+ token-cut verify
53
+ ```
54
+
55
+ Calls `https://www.token-cut.com` (override with `TOKEN_CUT_API_URL`).
56
+
57
+ ## IDE setup (Cursor · Claude · Copilot)
58
+
59
+ ```bash
60
+ export TOKEN_CUT_API_KEY=cc_live_your_key
61
+ token-cut mcp-config > ~/.cursor/mcp.json
62
+ # merge if file already exists — restart IDE
63
+ ```
64
+
65
+ Uses `python -m token_cut_cli.mcp_server` from the installed package (no git clone).
66
+
67
+ ## Commands
68
+
69
+ | Command | Purpose |
70
+ |---------|---------|
71
+ | `token-cut verify` | Test key + optimize + graph on a public sample repo |
72
+ | `token-cut optimize "…"` | One-off optimize JSON |
73
+ | `token-cut mcp-config` | Print MCP config for IDEs |
74
+ | `token-cut-mcp` | MCP stdio server (called by IDE) |
75
+
76
+ ## Publish (maintainers)
77
+
78
+ See [../docs/PYPI_PUBLISH.md](../docs/PYPI_PUBLISH.md).
@@ -0,0 +1,10 @@
1
+ token_cut_cli/__init__.py,sha256=QTYqXqSTHFRkM9TEgpDFcHvwLbvqHDqvqfQ9EiXkcAM,23
2
+ token_cut_cli/__main__.py,sha256=ZOTIMJIKCHdgAEnQD1IdjHB21MrU-CqL489m2CDbrYQ,97
3
+ token_cut_cli/client.py,sha256=vnEHJbQ4SOSn4S4vGwpTPljqyjvhCFol7xLCo1d5iH8,2392
4
+ token_cut_cli/main.py,sha256=Mq8S4Iz3vDZOEkyKvGU-aIlh0RAU-tSjvLhmCgV5pE0,3617
5
+ token_cut_cli/mcp_server.py,sha256=HUWb5Qabh_LF29l-RUJ0Kf8LHX7S0Pd-9ALFejCtHCs,5711
6
+ token_cut_cli/verify.py,sha256=fEigiSPnhRaCIwGhGmuEemHAwL-O6EoWkSKaqX-y8-Q,2785
7
+ token_cut_cli-0.1.0.dist-info/METADATA,sha256=P_wv-2M1T84i6-K0gp28qqZEPsB-nHYz76MTMal9_-g,2277
8
+ token_cut_cli-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
9
+ token_cut_cli-0.1.0.dist-info/entry_points.txt,sha256=JfFOeccsIr36Vz57-Z2ZQDJXAkvZl12iMsCFfnvw5lw,100
10
+ token_cut_cli-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ token-cut = token_cut_cli.main:main
3
+ token-cut-mcp = token_cut_cli.mcp_server:main