aicodeswitch 6.0.0 → 6.0.2

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.
@@ -12,6 +12,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
12
12
  return (mod && mod.__esModule) ? mod : { "default": mod };
13
13
  };
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.start = exports.gracefulShutdown = void 0;
15
16
  const express_1 = __importDefault(require("express"));
16
17
  const cors_1 = __importDefault(require("cors"));
17
18
  const dotenv_1 = __importDefault(require("dotenv"));
@@ -183,6 +184,30 @@ function applyWriteLocalRecords(proxyServer) {
183
184
  auth.OPENAI_API_KEY = key.apiKey;
184
185
  (0, config_merge_1.atomicWriteFile)(authPath, JSON.stringify(auth, null, 2));
185
186
  }
187
+ else if (target === 'opencode') {
188
+ // 将真实 Key 写入 opencode.json 的 provider.aicodeswitch.options.apiKey
189
+ const opencodeConfigPath = (0, config_metadata_1.getOpencodeConfigPath)();
190
+ const opencodeDir = path_1.default.dirname(opencodeConfigPath);
191
+ if (!fs_1.default.existsSync(opencodeDir)) {
192
+ fs_1.default.mkdirSync(opencodeDir, { recursive: true });
193
+ }
194
+ let oc = {};
195
+ if (fs_1.default.existsSync(opencodeConfigPath)) {
196
+ try {
197
+ oc = JSON.parse(fs_1.default.readFileSync(opencodeConfigPath, 'utf-8'));
198
+ }
199
+ catch ( /* ignore */_c) { /* ignore */ }
200
+ }
201
+ if (!oc.provider)
202
+ oc.provider = {};
203
+ if (!oc.provider.aicodeswitch || typeof oc.provider.aicodeswitch !== 'object') {
204
+ oc.provider.aicodeswitch = { npm: '@ai-sdk/openai-compatible', name: 'AICodeSwitch', options: {} };
205
+ }
206
+ if (!oc.provider.aicodeswitch.options)
207
+ oc.provider.aicodeswitch.options = {};
208
+ oc.provider.aicodeswitch.options.apiKey = key.apiKey;
209
+ (0, config_merge_1.atomicWriteFile)(opencodeConfigPath, JSON.stringify(oc, null, 2));
210
+ }
186
211
  }
