codexmate 0.0.44 → 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/README.md +20 -7
- package/README.zh.md +20 -7
- package/cli.js +473 -3
- package/package.json +2 -1
- package/web-ui/app.js +42 -5
- package/web-ui/index.html +2 -0
- package/web-ui/modules/app.computed.index.mjs +3 -1
- package/web-ui/modules/app.computed.main-tabs.mjs +3 -0
- package/web-ui/modules/app.computed.prompts.mjs +28 -0
- package/web-ui/modules/app.computed.session.mjs +23 -1
- package/web-ui/modules/app.constants.mjs +13 -0
- package/web-ui/modules/app.methods.agents.mjs +50 -4
- package/web-ui/modules/app.methods.index.mjs +6 -0
- package/web-ui/modules/app.methods.navigation.mjs +2 -1
- package/web-ui/modules/app.methods.opencode-config.mjs +228 -0
- package/web-ui/modules/app.methods.session-actions.mjs +10 -0
- package/web-ui/modules/app.methods.startup-claude.mjs +3 -1
- 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 +66 -5
- package/web-ui/modules/i18n/locales/ja.mjs +66 -5
- package/web-ui/modules/i18n/locales/vi.mjs +74 -0
- package/web-ui/modules/i18n/locales/zh-tw.mjs +1269 -0
- package/web-ui/modules/i18n/locales/zh.mjs +66 -5
- package/web-ui/modules/i18n.dict.mjs +2 -0
- package/web-ui/modules/i18n.mjs +3 -2
- package/web-ui/partials/index/layout-footer.html +1 -1
- package/web-ui/partials/index/layout-header.html +70 -2
- package/web-ui/partials/index/modal-config-template-agents.html +12 -13
- package/web-ui/partials/index/panel-config-claude.html +6 -12
- package/web-ui/partials/index/panel-config-codex.html +6 -11
- package/web-ui/partials/index/panel-config-opencode.html +166 -0
- package/web-ui/partials/index/panel-prompts.html +100 -0
- package/web-ui/partials/index/panel-sessions.html +30 -10
- package/web-ui/partials/index/panel-usage.html +34 -18
- package/web-ui/res/web-ui-render.precompiled.js +932 -183
- package/web-ui/styles/controls-forms.css +62 -4
- package/web-ui/styles/modals-core.css +162 -0
- package/web-ui/styles/responsive.css +65 -5
- package/web-ui/styles/sessions-toolbar-trash.css +45 -10
- package/web-ui/styles/sessions-usage.css +31 -2
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.
|
|
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
|
@@ -73,6 +73,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
73
73
|
showOpenclawConfigModal: false,
|
|
74
74
|
showConfigTemplateModal: false,
|
|
75
75
|
showAgentsModal: false,
|
|
76
|
+
promptsSubTab: 'codex',
|
|
76
77
|
showSkillsModal: false,
|
|
77
78
|
showHealthCheckModal: false,
|
|
78
79
|
showCodexBridgePoolModal: false,
|
|
@@ -382,14 +383,15 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
382
383
|
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
383
384
|
return {
|
|
384
385
|
codex: parsed.codex === true,
|
|
385
|
-
claude: parsed.claude === true
|
|
386
|
+
claude: parsed.claude === true,
|
|
387
|
+
opencode: parsed.opencode === true
|
|
386
388
|
};
|
|
387
389
|
}
|
|
388
390
|
}
|
|
389
391
|
} catch (_) {}
|
|
390
|
-
return { codex: false, claude: false };
|
|
392
|
+
return { codex: false, claude: false, opencode: false };
|
|
391
393
|
})(),
|
|
392
|
-
toolConfigPermissionSaving: { codex: false, claude: false },
|
|
394
|
+
toolConfigPermissionSaving: { codex: false, claude: false, opencode: false },
|
|
393
395
|
sessionTrashEnabled: true,
|
|
394
396
|
sessionTrashItems: [],
|
|
395
397
|
sessionTrashVisibleCount: SESSION_TRASH_PAGE_SIZE,
|
|
@@ -410,6 +412,28 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
410
412
|
claudeImportLoading: false,
|
|
411
413
|
codexImportLoading: false,
|
|
412
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: '',
|
|
413
437
|
forceCompactLayout: false,
|
|
414
438
|
taskOrchestrationTabEnabled: true,
|
|
415
439
|
taskOrchestration: {
|
|
@@ -486,7 +510,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
486
510
|
}
|
|
487
511
|
{
|
|
488
512
|
const NAV_STATE_STORAGE_KEY = 'codexmateNavState.v1';
|
|
489
|
-
const mainTabSet = new Set(['dashboard', 'config', 'sessions', 'usage', 'orchestration', 'market', 'plugins', 'docs', 'settings', 'trash']);
|
|
513
|
+
const mainTabSet = new Set(['dashboard', 'config', 'sessions', 'usage', 'orchestration', 'market', 'plugins', 'docs', 'settings', 'trash', 'prompts']);
|
|
490
514
|
let restored = null;
|
|
491
515
|
try {
|
|
492
516
|
const raw = localStorage.getItem(NAV_STATE_STORAGE_KEY) || '';
|
|
@@ -521,7 +545,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
521
545
|
if (nextConfigMode && typeof this.switchConfigMode === 'function') {
|
|
522
546
|
this.__navStateRestoring = true;
|
|
523
547
|
try {
|
|
524
|
-
if (nextConfigMode === 'codex' || nextConfigMode === 'claude' || nextConfigMode === 'openclaw') {
|
|
548
|
+
if (nextConfigMode === 'codex' || nextConfigMode === 'claude' || nextConfigMode === 'openclaw' || nextConfigMode === 'opencode') {
|
|
525
549
|
this.configMode = nextConfigMode;
|
|
526
550
|
}
|
|
527
551
|
if (resolvedMainTab && mainTabSet.has(resolvedMainTab) && resolvedMainTab !== this.mainTab) {
|
|
@@ -710,6 +734,19 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
710
734
|
this.clearSessionTimelineRefs();
|
|
711
735
|
},
|
|
712
736
|
|
|
737
|
+
watch: {
|
|
738
|
+
mainTab(newTab) {
|
|
739
|
+
if (newTab === 'prompts' && typeof this.loadPromptsContent === 'function') {
|
|
740
|
+
this.loadPromptsContent();
|
|
741
|
+
}
|
|
742
|
+
},
|
|
743
|
+
promptsSubTab() {
|
|
744
|
+
if (this.mainTab === 'prompts' && typeof this.loadPromptsContent === 'function') {
|
|
745
|
+
this.loadPromptsContent();
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
},
|
|
749
|
+
|
|
713
750
|
computed: createAppComputed(),
|
|
714
751
|
methods: createAppMethods()
|
|
715
752
|
};
|
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 -->
|
|
@@ -24,6 +25,7 @@
|
|
|
24
25
|
<!-- @include ./partials/index/panel-trash.html -->
|
|
25
26
|
<!-- @include ./partials/index/panel-market.html -->
|
|
26
27
|
<!-- @include ./partials/index/panel-plugins.html -->
|
|
28
|
+
<!-- @include ./partials/index/panel-prompts.html -->
|
|
27
29
|
<!-- @include ./partials/index/layout-footer.html -->
|
|
28
30
|
<!-- @include ./partials/index/modals-basic.html -->
|
|
29
31
|
<!-- @include ./partials/index/modal-webhook.html -->
|
|
@@ -4,6 +4,7 @@ import { createSessionComputed } from './app.computed.session.mjs';
|
|
|
4
4
|
import { createConfigModeComputed } from './config-mode.computed.mjs';
|
|
5
5
|
import { createSkillsComputed } from './skills.computed.mjs';
|
|
6
6
|
import { createPluginsComputed } from './plugins.computed.mjs';
|
|
7
|
+
import { createPromptsComputed } from './app.computed.prompts.mjs';
|
|
7
8
|
|
|
8
9
|
export function createAppComputed() {
|
|
9
10
|
return {
|
|
@@ -12,6 +13,7 @@ export function createAppComputed() {
|
|
|
12
13
|
...createMainTabsComputed(),
|
|
13
14
|
...createSkillsComputed(),
|
|
14
15
|
...createPluginsComputed(),
|
|
15
|
-
...createConfigModeComputed()
|
|
16
|
+
...createConfigModeComputed(),
|
|
17
|
+
...createPromptsComputed()
|
|
16
18
|
};
|
|
17
19
|
}
|