codexmate 0.0.45 → 0.0.48

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/cli.js CHANGED
@@ -185,6 +185,11 @@ const DEFAULT_WEB_OPEN_HOST = '127.0.0.1';
185
185
  const CONFIG_DIR = path.join(os.homedir(), '.codex');
186
186
  const CONFIG_FILE = path.join(CONFIG_DIR, 'config.toml');
187
187
  const AUTH_FILE = path.join(CONFIG_DIR, 'auth.json');
188
+ const OPENCODE_CONFIG_DIR = path.join(process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config'), 'opencode');
189
+ const OPENCODE_CONFIG_ENV_FILE = process.env.OPENCODE_CONFIG ? path.resolve(process.env.OPENCODE_CONFIG) : '';
190
+ const OPENCODE_GLOBAL_JSONC_CONFIG_FILE = path.join(OPENCODE_CONFIG_DIR, 'opencode.jsonc');
191
+ const OPENCODE_GLOBAL_JSON_CONFIG_FILE = path.join(OPENCODE_CONFIG_DIR, 'opencode.json');
192
+ const OPENCODE_LEGACY_CONFIG_FILE = path.join(OPENCODE_CONFIG_DIR, 'config.json');
188
193
  const AUTH_PROFILES_DIR = path.join(CONFIG_DIR, 'auth-profiles');
189
194
  const AUTH_REGISTRY_FILE = path.join(AUTH_PROFILES_DIR, 'registry.json');
190
195
  const MODELS_FILE = path.join(CONFIG_DIR, 'models.json');
@@ -214,6 +219,8 @@ const CODEBUDDY_DIR = path.join(os.homedir(), '.codebuddy');
214
219
  const CODEBUDDY_PROJECTS_DIR = path.join(CODEBUDDY_DIR, 'projects');
215
220
  const CODEXMATE_DIR = path.join(os.homedir(), '.codexmate');
216
221
  const CODEXMATE_PREFERENCES_FILE = path.join(CODEXMATE_DIR, 'preferences.json');
222
+ const CODEXMATE_OPENCODE_DIR = path.join(CODEXMATE_DIR, 'opencode');
223
+ const CODEXMATE_OPENCODE_PROVIDER_STORE_FILE = path.join(CODEXMATE_OPENCODE_DIR, 'providers.json');
217
224
  const CODEXMATE_SESSIONS_DIR = path.join(CODEXMATE_DIR, 'sessions');
218
225
  const CODEXMATE_DERIVED_SESSIONS_DIR = path.join(CODEXMATE_SESSIONS_DIR, 'derived');
219
226
  const CODEXMATE_DERIVED_CODEX_DIR = path.join(CODEXMATE_DERIVED_SESSIONS_DIR, 'codex');
@@ -860,8 +867,8 @@ function isPlainObject(value) {
860
867
  return !!value && typeof value === 'object' && !Array.isArray(value);
861
868
  }
862
869
 
863
- const TOOL_CONFIG_PERMISSION_TARGETS = new Set(['codex', 'claude']);
864
- const TOOL_CONFIG_PERMISSION_DEFAULTS = Object.freeze({ codex: false, claude: false });
870
+ const TOOL_CONFIG_PERMISSION_TARGETS = new Set(['codex', 'claude', 'opencode']);
871
+ const TOOL_CONFIG_PERMISSION_DEFAULTS = Object.freeze({ codex: false, claude: false, opencode: false });
865
872
  let toolConfigWriteGuardDepth = 0;
866
873
 
867
874
  function enterToolConfigWriteGuard() {
@@ -887,7 +894,8 @@ function normalizeToolConfigPermissions(value) {
887
894
  const source = isPlainObject(value) ? value : {};
888
895
  return {
889
896
  codex: source.codex === true,
890
- claude: source.claude === true
897
+ claude: source.claude === true,
898
+ opencode: source.opencode === true
891
899
  };
892
900
  }
893
901
 
@@ -967,8 +975,13 @@ function getApiToolConfigWriteTarget(action) {
967
975
  'claude-local-bridge-set-excluded',
968
976
  'claude-local-bridge-sync-providers'
969
977
  ]);
978
+ const opencodeWriteActions = new Set([
979
+ 'apply-opencode-config',
980
+ 'update-opencode-selection'
981
+ ]);
970
982
  if (codexWriteActions.has(name)) return 'codex';
971
983
  if (claudeWriteActions.has(name)) return 'claude';
984
+ if (opencodeWriteActions.has(name)) return 'opencode';
972
985
  return '';
973
986
  }
974
987
 
