token-cut-cli 0.1.5__tar.gz → 0.1.7__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.5 → token_cut_cli-0.1.7}/PKG-INFO +10 -3
- {token_cut_cli-0.1.5 → token_cut_cli-0.1.7}/README.md +9 -2
- {token_cut_cli-0.1.5 → token_cut_cli-0.1.7}/pyproject.toml +1 -1
- token_cut_cli-0.1.7/src/token_cut_cli/__init__.py +1 -0
- {token_cut_cli-0.1.5 → token_cut_cli-0.1.7}/src/token_cut_cli/client.py +7 -0
- {token_cut_cli-0.1.5 → token_cut_cli-0.1.7}/src/token_cut_cli/mcp_server.py +32 -13
- {token_cut_cli-0.1.5 → token_cut_cli-0.1.7}/src/token_cut_cli/verify.py +37 -0
- token_cut_cli-0.1.7/src/token_cut_cli/workspace.py +96 -0
- token_cut_cli-0.1.5/src/token_cut_cli/__init__.py +0 -1
- {token_cut_cli-0.1.5 → token_cut_cli-0.1.7}/.gitignore +0 -0
- {token_cut_cli-0.1.5 → token_cut_cli-0.1.7}/src/token_cut_cli/__main__.py +0 -0
- {token_cut_cli-0.1.5 → token_cut_cli-0.1.7}/src/token_cut_cli/main.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: token-cut-cli
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.7
|
|
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
|
|
@@ -48,16 +48,23 @@ cd cli && pip install .
|
|
|
48
48
|
## Verify integration (~1 min)
|
|
49
49
|
|
|
50
50
|
```bash
|
|
51
|
-
export
|
|
51
|
+
export TOKEN_CUT_API_URL='https://www.token-cut.com'
|
|
52
|
+
export TOKEN_CUT_API_KEY='cc_live_…' # full key from Connect — not the prefix
|
|
52
53
|
token-cut verify
|
|
53
54
|
```
|
|
54
55
|
|
|
56
|
+
Shows token count and **estimated cost** before vs after for:
|
|
57
|
+
|
|
58
|
+
1. A short auth/optimize check (no repo graph)
|
|
59
|
+
2. A repo-graph optimize on a small public sample repo
|
|
60
|
+
|
|
55
61
|
Calls `https://www.token-cut.com` (override with `TOKEN_CUT_API_URL`).
|
|
56
62
|
|
|
57
63
|
## IDE setup (Cursor · Claude · Copilot)
|
|
58
64
|
|
|
59
65
|
```bash
|
|
60
|
-
export
|
|
66
|
+
export TOKEN_CUT_API_URL='https://www.token-cut.com'
|
|
67
|
+
export TOKEN_CUT_API_KEY='cc_live_…'
|
|
61
68
|
token-cut mcp-config > ~/.cursor/mcp.json
|
|
62
69
|
# merge if file already exists — restart IDE
|
|
63
70
|
```
|
|
@@ -28,16 +28,23 @@ cd cli && pip install .
|
|
|
28
28
|
## Verify integration (~1 min)
|
|
29
29
|
|
|
30
30
|
```bash
|
|
31
|
-
export
|
|
31
|
+
export TOKEN_CUT_API_URL='https://www.token-cut.com'
|
|
32
|
+
export TOKEN_CUT_API_KEY='cc_live_…' # full key from Connect — not the prefix
|
|
32
33
|
token-cut verify
|
|
33
34
|
```
|
|
34
35
|
|
|
36
|
+
Shows token count and **estimated cost** before vs after for:
|
|
37
|
+
|
|
38
|
+
1. A short auth/optimize check (no repo graph)
|
|
39
|
+
2. A repo-graph optimize on a small public sample repo
|
|
40
|
+
|
|
35
41
|
Calls `https://www.token-cut.com` (override with `TOKEN_CUT_API_URL`).
|
|
36
42
|
|
|
37
43
|
## IDE setup (Cursor · Claude · Copilot)
|
|
38
44
|
|
|
39
45
|
```bash
|
|
40
|
-
export
|
|
46
|
+
export TOKEN_CUT_API_URL='https://www.token-cut.com'
|
|
47
|
+
export TOKEN_CUT_API_KEY='cc_live_…'
|
|
41
48
|
token-cut mcp-config > ~/.cursor/mcp.json
|
|
42
49
|
# merge if file already exists — restart IDE
|
|
43
50
|
```
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.7"
|
|
@@ -118,6 +118,13 @@ class TokenCutClient:
|
|
|
118
118
|
if r.status_code == 401:
|
|
119
119
|
detail = parse_api_error(r)
|
|
120
120
|
raise PermissionError(f"API key rejected (401): {detail}")
|
|
121
|
+
if r.status_code == 403:
|
|
122
|
+
detail = parse_api_error(r)
|
|
123
|
+
raise PermissionError(
|
|
124
|
+
f"Access denied (403): {detail}. "
|
|
125
|
+
"Confirm your email from the signup message, or sign in at "
|
|
126
|
+
"https://www.token-cut.com/connect and resend verification."
|
|
127
|
+
)
|
|
121
128
|
if r.status_code == 429:
|
|
122
129
|
detail = parse_api_error(r)
|
|
123
130
|
raise PermissionError(f"Plan limit reached (429): {detail}")
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Token-Cut MCP server —
|
|
1
|
+
"""Token-Cut MCP server — any MCP host (Cursor, Claude, Copilot, …)."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
@@ -7,10 +7,22 @@ import os
|
|
|
7
7
|
import sys
|
|
8
8
|
|
|
9
9
|
from token_cut_cli import __version__
|
|
10
|
-
from token_cut_cli.client import
|
|
10
|
+
from token_cut_cli.client import api_base, api_key
|
|
11
|
+
from token_cut_cli.workspace import detect_workspace, workspace_forward_headers
|
|
11
12
|
|
|
12
13
|
|
|
13
|
-
def
|
|
14
|
+
def _resolve_repo(args: dict) -> str | None:
|
|
15
|
+
for val in (
|
|
16
|
+
args.get("repo_path"),
|
|
17
|
+
os.getenv("TOKEN_CUT_REPO_PATH") or os.getenv("CONTEXT_CUT_REPO_PATH"),
|
|
18
|
+
detect_workspace(),
|
|
19
|
+
):
|
|
20
|
+
if val and str(val).strip():
|
|
21
|
+
return str(val).strip()
|
|
22
|
+
return None
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _post(path: str, body: dict, *, repo: str | None = None) -> dict:
|
|
14
26
|
import urllib.request
|
|
15
27
|
|
|
16
28
|
base = api_base().rstrip("/")
|
|
@@ -18,10 +30,14 @@ def _post(path: str, body: dict) -> dict:
|
|
|
18
30
|
if not key:
|
|
19
31
|
raise RuntimeError("Set TOKEN_CUT_API_KEY=cc_live_…")
|
|
20
32
|
|
|
33
|
+
headers = {"Content-Type": "application/json", "x-api-key": key}
|
|
34
|
+
if repo:
|
|
35
|
+
headers.update(workspace_forward_headers(repo))
|
|
36
|
+
|
|
21
37
|
req = urllib.request.Request(
|
|
22
38
|
f"{base}{path}",
|
|
23
39
|
data=json.dumps(body).encode(),
|
|
24
|
-
headers=
|
|
40
|
+
headers=headers,
|
|
25
41
|
method="POST",
|
|
26
42
|
)
|
|
27
43
|
timeout = int(os.getenv("TOKEN_CUT_HTTP_TIMEOUT", "360"))
|
|
@@ -39,14 +55,17 @@ def _tool_list() -> dict:
|
|
|
39
55
|
{
|
|
40
56
|
"name": "optimize_prompt",
|
|
41
57
|
"description": (
|
|
42
|
-
"Optimize a prompt
|
|
43
|
-
"Requires cc_live_ API key. IDE still runs the LLM
|
|
58
|
+
"Optimize a prompt. Open workspace auto-detected from the host IDE. "
|
|
59
|
+
"Requires cc_live_ API key (or local API URL). IDE still runs the LLM."
|
|
44
60
|
),
|
|
45
61
|
"inputSchema": {
|
|
46
62
|
"type": "object",
|
|
47
63
|
"properties": {
|
|
48
64
|
"prompt": {"type": "string"},
|
|
49
|
-
"repo_path": {
|
|
65
|
+
"repo_path": {
|
|
66
|
+
"type": "string",
|
|
67
|
+
"description": "Optional override; default is IDE open folder.",
|
|
68
|
+
},
|
|
50
69
|
"repo_url": {"type": "string"},
|
|
51
70
|
"auto_build_graph": {"type": "boolean"},
|
|
52
71
|
},
|
|
@@ -68,10 +87,6 @@ def _tool_list() -> dict:
|
|
|
68
87
|
}
|
|
69
88
|
|
|
70
89
|
|
|
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
90
|
def _env_repo_url() -> str | None:
|
|
76
91
|
return os.getenv("TOKEN_CUT_REPO_URL") or os.getenv("CONTEXT_CUT_REPO_URL")
|
|
77
92
|
|
|
@@ -82,16 +97,18 @@ def _handle_tools_call(msg: dict) -> None:
|
|
|
82
97
|
args = params.get("arguments", {})
|
|
83
98
|
try:
|
|
84
99
|
if name == "optimize_prompt":
|
|
100
|
+
repo = _resolve_repo(args)
|
|
85
101
|
data = _post(
|
|
86
102
|
"/v1/optimize",
|
|
87
103
|
{
|
|
88
104
|
"prompt": args["prompt"],
|
|
89
|
-
"repo_path":
|
|
105
|
+
"repo_path": repo,
|
|
90
106
|
"repo_url": args.get("repo_url") or _env_repo_url(),
|
|
91
107
|
"model": "gpt-4o",
|
|
92
108
|
"use_graph": True,
|
|
93
109
|
"auto_build_graph": args.get("auto_build_graph", True),
|
|
94
110
|
},
|
|
111
|
+
repo=repo,
|
|
95
112
|
)
|
|
96
113
|
graph_line = ""
|
|
97
114
|
if data.get("graph_ready"):
|
|
@@ -105,12 +122,14 @@ def _handle_tools_call(msg: dict) -> None:
|
|
|
105
122
|
f"{data['optimized_prompt'][:8000]}"
|
|
106
123
|
)
|
|
107
124
|
elif name == "build_repo_graph":
|
|
125
|
+
repo = _resolve_repo(args)
|
|
108
126
|
data = _post(
|
|
109
127
|
"/v1/graph/build",
|
|
110
128
|
{
|
|
111
|
-
"repo_path":
|
|
129
|
+
"repo_path": repo,
|
|
112
130
|
"repo_url": args.get("repo_url"),
|
|
113
131
|
},
|
|
132
|
+
repo=repo,
|
|
114
133
|
)
|
|
115
134
|
text = json.dumps(data, indent=2)
|
|
116
135
|
else:
|
|
@@ -27,6 +27,34 @@ def _fail(msg: str) -> None:
|
|
|
27
27
|
print(f" ✗ {msg}", file=sys.stderr)
|
|
28
28
|
|
|
29
29
|
|
|
30
|
+
def _pct_saved(original: int, optimized: int) -> float:
|
|
31
|
+
if original <= 0:
|
|
32
|
+
return 0.0
|
|
33
|
+
return max(0.0, (1 - optimized / original) * 100.0)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _print_savings(label: str, data: dict[str, Any], *, quiet: bool) -> None:
|
|
37
|
+
if quiet:
|
|
38
|
+
return
|
|
39
|
+
original = int(data.get("original_tokens") or 0)
|
|
40
|
+
optimized = int(data.get("optimized_tokens") or 0)
|
|
41
|
+
saved_tokens = max(0, original - optimized)
|
|
42
|
+
pct = _pct_saved(original, optimized)
|
|
43
|
+
before = float(data.get("estimated_cost_before_usd") or 0)
|
|
44
|
+
after = float(data.get("estimated_cost_after_usd") or 0)
|
|
45
|
+
cost_saved = float(
|
|
46
|
+
data.get("cost_saved_usd") or max(0.0, before - after)
|
|
47
|
+
)
|
|
48
|
+
factor = float(data.get("reduction_factor") or 1.0)
|
|
49
|
+
if original > 0 and optimized > 0 and factor <= 1.0:
|
|
50
|
+
factor = original / optimized
|
|
51
|
+
|
|
52
|
+
print(f"\n ── {label} ──")
|
|
53
|
+
print(f" Tokens: {original:,} → {optimized:,} ({saved_tokens:,} saved, {pct:.1f}% less)")
|
|
54
|
+
print(f" Factor: {factor:.2f}× smaller payload")
|
|
55
|
+
print(f" Est cost: ${before:.6f} → ${after:.6f} (${cost_saved:.6f} saved on this call)")
|
|
56
|
+
|
|
57
|
+
|
|
30
58
|
def run_verify(
|
|
31
59
|
client: TokenCutClient,
|
|
32
60
|
*,
|
|
@@ -84,6 +112,7 @@ def run_verify(
|
|
|
84
112
|
return 3
|
|
85
113
|
|
|
86
114
|
_ok("API key accepted")
|
|
115
|
+
_print_savings("Optimize (no repo graph)", auth_data, quiet=quiet)
|
|
87
116
|
o1 = int(auth_data.get("original_tokens") or 0)
|
|
88
117
|
o2 = int(auth_data.get("optimized_tokens") or 0)
|
|
89
118
|
_ok(f"Optimize (no graph): {o1} → {o2} tokens")
|
|
@@ -119,6 +148,14 @@ def run_verify(
|
|
|
119
148
|
if graph_ready:
|
|
120
149
|
cache = "built now" if graph_built else "cached"
|
|
121
150
|
_ok(f"Repo graph: {nodes:,} nodes ({cache})")
|
|
151
|
+
_print_savings("Optimize (with repo graph)", data, quiet=quiet)
|
|
152
|
+
if not quiet and o1 > 0 and o2 > 0:
|
|
153
|
+
g_orig = int(data.get("original_tokens") or 0)
|
|
154
|
+
g_opt = int(data.get("optimized_tokens") or 0)
|
|
155
|
+
print(
|
|
156
|
+
f"\n Compare: graph run used {g_orig:,} input tokens vs {o1:,} without graph "
|
|
157
|
+
f"({max(0, g_orig - o1):,} more context injected from repo index)."
|
|
158
|
+
)
|
|
122
159
|
elif "background" in msg.lower():
|
|
123
160
|
_ok("Repo index queued on server (background — avoids OOM)")
|
|
124
161
|
if not quiet:
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""IDE-agnostic open-folder detection for MCP (pip-installed CLI)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
# Keep in sync with api/app/workspace.py IDE_WORKSPACE_ENV_KEYS
|
|
9
|
+
IDE_WORKSPACE_ENV_KEYS = (
|
|
10
|
+
"CURSOR_WORKSPACE_FOLDER",
|
|
11
|
+
"CURSOR_PROJECT_DIR",
|
|
12
|
+
"CURSOR_WORKSPACE",
|
|
13
|
+
"VSCODE_CWD",
|
|
14
|
+
"VSCODE_WORKSPACE_FOLDER",
|
|
15
|
+
"VSCODE_WORKSPACE",
|
|
16
|
+
"CODE_WORKSPACE_FOLDER",
|
|
17
|
+
"WINDSURF_WORKSPACE_FOLDER",
|
|
18
|
+
"CODEIUM_WORKSPACE",
|
|
19
|
+
"CLAUDE_PROJECT_ROOT",
|
|
20
|
+
"CLAUDE_CODE_PROJECT_DIR",
|
|
21
|
+
"CLAUDE_PROJECT_DIR",
|
|
22
|
+
"ZED_WORKSPACE",
|
|
23
|
+
"ZED_ROOT",
|
|
24
|
+
"IDEA_INITIAL_DIRECTORY",
|
|
25
|
+
"PROJECT_DIR",
|
|
26
|
+
"PROJECT_ROOT",
|
|
27
|
+
"WORKSPACE_FOLDER",
|
|
28
|
+
"WORKSPACE",
|
|
29
|
+
"REPO_ROOT",
|
|
30
|
+
"INIT_CWD",
|
|
31
|
+
"GITHUB_WORKSPACE",
|
|
32
|
+
"PWD",
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _find_git_root(start: Path) -> Path | None:
|
|
37
|
+
current = start.resolve()
|
|
38
|
+
for _ in range(32):
|
|
39
|
+
if (current / ".git").exists():
|
|
40
|
+
return current
|
|
41
|
+
parent = current.parent
|
|
42
|
+
if parent == current:
|
|
43
|
+
break
|
|
44
|
+
current = parent
|
|
45
|
+
return None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _looks_like_repo(path: Path) -> bool:
|
|
49
|
+
if not path.is_dir():
|
|
50
|
+
return False
|
|
51
|
+
if (path / ".git").is_dir() or (path / ".git").is_file():
|
|
52
|
+
return True
|
|
53
|
+
markers = ("package.json", "pyproject.toml", "go.mod", "Cargo.toml", "requirements.txt")
|
|
54
|
+
return any((path / m).exists() for m in markers)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def detect_workspace() -> str | None:
|
|
58
|
+
candidates: list[Path] = []
|
|
59
|
+
for key in IDE_WORKSPACE_ENV_KEYS:
|
|
60
|
+
v = os.getenv(key)
|
|
61
|
+
if v:
|
|
62
|
+
candidates.append(Path(v).expanduser())
|
|
63
|
+
env_repo = os.getenv("TOKEN_CUT_REPO_PATH") or os.getenv("CONTEXT_CUT_REPO_PATH")
|
|
64
|
+
if env_repo:
|
|
65
|
+
candidates.append(Path(env_repo).expanduser())
|
|
66
|
+
try:
|
|
67
|
+
candidates.append(Path.cwd())
|
|
68
|
+
except OSError:
|
|
69
|
+
pass
|
|
70
|
+
|
|
71
|
+
seen: set[str] = set()
|
|
72
|
+
for raw in candidates:
|
|
73
|
+
try:
|
|
74
|
+
p = raw.resolve()
|
|
75
|
+
except OSError:
|
|
76
|
+
continue
|
|
77
|
+
key = str(p)
|
|
78
|
+
if key in seen:
|
|
79
|
+
continue
|
|
80
|
+
seen.add(key)
|
|
81
|
+
root = _find_git_root(p) if p.is_dir() else None
|
|
82
|
+
if root and _looks_like_repo(root):
|
|
83
|
+
return str(root)
|
|
84
|
+
if _looks_like_repo(p):
|
|
85
|
+
return str(p)
|
|
86
|
+
return None
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def workspace_forward_headers(repo_path: str) -> dict[str, str]:
|
|
90
|
+
path = str(repo_path).strip()
|
|
91
|
+
return {
|
|
92
|
+
"x-repo-path": path,
|
|
93
|
+
"x-workspace-path": path,
|
|
94
|
+
"x-workspace-folder": path,
|
|
95
|
+
"x-project-root": path,
|
|
96
|
+
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.1.5"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|