@tokenbuddy/tokenbuddy 1.0.40 → 1.0.41

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.
@@ -154,11 +154,9 @@ export declare function previewProviderInstall(options: ProviderInstallOptions):
154
154
  */
155
155
  export declare function applyProviderInstall(options: ProviderInstallOptions, store: BuyerStore): ProviderApplyResult[];
156
156
  /**
157
- * 回滚 provider 安装。
158
- * 从 `store` 读取安装前的快照,恢复原文件(如果快照里有原始内容)。
159
- * 恢复后仍会执行 provider cleanup,确保 repeated apply 或旧 TokenBuddy
160
- * 配置升级后的 disconnect 语义是移除 TokenBuddy,而不是恢复旧 TokenBuddy。
161
- * 没有快照的 provider 标记为 `missing_snapshot`。
157
+ * 断开 provider 安装。
158
+ * 从 `store` 读取首次 connect 前的快照,精确恢复原文件或删除当次创建的文件。
159
+ * 没有快照时不猜测、不清理第三方配置,只返回 `missing_snapshot`。
162
160
  *
163
161
  * @param options 回滚选项
164
162
  * @param store buyer store
@@ -5,22 +5,10 @@ export const PROXY_ACCESS_TOKEN_PLACEHOLDER = "tbp_local_17821_d7f4c9a2b8e1";
5
5
  const DESKTOP_PROFILE_ID = "00000000-0000-4000-8000-000000178210";
6
6
  const CODEX_PROVIDER_ID = "TokenBuddy";
7
7
  const HERMES_PROVIDER_ID = "TokenBuddy";
8
- const OPENCLAW_TOKENBUDDY_PROVIDER_IDS = ["tokenbuddy", "tokens-buddy"];
9
8
  const CLAUDE_ONE_M_MARKER = "[1M]";
10
9
  const CLAUDE_CLIENT_HAIKU_MODEL = "claude-haiku-4-5";
11
10
  const CLAUDE_CLIENT_SONNET_MODEL = "claude-sonnet-4-6";
12
11
  const CLAUDE_CLIENT_OPUS_MODEL = "claude-opus-4-7";
13
- const CLAUDE_CODE_TOKENBUDDY_ENV_KEYS = [
14
- "ANTHROPIC_BASE_URL",
15
- "ANTHROPIC_AUTH_TOKEN",
16
- "ANTHROPIC_DEFAULT_HAIKU_MODEL",
17
- "ANTHROPIC_DEFAULT_HAIKU_MODEL_NAME",
18
- "ANTHROPIC_DEFAULT_SONNET_MODEL",
19
- "ANTHROPIC_DEFAULT_SONNET_MODEL_NAME",
20
- "ANTHROPIC_DEFAULT_OPUS_MODEL",
21
- "ANTHROPIC_DEFAULT_OPUS_MODEL_NAME",
22
- "ANTHROPIC_MODEL",
23
- ];
24
12
  export const SUPPORTED_PROVIDER_IDS = [
25
13
  "codex",
26
14
  "claude-code",
@@ -223,85 +211,8 @@ function upsertTopLevelYamlObjectEntry(existing, sectionName, entryName, entryVa
223
211
  };
224
212
  return replaceTopLevelYamlSection(existing, sectionName, yamlContent(section));
225
213
  }
226
- function removeTopLevelYamlObjectEntry(existing, sectionName, entryName, shouldRemove) {
227
- const current = parseSimpleYamlObject(existing);
228
- const section = isPlainRecord(current[sectionName])
229
- ? { ...current[sectionName] }
230
- : {};
231
- const entry = section[entryName];
232
- if (!isPlainRecord(entry) || !shouldRemove(entry)) {
233
- return existing;
234
- }
235
- delete section[entryName];
236
- if (Object.keys(section).length === 0) {
237
- return removeTopLevelYamlSection(existing, sectionName);
238
- }
239
- return replaceTopLevelYamlSection(existing, sectionName, yamlContent(section));
240
- }
241
- function unquoteYamlScalar(value) {
242
- const trimmed = value.trim();
243
- if ((trimmed.startsWith('"') && trimmed.endsWith('"')) || (trimmed.startsWith("'") && trimmed.endsWith("'"))) {
244
- return trimmed.slice(1, -1);
245
- }
246
- return trimmed;
247
- }
248
- function normalizedProviderName(value) {
249
- return value.trim().toLowerCase().replace(/[^a-z0-9]/g, "");
250
- }
251
- function isTokenBuddyProviderIdentifier(value) {
252
- if (typeof value !== "string") {
253
- return false;
254
- }
255
- const normalized = normalizedProviderName(value);
256
- return normalized === "tokenbuddy" || normalized === "tokensbuddy";
257
- }
258
214
  function isTokenBuddyProxyUrl(value) {
259
- return typeof value === "string" && /(?:127\.0\.0\.1|localhost):17821\b/.test(value);
260
- }
261
- function yamlTopLevelListSectionEnd(lines, sectionStart) {
262
- let index = sectionStart + 1;
263
- while (index < lines.length) {
264
- const line = lines[index];
265
- if (line.trim() && /^[A-Za-z_][A-Za-z0-9_-]*:/.test(line)) {
266
- break;
267
- }
268
- index += 1;
269
- }
270
- return index;
271
- }
272
- function removeTopLevelYamlListItems(existing, sectionName, shouldRemove) {
273
- const lines = existing.split(/\r?\n/);
274
- const sectionStart = lines.findIndex((line) => line === `${sectionName}:` || line.startsWith(`${sectionName}: `));
275
- if (sectionStart < 0) {
276
- return existing;
277
- }
278
- const sectionEnd = yamlTopLevelListSectionEnd(lines, sectionStart);
279
- const sectionLines = lines.slice(sectionStart + 1, sectionEnd);
280
- const keptLines = [];
281
- let index = 0;
282
- while (index < sectionLines.length) {
283
- const line = sectionLines[index];
284
- if (!line.startsWith("- ")) {
285
- keptLines.push(line);
286
- index += 1;
287
- continue;
288
- }
289
- const itemStart = index;
290
- index += 1;
291
- while (index < sectionLines.length && !sectionLines[index].startsWith("- ")) {
292
- index += 1;
293
- }
294
- const itemLines = sectionLines.slice(itemStart, index);
295
- if (!shouldRemove(itemLines)) {
296
- keptLines.push(...itemLines);
297
- }
298
- }
299
- const before = lines.slice(0, sectionStart);
300
- const after = lines.slice(sectionEnd);
301
- const nextLines = keptLines.some((line) => line.trim())
302
- ? [...before, `${sectionName}:`, ...keptLines, ...after]
303
- : [...before, ...after];
304
- return `${nextLines.join("\n").replace(/\n*$/, "")}\n`;
215
+ return typeof value === "string" && /(?:127\.0\.0\.1|localhost):\d+\b/.test(value);
305
216
  }
306
217
  function readObjectField(value, key) {
307
218
  if (!value || typeof value !== "object" || Array.isArray(value)) {
@@ -312,13 +223,6 @@ function readObjectField(value, key) {
312
223
  ? field
313
224
  : undefined;
314
225
  }
315
- function removeObjectKey(value, key) {
316
- if (!Object.prototype.hasOwnProperty.call(value, key)) {
317
- return false;
318
- }
319
- delete value[key];
320
- return true;
321
- }
322
226
  function jsonContent(value) {
323
227
  return `${JSON.stringify(value, null, 2)}\n`;
324
228
  }
@@ -508,36 +412,13 @@ function codexConfig(home, proxyUrl, config) {
508
412
  function isCodexTokenBuddyConfigured(filePath) {
509
413
  const text = readText(filePath) || "";
510
414
  const providerSection = readTomlSection(text, `model_providers.${CODEX_PROVIDER_ID}`);
511
- const legacySection = readTomlSection(text, "tokenbuddy");
512
415
  const topLevelProvider = readTopLevelTomlString(text, "model_provider");
513
416
  const hasCurrentProvider = Boolean(providerSection) &&
514
417
  topLevelProvider === CODEX_PROVIDER_ID &&
515
418
  /wire_api\s*=\s*["']responses["']/.test(providerSection || "") &&
516
419
  /base_url\s*=\s*["'][^"']*127\.0\.0\.1[^"']*\/v1["']/.test(providerSection || "") &&
517
420
  new RegExp(`experimental_bearer_token\\s*=\\s*["']${escapeRegex(PROXY_ACCESS_TOKEN_PLACEHOLDER)}["']`).test(providerSection || "");
518
- const hasLegacyProvider = Boolean(legacySection) &&
519
- /proxy_url\s*=\s*["'][^"']*127\.0\.0\.1/.test(legacySection || "") &&
520
- new RegExp(`api_key\\s*=\\s*["']${escapeRegex(PROXY_ACCESS_TOKEN_PLACEHOLDER)}["']`).test(legacySection || "");
521
- return hasCurrentProvider || hasLegacyProvider;
522
- }
523
- function cleanupCodexConfig(home) {
524
- const configPath = path.join(home, ".codex", "config.toml");
525
- if (!fs.existsSync(configPath) || !isCodexTokenBuddyConfigured(configPath)) {
526
- return [];
527
- }
528
- let next = readText(configPath) || "";
529
- next = removeTomlSection(next, `model_providers.${CODEX_PROVIDER_ID}`);
530
- next = removeTomlSection(next, "tokenbuddy");
531
- if (readTopLevelTomlString(next, "model_provider") === CODEX_PROVIDER_ID) {
532
- next = removeTopLevelTomlKey(next, "model_provider");
533
- next = removeTopLevelTomlKey(next, "model");
534
- }
535
- if (next.trim()) {
536
- fs.writeFileSync(configPath, next, "utf8");
537
- return [{ providerId: "codex", path: configPath, action: "cleaned" }];
538
- }
539
- fs.rmSync(configPath, { force: true });
540
- return [{ providerId: "codex", path: configPath, action: "removed" }];
421
+ return hasCurrentProvider;
541
422
  }
542
423
  function resolveClaudeFallbackAlias(config) {
543
424
  if (config.roles.sonnet?.upstreamModel) {
@@ -630,36 +511,6 @@ function isClaudeCodeTokenBuddyConfigured(filePath) {
630
511
  typeof env.ANTHROPIC_BASE_URL === "string" &&
631
512
  env.ANTHROPIC_BASE_URL.trim().length > 0;
632
513
  }
633
- function cleanupClaudeCodeConfig(home) {
634
- const configPath = path.join(home, ".claude", "settings.json");
635
- if (!fs.existsSync(configPath) || !isClaudeCodeTokenBuddyConfigured(configPath)) {
636
- return [];
637
- }
638
- const current = readJsonObject(configPath);
639
- const env = readObjectField(current, "env");
640
- if (!env) {
641
- return [];
642
- }
643
- let changed = false;
644
- for (const key of CLAUDE_CODE_TOKENBUDDY_ENV_KEYS) {
645
- changed = removeObjectKey(env, key) || changed;
646
- }
647
- if (!changed) {
648
- return [];
649
- }
650
- if (Object.keys(env).length > 0) {
651
- current.env = env;
652
- }
653
- else {
654
- delete current.env;
655
- }
656
- if (Object.keys(current).length === 0) {
657
- fs.rmSync(configPath, { force: true });
658
- return [{ providerId: "claude-code", path: configPath, action: "removed" }];
659
- }
660
- fs.writeFileSync(configPath, jsonContent(current), "utf8");
661
- return [{ providerId: "claude-code", path: configPath, action: "cleaned" }];
662
- }
663
514
  function claudeDesktopConfig(home, proxyUrl, config) {
664
515
  const model = pickConfiguredModel(config);
665
516
  const models = orderedModelsForProtocol(config, "messages", model);
@@ -720,60 +571,6 @@ function isClaudeDesktopTokenBuddyConfigured(_filePath, home) {
720
571
  return meta.appliedId === DESKTOP_PROFILE_ID &&
721
572
  isClaudeDesktopProfileTokenBuddyConfigured(paths.profilePath);
722
573
  }
723
- function cleanupClaudeDesktopConfig(home) {
724
- const paths = claudeDesktopPaths(home);
725
- const results = [];
726
- const meta = readJsonObject(paths.metaPath);
727
- const tokenBuddyIsActive = meta.appliedId === DESKTOP_PROFILE_ID;
728
- const primary = readJsonObject(paths.configPath);
729
- if (tokenBuddyIsActive && primary.deploymentMode === "3p") {
730
- delete primary.deploymentMode;
731
- if (Object.keys(primary).length > 0) {
732
- fs.writeFileSync(paths.configPath, jsonContent(primary), "utf8");
733
- results.push({ providerId: "claude-desktop", path: paths.configPath, action: "cleaned" });
734
- }
735
- else {
736
- fs.rmSync(paths.configPath, { force: true });
737
- results.push({ providerId: "claude-desktop", path: paths.configPath, action: "removed" });
738
- }
739
- }
740
- const threep = readJsonObject(paths.threepConfigPath);
741
- if (tokenBuddyIsActive && threep.deploymentMode === "3p") {
742
- delete threep.deploymentMode;
743
- if (Object.keys(threep).length > 0) {
744
- fs.writeFileSync(paths.threepConfigPath, jsonContent(threep), "utf8");
745
- results.push({ providerId: "claude-desktop", path: paths.threepConfigPath, action: "cleaned" });
746
- }
747
- else {
748
- fs.rmSync(paths.threepConfigPath, { force: true });
749
- results.push({ providerId: "claude-desktop", path: paths.threepConfigPath, action: "removed" });
750
- }
751
- }
752
- if (fs.existsSync(paths.profilePath)) {
753
- fs.rmSync(paths.profilePath, { force: true });
754
- results.push({ providerId: "claude-desktop", path: paths.profilePath, action: "removed" });
755
- }
756
- let changedMeta = false;
757
- if (meta.appliedId === DESKTOP_PROFILE_ID) {
758
- delete meta.appliedId;
759
- changedMeta = true;
760
- }
761
- if (Array.isArray(meta.entries)) {
762
- const nextEntries = meta.entries.filter((entry) => {
763
- return !(isPlainRecord(entry) &&
764
- (entry.id === DESKTOP_PROFILE_ID || entry.name === "TokenBuddy"));
765
- });
766
- if (nextEntries.length !== meta.entries.length) {
767
- meta.entries = nextEntries;
768
- changedMeta = true;
769
- }
770
- }
771
- if (changedMeta) {
772
- fs.writeFileSync(paths.metaPath, jsonContent(meta), "utf8");
773
- results.push({ providerId: "claude-desktop", path: paths.metaPath, action: "cleaned" });
774
- }
775
- return results;
776
- }
777
574
  function openclawConfig(home, proxyUrl, config) {
778
575
  const model = pickConfiguredModel(config);
779
576
  const configuredModels = orderedModelsForProtocol(config, "chat_completions", model);
@@ -806,81 +603,19 @@ function openclawConfig(home, proxyUrl, config) {
806
603
  current.agents = agents;
807
604
  return [makeChange("openclaw", configPath, "configure OpenClaw proxy settings", jsonContent(current))];
808
605
  }
809
- function isOpenclawTokenBuddyModelRef(value) {
810
- if (typeof value !== "string") {
811
- return false;
812
- }
813
- return isTokenBuddyProviderIdentifier(value.split("/", 1)[0]);
814
- }
815
606
  function isOpenclawTokenBuddyConfigured(filePath) {
816
607
  const current = readJsonObject(filePath);
817
608
  const providers = readObjectField(readObjectField(current, "models"), "providers");
818
- const defaults = readObjectField(readObjectField(current, "agents"), "defaults");
819
609
  if (providers) {
820
- for (const providerId of OPENCLAW_TOKENBUDDY_PROVIDER_IDS) {
821
- if (Object.prototype.hasOwnProperty.call(providers, providerId)) {
822
- return true;
823
- }
824
- }
825
- }
826
- if (defaults && isOpenclawTokenBuddyModelRef(defaults.model)) {
827
- return true;
828
- }
829
- const defaultModels = readObjectField(defaults, "models");
830
- if (defaultModels && Object.keys(defaultModels).some((modelRef) => isOpenclawTokenBuddyModelRef(modelRef))) {
831
- return true;
832
- }
833
- const agentsList = readObjectField(current, "agents")?.list;
834
- return Array.isArray(agentsList) && agentsList.some((agent) => {
835
- return isPlainRecord(agent) && isOpenclawTokenBuddyModelRef(agent.model);
836
- });
837
- }
838
- function cleanupOpenclawConfig(home) {
839
- const configPath = path.join(home, ".openclaw", "openclaw.json");
840
- if (!fs.existsSync(configPath)) {
841
- return [];
842
- }
843
- const current = readJsonObject(configPath);
844
- const models = readObjectField(current, "models");
845
- const providers = readObjectField(models, "providers");
846
- const agents = readObjectField(current, "agents");
847
- const defaults = readObjectField(agents, "defaults");
848
- let changed = false;
849
- if (providers) {
850
- for (const providerId of OPENCLAW_TOKENBUDDY_PROVIDER_IDS) {
851
- changed = removeObjectKey(providers, providerId) || changed;
852
- }
853
- if (models && Object.keys(providers).length === 0) {
854
- delete models.providers;
855
- }
856
- }
857
- if (defaults && isOpenclawTokenBuddyModelRef(defaults.model)) {
858
- delete defaults.model;
859
- changed = true;
860
- }
861
- const defaultModels = readObjectField(defaults, "models");
862
- if (defaultModels) {
863
- for (const modelRef of Object.keys(defaultModels)) {
864
- changed = (isOpenclawTokenBuddyModelRef(modelRef) && removeObjectKey(defaultModels, modelRef)) || changed;
865
- }
866
- if (Object.keys(defaultModels).length === 0 && defaults) {
867
- delete defaults.models;
868
- }
869
- }
870
- const agentsList = agents?.list;
871
- if (Array.isArray(agentsList)) {
872
- for (const agent of agentsList) {
873
- if (isPlainRecord(agent) && isOpenclawTokenBuddyModelRef(agent.model)) {
874
- delete agent.model;
875
- changed = true;
876
- }
610
+ const provider = readObjectField(providers, "tokenbuddy");
611
+ if (provider?.apiKey === PROXY_ACCESS_TOKEN_PLACEHOLDER &&
612
+ isTokenBuddyProxyUrl(provider.baseUrl) &&
613
+ provider.auth === "api-key" &&
614
+ provider.api === "openai-completions") {
615
+ return true;
877
616
  }
878
617
  }
879
- if (!changed) {
880
- return [];
881
- }
882
- fs.writeFileSync(configPath, jsonContent(current), "utf8");
883
- return [{ providerId: "openclaw", path: configPath, action: "cleaned" }];
618
+ return false;
884
619
  }
885
620
  function openAiBaseUrl(proxyUrl) {
886
621
  const normalized = proxyUrl.replace(/\/+$/, "");
@@ -953,10 +688,6 @@ function opencodeDefaultModelRef(defaultModel, modelsByProvider) {
953
688
  }
954
689
  return `tokenbuddy/${defaultModel}`;
955
690
  }
956
- function isOpencodeTokenBuddyModelRef(value) {
957
- return typeof value === "string" &&
958
- OPENCODE_TOKENBUDDY_PROVIDERS.some((provider) => value.startsWith(`${provider.providerId}/`));
959
- }
960
691
  function opencodeConfig(home, proxyUrl, config) {
961
692
  const model = pickConfiguredModel(config);
962
693
  const configPath = path.join(home, ".config", "opencode", "opencode.json");
@@ -991,71 +722,25 @@ function opencodeConfig(home, proxyUrl, config) {
991
722
  current.small_model = defaultModelRef;
992
723
  return [makeChange("opencode", configPath, "configure OpenCode provider for TokenBuddy proxy", jsonContent(current))];
993
724
  }
994
- function isLegacyOpencodeTokenBuddyProvider(providerId, providerConfig) {
995
- if (providerId !== "openai" || !isPlainRecord(providerConfig)) {
725
+ function isCurrentOpencodeTokenBuddyProvider(providerConfig) {
726
+ if (!isPlainRecord(providerConfig)) {
996
727
  return false;
997
728
  }
998
729
  const options = readObjectField(providerConfig, "options");
999
- const apiKey = options?.apiKey;
1000
- const tokenBuddyKey = apiKey === PROXY_ACCESS_TOKEN_PLACEHOLDER ||
1001
- (typeof apiKey === "string" && apiKey.toUpperCase().startsWith("TOKENBUDDY_"));
1002
- return tokenBuddyKey && isTokenBuddyProxyUrl(options?.baseURL);
730
+ return options?.apiKey === PROXY_ACCESS_TOKEN_PLACEHOLDER &&
731
+ isTokenBuddyProxyUrl(options.baseURL);
1003
732
  }
1004
733
  function isOpencodeTokenBuddyConfigured(filePath) {
1005
734
  const current = readJsonObject(filePath);
1006
735
  const providers = readObjectField(current, "provider");
1007
736
  if (providers) {
1008
737
  for (const provider of OPENCODE_TOKENBUDDY_PROVIDERS) {
1009
- if (Object.prototype.hasOwnProperty.call(providers, provider.providerId)) {
1010
- return true;
1011
- }
1012
- }
1013
- for (const [providerId, providerConfig] of Object.entries(providers)) {
1014
- if (isLegacyOpencodeTokenBuddyProvider(providerId, providerConfig)) {
738
+ if (isCurrentOpencodeTokenBuddyProvider(providers[provider.providerId])) {
1015
739
  return true;
1016
740
  }
1017
741
  }
1018
742
  }
1019
- return isOpencodeTokenBuddyModelRef(current.model) || isOpencodeTokenBuddyModelRef(current.small_model);
1020
- }
1021
- function cleanupOpencodeConfig(home) {
1022
- const configPath = path.join(home, ".config", "opencode", "opencode.json");
1023
- if (!fs.existsSync(configPath)) {
1024
- return [];
1025
- }
1026
- const current = readJsonObject(configPath);
1027
- const providers = readObjectField(current, "provider");
1028
- let changed = false;
1029
- if (providers) {
1030
- for (const provider of OPENCODE_TOKENBUDDY_PROVIDERS) {
1031
- changed = removeObjectKey(providers, provider.providerId) || changed;
1032
- }
1033
- for (const [providerId, providerConfig] of Object.entries(providers)) {
1034
- changed = (isLegacyOpencodeTokenBuddyProvider(providerId, providerConfig) && removeObjectKey(providers, providerId)) || changed;
1035
- }
1036
- if (Object.keys(providers).length === 0) {
1037
- delete current.provider;
1038
- }
1039
- }
1040
- const model = current.model;
1041
- if (isOpencodeTokenBuddyModelRef(model)) {
1042
- delete current.model;
1043
- changed = true;
1044
- }
1045
- const smallModel = current.small_model;
1046
- if (isOpencodeTokenBuddyModelRef(smallModel)) {
1047
- delete current.small_model;
1048
- changed = true;
1049
- }
1050
- if (!changed) {
1051
- return [];
1052
- }
1053
- if (Object.keys(current).length === 0) {
1054
- fs.rmSync(configPath, { force: true });
1055
- return [{ providerId: "opencode", path: configPath, action: "removed" }];
1056
- }
1057
- fs.writeFileSync(configPath, jsonContent(current), "utf8");
1058
- return [{ providerId: "opencode", path: configPath, action: "cleaned" }];
743
+ return false;
1059
744
  }
1060
745
  function hermesConfig(home, proxyUrl, config) {
1061
746
  const model = pickConfiguredModel(config);
@@ -1092,50 +777,13 @@ function isHermesTokenBuddyProviderValue(value) {
1092
777
  return false;
1093
778
  }
1094
779
  const normalized = value.trim().toLowerCase();
1095
- return normalized === "custom" ||
1096
- normalized === HERMES_PROVIDER_ID.toLowerCase() ||
1097
- normalized === "custom:tokenbuddy";
780
+ return normalized === HERMES_PROVIDER_ID.toLowerCase();
1098
781
  }
1099
782
  function isHermesTokenBuddyProviderEntry(entry) {
1100
783
  return entry.api_key === PROXY_ACCESS_TOKEN_PLACEHOLDER &&
1101
784
  isHermesTokenBuddyBaseUrl(entry.base_url) &&
1102
785
  (entry.transport === "chat_completions" || entry.api_mode === "chat_completions");
1103
786
  }
1104
- function isHermesTokenBuddyCustomProviderItem(itemLines) {
1105
- let name = "";
1106
- let baseUrl = "";
1107
- let apiKey = "";
1108
- for (const line of itemLines) {
1109
- const nameMatch = /^\s*-\s*name:\s*(.+)\s*$/.exec(line) ?? /^\s*name:\s*(.+)\s*$/.exec(line);
1110
- if (nameMatch) {
1111
- name = unquoteYamlScalar(nameMatch[1]);
1112
- continue;
1113
- }
1114
- const baseUrlMatch = /^\s*base_url:\s*(.+)\s*$/.exec(line);
1115
- if (baseUrlMatch) {
1116
- baseUrl = unquoteYamlScalar(baseUrlMatch[1]);
1117
- continue;
1118
- }
1119
- const apiKeyMatch = /^\s*api_key:\s*(.+)\s*$/.exec(line);
1120
- if (apiKeyMatch) {
1121
- apiKey = unquoteYamlScalar(apiKeyMatch[1]);
1122
- }
1123
- }
1124
- const normalizedName = normalizedProviderName(name);
1125
- return normalizedName === "tokenbuddy" ||
1126
- normalizedName === "tokensbuddy" ||
1127
- (isHermesTokenBuddyBaseUrl(baseUrl) && apiKey === PROXY_ACCESS_TOKEN_PLACEHOLDER);
1128
- }
1129
- function hasHermesTokenBuddyCustomProvider(text) {
1130
- let found = false;
1131
- removeTopLevelYamlListItems(text, "custom_providers", (itemLines) => {
1132
- if (isHermesTokenBuddyCustomProviderItem(itemLines)) {
1133
- found = true;
1134
- }
1135
- return false;
1136
- });
1137
- return found;
1138
- }
1139
787
  function isHermesTokenBuddyModelConfig(modelConfig, namedProviderConfigured) {
1140
788
  return isHermesTokenBuddyProviderValue(modelConfig.provider) &&
1141
789
  modelConfig.api_key === PROXY_ACCESS_TOKEN_PLACEHOLDER &&
@@ -1145,41 +793,16 @@ function isHermesTokenBuddyModelConfig(modelConfig, namedProviderConfigured) {
1145
793
  modelConfig.default.length > 0;
1146
794
  }
1147
795
  function isHermesTokenBuddyConfigured(filePath) {
1148
- const text = readText(filePath) || "";
1149
796
  const current = readYamlObject(filePath);
1150
797
  const modelConfig = readObjectField(current, "model");
1151
798
  if (!modelConfig) {
1152
- return hasHermesTokenBuddyCustomProvider(text);
799
+ return false;
1153
800
  }
1154
- const hasTokenBuddyCustomProvider = hasHermesTokenBuddyCustomProvider(text);
1155
801
  const providersConfig = readObjectField(current, "providers");
1156
802
  const tokenBuddyProvider = readObjectField(providersConfig, HERMES_PROVIDER_ID);
1157
803
  const namedProviderConfigured = Boolean(tokenBuddyProvider && isHermesTokenBuddyProviderEntry(tokenBuddyProvider));
1158
804
  const hasTokenBuddyModel = isHermesTokenBuddyModelConfig(modelConfig, namedProviderConfigured);
1159
- return hasTokenBuddyModel || hasTokenBuddyCustomProvider;
1160
- }
1161
- function cleanupHermesConfig(home) {
1162
- const configPath = path.join(home, ".hermes", "config.yaml");
1163
- if (!fs.existsSync(configPath) || !isHermesTokenBuddyConfigured(configPath)) {
1164
- return [];
1165
- }
1166
- const existing = readText(configPath) || "";
1167
- const current = parseSimpleYamlObject(existing);
1168
- const modelConfig = readObjectField(current, "model");
1169
- const providersConfig = readObjectField(current, "providers");
1170
- const tokenBuddyProvider = readObjectField(providersConfig, HERMES_PROVIDER_ID);
1171
- const namedProviderConfigured = Boolean(tokenBuddyProvider && isHermesTokenBuddyProviderEntry(tokenBuddyProvider));
1172
- const withoutModel = modelConfig && isHermesTokenBuddyModelConfig(modelConfig, namedProviderConfigured)
1173
- ? removeTopLevelYamlSection(existing, "model")
1174
- : existing;
1175
- const withoutProvider = removeTopLevelYamlObjectEntry(withoutModel, "providers", HERMES_PROVIDER_ID, isHermesTokenBuddyProviderEntry);
1176
- const next = removeTopLevelYamlListItems(withoutProvider, "custom_providers", isHermesTokenBuddyCustomProviderItem);
1177
- if (next.trim()) {
1178
- fs.writeFileSync(configPath, next, "utf8");
1179
- return [{ providerId: "hermes", path: configPath, action: "cleaned" }];
1180
- }
1181
- fs.rmSync(configPath, { force: true });
1182
- return [{ providerId: "hermes", path: configPath, action: "removed" }];
805
+ return hasTokenBuddyModel;
1183
806
  }
1184
807
  const PROVIDERS = [
1185
808
  {
@@ -1189,7 +812,6 @@ const PROVIDERS = [
1189
812
  configPath: (home) => path.join(home, ".codex", "config.toml"),
1190
813
  isConfigured: isCodexTokenBuddyConfigured,
1191
814
  changes: codexConfig,
1192
- cleanup: cleanupCodexConfig,
1193
815
  modelSelectionKind: "single-model",
1194
816
  protocolPreference: "responses",
1195
817
  },
@@ -1200,7 +822,6 @@ const PROVIDERS = [
1200
822
  configPath: (home) => path.join(home, ".claude", "settings.json"),
1201
823
  isConfigured: isClaudeCodeTokenBuddyConfigured,
1202
824
  changes: claudeCodeConfig,
1203
- cleanup: cleanupClaudeCodeConfig,
1204
825
  modelSelectionKind: "claude-role-mapping",
1205
826
  protocolPreference: "messages",
1206
827
  },
@@ -1210,7 +831,6 @@ const PROVIDERS = [
1210
831
  configPath: (home) => path.join(home, "Library", "Application Support", "Claude", "claude_desktop_config.json"),
1211
832
  isConfigured: isClaudeDesktopTokenBuddyConfigured,
1212
833
  changes: claudeDesktopConfig,
1213
- cleanup: cleanupClaudeDesktopConfig,
1214
834
  modelSelectionKind: "single-model",
1215
835
  protocolPreference: "messages",
1216
836
  },
@@ -1225,7 +845,6 @@ const PROVIDERS = [
1225
845
  path.join(home, ".openclaw", "config.json"),
1226
846
  ],
1227
847
  changes: openclawConfig,
1228
- cleanup: cleanupOpenclawConfig,
1229
848
  modelSelectionKind: "single-model",
1230
849
  protocolPreference: "chat_completions",
1231
850
  },
@@ -1236,7 +855,6 @@ const PROVIDERS = [
1236
855
  configPath: (home) => path.join(home, ".config", "opencode", "opencode.json"),
1237
856
  isConfigured: isOpencodeTokenBuddyConfigured,
1238
857
  changes: opencodeConfig,
1239
- cleanup: cleanupOpencodeConfig,
1240
858
  modelSelectionKind: "single-model",
1241
859
  protocolPreference: "chat_completions",
1242
860
  },
@@ -1251,7 +869,6 @@ const PROVIDERS = [
1251
869
  path.join(home, ".hermes", "auth.json"),
1252
870
  ],
1253
871
  changes: hermesConfig,
1254
- cleanup: cleanupHermesConfig,
1255
872
  modelSelectionKind: "single-model",
1256
873
  protocolPreference: "chat_completions",
1257
874
  },
@@ -1367,15 +984,17 @@ export function applyProviderInstall(options, store) {
1367
984
  byProvider.set(change.providerId, [...(byProvider.get(change.providerId) || []), change]);
1368
985
  }
1369
986
  for (const [providerId, providerChanges] of byProvider) {
1370
- const snapshot = {
1371
- providerId,
1372
- files: providerChanges.map((change) => ({
1373
- path: change.path,
1374
- existed: fs.existsSync(change.path),
1375
- content: readText(change.path),
1376
- })),
1377
- };
1378
- store.saveProviderInstallSnapshot(snapshot);
987
+ if (!store.getProviderInstallSnapshot(providerId)) {
988
+ const snapshot = {
989
+ providerId,
990
+ files: providerChanges.map((change) => ({
991
+ path: change.path,
992
+ existed: fs.existsSync(change.path),
993
+ content: readText(change.path),
994
+ })),
995
+ };
996
+ store.saveProviderInstallSnapshot(snapshot);
997
+ }
1379
998
  }
1380
999
  for (const providerId of providerIds) {
1381
1000
  const provider = getProviderDefinition(providerId);
@@ -1398,31 +1017,21 @@ export function applyProviderInstall(options, store) {
1398
1017
  return applied;
1399
1018
  }
1400
1019
  /**
1401
- * 回滚 provider 安装。
1402
- * 从 `store` 读取安装前的快照,恢复原文件(如果快照里有原始内容)。
1403
- * 恢复后仍会执行 provider cleanup,确保 repeated apply 或旧 TokenBuddy
1404
- * 配置升级后的 disconnect 语义是移除 TokenBuddy,而不是恢复旧 TokenBuddy。
1405
- * 没有快照的 provider 标记为 `missing_snapshot`。
1020
+ * 断开 provider 安装。
1021
+ * 从 `store` 读取首次 connect 前的快照,精确恢复原文件或删除当次创建的文件。
1022
+ * 没有快照时不猜测、不清理第三方配置,只返回 `missing_snapshot`。
1406
1023
  *
1407
1024
  * @param options 回滚选项
1408
1025
  * @param store buyer store
1409
1026
  * @returns 回滚结果列表
1410
1027
  */
