@vantran-se/openchamber-web 1.22.1 → 1.22.2

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 (163) hide show
  1. package/README.md +2 -3
  2. package/bin/cli.js +4 -1
  3. package/bin/cli.test.js +92 -0
  4. package/bin/lib/cli-startup.js +49 -0
  5. package/bin/lib/commands-startup.js +41 -6
  6. package/dist/assets/{CodeMirrorCommentWidgets-C53Q7pbX.js → CodeMirrorCommentWidgets-CV1HNV5l.js} +1 -1
  7. package/dist/assets/{DiffView-Dh1ey2x9.js → DiffView-DkkSa30n.js} +3 -3
  8. package/dist/assets/DirtyBranchSwitchDialog-DW4WaXPc.js +1 -0
  9. package/dist/assets/FilesView-DDu8zDy5.js +17 -0
  10. package/dist/assets/GitView-1Ixo8z-M.js +6 -0
  11. package/dist/assets/{JsonTreeView-Bl0lp9Rs.js → JsonTreeView-DvCLL7dT.js} +1 -1
  12. package/dist/assets/LinearIssuesView-Bs1rZrF2.js +1 -0
  13. package/dist/assets/{MarkdownImageGallery-bA0PS-tn.js → MarkdownImageGallery-DyJUc6h9.js} +1 -1
  14. package/dist/assets/MarkdownRendererImpl-Bdm8vMkS.js +12 -0
  15. package/dist/assets/{OnboardingScreen-D3kYJIb6.js → OnboardingScreen-Dg66B6oB.js} +1 -1
  16. package/dist/assets/{PatchDiff-D6NXAfJk.js → PatchDiff-CAduNjfZ.js} +1 -1
  17. package/dist/assets/{PierreDiffViewer-Ct2A13NO.js → PierreDiffViewer-CTIb1GYD.js} +2 -2
  18. package/dist/assets/PlanView-trDaszfq.js +5 -0
  19. package/dist/assets/SettingsView-DStMfBmy.js +28 -0
  20. package/dist/assets/{SettingsWindow-BiWDBRvb.js → SettingsWindow-DQQFnrSZ.js} +1 -1
  21. package/dist/assets/{ToolOutputDialog-BQexMQIm.js → ToolOutputDialog-SjY2XZlA.js} +6 -6
  22. package/dist/assets/ToolPartDiffPreview-DJdzUQPE.js +8 -0
  23. package/dist/assets/{VirtualizedFileDiff-Dpz2ra8h.js → VirtualizedFileDiff-Iehm4iMH.js} +1 -1
  24. package/dist/assets/{WalkthroughView-AX-u2pFg.js → WalkthroughView-BALp2d9O.js} +2 -2
  25. package/dist/assets/{computeVirtualFileMetrics-DtyozJdt.js → computeVirtualFileMetrics-BKZA43vN.js} +2 -2
  26. package/dist/assets/{contextPanelEmbeddedChat-C9EcRSDF.js → contextPanelEmbeddedChat-D2PM_pEv.js} +1 -1
  27. package/dist/assets/{de-DqVn5vme.js → de-DlSNLAcy.js} +6 -5
  28. package/dist/assets/{es-hj43Z53i.js → es-D1GbowZ3.js} +6 -5
  29. package/dist/assets/{fr-BmkZt2mF.js → fr-BPOLXIAY.js} +6 -5
  30. package/dist/assets/{index-DmhujXbT.js → index-Ce0LR32V.js} +1 -1
  31. package/dist/assets/index-D42fPvt4.css +1 -0
  32. package/dist/assets/index-DlFbCkEy.js +39 -0
  33. package/dist/assets/{isDiffPlainText-XyulMHxx.js → isDiffPlainText-DaB2u_Kv.js} +1 -1
  34. package/dist/assets/{isFilePlainText-B19Ddps1.js → isFilePlainText-yJZa2IAC.js} +1 -1
  35. package/dist/assets/{ja-DioMIEGF.js → ja-Dm2-m9io.js} +7 -6
  36. package/dist/assets/{ko-BqPgaQJb.js → ko-CIXlzQ5-.js} +7 -6
  37. package/dist/assets/main-B7qTsd_w.js +930 -0
  38. package/dist/assets/{main-B2a_JfKZ.js → main-CyQrD9dG.js} +2 -2
  39. package/dist/assets/{markdownCore-CWRCqSH2.js → markdownCore-CeEwGgEK.js} +2 -2
  40. package/dist/assets/{miniChat-B3CSU_Ps.js → miniChat-7cPEx4sV.js} +2 -2
  41. package/dist/assets/{mobile-QNcFIVCW.js → mobile-QtzkPTM8.js} +2 -2
  42. package/dist/assets/{patchFileDiff-BpMQZnYu.js → patchFileDiff-No55fDiD.js} +1 -1
  43. package/dist/assets/pl-B2zmkOpw.js +16 -0
  44. package/dist/assets/{pt-BR-BqH8fuCH.js → pt-BR-C-JE5H6y.js} +6 -5
  45. package/dist/assets/{renderElectronMiniChatApp-D77bmLqo.js → renderElectronMiniChatApp-ePTkBJNS.js} +2 -2
  46. package/dist/assets/renderMobileApp-D9RjB9DX.js +25 -0
  47. package/dist/assets/runtimeEndpointReset-DfJIWwwo.js +17 -0
  48. package/dist/assets/{scheduledTasksApi-BSRCtP6T.js → scheduledTasksApi-DuhdyYEa.js} +1 -1
  49. package/dist/assets/{shikiHighlight-DE-OwqGw.js → shikiHighlight-EHEg58-n.js} +1 -1
  50. package/dist/assets/tr-MJmRIzEY.js +7 -0
  51. package/dist/assets/{uk-BkkfxbOs.js → uk-BmD7eM8k.js} +6 -5
  52. package/dist/assets/{useAppFontEffects-COAeIX-D.js → useAppFontEffects-CuJPKtI1.js} +151 -180
  53. package/dist/assets/useInlineCommentController-DD7_7OP_.js +2 -0
  54. package/dist/assets/{useRootScrollLock-C0N9mCwz.js → useRootScrollLock-DRGAe8PN.js} +1 -1
  55. package/dist/assets/{vendor-codemirror-language-data-BG3v1Ghq.js → vendor-codemirror-language-data-9ZzoH0o0.js} +2 -2
  56. package/dist/assets/vendor-zod-CK21ihK-.js +39 -0
  57. package/dist/assets/{web-components-BNMwa0qm.js → web-components-PttKBrhi.js} +1 -1
  58. package/dist/assets/{zh-CN-C6FMdVgv.js → zh-CN-JJHl4ehT.js} +6 -5
  59. package/dist/assets/{zh-TW-CdcMjDxv.js → zh-TW-DPc4DEEW.js} +6 -5
  60. package/dist/index.html +5 -5
  61. package/dist/mini-chat.html +4 -4
  62. package/dist/mobile.html +4 -4
  63. package/package.json +8 -4
  64. package/server/index.js +38 -5
  65. package/server/lib/agent-tool/runtime.js +2 -19
  66. package/server/lib/changelog/update-notes.js +82 -0
  67. package/server/lib/changelog/update-notes.test.js +59 -0
  68. package/server/lib/dev-tunnel/DOCUMENTATION.md +13 -4
  69. package/server/lib/dev-tunnel/runtime.js +4 -4
  70. package/server/lib/dev-tunnel/tunnel.test.js +20 -0
  71. package/server/lib/fs/DOCUMENTATION.md +2 -0
  72. package/server/lib/fs/routes.js +32 -21
  73. package/server/lib/fs/routes.test.js +77 -0
  74. package/server/lib/git/DOCUMENTATION.md +3 -1
  75. package/server/lib/git/routes.js +16 -0
  76. package/server/lib/git/service.js +107 -17
  77. package/server/lib/git/service.test.js +171 -0
  78. package/server/lib/mcp-reconnect/DOCUMENTATION.md +55 -0
  79. package/server/lib/mcp-reconnect/runtime.js +122 -0
  80. package/server/lib/mcp-reconnect/runtime.test.js +140 -0
  81. package/server/lib/message-queue/DOCUMENTATION.md +158 -0
  82. package/server/lib/message-queue/runtime.js +870 -0
  83. package/server/lib/message-queue/runtime.test.js +482 -0
  84. package/server/lib/openchamber-sessions/routes.js +102 -0
  85. package/server/lib/openchamber-sessions/routes.test.js +88 -0
  86. package/server/lib/opencode/DOCUMENTATION.md +13 -5
  87. package/server/lib/opencode/config-paths.test.js +116 -0
  88. package/server/lib/opencode/core-routes.js +1 -0
  89. package/server/lib/opencode/feature-routes-runtime.js +5 -0
  90. package/server/lib/opencode/input-history-scope.js +14 -0
  91. package/server/lib/opencode/managed-plugin-config.js +35 -0
  92. package/server/lib/opencode/openchamber-routes.test.js +2 -2
  93. package/server/lib/opencode/plugins.js +2 -2
  94. package/server/lib/opencode/providers.js +44 -2
  95. package/server/lib/opencode/providers.test.js +131 -4
  96. package/server/lib/opencode/routes.js +4 -4
  97. package/server/lib/opencode/server-startup-runtime.js +1 -1
  98. package/server/lib/opencode/settings-helpers.js +28 -0
  99. package/server/lib/opencode/settings-helpers.test.js +177 -0
  100. package/server/lib/opencode/settings-runtime.js +2 -2
  101. package/server/lib/opencode/settings-runtime.test.js +29 -0
  102. package/server/lib/opencode/shared.js +4 -1
  103. package/server/lib/opencode/shutdown-runtime.js +2 -0
  104. package/server/lib/opencode/skill-routes.js +2 -1
  105. package/server/lib/opencode/snippets.js +1 -2
  106. package/server/lib/package-manager.js +4 -29
  107. package/server/lib/package-manager.test.js +2 -2
  108. package/server/lib/quota/DOCUMENTATION.md +2 -1
  109. package/server/lib/quota/credentials/providers.js +4 -0
  110. package/server/lib/quota/credentials/store.js +1 -1
  111. package/server/lib/quota/credentials/store.test.js +4 -4
  112. package/server/lib/quota/providers/exe-dev.js +75 -0
  113. package/server/lib/quota/providers/exe-dev.test.js +47 -0
  114. package/server/lib/quota/providers/index.js +7 -0
  115. package/server/lib/quota/providers/opencode-go.js +1 -0
  116. package/server/lib/quota/providers/opencode-go.test.js +5 -1
  117. package/server/lib/quota/routes.js +2 -0
  118. package/server/lib/quota/utils/auth.js +1 -1
  119. package/server/lib/relay/DOCUMENTATION.md +1 -1
  120. package/server/lib/relay/host-client.test.js +9 -1
  121. package/server/lib/relay/tunnel-host.js +3 -1
  122. package/server/lib/relay/tunnel-host.test.js +9 -1
  123. package/server/lib/security/request-security.js +21 -6
  124. package/server/lib/security/request-security.test.js +42 -8
  125. package/server/lib/session-assist/runtime.js +1 -0
  126. package/server/lib/session-goal/DOCUMENTATION.md +20 -7
  127. package/server/lib/session-goal/create.js +3 -1
  128. package/server/lib/session-goal/runtime.js +77 -11
  129. package/server/lib/session-goal/runtime.test.js +426 -0
  130. package/server/lib/skills-catalog/DOCUMENTATION.md +1 -1
  131. package/server/lib/skills-catalog/install.js +3 -2
  132. package/server/lib/small-model/DOCUMENTATION.md +4 -0
  133. package/server/lib/small-model/call.js +8 -2
  134. package/server/lib/small-model/call.test.js +23 -0
  135. package/server/lib/small-model/index.js +2 -1
  136. package/server/lib/small-model/routes.js +2 -1
  137. package/server/lib/system-prompt/runtime.js +2 -21
  138. package/server/lib/terminal/DOCUMENTATION.md +11 -7
  139. package/server/lib/terminal/runtime.js +180 -26
  140. package/server/lib/terminal/runtime.test.js +553 -26
  141. package/server/lib/terminal/shells.js +17 -0
  142. package/server/lib/terminal/shells.test.js +32 -1
  143. package/server/lib/ui-auth/ui-auth.js +4 -1
  144. package/server/lib/ui-auth/ui-auth.test.js +16 -0
  145. package/server/lib/walkthrough/index.js +1 -0
  146. package/server/lib/walkthrough/routes.test.js +26 -5
  147. package/dist/assets/CommitSection-X-f2RgYr.js +0 -1
  148. package/dist/assets/FilesView-3ZYP72ki.js +0 -17
  149. package/dist/assets/GitView-CMuN7z2Z.js +0 -6
  150. package/dist/assets/LinearIssuesView-CQiUDU8b.js +0 -1
  151. package/dist/assets/MarkdownRendererImpl-BGQi2PLq.js +0 -12
  152. package/dist/assets/PlanView-6AUjzNQn.js +0 -5
  153. package/dist/assets/SettingsView-CHI8fFaK.js +0 -28
  154. package/dist/assets/ToolPartDiffPreview-DPAATNn6.js +0 -8
  155. package/dist/assets/index-L_ncV-6H.css +0 -1
  156. package/dist/assets/index-qES19dlJ.js +0 -7
  157. package/dist/assets/main-CYgSu6dx.js +0 -928
  158. package/dist/assets/pl-slULBF4R.js +0 -15
  159. package/dist/assets/renderMobileApp-BQ3Kdm-d.js +0 -25
  160. package/dist/assets/runtimeEndpointReset-CRGn1gVj.js +0 -16
  161. package/dist/assets/tr-CjEbw1Aw.js +0 -6
  162. package/dist/assets/useInlineCommentController-JsBboQwt.js +0 -2
  163. package/dist/assets/vendor-zod-BLfBSgBh.js +0 -39
