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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: token-cut-cli
3
- Version: 0.1.5
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 TOKEN_CUT_API_KEY=cc_live_your_key
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 TOKEN_CUT_API_KEY=cc_live_your_key
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 TOKEN_CUT_API_KEY=cc_live_your_key
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 TOKEN_CUT_API_KEY=cc_live_your_key
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
  ```
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "token-cut-cli"
7
- version = "0.1.5"
7
+ version = "0.1.7"
8
8
  description = "Verify Token-Cut API integration from the terminal (no repo clone required)"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -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 — shipped in token-cut-cli (calls your cloud API with cc_live_ key)."""
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 DEFAULT_API_URL, api_base, api_key
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 _post(path: str, body: dict) -> dict:
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={"Content-Type": "application/json", "x-api-key": key},
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 (Token-Cut cloud). Auto-builds graph.json when missing. "
43
- "Requires cc_live_ API key. IDE still runs the LLM on your plan."
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": {"type": "string"},
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": args.get("repo_path") or _env_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": args.get("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