aicodeswitch 6.0.0 → 6.0.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.
@@ -183,6 +183,30 @@ function applyWriteLocalRecords(proxyServer) {
183
183
  auth.OPENAI_API_KEY = key.apiKey;
184
184
  (0, config_merge_1.atomicWriteFile)(authPath, JSON.stringify(auth, null, 2));
185
185
  }
186
+ else if (target === 'opencode') {
187
+ // 将真实 Key 写入 opencode.json 的 provider.aicodeswitch.options.apiKey
188
+ const opencodeConfigPath = (0, config_metadata_1.getOpencodeConfigPath)();
189
+ const opencodeDir = path_1.default.dirname(opencodeConfigPath);
190
+ if (!fs_1.default.existsSync(opencodeDir)) {
191
+ fs_1.default.mkdirSync(opencodeDir, { recursive: true });
192
+ }
193
+ let oc = {};
194
+ if (fs_1.default.existsSync(opencodeConfigPath)) {
195
+ try {
196
+ oc = JSON.parse(fs_1.default.readFileSync(opencodeConfigPath, 'utf-8'));
197
+ }
198
+ catch ( /* ignore */_c) { /* ignore */ }
199
+ }
200
+ if (!oc.provider)
201
+ oc.provider = {};
202
+ if (!oc.provider.aicodeswitch || typeof oc.provider.aicodeswitch !== 'object') {
203
+ oc.provider.aicodeswitch = { npm: '@ai-sdk/openai-compatible', name: 'AICodeSwitch', options: {} };
204
+ }
205
+ if (!oc.provider.aicodeswitch.options)
206
+ oc.provider.aicodeswitch.options = {};
207
+ oc.provider.aicodeswitch.options.apiKey = key.apiKey;
208
+ (0, config_merge_1.atomicWriteFile)(opencodeConfigPath, JSON.stringify(oc, null, 2));
209
+ }
186
210
  }
187
211
  catch (error) {
188
212
  console.error(`[WriteLocal] Failed to apply key ${record.accessKeyId} to ${target}:`, error);
@@ -662,6 +686,142 @@ const restoreCodexConfig = () => __awaiter(void 0, void 0, void 0, function* ()
662
686
  return false;
663
687
  }
664
688
  });
