codexmate 0.0.45 → 0.0.47

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
 
@@ -9628,6 +9641,454 @@ function applyClaudeSettingsRaw(params = {}) {
9628
9641
  }
9629
9642
  }
9630
9643
 
9644
+ function getOpencodeConfigCandidates() {
9645
+ const candidates = [
9646
+ OPENCODE_CONFIG_ENV_FILE,
9647
+ OPENCODE_GLOBAL_JSONC_CONFIG_FILE,
9648
+ OPENCODE_GLOBAL_JSON_CONFIG_FILE,
9649
+ OPENCODE_LEGACY_CONFIG_FILE
9650
+ ]
9651
+ .filter(Boolean)
9652
+ .map(item => path.resolve(item));
9653
+ return [...new Set(candidates)];
9654
+ }
9655
+
9656
+ function resolveOpencodeConfigFile() {
9657
+ const candidates = getOpencodeConfigCandidates();
9658
+ for (const candidate of candidates) {
9659
+ if (fs.existsSync(candidate)) {
9660
+ return candidate;
9661
+ }
9662
+ }
9663
+ return OPENCODE_CONFIG_ENV_FILE || OPENCODE_GLOBAL_JSONC_CONFIG_FILE;
9664
+ }
9665
+
9666
+ function readOpencodeConfigObject(content) {
9667
+ const raw = typeof content === 'string' ? content : '';
9668
+ if (!raw.trim()) {
9669
+ return {};
9670
+ }
9671
+ const parsed = JSON5.parse(raw);
9672
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
9673
+ throw new Error('OpenCode config must be a JSON/JSONC object');
9674
+ }
9675
+ return parsed;
9676
+ }
9677
+
9678
+ function normalizeOpencodeAgentName(value) {
9679
+ const name = typeof value === 'string' ? value.trim() : '';
9680
+ return /^[a-zA-Z0-9_.-]+$/.test(name) ? name : '';
9681
+ }
9682
+
9683
+ function normalizeOpencodeProviderName(value) {
9684
+ const name = typeof value === 'string' ? value.trim().toLowerCase() : '';
9685
+ return /^[a-z0-9_.-]+$/.test(name) ? name : '';
9686
+ }
9687
+
9688
+ function splitOpencodeModelRef(modelRef) {
9689
+ const raw = typeof modelRef === 'string' ? modelRef.trim() : '';
9690
+ const slash = raw.indexOf('/');
9691
+ if (slash <= 0 || slash === raw.length - 1) {
9692
+ return { provider: '', model: raw };
9693
+ }
9694
+ return {
9695
+ provider: normalizeOpencodeProviderName(raw.slice(0, slash)),
9696
+ model: raw.slice(slash + 1).trim()
9697
+ };
9698
+ }
9699
+
9700
+ function joinOpencodeModelRef(providerName, model) {
9701
+ const provider = normalizeOpencodeProviderName(providerName);
9702
+ const modelName = typeof model === 'string' ? model.trim().replace(/^\/+/, '') : '';
9703
+ return provider && modelName ? `${provider}/${modelName}` : '';
9704
+ }
9705
+
9706
+ function getRecord(value) {
9707
+ return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
9708
+ }
9709
+
9710
+ function stableOpencodeJson(value) {
9711
+ if (Array.isArray(value)) {
9712
+ return `[${value.map(item => stableOpencodeJson(item)).join(',')}]`;
9713
+ }
9714
+ if (value && typeof value === 'object') {
9715
+ return `{${Object.keys(value).sort().map(key => `${JSON.stringify(key)}:${stableOpencodeJson(value[key])}`).join(',')}}`;
9716
+ }
9717
+ return JSON.stringify(value);
9718
+ }
9719
+
9720
+ function hashOpencodeManagedValue(value) {
9721
+ return crypto.createHash('sha256').update(stableOpencodeJson(value === undefined ? null : value)).digest('hex');
9722
+ }
9723
+
9724
+ function normalizeOpencodeProviderStore(raw) {
9725
+ const source = raw && typeof raw === 'object' && !Array.isArray(raw) ? raw : {};
9726
+ const providers = {};
9727
+ const sourceProviders = getRecord(source.providers);
9728
+ for (const [rawName, rawProvider] of Object.entries(sourceProviders)) {
9729
+ const name = normalizeOpencodeProviderName(rawName);
9730
+ const provider = getRecord(rawProvider);
9731
+ if (!name) continue;
9732
+ const apiKey = typeof provider.apiKey === 'string' ? provider.apiKey : '';
9733
+ const model = typeof provider.model === 'string' ? provider.model.trim() : '';
9734
+ const maxTokens = Number.isFinite(Number(provider.maxTokens)) && Number(provider.maxTokens) > 0
9735
+ ? Number(provider.maxTokens)
9736
+ : null;
9737
+ const reasoningEffort = typeof provider.reasoningEffort === 'string' ? provider.reasoningEffort.trim().toLowerCase() : '';
9738
+ providers[name] = {
9739
+ apiKey,
9740
+ model,
9741
+ disabled: provider.disabled === true,
9742
+ maxTokens,
9743
+ reasoningEffort: ['low', 'medium', 'high'].includes(reasoningEffort) ? reasoningEffort : '',
9744
+ updatedAt: typeof provider.updatedAt === 'string' ? provider.updatedAt : ''
9745
+ };
9746
+ }
9747
+ const rawLastApplied = getRecord(source.lastApplied);
9748
+ const lastAppliedProvider = normalizeOpencodeProviderName(rawLastApplied.provider);
9749
+ const lastApplied = lastAppliedProvider
9750
+ ? {
9751
+ provider: lastAppliedProvider,
9752
+ modelRef: typeof rawLastApplied.modelRef === 'string' ? rawLastApplied.modelRef : '',
9753
+ providerHash: typeof rawLastApplied.providerHash === 'string' ? rawLastApplied.providerHash : '',
9754
+ disabledProvidersHash: typeof rawLastApplied.disabledProvidersHash === 'string' ? rawLastApplied.disabledProvidersHash : '',
9755
+ providerCreatedByCodexMate: rawLastApplied.providerCreatedByCodexMate === true,
9756
+ disabledProviderAddedByCodexMate: rawLastApplied.disabledProviderAddedByCodexMate === true
9757
+ }
9758
+ : null;
9759
+ return {
9760
+ version: 1,
9761
+ providers,
9762
+ lastApplied
9763
+ };
9764
+ }
9765
+
9766
+ function readOpencodeProviderStore() {
9767
+ if (!fs.existsSync(CODEXMATE_OPENCODE_PROVIDER_STORE_FILE)) {
9768
+ return normalizeOpencodeProviderStore({});
9769
+ }
9770
+ try {
9771
+ const raw = JSON.parse(fs.readFileSync(CODEXMATE_OPENCODE_PROVIDER_STORE_FILE, 'utf-8') || '{}');
9772
+ return normalizeOpencodeProviderStore(raw);
9773
+ } catch (e) {
9774
+ return normalizeOpencodeProviderStore({});
9775
+ }
9776
+ }
9777
+
9778
+ function writeOpencodeProviderStore(store) {
9779
+ ensureDir(CODEXMATE_OPENCODE_DIR);
9780
+ writeJsonAtomic(CODEXMATE_OPENCODE_PROVIDER_STORE_FILE, normalizeOpencodeProviderStore(store));
9781
+ try {
9782
+ fs.chmodSync(CODEXMATE_OPENCODE_PROVIDER_STORE_FILE, 0o600);
9783
+ } catch (e) {}
9784
+ }
9785
+
9786
+ function summarizeOpencodeConfig(config = {}, targetPath = resolveOpencodeConfigFile(), exists = false, providerStore = readOpencodeProviderStore()) {
9787
+ const providers = getRecord(config.provider);
9788
+ const storedProviders = getRecord(providerStore.providers);
9789
+ const agents = getRecord(config.agent);
9790
+ const disabledProviders = Array.isArray(config.disabled_providers)
9791
+ ? config.disabled_providers.map(item => normalizeOpencodeProviderName(item)).filter(Boolean)
9792
+ : [];
9793
+ const topLevelModel = splitOpencodeModelRef(config.model);
9794
+ const agentEntries = Object.entries(agents)
9795
+ .filter(([, agent]) => agent && typeof agent === 'object' && !Array.isArray(agent))
9796
+ .map(([name, agent]) => {
9797
+ const modelRef = typeof agent.model === 'string' ? agent.model : '';
9798
+ const parsedModel = splitOpencodeModelRef(modelRef);
9799
+ const requestBody = getRecord(getRecord(agent.request).body);
9800
+ return {
9801
+ name,
9802
+ model: parsedModel.model || modelRef,
9803
+ modelRef,
9804
+ provider: parsedModel.provider,
9805
+ maxTokens: Number.isFinite(Number(requestBody.max_tokens)) ? Number(requestBody.max_tokens) : null,
9806
+ reasoningEffort: typeof requestBody.reasoning_effort === 'string' ? requestBody.reasoning_effort : ''
9807
+ };
9808
+ });
9809
+ const preferredAgentName = normalizeOpencodeAgentName(config.default_agent) || 'build';
9810
+ const primaryAgent = agentEntries.find(item => item.name === preferredAgentName)
9811
+ || agentEntries.find(item => item.name === 'build')
9812
+ || agentEntries[0]
9813
+ || null;
9814
+ const currentProvider = topLevelModel.provider || (primaryAgent && primaryAgent.provider) || '';
9815
+ const currentModel = topLevelModel.model || (primaryAgent && primaryAgent.model) || '';
9816
+ const providerNames = [...new Set([
9817
+ ...Object.keys(storedProviders),
9818
+ ...Object.keys(providers),
9819
+ currentProvider,
9820
+ ...(agentEntries.map(item => item.provider))
9821
+ ].map(item => normalizeOpencodeProviderName(item)).filter(Boolean))];
9822
+ return {
9823
+ exists: !!exists,
9824
+ targetPath,
9825
+ providerStorePath: CODEXMATE_OPENCODE_PROVIDER_STORE_FILE,
9826
+ providers: providerNames.map((name) => {
9827
+ const provider = getRecord(providers[name]);
9828
+ const storedProvider = getRecord(storedProviders[name]);
9829
+ const options = getRecord(provider.options);
9830
+ const apiKey = typeof options.apiKey === 'string' && options.apiKey.trim()
9831
+ ? options.apiKey
9832
+ : (typeof storedProvider.apiKey === 'string' ? storedProvider.apiKey : '');
9833
+ return {
9834
+ name,
9835
+ apiKey: maskKey(apiKey),
9836
+ hasKey: apiKey.trim().length > 0,
9837
+ disabled: disabledProviders.includes(name) || storedProvider.disabled === true,
9838
+ source: Object.prototype.hasOwnProperty.call(providers, name) ? 'opencode' : 'codexmate'
9839
+ };
9840
+ }),
9841
+ agents: agentEntries,
9842
+ currentAgent: primaryAgent ? primaryAgent.name : preferredAgentName,
9843
+ currentProvider,
9844
+ currentModel,
9845
+ currentModelRef: joinOpencodeModelRef(currentProvider, currentModel),
9846
+ autoCompact: getRecord(config.compaction).auto !== false,
9847
+ redacted: true
9848
+ };
9849
+ }
9850
+
9851
+ function readOpencodeConfigInfo() {
9852
+ const targetPath = resolveOpencodeConfigFile();
9853
+ if (!fs.existsSync(targetPath)) {
9854
+ const config = { $schema: 'https://opencode.ai/config.json' };
9855
+ return {
9856
+ ...summarizeOpencodeConfig(config, targetPath, false),
9857
+ content: JSON.stringify(config, null, 2) + '\n',
9858
+ candidates: getOpencodeConfigCandidates()
9859
+ };
9860
+ }
9861
+ try {
9862
+ const raw = fs.readFileSync(targetPath, 'utf-8');
9863
+ const config = readOpencodeConfigObject(raw);
9864
+ return {
9865
+ ...summarizeOpencodeConfig(config, targetPath, true),
9866
+ content: raw || '{}',
9867
+ candidates: getOpencodeConfigCandidates()
9868
+ };
9869
+ } catch (e) {
9870
+ return {
9871
+ error: e.message || '读取 OpenCode 配置失败',
9872
+ exists: true,
9873
+ targetPath,
9874
+ candidates: getOpencodeConfigCandidates()
9875
+ };
9876
+ }
9877
+ }
9878
+
9879
+ function applyOpencodeConfigRaw(params = {}) {
9880
+ assertToolConfigWriteAllowed('opencode');
9881
+ const content = typeof params.content === 'string' ? params.content : '';
9882
+ if (!content.trim()) {
9883
+ return { error: '内容不能为空' };
9884
+ }
9885
+ if (content.length > 1024 * 1024) {
9886
+ return { error: '内容过大(最大 1MB)' };
9887
+ }
9888
+ let parsed;
9889
+ try {
9890
+ parsed = readOpencodeConfigObject(content);
9891
+ } catch (e) {
9892
+ return { error: `OpenCode JSON/JSONC 解析失败: ${e.message}` };
9893
+ }
9894
+ const targetPath = resolveOpencodeConfigFile();
9895
+ try {
9896
+ ensureDir(path.dirname(targetPath));
9897
+ backupFileIfNeededOnce(targetPath);
9898
+ fs.writeFileSync(targetPath, JSON.stringify(parsed, null, 2) + '\n', { encoding: 'utf-8', mode: 0o600 });
9899
+ return {
9900
+ success: true,
9901
+ targetPath,
9902
+ content: JSON.stringify(parsed, null, 2) + '\n',
9903
+ ...summarizeOpencodeConfig(parsed, targetPath, true)
9904
+ };
9905
+ } catch (e) {
9906
+ return { error: e.message || '写入 OpenCode 配置失败' };
9907
+ }
9908
+ }
9909
+
9910
+ function removePreviousCodexMateOpencodeProjection(config, lastApplied, nextProviderName) {
9911
+ const previousProvider = normalizeOpencodeProviderName(lastApplied && lastApplied.provider);
9912
+ const nextProvider = normalizeOpencodeProviderName(nextProviderName);
9913
+ if (!previousProvider || previousProvider === nextProvider) return;
9914
+
9915
+ const providers = getRecord(config.provider);
9916
+ if (lastApplied.providerCreatedByCodexMate === true && providers[previousProvider] && lastApplied.providerHash) {
9917
+ const currentHash = hashOpencodeManagedValue(providers[previousProvider]);
9918
+ if (currentHash === lastApplied.providerHash) {
9919
+ delete providers[previousProvider];
9920
+ }
9921
+ }
9922
+ if (Object.keys(providers).length) {
9923
+ config.provider = providers;
9924
+ } else {
9925
+ delete config.provider;
9926
+ }
9927
+
9928
+ if (lastApplied.disabledProviderAddedByCodexMate === true && Array.isArray(config.disabled_providers) && lastApplied.disabledProvidersHash) {
9929
+ const currentDisabled = config.disabled_providers.map(item => normalizeOpencodeProviderName(item)).filter(Boolean).sort();
9930
+ if (hashOpencodeManagedValue(currentDisabled) === lastApplied.disabledProvidersHash) {
9931
+ const nextDisabled = currentDisabled.filter(item => item !== previousProvider);
9932
+ if (nextDisabled.length) {
9933
+ config.disabled_providers = nextDisabled;
9934
+ } else {
9935
+ delete config.disabled_providers;
9936
+ }
9937
+ }
9938
+ }
9939
+ }
9940
+
9941
+ function updateOpencodeSelection(params = {}) {
9942
+ assertToolConfigWriteAllowed('opencode');
9943
+ const providerName = normalizeOpencodeProviderName(params.provider);
9944
+ const model = typeof params.model === 'string' ? params.model.trim() : '';
9945
+ const agentName = normalizeOpencodeAgentName(params.agent || 'build') || 'build';
9946
+ if (!providerName) {
9947
+ return { error: '请选择 OpenCode provider' };
9948
+ }
9949
+ if (!model) {
9950
+ return { error: '请选择或输入 OpenCode model' };
9951
+ }
9952
+
9953
+ const targetPath = resolveOpencodeConfigFile();
9954
+ let config = {};
9955
+ if (fs.existsSync(targetPath)) {
9956
+ try {
9957
+ config = readOpencodeConfigObject(fs.readFileSync(targetPath, 'utf-8'));
9958
+ } catch (e) {
9959
+ return { error: `OpenCode JSON/JSONC 解析失败: ${e.message}` };
9960
+ }
9961
+ }
9962
+ if (!config || typeof config !== 'object' || Array.isArray(config)) {
9963
+ config = {};
9964
+ }
9965
+
9966
+ const providerStore = readOpencodeProviderStore();
9967
+ removePreviousCodexMateOpencodeProjection(config, providerStore.lastApplied, providerName);
9968
+ const providerExistedBeforeApply = !!(getRecord(config.provider)[providerName]);
9969
+ const disabledContainedBeforeApply = Array.isArray(config.disabled_providers)
9970
+ && config.disabled_providers.map(item => normalizeOpencodeProviderName(item)).filter(Boolean).includes(providerName);
9971
+
9972
+ if (!config.$schema) {
9973
+ config.$schema = 'https://opencode.ai/config.json';
9974
+ }
9975
+ const modelRef = joinOpencodeModelRef(providerName, model);
9976
+ config.model = modelRef;
9977
+
9978
+ if (!config.provider || typeof config.provider !== 'object' || Array.isArray(config.provider)) {
9979
+ config.provider = {};
9980
+ }
9981
+ const storedProvider = getRecord(providerStore.providers[providerName]);
9982
+ const previousProvider = getRecord(config.provider[providerName]);
9983
+ const previousOptions = getRecord(previousProvider.options);
9984
+ const explicitApiKey = typeof params.apiKey === 'string' ? params.apiKey.trim() : '';
9985
+ const storedApiKey = typeof storedProvider.apiKey === 'string' ? storedProvider.apiKey.trim() : '';
9986
+ const apiKey = explicitApiKey || storedApiKey;
9987
+ config.provider[providerName] = {
9988
+ ...previousProvider,
9989
+ options: {
9990
+ ...previousOptions
9991
+ }
9992
+ };
9993
+ if (apiKey) {
9994
+ config.provider[providerName].options.apiKey = apiKey;
9995
+ }
9996
+ if (Object.keys(config.provider[providerName].options).length === 0) {
9997
+ delete config.provider[providerName].options;
9998
+ }
9999
+
10000
+ const disabledSet = new Set(Array.isArray(config.disabled_providers)
10001
+ ? config.disabled_providers.map(item => normalizeOpencodeProviderName(item)).filter(Boolean)
10002
+ : []);
10003
+ if (params.disabled === true) {
10004
+ disabledSet.add(providerName);
10005
+ } else {
10006
+ disabledSet.delete(providerName);
10007
+ }
10008
+ if (disabledSet.size) {
10009
+ config.disabled_providers = [...disabledSet].sort();
10010
+ } else {
10011
+ delete config.disabled_providers;
10012
+ }
10013
+
10014
+ if (!config.agent || typeof config.agent !== 'object' || Array.isArray(config.agent)) {
10015
+ config.agent = {};
10016
+ }
10017
+ const coreAgents = params.applyToCoreAgents === true
10018
+ ? ['build', 'plan', 'general', 'title', 'summary', 'compaction']
10019
+ : [agentName];
10020
+ for (const name of coreAgents) {
10021
+ const previousAgent = getRecord(config.agent[name]);
10022
+ const previousRequest = getRecord(previousAgent.request);
10023
+ const previousBody = getRecord(previousRequest.body);
10024
+ const nextAgent = {
10025
+ ...previousAgent,
10026
+ model: modelRef
10027
+ };
10028
+ const requestBody = { ...previousBody };
10029
+ const maxTokens = normalizePositiveIntegerParam(params.maxTokens);
10030
+ if (maxTokens !== null) {
10031
+ requestBody.max_tokens = maxTokens;
10032
+ }
10033
+ const effort = typeof params.reasoningEffort === 'string' ? params.reasoningEffort.trim().toLowerCase() : '';
10034
+ if (effort === 'low' || effort === 'medium' || effort === 'high') {
10035
+ requestBody.reasoning_effort = effort;
10036
+ }
10037
+ if (Object.keys(requestBody).length) {
10038
+ nextAgent.request = {
10039
+ ...previousRequest,
10040
+ body: requestBody
10041
+ };
10042
+ }
10043
+ config.agent[name] = nextAgent;
10044
+ }
10045
+ if (!config.default_agent) {
10046
+ config.default_agent = agentName;
10047
+ }
10048
+ if (!config.compaction || typeof config.compaction !== 'object' || Array.isArray(config.compaction)) {
10049
+ config.compaction = {};
10050
+ }
10051
+ config.compaction.auto = params.autoCompact !== false;
10052
+
10053
+ const maxTokens = normalizePositiveIntegerParam(params.maxTokens);
10054
+ const effort = typeof params.reasoningEffort === 'string' ? params.reasoningEffort.trim().toLowerCase() : '';
10055
+ providerStore.providers[providerName] = {
10056
+ ...storedProvider,
10057
+ apiKey: apiKey || '',
10058
+ model,
10059
+ disabled: params.disabled === true,
10060
+ maxTokens,
10061
+ reasoningEffort: ['low', 'medium', 'high'].includes(effort) ? effort : '',
10062
+ updatedAt: new Date().toISOString()
10063
+ };
10064
+
10065
+ try {
10066
+ ensureDir(path.dirname(targetPath));
10067
+ backupFileIfNeededOnce(targetPath);
10068
+ const content = JSON.stringify(config, null, 2) + '\n';
10069
+ fs.writeFileSync(targetPath, content, { encoding: 'utf-8', mode: 0o600 });
10070
+ const disabledProviders = Array.isArray(config.disabled_providers)
10071
+ ? config.disabled_providers.map(item => normalizeOpencodeProviderName(item)).filter(Boolean).sort()
10072
+ : [];
10073
+ providerStore.lastApplied = {
10074
+ provider: providerName,
10075
+ modelRef,
10076
+ providerHash: hashOpencodeManagedValue(getRecord(config.provider)[providerName]),
10077
+ disabledProvidersHash: hashOpencodeManagedValue(disabledProviders),
10078
+ providerCreatedByCodexMate: providerExistedBeforeApply !== true,
10079
+ disabledProviderAddedByCodexMate: params.disabled === true && disabledContainedBeforeApply !== true
10080
+ };
10081
+ writeOpencodeProviderStore(providerStore);
10082
+ return {
10083
+ success: true,
10084
+ targetPath,
10085
+ ...summarizeOpencodeConfig(config, targetPath, true, providerStore),
10086
+ content
10087
+ };
10088
+ } catch (e) {
10089
+ return { error: e.message || '写入 OpenCode 配置失败' };
10090
+ }
10091
+ }
9631
10092
  // API: 打包 Claude 配置目录(系统 zip 可用则使用,否则回退 zip-lib)