1411
1028
  export function rollbackProviderInstall(options, store) {
1412
- const home = resolveHome(options.home);
1413
1029
  const providerIds = assertProviderIds(options.providers);
1414
1030
  const results = [];
1415
1031
  for (const providerId of providerIds) {
1416
- const provider = getProviderDefinition(providerId);
1417
1032
  const snapshot = store.getProviderInstallSnapshot(providerId);
1418
1033
  if (!snapshot) {
1419
- const cleanupResults = provider.cleanup?.(home) ?? [];
1420
- if (cleanupResults.length > 0) {
1421
- results.push(...cleanupResults);
1422
- }
1423
- else {
1424
- results.push({ providerId, path: "", action: "missing_snapshot" });
1425
- }
1034
+ results.push({ providerId, path: "", action: "missing_snapshot" });
1426
1035
  store.removeProviderRuntimeConfig(providerId);
1427
1036
  continue;
1428
1037
  }
@@ -1440,7 +1049,6 @@ export function rollbackProviderInstall(options, store) {
1440
1049
  results.push({ providerId, path: file.path, action: "removed" });
1441
1050
  }
1442
1051
  }
1443
- results.push(...(provider.cleanup?.(home) ?? []));
1444
1052
  store.removeProviderInstallSnapshot(providerId);
1445
1053
  store.removeProviderRuntimeConfig(providerId);
1446
1054
  }