qwenproxy-cli 1.1.0 → 1.2.1

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.
@@ -33,12 +33,14 @@ export function syncClaudeCode(options: SyncOptions): ClientSyncResult {
33
33
  ANTHROPIC_DEFAULT_HAIKU_MODEL: "qwen3.7-plus",
34
34
  ANTHROPIC_DEFAULT_OPUS_MODEL: model,
35
35
  CLAUDE_CODE_MAX_CONTEXT_TOKENS: "1000000",
36
+ CLAUDE_CODE_DISABLE_ARTIFACT: "1",
36
37
  };
37
38
 
38
39
  const updatedSettings = {
39
40
  ...existingSettings,
40
41
  env,
41
42
  model,
43
+ enableArtifact: false,
42
44
  };
43
45
 
44
46
  fs.writeFileSync(filePath, JSON.stringify(updatedSettings, null, 2) + "\n", "utf-8");
@@ -63,13 +65,48 @@ export function syncClaudeCode(options: SyncOptions): ClientSyncResult {
63
65
  }
64
66
 
65
67
  export function restoreClaudeCode(filePath: string, backupPath?: string): ClientSyncResult {
66
- const restored = restoreFromBackup(filePath, backupPath);
68
+ const restoredFromBackup = restoreFromBackup(filePath, backupPath);
69
+
70
+ let manuallyCleaned = false;
71
+ if (fs.existsSync(filePath)) {
72
+ try {
73
+ const data = JSON.parse(fs.readFileSync(filePath, "utf-8"));
74
+ if (data.env && (data.env.ANTHROPIC_BASE_URL?.includes("7936") || data.env.ANTHROPIC_AUTH_TOKEN === "sk-qwenproxy-local" || data.env.ANTHROPIC_MODEL?.includes("qwen"))) {
75
+ delete data.env.ANTHROPIC_BASE_URL;
76
+ delete data.env.ANTHROPIC_AUTH_TOKEN;
77
+ delete data.env.ANTHROPIC_MODEL;
78
+ delete data.env.ANTHROPIC_CUSTOM_MODEL_OPTION;
79
+ delete data.env.ANTHROPIC_CUSTOM_MODEL_OPTION_NAME;
80
+ delete data.env.ANTHROPIC_CUSTOM_MODEL_OPTION_DESCRIPTION;
81
+ delete data.env.ANTHROPIC_DEFAULT_SONNET_MODEL;
82
+ delete data.env.ANTHROPIC_DEFAULT_HAIKU_MODEL;
83
+ delete data.env.ANTHROPIC_DEFAULT_OPUS_MODEL;
84
+ delete data.env.CLAUDE_CODE_MAX_CONTEXT_TOKENS;
85
+ delete data.env.CLAUDE_CODE_DISABLE_ARTIFACT;
86
+ delete data.enableArtifact;
87
+ if (data.model && data.model.toLowerCase().includes("qwen")) {
88
+ delete data.model;
89
+ }
90
+ if (Object.keys(data.env).length === 0) {
91
+ delete data.env;
92
+ }
93
+ fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n", "utf-8");
94
+ manuallyCleaned = true;
95
+ }
96
+ } catch {}
97
+ }
98
+
99
+ const success = restoredFromBackup || manuallyCleaned;
67
100
  return {
68
101
  client: "claude-code",
69
102
  filePath,
70
103
  backupPath,
71
- success: restored,
72
- action: restored ? "restored" : "failed",
73
- message: restored ? "Restored Claude Code settings from backup" : "Backup file not found",
104
+ success,
105
+ action: success ? "restored" : "failed",
106
+ message: success
107
+ ? restoredFromBackup
108
+ ? "Restored Claude Code settings from backup"
109
+ : "Removed QwenProxy configuration from Claude Code settings"
110
+ : "Backup file not found",
74
111
  };
75
112
  }
package/src/sync/cline.ts CHANGED
@@ -98,13 +98,43 @@ export function syncCline(options: SyncOptions): ClientSyncResult {
98
98
  }
99
99
 
