@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.
- package/dist/assets/index-D3jfzuKN.css +32 -0
- package/dist/assets/{index-eGqPeIEu.js → index-DTMWjJj8.js} +324 -305
- package/dist/index.html +2 -2
- package/dist-server/server/modules/commands/commands.routes.js +3 -3
- package/dist-server/server/modules/commands/commands.routes.js.map +1 -1
- package/dist-server/server/modules/commands/tests/commands.test.js +2 -7
- package/dist-server/server/modules/commands/tests/commands.test.js.map +1 -1
- package/dist-server/server/modules/database/index.js +2 -0
- package/dist-server/server/modules/database/index.js.map +1 -1
- package/dist-server/server/modules/database/migrations.js +18 -1
- package/dist-server/server/modules/database/migrations.js.map +1 -1
- package/dist-server/server/modules/database/repositories/provider-models.js +112 -0
- package/dist-server/server/modules/database/repositories/provider-models.js.map +1 -0
- package/dist-server/server/modules/database/repositories/sessions.db.js +63 -7
- package/dist-server/server/modules/database/repositories/sessions.db.js.map +1 -1
- package/dist-server/server/modules/database/schema.js +28 -3
- package/dist-server/server/modules/database/schema.js.map +1 -1
- package/dist-server/server/modules/database/tests/provider-models.db.integration.test.js +102 -0
- package/dist-server/server/modules/database/tests/provider-models.db.integration.test.js.map +1 -0
- package/dist-server/server/modules/database/tests/sessions-provider-mapping.test.js +2 -1
- package/dist-server/server/modules/database/tests/sessions-provider-mapping.test.js.map +1 -1
- package/dist-server/server/modules/database/tests/sessions.db.integration.test.js +22 -0
- package/dist-server/server/modules/database/tests/sessions.db.integration.test.js.map +1 -1
- package/dist-server/server/modules/file-tree/file-tree.module.js +5 -1
- package/dist-server/server/modules/file-tree/file-tree.module.js.map +1 -1
- package/dist-server/server/modules/file-tree/file-tree.service.js +50 -18
- package/dist-server/server/modules/file-tree/file-tree.service.js.map +1 -1
- package/dist-server/server/modules/file-tree/tests/file-tree.service.test.js +83 -7
- package/dist-server/server/modules/file-tree/tests/file-tree.service.test.js.map +1 -1
- package/dist-server/server/modules/providers/list/claude/claude-models.provider.js +46 -14
- package/dist-server/server/modules/providers/list/claude/claude-models.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/claude/claude-runtime.provider.js +8 -15
- package/dist-server/server/modules/providers/list/claude/claude-runtime.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/codex/codex-models.provider.js +58 -71
- package/dist-server/server/modules/providers/list/codex/codex-models.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/codex/codex-session-synchronizer.provider.js +1 -54
- package/dist-server/server/modules/providers/list/codex/codex-session-synchronizer.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/codex/codex-sessions.provider.js +7 -7
- package/dist-server/server/modules/providers/list/codex/codex-sessions.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/cursor/cursor-models.provider.js +32 -669
- package/dist-server/server/modules/providers/list/cursor/cursor-models.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/opencode/opencode-models.provider.js +105 -328
- package/dist-server/server/modules/providers/list/opencode/opencode-models.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.js +0 -12
- package/dist-server/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.js.map +1 -1
- package/dist-server/server/modules/providers/provider.routes.js +139 -31
- package/dist-server/server/modules/providers/provider.routes.js.map +1 -1
- package/dist-server/server/modules/providers/services/provider-models.service.js +167 -199
- package/dist-server/server/modules/providers/services/provider-models.service.js.map +1 -1
- package/dist-server/server/modules/providers/services/provider-runtime.service.js +2 -2
- package/dist-server/server/modules/providers/services/provider-runtime.service.js.map +1 -1
- package/dist-server/server/modules/providers/services/session-conversations-search.service.js +12 -4
- package/dist-server/server/modules/providers/services/session-conversations-search.service.js.map +1 -1
- package/dist-server/server/modules/providers/services/sessions.service.js +62 -3
- package/dist-server/server/modules/providers/services/sessions.service.js.map +1 -1
- package/dist-server/server/modules/providers/tests/codex-sessions.test.js +53 -17
- package/dist-server/server/modules/providers/tests/codex-sessions.test.js.map +1 -1
- package/dist-server/server/modules/providers/tests/opencode-models.test.js +15 -126
- package/dist-server/server/modules/providers/tests/opencode-models.test.js.map +1 -1
- package/dist-server/server/modules/providers/tests/opencode-sessions.test.js +5 -5
- package/dist-server/server/modules/providers/tests/opencode-sessions.test.js.map +1 -1
- package/dist-server/server/modules/providers/tests/provider-models.service.test.js +175 -292
- package/dist-server/server/modules/providers/tests/provider-models.service.test.js.map +1 -1
- package/dist-server/server/modules/providers/tests/provider-runtime.service.test.js +2 -6
- package/dist-server/server/modules/providers/tests/provider-runtime.service.test.js.map +1 -1
- package/dist-server/server/modules/providers/tests/provider-token-usage.service.test.js +1 -0
- package/dist-server/server/modules/providers/tests/provider-token-usage.service.test.js.map +1 -1
- package/dist-server/server/modules/providers/tests/provider.routes.test.js +126 -0
- package/dist-server/server/modules/providers/tests/provider.routes.test.js.map +1 -0
- package/dist-server/server/modules/providers/tests/sessions.service.test.js +87 -0
- package/dist-server/server/modules/providers/tests/sessions.service.test.js.map +1 -0
- package/dist-server/server/modules/websocket/services/chat-websocket.service.js +5 -1
- package/dist-server/server/modules/websocket/services/chat-websocket.service.js.map +1 -1
- package/package.json +3 -3
- package/server/modules/commands/commands.routes.ts +3 -3
- package/server/modules/commands/tests/commands.test.ts +2 -7
- package/server/modules/database/index.ts +2 -0
- package/server/modules/database/migrations.ts +20 -0
- package/server/modules/database/repositories/provider-models.ts +168 -0
- package/server/modules/database/repositories/sessions.db.ts +85 -8
- package/server/modules/database/schema.ts +29 -3
- package/server/modules/database/tests/provider-models.db.integration.test.ts +125 -0
- package/server/modules/database/tests/sessions-provider-mapping.test.ts +2 -1
- package/server/modules/database/tests/sessions.db.integration.test.ts +32 -0
- package/server/modules/file-tree/file-tree.module.ts +5 -1
- package/server/modules/file-tree/file-tree.service.ts +68 -20
- package/server/modules/file-tree/tests/file-tree.service.test.ts +104 -7
- package/server/modules/providers/list/claude/claude-models.provider.ts +46 -14
- package/server/modules/providers/list/claude/claude-runtime.provider.js +8 -16
- package/server/modules/providers/list/codex/codex-models.provider.ts +58 -99
- package/server/modules/providers/list/codex/codex-session-synchronizer.provider.ts +1 -60
- package/server/modules/providers/list/codex/codex-sessions.provider.ts +7 -7
- package/server/modules/providers/list/cursor/cursor-models.provider.ts +32 -710
- package/server/modules/providers/list/opencode/opencode-models.provider.ts +105 -413
- package/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.ts +0 -13
- package/server/modules/providers/provider.routes.ts +185 -33
- package/server/modules/providers/services/provider-models.service.ts +223 -288
- package/server/modules/providers/services/provider-runtime.service.ts +2 -3
- package/server/modules/providers/services/session-conversations-search.service.ts +12 -4
- package/server/modules/providers/services/sessions.service.ts +87 -3
- package/server/modules/providers/tests/codex-sessions.test.ts +57 -18
- package/server/modules/providers/tests/opencode-models.test.ts +34 -135
- package/server/modules/providers/tests/opencode-sessions.test.ts +5 -5
- package/server/modules/providers/tests/provider-models.service.test.ts +353 -450
- package/server/modules/providers/tests/provider-runtime.service.test.ts +2 -6
- package/server/modules/providers/tests/provider-token-usage.service.test.ts +1 -0
- package/server/modules/providers/tests/provider.routes.test.ts +183 -0
- package/server/modules/providers/tests/sessions.service.test.ts +127 -0
- package/server/modules/websocket/services/chat-websocket.service.ts +5 -1
- package/server/shared/interfaces.ts +4 -6
- package/server/shared/types.ts +29 -17
- 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
|
-
|
|
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
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
185
|
+
includeEntry: FileTreeEntryFilter,
|
|
186
|
+
remainingEntries: { value: number },
|
|
187
|
+
): Promise<FileTreeDirectoryEntry[]> {
|
|
188
|
+
const visibleEntries: FileTreeDirectoryEntry[] = [];
|
|
189
|
+
|
|
190
|
+
await acquire();
|
|
177
191
|
try {
|
|
178
|
-
await
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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: '
|
|
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:
|
|
45
|
-
label:
|
|
46
|
-
description:
|
|
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
|
|
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
|
|
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
|
|
89
|
-
description: 'Opus
|
|
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: '
|
|
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
|
|
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
|
|
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 {
|
|
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 =
|
|
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 ||
|
|
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 ||
|
|
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 =
|
|
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);
|