@glwhappen/web-code 1.32.7 → 1.32.10

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 (170) hide show
  1. package/README.de.md +1 -1
  2. package/README.ko.md +1 -1
  3. package/README.md +1 -1
  4. package/README.ru.md +1 -1
  5. package/README.tr.md +1 -1
  6. package/README.zh-CN.md +1 -1
  7. package/dist/api-docs.html +6 -7
  8. package/dist/assets/{index-CCGk0QgG.js → index-BLLsK3sG.js} +277 -262
  9. package/dist/assets/index-Dl5QP21C.css +32 -0
  10. package/dist/index.html +2 -2
  11. package/dist/modelConstants.js +841 -0
  12. package/dist-server/server/claude-sdk.js +57 -34
  13. package/dist-server/server/claude-sdk.js.map +1 -1
  14. package/dist-server/server/cursor-cli.js +6 -3
  15. package/dist-server/server/cursor-cli.js.map +1 -1
  16. package/dist-server/server/gemini-cli.js +3 -1
  17. package/dist-server/server/gemini-cli.js.map +1 -1
  18. package/dist-server/server/gemini-response-handler.js +34 -0
  19. package/dist-server/server/gemini-response-handler.js.map +1 -1
  20. package/dist-server/server/index.js +131 -19
  21. package/dist-server/server/index.js.map +1 -1
  22. package/dist-server/server/modules/database/index.js +1 -0
  23. package/dist-server/server/modules/database/index.js.map +1 -1
  24. package/dist-server/server/modules/projects/services/project-management.service.js +1 -0
  25. package/dist-server/server/modules/projects/services/project-management.service.js.map +1 -1
  26. package/dist-server/server/modules/projects/services/projects-with-sessions-fetch.service.js +4 -0
  27. package/dist-server/server/modules/projects/services/projects-with-sessions-fetch.service.js.map +1 -1
  28. package/dist-server/server/modules/providers/list/claude/claude-models.provider.js +143 -0
  29. package/dist-server/server/modules/providers/list/claude/claude-models.provider.js.map +1 -0
  30. package/dist-server/server/modules/providers/list/claude/claude.provider.js +2 -0
  31. package/dist-server/server/modules/providers/list/claude/claude.provider.js.map +1 -1
  32. package/dist-server/server/modules/providers/list/codex/codex-models.provider.js +84 -0
  33. package/dist-server/server/modules/providers/list/codex/codex-models.provider.js.map +1 -0
  34. package/dist-server/server/modules/providers/list/codex/codex-skills.provider.js +7 -39
  35. package/dist-server/server/modules/providers/list/codex/codex-skills.provider.js.map +1 -1
  36. package/dist-server/server/modules/providers/list/codex/codex.provider.js +2 -0
  37. package/dist-server/server/modules/providers/list/codex/codex.provider.js.map +1 -1
  38. package/dist-server/server/modules/providers/list/cursor/cursor-models.provider.js +754 -0
  39. package/dist-server/server/modules/providers/list/cursor/cursor-models.provider.js.map +1 -0
  40. package/dist-server/server/modules/providers/list/cursor/cursor-sessions.provider.js +2 -15
  41. package/dist-server/server/modules/providers/list/cursor/cursor-sessions.provider.js.map +1 -1
  42. package/dist-server/server/modules/providers/list/cursor/cursor.provider.js +2 -0
  43. package/dist-server/server/modules/providers/list/cursor/cursor.provider.js.map +1 -1
  44. package/dist-server/server/modules/providers/list/gemini/gemini-models.provider.js +27 -0
  45. package/dist-server/server/modules/providers/list/gemini/gemini-models.provider.js.map +1 -0
  46. package/dist-server/server/modules/providers/list/gemini/gemini-sessions.provider.js +3 -9
  47. package/dist-server/server/modules/providers/list/gemini/gemini-sessions.provider.js.map +1 -1
  48. package/dist-server/server/modules/providers/list/gemini/gemini.provider.js +2 -0
  49. package/dist-server/server/modules/providers/list/gemini/gemini.provider.js.map +1 -1
  50. package/dist-server/server/modules/providers/list/opencode/opencode-auth.provider.js +92 -0
  51. package/dist-server/server/modules/providers/list/opencode/opencode-auth.provider.js.map +1 -0
  52. package/dist-server/server/modules/providers/list/opencode/opencode-mcp.provider.js +181 -0
  53. package/dist-server/server/modules/providers/list/opencode/opencode-mcp.provider.js.map +1 -0
  54. package/dist-server/server/modules/providers/list/opencode/opencode-models.provider.js +267 -0
  55. package/dist-server/server/modules/providers/list/opencode/opencode-models.provider.js.map +1 -0
  56. package/dist-server/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.js +115 -0
  57. package/dist-server/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.js.map +1 -0
  58. package/dist-server/server/modules/providers/list/opencode/opencode-sessions.provider.js +410 -0
  59. package/dist-server/server/modules/providers/list/opencode/opencode-sessions.provider.js.map +1 -0
  60. package/dist-server/server/modules/providers/list/opencode/opencode-skills.provider.js +62 -0
  61. package/dist-server/server/modules/providers/list/opencode/opencode-skills.provider.js.map +1 -0
  62. package/dist-server/server/modules/providers/list/opencode/opencode.provider.js +19 -0
  63. package/dist-server/server/modules/providers/list/opencode/opencode.provider.js.map +1 -0
  64. package/dist-server/server/modules/providers/provider.registry.js +2 -0
  65. package/dist-server/server/modules/providers/provider.registry.js.map +1 -1
  66. package/dist-server/server/modules/providers/provider.routes.js +42 -1
  67. package/dist-server/server/modules/providers/provider.routes.js.map +1 -1
  68. package/dist-server/server/modules/providers/services/mcp.service.js +1 -9
  69. package/dist-server/server/modules/providers/services/mcp.service.js.map +1 -1
  70. package/dist-server/server/modules/providers/services/provider-models.service.js +199 -0
  71. package/dist-server/server/modules/providers/services/provider-models.service.js.map +1 -0
  72. package/dist-server/server/modules/providers/services/session-synchronizer.service.js +1 -0
  73. package/dist-server/server/modules/providers/services/session-synchronizer.service.js.map +1 -1
  74. package/dist-server/server/modules/providers/services/sessions-watcher.service.js +7 -0
  75. package/dist-server/server/modules/providers/services/sessions-watcher.service.js.map +1 -1
  76. package/dist-server/server/modules/providers/shared/base/abstract.provider.js.map +1 -1
  77. package/dist-server/server/modules/providers/tests/mcp.test.js +73 -6
  78. package/dist-server/server/modules/providers/tests/mcp.test.js.map +1 -1
  79. package/dist-server/server/modules/providers/tests/opencode-models.test.js +66 -0
  80. package/dist-server/server/modules/providers/tests/opencode-models.test.js.map +1 -0
  81. package/dist-server/server/modules/providers/tests/opencode-sessions.test.js +264 -0
  82. package/dist-server/server/modules/providers/tests/opencode-sessions.test.js.map +1 -0
  83. package/dist-server/server/modules/providers/tests/provider-models.service.test.js +270 -0
  84. package/dist-server/server/modules/providers/tests/provider-models.service.test.js.map +1 -0
  85. package/dist-server/server/modules/providers/tests/skills.test.js +33 -0
  86. package/dist-server/server/modules/providers/tests/skills.test.js.map +1 -1
  87. package/dist-server/server/modules/websocket/services/chat-websocket.service.js +18 -1
  88. package/dist-server/server/modules/websocket/services/chat-websocket.service.js.map +1 -1
  89. package/dist-server/server/modules/websocket/services/shell-websocket.service.js +9 -1
  90. package/dist-server/server/modules/websocket/services/shell-websocket.service.js.map +1 -1
  91. package/dist-server/server/modules/websocket/services/websocket-writer.service.js +6 -0
  92. package/dist-server/server/modules/websocket/services/websocket-writer.service.js.map +1 -1
  93. package/dist-server/server/openai-codex.js +32 -4
  94. package/dist-server/server/openai-codex.js.map +1 -1
  95. package/dist-server/server/opencode-cli.js +287 -0
  96. package/dist-server/server/opencode-cli.js.map +1 -0
  97. package/dist-server/server/opencode-cli.test.js +84 -0
  98. package/dist-server/server/opencode-cli.test.js.map +1 -0
  99. package/dist-server/server/routes/agent.js +21 -8
  100. package/dist-server/server/routes/agent.js.map +1 -1
  101. package/dist-server/server/routes/commands.js +202 -209
  102. package/dist-server/server/routes/commands.js.map +1 -1
  103. package/dist-server/server/routes/cursor.js +2 -2
  104. package/dist-server/server/routes/cursor.js.map +1 -1
  105. package/dist-server/server/routes/settings.js +0 -10
  106. package/dist-server/server/routes/settings.js.map +1 -1
  107. package/dist-server/server/routes/tests/commands.test.js +76 -0
  108. package/dist-server/server/routes/tests/commands.test.js.map +1 -0
  109. package/dist-server/server/shared/utils.js +286 -0
  110. package/dist-server/server/shared/utils.js.map +1 -1
  111. package/package.json +3 -1
  112. package/public/api-docs.html +878 -0
  113. package/public/modelConstants.js +841 -0
  114. package/server/claude-sdk.js +64 -35
  115. package/server/cursor-cli.js +6 -3
  116. package/server/gemini-cli.js +7 -1
  117. package/server/gemini-response-handler.js +38 -0
  118. package/server/index.js +150 -19
  119. package/server/modules/database/index.ts +1 -0
  120. package/server/modules/projects/services/project-management.service.ts +2 -0
  121. package/server/modules/projects/services/projects-with-sessions-fetch.service.ts +7 -1
  122. package/server/modules/providers/README.md +11 -3
  123. package/server/modules/providers/list/claude/claude-models.provider.ts +193 -0
  124. package/server/modules/providers/list/claude/claude.provider.ts +3 -0
  125. package/server/modules/providers/list/codex/codex-models.provider.ts +125 -0
  126. package/server/modules/providers/list/codex/codex-skills.provider.ts +10 -50
  127. package/server/modules/providers/list/codex/codex.provider.ts +3 -0
  128. package/server/modules/providers/list/cursor/cursor-models.provider.ts +820 -0
  129. package/server/modules/providers/list/cursor/cursor-sessions.provider.ts +7 -20
  130. package/server/modules/providers/list/cursor/cursor.provider.ts +3 -0
  131. package/server/modules/providers/list/gemini/gemini-models.provider.ts +42 -0
  132. package/server/modules/providers/list/gemini/gemini-sessions.provider.ts +3 -10
  133. package/server/modules/providers/list/gemini/gemini.provider.ts +3 -0
  134. package/server/modules/providers/list/opencode/opencode-auth.provider.ts +111 -0
  135. package/server/modules/providers/list/opencode/opencode-mcp.provider.ts +228 -0
  136. package/server/modules/providers/list/opencode/opencode-models.provider.ts +339 -0
  137. package/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.ts +158 -0
  138. package/server/modules/providers/list/opencode/opencode-sessions.provider.ts +506 -0
  139. package/server/modules/providers/list/opencode/opencode-skills.provider.ts +78 -0
  140. package/server/modules/providers/list/opencode/opencode.provider.ts +27 -0
  141. package/server/modules/providers/provider.registry.ts +2 -0
  142. package/server/modules/providers/provider.routes.ts +62 -2
  143. package/server/modules/providers/services/mcp.service.ts +1 -12
  144. package/server/modules/providers/services/provider-models.service.ts +325 -0
  145. package/server/modules/providers/services/session-synchronizer.service.ts +1 -0
  146. package/server/modules/providers/services/sessions-watcher.service.ts +8 -0
  147. package/server/modules/providers/shared/base/abstract.provider.ts +2 -0
  148. package/server/modules/providers/tests/mcp.test.ts +93 -6
  149. package/server/modules/providers/tests/opencode-models.test.ts +73 -0
  150. package/server/modules/providers/tests/opencode-sessions.test.ts +336 -0
  151. package/server/modules/providers/tests/provider-models.service.test.ts +318 -0
  152. package/server/modules/providers/tests/skills.test.ts +66 -0
  153. package/server/modules/websocket/services/chat-websocket.service.ts +21 -1
  154. package/server/modules/websocket/services/shell-websocket.service.ts +9 -0
  155. package/server/modules/websocket/services/websocket-writer.service.ts +7 -0
  156. package/server/openai-codex.js +40 -4
  157. package/server/opencode-cli.js +336 -0
  158. package/server/opencode-cli.test.js +95 -0
  159. package/server/routes/agent.js +22 -8
  160. package/server/routes/commands.js +254 -233
  161. package/server/routes/cursor.js +2 -2
  162. package/server/routes/settings.js +1 -10
  163. package/server/routes/tests/commands.test.js +82 -0
  164. package/server/shared/interfaces.ts +45 -0
  165. package/server/shared/types.ts +88 -1
  166. package/server/shared/utils.ts +384 -0
  167. package/dist/assets/index-DdxLnCfK.css +0 -32
  168. package/dist-server/shared/modelConstants.js +0 -99
  169. package/dist-server/shared/modelConstants.js.map +0 -1
  170. package/shared/modelConstants.js +0 -107
