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/README.md +20 -7
- package/README.zh.md +20 -7
- package/cli.js +522 -4
- package/package.json +2 -1
- package/web-ui/app.js +27 -4
- package/web-ui/index.html +1 -0
- package/web-ui/modules/app.constants.mjs +13 -0
- package/web-ui/modules/app.methods.codex-config.mjs +7 -3
- package/web-ui/modules/app.methods.index.mjs +6 -0
- package/web-ui/modules/app.methods.opencode-config.mjs +228 -0
- package/web-ui/modules/app.methods.startup-claude.mjs +6 -4
- package/web-ui/modules/app.methods.tool-config-permissions.mjs +3 -2
- package/web-ui/modules/config-mode.computed.mjs +17 -1
- package/web-ui/modules/i18n/locales/en.mjs +47 -0
- package/web-ui/modules/i18n/locales/ja.mjs +47 -0
- package/web-ui/modules/i18n/locales/vi.mjs +47 -0
- package/web-ui/modules/i18n/locales/zh-tw.mjs +1274 -0
- package/web-ui/modules/i18n/locales/zh.mjs +47 -0
- package/web-ui/modules/i18n.dict.mjs +2 -0
- package/web-ui/modules/i18n.mjs +3 -2
- package/web-ui/partials/index/layout-header.html +30 -2
- package/web-ui/partials/index/modal-config-template-agents.html +1 -1
- package/web-ui/partials/index/panel-config-claude.html +6 -6
- package/web-ui/partials/index/panel-config-codex.html +6 -6
- package/web-ui/partials/index/panel-config-opencode.html +166 -0
- package/web-ui/res/web-ui-render.precompiled.js +468 -56
- package/web-ui/styles/controls-forms.css +62 -4
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.
|
|
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 -->
|