@@ -1,12 +1,12 @@
1
1
  import express from 'express';
2
2
  import { createProjectIdFromPath } from '../projects/project-id.js';
3
3
  import fs from 'fs';
4
- import os from 'os';
5
4
  import path from 'path';
6
5
  import {
7
6
  buildDeferredRestartResponse,
8
7
  } from './config-mutation-response.js';
9
8
  import { getClaudeCliAuthStatus } from './claude-cli-auth.js';
9
+ import { OPENCODE_CONFIG_DIR } from './shared.js';
10
10
 
11
11
  export const registerOpenCodeRoutes = (app, dependencies) => {
12
12
  const {
@@ -807,7 +807,7 @@ ${desktopReturn ? `<a class="return" href="openchamber://focus/mcp-auth">Return
807
807
  });
808
808
 
809
809
  // Behavior / Global AGENTS.md endpoints
810
- const AGENTS_MD_PATH = path.join(os.homedir(), '.config', 'opencode', 'AGENTS.md');
810
+ const AGENTS_MD_PATH = path.join(OPENCODE_CONFIG_DIR, 'AGENTS.md');
811
811
  const MAX_BEHAVIOR_PROMPT_SIZE = 1024 * 1024; // 1 MB
812
812
 
813
813
  app.get('/api/behavior/agents-md', async (_req, res) => {
@@ -815,10 +815,10 @@ ${desktopReturn ? `<a class="return" href="openchamber://focus/mcp-auth">Return
815
815
  try {
816
816
  await fs.promises.access(AGENTS_MD_PATH);
817
817
  } catch {
818
- return res.json({ content: '', exists: false });
818
+ return res.json({ content: '', exists: false, path: AGENTS_MD_PATH });
819
819
  }
820
820
  const content = await fs.promises.readFile(AGENTS_MD_PATH, 'utf8');
821
- return res.json({ content, exists: true });
821
+ return res.json({ content, exists: true, path: AGENTS_MD_PATH });
822
822
  } catch (error) {
823
823
  console.error('Failed to read AGENTS.md:', error);
824
824
  return res.status(500).json({ error: 'Failed to read AGENTS.md' });
@@ -138,7 +138,7 @@ export const createServerStartupRuntime = (dependencies) => {
138
138
  // Cover every signal a shell or dev harness may use to stop/restart us, so
139
139
  // the managed OpenCode child is always torn down gracefully instead of
140
140
  // orphaned: SIGINT/SIGQUIT (Ctrl+C/Ctrl+\), SIGTERM (kill/default), SIGHUP
141
- // (terminal close), SIGUSR2 (nodemon restart for `dev:server:watch`).
141
+ // (terminal close), SIGUSR2 (nodemon restart outside Windows).
142
142
  process.on('SIGTERM', handleSignal);
143
143
  process.on('SIGINT', handleSignal);
144
144
  process.on('SIGQUIT', handleSignal);
@@ -1,4 +1,10 @@
1
1
  import { isAgentMemoryFeatureAvailable } from '../agent-memory/feature-flag.js';
2
+ import {
3
+ DEFAULT_INPUT_HISTORY_LIMIT,
4
+ DEFAULT_INPUT_HISTORY_SCOPE,
5
+ isInputHistoryLimit,
6
+ isInputHistoryScope,
7
+ } from './input-history-scope.js';
2
8
 
3
9
  export const createSettingsHelpers = (dependencies) => {
4
10
  const {
@@ -140,6 +146,12 @@ export const createSettingsHelpers = (dependencies) => {
140
146
  if (typeof candidate.themeVariant === 'string' && (candidate.themeVariant === 'light' || candidate.themeVariant === 'dark')) {
141
147
  result.themeVariant = candidate.themeVariant;
142
148
  }
149
+ if (typeof candidate.inputHistoryScope === 'string' && isInputHistoryScope(candidate.inputHistoryScope)) {
150
+ result.inputHistoryScope = candidate.inputHistoryScope;
151
+ }
152
+ if (isInputHistoryLimit(candidate.inputHistoryLimit)) {
153
+ result.inputHistoryLimit = candidate.inputHistoryLimit;
154
+ }
143
155
  if (typeof candidate.useSystemTheme === 'boolean') {
144
156
  result.useSystemTheme = candidate.useSystemTheme;
145
157
  }
@@ -519,6 +531,12 @@ export const createSettingsHelpers = (dependencies) => {
519
531
  if (typeof candidate.inputSpellcheckEnabled === 'boolean') {
520
532
  result.inputSpellcheckEnabled = candidate.inputSpellcheckEnabled;
521
533
  }
534
+ if (candidate.enterToSend === true || candidate.enterToSend === false) {
535
+ result.enterToSend = candidate.enterToSend;
536
+ }
537
+ if (candidate.enterToSendConfigured === true || candidate.enterToSendConfigured === false) {
538
+ result.enterToSendConfigured = candidate.enterToSendConfigured;
539
+ }
522
540
  if (typeof candidate.showOpenCodeUpdateNotifications === 'boolean') {
523
541
  result.showOpenCodeUpdateNotifications = candidate.showOpenCodeUpdateNotifications;
524
542
  }
@@ -687,6 +705,12 @@ export const createSettingsHelpers = (dependencies) => {
687
705
  result.gitChangesViewMode = mode;
688
706
  }
689
707
  }
708
+ if (typeof candidate.toolJsonViewMode === 'string') {
709
+ const mode = candidate.toolJsonViewMode.trim();
710
+ if (mode === 'summary' || mode === 'formatted' || mode === 'raw') {
711
+ result.toolJsonViewMode = mode;
712
+ }
713
+ }
690
714
  if (typeof candidate.directoryShowHidden === 'boolean') {
691
715
  result.directoryShowHidden = candidate.directoryShowHidden;
692
716
  }
@@ -927,6 +951,8 @@ export const createSettingsHelpers = (dependencies) => {
927
951
  const pwaAppName = normalizePwaAppName(settings?.pwaAppName, '');
928
952
  const pwaOrientation = normalizePwaOrientation(settings?.pwaOrientation, 'system');
929
953
  const mobileKeyboardMode = normalizeMobileKeyboardMode(settings?.mobileKeyboardMode, 'native');
954
+ const inputHistoryScope = sanitized.inputHistoryScope ?? DEFAULT_INPUT_HISTORY_SCOPE;
955
+ const inputHistoryLimit = sanitized.inputHistoryLimit ?? DEFAULT_INPUT_HISTORY_LIMIT;
930
956
 
931
957
  return {
932
958
  ...sanitized,
@@ -937,6 +963,8 @@ export const createSettingsHelpers = (dependencies) => {
937
963
  ...(pwaAppName ? { pwaAppName } : {}),
938
964
  pwaOrientation,
939
965
  mobileKeyboardMode,
966
+ inputHistoryScope,
967
+ inputHistoryLimit,
940
968
  securityScopedBookmarks: bookmarks,
941
969
  pinnedDirectories: normalizeStringArray(settings.pinnedDirectories),
942
970
  typographySizes: sanitizeTypographySizesPartial(settings.typographySizes),
@@ -1,8 +1,19 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import { mkdirSync, mkdtempSync, readdirSync, rmSync } from 'node:fs';
3
+ import { tmpdir } from 'node:os';
4
+ import { dirname, join } from 'node:path';
5
+ import { fileURLToPath, pathToFileURL } from 'node:url';
1
6
  import { describe, expect, it } from 'vitest';
2
7
 
8
+ import {
9
+ DEFAULT_INPUT_HISTORY_LIMIT,
10
+ } from './input-history-scope.js';
3
11
  import { createSettingsHelpers } from './settings-helpers.js';
4
12
  import { createSettingsNormalizationRuntime } from './settings-normalization-runtime.js';
5
13
 
14
+ const testFilePath = fileURLToPath(import.meta.url);
15
+ const packagesWebDir = join(dirname(testFilePath), '..', '..', '..');
16
+
6
17
  const createTestHelpers = () => createSettingsHelpers({
7
18
  normalizePathForPersistence: (value) => value,
8
19
  normalizeDirectoryPath: (value) => value,
@@ -58,6 +69,66 @@ const createTestHelpersWithRealSanitizers = () => {
58
69
  };
59
70
 
60
71
  describe('settings helpers', () => {
72
+ it('imports from the packed @openchamber/web tarball without escaping the published package', async () => {
73
+ const tempRoot = mkdtempSync(join(tmpdir(), 'settings-helpers-pack-'));
74
+ const packDir = join(tempRoot, 'pack');
75
+ const extractDir = join(tempRoot, 'extract');
76
+
77
+ try {
78
+ mkdirSync(packDir);
79
+ mkdirSync(extractDir);
80
+ execFileSync('npm', ['pack', '--silent', '--pack-destination', packDir], {
81
+ cwd: packagesWebDir,
82
+ stdio: 'pipe',
83
+ });
84
+
85
+ const tarballName = readdirSync(packDir).find((entry) => entry.endsWith('.tgz'));
86
+ expect(tarballName).toBeTruthy();
87
+
88
+ execFileSync('tar', ['-xzf', join(packDir, tarballName), '-C', extractDir], {
89
+ stdio: 'pipe',
90
+ });
91
+
92
+ const extractedModule = await import(
93
+ pathToFileURL(join(extractDir, 'package', 'server', 'lib', 'opencode', 'settings-helpers.js')).href
94
+ );
95
+
96
+ const helpers = extractedModule.createSettingsHelpers({
97
+ normalizePathForPersistence: (value) => value,
98
+ normalizeDirectoryPath: (value) => value,
99
+ normalizeTunnelBootstrapTtlMs: (value) => value,
100
+ normalizeTunnelSessionTtlMs: (value) => value,
101
+ normalizeTunnelProvider: (value) => value,
102
+ normalizeTunnelMode: (value) => value,
103
+ normalizeOptionalPath: (value) => value,
104
+ normalizeManagedRemoteTunnelHostname: (value) => value,
105
+ normalizeManagedRemoteTunnelPresets: () => undefined,
106
+ normalizeManagedRemoteTunnelPresetTokens: () => undefined,
107
+ sanitizeTypographySizesPartial: () => undefined,
108
+ normalizeStringArray: (input) => input,
109
+ sanitizeModelRefs: () => undefined,
110
+ sanitizeSkillCatalogs: () => undefined,
111
+ sanitizeProjects: () => undefined,
112
+ });
113
+
114
+ expect(helpers.sanitizeSettingsUpdate({ inputHistoryScope: 'global' })).toEqual({
115
+ inputHistoryScope: 'global',
116
+ });
117
+ expect(helpers.sanitizeSettingsUpdate({ inputHistoryScope: 'session' })).toEqual({
118
+ inputHistoryScope: 'session',
119
+ });
120
+ expect(helpers.sanitizeSettingsUpdate({ inputHistoryLimit: 40 })).toEqual({
121
+ inputHistoryLimit: 40,
122
+ });
123
+ expect(helpers.formatSettingsResponse({})).toMatchObject({
124
+ inputHistoryScope: 'session',
125
+ inputHistoryLimit: DEFAULT_INPUT_HISTORY_LIMIT,
126
+ });
127
+ } finally {
128
+ rmSync(tempRoot, { recursive: true, force: true });
129
+ }
130
+ });
131
+
61
132
  it('accepts only booleans for draft starter visibility', () => {
62
133
  const helpers = createTestHelpers();
63
134
 
@@ -66,6 +137,14 @@ describe('settings helpers', () => {
66
137
  expect(helpers.sanitizeSettingsUpdate({ draftStartersVisible: 'false' })).toEqual({});
67
138
  });
68
139
 
140
+ it('sanitizes both Enter settings independently', () => {
141
+ const helpers = createTestHelpers();
142
+
143
+ expect(helpers.sanitizeSettingsUpdate({ enterToSend: true })).toEqual({ enterToSend: true });
144
+ expect(helpers.sanitizeSettingsUpdate({ enterToSendConfigured: false })).toEqual({ enterToSendConfigured: false });
145
+ expect(helpers.sanitizeSettingsUpdate({ enterToSend: 'true', enterToSendConfigured: 1 })).toEqual({});
146
+ });
147
+
69
148
  it('sanitizes shared sidebar display preferences', () => {
70
149
  const helpers = createTestHelpers();
71
150
 
@@ -88,6 +167,15 @@ describe('settings helpers', () => {
88
167
  })).toEqual({});
89
168
  });
90
169
 
170
+ it('persists valid tool JSON view modes', () => {
171
+ const helpers = createTestHelpers();
172
+
173
+ expect(helpers.sanitizeSettingsUpdate({ toolJsonViewMode: 'summary' })).toEqual({ toolJsonViewMode: 'summary' });
174
+ expect(helpers.sanitizeSettingsUpdate({ toolJsonViewMode: 'formatted' })).toEqual({ toolJsonViewMode: 'formatted' });
175
+ expect(helpers.sanitizeSettingsUpdate({ toolJsonViewMode: 'raw' })).toEqual({ toolJsonViewMode: 'raw' });
176
+ expect(helpers.sanitizeSettingsUpdate({ toolJsonViewMode: 'unknown' })).toEqual({});
177
+ });
178
+
91
179
  it('accepts only booleans for wide chat layout', () => {
92
180
  const helpers = createTestHelpers();
93
181
 
@@ -134,6 +222,74 @@ describe('settings helpers', () => {
134
222
  expect(helpers.sanitizeSettingsUpdate({ messageStreamTransport: 'websocket' })).toEqual({});
135
223
  });
136
224
 
225
+ it('accepts inputHistoryScope as a persisted shared setting', () => {
226
+ const helpers = createTestHelpers();
227
+
228
+ expect(helpers.sanitizeSettingsUpdate({ inputHistoryScope: 'global' })).toEqual({
229
+ inputHistoryScope: 'global',
230
+ });
231
+ expect(helpers.sanitizeSettingsUpdate({ inputHistoryScope: 'session' })).toEqual({
232
+ inputHistoryScope: 'session',
233
+ });
234
+ });
235
+
236
+ it('accepts valid inputHistoryLimit values as a persisted shared setting', () => {
237
+ const helpers = createTestHelpers();
238
+
239
+ expect(helpers.sanitizeSettingsUpdate({ inputHistoryLimit: 1 })).toEqual({
240
+ inputHistoryLimit: 1,
241
+ });
242
+ expect(helpers.sanitizeSettingsUpdate({ inputHistoryLimit: 40 })).toEqual({
243
+ inputHistoryLimit: 40,
244
+ });
245
+ expect(helpers.sanitizeSettingsUpdate({ inputHistoryLimit: 100 })).toEqual({
246
+ inputHistoryLimit: 100,
247
+ });
248
+ });
249
+
250
+ it('rejects invalid inputHistoryLimit values', () => {
251
+ const helpers = createTestHelpers();
252
+
253
+ expect(helpers.sanitizeSettingsUpdate({ inputHistoryLimit: 0 })).toEqual({});
254
+ expect(helpers.sanitizeSettingsUpdate({ inputHistoryLimit: 101 })).toEqual({});
255
+ expect(helpers.sanitizeSettingsUpdate({ inputHistoryLimit: 1.5 })).toEqual({});
256
+ expect(helpers.sanitizeSettingsUpdate({ inputHistoryLimit: '40' })).toEqual({});
257
+ expect(helpers.sanitizeSettingsUpdate({ inputHistoryLimit: Number.NaN })).toEqual({});
258
+ expect(helpers.sanitizeSettingsUpdate({ inputHistoryLimit: Number.POSITIVE_INFINITY })).toEqual({});
259
+ expect(helpers.sanitizeSettingsUpdate({ inputHistoryLimit: Number.NEGATIVE_INFINITY })).toEqual({});
260
+ });
261
+
262
+ it('rejects invalid inputHistoryScope values', () => {
263
+ const helpers = createTestHelpers();
264
+
265
+ expect(helpers.sanitizeSettingsUpdate({ inputHistoryScope: 'workspace' })).toEqual({});
266
+ });
267
+
268
+ it('defaults inputHistoryScope to session in formatted settings responses', () => {
269
+ const helpers = createTestHelpers();
270
+
271
+ expect(helpers.formatSettingsResponse({ inputHistoryScope: 'global' })).toMatchObject({
272
+ inputHistoryScope: 'global',
273
+ });
274
+ expect(helpers.formatSettingsResponse({})).toMatchObject({
275
+ inputHistoryScope: 'session',
276
+ });
277
+ });
278
+
279
+ it('defaults missing inputHistoryLimit to 40 in formatted settings responses and preserves valid values', () => {
280
+ const helpers = createTestHelpers();
281
+
282
+ expect(helpers.formatSettingsResponse({})).toMatchObject({
283
+ inputHistoryLimit: DEFAULT_INPUT_HISTORY_LIMIT,
284
+ });
285
+ expect(helpers.formatSettingsResponse({ inputHistoryLimit: 1 })).toMatchObject({
286
+ inputHistoryLimit: 1,
287
+ });
288
+ expect(helpers.formatSettingsResponse({ inputHistoryLimit: 100 })).toMatchObject({
289
+ inputHistoryLimit: 100,
290
+ });
291
+ });
292
+
137
293
  it('sanitizes the persisted terminal shell', () => {
138
294
  const helpers = createTestHelpers();
139
295
 
@@ -316,6 +472,27 @@ describe('settings helpers', () => {
316
472
  });
317
473
  });
318
474
 
475
+ it('accepts and rejects invalid Enter-to-send preference values', () => {
476
+ const helpers = createTestHelpers();
477
+
478
+ expect(helpers.sanitizeSettingsUpdate({ enterToSend: true, enterToSendConfigured: true })).toEqual({
479
+ enterToSend: true,
480
+ enterToSendConfigured: true,
481
+ });
482
+ expect(helpers.sanitizeSettingsUpdate({ enterToSend: false, enterToSendConfigured: true })).toEqual({
483
+ enterToSend: false,
484
+ enterToSendConfigured: true,
485
+ });
486
+ expect(helpers.sanitizeSettingsUpdate({ enterToSend: false, enterToSendConfigured: false })).toEqual({
487
+ enterToSend: false,
488
+ enterToSendConfigured: false,
489
+ });
490
+ expect(helpers.sanitizeSettingsUpdate({ enterToSend: 'true' })).toEqual({});
491
+ expect(helpers.sanitizeSettingsUpdate({ enterToSend: 1 })).toEqual({});
492
+ expect(helpers.sanitizeSettingsUpdate({ enterToSendConfigured: 'true' })).toEqual({});
493
+ expect(helpers.sanitizeSettingsUpdate({ enterToSendConfigured: 1 })).toEqual({});
494
+ });
495
+
319
496
  it('accepts dismissed OpenCode update toast version as a persisted shared setting', () => {
320
497
  const helpers = createTestHelpers();
321
498
 
@@ -692,8 +692,8 @@ export const createSettingsRuntime = (deps) => {
692
692
  return { settings, changed: false };
693
693
  }
694
694
 
695
- const defaultLight = 'flexoki-light';
696
- const defaultDark = 'flexoki-dark';
695
+ const defaultLight = 'openchamber-light';
696
+ const defaultDark = 'openchamber-dark';
697
697
 
698
698
  let nextLightThemeId = hasLight ? settings.lightThemeId : undefined;
699
699
  let nextDarkThemeId = hasDark ? settings.darkThemeId : undefined;
@@ -39,6 +39,35 @@ const createRuntime = async () => {
39
39
  };
40
40
 
41
41
  describe('settings runtime', () => {
42
+ it('uses OpenChamber themes when a new install has no theme preferences', async () => {
43
+ const { runtime, cleanup } = await createRuntime();
44
+ try {
45
+ await expect(runtime.readSettingsFromDiskMigrated()).resolves.toMatchObject({
46
+ lightThemeId: 'openchamber-light',
47
+ darkThemeId: 'openchamber-dark',
48
+ });
49
+ } finally {
50
+ await cleanup();
51
+ }
52
+ });
53
+
54
+ it('preserves existing theme preferences during theme migration', async () => {
55
+ const { runtime, settingsFilePath, cleanup } = await createRuntime();
56
+ try {
57
+ await fsPromises.writeFile(settingsFilePath, JSON.stringify({
58
+ lightThemeId: 'flexoki-light',
59
+ darkThemeId: 'flexoki-dark',
60
+ }), 'utf8');
61
+
62
+ await expect(runtime.readSettingsFromDiskMigrated()).resolves.toMatchObject({
63
+ lightThemeId: 'flexoki-light',
64
+ darkThemeId: 'flexoki-dark',
65
+ });
66
+ } finally {
67
+ await cleanup();
68
+ }
69
+ });
70
+
42
71
  it('round-trips shared sidebar preferences through settings.json', async () => {
43
72
  const { runtime, settingsFilePath, cleanup } = await createRuntime();
44
73
  const preferences = {
@@ -6,7 +6,10 @@ import { parse as parseJsonc, printParseErrorCode } from 'jsonc-parser';
6
6
 
7
7
  // ============== PATH CONSTANTS ==============
8
8
 
9
- const OPENCODE_CONFIG_DIR = path.join(os.homedir(), '.config', 'opencode');
9
+ const OPENCODE_CONFIG_DIR = path.join(
10
+ process.env.XDG_CONFIG_HOME?.trim() || path.join(os.homedir(), '.config'),
11
+ 'opencode',
12
+ );
10
13
  const AGENT_DIR = path.join(OPENCODE_CONFIG_DIR, 'agents');
11
14
  const COMMAND_DIR = path.join(OPENCODE_CONFIG_DIR, 'commands');
12
15
  const SKILL_DIR = path.join(OPENCODE_CONFIG_DIR, 'skills');
@@ -11,6 +11,7 @@ export const createGracefulShutdownRuntime = (dependencies) => {
11
11
  sessionAssistRuntime,
12
12
  sessionGoalRuntime,
13
13
  contextObligatoryRuntime,
14
+ messageQueueRuntime,
14
15
  scheduledTasksRuntime,
15
16
  getHealthCheckInterval,
16
17
  clearHealthCheckInterval,
@@ -47,6 +48,7 @@ export const createGracefulShutdownRuntime = (dependencies) => {
47
48
  sessionAssistRuntime?.stop?.();
48
49
  sessionGoalRuntime?.stop?.();
49
50
  contextObligatoryRuntime?.stop?.();
51
+ messageQueueRuntime?.stop?.();
50
52
  scheduledTasksRuntime?.stop?.();
51
53
 
52
54
  const healthCheckInterval = getHealthCheckInterval();
@@ -1,5 +1,6 @@
1
1
  import { createOpencodeClient } from '@opencode-ai/sdk/v2';
2
2
  import { buildDeferredRestartResponse } from './config-mutation-response.js';
3
+ import { OPENCODE_CONFIG_DIR } from './shared.js';
3
4
 
4
5
  /**
5
6
  * Matches how OpenCode reads its own boolean env flags: any value other than
@@ -110,7 +111,7 @@ export const registerSkillRoutes = (app, dependencies) => {
110
111
  }
111
112
 
112
113
  const userRoots = [
113
- path.join(home, '.config', 'opencode'),
114
+ OPENCODE_CONFIG_DIR,
114
115
  path.join(home, '.opencode'),
115
116
  path.join(home, '.claude', 'skills'),
116
117
  path.join(home, '.agents', 'skills'),
@@ -1,9 +1,8 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
- import os from 'os';
4
3
  import yaml from 'yaml';
4
+ import { OPENCODE_CONFIG_DIR } from './shared.js';
5
5
 
6
- const OPENCODE_CONFIG_DIR = path.join(os.homedir(), '.config', 'opencode');
7
6
  const GLOBAL_SNIPPET_DIR = path.join(OPENCODE_CONFIG_DIR, 'snippet');
8
7
  const GLOBAL_SNIPPET_DIR_ALT = path.join(OPENCODE_CONFIG_DIR, 'snippets');
9
8
  const SNIPPET_EXTENSION = '.md';
@@ -4,15 +4,15 @@ import fs from 'fs';
4
4
  import os from 'os';
5
5
  import path from 'path';
6
6
  import { fileURLToPath } from 'url';
7
+ import { fetchUpdateNotes } from './changelog/update-notes.js';
7
8
 
8
9
  const __filename = fileURLToPath(import.meta.url);
9
10
  const __dirname = path.dirname(__filename);
10
11
 
11
12
  const PACKAGE_NAME = '@vantran-se/openchamber-web';
12
13
  const PACKAGE_PATH_SEGMENTS = PACKAGE_NAME.split('/');
13
- const PACKAGE_REGISTRY_URL = 'https://npm.pkg.github.com';
14
+ const PACKAGE_REGISTRY_URL = 'https://registry.npmjs.org';
14
15
  const UPSTREAM_NPM_REGISTRY_URL = 'https://registry.npmjs.org/@openchamber%2Fweb';
15
- const CHANGELOG_URL = 'https://raw.githubusercontent.com/openchamber/openchamber/main/CHANGELOG.md';
16
16
  const FORK_RELEASES_URL = 'https://github.com/vantran-se/openchamber/releases';
17
17
  const FORK_LATEST_RELEASE_API_URL = 'https://api.github.com/repos/vantran-se/openchamber/releases/latest';
18
18
  const UPSTREAM_RELEASES_URL = 'https://github.com/openchamber/openchamber/releases';
@@ -767,34 +767,9 @@ function compareVersions(left, right) {
767
767
  return 0;
768
768
  }
769
769
 
770
- /**
771
- * Fetch changelog notes between versions
772
- */
770
+ /** Release notes between the installed and the offered version, or undefined. */
773
771
  async function fetchChangelogNotes(fromVersion, toVersion) {
774
- try {
775
- const response = await fetch(CHANGELOG_URL, {
776
- signal: AbortSignal.timeout(10000),
777
- });
778
-
779
- if (!response.ok) return undefined;
780
-
781
- const changelog = await response.text();
782
- const sections = changelog.split(/^## /m).slice(1);
783
-
784
- const relevantSections = sections.filter((section) => {
785
- const match = section.match(/^\[(\d+\.\d+\.\d+)\]/);
786
- if (!match) return false;
787
- return compareVersions(match[1], fromVersion) > 0 && compareVersions(match[1], toVersion) <= 0;
788
- });
789
-
790
- if (relevantSections.length === 0) return undefined;
791
-
792
- return relevantSections
793
- .map((s) => '## ' + s.trim())
794
- .join('\n\n');
795
- } catch {
796
- return undefined;
797
- }
772
+ return (await fetchUpdateNotes(fromVersion, toVersion, compareVersions)) ?? undefined;
798
773
  }
799
774
 
800
775
  export async function checkForUpdates(options = {}) {
@@ -163,9 +163,9 @@ describe('checkForUpdates', () => {
163
163
  });
164
164
 
165
165
  describe('getUpdateCommand', () => {
166
- it('installs the fork package from GitHub Packages', () => {
166
+ it('installs the public fork package from npm', () => {
167
167
  expect(getUpdateCommand('npm')).toBe(
168
- 'npm install -g @vantran-se/openchamber-web@latest --registry=https://npm.pkg.github.com',
168
+ 'npm install -g @vantran-se/openchamber-web@latest --registry=https://registry.npmjs.org',
169
169
  );
170
170
  });
171
171
  });
@@ -23,6 +23,7 @@ These provider IDs are currently dispatchable via `fetchQuotaForProvider(provide
23
23
  | `cursor` | Cursor | `providers/cursor.js` | Environment/token files, OpenChamber-managed credentials, or explicit one-time Cursor import |
24
24
  | `crof` | CrofAI | `providers/crof.js` | `crof` (API key under `key` or `token`) |
25
25
  | `deepseek` | DeepSeek | `providers/deepseek.js` | `deepseek` (API key under `key` or `token`) |
26
+ | `exe-dev` | exe.dev | `providers/exe-dev.js` | Usage API token stored under `~/.config/openchamber/quota/` |
26
27
  | `google` | Google | `providers/google/index.js` | `google`, `google.oauth`, Antigravity accounts file |
27
28
  | `github-copilot` | GitHub Copilot | `providers/copilot.js` | `github-copilot`, `copilot` |
28
29
  | `github-copilot-addon` | GitHub Copilot Add-on | `providers/copilot.js` | `github-copilot`, `copilot` |
@@ -51,7 +52,7 @@ All providers should return results via shared helpers to preserve API shape:
51
52
  Provider modules must export `providerId`, `providerName`, `aliases`, `isConfigured(auth?)`, and `fetchQuota()`.
52
53
  `fetchQuota()` should return a quota result with `usage.windows` keyed by window name (for example `5h`, `7d`, `daily`) and optional provider-specific `usage.models` data.
53
54
 
54
- Ollama Cloud and Cursor credentials are explicitly managed through Settings. OpenCode Go usage uses `GET https://opencode.ai/zen/go/v1/usage` with the `opencode-go` API key from OpenCode `auth.json` as a bearer token. The server validates managed credentials before atomic `0600` writes and never returns secrets through its API. OpenChamber never scans browser cookie stores or automatically reads Cursor storage; Cursor import is an explicit one-time user action and never modifies Cursor's database.
55
+ exe.dev, Ollama Cloud, and Cursor credentials are explicitly managed through Settings. exe.dev usage uses a separately generated HTTPS API token restricted to `billing credits usage` and aggregates every `exe-*` model provider into one monthly credit window. Generate the token with `ssh exe.dev "ssh-key generate-api-key --label=openchamber --exp=30d --cmds='billing credits usage'"`. OpenCode Go usage uses `GET https://opencode.ai/zen/go/v1/usage` with the `opencode-go` API key from OpenCode `auth.json` as a bearer token and the stable `x-opencode-session: openchamber-usage` workload id. The server validates managed credentials before atomic `0600` writes and never returns secrets through its API. OpenChamber never scans browser cookie stores or automatically reads Cursor storage; Cursor import is an explicit one-time user action and never modifies Cursor's database.
55
56
 
56
57
  Command Code usage resolves account scope through `GET /alpha/whoami`, then reads server-backed credit balances and five-hour/weekly limits from `GET /alpha/billing/credits?orgId=...`. Personal accounts return `org: null` and use `/alpha/billing/credits` without an `orgId`; organization accounts include their organization id. Web/Electron and VS Code read the standard `command-code` OpenCode auth entry (including OAuth `access`) or `COMMAND_CODE_API_KEY`; credentials remain in the owning runtime and are never returned to shared UI.
57
58
 
@@ -3,6 +3,10 @@ import { deleteQuotaCredential, readQuotaCredential, writeQuotaCredential } from
3
3
  const clean = (value) => typeof value === 'string' && !/[\r\n]/.test(value) ? value.trim() : '';
4
4
 
5
5
  export const normalizers = {
6
+ 'exe-dev': (value) => {
7
+ const usageToken = clean(value?.usageToken);
8
+ return usageToken ? { usageToken } : null;
9
+ },
6
10
  'ollama-cloud': (value) => {
7
11
  const cookie = clean(value?.cookie);
8
12
  return cookie ? { cookie } : null;
@@ -2,7 +2,7 @@ import fs from 'node:fs';
2
2
  import os from 'node:os';
3
3
  import path from 'node:path';
4
4
 
5
- const MANAGED_QUOTA_PROVIDERS = new Set(['ollama-cloud', 'cursor']);
5
+ const MANAGED_QUOTA_PROVIDERS = new Set(['exe-dev', 'ollama-cloud', 'cursor']);
6
6
 
7
7
  const credentialsDirectory = () => path.join(
8
8
  process.env.OPENCHAMBER_DATA_DIR
@@ -10,12 +10,12 @@ process.env.OPENCHAMBER_DATA_DIR = temporaryDirectory;
10
10
 
11
11
  describe('quota credential store', () => {
12
12
  it('uses owner-only permissions and rejects arbitrary provider paths', () => {
13
- writeQuotaCredential('ollama-cloud', { cookie: 'secret' });
13
+ writeQuotaCredential('exe-dev', { usageToken: 'secret' });
14
14
  expect(fs.statSync(path.join(temporaryDirectory, 'quota')).mode & 0o777).toBe(0o700);
15
- expect(fs.statSync(path.join(temporaryDirectory, 'quota', 'ollama-cloud.json')).mode & 0o777).toBe(0o600);
16
- expect(readQuotaCredential('ollama-cloud', (value) => value)).toEqual({ cookie: 'secret' });
15
+ expect(fs.statSync(path.join(temporaryDirectory, 'quota', 'exe-dev.json')).mode & 0o777).toBe(0o600);
16
+ expect(readQuotaCredential('exe-dev', (value) => value)).toEqual({ usageToken: 'secret' });
17
17
  expect(() => writeQuotaCredential('../escape', {})).toThrow('Unsupported credential provider');
18
- deleteQuotaCredential('ollama-cloud');
18
+ deleteQuotaCredential('exe-dev');
19
19
  });
20
20
 
21
21
  it('removes the obsolete OpenCode Go credential without parsing it', () => {
@@ -0,0 +1,75 @@
1
+ import { readManagedCredential } from '../credentials/providers.js';
2
+ import { asObject, buildResult, formatMoney, toNumber, toTimestamp, toUsageWindow } from '../utils/index.js';
3
+
4
+ export const providerId = 'exe-dev';
5
+ export const providerName = 'exe.dev';
6
+ export const aliases = ['exe-dev'];
7
+ const EXEC_URL = 'https://exe.dev/exec';
8
+ const USAGE_COMMAND = 'billing credits usage --group=day --json';
9
+
10
+ export const parseExeDevUsage = (payload) => {
11
+ const data = asObject(payload);
12
+ if (!data) return null;
13
+
14
+ const totalCost = toNumber(data.total_cost_usd);
15
+ const monthlyAllowance = toNumber(data.monthly_allowance_usd);
16
+ const resetAt = toTimestamp(data.period_end);
17
+ if (totalCost === null || monthlyAllowance === null || monthlyAllowance < 0 || resetAt === null) return null;
18
+
19
+ const usedPercent = monthlyAllowance > 0
20
+ ? Math.min(100, Math.max(0, (totalCost / monthlyAllowance) * 100))
21
+ : null;
22
+ const spent = formatMoney(totalCost);
23
+ const allowance = formatMoney(monthlyAllowance);
24
+ if (spent === null || allowance === null) return null;
25
+
26
+ return {
27
+ monthly: toUsageWindow({
28
+ usedPercent,
29
+ windowSeconds: null,
30
+ resetAt,
31
+ valueLabel: `$${spent} / $${allowance}`,
32
+ }),
33
+ };
34
+ };
35
+
36
+ export const fetchExeDevUsage = async (credential, fetchImpl = fetch) => {
37
+ const response = await fetchImpl(EXEC_URL, {
38
+ method: 'POST',
39
+ headers: {
40
+ Accept: 'application/json',
41
+ Authorization: `Bearer ${credential.usageToken}`,
42
+ 'Content-Type': 'text/plain',
43
+ 'User-Agent': 'OpenChamber quota provider',
44
+ },
45
+ body: USAGE_COMMAND,
46
+ signal: AbortSignal.timeout(15_000),
47
+ });
48
+ if (response.status === 401 || response.status === 403) throw new Error('exe.dev authentication failed');
49
+ if (!response.ok) throw new Error(`exe.dev usage API returned HTTP ${response.status}`);
50
+ const windows = parseExeDevUsage(await response.json().catch(() => null));
51
+ if (!windows) throw new Error('exe.dev usage data could not be parsed');
52
+ return windows;
53
+ };
54
+
55
+ export const isConfigured = () => Boolean(readManagedCredential(providerId));
56
+
57
+ export const fetchQuota = async () => {
58
+ const credential = readManagedCredential(providerId);
59
+ if (!credential) {
60
+ return buildResult({ providerId, providerName, ok: false, configured: false, error: 'Not configured' });
61
+ }
62
+
63
+ try {
64
+ const windows = await fetchExeDevUsage(credential);
65
+ return buildResult({ providerId, providerName, ok: true, configured: true, usage: { windows } });
66
+ } catch (error) {
67
+ return buildResult({
68
+ providerId,
69
+ providerName,
70
+ ok: false,
71
+ configured: true,
72
+ error: error instanceof Error ? error.message : 'Request failed',
73
+ });
74
+ }
75
+ };