qwenproxy-cli 1.0.27 → 1.0.29

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.
package/src/sync/index.ts CHANGED
@@ -2,19 +2,26 @@ import "dotenv/config";
2
2
  import fs from "node:fs";
3
3
  import path from "node:path";
4
4
  import os from "node:os";
5
+ import Database from "better-sqlite3";
5
6
 
6
7
  import { config } from "../core/config.ts";
7
8
  import { getSyncStatePath } from "../core/paths.ts";
8
9
  import type {
9
10
  ClientSyncResult,
10
11
  SyncAllOptions,
11
- SyncRecord,
12
+ SyncClientName,
12
13
  SyncStateFile,
13
14
  } from "./types.ts";
14
15
  import { syncClaudeCode, restoreClaudeCode } from "./claude-code.ts";
15
16
  import { syncCodex, restoreCodex } from "./codex.ts";
16
17
  import { syncOpenCode, restoreOpenCode } from "./opencode.ts";
17
18
  import { syncOmp, restoreOmp } from "./omp.ts";
19
+ import { syncHermes, restoreHermes } from "./hermes.ts";
20
+ import { syncOpenClaw, restoreOpenClaw } from "./openclaw.ts";
21
+ import { syncKilo, restoreKilo } from "./kilo.ts";
22
+ import { syncCline, restoreCline } from "./cline.ts";
23
+ import { syncZed, restoreZed } from "./zed.ts";
24
+ import { syncAider, restoreAider } from "./aider.ts";
18
25
 
19
26
  export function resolveApiKey(overrideKey?: string, configKey?: string): string {
20
27
  if (overrideKey && overrideKey.trim().length > 0) {
@@ -26,12 +33,29 @@ export function resolveApiKey(overrideKey?: string, configKey?: string): string
26
33
  }
27
34
  return "sk-qwenproxy-local";
28
35
  }