@@ -2306,6 +2319,42 @@ function buildConfigTemplateDiff(params = {}) {
2306
2319
  };
2307
2320
  }
2308
2321
 
2322
+ function buildClaudeSettingsDiff(params = {}) {
2323
+ const content = typeof params.content === 'string' ? params.content : '';
2324
+ if (!content.trim()) {
2325
+ return { error: 'JSON 内容不能为空' };
2326
+ }
2327
+ if (content.length > 1024 * 1024) {
2328
+ return { error: '内容过大(最大 1MB)' };
2329
+ }
2330
+ let parsed;
2331
+ try {
2332
+ parsed = JSON.parse(content);
2333
+ } catch (e) {
2334
+ return { error: `JSON 解析失败: ${e.message}` };
2335
+ }
2336
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
2337
+ return { error: 'JSON 内容必须是一个对象' };
2338
+ }
2339
+ let beforeText = '';
2340
+ if (fs.existsSync(CLAUDE_SETTINGS_FILE)) {
2341
+ try {
2342
+ beforeText = fs.readFileSync(CLAUDE_SETTINGS_FILE, 'utf-8');
2343
+ } catch (e) {
2344
+ return { error: `读取 settings.json 失败: ${e.message}` };
2345
+ }
2346
+ }
2347
+ const afterText = JSON.stringify(parsed, null, 2) + '\n';
2348
+ const diff = buildLineDiff(beforeText, afterText);
2349
+ const hasChanges = (diff.stats.added || 0) + (diff.stats.removed || 0) > 0;
2350
+ return {
2351
+ diff: {
2352
+ ...diff,
2353
+ hasChanges
2354
+ }
2355
+ };
2356
+ }
2357
+
2309
2358
  function addProviderToConfig(params = {}) {
2310
2359
  const name = typeof params.name === 'string' ? params.name.trim() : '';
2311
2360
  const url = typeof params.url === 'string' ? params.url.trim() : '';
@@ -9515,6 +9564,12 @@ async function applyToClaudeSettings(config = {}) {
9515
9564
  };
9516
9565
  delete nextEnv.ANTHROPIC_AUTH_TOKEN;
9517
9566
  delete nextEnv.CLAUDE_CODE_USE_KEY;
9567
+ const subModels = {
9568
+ ANTHROPIC_DEFAULT_HAIKU_MODEL: model,
9569
+ ANTHROPIC_DEFAULT_SONNET_MODEL: model,
9570
+ ANTHROPIC_DEFAULT_OPUS_MODEL: model
9571
+ };
9572
+ Object.assign(nextEnv, subModels);
9518
9573
 
9519
9574
  const nextSettings = {
9520
9575
  ...currentSettings,
@@ -9533,7 +9588,10 @@ async function applyToClaudeSettings(config = {}) {
9533
9588
  updatedKeys: [
9534
9589
  'env.ANTHROPIC_API_KEY',
9535
9590
  'env.ANTHROPIC_BASE_URL',
9536
- 'env.ANTHROPIC_MODEL'
9591
+ 'env.ANTHROPIC_MODEL',
9592
+ 'env.ANTHROPIC_DEFAULT_HAIKU_MODEL',
9593
+ 'env.ANTHROPIC_DEFAULT_SONNET_MODEL',
9594
+ 'env.ANTHROPIC_DEFAULT_OPUS_MODEL'
9537
9595
  ]
9538
9596
  };
9539
9597
  if (proxyResult) {
@@ -9628,6 +9686,454 @@ function applyClaudeSettingsRaw(params = {}) {
9628
9686
  }
9629
9687
  }
9630
9688
 
9689
+ function getOpencodeConfigCandidates() {
9690
+ const candidates = [
9691
+ OPENCODE_CONFIG_ENV_FILE,
9692
+ OPENCODE_GLOBAL_JSONC_CONFIG_FILE,
9693
+ OPENCODE_GLOBAL_JSON_CONFIG_FILE,
9694
+ OPENCODE_LEGACY_CONFIG_FILE
9695
+ ]
9696
+ .filter(Boolean)
9697
+ .map(item => path.resolve(item));
9698
+ return [...new Set(candidates)];
9699
+ }
9700
+
9701
+ function resolveOpencodeConfigFile() {
9702
+ const candidates = getOpencodeConfigCandidates();
9703
+ for (const candidate of candidates) {
9704
+ if (fs.existsSync(candidate)) {
9705
+ return candidate;
9706
+ }
9707
+ }
9708
+ return OPENCODE_CONFIG_ENV_FILE || OPENCODE_GLOBAL_JSONC_CONFIG_FILE;
9709
+ }
9710
+
9711
+ function readOpencodeConfigObject(content) {
9712
+ const raw = typeof content === 'string' ? content : '';
9713
+ if (!raw.trim()) {
9714
+ return {};
9715
+ }
9716
+ const parsed = JSON5.parse(raw);
9717
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
9718
+ throw new Error('OpenCode config must be a JSON/JSONC object');
9719
+ }
9720
+ return parsed;
9721
+ }
9722
+
9723
+ function normalizeOpencodeAgentName(value) {
9724
+ const name = typeof value === 'string' ? value.trim() : '';
9725
+ return /^[a-zA-Z0-9_.-]+$/.test(name) ? name : '';
9726
+ }
9727
+
9728
+ function normalizeOpencodeProviderName(value) {
9729
+ const name = typeof value === 'string' ? value.trim().toLowerCase() : '';
9730
+ return /^[a-z0-9_.-]+$/.test(name) ? name : '';
9731
+ }
9732
+
9733
+ function splitOpencodeModelRef(modelRef) {
9734
+ const raw = typeof modelRef === 'string' ? modelRef.trim() : '';
9735
+ const slash = raw.indexOf('/');
9736
+ if (slash <= 0 || slash === raw.length - 1) {
9737
+ return { provider: '', model: raw };
9738
+ }
9739
+ return {
9740
+ provider: normalizeOpencodeProviderName(raw.slice(0, slash)),
9741
+ model: raw.slice(slash + 1).trim()
9742
+ };
9743
+ }
9744
+
9745
+ function joinOpencodeModelRef(providerName, model) {
9746
+ const provider = normalizeOpencodeProviderName(providerName);
9747
+ const modelName = typeof model === 'string' ? model.trim().replace(/^\/+/, '') : '';
9748
+ return provider && modelName ? `${provider}/${modelName}` : '';
9749
+ }
9750
+
9751
+ function getRecord(value) {
9752
+ return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
9753
+ }
9754
+
9755
+ function stableOpencodeJson(value) {
9756
+ if (Array.isArray(value)) {
9757
+ return `[${value.map(item => stableOpencodeJson(item)).join(',')}]`;
9758
+ }
9759
+ if (value && typeof value === 'object') {
9760
+ return `{${Object.keys(value).sort().map(key => `${JSON.stringify(key)}:${stableOpencodeJson(value[key])}`).join(',')}}`;
9761
+ }
9762
+ return JSON.stringify(value);
9763
+ }
9764
+
9765
+ function hashOpencodeManagedValue(value) {
9766
+ return crypto.createHash('sha256').update(stableOpencodeJson(value === undefined ? null : value)).digest('hex');
9767
+ }
9768
+
9769
+ function normalizeOpencodeProviderStore(raw) {
9770
+ const source = raw && typeof raw === 'object' && !Array.isArray(raw) ? raw : {};
9771
+ const providers = {};
9772
+ const sourceProviders = getRecord(source.providers);
9773
+ for (const [rawName, rawProvider] of Object.entries(sourceProviders)) {
9774
+ const name = normalizeOpencodeProviderName(rawName);
9775
+ const provider = getRecord(rawProvider);
9776
+ if (!name) continue;
9777
+ const apiKey = typeof provider.apiKey === 'string' ? provider.apiKey : '';
9778
+ const model = typeof provider.model === 'string' ? provider.model.trim() : '';
9779
+ const maxTokens = Number.isFinite(Number(provider.maxTokens)) && Number(provider.maxTokens) > 0
9780
+ ? Number(provider.maxTokens)
9781
+ : null;
9782
+ const reasoningEffort = typeof provider.reasoningEffort === 'string' ? provider.reasoningEffort.trim().toLowerCase() : '';
9783
+ providers[name] = {
9784
+ apiKey,
9785
+ model,
9786
+ disabled: provider.disabled === true,
9787
+ maxTokens,
9788
+ reasoningEffort: ['low', 'medium', 'high'].includes(reasoningEffort) ? reasoningEffort : '',
9789
+ updatedAt: typeof provider.updatedAt === 'string' ? provider.updatedAt : ''
9790
+ };
9791
+ }
9792
+ const rawLastApplied = getRecord(source.lastApplied);
9793
+ const lastAppliedProvider = normalizeOpencodeProviderName(rawLastApplied.provider);
9794
+ const lastApplied = lastAppliedProvider
9795
+ ? {
9796
+ provider: lastAppliedProvider,
9797
+ modelRef: typeof rawLastApplied.modelRef === 'string' ? rawLastApplied.modelRef : '',
9798
+ providerHash: typeof rawLastApplied.providerHash === 'string' ? rawLastApplied.providerHash : '',
9799
+ disabledProvidersHash: typeof rawLastApplied.disabledProvidersHash === 'string' ? rawLastApplied.disabledProvidersHash : '',
9800
+ providerCreatedByCodexMate: rawLastApplied.providerCreatedByCodexMate === true,
9801
+ disabledProviderAddedByCodexMate: rawLastApplied.disabledProviderAddedByCodexMate === true
9802
+ }
9803
+ : null;
9804
+ return {
9805
+ version: 1,
9806
+ providers,
9807
+ lastApplied
9808
+ };
9809
+ }
9810
+
9811
+ function readOpencodeProviderStore() {
9812
+ if (!fs.existsSync(CODEXMATE_OPENCODE_PROVIDER_STORE_FILE)) {
9813
+ return normalizeOpencodeProviderStore({});
9814
+ }
9815
+ try {
9816
+ const raw = JSON.parse(fs.readFileSync(CODEXMATE_OPENCODE_PROVIDER_STORE_FILE, 'utf-8') || '{}');
9817
+ return normalizeOpencodeProviderStore(raw);
9818
+ } catch (e) {
9819
+ return normalizeOpencodeProviderStore({});
9820
+ }
9821
+ }
9822
+
9823
+ function writeOpencodeProviderStore(store) {
9824
+ ensureDir(CODEXMATE_OPENCODE_DIR);
9825
+ writeJsonAtomic(CODEXMATE_OPENCODE_PROVIDER_STORE_FILE, normalizeOpencodeProviderStore(store));
9826
+ try {
9827
+ fs.chmodSync(CODEXMATE_OPENCODE_PROVIDER_STORE_FILE, 0o600);
9828
+ } catch (e) {}
9829
+ }
9830
+
9831
+ function summarizeOpencodeConfig(config = {}, targetPath = resolveOpencodeConfigFile(), exists = false, providerStore = readOpencodeProviderStore()) {
9832
+ const providers = getRecord(config.provider);
9833
+ const storedProviders = getRecord(providerStore.providers);
9834
+ const agents = getRecord(config.agent);
9835
+ const disabledProviders = Array.isArray(config.disabled_providers)
9836
+ ? config.disabled_providers.map(item => normalizeOpencodeProviderName(item)).filter(Boolean)
9837
+ : [];
9838
+ const topLevelModel = splitOpencodeModelRef(config.model);
9839
+ const agentEntries = Object.entries(agents)
9840
+ .filter(([, agent]) => agent && typeof agent === 'object' && !Array.isArray(agent))
9841
+ .map(([name, agent]) => {
9842
+ const modelRef = typeof agent.model === 'string' ? agent.model : '';
9843
+ const parsedModel = splitOpencodeModelRef(modelRef);
9844
+ const requestBody = getRecord(getRecord(agent.request).body);
9845
+ return {
9846
+ name,
9847
+ model: parsedModel.model || modelRef,
9848
+ modelRef,
9849
+ provider: parsedModel.provider,
9850
+ maxTokens: Number.isFinite(Number(requestBody.max_tokens)) ? Number(requestBody.max_tokens) : null,
9851
+ reasoningEffort: typeof requestBody.reasoning_effort === 'string' ? requestBody.reasoning_effort : ''
9852
+ };
9853
+ });
9854
+ const preferredAgentName = normalizeOpencodeAgentName(config.default_agent) || 'build';
9855
+ const primaryAgent = agentEntries.find(item => item.name === preferredAgentName)
9856
+ || agentEntries.find(item => item.name === 'build')
9857
+ || agentEntries[0]
9858
+ || null;
9859
+ const currentProvider = topLevelModel.provider || (primaryAgent && primaryAgent.provider) || '';
9860
+ const currentModel = topLevelModel.model || (primaryAgent && primaryAgent.model) || '';
9861
+ const providerNames = [...new Set([
9862
+ ...Object.keys(storedProviders),
9863
+ ...Object.keys(providers),
9864
+ currentProvider,
9865
+ ...(agentEntries.map(item => item.provider))
9866
+ ].map(item => normalizeOpencodeProviderName(item)).filter(Boolean))];
9867
+ return {
9868
+ exists: !!exists,
9869
+ targetPath,
9870
+ providerStorePath: CODEXMATE_OPENCODE_PROVIDER_STORE_FILE,
9871
+ providers: providerNames.map((name) => {
9872
+ const provider = getRecord(providers[name]);
9873
+ const storedProvider = getRecord(storedProviders[name]);
9874
+ const options = getRecord(provider.options);
9875
+ const apiKey = typeof options.apiKey === 'string' && options.apiKey.trim()
9876
+ ? options.apiKey
9877
+ : (typeof storedProvider.apiKey === 'string' ? storedProvider.apiKey : '');
9878
+ return {
9879
+ name,
9880
+ apiKey: maskKey(apiKey),
9881
+ hasKey: apiKey.trim().length > 0,
9882
+ disabled: disabledProviders.includes(name) || storedProvider.disabled === true,
9883
+ source: Object.prototype.hasOwnProperty.call(providers, name) ? 'opencode' : 'codexmate'
9884
+ };
9885
+ }),
9886
+ agents: agentEntries,
9887
+ currentAgent: primaryAgent ? primaryAgent.name : preferredAgentName,
9888
+ currentProvider,
9889
+ currentModel,
9890
+ currentModelRef: joinOpencodeModelRef(currentProvider, currentModel),
9891
+ autoCompact: getRecord(config.compaction).auto !== false,
9892
+ redacted: true
9893
+ };
9894
+ }
9895
+
9896
+ function readOpencodeConfigInfo() {
9897
+ const targetPath = resolveOpencodeConfigFile();
9898
+ if (!fs.existsSync(targetPath)) {
9899
+ const config = { $schema: 'https://opencode.ai/config.json' };
9900
+ return {
9901
+ ...summarizeOpencodeConfig(config, targetPath, false),
9902
+ content: JSON.stringify(config, null, 2) + '\n',
9903
+ candidates: getOpencodeConfigCandidates()
9904
+ };
9905
+ }
9906
+ try {
9907
+ const raw = fs.readFileSync(targetPath, 'utf-8');
9908
+ const config = readOpencodeConfigObject(raw);
9909
+ return {
9910
+ ...summarizeOpencodeConfig(config, targetPath, true),
9911
+ content: raw || '{}',
9912
+ candidates: getOpencodeConfigCandidates()
9913
+ };
9914
+ } catch (e) {
9915
+ return {
9916
+ error: e.message || '读取 OpenCode 配置失败',
9917
+ exists: true,
9918
+ targetPath,
9919
+ candidates: getOpencodeConfigCandidates()
9920
+ };
9921
+ }
9922
+ }
9923
+
9924
+ function applyOpencodeConfigRaw(params = {}) {
9925
+ assertToolConfigWriteAllowed('opencode');
9926
+ const content = typeof params.content === 'string' ? params.content : '';
9927
+ if (!content.trim()) {
9928
+ return { error: '内容不能为空' };
9929
+ }
9930
+ if (content.length > 1024 * 1024) {
9931
+ return { error: '内容过大(最大 1MB)' };
9932
+ }
9933
+ let parsed;
9934
+ try {
9935
+ parsed = readOpencodeConfigObject(content);
9936
+ } catch (e) {
9937
+ return { error: `OpenCode JSON/JSONC 解析失败: ${e.message}` };
9938
+ }
9939
+ const targetPath = resolveOpencodeConfigFile();
9940
+ try {
9941
+ ensureDir(path.dirname(targetPath));
9942
+ backupFileIfNeededOnce(targetPath);
9943
+ fs.writeFileSync(targetPath, JSON.stringify(parsed, null, 2) + '\n', { encoding: 'utf-8', mode: 0o600 });
9944
+ return {
9945
+ success: true,
9946
+ targetPath,
9947
+ content: JSON.stringify(parsed, null, 2) + '\n',
9948
+ ...summarizeOpencodeConfig(parsed, targetPath, true)
9949
+ };
9950
+ } catch (e) {
9951
+ return { error: e.message || '写入 OpenCode 配置失败' };
9952
+ }
9953
+ }
9954
+
9955
+ function removePreviousCodexMateOpencodeProjection(config, lastApplied, nextProviderName) {
9956
+ const previousProvider = normalizeOpencodeProviderName(lastApplied && lastApplied.provider);
9957
+ const nextProvider = normalizeOpencodeProviderName(nextProviderName);
9958
+ if (!previousProvider || previousProvider === nextProvider) return;
9959
+
9960
+ const providers = getRecord(config.provider);
9961
+ if (lastApplied.providerCreatedByCodexMate === true && providers[previousProvider] && lastApplied.providerHash) {
9962
+ const currentHash = hashOpencodeManagedValue(providers[previousProvider]);
9963
+ if (currentHash === lastApplied.providerHash) {
9964
+ delete providers[previousProvider];
9965
+ }
9966
+ }
9967
+ if (Object.keys(providers).length) {
9968
+ config.provider = providers;
9969
+ } else {
9970
+ delete config.provider;
9971
+ }
9972
+
9973
+ if (lastApplied.disabledProviderAddedByCodexMate === true && Array.isArray(config.disabled_providers) && lastApplied.disabledProvidersHash) {
9974
+ const currentDisabled = config.disabled_providers.map(item => normalizeOpencodeProviderName(item)).filter(Boolean).sort();
9975
+ if (hashOpencodeManagedValue(currentDisabled) === lastApplied.disabledProvidersHash) {
9976
+ const nextDisabled = currentDisabled.filter(item => item !== previousProvider);
9977
+ if (nextDisabled.length) {
9978
+ config.disabled_providers = nextDisabled;
9979
+ } else {
9980
+ delete config.disabled_providers;
9981
+ }
9982
+ }
9983
+ }
9984
+ }
9985
+
9986
+ function updateOpencodeSelection(params = {}) {
9987
+ assertToolConfigWriteAllowed('opencode');
9988
+ const providerName = normalizeOpencodeProviderName(params.provider);
9989
+ const model = typeof params.model === 'string' ? params.model.trim() : '';
9990
+ const agentName = normalizeOpencodeAgentName(params.agent || 'build') || 'build';
9991
+ if (!providerName) {
9992
+ return { error: '请选择 OpenCode provider' };
9993
+ }
9994
+ if (!model) {
9995
+ return { error: '请选择或输入 OpenCode model' };
9996
+ }
9997
+
9998
+ const targetPath = resolveOpencodeConfigFile();
9999
+ let config = {};
10000
+ if (fs.existsSync(targetPath)) {
10001
+ try {
10002
+ config = readOpencodeConfigObject(fs.readFileSync(targetPath, 'utf-8'));
10003
+ } catch (e) {
10004
+ return { error: `OpenCode JSON/JSONC 解析失败: ${e.message}` };
10005
+ }
10006
+ }
10007
+ if (!config || typeof config !== 'object' || Array.isArray(config)) {
10008
+ config = {};
10009
+ }
10010
+
10011
+ const providerStore = readOpencodeProviderStore();
10012
+ removePreviousCodexMateOpencodeProjection(config, providerStore.lastApplied, providerName);
10013
+ const providerExistedBeforeApply = !!(getRecord(config.provider)[providerName]);
10014
+ const disabledContainedBeforeApply = Array.isArray(config.disabled_providers)
10015
+ && config.disabled_providers.map(item => normalizeOpencodeProviderName(item)).filter(Boolean).includes(providerName);
10016
+
10017
+ if (!config.$schema) {
10018
+ config.$schema = 'https://opencode.ai/config.json';
10019
+ }
10020
+ const modelRef = joinOpencodeModelRef(providerName, model);
10021
+ config.model = modelRef;
10022
+
10023
+ if (!config.provider || typeof config.provider !== 'object' || Array.isArray(config.provider)) {
10024
+ config.provider = {};
10025
+ }
10026
+ const storedProvider = getRecord(providerStore.providers[providerName]);
10027
+ const previousProvider = getRecord(config.provider[providerName]);
10028
+ const previousOptions = getRecord(previousProvider.options);
10029
+ const explicitApiKey = typeof params.apiKey === 'string' ? params.apiKey.trim() : '';
10030
+ const storedApiKey = typeof storedProvider.apiKey === 'string' ? storedProvider.apiKey.trim() : '';
10031
+ const apiKey = explicitApiKey || storedApiKey;
10032
+ config.provider[providerName] = {
10033
+ ...previousProvider,
10034
+ options: {
10035
+ ...previousOptions
10036
+ }
10037
+ };
10038
+ if (apiKey) {
10039
+ config.provider[providerName].options.apiKey = apiKey;
10040
+ }
10041
+ if (Object.keys(config.provider[providerName].options).length === 0) {
10042
+ delete config.provider[providerName].options;
10043
+ }
10044
+
10045
+ const disabledSet = new Set(Array.isArray(config.disabled_providers)
10046
+ ? config.disabled_providers.map(item => normalizeOpencodeProviderName(item)).filter(Boolean)
10047
+ : []);
10048
+ if (params.disabled === true) {
10049
+ disabledSet.add(providerName);
10050
+ } else {
10051
+ disabledSet.delete(providerName);
10052
+ }
10053
+ if (disabledSet.size) {
10054
+ config.disabled_providers = [...disabledSet].sort();
10055
+ } else {
10056
+ delete config.disabled_providers;
10057
+ }
10058
+
10059
+ if (!config.agent || typeof config.agent !== 'object' || Array.isArray(config.agent)) {
10060
+ config.agent = {};
10061
+ }
10062
+ const coreAgents = params.applyToCoreAgents === true
10063
+ ? ['build', 'plan', 'general', 'title', 'summary', 'compaction']
10064
+ : [agentName];
10065
+ for (const name of coreAgents) {
10066
+ const previousAgent = getRecord(config.agent[name]);
10067
+ const previousRequest = getRecord(previousAgent.request);
10068
+ const previousBody = getRecord(previousRequest.body);
10069
+ const nextAgent = {
10070
+ ...previousAgent,
10071
+ model: modelRef
10072
+ };
10073
+ const requestBody = { ...previousBody };
10074
+ const maxTokens = normalizePositiveIntegerParam(params.maxTokens);
10075
+ if (maxTokens !== null) {
10076
+ requestBody.max_tokens = maxTokens;
10077
+ }
10078
+ const effort = typeof params.reasoningEffort === 'string' ? params.reasoningEffort.trim().toLowerCase() : '';
10079
+ if (effort === 'low' || effort === 'medium' || effort === 'high') {
10080
+ requestBody.reasoning_effort = effort;
10081
+ }
10082
+ if (Object.keys(requestBody).length) {
10083
+ nextAgent.request = {
10084
+ ...previousRequest,
10085
+ body: requestBody
10086
+ };
10087
+ }
10088
+ config.agent[name] = nextAgent;
10089
+ }
10090
+ if (!config.default_agent) {
10091
+ config.default_agent = agentName;
10092
+ }
10093
+ if (!config.compaction || typeof config.compaction !== 'object' || Array.isArray(config.compaction)) {
10094
+ config.compaction = {};
10095
+ }
10096
+ config.compaction.auto = params.autoCompact !== false;
10097
+
10098
+ const maxTokens = normalizePositiveIntegerParam(params.maxTokens);
10099
+ const effort = typeof params.reasoningEffort === 'string' ? params.reasoningEffort.trim().toLowerCase() : '';
10100
+ providerStore.providers[providerName] = {
10101
+ ...storedProvider,
10102
+ apiKey: apiKey || '',
10103
+ model,
10104
+ disabled: params.disabled === true,
10105
+ maxTokens,
10106
+ reasoningEffort: ['low', 'medium', 'high'].includes(effort) ? effort : '',
10107
+ updatedAt: new Date().toISOString()
10108
+ };
10109
+
10110
+ try {
10111
+ ensureDir(path.dirname(targetPath));
10112
+ backupFileIfNeededOnce(targetPath);
10113
+ const content = JSON.stringify(config, null, 2) + '\n';
10114
+ fs.writeFileSync(targetPath, content, { encoding: 'utf-8', mode: 0o600 });
10115
+ const disabledProviders = Array.isArray(config.disabled_providers)
10116
+ ? config.disabled_providers.map(item => normalizeOpencodeProviderName(item)).filter(Boolean).sort()
10117
+ : [];
10118
+ providerStore.lastApplied = {
10119
+ provider: providerName,
10120
+ modelRef,
10121
+ providerHash: hashOpencodeManagedValue(getRecord(config.provider)[providerName]),
10122
+ disabledProvidersHash: hashOpencodeManagedValue(disabledProviders),
10123
+ providerCreatedByCodexMate: providerExistedBeforeApply !== true,
10124
+ disabledProviderAddedByCodexMate: params.disabled === true && disabledContainedBeforeApply !== true
10125
+ };
10126
+ writeOpencodeProviderStore(providerStore);
10127
+ return {
10128
+ success: true,
10129
+ targetPath,
10130
+ ...summarizeOpencodeConfig(config, targetPath, true, providerStore),
10131
+ content
10132
+ };
10133
+ } catch (e) {
10134
+ return { error: e.message || '写入 OpenCode 配置失败' };
10135
+ }
10136
+ }
9631
10137
  // API: 打包 Claude 配置目录(系统 zip 可用则使用,否则回退 zip-lib)
9632
10138
  async function prepareClaudeDirDownload() {
9633
10139
  return await prepareDirectoryDownload(CLAUDE_DIR, {
@@ -11449,9 +11955,21 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
11449
11955
  case 'get-claude-settings-raw':
11450
11956
  result = readClaudeSettingsRaw();
11451
11957
  break;
11958
+ case 'preview-claude-settings-diff':
11959
+ result = buildClaudeSettingsDiff(params || {});
11960
+ break;
11452
11961
  case 'apply-claude-settings-raw':
11453
11962
  result = applyClaudeSettingsRaw(params || {});
11454
11963
  break;
11964
+ case 'get-opencode-config':
11965
+ result = readOpencodeConfigInfo();
11966
+ break;
11967
+ case 'apply-opencode-config':
11968
+ result = applyOpencodeConfigRaw(params || {});
11969
+ break;
11970
+ case 'update-opencode-selection':
11971
+ result = updateOpencodeSelection(params || {});
11972
+ break;
11455
11973
  case 'apply-claude-config':
11456
11974
  result = await applyToClaudeSettings(params.config);
11457
11975
  if (result && !result.error) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codexmate",
3
- "version": "0.0.45",
3
+ "version": "0.0.48",
4
4
  "description": "Codex/Claude Code/OpenClaw 配置、会话与任务编排 CLI + Web 工具",
5
5
  "main": "cli.js",
6
6
  "bin": {
@@ -72,6 +72,7 @@
72
72
  "license": "Apache-2.0",
73
73
  "devDependencies": {
74
74
  "@vue/compiler-dom": "^3.5.30",
75
+ "opencc-js": "^1.3.1",
75
76
  "vitepress": "^1.6.4"
76
77
  }
77
78
  }
package/web-ui/app.js CHANGED
@@ -383,14 +383,15 @@ document.addEventListener('DOMContentLoaded', () => {
383
383
  if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
384
384
  return {
385
385
  codex: parsed.codex === true,
386
- claude: parsed.claude === true
386
+ claude: parsed.claude === true,
387
+ opencode: parsed.opencode === true
387
388
  };
388
389
  }
389
390
  }
390
391
  } catch (_) {}
391
- return { codex: false, claude: false };
392
+ return { codex: false, claude: false, opencode: false };
392
393
  })(),
393
- toolConfigPermissionSaving: { codex: false, claude: false },
394
+ toolConfigPermissionSaving: { codex: false, claude: false, opencode: false },
394
395
  sessionTrashEnabled: true,
395
396
  sessionTrashItems: [],
396
397
  sessionTrashVisibleCount: SESSION_TRASH_PAGE_SIZE,
@@ -411,6 +412,28 @@ document.addEventListener('DOMContentLoaded', () => {
411
412
  claudeImportLoading: false,
412
413
  codexImportLoading: false,
413
414
  codexAuthProfiles: [],
415
+ opencodeConfigPath: '',
416
+ opencodeProviderStorePath: '',
417
+ opencodeConfigExists: false,
418
+ opencodeContent: '{}',
419
+ opencodeLoading: false,
420
+ opencodeSaving: false,
421
+ opencodeApplying: false,
422
+ opencodeError: '',
423
+ opencodeImportError: '',
424
+ opencodeImportFileName: '',
425
+ opencodeProviders: [],
426
+ opencodeAgents: [],
427
+ opencodeProvider: 'anthropic',
428
+ opencodeModel: '',
429
+ opencodeApiKey: '',
430
+ opencodeShowKey: false,
431
+ opencodeProviderDisabled: false,
432
+ opencodeAgent: 'build',
433
+ opencodeApplyToCoreAgents: true,
434
+ opencodeAutoCompact: true,
435
+ opencodeMaxTokens: '',
436
+ opencodeReasoningEffort: '',
414
437
  forceCompactLayout: false,
415
438
  taskOrchestrationTabEnabled: true,
416
439
  taskOrchestration: {
@@ -522,7 +545,7 @@ document.addEventListener('DOMContentLoaded', () => {
522
545
  if (nextConfigMode && typeof this.switchConfigMode === 'function') {
523
546
  this.__navStateRestoring = true;
524
547
  try {
525
- if (nextConfigMode === 'codex' || nextConfigMode === 'claude' || nextConfigMode === 'openclaw') {
548
+ if (nextConfigMode === 'codex' || nextConfigMode === 'claude' || nextConfigMode === 'openclaw' || nextConfigMode === 'opencode') {
526
549
  this.configMode = nextConfigMode;
527
550
  }
528
551
  if (resolvedMainTab && mainTabSet.has(resolvedMainTab) && resolvedMainTab !== this.mainTab) {
package/web-ui/index.html CHANGED
@@ -16,6 +16,7 @@
16
16
  <!-- @include ./partials/index/panel-config-codex.html -->
17
17
  <!-- @include ./partials/index/panel-config-claude.html -->
18
18
  <!-- @include ./partials/index/panel-config-openclaw.html -->
19
+ <!-- @include ./partials/index/panel-config-opencode.html -->
19
20
  <!-- @include ./partials/index/panel-sessions.html -->
20
21
  <!-- @include ./partials/index/panel-usage.html -->
21
22
  <!-- @include ./partials/index/panel-orchestration.html -->