@@ -37,6 +37,7 @@ Current provider ids in this repo are:
37
37
  - `codex`
38
38
  - `cursor`
39
39
  - `gemini`
40
+ - `opencode`
40
41
 
41
42
  Those ids are mirrored in backend unions and frontend provider constants. If
42
43
  adding a new provider, update every place that hardcodes this list.
@@ -55,7 +56,8 @@ server/modules/providers/list/<provider>/
55
56
  <provider>-session-synchronizer.provider.ts
56
57
  ```
57
58
 
58
- The existing provider folders are `claude`, `codex`, `cursor`, and `gemini`.
59
+ The existing provider folders are `claude`, `codex`, `cursor`, `gemini`, and
60
+ `opencode`.
59
61
 
60
62
  ## What Each Facet Does
61
63
 
@@ -81,7 +83,7 @@ The existing provider folders are `claude`, `codex`, `cursor`, and `gemini`.
81
83
  - Update `server/modules/providers/provider.routes.ts`.
82
84
  - Update `server/routes/agent.js` if the provider is launchable from the agent runtime.
83
85
  - Update `server/index.js` if the provider needs runtime boot or shutdown wiring.
84
- - Update `shared/modelConstants.js` if the provider appears in UI provider pickers.
86
+ - Update `public/modelConstants.js` if the provider appears in README or public API docs.
85
87
  - Update `src/components/chat/hooks/useChatProviderState.ts` and
86
88
  `src/components/chat/view/subcomponents/ProviderSelectionEmptyState.tsx` if
87
89
  the provider should be selectable in chat.
@@ -122,6 +124,7 @@ Current MCP formats in this repo are:
122
124
  | Codex | `.codex/config.toml` | `user`, `project` | `stdio`, `http` |
123
125
  | Cursor | `.cursor/mcp.json` | `user`, `project` | `stdio`, `http` |
124
126
  | Gemini | `.gemini/settings.json` | `user`, `project` | `stdio`, `http` |
127
+ | OpenCode | `~/.config/opencode/opencode.json` or `<workspace>/opencode.json` (`.jsonc` is read when present) | `user`, `project` | `stdio`, `http` |
125
128
 
126
129
  5. Implement skills.
127
130
 
@@ -142,6 +145,7 @@ Current skill discovery roots are:
142
145
  | Codex | `~/.agents/skills`, `~/.codex/skills/.system`, `/etc/codex/skills` | `<workspace>/.agents/skills`, `path.dirname(workspacePath)/.agents/skills`, topmost git root `.agents/skills` | `$` | Overlapping roots are deduplicated before scanning. |
143
146
  | Cursor | `~/.cursor/skills` | `<workspace>/.cursor/skills`, `<workspace>/.agents/skills` | `/` | Uses slash-style commands. |
144
147
  | Gemini | `~/.gemini/skills`, `~/.agents/skills` | `<workspace>/.gemini/skills`, `<workspace>/.agents/skills` | `/` | Uses slash-style commands. |
148
+ | OpenCode | `~/.config/opencode/skills`, `~/.claude/skills`, `~/.agents/skills` | Cwd-to-topmost-git-root `.opencode/skills`, `.claude/skills`, and `.agents/skills` | `/` | Reuses OpenCode, Claude, and Agents skill locations. Overlapping roots are deduplicated before scanning. |
145
149
 
146
150
  Command forms currently used by the providers are:
147
151
 
@@ -150,6 +154,7 @@ Command forms currently used by the providers are:
150
154
  - Codex skills: `$skill-name`
151
155
  - Cursor skills: `/skill-name`
152
156
  - Gemini skills: `/skill-name`
157
+ - OpenCode skills: `/skill-name`
153
158
 
154
159
  6. Implement sessions.
155
160
 
@@ -187,6 +192,7 @@ Current session sync roots are:
187
192
  | Codex | `~/.codex/sessions/**/*.jsonl` | Uses `~/.codex/session_index.jsonl` for title lookup and the last `task_complete` message for a fallback title. |
188
193
  | Cursor | `~/.cursor/projects/**/*.jsonl` | Uses sibling `worker.log` to recover `workspacePath`, then derives the session title from the first user prompt. |
189
194
  | Gemini | `~/.gemini/tmp/**/*.jsonl` | Current full scans only index temp JSONL chat artifacts. Single-file sync also accepts legacy `.json` files. |
195
+ | OpenCode | `~/.local/share/opencode/opencode.db` | Reads active sessions/messages/parts from OpenCode's shared SQLite database and stores `jsonl_path` as `null` so deleting one app session cannot remove the shared DB. |
190
196
 
191
197
  8. Register the provider.
192
198
 
@@ -203,10 +209,11 @@ If the provider can run live chat sessions, update the runtime entrypoints too:
203
209
 
204
210
  If the provider is visible in the UI, update:
205
211
 
206
- - `shared/modelConstants.js`
212
+ - provider model fallback files under `server/modules/providers/list/<provider>/`
207
213
  - `src/components/chat/hooks/useChatProviderState.ts`
208
214
  - `src/components/chat/view/subcomponents/ProviderSelectionEmptyState.tsx`
209
215
  - `src/components/provider-auth/view/ProviderLoginModal.tsx`
216
+ - `src/components/mcp/constants.ts`
210
217
 
211
218
  ## Minimal Wrapper Template
212
219
 
@@ -324,6 +331,7 @@ Useful tests in this repo:
324
331
 
325
332
  - `server/modules/providers/tests/mcp.test.ts`
326
333
  - `server/modules/providers/tests/skills.test.ts`
334
+ - `server/modules/providers/tests/opencode-sessions.test.ts`
327
335
 
328
336
  If you touch sessions or session synchronization, add or update focused tests
329
337
  alongside the implementation.
@@ -0,0 +1,193 @@
1
+ import { readFile } from 'node:fs/promises';
2
+
3
+ import { sessionsDb } from '@/modules/database/index.js';
4
+ import { getDefaultOwnerUserId } from '@/shared/default-user.js';
5
+ import type { IProviderModels } from '@/shared/interfaces.js';
6
+ import type {
7
+ ProviderChangeActiveModelInput,
8
+ ProviderCurrentActiveModel,
9
+ ProviderModelsDefinition,
10
+ ProviderSessionActiveModelChange,
11
+ } from '@/shared/types.js';
12
+ import {
13
+ buildDefaultProviderCurrentActiveModel,
14
+ writeProviderSessionActiveModelChange,
15
+ } from '@/shared/utils.js';
16
+
17
+ export const CLAUDE_FALLBACK_MODELS: ProviderModelsDefinition = {
18
+ OPTIONS: [
19
+ {
20
+ value: 'default',
21
+ label: 'Default (recommended)',
22
+ description: 'Use the default model (currently Opus 4.7 (1M context)) · $5/$25 per Mtok',
23
+ },
24
+ {
25
+ value: 'sonnet',
26
+ label: 'Sonnet',
27
+ description: 'Sonnet 4.6 · Best for everyday tasks · $3/$15 per Mtok',
28
+ },
29
+ {
30
+ value: 'sonnet[1m]',
31
+ label: 'Sonnet (1M context)',
32
+ description: 'Sonnet 4.6 for long sessions · $3/$15 per Mtok',
33
+ },
34
+ {
35
+ value: 'haiku',
36
+ label: 'Haiku',
37
+ description: 'Haiku 4.5 · Fastest for quick answers · $1/$5 per Mtok',
38
+ },
39
+ ],
40
+ DEFAULT: 'default',
41
+ };
42
+ type ClaudeInitEvent = {
43
+ sessionId?: string;
44
+ session_id?: string;
45
+ type?: string;
46
+ subtype?: string;
47
+ model?: string;
48
+ message?: {
49
+ content?: unknown;
50
+ model?: string;
51
+ };
52
+ };
53
+
54
+ const ANSI_PATTERN = new RegExp(
55
+ '[\\u001B\\u009B][[\\]()#;?]*(?:'
56
+ + '(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]'
57
+ + '|(?:[\\dA-PR-TZcf-ntqry=><~]))',
58
+ 'g',
59
+ );
60
+
61
+ const extractClaudeEventModel = (event: ClaudeInitEvent, sessionId: string): string | null => {
62
+ const eventSessionId = event.sessionId ?? event.session_id;
63
+ if (eventSessionId && eventSessionId !== sessionId) {
64
+ return null;
65
+ }
66
+
67
+ const contentModel = extractClaudeModelFromMessageContent(event.message?.content);
68
+ if (contentModel) {
69
+ return contentModel;
70
+ }
71
+
72
+ const directModel = event.model?.trim();
73
+ if (directModel) {
74
+ return directModel;
75
+ }
76
+
77
+ const messageModel = event.message?.model?.trim();
78
+ return messageModel || null;
79
+ };
80
+
81
+ const stripAnsi = (value: string): string => value.replace(ANSI_PATTERN, '');
82
+
83
+ const extractTaggedContent = (content: string, tagName: string): string | null => {
84
+ const escapedTagName = tagName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
85
+ const match = new RegExp(`<${escapedTagName}>([\\s\\S]*?)<\\/${escapedTagName}>`).exec(content);
86
+ return match ? match[1] : null;
87
+ };
88
+
89
+ const extractClaudeModelFromTextContent = (content: string): string | null => {
90
+ const localCommandStdout = extractTaggedContent(content, 'local-command-stdout');
91
+ if (localCommandStdout !== null) {
92
+ const cleanedStdout = stripAnsi(localCommandStdout).replace(/\s+/g, ' ').trim();
93
+ const changedModel = /(?:set|changed|switched)\s+model\s+to\s+(.+?)\.?$/i.exec(cleanedStdout);
94
+ if (changedModel?.[1]?.trim()) {
95
+ return changedModel[1].trim();
96
+ }
97
+ }
98
+
99
+ const modelTag = extractTaggedContent(content, 'model')?.trim();
100
+ return modelTag || null;
101
+ };
102
+
103
+ const extractClaudeModelFromMessageContent = (content: unknown): string | null => {
104
+ if (typeof content === 'string') {
105
+ return extractClaudeModelFromTextContent(content);
106
+ }
107
+
108
+ if (!Array.isArray(content)) {
109
+ return null;
110
+ }
111
+
112
+ for (const part of content) {
113
+ if (!part || typeof part !== 'object' || !('text' in part) || typeof part.text !== 'string') {
114
+ continue;
115
+ }
116
+
117
+ const model = extractClaudeModelFromTextContent(part.text);
118
+ if (model) {
119
+ return model;
120
+ }
121
+ }
122
+
123
+ return null;
124
+ };
125
+
126
+ const readClaudeSessionModelFromJsonl = async (
127
+ sessionId: string,
128
+ jsonlPath: string,
129
+ ): Promise<ProviderCurrentActiveModel | null> => {
130
+ const content = await readFile(jsonlPath, 'utf8');
131
+ const lines = content
132
+ .split(/\r?\n/)
133
+ .map((line) => line.trim())
134
+ .filter(Boolean);
135
+
136
+ for (let index = lines.length - 1; index >= 0; index -= 1) {
137
+ try {
138
+ const event = JSON.parse(lines[index]) as ClaudeInitEvent;
139
+ const model = extractClaudeEventModel(event, sessionId);
140
+ if (model) {
141
+ return { model };
142
+ }
143
+ } catch {
144
+ // Skip malformed JSONL lines that can happen during concurrent writes.
145
+ }
146
+ }
147
+
148
+ return null;
149
+ };
150
+
151
+ export class ClaudeProviderModels implements IProviderModels {
152
+ async getSupportedModels(): Promise<ProviderModelsDefinition> {
153
+ // claude creates a new jsonl file as a separate session for this request.
154
+ // As a result, it lists the workspace where this is invoked when it shouldn't.
155
+ //
156
+ // Disabled for now:
157
+ // const queryInstance = query({
158
+ // prompt: 'Get supported models',
159
+ // options: buildClaudeQueryOptions(),
160
+ // });
161
+ // const supportedModels = await queryInstance.supportedModels();
162
+ // queryInstance.close();
163
+ // return buildClaudeModelsDefinition(supportedModels);
164
+ return CLAUDE_FALLBACK_MODELS;
165
+ }
166
+
167
+ async getCurrentActiveModel(sessionId?: string): Promise<ProviderCurrentActiveModel> {
168
+ if (!sessionId?.trim()) {
169
+ return buildDefaultProviderCurrentActiveModel(await this.getSupportedModels());
170
+ }
171
+
172
+ try {
173
+ const ownerUserId = getDefaultOwnerUserId();
174
+ const jsonlPath = sessionsDb.getSessionById(ownerUserId, sessionId)?.jsonl_path;
175
+ const activeModel = jsonlPath
176
+ ? await readClaudeSessionModelFromJsonl(sessionId, jsonlPath)
177
+ : null;
178
+ if (activeModel?.model) {
179
+ return activeModel;
180
+ }
181
+ } catch {
182
+ // Fall through to the provider default when the session-backed lookup fails.
183
+ }
184
+
185
+ return buildDefaultProviderCurrentActiveModel(await this.getSupportedModels());
186
+ }
187
+
188
+ async changeActiveModel(
189
+ input: ProviderChangeActiveModelInput,
190
+ ): Promise<ProviderSessionActiveModelChange> {
191
+ return writeProviderSessionActiveModelChange('claude', input);
192
+ }
193
+ }
@@ -1,17 +1,20 @@
1
1
  import { AbstractProvider } from '@/modules/providers/shared/base/abstract.provider.js';
2
2
  import { ClaudeProviderAuth } from '@/modules/providers/list/claude/claude-auth.provider.js';
3
+ import { ClaudeProviderModels } from '@/modules/providers/list/claude/claude-models.provider.js';
3
4
  import { ClaudeMcpProvider } from '@/modules/providers/list/claude/claude-mcp.provider.js';
4
5
  import { ClaudeSessionSynchronizer } from '@/modules/providers/list/claude/claude-session-synchronizer.provider.js';
5
6
  import { ClaudeSessionsProvider } from '@/modules/providers/list/claude/claude-sessions.provider.js';
6
7
  import { ClaudeSkillsProvider } from '@/modules/providers/list/claude/claude-skills.provider.js';
7
8
  import type {
8
9
  IProviderAuth,
10
+ IProviderModels,
9
11
  IProviderSessionSynchronizer,
10
12
  IProviderSkills,
11
13
  IProviderSessions,
12
14
  } from '@/shared/interfaces.js';
13
15
 
14
16
  export class ClaudeProvider extends AbstractProvider {
17
+ readonly models: IProviderModels = new ClaudeProviderModels();
15
18
  readonly mcp = new ClaudeMcpProvider();
16
19
  readonly auth: IProviderAuth = new ClaudeProviderAuth();
17
20
  readonly skills: IProviderSkills = new ClaudeSkillsProvider();
@@ -0,0 +1,125 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+
5
+ import TOML from '@iarna/toml';
6
+
7
+ import type { IProviderModels } from '@/shared/interfaces.js';
8
+ import type {
9
+ ProviderChangeActiveModelInput,
10
+ ProviderCurrentActiveModel,
11
+ ProviderModelOption,
12
+ ProviderModelsDefinition,
13
+ ProviderSessionActiveModelChange,
14
+ } from '@/shared/types.js';
15
+ import {
16
+ buildDefaultProviderCurrentActiveModel,
17
+ readObjectRecord,
18
+ readOptionalString,
19
+ writeProviderSessionActiveModelChange,
20
+ } from '@/shared/utils.js';
21
+
22
+ export const CODEX_FALLBACK_MODELS: ProviderModelsDefinition = {
23
+ OPTIONS: [
24
+ { value: 'gpt-5.5', label: 'gpt-5.5' },
25
+ { value: 'gpt-5.4', label: 'gpt-5.4' },
26
+ { value: 'gpt-5.4-mini', label: 'gpt-5.4-mini' },
27
+ { value: 'gpt-5.3-codex', label: 'gpt-5.3-codex' },
28
+ { value: 'gpt-5.2', label: 'gpt-5.2' },
29
+ ],
30
+ DEFAULT: 'gpt-5.4',
31
+ };
32
+
33
+ type CodexCachedModel = {
34
+ slug?: string;
35
+ display_name?: string;
36
+ description?: string;
37
+ priority?: number;
38
+ visibility?: string;
39
+ supported_in_api?: boolean;
40
+ };
41
+
42
+ const CODEX_MODELS_CACHE_PATH = path.join(os.homedir(), '.codex', 'models_cache.json');
43
+ const CODEX_CONFIG_PATH = path.join(os.homedir(), '.codex', 'config.toml');
44
+
45
+ const isCodexCachedModel = (value: unknown): value is CodexCachedModel => {
46
+ const record = readObjectRecord(value);
47
+ return Boolean(record && readOptionalString(record.slug));
48
+ };
49
+
50
+ const readCodexPriority = (value: unknown): number => (
51
+ typeof value === 'number' && Number.isFinite(value) ? value : Number.MAX_SAFE_INTEGER
52
+ );
53
+
54
+ const mapCodexModel = (model: CodexCachedModel): ProviderModelOption => ({
55
+ value: model.slug as string,
56
+ label: readOptionalString(model.display_name) ?? (model.slug as string),
57
+ description: readOptionalString(model.description),
58
+ });
59
+
60
+ const buildCodexModelsDefinition = (models: CodexCachedModel[]): ProviderModelsDefinition => {
61
+ const sortedModels = [...models]
62
+ .filter((model) => model.visibility !== 'hidden' && model.supported_in_api !== false)
63
+ .sort((left, right) => readCodexPriority(left.priority) - readCodexPriority(right.priority));
64
+
65
+ const options: ProviderModelOption[] = [];
66
+ const seenValues = new Set<string>();
67
+
68
+ for (const model of sortedModels) {
69
+ const mappedModel = mapCodexModel(model);
70
+ if (seenValues.has(mappedModel.value)) {
71
+ continue;
72
+ }
73
+
74
+ seenValues.add(mappedModel.value);
75
+ options.push(mappedModel);
76
+ }
77
+
78
+ if (options.length === 0) {
79
+ return CODEX_FALLBACK_MODELS;
80
+ }
81
+
82
+ return {
83
+ OPTIONS: options,
84
+ DEFAULT: options[0]?.value ?? CODEX_FALLBACK_MODELS.DEFAULT,
85
+ };
86
+ };
87
+
88
+ export class CodexProviderModels implements IProviderModels {
89
+ async getSupportedModels(): Promise<ProviderModelsDefinition> {
90
+ try {
91
+ const raw = await readFile(CODEX_MODELS_CACHE_PATH, 'utf8');
92
+ const parsed = readObjectRecord(JSON.parse(raw));
93
+ const models = Array.isArray(parsed?.models)
94
+ ? parsed.models.filter(isCodexCachedModel)
95
+ : [];
96
+
97
+ return buildCodexModelsDefinition(models);
98
+ } catch {
99
+ return CODEX_FALLBACK_MODELS;
100
+ }
101
+ }
102
+
103
+ async getCurrentActiveModel(): Promise<ProviderCurrentActiveModel> {
104
+ try {
105
+ const raw = await readFile(CODEX_CONFIG_PATH, 'utf8');
106
+ const parsed = readObjectRecord(TOML.parse(raw));
107
+ const model = readOptionalString(parsed?.model);
108
+ if (!model) {
109
+ return buildDefaultProviderCurrentActiveModel(await this.getSupportedModels());
110
+ }
111
+
112
+ return {
113
+ model,
114
+ };
115
+ } catch {
116
+ return buildDefaultProviderCurrentActiveModel(await this.getSupportedModels());
117
+ }
118
+ }
119
+
120
+ async changeActiveModel(
121
+ input: ProviderChangeActiveModelInput,
122
+ ): Promise<ProviderSessionActiveModelChange> {
123
+ return writeProviderSessionActiveModelChange('codex', input);
124
+ }
125
+ }
@@ -1,52 +1,12 @@
1
- import fs from 'node:fs/promises';
2
1
  import os from 'node:os';
3
2
  import path from 'node:path';
4
3
 
5
4
  import { SkillsProvider } from '@/modules/providers/shared/skills/skills.provider.js';
6
5
  import type { ProviderSkillSource } from '@/shared/types.js';
7
-
8
- const hasGitMarker = async (dirPath: string): Promise<boolean> => {
9
- try {
10
- const gitMarkerStats = await fs.stat(path.join(dirPath, '.git'));
11
- return gitMarkerStats.isDirectory() || gitMarkerStats.isFile();
12
- } catch {
13
- return false;
14
- }
15
- };
16
-
17
- const findTopmostGitRoot = async (startPath: string): Promise<string | null> => {
18
- let currentPath = path.resolve(startPath);
19
- let topmostGitRoot: string | null = null;
20
-
21
- while (true) {
22
- if (await hasGitMarker(currentPath)) {
23
- topmostGitRoot = currentPath;
24
- }
25
-
26
- const parentPath = path.dirname(currentPath);
27
- if (parentPath === currentPath) {
28
- break;
29
- }
30
-
31
- currentPath = parentPath;
32
- }
33
-
34
- return topmostGitRoot;
35
- };
36
-
37
- const addUniqueSource = (
38
- sources: ProviderSkillSource[],
39
- seenRootDirs: Set<string>,
40
- source: ProviderSkillSource,
41
- ): void => {
42
- const normalizedRootDir = path.resolve(source.rootDir);
43
- if (seenRootDirs.has(normalizedRootDir)) {
44
- return;
45
- }
46
-
47
- seenRootDirs.add(normalizedRootDir);
48
- sources.push({ ...source, rootDir: normalizedRootDir });
49
- };
6
+ import {
7
+ addUniqueProviderSkillSource,
8
+ findTopmostGitRoot,
9
+ } from '@/shared/utils.js';
50
10
 
51
11
  export class CodexSkillsProvider extends SkillsProvider {
52
12
  constructor() {
@@ -58,7 +18,7 @@ export class CodexSkillsProvider extends SkillsProvider {
58
18
  const seenRootDirs = new Set<string>();
59
19
  const repoRoot = await findTopmostGitRoot(workspacePath);
60
20
 
61
- addUniqueSource(sources, seenRootDirs, {
21
+ addUniqueProviderSkillSource(sources, seenRootDirs, {
62
22
  scope: 'repo',
63
23
  rootDir: path.join(workspacePath, '.agents', 'skills'),
64
24
  commandPrefix: '$',
@@ -67,29 +27,29 @@ export class CodexSkillsProvider extends SkillsProvider {
67
27
  if (repoRoot) {
68
28
  // Codex checks repository skills at the launch folder, one folder above it,
69
29
  // and the topmost git root; these can collapse to the same directory.
70
- addUniqueSource(sources, seenRootDirs, {
30
+ addUniqueProviderSkillSource(sources, seenRootDirs, {
71
31
  scope: 'repo',
72
32
  rootDir: path.join(path.dirname(workspacePath), '.agents', 'skills'),
73
33
  commandPrefix: '$',
74
34
  });
75
- addUniqueSource(sources, seenRootDirs, {
35
+ addUniqueProviderSkillSource(sources, seenRootDirs, {
76
36
  scope: 'repo',
77
37
  rootDir: path.join(repoRoot, '.agents', 'skills'),
78
38
  commandPrefix: '$',
79
39
  });
80
40
  }
81
41
 
82
- addUniqueSource(sources, seenRootDirs, {
42
+ addUniqueProviderSkillSource(sources, seenRootDirs, {
83
43
  scope: 'user',
84
44
  rootDir: path.join(os.homedir(), '.agents', 'skills'),
85
45
  commandPrefix: '$',
86
46
  });
87
- addUniqueSource(sources, seenRootDirs, {
47
+ addUniqueProviderSkillSource(sources, seenRootDirs, {
88
48
  scope: 'admin',
89
49
  rootDir: path.join('/etc', 'codex', 'skills'),
90
50
  commandPrefix: '$',
91
51
  });
92
- addUniqueSource(sources, seenRootDirs, {
52
+ addUniqueProviderSkillSource(sources, seenRootDirs, {
93
53
  scope: 'system',
94
54
  rootDir: path.join(os.homedir(), '.codex', 'skills', '.system'),
95
55
  commandPrefix: '$',
@@ -1,17 +1,20 @@
1
1
  import { AbstractProvider } from '@/modules/providers/shared/base/abstract.provider.js';
2
2
  import { CodexProviderAuth } from '@/modules/providers/list/codex/codex-auth.provider.js';
3
+ import { CodexProviderModels } from '@/modules/providers/list/codex/codex-models.provider.js';
3
4
  import { CodexMcpProvider } from '@/modules/providers/list/codex/codex-mcp.provider.js';
4
5
  import { CodexSessionSynchronizer } from '@/modules/providers/list/codex/codex-session-synchronizer.provider.js';
5
6
  import { CodexSessionsProvider } from '@/modules/providers/list/codex/codex-sessions.provider.js';
6
7
  import { CodexSkillsProvider } from '@/modules/providers/list/codex/codex-skills.provider.js';
7
8
  import type {
8
9
  IProviderAuth,
10
+ IProviderModels,
9
11
  IProviderSessionSynchronizer,
10
12
  IProviderSkills,
11
13
  IProviderSessions,
12
14
  } from '@/shared/interfaces.js';
13
15
 
14
16
  export class CodexProvider extends AbstractProvider {
17
+ readonly models: IProviderModels = new CodexProviderModels();
15
18
  readonly mcp = new CodexMcpProvider();
16
19
  readonly auth: IProviderAuth = new CodexProviderAuth();
17
20
  readonly skills: IProviderSkills = new CodexSkillsProvider();