qwenproxy-cli 1.0.28 → 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/package.json +3 -3
- package/src/sync/aider.ts +118 -0
- package/src/sync/cline.ts +110 -0
- package/src/sync/hermes.ts +105 -0
- package/src/sync/index.ts +323 -8
- package/src/sync/kilo.ts +230 -0
- package/src/sync/openclaw.ts +237 -0
- package/src/sync/types.ts +29 -3
- package/src/sync/zed.ts +210 -0
- package/src/sync-clients.ts +32 -18
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
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
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:
|
|
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
|
-
:
|
|
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:
|
|
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);
|
package/src/sync/kilo.ts
ADDED
|
@@ -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
|
+
}
|