codehere 0.6.0-alpha.1 → 0.6.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.
@@ -26,6 +26,7 @@ import { collectFilesForMCP, scoreFileForMCP } from './mcp-helpers.js';
26
26
  import { scanPayload, calculateRiskScore, AUDIT_FILE, verifyAuditChain, } from '../hooks/claude-sdk-hook.js';
27
27
  import { getVersion } from '../../utils/version.js';
28
28
  import { TASK_HTML, HISTORY_HTML, THREAD_HTML } from './ui-page.js';
29
+ import { TASK_HTML_V2 } from './ui-page-v2.js';
29
30
  import { SessionManager, availableAgents, readinessForAllAgents, getAdapter, allAdapters, } from '../orchestration/session-manager.js';
30
31
  import { isAbsolute } from 'path';
31
32
  import { readCrossToolAgents } from '../storage/cross-tool-agents.js';
@@ -319,8 +320,14 @@ export function startMCPServer(options = {}) {
319
320
  res.end(JSON.stringify({ status: 'ok', server: 'codehere-mcp', version: getVersion(), tools: TOOLS.length }));
320
321
  return;
321
322
  }
322
- // v0.3 primary UI: task intake + active sessions
323
+ // v2 Task-Centric PM UI (primary — served at /)
323
324
  if (req.method === 'GET' && (req.url === '/' || req.url === '/index.html')) {
325
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
326
+ res.end(withDevElu(TASK_HTML_V2));
327
+ return;
328
+ }
329
+ // v1 dashboard (legacy — at /v1)
330
+ if (req.method === 'GET' && (req.url === '/v1' || req.url === '/v1/')) {
324
331
  res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
325
332
  res.end(withDevElu(TASK_HTML));
326
333
  return;
@@ -394,6 +401,58 @@ export function startMCPServer(options = {}) {
394
401
  });
395
402
  return;
396
403
  }
404
+ // ── P4 SSE — session lifecycle events (realtime /v2 updates) ────────────
405
+ // Tails sessions.jsonl and pushes "change" events so the /v2 UI can
406
+ // re-fetch task state instantly instead of waiting 8s for the next poll.
407
+ // Same pattern as /events (audit log tail) but for sessions.
408
+ const SESSIONS_FILE = join(homedir(), '.codehere', 'sessions.jsonl');
409
+ if (req.method === 'GET' && req.url === '/api/events/sessions') {
410
+ res.writeHead(200, {
411
+ 'Content-Type': 'text/event-stream',
412
+ 'Cache-Control': 'no-cache',
413
+ Connection: 'keep-alive',
414
+ });
415
+ // Send current session state on connect
416
+ try {
417
+ const manager = SessionManager.getInstance();
418
+ const sessions = manager.list().map(s => ({
419
+ sessionId: s.sessionId,
420
+ status: s.status,
421
+ agent: s.agent,
422
+ }));
423
+ res.write(`data: ${JSON.stringify({ type: 'init', sessions })}\n\n`);
424
+ }
425
+ catch {
426
+ res.write(`data: ${JSON.stringify({ type: 'init', sessions: [] })}\n\n`);
427
+ }
428
+ // Tail sessions.jsonl for changes
429
+ let lastSize = 0;
430
+ if (existsSync(SESSIONS_FILE)) {
431
+ try {
432
+ lastSize = statSync(SESSIONS_FILE).size;
433
+ }
434
+ catch { /* first time */ }
435
+ }
436
+ const onChange = () => {
437
+ try {
438
+ if (!existsSync(SESSIONS_FILE))
439
+ return;
440
+ const currentSize = statSync(SESSIONS_FILE).size;
441
+ if (currentSize <= lastSize)
442
+ return;
443
+ lastSize = currentSize;
444
+ res.write(`data: ${JSON.stringify({ type: 'change' })}\n\n`);
445
+ }
446
+ catch { /* transient */ }
447
+ };
448
+ watchFile(SESSIONS_FILE, { interval: 500 }, onChange);
449
+ const ping = setInterval(() => res.write(': ping\n\n'), 15000);
450
+ req.on('close', () => {
451
+ clearInterval(ping);
452
+ unwatchFile(SESSIONS_FILE, onChange);
453
+ });
454
+ return;
455
+ }
397
456
  // ── F2 agent registry ─────────────────────────────────────────────────────
