claudecto 0.1.0

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 (115) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +275 -0
  3. package/dist/__tests__/package.test.d.ts +2 -0
  4. package/dist/__tests__/package.test.d.ts.map +1 -0
  5. package/dist/__tests__/package.test.js +53 -0
  6. package/dist/__tests__/package.test.js.map +1 -0
  7. package/dist/cli.d.ts +6 -0
  8. package/dist/cli.d.ts.map +1 -0
  9. package/dist/cli.js +200 -0
  10. package/dist/cli.js.map +1 -0
  11. package/dist/index.d.ts +12 -0
  12. package/dist/index.d.ts.map +1 -0
  13. package/dist/index.js +12 -0
  14. package/dist/index.js.map +1 -0
  15. package/dist/server/index.d.ts +11 -0
  16. package/dist/server/index.d.ts.map +1 -0
  17. package/dist/server/index.js +1207 -0
  18. package/dist/server/index.js.map +1 -0
  19. package/dist/services/advisor.d.ts +117 -0
  20. package/dist/services/advisor.d.ts.map +1 -0
  21. package/dist/services/advisor.js +2636 -0
  22. package/dist/services/advisor.js.map +1 -0
  23. package/dist/services/agent-generator.d.ts +71 -0
  24. package/dist/services/agent-generator.d.ts.map +1 -0
  25. package/dist/services/agent-generator.js +295 -0
  26. package/dist/services/agent-generator.js.map +1 -0
  27. package/dist/services/agents.d.ts +67 -0
  28. package/dist/services/agents.d.ts.map +1 -0
  29. package/dist/services/agents.js +405 -0
  30. package/dist/services/agents.js.map +1 -0
  31. package/dist/services/analytics.d.ts +145 -0
  32. package/dist/services/analytics.d.ts.map +1 -0
  33. package/dist/services/analytics.js +609 -0
  34. package/dist/services/analytics.js.map +1 -0
  35. package/dist/services/blueprints.d.ts +31 -0
  36. package/dist/services/blueprints.d.ts.map +1 -0
  37. package/dist/services/blueprints.js +317 -0
  38. package/dist/services/blueprints.js.map +1 -0
  39. package/dist/services/claude-dir.d.ts +50 -0
  40. package/dist/services/claude-dir.d.ts.map +1 -0
  41. package/dist/services/claude-dir.js +193 -0
  42. package/dist/services/claude-dir.js.map +1 -0
  43. package/dist/services/hooks.d.ts +38 -0
  44. package/dist/services/hooks.d.ts.map +1 -0
  45. package/dist/services/hooks.js +165 -0
  46. package/dist/services/hooks.js.map +1 -0
  47. package/dist/services/insights.d.ts +52 -0
  48. package/dist/services/insights.d.ts.map +1 -0
  49. package/dist/services/insights.js +1035 -0
  50. package/dist/services/insights.js.map +1 -0
  51. package/dist/services/memory.d.ts +14 -0
  52. package/dist/services/memory.d.ts.map +1 -0
  53. package/dist/services/memory.js +25 -0
  54. package/dist/services/memory.js.map +1 -0
  55. package/dist/services/plans.d.ts +20 -0
  56. package/dist/services/plans.d.ts.map +1 -0
  57. package/dist/services/plans.js +149 -0
  58. package/dist/services/plans.js.map +1 -0
  59. package/dist/services/project-intelligence.d.ts +75 -0
  60. package/dist/services/project-intelligence.d.ts.map +1 -0
  61. package/dist/services/project-intelligence.js +731 -0
  62. package/dist/services/project-intelligence.js.map +1 -0
  63. package/dist/services/search.d.ts +32 -0
  64. package/dist/services/search.d.ts.map +1 -0
  65. package/dist/services/search.js +203 -0
  66. package/dist/services/search.js.map +1 -0
  67. package/dist/services/sessions.d.ts +25 -0
  68. package/dist/services/sessions.d.ts.map +1 -0
  69. package/dist/services/sessions.js +248 -0
  70. package/dist/services/sessions.js.map +1 -0
  71. package/dist/services/skills.d.ts +30 -0
  72. package/dist/services/skills.d.ts.map +1 -0
  73. package/dist/services/skills.js +197 -0
  74. package/dist/services/skills.js.map +1 -0
  75. package/dist/services/stats.d.ts +23 -0
  76. package/dist/services/stats.d.ts.map +1 -0
  77. package/dist/services/stats.js +88 -0
  78. package/dist/services/stats.js.map +1 -0
  79. package/dist/services/teams.d.ts +115 -0
  80. package/dist/services/teams.d.ts.map +1 -0
  81. package/dist/services/teams.js +421 -0
  82. package/dist/services/teams.js.map +1 -0
  83. package/dist/services/tech-stack.d.ts +98 -0
  84. package/dist/services/tech-stack.d.ts.map +1 -0
  85. package/dist/services/tech-stack.js +1088 -0
  86. package/dist/services/tech-stack.js.map +1 -0
  87. package/dist/services/terminal.d.ts +75 -0
  88. package/dist/services/terminal.d.ts.map +1 -0
  89. package/dist/services/terminal.js +224 -0
  90. package/dist/services/terminal.js.map +1 -0
  91. package/dist/types.d.ts +1095 -0
  92. package/dist/types.d.ts.map +1 -0
  93. package/dist/types.js +18 -0
  94. package/dist/types.js.map +1 -0
  95. package/dist/ui/assets/index-BiH4Nhdk.css +1 -0
  96. package/dist/ui/assets/index-Brv-K8bd.css +1 -0
  97. package/dist/ui/assets/index-BwMBEdQz.js +3108 -0
  98. package/dist/ui/assets/index-BwMBEdQz.js.map +1 -0
  99. package/dist/ui/assets/index-CEWz7ABD.js +3108 -0
  100. package/dist/ui/assets/index-CEWz7ABD.js.map +1 -0
  101. package/dist/ui/assets/index-CIZ3vvc-.css +1 -0
  102. package/dist/ui/assets/index-CsU3cI0n.js +3108 -0
  103. package/dist/ui/assets/index-CsU3cI0n.js.map +1 -0
  104. package/dist/ui/assets/index-D3AY6iCS.js +3133 -0
  105. package/dist/ui/assets/index-D3AY6iCS.js.map +1 -0
  106. package/dist/ui/assets/index-D8lNZ0Ye.css +1 -0
  107. package/dist/ui/assets/index-DmgeppSA.js +3108 -0
  108. package/dist/ui/assets/index-DmgeppSA.js.map +1 -0
  109. package/dist/ui/favicon.svg +43 -0
  110. package/dist/ui/index.html +23 -0
  111. package/dist/utils/jsonl.d.ts +16 -0
  112. package/dist/utils/jsonl.d.ts.map +1 -0
  113. package/dist/utils/jsonl.js +51 -0
  114. package/dist/utils/jsonl.js.map +1 -0
  115. package/package.json +106 -0
