qwenproxy-cli 1.0.31 → 1.0.32

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/sync/index.ts +124 -80
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qwenproxy-cli",
3
- "version": "1.0.31",
3
+ "version": "1.0.32",
4
4
  "description": "High-performance OpenAI & Anthropic compatible API gateway for Qwen with multi-account rotation, interactive TUI, and resilient tool calling.",
5
5
  "main": "src/index.ts",
6
6
  "bin": {
package/src/sync/index.ts CHANGED
@@ -151,6 +151,98 @@ export interface ClientDetectionStatus {
151
151
  url?: string;
152
152
  }
153
153
 
154
+ export function isExecutableInPath(name: string): boolean {
155
+ const envPath = process.env.PATH || "";
156
+ const dirs = envPath.split(path.delimiter).filter(Boolean);
157
+ const extensions = process.platform === "win32"
158
+ ? (process.env.PATHEXT || ".COM;.EXE;.BAT;.CMD").split(";").map((e) => e.toLowerCase())
159
+ : [""];
160
+
161
+ for (const dir of dirs) {
162
+ for (const ext of extensions) {
163
+ const fullPath = path.join(dir, name + ext);
164
+ try {
165
+ if (fs.existsSync(fullPath) && !fs.statSync(fullPath).isDirectory()) {
166
+ return true;
167
+ }
168
+ } catch {}
169
+ }
170
+ }
171
+ return false;
172
+ }
173
+
174
+ export function isVscodeExtensionInstalled(pattern: RegExp): boolean {
175
+ const home = os.homedir();
176
+ const candidateDirs = [
177
+ path.join(home, ".vscode", "extensions"),
178
+ path.join(home, ".vscode-insiders", "extensions"),
179
+ path.join(home, ".cursor", "extensions"),
180
+ path.join(home, ".windsurf", "extensions"),
181
+ ];
182
+ for (const d of candidateDirs) {
183
+ if (fs.existsSync(d)) {
184
+ try {
185
+ const entries = fs.readdirSync(d);
186
+ if (entries.some((e) => pattern.test(e))) return true;
187
+ } catch {}
188
+ }
189
+ }
190
+ return false;
191
+ }
192
+
193
+ export function isZedInstalled(): boolean {
194
+ if (isExecutableInPath("zed")) return true;
195
+ if (process.platform === "win32") {
196
+ const local = process.env.LOCALAPPDATA || "";
197
+ if (fs.existsSync(path.join(local, "Programs", "Zed"))) return true;
198
+ } else if (process.platform === "darwin") {
199
+ if (fs.existsSync("/Applications/Zed.app")) return true;
200
+ }
201
+ return false;
202
+ }
203
+
204
+ export function isMatchingLocalHost(text: string, port = 7936): boolean {
205
+ if (!text) return false;
206
+ const configuredPort = config.server?.port || 7936;
207
+ const ports = new Set([String(port), String(configuredPort), "7936", "3000"]);
208
+ const isLocal = text.includes("127.0.0.1") || text.includes("localhost") || text.includes("0.0.0.0");
209
+ return isLocal && Array.from(ports).some((p) => text.includes(`:${p}`));
210
+ }
211
+
212
+ export function isClientToolInstalled(
213
+ id: SyncClientName,
214
+ targetPath: string,
215
+ isCustomTestPath = false,
216
+ ): boolean {
217
+ if (isCustomTestPath) {
218
+ return fs.existsSync(targetPath);
219
+ }
220
+ switch (id) {
221
+ case "hermes":
222
+ return isExecutableInPath("hermes");
223
+ case "openclaw":
224
+ return isExecutableInPath("openclaw") || isExecutableInPath("clawdbot") || isExecutableInPath("moltbot");
225
+ case "aider":
226
+ return isExecutableInPath("aider");
227
+ case "kilo":
228
+ return isExecutableInPath("kilo") || isVscodeExtensionInstalled(/kilo/i);
229
+ case "cline":
230
+ return isExecutableInPath("cline") || isVscodeExtensionInstalled(/cline|zoo-code|roo-cline/i);
231
+ case "zed":
232
+ return isZedInstalled();
233
+ case "claude-code":
234
+ return isExecutableInPath("claude") || fs.existsSync(targetPath);
235
+ case "codex":
236
+ return isExecutableInPath("codex") || fs.existsSync(targetPath);
237
+ case "opencode":
238
+ return isExecutableInPath("opencode") || fs.existsSync(targetPath);
239
+ case "omp":
240
+ return isExecutableInPath("omp") || fs.existsSync(targetPath);
241
+ default:
242
+ return fs.existsSync(targetPath);
243
+ }
244
+ }
245
+
154
246
  /**
155
247
  * Inspects a client configuration file to determine whether the client is installed
156
248
  * and whether it is actively configured to route to QwenProxy.
@@ -161,37 +253,26 @@ export function inspectClientSyncStatus(
161
253
  port = 7936,
162
254
  ): ClientDetectionStatus {
163
255
  const defaultPaths = getDefaultPaths();
164
- const targetPath =
165
- filePath ||
166
- (id === "claude-code"
167
- ? defaultPaths.claudeCode
168
- : id === "codex"
169
- ? defaultPaths.codex
170
- : id === "opencode"
171
- ? defaultPaths.openCode
172
- : id === "omp"
173
- ? defaultPaths.omp
174
- : id === "hermes"
175
- ? defaultPaths.hermes
176
- : id === "openclaw"
177
- ? defaultPaths.openClaw
178
- : id === "kilo"
179
- ? defaultPaths.kilo
180
- : id === "cline"
181
- ? defaultPaths.cline
182
- : id === "zed"
183
- ? defaultPaths.zed
184
- : defaultPaths.aider);
256
+ const defaultTarget = (defaultPaths as Record<string, string>)[id] || "";
257
+ const isCustomTestPath = Boolean(
258
+ filePath && defaultTarget && path.resolve(filePath) !== path.resolve(defaultTarget),
259
+ );
260
+ const targetPath = filePath || defaultTarget;
185
261
 
186
262
  if (!fs.existsSync(targetPath)) {
187
263
  return { id, installed: false, synced: false };
188
264
  }
189
265
 
266
+ const toolInstalled = isClientToolInstalled(id, targetPath, isCustomTestPath);
267
+ if (!toolInstalled) {
268
+ return { id, installed: false, synced: false };
269
+ }
270
+
190
271
  try {
191
272
  if (id === "cline") {
192
- // Inspect SQLite DB
193
273
  let isSynced = false;
194
274
  let model: string | undefined;
275
+ let rowExists = false;
195
276
  try {
196
277
  const db = new Database(targetPath, { readonly: true });
197
278
  const row = db
@@ -201,18 +282,19 @@ export function inspectClientSyncStatus(
201
282
  .get() as { value: string } | undefined;
202
283
  db.close();
203
284
  if (row && row.value) {
285
+ rowExists = true;
204
286
  const parsed = JSON.parse(row.value);
205
287
  const url = parsed.openAiBaseUrl || "";
206
288
  model = parsed.openAiModelId;
207
- isSynced = Boolean(
208
- url &&
209
- (url.includes(String(port)) ||
210
- url.includes(`127.0.0.1:${port}`) ||
211
- url.includes(`localhost:${port}`)),
212
- );
289
+ isSynced = isMatchingLocalHost(url, port);
213
290
  }
214
291
  } catch {}
215
- return { id, installed: true, synced: isSynced, model };
292
+
293
+ const isInstalled = isCustomTestPath
294
+ ? true
295
+ : rowExists || isExecutableInPath("cline") || isVscodeExtensionInstalled(/cline|zoo-code|roo-cline/i);
296
+
297
+ return { id, installed: isInstalled, synced: isInstalled && isSynced, model };
216
298
  }
217
299
 
218
300
  const raw = fs.readFileSync(targetPath, "utf-8");
@@ -221,12 +303,8 @@ export function inspectClientSyncStatus(
221
303
  const data = JSON.parse(raw);
222
304
  const url = data?.env?.ANTHROPIC_BASE_URL || "";
223
305
  const model = data?.env?.ANTHROPIC_MODEL || data?.model || "";
224
- const isLocalHost =
225
- url.includes(String(port)) ||
226
- url.includes(`127.0.0.1:${port}`) ||
227
- url.includes(`localhost:${port}`);
228
306
  const isSynced =
229
- isLocalHost &&
307
+ isMatchingLocalHost(url, port) &&
230
308
  (model.toLowerCase().includes("qwen") || Boolean(data?.env?.ANTHROPIC_AUTH_TOKEN));
231
309
  return {
232
310
  id,
@@ -244,13 +322,11 @@ export function inspectClientSyncStatus(
244
322
  const model = modelMatch ? modelMatch[1] : undefined;
245
323
  const urlMatch = raw.match(/\[model_providers\.qwenproxy\][\s\S]*?base_url\s*=\s*["']([^"']+)["']/);
246
324
  const url = urlMatch ? urlMatch[1] : "";
247
- const isLocalHost = Boolean(
248
- url && (url.includes(String(port)) || url.includes(`127.0.0.1:${port}`) || url.includes(`localhost:${port}`)),
249
- );
325
+ const isSynced = hasProvider && isProviderActive && isMatchingLocalHost(url, port);
250
326
  return {
251
327
  id,
252
328
  installed: true,
253
- synced: hasProvider && isProviderActive && isLocalHost,
329
+ synced: isSynced,
254
330
  model,
255
331
  };
256
332
  }
@@ -261,20 +337,13 @@ export function inspectClientSyncStatus(
261
337
  const data = JSON.parse(raw);
262
338
  const provider = data?.provider?.qwenproxy;
263
339
  const url = provider?.options?.baseURL || "";
264
- const isLocalHost =
265
- url.includes(String(port)) ||
266
- url.includes(`127.0.0.1:${port}`) ||
267
- url.includes(`localhost:${port}`);
268
- isSynced = Boolean(provider && isLocalHost);
340
+ const isSynced = Boolean(provider && isMatchingLocalHost(url, port));
341
+ return { id, installed: true, synced: isSynced };
269
342
  } catch {
270
343
  const qwenBlockMatch = raw.match(/"qwenproxy"\s*:\s*\{[\s\S]*?"baseURL"\s*:\s*"([^"]+)"/);
271
344
  const url = qwenBlockMatch ? qwenBlockMatch[1] : "";
272
- isSynced = Boolean(
273
- url &&
274
- (url.includes(String(port)) ||
275
- url.includes(`127.0.0.1:${port}`) ||
276
- url.includes(`localhost:${port}`)),
277
- );
345
+ const isSynced = Boolean(url && isMatchingLocalHost(url, port));
346
+ return { id, installed: true, synced: isSynced };
278
347
  }
279
348
  return {
280
349
  id,
@@ -290,12 +359,7 @@ export function inspectClientSyncStatus(
290
359
  if (ompMatch) {
291
360
  const urlMatch = ompMatch[1].match(/baseUrl:\s*(\S+)/);
292
361
  url = urlMatch ? urlMatch[1].replace(/['"]/g, "") : undefined;
293
- isSynced = Boolean(
294
- url &&
295
- (url.includes(String(port)) ||
296
- url.includes(`127.0.0.1:${port}`) ||
297
- url.includes(`localhost:${port}`)),
298
- );
362
+ isSynced = Boolean(url && isMatchingLocalHost(url, port));
299
363
  }
300
364
  return {
301
365
  id,
@@ -306,47 +370,27 @@ export function inspectClientSyncStatus(
306
370
  }
307
371
 
308
372
  if (id === "hermes") {
309
- const isLocalHost =
310
- raw.includes(String(port)) ||
311
- raw.includes(`127.0.0.1:${port}`) ||
312
- raw.includes(`localhost:${port}`);
313
- const isSynced = isLocalHost && (raw.includes("qwenproxy") || raw.includes("qwen"));
373
+ const isSynced = isMatchingLocalHost(raw, port) && (raw.includes("qwenproxy") || raw.includes("qwen"));
314
374
  return { id, installed: true, synced: isSynced };
315
375
  }
316
376
 
317
377
  if (id === "openclaw") {
318
- const isLocalHost =
319
- raw.includes(String(port)) ||
320
- raw.includes(`127.0.0.1:${port}`) ||
321
- raw.includes(`localhost:${port}`);
322
- const isSynced = isLocalHost && raw.includes("qwenproxy");
378
+ const isSynced = isMatchingLocalHost(raw, port) && raw.includes("qwenproxy");
323
379
  return { id, installed: true, synced: isSynced };
324
380
  }
325
381
 
326
382
  if (id === "kilo") {
327
- const isLocalHost =
328
- raw.includes(String(port)) ||
329
- raw.includes(`127.0.0.1:${port}`) ||
330
- raw.includes(`localhost:${port}`);
331
- const isSynced = isLocalHost && raw.includes("qwenproxy");
383
+ const isSynced = isMatchingLocalHost(raw, port) && raw.includes("qwenproxy");
332
384
  return { id, installed: true, synced: isSynced };
333
385
  }
334
386
 
335
387
  if (id === "zed") {
336
- const isLocalHost =
337
- raw.includes(String(port)) ||
338
- raw.includes(`127.0.0.1:${port}`) ||
339
- raw.includes(`localhost:${port}`);
340
- const isSynced = isLocalHost && raw.includes("QwenProxy");
388
+ const isSynced = isMatchingLocalHost(raw, port) && raw.includes("QwenProxy");
341
389
  return { id, installed: true, synced: isSynced };
342
390
  }
343
391
 
344
392
  if (id === "aider") {
345
- const isLocalHost =
346
- raw.includes(String(port)) ||
347
- raw.includes(`127.0.0.1:${port}`) ||
348
- raw.includes(`localhost:${port}`);
349
- const isSynced = isLocalHost && raw.includes("qwen");
393
+ const isSynced = isMatchingLocalHost(raw, port) && raw.includes("qwen");
350
394
  return { id, installed: true, synced: isSynced };
351
395
  }
352
396
  } catch {