token-cut-cli 0.1.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.
- token_cut_cli-0.1.0/.gitignore +17 -0
- token_cut_cli-0.1.0/PKG-INFO +78 -0
- token_cut_cli-0.1.0/README.md +58 -0
- token_cut_cli-0.1.0/pyproject.toml +37 -0
- token_cut_cli-0.1.0/src/token_cut_cli/__init__.py +1 -0
- token_cut_cli-0.1.0/src/token_cut_cli/__main__.py +4 -0
- token_cut_cli-0.1.0/src/token_cut_cli/client.py +82 -0
- token_cut_cli-0.1.0/src/token_cut_cli/main.py +116 -0
- token_cut_cli-0.1.0/src/token_cut_cli/mcp_server.py +170 -0
- token_cut_cli-0.1.0/src/token_cut_cli/verify.py +93 -0
|
@@ -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,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).
|
|
@@ -0,0 +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.0"
|
|
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.0"
|
|
@@ -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()
|
|
@@ -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
|
+
)
|