codexmate 0.0.39 → 0.0.40

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.
@@ -273,7 +273,12 @@ stream_idle_timeout_ms = 300000
273
273
  fs.writeFileSync(INIT_MARK_FILE, JSON.stringify(payload, null, 2), 'utf-8');
274
274
  }
275
275
 
276
- function ensureManagedConfigBootstrap() {
276
+ function ensureManagedConfigBootstrap(options = {}) {
277
+ const allowWrite = !(options && options.allowWrite === false);
278
+ if (!allowWrite) {
279
+ initNotice = '';
280
+ return { notice: '', readOnly: true };
281
+ }
277
282
  ensureConfigDir();
278
283
 
279
284
  const initializedAt = new Date().toISOString();
package/cli.js CHANGED
@@ -213,6 +213,7 @@ const CLAUDE_PROJECTS_DIR = path.join(os.homedir(), '.claude', 'projects');
213
213
  const CODEBUDDY_DIR = path.join(os.homedir(), '.codebuddy');
214
214
  const CODEBUDDY_PROJECTS_DIR = path.join(CODEBUDDY_DIR, 'projects');
215
215
  const CODEXMATE_DIR = path.join(os.homedir(), '.codexmate');
216
+ const CODEXMATE_PREFERENCES_FILE = path.join(CODEXMATE_DIR, 'preferences.json');
216
217
  const CODEXMATE_SESSIONS_DIR = path.join(CODEXMATE_DIR, 'sessions');
217
218
  const CODEXMATE_DERIVED_SESSIONS_DIR = path.join(CODEXMATE_SESSIONS_DIR, 'derived');
218
219
  const CODEXMATE_DERIVED_CODEX_DIR = path.join(CODEXMATE_DERIVED_SESSIONS_DIR, 'codex');
@@ -717,6 +718,7 @@ function readConfig() {
717
718
  }
718
719
 
719
720
  function writeConfig(content) {
721
+ assertToolConfigWriteAllowed('codex');
720
722
  try {
721
723
  fs.writeFileSync(CONFIG_FILE, content, 'utf-8');
722
724
  } catch (e) {
@@ -734,6 +736,7 @@ function readModels() {
734
736
  }
735
737
 
736
738
  function writeModels(models) {
739
+ assertToolConfigWriteAllowed('codex');
737
740
  fs.writeFileSync(MODELS_FILE, JSON.stringify(models, null, 2), 'utf-8');
738
741
  }
739
742
 
@@ -747,10 +750,12 @@ function readCurrentModels() {
747
750
  }
748
751
 
749
752
  function writeCurrentModels(data) {
753
+ assertToolConfigWriteAllowed('codex');
750
754
  fs.writeFileSync(CURRENT_MODELS_FILE, JSON.stringify(data, null, 2), 'utf-8');
751
755
  }
752
756
 
753
757
  function updateAuthJson(apiKey) {
758
+ assertToolConfigWriteAllowed('codex');
754
759
  let authData = {};
755
760
  if (fs.existsSync(AUTH_FILE)) {
756
761
  try {
@@ -766,6 +771,141 @@ function isPlainObject(value) {
766
771
  return !!value && typeof value === 'object' && !Array.isArray(value);
767
772
  }
768
773
 
774
+ const TOOL_CONFIG_PERMISSION_TARGETS = new Set(['codex', 'claude']);
775
+ const TOOL_CONFIG_PERMISSION_DEFAULTS = Object.freeze({ codex: false, claude: false });
776
+ let toolConfigWriteGuardDepth = 0;
777
+
778
+ function enterToolConfigWriteGuard() {
779
+ toolConfigWriteGuardDepth += 1;
780
+ let active = true;
781
+ return () => {
782
+ if (!active) return;
783
+ active = false;
784
+ toolConfigWriteGuardDepth = Math.max(0, toolConfigWriteGuardDepth - 1);
785
+ };
786
+ }
787
+
788
+ function isToolConfigWriteGuardActive() {
789
+ return toolConfigWriteGuardDepth > 0;
790
+ }
791
+
792
+ function normalizeToolConfigTarget(value) {
793
+ const target = typeof value === 'string' ? value.trim().toLowerCase() : '';
794
+ return TOOL_CONFIG_PERMISSION_TARGETS.has(target) ? target : '';
795
+ }
796
+
797
+ function normalizeToolConfigPermissions(value) {
798
+ const source = isPlainObject(value) ? value : {};
799
+ return {
800
+ codex: source.codex === true,
801
+ claude: source.claude === true
802
+ };
803
+ }
804
+
805
+ function readCodexmatePreferences() {
806
+ if (!fs.existsSync(CODEXMATE_PREFERENCES_FILE)) return {};
807
+ try {
808
+ const raw = fs.readFileSync(CODEXMATE_PREFERENCES_FILE, 'utf-8');
809
+ const parsed = raw && raw.trim() ? JSON.parse(raw) : {};
810
+ return isPlainObject(parsed) ? parsed : {};
811
+ } catch (_) {
812
+ return {};
813
+ }
814
+ }
815
+
816
+ function writeCodexmatePreferences(preferences) {
817
+ ensureDir(CODEXMATE_DIR);
818
+ writeJsonAtomic(CODEXMATE_PREFERENCES_FILE, isPlainObject(preferences) ? preferences : {});
819
+ }
820
+
821
+ function readToolConfigPermissions() {
822
+ const preferences = readCodexmatePreferences();
823
+ return normalizeToolConfigPermissions(preferences.toolConfigPermissions || TOOL_CONFIG_PERMISSION_DEFAULTS);
824
+ }
825
+
826
+ function isToolConfigWriteAllowed(target) {
827
+ const normalizedTarget = normalizeToolConfigTarget(target);
828
+ if (!normalizedTarget) return false;
829
+ return readToolConfigPermissions()[normalizedTarget] === true;
830
+ }
831
+
832
+ function buildToolConfigWriteDeniedPayload(target) {
833
+ const normalizedTarget = normalizeToolConfigTarget(target) || target || '';
834
+ return {
835
+ error: '当前为仅浏览,未修改配置。',
836
+ errorCode: 'tool-config-write-disabled',
837
+ target: normalizedTarget,
838
+ permissions: readToolConfigPermissions()
839
+ };
840
+ }
841
+
842
+ function assertToolConfigWriteAllowed(target) {
843
+ if (!isToolConfigWriteGuardActive()) return;
844
+ if (isToolConfigWriteAllowed(target)) return;
845
+ const payload = buildToolConfigWriteDeniedPayload(target);
846
+ const err = new Error(payload.error);
847
+ err.code = payload.errorCode;
848
+ err.target = payload.target;
849
+ throw err;
850
+ }
851
+
852
+ function getApiToolConfigWriteTarget(action) {
853
+ const name = typeof action === 'string' ? action.trim() : '';
854
+ if (!name) return '';
855
+ const codexWriteActions = new Set([
856
+ 'apply-config-template',
857
+ 'add-provider',
858
+ 'update-provider',
859
+ 'delete-provider',
860
+ 'reset-config',
861
+ 'add-model',
862
+ 'delete-model',
863
+ 'restore-codex-dir',
864
+ 'import-config',
865
+ 'import-auth-profile',
866
+ 'switch-auth-profile',
867
+ 'delete-auth-profile',
868
+ 'proxy-enable-codex-default',
869
+ 'proxy-apply-provider',
870
+ 'local-bridge-toggle',
871
+ 'local-bridge-set-excluded'
872
+ ]);
873
+ const claudeWriteActions = new Set([
874
+ 'apply-claude-settings-raw',
875
+ 'apply-claude-config',
876
+ 'restore-claude-dir',
877
+ 'claude-local-bridge-toggle',
878
+ 'claude-local-bridge-set-excluded',
879
+ 'claude-local-bridge-sync-providers'
880
+ ]);
881
+ if (codexWriteActions.has(name)) return 'codex';
882
+ if (claudeWriteActions.has(name)) return 'claude';
883
+ return '';
884
+ }
885
+
886
+ function setToolConfigPermission(params = {}) {
887
+ const target = normalizeToolConfigTarget(params && params.target);
888
+ if (!target) return { error: '未知配置对象' };
889
+ const preferences = readCodexmatePreferences();
890
+ const current = normalizeToolConfigPermissions(preferences.toolConfigPermissions || TOOL_CONFIG_PERMISSION_DEFAULTS);
891
+ current[target] = params && params.allowWrite === true;
892
+ preferences.toolConfigPermissions = current;
893
+ writeCodexmatePreferences(preferences);
894
+
895
+ let bootstrapNotice = '';
896
+ if (target === 'codex' && current.codex) {
897
+ const bootstrap = ensureManagedConfigBootstrap({ allowWrite: true });
898
+ bootstrapNotice = bootstrap && bootstrap.notice ? bootstrap.notice : '';
899
+ }
900
+
901
+ return {
902
+ success: true,
903
+ target,
904
+ permissions: current,
905
+ bootstrapNotice
906
+ };
907
+ }
908
+
769
909
  const PROVIDER_CONFIG_KEYS = new Set([
770
910
  'name',
771
911
  'base_url',
@@ -5543,6 +5683,7 @@ function readLocalBridgeSettings() {
5543
5683
  }
5544
5684
 
5545
5685
  function writeLocalBridgeSettings(settings) {
5686
+ assertToolConfigWriteAllowed('codex');
5546
5687
  fs.writeFileSync(LOCAL_BRIDGE_SETTINGS_FILE, JSON.stringify(settings, null, 2), 'utf-8');
5547
5688
  }
5548
5689
 
@@ -5641,6 +5782,7 @@ function readClaudeLocalBridgeSettings() {
5641
5782
  }
5642
5783
 
5643
5784
  function writeClaudeLocalBridgeSettings(settings) {
5785
+ assertToolConfigWriteAllowed('claude');
5644
5786
  fs.writeFileSync(CLAUDE_LOCAL_BRIDGE_SETTINGS_FILE, JSON.stringify(settings, null, 2), 'utf-8');
5645
5787
  }
5646
5788
 
@@ -5655,6 +5797,7 @@ function readClaudeLocalProvidersFile() {
5655
5797
  }
5656
5798
 
5657
5799
  function writeClaudeLocalProvidersFile(data) {
5800
+ assertToolConfigWriteAllowed('claude');
5658
5801
  ensureDir(CONFIG_DIR);
5659
5802
  fs.writeFileSync(CLAUDE_LOCAL_PROVIDERS_FILE, JSON.stringify(data, null, 2), 'utf-8');
5660
5803
  }
@@ -5669,6 +5812,7 @@ function syncClaudeProvidersToBridgeFile() {
5669
5812
  }
5670
5813
 
5671
5814
  function toggleClaudeLocalBridge(params = {}) {
5815
+ assertToolConfigWriteAllowed('claude');
5672
5816
  const enable = !!params.enable;
5673
5817
  const settings = readClaudeLocalBridgeSettings();
5674
5818
 
@@ -9177,6 +9321,7 @@ function maskKey(key) {
9177
9321
 
9178
9322
  // 应用到 Claude Code settings.json(跨平台)
9179
9323
  function applyToClaudeSettings(config = {}) {
9324
+ assertToolConfigWriteAllowed('claude');
9180
9325
  try {
9181
9326
  const apiKey = (config.apiKey || '').trim();
9182
9327
  if (!apiKey) {
@@ -9276,6 +9421,7 @@ function readClaudeSettingsRaw() {
9276
9421
  }
9277
9422
 
9278
9423
  function applyClaudeSettingsRaw(params = {}) {
9424
+ assertToolConfigWriteAllowed('claude');
9279
9425
  const content = typeof params.content === 'string' ? params.content : '';
9280
9426
  if (!content.trim()) {
9281
9427
  return { error: '内容不能为空' };
@@ -10769,14 +10915,28 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
10769
10915
  });
10770
10916
  req.on('end', async () => {
10771
10917
  if (bodyTooLarge) return;
10918
+ let leaveToolConfigWriteGuard = null;
10772
10919
  try {
10773
10920
  const { action, params } = JSON.parse(body || '{}');
10921
+ leaveToolConfigWriteGuard = typeof enterToolConfigWriteGuard === 'function'
10922
+ ? enterToolConfigWriteGuard()
10923
+ : () => {};
10774
10924
  let result;
10775
10925
 
10776
- switch (action) {
10926
+ const guardedToolConfigTarget = getApiToolConfigWriteTarget(action);
10927
+ if (guardedToolConfigTarget && !isToolConfigWriteAllowed(guardedToolConfigTarget)) {
10928
+ result = buildToolConfigWriteDeniedPayload(guardedToolConfigTarget);
10929
+ } else {
10930
+ switch (action) {
10777
10931
  case 'health-check':
10778
10932
  result = { ok: true };
10779
10933
  break;
10934
+ case 'get-tool-config-permissions':
10935
+ result = { permissions: readToolConfigPermissions() };
10936
+ break;
10937
+ case 'set-tool-config-permission':
10938
+ result = setToolConfigPermission(params || {});
10939
+ break;
10780
10940
  case 'status': {
10781
10941
  const statusConfigResult = readConfigOrVirtualDefault();
10782
10942
  const config = statusConfigResult.config;
@@ -10815,7 +10975,8 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
10815
10975
  configReady: !statusConfigResult.isVirtual,
10816
10976
  configErrorType: statusConfigResult.errorType || '',
10817
10977
  configNotice: statusConfigResult.reason || '',
10818
- initNotice: consumeInitNotice()
10978
+ initNotice: consumeInitNotice(),
10979
+ toolConfigPermissions: readToolConfigPermissions()
10819
10980
  };
10820
10981
  break;
10821
10982
  }
@@ -11455,6 +11616,7 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
11455
11616
  break;
11456
11617
  default:
11457
11618
  result = { error: '未知操作' };
11619
+ }
11458
11620
  }
11459
11621
 
11460
11622
  const responseBody = JSON.stringify(result, null, 2);
@@ -11463,7 +11625,9 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
11463
11625
  'Content-Length': Buffer.byteLength(responseBody, 'utf-8')
11464
11626
  });
11465
11627
  res.end(responseBody, 'utf-8');
11628
+ if (leaveToolConfigWriteGuard) leaveToolConfigWriteGuard();
11466
11629
  } catch (e) {
11630
+ if (leaveToolConfigWriteGuard) leaveToolConfigWriteGuard();
11467
11631
  const errorBody = JSON.stringify({ error: e.message }, null, 2);
11468
11632
  res.writeHead(500, {
11469
11633
  'Content-Type': 'application/json; charset=utf-8',
@@ -16062,7 +16226,10 @@ async function main() {
16062
16226
  const args = process.argv.slice(2);
16063
16227
  const command = args[0];
16064
16228
  const isMcpCommand = command === 'mcp';
16065
- const bootstrap = ensureManagedConfigBootstrap();
16229
+ const shouldGateInitialBootstrap = command === 'run' || isMcpCommand;
16230
+ const bootstrap = ensureManagedConfigBootstrap({
16231
+ allowWrite: shouldGateInitialBootstrap ? isToolConfigWriteAllowed('codex') : true
16232
+ });
16066
16233
  if (bootstrap && bootstrap.notice) {
16067
16234
  // MCP stdio transport requires stdout to be protocol-clean.
16068
16235
  if (!isMcpCommand) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codexmate",
3
- "version": "0.0.39",
3
+ "version": "0.0.40",
4
4
  "description": "Codex/Claude Code/OpenClaw 配置、会话与任务编排 CLI + Web 工具",
5
5
  "main": "cli.js",
6
6
  "bin": {
@@ -102,18 +102,75 @@ function renderTemplate(templateText, values = {}) {
102
102
  });
103
103
  }
104
104
 
105
+ function translate(t, key, fallback, params = null) {
106
+ if (typeof t !== 'function') return fallback;
107
+ const translated = t(key, params);
108
+ return translated === key ? fallback : translated;
109
+ }
110
+
111
+ function localizePluginMeta(meta, t) {
112
+ const safe = meta && typeof meta === 'object' ? meta : {};
113
+ const titleKey = typeof safe.titleKey === 'string' ? safe.titleKey : '';
114
+ const descriptionKey = typeof safe.descriptionKey === 'string' ? safe.descriptionKey : '';
115
+ const statusLabelKey = typeof safe.statusLabelKey === 'string' ? safe.statusLabelKey : '';
116
+ return {
117
+ ...safe,
118
+ title: titleKey ? translate(t, titleKey, safe.title || '') : (safe.title || ''),
119
+ description: descriptionKey ? translate(t, descriptionKey, safe.description || '') : (safe.description || ''),
120
+ statusLabel: statusLabelKey ? translate(t, statusLabelKey, safe.statusLabel || '') : (safe.statusLabel || '')
121
+ };
122
+ }
123
+
124
+ const BUILTIN_TEMPLATE_I18N = Object.freeze({
125
+ builtin_comment_polish: Object.freeze({
126
+ nameKey: 'plugins.builtin.commentPolish.name',
127
+ descKey: 'plugins.builtin.commentPolish.desc',
128
+ lineKey: 'plugins.builtin.commentPolish.line1',
129
+ fallbackName: '代码注释润色',
130
+ fallbackDesc: '轻微收敛以下代码注释 {{code}}',
131
+ fallbackLine: '轻微收敛以下代码注释',
132
+ vars: ['{{code}}']
133
+ }),
134
+ builtin_rule_ack: Object.freeze({
135
+ nameKey: 'plugins.builtin.ruleAck.name',
136
+ descKey: 'plugins.builtin.ruleAck.desc',
137
+ lineKey: 'plugins.builtin.ruleAck.line1',
138
+ fallbackName: '规则确认回复',
139
+ fallbackDesc: '请根据【{{rule}}】,收到请回复',
140
+ fallbackLine: '请根据【{{rule}}】,收到请回复',
141
+ vars: []
142
+ })
143
+ });
144
+
145
+ function localizeBuiltinPromptTemplate(item, t) {
146
+ const safe = item && typeof item === 'object' ? item : {};
147
+ if (safe.isBuiltin !== true) return safe;
148
+ const spec = BUILTIN_TEMPLATE_I18N[safe.id];
149
+ if (!spec) return safe;
150
+ const line = translate(t, spec.lineKey, spec.fallbackLine);
151
+ return {
152
+ ...safe,
153
+ name: translate(t, spec.nameKey, spec.fallbackName),
154
+ description: translate(t, spec.descKey, spec.fallbackDesc),
155
+ template: spec.vars && spec.vars.length ? [line, '', ...spec.vars].join('\n') : line
156
+ };
157
+ }
158
+
105
159
  import { pluginsRegistry } from '../registry.mjs';
106
160
 
107
161
  export function createPluginsComputed() {
108
162
  return {
109
163
  pluginsCatalog() {
110
- return pluginsRegistry.map((entry) => entry && entry.meta).filter(Boolean);
164
+ return pluginsRegistry
165
+ .map((entry) => entry && entry.meta)
166
+ .filter(Boolean)
167
+ .map((meta) => localizePluginMeta(meta, this.t));
111
168
  },
112
169
 
113
170
  pluginsActiveMeta() {
114
171
  const id = typeof this.pluginsActiveId === 'string' ? this.pluginsActiveId.trim() : '';
115
172
  const entry = pluginsRegistry.find((item) => item && item.id === id) || null;
116
- return entry && entry.meta ? entry.meta : null;
173
+ return entry && entry.meta ? localizePluginMeta(entry.meta, this.t) : null;
117
174
  },
118
175
 
119
176
  pluginsActiveAttribution() {
@@ -138,6 +195,7 @@ export function createPluginsComputed() {
138
195
  const list = Array.isArray(this.promptTemplatesListRaw) ? this.promptTemplatesListRaw : [];
139
196
  return list
140
197
  .map((item) => normalizePromptTemplateEntry(item))
198
+ .map((item) => localizeBuiltinPromptTemplate(item, this.t))
141
199
  .filter((item) => item.id && item.name)
142
200
  .map((item) => {
143
201
  const vars = parseTemplateVariables(item.template);
@@ -178,7 +236,7 @@ export function createPluginsComputed() {
178
236
  const id = typeof draft.id === 'string' ? draft.id : '';
179
237
  const name = typeof draft.name === 'string' ? draft.name : '';
180
238
  if (!id && !name) return null;
181
- return normalizePromptTemplateEntry(draft);
239
+ return localizeBuiltinPromptTemplate(normalizePromptTemplateEntry(draft), this.t);
182
240
  },
183
241
 
184
242
  promptTemplateVars() {
@@ -3,8 +3,11 @@ import { pluginOwnership } from './ownership.mjs';
3
3
  const baseMeta = {
4
4
  id: 'prompt-templates',
5
5
  title: 'Prompt Templates',
6
+ titleKey: 'plugins.catalog.promptTemplates.title',
6
7
  description: 'Standardized, template-driven prompts with variables and copy/export helpers.',
8
+ descriptionKey: 'plugins.catalog.promptTemplates.description',
7
9
  statusLabel: 'standard',
10
+ statusLabelKey: 'plugins.status.standard',
8
11
  tone: 'configured'
9
12
  };
10
13
 
package/web-ui/app.js CHANGED
@@ -366,6 +366,8 @@ document.addEventListener('DOMContentLoaded', () => {
366
366
  codexDownloadProgress: 0,
367
367
  codexDownloadTimer: null,
368
368
  settingsTab: 'general',
369
+ toolConfigPermissions: { codex: false, claude: false },
370
+ toolConfigPermissionSaving: { codex: false, claude: false },
369
371
  sessionTrashEnabled: true,
370
372
  sessionTrashItems: [],
371
373
  sessionTrashVisibleCount: SESSION_TRASH_PAGE_SIZE,
@@ -44,7 +44,9 @@ function readTaskOrchestrationDraftMetrics(taskOrchestration) {
44
44
  }
45
45
 
46
46
  function translateTaskText(t, key, fallback, params = null) {
47
- return typeof t === 'function' ? t(key, params) : fallback;
47
+ if (typeof t !== 'function') return fallback;
48
+ const translated = t(key, params);
49
+ return translated === key ? fallback : translated;
48
50
  }
49
51
 
50
52
  function createTaskDraftChecklist(metrics, t = null) {
@@ -20,6 +20,7 @@ import { createOpenclawEditingMethods } from './app.methods.openclaw-editing.mjs
20
20
  import { createOpenclawPersistMethods } from './app.methods.openclaw-persist.mjs';
21
21
  import { createProvidersMethods } from './app.methods.providers.mjs';
22
22
  import { createRuntimeMethods } from './app.methods.runtime.mjs';
23
+ import { createToolConfigPermissionMethods } from './app.methods.tool-config-permissions.mjs';
23
24
  import { createTaskOrchestrationMethods } from './app.methods.task-orchestration.mjs';
24
25
  import { createSessionActionMethods } from './app.methods.session-actions.mjs';
25
26
  import { createSessionBrowserMethods } from './app.methods.session-browser.mjs';
@@ -81,6 +82,7 @@ export function createAppMethods() {
81
82
  ...createAgentsMethods({ api, apiWithMeta }),
82
83
  ...createProvidersMethods({ api }),
83
84
  ...createClaudeConfigMethods({ api }),
85
+ ...createToolConfigPermissionMethods({ api }),
84
86
  ...createOpenclawCoreMethods(),
85
87
  ...createOpenclawEditingMethods(),
86
88
  ...createOpenclawPersistMethods({
@@ -120,6 +120,12 @@ export function createStartupClaudeMethods(options = {}) {
120
120
  : String(defaultModelAutoCompactTokenLimit);
121
121
  }
122
122
  }
123
+ if (statusRes.toolConfigPermissions && typeof statusRes.toolConfigPermissions === 'object') {
124
+ this.toolConfigPermissions = {
125
+ codex: statusRes.toolConfigPermissions.codex === true,
126
+ claude: statusRes.toolConfigPermissions.claude === true
127
+ };
128
+ }
123
129
  this.providersList = listRes.providers;
124
130
  if (typeof this.loadLocalBridgeExcluded === 'function') { this.loadLocalBridgeExcluded(); }
125
131
  if (typeof this.loadClaudeLocalBridgeStatus === 'function') { this.loadClaudeLocalBridgeStatus(); }
@@ -0,0 +1,87 @@
1
+ export function createToolConfigPermissionMethods(options = {}) {
2
+ const { api } = options;
3
+
4
+ function normalizeTarget(value) {
5
+ const target = typeof value === 'string' ? value.trim().toLowerCase() : '';
6
+ return target === 'codex' || target === 'claude' ? target : '';
7
+ }
8
+
9
+ function normalizePermissions(value) {
10
+ const source = value && typeof value === 'object' && !Array.isArray(value) ? value : {};
11
+ return {
12
+ codex: source.codex === true,
13
+ claude: source.claude === true
14
+ };
15
+ }
16
+
17
+ return {
18
+ isToolConfigWriteAllowed(target) {
19
+ const normalizedTarget = normalizeTarget(target);
20
+ if (!normalizedTarget) return false;
21
+ return normalizePermissions(this.toolConfigPermissions)[normalizedTarget] === true;
22
+ },
23
+
24
+ toolConfigPermissionStatusLabel(target) {
25
+ return this.isToolConfigWriteAllowed(target)
26
+ ? this.t('toolConfig.allow')
27
+ : this.t('toolConfig.viewOnly');
28
+ },
29
+
30
+ async setToolConfigPermission(target, allowWrite) {
31
+ const normalizedTarget = normalizeTarget(target);
32
+ if (!normalizedTarget || this.toolConfigPermissionSaving[normalizedTarget]) return;
33
+
34
+ const nextAllowWrite = allowWrite === true;
35
+ const previous = normalizePermissions(this.toolConfigPermissions);
36
+ if (previous[normalizedTarget] === nextAllowWrite) return;
37
+
38
+ if (nextAllowWrite) {
39
+ const confirmed = await this.requestConfirmDialog({
40
+ title: this.t('toolConfig.confirmTitle'),
41
+ message: this.t(`toolConfig.${normalizedTarget}.confirmMessage`),
42
+ confirmText: this.t('toolConfig.confirmAllow'),
43
+ cancelText: this.t('confirm.cancel'),
44
+ danger: true
45
+ });
46
+ if (!confirmed) {
47
+ this.toolConfigPermissions = { ...previous };
48
+ return;
49
+ }
50
+ }
51
+
52
+ this.toolConfigPermissionSaving = {
53
+ ...this.toolConfigPermissionSaving,
54
+ [normalizedTarget]: true
55
+ };
56
+ try {
57
+ const res = await api('set-tool-config-permission', {
58
+ target: normalizedTarget,
59
+ allowWrite: nextAllowWrite
60
+ });
61
+ if (res && res.error) {
62
+ this.toolConfigPermissions = { ...previous };
63
+ this.showMessage(res.error, 'error');
64
+ return;
65
+ }
66
+ this.toolConfigPermissions = normalizePermissions(res && res.permissions);
67
+ this.showMessage(
68
+ nextAllowWrite
69
+ ? this.t('toolConfig.allowToast')
70
+ : this.t('toolConfig.viewOnlyToast'),
71
+ 'success'
72
+ );
73
+ try {
74
+ await this.loadAll({ preserveLoading: true });
75
+ } catch (_) {}
76
+ } catch (_) {
77
+ this.toolConfigPermissions = { ...previous };
78
+ this.showMessage(this.t('toolConfig.saveFailed'), 'error');
79
+ } finally {
80
+ this.toolConfigPermissionSaving = {
81
+ ...this.toolConfigPermissionSaving,
82
+ [normalizedTarget]: false
83
+ };
84
+ }
85
+ }
86
+ };
87
+ }