qwen-claude 0.1.0__py3-none-any.whl → 0.1.1__py3-none-any.whl
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/cli.py +164 -36
- {qwen_claude-0.1.0.dist-info → qwen_claude-0.1.1.dist-info}/METADATA +3 -3
- qwen_claude-0.1.1.dist-info/RECORD +7 -0
- qwen_claude-0.1.0.dist-info/RECORD +0 -7
- {qwen_claude-0.1.0.dist-info → qwen_claude-0.1.1.dist-info}/WHEEL +0 -0
- {qwen_claude-0.1.0.dist-info → qwen_claude-0.1.1.dist-info}/entry_points.txt +0 -0
qwen_claude/cli.py
CHANGED
|
@@ -45,8 +45,11 @@ def which(cmd: str) -> Optional[str]:
|
|
|
45
45
|
return shutil.which(cmd)
|
|
46
46
|
|
|
47
47
|
|
|
48
|
-
# ------------------- DEPENDENCY
|
|
48
|
+
# ------------------- NEW: DEPENDENCY CHECKS -------------------
|
|
49
49
|
def verify_required_tools() -> None:
|
|
50
|
+
"""
|
|
51
|
+
Ensure required global CLIs are installed before continuing.
|
|
52
|
+
"""
|
|
50
53
|
requirements = [
|
|
51
54
|
{
|
|
52
55
|
"name": "qwen",
|
|
@@ -64,14 +67,20 @@ def verify_required_tools() -> None:
|
|
|
64
67
|
"install": "npm install -g @anthropic-ai/claude-code",
|
|
65
68
|
},
|
|
66
69
|
]
|
|
70
|
+
|
|
67
71
|
for req in requirements:
|
|
68
72
|
path = which(req["binary"])
|
|
69
73
|
if not path:
|
|
70
74
|
print(
|
|
71
|
-
f"[ERR] Required package '{req['name']}' is not installed
|
|
75
|
+
f"[ERR] Required package '{req['name']}' is not installed or not in PATH."
|
|
72
76
|
)
|
|
77
|
+
print(f" Install it with:\n {req['install']}")
|
|
73
78
|
raise SystemExit(1)
|
|
74
|
-
|
|
79
|
+
else:
|
|
80
|
+
print(f"[OK] Found {req['name']} → {path}")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
# -------------------------------------------------------------
|
|
75
84
|
|
|
76
85
|
|
|
77
86
|
def schema_validation() -> None:
|
|
@@ -102,10 +111,10 @@ def schema_validation() -> None:
|
|
|
102
111
|
|
|
103
112
|
needs_reset = False
|
|
104
113
|
if not qwen_provider:
|
|
105
|
-
print("[INFO] 'qwen' provider block missing in config.")
|
|
114
|
+
print("[INFO] 'qwen' provider block missing in 'ccr' config.")
|
|
106
115
|
needs_reset = True
|
|
107
116
|
elif "api_base_url" not in qwen_provider:
|
|
108
|
-
print("[INFO] 'api_base_url' missing in
|
|
117
|
+
print("[INFO] 'api_base_url' missing in 'ccr' config.")
|
|
109
118
|
needs_reset = True
|
|
110
119
|
elif qwen_provider.get("api_base_url") != target_url:
|
|
111
120
|
print(f"[INFO] URL mismatch. Found: {qwen_provider.get('api_base_url')}")
|
|
@@ -122,6 +131,12 @@ def schema_validation() -> None:
|
|
|
122
131
|
|
|
123
132
|
|
|
124
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
|
+
"""
|
|
125
140
|
exe_lower = exe_path.lower()
|
|
126
141
|
stdout = subprocess.DEVNULL if quiet else None
|
|
127
142
|
stderr = subprocess.DEVNULL if quiet else None
|
|
@@ -136,27 +151,46 @@ def run_windows_cmd(exe_path: str, args: list[str], quiet: bool = True) -> int:
|
|
|
136
151
|
exe_path,
|
|
137
152
|
*args,
|
|
138
153
|
]
|
|
139
|
-
|
|
154
|
+
p = subprocess.run(cmd, check=False, stdout=stdout, stderr=stderr)
|
|
155
|
+
return p.returncode
|
|
156
|
+
|
|
140
157
|
if exe_lower.endswith(".cmd") or exe_lower.endswith(".bat"):
|
|
141
158
|
cmdline = subprocess.list2cmdline([exe_path, *args])
|
|
142
|
-
|
|
159
|
+
p = subprocess.run(
|
|
143
160
|
["cmd.exe", "/d", "/s", "/c", cmdline],
|
|
144
161
|
check=False,
|
|
145
162
|
stdout=stdout,
|
|
146
163
|
stderr=stderr,
|
|
147
|
-
)
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
)
|
|
164
|
+
)
|
|
165
|
+
return p.returncode
|
|
166
|
+
|
|
167
|
+
p = subprocess.run([exe_path, *args], check=False, stdout=stdout, stderr=stderr)
|
|
168
|
+
return p.returncode
|
|
151
169
|
|
|
152
170
|
|
|
153
171
|
def force_qwen_refresh(qwen_path: str) -> None:
|
|
154
|
-
|
|
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
|
|
155
186
|
|
|
156
187
|
|
|
157
188
|
def update_ccr_api_key(ccr_config: Path, new_token: str) -> bool:
|
|
158
189
|
cfg = load_json(ccr_config)
|
|
159
190
|
providers = cfg.get("Providers", [])
|
|
191
|
+
if not isinstance(providers, list) or not providers:
|
|
192
|
+
raise RuntimeError("CCR config has no Providers[] array.")
|
|
193
|
+
|
|
160
194
|
changed = False
|
|
161
195
|
for p in providers:
|
|
162
196
|
if isinstance(p, dict) and p.get("name") == "qwen":
|
|
@@ -165,72 +199,166 @@ def update_ccr_api_key(ccr_config: Path, new_token: str) -> bool:
|
|
|
165
199
|
changed = True
|
|
166
200
|
|
|
167
201
|
if changed:
|
|
202
|
+
try:
|
|
203
|
+
shutil.copy2(ccr_config, ccr_config.with_suffix(".json.bak"))
|
|
204
|
+
except Exception:
|
|
205
|
+
pass
|
|
168
206
|
save_json_atomic(ccr_config, cfg)
|
|
207
|
+
|
|
169
208
|
return changed
|
|
170
209
|
|
|
171
210
|
|
|
172
211
|
def print_install_info():
|
|
173
|
-
print("[INFO] qwen-claude installed at:")
|
|
174
212
|
for p in site.getsitepackages():
|
|
175
213
|
if "qwen_claude" in p:
|
|
176
214
|
print(" ", p)
|
|
177
215
|
|
|
178
|
-
print("[INFO] Executable
|
|
179
|
-
print(" ", Path(sys.executable).parent)
|
|
216
|
+
print(f"[INFO] Executable Path: '{Path(sys.executable).parent}'")
|
|
180
217
|
|
|
181
218
|
|
|
182
219
|
def main() -> int:
|
|
183
220
|
print_install_info()
|
|
221
|
+
# 🔒 NEW: Verify required tools first
|
|
184
222
|
verify_required_tools()
|
|
223
|
+
# 🔒 NEW: Validate the schema first
|
|
185
224
|
schema_validation()
|
|
186
225
|
|
|
187
226
|
qwen_path = which("qwen")
|
|
188
227
|
ccr_path = which("ccr")
|
|
189
228
|
|
|
190
|
-
# Auth logic
|
|
191
229
|
if not QWEN_OAUTH.exists():
|
|
230
|
+
print(f"[WARN] Qwen oauth file not found: {QWEN_OAUTH}")
|
|
192
231
|
print("[INFO] Launching Qwen for authentication...")
|
|
193
|
-
|
|
194
|
-
|
|
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
|
|
195
246
|
if proc.poll() is not None:
|
|
247
|
+
print("[ERR] Qwen exited before authentication completed.")
|
|
196
248
|
return 6
|
|
249
|
+
|
|
197
250
|
time.sleep(1)
|
|
198
|
-
|
|
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
|
|
199
273
|
|
|
200
274
|
oauth = load_json(QWEN_OAUTH)
|
|
275
|
+
|
|
201
276
|
if is_expiring_soon(oauth, REFRESH_BUFFER_SECONDS):
|
|
277
|
+
print("[INFO] Token expired/near expiry → triggering Qwen refresh...")
|
|
202
278
|
force_qwen_refresh(qwen_path)
|
|
203
279
|
oauth = load_json(QWEN_OAUTH)
|
|
204
280
|
|
|
205
281
|
access_token = oauth.get("access_token")
|
|
206
|
-
|
|
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
|
|
207
301
|
|
|
208
|
-
|
|
209
|
-
ccr_needs_restart = changed and RESTART_CCR_ON_CHANGE
|
|
302
|
+
time.sleep(1)
|
|
210
303
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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)
|
|
214
334
|
else:
|
|
215
|
-
print("[OK] CCR config
|
|
335
|
+
print("[OK] CCR config already has the latest token.")
|
|
216
336
|
|
|
217
337
|
if RUN_CCR_CODE and ccr_path:
|
|
218
|
-
|
|
219
|
-
|
|
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)
|
|
338
|
+
print("[INFO] Restarting CCR...")
|
|
339
|
+
run_windows_cmd(ccr_path, ["restart"], quiet=False)
|
|
224
340
|
|
|
225
|
-
print("[INFO] Launching Claude Code...")
|
|
226
|
-
|
|
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
|
|
227
347
|
|
|
228
348
|
return 0
|
|
229
349
|
|
|
230
350
|
|
|
231
351
|
def run():
|
|
232
|
-
|
|
352
|
+
try:
|
|
353
|
+
raise SystemExit(main())
|
|
354
|
+
except KeyboardInterrupt:
|
|
355
|
+
print("\n[INFO] Interrupted by user. Exiting...")
|
|
356
|
+
raise SystemExit(130)
|
|
233
357
|
|
|
234
358
|
|
|
235
359
|
if __name__ == "__main__":
|
|
236
|
-
|
|
360
|
+
try:
|
|
361
|
+
raise SystemExit(main())
|
|
362
|
+
except KeyboardInterrupt:
|
|
363
|
+
print("\n[INFO] Interrupted by user. Exiting...")
|
|
364
|
+
raise SystemExit(130)
|
|
@@ -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
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
qwen_claude/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
qwen_claude/cli.py,sha256=w_6jQ9psUWecSh3uCSaycfWE_6bk4kAcs57bMgTuu2o,11435
|
|
3
|
+
qwen_claude/schema.json,sha256=ifOWG5YRSsu7o6imu8pZLYJnHlzdqrKt7LxLMFb8wok,952
|
|
4
|
+
qwen_claude-0.1.1.dist-info/WHEEL,sha256=fAguSjoiATBe7TNBkJwOjyL1Tt4wwiaQGtNtjRPNMQA,80
|
|
5
|
+
qwen_claude-0.1.1.dist-info/entry_points.txt,sha256=TOf35RaxMV8MnDInvpY1CQP35uqHwxElhtH9OKuuENo,44
|
|
6
|
+
qwen_claude-0.1.1.dist-info/METADATA,sha256=fbAGCDSRyAYtJ5EhQAP3fHYtaStFDtGvyOMg-sFlmXE,253
|
|
7
|
+
qwen_claude-0.1.1.dist-info/RECORD,,
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
qwen_claude/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
-
qwen_claude/cli.py,sha256=PemEV1svetkWepw6lIjAJh7_x7NlfoU5CdgJlGDrNg0,7332
|
|
3
|
-
qwen_claude/schema.json,sha256=ifOWG5YRSsu7o6imu8pZLYJnHlzdqrKt7LxLMFb8wok,952
|
|
4
|
-
qwen_claude-0.1.0.dist-info/WHEEL,sha256=fAguSjoiATBe7TNBkJwOjyL1Tt4wwiaQGtNtjRPNMQA,80
|
|
5
|
-
qwen_claude-0.1.0.dist-info/entry_points.txt,sha256=TOf35RaxMV8MnDInvpY1CQP35uqHwxElhtH9OKuuENo,44
|
|
6
|
-
qwen_claude-0.1.0.dist-info/METADATA,sha256=sW0SiOOLc-R7O8hmxYeUtenzhMlWNVqr_jJaaafuMfA,263
|
|
7
|
-
qwen_claude-0.1.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|