qwenproxy-cli 1.3.5 → 1.4.0

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.
@@ -0,0 +1,2824 @@
1
+ // src/sync-clients.ts
2
+ import fs14 from "node:fs";
3
+ import dotenv from "dotenv";
4
+
5
+ // src/core/paths.ts
6
+ import fs from "node:fs";
7
+ import path from "node:path";
8
+ import os from "node:os";
9
+ function isRunningUnderNodeTest() {
10
+ return process.argv.some(
11
+ (arg) => arg === "--test" || arg.includes("src/tests/") || arg.includes("src\\tests\\")
12
+ );
13
+ }
14
+ function getOsGlobalDataDir(options) {
15
+ const currentPlatform = options?.platform || process.platform;
16
+ const home = options?.homeDir || os.homedir();
17
+ if (currentPlatform === "win32") {
18
+ const winPath = path.win32 || path;
19
+ const appData = options?.appData || process.env.APPDATA || winPath.join(home, "AppData", "Roaming");
20
+ return winPath.join(appData, "qwenproxy");
21
+ }
22
+ if (currentPlatform === "darwin") {
23
+ return path.join(home, "Library", "Application Support", "qwenproxy");
24
+ }
25
+ const xdgDataHome = options?.xdgDataHome || process.env.XDG_DATA_HOME || path.join(home, ".local", "share");
26
+ return path.join(xdgDataHome, "qwenproxy");
27
+ }
28
+ function resolveDataDir(options) {
29
+ const envDir = options?.envDataDir ?? process.env.QWEN_DATA_DIR;
30
+ if (envDir && envDir.trim().length > 0) {
31
+ return path.resolve(envDir.trim());
32
+ }
33
+ const isTest = options?.isNodeTest ?? isRunningUnderNodeTest();
34
+ if (isTest) {
35
+ return path.resolve("data-test");
36
+ }
37
+ const localDir = path.resolve("data");
38
+ const localExists = options?.localDataExists ?? (fs.existsSync(path.join(localDir, "db", "qwenproxy.db")) || fs.existsSync(localDir));
39
+ if (localExists) {
40
+ return localDir;
41
+ }
42
+ return getOsGlobalDataDir({
43
+ platform: options?.platform,
44
+ appData: options?.appData,
45
+ homeDir: options?.homeDir,
46
+ xdgDataHome: options?.xdgDataHome
47
+ });
48
+ }
49
+ function getDataDir() {
50
+ return resolveDataDir();
51
+ }
52
+ function getDbDir(customDataDir) {
53
+ return path.join(customDataDir || getDataDir(), "db");
54
+ }
55
+ function getProfilesDir(customDataDir) {
56
+ return path.join(customDataDir || getDataDir(), "qwen_profiles");
57
+ }
58
+ function getSyncStatePath(customDataDir) {
59
+ return path.join(customDataDir || getDataDir(), "sync-state.json");
60
+ }
61
+ function getEnvFilePath(customDataDir) {
62
+ if (isRunningUnderNodeTest()) {
63
+ return path.join(customDataDir || getDataDir(), ".env");
64
+ }
65
+ const localEnv = path.resolve(".env");
66
+ if (fs.existsSync(localEnv)) {
67
+ return localEnv;
68
+ }
69
+ return path.join(customDataDir || getDataDir(), ".env");
70
+ }
71
+ function getLogsDir(customDataDir) {
72
+ return path.join(customDataDir || getDataDir(), "logs");
73
+ }
74
+ function ensureDataDirs(targetDataDir) {
75
+ const root = targetDataDir || getDataDir();
76
+ const dbDir = getDbDir(root);
77
+ const profilesDir = getProfilesDir(root);
78
+ const logsDir = getLogsDir(root);
79
+ try {
80
+ if (!fs.existsSync(root)) fs.mkdirSync(root, { recursive: true });
81
+ if (!fs.existsSync(dbDir)) fs.mkdirSync(dbDir, { recursive: true });
82
+ if (!fs.existsSync(profilesDir)) fs.mkdirSync(profilesDir, { recursive: true });
83
+ if (!fs.existsSync(logsDir)) fs.mkdirSync(logsDir, { recursive: true });
84
+ } catch (err) {
85
+ console.error(`[Paths] Error ensuring data directories at ${root}:`, err?.message || String(err));
86
+ }
87
+ }
88
+
89
+ // src/sync/index.ts
90
+ import "dotenv/config";
91
+ import fs13 from "node:fs";
92
+ import path13 from "node:path";
93
+ import os3 from "node:os";
94
+ import Database2 from "better-sqlite3";
95
+
96
+ // src/core/config.ts
97
+ import { z } from "zod";
98
+ var envSchema = z.object({
99
+ PORT: z.string().regex(/^\d+$/, "PORT must be a number").refine((value) => {
100
+ const port = Number(value);
101
+ return port >= 1 && port <= 65535;
102
+ }, "PORT must be between 1 and 65535").default("7936"),
103
+ HOST: z.string().default("127.0.0.1"),
104
+ INTERNAL_HOST: z.string().default("127.0.0.1"),
105
+ USER_AGENT: z.string().default(
106
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/149.0.0.0 Safari/537.36"
107
+ ),
108
+ QWEN_BX_V: z.string().default("2.5.37"),
109
+ // Version header on Qwen API requests = the deployed web bundle version
110
+ // (fallback when dynamic DOM detection is not yet completed).
111
+ // Override via env when Qwen ships a new bundle.
112
+ QWEN_WEB_VERSION: z.string().default("0.2.91"),
113
+ // Controls bx-ua/bx-umidtoken injection on the GENERAL API paths
114
+ // (chats/new, settings): those work without them (live-probed). The
115
+ // completions path is the exception — the 0.2.86 HAR shows the real
116
+ // client POSTs completions WITH bx-ua, so it always includes the captured
117
+ // tokens regardless of this flag (buildCompletionHeaders). Set true
118
+ // to inject them everywhere (legacy behavior).
119
+ QWEN_SEND_BX_UA: z.string().default("false"),
120
+ // Conversation mode: "thread" (default) reuses the upstream Qwen chat via
121
+ // parent_id and sends the thread-native delta; "temp" creates a NEW Qwen
122
+ // temp chat (chat_mode:"local") for every request and sends the full
123
+ // history inline (OpenAI standard). Temp chats are ephemeral and never
124
+ // appear in the account's chat list (live-probed).
125
+ QWEN_CHAT_MODE: z.enum([
126
+ "thread",
127
+ "temp",
128
+ "temp-thread",
129
+ "thread-temp",
130
+ "stateless",
131
+ "stateles",
132
+ "stateless-temp",
133
+ "stateles-temp"
134
+ ]).default("thread"),
135
+ PLAYWRIGHT_HEADLESS: z.string().default("true"),
136
+ PLAYWRIGHT_BROWSER: z.enum(["chromium", "chrome", "edge"]).default("chromium"),
137
+ PLAYWRIGHT_INIT_BATCH_SIZE: z.string().default("1"),
138
+ PLAYWRIGHT_CONTEXT_CLOSE_TIMEOUT_MS: z.string().default("10000"),
139
+ PLAYWRIGHT_IDLE_CONTEXT_TTL_MS: z.string().default("60000"),
140
+ PLAYWRIGHT_JS_HEAP_MB: z.string().default("256"),
141
+ PLAYWRIGHT_LOW_MEMORY_FLAGS: z.string().default("true"),
142
+ // Keep 2 warm contexts by default ({main + reserve} covers the common
143
+ // failover hop without opening one browser per account): after warmup two
144
+ // browsers stay open; any extra context (simultaneous use / failover) is
145
+ // closed once idle. The cap only evicts IDLE contexts — busy mutexes and
146
+ // active streams are never touched, so concurrent accounts each keep their
147
+ // own context while serving. Accounts in cooldown (rate-limited) sit idle,
148
+ // drop out of the warm set and get evicted.
149
+ PLAYWRIGHT_MAX_ACTIVE_CONTEXTS: z.string().default("2"),
150
+ PLAYWRIGHT_PREPARE_ALL_ON_STARTUP: z.string().default("false"),
151
+ CAPTCHA_SOLVER_ENABLED: z.string().default("true"),
152
+ CAPTCHA_SOLVER_MAX_ATTEMPTS: z.string().default("3"),
153
+ CAPTCHA_SOLVER_TIMEOUT_MS: z.string().default("15000"),
154
+ CAPTCHA_SOLVER_RETRY_DELAY_MS: z.string().default("1000"),
155
+ CAPTCHA_SOLVER_SETTLE_MS: z.string().default("2000"),
156
+ CAPTCHA_ACCOUNT_COOLDOWN_MS: z.string().default("120000"),
157
+ // Cap for the escalating quarantine applied when a challenge could NOT be
158
+ // solved (hard block). Each consecutive hard block doubles the window
159
+ // (base = CAPTCHA_ACCOUNT_COOLDOWN_MS) up to this cap.
160
+ CAPTCHA_HARD_BLOCK_MAX_COOLDOWN_MS: z.string().default("3600000"),
161
+ OSS_MULTIPART_THRESHOLD_MB: z.string().default("5"),
162
+ CHAT_REQUEST_LOG: z.string().default("false"),
163
+ HTTP_TIMEOUT: z.string().default("15000"),
164
+ CHAT_TIMEOUT: z.string().default("180000"),
165
+ NAVIGATION_TIMEOUT: z.string().default("60000"),
166
+ PAGE_TIMEOUT: z.string().default("60000"),
167
+ HEADERS_TIMEOUT: z.string().default("120000"),
168
+ TIME_TO_FIRST_BYTE: z.string().default("60000"),
169
+ IDLE_STREAM_TIMEOUT: z.string().default("60000"),
170
+ // Deadline for the FIRST upstream chunk on thinking models (the reasoning
171
+ // idle of 600s is for gaps AFTER data flows; a stream that produced
172
+ // nothing in this window is dead and should fail fast, retryable).
173
+ QWEN_FIRST_CHUNK_TIMEOUT: z.string().default("60000"),
174
+ TOTAL_REQUEST_TIMEOUT: z.string().default("600000"),
175
+ // Mid-stream silence window for thinking models: 3 min with ZERO upstream
176
+ // bytes is a dead stream (WAF swallow / dropped connection) — fail fast and
177
+ // let the retry policy rotate accounts. Flowing reasoning chunks RESET this
178
+ // timer, so legitimate slow thinking is never cut; only total silence is.
179
+ REASONING_MODEL_TIMEOUT: z.string().default("180000"),
180
+ CACHE_TTL: z.string().default("3600"),
181
+ RESPONSE_TTL: z.string().default("1800"),
182
+ CACHE_COMPRESSION_ENABLED: z.string().default("true"),
183
+ CACHE_COMPRESSION_THRESHOLD: z.string().default("1024"),
184
+ CACHE_COMPRESSION_LEVEL: z.string().default("6"),
185
+ METRICS_INTERVAL: z.string().default("10000"),
186
+ WATCHDOG_INTERVAL: z.string().default("5000"),
187
+ WATCHDOG_FAILURES: z.string().default("3"),
188
+ RAM_WARNING: z.string().default("80"),
189
+ RAM_CRITICAL: z.string().default("95"),
190
+ WS_WARNING: z.string().default("50"),
191
+ WS_CRITICAL: z.string().default("100"),
192
+ RETRY_BASE_DELAY_MS: z.string().default(process.env.TEST_MOCK_QWEN_AUTH === "true" ? "50" : "1000"),
193
+ RETRY_MAX_DELAY_MS: z.string().default(process.env.TEST_MOCK_QWEN_AUTH === "true" ? "200" : "10000"),
194
+ RETRY_MAX_ATTEMPTS: z.string().default("3"),
195
+ RETRY_MAX_ACCOUNT_SWITCHES: z.string().default("2"),
196
+ RETRY_ON_UNKNOWN_UPSTREAM: z.string().default("true"),
197
+ RETRY_AUTO_MALFORMED_TOOLS: z.string().default("true"),
198
+ RETRY_AUTO_MALFORMED_TOOLS_MAX: z.string().default("2"),
199
+ MAX_TOOL_CALLS_PER_TURN: z.string().default("6"),
200
+ QWEN_REPEATED_TOOL_CALL_WARN: z.string().default("2"),
201
+ ACCOUNT_MAX_CONCURRENT_STREAMS: z.string().default("2"),
202
+ ACCOUNT_BUSY_WAIT_MS: z.string().default("30000"),
203
+ // Cap for the "wait forever" account-lease queue (thread owner / last
204
+ // usable account): the queue previously had NO deadline, so a stuck lease
205
+ // holder made the next turn wait up to ~600s. A generous finite cap keeps
206
+ // the wait far above the normal 30s but still bounded.
207
+ ACCOUNT_QUEUE_WAIT_FOREVER_CAP_MS: z.string().default("120000"),
208
+ // Hard deadline for one stream-acquire attempt. A dead account (upstream
209
+ // swallows the completion fetch) otherwise chains metadata/header timeouts
210
+ // for minutes; this fails the attempt visibly so the retry loop switches.
211
+ ACQUIRE_DEADLINE_MS: z.string().default("120000"),
212
+ ACCOUNT_LEASE_MAX_DURATION_MS: z.string().default("600000"),
213
+ ACCOUNT_INIT_FAILURE_COOLDOWN_MS: z.string().default("300000"),
214
+ // Timeout before a request waiting on the CHAT lock gives up. The chat lock
215
+ // is held for the entire stream lifetime, so it must cover the longest
216
+ // legitimate generation (reasoning models with huge contexts can spend
217
+ // 2-3 min producing the first byte chain). A 60s hard cap turned a normal
218
+ // long turn into a 500 for every concurrent request on the same chat
219
+ // (2026-08-22 production log: lock held 130s -> acquire_deadline 120s).
220
+ CHAT_LOCK_TIMEOUT_MS: z.string().regex(/^\d+$/, "CHAT_LOCK_TIMEOUT_MS must be a number").default("180000"),
221
+ STREAM_DISCONNECT_GRACE_MS: z.string().regex(/^\d+$/, "STREAM_DISCONNECT_GRACE_MS must be a number").default("4000"),
222
+ CHAT_IN_PROGRESS_RETRY_DELAY_MS: z.string().default("2000"),
223
+ // Temporarily-busy window after a chat_in_progress: long enough to absorb
224
+ // the upstream chat settle (measured ~1-2s), short enough that the next
225
+ // turn of the sticky owner is not pushed to a cold account with a full
226
+ // context replay (8s caused a needless 13.3s hop in the 20:04 session).
227
+ CHAT_IN_PROGRESS_BUSY_MS: z.string().default("4000"),
228
+ // Same-chat retry budget for chat_in_progress. The upstream chat stays "in
229
+ // progress" for 2-16s after a completed turn (grows with turn size); each
230
+ // retry waits a jittered window (busyMs-based) and NO retry re-sends the
231
+ // full context — the escalation (new chat + full replay on a cold account)
232
+ // was the ~1MB re-upload that made tool loops feel like ~40 minutes. After
233
+ // this budget the request FAILS (thread binding kept) and the client's own
234
+ // retry lands on the settled chat with the delta intact.
235
+ CHAT_IN_PROGRESS_MAX_RETRIES: z.string().default("6"),
236
+ MID_STREAM_FAILOVER_THRESHOLD: z.string().default("2"),
237
+ MID_STREAM_FAILOVER_BUSY_MS: z.string().default("60000"),
238
+ QWEN_BASE_URL: z.string().default("https://chat.qwen.ai"),
239
+ QWEN_CHAT_POOL_SIZE: z.string().default("0"),
240
+ QWEN_CHAT_POOL_MODELS: z.string().default("qwen3.7-plus"),
241
+ QWEN_PERSONALIZATION_FROM_REQUEST: z.string().default("true"),
242
+ QWEN_PERSONALIZATION_VERIFY_GET: z.string().default("true"),
243
+ QWEN_BROWSER_ONLY_FETCH: z.string().default("true"),
244
+ QWEN_MAP_OPENAI_MODELS: z.string().default("true"),
245
+ QWEN_MAX_PROMPT_BYTES: z.string().default("0"),
246
+ QWEN_MAX_PERSONALIZATION_BYTES: z.string().default("500000"),
247
+ CONTEXT_METER_ENABLED: z.string().default("true"),
248
+ CONTEXT_METER_WINDOW_TOKENS: z.string().default("0"),
249
+ CONTEXT_METER_REPORT_USAGE: z.string().default("true"),
250
+ DELETE_ALL_CHATS_ON_SHUTDOWN: z.string().default("false"),
251
+ AUTO_CLEAN_CHATS_ON_STARTUP: z.string().default("true"),
252
+ AUTO_CLEAN_ORPHAN_CHATS: z.string().default("true"),
253
+ AUTO_CLEAN_CHAT_MAX_AGE_HOURS: z.string().default("168"),
254
+ // The Baxia WAF scores live page behavior (pointer/scroll events, open
255
+ // session) — an account whose page sits frozen for minutes returns a low
256
+ // trust score and gets TMD-challenged on the next request. On by default;
257
+ // the keeper skips accounts that are mid-stream or mutex-busy.
258
+ SESSION_KEEP_ALIVE_ENABLED: z.string().default("true"),
259
+ SESSION_KEEP_ALIVE_INTERVAL_MS: z.string().default("180000"),
260
+ SESSION_KEEP_ALIVE_IDLE_MS: z.string().default("120000"),
261
+ SESSION_KEEP_ALIVE_NAVIGATION_INTERVAL_MS: z.string().default("480000"),
262
+ API_KEY: z.string().default(""),
263
+ // Static x-ratelimit-* response headers (OpenAI-shaped, doc §5.2). The proxy
264
+ // does not enforce a token/request quota; these exist for SDK/tool parsing.
265
+ RATE_LIMIT_REQUESTS: z.string().default("5000"),
266
+ RATE_LIMIT_TOKENS: z.string().default("200000")
267
+ });
268
+ var env = envSchema.parse(process.env);
269
+ var config = {
270
+ server: {
271
+ port: parseInt(env.PORT),
272
+ host: env.HOST,
273
+ internalHost: env.INTERNAL_HOST,
274
+ rateLimit: {
275
+ requests: parseInt(env.RATE_LIMIT_REQUESTS),
276
+ tokens: parseInt(env.RATE_LIMIT_TOKENS)
277
+ }
278
+ },
279
+ logging: {
280
+ chatRequests: env.CHAT_REQUEST_LOG === "true"
281
+ },
282
+ auth: {
283
+ userAgent: env.USER_AGENT,
284
+ bxV: env.QWEN_BX_V
285
+ },
286
+ playwright: {
287
+ headless: env.PLAYWRIGHT_HEADLESS !== "false",
288
+ browser: env.PLAYWRIGHT_BROWSER,
289
+ initBatchSize: Math.max(1, parseInt(env.PLAYWRIGHT_INIT_BATCH_SIZE)),
290
+ contextCloseTimeoutMs: Math.max(
291
+ 1e3,
292
+ parseInt(env.PLAYWRIGHT_CONTEXT_CLOSE_TIMEOUT_MS)
293
+ ),
294
+ idleContextTtlMs: Math.max(0, parseInt(env.PLAYWRIGHT_IDLE_CONTEXT_TTL_MS)),
295
+ jsHeapMb: Math.max(64, parseInt(env.PLAYWRIGHT_JS_HEAP_MB)),
296
+ lowMemoryFlags: env.PLAYWRIGHT_LOW_MEMORY_FLAGS !== "false",
297
+ maxActiveContexts: Math.max(0, parseInt(env.PLAYWRIGHT_MAX_ACTIVE_CONTEXTS)),
298
+ prepareAllOnStartup: env.PLAYWRIGHT_PREPARE_ALL_ON_STARTUP !== "false"
299
+ },
300
+ captcha: {
301
+ enabled: env.CAPTCHA_SOLVER_ENABLED === "true",
302
+ maxAttempts: Math.max(1, Math.min(5, parseInt(env.CAPTCHA_SOLVER_MAX_ATTEMPTS))),
303
+ timeoutMs: Math.max(0, parseInt(env.CAPTCHA_SOLVER_TIMEOUT_MS)),
304
+ retryDelayMs: Math.max(0, parseInt(env.CAPTCHA_SOLVER_RETRY_DELAY_MS)),
305
+ settleMs: Math.max(0, parseInt(env.CAPTCHA_SOLVER_SETTLE_MS)),
306
+ /** Rest an account whose challenge could not be cleared before reusing it. */
307
+ accountCooldownMs: Math.max(0, parseInt(env.CAPTCHA_ACCOUNT_COOLDOWN_MS)),
308
+ /** Cap for the escalating hard-block quarantine (×2 per consecutive block). */
309
+ hardBlockMaxCooldownMs: Math.max(
310
+ 0,
311
+ parseInt(env.CAPTCHA_HARD_BLOCK_MAX_COOLDOWN_MS)
312
+ )
313
+ },
314
+ oss: {
315
+ multipartThresholdBytes: Math.max(
316
+ 1 * 1024 * 1024,
317
+ parseInt(env.OSS_MULTIPART_THRESHOLD_MB) * 1024 * 1024
318
+ )
319
+ },
320
+ timeouts: {
321
+ http: parseInt(env.HTTP_TIMEOUT),
322
+ chat: parseInt(env.CHAT_TIMEOUT),
323
+ navigation: parseInt(env.NAVIGATION_TIMEOUT),
324
+ page: parseInt(env.PAGE_TIMEOUT),
325
+ headers: parseInt(env.HEADERS_TIMEOUT),
326
+ timeToFirstByte: parseInt(env.TIME_TO_FIRST_BYTE),
327
+ idleStreamTimeout: parseInt(env.IDLE_STREAM_TIMEOUT),
328
+ totalRequestTimeout: parseInt(env.TOTAL_REQUEST_TIMEOUT),
329
+ reasoningModelTimeout: parseInt(env.REASONING_MODEL_TIMEOUT),
330
+ firstChunkTimeout: parseInt(env.QWEN_FIRST_CHUNK_TIMEOUT)
331
+ },
332
+ cache: {
333
+ defaultTTL: parseInt(env.CACHE_TTL),
334
+ responseTTL: parseInt(env.RESPONSE_TTL),
335
+ compression: {
336
+ enabled: env.CACHE_COMPRESSION_ENABLED !== "false",
337
+ threshold: parseInt(env.CACHE_COMPRESSION_THRESHOLD),
338
+ level: parseInt(env.CACHE_COMPRESSION_LEVEL)
339
+ }
340
+ },
341
+ metrics: {
342
+ interval: parseInt(env.METRICS_INTERVAL)
343
+ },
344
+ watchdog: {
345
+ checkInterval: parseInt(env.WATCHDOG_INTERVAL),
346
+ consecutiveFailuresThreshold: parseInt(env.WATCHDOG_FAILURES),
347
+ ram: {
348
+ warningThreshold: parseInt(env.RAM_WARNING),
349
+ criticalThreshold: parseInt(env.RAM_CRITICAL)
350
+ },
351
+ streams: {
352
+ warningThreshold: parseInt(env.WS_WARNING),
353
+ criticalThreshold: parseInt(env.WS_CRITICAL)
354
+ }
355
+ },
356
+ retry: {
357
+ baseDelayMs: parseInt(env.RETRY_BASE_DELAY_MS),
358
+ maxDelayMs: parseInt(env.RETRY_MAX_DELAY_MS),
359
+ maxAttempts: Math.max(1, parseInt(env.RETRY_MAX_ATTEMPTS)),
360
+ maxAccountSwitches: Math.max(0, parseInt(env.RETRY_MAX_ACCOUNT_SWITCHES)),
361
+ onUnknownUpstream: env.RETRY_ON_UNKNOWN_UPSTREAM !== "false",
362
+ chatInProgressDelayMs: Math.max(0, parseInt(env.CHAT_IN_PROGRESS_RETRY_DELAY_MS)),
363
+ chatInProgressBusyMs: Math.max(0, parseInt(env.CHAT_IN_PROGRESS_BUSY_MS)),
364
+ chatInProgressMaxAttempts: Math.max(1, parseInt(env.CHAT_IN_PROGRESS_MAX_RETRIES)),
365
+ midStreamFailoverThreshold: Math.max(
366
+ 0,
367
+ parseInt(env.MID_STREAM_FAILOVER_THRESHOLD)
368
+ ),
369
+ midStreamFailoverBusyMs: Math.max(
370
+ 0,
371
+ parseInt(env.MID_STREAM_FAILOVER_BUSY_MS)
372
+ ),
373
+ autoRetryMalformedTools: env.RETRY_AUTO_MALFORMED_TOOLS !== "false",
374
+ autoRetryMalformedToolsMax: Math.max(1, parseInt(env.RETRY_AUTO_MALFORMED_TOOLS_MAX)),
375
+ maxToolCallsPerTurn: Math.max(0, parseInt(env.MAX_TOOL_CALLS_PER_TURN)),
376
+ repeatedToolCallWarnThreshold: Math.max(
377
+ 1,
378
+ parseInt(env.QWEN_REPEATED_TOOL_CALL_WARN)
379
+ )
380
+ },
381
+ concurrency: {
382
+ maxStreamsPerAccount: Math.max(1, parseInt(env.ACCOUNT_MAX_CONCURRENT_STREAMS)),
383
+ busyWaitMs: Math.max(
384
+ 0,
385
+ Number.isFinite(parseInt(env.ACCOUNT_BUSY_WAIT_MS)) ? parseInt(env.ACCOUNT_BUSY_WAIT_MS) : 3e4
386
+ ),
387
+ /** Bound for the "wait forever" lease queue (default 2 min). */
388
+ queueWaitForeverCapMs: Math.max(
389
+ 0,
390
+ Number.isFinite(parseInt(env.ACCOUNT_QUEUE_WAIT_FOREVER_CAP_MS)) ? parseInt(env.ACCOUNT_QUEUE_WAIT_FOREVER_CAP_MS) : 12e4
391
+ ),
392
+ /** Hard deadline for one stream-acquire attempt (default 2 min). */
393
+ acquireDeadlineMs: Math.max(
394
+ 0,
395
+ Number.isFinite(parseInt(env.ACQUIRE_DEADLINE_MS)) ? parseInt(env.ACQUIRE_DEADLINE_MS) : 12e4
396
+ ),
397
+ /** Safety net: force-release leases held longer than this (default 10 min). */
398
+ leaseMaxDurationMs: Math.max(
399
+ 0,
400
+ parseInt(env.ACCOUNT_LEASE_MAX_DURATION_MS)
401
+ ),
402
+ initFailureCooldownMs: Math.max(
403
+ 3e4,
404
+ parseInt(env.ACCOUNT_INIT_FAILURE_COOLDOWN_MS)
405
+ ),
406
+ /** Max time a request waits on the per-chat lock (default 3 min). */
407
+ chatLockTimeoutMs: Math.max(
408
+ 0,
409
+ parseInt(env.CHAT_LOCK_TIMEOUT_MS)
410
+ )
411
+ },
412
+ stream: {
413
+ disconnectGraceMs: Math.max(
414
+ 0,
415
+ parseInt(env.STREAM_DISCONNECT_GRACE_MS)
416
+ )
417
+ },
418
+ sessionKeeper: {
419
+ enabled: env.SESSION_KEEP_ALIVE_ENABLED !== "false",
420
+ intervalMs: parseInt(env.SESSION_KEEP_ALIVE_INTERVAL_MS),
421
+ idleMs: parseInt(env.SESSION_KEEP_ALIVE_IDLE_MS),
422
+ navigationIntervalMs: parseInt(
423
+ env.SESSION_KEEP_ALIVE_NAVIGATION_INTERVAL_MS
424
+ )
425
+ },
426
+ apiKey: env.API_KEY,
427
+ qwen: {
428
+ baseUrl: env.QWEN_BASE_URL,
429
+ chatPoolSize: Math.max(0, parseInt(env.QWEN_CHAT_POOL_SIZE)),
430
+ chatPoolModels: env.QWEN_CHAT_POOL_MODELS.split(",").map((model) => model.trim()).filter(Boolean),
431
+ personalizationFromRequest: env.QWEN_PERSONALIZATION_FROM_REQUEST === "true",
432
+ personalizationVerifyGet: env.QWEN_PERSONALIZATION_VERIFY_GET !== "false",
433
+ /** "thread" (reuse upstream chat) or "temp" (new ephemeral chat per request). */
434
+ /** When true, all requests (personalization, models, media, chat) route exclusively through the browser page (no direct Node fetch). */
435
+ browserOnlyFetch: env.QWEN_BROWSER_ONLY_FETCH !== "false",
436
+ mapOpenAiModels: env.QWEN_MAP_OPENAI_MODELS !== "false",
437
+ chatMode: normalizeChatMode(env.QWEN_CHAT_MODE),
438
+ maxPromptBytes: Math.max(0, parseInt(env.QWEN_MAX_PROMPT_BYTES)),
439
+ maxPersonalizationBytes: Math.max(
440
+ 0,
441
+ parseInt(env.QWEN_MAX_PERSONALIZATION_BYTES)
442
+ ),
443
+ deleteAllChatsOnShutdown: env.DELETE_ALL_CHATS_ON_SHUTDOWN === "true",
444
+ autoCleanChatsOnStartup: env.AUTO_CLEAN_CHATS_ON_STARTUP !== "false",
445
+ autoCleanOrphanChats: env.AUTO_CLEAN_ORPHAN_CHATS !== "false",
446
+ autoCleanChatMaxAgeHours: Math.max(
447
+ 1,
448
+ parseInt(env.AUTO_CLEAN_CHAT_MAX_AGE_HOURS) || 168
449
+ ),
450
+ sendBxUa: env.QWEN_SEND_BX_UA === "true",
451
+ /** Deployed web bundle version sent as the `version` API header. */
452
+ webVersion: env.QWEN_WEB_VERSION
453
+ },
454
+ contextMeter: {
455
+ enabled: env.CONTEXT_METER_ENABLED === "true",
456
+ windowTokens: Math.max(0, parseInt(env.CONTEXT_METER_WINDOW_TOKENS)),
457
+ reportUsage: env.CONTEXT_METER_REPORT_USAGE === "true"
458
+ }
459
+ };
460
+ function normalizeChatMode(mode) {
461
+ if (!mode) return "thread";
462
+ const m = mode.trim().toLowerCase().replace(/_/g, "-");
463
+ if (m === "stateless-temp" || m === "stateles-temp" || m === "temp") {
464
+ return "stateless-temp";
465
+ }
466
+ if (m === "stateless" || m === "stateles") {
467
+ return "stateless";
468
+ }
469
+ if (m === "thread-temp" || m === "temp-thread") {
470
+ return "thread-temp";
471
+ }
472
+ return "thread";
473
+ }
474
+
475
+ // src/core/local-auth.ts
476
+ var PLACEHOLDER_API_KEY = "sk-qwenproxy-local";
477
+ function isPlaceholderApiKey(key) {
478
+ const value = (key || "").trim();
479
+ return value.length === 0 || value === PLACEHOLDER_API_KEY;
480
+ }
481
+ function isLoopbackHost(host) {
482
+ const normalized = host.trim().toLowerCase().replace(/^\[|\]$/g, "");
483
+ return normalized === "127.0.0.1" || normalized === "localhost" || normalized === "::1" || normalized === "0:0:0:0:0:0:0:1";
484
+ }
485
+
486
+ // src/sync/claude-code.ts
487
+ import fs3 from "node:fs";
488
+ import path3 from "node:path";
489
+
490
+ // src/sync/utils.ts
491
+ import fs2 from "node:fs";
492
+ import path2 from "node:path";
493
+ function createTimestampBackup(filePath) {
494
+ const dir = path2.dirname(filePath);
495
+ const ext = path2.extname(filePath);
496
+ const base = path2.basename(filePath, ext);
497
+ const now = /* @__PURE__ */ new Date();
498
+ const pad = (n) => String(n).padStart(2, "0");
499
+ const timestamp = `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}_${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
500
+ const backupPath = path2.join(dir, `${base}.qwenproxy.${timestamp}${ext}.bak`);
501
+ fs2.copyFileSync(filePath, backupPath);
502
+ return backupPath;
503
+ }
504
+ function findLatestBackup(filePath) {
505
+ const dir = path2.dirname(filePath);
506
+ const ext = path2.extname(filePath);
507
+ const base = path2.basename(filePath, ext);
508
+ if (!fs2.existsSync(dir)) return void 0;
509
+ try {
510
+ const files = fs2.readdirSync(dir);
511
+ const candidates = files.filter((f) => f.startsWith(base) && f.includes("qwenproxy") && f.endsWith(".bak")).map((f) => path2.join(dir, f)).sort((a, b) => {
512
+ try {
513
+ return fs2.statSync(b).mtimeMs - fs2.statSync(a).mtimeMs;
514
+ } catch {
515
+ return 0;
516
+ }
517
+ });
518
+ return candidates[0];
519
+ } catch {
520
+ return void 0;
521
+ }
522
+ }
523
+ function restoreFromBackup(filePath, backupPath) {
524
+ const targetBackup = backupPath && fs2.existsSync(backupPath) ? backupPath : findLatestBackup(filePath);
525
+ if (!targetBackup || !fs2.existsSync(targetBackup)) {
526
+ return false;
527
+ }
528
+ fs2.copyFileSync(targetBackup, filePath);
529
+ try {
530
+ fs2.unlinkSync(targetBackup);
531
+ } catch {
532
+ }
533
+ return true;
534
+ }
535
+ function formatModelDisplayName(model) {
536
+ if (model === "qwen3.8-max") return "Qwen 3.8 Max";
537
+ if (model === "qwen3.8-omni-flash") return "Qwen 3.8 Omni Flash";
538
+ if (model === "qwen3.7-plus") return "Qwen 3.7 Plus";
539
+ if (model === "qwen3.6-plus") return "Qwen 3.6 Plus";
540
+ return model.replace(/^qwen/i, "Qwen ").split(/[-_]/).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ").replace(/\s+/g, " ").trim();
541
+ }
542
+
543
+ // src/sync/claude-code.ts
544
+ function syncClaudeCode(options) {
545
+ const { filePath, apiKey, baseUrl, model = "qwen3.8-max" } = options;
546
+ try {
547
+ fs3.mkdirSync(path3.dirname(filePath), { recursive: true });
548
+ let backupPath;
549
+ let existingSettings = {};
550
+ if (fs3.existsSync(filePath)) {
551
+ backupPath = createTimestampBackup(filePath);
552
+ try {
553
+ const content = fs3.readFileSync(filePath, "utf-8");
554
+ existingSettings = JSON.parse(content);
555
+ } catch {
556
+ existingSettings = {};
557
+ }
558
+ }
559
+ const env2 = {
560
+ ...existingSettings.env || {},
561
+ ANTHROPIC_BASE_URL: baseUrl,
562
+ ANTHROPIC_AUTH_TOKEN: apiKey,
563
+ ANTHROPIC_MODEL: model,
564
+ ANTHROPIC_CUSTOM_MODEL_OPTION: model,
565
+ ANTHROPIC_CUSTOM_MODEL_OPTION_NAME: formatModelDisplayName(model),
566
+ ANTHROPIC_CUSTOM_MODEL_OPTION_DESCRIPTION: `QwenProxy ${model}`,
567
+ ANTHROPIC_DEFAULT_SONNET_MODEL: model,
568
+ ANTHROPIC_DEFAULT_HAIKU_MODEL: "qwen3.7-plus",
569
+ ANTHROPIC_DEFAULT_OPUS_MODEL: model,
570
+ CLAUDE_CODE_MAX_CONTEXT_TOKENS: "1000000",
571
+ CLAUDE_CODE_DISABLE_ARTIFACT: "1"
572
+ };
573
+ const updatedSettings = {
574
+ ...existingSettings,
575
+ env: env2,
576
+ model,
577
+ enableArtifact: false
578
+ };
579
+ fs3.writeFileSync(filePath, JSON.stringify(updatedSettings, null, 2) + "\n", "utf-8");
580
+ return {
581
+ client: "claude-code",
582
+ filePath,
583
+ backupPath,
584
+ success: true,
585
+ action: backupPath ? "updated" : "created",
586
+ message: `Configured Claude Code with model ${model} and baseUrl ${baseUrl}`
587
+ };
588
+ } catch (err) {
589
+ return {
590
+ client: "claude-code",
591
+ filePath,
592
+ success: false,
593
+ action: "failed",
594
+ error: err?.message || String(err)
595
+ };
596
+ }
597
+ }
598
+ function restoreClaudeCode(filePath, backupPath) {
599
+ const restoredFromBackup = restoreFromBackup(filePath, backupPath);
600
+ let manuallyCleaned = false;
601
+ if (fs3.existsSync(filePath)) {
602
+ try {
603
+ const data = JSON.parse(fs3.readFileSync(filePath, "utf-8"));
604
+ if (data.env && (data.env.ANTHROPIC_BASE_URL?.includes("7936") || data.env.ANTHROPIC_AUTH_TOKEN === "sk-qwenproxy-local" || data.env.ANTHROPIC_MODEL?.includes("qwen"))) {
605
+ delete data.env.ANTHROPIC_BASE_URL;
606
+ delete data.env.ANTHROPIC_AUTH_TOKEN;
607
+ delete data.env.ANTHROPIC_MODEL;
608
+ delete data.env.ANTHROPIC_CUSTOM_MODEL_OPTION;
609
+ delete data.env.ANTHROPIC_CUSTOM_MODEL_OPTION_NAME;
610
+ delete data.env.ANTHROPIC_CUSTOM_MODEL_OPTION_DESCRIPTION;
611
+ delete data.env.ANTHROPIC_DEFAULT_SONNET_MODEL;
612
+ delete data.env.ANTHROPIC_DEFAULT_HAIKU_MODEL;
613
+ delete data.env.ANTHROPIC_DEFAULT_OPUS_MODEL;
614
+ delete data.env.CLAUDE_CODE_MAX_CONTEXT_TOKENS;
615
+ delete data.env.CLAUDE_CODE_DISABLE_ARTIFACT;
616
+ delete data.enableArtifact;
617
+ if (data.model && data.model.toLowerCase().includes("qwen")) {
618
+ delete data.model;
619
+ }
620
+ if (Object.keys(data.env).length === 0) {
621
+ delete data.env;
622
+ }
623
+ fs3.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n", "utf-8");
624
+ manuallyCleaned = true;
625
+ }
626
+ } catch {
627
+ }
628
+ }
629
+ const success = restoredFromBackup || manuallyCleaned;
630
+ return {
631
+ client: "claude-code",
632
+ filePath,
633
+ backupPath,
634
+ success,
635
+ action: success ? "restored" : "failed",
636
+ message: success ? restoredFromBackup ? "Restored Claude Code settings from backup" : "Removed QwenProxy configuration from Claude Code settings" : "Backup file not found"
637
+ };
638
+ }
639
+
640
+ // src/sync/codex.ts
641
+ import fs4 from "node:fs";
642
+ import path4 from "node:path";
643
+ import os2 from "node:os";
644
+ function removeTomlSection(content, sectionHeader) {
645
+ const lines = content.split(/\r?\n/);
646
+ const result = [];
647
+ let inTargetSection = false;
648
+ const targetNorm = sectionHeader.replace(/\s+/g, "");
649
+ for (const line of lines) {
650
+ const trimmed = line.trim();
651
+ if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
652
+ const headerInside = trimmed.slice(1, -1).trim().replace(/\s+/g, "");
653
+ if (headerInside === targetNorm) {
654
+ inTargetSection = true;
655
+ continue;
656
+ } else {
657
+ inTargetSection = false;
658
+ }
659
+ }
660
+ if (!inTargetSection) {
661
+ result.push(line);
662
+ }
663
+ }
664
+ return result.join("\n").replace(/\n{3,}/g, "\n\n").trim();
665
+ }
666
+ function updateTopLevelKey(content, key, value) {
667
+ const firstSectionIdx = content.search(/^\[/m);
668
+ const topPart = firstSectionIdx === -1 ? content : content.slice(0, firstSectionIdx);
669
+ const restPart = firstSectionIdx === -1 ? "" : content.slice(firstSectionIdx);
670
+ const formattedValue = typeof value === "string" ? `"${value}"` : String(value);
671
+ const keyRegex = new RegExp(`^${key}\\s*=.*$`, "m");
672
+ let newTopPart;
673
+ if (keyRegex.test(topPart)) {
674
+ newTopPart = topPart.replace(keyRegex, `${key} = ${formattedValue}`);
675
+ } else {
676
+ newTopPart = `${key} = ${formattedValue}
677
+ ` + topPart.trimStart();
678
+ }
679
+ return newTopPart + restPart;
680
+ }
681
+ function syncCodexSingleFile(targetPath, options) {
682
+ fs4.mkdirSync(path4.dirname(targetPath), { recursive: true });
683
+ let backupPath;
684
+ let content = "";
685
+ if (fs4.existsSync(targetPath)) {
686
+ backupPath = createTimestampBackup(targetPath);
687
+ content = fs4.readFileSync(targetPath, "utf-8");
688
+ }
689
+ const { apiKey, baseUrl, model = "qwen3.8-max", setActive = true } = options;
690
+ const providerBlock = `[model_providers.qwenproxy]
691
+ name = "QwenProxy"
692
+ base_url = "${baseUrl}"
693
+ wire_api = "responses"
694
+ experimental_bearer_token = "${apiKey}"
695
+ `;
696
+ content = removeTomlSection(content, "model_providers.qwenproxy");
697
+ content = content.trimEnd() + (content.length > 0 ? "\n\n" : "") + providerBlock;
698
+ if (setActive) {
699
+ content = updateTopLevelKey(content, "model", model);
700
+ content = updateTopLevelKey(content, "model_provider", "qwenproxy");
701
+ content = updateTopLevelKey(content, "model_context_window", 1e6);
702
+ }
703
+ const catalogMatch = content.match(/^model_catalog_json\s*=\s*["']([^"']+)["']/m);
704
+ if (catalogMatch && catalogMatch[1]) {
705
+ try {
706
+ const rawCatalogPath = catalogMatch[1];
707
+ const catalogPath = path4.isAbsolute(rawCatalogPath) ? rawCatalogPath : path4.resolve(path4.dirname(targetPath), rawCatalogPath);
708
+ if (fs4.existsSync(catalogPath)) {
709
+ const catalog = JSON.parse(fs4.readFileSync(catalogPath, "utf-8"));
710
+ if (Array.isArray(catalog.models)) {
711
+ const hasModel = catalog.models.some((m) => m?.slug === model);
712
+ if (!hasModel) {
713
+ const template = catalog.models[0] || {};
714
+ catalog.models.unshift({
715
+ ...template,
716
+ slug: model,
717
+ display_name: formatModelDisplayName(model),
718
+ description: `QwenProxy ${model}`,
719
+ context_window: 1e6,
720
+ max_context_window: 1e6
721
+ });
722
+ fs4.writeFileSync(catalogPath, JSON.stringify(catalog, null, 2), "utf-8");
723
+ }
724
+ }
725
+ }
726
+ } catch {
727
+ }
728
+ }
729
+ fs4.writeFileSync(targetPath, content.trimEnd() + "\n", "utf-8");
730
+ return { backupPath };
731
+ }
732
+ function syncCodex(options) {
733
+ const { filePath, baseUrl } = options;
734
+ try {
735
+ const { backupPath } = syncCodexSingleFile(filePath, options);
736
+ const home = os2.homedir();
737
+ const standardPath = path4.join(home, ".codex", "config.toml");
738
+ const codexHomePath = process.env.CODEX_HOME ? path4.join(process.env.CODEX_HOME, "config.toml") : void 0;
739
+ const isStandard = path4.resolve(filePath).toLowerCase() === path4.resolve(standardPath).toLowerCase();
740
+ const isCodexHome = Boolean(codexHomePath) && path4.resolve(filePath).toLowerCase() === path4.resolve(codexHomePath).toLowerCase();
741
+ if ((isStandard || isCodexHome) && !isRunningUnderNodeTest()) {
742
+ const alternatePath = isStandard ? codexHomePath : standardPath;
743
+ if (alternatePath && path4.resolve(alternatePath).toLowerCase() !== path4.resolve(filePath).toLowerCase()) {
744
+ try {
745
+ if (fs4.existsSync(alternatePath) || fs4.existsSync(path4.dirname(alternatePath))) {
746
+ syncCodexSingleFile(alternatePath, options);
747
+ }
748
+ } catch {
749
+ }
750
+ }
751
+ }
752
+ return {
753
+ client: "codex",
754
+ filePath,
755
+ backupPath,
756
+ success: true,
757
+ action: backupPath ? "updated" : "created",
758
+ message: `Configured Codex with provider qwenproxy (${baseUrl})`
759
+ };
760
+ } catch (err) {
761
+ return {
762
+ client: "codex",
763
+ filePath,
764
+ success: false,
765
+ action: "failed",
766
+ error: err?.message || String(err)
767
+ };
768
+ }
769
+ }
770
+ function restoreCodex(filePath, backupPath) {
771
+ const restoredFromBackup = restoreFromBackup(filePath, backupPath);
772
+ let manuallyCleaned = false;
773
+ const cleanSingleFile = (p) => {
774
+ if (fs4.existsSync(p)) {
775
+ try {
776
+ let content = fs4.readFileSync(p, "utf-8");
777
+ if (content.includes("[model_providers.qwenproxy]") || /^model_provider\s*=\s*["']qwenproxy["']/m.test(content)) {
778
+ content = removeTomlSection(content, "model_providers.qwenproxy");
779
+ content = content.replace(/^model_provider\s*=\s*["']qwenproxy["']\r?\n?/m, "");
780
+ if (/^model\s*=\s*["']qwen/m.test(content)) {
781
+ content = content.replace(/^model\s*=\s*["']qwen[^"']*["']\r?\n?/m, "");
782
+ }
783
+ fs4.writeFileSync(p, content.trimEnd() + "\n", "utf-8");
784
+ manuallyCleaned = true;
785
+ }
786
+ } catch {
787
+ }
788
+ }
789
+ };
790
+ cleanSingleFile(filePath);
791
+ const home = os2.homedir();
792
+ const standardPath = path4.join(home, ".codex", "config.toml");
793
+ const codexHomePath = process.env.CODEX_HOME ? path4.join(process.env.CODEX_HOME, "config.toml") : void 0;
794
+ const isStandard = path4.resolve(filePath).toLowerCase() === path4.resolve(standardPath).toLowerCase();
795
+ const isCodexHome = Boolean(codexHomePath) && path4.resolve(filePath).toLowerCase() === path4.resolve(codexHomePath).toLowerCase();
796
+ if ((isStandard || isCodexHome) && !isRunningUnderNodeTest()) {
797
+ const alternatePath = isStandard ? codexHomePath : standardPath;
798
+ if (alternatePath && path4.resolve(alternatePath).toLowerCase() !== path4.resolve(filePath).toLowerCase()) {
799
+ cleanSingleFile(alternatePath);
800
+ }
801
+ }
802
+ const success = restoredFromBackup || manuallyCleaned;
803
+ return {
804
+ client: "codex",
805
+ filePath,
806
+ backupPath,
807
+ success,
808
+ action: success ? "restored" : "failed",
809
+ message: success ? restoredFromBackup ? "Restored Codex config from backup" : "Removed QwenProxy configuration from Codex config" : "Backup file not found"
810
+ };
811
+ }
812
+
813
+ // src/sync/opencode.ts
814
+ import fs5 from "node:fs";
815
+ import path5 from "node:path";
816
+ function buildOpenCodeProviderObject(baseUrl, apiKey, primaryModel = "qwen3.8-max", models) {
817
+ const modelsObj = {};
818
+ const modelList = Array.from(
819
+ new Set([primaryModel, ...models && models.length > 0 ? models : [primaryModel, "qwen3.7-plus"]].filter(Boolean))
820
+ );
821
+ for (const m of modelList) {
822
+ modelsObj[m] = {
823
+ name: formatModelDisplayName(m),
824
+ limit: { context: 1048576, output: 65536 },
825
+ modalities: { input: ["text", "image"], output: ["text"] },
826
+ reasoning: true,
827
+ variants: {
828
+ low: { effort: "low" },
829
+ medium: { effort: "medium" },
830
+ high: { effort: "high" }
831
+ }
832
+ };
833
+ }
834
+ return {
835
+ npm: "@ai-sdk/openai-compatible",
836
+ name: "QwenProxy",
837
+ options: {
838
+ baseURL: baseUrl,
839
+ apiKey
840
+ },
841
+ models: modelsObj
842
+ };
843
+ }
844
+ function findKeyObjectSpan(content, key) {
845
+ const regex = new RegExp(`"${key}"\\s*:\\s*\\{`);
846
+ const match = content.match(regex);
847
+ if (!match || match.index === void 0) return null;
848
+ const startIndex = match.index;
849
+ const braceIndex = content.indexOf("{", startIndex + match[0].length - 1);
850
+ if (braceIndex === -1) return null;
851
+ let depth = 0;
852
+ let inString = false;
853
+ let inLineComment = false;
854
+ let inBlockComment = false;
855
+ let escape = false;
856
+ for (let i = braceIndex; i < content.length; i++) {
857
+ const ch = content[i];
858
+ const nextCh = content[i + 1] || "";
859
+ if (inLineComment) {
860
+ if (ch === "\n") inLineComment = false;
861
+ continue;
862
+ }
863
+ if (inBlockComment) {
864
+ if (ch === "*" && nextCh === "/") {
865
+ inBlockComment = false;
866
+ i++;
867
+ }
868
+ continue;
869
+ }
870
+ if (inString) {
871
+ if (escape) {
872
+ escape = false;
873
+ } else if (ch === "\\") {
874
+ escape = true;
875
+ } else if (ch === '"') {
876
+ inString = false;
877
+ }
878
+ continue;
879
+ }
880
+ if (ch === "/" && nextCh === "/") {
881
+ inLineComment = true;
882
+ i++;
883
+ continue;
884
+ }
885
+ if (ch === "/" && nextCh === "*") {
886
+ inBlockComment = true;
887
+ i++;
888
+ continue;
889
+ }
890
+ if (ch === '"') {
891
+ inString = true;
892
+ continue;
893
+ }
894
+ if (ch === "{") {
895
+ depth++;
896
+ } else if (ch === "}") {
897
+ depth--;
898
+ if (depth === 0) {
899
+ let endIndex = i + 1;
900
+ let hasTrailingComma = false;
901
+ while (endIndex < content.length && /[\s,]/.test(content[endIndex])) {
902
+ if (content[endIndex] === ",") {
903
+ hasTrailingComma = true;
904
+ endIndex++;
905
+ break;
906
+ }
907
+ if (content[endIndex] === "\n") {
908
+ break;
909
+ }
910
+ endIndex++;
911
+ }
912
+ return { start: startIndex, end: endIndex, hasTrailingComma };
913
+ }
914
+ }
915
+ }
916
+ return null;
917
+ }
918
+ function openCodeModelId(model) {
919
+ return `qwenproxy/${model}`;
920
+ }
921
+ function applyOpenCodeActiveModel(content, model, setActive) {
922
+ if (!setActive) return content;
923
+ const id = openCodeModelId(model);
924
+ if (/"model"\s*:/.test(content)) {
925
+ return content.replace(/"model"\s*:\s*("[^"]*"|'[^']*')/, `"model": "${id}"`);
926
+ }
927
+ return content.replace(/\{/, `{
928
+ "model": "${id}",`);
929
+ }
930
+ function syncOpenCode(options) {
931
+ const {
932
+ filePath,
933
+ apiKey,
934
+ baseUrl,
935
+ model = "qwen3.8-max",
936
+ models,
937
+ setActive = true
938
+ } = options;
939
+ try {
940
+ fs5.mkdirSync(path5.dirname(filePath), { recursive: true });
941
+ let backupPath;
942
+ let content = "";
943
+ if (fs5.existsSync(filePath)) {
944
+ backupPath = createTimestampBackup(filePath);
945
+ content = fs5.readFileSync(filePath, "utf-8");
946
+ }
947
+ const providerObj = buildOpenCodeProviderObject(baseUrl, apiKey, model, models);
948
+ const providerJson = JSON.stringify(providerObj, null, 6).split("\n").map((line, idx) => idx === 0 ? line : ` ${line}`).join("\n");
949
+ const qwenEntry = ` "qwenproxy": ${providerJson}`;
950
+ if (!content.trim()) {
951
+ const initial = {
952
+ $schema: "https://opencode.ai/config.json"
953
+ };
954
+ if (setActive) {
955
+ initial.model = openCodeModelId(model);
956
+ }
957
+ initial.provider = {
958
+ qwenproxy: providerObj
959
+ };
960
+ fs5.writeFileSync(filePath, JSON.stringify(initial, null, 2) + "\n", "utf-8");
961
+ } else {
962
+ const existingSpan = findKeyObjectSpan(content, "qwenproxy");
963
+ if (existingSpan) {
964
+ const comma = existingSpan.hasTrailingComma ? "," : "";
965
+ content = content.slice(0, existingSpan.start) + `"qwenproxy": ${providerJson}${comma}` + content.slice(existingSpan.end);
966
+ } else {
967
+ const providerMatch = content.match(/"provider"\s*:\s*\{/);
968
+ if (providerMatch && providerMatch.index !== void 0) {
969
+ const insertIdx = providerMatch.index + providerMatch[0].length;
970
+ content = content.slice(0, insertIdx) + "\n" + qwenEntry + "," + content.slice(insertIdx);
971
+ } else {
972
+ const lastBraceIdx = content.lastIndexOf("}");
973
+ if (lastBraceIdx !== -1) {
974
+ const comma = content.slice(0, lastBraceIdx).trimEnd().endsWith("{") ? "" : ",";
975
+ content = content.slice(0, lastBraceIdx).trimEnd() + `${comma}
976
+ "provider": {
977
+ ${qwenEntry}
978
+ }
979
+ }
980
+ `;
981
+ }
982
+ }
983
+ }
984
+ fs5.writeFileSync(
985
+ filePath,
986
+ applyOpenCodeActiveModel(content, model, setActive),
987
+ "utf-8"
988
+ );
989
+ }
990
+ return {
991
+ client: "opencode",
992
+ filePath,
993
+ backupPath,
994
+ success: true,
995
+ action: backupPath ? "updated" : "created",
996
+ message: `Configured OpenCode with provider qwenproxy (${baseUrl})`
997
+ };
998
+ } catch (err) {
999
+ return {
1000
+ client: "opencode",
1001
+ filePath,
1002
+ success: false,
1003
+ action: "failed",
1004
+ error: err?.message || String(err)
1005
+ };
1006
+ }
1007
+ }
1008
+ function restoreOpenCode(filePath, backupPath) {
1009
+ const restoredFromBackup = restoreFromBackup(filePath, backupPath);
1010
+ let manuallyCleaned = false;
1011
+ if (fs5.existsSync(filePath)) {
1012
+ try {
1013
+ const data = JSON.parse(fs5.readFileSync(filePath, "utf-8"));
1014
+ if (data.provider && data.provider.qwenproxy) {
1015
+ delete data.provider.qwenproxy;
1016
+ if (data.model && data.model.includes("qwen")) {
1017
+ delete data.model;
1018
+ }
1019
+ if (Object.keys(data.provider).length === 0) {
1020
+ delete data.provider;
1021
+ }
1022
+ fs5.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n", "utf-8");
1023
+ manuallyCleaned = true;
1024
+ }
1025
+ } catch {
1026
+ }
1027
+ }
1028
+ const success = restoredFromBackup || manuallyCleaned;
1029
+ return {
1030
+ client: "opencode",
1031
+ filePath,
1032
+ backupPath,
1033
+ success,
1034
+ action: success ? "restored" : "failed",
1035
+ message: success ? restoredFromBackup ? "Restored OpenCode config from backup" : "Removed QwenProxy configuration from OpenCode config" : "Backup file not found"
1036
+ };
1037
+ }
1038
+
1039
+ // src/sync/omp.ts
1040
+ import fs6 from "node:fs";
1041
+ import path6 from "node:path";
1042
+ function buildOmpProviderYaml(baseUrl, apiKey, primaryModel = "qwen3.8-max", models) {
1043
+ const modelList = Array.from(
1044
+ new Set([primaryModel, ...models && models.length > 0 ? models : [primaryModel, "qwen3.7-plus"]].filter(Boolean))
1045
+ );
1046
+ const formattedModels = modelList.map(
1047
+ (m) => ` - id: ${m}
1048
+ name: ${formatModelDisplayName(m).replace(/\s+/g, "")}
1049
+ input: [text, image]
1050
+ contextWindow: 1000000
1051
+ maxTokens: 131072
1052
+ reasoning: true
1053
+ thinking:
1054
+ mode: effort
1055
+ efforts: [low, medium, high]`
1056
+ ).join("\n");
1057
+ return ` qwenproxy:
1058
+ baseUrl: ${baseUrl}
1059
+ api: openai-completions
1060
+ apiKey: "${apiKey}"
1061
+ compat:
1062
+ supportsStore: true
1063
+ supportsReasoningEffort: true
1064
+ maxTokensField: max_completion_tokens
1065
+ models:
1066
+ ${formattedModels}
1067
+ `;
1068
+ }
1069
+ function syncOmp(options) {
1070
+ const { filePath, apiKey, baseUrl, model = "qwen3.8-max", models } = options;
1071
+ try {
1072
+ fs6.mkdirSync(path6.dirname(filePath), { recursive: true });
1073
+ let backupPath;
1074
+ let content = "";
1075
+ if (fs6.existsSync(filePath)) {
1076
+ backupPath = createTimestampBackup(filePath);
1077
+ content = fs6.readFileSync(filePath, "utf-8");
1078
+ }
1079
+ const providerBlock = buildOmpProviderYaml(baseUrl, apiKey, model, models);
1080
+ if (!content.trim()) {
1081
+ content = `providers:
1082
+ ${providerBlock}`;
1083
+ } else {
1084
+ const existingQwenRegex = /^ {2}qwenproxy:[\s\S]*?(?=(?:^ {2}[a-zA-Z0-9_-]+:|\Z))/m;
1085
+ if (existingQwenRegex.test(content)) {
1086
+ content = content.replace(existingQwenRegex, providerBlock);
1087
+ } else {
1088
+ const providersMatch = content.match(/^providers:\s*$/m);
1089
+ if (providersMatch && providersMatch.index !== void 0) {
1090
+ const insertIdx = providersMatch.index + providersMatch[0].length;
1091
+ content = content.slice(0, insertIdx) + "\n" + providerBlock + content.slice(insertIdx);
1092
+ } else {
1093
+ content = content.trimEnd() + "\n\nproviders:\n" + providerBlock;
1094
+ }
1095
+ }
1096
+ }
1097
+ fs6.writeFileSync(filePath, content.trimEnd() + "\n", "utf-8");
1098
+ return {
1099
+ client: "omp",
1100
+ filePath,
1101
+ backupPath,
1102
+ success: true,
1103
+ action: backupPath ? "updated" : "created",
1104
+ message: `Configured OMP with provider qwenproxy (${baseUrl})`
1105
+ };
1106
+ } catch (err) {
1107
+ return {
1108
+ client: "omp",
1109
+ filePath,
1110
+ success: false,
1111
+ action: "failed",
1112
+ error: err?.message || String(err)
1113
+ };
1114
+ }
1115
+ }
1116
+ function restoreOmp(filePath, backupPath) {
1117
+ const restoredFromBackup = restoreFromBackup(filePath, backupPath);
1118
+ let manuallyCleaned = false;
1119
+ if (fs6.existsSync(filePath)) {
1120
+ try {
1121
+ let content = fs6.readFileSync(filePath, "utf-8");
1122
+ if (content.includes("qwenproxy:")) {
1123
+ const regex = /^\s*qwenproxy:\s*\r?\n(?:^[ \t].*\r?\n?)*/m;
1124
+ content = content.replace(regex, "");
1125
+ fs6.writeFileSync(filePath, content, "utf-8");
1126
+ manuallyCleaned = true;
1127
+ }
1128
+ } catch {
1129
+ }
1130
+ }
1131
+ const success = restoredFromBackup || manuallyCleaned;
1132
+ return {
1133
+ client: "omp",
1134
+ filePath,
1135
+ backupPath,
1136
+ success,
1137
+ action: success ? "restored" : "failed",
1138
+ message: success ? restoredFromBackup ? "Restored OMP models config from backup" : "Removed QwenProxy configuration from OMP config" : "Backup file not found"
1139
+ };
1140
+ }
1141
+
1142
+ // src/sync/hermes.ts
1143
+ import fs7 from "node:fs";
1144
+ import path7 from "node:path";
1145
+ function buildHermesModelBlock(baseUrl, apiKey, model = "qwen3.8-max", reasoningEffort = "high") {
1146
+ return `model:
1147
+ default: "${model}"
1148
+ provider: "custom"
1149
+ base_url: "${baseUrl}"
1150
+ api_key: "${apiKey}"
1151
+ reasoning_effort: "${reasoningEffort}"
1152
+ reasoning_overrides:
1153
+ "qwen3.8-max": "${reasoningEffort}"
1154
+ "qwen3.7-plus": "medium"
1155
+ `;
1156
+ }
1157
+ function buildHermesProviderBlock(baseUrl, apiKey) {
1158
+ return ` qwenproxy:
1159
+ base_url: "${baseUrl}"
1160
+ api_mode: chat_completions
1161
+ api_key: "${apiKey}"
1162
+ `;
1163
+ }
1164
+ function syncHermes(options) {
1165
+ const { filePath, apiKey, baseUrl, model = "qwen3.8-max", reasoningEffort = "high" } = options;
1166
+ try {
1167
+ fs7.mkdirSync(path7.dirname(filePath), { recursive: true });
1168
+ let backupPath;
1169
+ let content = "";
1170
+ if (fs7.existsSync(filePath)) {
1171
+ backupPath = createTimestampBackup(filePath);
1172
+ content = fs7.readFileSync(filePath, "utf-8");
1173
+ }
1174
+ const modelBlock = buildHermesModelBlock(baseUrl, apiKey, model, reasoningEffort);
1175
+ const providerBlock = buildHermesProviderBlock(baseUrl, apiKey);
1176
+ const modelRegex = /^model:[\s\S]*?(?=(?:^[a-zA-Z0-9_-]+:|\Z))/m;
1177
+ if (modelRegex.test(content)) {
1178
+ content = content.replace(modelRegex, modelBlock);
1179
+ } else {
1180
+ content = modelBlock + "\n" + content.trimStart();
1181
+ }
1182
+ if (!content.includes("providers:")) {
1183
+ content = content.trimEnd() + "\n\nproviders:\n" + providerBlock;
1184
+ } else {
1185
+ const existingQwenRegex = /^ {2}qwenproxy:[\s\S]*?(?=(?:^ {2}[a-zA-Z0-9_-]+:|^[a-zA-Z0-9_-]+:|\Z))/m;
1186
+ if (existingQwenRegex.test(content)) {
1187
+ content = content.replace(existingQwenRegex, providerBlock);
1188
+ } else {
1189
+ const providersMatch = content.match(/^providers:\s*$/m);
1190
+ if (providersMatch && providersMatch.index !== void 0) {
1191
+ const insertIdx = providersMatch.index + providersMatch[0].length;
1192
+ content = content.slice(0, insertIdx) + "\n" + providerBlock + content.slice(insertIdx);
1193
+ } else {
1194
+ content = content.trimEnd() + "\n\nproviders:\n" + providerBlock;
1195
+ }
1196
+ }
1197
+ }
1198
+ fs7.writeFileSync(filePath, content.trimEnd() + "\n", "utf-8");
1199
+ return {
1200
+ client: "hermes",
1201
+ filePath,
1202
+ backupPath,
1203
+ success: true,
1204
+ action: backupPath ? "updated" : "created",
1205
+ message: `Configured Hermes Agent with model ${model} and reasoning effort ${reasoningEffort}`
1206
+ };
1207
+ } catch (err) {
1208
+ return {
1209
+ client: "hermes",
1210
+ filePath,
1211
+ success: false,
1212
+ action: "failed",
1213
+ error: err?.message || String(err)
1214
+ };
1215
+ }
1216
+ }
1217
+ function restoreHermes(filePath, backupPath) {
1218
+ const restored = restoreFromBackup(filePath, backupPath);
1219
+ return {
1220
+ client: "hermes",
1221
+ filePath,
1222
+ backupPath,
1223
+ success: restored,
1224
+ action: restored ? "restored" : "failed",
1225
+ message: restored ? "Restored Hermes config from backup" : "Backup file not found"
1226
+ };
1227
+ }
1228
+
1229
+ // src/sync/openclaw.ts
1230
+ import fs8 from "node:fs";
1231
+ import path8 from "node:path";
1232
+ function findKeyObjectSpan2(content, key) {
1233
+ const regex = new RegExp(`"${key}"\\s*:\\s*\\{`);
1234
+ const match = content.match(regex);
1235
+ if (!match || match.index === void 0) return null;
1236
+ const startIndex = match.index;
1237
+ const braceIndex = content.indexOf("{", startIndex + match[0].length - 1);
1238
+ if (braceIndex === -1) return null;
1239
+ let depth = 0;
1240
+ let inString = false;
1241
+ let inLineComment = false;
1242
+ let inBlockComment = false;
1243
+ let escape = false;
1244
+ for (let i = braceIndex; i < content.length; i++) {
1245
+ const ch = content[i];
1246
+ const nextCh = content[i + 1] || "";
1247
+ if (inLineComment) {
1248
+ if (ch === "\n") inLineComment = false;
1249
+ continue;
1250
+ }
1251
+ if (inBlockComment) {
1252
+ if (ch === "*" && nextCh === "/") {
1253
+ inBlockComment = false;
1254
+ i++;
1255
+ }
1256
+ continue;
1257
+ }
1258
+ if (inString) {
1259
+ if (escape) {
1260
+ escape = false;
1261
+ } else if (ch === "\\") {
1262
+ escape = true;
1263
+ } else if (ch === '"') {
1264
+ inString = false;
1265
+ }
1266
+ continue;
1267
+ }
1268
+ if (ch === "/" && nextCh === "/") {
1269
+ inLineComment = true;
1270
+ i++;
1271
+ continue;
1272
+ }
1273
+ if (ch === "/" && nextCh === "*") {
1274
+ inBlockComment = true;
1275
+ i++;
1276
+ continue;
1277
+ }
1278
+ if (ch === '"') {
1279
+ inString = true;
1280
+ continue;
1281
+ }
1282
+ if (ch === "{") {
1283
+ depth++;
1284
+ } else if (ch === "}") {
1285
+ depth--;
1286
+ if (depth === 0) {
1287
+ let endIndex = i + 1;
1288
+ let hasTrailingComma = false;
1289
+ while (endIndex < content.length && /[\s,]/.test(content[endIndex])) {
1290
+ if (content[endIndex] === ",") {
1291
+ hasTrailingComma = true;
1292
+ endIndex++;
1293
+ break;
1294
+ }
1295
+ if (content[endIndex] === "\n") {
1296
+ break;
1297
+ }
1298
+ endIndex++;
1299
+ }
1300
+ return { start: startIndex, end: endIndex, hasTrailingComma };
1301
+ }
1302
+ }
1303
+ }
1304
+ return null;
1305
+ }
1306
+ function buildOpenClawProviderObject(baseUrl, apiKey, model = "qwen3.8-max", models) {
1307
+ const modelList = Array.from(
1308
+ new Set([model, ...models && models.length > 0 ? models : [model, "qwen3.7-plus"]].filter(Boolean))
1309
+ );
1310
+ const modelEntries = modelList.map((m) => ({
1311
+ id: m,
1312
+ name: formatModelDisplayName(m),
1313
+ reasoning: true,
1314
+ supportsReasoningEffort: true,
1315
+ supportedReasoningEfforts: ["low", "medium", "high"],
1316
+ contextWindow: 1e6,
1317
+ maxTokens: 65536
1318
+ }));
1319
+ return {
1320
+ baseUrl,
1321
+ apiKey,
1322
+ api: "openai-completions",
1323
+ models: modelEntries
1324
+ };
1325
+ }
1326
+ function syncOpenClaw(options) {
1327
+ const { filePath, apiKey, baseUrl, model = "qwen3.8-max", models, reasoningEffort = "high" } = options;
1328
+ try {
1329
+ fs8.mkdirSync(path8.dirname(filePath), { recursive: true });
1330
+ let backupPath;
1331
+ let content = "";
1332
+ if (fs8.existsSync(filePath)) {
1333
+ backupPath = createTimestampBackup(filePath);
1334
+ content = fs8.readFileSync(filePath, "utf-8");
1335
+ }
1336
+ const providerObj = buildOpenClawProviderObject(baseUrl, apiKey, model, models);
1337
+ const providerJson = JSON.stringify(providerObj, null, 6).split("\n").map((line, idx) => idx === 0 ? line : ` ${line}`).join("\n");
1338
+ const qwenEntry = ` "qwenproxy": ${providerJson}`;
1339
+ if (!content.trim()) {
1340
+ const initial = {
1341
+ models: {
1342
+ mode: "merge",
1343
+ providers: {
1344
+ qwenproxy: providerObj
1345
+ }
1346
+ },
1347
+ agents: {
1348
+ defaults: {
1349
+ model: {
1350
+ primary: `qwenproxy/${model}`
1351
+ },
1352
+ thinking: {
1353
+ effort: reasoningEffort
1354
+ }
1355
+ }
1356
+ }
1357
+ };
1358
+ fs8.writeFileSync(filePath, JSON.stringify(initial, null, 2) + "\n", "utf-8");
1359
+ } else {
1360
+ const existingSpan = findKeyObjectSpan2(content, "qwenproxy");
1361
+ if (existingSpan) {
1362
+ const comma = existingSpan.hasTrailingComma ? "," : "";
1363
+ content = content.slice(0, existingSpan.start) + `"qwenproxy": ${providerJson}${comma}` + content.slice(existingSpan.end);
1364
+ } else {
1365
+ const providersMatch = content.match(/"providers"\s*:\s*\{/);
1366
+ if (providersMatch && providersMatch.index !== void 0) {
1367
+ const insertIdx = providersMatch.index + providersMatch[0].length;
1368
+ content = content.slice(0, insertIdx) + "\n" + qwenEntry + "," + content.slice(insertIdx);
1369
+ } else {
1370
+ const modelsMatch = content.match(/"models"\s*:\s*\{/);
1371
+ if (modelsMatch && modelsMatch.index !== void 0) {
1372
+ const insertIdx = modelsMatch.index + modelsMatch[0].length;
1373
+ const providersBlock = `
1374
+ "providers": {
1375
+ ${qwenEntry}
1376
+ },`;
1377
+ content = content.slice(0, insertIdx) + providersBlock + content.slice(insertIdx);
1378
+ } else {
1379
+ const lastBraceIdx = content.lastIndexOf("}");
1380
+ if (lastBraceIdx !== -1) {
1381
+ const comma = content.slice(0, lastBraceIdx).trimEnd().endsWith("{") ? "" : ",";
1382
+ const modelsBlock = `${comma}
1383
+ "models": {
1384
+ "mode": "merge",
1385
+ "providers": {
1386
+ ${qwenEntry}
1387
+ }
1388
+ }
1389
+ `;
1390
+ content = content.slice(0, lastBraceIdx).trimEnd() + modelsBlock + "}\n";
1391
+ }
1392
+ }
1393
+ }
1394
+ }
1395
+ fs8.writeFileSync(filePath, content, "utf-8");
1396
+ }
1397
+ return {
1398
+ client: "openclaw",
1399
+ filePath,
1400
+ backupPath,
1401
+ success: true,
1402
+ action: backupPath ? "updated" : "created",
1403
+ message: `Configured OpenClaw with provider qwenproxy (${baseUrl}) and reasoning effort ${reasoningEffort}`
1404
+ };
1405
+ } catch (err) {
1406
+ return {
1407
+ client: "openclaw",
1408
+ filePath,
1409
+ success: false,
1410
+ action: "failed",
1411
+ error: err?.message || String(err)
1412
+ };
1413
+ }
1414
+ }
1415
+ function restoreOpenClaw(filePath, backupPath) {
1416
+ const restored = restoreFromBackup(filePath, backupPath);
1417
+ return {
1418
+ client: "openclaw",
1419
+ filePath,
1420
+ backupPath,
1421
+ success: restored,
1422
+ action: restored ? "restored" : "failed",
1423
+ message: restored ? "Restored OpenClaw config from backup" : "Backup file not found"
1424
+ };
1425
+ }
1426
+
1427
+ // src/sync/kilo.ts
1428
+ import fs9 from "node:fs";
1429
+ import path9 from "node:path";
1430
+ function findKeyObjectSpan3(content, key) {
1431
+ const regex = new RegExp(`"${key}"\\s*:\\s*\\{`);
1432
+ const match = content.match(regex);
1433
+ if (!match || match.index === void 0) return null;
1434
+ const startIndex = match.index;
1435
+ const braceIndex = content.indexOf("{", startIndex + match[0].length - 1);
1436
+ if (braceIndex === -1) return null;
1437
+ let depth = 0;
1438
+ let inString = false;
1439
+ let inLineComment = false;
1440
+ let inBlockComment = false;
1441
+ let escape = false;
1442
+ for (let i = braceIndex; i < content.length; i++) {
1443
+ const ch = content[i];
1444
+ const nextCh = content[i + 1] || "";
1445
+ if (inLineComment) {
1446
+ if (ch === "\n") inLineComment = false;
1447
+ continue;
1448
+ }
1449
+ if (inBlockComment) {
1450
+ if (ch === "*" && nextCh === "/") {
1451
+ inBlockComment = false;
1452
+ i++;
1453
+ }
1454
+ continue;
1455
+ }
1456
+ if (inString) {
1457
+ if (escape) {
1458
+ escape = false;
1459
+ } else if (ch === "\\") {
1460
+ escape = true;
1461
+ } else if (ch === '"') {
1462
+ inString = false;
1463
+ }
1464
+ continue;
1465
+ }
1466
+ if (ch === "/" && nextCh === "/") {
1467
+ inLineComment = true;
1468
+ i++;
1469
+ continue;
1470
+ }
1471
+ if (ch === "/" && nextCh === "*") {
1472
+ inBlockComment = true;
1473
+ i++;
1474
+ continue;
1475
+ }
1476
+ if (ch === '"') {
1477
+ inString = true;
1478
+ continue;
1479
+ }
1480
+ if (ch === "{") {
1481
+ depth++;
1482
+ } else if (ch === "}") {
1483
+ depth--;
1484
+ if (depth === 0) {
1485
+ let endIndex = i + 1;
1486
+ let hasTrailingComma = false;
1487
+ while (endIndex < content.length && /[\s,]/.test(content[endIndex])) {
1488
+ if (content[endIndex] === ",") {
1489
+ hasTrailingComma = true;
1490
+ endIndex++;
1491
+ break;
1492
+ }
1493
+ if (content[endIndex] === "\n") {
1494
+ break;
1495
+ }
1496
+ endIndex++;
1497
+ }
1498
+ return { start: startIndex, end: endIndex, hasTrailingComma };
1499
+ }
1500
+ }
1501
+ }
1502
+ return null;
1503
+ }
1504
+ function buildKiloProviderObject(baseUrl, apiKey, primaryModel = "qwen3.8-max", models) {
1505
+ const modelsObj = {};
1506
+ const modelList = Array.from(
1507
+ new Set([primaryModel, ...models && models.length > 0 ? models : [primaryModel, "qwen3.7-plus"]].filter(Boolean))
1508
+ );
1509
+ for (const m of modelList) {
1510
+ modelsObj[m] = {
1511
+ name: formatModelDisplayName(m),
1512
+ limit: { context: 1048576, output: 65536 },
1513
+ modalities: { input: ["text", "image"], output: ["text"] },
1514
+ reasoning: true,
1515
+ variants: {
1516
+ low: { effort: "low" },
1517
+ medium: { effort: "medium" },
1518
+ high: { effort: "high" }
1519
+ }
1520
+ };
1521
+ }
1522
+ return {
1523
+ npm: "@ai-sdk/openai-compatible",
1524
+ name: "QwenProxy",
1525
+ options: {
1526
+ baseURL: baseUrl,
1527
+ apiKey
1528
+ },
1529
+ models: modelsObj
1530
+ };
1531
+ }
1532
+ function syncKilo(options) {
1533
+ const { filePath, apiKey, baseUrl, model = "qwen3.8-max", models, setActive = true } = options;
1534
+ try {
1535
+ fs9.mkdirSync(path9.dirname(filePath), { recursive: true });
1536
+ let backupPath;
1537
+ let content = "";
1538
+ if (fs9.existsSync(filePath)) {
1539
+ backupPath = createTimestampBackup(filePath);
1540
+ content = fs9.readFileSync(filePath, "utf-8");
1541
+ }
1542
+ const providerObj = buildKiloProviderObject(baseUrl, apiKey, model, models);
1543
+ const providerJson = JSON.stringify(providerObj, null, 6).split("\n").map((line, idx) => idx === 0 ? line : ` ${line}`).join("\n");
1544
+ const qwenEntry = ` "qwenproxy": ${providerJson}`;
1545
+ if (!content.trim()) {
1546
+ const initial = {
1547
+ $schema: "https://kilo.ai/config.json",
1548
+ provider: {
1549
+ qwenproxy: providerObj
1550
+ }
1551
+ };
1552
+ if (setActive) {
1553
+ initial.model = `qwenproxy/${model}`;
1554
+ }
1555
+ fs9.writeFileSync(filePath, JSON.stringify(initial, null, 2) + "\n", "utf-8");
1556
+ } else {
1557
+ const existingSpan = findKeyObjectSpan3(content, "qwenproxy");
1558
+ if (existingSpan) {
1559
+ const comma = existingSpan.hasTrailingComma ? "," : "";
1560
+ content = content.slice(0, existingSpan.start) + `"qwenproxy": ${providerJson}${comma}` + content.slice(existingSpan.end);
1561
+ } else {
1562
+ const providerMatch = content.match(/"provider"\s*:\s*\{/);
1563
+ if (providerMatch && providerMatch.index !== void 0) {
1564
+ const insertIdx = providerMatch.index + providerMatch[0].length;
1565
+ content = content.slice(0, insertIdx) + "\n" + qwenEntry + "," + content.slice(insertIdx);
1566
+ } else {
1567
+ const lastBraceIdx = content.lastIndexOf("}");
1568
+ if (lastBraceIdx !== -1) {
1569
+ const comma = content.slice(0, lastBraceIdx).trimEnd().endsWith("{") ? "" : ",";
1570
+ content = content.slice(0, lastBraceIdx).trimEnd() + `${comma}
1571
+ "provider": {
1572
+ ${qwenEntry}
1573
+ }
1574
+ }
1575
+ `;
1576
+ }
1577
+ }
1578
+ }
1579
+ if (setActive) {
1580
+ const modelRegex = /"model"\s*:\s*"[^"]*"/;
1581
+ if (modelRegex.test(content)) {
1582
+ content = content.replace(modelRegex, `"model": "qwenproxy/${model}"`);
1583
+ }
1584
+ }
1585
+ fs9.writeFileSync(filePath, content, "utf-8");
1586
+ }
1587
+ return {
1588
+ client: "kilo",
1589
+ filePath,
1590
+ backupPath,
1591
+ success: true,
1592
+ action: backupPath ? "updated" : "created",
1593
+ message: `Configured Kilo Code with provider qwenproxy (${baseUrl})`
1594
+ };
1595
+ } catch (err) {
1596
+ return {
1597
+ client: "kilo",
1598
+ filePath,
1599
+ success: false,
1600
+ action: "failed",
1601
+ error: err?.message || String(err)
1602
+ };
1603
+ }
1604
+ }
1605
+ function restoreKilo(filePath, backupPath) {
1606
+ const restored = restoreFromBackup(filePath, backupPath);
1607
+ return {
1608
+ client: "kilo",
1609
+ filePath,
1610
+ backupPath,
1611
+ success: restored,
1612
+ action: restored ? "restored" : "failed",
1613
+ message: restored ? "Restored Kilo Code config from backup" : "Backup file not found"
1614
+ };
1615
+ }
1616
+
1617
+ // src/sync/cline.ts
1618
+ import fs10 from "node:fs";
1619
+ import path10 from "node:path";
1620
+ import Database from "better-sqlite3";
1621
+ var TARGET_KEYS = [
1622
+ "saoudrizwan.claude-dev",
1623
+ "ZooCodeOrganization.zoo-code",
1624
+ "RooVeterinaryInc.roo-cline"
1625
+ ];
1626
+ function syncCline(options) {
1627
+ const { filePath, baseUrl, model = "qwen3.8-max", reasoningEffort = "high" } = options;
1628
+ try {
1629
+ fs10.mkdirSync(path10.dirname(filePath), { recursive: true });
1630
+ let backupPath;
1631
+ if (fs10.existsSync(filePath)) {
1632
+ backupPath = createTimestampBackup(filePath);
1633
+ }
1634
+ const db = new Database(filePath);
1635
+ db.exec(`CREATE TABLE IF NOT EXISTS ItemTable (key TEXT UNIQUE ON CONFLICT REPLACE, value TEXT)`);
1636
+ const selectStmt = db.prepare(`SELECT value FROM ItemTable WHERE key = ?`);
1637
+ const insertStmt = db.prepare(`INSERT INTO ItemTable (key, value) VALUES (?, ?)`);
1638
+ const updateConfig = (existingVal) => {
1639
+ let parsed = {};
1640
+ if (existingVal) {
1641
+ try {
1642
+ parsed = JSON.parse(existingVal);
1643
+ } catch {
1644
+ parsed = {};
1645
+ }
1646
+ }
1647
+ parsed.apiProvider = "openai";
1648
+ parsed.openAiBaseUrl = baseUrl;
1649
+ parsed.openAiModelId = model;
1650
+ parsed.enableReasoningEffort = true;
1651
+ parsed.reasoningEffort = reasoningEffort;
1652
+ parsed.openAiCustomModelInfo = {
1653
+ ...parsed.openAiCustomModelInfo || {},
1654
+ contextWindow: 1e6,
1655
+ maxTokens: -1,
1656
+ supportsImages: true,
1657
+ supportsPromptCache: false,
1658
+ inputPrice: 0,
1659
+ outputPrice: 0
1660
+ };
1661
+ if (Array.isArray(parsed.listApiConfigMeta)) {
1662
+ if (!parsed.listApiConfigMeta.some((p) => p?.name === "QwenProxy")) {
1663
+ parsed.listApiConfigMeta.push({
1664
+ name: "QwenProxy",
1665
+ id: "qwenproxy-local",
1666
+ apiProvider: "openai",
1667
+ modelId: model
1668
+ });
1669
+ }
1670
+ parsed.currentApiConfigName = "QwenProxy";
1671
+ }
1672
+ return JSON.stringify(parsed);
1673
+ };
1674
+ for (const key of TARGET_KEYS) {
1675
+ const row = selectStmt.get(key);
1676
+ if (row || key === "saoudrizwan.claude-dev" || key === "ZooCodeOrganization.zoo-code") {
1677
+ const newVal = updateConfig(row?.value);
1678
+ insertStmt.run(key, newVal);
1679
+ }
1680
+ }
1681
+ db.close();
1682
+ return {
1683
+ client: "cline",
1684
+ filePath,
1685
+ backupPath,
1686
+ success: true,
1687
+ action: backupPath ? "updated" : "created",
1688
+ message: `Configured Cline in state.vscdb with model ${model} and reasoning effort ${reasoningEffort}`
1689
+ };
1690
+ } catch (err) {
1691
+ return {
1692
+ client: "cline",
1693
+ filePath,
1694
+ success: false,
1695
+ action: "failed",
1696
+ error: err?.message || String(err)
1697
+ };
1698
+ }
1699
+ }
1700
+ function restoreCline(filePath, backupPath) {
1701
+ const restoredFromBackup = restoreFromBackup(filePath, backupPath);
1702
+ let manuallyCleaned = false;
1703
+ if (fs10.existsSync(filePath)) {
1704
+ try {
1705
+ const db = new Database(filePath);
1706
+ const rows = db.prepare("SELECT key, value FROM ItemTable WHERE key = 'saoudrizwan.claude-dev' OR key = 'ZooCodeOrganization.zoo-code'").all();
1707
+ for (const row of rows) {
1708
+ try {
1709
+ const parsed = JSON.parse(row.value);
1710
+ if (parsed.openAiBaseUrl?.includes("7936") || parsed.openAiModelId?.includes("qwen")) {
1711
+ delete parsed.openAiBaseUrl;
1712
+ delete parsed.openAiApiKey;
1713
+ delete parsed.openAiModelId;
1714
+ db.prepare("UPDATE ItemTable SET value = ? WHERE key = ?").run(JSON.stringify(parsed), row.key);
1715
+ manuallyCleaned = true;
1716
+ }
1717
+ } catch {
1718
+ }
1719
+ }
1720
+ db.close();
1721
+ } catch {
1722
+ }
1723
+ }
1724
+ const success = restoredFromBackup || manuallyCleaned;
1725
+ return {
1726
+ client: "cline",
1727
+ filePath,
1728
+ backupPath,
1729
+ success,
1730
+ action: success ? "restored" : "failed",
1731
+ message: success ? restoredFromBackup ? "Restored Cline settings from backup" : "Removed QwenProxy configuration from Cline settings" : "Backup file not found"
1732
+ };
1733
+ }
1734
+
1735
+ // src/sync/zed.ts
1736
+ import fs11 from "node:fs";
1737
+ import path11 from "node:path";
1738
+ function parseJsonWithComments(raw) {
1739
+ let result = "";
1740
+ let inString = false;
1741
+ let inLineComment = false;
1742
+ let inBlockComment = false;
1743
+ let escape = false;
1744
+ for (let i = 0; i < raw.length; i++) {
1745
+ const ch = raw[i];
1746
+ const nextCh = raw[i + 1] || "";
1747
+ if (inLineComment) {
1748
+ if (ch === "\n") {
1749
+ inLineComment = false;
1750
+ result += ch;
1751
+ }
1752
+ continue;
1753
+ }
1754
+ if (inBlockComment) {
1755
+ if (ch === "*" && nextCh === "/") {
1756
+ inBlockComment = false;
1757
+ i++;
1758
+ }
1759
+ continue;
1760
+ }
1761
+ if (inString) {
1762
+ result += ch;
1763
+ if (escape) {
1764
+ escape = false;
1765
+ } else if (ch === "\\") {
1766
+ escape = true;
1767
+ } else if (ch === '"') {
1768
+ inString = false;
1769
+ }
1770
+ continue;
1771
+ }
1772
+ if (ch === "/" && nextCh === "/") {
1773
+ inLineComment = true;
1774
+ i++;
1775
+ continue;
1776
+ }
1777
+ if (ch === "/" && nextCh === "*") {
1778
+ inBlockComment = true;
1779
+ i++;
1780
+ continue;
1781
+ }
1782
+ if (ch === '"') {
1783
+ inString = true;
1784
+ result += ch;
1785
+ continue;
1786
+ }
1787
+ result += ch;
1788
+ }
1789
+ const cleaned = result.replace(/,\s*([\}\]])/g, "$1");
1790
+ return JSON.parse(cleaned);
1791
+ }
1792
+ function buildZedAvailableModels(primaryModel = "qwen3.8-max", models) {
1793
+ const modelList = Array.from(
1794
+ new Set([primaryModel, ...models && models.length > 0 ? models : [primaryModel, "qwen3.7-plus"]].filter(Boolean))
1795
+ );
1796
+ const zedModels = [];
1797
+ for (const m of modelList) {
1798
+ zedModels.push({
1799
+ name: m,
1800
+ max_tokens: 1e6,
1801
+ max_output_tokens: 131072,
1802
+ max_completion_tokens: 131072,
1803
+ capabilities: {
1804
+ tools: true,
1805
+ images: true,
1806
+ parallel_tool_calls: true,
1807
+ prompt_cache_key: true,
1808
+ chat_completions: true,
1809
+ interleaved_reasoning: true
1810
+ }
1811
+ });
1812
+ if (m === primaryModel) {
1813
+ zedModels.push(
1814
+ {
1815
+ name: `${m}-thinking`,
1816
+ max_tokens: 1e6,
1817
+ max_output_tokens: 131072,
1818
+ max_completion_tokens: 131072,
1819
+ capabilities: {
1820
+ tools: true,
1821
+ images: true,
1822
+ parallel_tool_calls: true,
1823
+ prompt_cache_key: true,
1824
+ chat_completions: true,
1825
+ interleaved_reasoning: true
1826
+ }
1827
+ },
1828
+ {
1829
+ name: `${m}-fast`,
1830
+ max_tokens: 1e6,
1831
+ max_output_tokens: 131072,
1832
+ max_completion_tokens: 131072,
1833
+ capabilities: {
1834
+ tools: true,
1835
+ images: true,
1836
+ parallel_tool_calls: true,
1837
+ prompt_cache_key: true,
1838
+ chat_completions: true,
1839
+ interleaved_reasoning: true
1840
+ }
1841
+ }
1842
+ );
1843
+ }
1844
+ }
1845
+ return zedModels;
1846
+ }
1847
+ function syncZed(options) {
1848
+ const { filePath, baseUrl, model = "qwen3.8-max", models, setActive = true } = options;
1849
+ try {
1850
+ fs11.mkdirSync(path11.dirname(filePath), { recursive: true });
1851
+ let backupPath;
1852
+ let existingSettings = {};
1853
+ if (fs11.existsSync(filePath)) {
1854
+ backupPath = createTimestampBackup(filePath);
1855
+ try {
1856
+ const content = fs11.readFileSync(filePath, "utf-8");
1857
+ existingSettings = parseJsonWithComments(content);
1858
+ } catch {
1859
+ existingSettings = {};
1860
+ }
1861
+ }
1862
+ const languageModels = existingSettings.language_models || {};
1863
+ const openaiCompatible = languageModels.openai_compatible || {};
1864
+ openaiCompatible.QwenProxy = {
1865
+ api_url: baseUrl,
1866
+ available_models: buildZedAvailableModels(model, models)
1867
+ };
1868
+ const updatedSettings = {
1869
+ ...existingSettings,
1870
+ language_models: {
1871
+ ...languageModels,
1872
+ openai_compatible: openaiCompatible
1873
+ }
1874
+ };
1875
+ if (setActive) {
1876
+ const existingAgent = existingSettings.agent || {};
1877
+ updatedSettings.agent = {
1878
+ ...existingAgent,
1879
+ default_model: {
1880
+ provider: "QwenProxy",
1881
+ model,
1882
+ enable_thinking: true
1883
+ }
1884
+ };
1885
+ }
1886
+ fs11.writeFileSync(filePath, JSON.stringify(updatedSettings, null, 2) + "\n", "utf-8");
1887
+ return {
1888
+ client: "zed",
1889
+ filePath,
1890
+ backupPath,
1891
+ success: true,
1892
+ action: backupPath ? "updated" : "created",
1893
+ message: `Configured Zed Editor with QwenProxy (${baseUrl}) and model ${model}`
1894
+ };
1895
+ } catch (err) {
1896
+ return {
1897
+ client: "zed",
1898
+ filePath,
1899
+ success: false,
1900
+ action: "failed",
1901
+ error: err?.message || String(err)
1902
+ };
1903
+ }
1904
+ }
1905
+ function restoreZed(filePath, backupPath) {
1906
+ const restoredFromBackup = restoreFromBackup(filePath, backupPath);
1907
+ let manuallyCleaned = false;
1908
+ if (fs11.existsSync(filePath)) {
1909
+ try {
1910
+ let content = fs11.readFileSync(filePath, "utf-8");
1911
+ if (content.includes("127.0.0.1:7936") || content.includes("qwen3.8-max")) {
1912
+ const data = parseJsonWithComments(content);
1913
+ if (data.language_models?.openai?.api_url?.includes("7936")) {
1914
+ delete data.language_models.openai;
1915
+ if (Object.keys(data.language_models).length === 0) {
1916
+ delete data.language_models;
1917
+ }
1918
+ fs11.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n", "utf-8");
1919
+ manuallyCleaned = true;
1920
+ }
1921
+ }
1922
+ } catch {
1923
+ }
1924
+ }
1925
+ const success = restoredFromBackup || manuallyCleaned;
1926
+ return {
1927
+ client: "zed",
1928
+ filePath,
1929
+ backupPath,
1930
+ success,
1931
+ action: success ? "restored" : "failed",
1932
+ message: success ? restoredFromBackup ? "Restored Zed settings from backup" : "Removed QwenProxy configuration from Zed settings" : "Backup file not found"
1933
+ };
1934
+ }
1935
+
1936
+ // src/sync/aider.ts
1937
+ import fs12 from "node:fs";
1938
+ import path12 from "node:path";
1939
+ function updateYamlKeyValue(content, key, value) {
1940
+ const regex = new RegExp(`^${key}:.*$`, "m");
1941
+ const line = `${key}: ${value}`;
1942
+ if (regex.test(content)) {
1943
+ return content.replace(regex, line);
1944
+ }
1945
+ return content.trimEnd() + (content.length > 0 ? "\n" : "") + line + "\n";
1946
+ }
1947
+ function syncAider(options) {
1948
+ const {
1949
+ filePath,
1950
+ apiKey,
1951
+ baseUrl,
1952
+ model = "qwen3.8-max",
1953
+ reasoningEffort = "high",
1954
+ modelSettingsPath
1955
+ } = options;
1956
+ try {
1957
+ fs12.mkdirSync(path12.dirname(filePath), { recursive: true });
1958
+ let backupPath;
1959
+ let confContent = "";
1960
+ if (fs12.existsSync(filePath)) {
1961
+ backupPath = createTimestampBackup(filePath);
1962
+ confContent = fs12.readFileSync(filePath, "utf-8");
1963
+ }
1964
+ const fullModelName = model.startsWith("openai/") ? model : `openai/${model}`;
1965
+ const weakModelName = "openai/qwen3.7-plus";
1966
+ confContent = updateYamlKeyValue(confContent, "openai-api-base", baseUrl);
1967
+ confContent = updateYamlKeyValue(confContent, "openai-api-key", apiKey);
1968
+ confContent = updateYamlKeyValue(confContent, "model", fullModelName);
1969
+ confContent = updateYamlKeyValue(confContent, "editor-model", fullModelName);
1970
+ confContent = updateYamlKeyValue(confContent, "weak-model", weakModelName);
1971
+ fs12.writeFileSync(filePath, confContent.trimEnd() + "\n", "utf-8");
1972
+ let extraBackupPath;
1973
+ const resolvedSettingsPath = modelSettingsPath || path12.join(path12.dirname(filePath), ".aider.model.settings.yml");
1974
+ if (resolvedSettingsPath) {
1975
+ fs12.mkdirSync(path12.dirname(resolvedSettingsPath), { recursive: true });
1976
+ let settingsContent = "";
1977
+ if (fs12.existsSync(resolvedSettingsPath)) {
1978
+ extraBackupPath = createTimestampBackup(resolvedSettingsPath);
1979
+ settingsContent = fs12.readFileSync(resolvedSettingsPath, "utf-8");
1980
+ }
1981
+ const modelBlock = `- name: ${fullModelName}
1982
+ extra_params:
1983
+ extra_body:
1984
+ reasoning_effort: ${reasoningEffort}
1985
+ streaming: true
1986
+ `;
1987
+ const existingModelRegex = new RegExp(`- name: ${fullModelName}[\\s\\S]*?(?=(?:^-[ \\t]+name:|\\Z))`, "m");
1988
+ if (existingModelRegex.test(settingsContent)) {
1989
+ settingsContent = settingsContent.replace(existingModelRegex, modelBlock.trimEnd() + "\n");
1990
+ } else {
1991
+ settingsContent = settingsContent.trimEnd() + (settingsContent.length > 0 ? "\n\n" : "") + modelBlock;
1992
+ }
1993
+ fs12.writeFileSync(resolvedSettingsPath, settingsContent.trimEnd() + "\n", "utf-8");
1994
+ }
1995
+ return {
1996
+ client: "aider",
1997
+ filePath,
1998
+ backupPath,
1999
+ extraBackupPath,
2000
+ success: true,
2001
+ action: backupPath ? "updated" : "created",
2002
+ message: `Configured Aider with model ${fullModelName} and reasoning effort ${reasoningEffort}`
2003
+ };
2004
+ } catch (err) {
2005
+ return {
2006
+ client: "aider",
2007
+ filePath,
2008
+ success: false,
2009
+ action: "failed",
2010
+ error: err?.message || String(err)
2011
+ };
2012
+ }
2013
+ }
2014
+ function restoreAider(filePath, backupPath, modelSettingsPath, extraBackupPath) {
2015
+ const mainRestored = restoreFromBackup(filePath, backupPath);
2016
+ if (modelSettingsPath && extraBackupPath) {
2017
+ restoreFromBackup(modelSettingsPath, extraBackupPath);
2018
+ }
2019
+ return {
2020
+ client: "aider",
2021
+ filePath,
2022
+ backupPath,
2023
+ extraBackupPath,
2024
+ success: mainRestored,
2025
+ action: mainRestored ? "restored" : "failed",
2026
+ message: mainRestored ? "Restored Aider settings from backup" : "Backup file not found"
2027
+ };
2028
+ }
2029
+
2030
+ // src/sync/index.ts
2031
+ function resolveApiKey(overrideKey, configKey, host = "127.0.0.1") {
2032
+ if (overrideKey && overrideKey.trim().length > 0) {
2033
+ if (isPlaceholderApiKey(overrideKey) && !isLoopbackHost(host)) {
2034
+ throw new Error(
2035
+ `Refusing to sync clients with placeholder API key ${PLACEHOLDER_API_KEY}. Set API_KEY first.`
2036
+ );
2037
+ }
2038
+ return overrideKey.trim();
2039
+ }
2040
+ const envKey = (!isPlaceholderApiKey(process.env.API_KEY) ? process.env.API_KEY : void 0) || (!isPlaceholderApiKey(process.env.ADMIN_PASSWORD) ? process.env.ADMIN_PASSWORD : void 0) || configKey;
2041
+ if (envKey && !isPlaceholderApiKey(envKey)) {
2042
+ return envKey.trim();
2043
+ }
2044
+ if (isLoopbackHost(host)) return PLACEHOLDER_API_KEY;
2045
+ throw new Error(
2046
+ `Refusing to sync clients with placeholder API key ${PLACEHOLDER_API_KEY}. Set API_KEY or start the proxy once to generate one.`
2047
+ );
2048
+ }
2049
+ function normalizeClientName(name) {
2050
+ const clean = name.trim().toLowerCase().replace(/[-_ ]/g, "");
2051
+ if (clean === "claude" || clean === "claudecode" || clean === "anthropic") return "claude-code";
2052
+ if (clean === "codex" || clean === "codexcli" || clean === "openai") return "codex";
2053
+ if (clean === "opencode") return "opencode";
2054
+ if (clean === "omp" || clean === "ohmypi" || clean === "pi") return "omp";
2055
+ if (clean === "hermes" || clean === "hermesagent" || clean === "nous" || clean === "nousresearch") return "hermes";
2056
+ if (clean === "openclaw" || clean === "claw" || clean === "clawdbot" || clean === "moltbot") return "openclaw";
2057
+ if (clean === "kilo" || clean === "kilocode") return "kilo";
2058
+ if (clean === "cline" || clean === "claudedev" || clean === "zoo" || clean === "zoocode" || clean === "roo" || clean === "roocode" || clean === "roocline") {
2059
+ return "cline";
2060
+ }
2061
+ if (clean === "zed" || clean === "zededitor") return "zed";
2062
+ if (clean === "aider" || clean === "aiderchat") return "aider";
2063
+ return null;
2064
+ }
2065
+ function resolveBaseUrls(port = 7936, host = "127.0.0.1") {
2066
+ const cleanHost = host === "0.0.0.0" ? "127.0.0.1" : host;
2067
+ return {
2068
+ anthropicBaseUrl: `http://${cleanHost}:${port}`,
2069
+ openaiBaseUrl: `http://${cleanHost}:${port}/v1`
2070
+ };
2071
+ }
2072
+ function getDefaultPaths() {
2073
+ const home = os3.homedir();
2074
+ const isWindows = process.platform === "win32";
2075
+ const isMac = process.platform === "darwin";
2076
+ const appData = process.env.APPDATA || (isWindows ? path13.join(home, "AppData", "Roaming") : "");
2077
+ const openCodeCandidates = [
2078
+ path13.join(home, ".config", "opencode", "opencode.jsonc"),
2079
+ path13.join(home, ".config", "opencode", "opencode.json"),
2080
+ path13.join(home, ".opencode", "opencode.jsonc"),
2081
+ path13.join(home, ".opencode", "opencode.json")
2082
+ ];
2083
+ const existingOpenCode = openCodeCandidates.find((p) => fs13.existsSync(p));
2084
+ const kiloCandidates = [
2085
+ path13.join(home, ".config", "kilo", "kilo.json"),
2086
+ path13.join(home, ".kilo", "kilo.json"),
2087
+ path13.join(home, ".config", "kilo", "config.json"),
2088
+ path13.join(home, ".kilo", "config.json")
2089
+ ];
2090
+ const existingKilo = kiloCandidates.find((p) => fs13.existsSync(p));
2091
+ let clinePath;
2092
+ if (isWindows) {
2093
+ clinePath = path13.join(appData, "Code", "User", "globalStorage", "state.vscdb");
2094
+ } else if (isMac) {
2095
+ clinePath = path13.join(home, "Library", "Application Support", "Code", "User", "globalStorage", "state.vscdb");
2096
+ } else {
2097
+ clinePath = path13.join(home, ".config", "Code", "User", "globalStorage", "state.vscdb");
2098
+ }
2099
+ let zedPath;
2100
+ if (isWindows) {
2101
+ zedPath = path13.join(appData, "Zed", "settings.json");
2102
+ } else {
2103
+ zedPath = path13.join(home, ".config", "zed", "settings.json");
2104
+ }
2105
+ return {
2106
+ claudeCode: path13.join(home, ".claude", "settings.json"),
2107
+ codex: fs13.existsSync(path13.join(home, ".codex", "config.toml")) ? path13.join(home, ".codex", "config.toml") : process.env.CODEX_HOME ? path13.join(process.env.CODEX_HOME, "config.toml") : path13.join(home, ".codex", "config.toml"),
2108
+ openCode: existingOpenCode || openCodeCandidates[0],
2109
+ omp: path13.join(home, ".omp", "agent", "models.yml"),
2110
+ hermes: path13.join(home, ".hermes", "config.yaml"),
2111
+ openClaw: path13.join(home, ".openclaw", "openclaw.json"),
2112
+ kilo: existingKilo || kiloCandidates[0],
2113
+ cline: clinePath,
2114
+ zed: zedPath,
2115
+ aider: path13.join(home, ".aider.conf.yml")
2116
+ };
2117
+ }
2118
+ function getDefaultStateFilePath() {
2119
+ return getSyncStatePath();
2120
+ }
2121
+ function isExecutableInPath(name) {
2122
+ const envPath2 = process.env.PATH || "";
2123
+ const dirs = envPath2.split(path13.delimiter).filter(Boolean);
2124
+ const extensions = process.platform === "win32" ? (process.env.PATHEXT || ".COM;.EXE;.BAT;.CMD").split(";").map((e) => e.toLowerCase()) : [""];
2125
+ for (const dir of dirs) {
2126
+ for (const ext of extensions) {
2127
+ const fullPath = path13.join(dir, name + ext);
2128
+ try {
2129
+ if (fs13.existsSync(fullPath) && !fs13.statSync(fullPath).isDirectory()) {
2130
+ return true;
2131
+ }
2132
+ } catch {
2133
+ }
2134
+ }
2135
+ }
2136
+ return false;
2137
+ }
2138
+ function isVscodeExtensionInstalled(pattern) {
2139
+ const home = os3.homedir();
2140
+ const candidateDirs = [
2141
+ path13.join(home, ".vscode", "extensions"),
2142
+ path13.join(home, ".vscode-insiders", "extensions"),
2143
+ path13.join(home, ".cursor", "extensions"),
2144
+ path13.join(home, ".windsurf", "extensions")
2145
+ ];
2146
+ for (const d of candidateDirs) {
2147
+ if (fs13.existsSync(d)) {
2148
+ try {
2149
+ const entries = fs13.readdirSync(d);
2150
+ if (entries.some((e) => pattern.test(e))) return true;
2151
+ } catch {
2152
+ }
2153
+ }
2154
+ }
2155
+ return false;
2156
+ }
2157
+ function isZedInstalled() {
2158
+ if (isExecutableInPath("zed")) return true;
2159
+ if (process.platform === "win32") {
2160
+ const local = process.env.LOCALAPPDATA || "";
2161
+ if (fs13.existsSync(path13.join(local, "Programs", "Zed"))) return true;
2162
+ } else if (process.platform === "darwin") {
2163
+ if (fs13.existsSync("/Applications/Zed.app")) return true;
2164
+ }
2165
+ return false;
2166
+ }
2167
+ function isMatchingLocalHost(text, port = 7936) {
2168
+ if (!text) return false;
2169
+ const configuredPort = config.server?.port || 7936;
2170
+ const ports = /* @__PURE__ */ new Set([String(port), String(configuredPort), "7936", "3000"]);
2171
+ const isLocal = text.includes("127.0.0.1") || text.includes("localhost") || text.includes("0.0.0.0");
2172
+ return isLocal && Array.from(ports).some((p) => text.includes(`:${p}`));
2173
+ }
2174
+ function isClientToolInstalled(id, targetPath, isCustomTestPath = false) {
2175
+ if (isCustomTestPath) {
2176
+ return fs13.existsSync(targetPath);
2177
+ }
2178
+ switch (id) {
2179
+ case "hermes":
2180
+ return isExecutableInPath("hermes");
2181
+ case "openclaw":
2182
+ return isExecutableInPath("openclaw") || isExecutableInPath("clawdbot") || isExecutableInPath("moltbot");
2183
+ case "aider":
2184
+ return isExecutableInPath("aider");
2185
+ case "kilo":
2186
+ return isExecutableInPath("kilo") || isVscodeExtensionInstalled(/kilo/i);
2187
+ case "cline":
2188
+ return isExecutableInPath("cline") || isVscodeExtensionInstalled(/cline|zoo-code|roo-cline/i);
2189
+ case "zed":
2190
+ return isZedInstalled();
2191
+ case "claude-code":
2192
+ return isExecutableInPath("claude") || fs13.existsSync(targetPath);
2193
+ case "codex":
2194
+ return isExecutableInPath("codex") || fs13.existsSync(targetPath);
2195
+ case "opencode":
2196
+ return isExecutableInPath("opencode") || fs13.existsSync(targetPath);
2197
+ case "omp":
2198
+ return isExecutableInPath("omp") || fs13.existsSync(targetPath);
2199
+ default:
2200
+ return fs13.existsSync(targetPath);
2201
+ }
2202
+ }
2203
+ function inspectClientSyncStatus(id, filePath, port = 7936) {
2204
+ const defaultPaths = getDefaultPaths();
2205
+ const defaultTarget = defaultPaths[id] || "";
2206
+ const isCustomTestPath = Boolean(
2207
+ filePath && defaultTarget && path13.resolve(filePath) !== path13.resolve(defaultTarget)
2208
+ );
2209
+ const targetPath = filePath || defaultTarget;
2210
+ if (!fs13.existsSync(targetPath)) {
2211
+ return { id, installed: false, synced: false };
2212
+ }
2213
+ const toolInstalled = isClientToolInstalled(id, targetPath, isCustomTestPath);
2214
+ if (!toolInstalled) {
2215
+ return { id, installed: false, synced: false };
2216
+ }
2217
+ try {
2218
+ if (id === "cline") {
2219
+ let isSynced = false;
2220
+ let model;
2221
+ let rowExists = false;
2222
+ try {
2223
+ const db = new Database2(targetPath, { readonly: true });
2224
+ const row = db.prepare(
2225
+ `SELECT value FROM ItemTable WHERE key = 'saoudrizwan.claude-dev' OR key = 'ZooCodeOrganization.zoo-code' LIMIT 1`
2226
+ ).get();
2227
+ db.close();
2228
+ if (row && row.value) {
2229
+ rowExists = true;
2230
+ const parsed = JSON.parse(row.value);
2231
+ const url = parsed.openAiBaseUrl || "";
2232
+ model = parsed.openAiModelId;
2233
+ isSynced = isMatchingLocalHost(url, port);
2234
+ }
2235
+ } catch {
2236
+ }
2237
+ const isInstalled = isCustomTestPath ? true : rowExists || isExecutableInPath("cline") || isVscodeExtensionInstalled(/cline|zoo-code|roo-cline/i);
2238
+ return { id, installed: isInstalled, synced: isInstalled && isSynced, model };
2239
+ }
2240
+ const raw = fs13.readFileSync(targetPath, "utf-8");
2241
+ if (id === "claude-code") {
2242
+ const data = JSON.parse(raw);
2243
+ const url = data?.env?.ANTHROPIC_BASE_URL || "";
2244
+ const model = data?.env?.ANTHROPIC_MODEL || data?.model || "";
2245
+ const isSynced = isMatchingLocalHost(url, port) && (model.toLowerCase().includes("qwen") || Boolean(data?.env?.ANTHROPIC_AUTH_TOKEN));
2246
+ return {
2247
+ id,
2248
+ installed: true,
2249
+ synced: isSynced,
2250
+ model: model || void 0,
2251
+ url: url || void 0
2252
+ };
2253
+ }
2254
+ if (id === "codex") {
2255
+ const hasProvider = raw.includes("[model_providers.qwenproxy]");
2256
+ const isProviderActive = /^model_provider\s*=\s*["']qwenproxy["']/m.test(raw);
2257
+ const modelMatch = raw.match(/^model\s*=\s*["']([^"']+)["']/m);
2258
+ const model = modelMatch ? modelMatch[1] : void 0;
2259
+ const urlMatch = raw.match(/\[model_providers\.qwenproxy\][\s\S]*?base_url\s*=\s*["']([^"']+)["']/);
2260
+ const url = urlMatch ? urlMatch[1] : "";
2261
+ const isSynced = hasProvider && isProviderActive && isMatchingLocalHost(url, port);
2262
+ return {
2263
+ id,
2264
+ installed: true,
2265
+ synced: isSynced,
2266
+ model
2267
+ };
2268
+ }
2269
+ if (id === "opencode") {
2270
+ let isSynced = false;
2271
+ try {
2272
+ const data = JSON.parse(raw);
2273
+ const provider = data?.provider?.qwenproxy;
2274
+ const url = provider?.options?.baseURL || "";
2275
+ const isSynced2 = Boolean(provider && isMatchingLocalHost(url, port));
2276
+ return { id, installed: true, synced: isSynced2 };
2277
+ } catch {
2278
+ const qwenBlockMatch = raw.match(/"qwenproxy"\s*:\s*\{[\s\S]*?"baseURL"\s*:\s*"([^"]+)"/);
2279
+ const url = qwenBlockMatch ? qwenBlockMatch[1] : "";
2280
+ const isSynced2 = Boolean(url && isMatchingLocalHost(url, port));
2281
+ return { id, installed: true, synced: isSynced2 };
2282
+ }
2283
+ return {
2284
+ id,
2285
+ installed: true,
2286
+ synced: isSynced
2287
+ };
2288
+ }
2289
+ if (id === "omp") {
2290
+ const ompMatch = raw.match(/^[ \t]*qwenproxy:\s*\r?\n((?:[ \t]{4,}.*\r?\n?)*)/m);
2291
+ let isSynced = false;
2292
+ let url;
2293
+ if (ompMatch) {
2294
+ const urlMatch = ompMatch[1].match(/baseUrl:\s*(\S+)/);
2295
+ url = urlMatch ? urlMatch[1].replace(/['"]/g, "") : void 0;
2296
+ isSynced = Boolean(url && isMatchingLocalHost(url, port));
2297
+ }
2298
+ return {
2299
+ id,
2300
+ installed: true,
2301
+ synced: isSynced,
2302
+ url
2303
+ };
2304
+ }
2305
+ if (id === "hermes") {
2306
+ const isSynced = isMatchingLocalHost(raw, port) && (raw.includes("qwenproxy") || raw.includes("qwen"));
2307
+ return { id, installed: true, synced: isSynced };
2308
+ }
2309
+ if (id === "openclaw") {
2310
+ const isSynced = isMatchingLocalHost(raw, port) && raw.includes("qwenproxy");
2311
+ return { id, installed: true, synced: isSynced };
2312
+ }
2313
+ if (id === "kilo") {
2314
+ const isSynced = isMatchingLocalHost(raw, port) && raw.includes("qwenproxy");
2315
+ return { id, installed: true, synced: isSynced };
2316
+ }
2317
+ if (id === "zed") {
2318
+ const isSynced = isMatchingLocalHost(raw, port) && raw.includes("QwenProxy");
2319
+ return { id, installed: true, synced: isSynced };
2320
+ }
2321
+ if (id === "aider") {
2322
+ const isSynced = isMatchingLocalHost(raw, port) && raw.includes("qwen");
2323
+ return { id, installed: true, synced: isSynced };
2324
+ }
2325
+ } catch {
2326
+ return { id, installed: true, synced: false };
2327
+ }
2328
+ return { id, installed: true, synced: false };
2329
+ }
2330
+ function syncAllClients(options = {}) {
2331
+ const defaultPaths = getDefaultPaths();
2332
+ const paths = {
2333
+ claudeCode: options.customPaths?.claudeCode || defaultPaths.claudeCode,
2334
+ codex: options.customPaths?.codex || defaultPaths.codex,
2335
+ openCode: options.customPaths?.openCode || defaultPaths.openCode,
2336
+ omp: options.customPaths?.omp || defaultPaths.omp,
2337
+ hermes: options.customPaths?.hermes || defaultPaths.hermes,
2338
+ openClaw: options.customPaths?.openClaw || defaultPaths.openClaw,
2339
+ kilo: options.customPaths?.kilo || defaultPaths.kilo,
2340
+ cline: options.customPaths?.cline || defaultPaths.cline,
2341
+ zed: options.customPaths?.zed || defaultPaths.zed,
2342
+ aider: options.customPaths?.aider || defaultPaths.aider
2343
+ };
2344
+ const port = options.port ?? (config.server?.port || 7936);
2345
+ const configuredHost = config.server?.host;
2346
+ const host = options.host ?? (configuredHost && configuredHost !== "0.0.0.0" ? configuredHost : "127.0.0.1");
2347
+ const authHost = options.host ?? configuredHost ?? host;
2348
+ const apiKey = resolveApiKey(options.apiKey, config.apiKey, authHost);
2349
+ const { anthropicBaseUrl, openaiBaseUrl } = resolveBaseUrls(port, host);
2350
+ const stateFilePath = options.stateFilePath || getDefaultStateFilePath();
2351
+ const selectedModel = options.model || "qwen3.8-max";
2352
+ const allModels = options.models && options.models.length > 0 ? options.models : void 0;
2353
+ const syncModels = options.syncAllModels !== false && allModels ? allModels : [selectedModel];
2354
+ const results = {
2355
+ apiKey,
2356
+ port,
2357
+ host,
2358
+ clients: {}
2359
+ };
2360
+ const stateRecords = {};
2361
+ const shouldSync = (client) => {
2362
+ if (!options.targets || options.targets.length === 0) return true;
2363
+ return options.targets.includes(client);
2364
+ };
2365
+ if (shouldSync("claude-code")) {
2366
+ const claudeExisted = fs13.existsSync(paths.claudeCode);
2367
+ const claudeRes = syncClaudeCode({
2368
+ filePath: paths.claudeCode,
2369
+ apiKey,
2370
+ baseUrl: anthropicBaseUrl,
2371
+ model: selectedModel
2372
+ });
2373
+ results.clients.claudeCode = claudeRes;
2374
+ if (claudeRes.success && claudeRes.backupPath) {
2375
+ stateRecords.claudeCode = {
2376
+ filePath: paths.claudeCode,
2377
+ backupPath: claudeRes.backupPath,
2378
+ existedBefore: claudeExisted,
2379
+ syncedAt: Date.now()
2380
+ };
2381
+ }
2382
+ }
2383
+ if (shouldSync("codex")) {
2384
+ const codexExisted = fs13.existsSync(paths.codex);
2385
+ const codexRes = syncCodex({
2386
+ filePath: paths.codex,
2387
+ apiKey,
2388
+ baseUrl: openaiBaseUrl,
2389
+ setActive: options.setActive ?? true,
2390
+ model: selectedModel
2391
+ });
2392
+ results.clients.codex = codexRes;
2393
+ if (codexRes.success && codexRes.backupPath) {
2394
+ stateRecords.codex = {
2395
+ filePath: paths.codex,
2396
+ backupPath: codexRes.backupPath,
2397
+ existedBefore: codexExisted,
2398
+ syncedAt: Date.now()
2399
+ };
2400
+ }
2401
+ }
2402
+ if (shouldSync("opencode")) {
2403
+ const openCodeExisted = fs13.existsSync(paths.openCode);
2404
+ const openCodeRes = syncOpenCode({
2405
+ filePath: paths.openCode,
2406
+ apiKey,
2407
+ baseUrl: openaiBaseUrl,
2408
+ model: selectedModel,
2409
+ models: syncModels,
2410
+ setActive: options.setActive ?? true
2411
+ });
2412
+ results.clients.openCode = openCodeRes;
2413
+ if (openCodeRes.success && openCodeRes.backupPath) {
2414
+ stateRecords.openCode = {
2415
+ filePath: paths.openCode,
2416
+ backupPath: openCodeRes.backupPath,
2417
+ existedBefore: openCodeExisted,
2418
+ syncedAt: Date.now()
2419
+ };
2420
+ }
2421
+ }
2422
+ if (shouldSync("omp")) {
2423
+ const ompExisted = fs13.existsSync(paths.omp);
2424
+ const ompRes = syncOmp({
2425
+ filePath: paths.omp,
2426
+ apiKey,
2427
+ baseUrl: openaiBaseUrl,
2428
+ model: selectedModel,
2429
+ models: syncModels
2430
+ });
2431
+ results.clients.omp = ompRes;
2432
+ if (ompRes.success && ompRes.backupPath) {
2433
+ stateRecords.omp = {
2434
+ filePath: paths.omp,
2435
+ backupPath: ompRes.backupPath,
2436
+ existedBefore: ompExisted,
2437
+ syncedAt: Date.now()
2438
+ };
2439
+ }
2440
+ }
2441
+ if (shouldSync("hermes")) {
2442
+ const hermesExisted = fs13.existsSync(paths.hermes);
2443
+ const hermesRes = syncHermes({
2444
+ filePath: paths.hermes,
2445
+ apiKey,
2446
+ baseUrl: openaiBaseUrl,
2447
+ model: selectedModel
2448
+ });
2449
+ results.clients.hermes = hermesRes;
2450
+ if (hermesRes.success && hermesRes.backupPath) {
2451
+ stateRecords.hermes = {
2452
+ filePath: paths.hermes,
2453
+ backupPath: hermesRes.backupPath,
2454
+ existedBefore: hermesExisted,
2455
+ syncedAt: Date.now()
2456
+ };
2457
+ }
2458
+ }
2459
+ if (shouldSync("openclaw")) {
2460
+ const openClawExisted = fs13.existsSync(paths.openClaw);
2461
+ const openClawRes = syncOpenClaw({
2462
+ filePath: paths.openClaw,
2463
+ apiKey,
2464
+ baseUrl: openaiBaseUrl,
2465
+ model: selectedModel,
2466
+ models: syncModels
2467
+ });
2468
+ results.clients.openClaw = openClawRes;
2469
+ if (openClawRes.success && openClawRes.backupPath) {
2470
+ stateRecords.openClaw = {
2471
+ filePath: paths.openClaw,
2472
+ backupPath: openClawRes.backupPath,
2473
+ existedBefore: openClawExisted,
2474
+ syncedAt: Date.now()
2475
+ };
2476
+ }
2477
+ }
2478
+ if (shouldSync("kilo")) {
2479
+ const kiloExisted = fs13.existsSync(paths.kilo);
2480
+ const kiloRes = syncKilo({
2481
+ filePath: paths.kilo,
2482
+ apiKey,
2483
+ baseUrl: openaiBaseUrl,
2484
+ setActive: options.setActive ?? true,
2485
+ model: selectedModel,
2486
+ models: syncModels
2487
+ });
2488
+ results.clients.kilo = kiloRes;
2489
+ if (kiloRes.success && kiloRes.backupPath) {
2490
+ stateRecords.kilo = {
2491
+ filePath: paths.kilo,
2492
+ backupPath: kiloRes.backupPath,
2493
+ existedBefore: kiloExisted,
2494
+ syncedAt: Date.now()
2495
+ };
2496
+ }
2497
+ }
2498
+ if (shouldSync("cline")) {
2499
+ const clineExisted = fs13.existsSync(paths.cline);
2500
+ const clineRes = syncCline({
2501
+ filePath: paths.cline,
2502
+ apiKey,
2503
+ baseUrl: openaiBaseUrl,
2504
+ model: selectedModel
2505
+ });
2506
+ results.clients.cline = clineRes;
2507
+ if (clineRes.success && clineRes.backupPath) {
2508
+ stateRecords.cline = {
2509
+ filePath: paths.cline,
2510
+ backupPath: clineRes.backupPath,
2511
+ existedBefore: clineExisted,
2512
+ syncedAt: Date.now()
2513
+ };
2514
+ }
2515
+ }
2516
+ if (shouldSync("zed")) {
2517
+ const zedExisted = fs13.existsSync(paths.zed);
2518
+ const zedRes = syncZed({
2519
+ filePath: paths.zed,
2520
+ apiKey,
2521
+ baseUrl: openaiBaseUrl,
2522
+ setActive: options.setActive ?? true,
2523
+ model: selectedModel,
2524
+ models: syncModels
2525
+ });
2526
+ results.clients.zed = zedRes;
2527
+ if (zedRes.success && zedRes.backupPath) {
2528
+ stateRecords.zed = {
2529
+ filePath: paths.zed,
2530
+ backupPath: zedRes.backupPath,
2531
+ existedBefore: zedExisted,
2532
+ syncedAt: Date.now()
2533
+ };
2534
+ }
2535
+ }
2536
+ if (shouldSync("aider")) {
2537
+ const aiderExisted = fs13.existsSync(paths.aider);
2538
+ const aiderRes = syncAider({
2539
+ filePath: paths.aider,
2540
+ apiKey,
2541
+ baseUrl: openaiBaseUrl,
2542
+ model: selectedModel
2543
+ });
2544
+ results.clients.aider = aiderRes;
2545
+ if (aiderRes.success && aiderRes.backupPath) {
2546
+ stateRecords.aider = {
2547
+ filePath: paths.aider,
2548
+ backupPath: aiderRes.backupPath,
2549
+ existedBefore: aiderExisted,
2550
+ syncedAt: Date.now()
2551
+ };
2552
+ }
2553
+ }
2554
+ try {
2555
+ fs13.mkdirSync(path13.dirname(stateFilePath), { recursive: true });
2556
+ let existingClients = {};
2557
+ if (fs13.existsSync(stateFilePath)) {
2558
+ try {
2559
+ const raw = fs13.readFileSync(stateFilePath, "utf-8");
2560
+ const parsed = JSON.parse(raw);
2561
+ if (parsed?.clients && typeof parsed.clients === "object") {
2562
+ existingClients = parsed.clients;
2563
+ }
2564
+ } catch {
2565
+ }
2566
+ }
2567
+ const stateContent = {
2568
+ version: 1,
2569
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
2570
+ apiKey,
2571
+ port,
2572
+ host,
2573
+ clients: {
2574
+ ...existingClients,
2575
+ ...stateRecords
2576
+ }
2577
+ };
2578
+ fs13.writeFileSync(stateFilePath, JSON.stringify(stateContent, null, 2) + "\n", "utf-8");
2579
+ } catch (err) {
2580
+ console.error("Warning: could not write sync state file:", err);
2581
+ }
2582
+ return results;
2583
+ }
2584
+ function restoreAllClients(options = {}) {
2585
+ const defaultPaths = getDefaultPaths();
2586
+ const stateFilePath = options.stateFilePath || getDefaultStateFilePath();
2587
+ const details = [];
2588
+ let restoredCount = 0;
2589
+ let state = null;
2590
+ if (fs13.existsSync(stateFilePath)) {
2591
+ try {
2592
+ const raw = fs13.readFileSync(stateFilePath, "utf-8");
2593
+ state = JSON.parse(raw);
2594
+ } catch {
2595
+ }
2596
+ }
2597
+ const shouldRestore = (client) => {
2598
+ if (!options.targets || options.targets.length === 0) return true;
2599
+ return options.targets.includes(client);
2600
+ };
2601
+ const restoreClientsList = [
2602
+ { id: "claude-code", stateKey: "claudeCode", defaultPath: defaultPaths.claudeCode, stateRecord: state?.clients?.claudeCode, restoreFn: restoreClaudeCode },
2603
+ { id: "codex", stateKey: "codex", defaultPath: defaultPaths.codex, stateRecord: state?.clients?.codex, restoreFn: restoreCodex },
2604
+ { id: "opencode", stateKey: "openCode", defaultPath: defaultPaths.openCode, stateRecord: state?.clients?.openCode, restoreFn: restoreOpenCode },
2605
+ { id: "omp", stateKey: "omp", defaultPath: defaultPaths.omp, stateRecord: state?.clients?.omp, restoreFn: restoreOmp },
2606
+ { id: "hermes", stateKey: "hermes", defaultPath: defaultPaths.hermes, stateRecord: state?.clients?.hermes, restoreFn: restoreHermes },
2607
+ { id: "openclaw", stateKey: "openClaw", defaultPath: defaultPaths.openClaw, stateRecord: state?.clients?.openClaw, restoreFn: restoreOpenClaw },
2608
+ { id: "kilo", stateKey: "kilo", defaultPath: defaultPaths.kilo, stateRecord: state?.clients?.kilo, restoreFn: restoreKilo },
2609
+ { id: "cline", stateKey: "cline", defaultPath: defaultPaths.cline, stateRecord: state?.clients?.cline, restoreFn: restoreCline },
2610
+ { id: "zed", stateKey: "zed", defaultPath: defaultPaths.zed, stateRecord: state?.clients?.zed, restoreFn: restoreZed },
2611
+ { id: "aider", stateKey: "aider", defaultPath: defaultPaths.aider, stateRecord: state?.clients?.aider, restoreFn: restoreAider }
2612
+ ];
2613
+ for (const c of restoreClientsList) {
2614
+ if (!shouldRestore(c.id)) continue;
2615
+ const filePath = c.stateRecord?.filePath || c.defaultPath;
2616
+ const backupPath = c.stateRecord?.backupPath;
2617
+ const canAttempt = options.stateFilePath ? Boolean(c.stateRecord) : Boolean(c.stateRecord || fs13.existsSync(filePath));
2618
+ if (canAttempt) {
2619
+ const res = c.restoreFn(filePath, backupPath);
2620
+ details.push(res);
2621
+ if (res.success) {
2622
+ restoredCount++;
2623
+ if (state?.clients) {
2624
+ delete state.clients[c.stateKey];
2625
+ delete state.clients[c.id];
2626
+ }
2627
+ }
2628
+ }
2629
+ }
2630
+ if (stateFilePath && fs13.existsSync(stateFilePath)) {
2631
+ try {
2632
+ if (state && state.clients && Object.keys(state.clients).length > 0) {
2633
+ fs13.writeFileSync(stateFilePath, JSON.stringify(state, null, 2) + "\n", "utf-8");
2634
+ } else {
2635
+ fs13.unlinkSync(stateFilePath);
2636
+ }
2637
+ } catch {
2638
+ }
2639
+ }
2640
+ return { restoredCount, details };
2641
+ }
2642
+
2643
+ // src/sync-clients.ts
2644
+ process.env.DOTENV_CONFIG_QUIET = "true";
2645
+ ensureDataDirs();
2646
+ var envPath = getEnvFilePath();
2647
+ if (fs14.existsSync(envPath)) {
2648
+ dotenv.config({ path: envPath, quiet: true });
2649
+ } else {
2650
+ dotenv.config({ quiet: true });
2651
+ }
2652
+ function parseArgs() {
2653
+ const args = process.argv.slice(2);
2654
+ const options = {
2655
+ restore: false,
2656
+ list: false,
2657
+ help: false,
2658
+ setActive: true,
2659
+ targets: []
2660
+ };
2661
+ for (let i = 0; i < args.length; i++) {
2662
+ const arg = args[i];
2663
+ if (arg === "--restore" || arg === "restore") {
2664
+ options.restore = true;
2665
+ } else if (arg === "--list" || arg === "list") {
2666
+ options.list = true;
2667
+ } else if (arg === "--help" || arg === "-h" || arg === "help") {
2668
+ options.help = true;
2669
+ } else if (arg === "--api-key" && args[i + 1]) {
2670
+ options.apiKey = args[++i];
2671
+ } else if (arg === "--port" && args[i + 1]) {
2672
+ options.port = parseInt(args[++i], 10);
2673
+ } else if (arg === "--host" && args[i + 1]) {
2674
+ options.host = args[++i];
2675
+ } else if (arg === "--no-active") {
2676
+ options.setActive = false;
2677
+ } else if ((arg === "--model" || arg === "-m") && args[i + 1]) {
2678
+ options.model = args[++i];
2679
+ } else if (arg === "--client" && args[i + 1]) {
2680
+ const normalized = normalizeClientName(args[++i]);
2681
+ if (normalized) options.targets.push(normalized);
2682
+ } else if (!arg.startsWith("-")) {
2683
+ const normalized = normalizeClientName(arg);
2684
+ if (normalized) options.targets.push(normalized);
2685
+ }
2686
+ }
2687
+ return options;
2688
+ }
2689
+ function printHelp() {
2690
+ console.log(`
2691
+ Uso:
2692
+ npm run sync [clientes...] [op\xE7\xF5es]
2693
+
2694
+ Exemplos:
2695
+ npm run sync # Sincroniza todos os 10 clientes detectados
2696
+ npm run sync hermes # Sincroniza apenas o Hermes Agent
2697
+ npm run sync opencode # Sincroniza apenas o OpenCode
2698
+ npm run sync claude # Sincroniza apenas o Claude Code
2699
+ npm run sync openclaw # Sincroniza apenas o OpenClaw
2700
+ npm run sync kilo # Sincroniza apenas o Kilo Code
2701
+ npm run sync cline # Sincroniza apenas o Cline
2702
+ npm run sync omp # Sincroniza apenas o OMP (Oh My Pi)
2703
+ npm run sync codex # Sincroniza apenas o Codex CLI
2704
+ npm run sync zed # Sincroniza apenas o Zed Editor
2705
+ npm run sync aider # Sincroniza apenas o Aider
2706
+ npm run sync claude codex # Sincroniza m\xFAltiplos clientes espec\xEDficos
2707
+ npm run sync --list # Lista status de detec\xE7\xE3o de todos os 10 clientes (ou qpx sync --list)
2708
+ npm run sync --restore # Restaura as configura\xE7\xF5es originais (ou qpx sync --restore)
2709
+
2710
+ Op\xE7\xF5es:
2711
+ --client <nome> Nome do cliente (hermes, opencode, claude, openclaw, kilo, cline, omp, codex, zed, aider)
2712
+ --model <modelo> Modelo padr\xE3o a configurar (padr\xE3o: qwen3.8-max)
2713
+ --api-key <chave> Sobrescrever chave de API (obrigat\xF3ria; n\xE3o usa placeholder)
2714
+ --port <porta> Sobrescrever porta do servidor (padr\xE3o: l\xEA do .env ou usa 7936)
2715
+ --host <host> Sobrescrever host do servidor (padr\xE3o: 127.0.0.1)
2716
+ --no-active N\xE3o definir o modelo ativo como padr\xE3o (apenas adiciona o provider)
2717
+ --restore Desfaz altera\xE7\xF5es restaurando backups
2718
+ --list Mostra status de detec\xE7\xE3o dos arquivos de configura\xE7\xE3o
2719
+ `);
2720
+ }
2721
+ async function main() {
2722
+ const options = parseArgs();
2723
+ console.log("==================================================");
2724
+ console.log(" \u{1F680} QwenProxy - Top 10 Client Configuration Sync");
2725
+ console.log("==================================================");
2726
+ if (options.help) {
2727
+ printHelp();
2728
+ return;
2729
+ }
2730
+ if (options.list) {
2731
+ console.log("\n\u{1F4C1} Status de detec\xE7\xE3o dos 10 clientes no seu computador:\n");
2732
+ const defaultPaths = getDefaultPaths();
2733
+ const clients = [
2734
+ { id: "hermes", name: "1. Hermes Agent", path: defaultPaths.hermes },
2735
+ { id: "opencode", name: "2. OpenCode", path: defaultPaths.openCode },
2736
+ { id: "claude-code", name: "3. Claude Code", path: defaultPaths.claudeCode },
2737
+ { id: "openclaw", name: "4. OpenClaw", path: defaultPaths.openClaw },
2738
+ { id: "kilo", name: "5. Kilo Code", path: defaultPaths.kilo },
2739
+ { id: "cline", name: "6. Cline", path: defaultPaths.cline },
2740
+ { id: "omp", name: "7. OMP (Oh My Pi)", path: defaultPaths.omp },
2741
+ { id: "codex", name: "8. Codex CLI", path: defaultPaths.codex },
2742
+ { id: "zed", name: "9. Zed Editor", path: defaultPaths.zed },
2743
+ { id: "aider", name: "10. Aider", path: defaultPaths.aider }
2744
+ ];
2745
+ for (const c of clients) {
2746
+ const status = inspectClientSyncStatus(c.id, c.path);
2747
+ let badge = "";
2748
+ let icon = "\u26AA";
2749
+ if (status.synced) {
2750
+ icon = "\u2705";
2751
+ badge = status.model ? `[Sincronizado: ${status.model}]` : "[Sincronizado]";
2752
+ } else if (status.installed) {
2753
+ icon = "\u26A0\uFE0F ";
2754
+ badge = "[Detectado - Outro provedor]";
2755
+ } else {
2756
+ icon = "\u26AA";
2757
+ badge = "[N\xE3o instalado]";
2758
+ }
2759
+ console.log(` ${icon} ${c.name.padEnd(20)} ${badge}`);
2760
+ console.log(` ${c.path}`);
2761
+ }
2762
+ console.log("\nPara sincronizar um ou todos, execute:");
2763
+ console.log(" npm run sync");
2764
+ console.log(" npm run sync hermes");
2765
+ console.log(" npm run sync claude cline zed\n");
2766
+ return;
2767
+ }
2768
+ if (options.restore) {
2769
+ console.log("\n\u{1F504} Restaurando configura\xE7\xF5es originais a partir dos backups...\n");
2770
+ const result2 = restoreAllClients();
2771
+ if (result2.restoredCount === 0) {
2772
+ console.log("\u2139\uFE0F Nenhum backup ou arquivo de estado anterior encontrado para restaurar.");
2773
+ } else {
2774
+ for (const item of result2.details) {
2775
+ console.log(` \u2713 Restaurado [${item.client}]: ${item.filePath}`);
2776
+ }
2777
+ console.log(`
2778
+ \u2705 ${result2.restoredCount} configura\xE7\xE3o(\xF5es) restaurada(s) com sucesso!`);
2779
+ }
2780
+ return;
2781
+ }
2782
+ const targetNames = options.targets.length > 0 ? options.targets.join(", ") : "todos os detectados";
2783
+ console.log(`
2784
+ \u{1F4E1} Sincronizando clientes: [${targetNames}]...
2785
+ `);
2786
+ const result = syncAllClients({
2787
+ apiKey: options.apiKey,
2788
+ port: options.port,
2789
+ host: options.host,
2790
+ setActive: options.setActive,
2791
+ targets: options.targets.length > 0 ? options.targets : void 0,
2792
+ model: options.model
2793
+ });
2794
+ console.log(`\u{1F511} Chave API: ${result.apiKey}`);
2795
+ console.log(`\u{1F310} Porta: ${result.port}`);
2796
+ console.log(`\u{1F3E0} Host: ${result.host}
2797
+ `);
2798
+ let count = 0;
2799
+ for (const client of Object.values(result.clients)) {
2800
+ if (!client) continue;
2801
+ count++;
2802
+ const icon = client.success ? "\u2705" : "\u274C";
2803
+ console.log(`${icon} [${client.client}]`);
2804
+ console.log(` Arquivo: ${client.filePath}`);
2805
+ if (client.backupPath) {
2806
+ console.log(` Backup: ${client.backupPath}`);
2807
+ }
2808
+ if (client.message) {
2809
+ console.log(` Status: ${client.message}`);
2810
+ }
2811
+ if (client.error) {
2812
+ console.log(` Erro: ${client.error}`);
2813
+ }
2814
+ console.log("");
2815
+ }
2816
+ console.log("--------------------------------------------------");
2817
+ console.log(`\u2728 ${count} cliente(s) sincronizado(s) com zero perda de outras configs/provedores!`);
2818
+ console.log("\u{1F4A1} Para desfazer e restaurar a qualquer momento:");
2819
+ console.log(" qpx sync --restore (ou npm run sync --restore)");
2820
+ }
2821
+ main().catch((err) => {
2822
+ console.error("\u274C Erro fatal durante a sincroniza\xE7\xE3o:", err);
2823
+ process.exit(1);
2824
+ });