100
100
  export function restoreCline(filePath: string, backupPath?: string): ClientSyncResult {
101
- const restored = restoreFromBackup(filePath, backupPath);
101
+ const restoredFromBackup = restoreFromBackup(filePath, backupPath);
102
+
103
+ let manuallyCleaned = false;
104
+ if (fs.existsSync(filePath)) {
105
+ try {
106
+ const db = new Database(filePath);
107
+ const rows = db
108
+ .prepare("SELECT key, value FROM ItemTable WHERE key = 'saoudrizwan.claude-dev' OR key = 'ZooCodeOrganization.zoo-code'")
109
+ .all() as Array<{ key: string; value: string }>;
110
+
111
+ for (const row of rows) {
112
+ try {
113
+ const parsed = JSON.parse(row.value);
114
+ if (parsed.openAiBaseUrl?.includes("7936") || parsed.openAiModelId?.includes("qwen")) {
115
+ delete parsed.openAiBaseUrl;
116
+ delete parsed.openAiApiKey;
117
+ delete parsed.openAiModelId;
118
+ db.prepare("UPDATE ItemTable SET value = ? WHERE key = ?").run(JSON.stringify(parsed), row.key);
119
+ manuallyCleaned = true;
120
+ }
121
+ } catch {}
122
+ }
123
+ db.close();
124
+ } catch {}
125
+ }
126
+
127
+ const success = restoredFromBackup || manuallyCleaned;
102
128
  return {
103
129
  client: "cline",
104
130
  filePath,
105
131
  backupPath,
106
- success: restored,
107
- action: restored ? "restored" : "failed",
108
- message: restored ? "Restored Cline settings from backup" : "Backup file not found",
132
+ success,
133
+ action: success ? "restored" : "failed",
134
+ message: success
135
+ ? restoredFromBackup
136
+ ? "Restored Cline settings from backup"
137
+ : "Removed QwenProxy configuration from Cline settings"
138
+ : "Backup file not found",
109
139
  };
110
140
  }
package/src/sync/codex.ts CHANGED
@@ -111,13 +111,38 @@ experimental_bearer_token = "${apiKey}"
111
111
  }
112
112
 
