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 CHANGED
@@ -45,8 +45,11 @@ def which(cmd: str) -> Optional[str]:
45
45
  return shutil.which(cmd)
46
46
 
47
47
 
48
- # ------------------- DEPENDENCY & SCHEMA CHECKS -------------------
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.\n Install: {req['install']}"
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
- print(f"[OK] Found {req['name']} → {path}")
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 qwen provider config.")
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
- return subprocess.run(cmd, check=False, stdout=stdout, stderr=stderr).returncode
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
- return subprocess.run(
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
- ).returncode
148
- return subprocess.run(
149
- [exe_path, *args], check=False, stdout=stdout, stderr=stderr
150
- ).returncode
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
- run_windows_cmd(qwen_path, ["--version"], quiet=True)
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 location:")
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
- proc = subprocess.Popen([qwen_path])
194
- while not QWEN_OAUTH.exists():
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
- proc.terminate()
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
- changed = update_ccr_api_key(CCR_CONFIG, 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
207
301
 
208
- # RESTART LOGIC FIX: Only restart once
209
- ccr_needs_restart = changed and RESTART_CCR_ON_CHANGE
302
+ time.sleep(1)
210
303
 
211
- if ccr_needs_restart:
212
- print("[OK] Token updated. Restarting CCR...")
213
- run_windows_cmd(ccr_path, ["restart"], quiet=False)
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 is up to date.")
335
+ print("[OK] CCR config already has the latest token.")
216
336
 
217
337
  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)
338
+ print("[INFO] Restarting CCR...")
339
+ run_windows_cmd(ccr_path, ["restart"], quiet=False)
224
340
 
225
- print("[INFO] Launching Claude Code...")
226
- run_windows_cmd(ccr_path, ["code"], quiet=False)
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
- raise SystemExit(main())
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
- raise SystemExit(main())
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.0
3
+ Version: 0.1.1
4
4
  Summary: Qwen + Claude Code Router bootstrapper
5
- Author: Saad-Kamran-2006
6
- Author-email: Saad-Kamran-2006 <saadkamran6ft@gmail.com>
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,,