qwen-claude 0.1.0__tar.gz → 0.1.1__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.
- {qwen_claude-0.1.0 → qwen_claude-0.1.1}/PKG-INFO +3 -3
- {qwen_claude-0.1.0 → qwen_claude-0.1.1}/pyproject.toml +2 -2
- qwen_claude-0.1.1/src/qwen_claude/cli.py +364 -0
- qwen_claude-0.1.0/src/qwen_claude/cli.py +0 -236
- {qwen_claude-0.1.0 → qwen_claude-0.1.1}/README.md +0 -0
- {qwen_claude-0.1.0 → qwen_claude-0.1.1}/src/qwen_claude/__init__.py +0 -0
- {qwen_claude-0.1.0 → qwen_claude-0.1.1}/src/qwen_claude/schema.json +0 -0
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: qwen-claude
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.1
|
|
4
4
|
Summary: Qwen + Claude Code Router bootstrapper
|
|
5
|
-
Author: Saad
|
|
6
|
-
Author-email: Saad
|
|
5
|
+
Author: Saad Kamran
|
|
6
|
+
Author-email: Saad Kamran <saadkamran6ft@gmail.com>
|
|
7
7
|
License: MIT
|
|
8
8
|
Requires-Python: >=3.13
|
|
9
9
|
Description-Content-Type: text/markdown
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "qwen-claude"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.1"
|
|
4
4
|
description = "Qwen + Claude Code Router bootstrapper"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = { text = "MIT" }
|
|
7
7
|
authors = [
|
|
8
|
-
{ name = "Saad
|
|
8
|
+
{ name = "Saad Kamran", email = "saadkamran6ft@gmail.com" }
|
|
9
9
|
]
|
|
10
10
|
requires-python = ">=3.13"
|
|
11
11
|
dependencies = []
|
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
import site
|
|
3
|
+
import sys
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import time
|
|
7
|
+
import shutil
|
|
8
|
+
import subprocess
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Optional
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# ------------------- CONFIG (EDIT IF NEEDED) -------------------
|
|
14
|
+
QWEN_OAUTH = Path(os.environ["USERPROFILE"]) / ".qwen" / "oauth_creds.json"
|
|
15
|
+
CCR_CONFIG = Path(os.environ["USERPROFILE"]) / ".claude-code-router" / "config.json"
|
|
16
|
+
SCHEMA_FILE = Path(__file__).parent / "schema.json"
|
|
17
|
+
|
|
18
|
+
REFRESH_BUFFER_SECONDS = 120
|
|
19
|
+
RESTART_CCR_ON_CHANGE = True
|
|
20
|
+
RUN_CCR_CODE = True
|
|
21
|
+
# ---------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def load_json(path: Path) -> dict:
|
|
25
|
+
with path.open("r", encoding="utf-8") as f:
|
|
26
|
+
return json.load(f)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def save_json_atomic(path: Path, data: dict) -> None:
|
|
30
|
+
tmp = path.with_suffix(path.suffix + ".tmp")
|
|
31
|
+
with tmp.open("w", encoding="utf-8") as f:
|
|
32
|
+
json.dump(data, f, indent=2)
|
|
33
|
+
tmp.replace(path)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def is_expiring_soon(oauth: dict, buffer_seconds: int) -> bool:
|
|
37
|
+
exp_ms = int(oauth.get("expiry_date", 0))
|
|
38
|
+
if exp_ms <= 0:
|
|
39
|
+
return True
|
|
40
|
+
exp_s = exp_ms / 1000.0
|
|
41
|
+
return time.time() >= (exp_s - buffer_seconds)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def which(cmd: str) -> Optional[str]:
|
|
45
|
+
return shutil.which(cmd)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
# ------------------- NEW: DEPENDENCY CHECKS -------------------
|
|
49
|
+
def verify_required_tools() -> None:
|
|
50
|
+
"""
|
|
51
|
+
Ensure required global CLIs are installed before continuing.
|
|
52
|
+
"""
|
|
53
|
+
requirements = [
|
|
54
|
+
{
|
|
55
|
+
"name": "qwen",
|
|
56
|
+
"binary": "qwen",
|
|
57
|
+
"install": "npm install -g @qwen-code/qwen-code@latest",
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
"name": "claude-code-router",
|
|
61
|
+
"binary": "ccr",
|
|
62
|
+
"install": "npm install -g @musistudio/claude-code-router",
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
"name": "claude",
|
|
66
|
+
"binary": "claude",
|
|
67
|
+
"install": "npm install -g @anthropic-ai/claude-code",
|
|
68
|
+
},
|
|
69
|
+
]
|
|
70
|
+
|
|
71
|
+
for req in requirements:
|
|
72
|
+
path = which(req["binary"])
|
|
73
|
+
if not path:
|
|
74
|
+
print(
|
|
75
|
+
f"[ERR] Required package '{req['name']}' is not installed or not in PATH."
|
|
76
|
+
)
|
|
77
|
+
print(f" Install it with:\n {req['install']}")
|
|
78
|
+
raise SystemExit(1)
|
|
79
|
+
else:
|
|
80
|
+
print(f"[OK] Found {req['name']} → {path}")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
# -------------------------------------------------------------
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def schema_validation() -> None:
|
|
87
|
+
"""
|
|
88
|
+
Checks for api_base_url within the 'qwen' provider in config.json.
|
|
89
|
+
"""
|
|
90
|
+
target_url = "https://portal.qwen.ai/v1/chat/completions"
|
|
91
|
+
|
|
92
|
+
if not SCHEMA_FILE.exists():
|
|
93
|
+
print(f"[ERR] Schema file missing at {SCHEMA_FILE}.")
|
|
94
|
+
return
|
|
95
|
+
|
|
96
|
+
if not CCR_CONFIG.exists():
|
|
97
|
+
print(f"[INFO] {CCR_CONFIG.name} missing. Initializing from schema...")
|
|
98
|
+
save_json_atomic(CCR_CONFIG, load_json(SCHEMA_FILE))
|
|
99
|
+
return
|
|
100
|
+
|
|
101
|
+
try:
|
|
102
|
+
config_data = load_json(CCR_CONFIG)
|
|
103
|
+
except Exception:
|
|
104
|
+
config_data = {}
|
|
105
|
+
|
|
106
|
+
# Find the qwen provider block
|
|
107
|
+
providers = config_data.get("Providers", [])
|
|
108
|
+
qwen_provider = next(
|
|
109
|
+
(p for p in providers if isinstance(p, dict) and p.get("name") == "qwen"), None
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
needs_reset = False
|
|
113
|
+
if not qwen_provider:
|
|
114
|
+
print("[INFO] 'qwen' provider block missing in 'ccr' config.")
|
|
115
|
+
needs_reset = True
|
|
116
|
+
elif "api_base_url" not in qwen_provider:
|
|
117
|
+
print("[INFO] 'api_base_url' missing in 'ccr' config.")
|
|
118
|
+
needs_reset = True
|
|
119
|
+
elif qwen_provider.get("api_base_url") != target_url:
|
|
120
|
+
print(f"[INFO] URL mismatch. Found: {qwen_provider.get('api_base_url')}")
|
|
121
|
+
needs_reset = True
|
|
122
|
+
|
|
123
|
+
if needs_reset:
|
|
124
|
+
print(f"[INFO] Rewriting {CCR_CONFIG.name} using {SCHEMA_FILE.name}...")
|
|
125
|
+
save_json_atomic(CCR_CONFIG, load_json(SCHEMA_FILE))
|
|
126
|
+
else:
|
|
127
|
+
print("[OK] Schema validation passed.")
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
# -------------------------------------------------------------
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def run_windows_cmd(exe_path: str, args: list[str], quiet: bool = True) -> int:
|
|
134
|
+
"""
|
|
135
|
+
Windows-safe runner:
|
|
136
|
+
- .ps1 => powershell -File
|
|
137
|
+
- .cmd/.bat => cmd.exe /c <properly quoted>
|
|
138
|
+
- else => direct execute
|
|
139
|
+
"""
|
|
140
|
+
exe_lower = exe_path.lower()
|
|
141
|
+
stdout = subprocess.DEVNULL if quiet else None
|
|
142
|
+
stderr = subprocess.DEVNULL if quiet else None
|
|
143
|
+
|
|
144
|
+
if exe_lower.endswith(".ps1"):
|
|
145
|
+
cmd = [
|
|
146
|
+
"powershell",
|
|
147
|
+
"-NoProfile",
|
|
148
|
+
"-ExecutionPolicy",
|
|
149
|
+
"Bypass",
|
|
150
|
+
"-File",
|
|
151
|
+
exe_path,
|
|
152
|
+
*args,
|
|
153
|
+
]
|
|
154
|
+
p = subprocess.run(cmd, check=False, stdout=stdout, stderr=stderr)
|
|
155
|
+
return p.returncode
|
|
156
|
+
|
|
157
|
+
if exe_lower.endswith(".cmd") or exe_lower.endswith(".bat"):
|
|
158
|
+
cmdline = subprocess.list2cmdline([exe_path, *args])
|
|
159
|
+
p = subprocess.run(
|
|
160
|
+
["cmd.exe", "/d", "/s", "/c", cmdline],
|
|
161
|
+
check=False,
|
|
162
|
+
stdout=stdout,
|
|
163
|
+
stderr=stderr,
|
|
164
|
+
)
|
|
165
|
+
return p.returncode
|
|
166
|
+
|
|
167
|
+
p = subprocess.run([exe_path, *args], check=False, stdout=stdout, stderr=stderr)
|
|
168
|
+
return p.returncode
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def force_qwen_refresh(qwen_path: str) -> None:
|
|
172
|
+
"""
|
|
173
|
+
Trigger Qwen so it refreshes credentials if refresh_token is valid.
|
|
174
|
+
"""
|
|
175
|
+
candidates = [
|
|
176
|
+
["--version"],
|
|
177
|
+
["--help"],
|
|
178
|
+
["-h"],
|
|
179
|
+
["-p", "ping"],
|
|
180
|
+
["hi"],
|
|
181
|
+
]
|
|
182
|
+
for args in candidates:
|
|
183
|
+
rc = run_windows_cmd(qwen_path, args, quiet=True)
|
|
184
|
+
if rc == 0:
|
|
185
|
+
break
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def update_ccr_api_key(ccr_config: Path, new_token: str) -> bool:
|
|
189
|
+
cfg = load_json(ccr_config)
|
|
190
|
+
providers = cfg.get("Providers", [])
|
|
191
|
+
if not isinstance(providers, list) or not providers:
|
|
192
|
+
raise RuntimeError("CCR config has no Providers[] array.")
|
|
193
|
+
|
|
194
|
+
changed = False
|
|
195
|
+
for p in providers:
|
|
196
|
+
if isinstance(p, dict) and p.get("name") == "qwen":
|
|
197
|
+
if p.get("api_key") != new_token:
|
|
198
|
+
p["api_key"] = new_token
|
|
199
|
+
changed = True
|
|
200
|
+
|
|
201
|
+
if changed:
|
|
202
|
+
try:
|
|
203
|
+
shutil.copy2(ccr_config, ccr_config.with_suffix(".json.bak"))
|
|
204
|
+
except Exception:
|
|
205
|
+
pass
|
|
206
|
+
save_json_atomic(ccr_config, cfg)
|
|
207
|
+
|
|
208
|
+
return changed
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def print_install_info():
|
|
212
|
+
for p in site.getsitepackages():
|
|
213
|
+
if "qwen_claude" in p:
|
|
214
|
+
print(" ", p)
|
|
215
|
+
|
|
216
|
+
print(f"[INFO] Executable Path: '{Path(sys.executable).parent}'")
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def main() -> int:
|
|
220
|
+
print_install_info()
|
|
221
|
+
# 🔒 NEW: Verify required tools first
|
|
222
|
+
verify_required_tools()
|
|
223
|
+
# 🔒 NEW: Validate the schema first
|
|
224
|
+
schema_validation()
|
|
225
|
+
|
|
226
|
+
qwen_path = which("qwen")
|
|
227
|
+
ccr_path = which("ccr")
|
|
228
|
+
|
|
229
|
+
if not QWEN_OAUTH.exists():
|
|
230
|
+
print(f"[WARN] Qwen oauth file not found: {QWEN_OAUTH}")
|
|
231
|
+
print("[INFO] Launching Qwen for authentication...")
|
|
232
|
+
|
|
233
|
+
# Launch qwen interactively
|
|
234
|
+
proc = subprocess.Popen(
|
|
235
|
+
[qwen_path], creationflags=subprocess.CREATE_NEW_PROCESS_GROUP
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
# Wait for oauth file to appear
|
|
239
|
+
print("[INFO] Waiting for Qwen authentication to complete...")
|
|
240
|
+
while True:
|
|
241
|
+
if QWEN_OAUTH.exists():
|
|
242
|
+
print("[OK] Qwen authentication completed.")
|
|
243
|
+
break
|
|
244
|
+
|
|
245
|
+
# If user closed Qwen without authenticating
|
|
246
|
+
if proc.poll() is not None:
|
|
247
|
+
print("[ERR] Qwen exited before authentication completed.")
|
|
248
|
+
return 6
|
|
249
|
+
|
|
250
|
+
time.sleep(1)
|
|
251
|
+
|
|
252
|
+
# Stop qwen after successful auth
|
|
253
|
+
try:
|
|
254
|
+
subprocess.run(
|
|
255
|
+
["taskkill", "/PID", str(proc.pid), "/T", "/F"],
|
|
256
|
+
stdout=subprocess.DEVNULL,
|
|
257
|
+
stderr=subprocess.DEVNULL,
|
|
258
|
+
check=False,
|
|
259
|
+
)
|
|
260
|
+
print("[INFO] Qwen CLI is Terminated...")
|
|
261
|
+
except Exception:
|
|
262
|
+
pass
|
|
263
|
+
|
|
264
|
+
# print(f"[WARN] Qwen Oauth file not found: {QWEN_OAUTH}")
|
|
265
|
+
# print("[INFO] Launching Qwen for authentication...")
|
|
266
|
+
# run_windows_cmd(qwen_path, [], quiet=False)
|
|
267
|
+
# return 0
|
|
268
|
+
|
|
269
|
+
if not CCR_CONFIG.exists():
|
|
270
|
+
print(f"[ERR] CCR config file not found: {CCR_CONFIG}")
|
|
271
|
+
print(" Edit CCR_CONFIG in the script to the correct location.")
|
|
272
|
+
return 5
|
|
273
|
+
|
|
274
|
+
oauth = load_json(QWEN_OAUTH)
|
|
275
|
+
|
|
276
|
+
if is_expiring_soon(oauth, REFRESH_BUFFER_SECONDS):
|
|
277
|
+
print("[INFO] Token expired/near expiry → triggering Qwen refresh...")
|
|
278
|
+
force_qwen_refresh(qwen_path)
|
|
279
|
+
oauth = load_json(QWEN_OAUTH)
|
|
280
|
+
|
|
281
|
+
access_token = oauth.get("access_token")
|
|
282
|
+
if not access_token:
|
|
283
|
+
print("[WARN] Silent refresh failed. Interactive login required.")
|
|
284
|
+
|
|
285
|
+
# Launch qwen interactively
|
|
286
|
+
proc = subprocess.Popen(
|
|
287
|
+
[qwen_path], creationflags=subprocess.CREATE_NEW_PROCESS_GROUP
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
# Wait for oauth file to appear
|
|
291
|
+
print("[INFO] Waiting for Qwen authentication to complete...")
|
|
292
|
+
while True:
|
|
293
|
+
if QWEN_OAUTH.exists():
|
|
294
|
+
print("[OK] Qwen authentication completed.")
|
|
295
|
+
break
|
|
296
|
+
|
|
297
|
+
# If user closed Qwen without authenticating
|
|
298
|
+
if proc.poll() is not None:
|
|
299
|
+
print("[ERR] Qwen exited before authentication completed.")
|
|
300
|
+
return 6
|
|
301
|
+
|
|
302
|
+
time.sleep(1)
|
|
303
|
+
|
|
304
|
+
# Stop qwen after successful auth
|
|
305
|
+
try:
|
|
306
|
+
subprocess.run(
|
|
307
|
+
["taskkill", "/PID", str(proc.pid), "/T", "/F"],
|
|
308
|
+
stdout=subprocess.DEVNULL,
|
|
309
|
+
stderr=subprocess.DEVNULL,
|
|
310
|
+
check=False,
|
|
311
|
+
)
|
|
312
|
+
print("[INFO] Qwen CLI is Terminated...")
|
|
313
|
+
except Exception:
|
|
314
|
+
pass
|
|
315
|
+
|
|
316
|
+
# Reload oauth after interactive login
|
|
317
|
+
oauth = load_json(QWEN_OAUTH)
|
|
318
|
+
access_token = oauth.get("access_token")
|
|
319
|
+
|
|
320
|
+
if not access_token:
|
|
321
|
+
print("[ERR] access_token still missing after interactive login.")
|
|
322
|
+
return 7
|
|
323
|
+
|
|
324
|
+
# print("[ERR] access_token missing after refresh attempt.")
|
|
325
|
+
# print(" Refresh token may be invalid; re-authenticate in Qwen.")
|
|
326
|
+
# return 6
|
|
327
|
+
|
|
328
|
+
changed = update_ccr_api_key(CCR_CONFIG, access_token)
|
|
329
|
+
if changed:
|
|
330
|
+
print("[OK] Updated CCR config with latest Qwen access_token.")
|
|
331
|
+
# if RESTART_CCR_ON_CHANGE and ccr_path:
|
|
332
|
+
# print("[INFO] Restarting CCR...")
|
|
333
|
+
# run_windows_cmd(ccr_path, ["restart"], quiet=False)
|
|
334
|
+
else:
|
|
335
|
+
print("[OK] CCR config already has the latest token.")
|
|
336
|
+
|
|
337
|
+
if RUN_CCR_CODE and ccr_path:
|
|
338
|
+
print("[INFO] Restarting CCR...")
|
|
339
|
+
run_windows_cmd(ccr_path, ["restart"], quiet=False)
|
|
340
|
+
|
|
341
|
+
print("[INFO] Launching Claude Code via CCR (ccr code)...")
|
|
342
|
+
try:
|
|
343
|
+
run_windows_cmd(ccr_path, ["code"], quiet=False)
|
|
344
|
+
except KeyboardInterrupt:
|
|
345
|
+
print("\n[INFO] CCR session interrupted by user.")
|
|
346
|
+
return 130
|
|
347
|
+
|
|
348
|
+
return 0
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def run():
|
|
352
|
+
try:
|
|
353
|
+
raise SystemExit(main())
|
|
354
|
+
except KeyboardInterrupt:
|
|
355
|
+
print("\n[INFO] Interrupted by user. Exiting...")
|
|
356
|
+
raise SystemExit(130)
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
if __name__ == "__main__":
|
|
360
|
+
try:
|
|
361
|
+
raise SystemExit(main())
|
|
362
|
+
except KeyboardInterrupt:
|
|
363
|
+
print("\n[INFO] Interrupted by user. Exiting...")
|
|
364
|
+
raise SystemExit(130)
|
|
@@ -1,236 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
import site
|
|
3
|
-
import sys
|
|
4
|
-
import json
|
|
5
|
-
import os
|
|
6
|
-
import time
|
|
7
|
-
import shutil
|
|
8
|
-
import subprocess
|
|
9
|
-
from pathlib import Path
|
|
10
|
-
from typing import Optional
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
# ------------------- CONFIG (EDIT IF NEEDED) -------------------
|
|
14
|
-
QWEN_OAUTH = Path(os.environ["USERPROFILE"]) / ".qwen" / "oauth_creds.json"
|
|
15
|
-
CCR_CONFIG = Path(os.environ["USERPROFILE"]) / ".claude-code-router" / "config.json"
|
|
16
|
-
SCHEMA_FILE = Path(__file__).parent / "schema.json"
|
|
17
|
-
|
|
18
|
-
REFRESH_BUFFER_SECONDS = 120
|
|
19
|
-
RESTART_CCR_ON_CHANGE = True
|
|
20
|
-
RUN_CCR_CODE = True
|
|
21
|
-
# ---------------------------------------------------------------
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
def load_json(path: Path) -> dict:
|
|
25
|
-
with path.open("r", encoding="utf-8") as f:
|
|
26
|
-
return json.load(f)
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
def save_json_atomic(path: Path, data: dict) -> None:
|
|
30
|
-
tmp = path.with_suffix(path.suffix + ".tmp")
|
|
31
|
-
with tmp.open("w", encoding="utf-8") as f:
|
|
32
|
-
json.dump(data, f, indent=2)
|
|
33
|
-
tmp.replace(path)
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
def is_expiring_soon(oauth: dict, buffer_seconds: int) -> bool:
|
|
37
|
-
exp_ms = int(oauth.get("expiry_date", 0))
|
|
38
|
-
if exp_ms <= 0:
|
|
39
|
-
return True
|
|
40
|
-
exp_s = exp_ms / 1000.0
|
|
41
|
-
return time.time() >= (exp_s - buffer_seconds)
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
def which(cmd: str) -> Optional[str]:
|
|
45
|
-
return shutil.which(cmd)
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
# ------------------- DEPENDENCY & SCHEMA CHECKS -------------------
|
|
49
|
-
def verify_required_tools() -> None:
|
|
50
|
-
requirements = [
|
|
51
|
-
{
|
|
52
|
-
"name": "qwen",
|
|
53
|
-
"binary": "qwen",
|
|
54
|
-
"install": "npm install -g @qwen-code/qwen-code@latest",
|
|
55
|
-
},
|
|
56
|
-
{
|
|
57
|
-
"name": "claude-code-router",
|
|
58
|
-
"binary": "ccr",
|
|
59
|
-
"install": "npm install -g @musistudio/claude-code-router",
|
|
60
|
-
},
|
|
61
|
-
{
|
|
62
|
-
"name": "claude",
|
|
63
|
-
"binary": "claude",
|
|
64
|
-
"install": "npm install -g @anthropic-ai/claude-code",
|
|
65
|
-
},
|
|
66
|
-
]
|
|
67
|
-
for req in requirements:
|
|
68
|
-
path = which(req["binary"])
|
|
69
|
-
if not path:
|
|
70
|
-
print(
|
|
71
|
-
f"[ERR] Required package '{req['name']}' is not installed.\n Install: {req['install']}"
|
|
72
|
-
)
|
|
73
|
-
raise SystemExit(1)
|
|
74
|
-
print(f"[OK] Found {req['name']} → {path}")
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
def schema_validation() -> None:
|
|
78
|
-
"""
|
|
79
|
-
Checks for api_base_url within the 'qwen' provider in config.json.
|
|
80
|
-
"""
|
|
81
|
-
target_url = "https://portal.qwen.ai/v1/chat/completions"
|
|
82
|
-
|
|
83
|
-
if not SCHEMA_FILE.exists():
|
|
84
|
-
print(f"[ERR] Schema file missing at {SCHEMA_FILE}.")
|
|
85
|
-
return
|
|
86
|
-
|
|
87
|
-
if not CCR_CONFIG.exists():
|
|
88
|
-
print(f"[INFO] {CCR_CONFIG.name} missing. Initializing from schema...")
|
|
89
|
-
save_json_atomic(CCR_CONFIG, load_json(SCHEMA_FILE))
|
|
90
|
-
return
|
|
91
|
-
|
|
92
|
-
try:
|
|
93
|
-
config_data = load_json(CCR_CONFIG)
|
|
94
|
-
except Exception:
|
|
95
|
-
config_data = {}
|
|
96
|
-
|
|
97
|
-
# Find the qwen provider block
|
|
98
|
-
providers = config_data.get("Providers", [])
|
|
99
|
-
qwen_provider = next(
|
|
100
|
-
(p for p in providers if isinstance(p, dict) and p.get("name") == "qwen"), None
|
|
101
|
-
)
|
|
102
|
-
|
|
103
|
-
needs_reset = False
|
|
104
|
-
if not qwen_provider:
|
|
105
|
-
print("[INFO] 'qwen' provider block missing in config.")
|
|
106
|
-
needs_reset = True
|
|
107
|
-
elif "api_base_url" not in qwen_provider:
|
|
108
|
-
print("[INFO] 'api_base_url' missing in qwen provider config.")
|
|
109
|
-
needs_reset = True
|
|
110
|
-
elif qwen_provider.get("api_base_url") != target_url:
|
|
111
|
-
print(f"[INFO] URL mismatch. Found: {qwen_provider.get('api_base_url')}")
|
|
112
|
-
needs_reset = True
|
|
113
|
-
|
|
114
|
-
if needs_reset:
|
|
115
|
-
print(f"[INFO] Rewriting {CCR_CONFIG.name} using {SCHEMA_FILE.name}...")
|
|
116
|
-
save_json_atomic(CCR_CONFIG, load_json(SCHEMA_FILE))
|
|
117
|
-
else:
|
|
118
|
-
print("[OK] Schema validation passed.")
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
# -------------------------------------------------------------
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
def run_windows_cmd(exe_path: str, args: list[str], quiet: bool = True) -> int:
|
|
125
|
-
exe_lower = exe_path.lower()
|
|
126
|
-
stdout = subprocess.DEVNULL if quiet else None
|
|
127
|
-
stderr = subprocess.DEVNULL if quiet else None
|
|
128
|
-
|
|
129
|
-
if exe_lower.endswith(".ps1"):
|
|
130
|
-
cmd = [
|
|
131
|
-
"powershell",
|
|
132
|
-
"-NoProfile",
|
|
133
|
-
"-ExecutionPolicy",
|
|
134
|
-
"Bypass",
|
|
135
|
-
"-File",
|
|
136
|
-
exe_path,
|
|
137
|
-
*args,
|
|
138
|
-
]
|
|
139
|
-
return subprocess.run(cmd, check=False, stdout=stdout, stderr=stderr).returncode
|
|
140
|
-
if exe_lower.endswith(".cmd") or exe_lower.endswith(".bat"):
|
|
141
|
-
cmdline = subprocess.list2cmdline([exe_path, *args])
|
|
142
|
-
return subprocess.run(
|
|
143
|
-
["cmd.exe", "/d", "/s", "/c", cmdline],
|
|
144
|
-
check=False,
|
|
145
|
-
stdout=stdout,
|
|
146
|
-
stderr=stderr,
|
|
147
|
-
).returncode
|
|
148
|
-
return subprocess.run(
|
|
149
|
-
[exe_path, *args], check=False, stdout=stdout, stderr=stderr
|
|
150
|
-
).returncode
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
def force_qwen_refresh(qwen_path: str) -> None:
|
|
154
|
-
run_windows_cmd(qwen_path, ["--version"], quiet=True)
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
def update_ccr_api_key(ccr_config: Path, new_token: str) -> bool:
|
|
158
|
-
cfg = load_json(ccr_config)
|
|
159
|
-
providers = cfg.get("Providers", [])
|
|
160
|
-
changed = False
|
|
161
|
-
for p in providers:
|
|
162
|
-
if isinstance(p, dict) and p.get("name") == "qwen":
|
|
163
|
-
if p.get("api_key") != new_token:
|
|
164
|
-
p["api_key"] = new_token
|
|
165
|
-
changed = True
|
|
166
|
-
|
|
167
|
-
if changed:
|
|
168
|
-
save_json_atomic(ccr_config, cfg)
|
|
169
|
-
return changed
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
def print_install_info():
|
|
173
|
-
print("[INFO] qwen-claude installed at:")
|
|
174
|
-
for p in site.getsitepackages():
|
|
175
|
-
if "qwen_claude" in p:
|
|
176
|
-
print(" ", p)
|
|
177
|
-
|
|
178
|
-
print("[INFO] Executable location:")
|
|
179
|
-
print(" ", Path(sys.executable).parent)
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
def main() -> int:
|
|
183
|
-
print_install_info()
|
|
184
|
-
verify_required_tools()
|
|
185
|
-
schema_validation()
|
|
186
|
-
|
|
187
|
-
qwen_path = which("qwen")
|
|
188
|
-
ccr_path = which("ccr")
|
|
189
|
-
|
|
190
|
-
# Auth logic
|
|
191
|
-
if not QWEN_OAUTH.exists():
|
|
192
|
-
print("[INFO] Launching Qwen for authentication...")
|
|
193
|
-
proc = subprocess.Popen([qwen_path])
|
|
194
|
-
while not QWEN_OAUTH.exists():
|
|
195
|
-
if proc.poll() is not None:
|
|
196
|
-
return 6
|
|
197
|
-
time.sleep(1)
|
|
198
|
-
proc.terminate()
|
|
199
|
-
|
|
200
|
-
oauth = load_json(QWEN_OAUTH)
|
|
201
|
-
if is_expiring_soon(oauth, REFRESH_BUFFER_SECONDS):
|
|
202
|
-
force_qwen_refresh(qwen_path)
|
|
203
|
-
oauth = load_json(QWEN_OAUTH)
|
|
204
|
-
|
|
205
|
-
access_token = oauth.get("access_token")
|
|
206
|
-
changed = update_ccr_api_key(CCR_CONFIG, access_token)
|
|
207
|
-
|
|
208
|
-
# RESTART LOGIC FIX: Only restart once
|
|
209
|
-
ccr_needs_restart = changed and RESTART_CCR_ON_CHANGE
|
|
210
|
-
|
|
211
|
-
if ccr_needs_restart:
|
|
212
|
-
print("[OK] Token updated. Restarting CCR...")
|
|
213
|
-
run_windows_cmd(ccr_path, ["restart"], quiet=False)
|
|
214
|
-
else:
|
|
215
|
-
print("[OK] CCR config is up to date.")
|
|
216
|
-
|
|
217
|
-
if RUN_CCR_CODE and ccr_path:
|
|
218
|
-
# If we didn't just restart it above, we might need to restart/start it here
|
|
219
|
-
# or just run 'code'. Since you had a restart here before, I'll keep it
|
|
220
|
-
# but ensure it doesn't double-trigger if changed was true.
|
|
221
|
-
if not ccr_needs_restart:
|
|
222
|
-
print("[INFO] Starting CCR...")
|
|
223
|
-
run_windows_cmd(ccr_path, ["restart"], quiet=False)
|
|
224
|
-
|
|
225
|
-
print("[INFO] Launching Claude Code...")
|
|
226
|
-
run_windows_cmd(ccr_path, ["code"], quiet=False)
|
|
227
|
-
|
|
228
|
-
return 0
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
def run():
|
|
232
|
-
raise SystemExit(main())
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
if __name__ == "__main__":
|
|
236
|
-
raise SystemExit(main())
|
|
File without changes
|
|
File without changes
|
|
File without changes
|