@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
@@ -61,12 +61,8 @@ function createService(providers: IProvider[]) {
61
61
  },
62
62
  async getProviderModels() {
63
63
  return {
64
- models: { OPTIONS: [], DEFAULT: 'default-model' },
65
- cache: {
66
- updatedAt: new Date(0).toISOString(),
67
- expiresAt: new Date(0).toISOString(),
68
- source: 'fresh',
69
- },
64
+ OPTIONS: [],
65
+ DEFAULT: 'default-model',
70
66
  };
71
67
  },
72
68
  });
@@ -18,6 +18,7 @@ function createSessionRow(overrides: Record<string, unknown> = {}) {
18
18
  jsonl_path: null,
19
19
  custom_name: null,
20
20
  model: null,
21
+ effort: null,
21
22
  isArchived: 0,
22
23
  created_at: '2026-01-01T00:00:00.000Z',
23
24
  updated_at: '2026-01-01T00:00:00.000Z',
@@ -0,0 +1,183 @@
1
+ import assert from 'node:assert/strict';
2
+ import { once } from 'node:events';
3
+ import { mkdtemp, rm, writeFile } from 'node:fs/promises';
4
+ import type { AddressInfo } from 'node:net';
5
+ import os from 'node:os';
6
+ import path from 'node:path';
7
+ import test from 'node:test';
8
+
9
+ import express, { type NextFunction, type Request, type Response } from 'express';
10
+
11
+ import { closeConnection, initializeDatabase, sessionsDb } from '@/modules/database/index.js';
12
+ import providerRouter from '@/modules/providers/provider.routes.js';
13
+ import { AppError } from '@/shared/utils.js';
14
+
15
+ async function withProviderServer(
16
+ run: (baseUrl: string, workspacePath: string) => Promise<void>,
17
+ ): Promise<void> {
18
+ const previousDatabasePath = process.env.DATABASE_PATH;
19
+ const tempDirectory = await mkdtemp(path.join(os.tmpdir(), 'provider-routes-'));
20
+
21
+ closeConnection();
22
+ process.env.DATABASE_PATH = path.join(tempDirectory, 'auth.db');
23
+ await writeFile(process.env.DATABASE_PATH, '');
24
+ await initializeDatabase();
25
+
26
+ const app = express().use(express.json()).use('/api/providers', providerRouter);
27
+ app.use((error: unknown, _req: Request, res: Response, _next: NextFunction) => {
28
+ if (error instanceof AppError) {
29
+ res.status(error.statusCode).json({
30
+ success: false,
31
+ error: { code: error.code, message: error.message },
32
+ });
33
+ return;
34
+ }
35
+ res.status(500).json({ success: false, error: { code: 'INTERNAL_ERROR' } });
36
+ });
37
+ const server = app.listen(0, '127.0.0.1');
38
+ await once(server, 'listening');
39
+
40
+ try {
41
+ const address = server.address() as AddressInfo;
42
+ await run(`http://127.0.0.1:${address.port}`, path.join(tempDirectory, 'workspace'));
43
+ } finally {
44
+ await new Promise<void>((resolve, reject) => {
45
+ server.close((error) => error ? reject(error) : resolve());
46
+ });
47
+ closeConnection();
48
+ if (previousDatabasePath === undefined) {
49
+ delete process.env.DATABASE_PATH;
50
+ } else {
51
+ process.env.DATABASE_PATH = previousDatabasePath;
52
+ }
53
+ await rm(tempDirectory, { recursive: true, force: true });
54
+ }
55
+ }
56
+
57
+ test('session creation route names a CloudCLI session from the initial message', async () => {
58
+ await withProviderServer(async (baseUrl, workspacePath) => {
59
+ const response = await fetch(`${baseUrl}/api/providers/sessions`, {
60
+ method: 'POST',
61
+ headers: { 'content-type': 'application/json' },
62
+ body: JSON.stringify({
63
+ provider: 'codex',
64
+ projectPath: workspacePath,
65
+ initialMessage: 'abcd efg\nhij klm nop',
66
+ }),
67
+ });
68
+ const payload = await response.json() as {
69
+ data: { sessionId: string; sessionName: string };
70
+ };
71
+
72
+ assert.equal(response.status, 201);
73
+ assert.equal(payload.data.sessionName, 'abcd efg hij klm');
74
+ assert.equal(
75
+ sessionsDb.getSessionById(payload.data.sessionId)?.custom_name,
76
+ 'abcd efg hij klm',
77
+ );
78
+ });
79
+ });
80
+
81
+ test('reasoning effort is persisted and returned with the active session model', async () => {
82
+ await withProviderServer(async (baseUrl, workspacePath) => {
83
+ sessionsDb.createAppSession('effort-session', 'codex', workspacePath);
84
+
85
+ const updateResponse = await fetch(
86
+ `${baseUrl}/api/providers/codex/sessions/effort-session/active-effort`,
87
+ {
88
+ method: 'POST',
89
+ headers: { 'content-type': 'application/json' },
90
+ body: JSON.stringify({ effort: 'ultra' }),
91
+ },
92
+ );
93
+ const updatePayload = await updateResponse.json() as {
94
+ data: { effort: string; sessionId: string };
95
+ };
96
+
97
+ assert.equal(updateResponse.status, 200);
98
+ assert.equal(updatePayload.data.effort, 'ultra');
99
+ assert.equal(sessionsDb.getSessionById('effort-session')?.effort, 'ultra');
100
+
101
+ const readResponse = await fetch(
102
+ `${baseUrl}/api/providers/codex/sessions/effort-session/active-model`,
103
+ );
104
+ const readPayload = await readResponse.json() as {
105
+ data: { effort: string | null; sessionId: string };
106
+ };
107
+
108
+ assert.equal(readResponse.status, 200);
109
+ assert.equal(readPayload.data.sessionId, 'effort-session');
110
+ assert.equal(readPayload.data.effort, 'ultra');
111
+ });
112
+ });
113
+
114
+ test('model routes expose immutable defaults and full custom model CRUD', async () => {
115
+ await withProviderServer(async (baseUrl) => {
116
+ const initialResponse = await fetch(`${baseUrl}/api/providers/codex/models`);
117
+ const initialPayload = await initialResponse.json() as {
118
+ data: {
119
+ cache?: unknown;
120
+ models: {
121
+ OPTIONS: Array<{ recordId?: number; value: string; isCustom: boolean }>;
122
+ };
123
+ };
124
+ };
125
+ assert.equal(initialResponse.status, 200);
126
+ assert.equal('cache' in initialPayload.data, false);
127
+ const predefined = initialPayload.data.models.OPTIONS[0];
128
+ assert.equal(predefined.isCustom, false);
129
+ assert.equal(predefined.recordId, undefined);
130
+
131
+ const createResponse = await fetch(`${baseUrl}/api/providers/codex/models`, {
132
+ method: 'POST',
133
+ headers: { 'content-type': 'application/json' },
134
+ body: JSON.stringify({ model: 'Gateway GPT', id: 'gateway/gpt' }),
135
+ });
136
+ const createPayload = await createResponse.json() as {
137
+ data: { model: { recordId: number; value: string; label: string; isCustom: boolean } };
138
+ };
139
+ assert.equal(createResponse.status, 201);
140
+ assert.equal(createPayload.data.model.isCustom, true);
141
+ const customRecordId = createPayload.data.model.recordId;
142
+
143
+ const updateResponse = await fetch(
144
+ `${baseUrl}/api/providers/codex/models/${customRecordId}`,
145
+ {
146
+ method: 'PATCH',
147
+ headers: { 'content-type': 'application/json' },
148
+ body: JSON.stringify({ model: 'Gateway GPT Updated', id: 'gateway/gpt-v2' }),
149
+ },
150
+ );
151
+ const updatePayload = await updateResponse.json() as {
152
+ data: { model: { value: string; label: string } };
153
+ };
154
+ assert.equal(updateResponse.status, 200);
155
+ assert.equal(updatePayload.data.model.value, 'gateway/gpt-v2');
156
+ assert.equal(updatePayload.data.model.label, 'Gateway GPT Updated');
157
+
158
+ const immutableResponse = await fetch(
159
+ `${baseUrl}/api/providers/codex/models/999999`,
160
+ {
161
+ method: 'PATCH',
162
+ headers: { 'content-type': 'application/json' },
163
+ body: JSON.stringify({ model: 'Changed', id: 'changed' }),
164
+ },
165
+ );
166
+ const immutablePayload = await immutableResponse.json() as { error: { code: string } };
167
+ assert.equal(immutableResponse.status, 404);
168
+ assert.equal(immutablePayload.error.code, 'MODEL_NOT_FOUND');
169
+
170
+ const deleteResponse = await fetch(
171
+ `${baseUrl}/api/providers/codex/models/${customRecordId}`,
172
+ { method: 'DELETE' },
173
+ );
174
+ const deletePayload = await deleteResponse.json() as {
175
+ data: { models: { OPTIONS: Array<{ recordId: number }> } };
176
+ };
177
+ assert.equal(deleteResponse.status, 200);
178
+ assert.equal(
179
+ deletePayload.data.models.OPTIONS.some((option) => option.recordId === customRecordId),
180
+ false,
181
+ );
182
+ });
183
+ });
@@ -0,0 +1,127 @@
1
+ import assert from 'node:assert/strict';
2
+ import { mkdtemp, rm } from 'node:fs/promises';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import test from 'node:test';
6
+
7
+ import { closeConnection, initializeDatabase, projectsDb, sessionsDb } from '@/modules/database/index.js';
8
+ import { sessionsService } from '@/modules/providers/services/sessions.service.js';
9
+
10
+ async function withIsolatedDatabase(runTest: () => void | Promise<void>): Promise<void> {
11
+ const previousDatabasePath = process.env.DATABASE_PATH;
12
+ const tempDirectory = await mkdtemp(path.join(os.tmpdir(), 'sessions-service-db-'));
13
+
14
+ closeConnection();
15
+ process.env.DATABASE_PATH = path.join(tempDirectory, 'auth.db');
16
+ await initializeDatabase();
17
+
18
+ try {
19
+ await runTest();
20
+ } finally {
21
+ closeConnection();
22
+ if (previousDatabasePath === undefined) {
23
+ delete process.env.DATABASE_PATH;
24
+ } else {
25
+ process.env.DATABASE_PATH = previousDatabasePath;
26
+ }
27
+ await rm(tempDirectory, { recursive: true, force: true });
28
+ }
29
+ }
30
+
31
+ test('provider session id returns the mapped native id', { concurrency: false }, async () => {
32
+ await withIsolatedDatabase(() => {
33
+ sessionsDb.createAppSession('app-session-id', 'codex', '/tmp/session-id-copy-project');
34
+ sessionsDb.assignProviderSessionId('app-session-id', 'codex-native-session-id');
35
+
36
+ assert.equal(sessionsService.getProviderSessionId('app-session-id'), 'codex-native-session-id');
37
+ });
38
+ });
39
+
40
+ test('app session names use at most four whole words from the initial message', { concurrency: false }, async () => {
41
+ await withIsolatedDatabase(() => {
42
+ const result = sessionsService.createAppSession(
43
+ 'codex',
44
+ '/tmp/session-name-project',
45
+ ' supercalifragilisticexpialidocious\nsecond third fourth fifth ',
46
+ );
47
+
48
+ assert.equal(result.sessionName, 'supercalifragilisticexpialidocious second third fourth');
49
+ assert.equal(
50
+ sessionsDb.getSessionById(result.sessionId)?.custom_name,
51
+ 'supercalifragilisticexpialidocious second third fourth',
52
+ );
53
+ });
54
+ });
55
+
56
+ test('app sessions without message text receive a stable fallback name', { concurrency: false }, async () => {
57
+ await withIsolatedDatabase(() => {
58
+ const result = sessionsService.createAppSession('claude', '/tmp/attachment-only-project', ' \n ');
59
+
60
+ assert.equal(result.sessionName, 'Untitled Session');
61
+ assert.equal(sessionsDb.getSessionById(result.sessionId)?.custom_name, 'Untitled Session');
62
+ });
63
+ });
64
+
65
+ test('provider session id is unavailable until the provider assigns one', { concurrency: false }, async () => {
66
+ await withIsolatedDatabase(() => {
67
+ sessionsDb.createAppSession('pending-app-session', 'claude', '/tmp/session-id-copy-project');
68
+
69
+ assert.throws(
70
+ () => sessionsService.getProviderSessionId('pending-app-session'),
71
+ (error: unknown) => {
72
+ const typedError = error as { code?: string; statusCode?: number };
73
+ return typedError.code === 'PROVIDER_SESSION_ID_NOT_AVAILABLE' && typedError.statusCode === 409;
74
+ },
75
+ );
76
+ });
77
+ });
78
+
79
+ test('provider session id reports a missing app session', { concurrency: false }, async () => {
80
+ await withIsolatedDatabase(() => {
81
+ assert.throws(
82
+ () => sessionsService.getProviderSessionId('missing-session'),
83
+ (error: unknown) => {
84
+ const typedError = error as { code?: string; statusCode?: number };
85
+ return typedError.code === 'SESSION_NOT_FOUND' && typedError.statusCode === 404;
86
+ },
87
+ );
88
+ });
89
+ });
90
+
91
+ test('recent sessions map project metadata and preserve database pagination', { concurrency: false }, async () => {
92
+ await withIsolatedDatabase(() => {
93
+ sessionsDb.createSession(
94
+ 'older-session',
95
+ 'claude',
96
+ '/tmp/recent-project',
97
+ 'Older conversation',
98
+ '2026-08-01T08:00:00.000Z',
99
+ '2026-08-01T09:00:00.000Z',
100
+ );
101
+ sessionsDb.createSession(
102
+ 'newer-session',
103
+ 'codex',
104
+ '/tmp/recent-project',
105
+ 'Newer conversation',
106
+ '2026-08-01T10:00:00.000Z',
107
+ '2026-08-01T11:00:00.000Z',
108
+ );
109
+ projectsDb.updateCustomProjectName('/tmp/recent-project', 'Recent Project');
110
+
111
+ const project = projectsDb.getProjectPath('/tmp/recent-project');
112
+ const page = sessionsService.listRecentSessions(1, 0);
113
+
114
+ assert.deepEqual(page, {
115
+ conversations: [{
116
+ sessionId: 'newer-session',
117
+ provider: 'codex',
118
+ projectId: project?.project_id ?? null,
119
+ projectDisplayName: 'Recent Project',
120
+ sessionTitle: 'Newer conversation',
121
+ lastActivity: '2026-08-01T11:00:00.000Z',
122
+ }],
123
+ total: 2,
124
+ hasMore: true,
125
+ });
126
+ });
127
+ });
@@ -194,10 +194,14 @@ async function handleChatSend(
194
194
  const command = typeof data.content === 'string' ? data.content : '';
195
195
 
196
196
  // Record what this turn runs with so reopening the session later restores the
197
- // same model, and so the resume path has a session-scoped answer to use.
197
+ // same model and reasoning effort, and so the resume path has a
198
+ // session-scoped model answer to use.
198
199
  if (typeof clientOptions.model === 'string' && clientOptions.model.trim()) {
199
200
  providerModelsService.setSessionModel(provider, sessionId, clientOptions.model);
200
201
  }
202
+ if (typeof clientOptions.effort === 'string' && clientOptions.effort.trim()) {
203
+ providerModelsService.setSessionEffort(provider, sessionId, clientOptions.effort);
204
+ }
201
205
 
202
206
  const attachmentCandidates = [
203
207
  ...normalizeAttachmentDescriptors(clientOptions.images),
@@ -60,15 +60,13 @@ export interface IProvider {
60
60
  /**
61
61
  * Model catalog contract for one provider.
62
62
  *
63
- * Implementations are responsible for resolving the provider's currently
64
- * supported models and converting them into the shared
65
- * `ProviderModelsDefinition` shape used by backend routes and frontend model
66
- * pickers. The `DEFAULT` field should be the most appropriate default selection
67
- * for that provider at the time the catalog is read.
63
+ * Implementations supply CloudCLI's curated predefined models and can inspect
64
+ * provider-native session state. The Providers service merges these immutable
65
+ * source-controlled definitions with user-created SQLite rows at read time.
68
66
  */
69
67
  export interface IProviderModels {
70
68
  /**
71
- * Returns the provider's currently supported model catalog.
69
+ * Returns the curated predefined catalog owned by this provider adapter.
72
70
  */
73
71
  getSupportedModels(): Promise<ProviderModelsDefinition>;
74
72
 
@@ -75,6 +75,10 @@ export type ProviderModelOption = {
75
75
  value: string;
76
76
  label: string;
77
77
  description?: string;
78
+ /** Stable SQLite row id used only by model-management actions. */
79
+ recordId?: number;
80
+ /** True for user-created rows; false for immutable CloudCLI defaults. */
81
+ isCustom?: boolean;
78
82
  effort?: {
79
83
  default?: string;
80
84
  values: {
@@ -93,28 +97,31 @@ export type ProviderModelsDefinition = {
93
97
  };
94
98
 
95
99
  /**
96
- * Cache metadata returned alongside one provider model catalog.
100
+ * One persisted custom-model row in the provider model library.
97
101
  *
98
- * `updatedAt` is when the current cached snapshot was last refreshed from the
99
- * provider itself. `expiresAt` is the backend cache expiry timestamp, and
100
- * `source` tells callers whether the current response came from in-memory cache,
101
- * persisted disk cache, or a fresh provider fetch.
102
+ * Provider modules use this shape at the database boundary. Predefined models
103
+ * never use this type because they remain source-controlled in provider
104
+ * adapters. `modelId` is sent to the provider runtime, while `model` is the
105
+ * user-supplied display name shown in pickers.
102
106
  */
103
- export type ProviderModelsCacheInfo = {
104
- updatedAt: string;
105
- expiresAt: string;
106
- source: 'memory' | 'disk' | 'fresh';
107
+ export type CustomProviderModelRecord = {
108
+ recordId: number;
109
+ provider: LLMProvider;
110
+ modelId: string;
111
+ model: string;
112
+ sortOrder: number;
107
113
  };
108
114
 
109
115
  /**
110
- * Full provider model lookup result returned by the backend service layer.
116
+ * User-editable values accepted when creating or changing a custom model.
111
117
  *
112
- * Use this shape when a caller needs both the selectable model catalog and the
113
- * cache metadata that explains how current the catalog is.
118
+ * `id` must be the exact provider-facing model identifier and cannot contain
119
+ * whitespace. `model` is a concise display name. The provider is supplied by
120
+ * the route path so a row can never be moved across providers accidentally.
114
121
  */
115
- export type ProviderModelsResult = {
116
- models: ProviderModelsDefinition;
117
- cache: ProviderModelsCacheInfo;
122
+ export type CustomProviderModelInput = {
123
+ id: string;
124
+ model: string;
118
125
  };
119
126
 
120
127
  // ---------------------------
@@ -147,7 +154,8 @@ export type ProviderCurrentActiveModel = {
147
154
  export type ProviderSessionModelSource = 'session' | 'provider' | 'default';
148
155
 
149
156
  /**
150
- * The model one session runs with, plus where that answer came from.
157
+ * The model one session runs with, its persisted reasoning effort when one has
158
+ * been recorded, and where the model answer came from.
151
159
  *
152
160
  * Returned by `providerModelsService.resolveSessionModel` and used by the
153
161
  * `/models`, `/cost` and `/status` commands, the active-model route, and the
@@ -157,6 +165,8 @@ export type ProviderSessionModel = {
157
165
  provider: LLMProvider;
158
166
  sessionId: string | null;
159
167
  model: string;
168
+ /** NULL means this session has not recorded an effort choice yet. */
169
+ effort: string | null;
160
170
  source: ProviderSessionModelSource;
161
171
  };
162
172
 
@@ -944,7 +954,9 @@ export type FileTreeFileSystem = {
944
954
  access(candidatePath: string): Promise<void>;
945
955
  stat(candidatePath: string): Promise<FileTreeStats>;
946
956
  lstat(candidatePath: string): Promise<FileTreeStats>;
947
- readdir(directoryPath: string): Promise<FileTreeDirectoryEntry[]>;
957
+ // Streamed rather than returned as an array so a directory with millions of
958
+ // children is abandoned at the entry limit instead of being materialized.
959
+ openDirectory(directoryPath: string): AsyncIterable<FileTreeDirectoryEntry>;
948
960
  realpath(candidatePath: string): Promise<string>;
949
961
  readTextFile(filePath: string): Promise<string>;
950
962
  writeTextFile(filePath: string, content: string): Promise<void>;