@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
@@ -4,7 +4,12 @@ import path from 'node:path';
4
4
 
5
5
  import type { IProviderSessions } from '@/shared/interfaces.js';
6
6
  import type { AnyRecord, FetchHistoryOptions, FetchHistoryResult, NormalizedMessage } from '@/shared/types.js';
7
- import { createNormalizedMessage, generateMessageId, readObjectRecord } from '@/shared/utils.js';
7
+ import {
8
+ createNormalizedMessage,
9
+ generateMessageId,
10
+ readObjectRecord,
11
+ sanitizeLeafDirectoryName,
12
+ } from '@/shared/utils.js';
8
13
 
9
14
  const PROVIDER = 'cursor';
10
15
 
@@ -186,24 +191,6 @@ function normalizeCursorToolInput(toolName: string, rawInput: unknown): unknown
186
191
  return normalized;
187
192
  }
188
193
 
189
- function sanitizeCursorSessionId(sessionId: string): string {
190
- const normalized = sessionId.trim();
191
- if (!normalized) {
192
- throw new Error('Cursor session id is required.');
193
- }
194
-
195
- if (
196
- normalized.includes('..')
197
- || normalized.includes(path.posix.sep)
198
- || normalized.includes(path.win32.sep)
199
- || normalized !== path.basename(normalized)
200
- ) {
201
- throw new Error(`Invalid cursor session id "${sessionId}".`);
202
- }
203
-
204
- return normalized;
205
- }
206
-
207
194
  export class CursorSessionsProvider implements IProviderSessions {
208
195
  /**
209
196
  * Loads Cursor's SQLite blob DAG and returns message blobs in conversation
@@ -214,7 +201,7 @@ export class CursorSessionsProvider implements IProviderSessions {
214
201
  const { default: Database } = await import('better-sqlite3');
215
202
 
216
203
  const cwdId = crypto.createHash('md5').update(projectPath || process.cwd()).digest('hex');
217
- const safeSessionId = sanitizeCursorSessionId(sessionId);
204
+ const safeSessionId = sanitizeLeafDirectoryName(sessionId, 'cursor session id');
218
205
  const baseChatsPath = path.join(os.homedir(), '.cursor', 'chats', cwdId);
219
206
  const storeDbPath = path.join(baseChatsPath, safeSessionId, 'store.db');
220
207
  const resolvedBaseChatsPath = path.resolve(baseChatsPath);
@@ -1,17 +1,20 @@
1
1
  import { AbstractProvider } from '@/modules/providers/shared/base/abstract.provider.js';
2
2
  import { CursorProviderAuth } from '@/modules/providers/list/cursor/cursor-auth.provider.js';
3
+ import { CursorProviderModels } from '@/modules/providers/list/cursor/cursor-models.provider.js';
3
4
  import { CursorMcpProvider } from '@/modules/providers/list/cursor/cursor-mcp.provider.js';
4
5
  import { CursorSessionSynchronizer } from '@/modules/providers/list/cursor/cursor-session-synchronizer.provider.js';
5
6
  import { CursorSessionsProvider } from '@/modules/providers/list/cursor/cursor-sessions.provider.js';
6
7
  import { CursorSkillsProvider } from '@/modules/providers/list/cursor/cursor-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 CursorProvider extends AbstractProvider {
17
+ readonly models: IProviderModels = new CursorProviderModels();
15
18
  readonly mcp = new CursorMcpProvider();
16
19
  readonly auth: IProviderAuth = new CursorProviderAuth();
17
20
  readonly skills: IProviderSkills = new CursorSkillsProvider();
@@ -0,0 +1,42 @@
1
+ import type { IProviderModels } from '@/shared/interfaces.js';
2
+ import type {
3
+ ProviderChangeActiveModelInput,
4
+ ProviderCurrentActiveModel,
5
+ ProviderModelsDefinition,
6
+ ProviderSessionActiveModelChange,
7
+ } from '@/shared/types.js';
8
+ import {
9
+ buildDefaultProviderCurrentActiveModel,
10
+ writeProviderSessionActiveModelChange,
11
+ } from '@/shared/utils.js';
12
+
13
+ export const GEMINI_FALLBACK_MODELS: ProviderModelsDefinition = {
14
+ OPTIONS: [
15
+ { value: 'gemini-3.1-pro-preview', label: 'Gemini 3.1 Pro Preview' },
16
+ { value: 'gemini-3-pro-preview', label: 'Gemini 3 Pro Preview' },
17
+ { value: 'gemini-3-flash-preview', label: 'Gemini 3 Flash Preview' },
18
+ { value: 'gemini-2.5-flash', label: 'Gemini 2.5 Flash' },
19
+ { value: 'gemini-2.5-pro', label: 'Gemini 2.5 Pro' },
20
+ { value: 'gemini-2.0-flash-lite', label: 'Gemini 2.0 Flash Lite' },
21
+ { value: 'gemini-2.0-flash', label: 'Gemini 2.0 Flash' },
22
+ { value: 'gemini-2.0-pro-exp', label: 'Gemini 2.0 Pro Experimental' },
23
+ { value: 'gemini-2.0-flash-thinking-exp', label: 'Gemini 2.0 Flash Thinking' },
24
+ ],
25
+ DEFAULT: 'gemini-3.1-pro-preview',
26
+ };
27
+
28
+ export class GeminiProviderModels implements IProviderModels {
29
+ async getSupportedModels(): Promise<ProviderModelsDefinition> {
30
+ return GEMINI_FALLBACK_MODELS;
31
+ }
32
+
33
+ async getCurrentActiveModel(): Promise<ProviderCurrentActiveModel> {
34
+ return buildDefaultProviderCurrentActiveModel(GEMINI_FALLBACK_MODELS);
35
+ }
36
+
37
+ async changeActiveModel(
38
+ input: ProviderChangeActiveModelInput,
39
+ ): Promise<ProviderSessionActiveModelChange> {
40
+ return writeProviderSessionActiveModelChange('gemini', input);
41
+ }
42
+ }
@@ -88,22 +88,15 @@ function buildGeminiTokenUsage(tokens: unknown): AnyRecord | undefined {
88
88
  const record = tokens as AnyRecord;
89
89
  const input = Number(record.input || 0);
90
90
  const output = Number(record.output || 0);
91
- const cached = Number(record.cached || 0);
92
- const thoughts = Number(record.thoughts || 0);
93
- const tool = Number(record.tool || 0);
94
-
95
- const totalFromFields = input + output + cached + thoughts + tool;
96
- const total = Number(record.total || totalFromFields || 0);
91
+ const total = Number(record.total || input + output || 0);
97
92
 
98
93
  return {
99
94
  used: total,
100
- total: total,
95
+ inputTokens: input,
96
+ outputTokens: output,
101
97
  breakdown: {
102
98
  input,
103
99
  output,
104
- cached,
105
- thoughts,
106
- tool,
107
100
  },
108
101
  };
109
102
  }
@@ -1,17 +1,20 @@
1
1
  import { AbstractProvider } from '@/modules/providers/shared/base/abstract.provider.js';
2
2
  import { GeminiProviderAuth } from '@/modules/providers/list/gemini/gemini-auth.provider.js';
3
+ import { GeminiProviderModels } from '@/modules/providers/list/gemini/gemini-models.provider.js';
3
4
  import { GeminiMcpProvider } from '@/modules/providers/list/gemini/gemini-mcp.provider.js';
4
5
  import { GeminiSessionSynchronizer } from '@/modules/providers/list/gemini/gemini-session-synchronizer.provider.js';
5
6
  import { GeminiSessionsProvider } from '@/modules/providers/list/gemini/gemini-sessions.provider.js';
6
7
  import { GeminiSkillsProvider } from '@/modules/providers/list/gemini/gemini-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 GeminiProvider extends AbstractProvider {
17
+ readonly models: IProviderModels = new GeminiProviderModels();
15
18
  readonly mcp = new GeminiMcpProvider();
16
19
  readonly auth: IProviderAuth = new GeminiProviderAuth();
17
20
  readonly skills: IProviderSkills = new GeminiSkillsProvider();
@@ -0,0 +1,111 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+
5
+ import spawn from 'cross-spawn';
6
+
7
+ import type { IProviderAuth } from '@/shared/interfaces.js';
8
+ import type { ProviderAuthStatus } from '@/shared/types.js';
9
+ import { readObjectRecord, readOptionalString } from '@/shared/utils.js';
10
+
11
+ type OpenCodeCredentialsStatus = {
12
+ authenticated: boolean;
13
+ email: string | null;
14
+ method: string | null;
15
+ error?: string;
16
+ };
17
+
18
+ const OPENCODE_ENV_CREDENTIAL_KEYS = [
19
+ 'ANTHROPIC_API_KEY',
20
+ 'OPENAI_API_KEY',
21
+ 'GOOGLE_GENERATIVE_AI_API_KEY',
22
+ 'GEMINI_API_KEY',
23
+ 'GROQ_API_KEY',
24
+ 'OPENROUTER_API_KEY',
25
+ ];
26
+
27
+ export class OpenCodeProviderAuth implements IProviderAuth {
28
+ /**
29
+ * Checks whether the OpenCode CLI is available to the server process.
30
+ */
31
+ private checkInstalled(): boolean {
32
+ try {
33
+ const result = spawn.sync('opencode', ['--version'], { stdio: 'ignore', timeout: 5000 });
34
+ return !result.error && result.status === 0;
35
+ } catch {
36
+ return false;
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Returns OpenCode CLI installation and credential status.
42
+ */
43
+ async getStatus(): Promise<ProviderAuthStatus> {
44
+ const installed = this.checkInstalled();
45
+ const credentials = await this.checkCredentials();
46
+
47
+ return {
48
+ installed,
49
+ provider: 'opencode',
50
+ authenticated: credentials.authenticated,
51
+ email: credentials.email,
52
+ method: credentials.method,
53
+ error: credentials.authenticated ? undefined : credentials.error || 'Not authenticated',
54
+ };
55
+ }
56
+
57
+ /**
58
+ * Reads OpenCode's auth store or falls back to provider API key environment variables.
59
+ */
60
+ private async checkCredentials(): Promise<OpenCodeCredentialsStatus> {
61
+ try {
62
+ const authPath = path.join(os.homedir(), '.local', 'share', 'opencode', 'auth.json');
63
+ const content = await readFile(authPath, 'utf8');
64
+ const auth = readObjectRecord(JSON.parse(content)) ?? {};
65
+
66
+ for (const [providerId, providerAuth] of Object.entries(auth)) {
67
+ const providerRecord = readObjectRecord(providerAuth);
68
+ if (!providerRecord) {
69
+ continue;
70
+ }
71
+
72
+ const hasCredential = Object.values(providerRecord).some(
73
+ (value) => readOptionalString(value) !== undefined || Boolean(readObjectRecord(value)),
74
+ );
75
+ if (hasCredential) {
76
+ return {
77
+ authenticated: true,
78
+ email: `${providerId} credentials`,
79
+ method: 'credentials_file',
80
+ };
81
+ }
82
+ }
83
+ } catch (error) {
84
+ const code = (error as NodeJS.ErrnoException).code;
85
+ if (code !== 'ENOENT') {
86
+ return {
87
+ authenticated: false,
88
+ email: null,
89
+ method: null,
90
+ error: error instanceof Error ? error.message : 'Failed to read OpenCode auth',
91
+ };
92
+ }
93
+ }
94
+
95
+ const envCredential = OPENCODE_ENV_CREDENTIAL_KEYS.find((key) => process.env[key]?.trim());
96
+ if (envCredential) {
97
+ return {
98
+ authenticated: true,
99
+ email: envCredential,
100
+ method: 'environment',
101
+ };
102
+ }
103
+
104
+ return {
105
+ authenticated: false,
106
+ email: null,
107
+ method: null,
108
+ error: 'OpenCode not configured',
109
+ };
110
+ }
111
+ }
@@ -0,0 +1,228 @@
1
+ import { access, mkdir, readFile, writeFile } from 'node:fs/promises';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+
5
+ import { McpProvider } from '@/modules/providers/shared/mcp/mcp.provider.js';
6
+ import type { McpScope, ProviderMcpServer, UpsertProviderMcpServerInput } from '@/shared/types.js';
7
+ import {
8
+ AppError,
9
+ readObjectRecord,
10
+ readOptionalString,
11
+ readStringArray,
12
+ readStringRecord,
13
+ } from '@/shared/utils.js';
14
+
15
+ type OpenCodeConfigPath = {
16
+ filePath: string;
17
+ exists: boolean;
18
+ };
19
+
20
+ const fileExists = async (filePath: string): Promise<boolean> => {
21
+ try {
22
+ await access(filePath);
23
+ return true;
24
+ } catch {
25
+ return false;
26
+ }
27
+ };
28
+
29
+ /**
30
+ * Removes JSONC comments without touching comment-like text inside strings.
31
+ */
32
+ const stripJsonComments = (content: string): string => {
33
+ let output = '';
34
+ let inString = false;
35
+ let quote = '';
36
+ let escaped = false;
37
+
38
+ for (let index = 0; index < content.length; index += 1) {
39
+ const char = content[index];
40
+ const next = content[index + 1];
41
+
42
+ if (inString) {
43
+ output += char;
44
+ if (escaped) {
45
+ escaped = false;
46
+ } else if (char === '\\') {
47
+ escaped = true;
48
+ } else if (char === quote) {
49
+ inString = false;
50
+ quote = '';
51
+ }
52
+ continue;
53
+ }
54
+
55
+ if (char === '"' || char === '\'') {
56
+ inString = true;
57
+ quote = char;
58
+ output += char;
59
+ continue;
60
+ }
61
+
62
+ if (char === '/' && next === '/') {
63
+ while (index < content.length && content[index] !== '\n') {
64
+ index += 1;
65
+ }
66
+ output += '\n';
67
+ continue;
68
+ }
69
+
70
+ if (char === '/' && next === '*') {
71
+ index += 2;
72
+ while (index < content.length && !(content[index] === '*' && content[index + 1] === '/')) {
73
+ index += 1;
74
+ }
75
+ index += 1;
76
+ continue;
77
+ }
78
+
79
+ output += char;
80
+ }
81
+
82
+ return output;
83
+ };
84
+
85
+ const stripTrailingCommas = (content: string): string =>
86
+ content.replace(/,\s*([}\]])/g, '$1');
87
+
88
+ const readOpenCodeConfig = async (filePath: string): Promise<Record<string, unknown>> => {
89
+ try {
90
+ const content = await readFile(filePath, 'utf8');
91
+ const parsed = JSON.parse(stripTrailingCommas(stripJsonComments(content))) as unknown;
92
+ return readObjectRecord(parsed) ?? {};
93
+ } catch (error) {
94
+ const code = (error as NodeJS.ErrnoException).code;
95
+ if (code === 'ENOENT') {
96
+ return {};
97
+ }
98
+
99
+ throw error;
100
+ }
101
+ };
102
+
103
+ const writeOpenCodeConfig = async (filePath: string, data: Record<string, unknown>): Promise<void> => {
104
+ await mkdir(path.dirname(filePath), { recursive: true });
105
+ await writeFile(filePath, `${JSON.stringify(data, null, 2)}\n`, 'utf8');
106
+ };
107
+
108
+ const resolveOpenCodeConfigPath = async (scope: McpScope, workspacePath: string): Promise<OpenCodeConfigPath> => {
109
+ const root = scope === 'user'
110
+ ? path.join(os.homedir(), '.config', 'opencode')
111
+ : workspacePath;
112
+ const jsonPath = path.join(root, 'opencode.json');
113
+ const jsoncPath = path.join(root, 'opencode.jsonc');
114
+
115
+ if (await fileExists(jsonPath)) {
116
+ return { filePath: jsonPath, exists: true };
117
+ }
118
+
119
+ if (await fileExists(jsoncPath)) {
120
+ return { filePath: jsoncPath, exists: true };
121
+ }
122
+
123
+ return { filePath: jsonPath, exists: false };
124
+ };
125
+
126
+ export class OpenCodeMcpProvider extends McpProvider {
127
+ constructor() {
128
+ super('opencode', ['user', 'project'], ['stdio', 'http']);
129
+ }
130
+
131
+ protected async readScopedServers(scope: McpScope, workspacePath: string): Promise<Record<string, unknown>> {
132
+ const { filePath } = await resolveOpenCodeConfigPath(scope, workspacePath);
133
+ const config = await readOpenCodeConfig(filePath);
134
+ return readObjectRecord(config.mcp) ?? {};
135
+ }
136
+
137
+ protected async writeScopedServers(
138
+ scope: McpScope,
139
+ workspacePath: string,
140
+ servers: Record<string, unknown>,
141
+ ): Promise<void> {
142
+ const { filePath } = await resolveOpenCodeConfigPath(scope, workspacePath);
143
+ const config = await readOpenCodeConfig(filePath);
144
+ config.mcp = servers;
145
+ await writeOpenCodeConfig(filePath, config);
146
+ }
147
+
148
+ protected buildServerConfig(input: UpsertProviderMcpServerInput): Record<string, unknown> {
149
+ if (input.transport === 'stdio') {
150
+ if (!input.command?.trim()) {
151
+ throw new AppError('command is required for stdio MCP servers.', {
152
+ code: 'MCP_COMMAND_REQUIRED',
153
+ statusCode: 400,
154
+ });
155
+ }
156
+
157
+ return {
158
+ type: 'local',
159
+ command: [input.command, ...(input.args ?? [])],
160
+ enabled: true,
161
+ environment: input.env ?? {},
162
+ };
163
+ }
164
+
165
+ if (!input.url?.trim()) {
166
+ throw new AppError('url is required for http MCP servers.', {
167
+ code: 'MCP_URL_REQUIRED',
168
+ statusCode: 400,
169
+ });
170
+ }
171
+
172
+ return {
173
+ type: 'remote',
174
+ url: input.url,
175
+ enabled: true,
176
+ headers: input.headers ?? {},
177
+ };
178
+ }
179
+
180
+ protected normalizeServerConfig(
181
+ scope: McpScope,
182
+ name: string,
183
+ rawConfig: unknown,
184
+ ): ProviderMcpServer | null {
185
+ const config = readObjectRecord(rawConfig);
186
+ if (!config) {
187
+ return null;
188
+ }
189
+
190
+ if (config.type === 'local' || config.command !== undefined) {
191
+ const commandParts = typeof config.command === 'string'
192
+ ? [config.command, ...(readStringArray(config.args) ?? [])]
193
+ : readStringArray(config.command);
194
+ const command = commandParts?.[0];
195
+ if (!command) {
196
+ return null;
197
+ }
198
+
199
+ return {
200
+ provider: 'opencode',
201
+ name,
202
+ scope,
203
+ transport: 'stdio',
204
+ command,
205
+ args: commandParts.slice(1),
206
+ env: readStringRecord(config.environment) ?? readStringRecord(config.env),
207
+ };
208
+ }
209
+
210
+ if (config.type === 'remote' || typeof config.url === 'string') {
211
+ const url = readOptionalString(config.url);
212
+ if (!url) {
213
+ return null;
214
+ }
215
+
216
+ return {
217
+ provider: 'opencode',
218
+ name,
219
+ scope,
220
+ transport: 'http',
221
+ url,
222
+ headers: readStringRecord(config.headers),
223
+ };
224
+ }
225
+
226
+ return null;
227
+ }
228
+ }