9632
10093
  async function prepareClaudeDirDownload() {
9633
10094
  return await prepareDirectoryDownload(CLAUDE_DIR, {
@@ -11452,6 +11913,15 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
11452
11913
  case 'apply-claude-settings-raw':
11453
11914
  result = applyClaudeSettingsRaw(params || {});
11454
11915
  break;
11916
+ case 'get-opencode-config':
11917
+ result = readOpencodeConfigInfo();
11918
+ break;
11919
+ case 'apply-opencode-config':
11920
+ result = applyOpencodeConfigRaw(params || {});
11921
+ break;
11922
+ case 'update-opencode-selection':
11923
+ result = updateOpencodeSelection(params || {});
11924
+ break;
11455
11925
  case 'apply-claude-config':
11456
11926
  result = await applyToClaudeSettings(params.config);
11457
11927
  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.47",
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 -->
@@ -2,6 +2,19 @@ export const SESSION_TRASH_LIST_LIMIT = 500;
2
2
  export const SESSION_TRASH_PAGE_SIZE = 200;
3
3
  export const DEFAULT_MODEL_CONTEXT_WINDOW = 190000;
4
4
  export const DEFAULT_MODEL_AUTO_COMPACT_TOKEN_LIMIT = 185000;
5
+ export const OPENCODE_MODEL_CATALOG = Object.freeze({
6
+ anthropic: Object.freeze(['claude-4-sonnet', 'claude-4-opus', 'claude-3.7-sonnet', 'claude-3.5-sonnet']),
7
+ openai: Object.freeze(['gpt-4.1', 'gpt-4o', 'o4-mini', 'o3-mini']),
8
+ gemini: Object.freeze(['gemini-2.5-pro', 'gemini-2.5-flash', 'gemini-1.5-pro']),
9
+ groq: Object.freeze(['llama-3.3-70b-versatile', 'llama-3.1-8b-instant']),
10
+ openrouter: Object.freeze(['anthropic/claude-3.7-sonnet', 'openai/gpt-4.1', 'google/gemini-2.5-pro']),
11
+ copilot: Object.freeze(['gpt-4o', 'claude-3.7-sonnet']),
12
+ azure: Object.freeze(['gpt-4.1', 'gpt-4o']),
13
+ bedrock: Object.freeze(['anthropic.claude-3-7-sonnet-20250219-v1:0']),
14
+ vertexai: Object.freeze(['gemini-2.5-pro', 'gemini-2.5-flash']),
15
+ xai: Object.freeze(['grok-3', 'grok-3-mini']),
16
+ local: Object.freeze(['local.model'])
17
+ });
5
18
  export const DEFAULT_OPENCLAW_TEMPLATE = `{
6
19
  // OpenClaw config (JSON5)
7
20
  agent: {
@@ -7,6 +7,7 @@ import {
7
7
  DEFAULT_MODEL_AUTO_COMPACT_TOKEN_LIMIT,
8
8
  DEFAULT_MODEL_CONTEXT_WINDOW,
9
9
  DEFAULT_OPENCLAW_TEMPLATE,
10
+ OPENCODE_MODEL_CATALOG,
10
11
  SESSION_TRASH_LIST_LIMIT,
11
12
  SESSION_TRASH_PAGE_SIZE
12
13
  } from './app.constants.mjs';
@@ -18,6 +19,7 @@ import { createNavigationMethods } from './app.methods.navigation.mjs';
18
19
  import { createOpenclawCoreMethods } from './app.methods.openclaw-core.mjs';
19
20
  import { createOpenclawEditingMethods } from './app.methods.openclaw-editing.mjs';
20
21
  import { createOpenclawPersistMethods } from './app.methods.openclaw-persist.mjs';
22
+ import { createOpencodeConfigMethods } from './app.methods.opencode-config.mjs';
21
23
  import { createProvidersMethods } from './app.methods.providers.mjs';
22
24
  import { createRuntimeMethods } from './app.methods.runtime.mjs';
23
25
  import { createToolConfigPermissionMethods } from './app.methods.tool-config-permissions.mjs';
@@ -89,6 +91,10 @@ export function createAppMethods() {
89
91
  api,
90
92
  defaultOpenclawTemplate: DEFAULT_OPENCLAW_TEMPLATE
91
93
  }),
94
+ ...createOpencodeConfigMethods({
95
+ api,
96
+ modelCatalog: OPENCODE_MODEL_CATALOG
97
+ }),
92
98
  ...createInstallMethods({ api }),
93
99
  ...createRuntimeMethods({ api }),
94
100
  ...createTaskOrchestrationMethods({ api })