@cloudcli-ai/cloudcli 1.37.0 → 1.37.1

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 (112) hide show
  1. package/dist/assets/index-D3jfzuKN.css +32 -0
  2. package/dist/assets/{index-eGqPeIEu.js → index-DTMWjJj8.js} +324 -305
  3. package/dist/index.html +2 -2
  4. package/dist-server/server/modules/commands/commands.routes.js +3 -3
  5. package/dist-server/server/modules/commands/commands.routes.js.map +1 -1
  6. package/dist-server/server/modules/commands/tests/commands.test.js +2 -7
  7. package/dist-server/server/modules/commands/tests/commands.test.js.map +1 -1
  8. package/dist-server/server/modules/database/index.js +2 -0
  9. package/dist-server/server/modules/database/index.js.map +1 -1
  10. package/dist-server/server/modules/database/migrations.js +18 -1
  11. package/dist-server/server/modules/database/migrations.js.map +1 -1
  12. package/dist-server/server/modules/database/repositories/provider-models.js +112 -0
  13. package/dist-server/server/modules/database/repositories/provider-models.js.map +1 -0
  14. package/dist-server/server/modules/database/repositories/sessions.db.js +63 -7
  15. package/dist-server/server/modules/database/repositories/sessions.db.js.map +1 -1
  16. package/dist-server/server/modules/database/schema.js +28 -3
  17. package/dist-server/server/modules/database/schema.js.map +1 -1
  18. package/dist-server/server/modules/database/tests/provider-models.db.integration.test.js +102 -0
  19. package/dist-server/server/modules/database/tests/provider-models.db.integration.test.js.map +1 -0
  20. package/dist-server/server/modules/database/tests/sessions-provider-mapping.test.js +2 -1
  21. package/dist-server/server/modules/database/tests/sessions-provider-mapping.test.js.map +1 -1
  22. package/dist-server/server/modules/database/tests/sessions.db.integration.test.js +22 -0
  23. package/dist-server/server/modules/database/tests/sessions.db.integration.test.js.map +1 -1
  24. package/dist-server/server/modules/file-tree/file-tree.module.js +5 -1
  25. package/dist-server/server/modules/file-tree/file-tree.module.js.map +1 -1
  26. package/dist-server/server/modules/file-tree/file-tree.service.js +50 -18
  27. package/dist-server/server/modules/file-tree/file-tree.service.js.map +1 -1
  28. package/dist-server/server/modules/file-tree/tests/file-tree.service.test.js +83 -7
  29. package/dist-server/server/modules/file-tree/tests/file-tree.service.test.js.map +1 -1
  30. package/dist-server/server/modules/providers/list/claude/claude-models.provider.js +46 -14
  31. package/dist-server/server/modules/providers/list/claude/claude-models.provider.js.map +1 -1
  32. package/dist-server/server/modules/providers/list/claude/claude-runtime.provider.js +8 -15
  33. package/dist-server/server/modules/providers/list/claude/claude-runtime.provider.js.map +1 -1
  34. package/dist-server/server/modules/providers/list/codex/codex-models.provider.js +58 -71
  35. package/dist-server/server/modules/providers/list/codex/codex-models.provider.js.map +1 -1
  36. package/dist-server/server/modules/providers/list/codex/codex-session-synchronizer.provider.js +1 -54
  37. package/dist-server/server/modules/providers/list/codex/codex-session-synchronizer.provider.js.map +1 -1
  38. package/dist-server/server/modules/providers/list/codex/codex-sessions.provider.js +7 -7
  39. package/dist-server/server/modules/providers/list/codex/codex-sessions.provider.js.map +1 -1
  40. package/dist-server/server/modules/providers/list/cursor/cursor-models.provider.js +32 -669
  41. package/dist-server/server/modules/providers/list/cursor/cursor-models.provider.js.map +1 -1
  42. package/dist-server/server/modules/providers/list/opencode/opencode-models.provider.js +105 -328
  43. package/dist-server/server/modules/providers/list/opencode/opencode-models.provider.js.map +1 -1
  44. package/dist-server/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.js +0 -12
  45. package/dist-server/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.js.map +1 -1
  46. package/dist-server/server/modules/providers/provider.routes.js +139 -31
  47. package/dist-server/server/modules/providers/provider.routes.js.map +1 -1
  48. package/dist-server/server/modules/providers/services/provider-models.service.js +167 -199
  49. package/dist-server/server/modules/providers/services/provider-models.service.js.map +1 -1
  50. package/dist-server/server/modules/providers/services/provider-runtime.service.js +2 -2
  51. package/dist-server/server/modules/providers/services/provider-runtime.service.js.map +1 -1
  52. package/dist-server/server/modules/providers/services/session-conversations-search.service.js +12 -4
  53. package/dist-server/server/modules/providers/services/session-conversations-search.service.js.map +1 -1
  54. package/dist-server/server/modules/providers/services/sessions.service.js +62 -3
  55. package/dist-server/server/modules/providers/services/sessions.service.js.map +1 -1
  56. package/dist-server/server/modules/providers/tests/codex-sessions.test.js +53 -17
  57. package/dist-server/server/modules/providers/tests/codex-sessions.test.js.map +1 -1
  58. package/dist-server/server/modules/providers/tests/opencode-models.test.js +15 -126
  59. package/dist-server/server/modules/providers/tests/opencode-models.test.js.map +1 -1
  60. package/dist-server/server/modules/providers/tests/opencode-sessions.test.js +5 -5
  61. package/dist-server/server/modules/providers/tests/opencode-sessions.test.js.map +1 -1
  62. package/dist-server/server/modules/providers/tests/provider-models.service.test.js +175 -292
  63. package/dist-server/server/modules/providers/tests/provider-models.service.test.js.map +1 -1
  64. package/dist-server/server/modules/providers/tests/provider-runtime.service.test.js +2 -6
  65. package/dist-server/server/modules/providers/tests/provider-runtime.service.test.js.map +1 -1
  66. package/dist-server/server/modules/providers/tests/provider-token-usage.service.test.js +1 -0
  67. package/dist-server/server/modules/providers/tests/provider-token-usage.service.test.js.map +1 -1
  68. package/dist-server/server/modules/providers/tests/provider.routes.test.js +126 -0
  69. package/dist-server/server/modules/providers/tests/provider.routes.test.js.map +1 -0
  70. package/dist-server/server/modules/providers/tests/sessions.service.test.js +87 -0
  71. package/dist-server/server/modules/providers/tests/sessions.service.test.js.map +1 -0
  72. package/dist-server/server/modules/websocket/services/chat-websocket.service.js +5 -1
  73. package/dist-server/server/modules/websocket/services/chat-websocket.service.js.map +1 -1
  74. package/package.json +3 -3
  75. package/server/modules/commands/commands.routes.ts +3 -3
  76. package/server/modules/commands/tests/commands.test.ts +2 -7
  77. package/server/modules/database/index.ts +2 -0
  78. package/server/modules/database/migrations.ts +20 -0
  79. package/server/modules/database/repositories/provider-models.ts +168 -0
  80. package/server/modules/database/repositories/sessions.db.ts +85 -8
  81. package/server/modules/database/schema.ts +29 -3
  82. package/server/modules/database/tests/provider-models.db.integration.test.ts +125 -0
  83. package/server/modules/database/tests/sessions-provider-mapping.test.ts +2 -1
  84. package/server/modules/database/tests/sessions.db.integration.test.ts +32 -0
  85. package/server/modules/file-tree/file-tree.module.ts +5 -1
  86. package/server/modules/file-tree/file-tree.service.ts +68 -20
  87. package/server/modules/file-tree/tests/file-tree.service.test.ts +104 -7
  88. package/server/modules/providers/list/claude/claude-models.provider.ts +46 -14
  89. package/server/modules/providers/list/claude/claude-runtime.provider.js +8 -16
  90. package/server/modules/providers/list/codex/codex-models.provider.ts +58 -99
  91. package/server/modules/providers/list/codex/codex-session-synchronizer.provider.ts +1 -60
  92. package/server/modules/providers/list/codex/codex-sessions.provider.ts +7 -7
  93. package/server/modules/providers/list/cursor/cursor-models.provider.ts +32 -710
  94. package/server/modules/providers/list/opencode/opencode-models.provider.ts +105 -413
  95. package/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.ts +0 -13
  96. package/server/modules/providers/provider.routes.ts +185 -33
  97. package/server/modules/providers/services/provider-models.service.ts +223 -288
  98. package/server/modules/providers/services/provider-runtime.service.ts +2 -3
  99. package/server/modules/providers/services/session-conversations-search.service.ts +12 -4
  100. package/server/modules/providers/services/sessions.service.ts +87 -3
  101. package/server/modules/providers/tests/codex-sessions.test.ts +57 -18
  102. package/server/modules/providers/tests/opencode-models.test.ts +34 -135
  103. package/server/modules/providers/tests/opencode-sessions.test.ts +5 -5
  104. package/server/modules/providers/tests/provider-models.service.test.ts +353 -450
  105. package/server/modules/providers/tests/provider-runtime.service.test.ts +2 -6
  106. package/server/modules/providers/tests/provider-token-usage.service.test.ts +1 -0
  107. package/server/modules/providers/tests/provider.routes.test.ts +183 -0
  108. package/server/modules/providers/tests/sessions.service.test.ts +127 -0
  109. package/server/modules/websocket/services/chat-websocket.service.ts +5 -1
  110. package/server/shared/interfaces.ts +4 -6
  111. package/server/shared/types.ts +29 -17
  112. package/dist/assets/index-DSlds633.css +0 -32
