codexmate 0.0.27 → 0.0.29

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.
Files changed (51) hide show
  1. package/README.md +1 -1
  2. package/README.zh.md +1 -1
  3. package/cli/builtin-proxy.js +430 -4
  4. package/cli/openai-bridge.js +498 -13
  5. package/cli.js +130 -41
  6. package/lib/cli-models-utils.js +71 -10
  7. package/lib/cli-webhook.js +126 -0
  8. package/package.json +76 -74
  9. package/plugins/prompt-templates/computed.mjs +1 -1
  10. package/plugins/prompt-templates/methods.mjs +0 -66
  11. package/plugins/prompt-templates/overview.mjs +1 -0
  12. package/web-ui/app.js +21 -16
  13. package/web-ui/index.html +1 -0
  14. package/web-ui/logic.codex.mjs +69 -0
  15. package/web-ui/modules/app.computed.dashboard.mjs +54 -0
  16. package/web-ui/modules/app.computed.session.mjs +22 -17
  17. package/web-ui/modules/app.methods.claude-config.mjs +24 -8
  18. package/web-ui/modules/app.methods.codex-config.mjs +35 -3
  19. package/web-ui/modules/app.methods.index.mjs +2 -0
  20. package/web-ui/modules/app.methods.navigation.mjs +21 -3
  21. package/web-ui/modules/app.methods.providers.mjs +96 -7
  22. package/web-ui/modules/app.methods.session-actions.mjs +3 -6
  23. package/web-ui/modules/app.methods.session-browser.mjs +1 -6
  24. package/web-ui/modules/app.methods.session-trash.mjs +6 -7
  25. package/web-ui/modules/app.methods.startup-claude.mjs +8 -1
  26. package/web-ui/modules/app.methods.webhook.mjs +79 -0
  27. package/web-ui/modules/i18n.dict.mjs +1104 -104
  28. package/web-ui/modules/i18n.mjs +9 -3
  29. package/web-ui/modules/provider-url-display.mjs +17 -0
  30. package/web-ui/partials/index/layout-header.html +25 -0
  31. package/web-ui/partials/index/modals-basic.html +0 -3
  32. package/web-ui/partials/index/panel-config-claude.html +10 -3
  33. package/web-ui/partials/index/panel-config-codex.html +44 -4
  34. package/web-ui/partials/index/panel-plugins.html +3 -29
  35. package/web-ui/partials/index/panel-sessions.html +0 -10
  36. package/web-ui/partials/index/panel-settings.html +93 -177
  37. package/web-ui/partials/index/panel-trash.html +88 -0
  38. package/web-ui/session-helpers.mjs +2 -2
  39. package/web-ui/styles/base-theme.css +47 -34
  40. package/web-ui/styles/controls-forms.css +27 -28
  41. package/web-ui/styles/docs-panel.css +63 -39
  42. package/web-ui/styles/layout-shell.css +69 -46
  43. package/web-ui/styles/modals-core.css +12 -10
  44. package/web-ui/styles/navigation-panels.css +36 -35
  45. package/web-ui/styles/responsive.css +4 -4
  46. package/web-ui/styles/sessions-list.css +10 -6
  47. package/web-ui/styles/settings-panel.css +197 -33
  48. package/web-ui/styles/titles-cards.css +90 -26
  49. package/web-ui/styles/trash-panel.css +90 -0
  50. package/web-ui/styles/webhook.css +81 -0
  51. package/web-ui/styles.css +2 -0
package/cli.js CHANGED
@@ -83,6 +83,13 @@ const {
83
83
  dispatchAutomationNotifiers,
84
84
  formatTaskRunNotificationPayload
85
85
  } = require('./lib/automation');
86
+ const {
87
+ ALLOWED_EVENTS: WEBHOOK_ALLOWED_EVENTS,
88
+ defaultConfigPath: defaultWebhookConfigPath,
89
+ loadWebhookConfig,
90
+ saveWebhookConfig,
91
+ notifyWebhook
92
+ } = require('./lib/cli-webhook');
86
93
  const { buildConfigHealthReport: buildConfigHealthReportCore } = require('./cli/config-health');
87
94
  const { buildDoctorReport, buildDoctorLegacyPayload, renderDoctorMarkdown } = require('./cli/doctor-core');
88
95
  const {
@@ -304,8 +311,16 @@ const CLI_INSTALL_TARGETS = Object.freeze([
304
311
  }
305
312
  ]);
306
313
 
307
- const HTTP_KEEP_ALIVE_AGENT = new http.Agent({ keepAlive: true });
308
- const HTTPS_KEEP_ALIVE_AGENT = new https.Agent({ keepAlive: true });
314
+ const HTTP_KEEP_ALIVE_AGENT = new http.Agent({
315
+ keepAlive: true,
316
+ keepAliveMsecs: 1000,
317
+ maxFreeSockets: 4
318
+ });
319
+ const HTTPS_KEEP_ALIVE_AGENT = new https.Agent({
320
+ keepAlive: true,
321
+ keepAliveMsecs: 1000,
322
+ maxFreeSockets: 4
323
+ });
309
324
 