@@ -0,0 +1,1207 @@
1
+ /**
2
+ * claudecto server - Hono-based API server
3
+ */
4
+ import fs from 'node:fs';
5
+ import http from 'node:http';
6
+ import path from 'node:path';
7
+ import { fileURLToPath } from 'node:url';
8
+ import { Hono } from 'hono';
9
+ import { cors } from 'hono/cors';
10
+ import { WebSocketServer, WebSocket } from 'ws';
11
+ import { ClaudeDir } from '../services/claude-dir.js';
12
+ import { SessionService } from '../services/sessions.js';
13
+ import { StatsService } from '../services/stats.js';
14
+ import { PlansService } from '../services/plans.js';
15
+ import { SkillsService } from '../services/skills.js';
16
+ import { SearchService } from '../services/search.js';
17
+ import { MemoryService } from '../services/memory.js';
18
+ import { HooksService } from '../services/hooks.js';
19
+ import { AnalyticsService } from '../services/analytics.js';
20
+ import { TerminalService } from '../services/terminal.js';
21
+ import { InsightsService } from '../services/insights.js';
22
+ import { TechStackService } from '../services/tech-stack.js';
23
+ import { AgentsService } from '../services/agents.js';
24
+ import { TeamsService } from '../services/teams.js';
25
+ import { BlueprintsService } from '../services/blueprints.js';
26
+ import { AgentGeneratorService } from '../services/agent-generator.js';
27
+ import { AdvisorService } from '../services/advisor.js';
28
+ import { ProjectIntelligenceService } from '../services/project-intelligence.js';
29
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
30
+ /**
31
+ * Validates and decodes a base64-encoded project path.
32
+ * Prevents path traversal attacks by checking for '..' sequences.
33
+ * @returns The decoded path or null if invalid
34
+ */
35
+ function validateProjectPath(encoded) {
36
+ try {
37
+ const decoded = Buffer.from(encoded, 'base64').toString('utf-8');
38
+ // Prevent path traversal attacks
39
+ if (decoded.includes('..') || decoded.includes('\0')) {
40
+ return null;
41
+ }
42
+ // Must be an absolute path
43
+ if (!path.isAbsolute(decoded)) {
44
+ return null;
45
+ }
46
+ return decoded;
47
+ }
48
+ catch {
49
+ return null;
50
+ }
51
+ }
52
+ function resolveUiRoot() {
53
+ const candidates = [
54
+ path.resolve(__dirname, '../ui'),
55
+ path.resolve(__dirname, '../../ui/dist'),
56
+ path.resolve(process.cwd(), 'ui/dist'),
57
+ ];
58
+ for (const dir of candidates) {
59
+ if (fs.existsSync(path.join(dir, 'index.html'))) {
60
+ return dir;
61
+ }
62
+ }
63
+ return null;
64
+ }
65
+ function contentTypeForExt(ext) {
66
+ const types = {
67
+ '.html': 'text/html; charset=utf-8',
68
+ '.js': 'application/javascript; charset=utf-8',
69
+ '.mjs': 'application/javascript; charset=utf-8',
70
+ '.css': 'text/css; charset=utf-8',
71
+ '.json': 'application/json; charset=utf-8',
72
+ '.svg': 'image/svg+xml',
73
+ '.png': 'image/png',
74
+ '.jpg': 'image/jpeg',
75
+ '.jpeg': 'image/jpeg',
76
+ '.ico': 'image/x-icon',
77
+ '.woff': 'font/woff',
78
+ '.woff2': 'font/woff2',
79
+ };
80
+ return types[ext] ?? 'application/octet-stream';
81
+ }
82
+ function serveStatic(req, res, root) {
83
+ const url = new URL(req.url ?? '/', 'http://localhost');
84
+ if (url.pathname.startsWith('/api/'))
85
+ return false;
86
+ let relPath = url.pathname.slice(1) || 'index.html';
87
+ if (relPath.endsWith('/'))
88
+ relPath += 'index.html';
89
+ const normalized = path.posix.normalize(relPath);
90
+ if (normalized.startsWith('../') || normalized.includes('\0'))
91
+ return false;
92
+ const filePath = path.join(root, relPath);
93
+ if (!filePath.startsWith(root))
94
+ return false;
95
+ if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) {
96
+ res.setHeader('Content-Type', contentTypeForExt(path.extname(filePath)));
97
+ res.setHeader('Cache-Control', 'no-cache');
98
+ res.end(fs.readFileSync(filePath));
99
+ return true;
100
+ }
101
+ // SPA fallback
102
+ const indexPath = path.join(root, 'index.html');
103
+ if (fs.existsSync(indexPath)) {
104
+ res.setHeader('Content-Type', 'text/html; charset=utf-8');
105
+ res.setHeader('Cache-Control', 'no-cache');
106
+ res.end(fs.readFileSync(indexPath));
107
+ return true;
108
+ }
109
+ return false;
110
+ }
111
+ export async function createServer(config) {
112
+ const { claudeDir: claudeDirPath, port, host } = config;
113
+ // Initialize services
114
+ const claudeDir = new ClaudeDir(claudeDirPath);
115
+ const sessionService = new SessionService(claudeDir);
116
+ const statsService = new StatsService(claudeDir);
117
+ const plansService = new PlansService(claudeDir);
118
+ const skillsService = new SkillsService(claudeDir);
119
+ const searchService = new SearchService(claudeDir);
120
+ const hooksService = new HooksService(claudeDir);
121
+ const memoryService = new MemoryService(claudeDir);
122
+ const analyticsService = new AnalyticsService(claudeDir);
123
+ const terminalService = new TerminalService();
124
+ const insightsService = new InsightsService(claudeDir, sessionService, analyticsService);
125
+ const techStackService = new TechStackService(claudeDir);
126
+ const agentsService = new AgentsService(claudeDir);
127
+ const teamsService = new TeamsService(claudeDir);
128
+ const blueprintsService = new BlueprintsService(claudeDir);
129
+ const agentGeneratorService = new AgentGeneratorService(claudeDir);
130
+ const advisorService = new AdvisorService(claudeDir, sessionService, analyticsService, skillsService, hooksService, memoryService, agentsService);
131
+ const projectIntelligenceService = new ProjectIntelligenceService(claudeDir, sessionService);
132
+ // Cache for generation results (in-memory for session)
133
+ const generationCache = new Map();
134
+ // Create Hono app
135
+ const app = new Hono();
136
+ // Restrict CORS to configured origins (security fix)
137
+ const allowedOrigins = process.env.ALLOWED_ORIGINS?.split(',') || [
138
+ 'http://localhost:18791',
139
+ 'http://127.0.0.1:18791',
140
+ `http://localhost:${port}`,
141
+ `http://127.0.0.1:${port}`,
142
+ ];
143
+ app.use('*', cors({
144
+ origin: (origin) => {
145
+ // Allow requests with no origin (same-origin, curl, etc)
146
+ if (!origin)
147
+ return allowedOrigins[0] || 'http://localhost:18791';
148
+ // Check if origin is in allowed list
149
+ if (allowedOrigins.includes(origin))
150
+ return origin;
151
+ // Fallback - don't allow unknown origins
152
+ return null;
153
+ },
154
+ credentials: true,
155
+ }));
156
+ // ============================================================================
157
+ // Status & Health
158
+ // ============================================================================
159
+ app.get('/api/status', async (c) => {
160
+ const exists = await claudeDir.exists();
161
+ if (!exists) {
162
+ return c.json({ error: 'Claude directory not found', code: 'NOT_FOUND' }, 404);
163
+ }
164
+ const projects = await sessionService.listProjects();
165
+ const { sessions } = await sessionService.listSessions({ limit: 1 });
166
+ const plans = await plansService.listPlans();
167
+ const skills = await skillsService.listSkills();
168
+ const stats = await statsService.getStats();
169
+ return c.json({
170
+ claudeDir: claudeDir.root,
171
+ projectCount: projects.length,
172
+ sessionCount: sessions.length > 0 ? (await sessionService.listSessions()).total : 0,
173
+ planCount: plans.length,
174
+ skillCount: skills.length,
175
+ hasStats: stats !== null,
176
+ });
177
+ });
178
+ app.get('/api/health', (c) => c.json({ ok: true }));
179
+ // ============================================================================
180
+ // OAuth (for Electron Google Sign-In)
181
+ // ============================================================================
182
+ // Store pending auth tokens (cleared after retrieval)
183
+ let pendingAuthTokens = null;
184
+ let pendingAuthError = null;
185
+ // Endpoint to check for pending auth tokens
186
+ app.get('/api/auth/pending', (c) => {
187
+ if (pendingAuthError) {
188
+ const error = pendingAuthError;
189
+ pendingAuthError = null;
190
+ return c.json({ error });
191
+ }
192
+ if (pendingAuthTokens) {
193
+ const tokens = pendingAuthTokens;
194
+ pendingAuthTokens = null; // Clear after retrieval
195
+ return c.json({ tokens });
196
+ }
197
+ return c.json({ pending: true });
198
+ });
199
+ // OAuth callback from Google
200
+ app.get('/auth/callback', async (c) => {
201
+ const code = c.req.query('code');
202
+ const error = c.req.query('error');
203
+ const successHtml = `
204
+ <!DOCTYPE html>
205
+ <html>
206
+ <head>
207
+ <title>Authentication Successful</title>
208
+ <style>
209
+ body { font-family: system-ui, sans-serif; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; background: #0f172a; color: white; }
210
+ .container { text-align: center; padding: 2rem; }
211
+ h1 { color: #22c55e; }
212
+ </style>
213
+ </head>
214
+ <body>
215
+ <div class="container">
216
+ <h1>Authentication Successful!</h1>
217
+ <p>You can close this tab and return to the app.</p>
218
+ </div>
219
+ </body>
220
+ </html>
221
+ `;
222
+ const errorHtml = (msg) => `
223
+ <!DOCTYPE html>
224
+ <html>
225
+ <head>
226
+ <title>Authentication Failed</title>
227
+ <style>
228
+ body { font-family: system-ui, sans-serif; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; background: #0f172a; color: white; }
229
+ .container { text-align: center; padding: 2rem; }
230
+ h1 { color: #ef4444; }
231
+ </style>
232
+ </head>
233
+ <body>
234
+ <div class="container">
235
+ <h1>Authentication Failed</h1>
236
+ <p>${msg}</p>
237
+ <p>You can close this tab and try again.</p>
238
+ </div>
239
+ </body>
240
+ </html>
241
+ `;
242
+ if (error) {
243
+ pendingAuthError = c.req.query('error_description') || error;
244
+ return c.html(errorHtml(pendingAuthError));
245
+ }
246
+ if (!code) {
247
+ pendingAuthError = 'No authorization code received';
248
+ return c.html(errorHtml(pendingAuthError));
249
+ }
250
+ if (!config.oauth) {
251
+ pendingAuthError = 'OAuth not configured';
252
+ return c.html(errorHtml(pendingAuthError));
253
+ }
254
+ try {
255
+ // Validate OAuth credentials are configured
256
+ if (!config.oauth.googleClientId || config.oauth.googleClientId.includes('REPLACE_WITH')) {
257
+ throw new Error('Google OAuth client ID not configured. Set GOOGLE_CLIENT_ID environment variable.');
258
+ }
259
+ if (!config.oauth.googleClientSecret || config.oauth.googleClientSecret.includes('REPLACE_WITH')) {
260
+ throw new Error('Google OAuth client secret not configured. Set GOOGLE_CLIENT_SECRET environment variable.');
261
+ }
262
+ // Exchange code for tokens
263
+ const tokenResponse = await fetch('https://oauth2.googleapis.com/token', {
264
+ method: 'POST',
265
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
266
+ body: new URLSearchParams({
267
+ code,
268
+ client_id: config.oauth.googleClientId,
269
+ client_secret: config.oauth.googleClientSecret,
270
+ redirect_uri: `http://localhost:${port}/auth/callback`,
271
+ grant_type: 'authorization_code',
272
+ }),
273
+ });
274
+ const tokens = await tokenResponse.json();
275
+ if (tokens.error) {
276
+ // Provide helpful error messages for common OAuth errors
277
+ let errorMessage = tokens.error_description || tokens.error;
278
+ if (tokens.error === 'invalid_grant') {
279
+ errorMessage = 'Authorization code expired or already used. Please try signing in again.';
280
+ }
281
+ else if (tokens.error === 'invalid_client') {
282
+ errorMessage = 'Invalid OAuth client credentials. Please check GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET.';
283
+ }
284
+ else if (tokens.error === 'redirect_uri_mismatch') {
285
+ errorMessage = 'Redirect URI mismatch. Add http://localhost:18791/auth/callback to your OAuth client in Google Cloud Console.';
286
+ }
287
+ throw new Error(errorMessage);
288
+ }
289
+ if (!tokens.id_token) {
290
+ throw new Error('No ID token received. Ensure "openid" scope is included in OAuth request.');
291
+ }
292
+ // Store tokens for the app to retrieve
293
+ pendingAuthTokens = {
294
+ idToken: tokens.id_token,
295
+ accessToken: tokens.access_token,
296
+ };
297
+ return c.html(successHtml);
298
+ }
299
+ catch (err) {
300
+ const errorMsg = err.message || 'Token exchange failed';
301
+ console.error('OAuth callback error:', errorMsg);
302
+ pendingAuthError = errorMsg;
303
+ return c.html(errorHtml(errorMsg));
304
+ }
305
+ });
306
+ // ============================================================================
307
+ // Stats
308
+ // ============================================================================
309
+ app.get('/api/stats', async (c) => {
310
+ const summary = await statsService.getSummary();
311
+ return c.json(summary);
312
+ });
313
+ app.get('/api/stats/daily', async (c) => {
314
+ const days = Number.parseInt(c.req.query('days') ?? '30', 10);
315
+ const activity = await statsService.getDailyActivity(days);
316
+ return c.json({ activity });
317
+ });
318
+ // ============================================================================
319
+ // Projects & Sessions
320
+ // ============================================================================
321
+ app.get('/api/projects', async (c) => {
322
+ const projects = await sessionService.listProjects();
323
+ return c.json({ projects });
324
+ });
325
+ app.get('/api/sessions', async (c) => {
326
+ const project = c.req.query('project');
327
+ const page = Number.parseInt(c.req.query('page') ?? '1', 10);
328
+ const limit = Number.parseInt(c.req.query('limit') ?? '20', 10);
329
+ const offset = (page - 1) * limit;
330
+ const { sessions, total } = await sessionService.listSessions({ project, limit, offset });
331
+ return c.json({
332
+ items: sessions,
333
+ total,
334
+ page,
335
+ limit,
336
+ hasMore: offset + sessions.length < total,
337
+ });
338
+ });
339
+ app.get('/api/sessions/:id', async (c) => {
340
+ const session = await sessionService.getSession(c.req.param('id'));
341
+ if (!session) {
342
+ return c.json({ error: 'Session not found', code: 'NOT_FOUND' }, 404);
343
+ }
344
+ return c.json({ session });
345
+ });
346
+ app.get('/api/projects/:id/plans', async (c) => {
347
+ // Project ID is base64-encoded path - validate to prevent path traversal
348
+ const projectPath = validateProjectPath(c.req.param('id'));
349
+ if (!projectPath) {
350
+ return c.json({ error: 'Invalid project path', code: 'INVALID' }, 400);
351
+ }
352
+ const plans = await plansService.getPlansForProject(projectPath);
353
+ return c.json({ plans });
354
+ });
355
+ // ============================================================================
356
+ // Search
357
+ // ============================================================================
358
+ app.post('/api/search', async (c) => {
359
+ const body = await c.req.json();
360
+ if (!body.query?.trim()) {
361
+ return c.json({ error: 'Query required', code: 'INVALID' }, 400);
362
+ }
363
+ const { results, total } = await searchService.search(body);
364
+ return c.json({ results, total, query: body.query });
365
+ });
366
+ // ============================================================================
367
+ // Plans
368
+ // ============================================================================
369
+ app.get('/api/plans', async (c) => {
370
+ const search = c.req.query('search');
371
+ const plans = search
372
+ ? await plansService.searchPlans(search)
373
+ : await plansService.listPlans();
374
+ return c.json({ plans });
375
+ });
376
+ app.get('/api/plans/:name', async (c) => {
377
+ const plan = await plansService.getPlan(c.req.param('name'));
378
+ if (!plan) {
379
+ return c.json({ error: 'Plan not found', code: 'NOT_FOUND' }, 404);
380
+ }
381
+ return c.json({ plan });
382
+ });
383
+ // ============================================================================
384
+ // Skills
385
+ // ============================================================================
386
+ app.get('/api/skills', async (c) => {
387
+ const skills = await skillsService.listSkills();
388
+ return c.json({ skills });
389
+ });
390
+ app.get('/api/skills/:path', async (c) => {
391
+ const skill = await skillsService.getSkill(c.req.param('path'));
392
+ if (!skill) {
393
+ return c.json({ error: 'Skill not found', code: 'NOT_FOUND' }, 404);
394
+ }
395
+ return c.json({ skill });
396
+ });
397
+ app.post('/api/skills', async (c) => {
398
+ const body = await c.req.json();
399
+ if (!body.name || !body.content) {
400
+ return c.json({ error: 'Name and content required', code: 'INVALID' }, 400);
401
+ }
402
+ const result = await skillsService.createSkill(body.name, body.content);
403
+ if (!result.success) {
404
+ return c.json({ error: result.error, code: 'FAILED' }, 400);
405
+ }
406
+ return c.json({ success: true });
407
+ });
408
+ app.put('/api/skills/:path', async (c) => {
409
+ const body = await c.req.json();
410
+ if (!body.content) {
411
+ return c.json({ error: 'Content required', code: 'INVALID' }, 400);
412
+ }
413
+ const result = await skillsService.updateSkill(c.req.param('path'), body.content);
414
+ if (!result.success) {
415
+ return c.json({ error: result.error, code: 'FAILED' }, 400);
416
+ }
417
+ return c.json({ success: true });
418
+ });
419
+ app.delete('/api/skills/:path', async (c) => {
420
+ const result = await skillsService.deleteSkill(c.req.param('path'));
421
+ if (!result.success) {
422
+ return c.json({ error: result.error, code: 'FAILED' }, 400);
423
+ }
424
+ return c.json({ success: true });
425
+ });
426
+ app.post('/api/skills/validate', async (c) => {
427
+ const body = await c.req.json();
428
+ if (!body.content) {
429
+ return c.json({ error: 'Content required', code: 'INVALID' }, 400);
430
+ }
431
+ const validation = skillsService.validateSkill(body.content);
432
+ return c.json(validation);
433
+ });
434
+ app.get('/api/skills/template', (c) => {
435
+ return c.json({ template: skillsService.getSkillTemplate() });
436
+ });
437
+ // ============================================================================
438
+ // Hooks
439
+ // ============================================================================
440
+ app.get('/api/hooks', async (c) => {
441
+ const hooks = await hooksService.listHooks();
442
+ return c.json({ hooks });
443
+ });
444
+ app.get('/api/hooks/:id', async (c) => {
445
+ const hook = await hooksService.getHook(c.req.param('id'));
446
+ if (!hook) {
447
+ return c.json({ error: 'Hook not found', code: 'NOT_FOUND' }, 404);
448
+ }
449
+ return c.json({ hook });
450
+ });
451
+ app.post('/api/hooks', async (c) => {
452
+ const body = await c.req.json();
453
+ if (!body.event || !body.command) {
454
+ return c.json({ error: 'Event and command required', code: 'INVALID' }, 400);
455
+ }
456
+ const result = await hooksService.createHook(body);
457
+ if (!result.success) {
458
+ return c.json({ error: result.error, code: 'FAILED' }, 400);
459
+ }
460
+ return c.json({ success: true, id: result.id });
461
+ });
462
+ app.put('/api/hooks/:id', async (c) => {
463
+ const body = await c.req.json();
464
+ const result = await hooksService.updateHook(c.req.param('id'), body);
465
+ if (!result.success) {
466
+ return c.json({ error: result.error, code: 'FAILED' }, 400);
467
+ }
468
+ return c.json({ success: true });
469
+ });
470
+ app.delete('/api/hooks/:id', async (c) => {
471
+ const result = await hooksService.deleteHook(c.req.param('id'));
472
+ if (!result.success) {
473
+ return c.json({ error: result.error, code: 'FAILED' }, 400);
474
+ }
475
+ return c.json({ success: true });
476
+ });
477
+ app.post('/api/hooks/:id/test', async (c) => {
478
+ const result = await hooksService.testHook(c.req.param('id'));
479
+ return c.json(result);
480
+ });
481
+ // ============================================================================
482
+ // Memory
483
+ // ============================================================================
484
+ app.get('/api/memory', async (c) => {
485
+ const memory = await memoryService.getMemory();
486
+ return c.json({ content: memory });
487
+ });
488
+ app.post('/api/memory', async (c) => {
489
+ const body = await c.req.json();
490
+ if (typeof body.content !== 'string') {
491
+ return c.json({ error: 'Content required', code: 'INVALID' }, 400);
492
+ }
493
+ const result = await memoryService.updateMemory(body.content);
494
+ if (!result.success) {
495
+ return c.json({ error: result.error, code: 'FAILED' }, 400);
496
+ }
497
+ return c.json({ success: true });
498
+ });
499
+ // ============================================================================
500
+ // Analytics - Real Token/Cost Tracking
501
+ // ============================================================================
502
+ app.get('/api/analytics', async (c) => {
503
+ const daysParam = c.req.query('days');
504
+ const days = daysParam ? Number.parseInt(daysParam, 10) : undefined;
505
+ const analytics = await analyticsService.getGlobalAnalytics(days);
506
+ return c.json(analytics);
507
+ });
508
+ app.get('/api/analytics/daily', async (c) => {
509
+ const days = Number.parseInt(c.req.query('days') ?? '30', 10);
510
+ const daily = await analyticsService.getDailyAnalytics(days);
511
+ return c.json({ daily });
512
+ });
513
+ app.get('/api/analytics/projects/:id', async (c) => {
514
+ const projectDir = validateProjectPath(c.req.param('id'));
515
+ if (!projectDir) {
516
+ return c.json({ error: 'Invalid project path', code: 'INVALID' }, 400);
517
+ }
518
+ const analytics = await analyticsService.getProjectAnalytics(projectDir);
519
+ if (!analytics) {
520
+ return c.json({ error: 'Project not found', code: 'NOT_FOUND' }, 404);
521
+ }
522
+ return c.json(analytics);
523
+ });
524
+ app.get('/api/analytics/sessions/:id', async (c) => {
525
+ const sessionId = c.req.param('id');
526
+ // Find the session file
527
+ const projectDirs = await claudeDir.listProjects();
528
+ for (const projectDir of projectDirs) {
529
+ const sessionFiles = await claudeDir.listSessionFiles(projectDir);
530
+ for (const filePath of sessionFiles) {
531
+ const fileName = path.basename(filePath, '.jsonl');
532
+ if (fileName === sessionId || fileName.startsWith(sessionId)) {
533
+ const analytics = await analyticsService.getSessionAnalytics(filePath, projectDir);
534
+ if (analytics) {
535
+ return c.json(analytics);
536
+ }
537
+ }
538
+ }
539
+ }
540
+ return c.json({ error: 'Session not found', code: 'NOT_FOUND' }, 404);
541
+ });
542
+ app.get('/api/analytics/tools', async (c) => {
543
+ const days = Number.parseInt(c.req.query('days') ?? '30', 10);
544
+ const toolAnalytics = await analyticsService.getToolAnalytics(days);
545
+ return c.json(toolAnalytics);
546
+ });
547
+ // ============================================================================
548
+ // Insights - AI-Powered Session Analysis
549
+ // ============================================================================
550
+ app.get('/api/insights/status', async (c) => {
551
+ const available = await insightsService.isClaudeCodeAvailable();
552
+ return c.json({ available });
553
+ });
554
+ app.get('/api/insights', async (c) => {
555
+ const project = c.req.query('project');
556
+ const insights = await insightsService.listInsights(project);
557
+ return c.json({ insights });
558
+ });
559
+ // Global and project insights — must be before /:sessionId to avoid "global"/"project" matching as a sessionId
560
+ app.get('/api/insights/global', async (c) => {
561
+ const insight = await insightsService.getGlobalAIInsight();
562
+ if (!insight) {
563
+ return c.json({ error: 'Global insight not found', code: 'NOT_FOUND' }, 404);
564
+ }
565
+ return c.json({ insight });
566
+ });
567
+ app.post('/api/insights/global/generate', async (c) => {
568
+ const body = await c.req.json().catch(() => ({ force: false }));
569
+ const insight = await insightsService.generateGlobalAIInsight(body.force ?? false);
570
+ return c.json({ insight });
571
+ });
572
+ app.get('/api/insights/project/:projectId', async (c) => {
573
+ const projectPath = validateProjectPath(c.req.param('projectId'));
574
+ if (!projectPath) {
575
+ return c.json({ error: 'Invalid project path', code: 'INVALID' }, 400);
576
+ }
577
+ const insight = await insightsService.getProjectAIInsight(projectPath);
578
+ if (!insight) {
579
+ return c.json({ error: 'Project insight not found', code: 'NOT_FOUND' }, 404);
580
+ }
581
+ return c.json({ insight });
582
+ });
583
+ app.post('/api/insights/project/:projectId/generate', async (c) => {
584
+ const projectPath = validateProjectPath(c.req.param('projectId'));
585
+ if (!projectPath) {
586
+ return c.json({ error: 'Invalid project path', code: 'INVALID' }, 400);
587
+ }
588
+ const body = await c.req.json().catch(() => ({ force: false }));
589
+ const insight = await insightsService.generateProjectAIInsight(projectPath, body.force ?? false);
590
+ return c.json({ insight });
591
+ });
592
+ // Session-level insights — after static routes so "global"/"project" don't match as :sessionId
593
+ app.get('/api/insights/:sessionId', async (c) => {
594
+ const insight = await insightsService.getInsight(c.req.param('sessionId'));
595
+ if (!insight) {
596
+ return c.json({ error: 'Insight not found', code: 'NOT_FOUND' }, 404);
597
+ }
598
+ return c.json({ insight });
599
+ });
600
+ app.post('/api/insights/:sessionId/generate', async (c) => {
601
+ const sessionId = c.req.param('sessionId');
602
+ const body = await c.req.json().catch(() => ({ force: false }));
603
+ // Verify session exists
604
+ const session = await sessionService.getSession(sessionId);
605
+ if (!session) {
606
+ return c.json({ error: 'Session not found', code: 'NOT_FOUND' }, 404);
607
+ }
608
+ // Start generation (returns immediately with pending status if not cached)
609
+ const insight = await insightsService.generateInsight(sessionId, body.force ?? false);
610
+ return c.json({ insight });
611
+ });
612
+ // ============================================================================
613
+ // Tech Stack - Technology Usage Aggregation
614
+ // ============================================================================
615
+ app.get('/api/tech-stack', async (c) => {
616
+ const forceRefresh = c.req.query('refresh') === 'true';
617
+ const summary = await techStackService.getSummary(forceRefresh);
618
+ return c.json(summary);
619
+ });
620
+ app.get('/api/tech-stack/projects/:id', async (c) => {
621
+ const projectPath = validateProjectPath(c.req.param('id'));
622
+ if (!projectPath) {
623
+ return c.json({ error: 'Invalid project path', code: 'INVALID' }, 400);
624
+ }
625
+ const breakdown = await techStackService.getProjectBreakdown(projectPath);
626
+ if (!breakdown) {
627
+ return c.json({ error: 'Project not found', code: 'NOT_FOUND' }, 404);
628
+ }
629
+ return c.json(breakdown);
630
+ });
631
+ app.post('/api/tech-stack/refresh', async (c) => {
632
+ const summary = await techStackService.refresh();
633
+ return c.json(summary);
634
+ });
635
+ // Deep insights with patterns, correlations, evolution, and similarity
636
+ app.get('/api/tech-stack/insights', async (c) => {
637
+ const forceRefresh = c.req.query('refresh') === 'true';
638
+ const insights = await techStackService.getDeepInsights(forceRefresh);
639
+ return c.json(insights);
640
+ });
641
+ // AI-powered tech stack analysis
642
+ app.get('/api/tech-stack/ai-insight', async (c) => {
643
+ const insight = await techStackService.getAIInsight();
644
+ return c.json({ insight });
645
+ });
646
+ app.post('/api/tech-stack/ai-insight', async (c) => {
647
+ const body = await c.req.json().catch(() => ({}));
648
+ const force = body.force === true;
649
+ const insight = await techStackService.generateAIInsight(force);
650
+ return c.json({ insight });
651
+ });
652
+ // ============================================================================
653
+ // Agents - Custom Claude Code Subagents
654
+ // ============================================================================
655
+ app.get('/api/agents', async (c) => {
656
+ const agents = await agentsService.listAgents();
657
+ return c.json({ agents });
658
+ });
659
+ app.get('/api/agents/templates', (c) => {
660
+ const templates = agentsService.getAgentTemplates();
661
+ return c.json({ templates });
662
+ });
663
+ app.get('/api/agents/template/:id', (c) => {
664
+ const template = agentsService.getAgentTemplate(c.req.param('id'));
665
+ return c.json({ template });
666
+ });
667
+ app.get('/api/agents/:path', async (c) => {
668
+ const agent = await agentsService.getAgent(c.req.param('path'));
669
+ if (!agent) {
670
+ return c.json({ error: 'Agent not found', code: 'NOT_FOUND' }, 404);
671
+ }
672
+ return c.json({ agent });
673
+ });
674
+ app.post('/api/agents', async (c) => {
675
+ const body = await c.req.json();
676
+ if (!body.name || !body.content) {
677
+ return c.json({ error: 'Name and content required', code: 'INVALID' }, 400);
678
+ }
679
+ const result = await agentsService.createAgent(body.name, body.content);
680
+ if (!result.success) {
681
+ return c.json({ error: result.error, code: 'FAILED' }, 400);
682
+ }
683
+ return c.json({ success: true });
684
+ });
685
+ app.put('/api/agents/:path', async (c) => {
686
+ const body = await c.req.json();
687
+ if (!body.content) {
688
+ return c.json({ error: 'Content required', code: 'INVALID' }, 400);
689
+ }
690
+ const result = await agentsService.updateAgent(c.req.param('path'), body.content);
691
+ if (!result.success) {
692
+ return c.json({ error: result.error, code: 'FAILED' }, 400);
693
+ }
694
+ return c.json({ success: true });
695
+ });
696
+ app.delete('/api/agents/:path', async (c) => {
697
+ const result = await agentsService.deleteAgent(c.req.param('path'));
698
+ if (!result.success) {
699
+ return c.json({ error: result.error, code: 'FAILED' }, 400);
700
+ }
701
+ return c.json({ success: true });
702
+ });
703
+ app.post('/api/agents/validate', async (c) => {
704
+ const body = await c.req.json();
705
+ if (!body.content) {
706
+ return c.json({ error: 'Content required', code: 'INVALID' }, 400);
707
+ }
708
+ const validation = agentsService.validateAgent(body.content);
709
+ return c.json(validation);
710
+ });
711
+ // AI-powered generation endpoints
712
+ app.post('/api/agents/generate', async (c) => {
713
+ const body = await c.req.json();
714
+ if (!body.prompt || body.prompt.trim().length < 10) {
715
+ return c.json({
716
+ error: 'Please provide a more detailed description (at least 10 characters)',
717
+ code: 'INVALID'
718
+ }, 400);
719
+ }
720
+ const result = await agentGeneratorService.generateFromPrompt(body.prompt);
721
+ // Cache the result for apply
722
+ generationCache.set(result.id, result);
723
+ // Clean old cache entries (keep last 10)
724
+ if (generationCache.size > 10) {
725
+ const oldest = Array.from(generationCache.keys()).slice(0, generationCache.size - 10);
726
+ oldest.forEach(key => generationCache.delete(key));
727
+ }
728
+ return c.json(result);
729
+ });
730
+ app.post('/api/agents/generate/apply', async (c) => {
731
+ const body = await c.req.json();
732
+ if (!body.generationId) {
733
+ return c.json({ error: 'Generation ID required', code: 'INVALID' }, 400);
734
+ }
735
+ const result = generationCache.get(body.generationId);
736
+ if (!result) {
737
+ return c.json({
738
+ error: 'Generation not found. Please generate again.',
739
+ code: 'NOT_FOUND'
740
+ }, 404);
741
+ }
742
+ const agents = body.selectedAgents || result.agents.map(a => a.name);
743
+ const teams = body.selectedTeams || result.teams.map(t => t.name);
744
+ const applyResult = await agentGeneratorService.applyGeneration(result, agents, teams);
745
+ // Clear from cache after applying
746
+ generationCache.delete(body.generationId);
747
+ return c.json(applyResult);
748
+ });
749
+ // ============================================================================
750
+ // Teams - Multi-Agent Team Configurations
751
+ // ============================================================================
752
+ app.get('/api/teams', async (c) => {
753
+ const teams = await teamsService.listTeams();
754
+ return c.json({ teams });
755
+ });
756
+ app.get('/api/teams/templates', (c) => {
757
+ const templates = teamsService.getTeamTemplates();
758
+ return c.json({ templates });
759
+ });
760
+ app.get('/api/teams/:name', async (c) => {
761
+ const team = await teamsService.getTeam(c.req.param('name'));
762
+ if (!team) {
763
+ return c.json({ error: 'Team not found', code: 'NOT_FOUND' }, 404);
764
+ }
765
+ return c.json({ team });
766
+ });
767
+ app.post('/api/teams', async (c) => {
768
+ const body = await c.req.json();
769
+ if (!body.name) {
770
+ return c.json({ error: 'Name required', code: 'INVALID' }, 400);
771
+ }
772
+ const result = await teamsService.createTeam(body.name, {
773
+ description: body.description,
774
+ leader: body.leader,
775
+ teammates: body.teammates,
776
+ planFile: body.planFile,
777
+ });
778
+ if (!result.success) {
779
+ return c.json({ error: result.error, code: 'FAILED' }, 400);
780
+ }
781
+ return c.json({ success: true });
782
+ });
783
+ app.put('/api/teams/:name', async (c) => {
784
+ const body = await c.req.json();
785
+ const result = await teamsService.updateTeam(c.req.param('name'), body);
786
+ if (!result.success) {
787
+ return c.json({ error: result.error, code: 'FAILED' }, 400);
788
+ }
789
+ return c.json({ success: true });
790
+ });
791
+ app.delete('/api/teams/:name', async (c) => {
792
+ const result = await teamsService.deleteTeam(c.req.param('name'));
793
+ if (!result.success) {
794
+ return c.json({ error: result.error, code: 'FAILED' }, 400);
795
+ }
796
+ return c.json({ success: true });
797
+ });
798
+ app.post('/api/teams/:name/export', async (c) => {
799
+ const result = await teamsService.exportTeam(c.req.param('name'));
800
+ if (!result.success) {
801
+ return c.json({ error: result.error, code: 'FAILED' }, 400);
802
+ }
803
+ return c.json({ blueprint: result.blueprint });
804
+ });
805
+ // Get available agents for team member linking
806
+ app.get('/api/teams/available-agents', async (c) => {
807
+ const agents = await teamsService.listAvailableAgents();
808
+ return c.json({ agents });
809
+ });
810
+ // Validate agent references in team config
811
+ app.post('/api/teams/validate-agents', async (c) => {
812
+ const body = await c.req.json();
813
+ if (!body.teammates) {
814
+ return c.json({ valid: true, missing: [] });
815
+ }
816
+ const result = await teamsService.validateAgentRefs(body.teammates);
817
+ return c.json(result);
818
+ });
819
+ // Get invocation instructions for a team
820
+ app.get('/api/teams/:name/instructions', async (c) => {
821
+ const instructions = await teamsService.getInvocationInstructions(c.req.param('name'));
822
+ if (!instructions) {
823
+ return c.json({ error: 'Team not found', code: 'NOT_FOUND' }, 404);
824
+ }
825
+ return c.json({ instructions });
826
+ });
827
+ // Generate setup script for a team
828
+ app.get('/api/teams/:name/setup-script', async (c) => {
829
+ const result = await teamsService.generateSetupScript(c.req.param('name'));
830
+ if (!result.success) {
831
+ return c.json({ error: result.error, code: 'FAILED' }, 400);
832
+ }
833
+ return c.json({ script: result.script });
834
+ });
835
+ // ============================================================================
836
+ // Blueprints - Pre-built Agent/Team Templates
837
+ // ============================================================================
838
+ app.get('/api/blueprints', async (c) => {
839
+ const type = c.req.query('type');
840
+ const category = c.req.query('category');
841
+ let blueprints;
842
+ if (type) {
843
+ blueprints = await blueprintsService.getBlueprintsByType(type);
844
+ }
845
+ else if (category) {
846
+ blueprints = await blueprintsService.getBlueprintsByCategory(category);
847
+ }
848
+ else {
849
+ blueprints = await blueprintsService.listBlueprints();
850
+ }
851
+ return c.json({ blueprints });
852
+ });
853
+ app.get('/api/blueprints/:id', async (c) => {
854
+ const blueprint = await blueprintsService.getBlueprint(c.req.param('id'));
855
+ if (!blueprint) {
856
+ return c.json({ error: 'Blueprint not found', code: 'NOT_FOUND' }, 404);
857
+ }
858
+ return c.json({ blueprint });
859
+ });
860
+ app.post('/api/blueprints/:id/apply', async (c) => {
861
+ const blueprint = await blueprintsService.getBlueprint(c.req.param('id'));
862
+ if (!blueprint) {
863
+ return c.json({ error: 'Blueprint not found', code: 'NOT_FOUND' }, 404);
864
+ }
865
+ // Apply the blueprint based on type
866
+ if (blueprint.type === 'agent') {
867
+ // Extract name from agent content
868
+ const nameMatch = blueprint.content.match(/name:\s*([a-z0-9-]+)/);
869
+ const name = nameMatch ? nameMatch[1] : blueprint.id.replace('agent-', '');
870
+ const result = await agentsService.createAgent(name, blueprint.content);
871
+ if (!result.success) {
872
+ return c.json({ error: result.error, code: 'FAILED' }, 400);
873
+ }
874
+ return c.json({ success: true, type: 'agent', name });
875
+ }
876
+ else {
877
+ // Team blueprint
878
+ const config = JSON.parse(blueprint.content);
879
+ const result = await teamsService.createTeam(config.name, config);
880
+ if (!result.success) {
881
+ return c.json({ error: result.error, code: 'FAILED' }, 400);
882
+ }
883
+ return c.json({ success: true, type: 'team', name: config.name });
884
+ }
885
+ });
886
+ // ============================================================================
887
+ // Insight Progress - SSE for real-time updates
888
+ // ============================================================================
889
+ app.get('/api/insights/:sessionId/progress', async (c) => {
890
+ const sessionId = c.req.param('sessionId');
891
+ const progress = insightsService.getProgress(sessionId);
892
+ return c.json({ progress });
893
+ });
894
+ // ============================================================================
895
+ // AI Advisor - Proactive Intelligence
896
+ // ============================================================================
897
+ app.get('/api/advisor/recommendations', async (c) => {
898
+ const all = c.req.query('all') === 'true';
899
+ const recommendations = all
900
+ ? await advisorService.getAllRecommendations()
901
+ : await advisorService.getRecommendations();
902
+ return c.json({ recommendations });
903
+ });
904
+ app.get('/api/advisor/stats', async (c) => {
905
+ const state = await advisorService.getState();
906
+ const recs = state.recommendations;
907
+ const stats = {
908
+ pending: recs.filter(r => r.status === 'pending').length,
909
+ applied: recs.filter(r => r.status === 'applied').length,
910
+ dismissed: recs.filter(r => r.status === 'dismissed').length,
911
+ lastAnalyzed: state.lastAnalyzedAt,
912
+ byType: {
913
+ skill: recs.filter(r => r.type === 'skill' && r.status === 'pending').length,
914
+ agent: recs.filter(r => r.type === 'agent' && r.status === 'pending').length,
915
+ hook: recs.filter(r => r.type === 'hook' && r.status === 'pending').length,
916
+ 'claude-md': recs.filter(r => r.type === 'claude-md' && r.status === 'pending').length,
917
+ 'mcp-server': recs.filter(r => r.type === 'mcp-server' && r.status === 'pending').length,
918
+ settings: recs.filter(r => r.type === 'settings' && r.status === 'pending').length,
919
+ model: recs.filter(r => r.type === 'model' && r.status === 'pending').length,
920
+ },
921
+ };
922
+ return c.json({ stats });
923
+ });
924
+ app.post('/api/advisor/analyze', async (c) => {
925
+ const body = await c.req.json().catch(() => ({ force: false }));
926
+ // Use AI-powered analysis for better recommendation quality
927
+ const result = await advisorService.runAnalysisWithAI(body.force ?? false);
928
+ return c.json(result);
929
+ });
930
+ app.post('/api/advisor/recommendations/:id/apply', async (c) => {
931
+ const result = await advisorService.applyRecommendation(c.req.param('id'));
932
+ if (!result.success) {
933
+ return c.json({ error: result.error, code: 'FAILED' }, 400);
934
+ }
935
+ return c.json({ success: true });
936
+ });
937
+ app.post('/api/advisor/recommendations/:id/dismiss', async (c) => {
938
+ await advisorService.dismissRecommendation(c.req.param('id'));
939
+ return c.json({ success: true });
940
+ });
941
+ // ============================================================================
942
+ // Project Intelligence - Cross-Session Project Analysis
943
+ // ============================================================================
944
+ app.get('/api/intelligence', async (c) => {
945
+ const summaries = await projectIntelligenceService.listProjectIntelligence();
946
+ return c.json({ projects: summaries });
947
+ });
948
+ app.get('/api/intelligence/global', async (c) => {
949
+ const forceRefresh = c.req.query('refresh') === 'true';
950
+ const intelligence = await projectIntelligenceService.getGlobalIntelligence(forceRefresh);
951
+ return c.json({ intelligence });
952
+ });
953
+ app.get('/api/intelligence/project/:id', async (c) => {
954
+ const projectPath = validateProjectPath(c.req.param('id'));
955
+ if (!projectPath) {
956
+ return c.json({ error: 'Invalid project path', code: 'INVALID' }, 400);
957
+ }
958
+ const intelligence = await projectIntelligenceService.getProjectIntelligence(projectPath);
959
+ if (!intelligence) {
960
+ return c.json({ error: 'Intelligence not generated yet', code: 'NOT_FOUND' }, 404);
961
+ }
962
+ return c.json({ intelligence });
963
+ });
964
+ app.post('/api/intelligence/project/:id/generate', async (c) => {
965
+ const projectPath = validateProjectPath(c.req.param('id'));
966
+ if (!projectPath) {
967
+ return c.json({ error: 'Invalid project path', code: 'INVALID' }, 400);
968
+ }
969
+ const body = await c.req.json().catch(() => ({ force: false }));
970
+ const intelligence = await projectIntelligenceService.generateProjectIntelligence(projectPath, body.force);
971
+ return c.json({ intelligence });
972
+ });
973
+ app.post('/api/intelligence/generate-all', async (c) => {
974
+ const body = await c.req.json().catch(() => ({ force: false }));
975
+ const projects = await sessionService.listProjects();
976
+ // Generate for all projects (limited to prevent overload)
977
+ const results = [];
978
+ for (const project of projects.slice(0, 10)) {
979
+ const intelligence = await projectIntelligenceService.generateProjectIntelligence(project.path, body.force);
980
+ results.push({
981
+ projectPath: project.path,
982
+ projectName: project.name,
983
+ status: intelligence.status,
984
+ });
985
+ }
986
+ return c.json({ results });
987
+ });
988
+ // ============================================================================
989
+ // Terminal - Claude Code Integration
990
+ // ============================================================================
991
+ app.get('/api/terminal/sessions', (c) => {
992
+ const sessions = terminalService.listSessions();
993
+ return c.json({ sessions });
994
+ });
995
+ app.post('/api/terminal/sessions', async (c) => {
996
+ const body = await c.req.json();
997
+ try {
998
+ const sessionId = await terminalService.createSession({
999
+ workingDir: body.workingDir ?? process.cwd(),
1000
+ cols: body.cols,
1001
+ rows: body.rows,
1002
+ });
1003
+ return c.json({ sessionId });
1004
+ }
1005
+ catch (err) {
1006
+ return c.json({ error: err.message, code: 'FAILED' }, 500);
1007
+ }
1008
+ });
1009
+ app.post('/api/terminal/claude', async (c) => {
1010
+ const body = await c.req.json();
1011
+ try {
1012
+ const sessionId = await terminalService.startClaudeCode({
1013
+ workingDir: body.workingDir ?? process.cwd(),
1014
+ resumeSessionId: body.resumeSessionId,
1015
+ prompt: body.prompt,
1016
+ cols: body.cols,
1017
+ rows: body.rows,
1018
+ });
1019
+ return c.json({ sessionId });
1020
+ }
1021
+ catch (err) {
1022
+ return c.json({ error: err.message, code: 'FAILED' }, 500);
1023
+ }
1024
+ });
1025
+ app.post('/api/terminal/sessions/:id/write', async (c) => {
1026
+ const body = await c.req.json();
1027
+ const success = terminalService.write(c.req.param('id'), body.data);
1028
+ if (!success) {
1029
+ return c.json({ error: 'Session not found or closed', code: 'NOT_FOUND' }, 404);
1030
+ }
1031
+ return c.json({ success: true });
1032
+ });
1033
+ app.post('/api/terminal/sessions/:id/resize', async (c) => {
1034
+ const body = await c.req.json();
1035
+ const success = terminalService.resize(c.req.param('id'), body.cols, body.rows);
1036
+ if (!success) {
1037
+ return c.json({ error: 'Session not found or closed', code: 'NOT_FOUND' }, 404);
1038
+ }
1039
+ return c.json({ success: true });
1040
+ });
1041
+ app.post('/api/terminal/sessions/:id/interrupt', (c) => {
1042
+ const success = terminalService.interrupt(c.req.param('id'));
1043
+ if (!success) {
1044
+ return c.json({ error: 'Session not found or closed', code: 'NOT_FOUND' }, 404);
1045
+ }
1046
+ return c.json({ success: true });
1047
+ });
1048
+ app.delete('/api/terminal/sessions/:id', (c) => {
1049
+ const success = terminalService.destroySession(c.req.param('id'));
1050
+ if (!success) {
1051
+ return c.json({ error: 'Session not found', code: 'NOT_FOUND' }, 404);
1052
+ }
1053
+ return c.json({ success: true });
1054
+ });
1055
+ // ============================================================================
1056
+ // Server Setup
1057
+ // ============================================================================
1058
+ const uiRoot = resolveUiRoot();
1059
+ let server = null;
1060
+ let wss = null;
1061
+ // WebSocket client management
1062
+ const terminalClients = new Map();
1063
+ const setupTerminalWebSocket = (wsServer) => {
1064
+ // Forward terminal events to connected WebSocket clients
1065
+ const forwardToClients = (msg) => {
1066
+ const clients = terminalClients.get(msg.sessionId);
1067
+ if (!clients)
1068
+ return;
1069
+ const data = JSON.stringify(msg);
1070
+ for (const ws of clients) {
1071
+ if (ws.readyState === WebSocket.OPEN) {
1072
+ ws.send(data);
1073
+ }
1074
+ }
1075
+ };
1076
+ terminalService.on('output', forwardToClients);
1077
+ terminalService.on('exit', forwardToClients);
1078
+ terminalService.on('resize', forwardToClients);
1079
+ terminalService.on('status', forwardToClients);
1080
+ wsServer.on('connection', (ws, req) => {
1081
+ const url = new URL(req.url ?? '/', `http://${req.headers.host}`);
1082
+ const sessionId = url.searchParams.get('sessionId');
1083
+ if (!sessionId) {
1084
+ ws.close(4000, 'Missing sessionId parameter');
1085
+ return;
1086
+ }
1087
+ // Add client to session's client set
1088
+ if (!terminalClients.has(sessionId)) {
1089
+ terminalClients.set(sessionId, new Set());
1090
+ }
1091
+ terminalClients.get(sessionId).add(ws);
1092
+ // Send current session status
1093
+ const session = terminalService.getSession(sessionId);
1094
+ if (session) {
1095
+ ws.send(JSON.stringify({
1096
+ type: 'status',
1097
+ sessionId,
1098
+ status: session.status,
1099
+ }));
1100
+ }
1101
+ // Handle incoming messages
1102
+ ws.on('message', (rawData) => {
1103
+ try {
1104
+ const msg = JSON.parse(rawData.toString());
1105
+ switch (msg.type) {
1106
+ case 'input':
1107
+ if (msg.data) {
1108
+ terminalService.write(sessionId, msg.data);
1109
+ }
1110
+ break;
1111
+ case 'resize':
1112
+ if (msg.cols && msg.rows) {
1113
+ terminalService.resize(sessionId, msg.cols, msg.rows);
1114
+ }
1115
+ break;
1116
+ case 'interrupt':
1117
+ terminalService.interrupt(sessionId);
1118
+ break;
1119
+ }
1120
+ }
1121
+ catch {
1122
+ // Ignore malformed messages
1123
+ }
1124
+ });
1125
+ // Cleanup on close
1126
+ ws.on('close', () => {
1127
+ const clients = terminalClients.get(sessionId);
1128
+ if (clients) {
1129
+ clients.delete(ws);
1130
+ if (clients.size === 0) {
1131
+ terminalClients.delete(sessionId);
1132
+ }
1133
+ }
1134
+ });
1135
+ });
1136
+ };
1137
+ return {
1138
+ async start() {
1139
+ return new Promise((resolve, reject) => {
1140
+ server = http.createServer(async (req, res) => {
1141
+ if (uiRoot && serveStatic(req, res, uiRoot))
1142
+ return;
1143
+ const url = new URL(req.url ?? '/', `http://${req.headers.host}`);
1144
+ const headers = new Headers();
1145
+ for (const [k, v] of Object.entries(req.headers)) {
1146
+ if (v)
1147
+ headers.set(k, Array.isArray(v) ? v[0] : v);
1148
+ }
1149
+ let body;
1150
+ if (req.method !== 'GET' && req.method !== 'HEAD') {
1151
+ const chunks = [];
1152
+ for await (const chunk of req)
1153
+ chunks.push(chunk);
1154
+ body = Buffer.concat(chunks);
1155
+ }
1156
+ try {
1157
+ const fetchReq = new Request(url.toString(), {
1158
+ method: req.method,
1159
+ headers,
1160
+ body,
1161
+ });
1162
+ const fetchRes = await app.fetch(fetchReq);
1163
+ res.statusCode = fetchRes.status;
1164
+ fetchRes.headers.forEach((v, k) => res.setHeader(k, v));
1165
+ res.end(Buffer.from(await fetchRes.arrayBuffer()));
1166
+ }
1167
+ catch {
1168
+ res.statusCode = 500;
1169
+ res.end(JSON.stringify({ error: 'Internal error' }));
1170
+ }
1171
+ });
1172
+ // Setup WebSocket server for terminal
1173
+ wss = new WebSocketServer({ server, path: '/api/terminal/ws' });
1174
+ setupTerminalWebSocket(wss);
1175
+ server.on('error', reject);
1176
+ server.listen(port, host, () => {
1177
+ console.log(`claudecto server running at http://${host}:${port}`);
1178
+ console.log(`WebSocket terminal available at ws://${host}:${port}/api/terminal/ws`);
1179
+ if (!uiRoot) {
1180
+ console.log("UI not found. Run 'npm run build:ui' to build it.");
1181
+ }
1182
+ resolve();
1183
+ });
1184
+ });
1185
+ },
1186
+ async stop() {
1187
+ return new Promise((resolve) => {
1188
+ // Cleanup terminal sessions
1189
+ terminalService.destroyAll();
1190
+ // Close WebSocket server
1191
+ if (wss) {
1192
+ wss.close();
1193
+ }
1194
+ if (server) {
1195
+ server.close(() => resolve());
1196
+ }
1197
+ else {
1198
+ resolve();
1199
+ }
1200
+ });
1201
+ },
1202
+ getUrl() {
1203
+ return `http://${host === '0.0.0.0' ? 'localhost' : host}:${port}`;
1204
+ },
1205
+ };
1206
+ }
1207
+ //# sourceMappingURL=index.js.map