113
113
  export function restoreCodex(filePath: string, backupPath?: string): ClientSyncResult {
114
- const restored = restoreFromBackup(filePath, backupPath);
114
+ const restoredFromBackup = restoreFromBackup(filePath, backupPath);
115
+
116
+ // If backup was restored but still had qwenproxy (or if no backup was found),
117
+ // strip the QwenProxy provider block cleanly so the file is guaranteed un-synced.
118
+ let manuallyCleaned = false;
119
+ if (fs.existsSync(filePath)) {
120
+ try {
121
+ let content = fs.readFileSync(filePath, "utf-8");
122
+ if (content.includes("[model_providers.qwenproxy]") || /^model_provider\s*=\s*["']qwenproxy["']/m.test(content)) {
123
+ const providerRegex = /\[model_providers\.qwenproxy\][\s\S]*?(?=(?:^\[|\Z))/m;
124
+ content = content.replace(providerRegex, "").trimEnd();
125
+ content = content.replace(/^model_provider\s*=\s*["']qwenproxy["']\r?\n?/m, "");
126
+ if (/^model\s*=\s*["']qwen/m.test(content)) {
127
+ content = content.replace(/^model\s*=\s*["']qwen[^"']*["']\r?\n?/m, "");
128
+ }
129
+ fs.writeFileSync(filePath, content.trimEnd() + "\n", "utf-8");
130
+ manuallyCleaned = true;
131
+ }
132
+ } catch {}
133
+ }
134
+
135
+ const success = restoredFromBackup || manuallyCleaned;
115
136
  return {
116
137
  client: "codex",
117
138
  filePath,
118
139
  backupPath,
119
- success: restored,
120
- action: restored ? "restored" : "failed",
121
- message: restored ? "Restored Codex config from backup" : "Backup file not found",
140
+ success,
141
+ action: success ? "restored" : "failed",
142
+ message: success
143
+ ? restoredFromBackup
144
+ ? "Restored Codex config from backup"
145
+ : "Removed QwenProxy configuration from Codex config"
146
+ : "Backup file not found",
122
147
  };
123
148
  }
package/src/sync/index.ts CHANGED
@@ -10,6 +10,7 @@ import type {
10
10
  ClientSyncResult,
11
11
  SyncAllOptions,
12
12
  SyncClientName,
13
+ SyncRecord,
13
14
  SyncStateFile,
14
15
  } from "./types.ts";
15
16
  import { syncClaudeCode, restoreClaudeCode } from "./claude-code.ts";
@@ -23,6 +24,28 @@ import { syncCline, restoreCline } from "./cline.ts";
23
24
  import { syncZed, restoreZed } from "./zed.ts";
24
25
  import { syncAider, restoreAider } from "./aider.ts";
25
26
 
27
+ export {
28
+ syncClaudeCode,
29
+ restoreClaudeCode,
30
+ syncCodex,
31
+ restoreCodex,
32
+ syncOpenCode,
33
+ restoreOpenCode,
34
+ syncOmp,
35
+ restoreOmp,
36
+ syncHermes,
37
+ restoreHermes,
38
+ syncOpenClaw,
39
+ restoreOpenClaw,
40
+ syncKilo,
41
+ restoreKilo,
42
+ syncCline,
43
+ restoreCline,
44
+ syncZed,
45
+ restoreZed,
46
+ syncAider,
47
+ restoreAider,
48
+ };
26
49
  export function resolveApiKey(overrideKey?: string, configKey?: string): string {
27
50
  if (overrideKey && overrideKey.trim().length > 0) {
28
51
  return overrideKey.trim();
@@ -646,16 +669,29 @@ export function syncAllClients(options: SyncAllOptions = {}): SyncAllResult {
646
669
  }
647
670
  }
648
671
 
649
- // Persist sync state
672
+ // Persist sync state (merge with existing state if present)
650
673
  try {
651
674
  fs.mkdirSync(path.dirname(stateFilePath), { recursive: true });
675
+ let existingClients: SyncStateFile["clients"] = {};
676
+ if (fs.existsSync(stateFilePath)) {
677
+ try {
678
+ const raw = fs.readFileSync(stateFilePath, "utf-8");
679
+ const parsed = JSON.parse(raw);
680
+ if (parsed?.clients && typeof parsed.clients === "object") {
681
+ existingClients = parsed.clients;
682
+ }
683
+ } catch {}
684
+ }
652
685
  const stateContent: SyncStateFile = {
653
686
  version: 1,
654
687
  updatedAt: new Date().toISOString(),
655
688
  apiKey,
656
689
  port,
657
690
  host,
658
- clients: stateRecords,
691
+ clients: {
692
+ ...existingClients,
693
+ ...stateRecords,
694
+ },
659
695
  };
660
696
  fs.writeFileSync(stateFilePath, JSON.stringify(stateContent, null, 2) + "\n", "utf-8");
661
697
  } catch (err) {
@@ -670,87 +706,79 @@ export interface RestoreAllResult {
670
706
  details: ClientSyncResult[];
671
707
  }
672
708
 
673
- export function restoreAllClients(options: { stateFilePath?: string } = {}): RestoreAllResult {
709
+ export interface RestoreAllOptions {
710
+ stateFilePath?: string;
711
+ targets?: SyncClientName[];
712
+ }
713
+
714
+ export function restoreAllClients(options: RestoreAllOptions = {}): RestoreAllResult {
715
+ const defaultPaths = getDefaultPaths();
674
716
  const stateFilePath = options.stateFilePath || getDefaultStateFilePath();
675
717
  const details: ClientSyncResult[] = [];
676
718
  let restoredCount = 0;
677
719
 
678
- if (!fs.existsSync(stateFilePath)) {
679
- return { restoredCount: 0, details };
720
+ let state: SyncStateFile | null = null;
721
+ if (fs.existsSync(stateFilePath)) {
722
+ try {
723
+ const raw = fs.readFileSync(stateFilePath, "utf-8");
724
+ state = JSON.parse(raw);
725
+ } catch {}
680
726
  }
681
727
 
682
- try {
683
- const raw = fs.readFileSync(stateFilePath, "utf-8");
684
- const state: SyncStateFile = JSON.parse(raw);
685
-
686
- if (state.clients.claudeCode?.backupPath) {
687
- const res = restoreClaudeCode(state.clients.claudeCode.filePath, state.clients.claudeCode.backupPath);
688
- details.push(res);
689
- if (res.success) restoredCount++;
690
- }
691
-
692
- if (state.clients.codex?.backupPath) {
693
- const res = restoreCodex(state.clients.codex.filePath, state.clients.codex.backupPath);
694
- details.push(res);
695
- if (res.success) restoredCount++;
696
- }
697
-
698
- if (state.clients.openCode?.backupPath) {
699
- const res = restoreOpenCode(state.clients.openCode.filePath, state.clients.openCode.backupPath);
700
- details.push(res);
701
- if (res.success) restoredCount++;
702
- }
703
-
704
- if (state.clients.omp?.backupPath) {
705
- const res = restoreOmp(state.clients.omp.filePath, state.clients.omp.backupPath);
706
- details.push(res);
707
- if (res.success) restoredCount++;
708
- }
709
-
710
- if (state.clients.hermes?.backupPath) {
711
- const res = restoreHermes(state.clients.hermes.filePath, state.clients.hermes.backupPath);
712
- details.push(res);
713
- if (res.success) restoredCount++;
714
- }
715
-
716
- if (state.clients.openClaw?.backupPath) {
717
- const res = restoreOpenClaw(state.clients.openClaw.filePath, state.clients.openClaw.backupPath);
718
- details.push(res);
719
- if (res.success) restoredCount++;
720
- }
728
+ const shouldRestore = (client: SyncClientName) => {
729
+ if (!options.targets || options.targets.length === 0) return true;
730
+ return options.targets.includes(client);
731
+ };
721
732
 
722
- if (state.clients.kilo?.backupPath) {
723
- const res = restoreKilo(state.clients.kilo.filePath, state.clients.kilo.backupPath);
724
- details.push(res);
725
- if (res.success) restoredCount++;
726
- }
733
+ const restoreClientsList: Array<{
734
+ id: SyncClientName;
735
+ stateKey: keyof SyncStateFile["clients"];
736
+ defaultPath: string;
737
+ stateRecord?: SyncRecord;
738
+ restoreFn: (filePath: string, backupPath?: string) => ClientSyncResult;
739
+ }> = [
740
+ { id: "claude-code", stateKey: "claudeCode", defaultPath: defaultPaths.claudeCode, stateRecord: state?.clients?.claudeCode, restoreFn: restoreClaudeCode },
741
+ { id: "codex", stateKey: "codex", defaultPath: defaultPaths.codex, stateRecord: state?.clients?.codex, restoreFn: restoreCodex },
742
+ { id: "opencode", stateKey: "openCode", defaultPath: defaultPaths.openCode, stateRecord: state?.clients?.openCode, restoreFn: restoreOpenCode },
743
+ { id: "omp", stateKey: "omp", defaultPath: defaultPaths.omp, stateRecord: state?.clients?.omp, restoreFn: restoreOmp },
744
+ { id: "hermes", stateKey: "hermes", defaultPath: defaultPaths.hermes, stateRecord: state?.clients?.hermes, restoreFn: restoreHermes },
745
+ { id: "openclaw", stateKey: "openClaw", defaultPath: defaultPaths.openClaw, stateRecord: state?.clients?.openClaw, restoreFn: restoreOpenClaw },
746
+ { id: "kilo", stateKey: "kilo", defaultPath: defaultPaths.kilo, stateRecord: state?.clients?.kilo, restoreFn: restoreKilo },
747
+ { id: "cline", stateKey: "cline", defaultPath: defaultPaths.cline, stateRecord: state?.clients?.cline, restoreFn: restoreCline },
748
+ { id: "zed", stateKey: "zed", defaultPath: defaultPaths.zed, stateRecord: state?.clients?.zed, restoreFn: restoreZed },
749
+ { id: "aider", stateKey: "aider", defaultPath: defaultPaths.aider, stateRecord: state?.clients?.aider, restoreFn: restoreAider },
750
+ ];
727
751
 
728
- if (state.clients.cline?.backupPath) {
729
- const res = restoreCline(state.clients.cline.filePath, state.clients.cline.backupPath);
730
- details.push(res);
731
- if (res.success) restoredCount++;
732
- }
752
+ for (const c of restoreClientsList) {
753
+ if (!shouldRestore(c.id)) continue;
754
+ const filePath = c.stateRecord?.filePath || c.defaultPath;
755
+ const backupPath = c.stateRecord?.backupPath;
733
756
 
734
- if (state.clients.zed?.backupPath) {
735
- const res = restoreZed(state.clients.zed.filePath, state.clients.zed.backupPath);
736
- details.push(res);
737
- if (res.success) restoredCount++;
738
- }
757
+ const canAttempt = options.stateFilePath
758
+ ? Boolean(c.stateRecord)
759
+ : Boolean(c.stateRecord || fs.existsSync(filePath));
739
760
 
740
- if (state.clients.aider?.backupPath) {
741
- const res = restoreAider(state.clients.aider.filePath, state.clients.aider.backupPath);
761
+ if (canAttempt) {
762
+ const res = c.restoreFn(filePath, backupPath);
742
763
  details.push(res);
743
- if (res.success) restoredCount++;
764
+ if (res.success) {
765
+ restoredCount++;
766
+ if (state?.clients) {
767
+ delete state.clients[c.stateKey];
768
+ delete (state.clients as any)[c.id];
769
+ }
770
+ }
744
771
  }
745
-
746
- // Remove state file after successful restoration
772
+ }
773
+ // Update or delete state file
774
+ if (stateFilePath && fs.existsSync(stateFilePath)) {
747
775
  try {
748
- fs.unlinkSync(stateFilePath);
749
- } catch {
750
- // Ignore
751
- }
752
- } catch (err) {
753
- console.error("Error reading sync state file:", err);
776
+ if (state && state.clients && Object.keys(state.clients).length > 0) {
777
+ fs.writeFileSync(stateFilePath, JSON.stringify(state, null, 2) + "\n", "utf-8");
778
+ } else {
779
+ fs.unlinkSync(stateFilePath);
780
+ }
781
+ } catch {}
754
782
  }
755
783
 
756
784
  return { restoredCount, details };
package/src/sync/omp.ts CHANGED
@@ -99,13 +99,32 @@ export function syncOmp(options: SyncOptions): ClientSyncResult {
99
99
  }
100
100
 
101
101
  export function restoreOmp(filePath: string, backupPath?: string): ClientSyncResult {
102
- const restored = restoreFromBackup(filePath, backupPath);
102
+ const restoredFromBackup = restoreFromBackup(filePath, backupPath);
103
+
104
+ let manuallyCleaned = false;
105
+ if (fs.existsSync(filePath)) {
106
+ try {
107
+ let content = fs.readFileSync(filePath, "utf-8");
108
+ if (content.includes("qwenproxy:")) {
109
+ const regex = /^\s*qwenproxy:\s*\r?\n(?:^[ \t].*\r?\n?)*/m;
110
+ content = content.replace(regex, "");
111
+ fs.writeFileSync(filePath, content, "utf-8");
112
+ manuallyCleaned = true;
113
+ }
114
+ } catch {}
115
+ }
116
+
117
+ const success = restoredFromBackup || manuallyCleaned;
103
118
  return {
104
119
  client: "omp",
105
120
  filePath,
106
121
  backupPath,
107
- success: restored,
108
- action: restored ? "restored" : "failed",
109
- message: restored ? "Restored OMP models config from backup" : "Backup file not found",
122
+ success,
123
+ action: success ? "restored" : "failed",
124
+ message: success
125
+ ? restoredFromBackup
126
+ ? "Restored OMP models config from backup"
127
+ : "Removed QwenProxy configuration from OMP config"
128
+ : "Backup file not found",
110
129
  };
111
130
  }
@@ -206,13 +206,37 @@ export function syncOpenCode(options: SyncOptions): ClientSyncResult {
206
206
  }
207
207
 
208
208
  export function restoreOpenCode(filePath: string, backupPath?: string): ClientSyncResult {
209
- const restored = restoreFromBackup(filePath, backupPath);
209
+ const restoredFromBackup = restoreFromBackup(filePath, backupPath);
210
+
211
+ let manuallyCleaned = false;
212
+ if (fs.existsSync(filePath)) {
213
+ try {
214
+ const data = JSON.parse(fs.readFileSync(filePath, "utf-8"));
215
+ if (data.provider && data.provider.qwenproxy) {
216
+ delete data.provider.qwenproxy;
217
+ if (data.model && data.model.includes("qwen")) {
218
+ delete data.model;
219
+ }
220
+ if (Object.keys(data.provider).length === 0) {
221
+ delete data.provider;
222
+ }
223
+ fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n", "utf-8");
224
+ manuallyCleaned = true;
225
+ }
226
+ } catch {}
227
+ }
228
+
229
+ const success = restoredFromBackup || manuallyCleaned;
210
230
  return {
211
231
  client: "opencode",
212
232
  filePath,
213
233
  backupPath,
214
- success: restored,
215
- action: restored ? "restored" : "failed",
216
- message: restored ? "Restored OpenCode config from backup" : "Backup file not found",
234
+ success,
235
+ action: success ? "restored" : "failed",
236
+ message: success
237
+ ? restoredFromBackup
238
+ ? "Restored OpenCode config from backup"
239
+ : "Removed QwenProxy configuration from OpenCode config"
240
+ : "Backup file not found",
217
241
  };
218
242
  }
package/src/sync/utils.ts CHANGED
@@ -13,13 +13,42 @@ export function createTimestampBackup(filePath: string): string {
13
13
  return backupPath;
14
14
  }
15
15
 
16
+ export function findLatestBackup(filePath: string): string | undefined {
17
+ const dir = path.dirname(filePath);
18
+ const ext = path.extname(filePath);
19
+ const base = path.basename(filePath, ext);
20
+ if (!fs.existsSync(dir)) return undefined;
21
+
22
+ try {
23
+ const files = fs.readdirSync(dir);
24
+ const candidates = files
25
+ .filter((f) => f.startsWith(base) && f.includes("qwenproxy") && f.endsWith(".bak"))
26
+ .map((f) => path.join(dir, f))
27
+ .sort((a, b) => {
28
+ try {
29
+ return fs.statSync(b).mtimeMs - fs.statSync(a).mtimeMs;
30
+ } catch {
31
+ return 0;
32
+ }
33
+ });
34
+
35
+ return candidates[0];
36
+ } catch {
37
+ return undefined;
38
+ }
39
+ }
40
+
16
41
  export function restoreFromBackup(filePath: string, backupPath?: string): boolean {
17
- if (!backupPath || !fs.existsSync(backupPath)) {
42
+ const targetBackup = (backupPath && fs.existsSync(backupPath))
43
+ ? backupPath
44
+ : findLatestBackup(filePath);
45
+
46
+ if (!targetBackup || !fs.existsSync(targetBackup)) {
18
47
  return false;
19
48
  }
20
- fs.copyFileSync(backupPath, filePath);
49
+ fs.copyFileSync(targetBackup, filePath);
21
50
  try {
22
- fs.unlinkSync(backupPath);
51
+ fs.unlinkSync(targetBackup);
23
52
  } catch {
24
53
  // Ignore cleanup error
25
54
  }
package/src/sync/zed.ts CHANGED
@@ -198,13 +198,37 @@ export function syncZed(options: SyncOptions): ClientSyncResult {
198
198
  }
199
199
 
200
200
  export function restoreZed(filePath: string, backupPath?: string): ClientSyncResult {
201
- const restored = restoreFromBackup(filePath, backupPath);
201
+ const restoredFromBackup = restoreFromBackup(filePath, backupPath);
202
+
203
+ let manuallyCleaned = false;
204
+ if (fs.existsSync(filePath)) {
205
+ try {
206
+ let content = fs.readFileSync(filePath, "utf-8");
207
+ if (content.includes("127.0.0.1:7936") || content.includes("qwen3.8-max")) {
208
+ const data = parseJsonWithComments(content);
209
+ if (data.language_models?.openai?.api_url?.includes("7936")) {
210
+ delete data.language_models.openai;
211
+ if (Object.keys(data.language_models).length === 0) {
212
+ delete data.language_models;
213
+ }
214
+ fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n", "utf-8");
215
+ manuallyCleaned = true;
216
+ }
217
+ }
218
+ } catch {}
219
+ }
220
+
221
+ const success = restoredFromBackup || manuallyCleaned;
202
222
  return {
203
223
  client: "zed",
204
224
  filePath,
205
225
  backupPath,
206
- success: restored,
207
- action: restored ? "restored" : "failed",
208
- message: restored ? "Restored Zed settings from backup" : "Backup file not found",
226
+ success,
227
+ action: success ? "restored" : "failed",
228
+ message: success
229
+ ? restoredFromBackup
230
+ ? "Restored Zed settings from backup"
231
+ : "Removed QwenProxy configuration from Zed settings"
232
+ : "Backup file not found",
209
233
  };
210
234
  }
@@ -73,8 +73,8 @@ Exemplos:
73
73
  npm run sync zed # Sincroniza apenas o Zed Editor
74
74
  npm run sync aider # Sincroniza apenas o Aider
75
75
  npm run sync claude codex # Sincroniza múltiplos clientes específicos
76
- npm run sync -- --list # Lista status de detecção de todos os 10 clientes
77
- npm run sync -- --restore # Restaura as configurações originais (rollback)
76
+ npm run sync --list # Lista status de detecção de todos os 10 clientes (ou qpx sync --list)
77
+ npm run sync --restore # Restaura as configurações originais (ou qpx sync --restore)
78
78
 
79
79
  Opções:
80
80
  --client <nome> Nome do cliente (hermes, opencode, claude, openclaw, kilo, cline, omp, codex, zed, aider)
@@ -193,8 +193,7 @@ async function main() {
193
193
  console.log("--------------------------------------------------");
194
194
  console.log(`✨ ${count} cliente(s) sincronizado(s) com zero perda de outras configs/provedores!`);
195
195
  console.log("💡 Para desfazer e restaurar a qualquer momento:");
196
- console.log(" npm run sync -- --restore");
197
- console.log("==================================================\n");
196
+ console.log(" qpx sync --restore (ou npm run sync --restore)");
198
197
  }
199
198
 
200
199
  main().catch((err) => {
@@ -2043,18 +2043,23 @@ export class StreamingToolParser {
2043
2043
  recoveryAttempts: truncRecoveryAttempts,
2044
2044
  });
2045
2045
  logger.warn(
2046
- "[parser] Dropping unrecoverable unclosed tool call at end of stream",
2047
- {
2048
- toolName,
2049
- category: "truncated",
2050
- contentLength: trimmed.length,
2051
- content: trimmed.substring(0, 2000),
2052
- failureReason:
2053
- "stream ended before tool_call closing tag; content too incomplete to reconstruct",
2054
- recoveryAttempts: truncRecoveryAttempts,
2055
- emittedToolCallsSoFar: this.emittedToolCallCount,
2056
- },
2046
+ `[parser] Dropping unrecoverable unclosed tool call (${toolName || "unknown"}) at end of stream: stream ended before closing tag (${trimmed.length} chars)`,
2057
2047
  );
2048
+ if (isToolcallDebugEnabled()) {
2049
+ logger.debug(
2050
+ "[parser] Unclosed tool call payload details",
2051
+ {
2052
+ toolName,
2053
+ category: "truncated",
2054
+ contentLength: trimmed.length,
2055
+ content: trimmed.substring(0, 2000),
2056
+ failureReason:
2057
+ "stream ended before tool_call closing tag; content too incomplete to reconstruct",
2058
+ recoveryAttempts: truncRecoveryAttempts,
2059
+ emittedToolCallsSoFar: this.emittedToolCallCount,
2060
+ },
2061
+ );
2062
+ }
2058
2063
  if (
2059
2064
  this.emittedToolCallCount === 0 &&
2060
2065
  this.pendingLeadIn.trim().length > 0
package/src/tui/app.ts CHANGED
@@ -5,7 +5,7 @@
5
5
  import { Screen, type KeyEvent } from "./screen.ts";
6
6
  import type { TuiView, ProxyStatusSnapshot } from "./types.ts";
7
7
  import { theme, glyphs, drawBox, stringWidth } from "./theme.ts";
8
- import { fetchProxyStatus } from "./proxy-client.ts";
8
+ import { fetchProxyStatus, fetchLiveModels, getCachedLiveModels } from "./proxy-client.ts";
9
9
  import { ServerManager } from "./server-manager.ts";
10
10
  import fs from "node:fs";
11
11
  import path from "node:path";
@@ -128,10 +128,28 @@ export class TuiApp {
128
128
  } catch {}
129
129
 
130
130
  // Background polling every 1s for live status updates and model catalog synchronization
131
+ let pollCount = 0;
131
132
  this.pollInterval = setInterval(async () => {
132
133
  if (!this.isRunning) return;
134
+ pollCount++;
133
135
  try {
134
136
  this.statusSnapshot = await fetchProxyStatus();
137
+
138
+ // Sync live models catalog when server is running:
139
+ // Try every 5s until first successful catalog load, then refresh periodically every 30s
140
+ const hasLiveModels = getCachedLiveModels() !== null;
141
+ const shouldSync = !hasLiveModels ? pollCount % 5 === 1 : pollCount % 30 === 1;
142
+ if (shouldSync) {
143
+ const models = await fetchLiveModels(hasLiveModels);
144
+ if (models.length > 0) {
145
+ for (const view of this.views) {
146
+ if ("refreshModels" in view && typeof (view as any).refreshModels === "function") {
147
+ void (view as any).refreshModels();
148
+ }
149
+ }
150
+ }
151
+ }
152
+
135
153
  this.requestRender();
136
154
  } catch {}
137
155
  }, 1000);