claude-code-workflow 6.3.25 → 6.3.26
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/.claude/commands/issue/discover-by-prompt.md +764 -0
- package/ccw/dist/core/routes/help-routes.d.ts.map +1 -1
- package/ccw/dist/core/routes/help-routes.js +43 -7
- package/ccw/dist/core/routes/help-routes.js.map +1 -1
- package/ccw/dist/core/routes/litellm-api-routes.d.ts.map +1 -1
- package/ccw/dist/core/routes/litellm-api-routes.js +31 -5
- package/ccw/dist/core/routes/litellm-api-routes.js.map +1 -1
- package/ccw/dist/core/routes/memory-routes.d.ts.map +1 -1
- package/ccw/dist/core/routes/memory-routes.js +73 -0
- package/ccw/dist/core/routes/memory-routes.js.map +1 -1
- package/ccw/dist/core/routes/status-routes.d.ts.map +1 -1
- package/ccw/dist/core/routes/status-routes.js +36 -4
- package/ccw/dist/core/routes/status-routes.js.map +1 -1
- package/ccw/dist/core/server.d.ts.map +1 -1
- package/ccw/dist/core/server.js +58 -0
- package/ccw/dist/core/server.js.map +1 -1
- package/ccw/dist/core/services/api-key-tester.d.ts.map +1 -1
- package/ccw/dist/core/services/api-key-tester.js +8 -3
- package/ccw/dist/core/services/api-key-tester.js.map +1 -1
- package/ccw/dist/tools/claude-cli-tools.d.ts +7 -0
- package/ccw/dist/tools/claude-cli-tools.d.ts.map +1 -1
- package/ccw/dist/tools/claude-cli-tools.js +11 -1
- package/ccw/dist/tools/claude-cli-tools.js.map +1 -1
- package/ccw/dist/tools/cli-executor-core.d.ts +11 -0
- package/ccw/dist/tools/cli-executor-core.d.ts.map +1 -1
- package/ccw/dist/tools/cli-executor-core.js +89 -2
- package/ccw/dist/tools/cli-executor-core.js.map +1 -1
- package/ccw/dist/tools/codex-lens.d.ts +2 -1
- package/ccw/dist/tools/codex-lens.d.ts.map +1 -1
- package/ccw/dist/tools/codex-lens.js +51 -8
- package/ccw/dist/tools/codex-lens.js.map +1 -1
- package/ccw/dist/tools/index.d.ts.map +1 -1
- package/ccw/dist/tools/index.js +2 -0
- package/ccw/dist/tools/index.js.map +1 -1
- package/ccw/dist/tools/litellm-client.d.ts +6 -0
- package/ccw/dist/tools/litellm-client.d.ts.map +1 -1
- package/ccw/dist/tools/litellm-client.js +22 -1
- package/ccw/dist/tools/litellm-client.js.map +1 -1
- package/ccw/dist/tools/litellm-executor.js +2 -2
- package/ccw/dist/tools/litellm-executor.js.map +1 -1
- package/ccw/dist/tools/memory-update-queue.d.ts +172 -0
- package/ccw/dist/tools/memory-update-queue.d.ts.map +1 -0
- package/ccw/dist/tools/memory-update-queue.js +431 -0
- package/ccw/dist/tools/memory-update-queue.js.map +1 -0
- package/ccw/src/core/routes/help-routes.ts +46 -7
- package/ccw/src/core/routes/litellm-api-routes.ts +35 -4
- package/ccw/src/core/routes/memory-routes.ts +84 -0
- package/ccw/src/core/routes/status-routes.ts +39 -4
- package/ccw/src/core/server.ts +62 -0
- package/ccw/src/core/services/api-key-tester.ts +9 -3
- package/ccw/src/templates/dashboard-css/21-cli-toolmgmt.css +45 -0
- package/ccw/src/templates/dashboard-js/components/cli-status.js +36 -5
- package/ccw/src/templates/dashboard-js/components/hook-manager.js +42 -81
- package/ccw/src/templates/dashboard-js/components/mcp-manager.js +170 -28
- package/ccw/src/templates/dashboard-js/components/notifications.js +14 -4
- package/ccw/src/templates/dashboard-js/i18n.js +26 -0
- package/ccw/src/templates/dashboard-js/views/cli-manager.js +72 -2
- package/ccw/src/templates/dashboard-js/views/codexlens-manager.js +11 -1
- package/ccw/src/tools/claude-cli-tools.ts +17 -1
- package/ccw/src/tools/cli-executor-core.ts +103 -2
- package/ccw/src/tools/codex-lens.ts +63 -8
- package/ccw/src/tools/index.ts +2 -0
- package/ccw/src/tools/litellm-client.ts +25 -3
- package/ccw/src/tools/litellm-executor.ts +2 -2
- package/ccw/src/tools/memory-update-queue.js +499 -0
- package/package.json +1 -1
|
@@ -7,6 +7,29 @@ import { join } from 'path';
|
|
|
7
7
|
import { homedir } from 'os';
|
|
8
8
|
import type { RouteContext } from './types.js';
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* Get the ccw-help index directory path (pure function)
|
|
12
|
+
* Priority: project path (.claude/skills/ccw-help/index) > user path (~/.claude/skills/ccw-help/index)
|
|
13
|
+
* @param projectPath - The project path to check first
|
|
14
|
+
*/
|
|
15
|
+
function getIndexDir(projectPath: string | null): string | null {
|
|
16
|
+
// Try project path first
|
|
17
|
+
if (projectPath) {
|
|
18
|
+
const projectIndexDir = join(projectPath, '.claude', 'skills', 'ccw-help', 'index');
|
|
19
|
+
if (existsSync(projectIndexDir)) {
|
|
20
|
+
return projectIndexDir;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Fall back to user path
|
|
25
|
+
const userIndexDir = join(homedir(), '.claude', 'skills', 'ccw-help', 'index');
|
|
26
|
+
if (existsSync(userIndexDir)) {
|
|
27
|
+
return userIndexDir;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
10
33
|
// ========== In-Memory Cache ==========
|
|
11
34
|
interface CacheEntry {
|
|
12
35
|
data: any;
|
|
@@ -61,14 +84,15 @@ let watchersInitialized = false;
|
|
|
61
84
|
|
|
62
85
|
/**
|
|
63
86
|
* Initialize file watchers for JSON indexes
|
|
87
|
+
* @param projectPath - The project path to resolve index directory
|
|
64
88
|
*/
|
|
65
|
-
function initializeFileWatchers(): void {
|
|
89
|
+
function initializeFileWatchers(projectPath: string | null): void {
|
|
66
90
|
if (watchersInitialized) return;
|
|
67
91
|
|
|
68
|
-
const indexDir =
|
|
92
|
+
const indexDir = getIndexDir(projectPath);
|
|
69
93
|
|
|
70
|
-
if (!
|
|
71
|
-
console.warn(`
|
|
94
|
+
if (!indexDir) {
|
|
95
|
+
console.warn(`ccw-help index directory not found in project or user paths`);
|
|
72
96
|
return;
|
|
73
97
|
}
|
|
74
98
|
|
|
@@ -152,15 +176,20 @@ function groupCommandsByCategory(commands: any[]): any {
|
|
|
152
176
|
* @returns true if route was handled, false otherwise
|
|
153
177
|
*/
|
|
154
178
|
export async function handleHelpRoutes(ctx: RouteContext): Promise<boolean> {
|
|
155
|
-
const { pathname, url, req, res } = ctx;
|
|
179
|
+
const { pathname, url, req, res, initialPath } = ctx;
|
|
156
180
|
|
|
157
181
|
// Initialize file watchers on first request
|
|
158
|
-
initializeFileWatchers();
|
|
182
|
+
initializeFileWatchers(initialPath);
|
|
159
183
|
|
|
160
|
-
const indexDir =
|
|
184
|
+
const indexDir = getIndexDir(initialPath);
|
|
161
185
|
|
|
162
186
|
// API: Get all commands with optional search
|
|
163
187
|
if (pathname === '/api/help/commands') {
|
|
188
|
+
if (!indexDir) {
|
|
189
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
190
|
+
res.end(JSON.stringify({ error: 'ccw-help index directory not found' }));
|
|
191
|
+
return true;
|
|
192
|
+
}
|
|
164
193
|
const searchQuery = url.searchParams.get('q') || '';
|
|
165
194
|
const filePath = join(indexDir, 'all-commands.json');
|
|
166
195
|
|
|
@@ -191,6 +220,11 @@ export async function handleHelpRoutes(ctx: RouteContext): Promise<boolean> {
|
|
|
191
220
|
|
|
192
221
|
// API: Get workflow command relationships
|
|
193
222
|
if (pathname === '/api/help/workflows') {
|
|
223
|
+
if (!indexDir) {
|
|
224
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
225
|
+
res.end(JSON.stringify({ error: 'ccw-help index directory not found' }));
|
|
226
|
+
return true;
|
|
227
|
+
}
|
|
194
228
|
const filePath = join(indexDir, 'command-relationships.json');
|
|
195
229
|
const relationships = getCachedData('command-relationships', filePath);
|
|
196
230
|
|
|
@@ -207,6 +241,11 @@ export async function handleHelpRoutes(ctx: RouteContext): Promise<boolean> {
|
|
|
207
241
|
|
|
208
242
|
// API: Get commands by category
|
|
209
243
|
if (pathname === '/api/help/commands/by-category') {
|
|
244
|
+
if (!indexDir) {
|
|
245
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
246
|
+
res.end(JSON.stringify({ error: 'ccw-help index directory not found' }));
|
|
247
|
+
return true;
|
|
248
|
+
}
|
|
210
249
|
const filePath = join(indexDir, 'by-category.json');
|
|
211
250
|
const byCategory = getCachedData('by-category', filePath);
|
|
212
251
|
|
|
@@ -334,12 +334,43 @@ export async function handleLiteLLMApiRoutes(ctx: RouteContext): Promise<boolean
|
|
|
334
334
|
return true;
|
|
335
335
|
}
|
|
336
336
|
|
|
337
|
-
//
|
|
338
|
-
|
|
339
|
-
|
|
337
|
+
// Get the API key to test (prefer first key from apiKeys array, fall back to default apiKey)
|
|
338
|
+
let apiKeyValue: string | null = null;
|
|
339
|
+
if (provider.apiKeys && provider.apiKeys.length > 0) {
|
|
340
|
+
apiKeyValue = provider.apiKeys[0].key;
|
|
341
|
+
} else if (provider.apiKey) {
|
|
342
|
+
apiKeyValue = provider.apiKey;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (!apiKeyValue) {
|
|
346
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
347
|
+
res.end(JSON.stringify({ success: false, error: 'No API key configured for this provider' }));
|
|
348
|
+
return true;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Resolve environment variables in the API key
|
|
352
|
+
const { resolveEnvVar } = await import('../../config/litellm-api-config-manager.js');
|
|
353
|
+
const resolvedKey = resolveEnvVar(apiKeyValue);
|
|
354
|
+
|
|
355
|
+
if (!resolvedKey) {
|
|
356
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
357
|
+
res.end(JSON.stringify({ success: false, error: 'API key is empty or environment variable not set' }));
|
|
358
|
+
return true;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Determine API base URL
|
|
362
|
+
const apiBase = provider.apiBase || getDefaultApiBase(provider.type);
|
|
363
|
+
|
|
364
|
+
// Test the API key connection
|
|
365
|
+
const testResult = await testApiKeyConnection(provider.type, apiBase, resolvedKey);
|
|
340
366
|
|
|
341
367
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
342
|
-
res.end(JSON.stringify({
|
|
368
|
+
res.end(JSON.stringify({
|
|
369
|
+
success: testResult.valid,
|
|
370
|
+
provider: provider.type,
|
|
371
|
+
latencyMs: testResult.latencyMs,
|
|
372
|
+
error: testResult.error,
|
|
373
|
+
}));
|
|
343
374
|
} catch (err) {
|
|
344
375
|
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
345
376
|
res.end(JSON.stringify({ success: false, error: (err as Error).message }));
|
|
@@ -1256,5 +1256,89 @@ RULES: Be concise. Focus on practical understanding. Include function signatures
|
|
|
1256
1256
|
return true;
|
|
1257
1257
|
}
|
|
1258
1258
|
|
|
1259
|
+
// API: Memory Queue - Add path to queue
|
|
1260
|
+
if (pathname === '/api/memory/queue/add' && req.method === 'POST') {
|
|
1261
|
+
handlePostRequest(req, res, async (body) => {
|
|
1262
|
+
const { path: modulePath, tool = 'gemini', strategy = 'single-layer' } = body;
|
|
1263
|
+
|
|
1264
|
+
if (!modulePath) {
|
|
1265
|
+
return { error: 'path is required', status: 400 };
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
try {
|
|
1269
|
+
const { memoryQueueTool } = await import('../../tools/memory-update-queue.js');
|
|
1270
|
+
const result = await memoryQueueTool.execute({
|
|
1271
|
+
action: 'add',
|
|
1272
|
+
path: modulePath,
|
|
1273
|
+
tool,
|
|
1274
|
+
strategy
|
|
1275
|
+
}) as { queueSize?: number; willFlush?: boolean; flushed?: boolean };
|
|
1276
|
+
|
|
1277
|
+
// Broadcast queue update event
|
|
1278
|
+
broadcastToClients({
|
|
1279
|
+
type: 'MEMORY_QUEUE_UPDATED',
|
|
1280
|
+
payload: {
|
|
1281
|
+
action: 'add',
|
|
1282
|
+
path: modulePath,
|
|
1283
|
+
queueSize: result.queueSize || 0,
|
|
1284
|
+
willFlush: result.willFlush || false,
|
|
1285
|
+
flushed: result.flushed || false,
|
|
1286
|
+
timestamp: new Date().toISOString()
|
|
1287
|
+
}
|
|
1288
|
+
});
|
|
1289
|
+
|
|
1290
|
+
return { success: true, ...result };
|
|
1291
|
+
} catch (error: unknown) {
|
|
1292
|
+
return { error: (error as Error).message, status: 500 };
|
|
1293
|
+
}
|
|
1294
|
+
});
|
|
1295
|
+
return true;
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
// API: Memory Queue - Get queue status
|
|
1299
|
+
if (pathname === '/api/memory/queue/status' && req.method === 'GET') {
|
|
1300
|
+
try {
|
|
1301
|
+
const { memoryQueueTool } = await import('../../tools/memory-update-queue.js');
|
|
1302
|
+
const result = await memoryQueueTool.execute({ action: 'status' }) as Record<string, unknown>;
|
|
1303
|
+
|
|
1304
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1305
|
+
res.end(JSON.stringify({ success: true, ...result }));
|
|
1306
|
+
} catch (error: unknown) {
|
|
1307
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
1308
|
+
res.end(JSON.stringify({ error: (error as Error).message }));
|
|
1309
|
+
}
|
|
1310
|
+
return true;
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
// API: Memory Queue - Flush queue immediately
|
|
1314
|
+
if (pathname === '/api/memory/queue/flush' && req.method === 'POST') {
|
|
1315
|
+
handlePostRequest(req, res, async () => {
|
|
1316
|
+
try {
|
|
1317
|
+
const { memoryQueueTool } = await import('../../tools/memory-update-queue.js');
|
|
1318
|
+
const result = await memoryQueueTool.execute({ action: 'flush' }) as {
|
|
1319
|
+
processed?: number;
|
|
1320
|
+
success?: boolean;
|
|
1321
|
+
errors?: unknown[];
|
|
1322
|
+
};
|
|
1323
|
+
|
|
1324
|
+
// Broadcast queue flushed event
|
|
1325
|
+
broadcastToClients({
|
|
1326
|
+
type: 'MEMORY_QUEUE_FLUSHED',
|
|
1327
|
+
payload: {
|
|
1328
|
+
processed: result.processed || 0,
|
|
1329
|
+
success: result.success || false,
|
|
1330
|
+
errors: result.errors?.length || 0,
|
|
1331
|
+
timestamp: new Date().toISOString()
|
|
1332
|
+
}
|
|
1333
|
+
});
|
|
1334
|
+
|
|
1335
|
+
return { success: true, ...result };
|
|
1336
|
+
} catch (error: unknown) {
|
|
1337
|
+
return { error: (error as Error).message, status: 500 };
|
|
1338
|
+
}
|
|
1339
|
+
});
|
|
1340
|
+
return true;
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1259
1343
|
return false;
|
|
1260
1344
|
}
|
|
@@ -9,6 +9,15 @@ import { getCliToolsStatus } from '../../tools/cli-executor.js';
|
|
|
9
9
|
import { checkVenvStatus, checkSemanticStatus } from '../../tools/codex-lens.js';
|
|
10
10
|
import type { RouteContext } from './types.js';
|
|
11
11
|
|
|
12
|
+
// Performance logging helper
|
|
13
|
+
const PERF_LOG_ENABLED = process.env.CCW_PERF_LOG === '1' || true; // Enable by default for debugging
|
|
14
|
+
function perfLog(label: string, startTime: number, extra?: Record<string, unknown>): void {
|
|
15
|
+
if (!PERF_LOG_ENABLED) return;
|
|
16
|
+
const duration = Date.now() - startTime;
|
|
17
|
+
const extraStr = extra ? ` | ${JSON.stringify(extra)}` : '';
|
|
18
|
+
console.log(`[PERF][Status] ${label}: ${duration}ms${extraStr}`);
|
|
19
|
+
}
|
|
20
|
+
|
|
12
21
|
/**
|
|
13
22
|
* Check CCW installation status
|
|
14
23
|
* Verifies that required workflow files are installed in user's home directory
|
|
@@ -62,16 +71,39 @@ export async function handleStatusRoutes(ctx: RouteContext): Promise<boolean> {
|
|
|
62
71
|
|
|
63
72
|
// API: Aggregated Status (all statuses in one call)
|
|
64
73
|
if (pathname === '/api/status/all') {
|
|
74
|
+
const totalStart = Date.now();
|
|
75
|
+
console.log('[PERF][Status] === /api/status/all START ===');
|
|
76
|
+
|
|
65
77
|
try {
|
|
66
78
|
// Check CCW installation status (sync, fast)
|
|
79
|
+
const ccwStart = Date.now();
|
|
67
80
|
const ccwInstallStatus = checkCcwInstallStatus();
|
|
81
|
+
perfLog('checkCcwInstallStatus', ccwStart);
|
|
82
|
+
|
|
83
|
+
// Execute all status checks in parallel with individual timing
|
|
84
|
+
const cliStart = Date.now();
|
|
85
|
+
const codexStart = Date.now();
|
|
86
|
+
const semanticStart = Date.now();
|
|
68
87
|
|
|
69
|
-
// Execute all status checks in parallel
|
|
70
88
|
const [cliStatus, codexLensStatus, semanticStatus] = await Promise.all([
|
|
71
|
-
getCliToolsStatus()
|
|
72
|
-
|
|
89
|
+
getCliToolsStatus().then(result => {
|
|
90
|
+
perfLog('getCliToolsStatus', cliStart, { toolCount: Object.keys(result).length });
|
|
91
|
+
return result;
|
|
92
|
+
}),
|
|
93
|
+
checkVenvStatus().then(result => {
|
|
94
|
+
perfLog('checkVenvStatus', codexStart, { ready: result.ready });
|
|
95
|
+
return result;
|
|
96
|
+
}),
|
|
73
97
|
// Always check semantic status (will return available: false if CodexLens not ready)
|
|
74
|
-
checkSemanticStatus()
|
|
98
|
+
checkSemanticStatus()
|
|
99
|
+
.then(result => {
|
|
100
|
+
perfLog('checkSemanticStatus', semanticStart, { available: result.available });
|
|
101
|
+
return result;
|
|
102
|
+
})
|
|
103
|
+
.catch(() => {
|
|
104
|
+
perfLog('checkSemanticStatus (error)', semanticStart);
|
|
105
|
+
return { available: false, backend: null };
|
|
106
|
+
})
|
|
75
107
|
]);
|
|
76
108
|
|
|
77
109
|
const response = {
|
|
@@ -82,10 +114,13 @@ export async function handleStatusRoutes(ctx: RouteContext): Promise<boolean> {
|
|
|
82
114
|
timestamp: new Date().toISOString()
|
|
83
115
|
};
|
|
84
116
|
|
|
117
|
+
perfLog('=== /api/status/all TOTAL ===', totalStart);
|
|
118
|
+
|
|
85
119
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
86
120
|
res.end(JSON.stringify(response));
|
|
87
121
|
return true;
|
|
88
122
|
} catch (error) {
|
|
123
|
+
perfLog('=== /api/status/all ERROR ===', totalStart);
|
|
89
124
|
console.error('[Status Routes] Error fetching aggregated status:', error);
|
|
90
125
|
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
91
126
|
res.end(JSON.stringify({ error: (error as Error).message }));
|
package/ccw/src/core/server.ts
CHANGED
|
@@ -42,6 +42,10 @@ import { randomBytes } from 'crypto';
|
|
|
42
42
|
// Import health check service
|
|
43
43
|
import { getHealthCheckService } from './services/health-check-service.js';
|
|
44
44
|
|
|
45
|
+
// Import status check functions for warmup
|
|
46
|
+
import { checkSemanticStatus, checkVenvStatus } from '../tools/codex-lens.js';
|
|
47
|
+
import { getCliToolsStatus } from '../tools/cli-executor.js';
|
|
48
|
+
|
|
45
49
|
import type { ServerConfig } from '../types/config.js';
|
|
46
50
|
import type { PostRequestHandler } from './routes/types.js';
|
|
47
51
|
|
|
@@ -290,6 +294,56 @@ function setCsrfCookie(res: http.ServerResponse, token: string, maxAgeSeconds: n
|
|
|
290
294
|
appendSetCookie(res, attributes.join('; '));
|
|
291
295
|
}
|
|
292
296
|
|
|
297
|
+
/**
|
|
298
|
+
* Warmup function to pre-populate caches on server startup
|
|
299
|
+
* This runs asynchronously and non-blocking after the server starts
|
|
300
|
+
*/
|
|
301
|
+
async function warmupCaches(initialPath: string): Promise<void> {
|
|
302
|
+
console.log('[WARMUP] Starting cache warmup...');
|
|
303
|
+
const startTime = Date.now();
|
|
304
|
+
|
|
305
|
+
// Run all warmup tasks in parallel for faster startup
|
|
306
|
+
const warmupTasks = [
|
|
307
|
+
// Warmup semantic status cache (Python process startup - can be slow first time)
|
|
308
|
+
(async () => {
|
|
309
|
+
const taskStart = Date.now();
|
|
310
|
+
try {
|
|
311
|
+
const semanticStatus = await checkSemanticStatus();
|
|
312
|
+
console.log(`[WARMUP] Semantic status: ${semanticStatus.available ? 'available' : 'not available'} (${Date.now() - taskStart}ms)`);
|
|
313
|
+
} catch (err) {
|
|
314
|
+
console.warn(`[WARMUP] Semantic status check failed: ${(err as Error).message}`);
|
|
315
|
+
}
|
|
316
|
+
})(),
|
|
317
|
+
|
|
318
|
+
// Warmup venv status cache
|
|
319
|
+
(async () => {
|
|
320
|
+
const taskStart = Date.now();
|
|
321
|
+
try {
|
|
322
|
+
const venvStatus = await checkVenvStatus();
|
|
323
|
+
console.log(`[WARMUP] Venv status: ${venvStatus.ready ? 'ready' : 'not ready'} (${Date.now() - taskStart}ms)`);
|
|
324
|
+
} catch (err) {
|
|
325
|
+
console.warn(`[WARMUP] Venv status check failed: ${(err as Error).message}`);
|
|
326
|
+
}
|
|
327
|
+
})(),
|
|
328
|
+
|
|
329
|
+
// Warmup CLI tools status cache
|
|
330
|
+
(async () => {
|
|
331
|
+
const taskStart = Date.now();
|
|
332
|
+
try {
|
|
333
|
+
const cliStatus = await getCliToolsStatus();
|
|
334
|
+
const availableCount = Object.values(cliStatus).filter(s => s.available).length;
|
|
335
|
+
const totalCount = Object.keys(cliStatus).length;
|
|
336
|
+
console.log(`[WARMUP] CLI tools status: ${availableCount}/${totalCount} available (${Date.now() - taskStart}ms)`);
|
|
337
|
+
} catch (err) {
|
|
338
|
+
console.warn(`[WARMUP] CLI tools status check failed: ${(err as Error).message}`);
|
|
339
|
+
}
|
|
340
|
+
})()
|
|
341
|
+
];
|
|
342
|
+
|
|
343
|
+
await Promise.allSettled(warmupTasks);
|
|
344
|
+
console.log(`[WARMUP] Cache warmup complete (${Date.now() - startTime}ms total)`);
|
|
345
|
+
}
|
|
346
|
+
|
|
293
347
|
/**
|
|
294
348
|
* Generate dashboard HTML with embedded CSS and JS
|
|
295
349
|
*/
|
|
@@ -650,6 +704,14 @@ export async function startServer(options: ServerOptions = {}): Promise<http.Ser
|
|
|
650
704
|
console.warn('[Server] Failed to start health check service:', err);
|
|
651
705
|
}
|
|
652
706
|
|
|
707
|
+
// Start cache warmup asynchronously (non-blocking)
|
|
708
|
+
// Uses setImmediate to not delay server startup response
|
|
709
|
+
setImmediate(() => {
|
|
710
|
+
warmupCaches(initialPath).catch((err) => {
|
|
711
|
+
console.warn('[WARMUP] Cache warmup failed:', err);
|
|
712
|
+
});
|
|
713
|
+
});
|
|
714
|
+
|
|
653
715
|
resolve(server);
|
|
654
716
|
});
|
|
655
717
|
server.on('error', reject);
|
|
@@ -72,6 +72,10 @@ export async function testApiKeyConnection(
|
|
|
72
72
|
return { valid: false, error: urlValidation.error };
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
// Normalize apiBase: remove trailing slashes to prevent URL construction issues
|
|
76
|
+
// e.g., "https://api.openai.com/v1/" -> "https://api.openai.com/v1"
|
|
77
|
+
const normalizedApiBase = apiBase.replace(/\/+$/, '');
|
|
78
|
+
|
|
75
79
|
const controller = new AbortController();
|
|
76
80
|
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
77
81
|
const startTime = Date.now();
|
|
@@ -80,7 +84,7 @@ export async function testApiKeyConnection(
|
|
|
80
84
|
if (providerType === 'anthropic') {
|
|
81
85
|
// Anthropic format: Use /v1/models endpoint (no cost, no model dependency)
|
|
82
86
|
// This validates the API key without making a billable request
|
|
83
|
-
const response = await fetch(`${
|
|
87
|
+
const response = await fetch(`${normalizedApiBase}/models`, {
|
|
84
88
|
method: 'GET',
|
|
85
89
|
headers: {
|
|
86
90
|
'x-api-key': apiKey,
|
|
@@ -114,8 +118,10 @@ export async function testApiKeyConnection(
|
|
|
114
118
|
|
|
115
119
|
return { valid: false, error: errorMessage };
|
|
116
120
|
} else {
|
|
117
|
-
// OpenAI-compatible format: GET /
|
|
118
|
-
|
|
121
|
+
// OpenAI-compatible format: GET /v{N}/models
|
|
122
|
+
// Detect if URL already ends with a version pattern like /v1, /v2, /v4, etc.
|
|
123
|
+
const hasVersionSuffix = /\/v\d+$/.test(normalizedApiBase);
|
|
124
|
+
const modelsUrl = hasVersionSuffix ? `${normalizedApiBase}/models` : `${normalizedApiBase}/v1/models`;
|
|
119
125
|
const response = await fetch(modelsUrl, {
|
|
120
126
|
method: 'GET',
|
|
121
127
|
headers: {
|
|
@@ -304,6 +304,51 @@
|
|
|
304
304
|
margin-top: 0;
|
|
305
305
|
}
|
|
306
306
|
|
|
307
|
+
/* Environment File Input Group */
|
|
308
|
+
.env-file-input-group {
|
|
309
|
+
display: flex;
|
|
310
|
+
flex-direction: column;
|
|
311
|
+
gap: 0.5rem;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
.env-file-input-row {
|
|
315
|
+
display: flex;
|
|
316
|
+
gap: 0.5rem;
|
|
317
|
+
align-items: center;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
.env-file-input-row .tool-config-input {
|
|
321
|
+
flex: 1;
|
|
322
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
|
323
|
+
font-size: 0.8125rem;
|
|
324
|
+
margin-top: 0;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
.env-file-input-row .btn-sm {
|
|
328
|
+
flex-shrink: 0;
|
|
329
|
+
display: inline-flex;
|
|
330
|
+
align-items: center;
|
|
331
|
+
gap: 0.25rem;
|
|
332
|
+
padding: 0.5rem 0.75rem;
|
|
333
|
+
font-size: 0.8125rem;
|
|
334
|
+
white-space: nowrap;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
.env-file-hint {
|
|
338
|
+
display: flex;
|
|
339
|
+
align-items: center;
|
|
340
|
+
gap: 0.375rem;
|
|
341
|
+
font-size: 0.75rem;
|
|
342
|
+
color: hsl(var(--muted-foreground));
|
|
343
|
+
margin: 0;
|
|
344
|
+
padding: 0;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
.env-file-hint i {
|
|
348
|
+
flex-shrink: 0;
|
|
349
|
+
opacity: 0.7;
|
|
350
|
+
}
|
|
351
|
+
|
|
307
352
|
.btn-ghost.text-destructive:hover {
|
|
308
353
|
background: hsl(var(--destructive) / 0.1);
|
|
309
354
|
}
|
|
@@ -33,11 +33,14 @@ function initCliStatus() {
|
|
|
33
33
|
* Load all statuses using aggregated endpoint (single API call)
|
|
34
34
|
*/
|
|
35
35
|
async function loadAllStatuses() {
|
|
36
|
+
const totalStart = performance.now();
|
|
37
|
+
console.log('[PERF][Frontend] loadAllStatuses START');
|
|
38
|
+
|
|
36
39
|
// 1. 尝试从缓存获取(预加载的数据)
|
|
37
40
|
if (window.cacheManager) {
|
|
38
41
|
const cached = window.cacheManager.get('all-status');
|
|
39
42
|
if (cached) {
|
|
40
|
-
console.log(
|
|
43
|
+
console.log(`[PERF][Frontend] Cache hit: ${(performance.now() - totalStart).toFixed(1)}ms`);
|
|
41
44
|
// 应用缓存数据
|
|
42
45
|
cliToolStatus = cached.cli || {};
|
|
43
46
|
codexLensStatus = cached.codexLens || { ready: false };
|
|
@@ -45,25 +48,32 @@ async function loadAllStatuses() {
|
|
|
45
48
|
ccwInstallStatus = cached.ccwInstall || { installed: true, workflowsInstalled: true, missingFiles: [], installPath: '' };
|
|
46
49
|
|
|
47
50
|
// Load CLI tools config, API endpoints, and CLI Settings(这些有自己的缓存)
|
|
51
|
+
const configStart = performance.now();
|
|
48
52
|
await Promise.all([
|
|
49
53
|
loadCliToolsConfig(),
|
|
50
54
|
loadApiEndpoints(),
|
|
51
55
|
loadCliSettingsEndpoints()
|
|
52
56
|
]);
|
|
57
|
+
console.log(`[PERF][Frontend] Config/Endpoints load: ${(performance.now() - configStart).toFixed(1)}ms`);
|
|
53
58
|
|
|
54
59
|
// Update badges
|
|
55
60
|
updateCliBadge();
|
|
56
61
|
updateCodexLensBadge();
|
|
57
62
|
updateCcwInstallBadge();
|
|
63
|
+
|
|
64
|
+
console.log(`[PERF][Frontend] loadAllStatuses TOTAL (cached): ${(performance.now() - totalStart).toFixed(1)}ms`);
|
|
58
65
|
return cached;
|
|
59
66
|
}
|
|
60
67
|
}
|
|
61
68
|
|
|
62
69
|
// 2. 缓存未命中,从服务器获取
|
|
63
70
|
try {
|
|
71
|
+
const fetchStart = performance.now();
|
|
72
|
+
console.log('[PERF][Frontend] Fetching /api/status/all...');
|
|
64
73
|
const response = await fetch('/api/status/all');
|
|
65
74
|
if (!response.ok) throw new Error('Failed to load status');
|
|
66
75
|
const data = await response.json();
|
|
76
|
+
console.log(`[PERF][Frontend] /api/status/all fetch: ${(performance.now() - fetchStart).toFixed(1)}ms`);
|
|
67
77
|
|
|
68
78
|
// 存入缓存
|
|
69
79
|
if (window.cacheManager) {
|
|
@@ -77,10 +87,11 @@ async function loadAllStatuses() {
|
|
|
77
87
|
ccwInstallStatus = data.ccwInstall || { installed: true, workflowsInstalled: true, missingFiles: [], installPath: '' };
|
|
78
88
|
|
|
79
89
|
// Load CLI tools config, API endpoints, and CLI Settings
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
90
|
+
const configStart = performance.now();
|
|
91
|
+
const [configResult, endpointsResult, settingsResult] = await Promise.all([
|
|
92
|
+
loadCliToolsConfig().then(r => { console.log(`[PERF][Frontend] loadCliToolsConfig: ${(performance.now() - configStart).toFixed(1)}ms`); return r; }),
|
|
93
|
+
loadApiEndpoints().then(r => { console.log(`[PERF][Frontend] loadApiEndpoints: ${(performance.now() - configStart).toFixed(1)}ms`); return r; }),
|
|
94
|
+
loadCliSettingsEndpoints().then(r => { console.log(`[PERF][Frontend] loadCliSettingsEndpoints: ${(performance.now() - configStart).toFixed(1)}ms`); return r; })
|
|
84
95
|
]);
|
|
85
96
|
|
|
86
97
|
// Update badges
|
|
@@ -88,9 +99,11 @@ async function loadAllStatuses() {
|
|
|
88
99
|
updateCodexLensBadge();
|
|
89
100
|
updateCcwInstallBadge();
|
|
90
101
|
|
|
102
|
+
console.log(`[PERF][Frontend] loadAllStatuses TOTAL: ${(performance.now() - totalStart).toFixed(1)}ms`);
|
|
91
103
|
return data;
|
|
92
104
|
} catch (err) {
|
|
93
105
|
console.error('Failed to load aggregated status:', err);
|
|
106
|
+
console.log(`[PERF][Frontend] loadAllStatuses ERROR after: ${(performance.now() - totalStart).toFixed(1)}ms`);
|
|
94
107
|
// Fallback to individual calls if aggregated endpoint fails
|
|
95
108
|
return await loadAllStatusesFallback();
|
|
96
109
|
}
|
|
@@ -1034,6 +1047,15 @@ async function startCodexLensInstall() {
|
|
|
1034
1047
|
progressBar.style.width = '100%';
|
|
1035
1048
|
statusText.textContent = 'Installation complete!';
|
|
1036
1049
|
|
|
1050
|
+
// 清理缓存以确保刷新后获取最新状态
|
|
1051
|
+
if (window.cacheManager) {
|
|
1052
|
+
window.cacheManager.invalidate('all-status');
|
|
1053
|
+
window.cacheManager.invalidate('dashboard-init');
|
|
1054
|
+
}
|
|
1055
|
+
if (typeof window.invalidateCodexLensCache === 'function') {
|
|
1056
|
+
window.invalidateCodexLensCache();
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1037
1059
|
setTimeout(() => {
|
|
1038
1060
|
closeCodexLensInstallWizard();
|
|
1039
1061
|
showRefreshToast('CodexLens installed successfully!', 'success');
|
|
@@ -1184,6 +1206,15 @@ async function startCodexLensUninstall() {
|
|
|
1184
1206
|
progressBar.style.width = '100%';
|
|
1185
1207
|
statusText.textContent = 'Uninstallation complete!';
|
|
1186
1208
|
|
|
1209
|
+
// 清理缓存以确保刷新后获取最新状态
|
|
1210
|
+
if (window.cacheManager) {
|
|
1211
|
+
window.cacheManager.invalidate('all-status');
|
|
1212
|
+
window.cacheManager.invalidate('dashboard-init');
|
|
1213
|
+
}
|
|
1214
|
+
if (typeof window.invalidateCodexLensCache === 'function') {
|
|
1215
|
+
window.invalidateCodexLensCache();
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1187
1218
|
setTimeout(() => {
|
|
1188
1219
|
closeCodexLensUninstallWizard();
|
|
1189
1220
|
showRefreshToast('CodexLens uninstalled successfully!', 'success');
|