@cloudcli-ai/cloudcli 1.33.0 → 1.33.2

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 (51) hide show
  1. package/README.de.md +1 -1
  2. package/README.ja.md +1 -1
  3. package/README.ko.md +1 -1
  4. package/README.md +1 -1
  5. package/README.ru.md +1 -1
  6. package/README.tr.md +1 -1
  7. package/README.zh-CN.md +1 -1
  8. package/README.zh-TW.md +242 -0
  9. package/dist/assets/{index-skXSjBje.js → index-B0mnqBF-.js} +256 -256
  10. package/dist/assets/index-FSP9Hz7V.css +32 -0
  11. package/dist/index.html +2 -2
  12. package/dist/modelConstants.js +8 -1
  13. package/dist-server/server/claude-sdk.js +8 -1
  14. package/dist-server/server/claude-sdk.js.map +1 -1
  15. package/dist-server/server/index.js +118 -50
  16. package/dist-server/server/index.js.map +1 -1
  17. package/dist-server/server/modules/projects/projects.routes.js +10 -2
  18. package/dist-server/server/modules/projects/projects.routes.js.map +1 -1
  19. package/dist-server/server/modules/providers/list/claude/claude-auth.provider.js +3 -0
  20. package/dist-server/server/modules/providers/list/claude/claude-auth.provider.js.map +1 -1
  21. package/dist-server/server/modules/providers/list/claude/claude-models.provider.js +9 -4
  22. package/dist-server/server/modules/providers/list/claude/claude-models.provider.js.map +1 -1
  23. package/dist-server/server/modules/providers/services/provider-models.service.js +27 -0
  24. package/dist-server/server/modules/providers/services/provider-models.service.js.map +1 -1
  25. package/dist-server/server/modules/providers/tests/provider-models.service.test.js +28 -0
  26. package/dist-server/server/modules/providers/tests/provider-models.service.test.js.map +1 -1
  27. package/dist-server/server/modules/websocket/services/shell-websocket.service.js +9 -2
  28. package/dist-server/server/modules/websocket/services/shell-websocket.service.js.map +1 -1
  29. package/dist-server/server/modules/websocket/services/websocket-auth.service.js +6 -2
  30. package/dist-server/server/modules/websocket/services/websocket-auth.service.js.map +1 -1
  31. package/dist-server/server/modules/websocket/services/websocket-server.service.js +18 -0
  32. package/dist-server/server/modules/websocket/services/websocket-server.service.js.map +1 -1
  33. package/dist-server/server/routes/agent.js +3 -2
  34. package/dist-server/server/routes/agent.js.map +1 -1
  35. package/dist-server/server/routes/commands.js +19 -6
  36. package/dist-server/server/routes/commands.js.map +1 -1
  37. package/package.json +3 -2
  38. package/public/modelConstants.js +8 -1
  39. package/server/claude-sdk.js +8 -1
  40. package/server/index.js +131 -58
  41. package/server/modules/projects/projects.routes.ts +11 -2
  42. package/server/modules/providers/list/claude/claude-auth.provider.ts +4 -0
  43. package/server/modules/providers/list/claude/claude-models.provider.ts +9 -4
  44. package/server/modules/providers/services/provider-models.service.ts +33 -0
  45. package/server/modules/providers/tests/provider-models.service.test.ts +31 -0
  46. package/server/modules/websocket/services/shell-websocket.service.ts +13 -2
  47. package/server/modules/websocket/services/websocket-auth.service.ts +7 -2
  48. package/server/modules/websocket/services/websocket-server.service.ts +18 -0
  49. package/server/routes/agent.js +4 -2
  50. package/server/routes/commands.js +29 -9
  51. package/dist/assets/index-MOIvpy53.css +0 -32
@@ -17,6 +17,7 @@ import { readProviderSessionActiveModelChange } from '@/shared/utils.js';
17
17
 
18
18
  export const PROVIDER_MODELS_CACHE_TTL_MS = 3 * 24 * 60 * 60 * 1000;
19
19
  const PROVIDER_MODELS_CACHE_VERSION = 1;
