tlc-claude-code 1.4.4 → 1.4.6
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/dashboard/dist/App.js +28 -2
- package/dashboard/dist/api/health-diagnostics.d.ts +26 -0
- package/dashboard/dist/api/health-diagnostics.js +85 -0
- package/dashboard/dist/api/health-diagnostics.test.d.ts +1 -0
- package/dashboard/dist/api/health-diagnostics.test.js +126 -0
- package/dashboard/dist/api/index.d.ts +5 -0
- package/dashboard/dist/api/index.js +5 -0
- package/dashboard/dist/api/notes-api.d.ts +18 -0
- package/dashboard/dist/api/notes-api.js +68 -0
- package/dashboard/dist/api/notes-api.test.d.ts +1 -0
- package/dashboard/dist/api/notes-api.test.js +113 -0
- package/dashboard/dist/api/safeFetch.d.ts +50 -0
- package/dashboard/dist/api/safeFetch.js +135 -0
- package/dashboard/dist/api/safeFetch.test.d.ts +1 -0
- package/dashboard/dist/api/safeFetch.test.js +215 -0
- package/dashboard/dist/api/tasks-api.d.ts +32 -0
- package/dashboard/dist/api/tasks-api.js +98 -0
- package/dashboard/dist/api/tasks-api.test.d.ts +1 -0
- package/dashboard/dist/api/tasks-api.test.js +383 -0
- package/dashboard/dist/components/BugsPane.d.ts +20 -0
- package/dashboard/dist/components/BugsPane.js +210 -0
- package/dashboard/dist/components/BugsPane.test.d.ts +1 -0
- package/dashboard/dist/components/BugsPane.test.js +256 -0
- package/dashboard/dist/components/HealthPane.d.ts +3 -1
- package/dashboard/dist/components/HealthPane.js +44 -6
- package/dashboard/dist/components/HealthPane.test.js +105 -2
- package/dashboard/dist/components/RouterPane.d.ts +4 -3
- package/dashboard/dist/components/RouterPane.js +60 -57
- package/dashboard/dist/components/RouterPane.test.js +150 -96
- package/dashboard/dist/components/UpdateBanner.d.ts +26 -0
- package/dashboard/dist/components/UpdateBanner.js +30 -0
- package/dashboard/dist/components/UpdateBanner.test.d.ts +1 -0
- package/dashboard/dist/components/UpdateBanner.test.js +96 -0
- package/dashboard/dist/components/ui/EmptyState.d.ts +14 -0
- package/dashboard/dist/components/ui/EmptyState.js +58 -0
- package/dashboard/dist/components/ui/EmptyState.test.d.ts +1 -0
- package/dashboard/dist/components/ui/EmptyState.test.js +97 -0
- package/dashboard/dist/components/ui/ErrorState.d.ts +17 -0
- package/dashboard/dist/components/ui/ErrorState.js +80 -0
- package/dashboard/dist/components/ui/ErrorState.test.d.ts +1 -0
- package/dashboard/dist/components/ui/ErrorState.test.js +166 -0
- package/dashboard/package.json +3 -0
- package/package.json +4 -1
- package/server/dashboard/index.html +284 -13
- package/server/dashboard/login.html +262 -0
- package/server/index.js +304 -0
- package/server/lib/api-provider.js +104 -186
- package/server/lib/api-provider.test.js +238 -336
- package/server/lib/cli-detector.js +90 -166
- package/server/lib/cli-detector.test.js +114 -269
- package/server/lib/cli-provider.js +142 -212
- package/server/lib/cli-provider.test.js +196 -349
- package/server/lib/debug.test.js +3 -3
- package/server/lib/devserver-router-api.js +54 -249
- package/server/lib/devserver-router-api.test.js +126 -426
- package/server/lib/introspect.js +309 -0
- package/server/lib/introspect.test.js +286 -0
- package/server/lib/model-router.js +107 -245
- package/server/lib/model-router.test.js +122 -313
- package/server/lib/output-schemas.js +146 -269
- package/server/lib/output-schemas.test.js +106 -307
- package/server/lib/plan-parser.js +59 -16
- package/server/lib/provider-interface.js +99 -153
- package/server/lib/provider-interface.test.js +228 -394
- package/server/lib/provider-queue.js +164 -158
- package/server/lib/provider-queue.test.js +186 -315
- package/server/lib/router-config.js +99 -221
- package/server/lib/router-config.test.js +83 -237
- package/server/lib/router-setup-command.js +94 -419
- package/server/lib/router-setup-command.test.js +96 -375
- package/server/lib/router-status-api.js +93 -0
- package/server/lib/router-status-api.test.js +270 -0
package/server/index.js
CHANGED
|
@@ -12,6 +12,14 @@ const chokidar = require('chokidar');
|
|
|
12
12
|
const { detectProject } = require('./lib/project-detector');
|
|
13
13
|
const { parsePlan, parseBugs } = require('./lib/plan-parser');
|
|
14
14
|
const { autoProvision, stopDatabase } = require('./lib/auto-database');
|
|
15
|
+
const {
|
|
16
|
+
createUserStore,
|
|
17
|
+
createAuthMiddleware,
|
|
18
|
+
generateJWT,
|
|
19
|
+
verifyJWT,
|
|
20
|
+
hashPassword,
|
|
21
|
+
verifyPassword,
|
|
22
|
+
} = require('./lib/auth-system');
|
|
15
23
|
|
|
16
24
|
// Handle PGlite WASM crashes gracefully
|
|
17
25
|
process.on('uncaughtException', (err) => {
|
|
@@ -48,6 +56,150 @@ const wss = new WebSocketServer({ server });
|
|
|
48
56
|
|
|
49
57
|
// Middleware
|
|
50
58
|
app.use(express.json());
|
|
59
|
+
const cookieParser = require('cookie-parser');
|
|
60
|
+
app.use(cookieParser());
|
|
61
|
+
|
|
62
|
+
// ============================================
|
|
63
|
+
// Authentication Setup
|
|
64
|
+
// ============================================
|
|
65
|
+
const userStore = createUserStore();
|
|
66
|
+
const JWT_SECRET = process.env.TLC_JWT_SECRET || 'tlc-dashboard-secret-change-in-production';
|
|
67
|
+
const AUTH_ENABLED = process.env.TLC_AUTH !== 'false';
|
|
68
|
+
|
|
69
|
+
// Initialize admin user from config or environment
|
|
70
|
+
async function initializeAuth() {
|
|
71
|
+
const tlcConfigPath = path.join(PROJECT_DIR, '.tlc.json');
|
|
72
|
+
let adminEmail = process.env.TLC_ADMIN_EMAIL || 'admin@localhost';
|
|
73
|
+
let adminPassword = process.env.TLC_ADMIN_PASSWORD;
|
|
74
|
+
|
|
75
|
+
// Try to read from .tlc.json
|
|
76
|
+
if (fs.existsSync(tlcConfigPath)) {
|
|
77
|
+
try {
|
|
78
|
+
const config = JSON.parse(fs.readFileSync(tlcConfigPath, 'utf-8'));
|
|
79
|
+
if (config.auth?.adminEmail) adminEmail = config.auth.adminEmail;
|
|
80
|
+
if (config.auth?.adminPassword) adminPassword = config.auth.adminPassword;
|
|
81
|
+
} catch (e) {
|
|
82
|
+
// Ignore parse errors
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Create admin user if password is set
|
|
87
|
+
if (adminPassword) {
|
|
88
|
+
try {
|
|
89
|
+
await userStore.createUser({
|
|
90
|
+
email: adminEmail,
|
|
91
|
+
password: adminPassword,
|
|
92
|
+
name: 'Admin',
|
|
93
|
+
role: 'admin',
|
|
94
|
+
});
|
|
95
|
+
console.log(`[TLC] Admin user initialized: ${adminEmail}`);
|
|
96
|
+
} catch (e) {
|
|
97
|
+
// User may already exist
|
|
98
|
+
if (!e.message.includes('already registered')) {
|
|
99
|
+
console.error('[TLC] Failed to create admin user:', e.message);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Auth middleware for protected routes
|
|
106
|
+
const authMiddleware = createAuthMiddleware({
|
|
107
|
+
userStore,
|
|
108
|
+
jwtSecret: JWT_SECRET,
|
|
109
|
+
requireAuth: true,
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// Public paths that don't require auth
|
|
113
|
+
const publicPaths = ['/api/auth/login', '/api/auth/status', '/login.html', '/login'];
|
|
114
|
+
|
|
115
|
+
// Apply auth to API routes (except public paths)
|
|
116
|
+
app.use((req, res, next) => {
|
|
117
|
+
// Skip auth if disabled
|
|
118
|
+
if (!AUTH_ENABLED) return next();
|
|
119
|
+
|
|
120
|
+
// Allow public paths
|
|
121
|
+
if (publicPaths.some(p => req.path === p || req.path.startsWith(p))) {
|
|
122
|
+
return next();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Allow static assets
|
|
126
|
+
if (req.path.match(/\.(js|css|png|jpg|ico|svg|woff|woff2)$/)) {
|
|
127
|
+
return next();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Check for auth cookie or header
|
|
131
|
+
const token = req.cookies?.tlc_token || req.headers.authorization?.replace('Bearer ', '');
|
|
132
|
+
|
|
133
|
+
if (!token) {
|
|
134
|
+
// Redirect browser requests to login, return 401 for API
|
|
135
|
+
if (req.path.startsWith('/api/')) {
|
|
136
|
+
return res.status(401).json({ error: 'Authentication required' });
|
|
137
|
+
}
|
|
138
|
+
return res.redirect('/login.html');
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Verify token
|
|
142
|
+
const payload = verifyJWT(token, JWT_SECRET);
|
|
143
|
+
if (!payload) {
|
|
144
|
+
if (req.path.startsWith('/api/')) {
|
|
145
|
+
return res.status(401).json({ error: 'Invalid or expired token' });
|
|
146
|
+
}
|
|
147
|
+
return res.redirect('/login.html');
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Attach user to request
|
|
151
|
+
req.user = payload;
|
|
152
|
+
next();
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// Auth routes
|
|
156
|
+
app.get('/api/auth/status', (req, res) => {
|
|
157
|
+
res.json({ authEnabled: AUTH_ENABLED });
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
app.post('/api/auth/login', async (req, res) => {
|
|
161
|
+
const { email, password } = req.body;
|
|
162
|
+
|
|
163
|
+
if (!email || !password) {
|
|
164
|
+
return res.status(400).json({ error: 'Email and password required' });
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const user = await userStore.authenticate(email, password);
|
|
168
|
+
if (!user) {
|
|
169
|
+
return res.status(401).json({ error: 'Invalid credentials' });
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Generate JWT
|
|
173
|
+
const token = generateJWT(
|
|
174
|
+
{ sub: user.id, email: user.email, role: user.role, name: user.name },
|
|
175
|
+
JWT_SECRET,
|
|
176
|
+
{ expiresIn: 86400 * 7 } // 7 days
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
// Set cookie
|
|
180
|
+
res.cookie('tlc_token', token, {
|
|
181
|
+
httpOnly: true,
|
|
182
|
+
secure: process.env.NODE_ENV === 'production',
|
|
183
|
+
sameSite: 'lax',
|
|
184
|
+
maxAge: 86400 * 7 * 1000, // 7 days
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
res.json({ success: true, user: { email: user.email, name: user.name, role: user.role } });
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
app.post('/api/auth/logout', (req, res) => {
|
|
191
|
+
res.clearCookie('tlc_token');
|
|
192
|
+
res.json({ success: true });
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
app.get('/api/auth/me', (req, res) => {
|
|
196
|
+
if (!req.user) {
|
|
197
|
+
return res.status(401).json({ error: 'Not authenticated' });
|
|
198
|
+
}
|
|
199
|
+
res.json({ user: req.user });
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
// Serve static files (after auth middleware)
|
|
51
203
|
app.use(express.static(path.join(__dirname, 'dashboard')));
|
|
52
204
|
|
|
53
205
|
// Broadcast to all WebSocket clients
|
|
@@ -158,6 +310,7 @@ async function startApp() {
|
|
|
158
310
|
|
|
159
311
|
// Run tests
|
|
160
312
|
function runTests() {
|
|
313
|
+
broadcast('test-start', { timestamp: Date.now() });
|
|
161
314
|
addLog('test', '--- Running tests ---', 'info');
|
|
162
315
|
|
|
163
316
|
// Try to detect test command
|
|
@@ -202,6 +355,86 @@ function runTests() {
|
|
|
202
355
|
}
|
|
203
356
|
|
|
204
357
|
// API Routes
|
|
358
|
+
|
|
359
|
+
// Project info endpoint - returns real project data
|
|
360
|
+
app.get('/api/project', (req, res) => {
|
|
361
|
+
try {
|
|
362
|
+
const { execSync } = require('child_process');
|
|
363
|
+
|
|
364
|
+
// Get project name and description from package.json or .tlc.json
|
|
365
|
+
let projectName = 'Unknown Project';
|
|
366
|
+
let projectDesc = '';
|
|
367
|
+
let version = '';
|
|
368
|
+
|
|
369
|
+
const pkgPath = path.join(PROJECT_DIR, 'package.json');
|
|
370
|
+
if (fs.existsSync(pkgPath)) {
|
|
371
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
372
|
+
projectName = pkg.name || projectName;
|
|
373
|
+
projectDesc = pkg.description || '';
|
|
374
|
+
version = pkg.version || '';
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const tlcPath = path.join(PROJECT_DIR, '.tlc.json');
|
|
378
|
+
if (fs.existsSync(tlcPath)) {
|
|
379
|
+
const tlc = JSON.parse(fs.readFileSync(tlcPath, 'utf-8'));
|
|
380
|
+
if (tlc.project) projectName = tlc.project;
|
|
381
|
+
if (tlc.description) projectDesc = tlc.description;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Get git info
|
|
385
|
+
let branch = 'unknown';
|
|
386
|
+
let lastCommit = null;
|
|
387
|
+
try {
|
|
388
|
+
branch = execSync('git rev-parse --abbrev-ref HEAD', { cwd: PROJECT_DIR, encoding: 'utf-8' }).trim();
|
|
389
|
+
const commitInfo = execSync('git log -1 --pretty=format:"%h|%s|%ar"', { cwd: PROJECT_DIR, encoding: 'utf-8' }).trim();
|
|
390
|
+
const [hash, message, time] = commitInfo.split('|');
|
|
391
|
+
lastCommit = { hash, message, time };
|
|
392
|
+
} catch (e) {
|
|
393
|
+
// Not a git repo
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Get phase info
|
|
397
|
+
const plan = parsePlan(PROJECT_DIR);
|
|
398
|
+
|
|
399
|
+
// Count phases from roadmap
|
|
400
|
+
let totalPhases = 0;
|
|
401
|
+
let completedPhases = 0;
|
|
402
|
+
const roadmapPath = path.join(PROJECT_DIR, '.planning', 'ROADMAP.md');
|
|
403
|
+
if (fs.existsSync(roadmapPath)) {
|
|
404
|
+
const content = fs.readFileSync(roadmapPath, 'utf-8');
|
|
405
|
+
const phases = content.match(/##\s+Phase\s+\d+/g) || [];
|
|
406
|
+
totalPhases = phases.length;
|
|
407
|
+
const completed = content.match(/##\s+Phase\s+\d+[^[]*\[x\]/gi) || [];
|
|
408
|
+
completedPhases = completed.length;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// Calculate progress
|
|
412
|
+
const tasksDone = plan.tasks?.filter(t => t.status === 'done' || t.status === 'complete').length || 0;
|
|
413
|
+
const tasksTotal = plan.tasks?.length || 0;
|
|
414
|
+
const progress = tasksTotal > 0 ? Math.round((tasksDone / tasksTotal) * 100) : 0;
|
|
415
|
+
|
|
416
|
+
res.json({
|
|
417
|
+
name: projectName,
|
|
418
|
+
description: projectDesc,
|
|
419
|
+
version,
|
|
420
|
+
branch,
|
|
421
|
+
lastCommit,
|
|
422
|
+
phase: plan.currentPhase,
|
|
423
|
+
phaseName: plan.currentPhaseName,
|
|
424
|
+
totalPhases,
|
|
425
|
+
completedPhases,
|
|
426
|
+
tasks: {
|
|
427
|
+
total: tasksTotal,
|
|
428
|
+
done: tasksDone,
|
|
429
|
+
progress
|
|
430
|
+
},
|
|
431
|
+
projectDir: PROJECT_DIR
|
|
432
|
+
});
|
|
433
|
+
} catch (err) {
|
|
434
|
+
res.status(500).json({ error: err.message });
|
|
435
|
+
}
|
|
436
|
+
});
|
|
437
|
+
|
|
205
438
|
app.get('/api/status', (req, res) => {
|
|
206
439
|
const bugs = parseBugs(PROJECT_DIR);
|
|
207
440
|
const plan = parsePlan(PROJECT_DIR);
|
|
@@ -372,6 +605,32 @@ app.post('/api/playwright', (req, res) => {
|
|
|
372
605
|
res.json({ success: true });
|
|
373
606
|
});
|
|
374
607
|
|
|
608
|
+
// POST /api/tasks - Create a new task
|
|
609
|
+
app.post('/api/tasks', (req, res) => {
|
|
610
|
+
const { title, phase, owner } = req.body;
|
|
611
|
+
|
|
612
|
+
if (!title) {
|
|
613
|
+
return res.status(400).json({ error: 'Title required' });
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
const plan = parsePlan(PROJECT_DIR);
|
|
617
|
+
const taskId = `task-${Date.now()}`;
|
|
618
|
+
const task = {
|
|
619
|
+
id: taskId,
|
|
620
|
+
title,
|
|
621
|
+
status: 'pending',
|
|
622
|
+
phase: phase || plan.currentPhase || 1,
|
|
623
|
+
owner: owner || null,
|
|
624
|
+
createdAt: new Date().toISOString()
|
|
625
|
+
};
|
|
626
|
+
|
|
627
|
+
// Broadcast to all connected clients
|
|
628
|
+
broadcast('task-created', task);
|
|
629
|
+
addLog('app', `Task created: ${title}`, 'info');
|
|
630
|
+
|
|
631
|
+
res.status(201).json(task);
|
|
632
|
+
});
|
|
633
|
+
|
|
375
634
|
app.post('/api/bug', (req, res) => {
|
|
376
635
|
const { description, url, screenshot, severity, images } = req.body;
|
|
377
636
|
|
|
@@ -570,6 +829,11 @@ app.post('/api/agents', (req, res) => {
|
|
|
570
829
|
});
|
|
571
830
|
|
|
572
831
|
const agent = registry.getAgent(agentId);
|
|
832
|
+
|
|
833
|
+
// Broadcast agent creation
|
|
834
|
+
broadcast('agent-created', formatAgent(agent));
|
|
835
|
+
addLog('app', `Agent registered: ${name}`, 'info');
|
|
836
|
+
|
|
573
837
|
res.status(201).json({ success: true, agent: formatAgent(agent) });
|
|
574
838
|
} catch (err) {
|
|
575
839
|
res.status(500).json({ success: false, error: err.message });
|
|
@@ -604,6 +868,9 @@ app.patch('/api/agents/:id', (req, res) => {
|
|
|
604
868
|
});
|
|
605
869
|
}
|
|
606
870
|
|
|
871
|
+
// Broadcast agent update
|
|
872
|
+
broadcast('agent-updated', formatAgent(agent));
|
|
873
|
+
|
|
607
874
|
res.json({ success: true, agent: formatAgent(agent) });
|
|
608
875
|
} catch (err) {
|
|
609
876
|
res.status(500).json({ success: false, error: err.message });
|
|
@@ -758,6 +1025,34 @@ async function shutdown() {
|
|
|
758
1025
|
process.on('SIGINT', shutdown);
|
|
759
1026
|
process.on('SIGTERM', shutdown);
|
|
760
1027
|
|
|
1028
|
+
// Get health data for broadcasting
|
|
1029
|
+
function getHealthData() {
|
|
1030
|
+
const os = require('os');
|
|
1031
|
+
const memUsed = process.memoryUsage().heapUsed;
|
|
1032
|
+
const memTotal = os.totalmem();
|
|
1033
|
+
const loadAvg = os.loadavg()[0];
|
|
1034
|
+
const cpuCount = os.cpus().length;
|
|
1035
|
+
const cpuPercent = Math.round((loadAvg / cpuCount) * 100);
|
|
1036
|
+
|
|
1037
|
+
return {
|
|
1038
|
+
status: appProcess ? 'healthy' : 'degraded',
|
|
1039
|
+
memory: memUsed,
|
|
1040
|
+
cpu: Math.min(cpuPercent, 100),
|
|
1041
|
+
uptime: process.uptime(),
|
|
1042
|
+
appRunning: appProcess !== null,
|
|
1043
|
+
appPort: appPort
|
|
1044
|
+
};
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
// Periodic health broadcast
|
|
1048
|
+
let healthInterval = null;
|
|
1049
|
+
function startHealthBroadcast() {
|
|
1050
|
+
if (healthInterval) clearInterval(healthInterval);
|
|
1051
|
+
healthInterval = setInterval(() => {
|
|
1052
|
+
broadcast('health-update', getHealthData());
|
|
1053
|
+
}, 30000); // Every 30 seconds
|
|
1054
|
+
}
|
|
1055
|
+
|
|
761
1056
|
// Start server
|
|
762
1057
|
async function main() {
|
|
763
1058
|
console.log(`
|
|
@@ -771,13 +1066,22 @@ async function main() {
|
|
|
771
1066
|
TLC Dev Server
|
|
772
1067
|
`);
|
|
773
1068
|
|
|
1069
|
+
// Initialize authentication
|
|
1070
|
+
await initializeAuth();
|
|
1071
|
+
|
|
774
1072
|
server.listen(TLC_PORT, () => {
|
|
775
1073
|
console.log(` Dashboard: http://localhost:${TLC_PORT}`);
|
|
776
1074
|
console.log(` Share: http://${getLocalIP()}:${TLC_PORT}`);
|
|
1075
|
+
if (AUTH_ENABLED) {
|
|
1076
|
+
console.log(` Auth: ENABLED (set TLC_AUTH=false to disable)`);
|
|
1077
|
+
} else {
|
|
1078
|
+
console.log(` Auth: DISABLED`);
|
|
1079
|
+
}
|
|
777
1080
|
console.log('');
|
|
778
1081
|
});
|
|
779
1082
|
|
|
780
1083
|
setupWatchers();
|
|
1084
|
+
startHealthBroadcast();
|
|
781
1085
|
|
|
782
1086
|
if (PROXY_ONLY) {
|
|
783
1087
|
// In proxy-only mode, just set the app port for proxying
|
|
@@ -1,186 +1,104 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* API Provider - Provider implementation for REST API endpoints
|
|
3
|
-
*
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
};
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
106
|
-
try {
|
|
107
|
-
const response = await fetch(url, {
|
|
108
|
-
method: 'POST',
|
|
109
|
-
headers: {
|
|
110
|
-
'Content-Type': 'application/json',
|
|
111
|
-
'Authorization': `Bearer ${apiKey}`,
|
|
112
|
-
},
|
|
113
|
-
body: JSON.stringify(body),
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
// Handle rate limiting
|
|
117
|
-
if (response.status === 429) {
|
|
118
|
-
const retryAfter = parseInt(response.headers.get('Retry-After') || '1', 10);
|
|
119
|
-
await new Promise(r => setTimeout(r, retryAfter * 1000 || retryDelay));
|
|
120
|
-
continue;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
if (!response.ok) {
|
|
124
|
-
const errorBody = await response.json().catch(() => ({}));
|
|
125
|
-
lastError = new Error(errorBody.error?.message || response.statusText);
|
|
126
|
-
continue;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
const data = await response.json();
|
|
130
|
-
const parsed = parseResponse(data);
|
|
131
|
-
|
|
132
|
-
// Get pricing for cost calculation
|
|
133
|
-
const pricing = API_PRICING[model] || API_PRICING.default;
|
|
134
|
-
const cost = calculateCost(parsed.tokenUsage, pricing);
|
|
135
|
-
|
|
136
|
-
return {
|
|
137
|
-
...parsed,
|
|
138
|
-
exitCode: 0,
|
|
139
|
-
cost,
|
|
140
|
-
};
|
|
141
|
-
} catch (err) {
|
|
142
|
-
lastError = err;
|
|
143
|
-
|
|
144
|
-
if (attempt < maxRetries - 1) {
|
|
145
|
-
await new Promise(r => setTimeout(r, retryDelay));
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
return {
|
|
151
|
-
raw: '',
|
|
152
|
-
parsed: null,
|
|
153
|
-
exitCode: 1,
|
|
154
|
-
error: lastError?.message || 'API call failed',
|
|
155
|
-
tokenUsage: null,
|
|
156
|
-
cost: null,
|
|
157
|
-
};
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
/**
|
|
161
|
-
* Create an API provider instance
|
|
162
|
-
* @param {Object} config - Provider configuration
|
|
163
|
-
* @returns {Object} Provider instance
|
|
164
|
-
*/
|
|
165
|
-
export function createAPIProvider(config) {
|
|
166
|
-
const runner = async (prompt, opts) => {
|
|
167
|
-
if (!config.apiKey && !process.env[`${config.name.toUpperCase()}_API_KEY`]) {
|
|
168
|
-
throw new Error(`API key not configured for ${config.name}`);
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
return callAPI({
|
|
172
|
-
baseUrl: config.baseUrl,
|
|
173
|
-
model: config.model || config.name,
|
|
174
|
-
prompt,
|
|
175
|
-
apiKey: config.apiKey || process.env[`${config.name.toUpperCase()}_API_KEY`],
|
|
176
|
-
outputSchema: opts.outputSchema,
|
|
177
|
-
});
|
|
178
|
-
};
|
|
179
|
-
|
|
180
|
-
return createProvider({
|
|
181
|
-
...config,
|
|
182
|
-
type: PROVIDER_TYPES.API,
|
|
183
|
-
devserverOnly: config.devserverOnly ?? true,
|
|
184
|
-
runner,
|
|
185
|
-
});
|
|
186
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* API Provider - Provider implementation for REST API endpoints
|
|
3
|
+
* Phase 33, Task 4
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export function calculateCost(tokenUsage, pricing) {
|
|
7
|
+
const inputCost = (tokenUsage.input / 1000) * pricing.inputPer1k;
|
|
8
|
+
const outputCost = (tokenUsage.output / 1000) * pricing.outputPer1k;
|
|
9
|
+
return inputCost + outputCost;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export class APIProvider {
|
|
13
|
+
constructor(config) {
|
|
14
|
+
this.name = config.name;
|
|
15
|
+
this.baseUrl = config.baseUrl;
|
|
16
|
+
this.model = config.model;
|
|
17
|
+
this.apiKey = config.apiKey;
|
|
18
|
+
this.pricing = config.pricing || { inputPer1k: 0, outputPer1k: 0 };
|
|
19
|
+
this.maxRetries = config.maxRetries || 3;
|
|
20
|
+
this._fetch = null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async run(prompt, options = {}) {
|
|
24
|
+
const fetch = this._fetch || globalThis.fetch;
|
|
25
|
+
const url = this.baseUrl + '/v1/chat/completions';
|
|
26
|
+
|
|
27
|
+
const body = {
|
|
28
|
+
model: this.model,
|
|
29
|
+
messages: [{ role: 'user', content: prompt }],
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
if (options.outputSchema) {
|
|
33
|
+
body.response_format = {
|
|
34
|
+
type: 'json_schema',
|
|
35
|
+
json_schema: {
|
|
36
|
+
name: 'response',
|
|
37
|
+
schema: options.outputSchema,
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
let lastError;
|
|
43
|
+
for (let attempt = 0; attempt < this.maxRetries; attempt++) {
|
|
44
|
+
try {
|
|
45
|
+
const response = await fetch(url, {
|
|
46
|
+
method: 'POST',
|
|
47
|
+
headers: {
|
|
48
|
+
'Content-Type': 'application/json',
|
|
49
|
+
Authorization: 'Bearer ' + this.apiKey,
|
|
50
|
+
},
|
|
51
|
+
body: JSON.stringify(body),
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
if (response.status === 429) {
|
|
55
|
+
// Rate limited, wait and retry
|
|
56
|
+
await new Promise(r => setTimeout(r, 1000 * (attempt + 1)));
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (!response.ok) {
|
|
61
|
+
const error = await response.json();
|
|
62
|
+
throw new Error(error.error?.message || 'API error');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const data = await response.json();
|
|
66
|
+
return this._parseResponse(data);
|
|
67
|
+
} catch (err) {
|
|
68
|
+
lastError = err;
|
|
69
|
+
if (err.message.includes('network') || err.message.includes('Network')) {
|
|
70
|
+
throw err;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
throw lastError || new Error('Max retries exceeded');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
_parseResponse(data) {
|
|
79
|
+
const content = data.choices?.[0]?.message?.content || '';
|
|
80
|
+
const usage = data.usage || {};
|
|
81
|
+
|
|
82
|
+
let parsed = null;
|
|
83
|
+
try {
|
|
84
|
+
parsed = JSON.parse(content);
|
|
85
|
+
} catch {
|
|
86
|
+
// Not JSON
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const tokenUsage = {
|
|
90
|
+
input: usage.prompt_tokens || 0,
|
|
91
|
+
output: usage.completion_tokens || 0,
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
raw: content,
|
|
96
|
+
parsed,
|
|
97
|
+
exitCode: 0,
|
|
98
|
+
tokenUsage,
|
|
99
|
+
cost: calculateCost(tokenUsage, this.pricing),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export default { APIProvider, calculateCost };
|