398
457
  if (req.method === 'GET' && req.url === '/api/agents') {
399
458
  res.writeHead(200, { 'Content-Type': 'application/json' });
@@ -1013,13 +1072,12 @@ export function startMCPServer(options = {}) {
1013
1072
  }
1014
1073
  if (req.method === 'GET' && req.url === '/api/tasks') {
1015
1074
  const spawned = manager.list().map((s) => {
1016
- // Bug 2 sister-fix (May 2): include completedAt for in-memory exited
1017
- // sessions so the UI can compute duration. TaskSession interface
1018
- // intentionally lacks completedAt; SessionManager tracks it separately
1019
- // in completedAtBySession map (populated when exit event fires).
1020
1075
  const completedAt = manager.getCompletedAt(s.sessionId);
1021
1076
  const finalReport = manager.getFinalReport(s.sessionId);
1022
1077
  const cwd = manager.getCwd(s.sessionId);
1078
+ const continuedFrom = manager.getContinuedFrom(s.sessionId);
1079
+ const continuedAs = manager.getContinuedAs(s.sessionId);
1080
+ const failedReport = manager.hasFailedReport(s.sessionId);
1023
1081
  return {
1024
1082
  sessionId: s.sessionId,
1025
1083
  prompt: s.prompt,
@@ -1030,13 +1088,11 @@ export function startMCPServer(options = {}) {
1030
1088
  artifacts: s.artifacts,
1031
1089
  ...(completedAt !== undefined ? { completedAt, durationMs: completedAt - s.startedAt } : {}),
1032
1090
  ...(finalReport !== undefined ? { finalReport } : {}),
1033
- // Iter 12 — surface canAppendMessage so the UI knows whether to
1034
- // render the reply input or a disabled "follow-up unsupported" state.
1091
+ ...(failedReport ? { failedReport: true } : {}),
1035
1092
  canAppendMessage: s.canAppendMessage === true,
1036
- // v0.6.x Slack-shape spike — surface cwd so the dashboard's
1037
- // "Continue thread" intake button can find the most recent
1038
- // in-memory session for the current folder.
1039
1093
  ...(cwd !== undefined ? { cwd } : {}),
1094
+ ...(continuedFrom !== undefined ? { continuedFrom } : {}),
1095
+ ...(continuedAs !== undefined ? { continuedAs } : {}),
1040
1096
  provenance: 'spawned',
1041
1097
  };
1042
1098
  });
@@ -1207,6 +1263,62 @@ export function startMCPServer(options = {}) {
1207
1263
  res.end(JSON.stringify({ error: 'Method not allowed for this path' }));
1208
1264
  return;
1209
1265
  }
1266
+ // v0.6.x P0 — GET /api/failover-offers
1267
+ // Returns active failover offers so the dashboard can surface a
1268
+ // "Claude rate-limited — switch to Codex?" banner. Offers expire
1269
+ // after 60 s (handled by SessionManager).
1270
+ if (req.method === 'GET' && req.url === '/api/failover-offers') {
1271
+ const offers = SessionManager.getInstance().getFailoverOffers();
1272
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1273
+ res.end(JSON.stringify({ offers }));
1274
+ return;
1275
+ }
1276
+ // v0.6.x P0 — POST /api/failover-accept
1277
+ // User clicked "Switch" on the failoffer banner. Spawns a
1278
+ // replacement session on the target agent, preserving the
1279
+ // original prompt + cwd.
1280
+ if (req.method === 'POST' && req.url === '/api/failover-accept') {
1281
+ let body = '';
1282
+ req.on('data', (c) => { body += c; });
1283
+ req.on('end', () => {
1284
+ let parsed;
1285
+ try {
1286
+ const raw = JSON.parse(body);
1287
+ if (raw === null || typeof raw !== 'object' || Array.isArray(raw)) {
1288
+ res.writeHead(400, { 'Content-Type': 'application/json' });
1289
+ res.end(JSON.stringify({ error: 'invalid_body' }));
1290
+ return;
1291
+ }
1292
+ parsed = raw;
1293
+ }
1294
+ catch {
1295
+ res.writeHead(400, { 'Content-Type': 'application/json' });
1296
+ res.end(JSON.stringify({ error: 'invalid_body' }));
1297
+ return;
1298
+ }
1299
+ if (typeof parsed.sessionId !== 'string' || typeof parsed.targetAgent !== 'string') {
1300
+ res.writeHead(400, { 'Content-Type': 'application/json' });
1301
+ res.end(JSON.stringify({ error: 'missing_sessionId_or_targetAgent' }));
1302
+ return;
1303
+ }
1304
+ try {
1305
+ const result = SessionManager.getInstance().acceptFailover(parsed.sessionId, parsed.targetAgent);
1306
+ if (result) {
1307
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1308
+ res.end(JSON.stringify({ ok: true, newSessionId: result.sessionId }));
1309
+ }
1310
+ else {
1311
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1312
+ res.end(JSON.stringify({ ok: false, error: 'target_unavailable', detail: `Could not spawn ${parsed.targetAgent}. Check if it's installed and authenticated.` }));
1313
+ }
1314
+ }
1315
+ catch (e) {
1316
+ res.writeHead(500, { 'Content-Type': 'application/json' });
1317
+ res.end(JSON.stringify({ error: 'failover_failed', detail: String(e) }));
1318
+ }
1319
+ });
1320
+ return;
1321
+ }
1210
1322
  // MCP endpoint — POST jsonrpc; reject other verbs with 405 + Allow header
1211
1323
  // (rc.17: was 404, which is wrong HTTP semantic — UX_OBSERVATIONS P3).
1212
1324
  if (req.url === '/mcp') {