310
325
  const openaiBridgeHandler = createOpenaiBridgeHttpHandler({
311
326
  settingsFile: OPENAI_BRIDGE_SETTINGS_FILE,
@@ -3639,12 +3654,19 @@ function readTotalTokensFromUsage(usage) {
3639
3654
  return explicitTotal;
3640
3655
  }
3641
3656
  const inputTokens = readNonNegativeInteger(usage.input_tokens ?? usage.inputTokens);
3657
+ const cachedInputTokens = readNonNegativeInteger(
3658
+ usage.cached_input_tokens ?? usage.cachedInputTokens
3659
+ ?? usage.cache_read_input_tokens ?? usage.cacheReadInputTokens
3660
+ );
3661
+ const cacheCreationInputTokens = readNonNegativeInteger(
3662
+ usage.cache_creation_input_tokens ?? usage.cacheCreationInputTokens
3663
+ );
3642
3664
  const outputTokens = readNonNegativeInteger(usage.output_tokens ?? usage.outputTokens);
3643
3665
  const reasoningOutputTokens = readNonNegativeInteger(usage.reasoning_output_tokens ?? usage.reasoningOutputTokens);
3644
- if (inputTokens === null && outputTokens === null && reasoningOutputTokens === null) {
3666
+ if (inputTokens === null && cachedInputTokens === null && cacheCreationInputTokens === null && outputTokens === null && reasoningOutputTokens === null) {
3645
3667
  return null;
3646
3668
  }
3647
- return (inputTokens || 0) + (outputTokens || 0) + (reasoningOutputTokens || 0);
3669
+ return (inputTokens || 0) + (cachedInputTokens || 0) + (cacheCreationInputTokens || 0) + (outputTokens || 0) + (reasoningOutputTokens || 0);
3648
3670
  }
3649
3671
 
3650
3672
  function readUsageTotalsFromUsage(usage) {
@@ -3652,19 +3674,26 @@ function readUsageTotalsFromUsage(usage) {
3652
3674
  return null;
3653
3675
  }
3654
3676
  const inputTokens = readNonNegativeInteger(usage.input_tokens ?? usage.inputTokens);
3655
- const cachedInputTokens = readNonNegativeInteger(usage.cached_input_tokens ?? usage.cachedInputTokens);
3677
+ const cachedInputTokens = readNonNegativeInteger(
3678
+ usage.cached_input_tokens ?? usage.cachedInputTokens
3679
+ ?? usage.cache_read_input_tokens ?? usage.cacheReadInputTokens
3680
+ );
3681
+ const cacheCreationInputTokens = readNonNegativeInteger(
3682
+ usage.cache_creation_input_tokens ?? usage.cacheCreationInputTokens
3683
+ );
3656
3684
  const outputTokens = readNonNegativeInteger(usage.output_tokens ?? usage.outputTokens);
3657
3685
  const reasoningOutputTokens = readNonNegativeInteger(usage.reasoning_output_tokens ?? usage.reasoningOutputTokens);
3658
3686
  const totalTokens = readNonNegativeInteger(usage.total_tokens ?? usage.totalTokens)
3659
- ?? ((inputTokens === null && cachedInputTokens === null && outputTokens === null && reasoningOutputTokens === null)
3687
+ ?? ((inputTokens === null && cachedInputTokens === null && cacheCreationInputTokens === null && outputTokens === null && reasoningOutputTokens === null)
3660
3688
  ? null
3661
- : ((inputTokens || 0) + (outputTokens || 0) + (reasoningOutputTokens || 0)));
3662
- if (inputTokens === null && cachedInputTokens === null && outputTokens === null && reasoningOutputTokens === null && totalTokens === null) {
3689
+ : ((inputTokens || 0) + (cachedInputTokens || 0) + (cacheCreationInputTokens || 0) + (outputTokens || 0) + (reasoningOutputTokens || 0)));
3690
+ if (inputTokens === null && cachedInputTokens === null && cacheCreationInputTokens === null && outputTokens === null && reasoningOutputTokens === null && totalTokens === null) {
3663
3691
  return null;
3664
3692
  }
3665
3693
  return {
3666
3694
  inputTokens,
3667
3695
  cachedInputTokens,
3696
+ cacheCreationInputTokens,
3668
3697
  outputTokens,
3669
3698
  reasoningOutputTokens,
3670
3699
  totalTokens
@@ -3690,6 +3719,7 @@ function applyUsageTotalsToState(state, usageTotals) {
3690
3719
  const pairs = [
3691
3720
  ['inputTokens', usageTotals.inputTokens],
3692
3721
  ['cachedInputTokens', usageTotals.cachedInputTokens],
3722
+ ['cacheCreationInputTokens', usageTotals.cacheCreationInputTokens],
3693
3723
  ['outputTokens', usageTotals.outputTokens],
3694
3724
  ['reasoningOutputTokens', usageTotals.reasoningOutputTokens],
3695
3725
  ['totalTokens', usageTotals.totalTokens]
@@ -3940,12 +3970,13 @@ function parseCodexSessionSummary(filePath, options = {}) {
3940
3970
  let contextWindow = 0;
3941
3971
  let inputTokens = 0;
3942
3972
  let cachedInputTokens = 0;
3973
+ let cacheCreationInputTokens = 0;
3943
3974
  let outputTokens = 0;
3944
3975
  let reasoningOutputTokens = 0;
3945
3976
  let provider = 'codex';
3946
3977
  let model = '';
3947
3978
  const models = [];
3948
- const usageState = { totalTokens, contextWindow, inputTokens, cachedInputTokens, outputTokens, reasoningOutputTokens };
3979
+ const usageState = { totalTokens, contextWindow, inputTokens, cachedInputTokens, cacheCreationInputTokens, outputTokens, reasoningOutputTokens };
3949
3980
  const previewMessages = [];
3950
3981
 
3951
3982
  for (const record of records) {
@@ -3958,6 +3989,7 @@ function parseCodexSessionSummary(filePath, options = {}) {
3958
3989
  contextWindow = usageState.contextWindow || 0;
3959
3990
  inputTokens = usageState.inputTokens || 0;
3960
3991
  cachedInputTokens = usageState.cachedInputTokens || 0;
3992
+ cacheCreationInputTokens = usageState.cacheCreationInputTokens || 0;
3961
3993
  outputTokens = usageState.outputTokens || 0;
3962
3994
  reasoningOutputTokens = usageState.reasoningOutputTokens || 0;
3963
3995
 
@@ -3992,6 +4024,7 @@ function parseCodexSessionSummary(filePath, options = {}) {
3992
4024
  contextWindow = usageState.contextWindow || 0;
3993
4025
  inputTokens = usageState.inputTokens || 0;
3994
4026
  cachedInputTokens = usageState.cachedInputTokens || 0;
4027
+ cacheCreationInputTokens = usageState.cacheCreationInputTokens || 0;
3995
4028
  outputTokens = usageState.outputTokens || 0;
3996
4029
  reasoningOutputTokens = usageState.reasoningOutputTokens || 0;
3997
4030
  provider = readExplicitSessionProviderFromRecord(record) || provider;
@@ -4051,6 +4084,7 @@ function parseCodexSessionSummary(filePath, options = {}) {
4051
4084
  contextWindow,
4052
4085
  inputTokens,
4053
4086
  cachedInputTokens,
4087
+ cacheCreationInputTokens,
4054
4088
  outputTokens,
4055
4089
  reasoningOutputTokens,
4056
4090
  __messageCountExact: isSessionSummaryMessageCountExact(stat, summaryReadBytes),
@@ -4087,12 +4121,13 @@ function parseClaudeSessionSummary(filePath, options = {}) {
4087
4121
  let contextWindow = 0;
4088
4122
  let inputTokens = 0;
4089
4123
  let cachedInputTokens = 0;
4124
+ let cacheCreationInputTokens = 0;
4090
4125
  let outputTokens = 0;
4091
4126
  let reasoningOutputTokens = 0;
4092
4127
  let provider = 'claude';
4093
4128
  let model = '';
4094
4129
  const models = [];
4095
- const usageState = { totalTokens, contextWindow, inputTokens, cachedInputTokens, outputTokens, reasoningOutputTokens };
4130
+ const usageState = { totalTokens, contextWindow, inputTokens, cachedInputTokens, cacheCreationInputTokens, outputTokens, reasoningOutputTokens };
4096
4131
  const previewMessages = [];
4097
4132
  let createdAt = '';
4098
4133
  let updatedAt = stat.mtime.toISOString();
@@ -4110,6 +4145,7 @@ function parseClaudeSessionSummary(filePath, options = {}) {
4110
4145
  contextWindow = usageState.contextWindow || 0;
4111
4146
  inputTokens = usageState.inputTokens || 0;
4112
4147
  cachedInputTokens = usageState.cachedInputTokens || 0;
4148
+ cacheCreationInputTokens = usageState.cacheCreationInputTokens || 0;
4113
4149
  outputTokens = usageState.outputTokens || 0;
4114
4150
  reasoningOutputTokens = usageState.reasoningOutputTokens || 0;
4115
4151
 
@@ -4143,6 +4179,7 @@ function parseClaudeSessionSummary(filePath, options = {}) {
4143
4179
  contextWindow = usageState.contextWindow || 0;
4144
4180
  inputTokens = usageState.inputTokens || 0;
4145
4181
  cachedInputTokens = usageState.cachedInputTokens || 0;
4182
+ cacheCreationInputTokens = usageState.cacheCreationInputTokens || 0;
4146
4183
  outputTokens = usageState.outputTokens || 0;
4147
4184
  reasoningOutputTokens = usageState.reasoningOutputTokens || 0;
4148
4185
  provider = readExplicitSessionProviderFromRecord(record) || provider;
@@ -4201,6 +4238,7 @@ function parseClaudeSessionSummary(filePath, options = {}) {
4201
4238
  contextWindow,
4202
4239
  inputTokens,
4203
4240
  cachedInputTokens,
4241
+ cacheCreationInputTokens,
4204
4242
  outputTokens,
4205
4243
  reasoningOutputTokens,
4206
4244
  __messageCountExact: isSessionSummaryMessageCountExact(stat, summaryReadBytes),
@@ -4237,12 +4275,13 @@ function parseCodeBuddySessionSummary(filePath, options = {}) {
4237
4275
  let contextWindow = 0;
4238
4276
  let inputTokens = 0;
4239
4277
  let cachedInputTokens = 0;
4278
+ let cacheCreationInputTokens = 0;
4240
4279
  let outputTokens = 0;
4241
4280
  let reasoningOutputTokens = 0;
4242
4281
  let provider = 'codebuddy';
4243
4282
  let model = '';
4244
4283
  const models = [];
4245
- const usageState = { totalTokens, contextWindow, inputTokens, cachedInputTokens, outputTokens, reasoningOutputTokens };
4284
+ const usageState = { totalTokens, contextWindow, inputTokens, cachedInputTokens, cacheCreationInputTokens, outputTokens, reasoningOutputTokens };
4246
4285
  const previewMessages = [];
4247
4286
  let createdAt = '';
4248
4287
  let updatedAt = stat.mtime.toISOString();
@@ -4260,6 +4299,7 @@ function parseCodeBuddySessionSummary(filePath, options = {}) {
4260
4299
  contextWindow = usageState.contextWindow || 0;
4261
4300
  inputTokens = usageState.inputTokens || 0;
4262
4301
  cachedInputTokens = usageState.cachedInputTokens || 0;
4302
+ cacheCreationInputTokens = usageState.cacheCreationInputTokens || 0;
4263
4303
  outputTokens = usageState.outputTokens || 0;
4264
4304
  reasoningOutputTokens = usageState.reasoningOutputTokens || 0;
4265
4305
 
@@ -4298,6 +4338,7 @@ function parseCodeBuddySessionSummary(filePath, options = {}) {
4298
4338
  contextWindow = usageState.contextWindow || 0;
4299
4339
  inputTokens = usageState.inputTokens || 0;
4300
4340
  cachedInputTokens = usageState.cachedInputTokens || 0;
4341
+ cacheCreationInputTokens = usageState.cacheCreationInputTokens || 0;
4301
4342
  outputTokens = usageState.outputTokens || 0;
4302
4343
  reasoningOutputTokens = usageState.reasoningOutputTokens || 0;
4303
4344
  provider = readExplicitSessionProviderFromRecord(record) || provider;
@@ -4358,6 +4399,7 @@ function parseCodeBuddySessionSummary(filePath, options = {}) {
4358
4399
  contextWindow,
4359
4400
  inputTokens,
4360
4401
  cachedInputTokens,
4402
+ cacheCreationInputTokens,
4361
4403
  outputTokens,
4362
4404
  reasoningOutputTokens,
4363
4405
  __messageCountExact: isSessionSummaryMessageCountExact(stat, summaryReadBytes),
@@ -4647,17 +4689,19 @@ function listClaudeSessions(limit, options = {}) {
4647
4689
  let contextWindow = 0;
4648
4690
  let inputTokens = 0;
4649
4691
  let cachedInputTokens = 0;
4692
+ let cacheCreationInputTokens = 0;
4650
4693
  let outputTokens = 0;
4651
4694
  let reasoningOutputTokens = 0;
4652
4695
  let model = typeof entry.model === 'string' ? entry.model.trim() : '';
4653
4696
  const models = model ? [model] : [];
4654
4697
 
4655
- const usageState = { totalTokens, contextWindow, inputTokens, cachedInputTokens, outputTokens, reasoningOutputTokens };
4698
+ const usageState = { totalTokens, contextWindow, inputTokens, cachedInputTokens, cacheCreationInputTokens, outputTokens, reasoningOutputTokens };
4656
4699
  applySessionUsageSummaryFromIndexEntry(usageState, entry);
4657
4700
  totalTokens = usageState.totalTokens || 0;
4658
4701
  contextWindow = usageState.contextWindow || 0;
4659
4702
  inputTokens = usageState.inputTokens || 0;
4660
4703
  cachedInputTokens = usageState.cachedInputTokens || 0;
4704
+ cacheCreationInputTokens = usageState.cacheCreationInputTokens || 0;
4661
4705
  outputTokens = usageState.outputTokens || 0;
4662
4706
  reasoningOutputTokens = usageState.reasoningOutputTokens || 0;
4663
4707
 
@@ -4688,6 +4732,7 @@ function listClaudeSessions(limit, options = {}) {
4688
4732
  contextWindow = usageState.contextWindow || 0;
4689
4733
  inputTokens = usageState.inputTokens || 0;
4690
4734
  cachedInputTokens = usageState.cachedInputTokens || 0;
4735
+ cacheCreationInputTokens = usageState.cacheCreationInputTokens || 0;
4691
4736
  outputTokens = usageState.outputTokens || 0;
4692
4737
  reasoningOutputTokens = usageState.reasoningOutputTokens || 0;
4693
4738
  const filteredQuickMessages = removeLeadingSystemMessage(quickMessages);
@@ -4712,6 +4757,7 @@ function listClaudeSessions(limit, options = {}) {
4712
4757
  contextWindow = usageState.contextWindow || 0;
4713
4758
  inputTokens = usageState.inputTokens || 0;
4714
4759
  cachedInputTokens = usageState.cachedInputTokens || 0;
4760
+ cacheCreationInputTokens = usageState.cacheCreationInputTokens || 0;
4715
4761
  outputTokens = usageState.outputTokens || 0;
4716
4762
  reasoningOutputTokens = usageState.reasoningOutputTokens || 0;
4717
4763
 
@@ -4735,6 +4781,7 @@ function listClaudeSessions(limit, options = {}) {
4735
4781
  contextWindow,
4736
4782
  inputTokens,
4737
4783
  cachedInputTokens,
4784
+ cacheCreationInputTokens,
4738
4785
  outputTokens,
4739
4786
  reasoningOutputTokens,
4740
4787
  model,
@@ -10017,6 +10064,7 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
10017
10064
  result = {
10018
10065
  provider: config.model_provider || '未设置',
10019
10066
  model: config.model || '未设置',
10067
+ currentModels: readCurrentModels(),
10020
10068
  serviceTier,
10021
10069
  modelReasoningEffort,
10022
10070
  modelContextWindow,
@@ -10133,6 +10181,10 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
10133
10181
  break;
10134
10182
  case 'apply-claude-md-file':
10135
10183
  result = applyClaudeMdFile(params || {});
10184
+ if (result && !result.error) {
10185
+ const mdTarget = (params && params.targetPath) ? String(params.targetPath) : 'CLAUDE.md';
10186
+ notifyWebhook('claude-md-edit', 'CLAUDE.md modified: ' + mdTarget, { targetPath: mdTarget }).catch(function () {});
10187
+ }
10136
10188
  break;
10137
10189
  case 'preview-agents-diff':
10138
10190
  result = buildAgentsDiff(params || {});
@@ -10208,7 +10260,32 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
10208
10260
  break;
10209
10261
  case 'apply-claude-config':
10210
10262
  result = applyToClaudeSettings(params.config);
10263
+ if (result && !result.error) {
10264
+ const cfgName = (params && params.config && typeof params.config.name === 'string') ? params.config.name : '';
10265
+ const cfgFrom = (params && typeof params.previousName === 'string') ? params.previousName : '';
10266
+ const summary = cfgFrom
10267
+ ? ('Provider switched: ' + cfgFrom + ' -> ' + cfgName)
10268
+ : ('Provider applied: ' + cfgName);
10269
+ notifyWebhook('provider-switch', summary, { name: cfgName, previousName: cfgFrom }).catch(function () {});
10270
+ }
10211
10271
  break;
10272
+ case 'get-webhook-config':
10273
+ result = loadWebhookConfig();
10274
+ break;
10275
+ case 'set-webhook-config':
10276
+ result = saveWebhookConfig(params && params.config ? params.config : {});
10277
+ break;
10278
+ case 'test-webhook': {
10279
+ const overrideCfg = params && params.config ? params.config : null;
10280
+ const probe = await notifyWebhook(
10281
+ 'provider-switch',
10282
+ 'codexmate webhook test ping',
10283
+ { test: true },
10284
+ overrideCfg ? { config: overrideCfg } : {}
10285
+ );
10286
+ result = probe;
10287
+ break;
10288
+ }
10212
10289
  case 'export-claude-share':
10213
10290
  result = buildClaudeSharePayload(params && params.config ? params.config : {});
10214
10291
  break;
@@ -12360,35 +12437,47 @@ function buildMcpProviderListPayload() {
12360
12437
  configReady: !listConfigResult.isVirtual,
12361
12438
  configErrorType: listConfigResult.errorType || '',
12362
12439
  configNotice: listConfigResult.reason || '',
12363
- providers: Object.entries(providers).map(([name, p]) => ({
12364
- name,
12365
- url: p.base_url || '',
12366
- key: maskKey(p.preferred_auth_method || ''),
12367
- hasKey: !!(p.preferred_auth_method && p.preferred_auth_method.trim()),
12368
- models: Array.isArray(p.models)
12369
- ? p.models
12370
- .filter((model) => model && typeof model === 'object' && !Array.isArray(model))
12371
- .map((model) => ({
12372
- id: typeof model.id === 'string' ? model.id : '',
12373
- name: typeof model.name === 'string' ? model.name : '',
12374
- cost: model.cost && typeof model.cost === 'object' && !Array.isArray(model.cost)
12375
- ? {
12376
- input: model.cost.input,
12377
- output: model.cost.output,
12378
- cacheRead: model.cost.cacheRead,
12379
- cacheWrite: model.cost.cacheWrite
12380
- }
12381
- : null,
12382
- contextWindow: model.contextWindow,
12383
- maxTokens: model.maxTokens
12384
- }))
12385
- .filter((model) => model.id)
12386
- : [],
12387
- current: name === current,
12388
- readOnly: isBuiltinManagedProvider(name),
12389
- nonDeletable: isNonDeletableProvider(name),
12390
- nonEditable: isNonEditableProvider(name)
12391
- }))
12440
+ providers: Object.entries(providers).map(([name, p]) => {
12441
+ const bridge = typeof p.codexmate_bridge === 'string' ? p.codexmate_bridge.trim() : '';
12442
+ let upstreamUrl = '';
12443
+ if (bridge === 'openai') {
12444
+ const upstream = resolveOpenaiBridgeUpstream(OPENAI_BRIDGE_SETTINGS_FILE, name);
12445
+ if (upstream && !upstream.error && typeof upstream.baseUrl === 'string') {
12446
+ upstreamUrl = upstream.baseUrl.trim();
12447
+ }
12448
+ }
12449
+ return {
12450
+ name,
12451
+ url: p.base_url || '',
12452
+ upstreamUrl,
12453
+ codexmate_bridge: bridge,
12454
+ key: maskKey(p.preferred_auth_method || ''),
12455
+ hasKey: !!(p.preferred_auth_method && p.preferred_auth_method.trim()),
12456
+ models: Array.isArray(p.models)
12457
+ ? p.models
12458
+ .filter((model) => model && typeof model === 'object' && !Array.isArray(model))
12459
+ .map((model) => ({
12460
+ id: typeof model.id === 'string' ? model.id : '',
12461
+ name: typeof model.name === 'string' ? model.name : '',
12462
+ cost: model.cost && typeof model.cost === 'object' && !Array.isArray(model.cost)
12463
+ ? {
12464
+ input: model.cost.input,
12465
+ output: model.cost.output,
12466
+ cacheRead: model.cost.cacheRead,
12467
+ cacheWrite: model.cost.cacheWrite
12468
+ }
12469
+ : null,
12470
+ contextWindow: model.contextWindow,
12471
+ maxTokens: model.maxTokens
12472
+ }))
12473
+ .filter((model) => model.id)
12474
+ : [],
12475
+ current: name === current,
12476
+ readOnly: isBuiltinManagedProvider(name),
12477
+ nonDeletable: isNonDeletableProvider(name),
12478
+ nonEditable: isNonEditableProvider(name)
12479
+ };
12480
+ })
12392
12481
  };
12393
12482
  }
12394
12483
 
@@ -48,37 +48,86 @@ const ANTHROPIC_CLAUDE_MODELS = Object.freeze([
48
48
  'claude-3-haiku'
49
49
  ]);
50
50
 
51
+ const DEEPSEEK_CLAUDE_COMPAT_MODELS = Object.freeze([
52
+ 'DeepSeek-V3.2',
53
+ 'DeepSeek-V3',
54
+ 'DeepSeek-R1',
55
+ 'deepseek-chat'
56
+ ]);
57
+
58
+ const QWEN_CLAUDE_COMPAT_MODELS = Object.freeze([
59
+ 'qwen3-coder',
60
+ 'qwen-max',
61
+ 'qwen-plus',
62
+ 'qwen-turbo'
63
+ ]);
64
+
65
+ const MODELSCOPE_CLAUDE_COMPAT_MODELS = Object.freeze([
66
+ 'ZhipuAI/GLM-5'
67
+ ]);
68
+
51
69
  function normalizeModelCatalogId(value) {
52
70
  return typeof value === 'string' ? value.trim().toLowerCase() : '';
53
71
  }
54
72
 
55
- function isBigModelClaudeCompatibleBaseUrl(baseUrl) {
73
+ function hasPathSegment(baseUrl, segment) {
56
74
  const normalized = normalizeBaseUrl(baseUrl);
57
75
  if (!normalized) return false;
58
76
  try {
59
77
  const parsed = new URL(normalized);
60
- const host = String(parsed.hostname || '').toLowerCase();
61
78
  const pathname = String(parsed.pathname || '').toLowerCase();
62
- const isBigModelHost = host === 'bigmodel.cn' || host.endsWith('.bigmodel.cn');
63
- const hasAnthropicSegment = /(^|\/)anthropic(\/|$)/.test(pathname);
64
- return isBigModelHost && hasAnthropicSegment;
79
+ const escaped = String(segment || '').replace(/[.*+?^${}()|[\]\\]/g, '\\$&').toLowerCase();
80
+ return new RegExp(`(^|/)${escaped}(/|$)`).test(pathname);
65
81
  } catch (_) {
66
82
  return false;
67
83
  }
68
84
  }
69
85
 
70
- function isAnthropicBaseUrl(baseUrl) {
86
+ function getBaseUrlHost(baseUrl) {
71
87
  const normalized = normalizeBaseUrl(baseUrl);
72
- if (!normalized) return false;
88
+ if (!normalized) return '';
73
89
  try {
74
90
  const parsed = new URL(normalized);
75
- const host = String(parsed.hostname || '').toLowerCase();
76
- return host === 'api.anthropic.com' || host.endsWith('.anthropic.com');
91
+ return String(parsed.hostname || '').toLowerCase();
77
92
  } catch (_) {
78
- return false;
93
+ return '';
79
94
  }
80
95
  }
81
96
 
97
+ function isHostOrSubdomain(host, domain) {
98
+ return host === domain || host.endsWith(`.${domain}`);
99
+ }
100
+
101
+ function isBigModelClaudeCompatibleBaseUrl(baseUrl) {
102
+ const host = getBaseUrlHost(baseUrl);
103
+ return isHostOrSubdomain(host, 'bigmodel.cn') && hasPathSegment(baseUrl, 'anthropic');
104
+ }
105
+
106
+ function isAnthropicBaseUrl(baseUrl) {
107
+ const host = getBaseUrlHost(baseUrl);
108
+ return isHostOrSubdomain(host, 'anthropic.com');
109
+ }
110
+
111
+ function isDeepSeekClaudeCompatibleBaseUrl(baseUrl) {
112
+ const host = getBaseUrlHost(baseUrl);
113
+ return isHostOrSubdomain(host, 'deepseek.com') && hasPathSegment(baseUrl, 'anthropic');
114
+ }
115
+
116
+ function isQwenClaudeCompatibleBaseUrl(baseUrl) {
117
+ const host = getBaseUrlHost(baseUrl);
118
+ return isHostOrSubdomain(host, 'dashscope.aliyuncs.com') && hasPathSegment(baseUrl, 'anthropic');
119
+ }
120
+
121
+ function isZaiClaudeCompatibleBaseUrl(baseUrl) {
122
+ const host = getBaseUrlHost(baseUrl);
123
+ return isHostOrSubdomain(host, 'z.ai') && hasPathSegment(baseUrl, 'anthropic');
124
+ }
125
+
126
+ function isModelScopeBaseUrl(baseUrl) {
127
+ const host = getBaseUrlHost(baseUrl);
128
+ return isHostOrSubdomain(host, 'modelscope.cn');
129
+ }
130
+
82
131
  function getSupplementalModelsForBaseUrl(baseUrl) {
83
132
  if (isBigModelClaudeCompatibleBaseUrl(baseUrl)) {
84
133
  return [...BIGMODEL_CLAUDE_COMPAT_MODELS];
@@ -86,6 +135,18 @@ function getSupplementalModelsForBaseUrl(baseUrl) {
86
135
  if (isAnthropicBaseUrl(baseUrl)) {
87
136
  return [...ANTHROPIC_CLAUDE_MODELS];
88
137
  }
138
+ if (isDeepSeekClaudeCompatibleBaseUrl(baseUrl)) {
139
+ return [...DEEPSEEK_CLAUDE_COMPAT_MODELS];
140
+ }
141
+ if (isQwenClaudeCompatibleBaseUrl(baseUrl)) {
142
+ return [...QWEN_CLAUDE_COMPAT_MODELS];
143
+ }
144
+ if (isZaiClaudeCompatibleBaseUrl(baseUrl)) {
145
+ return [...BIGMODEL_CLAUDE_COMPAT_MODELS];
146
+ }
147
+ if (isModelScopeBaseUrl(baseUrl)) {
148
+ return [...MODELSCOPE_CLAUDE_COMPAT_MODELS];
149
+ }
89
150
  return [];
90
151
  }
91
152
 
@@ -0,0 +1,126 @@
1
+ const path = require('path');
2
+ const fs = require('fs');
3
+ const http = require('http');
4
+ const https = require('https');
5
+ const os = require('os');
6
+
7
+ const ALLOWED_EVENTS = ['provider-switch', 'claude-md-edit'];
8
+ const DEFAULT_TIMEOUT_MS = 5000;
9
+
10
+ function defaultConfigPath() {
11
+ return path.join(os.homedir(), '.codex', 'codexmate-webhook.json');
12
+ }
13
+
14
+ function normalizeConfig(cfg) {
15
+ const out = { enabled: false, url: '', events: ALLOWED_EVENTS.slice() };
16
+ if (!cfg || typeof cfg !== 'object') return out;
17
+ out.enabled = !!cfg.enabled;
18
+ out.url = typeof cfg.url === 'string' ? cfg.url.trim() : '';
19
+ if (Array.isArray(cfg.events)) {
20
+ const filtered = cfg.events.filter(function (e) { return ALLOWED_EVENTS.indexOf(e) !== -1; });
21
+ out.events = filtered.length ? filtered : ALLOWED_EVENTS.slice();
22
+ }
23
+ return out;
24
+ }
25
+
26
+ function loadWebhookConfig(filePath) {
27
+ const target = filePath || defaultConfigPath();
28
+ try {
29
+ if (!fs.existsSync(target)) {
30
+ return normalizeConfig({});
31
+ }
32
+ const raw = fs.readFileSync(target, 'utf-8');
33
+ return normalizeConfig(JSON.parse(raw));
34
+ } catch (_) {
35
+ return normalizeConfig({});
36
+ }
37
+ }
38
+
39
+ function saveWebhookConfig(cfg, filePath) {
40
+ const target = filePath || defaultConfigPath();
41
+ const normalized = normalizeConfig(cfg);
42
+ try {
43
+ fs.mkdirSync(path.dirname(target), { recursive: true });
44
+ } catch (_) {}
45
+ fs.writeFileSync(target, JSON.stringify(normalized, null, 2), 'utf-8');
46
+ return normalized;
47
+ }
48
+
49
+ function postJson(targetUrl, payload, timeoutMs) {
50
+ return new Promise(function (resolve) {
51
+ let parsed;
52
+ try {
53
+ parsed = new URL(targetUrl);
54
+ } catch (_) {
55
+ resolve({ ok: false, error: 'invalid-url' });
56
+ return;
57
+ }
58
+ const transport = parsed.protocol === 'https:' ? https : http;
59
+ const body = JSON.stringify(payload || {});
60
+ let req;
61
+ try {
62
+ req = transport.request({
63
+ method: 'POST',
64
+ protocol: parsed.protocol,
65
+ hostname: parsed.hostname,
66
+ port: parsed.port || (parsed.protocol === 'https:' ? 443 : 80),
67
+ path: (parsed.pathname || '/') + (parsed.search || ''),
68
+ headers: {
69
+ 'Content-Type': 'application/json; charset=utf-8',
70
+ 'Content-Length': Buffer.byteLength(body, 'utf-8'),
71
+ 'User-Agent': 'codexmate-webhook/1'
72
+ }
73
+ }, function (res) {
74
+ let raw = '';
75
+ res.on('data', function (chunk) {
76
+ if (raw.length < 1024) raw += chunk.toString('utf-8');
77
+ });
78
+ res.on('end', function () {
79
+ const status = res.statusCode || 0;
80
+ resolve({ ok: status >= 200 && status < 300, status: status, body: raw.slice(0, 200) });
81
+ });
82
+ });
83
+ } catch (e) {
84
+ resolve({ ok: false, error: e && e.message ? e.message : String(e) });
85
+ return;
86
+ }
87
+ const wait = Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : DEFAULT_TIMEOUT_MS;
88
+ req.setTimeout(wait, function () { req.destroy(new Error('timeout')); });
89
+ req.on('error', function (err) { resolve({ ok: false, error: err && err.message ? err.message : String(err) }); });
90
+ req.write(body);
91
+ req.end();
92
+ });
93
+ }
94
+
95
+ function buildPayload(event, summary, details) {
96
+ return {
97
+ event: String(event || ''),
98
+ summary: String(summary || ''),
99
+ operator: process.env.USER || process.env.USERNAME || (os.userInfo && os.userInfo().username) || '',
100
+ timestamp: new Date().toISOString(),
101
+ details: details && typeof details === 'object' ? details : {}
102
+ };
103
+ }
104
+
105
+ function notifyWebhook(event, summary, details, options) {
106
+ const opts = options || {};
107
+ const cfg = opts.config ? normalizeConfig(opts.config) : loadWebhookConfig(opts.filePath);
108
+ if (!cfg.enabled || !cfg.url) {
109
+ return Promise.resolve({ ok: false, skipped: true, reason: 'disabled' });
110
+ }
111
+ if (cfg.events.indexOf(event) === -1) {
112
+ return Promise.resolve({ ok: false, skipped: true, reason: 'event-filtered' });
113
+ }
114
+ return postJson(cfg.url, buildPayload(event, summary, details), opts.timeoutMs);
115
+ }
116
+
117
+ module.exports = {
118
+ ALLOWED_EVENTS,
119
+ defaultConfigPath,
120
+ normalizeConfig,
121
+ loadWebhookConfig,
122
+ saveWebhookConfig,
123
+ notifyWebhook,
124
+ buildPayload,
125
+ postJson
126
+ };