29
- export function normalizeClientName(name: string): "claude-code" | "codex" | "opencode" | "omp" | null {
36
+
37
+ export function normalizeClientName(name: string): SyncClientName | null {
30
38
  const clean = name.trim().toLowerCase().replace(/[-_ ]/g, "");
31
39
  if (clean === "claude" || clean === "claudecode" || clean === "anthropic") return "claude-code";
32
40
  if (clean === "codex" || clean === "codexcli" || clean === "openai") return "codex";
33
41
  if (clean === "opencode") return "opencode";
34
- if (clean === "omp" || clean === "ohmypi") return "omp";
42
+ if (clean === "omp" || clean === "ohmypi" || clean === "pi") return "omp";
43
+ if (clean === "hermes" || clean === "hermesagent" || clean === "nous" || clean === "nousresearch") return "hermes";
44
+ if (clean === "openclaw" || clean === "claw" || clean === "clawdbot" || clean === "moltbot") return "openclaw";
45
+ if (clean === "kilo" || clean === "kilocode") return "kilo";
46
+ if (
47
+ clean === "cline" ||
48
+ clean === "claudedev" ||
49
+ clean === "zoo" ||
50
+ clean === "zoocode" ||
51
+ clean === "roo" ||
52
+ clean === "roocode" ||
53
+ clean === "roocline"
54
+ ) {
55
+ return "cline";
56
+ }
57
+ if (clean === "zed" || clean === "zededitor") return "zed";
58
+ if (clean === "aider" || clean === "aiderchat") return "aider";
35
59
  return null;
36
60
  }
37
61
 
@@ -51,10 +75,19 @@ export function getDefaultPaths(): {
51
75
  codex: string;
52
76
  openCode: string;
53
77
  omp: string;
78
+ hermes: string;
79
+ openClaw: string;
80
+ kilo: string;
81
+ cline: string;
82
+ zed: string;
83
+ aider: string;
54
84
  } {
55
85
  const home = os.homedir();
86
+ const isWindows = process.platform === "win32";
87
+ const isMac = process.platform === "darwin";
88
+ const appData = process.env.APPDATA || (isWindows ? path.join(home, "AppData", "Roaming") : "");
56
89
 
57
- // OpenCode can be ~/.config/opencode/opencode.jsonc or ~/.opencode/opencode.jsonc
90
+ // OpenCode candidates
58
91
  const openCodeCandidates = [
59
92
  path.join(home, ".config", "opencode", "opencode.jsonc"),
60
93
  path.join(home, ".config", "opencode", "opencode.json"),
@@ -63,6 +96,33 @@ export function getDefaultPaths(): {
63
96
  ];
64
97
  const existingOpenCode = openCodeCandidates.find((p) => fs.existsSync(p));
65
98
 
99
+ // Kilo candidates
100
+ const kiloCandidates = [
101
+ path.join(home, ".config", "kilo", "kilo.json"),
102
+ path.join(home, ".kilo", "kilo.json"),
103
+ path.join(home, ".config", "kilo", "config.json"),
104
+ path.join(home, ".kilo", "config.json"),
105
+ ];
106
+ const existingKilo = kiloCandidates.find((p) => fs.existsSync(p));
107
+
108
+ // Cline state.vscdb
109
+ let clinePath: string;
110
+ if (isWindows) {
111
+ clinePath = path.join(appData, "Code", "User", "globalStorage", "state.vscdb");
112
+ } else if (isMac) {
113
+ clinePath = path.join(home, "Library", "Application Support", "Code", "User", "globalStorage", "state.vscdb");
114
+ } else {
115
+ clinePath = path.join(home, ".config", "Code", "User", "globalStorage", "state.vscdb");
116
+ }
117
+
118
+ // Zed settings.json
119
+ let zedPath: string;
120
+ if (isWindows) {
121
+ zedPath = path.join(appData, "Zed", "settings.json");
122
+ } else {
123
+ zedPath = path.join(home, ".config", "zed", "settings.json");
124
+ }
125
+
66
126
  return {
67
127
  claudeCode: path.join(home, ".claude", "settings.json"),
68
128
  codex: process.env.CODEX_HOME
@@ -70,14 +130,21 @@ export function getDefaultPaths(): {
70
130
  : path.join(home, ".codex", "config.toml"),
71
131
  openCode: existingOpenCode || openCodeCandidates[0],
72
132
  omp: path.join(home, ".omp", "agent", "models.yml"),
133
+ hermes: path.join(home, ".hermes", "config.yaml"),
134
+ openClaw: path.join(home, ".openclaw", "openclaw.json"),
135
+ kilo: existingKilo || kiloCandidates[0],
136
+ cline: clinePath,
137
+ zed: zedPath,
138
+ aider: path.join(home, ".aider.conf.yml"),
73
139
  };
74
140
  }
75
141
 
76
142
  export function getDefaultStateFilePath(): string {
77
143
  return getSyncStatePath();
78
144
  }
145
+
79
146
  export interface ClientDetectionStatus {
80
- id: "claude-code" | "codex" | "opencode" | "omp";
147
+ id: SyncClientName;
81
148
  installed: boolean;
82
149
  synced: boolean;
83
150
  model?: string;
@@ -89,7 +156,7 @@ export interface ClientDetectionStatus {
89
156
  * and whether it is actively configured to route to QwenProxy.
90
157
  */
91
158
  export function inspectClientSyncStatus(
92
- id: "claude-code" | "codex" | "opencode" | "omp",
159
+ id: SyncClientName,
93
160
  filePath?: string,
94
161
  port = 7936,
95
162
  ): ClientDetectionStatus {
@@ -102,13 +169,52 @@ export function inspectClientSyncStatus(
102
169
  ? defaultPaths.codex
103
170
  : id === "opencode"
104
171
  ? defaultPaths.openCode
105
- : defaultPaths.omp);
172
+ : id === "omp"
173
+ ? defaultPaths.omp
174
+ : id === "hermes"
175
+ ? defaultPaths.hermes
176
+ : id === "openclaw"
177
+ ? defaultPaths.openClaw
178
+ : id === "kilo"
179
+ ? defaultPaths.kilo
180
+ : id === "cline"
181
+ ? defaultPaths.cline
182
+ : id === "zed"
183
+ ? defaultPaths.zed
184
+ : defaultPaths.aider);
106
185
 
107
186
  if (!fs.existsSync(targetPath)) {
108
187
  return { id, installed: false, synced: false };
109
188
  }
110
189
 
111
190
  try {
191
+ if (id === "cline") {
192
+ // Inspect SQLite DB
193
+ let isSynced = false;
194
+ let model: string | undefined;
195
+ try {
196
+ const db = new Database(targetPath, { readonly: true });
197
+ const row = db
198
+ .prepare(
199
+ `SELECT value FROM ItemTable WHERE key = 'saoudrizwan.claude-dev' OR key = 'ZooCodeOrganization.zoo-code' LIMIT 1`,
200
+ )
201
+ .get() as { value: string } | undefined;
202
+ db.close();
203
+ if (row && row.value) {
204
+ const parsed = JSON.parse(row.value);
205
+ const url = parsed.openAiBaseUrl || "";
206
+ model = parsed.openAiModelId;
207
+ isSynced = Boolean(
208
+ url &&
209
+ (url.includes(String(port)) ||
210
+ url.includes(`127.0.0.1:${port}`) ||
211
+ url.includes(`localhost:${port}`)),
212
+ );
213
+ }
214
+ } catch {}
215
+ return { id, installed: true, synced: isSynced, model };
216
+ }
217
+
112
218
  const raw = fs.readFileSync(targetPath, "utf-8");
113
219
 
114
220
  if (id === "claude-code") {
@@ -198,6 +304,51 @@ export function inspectClientSyncStatus(
198
304
  url,
199
305
  };
200
306
  }
307
+
308
+ if (id === "hermes") {
309
+ const isLocalHost =
310
+ raw.includes(String(port)) ||
311
+ raw.includes(`127.0.0.1:${port}`) ||
312
+ raw.includes(`localhost:${port}`);
313
+ const isSynced = isLocalHost && (raw.includes("qwenproxy") || raw.includes("qwen"));
314
+ return { id, installed: true, synced: isSynced };
315
+ }
316
+
317
+ if (id === "openclaw") {
318
+ const isLocalHost =
319
+ raw.includes(String(port)) ||
320
+ raw.includes(`127.0.0.1:${port}`) ||
321
+ raw.includes(`localhost:${port}`);
322
+ const isSynced = isLocalHost && raw.includes("qwenproxy");
323
+ return { id, installed: true, synced: isSynced };
324
+ }
325
+
326
+ if (id === "kilo") {
327
+ const isLocalHost =
328
+ raw.includes(String(port)) ||
329
+ raw.includes(`127.0.0.1:${port}`) ||
330
+ raw.includes(`localhost:${port}`);
331
+ const isSynced = isLocalHost && raw.includes("qwenproxy");
332
+ return { id, installed: true, synced: isSynced };
333
+ }
334
+
335
+ if (id === "zed") {
336
+ const isLocalHost =
337
+ raw.includes(String(port)) ||
338
+ raw.includes(`127.0.0.1:${port}`) ||
339
+ raw.includes(`localhost:${port}`);
340
+ const isSynced = isLocalHost && raw.includes("QwenProxy");
341
+ return { id, installed: true, synced: isSynced };
342
+ }
343
+
344
+ if (id === "aider") {
345
+ const isLocalHost =
346
+ raw.includes(String(port)) ||
347
+ raw.includes(`127.0.0.1:${port}`) ||
348
+ raw.includes(`localhost:${port}`);
349
+ const isSynced = isLocalHost && raw.includes("qwen");
350
+ return { id, installed: true, synced: isSynced };
351
+ }
201
352
  } catch {
202
353
  return { id, installed: true, synced: false };
203
354
  }
@@ -214,6 +365,12 @@ export interface SyncAllResult {
214
365
  codex?: ClientSyncResult;
215
366
  openCode?: ClientSyncResult;
216
367
  omp?: ClientSyncResult;
368
+ hermes?: ClientSyncResult;
369
+ openClaw?: ClientSyncResult;
370
+ kilo?: ClientSyncResult;
371
+ cline?: ClientSyncResult;
372
+ zed?: ClientSyncResult;
373
+ aider?: ClientSyncResult;
217
374
  };
218
375
  }
219
376
 
@@ -224,6 +381,12 @@ export function syncAllClients(options: SyncAllOptions = {}): SyncAllResult {
224
381
  codex: options.customPaths?.codex || defaultPaths.codex,
225
382
  openCode: options.customPaths?.openCode || defaultPaths.openCode,
226
383
  omp: options.customPaths?.omp || defaultPaths.omp,
384
+ hermes: options.customPaths?.hermes || defaultPaths.hermes,
385
+ openClaw: options.customPaths?.openClaw || defaultPaths.openClaw,
386
+ kilo: options.customPaths?.kilo || defaultPaths.kilo,
387
+ cline: options.customPaths?.cline || defaultPaths.cline,
388
+ zed: options.customPaths?.zed || defaultPaths.zed,
389
+ aider: options.customPaths?.aider || defaultPaths.aider,
227
390
  };
228
391
 
229
392
  const port = options.port ?? (config.server?.port || 7936);
@@ -241,7 +404,7 @@ export function syncAllClients(options: SyncAllOptions = {}): SyncAllResult {
241
404
  };
242
405
 
243
406
  const stateRecords: SyncStateFile["clients"] = {};
244
- const shouldSync = (client: "claude-code" | "codex" | "opencode" | "omp") => {
407
+ const shouldSync = (client: SyncClientName) => {
245
408
  if (!options.targets || options.targets.length === 0) return true;
246
409
  return options.targets.includes(client);
247
410
  };
@@ -323,6 +486,122 @@ export function syncAllClients(options: SyncAllOptions = {}): SyncAllResult {
323
486
  }
324
487
  }
325
488
 
489
+ // 5. Hermes Agent
490
+ if (shouldSync("hermes")) {
491
+ const hermesExisted = fs.existsSync(paths.hermes);
492
+ const hermesRes = syncHermes({
493
+ filePath: paths.hermes,
494
+ apiKey,
495
+ baseUrl: openaiBaseUrl,
496
+ });
497
+ results.clients.hermes = hermesRes;
498
+ if (hermesRes.success && hermesRes.backupPath) {
499
+ stateRecords.hermes = {
500
+ filePath: paths.hermes,
501
+ backupPath: hermesRes.backupPath,
502
+ existedBefore: hermesExisted,
503
+ syncedAt: Date.now(),
504
+ };
505
+ }
506
+ }
507
+
508
+ // 6. OpenClaw
509
+ if (shouldSync("openclaw")) {
510
+ const openClawExisted = fs.existsSync(paths.openClaw);
511
+ const openClawRes = syncOpenClaw({
512
+ filePath: paths.openClaw,
513
+ apiKey,
514
+ baseUrl: openaiBaseUrl,
515
+ });
516
+ results.clients.openClaw = openClawRes;
517
+ if (openClawRes.success && openClawRes.backupPath) {
518
+ stateRecords.openClaw = {
519
+ filePath: paths.openClaw,
520
+ backupPath: openClawRes.backupPath,
521
+ existedBefore: openClawExisted,
522
+ syncedAt: Date.now(),
523
+ };
524
+ }
525
+ }
526
+
527
+ // 7. Kilo Code
528
+ if (shouldSync("kilo")) {
529
+ const kiloExisted = fs.existsSync(paths.kilo);
530
+ const kiloRes = syncKilo({
531
+ filePath: paths.kilo,
532
+ apiKey,
533
+ baseUrl: openaiBaseUrl,
534
+ setActive: options.setActive ?? true,
535
+ });
536
+ results.clients.kilo = kiloRes;
537
+ if (kiloRes.success && kiloRes.backupPath) {
538
+ stateRecords.kilo = {
539
+ filePath: paths.kilo,
540
+ backupPath: kiloRes.backupPath,
541
+ existedBefore: kiloExisted,
542
+ syncedAt: Date.now(),
543
+ };
544
+ }
545
+ }
546
+
547
+ // 8. Cline & Zoo Code
548
+ if (shouldSync("cline")) {
549
+ const clineExisted = fs.existsSync(paths.cline);
550
+ const clineRes = syncCline({
551
+ filePath: paths.cline,
552
+ apiKey,
553
+ baseUrl: openaiBaseUrl,
554
+ });
555
+ results.clients.cline = clineRes;
556
+ if (clineRes.success && clineRes.backupPath) {
557
+ stateRecords.cline = {
558
+ filePath: paths.cline,
559
+ backupPath: clineRes.backupPath,
560
+ existedBefore: clineExisted,
561
+ syncedAt: Date.now(),
562
+ };
563
+ }
564
+ }
565
+
566
+ // 9. Zed Editor
567
+ if (shouldSync("zed")) {
568
+ const zedExisted = fs.existsSync(paths.zed);
569
+ const zedRes = syncZed({
570
+ filePath: paths.zed,
571
+ apiKey,
572
+ baseUrl: openaiBaseUrl,
573
+ setActive: options.setActive ?? true,
574
+ });
575
+ results.clients.zed = zedRes;
576
+ if (zedRes.success && zedRes.backupPath) {
577
+ stateRecords.zed = {
578
+ filePath: paths.zed,
579
+ backupPath: zedRes.backupPath,
580
+ existedBefore: zedExisted,
581
+ syncedAt: Date.now(),
582
+ };
583
+ }
584
+ }
585
+
586
+ // 10. Aider
587
+ if (shouldSync("aider")) {
588
+ const aiderExisted = fs.existsSync(paths.aider);
589
+ const aiderRes = syncAider({
590
+ filePath: paths.aider,
591
+ apiKey,
592
+ baseUrl: openaiBaseUrl,
593
+ });
594
+ results.clients.aider = aiderRes;
595
+ if (aiderRes.success && aiderRes.backupPath) {
596
+ stateRecords.aider = {
597
+ filePath: paths.aider,
598
+ backupPath: aiderRes.backupPath,
599
+ existedBefore: aiderExisted,
600
+ syncedAt: Date.now(),
601
+ };
602
+ }
603
+ }
604
+
326
605
  // Persist sync state
327
606
  try {
328
607
  fs.mkdirSync(path.dirname(stateFilePath), { recursive: true });
@@ -384,6 +663,42 @@ export function restoreAllClients(options: { stateFilePath?: string } = {}): Res
384
663
  if (res.success) restoredCount++;
385
664
  }
386
665
 
666
+ if (state.clients.hermes?.backupPath) {
667
+ const res = restoreHermes(state.clients.hermes.filePath, state.clients.hermes.backupPath);
668
+ details.push(res);
669
+ if (res.success) restoredCount++;
670
+ }
671
+
672
+ if (state.clients.openClaw?.backupPath) {
673
+ const res = restoreOpenClaw(state.clients.openClaw.filePath, state.clients.openClaw.backupPath);
674
+ details.push(res);
675
+ if (res.success) restoredCount++;
676
+ }
677
+
678
+ if (state.clients.kilo?.backupPath) {
679
+ const res = restoreKilo(state.clients.kilo.filePath, state.clients.kilo.backupPath);
680
+ details.push(res);
681
+ if (res.success) restoredCount++;
682
+ }
683
+
684
+ if (state.clients.cline?.backupPath) {
685
+ const res = restoreCline(state.clients.cline.filePath, state.clients.cline.backupPath);
686
+ details.push(res);
687
+ if (res.success) restoredCount++;
688
+ }
689
+
690
+ if (state.clients.zed?.backupPath) {
691
+ const res = restoreZed(state.clients.zed.filePath, state.clients.zed.backupPath);
692
+ details.push(res);
693
+ if (res.success) restoredCount++;
694
+ }
695
+
696
+ if (state.clients.aider?.backupPath) {
697
+ const res = restoreAider(state.clients.aider.filePath, state.clients.aider.backupPath);
698
+ details.push(res);
699
+ if (res.success) restoredCount++;
700
+ }
701
+
387
702
  // Remove state file after successful restoration
388
703
  try {
389
704
  fs.unlinkSync(stateFilePath);
@@ -0,0 +1,230 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import type { ClientSyncResult, SyncOptions } from "./types.ts";
4
+ import { createTimestampBackup, restoreFromBackup } from "./utils.ts";
5
+
6
+ function findKeyObjectSpan(content: string, key: string): { start: number; end: number; hasTrailingComma: boolean } | null {
7
+ const regex = new RegExp(`"${key}"\\s*:\\s*\\{`);
8
+ const match = content.match(regex);
9
+ if (!match || match.index === undefined) return null;
10
+
11
+ const startIndex = match.index;
12
+ const braceIndex = content.indexOf("{", startIndex + match[0].length - 1);
13
+ if (braceIndex === -1) return null;
14
+
15
+ let depth = 0;
16
+ let inString = false;
17
+ let inLineComment = false;
18
+ let inBlockComment = false;
19
+ let escape = false;
20
+
21
+ for (let i = braceIndex; i < content.length; i++) {
22
+ const ch = content[i];
23
+ const nextCh = content[i + 1] || "";
24
+
25
+ if (inLineComment) {
26
+ if (ch === "\n") inLineComment = false;
27
+ continue;
28
+ }
29
+ if (inBlockComment) {
30
+ if (ch === "*" && nextCh === "/") {
31
+ inBlockComment = false;
32
+ i++;
33
+ }
34
+ continue;
35
+ }
36
+ if (inString) {
37
+ if (escape) {
38
+ escape = false;
39
+ } else if (ch === "\\") {
40
+ escape = true;
41
+ } else if (ch === '"') {
42
+ inString = false;
43
+ }
44
+ continue;
45
+ }
46
+
47
+ if (ch === "/" && nextCh === "/") {
48
+ inLineComment = true;
49
+ i++;
50
+ continue;
51
+ }
52
+ if (ch === "/" && nextCh === "*") {
53
+ inBlockComment = true;
54
+ i++;
55
+ continue;
56
+ }
57
+
58
+ if (ch === '"') {
59
+ inString = true;
60
+ continue;
61
+ }
62
+
63
+ if (ch === "{") {
64
+ depth++;
65
+ } else if (ch === "}") {
66
+ depth--;
67
+ if (depth === 0) {
68
+ let endIndex = i + 1;
69
+ let hasTrailingComma = false;
70
+ while (endIndex < content.length && /[\s,]/.test(content[endIndex])) {
71
+ if (content[endIndex] === ",") {
72
+ hasTrailingComma = true;
73
+ endIndex++;
74
+ break;
75
+ }
76
+ if (content[endIndex] === "\n") {
77
+ break;
78
+ }
79
+ endIndex++;
80
+ }
81
+ return { start: startIndex, end: endIndex, hasTrailingComma };
82
+ }
83
+ }
84
+ }
85
+
86
+ return null;
87
+ }
88
+
89
+ function buildKiloProviderObject(
90
+ baseUrl: string,
91
+ apiKey: string,
92
+ primaryModel: string = "qwen3.8-max",
93
+ ): Record<string, any> {
94
+ const modelsObj: Record<string, any> = {};
95
+ const modelList = [primaryModel];
96
+ if (primaryModel !== "qwen3.7-plus") {
97
+ modelList.push("qwen3.7-plus");
98
+ }
99
+
100
+ for (const m of modelList) {
101
+ modelsObj[m] = {
102
+ name:
103
+ m === "qwen3.8-max"
104
+ ? "Qwen 3.8 Max"
105
+ : m === "qwen3.7-plus"
106
+ ? "Qwen 3.7 Plus"
107
+ : m,
108
+ limit: { context: 1048576, output: 65536 },
109
+ modalities: { input: ["text", "image"], output: ["text"] },
110
+ reasoning: true,
111
+ variants: {
112
+ low: { effort: "low" },
113
+ medium: { effort: "medium" },
114
+ high: { effort: "high" },
115
+ },
116
+ };
117
+ }
118
+
119
+ return {
120
+ npm: "@ai-sdk/openai-compatible",
121
+ name: "QwenProxy",
122
+ options: {
123
+ baseURL: baseUrl,
124
+ apiKey: apiKey,
125
+ },
126
+ models: modelsObj,
127
+ };
128
+ }
129
+
130
+ export function syncKilo(options: SyncOptions): ClientSyncResult {
131
+ const { filePath, apiKey, baseUrl, model = "qwen3.8-max", setActive = true } = options;
132
+ try {
133
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
134
+
135
+ let backupPath: string | undefined;
136
+ let content = "";
137
+
138
+ if (fs.existsSync(filePath)) {
139
+ backupPath = createTimestampBackup(filePath);
140
+ content = fs.readFileSync(filePath, "utf-8");
141
+ }
142
+
143
+ const providerObj = buildKiloProviderObject(baseUrl, apiKey, model);
144
+ const providerJson = JSON.stringify(providerObj, null, 6)
145
+ .split("\n")
146
+ .map((line, idx) => (idx === 0 ? line : ` ${line}`))
147
+ .join("\n");
148
+
149
+ const qwenEntry = ` "qwenproxy": ${providerJson}`;
150
+
151
+ if (!content.trim()) {
152
+ const initial: Record<string, any> = {
153
+ $schema: "https://kilo.ai/config.json",
154
+ provider: {
155
+ qwenproxy: providerObj,
156
+ },
157
+ };
158
+ if (setActive) {
159
+ initial.model = `qwenproxy/${model}`;
160
+ }
161
+ fs.writeFileSync(filePath, JSON.stringify(initial, null, 2) + "\n", "utf-8");
162
+ } else {
163
+ const existingSpan = findKeyObjectSpan(content, "qwenproxy");
164
+ if (existingSpan) {
165
+ const comma = existingSpan.hasTrailingComma ? "," : "";
166
+ content =
167
+ content.slice(0, existingSpan.start) +
168
+ `"qwenproxy": ${providerJson}${comma}` +
169
+ content.slice(existingSpan.end);
170
+ } else {
171
+ const providerMatch = content.match(/"provider"\s*:\s*\{/);
172
+ if (providerMatch && providerMatch.index !== undefined) {
173
+ const insertIdx = providerMatch.index + providerMatch[0].length;
174
+ content =
175
+ content.slice(0, insertIdx) +
176
+ "\n" +
177
+ qwenEntry +
178
+ "," +
179
+ content.slice(insertIdx);
180
+ } else {
181
+ const lastBraceIdx = content.lastIndexOf("}");
182
+ if (lastBraceIdx !== -1) {
183
+ const comma = content.slice(0, lastBraceIdx).trimEnd().endsWith("{") ? "" : ",";
184
+ content =
185
+ content.slice(0, lastBraceIdx).trimEnd() +
186
+ `${comma}\n "provider": {\n${qwenEntry}\n }\n}\n`;
187
+ }
188
+ }
189
+ }
190
+
191
+ if (setActive) {
192
+ const modelRegex = /"model"\s*:\s*"[^"]*"/;
193
+ if (modelRegex.test(content)) {
194
+ content = content.replace(modelRegex, `"model": "qwenproxy/${model}"`);
195
+ }
196
+ }
197
+
198
+ fs.writeFileSync(filePath, content, "utf-8");
199
+ }
200
+
201
+ return {
202
+ client: "kilo",
203
+ filePath,
204
+ backupPath,
205
+ success: true,
206
+ action: backupPath ? "updated" : "created",
207
+ message: `Configured Kilo Code with provider qwenproxy (${baseUrl})`,
208
+ };
209
+ } catch (err: any) {
210
+ return {
211
+ client: "kilo",
212
+ filePath,
213
+ success: false,
214
+ action: "failed",
215
+ error: err?.message || String(err),
216
+ };
217
+ }
218
+ }
219
+
220
+ export function restoreKilo(filePath: string, backupPath?: string): ClientSyncResult {
221
+ const restored = restoreFromBackup(filePath, backupPath);
222
+ return {
223
+ client: "kilo",
224
+ filePath,
225
+ backupPath,
226
+ success: restored,
227
+ action: restored ? "restored" : "failed",
228
+ message: restored ? "Restored Kilo Code config from backup" : "Backup file not found",
229
+ };
230
+ }