@@ -3,12 +3,13 @@ import path from 'node:path';
3
3
  import ignore from 'ignore';
4
4
 
5
5
  import type {
6
+ FileTreeDirectoryEntry,
6
7
  FileTreeNode,
7
8
  FileTreeServiceDependencies,
8
9
  FileTreeServices,
9
10
  FileTreeUploadedFile,
10
11
  } from '@/shared/types.js';
11
- import { AppError } from '@/shared/utils.js';
12
+ import { AppError, FORBIDDEN_WORKSPACE_PATHS, normalizeProjectPath } from '@/shared/utils.js';
12
13
 
13
14
  const IGNORED_DIRECTORY_NAMES = new Set([
14
15
  'node_modules', 'dist', 'build', '.next', '.nuxt', '.cache', '.parcel-cache',
@@ -28,6 +29,11 @@ const COMMON_WORKSPACE_DIRECTORY_NAMES = [
28
29
  'workspace',
29
30
  ];
30
31
 
32
+ // File Tree consumes this guard when recursively listing a project so a very
33
+ // broad workspace (for example, a user's home directory) cannot exhaust the
34
+ // server heap before the browser has a chance to switch to a narrower project.
35
+ const MAXIMUM_FILE_TREE_ENTRIES = 10_000;
36
+
31
37
  type FileTreeEntryFilter = (entryPath: string, isDirectory: boolean) => boolean;
32
38
 
33
39
  function createFileTreeError(message: string, statusCode: number, code: string): AppError {
@@ -167,35 +173,70 @@ export function createFileTreeService(dependencies: FileTreeServiceDependencies)
167
173
  return projectRoot;
168
174
  }
169
175
 
170
- async function buildFileTree(
176
+ /**
177
+ * Streams one directory and keeps only the entries the tree will show.
178
+ *
179
+ * Entries are filtered and charged against the shared budget one at a time,
180
+ * so a pathologically large directory stops the walk at the cap instead of
181
+ * being read into memory in full first.
182
+ */
183
+ async function collectVisibleEntries(
171
184
  directoryPath: string,
172
- maximumDepth: number,
173
- currentDepth = 0,
174
- includeEntry: FileTreeEntryFilter = () => true,
175
- ): Promise<FileTreeNode[]> {
176
- let entries;
185
+ includeEntry: FileTreeEntryFilter,
186
+ remainingEntries: { value: number },
187
+ ): Promise<FileTreeDirectoryEntry[]> {
188
+ const visibleEntries: FileTreeDirectoryEntry[] = [];
189
+
190
+ await acquire();
177
191
  try {
178
- await acquire();
179
- try {
180
- entries = await fileSystem.readdir(directoryPath);
181
- } finally {
182
- release();
192
+ for await (const entry of fileSystem.openDirectory(directoryPath)) {
193
+ const isDirectory = entry.isDirectory();
194
+ if (isDirectory && IGNORED_DIRECTORY_NAMES.has(entry.name)) {
195
+ continue;
196
+ }
197
+ if (!includeEntry(path.join(directoryPath, entry.name), isDirectory)) {
198
+ continue;
199
+ }
200
+
201
+ // Charged after ignore filtering so generated directories and
202
+ // .gitignore exclusions never consume the budget. Every recursive
203
+ // branch shares one counter, so the cap applies to the whole tree
204
+ // rather than per directory.
205
+ if (remainingEntries.value <= 0) {
206
+ throw createFileTreeError(
207
+ `Project file tree exceeds the ${MAXIMUM_FILE_TREE_ENTRIES.toLocaleString()} entry limit. Choose a narrower project directory or add ignore rules.`,
208
+ 413,
209
+ 'FILE_TREE_TOO_LARGE',
210
+ );
211
+ }
212
+ remainingEntries.value -= 1;
213
+ visibleEntries.push(entry);
183
214
  }
184
215
  } catch (error) {
216
+ // The entry cap is a caller-visible outcome, not an unreadable directory.
217
+ if (error instanceof AppError) {
218
+ throw error;
219
+ }
185
220
  const errorCode = readErrorCode(error);
186
221
  if (errorCode !== 'EACCES' && errorCode !== 'EPERM') {
187
222
  dependencies.logger.error(`Error reading directory "${directoryPath}"`, error);
188
223
  }
189
224
  return [];
225
+ } finally {
226
+ release();
190
227
  }
191
228
 
192
- const visibleEntries = entries.filter((entry) => {
193
- const isDirectory = entry.isDirectory();
194
- if (isDirectory && IGNORED_DIRECTORY_NAMES.has(entry.name)) {
195
- return false;
196
- }
197
- return includeEntry(path.join(directoryPath, entry.name), isDirectory);
198
- });
229
+ return visibleEntries;
230
+ }
231
+
232
+ async function buildFileTree(
233
+ directoryPath: string,
234
+ maximumDepth: number,
235
+ currentDepth = 0,
236
+ includeEntry: FileTreeEntryFilter = () => true,
237
+ remainingEntries = { value: MAXIMUM_FILE_TREE_ENTRIES },
238
+ ): Promise<FileTreeNode[]> {
239
+ const visibleEntries = await collectVisibleEntries(directoryPath, includeEntry, remainingEntries);
199
240
 
200
241
  const items = await Promise.all(visibleEntries.map(async (entry): Promise<FileTreeNode> => {
201
242
  const itemPath = path.join(directoryPath, entry.name);
@@ -233,12 +274,19 @@ export function createFileTreeService(dependencies: FileTreeServiceDependencies)
233
274
  // Metadata failures should not hide an otherwise readable tree entry.
234
275
  }
235
276
 
236
- if (entry.isDirectory() && currentDepth < maximumDepth) {
277
+ // Skip recursing into pseudo-filesystems and other system-critical
278
+ // directories (e.g. /proc, /sys) — they're never valid project roots,
279
+ // and /proc in particular can contain thousands of virtual entries
280
+ // that make traversal from a broad root (e.g. "/") pathologically slow.
281
+ const isForbiddenSystemDir = FORBIDDEN_WORKSPACE_PATHS.includes(normalizeProjectPath(itemPath));
282
+
283
+ if (entry.isDirectory() && currentDepth < maximumDepth && !isForbiddenSystemDir) {
237
284
  item.children = await buildFileTree(
238
285
  itemPath,
239
286
  maximumDepth,
240
287
  currentDepth + 1,
241
288
  includeEntry,
289
+ remainingEntries,
242
290
  );
243
291
  }
244
292
 
@@ -19,6 +19,18 @@ function createDirectoryEntry(name: string, directory: boolean): FileTreeDirecto
19
19
  };
20
20
  }
21
21
 
22
+ /**
23
+ * Adapts a path-keyed listing to the streaming directory contract so tests keep
24
+ * describing directories as plain arrays.
25
+ */
26
+ function createDirectoryReader(
27
+ listDirectory: (directoryPath: string) => FileTreeDirectoryEntry[],
28
+ ): FileTreeFileSystem['openDirectory'] {
29
+ return async function* openDirectory(directoryPath) {
30
+ yield* listDirectory(directoryPath);
31
+ };
32
+ }
33
+
22
34
  function createStats(directory: boolean, mode: number): FileTreeStats {
23
35
  return {
24
36
  size: directory ? 0 : 24,
@@ -40,7 +52,9 @@ function createFakeFileSystem(
40
52
  access: unexpectedOperation,
41
53
  stat: unexpectedOperation,
42
54
  lstat: unexpectedOperation,
43
- readdir: unexpectedOperation,
55
+ openDirectory: () => ({
56
+ [Symbol.asyncIterator]: () => ({ next: unexpectedOperation }),
57
+ }),
44
58
  realpath: unexpectedOperation,
45
59
  readTextFile: unexpectedOperation,
46
60
  writeTextFile: unexpectedOperation,
@@ -78,7 +92,7 @@ test('listProjectFiles builds a sorted tree and skips generated directories', as
78
92
  const sourceDirectory = path.join(projectRoot, 'src');
79
93
  const fileSystem = createFakeFileSystem({
80
94
  access: async () => undefined,
81
- readdir: async (directoryPath) => {
95
+ openDirectory: createDirectoryReader((directoryPath) => {
82
96
  if (directoryPath === projectRoot) {
83
97
  return [
84
98
  createDirectoryEntry('node_modules', true),
@@ -90,7 +104,7 @@ test('listProjectFiles builds a sorted tree and skips generated directories', as
90
104
  return [createDirectoryEntry('index.ts', false)];
91
105
  }
92
106
  return [];
93
- },
107
+ }),
94
108
  lstat: async (candidatePath) => createStats(candidatePath === sourceDirectory, 0o754),
95
109
  });
96
110
  const service = createFileTreeService(createDependencies(fileSystem, projectRoot));
@@ -117,7 +131,7 @@ test('listProjectFiles excludes gitignored entries only when requested', async (
117
131
  assert.equal(filePath, path.join(projectRoot, '.gitignore'));
118
132
  return ['*.log', '!keep.log', 'cache/', 'src/generated.ts'].join('\n');
119
133
  },
120
- readdir: async (directoryPath) => {
134
+ openDirectory: createDirectoryReader((directoryPath) => {
121
135
  readDirectories.push(directoryPath);
122
136
  if (directoryPath === projectRoot) {
123
137
  return [
@@ -138,7 +152,7 @@ test('listProjectFiles excludes gitignored entries only when requested', async (
138
152
  ];
139
153
  }
140
154
  return [];
141
- },
155
+ }),
142
156
  lstat: async (candidatePath) => createStats(
143
157
  candidatePath === cacheDirectory || candidatePath === sourceDirectory,
144
158
  0o644,
@@ -160,9 +174,9 @@ test('listProjectFiles returns the normal tree when no gitignore exists', async
160
174
  readTextFile: async () => {
161
175
  throw Object.assign(new Error('missing'), { code: 'ENOENT' });
162
176
  },
163
- readdir: async (directoryPath) => directoryPath === projectRoot
177
+ openDirectory: createDirectoryReader((directoryPath) => directoryPath === projectRoot
164
178
  ? [createDirectoryEntry('debug.log', false)]
165
- : [],
179
+ : []),
166
180
  lstat: async () => createStats(false, 0o644),
167
181
  });
168
182
  const service = createFileTreeService(createDependencies(fileSystem, projectRoot));
@@ -172,6 +186,89 @@ test('listProjectFiles returns the normal tree when no gitignore exists', async
172
186
  assert.deepEqual(tree.map((entry) => entry.name), ['debug.log']);
173
187
  });
174
188
 
189
+ test('listProjectFiles rejects a tree that exceeds the server entry limit', async () => {
190
+ const projectRoot = path.resolve('file-tree-test-project');
191
+ const fileSystem = createFakeFileSystem({
192
+ access: async () => undefined,
193
+ openDirectory: createDirectoryReader((directoryPath) => directoryPath === projectRoot
194
+ ? Array.from({ length: 10_001 }, (_, index) => createDirectoryEntry(`file-${index}.txt`, false))
195
+ : []),
196
+ lstat: async () => createStats(false, 0o644),
197
+ });
198
+ const service = createFileTreeService(createDependencies(fileSystem, projectRoot));
199
+
200
+ await assert.rejects(
201
+ service.listProjectFiles('project-1'),
202
+ (error: unknown) => error instanceof AppError
203
+ && error.code === 'FILE_TREE_TOO_LARGE'
204
+ && error.statusCode === 413,
205
+ );
206
+ });
207
+
208
+ test('listProjectFiles abandons a directory stream as soon as the entry limit is passed', async () => {
209
+ const projectRoot = path.resolve('file-tree-test-project');
210
+ let streamedEntries = 0;
211
+ const fileSystem = createFakeFileSystem({
212
+ access: async () => undefined,
213
+ // Endless on purpose: the walk has to stop consuming the stream itself
214
+ // instead of waiting for the directory listing to be materialized.
215
+ openDirectory: async function* (directoryPath) {
216
+ if (directoryPath !== projectRoot) {
217
+ return;
218
+ }
219
+ for (let index = 0; ; index += 1) {
220
+ streamedEntries += 1;
221
+ yield createDirectoryEntry(`file-${index}.txt`, false);
222
+ }
223
+ },
224
+ lstat: async () => createStats(false, 0o644),
225
+ });
226
+ const service = createFileTreeService(createDependencies(fileSystem, projectRoot));
227
+
228
+ await assert.rejects(
229
+ service.listProjectFiles('project-1'),
230
+ (error: unknown) => error instanceof AppError
231
+ && error.code === 'FILE_TREE_TOO_LARGE'
232
+ && error.statusCode === 413,
233
+ );
234
+ // The budget plus the single entry that proves it was exceeded.
235
+ assert.equal(streamedEntries, 10_001);
236
+ });
237
+
238
+ test('listProjectFiles shares the entry limit across nested directories', async () => {
239
+ const projectRoot = path.resolve('file-tree-test-project');
240
+ const firstDirectory = path.join(projectRoot, 'first');
241
+ const secondDirectory = path.join(projectRoot, 'second');
242
+ const directoryPaths = new Set([firstDirectory, secondDirectory]);
243
+ const fileSystem = createFakeFileSystem({
244
+ access: async () => undefined,
245
+ openDirectory: createDirectoryReader((directoryPath) => {
246
+ if (directoryPath === projectRoot) {
247
+ return [
248
+ createDirectoryEntry('first', true),
249
+ createDirectoryEntry('second', true),
250
+ ];
251
+ }
252
+ if (directoryPaths.has(directoryPath)) {
253
+ return Array.from(
254
+ { length: 5_000 },
255
+ (_, index) => createDirectoryEntry(`${path.basename(directoryPath)}-${index}.txt`, false),
256
+ );
257
+ }
258
+ return [];
259
+ }),
260
+ lstat: async (candidatePath) => createStats(directoryPaths.has(candidatePath), 0o644),
261
+ });
262
+ const service = createFileTreeService(createDependencies(fileSystem, projectRoot));
263
+
264
+ await assert.rejects(
265
+ service.listProjectFiles('project-1'),
266
+ (error: unknown) => error instanceof AppError
267
+ && error.code === 'FILE_TREE_TOO_LARGE'
268
+ && error.statusCode === 413,
269
+ );
270
+ });
271
+
175
272
  test('readTextFile rejects traversal before invoking the filesystem adapter', async () => {
176
273
  const projectRoot = path.resolve('file-tree-test-project');
177
274
  const readPaths: string[] = [];
@@ -9,12 +9,12 @@ import type {
9
9
  } from '@/shared/types.js';
10
10
  import { buildDefaultProviderCurrentActiveModel } from '@/shared/utils.js';
11
11
 
12
- export const CLAUDE_FALLBACK_MODELS: ProviderModelsDefinition = {
12
+ export const CLAUDE_PREDEFINED_MODELS: ProviderModelsDefinition = {
13
13
  OPTIONS: [
14
14
  {
15
15
  value: 'default',
16
16
  label: 'Default (recommended)',
17
- description: 'Use the Claude Code default model (currently Sonnet 4.6)',
17
+ description: 'Use the recommended model for your Claude account and deployment.',
18
18
  effort: {
19
19
  default: 'high',
20
20
  values: [
@@ -25,10 +25,25 @@ export const CLAUDE_FALLBACK_MODELS: ProviderModelsDefinition = {
25
25
  ],
26
26
  },
27
27
  },
28
+ {
29
+ value: 'best',
30
+ label: 'Best available',
31
+ description: 'Use Fable 5 when available, otherwise the latest Opus model.',
32
+ effort: {
33
+ default: 'high',
34
+ values: [
35
+ { value: 'low' },
36
+ { value: 'medium' },
37
+ { value: 'high' },
38
+ { value: 'xhigh' },
39
+ { value: 'max' },
40
+ ],
41
+ },
42
+ },
28
43
  {
29
44
  value: 'fable',
30
- label: 'Fable',
31
- description: 'Fable 5 · Most capable for your hardest and longest-running tasks · Uses your limits ~2× faster than Opus',
45
+ label: 'Fable 5',
46
+ description: 'Most capable Claude model for the hardest, longest-running tasks.',
32
47
  effort: {
33
48
  default: 'high',
34
49
  values: [
@@ -41,15 +56,16 @@ export const CLAUDE_FALLBACK_MODELS: ProviderModelsDefinition = {
41
56
  },
42
57
  },
43
58
  {
44
- value: "sonnet",
45
- label: "Sonnet",
46
- description: "Sonnet 4.6 · Best for everyday tasks · $3/$15 per Mtok",
59
+ value: 'sonnet',
60
+ label: 'Sonnet',
61
+ description: 'Latest Sonnet model for everyday coding tasks.',
47
62
  effort: {
48
63
  default: 'high',
49
64
  values: [
50
65
  { value: 'low' },
51
66
  { value: 'medium' },
52
67
  { value: 'high' },
68
+ { value: 'xhigh' },
53
69
  { value: 'max' },
54
70
  ],
55
71
  },
@@ -57,13 +73,14 @@ export const CLAUDE_FALLBACK_MODELS: ProviderModelsDefinition = {
57
73
  {
58
74
  value: 'sonnet[1m]',
59
75
  label: 'Sonnet (1M context)',
60
- description: 'Sonnet 4.6 for long sessions · $3/$15 per Mtok',
76
+ description: 'Latest Sonnet model with a 1M context window.',
61
77
  effort: {
62
78
  default: 'high',
63
79
  values: [
64
80
  { value: 'low' },
65
81
  { value: 'medium' },
66
82
  { value: 'high' },
83
+ { value: 'xhigh' },
67
84
  { value: 'max' },
68
85
  ],
69
86
  },
@@ -71,7 +88,7 @@ export const CLAUDE_FALLBACK_MODELS: ProviderModelsDefinition = {
71
88
  {
72
89
  value: 'opus',
73
90
  label: 'Opus',
74
- description: 'Opus 4.8 · Best for everyday, complex tasks · ~2× usage vs Sonnet',
91
+ description: 'Latest Opus model for complex reasoning and coding tasks.',
75
92
  effort: {
76
93
  default: 'high',
77
94
  values: [
@@ -85,8 +102,8 @@ export const CLAUDE_FALLBACK_MODELS: ProviderModelsDefinition = {
85
102
  },
86
103
  {
87
104
  value: 'opus[1m]',
88
- label: 'Opus 4.8 (1M context)',
89
- description: 'Opus 4.8 with 1M context · Most capable for complex work · $5/$25 per Mtok',
105
+ label: 'Opus (1M context)',
106
+ description: 'Latest Opus model with a 1M context window.',
90
107
  effort: {
91
108
  default: 'high',
92
109
  values: [
@@ -101,7 +118,22 @@ export const CLAUDE_FALLBACK_MODELS: ProviderModelsDefinition = {
101
118
  {
102
119
  value: 'haiku',
103
120
  label: 'Haiku',
104
- description: 'Haiku 4.5 · Fastest for quick answers · $1/$5 per Mtok',
121
+ description: 'Fast and efficient Claude model for simple tasks.',
122
+ },
123
+ {
124
+ value: 'opusplan',
125
+ label: 'Opus Plan',
126
+ description: 'Use Opus while planning, then switch to Sonnet for execution.',
127
+ effort: {
128
+ default: 'high',
129
+ values: [
130
+ { value: 'low' },
131
+ { value: 'medium' },
132
+ { value: 'high' },
133
+ { value: 'xhigh' },
134
+ { value: 'max' },
135
+ ],
136
+ },
105
137
  },
106
138
  ],
107
139
  DEFAULT: 'default',
@@ -113,7 +145,7 @@ export const findClaudeModelOption = (model: string | undefined | null): Provide
113
145
  return null;
114
146
  }
115
147
 
116
- return CLAUDE_FALLBACK_MODELS.OPTIONS.find((option) => option.value === normalizedModel) ?? null;
148
+ return CLAUDE_PREDEFINED_MODELS.OPTIONS.find((option) => option.value === normalizedModel) ?? null;
117
149
  };
118
150
  type ClaudeInitEvent = {
119
151
  sessionId?: string;
@@ -237,7 +269,7 @@ export class ClaudeProviderModels implements IProviderModels {
237
269
  // const supportedModels = await queryInstance.supportedModels();
238
270
  // queryInstance.close();
239
271
  // return buildClaudeModelsDefinition(supportedModels);
240
- return CLAUDE_FALLBACK_MODELS;
272
+ return CLAUDE_PREDEFINED_MODELS;
241
273
  }
242
274
 
243
275
  async getCurrentActiveModel(sessionId?: string): Promise<ProviderCurrentActiveModel> {
@@ -24,7 +24,7 @@ import {
24
24
  buildClaudeUserContent,
25
25
  normalizeImageDescriptors
26
26
  } from '@/shared/image-attachments.js';
27
- import { CLAUDE_FALLBACK_MODELS } from '@/modules/providers/list/claude/claude-models.provider.js';
27
+ import { CLAUDE_PREDEFINED_MODELS } from '@/modules/providers/list/claude/claude-models.provider.js';
28
28
  import { resolveClaudeCodeExecutablePath } from '@/shared/claude-cli-path.js';
29
29
  import {
30
30
  createNotificationEvent,
@@ -45,7 +45,7 @@ const TOOL_APPROVAL_TIMEOUT_MS = parseInt(process.env.CLAUDE_TOOL_APPROVAL_TIMEO
45
45
 
46
46
  const TOOLS_REQUIRING_INTERACTION = new Set(['AskUserQuestion', 'ExitPlanMode']);
47
47
 
48
- function resolveClaudeEffort(model, effort, modelsDefinition = CLAUDE_FALLBACK_MODELS) {
48
+ function resolveClaudeEffort(model, effort, modelsDefinition = CLAUDE_PREDEFINED_MODELS) {
49
49
  const selectedModel = modelsDefinition?.OPTIONS?.find((option) => option.value === model) || null;
50
50
  const allowedEfforts = selectedModel?.effort?.values
51
51
  ?.map((value) => value.value) || [];
@@ -165,6 +165,9 @@ function mapCliOptionsToSDK(options = {}) {
165
165
 
166
166
  // Forward all host env vars (e.g. ANTHROPIC_BASE_URL) to the subprocess.
167
167
  // Since SDK 0.2.113, options.env replaces process.env instead of overlaying it.
168
+ // This snapshot is also how CLI-side knobs reach the child: e.g. how long the CLI waits for
169
+ // still-running background agents after a turn ends is set by CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS
170
+ // (default 600000 ms; 0 = wait indefinitely) on this server's own environment.
168
171
  sdkOptions.env = { ...process.env };
169
172
 
170
173
  // Resolve the executable eagerly on Windows because the SDK uses raw child_process.spawn,
@@ -209,12 +212,12 @@ function mapCliOptionsToSDK(options = {}) {
209
212
 
210
213
  sdkOptions.disallowedTools = settings.disallowedTools || [];
211
214
 
212
- sdkOptions.model = options.model || CLAUDE_FALLBACK_MODELS.DEFAULT;
215
+ sdkOptions.model = options.model || CLAUDE_PREDEFINED_MODELS.DEFAULT;
213
216
 
214
217
  const resolvedEffort = resolveClaudeEffort(
215
218
  sdkOptions.model,
216
219
  effort,
217
- options.effortModels || CLAUDE_FALLBACK_MODELS,
220
+ options.effortModels || CLAUDE_PREDEFINED_MODELS,
218
221
  );
219
222
  if (resolvedEffort) {
220
223
  sdkOptions.effort = resolvedEffort;
@@ -485,7 +488,7 @@ async function queryClaudeSDK(command, options = {}, ws, context) {
485
488
 
486
489
  try {
487
490
  const resolvedModel = await context.resolveResumeModel(sessionId, options.model);
488
- let effortModels = CLAUDE_FALLBACK_MODELS;
491
+ let effortModels = CLAUDE_PREDEFINED_MODELS;
489
492
  try {
490
493
  effortModels = await context.getProviderModels();
491
494
  } catch (error) {
@@ -610,10 +613,6 @@ async function queryClaudeSDK(command, options = {}, ws, context) {
610
613
  return { behavior: 'deny', message: decision.message ?? 'User denied tool use' };
611
614
  };
612
615
 
613
- // Query constructor reads this synchronously.
614
- const prevStreamTimeout = process.env.CLAUDE_CODE_STREAM_CLOSE_TIMEOUT;
615
- process.env.CLAUDE_CODE_STREAM_CLOSE_TIMEOUT = '300000';
616
-
617
616
  let queryInstance;
618
617
  try {
619
618
  queryInstance = query({
@@ -631,13 +630,6 @@ async function queryClaudeSDK(command, options = {}, ws, context) {
631
630
  });
632
631
  }
633
632
 
634
- // Restore immediately — Query constructor already captured the value
635
- if (prevStreamTimeout !== undefined) {
636
- process.env.CLAUDE_CODE_STREAM_CLOSE_TIMEOUT = prevStreamTimeout;
637
- } else {
638
- delete process.env.CLAUDE_CODE_STREAM_CLOSE_TIMEOUT;
639
- }
640
-
641
633
  // Track the query instance for abort capability
642
634
  if (sessionKey()) {
643
635
  addSession(sessionKey(), queryInstance, ws);