689
+ /**
690
+ * 默认 OpenCode 模型(当用户未配置 opencodeDefaultModel 时使用)
691
+ */
692
+ const DEFAULT_OPENCODE_MODEL = 'claude-sonnet-4-20250514';
693
+ /**
694
+ * 写入 OpenCode 配置(~/.config/opencode/opencode.json)
695
+ *
696
+ * 注入一个自定义 provider `aicodeswitch`,经 @ai-sdk/openai-compatible 指向本代理
697
+ * 的 /opencode/v1 端点(OpenAI Chat Completions 格式)。仅托管 provider.aicodeswitch
698
+ * 段与 model/small_model/mcp 字段,其余用户配置(其它 provider、agent、command 等)保留。
699
+ */
700
+ const writeOpencodeConfig = (_dbManager_1, defaultModel_1, ...args_1) => __awaiter(void 0, [_dbManager_1, defaultModel_1, ...args_1], void 0, function* (_dbManager, defaultModel, options = {}) {
701
+ var _a;
702
+ try {
703
+ const port = process.env.PORT ? parseInt(process.env.PORT, 10) : 4567;
704
+ const configPath = (0, config_metadata_1.getOpencodeConfigPath)();
705
+ const configBakPath = `${configPath}.aicodeswitch_backup`;
706
+ const configDir = path_1.default.dirname(configPath);
707
+ const configStatus = (0, config_metadata_1.checkOpencodeConfigStatus)();
708
+ const isRuntimeRefresh = options.allowOverwriteRefresh === true && configStatus.isOverwritten;
709
+ if (configStatus.isOverwritten && !isRuntimeRefresh) {
710
+ console.error('OpenCode config has already been overwritten. Please restore the original config first.');
711
+ return false;
712
+ }
713
+ let originalHash = isRuntimeRefresh
714
+ ? (_a = configStatus.metadata) === null || _a === void 0 ? void 0 : _a.originalHash
715
+ : undefined;
716
+ if (!isRuntimeRefresh) {
717
+ if (!fs_1.default.existsSync(configBakPath)) {
718
+ if (fs_1.default.existsSync(configPath)) {
719
+ originalHash = (0, crypto_1.createHash)('sha256').update(fs_1.default.readFileSync(configPath, 'utf-8')).digest('hex');
720
+ fs_1.default.renameSync(configPath, configBakPath);
721
+ }
722
+ }
723
+ else {
724
+ console.log('OpenCode backup file already exists, skipping backup step');
725
+ }
726
+ }
727
+ if (!fs_1.default.existsSync(configDir)) {
728
+ fs_1.default.mkdirSync(configDir, { recursive: true });
729
+ }
730
+ // 读取当前配置(保留用户其它 provider/agent/command 等)
731
+ let currentConfig = {};
732
+ if (fs_1.default.existsSync(configPath)) {
733
+ try {
734
+ currentConfig = JSON.parse(fs_1.default.readFileSync(configPath, 'utf-8'));
735
+ }
736
+ catch (error) {
737
+ console.warn('Failed to parse current opencode.json, using empty object:', error);
738
+ }
739
+ }
740
+ const model = (defaultModel && typeof defaultModel === 'string' && defaultModel.trim())
741
+ ? defaultModel.trim()
742
+ : DEFAULT_OPENCODE_MODEL;
743
+ // 构建代理配置
744
+ const proxyConfig = {
745
+ provider: {
746
+ aicodeswitch: {
747
+ npm: '@ai-sdk/openai-compatible',
748
+ name: 'AICodeSwitch',
749
+ options: {
750
+ baseURL: `http://${clientHost}:${port}/opencode/v1`,
751
+ apiKey: 'api_key'
752
+ },
753
+ models: {
754
+ [model]: { name: model }
755
+ }
756
+ }
757
+ },
758
+ model: `aicodeswitch/${model}`
759
+ };
760
+ const mergedConfig = (0, config_merge_1.mergeJsonConfig)(proxyConfig, currentConfig, config_managed_fields_1.OPENCODE_CONFIG_MANAGED_FIELDS);
761
+ (0, config_merge_1.atomicWriteFile)(configPath, JSON.stringify(mergedConfig, null, 2));
762
+ // 保存元数据
763
+ const currentHash = (0, crypto_1.createHash)('sha256').update(fs_1.default.readFileSync(configPath, 'utf-8')).digest('hex');
764
+ const metadata = {
765
+ configType: 'opencode',
766
+ timestamp: Date.now(),
767
+ originalHash,
768
+ proxyMarker: `http://${host}:${port}/opencode`,
769
+ files: [
770
+ {
771
+ originalPath: configPath,
772
+ backupPath: configBakPath,
773
+ currentHash
774
+ }
775
+ ]
776
+ };
777
+ (0, config_metadata_1.saveMetadata)(metadata);
778
+ return true;
779
+ }
780
+ catch (error) {
781
+ console.error('Failed to write OpenCode config file:', error);
782
+ return false;
783
+ }
784
+ });
785
+ const restoreOpencodeConfig = () => __awaiter(void 0, void 0, void 0, function* () {
786
+ try {
787
+ const configPath = (0, config_metadata_1.getOpencodeConfigPath)();
788
+ const configBakPath = `${configPath}.aicodeswitch_backup`;
789
+ let restoredAnyFile = false;
790
+ if (fs_1.default.existsSync(configBakPath)) {
791
+ const backupConfig = JSON.parse(fs_1.default.readFileSync(configBakPath, 'utf-8'));
792
+ let currentConfig = {};
793
+ if (fs_1.default.existsSync(configPath)) {
794
+ try {
795
+ currentConfig = JSON.parse(fs_1.default.readFileSync(configPath, 'utf-8'));
796
+ }
797
+ catch (error) {
798
+ console.warn('Failed to parse current opencode.json during restore, using empty object:', error);
799
+ }
800
+ }
801
+ // 备份作为基础,合并当前的非管理字段
802
+ const mergedConfig = (0, config_merge_1.mergeJsonConfig)(backupConfig, currentConfig, config_managed_fields_1.OPENCODE_CONFIG_MANAGED_FIELDS);
803
+ (0, config_merge_1.atomicWriteFile)(configPath, JSON.stringify(mergedConfig, null, 2));
804
+ fs_1.default.unlinkSync(configBakPath);
805
+ restoredAnyFile = true;
806
+ }
807
+ (0, config_metadata_1.deleteMetadata)('opencode');
808
+ return restoredAnyFile;
809
+ }
810
+ catch (error) {
811
+ console.error('Failed to restore OpenCode config file:', error);
812
+ return false;
813
+ }
814
+ });
815
+ const checkOpencodeBackupExists = () => {
816
+ try {
817
+ const status = (0, config_metadata_1.checkOpencodeConfigStatus)();
818
+ return status.hasBackup;
819
+ }
820
+ catch (error) {
821
+ console.error('Failed to check OpenCode backup:', error);
822
+ return false;
823
+ }
824
+ };
665
825
  const checkClaudeBackupExists = () => {
666
826
  try {
667
827
  // 清理可能的无效元数据
@@ -703,6 +863,8 @@ const syncConfigsOnServerStartup = (dbManager) => __awaiter(void 0, void 0, void
703
863
  : DEFAULT_CODEX_REASONING_EFFORT;
704
864
  const codexWritten = yield writeCodexConfig(dbManager, modelReasoningEffort, config.codexDefaultModel, config.codexEnableMemories);
705
865
  console.log(`[Startup Config Sync] Codex config ${codexWritten ? 'written' : 'skipped'}`);
866
+ const opencodeWritten = yield writeOpencodeConfig(dbManager, config.opencodeDefaultModel);
867
+ console.log(`[Startup Config Sync] OpenCode config ${opencodeWritten ? 'written' : 'skipped'}`);
706
868
  });
707
869
  const syncConfigsOnGlobalConfigUpdate = (dbManager) => __awaiter(void 0, void 0, void 0, function* () {
708
870
  const config = dbManager.getConfig();
@@ -716,6 +878,8 @@ const syncConfigsOnGlobalConfigUpdate = (dbManager) => __awaiter(void 0, void 0,
716
878
  : DEFAULT_CODEX_REASONING_EFFORT;
717
879
  const codexUpdated = yield writeCodexConfig(dbManager, modelReasoningEffort, config.codexDefaultModel, config.codexEnableMemories, { allowOverwriteRefresh: true });
718
880
  console.log(`[Config Update Sync] Codex config ${codexUpdated ? 'written' : 'skipped'}`);
881
+ const opencodeUpdated = yield writeOpencodeConfig(dbManager, config.opencodeDefaultModel, { allowOverwriteRefresh: true });
882
+ console.log(`[Config Update Sync] OpenCode config ${opencodeUpdated ? 'written' : 'skipped'}`);
719
883
  });
720
884
  const getCentralSkillsDir = () => {
721
885
  return path_1.default.join(os_1.default.homedir(), '.aicodeswitch', 'skills');
@@ -734,12 +898,20 @@ function getSkillDirByName(name) {
734
898
  return path_1.default.join(centralDir, sanitizedName);
735
899
  }
736
900
  const getSkillSymlinkPath = (skillId, targetType) => {
901
+ if (targetType === 'opencode') {
902
+ // OpenCode 没有 skills 目录,映射为全局 command:~/.config/opencode/commands/<skillId>.md
903
+ return path_1.default.join(os_1.default.homedir(), '.config', 'opencode', 'commands', `${skillId}.md`);
904
+ }
737
905
  const baseDir = targetType === 'claude-code' ? '.claude' : '.codex';
738
906
  return path_1.default.join(os_1.default.homedir(), baseDir, 'skills', skillId);
739
907
  };
740
908
  function isSkillSymlinkExists(skillId, targetType) {
741
909
  const symlinkPath = getSkillSymlinkPath(skillId, targetType);
742
910
  try {
911
+ if (targetType === 'opencode') {
912
+ // OpenCode 是普通文件,不是 symlink
913
+ return fs_1.default.existsSync(symlinkPath);
914
+ }
743
915
  const stats = fs_1.default.lstatSync(symlinkPath);
744
916
  return stats.isSymbolicLink();
745
917
  }
@@ -747,15 +919,77 @@ function isSkillSymlinkExists(skillId, targetType) {
747
919
  return false;
748
920
  }
749
921
  }
922
+ /**
923
+ * 从 SKILL.md 内容中剥离前导 YAML frontmatter,返回 { description?, body }
924
+ * 没有 frontmatter 时,description 为 undefined,body 为原文。
925
+ */
926
+ function parseSkillMd(skillMdContent) {
927
+ const trimmed = skillMdContent.replace(/^/, '');
928
+ const fmMatch = trimmed.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
929
+ if (!fmMatch) {
930
+ return { body: trimmed };
931
+ }
932
+ const frontmatter = fmMatch[1];
933
+ const body = fmMatch[2];
934
+ const descMatch = frontmatter.match(/^description:\s*(.+)$/m);
935
+ return {
936
+ description: descMatch ? descMatch[1].replace(/^["']|["']$/g, '').trim() : undefined,
937
+ body: body.trim(),
938
+ };
939
+ }
940
+ /**
941
+ * 为 OpenCode 生成 command markdown 文件内容。
942
+ * frontmatter 用 description(OpenCode 在 TUI 展示),正文用 SKILL.md 的 body。
943
+ */
944
+ function buildOpencodeSkillCommandMarkdown(skillDir, skillId, fallbackDescription) {
945
+ let description = fallbackDescription || '';
946
+ let body = '';
947
+ const skillMdPath = path_1.default.join(skillDir, 'SKILL.md');
948
+ if (fs_1.default.existsSync(skillMdPath)) {
949
+ try {
950
+ const parsed = parseSkillMd(fs_1.default.readFileSync(skillMdPath, 'utf-8'));
951
+ body = parsed.body || '';
952
+ if (!description && parsed.description) {
953
+ description = parsed.description;
954
+ }
955
+ }
956
+ catch ( /* ignore */_a) { /* ignore */ }
957
+ }
958
+ if (!description) {
959
+ description = skillId;
960
+ }
961
+ const bodySection = body ? `\n${body}\n` : '\n';
962
+ return `---\ndescription: ${description.replace(/\n/g, ' ')}\nagent: build\n---\n${bodySection}`;
963
+ }
750
964
  function createSkillSymlink(skillId, targetType) {
751
965
  return __awaiter(this, void 0, void 0, function* () {
966
+ var _a;
752
967
  try {
753
968
  const centralDir = getCentralSkillsDir();
754
969
  const skillDir = path_1.default.join(centralDir, skillId);
755
- const symlinkPath = getSkillSymlinkPath(skillId, targetType);
756
970
  if (!fs_1.default.existsSync(skillDir)) {
757
971
  return { success: false, error: 'Skill目录不存在' };
758
972
  }
973
+ // OpenCode:生成 command markdown 写入 ~/.config/opencode/commands/<skillId>.md
974
+ if (targetType === 'opencode') {
975
+ const commandPath = getSkillSymlinkPath(skillId, targetType);
976
+ const commandDir = path_1.default.dirname(commandPath);
977
+ if (!fs_1.default.existsSync(commandDir)) {
978
+ fs_1.default.mkdirSync(commandDir, { recursive: true });
979
+ }
980
+ let fallbackDescription;
981
+ const metadataPath = path_1.default.join(skillDir, 'skill.json');
982
+ if (fs_1.default.existsSync(metadataPath)) {
983
+ try {
984
+ fallbackDescription = (_a = JSON.parse(fs_1.default.readFileSync(metadataPath, 'utf-8'))) === null || _a === void 0 ? void 0 : _a.description;
985
+ }
986
+ catch ( /* ignore */_b) { /* ignore */ }
987
+ }
988
+ const content = buildOpencodeSkillCommandMarkdown(skillDir, skillId, fallbackDescription);
989
+ (0, config_merge_1.atomicWriteFile)(commandPath, content);
990
+ return { success: true };
991
+ }
992
+ const symlinkPath = getSkillSymlinkPath(skillId, targetType);
759
993
  const targetBaseDir = path_1.default.dirname(symlinkPath);
760
994
  if (!fs_1.default.existsSync(targetBaseDir)) {
761
995
  fs_1.default.mkdirSync(targetBaseDir, { recursive: true });
@@ -786,6 +1020,13 @@ function removeSkillSymlink(skillId, targetType) {
786
1020
  return __awaiter(this, void 0, void 0, function* () {
787
1021
  try {
788
1022
  const symlinkPath = getSkillSymlinkPath(skillId, targetType);
1023
+ if (targetType === 'opencode') {
1024
+ // OpenCode:删除 command 文件
1025
+ if (fs_1.default.existsSync(symlinkPath)) {
1026
+ fs_1.default.unlinkSync(symlinkPath);
1027
+ }
1028
+ return { success: true };
1029
+ }
789
1030
  if (!fs_1.default.existsSync(symlinkPath)) {
790
1031
  return { success: true };
791
1032
  }
@@ -1044,7 +1285,7 @@ const listInstalledSkills = () => {
1044
1285
  try {
1045
1286
  const metadata = JSON.parse(fs_1.default.readFileSync(metadataPath, 'utf-8'));
1046
1287
  const enabledTargets = [];
1047
- ['claude-code', 'codex'].forEach((targetType) => {
1288
+ ['claude-code', 'codex', 'opencode'].forEach((targetType) => {
1048
1289
  if (isSkillSymlinkExists(skillId, targetType)) {
1049
1290
  enabledTargets.push(targetType);
1050
1291
  }
@@ -1196,7 +1437,7 @@ const registerRoutes = (dbManager, proxyServer) => __awaiter(void 0, void 0, voi
1196
1437
  if (!tool || !routeId) {
1197
1438
  return res.status(400).json({ error: 'tool and routeId are required' });
1198
1439
  }
1199
- if (tool !== 'claude-code' && tool !== 'codex') {
1440
+ if (tool !== 'claude-code' && tool !== 'codex' && tool !== 'opencode') {
1200
1441
  return res.status(400).json({ error: 'Invalid tool name' });
1201
1442
  }
1202
1443
  const route = dbManager.getRoute(routeId);
@@ -1217,7 +1458,7 @@ const registerRoutes = (dbManager, proxyServer) => __awaiter(void 0, void 0, voi
1217
1458
  })));
1218
1459
  app.post('/api/tool-bindings/deactivate', asyncHandler((req, res) => __awaiter(void 0, void 0, void 0, function* () {
1219
1460
  const { tool } = req.body;
1220
- if (!tool || (tool !== 'claude-code' && tool !== 'codex')) {
1461
+ if (!tool || (tool !== 'claude-code' && tool !== 'codex' && tool !== 'opencode')) {
1221
1462
  return res.status(400).json({ error: 'Invalid tool name' });
1222
1463
  }
1223
1464
  const result = yield dbManager.deactivateToolRoute(tool);
@@ -2005,7 +2246,7 @@ ${instruction}
2005
2246
  app.post('/api/skills/:skillId/enable', asyncHandler((req, res) => __awaiter(void 0, void 0, void 0, function* () {
2006
2247
  const { skillId } = req.params;
2007
2248
  const { targetType } = req.body;
2008
- if (!targetType || (targetType !== 'claude-code' && targetType !== 'codex')) {
2249
+ if (!targetType || (targetType !== 'claude-code' && targetType !== 'codex' && targetType !== 'opencode')) {
2009
2250
  res.status(400).json({ success: false, error: '无效的目标类型' });
2010
2251
  return;
2011
2252
  }
@@ -2036,7 +2277,7 @@ ${instruction}
2036
2277
  app.post('/api/skills/:skillId/disable', asyncHandler((req, res) => __awaiter(void 0, void 0, void 0, function* () {
2037
2278
  const { skillId } = req.params;
2038
2279
  const { targetType } = req.body;
2039
- if (!targetType || (targetType !== 'claude-code' && targetType !== 'codex')) {
2280
+ if (!targetType || (targetType !== 'claude-code' && targetType !== 'codex' && targetType !== 'opencode')) {
2040
2281
  res.status(400).json({ success: false, error: '无效的目标类型' });
2041
2282
  return;
2042
2283
  }
@@ -2065,7 +2306,7 @@ ${instruction}
2065
2306
  res.status(404).json({ success: false, error: 'Skill不存在' });
2066
2307
  return;
2067
2308
  }
2068
- ['claude-code', 'codex'].forEach((targetType) => __awaiter(void 0, void 0, void 0, function* () {
2309
+ ['claude-code', 'codex', 'opencode'].forEach((targetType) => __awaiter(void 0, void 0, void 0, function* () {
2069
2310
  yield removeSkillSymlink(skillId, targetType);
2070
2311
  }));
2071
2312
  fs_1.default.rmSync(skillDir, { recursive: true, force: true });
@@ -2107,6 +2348,15 @@ ${instruction}
2107
2348
  applyWriteLocalRecords(proxyServer);
2108
2349
  res.json(result);
2109
2350
  })));
2351
+ app.post('/api/write-config/opencode', asyncHandler((req, res) => __awaiter(void 0, void 0, void 0, function* () {
2352
+ var _a;
2353
+ const appConfig = dbManager.getConfig();
2354
+ const requestedModel = typeof ((_a = req.body) === null || _a === void 0 ? void 0 : _a.defaultModel) === 'string' ? req.body.defaultModel : undefined;
2355
+ const defaultModel = requestedModel || appConfig.opencodeDefaultModel;
2356
+ const result = yield writeOpencodeConfig(dbManager, defaultModel);
2357
+ applyWriteLocalRecords(proxyServer);
2358
+ res.json(result);
2359
+ })));
2110
2360
  // 兼容接口:更新全局 Agent Teams 配置
2111
2361
  app.post('/api/update-claude-agent-teams', asyncHandler((req, res) => __awaiter(void 0, void 0, void 0, function* () {
2112
2362
  const { enableAgentTeams } = req.body;
@@ -2160,12 +2410,19 @@ ${instruction}
2160
2410
  const result = yield restoreCodexConfig();
2161
2411
  res.json(result);
2162
2412
  })));
2413
+ app.post('/api/restore-config/opencode', asyncHandler((_req, res) => __awaiter(void 0, void 0, void 0, function* () {
2414
+ const result = yield restoreOpencodeConfig();
2415
+ res.json(result);
2416
+ })));
2163
2417
  app.get('/api/check-backup/claude', (_req, res) => {
2164
2418
  res.json({ exists: checkClaudeBackupExists() });
2165
2419
  });
2166
2420
  app.get('/api/check-backup/codex', (_req, res) => {
2167
2421
  res.json({ exists: checkCodexBackupExists() });
2168
2422
  });
2423
+ app.get('/api/check-backup/opencode', (_req, res) => {
2424
+ res.json({ exists: checkOpencodeBackupExists() });
2425
+ });
2169
2426
  // 新的详细配置状态 API 端点
2170
2427
  app.get('/api/config-status/claude', (_req, res) => {
2171
2428
  const status = (0, config_metadata_1.checkClaudeConfigStatus)();
@@ -2175,6 +2432,10 @@ ${instruction}
2175
2432
  const status = (0, config_metadata_1.checkCodexConfigStatus)();
2176
2433
  res.json(status);
2177
2434
  });
2435
+ app.get('/api/config-status/opencode', (_req, res) => {
2436
+ const status = (0, config_metadata_1.checkOpencodeConfigStatus)();
2437
+ res.json(status);
2438
+ });
2178
2439
  // API 路径路由映射
2179
2440
  app.get('/api/api-path-bindings', (_req, res) => {
2180
2441
  res.json({ bindings: dbManager.getApiPathBindings(), models: dbManager.getApiPathModels() });
@@ -2939,6 +3200,31 @@ ${instruction}
2939
3200
  (0, config_merge_1.atomicWriteFile)(authPath, JSON.stringify(auth, null, 2));
2940
3201
  results['codex'] = true;
2941
3202
  }
3203
+ else if (target === 'opencode') {
3204
+ // 写入 ~/.config/opencode/opencode.json 的 provider.aicodeswitch.options.apiKey
3205
+ const opencodeConfigPath = (0, config_metadata_1.getOpencodeConfigPath)();
3206
+ const opencodeDir = path_1.default.dirname(opencodeConfigPath);
3207
+ if (!fs_1.default.existsSync(opencodeDir)) {
3208
+ fs_1.default.mkdirSync(opencodeDir, { recursive: true });
3209
+ }
3210
+ let oc = {};
3211
+ if (fs_1.default.existsSync(opencodeConfigPath)) {
3212
+ try {
3213
+ oc = JSON.parse(fs_1.default.readFileSync(opencodeConfigPath, 'utf-8'));
3214
+ }
3215
+ catch ( /* ignore */_c) { /* ignore */ }
3216
+ }
3217
+ if (!oc.provider)
3218
+ oc.provider = {};
3219
+ if (!oc.provider.aicodeswitch || typeof oc.provider.aicodeswitch !== 'object') {
3220
+ oc.provider.aicodeswitch = { npm: '@ai-sdk/openai-compatible', name: 'AICodeSwitch', options: {} };
3221
+ }
3222
+ if (!oc.provider.aicodeswitch.options)
3223
+ oc.provider.aicodeswitch.options = {};
3224
+ oc.provider.aicodeswitch.options.apiKey = key.apiKey;
3225
+ (0, config_merge_1.atomicWriteFile)(opencodeConfigPath, JSON.stringify(oc, null, 2));
3226
+ results['opencode'] = true;
3227
+ }
2942
3228
  }
2943
3229
  catch (error) {
2944
3230
  console.error(`Failed to write local config for ${target}:`, error);
@@ -3319,6 +3605,76 @@ ${instruction}
3319
3605
  console.log(`[MCP] Codex MCP config written: ${writtenMcpIds.length} server(s)`);
3320
3606
  return true;
3321
3607
  }
3608
+ else if (targetType === 'opencode') {
3609
+ // OpenCode 使用 JSON 格式的 opencode.json,MCP 配置位于 mcp 段
3610
+ // 格式:local → { type:"local", command:[...], enabled:true, env? }
3611
+ // remote → { type:"remote", url, enabled:true, headers? }
3612
+ const opencodeConfigPath = (0, config_metadata_1.getOpencodeConfigPath)();
3613
+ const opencodeDir = path_1.default.dirname(opencodeConfigPath);
3614
+ if (!fs_1.default.existsSync(opencodeDir)) {
3615
+ fs_1.default.mkdirSync(opencodeDir, { recursive: true });
3616
+ }
3617
+ let currentConfig = {};
3618
+ if (fs_1.default.existsSync(opencodeConfigPath)) {
3619
+ try {
3620
+ currentConfig = JSON.parse(fs_1.default.readFileSync(opencodeConfigPath, 'utf-8'));
3621
+ }
3622
+ catch (error) {
3623
+ console.warn('[MCP] Failed to parse opencode.json:', error);
3624
+ }
3625
+ }
3626
+ // 清除代理上次写入的 mcp 条目(通过 metadata 追踪,避免误删用户自配的 mcp)
3627
+ const mcpMetaPath = path_1.default.join(opencodeDir, '.aicodeswitch_mcp_servers.json');
3628
+ let previousMcpIds = [];
3629
+ if (fs_1.default.existsSync(mcpMetaPath)) {
3630
+ try {
3631
+ previousMcpIds = JSON.parse(fs_1.default.readFileSync(mcpMetaPath, 'utf8'));
3632
+ for (const id of previousMcpIds) {
3633
+ if (currentConfig.mcp && currentConfig.mcp[id]) {
3634
+ delete currentConfig.mcp[id];
3635
+ }
3636
+ }
3637
+ }
3638
+ catch (_b) {
3639
+ // ignore
3640
+ }
3641
+ }
3642
+ if (!currentConfig.mcp) {
3643
+ currentConfig.mcp = {};
3644
+ }
3645
+ const writtenMcpIds = [];
3646
+ for (const mcp of mcps) {
3647
+ const mcpConfig = { enabled: true };
3648
+ if (mcp.type === 'stdio') {
3649
+ mcpConfig.type = 'local';
3650
+ const cmdParts = [mcp.command || ''];
3651
+ if (Array.isArray(mcp.args)) {
3652
+ cmdParts.push(...mcp.args);
3653
+ }
3654
+ mcpConfig.command = cmdParts.filter((c, i) => i === 0 ? c !== '' : true);
3655
+ if (mcp.env && Object.keys(mcp.env).length > 0) {
3656
+ mcpConfig.environment = Object.assign({}, mcp.env);
3657
+ }
3658
+ }
3659
+ else {
3660
+ // http / sse 均使用 remote 类型
3661
+ mcpConfig.type = 'remote';
3662
+ mcpConfig.url = mcp.url || '';
3663
+ if (mcp.headers && Object.keys(mcp.headers).length > 0) {
3664
+ mcpConfig.headers = Object.assign({}, mcp.headers);
3665
+ }
3666
+ }
3667
+ currentConfig.mcp[mcp.id] = mcpConfig;
3668
+ writtenMcpIds.push(mcp.id);
3669
+ }
3670
+ if (Object.keys(currentConfig.mcp).length === 0) {
3671
+ delete currentConfig.mcp;
3672
+ }
3673
+ (0, config_merge_1.atomicWriteFile)(opencodeConfigPath, JSON.stringify(currentConfig, null, 2));
3674
+ fs_1.default.writeFileSync(mcpMetaPath, JSON.stringify(writtenMcpIds, null, 2));
3675
+ console.log(`[MCP] OpenCode MCP config written: ${writtenMcpIds.length} server(s)`);
3676
+ return true;
3677
+ }
3322
3678
  return false;
3323
3679
  }
3324
3680
  catch (error) {
@@ -3381,6 +3737,41 @@ ${instruction}
3381
3737
  }
3382
3738
  return true;
3383
3739
  }
3740
+ else if (targetType === 'opencode') {
3741
+ const opencodeConfigPath = (0, config_metadata_1.getOpencodeConfigPath)();
3742
+ if (!fs_1.default.existsSync(opencodeConfigPath)) {
3743
+ return true;
3744
+ }
3745
+ let currentConfig = {};
3746
+ try {
3747
+ currentConfig = JSON.parse(fs_1.default.readFileSync(opencodeConfigPath, 'utf-8'));
3748
+ }
3749
+ catch (error) {
3750
+ console.warn('[MCP] Failed to parse opencode.json for removal:', error);
3751
+ return false;
3752
+ }
3753
+ if (currentConfig.mcp && currentConfig.mcp[mcpId]) {
3754
+ delete currentConfig.mcp[mcpId];
3755
+ if (Object.keys(currentConfig.mcp).length === 0) {
3756
+ delete currentConfig.mcp;
3757
+ }
3758
+ (0, config_merge_1.atomicWriteFile)(opencodeConfigPath, JSON.stringify(currentConfig, null, 2));
3759
+ const opencodeDir = path_1.default.dirname(opencodeConfigPath);
3760
+ const mcpMetaPath = path_1.default.join(opencodeDir, '.aicodeswitch_mcp_servers.json');
3761
+ if (fs_1.default.existsSync(mcpMetaPath)) {
3762
+ try {
3763
+ const previousIds = JSON.parse(fs_1.default.readFileSync(mcpMetaPath, 'utf8'));
3764
+ const updatedIds = previousIds.filter(id => id !== mcpId);
3765
+ fs_1.default.writeFileSync(mcpMetaPath, JSON.stringify(updatedIds, null, 2));
3766
+ }
3767
+ catch (_b) {
3768
+ // ignore
3769
+ }
3770
+ }
3771
+ console.log(`[MCP] Removed MCP ${mcpId} from OpenCode config`);
3772
+ }
3773
+ return true;
3774
+ }
3384
3775
  return false;
3385
3776
  }
3386
3777
  catch (error) {
@@ -3590,6 +3981,13 @@ const start = () => __awaiter(void 0, void 0, void 0, function* () {
3590
3981
  catch (error) {
3591
3982
  console.error('[Shutdown ...] Failed to restore Codex config:', error);
3592
3983
  }
3984
+ try {
3985
+ const opencodeRestored = yield restoreOpencodeConfig();
3986
+ console.log(`[Shutdown ...] OpenCode config ${opencodeRestored ? 'restored' : 'was not modified'}`);
3987
+ }
3988
+ catch (error) {
3989
+ console.error('[Shutdown ...] Failed to restore OpenCode config:', error);
3990
+ }
3593
3991
  // Shutdown AccessKey module
3594
3992
  try {
3595
3993
  yield accessKeyModule.shutdown();
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.readOriginalConfig = exports.readCodexOriginalConfig = exports.readClaudeOriginalConfig = void 0;
6
+ exports.readOriginalConfig = exports.readOpencodeOriginalConfig = exports.readCodexOriginalConfig = exports.readClaudeOriginalConfig = void 0;
7
7
  const fs_1 = __importDefault(require("fs"));
8
8
  const path_1 = __importDefault(require("path"));
9
9
  const os_1 = __importDefault(require("os"));
@@ -232,6 +232,73 @@ const readCodexOriginalConfig = () => {
232
232
  }
233
233
  };
234
234
  exports.readCodexOriginalConfig = readCodexOriginalConfig;
235
+ /**
236
+ * 读取 OpenCode 原始配置
237
+ * fallback 场景下仅从备份文件(~/.config/opencode/opencode.json.aicodeswitch_backup)读取
238
+ *
239
+ * OpenCode 的模型形如 "<providerId>/<modelId>",真实上游由用户原始 opencode.json 中的
240
+ * provider 段(options.baseURL + options.apiKey)决定。此处解析备份中的真实 provider。
241
+ */
242
+ const readOpencodeOriginalConfig = () => {
243
+ var _a, _b;
244
+ try {
245
+ const homeDir = os_1.default.homedir();
246
+ const configBakPath = path_1.default.join(homeDir, '.config', 'opencode', 'opencode.json.aicodeswitch_backup');
247
+ if (!fs_1.default.existsSync(configBakPath)) {
248
+ console.log('No OpenCode backup config file found');
249
+ return null;
250
+ }
251
+ const content = fs_1.default.readFileSync(configBakPath, 'utf-8');
252
+ const config = JSON.parse(content);
253
+ // 解析 model:"providerId/modelId"
254
+ const modelField = typeof config.model === 'string' ? config.model : '';
255
+ const [providerId, modelId] = modelField.split('/');
256
+ const providers = (config.provider && typeof config.provider === 'object') ? config.provider : {};
257
+ // 选定真实 provider:优先 model 中声明的 providerId,否则取第一个非 aicodeswitch 的 provider
258
+ let providerConfig;
259
+ let resolvedProviderId = '';
260
+ if (providerId && providers[providerId]) {
261
+ providerConfig = providers[providerId];
262
+ resolvedProviderId = providerId;
263
+ }
264
+ else {
265
+ for (const [pid, pc] of Object.entries(providers)) {
266
+ if (pid !== 'aicodeswitch' && pc && typeof pc === 'object') {
267
+ providerConfig = pc;
268
+ resolvedProviderId = pid;
269
+ break;
270
+ }
271
+ }
272
+ }
273
+ const baseUrlRaw = ((_a = providerConfig === null || providerConfig === void 0 ? void 0 : providerConfig.options) === null || _a === void 0 ? void 0 : _a.baseURL) || '';
274
+ const baseUrl = normalizeApiUrl(baseUrlRaw);
275
+ // apiKey 可能内联在 options.apiKey,也可能经 {env:XXX}/{file:xxx} 引用(fallback 无法解析,置空)
276
+ let apiKey = '';
277
+ if (typeof ((_b = providerConfig === null || providerConfig === void 0 ? void 0 : providerConfig.options) === null || _b === void 0 ? void 0 : _b.apiKey) === 'string') {
278
+ const raw = providerConfig.options.apiKey.trim();
279
+ if (raw && !raw.startsWith('{env:') && !raw.startsWith('{file:')) {
280
+ apiKey = raw;
281
+ }
282
+ }
283
+ if (!baseUrl) {
284
+ console.log('No baseURL found in OpenCode backup config provider:', resolvedProviderId);
285
+ return null;
286
+ }
287
+ const sourceType = inferSourceTypeFromBaseUrlAndWireApi(baseUrl, undefined);
288
+ return {
289
+ apiUrl: baseUrl,
290
+ apiKey,
291
+ authType: inferAuthTypeFromSource(sourceType),
292
+ sourceType,
293
+ model: modelId || modelField || undefined,
294
+ };
295
+ }
296
+ catch (error) {
297
+ console.error('Failed to read OpenCode original config:', error);
298
+ return null;
299
+ }
300
+ };
301
+ exports.readOpencodeOriginalConfig = readOpencodeOriginalConfig;
235
302
  /**
236
303
  * 根据目标类型读取原始配置
237
304
  */
@@ -242,6 +309,9 @@ const readOriginalConfig = (targetType) => {
242
309
  else if (targetType === 'codex') {
243
310
  return (0, exports.readCodexOriginalConfig)();
244
311
  }
312
+ else if (targetType === 'opencode') {
313
+ return (0, exports.readOpencodeOriginalConfig)();
314
+ }
245
315
  return null;
246
316
  };
247
317
  exports.readOriginalConfig = readOriginalConfig;