20
+ const UNCACHED_PROVIDERS = new Set<LLMProvider>(['claude']);
20
21
 
21
22
  type ProviderModelsServiceDependencies = {
22
23
  resolveProvider?: (provider: LLMProvider) => Pick<IProvider, 'models'>;
@@ -232,10 +233,42 @@ export const createProviderModelsService = (dependencies: ProviderModelsServiceD
232
233
  return request;
233
234
  };
234
235
 
236
+ const loadDirectModels = (
237
+ provider: LLMProvider,
238
+ ): Promise<ProviderModelsResult> => {
239
+ const request = resolveProvider(provider).models.getSupportedModels()
240
+ .then((models) => {
241
+ const currentTime = now();
242
+ return {
243
+ models,
244
+ cache: {
245
+ updatedAt: new Date(currentTime).toISOString(),
246
+ expiresAt: new Date(currentTime).toISOString(),
247
+ source: 'fresh' as const,
248
+ },
249
+ };
250
+ })
251
+ .finally(() => {
252
+ pendingRequests.delete(provider);
253
+ });
254
+
255
+ pendingRequests.set(provider, request);
256
+ return request;
257
+ };
258
+
235
259
  const getProviderModels = async (
236
260
  provider: LLMProvider,
237
261
  options: ProviderModelsOptions = {},
238
262
  ): Promise<ProviderModelsResult> => {
263
+ if (UNCACHED_PROVIDERS.has(provider)) {
264
+ const pendingRequest = pendingRequests.get(provider);
265
+ if (pendingRequest) {
266
+ return pendingRequest;
267
+ }
268
+
269
+ return loadDirectModels(provider);
270
+ }
271
+
239
272
  if (options.bypassCache) {
240
273
  const pendingRequest = pendingRequests.get(provider);
241
274
  if (pendingRequest) {
@@ -130,6 +130,37 @@ test('provider models are cached for the three-day ttl', async () => {
130
130
  }
131
131
  });
132
132
 
133
+ test('claude provider models are always loaded directly from the provider', async () => {
134
+ const tempRoot = await mkdtemp(path.join(os.tmpdir(), 'provider-model-cache-claude-direct-'));
135
+ let loadCount = 0;
136
+
137
+ try {
138
+ const service = createProviderModelsService({
139
+ cachePath: path.join(tempRoot, 'models-cache.json'),
140
+ resolveProvider: (provider) => ({
141
+ models: {
142
+ getSupportedModels: async () => {
143
+ loadCount += 1;
144
+ return createModels(`${provider}-${loadCount}`);
145
+ },
146
+ getCurrentActiveModel: async () => createCurrentActiveModel(`${provider}-active`),
147
+ changeActiveModel: async (input) => createSessionActiveModelChange(provider, input),
148
+ },
149
+ }),
150
+ });
151
+
152
+ const first = await service.getProviderModels('claude');
153
+ const second = await service.getProviderModels('claude');
154
+
155
+ assert.equal(loadCount, 2);
156
+ assert.equal(first.models.DEFAULT, 'claude-1');
157
+ assert.equal(second.models.DEFAULT, 'claude-2');
158
+ assert.equal(second.cache.source, 'fresh');
159
+ } finally {
160
+ await rm(tempRoot, { recursive: true, force: true });
161
+ }
162
+ });
163
+
133
164
  test('provider model cache is persisted across service instances', async () => {
134
165
  const tempRoot = await mkdtemp(path.join(os.tmpdir(), 'provider-model-cache-file-'));
135
166
  const cachePath = path.join(tempRoot, 'models-cache.json');
@@ -18,6 +18,7 @@ type ShellIncomingMessage = {
18
18
  provider?: string;
19
19
  initialCommand?: string;
20
20
  isPlainShell?: boolean;
21
+ forceRestart?: boolean;
21
22
  };
22
23
 
23
24
  type PtySessionEntry = {
@@ -180,6 +181,7 @@ export function handleShellConnection(
180
181
  const hasSession = readBoolean(data.hasSession);
181
182
  const provider = readString(data.provider, 'claude');
182
183
  const initialCommand = readString(data.initialCommand);
184
+ const forceRestart = readBoolean(data.forceRestart);
183
185
  const isPlainShell =
184
186
  readBoolean(data.isPlainShell) ||
185
187
  (!!initialCommand && !hasSession) ||
@@ -200,7 +202,7 @@ export function handleShellConnection(
200
202
  : '';
201
203
  ptySessionKey = `${projectPath}_${sessionId ?? 'default'}${commandSuffix}`;
202
204
 
203
- if (isLoginCommand) {
205
+ if (isLoginCommand || forceRestart) {
204
206
  const oldSession = ptySessionsMap.get(ptySessionKey);
205
207
  if (oldSession) {
206
208
  if (oldSession.timeoutId) {
@@ -211,7 +213,8 @@ export function handleShellConnection(
211
213
  }
212
214
  }
213
215
 
214
- const existingSession = isLoginCommand ? null : ptySessionsMap.get(ptySessionKey);
216
+ const existingSession =
217
+ isLoginCommand || forceRestart ? null : ptySessionsMap.get(ptySessionKey);
215
218
  if (existingSession) {
216
219
  shellProcess = existingSession.pty;
217
220
  if (existingSession.timeoutId) {
@@ -368,6 +371,10 @@ export function handleShellConnection(
368
371
  }
369
372
 
370
373
  const session = ptySessionsMap.get(ptySessionKey);
374
+ if (session && session.pty !== shellProcess) {
375
+ return;
376
+ }
377
+
371
378
  if (session && session.ws && session.ws.readyState === WebSocket.OPEN) {
372
379
  session.ws.send(
373
380
  JSON.stringify({
@@ -451,6 +458,10 @@ export function handleShellConnection(
451
458
 
452
459
  session.ws = null;
453
460
  session.timeoutId = setTimeout(() => {
461
+ if (ptySessionsMap.get(ptySessionKey as string) !== session) {
462
+ return;
463
+ }
464
+
454
465
  session.pty.kill();
455
466
  ptySessionsMap.delete(ptySessionKey as string);
456
467
  }, PTY_SESSION_TIMEOUT);
@@ -20,7 +20,13 @@ export function verifyWebSocketClient(
20
20
  dependencies: WebSocketAuthDependencies
21
21
  ): boolean {
22
22
  const request = info.req as AuthenticatedWebSocketRequest;
23
- console.log('WebSocket connection attempt to:', request.url);
23
+ const upgradeUrl = new URL(request.url ?? '/', 'http://localhost');
24
+ const loggedUrl = new URL(upgradeUrl);
25
+ if (loggedUrl.searchParams.has('token')) {
26
+ loggedUrl.searchParams.set('token', 'REDACTED');
27
+ }
28
+
29
+ console.log('WebSocket connection attempt to:', `${loggedUrl.pathname}${loggedUrl.search}`);
24
30
 
25
31
  // Platform mode: use the first DB user and skip token checks.
26
32
  if (dependencies.isPlatform) {
@@ -36,7 +42,6 @@ export function verifyWebSocketClient(
36
42
  }
37
43
 
38
44
  // OSS mode: read JWT from query string first, then Authorization header.
39
- const upgradeUrl = new URL(request.url ?? '/', 'http://localhost');
40
45
  const token =
41
46
  upgradeUrl.searchParams.get('token') ??
42
47
  request.headers.authorization?.split(' ')[1] ??
@@ -31,6 +31,24 @@ export function createWebSocketServer(
31
31
  });
32
32
 
33
33
  wss.on('connection', (ws, request) => {
34
+ // Keep WebSocket alive across reverse-proxy idle timeouts (Cloudflare ~100s,
35
+ // AWS ALB 60s, nginx 60s, etc.). Without app-level pings these connections
36
+ // are silently torn down even when the UI is active, causing repeated
37
+ // reconnect cycles. ws library heartbeat is opt-in.
38
+ const HEARTBEAT_INTERVAL_MS = 30_000;
39
+ const heartbeat = setInterval(() => {
40
+ if (ws.readyState === ws.OPEN) {
41
+ try {
42
+ ws.ping();
43
+ } catch {
44
+ // socket may have been closed concurrently — interval will be cleared below
45
+ }
46
+ }
47
+ }, HEARTBEAT_INTERVAL_MS);
48
+ const stopHeartbeat = () => clearInterval(heartbeat);
49
+ ws.on('close', stopHeartbeat);
50
+ ws.on('error', stopHeartbeat);
51
+
34
52
  const incomingRequest = request as AuthenticatedWebSocketRequest;
35
53
  const url = incomingRequest.url ?? '/';
36
54
  const pathname = new URL(url, 'http://localhost').pathname;
@@ -592,12 +592,14 @@ class ResponseCollector {
592
592
  }
593
593
  }
594
594
 
595
+ const inputTokens = totalInput + totalCacheRead + totalCacheCreation;
596
+
595
597
  return {
596
- inputTokens: totalInput,
598
+ inputTokens,
597
599
  outputTokens: totalOutput,
598
600
  cacheReadTokens: totalCacheRead,
599
601
  cacheCreationTokens: totalCacheCreation,
600
- totalTokens: totalInput + totalOutput + totalCacheRead + totalCacheCreation
602
+ totalTokens: inputTokens + totalOutput
601
603
  };
602
604
  }
603
605
  }
@@ -268,16 +268,35 @@ Custom commands can be created in:
268
268
  tokenUsage.contextWindow ??
269
269
  0,
270
270
  ) || 0;
271
- const inputTokensRaw =
271
+ const normalizedInputValue =
272
+ tokenUsage.inputTokens ??
273
+ tokenUsage.input ??
274
+ tokenUsage.cumulativeInputTokens ??
275
+ tokenUsage.breakdown?.input ??
276
+ tokenUsage.promptTokens;
277
+ const directInputTokens =
272
278
  Number(
273
- tokenUsage.inputTokens ??
274
- tokenUsage.input ??
279
+ normalizedInputValue ??
275
280
  tokenUsage.input_tokens ??
276
- tokenUsage.cumulativeInputTokens ??
277
- tokenUsage.breakdown?.input ??
278
- tokenUsage.promptTokens ??
281
+ 0
282
+ ) || 0;
283
+ const cacheReadTokens =
284
+ Number(
285
+ tokenUsage.cacheReadTokens ??
286
+ tokenUsage.cache_read_input_tokens ??
287
+ tokenUsage.cacheReadInputTokens ??
288
+ 0,
289
+ ) || 0;
290
+ const cacheCreationTokens =
291
+ Number(
292
+ tokenUsage.cacheCreationTokens ??
293
+ tokenUsage.cache_creation_input_tokens ??
294
+ tokenUsage.cacheCreationInputTokens ??
279
295
  0,
280
296
  ) || 0;
297
+ const inputTokens = normalizedInputValue == null
298
+ ? directInputTokens + cacheReadTokens + cacheCreationTokens
299
+ : directInputTokens;
281
300
  const outputTokens =
282
301
  Number(
283
302
  tokenUsage.outputTokens ??
@@ -288,8 +307,9 @@ Custom commands can be created in:
288
307
  tokenUsage.completionTokens ??
289
308
  0,
290
309
  ) || 0;
291
- const hasTokenBreakdown = inputTokensRaw > 0 || outputTokens > 0;
292
- const used = reportedUsed || inputTokensRaw + outputTokens;
310
+ const computedUsed = inputTokens + outputTokens;
311
+ const hasTokenBreakdown = computedUsed > 0;
312
+ const used = Math.max(reportedUsed, computedUsed);
293
313
 
294
314
  return {
295
315
  type: "builtin",
@@ -302,7 +322,7 @@ Custom commands can be created in:
302
322
  ...(hasTokenBreakdown
303
323
  ? {
304
324
  tokenBreakdown: {
305
- input: inputTokensRaw,
325
+ input: inputTokens,
306
326
  output: outputTokens,
307
327
  },
308
328
  }