token-cut-cli 0.1.0__tar.gz → 0.1.2__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.
- {token_cut_cli-0.1.0 → token_cut_cli-0.1.2}/.gitignore +1 -0
- {token_cut_cli-0.1.0 → token_cut_cli-0.1.2}/PKG-INFO +1 -1
- {token_cut_cli-0.1.0 → token_cut_cli-0.1.2}/README.md +58 -58
- {token_cut_cli-0.1.0 → token_cut_cli-0.1.2}/pyproject.toml +37 -37
- token_cut_cli-0.1.2/src/token_cut_cli/__init__.py +1 -0
- {token_cut_cli-0.1.0 → token_cut_cli-0.1.2}/src/token_cut_cli/__main__.py +4 -4
- {token_cut_cli-0.1.0 → token_cut_cli-0.1.2}/src/token_cut_cli/client.py +132 -82
- {token_cut_cli-0.1.0 → token_cut_cli-0.1.2}/src/token_cut_cli/main.py +123 -116
- {token_cut_cli-0.1.0 → token_cut_cli-0.1.2}/src/token_cut_cli/mcp_server.py +170 -170
- token_cut_cli-0.1.2/src/token_cut_cli/verify.py +162 -0
- token_cut_cli-0.1.0/src/token_cut_cli/__init__.py +0 -1
- token_cut_cli-0.1.0/src/token_cut_cli/verify.py +0 -93
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: token-cut-cli
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Summary: Verify Token-Cut API integration from the terminal (no repo clone required)
|
|
5
5
|
Project-URL: Homepage, https://www.token-cut.com
|
|
6
6
|
Project-URL: Documentation, https://www.token-cut.com
|
|
@@ -1,58 +1,58 @@
|
|
|
1
|
-
# token-cut-cli
|
|
2
|
-
|
|
3
|
-
Public **client** for [Token-Cut](https://www.token-cut.com). Your proprietary API stays private; customers only get this thin CLI + MCP shim.
|
|
4
|
-
|
|
5
|
-
## Business model
|
|
6
|
-
|
|
7
|
-
| What | Public? | Paid? |
|
|
8
|
-
|------|---------|-------|
|
|
9
|
-
| This PyPI package (`token-cut-cli`) | Yes | Free to install |
|
|
10
|
-
| `cc_live_…` API key | — | Yes (your Stripe plans) |
|
|
11
|
-
| Optimization + Graphify on **your** API | Private server | Metered per key |
|
|
12
|
-
|
|
13
|
-
The CLI cannot optimize without a valid key against **your** deployment.
|
|
14
|
-
|
|
15
|
-
## Install
|
|
16
|
-
|
|
17
|
-
```bash
|
|
18
|
-
pip install token-cut-cli
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
Not on PyPI yet? Maintainer runs `./scripts/publish-cli.sh` from the repo root.
|
|
22
|
-
Until then:
|
|
23
|
-
|
|
24
|
-
```bash
|
|
25
|
-
cd cli && pip install .
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
## Verify integration (~1 min)
|
|
29
|
-
|
|
30
|
-
```bash
|
|
31
|
-
export TOKEN_CUT_API_KEY=cc_live_your_key
|
|
32
|
-
token-cut verify
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
Calls `https://www.token-cut.com` (override with `TOKEN_CUT_API_URL`).
|
|
36
|
-
|
|
37
|
-
## IDE setup (Cursor · Claude · Copilot)
|
|
38
|
-
|
|
39
|
-
```bash
|
|
40
|
-
export TOKEN_CUT_API_KEY=cc_live_your_key
|
|
41
|
-
token-cut mcp-config > ~/.cursor/mcp.json
|
|
42
|
-
# merge if file already exists — restart IDE
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
Uses `python -m token_cut_cli.mcp_server` from the installed package (no git clone).
|
|
46
|
-
|
|
47
|
-
## Commands
|
|
48
|
-
|
|
49
|
-
| Command | Purpose |
|
|
50
|
-
|---------|---------|
|
|
51
|
-
| `token-cut verify` | Test key + optimize + graph on a public sample repo |
|
|
52
|
-
| `token-cut optimize "…"` | One-off optimize JSON |
|
|
53
|
-
| `token-cut mcp-config` | Print MCP config for IDEs |
|
|
54
|
-
| `token-cut-mcp` | MCP stdio server (called by IDE) |
|
|
55
|
-
|
|
56
|
-
## Publish (maintainers)
|
|
57
|
-
|
|
58
|
-
See [../docs/PYPI_PUBLISH.md](../docs/PYPI_PUBLISH.md).
|
|
1
|
+
# token-cut-cli
|
|
2
|
+
|
|
3
|
+
Public **client** for [Token-Cut](https://www.token-cut.com). Your proprietary API stays private; customers only get this thin CLI + MCP shim.
|
|
4
|
+
|
|
5
|
+
## Business model
|
|
6
|
+
|
|
7
|
+
| What | Public? | Paid? |
|
|
8
|
+
|------|---------|-------|
|
|
9
|
+
| This PyPI package (`token-cut-cli`) | Yes | Free to install |
|
|
10
|
+
| `cc_live_…` API key | — | Yes (your Stripe plans) |
|
|
11
|
+
| Optimization + Graphify on **your** API | Private server | Metered per key |
|
|
12
|
+
|
|
13
|
+
The CLI cannot optimize without a valid key against **your** deployment.
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pip install token-cut-cli
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Not on PyPI yet? Maintainer runs `./scripts/publish-cli.sh` from the repo root.
|
|
22
|
+
Until then:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
cd cli && pip install .
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Verify integration (~1 min)
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
export TOKEN_CUT_API_KEY=cc_live_your_key
|
|
32
|
+
token-cut verify
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Calls `https://www.token-cut.com` (override with `TOKEN_CUT_API_URL`).
|
|
36
|
+
|
|
37
|
+
## IDE setup (Cursor · Claude · Copilot)
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
export TOKEN_CUT_API_KEY=cc_live_your_key
|
|
41
|
+
token-cut mcp-config > ~/.cursor/mcp.json
|
|
42
|
+
# merge if file already exists — restart IDE
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Uses `python -m token_cut_cli.mcp_server` from the installed package (no git clone).
|
|
46
|
+
|
|
47
|
+
## Commands
|
|
48
|
+
|
|
49
|
+
| Command | Purpose |
|
|
50
|
+
|---------|---------|
|
|
51
|
+
| `token-cut verify` | Test key + optimize + graph on a public sample repo |
|
|
52
|
+
| `token-cut optimize "…"` | One-off optimize JSON |
|
|
53
|
+
| `token-cut mcp-config` | Print MCP config for IDEs |
|
|
54
|
+
| `token-cut-mcp` | MCP stdio server (called by IDE) |
|
|
55
|
+
|
|
56
|
+
## Publish (maintainers)
|
|
57
|
+
|
|
58
|
+
See [../docs/PYPI_PUBLISH.md](../docs/PYPI_PUBLISH.md).
|
|
@@ -1,37 +1,37 @@
|
|
|
1
|
-
[build-system]
|
|
2
|
-
requires = ["hatchling>=1.25.0"]
|
|
3
|
-
build-backend = "hatchling.build"
|
|
4
|
-
|
|
5
|
-
[project]
|
|
6
|
-
name = "token-cut-cli"
|
|
7
|
-
version = "0.1.
|
|
8
|
-
description = "Verify Token-Cut API integration from the terminal (no repo clone required)"
|
|
9
|
-
readme = "README.md"
|
|
10
|
-
requires-python = ">=3.10"
|
|
11
|
-
license = { text = "MIT" }
|
|
12
|
-
authors = [{ name = "Token-Cut" }]
|
|
13
|
-
keywords = ["token-cut", "cursor", "copilot", "claude", "llm", "optimization"]
|
|
14
|
-
classifiers = [
|
|
15
|
-
"Development Status :: 4 - Beta",
|
|
16
|
-
"Environment :: Console",
|
|
17
|
-
"Intended Audience :: Developers",
|
|
18
|
-
"Programming Language :: Python :: 3",
|
|
19
|
-
"Programming Language :: Python :: 3.10",
|
|
20
|
-
"Programming Language :: Python :: 3.11",
|
|
21
|
-
"Programming Language :: Python :: 3.12",
|
|
22
|
-
]
|
|
23
|
-
dependencies = ["httpx>=0.27.0"]
|
|
24
|
-
|
|
25
|
-
[project.urls]
|
|
26
|
-
Homepage = "https://www.token-cut.com"
|
|
27
|
-
Documentation = "https://www.token-cut.com"
|
|
28
|
-
|
|
29
|
-
[project.scripts]
|
|
30
|
-
token-cut = "token_cut_cli.main:main"
|
|
31
|
-
token-cut-mcp = "token_cut_cli.mcp_server:main"
|
|
32
|
-
|
|
33
|
-
[tool.hatch.build.targets.wheel]
|
|
34
|
-
packages = ["src/token_cut_cli"]
|
|
35
|
-
|
|
36
|
-
[tool.hatch.build.targets.sdist]
|
|
37
|
-
include = ["src/token_cut_cli", "README.md"]
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.25.0"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "token-cut-cli"
|
|
7
|
+
version = "0.1.2"
|
|
8
|
+
description = "Verify Token-Cut API integration from the terminal (no repo clone required)"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "Token-Cut" }]
|
|
13
|
+
keywords = ["token-cut", "cursor", "copilot", "claude", "llm", "optimization"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Environment :: Console",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3.10",
|
|
20
|
+
"Programming Language :: Python :: 3.11",
|
|
21
|
+
"Programming Language :: Python :: 3.12",
|
|
22
|
+
]
|
|
23
|
+
dependencies = ["httpx>=0.27.0"]
|
|
24
|
+
|
|
25
|
+
[project.urls]
|
|
26
|
+
Homepage = "https://www.token-cut.com"
|
|
27
|
+
Documentation = "https://www.token-cut.com"
|
|
28
|
+
|
|
29
|
+
[project.scripts]
|
|
30
|
+
token-cut = "token_cut_cli.main:main"
|
|
31
|
+
token-cut-mcp = "token_cut_cli.mcp_server:main"
|
|
32
|
+
|
|
33
|
+
[tool.hatch.build.targets.wheel]
|
|
34
|
+
packages = ["src/token_cut_cli"]
|
|
35
|
+
|
|
36
|
+
[tool.hatch.build.targets.sdist]
|
|
37
|
+
include = ["src/token_cut_cli", "README.md"]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.2"
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from token_cut_cli.main import main
|
|
2
|
-
|
|
3
|
-
if __name__ == "__main__":
|
|
4
|
-
raise SystemExit(main())
|
|
1
|
+
from token_cut_cli.main import main
|
|
2
|
+
|
|
3
|
+
if __name__ == "__main__":
|
|
4
|
+
raise SystemExit(main())
|
|
@@ -1,82 +1,132 @@
|
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
or
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
or ""
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
+
# Small public repo — fastapi/fastapi OOMs 512MB cloud pods during Graphify.
|
|
11
|
+
DEFAULT_REPO_URL = "https://github.com/pallets/itsdangerous"
|
|
12
|
+
DEFAULT_TIMEOUT = 120.0
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def api_base() -> str:
|
|
16
|
+
raw = (
|
|
17
|
+
os.getenv("TOKEN_CUT_API_URL")
|
|
18
|
+
or os.getenv("CONTEXT_CUT_API_URL")
|
|
19
|
+
or DEFAULT_API_URL
|
|
20
|
+
).strip().rstrip("/")
|
|
21
|
+
return raw
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def api_key() -> str:
|
|
25
|
+
key = (
|
|
26
|
+
os.getenv("TOKEN_CUT_API_KEY")
|
|
27
|
+
or os.getenv("CONTEXT_CUT_API_KEY")
|
|
28
|
+
or ""
|
|
29
|
+
).strip()
|
|
30
|
+
return key
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def is_valid_api_key_format(key: str) -> bool:
|
|
34
|
+
"""Full cc_live_ key from dashboard — not placeholder or prefix-only."""
|
|
35
|
+
k = key.strip()
|
|
36
|
+
if not k.startswith("cc_live_"):
|
|
37
|
+
return False
|
|
38
|
+
if k in ("cc_live_YOUR_KEY", "cc_live_…", "cc_live_..."):
|
|
39
|
+
return False
|
|
40
|
+
if "…" in k or "..." in k:
|
|
41
|
+
return False
|
|
42
|
+
# generate_api_key uses secrets.token_urlsafe(32) after prefix
|
|
43
|
+
return len(k) >= 40
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def key_hint(key: str) -> str:
|
|
47
|
+
k = key.strip()
|
|
48
|
+
if len(k) <= 20:
|
|
49
|
+
return k[:8] + "…"
|
|
50
|
+
return f"{k[:16]}… ({len(k)} chars)"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def parse_api_error(response: httpx.Response) -> str:
|
|
54
|
+
try:
|
|
55
|
+
body = response.json()
|
|
56
|
+
detail = body.get("detail")
|
|
57
|
+
if isinstance(detail, str):
|
|
58
|
+
return detail
|
|
59
|
+
if isinstance(detail, list) and detail:
|
|
60
|
+
first = detail[0]
|
|
61
|
+
if isinstance(first, dict) and "msg" in first:
|
|
62
|
+
return str(first["msg"])
|
|
63
|
+
except Exception:
|
|
64
|
+
pass
|
|
65
|
+
return response.text.strip() or f"HTTP {response.status_code}"
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class TokenCutClient:
|
|
69
|
+
def __init__(
|
|
70
|
+
self,
|
|
71
|
+
base_url: str | None = None,
|
|
72
|
+
key: str | None = None,
|
|
73
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
74
|
+
) -> None:
|
|
75
|
+
self.base = (base_url or api_base()).rstrip("/")
|
|
76
|
+
self.key = (key or api_key()).strip()
|
|
77
|
+
self.timeout = timeout
|
|
78
|
+
|
|
79
|
+
def _headers(self) -> dict[str, str]:
|
|
80
|
+
if not self.key:
|
|
81
|
+
raise ValueError("Missing API key")
|
|
82
|
+
if not is_valid_api_key_format(self.key):
|
|
83
|
+
raise ValueError(
|
|
84
|
+
"Invalid TOKEN_CUT_API_KEY — need the full cc_live_… key from "
|
|
85
|
+
"Connect & Security (Generate API key). Not the placeholder or prefix only."
|
|
86
|
+
)
|
|
87
|
+
return {"x-api-key": self.key, "Content-Type": "application/json"}
|
|
88
|
+
|
|
89
|
+
def health(self) -> dict[str, Any]:
|
|
90
|
+
with httpx.Client(timeout=30.0) as client:
|
|
91
|
+
r = client.get(f"{self.base}/health")
|
|
92
|
+
r.raise_for_status()
|
|
93
|
+
return r.json()
|
|
94
|
+
|
|
95
|
+
def optimize(
|
|
96
|
+
self,
|
|
97
|
+
prompt: str,
|
|
98
|
+
*,
|
|
99
|
+
repo_url: str | None = None,
|
|
100
|
+
repo_path: str | None = None,
|
|
101
|
+
use_graph: bool = True,
|
|
102
|
+
) -> dict[str, Any]:
|
|
103
|
+
body: dict[str, Any] = {
|
|
104
|
+
"prompt": prompt,
|
|
105
|
+
"use_graph": use_graph,
|
|
106
|
+
"auto_build_graph": True,
|
|
107
|
+
}
|
|
108
|
+
if repo_url:
|
|
109
|
+
body["repo_url"] = repo_url
|
|
110
|
+
if repo_path:
|
|
111
|
+
body["repo_path"] = repo_path
|
|
112
|
+
with httpx.Client(timeout=self.timeout) as client:
|
|
113
|
+
r = client.post(
|
|
114
|
+
f"{self.base}/v1/optimize",
|
|
115
|
+
headers=self._headers(),
|
|
116
|
+
json=body,
|
|
117
|
+
)
|
|
118
|
+
if r.status_code == 401:
|
|
119
|
+
detail = parse_api_error(r)
|
|
120
|
+
raise PermissionError(f"API key rejected (401): {detail}")
|
|
121
|
+
if r.status_code == 429:
|
|
122
|
+
detail = parse_api_error(r)
|
|
123
|
+
raise PermissionError(f"Plan limit reached (429): {detail}")
|
|
124
|
+
if r.status_code in (502, 503, 504):
|
|
125
|
+
detail = parse_api_error(r)
|
|
126
|
+
raise RuntimeError(
|
|
127
|
+
f"Server overloaded or timed out ({r.status_code}): {detail}. "
|
|
128
|
+
"Cloud graph builds run in the background — retry in 1–2 min, "
|
|
129
|
+
"or use: token-cut verify --no-graph"
|
|
130
|
+
)
|
|
131
|
+
r.raise_for_status()
|
|
132
|
+
return r.json()
|
|
@@ -1,116 +1,123 @@
|
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
o.
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
)
|
|
57
|
-
|
|
58
|
-
m.
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
)
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
args.
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
)
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
+
v.add_argument(
|
|
46
|
+
"--no-graph",
|
|
47
|
+
action="store_true",
|
|
48
|
+
help="Only check API key + optimize (skip Graphify; safe on small Railway pods)",
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
o = sub.add_parser("optimize", help="Run one optimize call and print JSON")
|
|
52
|
+
o.add_argument("prompt", help="Prompt text")
|
|
53
|
+
o.add_argument("--api-url", default=None)
|
|
54
|
+
o.add_argument("--key", default=None)
|
|
55
|
+
o.add_argument("--repo-url", default=None)
|
|
56
|
+
o.add_argument("--repo-path", default=None)
|
|
57
|
+
|
|
58
|
+
m = sub.add_parser(
|
|
59
|
+
"mcp-config",
|
|
60
|
+
help="Print MCP JSON for Cursor / Claude (uses pip package, no git clone)",
|
|
61
|
+
)
|
|
62
|
+
m.add_argument("--api-url", default=None)
|
|
63
|
+
m.add_argument("--key", default=None)
|
|
64
|
+
m.add_argument("--repo-path", default=None, help="Default TOKEN_CUT_REPO_PATH in env")
|
|
65
|
+
|
|
66
|
+
args = parser.parse_args(argv)
|
|
67
|
+
|
|
68
|
+
try:
|
|
69
|
+
client = TokenCutClient(base_url=args.api_url, key=args.key)
|
|
70
|
+
except ValueError as e:
|
|
71
|
+
print(f"error: {e}", file=sys.stderr)
|
|
72
|
+
print(
|
|
73
|
+
" Set TOKEN_CUT_API_KEY or pass --key with the full key from the dashboard.",
|
|
74
|
+
file=sys.stderr,
|
|
75
|
+
)
|
|
76
|
+
return 2
|
|
77
|
+
|
|
78
|
+
if args.command == "verify":
|
|
79
|
+
return run_verify(
|
|
80
|
+
client,
|
|
81
|
+
repo_url=args.repo_url,
|
|
82
|
+
repo_path=args.repo_path,
|
|
83
|
+
quiet=args.quiet,
|
|
84
|
+
with_graph=not args.no_graph,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
if args.command == "optimize":
|
|
88
|
+
data = client.optimize(
|
|
89
|
+
args.prompt,
|
|
90
|
+
repo_url=args.repo_url,
|
|
91
|
+
repo_path=args.repo_path,
|
|
92
|
+
)
|
|
93
|
+
print(json.dumps(data, indent=2))
|
|
94
|
+
return 0
|
|
95
|
+
|
|
96
|
+
if args.command == "mcp-config":
|
|
97
|
+
env: dict[str, str] = {
|
|
98
|
+
"TOKEN_CUT_API_URL": client.base,
|
|
99
|
+
"TOKEN_CUT_API_KEY": client.key,
|
|
100
|
+
}
|
|
101
|
+
if getattr(args, "repo_path", None):
|
|
102
|
+
env["TOKEN_CUT_REPO_PATH"] = args.repo_path
|
|
103
|
+
cfg = {
|
|
104
|
+
"mcpServers": {
|
|
105
|
+
"token-cut": {
|
|
106
|
+
"command": sys.executable,
|
|
107
|
+
"args": ["-m", "token_cut_cli.mcp_server"],
|
|
108
|
+
"env": env,
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
print(json.dumps(cfg, indent=2))
|
|
113
|
+
print(
|
|
114
|
+
"\n# Paste into ~/.cursor/mcp.json or Claude Desktop config, then restart IDE.",
|
|
115
|
+
file=sys.stderr,
|
|
116
|
+
)
|
|
117
|
+
return 0
|
|
118
|
+
|
|
119
|
+
return 1
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
if __name__ == "__main__":
|
|
123
|
+
sys.exit(main())
|
|
@@ -1,170 +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()
|
|
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,162 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
import time
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from token_cut_cli.client import (
|
|
8
|
+
DEFAULT_REPO_URL,
|
|
9
|
+
TokenCutClient,
|
|
10
|
+
is_valid_api_key_format,
|
|
11
|
+
key_hint,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
AUTH_PROMPT = "Token-Cut verify: confirm API key and run prune/compress on this line."
|
|
15
|
+
|
|
16
|
+
GRAPH_PROMPT = (
|
|
17
|
+
"List the main modules in this repository and how they connect. "
|
|
18
|
+
"Focus on entrypoints only."
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _ok(msg: str) -> None:
|
|
23
|
+
print(f" ✓ {msg}")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _fail(msg: str) -> None:
|
|
27
|
+
print(f" ✗ {msg}", file=sys.stderr)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def run_verify(
|
|
31
|
+
client: TokenCutClient,
|
|
32
|
+
*,
|
|
33
|
+
repo_url: str = DEFAULT_REPO_URL,
|
|
34
|
+
repo_path: str | None = None,
|
|
35
|
+
quiet: bool = False,
|
|
36
|
+
with_graph: bool = True,
|
|
37
|
+
) -> int:
|
|
38
|
+
if not quiet:
|
|
39
|
+
print(f"Token-Cut verify → {client.base}")
|
|
40
|
+
print(f" repo: {repo_path or repo_url}")
|
|
41
|
+
print(f" key: {key_hint(client.key)}")
|
|
42
|
+
print()
|
|
43
|
+
|
|
44
|
+
if not is_valid_api_key_format(client.key):
|
|
45
|
+
_fail(
|
|
46
|
+
"TOKEN_CUT_API_KEY is missing or not a full cc_live_… key "
|
|
47
|
+
"(dashboard shows only a prefix after signup — generate a new key)"
|
|
48
|
+
)
|
|
49
|
+
print(
|
|
50
|
+
"\n1. https://www.token-cut.com → Connect & Security → Log in\n"
|
|
51
|
+
"2. Click “Generate API key” and copy the full key (shown once)\n"
|
|
52
|
+
"3. export TOKEN_CUT_API_KEY='cc_live_…'\n"
|
|
53
|
+
"4. token-cut verify",
|
|
54
|
+
file=sys.stderr,
|
|
55
|
+
)
|
|
56
|
+
return 2
|
|
57
|
+
|
|
58
|
+
try:
|
|
59
|
+
health = client.health()
|
|
60
|
+
version = health.get("version", "?")
|
|
61
|
+
_ok(f"API reachable (version {version})")
|
|
62
|
+
except Exception as e:
|
|
63
|
+
if not quiet:
|
|
64
|
+
print(f" … health check skipped ({e})")
|
|
65
|
+
|
|
66
|
+
# Phase 1: auth + optimize without graph (fast, no clone on cloud)
|
|
67
|
+
try:
|
|
68
|
+
auth_data = client.optimize(
|
|
69
|
+
AUTH_PROMPT,
|
|
70
|
+
repo_url=None,
|
|
71
|
+
repo_path=None,
|
|
72
|
+
use_graph=False,
|
|
73
|
+
)
|
|
74
|
+
except PermissionError as e:
|
|
75
|
+
_fail(str(e))
|
|
76
|
+
print(
|
|
77
|
+
"\nFix: https://www.token-cut.com → Connect & Security → Generate API key\n"
|
|
78
|
+
" export TOKEN_CUT_API_KEY='cc_live_…' (full key, not prefix or JWT)",
|
|
79
|
+
file=sys.stderr,
|
|
80
|
+
)
|
|
81
|
+
return 2
|
|
82
|
+
except Exception as e:
|
|
83
|
+
_fail(f"Auth/optimize check failed: {e}")
|
|
84
|
+
return 3
|
|
85
|
+
|
|
86
|
+
_ok("API key accepted")
|
|
87
|
+
o1 = int(auth_data.get("original_tokens") or 0)
|
|
88
|
+
o2 = int(auth_data.get("optimized_tokens") or 0)
|
|
89
|
+
_ok(f"Optimize (no graph): {o1} → {o2} tokens")
|
|
90
|
+
|
|
91
|
+
if not with_graph:
|
|
92
|
+
if not quiet:
|
|
93
|
+
print("\n Skipped graph test (--no-graph). For full Graphify: token-cut verify")
|
|
94
|
+
return 0
|
|
95
|
+
|
|
96
|
+
# Phase 2: graph path (cloud may build in background)
|
|
97
|
+
try:
|
|
98
|
+
data = client.optimize(
|
|
99
|
+
GRAPH_PROMPT,
|
|
100
|
+
repo_url=repo_url if not repo_path else None,
|
|
101
|
+
repo_path=repo_path,
|
|
102
|
+
use_graph=True,
|
|
103
|
+
)
|
|
104
|
+
except Exception as e:
|
|
105
|
+
_fail(f"Graph optimize failed: {e}")
|
|
106
|
+
print(
|
|
107
|
+
"\n Production builds graphs in the background to avoid OOM.\n"
|
|
108
|
+
" Wait 1–2 minutes and run token-cut verify again,\n"
|
|
109
|
+
" or: token-cut verify --no-graph",
|
|
110
|
+
file=sys.stderr,
|
|
111
|
+
)
|
|
112
|
+
return 4
|
|
113
|
+
|
|
114
|
+
graph_ready = bool(data.get("graph_ready"))
|
|
115
|
+
graph_built = bool(data.get("graph_built"))
|
|
116
|
+
nodes = int(data.get("graph_nodes") or 0)
|
|
117
|
+
msg = str(data.get("graph_message") or data.get("message") or "")
|
|
118
|
+
|
|
119
|
+
if graph_ready:
|
|
120
|
+
cache = "built now" if graph_built else "cached"
|
|
121
|
+
_ok(f"Graphify: {nodes:,} nodes ({cache})")
|
|
122
|
+
elif "background" in msg.lower():
|
|
123
|
+
_ok("Graphify queued on server (background — avoids OOM)")
|
|
124
|
+
if not quiet:
|
|
125
|
+
print(" … waiting 90s then retrying once …")
|
|
126
|
+
time.sleep(90)
|
|
127
|
+
try:
|
|
128
|
+
retry = client.optimize(
|
|
129
|
+
GRAPH_PROMPT,
|
|
130
|
+
repo_url=repo_url if not repo_path else None,
|
|
131
|
+
repo_path=repo_path,
|
|
132
|
+
use_graph=True,
|
|
133
|
+
)
|
|
134
|
+
if retry.get("graph_ready"):
|
|
135
|
+
nodes = int(retry.get("graph_nodes") or 0)
|
|
136
|
+
_ok(f"Graphify ready after background build: {nodes:,} nodes")
|
|
137
|
+
graph_ready = True
|
|
138
|
+
except Exception:
|
|
139
|
+
pass
|
|
140
|
+
if not graph_ready:
|
|
141
|
+
print(
|
|
142
|
+
"\n Graph still building. Re-run: token-cut verify (in a few minutes)",
|
|
143
|
+
file=sys.stderr,
|
|
144
|
+
)
|
|
145
|
+
return 0
|
|
146
|
+
else:
|
|
147
|
+
_fail(f"Graph not ready — {msg or 'check repo URL'}")
|
|
148
|
+
return 4
|
|
149
|
+
|
|
150
|
+
if not quiet:
|
|
151
|
+
print()
|
|
152
|
+
print(" Next: token-cut mcp-config → paste into Cursor / Claude MCP settings")
|
|
153
|
+
print(" Your IDE subscription still runs the LLM.")
|
|
154
|
+
|
|
155
|
+
return 0
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def format_result_summary(data: dict[str, Any]) -> str:
|
|
159
|
+
return (
|
|
160
|
+
f"tokens={data.get('original_tokens')}→{data.get('optimized_tokens')} "
|
|
161
|
+
f"graph_nodes={data.get('graph_nodes')}"
|
|
162
|
+
)
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.1.0"
|
|
@@ -1,93 +0,0 @@
|
|
|
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
|
-
)
|