187
212
  catch (error) {
188
213
  console.error(`[WriteLocal] Failed to apply key ${record.accessKeyId} to ${target}:`, error);
@@ -239,7 +264,7 @@ const isClaudePermissionDefaultMode = (value) => {
239
264
  const isValidAutocompactPct = (v) => {
240
265
  return typeof v === 'number' && Number.isInteger(v) && v >= 1 && v <= 100;
241
266
  };
242
- const writeClaudeConfig = (_dbManager_1, enableAgentTeams_1, enableBypassPermissionsSupport_1, permissionsDefaultMode_1, effortLevel_1, defaultModel_1, autocompactPctOverride_1, ...args_1) => __awaiter(void 0, [_dbManager_1, enableAgentTeams_1, enableBypassPermissionsSupport_1, permissionsDefaultMode_1, effortLevel_1, defaultModel_1, autocompactPctOverride_1, ...args_1], void 0, function* (_dbManager, enableAgentTeams, enableBypassPermissionsSupport, permissionsDefaultMode, effortLevel, defaultModel, autocompactPctOverride, options = {}) {
267
+ const writeClaudeConfig = (_dbManager_1, enableAgentTeams_1, enableBypassPermissionsSupport_1, permissionsDefaultMode_1, effortLevel_1, defaultModel_1, autocompactPctOverride_1, maxRetries_1, ...args_1) => __awaiter(void 0, [_dbManager_1, enableAgentTeams_1, enableBypassPermissionsSupport_1, permissionsDefaultMode_1, effortLevel_1, defaultModel_1, autocompactPctOverride_1, maxRetries_1, ...args_1], void 0, function* (_dbManager, enableAgentTeams, enableBypassPermissionsSupport, permissionsDefaultMode, effortLevel, defaultModel, autocompactPctOverride, maxRetries, options = {}) {
243
268
  var _a;
244
269
  try {
245
270
  const homeDir = os_1.default.homedir();
@@ -294,7 +319,7 @@ const writeClaudeConfig = (_dbManager_1, enableAgentTeams_1, enableBypassPermiss
294
319
  ANTHROPIC_BASE_URL: `http://${clientHost}:${port}/claude-code`,
295
320
  API_TIMEOUT_MS: "3000000",
296
321
  CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: 1,
297
- CLAUDE_CODE_MAX_RETRIES: 3
322
+ CLAUDE_CODE_MAX_RETRIES: maxRetries !== null && maxRetries !== void 0 ? maxRetries : 5
298
323
  };
299
324
  // 如果启用Agent Teams功能,添加对应的环境变量
300
325
  if (enableAgentTeams) {
@@ -392,7 +417,7 @@ const DEFAULT_CODEX_REASONING_EFFORT = 'high';
392
417
  const isCodexReasoningEffort = (value) => {
393
418
  return typeof value === 'string' && VALID_CODEX_REASONING_EFFORTS.includes(value);
394
419
  };
395
- const writeCodexConfig = (_dbManager_1, ...args_1) => __awaiter(void 0, [_dbManager_1, ...args_1], void 0, function* (_dbManager, modelReasoningEffort = DEFAULT_CODEX_REASONING_EFFORT, codexDefaultModel, enableMemories, options = {}) {
420
+ const writeCodexConfig = (_dbManager_1, ...args_1) => __awaiter(void 0, [_dbManager_1, ...args_1], void 0, function* (_dbManager, modelReasoningEffort = DEFAULT_CODEX_REASONING_EFFORT, codexDefaultModel, enableMemories, maxRetries, options = {}) {
396
421
  var _a;
397
422
  try {
398
423
  const homeDir = os_1.default.homedir();
@@ -454,7 +479,7 @@ const writeCodexConfig = (_dbManager_1, ...args_1) => __awaiter(void 0, [_dbMana
454
479
  name: "aicodeswitch",
455
480
  base_url: `http://${clientHost}:${port}/codex`,
456
481
  wire_api: "responses",
457
- stream_max_retries: 3,
482
+ stream_max_retries: maxRetries !== null && maxRetries !== void 0 ? maxRetries : 5,
458
483
  stream_retry_backoff: "fixed"
459
484
  }
460
485
  }
@@ -662,6 +687,143 @@ const restoreCodexConfig = () => __awaiter(void 0, void 0, void 0, function* ()
662
687
  return false;
663
688
  }
664
689
  });
690
+ /**
691
+ * 默认 OpenCode 模型(当用户未配置 opencodeDefaultModel 时使用)
692
+ */
693
+ const DEFAULT_OPENCODE_MODEL = 'claude-sonnet-4-20250514';
694
+ /**
695
+ * 写入 OpenCode 配置(~/.config/opencode/opencode.json)
696
+ *
697
+ * 注入一个自定义 provider `aicodeswitch`,经 @ai-sdk/openai-compatible 指向本代理
698
+ * 的 /opencode/v1 端点(OpenAI Chat Completions 格式)。仅托管 provider.aicodeswitch
699
+ * 段与 model/small_model/mcp 字段,其余用户配置(其它 provider、agent、command 等)保留。
700
+ */
701
+ const writeOpencodeConfig = (_dbManager_1, defaultModel_1, maxRetries_1, ...args_1) => __awaiter(void 0, [_dbManager_1, defaultModel_1, maxRetries_1, ...args_1], void 0, function* (_dbManager, defaultModel, maxRetries, options = {}) {
702
+ var _a;
703
+ try {
704
+ const port = process.env.PORT ? parseInt(process.env.PORT, 10) : 4567;
705
+ const configPath = (0, config_metadata_1.getOpencodeConfigPath)();
706
+ const configBakPath = `${configPath}.aicodeswitch_backup`;
707
+ const configDir = path_1.default.dirname(configPath);
708
+ const configStatus = (0, config_metadata_1.checkOpencodeConfigStatus)();
709
+ const isRuntimeRefresh = options.allowOverwriteRefresh === true && configStatus.isOverwritten;
710
+ if (configStatus.isOverwritten && !isRuntimeRefresh) {
711
+ console.error('OpenCode config has already been overwritten. Please restore the original config first.');
712
+ return false;
713
+ }
714
+ let originalHash = isRuntimeRefresh
715
+ ? (_a = configStatus.metadata) === null || _a === void 0 ? void 0 : _a.originalHash
716
+ : undefined;
717
+ if (!isRuntimeRefresh) {
718
+ if (!fs_1.default.existsSync(configBakPath)) {
719
+ if (fs_1.default.existsSync(configPath)) {
720
+ originalHash = (0, crypto_1.createHash)('sha256').update(fs_1.default.readFileSync(configPath, 'utf-8')).digest('hex');
721
+ fs_1.default.renameSync(configPath, configBakPath);
722
+ }
723
+ }
724
+ else {
725
+ console.log('OpenCode backup file already exists, skipping backup step');
726
+ }
727
+ }
728
+ if (!fs_1.default.existsSync(configDir)) {
729
+ fs_1.default.mkdirSync(configDir, { recursive: true });
730
+ }
731
+ // 读取当前配置(保留用户其它 provider/agent/command 等)
732
+ let currentConfig = {};
733
+ if (fs_1.default.existsSync(configPath)) {
734
+ try {
735
+ currentConfig = JSON.parse(fs_1.default.readFileSync(configPath, 'utf-8'));
736
+ }
737
+ catch (error) {
738
+ console.warn('Failed to parse current opencode.json, using empty object:', error);
739
+ }
740
+ }
741
+ const model = (defaultModel && typeof defaultModel === 'string' && defaultModel.trim())
742
+ ? defaultModel.trim()
743
+ : DEFAULT_OPENCODE_MODEL;
744
+ // 构建代理配置
745
+ const proxyConfig = {
746
+ provider: {
747
+ aicodeswitch: {
748
+ npm: '@ai-sdk/openai-compatible',
749
+ name: 'AICodeSwitch',
750
+ options: {
751
+ baseURL: `http://${clientHost}:${port}/opencode/v1`,
752
+ apiKey: 'api_key',
753
+ maxRetries: maxRetries !== null && maxRetries !== void 0 ? maxRetries : 5
754
+ },
755
+ models: {
756
+ [model]: { name: model }
757
+ }
758
+ }
759
+ },
760
+ model: `aicodeswitch/${model}`
761
+ };
762
+ const mergedConfig = (0, config_merge_1.mergeJsonConfig)(proxyConfig, currentConfig, config_managed_fields_1.OPENCODE_CONFIG_MANAGED_FIELDS);
763
+ (0, config_merge_1.atomicWriteFile)(configPath, JSON.stringify(mergedConfig, null, 2));
764
+ // 保存元数据
765
+ const currentHash = (0, crypto_1.createHash)('sha256').update(fs_1.default.readFileSync(configPath, 'utf-8')).digest('hex');
766
+ const metadata = {
767
+ configType: 'opencode',
768
+ timestamp: Date.now(),
769
+ originalHash,
770
+ proxyMarker: `http://${host}:${port}/opencode`,
771
+ files: [
772
+ {
773
+ originalPath: configPath,
774
+ backupPath: configBakPath,
775
+ currentHash
776
+ }
777
+ ]
778
+ };
779
+ (0, config_metadata_1.saveMetadata)(metadata);
780
+ return true;
781
+ }
782
+ catch (error) {
783
+ console.error('Failed to write OpenCode config file:', error);
784
+ return false;
785
+ }
786
+ });
787
+ const restoreOpencodeConfig = () => __awaiter(void 0, void 0, void 0, function* () {
788
+ try {
789
+ const configPath = (0, config_metadata_1.getOpencodeConfigPath)();
790
+ const configBakPath = `${configPath}.aicodeswitch_backup`;
791
+ let restoredAnyFile = false;
792
+ if (fs_1.default.existsSync(configBakPath)) {
793
+ const backupConfig = JSON.parse(fs_1.default.readFileSync(configBakPath, 'utf-8'));
794
+ let currentConfig = {};
795
+ if (fs_1.default.existsSync(configPath)) {
796
+ try {
797
+ currentConfig = JSON.parse(fs_1.default.readFileSync(configPath, 'utf-8'));
798
+ }
799
+ catch (error) {
800
+ console.warn('Failed to parse current opencode.json during restore, using empty object:', error);
801
+ }
802
+ }
803
+ // 备份作为基础,合并当前的非管理字段
804
+ const mergedConfig = (0, config_merge_1.mergeJsonConfig)(backupConfig, currentConfig, config_managed_fields_1.OPENCODE_CONFIG_MANAGED_FIELDS);
805
+ (0, config_merge_1.atomicWriteFile)(configPath, JSON.stringify(mergedConfig, null, 2));
806
+ fs_1.default.unlinkSync(configBakPath);
807
+ restoredAnyFile = true;
808
+ }
809
+ (0, config_metadata_1.deleteMetadata)('opencode');
810
+ return restoredAnyFile;
811
+ }
812
+ catch (error) {
813
+ console.error('Failed to restore OpenCode config file:', error);
814
+ return false;
815
+ }
816
+ });
817
+ const checkOpencodeBackupExists = () => {
818
+ try {
819
+ const status = (0, config_metadata_1.checkOpencodeConfigStatus)();
820
+ return status.hasBackup;
821
+ }
822
+ catch (error) {
823
+ console.error('Failed to check OpenCode backup:', error);
824
+ return false;
825
+ }
826
+ };
665
827
  const checkClaudeBackupExists = () => {
666
828
  try {
667
829
  // 清理可能的无效元数据
@@ -696,26 +858,30 @@ const syncConfigsOnServerStartup = (dbManager) => __awaiter(void 0, void 0, void
696
858
  const claudeEffortLevel = isClaudeEffortLevel(config.claudeEffortLevel)
697
859
  ? config.claudeEffortLevel
698
860
  : DEFAULT_CLAUDE_EFFORT_LEVEL;
699
- const claudeWritten = yield writeClaudeConfig(dbManager, config.enableAgentTeams, config.enableBypassPermissionsSupport, config.claudePermissionsDefaultMode, claudeEffortLevel, config.claudeDefaultModel, config.autocompactPctOverride);
861
+ const claudeWritten = yield writeClaudeConfig(dbManager, config.enableAgentTeams, config.enableBypassPermissionsSupport, config.claudePermissionsDefaultMode, claudeEffortLevel, config.claudeDefaultModel, config.autocompactPctOverride, config.claudeMaxRetries);
700
862
  console.log(`[Startup Config Sync] Claude Code config ${claudeWritten ? 'written' : 'skipped'}`);
701
863
  const modelReasoningEffort = isCodexReasoningEffort(config.codexModelReasoningEffort)
702
864
  ? config.codexModelReasoningEffort
703
865
  : DEFAULT_CODEX_REASONING_EFFORT;
704
- const codexWritten = yield writeCodexConfig(dbManager, modelReasoningEffort, config.codexDefaultModel, config.codexEnableMemories);
866
+ const codexWritten = yield writeCodexConfig(dbManager, modelReasoningEffort, config.codexDefaultModel, config.codexEnableMemories, config.codexMaxRetries);
705
867
  console.log(`[Startup Config Sync] Codex config ${codexWritten ? 'written' : 'skipped'}`);
868
+ const opencodeWritten = yield writeOpencodeConfig(dbManager, config.opencodeDefaultModel, config.opencodeMaxRetries);
869
+ console.log(`[Startup Config Sync] OpenCode config ${opencodeWritten ? 'written' : 'skipped'}`);
706
870
  });
707
871
  const syncConfigsOnGlobalConfigUpdate = (dbManager) => __awaiter(void 0, void 0, void 0, function* () {
708
872
  const config = dbManager.getConfig();
709
873
  const claudeEffortLevel = isClaudeEffortLevel(config.claudeEffortLevel)
710
874
  ? config.claudeEffortLevel
711
875
  : DEFAULT_CLAUDE_EFFORT_LEVEL;
712
- const claudeUpdated = yield writeClaudeConfig(dbManager, config.enableAgentTeams, config.enableBypassPermissionsSupport, config.claudePermissionsDefaultMode, claudeEffortLevel, config.claudeDefaultModel, config.autocompactPctOverride, { allowOverwriteRefresh: true });
876
+ const claudeUpdated = yield writeClaudeConfig(dbManager, config.enableAgentTeams, config.enableBypassPermissionsSupport, config.claudePermissionsDefaultMode, claudeEffortLevel, config.claudeDefaultModel, config.autocompactPctOverride, config.claudeMaxRetries, { allowOverwriteRefresh: true });
713
877
  console.log(`[Config Update Sync] Claude Code config ${claudeUpdated ? 'written' : 'skipped'}`);
714
878
  const modelReasoningEffort = isCodexReasoningEffort(config.codexModelReasoningEffort)
715
879
  ? config.codexModelReasoningEffort
716
880
  : DEFAULT_CODEX_REASONING_EFFORT;
717
- const codexUpdated = yield writeCodexConfig(dbManager, modelReasoningEffort, config.codexDefaultModel, config.codexEnableMemories, { allowOverwriteRefresh: true });
881
+ const codexUpdated = yield writeCodexConfig(dbManager, modelReasoningEffort, config.codexDefaultModel, config.codexEnableMemories, config.codexMaxRetries, { allowOverwriteRefresh: true });
718
882
  console.log(`[Config Update Sync] Codex config ${codexUpdated ? 'written' : 'skipped'}`);
883
+ const opencodeUpdated = yield writeOpencodeConfig(dbManager, config.opencodeDefaultModel, config.opencodeMaxRetries, { allowOverwriteRefresh: true });
884
+ console.log(`[Config Update Sync] OpenCode config ${opencodeUpdated ? 'written' : 'skipped'}`);
719
885
  });
720
886
  const getCentralSkillsDir = () => {
721
887
  return path_1.default.join(os_1.default.homedir(), '.aicodeswitch', 'skills');
@@ -734,12 +900,20 @@ function getSkillDirByName(name) {
734
900
  return path_1.default.join(centralDir, sanitizedName);
735
901
  }
736
902
  const getSkillSymlinkPath = (skillId, targetType) => {
903
+ if (targetType === 'opencode') {
904
+ // OpenCode 没有 skills 目录,映射为全局 command:~/.config/opencode/commands/<skillId>.md
905
+ return path_1.default.join(os_1.default.homedir(), '.config', 'opencode', 'commands', `${skillId}.md`);
906
+ }
737
907
  const baseDir = targetType === 'claude-code' ? '.claude' : '.codex';
738
908
  return path_1.default.join(os_1.default.homedir(), baseDir, 'skills', skillId);
739
909
  };
740
910
  function isSkillSymlinkExists(skillId, targetType) {
741
911
  const symlinkPath = getSkillSymlinkPath(skillId, targetType);
742
912
  try {
913
+ if (targetType === 'opencode') {
914
+ // OpenCode 是普通文件,不是 symlink
915
+ return fs_1.default.existsSync(symlinkPath);
916
+ }
743
917
  const stats = fs_1.default.lstatSync(symlinkPath);
744
918
  return stats.isSymbolicLink();
745
919
  }
@@ -747,15 +921,77 @@ function isSkillSymlinkExists(skillId, targetType) {
747
921
  return false;
748
922
  }
749
923
  }
924
+ /**
925
+ * 从 SKILL.md 内容中剥离前导 YAML frontmatter,返回 { description?, body }
926
+ * 没有 frontmatter 时,description 为 undefined,body 为原文。
927
+ */
928
+ function parseSkillMd(skillMdContent) {
929
+ const trimmed = skillMdContent.replace(/^/, '');
930
+ const fmMatch = trimmed.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
931
+ if (!fmMatch) {
932
+ return { body: trimmed };
933
+ }
934
+ const frontmatter = fmMatch[1];
935
+ const body = fmMatch[2];
936
+ const descMatch = frontmatter.match(/^description:\s*(.+)$/m);
937
+ return {
938
+ description: descMatch ? descMatch[1].replace(/^["']|["']$/g, '').trim() : undefined,
939
+ body: body.trim(),
940
+ };
941
+ }
942
+ /**
943
+ * 为 OpenCode 生成 command markdown 文件内容。
944
+ * frontmatter 用 description(OpenCode 在 TUI 展示),正文用 SKILL.md 的 body。
945
+ */
946
+ function buildOpencodeSkillCommandMarkdown(skillDir, skillId, fallbackDescription) {
947
+ let description = fallbackDescription || '';
948
+ let body = '';
949
+ const skillMdPath = path_1.default.join(skillDir, 'SKILL.md');
950
+ if (fs_1.default.existsSync(skillMdPath)) {
951
+ try {
952
+ const parsed = parseSkillMd(fs_1.default.readFileSync(skillMdPath, 'utf-8'));
953
+ body = parsed.body || '';
954
+ if (!description && parsed.description) {
955
+ description = parsed.description;
956
+ }
957
+ }
958
+ catch ( /* ignore */_a) { /* ignore */ }
959
+ }
960
+ if (!description) {
961
+ description = skillId;
962
+ }
963
+ const bodySection = body ? `\n${body}\n` : '\n';
964
+ return `---\ndescription: ${description.replace(/\n/g, ' ')}\nagent: build\n---\n${bodySection}`;
965
+ }
750
966
  function createSkillSymlink(skillId, targetType) {
751
967
  return __awaiter(this, void 0, void 0, function* () {
968
+ var _a;
752
969
  try {
753
970
  const centralDir = getCentralSkillsDir();
754
971
  const skillDir = path_1.default.join(centralDir, skillId);
755
- const symlinkPath = getSkillSymlinkPath(skillId, targetType);
756
972
  if (!fs_1.default.existsSync(skillDir)) {
757
973
  return { success: false, error: 'Skill目录不存在' };
758
974
  }
975
+ // OpenCode:生成 command markdown 写入 ~/.config/opencode/commands/<skillId>.md
976
+ if (targetType === 'opencode') {
977
+ const commandPath = getSkillSymlinkPath(skillId, targetType);
978
+ const commandDir = path_1.default.dirname(commandPath);
979
+ if (!fs_1.default.existsSync(commandDir)) {
980
+ fs_1.default.mkdirSync(commandDir, { recursive: true });
981
+ }
982
+ let fallbackDescription;
983
+ const metadataPath = path_1.default.join(skillDir, 'skill.json');
984
+ if (fs_1.default.existsSync(metadataPath)) {
985
+ try {
986
+ fallbackDescription = (_a = JSON.parse(fs_1.default.readFileSync(metadataPath, 'utf-8'))) === null || _a === void 0 ? void 0 : _a.description;
987
+ }
988
+ catch ( /* ignore */_b) { /* ignore */ }
989
+ }
990
+ const content = buildOpencodeSkillCommandMarkdown(skillDir, skillId, fallbackDescription);
991
+ (0, config_merge_1.atomicWriteFile)(commandPath, content);
992
+ return { success: true };
993
+ }
994
+ const symlinkPath = getSkillSymlinkPath(skillId, targetType);
759
995
  const targetBaseDir = path_1.default.dirname(symlinkPath);
760
996
  if (!fs_1.default.existsSync(targetBaseDir)) {
761
997
  fs_1.default.mkdirSync(targetBaseDir, { recursive: true });
@@ -786,6 +1022,13 @@ function removeSkillSymlink(skillId, targetType) {
786
1022
  return __awaiter(this, void 0, void 0, function* () {
787
1023
  try {
788
1024
  const symlinkPath = getSkillSymlinkPath(skillId, targetType);
1025
+ if (targetType === 'opencode') {
1026
+ // OpenCode:删除 command 文件
1027
+ if (fs_1.default.existsSync(symlinkPath)) {
1028
+ fs_1.default.unlinkSync(symlinkPath);
1029
+ }
1030
+ return { success: true };
1031
+ }
789
1032
  if (!fs_1.default.existsSync(symlinkPath)) {
790
1033
  return { success: true };
791
1034
  }
@@ -1044,7 +1287,7 @@ const listInstalledSkills = () => {
1044
1287
  try {
1045
1288
  const metadata = JSON.parse(fs_1.default.readFileSync(metadataPath, 'utf-8'));
1046
1289
  const enabledTargets = [];
1047
- ['claude-code', 'codex'].forEach((targetType) => {
1290
+ ['claude-code', 'codex', 'opencode'].forEach((targetType) => {
1048
1291
  if (isSkillSymlinkExists(skillId, targetType)) {
1049
1292
  enabledTargets.push(targetType);
1050
1293
  }
@@ -1076,7 +1319,7 @@ const listInstalledSkills = () => {
1076
1319
  const registerRoutes = (dbManager, proxyServer) => __awaiter(void 0, void 0, void 0, function* () {
1077
1320
  updateProxyConfig(dbManager.getConfig());
1078
1321
  app.get('/health', (_req, res) => res.json({ status: 'ok' }));
1079
- // 数据就绪验证端点(供 Tauri 启动阶段确认后端完全可用)
1322
+ // 数据就绪验证端点(供桌面端 Electron 启动阶段确认后端完全可用)
1080
1323
  app.get('/api/ready', (_req, res) => {
1081
1324
  const vendors = dbManager.getVendors();
1082
1325
  const routes = dbManager.getRoutes();
@@ -1196,7 +1439,7 @@ const registerRoutes = (dbManager, proxyServer) => __awaiter(void 0, void 0, voi
1196
1439
  if (!tool || !routeId) {
1197
1440
  return res.status(400).json({ error: 'tool and routeId are required' });
1198
1441
  }
1199
- if (tool !== 'claude-code' && tool !== 'codex') {
1442
+ if (tool !== 'claude-code' && tool !== 'codex' && tool !== 'opencode') {
1200
1443
  return res.status(400).json({ error: 'Invalid tool name' });
1201
1444
  }
1202
1445
  const route = dbManager.getRoute(routeId);
@@ -1217,7 +1460,7 @@ const registerRoutes = (dbManager, proxyServer) => __awaiter(void 0, void 0, voi
1217
1460
  })));
1218
1461
  app.post('/api/tool-bindings/deactivate', asyncHandler((req, res) => __awaiter(void 0, void 0, void 0, function* () {
1219
1462
  const { tool } = req.body;
1220
- if (!tool || (tool !== 'claude-code' && tool !== 'codex')) {
1463
+ if (!tool || (tool !== 'claude-code' && tool !== 'codex' && tool !== 'opencode')) {
1221
1464
  return res.status(400).json({ error: 'Invalid tool name' });
1222
1465
  }
1223
1466
  const result = yield dbManager.deactivateToolRoute(tool);
@@ -2005,7 +2248,7 @@ ${instruction}
2005
2248
  app.post('/api/skills/:skillId/enable', asyncHandler((req, res) => __awaiter(void 0, void 0, void 0, function* () {
2006
2249
  const { skillId } = req.params;
2007
2250
  const { targetType } = req.body;
2008
- if (!targetType || (targetType !== 'claude-code' && targetType !== 'codex')) {
2251
+ if (!targetType || (targetType !== 'claude-code' && targetType !== 'codex' && targetType !== 'opencode')) {
2009
2252
  res.status(400).json({ success: false, error: '无效的目标类型' });
2010
2253
  return;
2011
2254
  }
@@ -2036,7 +2279,7 @@ ${instruction}
2036
2279
  app.post('/api/skills/:skillId/disable', asyncHandler((req, res) => __awaiter(void 0, void 0, void 0, function* () {
2037
2280
  const { skillId } = req.params;
2038
2281
  const { targetType } = req.body;
2039
- if (!targetType || (targetType !== 'claude-code' && targetType !== 'codex')) {
2282
+ if (!targetType || (targetType !== 'claude-code' && targetType !== 'codex' && targetType !== 'opencode')) {
2040
2283
  res.status(400).json({ success: false, error: '无效的目标类型' });
2041
2284
  return;
2042
2285
  }
@@ -2065,7 +2308,7 @@ ${instruction}
2065
2308
  res.status(404).json({ success: false, error: 'Skill不存在' });
2066
2309
  return;
2067
2310
  }
2068
- ['claude-code', 'codex'].forEach((targetType) => __awaiter(void 0, void 0, void 0, function* () {
2311
+ ['claude-code', 'codex', 'opencode'].forEach((targetType) => __awaiter(void 0, void 0, void 0, function* () {
2069
2312
  yield removeSkillSymlink(skillId, targetType);
2070
2313
  }));
2071
2314
  fs_1.default.rmSync(skillDir, { recursive: true, force: true });
@@ -2087,7 +2330,7 @@ ${instruction}
2087
2330
  : isClaudePermissionDefaultMode(appConfig.claudePermissionsDefaultMode)
2088
2331
  ? appConfig.claudePermissionsDefaultMode
2089
2332
  : DEFAULT_CLAUDE_PERMISSION_DEFAULT_MODE;
2090
- const result = yield writeClaudeConfig(dbManager, enableAgentTeams, enableBypassPermissionsSupport, permissionsDefaultMode, undefined, appConfig.claudeDefaultModel, appConfig.autocompactPctOverride);
2333
+ const result = yield writeClaudeConfig(dbManager, enableAgentTeams, enableBypassPermissionsSupport, permissionsDefaultMode, undefined, appConfig.claudeDefaultModel, appConfig.autocompactPctOverride, appConfig.claudeMaxRetries);
2091
2334
  applyWriteLocalRecords(proxyServer);
2092
2335
  res.json(result);
2093
2336
  })));
@@ -2103,7 +2346,16 @@ ${instruction}
2103
2346
  const enableMemories = requestedEnableMemories !== undefined
2104
2347
  ? !!requestedEnableMemories
2105
2348
  : !!appConfig.codexEnableMemories;
2106
- const result = yield writeCodexConfig(dbManager, modelReasoningEffort, appConfig.codexDefaultModel, enableMemories);
2349
+ const result = yield writeCodexConfig(dbManager, modelReasoningEffort, appConfig.codexDefaultModel, enableMemories, appConfig.codexMaxRetries);
2350
+ applyWriteLocalRecords(proxyServer);
2351
+ res.json(result);
2352
+ })));
2353
+ app.post('/api/write-config/opencode', asyncHandler((req, res) => __awaiter(void 0, void 0, void 0, function* () {
2354
+ var _a;
2355
+ const appConfig = dbManager.getConfig();
2356
+ const requestedModel = typeof ((_a = req.body) === null || _a === void 0 ? void 0 : _a.defaultModel) === 'string' ? req.body.defaultModel : undefined;
2357
+ const defaultModel = requestedModel || appConfig.opencodeDefaultModel;
2358
+ const result = yield writeOpencodeConfig(dbManager, defaultModel, appConfig.opencodeMaxRetries);
2107
2359
  applyWriteLocalRecords(proxyServer);
2108
2360
  res.json(result);
2109
2361
  })));
@@ -2160,12 +2412,19 @@ ${instruction}
2160
2412
  const result = yield restoreCodexConfig();
2161
2413
  res.json(result);
2162
2414
  })));
2415
+ app.post('/api/restore-config/opencode', asyncHandler((_req, res) => __awaiter(void 0, void 0, void 0, function* () {
2416
+ const result = yield restoreOpencodeConfig();
2417
+ res.json(result);
2418
+ })));
2163
2419
  app.get('/api/check-backup/claude', (_req, res) => {
2164
2420
  res.json({ exists: checkClaudeBackupExists() });
2165
2421
  });
2166
2422
  app.get('/api/check-backup/codex', (_req, res) => {
2167
2423
  res.json({ exists: checkCodexBackupExists() });
2168
2424
  });
2425
+ app.get('/api/check-backup/opencode', (_req, res) => {
2426
+ res.json({ exists: checkOpencodeBackupExists() });
2427
+ });
2169
2428
  // 新的详细配置状态 API 端点
2170
2429
  app.get('/api/config-status/claude', (_req, res) => {
2171
2430
  const status = (0, config_metadata_1.checkClaudeConfigStatus)();
@@ -2175,6 +2434,10 @@ ${instruction}
2175
2434
  const status = (0, config_metadata_1.checkCodexConfigStatus)();
2176
2435
  res.json(status);
2177
2436
  });
2437
+ app.get('/api/config-status/opencode', (_req, res) => {
2438
+ const status = (0, config_metadata_1.checkOpencodeConfigStatus)();
2439
+ res.json(status);
2440
+ });
2178
2441
  // API 路径路由映射
2179
2442
  app.get('/api/api-path-bindings', (_req, res) => {
2180
2443
  res.json({ bindings: dbManager.getApiPathBindings(), models: dbManager.getApiPathModels() });
@@ -2939,6 +3202,31 @@ ${instruction}
2939
3202
  (0, config_merge_1.atomicWriteFile)(authPath, JSON.stringify(auth, null, 2));
2940
3203
  results['codex'] = true;
2941
3204
  }
3205
+ else if (target === 'opencode') {
3206
+ // 写入 ~/.config/opencode/opencode.json 的 provider.aicodeswitch.options.apiKey
3207
+ const opencodeConfigPath = (0, config_metadata_1.getOpencodeConfigPath)();
3208
+ const opencodeDir = path_1.default.dirname(opencodeConfigPath);
3209
+ if (!fs_1.default.existsSync(opencodeDir)) {
3210
+ fs_1.default.mkdirSync(opencodeDir, { recursive: true });
3211
+ }
3212
+ let oc = {};
3213
+ if (fs_1.default.existsSync(opencodeConfigPath)) {
3214
+ try {
3215
+ oc = JSON.parse(fs_1.default.readFileSync(opencodeConfigPath, 'utf-8'));
3216
+ }
3217
+ catch ( /* ignore */_c) { /* ignore */ }
3218
+ }
3219
+ if (!oc.provider)
3220
+ oc.provider = {};
3221
+ if (!oc.provider.aicodeswitch || typeof oc.provider.aicodeswitch !== 'object') {
3222
+ oc.provider.aicodeswitch = { npm: '@ai-sdk/openai-compatible', name: 'AICodeSwitch', options: {} };
3223
+ }
3224
+ if (!oc.provider.aicodeswitch.options)
3225
+ oc.provider.aicodeswitch.options = {};
3226
+ oc.provider.aicodeswitch.options.apiKey = key.apiKey;
3227
+ (0, config_merge_1.atomicWriteFile)(opencodeConfigPath, JSON.stringify(oc, null, 2));
3228
+ results['opencode'] = true;
3229
+ }
2942
3230
  }
2943
3231
  catch (error) {
2944
3232
  console.error(`Failed to write local config for ${target}:`, error);
@@ -3319,6 +3607,76 @@ ${instruction}
3319
3607
  console.log(`[MCP] Codex MCP config written: ${writtenMcpIds.length} server(s)`);
3320
3608
  return true;
3321
3609
  }
3610
+ else if (targetType === 'opencode') {
3611
+ // OpenCode 使用 JSON 格式的 opencode.json,MCP 配置位于 mcp 段
3612
+ // 格式:local → { type:"local", command:[...], enabled:true, env? }
3613
+ // remote → { type:"remote", url, enabled:true, headers? }
3614
+ const opencodeConfigPath = (0, config_metadata_1.getOpencodeConfigPath)();
3615
+ const opencodeDir = path_1.default.dirname(opencodeConfigPath);
3616
+ if (!fs_1.default.existsSync(opencodeDir)) {
3617
+ fs_1.default.mkdirSync(opencodeDir, { recursive: true });
3618
+ }
3619
+ let currentConfig = {};
3620
+ if (fs_1.default.existsSync(opencodeConfigPath)) {
3621
+ try {
3622
+ currentConfig = JSON.parse(fs_1.default.readFileSync(opencodeConfigPath, 'utf-8'));
3623
+ }
3624
+ catch (error) {
3625
+ console.warn('[MCP] Failed to parse opencode.json:', error);
3626
+ }
3627
+ }
3628
+ // 清除代理上次写入的 mcp 条目(通过 metadata 追踪,避免误删用户自配的 mcp)
3629
+ const mcpMetaPath = path_1.default.join(opencodeDir, '.aicodeswitch_mcp_servers.json');
3630
+ let previousMcpIds = [];
3631
+ if (fs_1.default.existsSync(mcpMetaPath)) {
3632
+ try {
3633
+ previousMcpIds = JSON.parse(fs_1.default.readFileSync(mcpMetaPath, 'utf8'));
3634
+ for (const id of previousMcpIds) {
3635
+ if (currentConfig.mcp && currentConfig.mcp[id]) {
3636
+ delete currentConfig.mcp[id];
3637
+ }
3638
+ }
3639
+ }
3640
+ catch (_b) {
3641
+ // ignore
3642
+ }
3643
+ }
3644
+ if (!currentConfig.mcp) {
3645
+ currentConfig.mcp = {};
3646
+ }
3647
+ const writtenMcpIds = [];
3648
+ for (const mcp of mcps) {
3649
+ const mcpConfig = { enabled: true };
3650
+ if (mcp.type === 'stdio') {
3651
+ mcpConfig.type = 'local';
3652
+ const cmdParts = [mcp.command || ''];
3653
+ if (Array.isArray(mcp.args)) {
3654
+ cmdParts.push(...mcp.args);
3655
+ }
3656
+ mcpConfig.command = cmdParts.filter((c, i) => i === 0 ? c !== '' : true);
3657
+ if (mcp.env && Object.keys(mcp.env).length > 0) {
3658
+ mcpConfig.environment = Object.assign({}, mcp.env);
3659
+ }
3660
+ }
3661
+ else {
3662
+ // http / sse 均使用 remote 类型
3663
+ mcpConfig.type = 'remote';
3664
+ mcpConfig.url = mcp.url || '';
3665
+ if (mcp.headers && Object.keys(mcp.headers).length > 0) {
3666
+ mcpConfig.headers = Object.assign({}, mcp.headers);
3667
+ }
3668
+ }
3669
+ currentConfig.mcp[mcp.id] = mcpConfig;
3670
+ writtenMcpIds.push(mcp.id);
3671
+ }
3672
+ if (Object.keys(currentConfig.mcp).length === 0) {
3673
+ delete currentConfig.mcp;
3674
+ }
3675
+ (0, config_merge_1.atomicWriteFile)(opencodeConfigPath, JSON.stringify(currentConfig, null, 2));
3676
+ fs_1.default.writeFileSync(mcpMetaPath, JSON.stringify(writtenMcpIds, null, 2));
3677
+ console.log(`[MCP] OpenCode MCP config written: ${writtenMcpIds.length} server(s)`);
3678
+ return true;
3679
+ }
3322
3680
  return false;
3323
3681
  }
3324
3682
  catch (error) {
@@ -3381,6 +3739,41 @@ ${instruction}
3381
3739
  }
3382
3740
  return true;
3383
3741
  }
3742
+ else if (targetType === 'opencode') {
3743
+ const opencodeConfigPath = (0, config_metadata_1.getOpencodeConfigPath)();
3744
+ if (!fs_1.default.existsSync(opencodeConfigPath)) {
3745
+ return true;
3746
+ }
3747
+ let currentConfig = {};
3748
+ try {
3749
+ currentConfig = JSON.parse(fs_1.default.readFileSync(opencodeConfigPath, 'utf-8'));
3750
+ }
3751
+ catch (error) {
3752
+ console.warn('[MCP] Failed to parse opencode.json for removal:', error);
3753
+ return false;
3754
+ }
3755
+ if (currentConfig.mcp && currentConfig.mcp[mcpId]) {
3756
+ delete currentConfig.mcp[mcpId];
3757
+ if (Object.keys(currentConfig.mcp).length === 0) {
3758
+ delete currentConfig.mcp;
3759
+ }
3760
+ (0, config_merge_1.atomicWriteFile)(opencodeConfigPath, JSON.stringify(currentConfig, null, 2));
3761
+ const opencodeDir = path_1.default.dirname(opencodeConfigPath);
3762
+ const mcpMetaPath = path_1.default.join(opencodeDir, '.aicodeswitch_mcp_servers.json');
3763
+ if (fs_1.default.existsSync(mcpMetaPath)) {
3764
+ try {
3765
+ const previousIds = JSON.parse(fs_1.default.readFileSync(mcpMetaPath, 'utf8'));
3766
+ const updatedIds = previousIds.filter(id => id !== mcpId);
3767
+ fs_1.default.writeFileSync(mcpMetaPath, JSON.stringify(updatedIds, null, 2));
3768
+ }
3769
+ catch (_b) {
3770
+ // ignore
3771
+ }
3772
+ }
3773
+ console.log(`[MCP] Removed MCP ${mcpId} from OpenCode config`);
3774
+ }
3775
+ return true;
3776
+ }
3384
3777
  return false;
3385
3778
  }
3386
3779
  catch (error) {
@@ -3413,6 +3806,26 @@ ${instruction}
3413
3806
  });
3414
3807
  // listen 就绪标志:区分"启动阶段"与"运行阶段",启动期致命异常应让进程退出
3415
3808
  let listenReady = false;
3809
+ /**
3810
+ * 是否以「内嵌进程」模式运行(例如被 Electron 主进程直接 require 并调用 start)。
3811
+ * 该模式下:
3812
+ * - shutdown 流程结束后不调用 process.exit,把退出时机交还给宿主(Electron)
3813
+ * - 模块被 require 时不自动执行 start(),由宿主显式调用导出的 start
3814
+ */
3815
+ const IN_PROCESS = process.env.AIC_IN_PROCESS === '1';
3816
+ // 保存当前服务实例的优雅关闭函数,供宿主(Electron 主进程)在退出前显式调用。
3817
+ let _gracefulShutdown = null;
3818
+ /**
3819
+ * 供宿主进程调用的优雅关闭入口。
3820
+ * 仅在 start() 成功注册 shutdown 后可用;调用后会恢复工具配置、关闭 DB / 日志、释放端口,
3821
+ * 内嵌模式下不会触发 process.exit。
3822
+ */
3823
+ const gracefulShutdown = (signal = 'HOST_QUIT') => {
3824
+ if (_gracefulShutdown)
3825
+ return _gracefulShutdown(signal);
3826
+ return Promise.resolve();
3827
+ };
3828
+ exports.gracefulShutdown = gracefulShutdown;
3416
3829
  const start = () => __awaiter(void 0, void 0, void 0, function* () {
3417
3830
  fs_1.default.mkdirSync(dataDir, { recursive: true });
3418
3831
  // 自动检测数据库类型并执行迁移(如果需要)
@@ -3590,6 +4003,13 @@ const start = () => __awaiter(void 0, void 0, void 0, function* () {
3590
4003
  catch (error) {
3591
4004
  console.error('[Shutdown ...] Failed to restore Codex config:', error);
3592
4005
  }
4006
+ try {
4007
+ const opencodeRestored = yield restoreOpencodeConfig();
4008
+ console.log(`[Shutdown ...] OpenCode config ${opencodeRestored ? 'restored' : 'was not modified'}`);
4009
+ }
4010
+ catch (error) {
4011
+ console.error('[Shutdown ...] Failed to restore OpenCode config:', error);
4012
+ }
3593
4013
  // Shutdown AccessKey module
3594
4014
  try {
3595
4015
  yield accessKeyModule.shutdown();
@@ -3623,25 +4043,31 @@ const start = () => __awaiter(void 0, void 0, void 0, function* () {
3623
4043
  })
3624
4044
  ]);
3625
4045
  console.log('Server stopped.');
3626
- process.exit(0);
4046
+ // 内嵌进程模式下不主动退出,交由宿主(Electron)控制进程生命周期
4047
+ if (!IN_PROCESS) {
4048
+ process.exit(0);
4049
+ }
3627
4050
  }))();
3628
4051
  return shutdownPromise;
3629
4052
  });
4053
+ // 注册到模块级句柄,供宿主在退出前显式触发完整关闭流程
4054
+ _gracefulShutdown = shutdown;
3630
4055
  process.on('SIGINT', () => { void shutdown('SIGINT'); });
3631
4056
  process.on('SIGTERM', () => { void shutdown('SIGTERM'); });
3632
- // 优雅关闭端点(供 Tauri 等外部调用者触发服务端完整清理流程)
4057
+ // 优雅关闭端点(供 Electron 等外部调用者触发服务端完整清理流程)
3633
4058
  // 放在 shutdown 定义之后注册,确保闭包可引用
3634
4059
  app.post('/api/shutdown', asyncHandler((_req, res) => __awaiter(void 0, void 0, void 0, function* () {
3635
4060
  res.json({ success: true });
3636
4061
  setImmediate(() => { void shutdown('HTTP_SHUTDOWN'); });
3637
4062
  })));
3638
4063
  });
4064
+ exports.start = start;
3639
4065
  // 全局未捕获异常处理 - 防止服务崩溃
3640
4066
  process.on('uncaughtException', (error) => {
3641
4067
  console.error('[Uncaught Exception] 服务遇到未捕获的异常:', error);
3642
4068
  console.error('[Uncaught Exception] 堆栈信息:', error.stack);
3643
4069
  // 启动阶段(listen 之前)的异常通常是致命的(依赖加载失败、初始化崩溃等),
3644
- // 静默吞掉会导致"进程在但不 listen",Tauri 只能干等超时;此时退出让上层重新探测/诊断。
4070
+ // 静默吞掉会导致"进程在但不 listen",桌面端只能干等超时;此时退出让上层重新探测/诊断。
3645
4071
  if (!listenReady) {
3646
4072
  console.error('[Uncaught Exception] 发生在服务监听之前,退出进程');
3647
4073
  process.exit(1);
@@ -3654,7 +4080,11 @@ process.on('unhandledRejection', (reason) => {
3654
4080
  process.exit(1);
3655
4081
  }
3656
4082
  });
3657
- start().catch((error) => {
3658
- console.error('Failed to start server:', error);
3659
- process.exit(1);
3660
- });
4083
+ // 仅在被直接运行时(如 aicos start node dist/server/main.js)自动启动;
4084
+ // 被 Electron 主进程 require 时(require.main !== module IN_PROCESS)由宿主显式调用 start()
4085
+ if (!IN_PROCESS && require.main === module) {
4086
+ (0, exports.start)().catch((error) => {
4087
+ console.error('Failed to start server:', error);
4088
+ process.exit(1);
4089
+ });
4090
+ }