codehere 0.5.0-alpha.4 → 0.5.0-alpha.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.
@@ -25,9 +25,13 @@ import { fileURLToPath } from 'url';
25
25
  import { collectFilesForMCP, scoreFileForMCP } from './mcp-helpers.js';
26
26
  import { scanPayload, calculateRiskScore, AUDIT_FILE } from '../hooks/claude-sdk-hook.js';
27
27
  import { getVersion } from '../../utils/version.js';
28
- import { TASK_HTML, HISTORY_HTML } from './ui-page.js';
28
+ import { TASK_HTML, HISTORY_HTML, THREAD_HTML } from './ui-page.js';
29
29
  import { SessionManager, availableAgents, readinessForAllAgents, getAdapter, allAdapters, } from '../orchestration/session-manager.js';
30
30
  import { isAbsolute } from 'path';
31
+ import { readCrossToolAgents } from '../storage/cross-tool-agents.js';
32
+ import { listProjects, readProjectThread } from '../storage/project-thread-reader.js';
33
+ import { mirrorReplyToThread } from '../storage/project-thread-mirror.js';
34
+ import { randomUUID } from 'crypto';
31
35
  import { installAgent } from '../orchestration/agent-installer.js';
32
36
  import { detectReadiness } from '../orchestration/readiness.js';
33
37
  // ── Tool definitions ─────────────────────────────────────────────────────────
@@ -81,6 +85,28 @@ const TOOLS = [
81
85
  // ── Tool handlers ────────────────────────────────────────────────────────────
82
86
  import { homedir } from 'os';
83
87
  const AUDIT_FILE_PATH = join(homedir(), '.codehere', 'audit', 'hook-events.jsonl');
88
+ /**
89
+ * Dev-only behavior-observer injection (v0.6.0 — elu joined the team roster
90
+ * 2026-05-03 as behavior reviewer). Activated only when CODEHERE_DEV_ELU=1
91
+ * is set in the founder's local env; never in shipped releases. The script's
92
+ * public key is meant for client-side embedding (analytics tracking key, not
93
+ * a secret) so committing the URL is safe.
94
+ *
95
+ * Why runtime injection instead of editing TASK_HTML/HISTORY_HTML directly:
96
+ * the templates stay identical between dev and release builds. The only
97
+ * delta is the env var. This keeps the release surface clean (zero-telemetry
98
+ * value per PRD §1.5 #2 stays intact) while letting elu observe founder
99
+ * dogfood sessions.
100
+ *
101
+ * Removal trigger lives in team_roster.md memory: if elu produces zero
102
+ * unique findings within one month, remove this helper + roster entry.
103
+ */
104
+ const ELU_DEV_SCRIPT = '<script async src="https://elu.dev/v1/elu_pk_live_HrKo9yFFLx03tz9xuSKFcE2Jpv.js"></script>';
105
+ function withDevElu(html) {
106
+ if (process.env.CODEHERE_DEV_ELU !== '1')
107
+ return html;
108
+ return html.replace('</head>', `${ELU_DEV_SCRIPT}\n</head>`);
109
+ }
84
110
  function handleTrust(params) {
85
111
  const { filepath, code, verbose } = params;
86
112
  let content;
@@ -270,13 +296,19 @@ export function startMCPServer(options = {}) {
270
296
  // v0.3 primary UI: task intake + active sessions
271
297
  if (req.method === 'GET' && (req.url === '/' || req.url === '/index.html')) {
272
298
  res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
273
- res.end(TASK_HTML);
299
+ res.end(withDevElu(TASK_HTML));
274
300
  return;
275
301
  }
276
302
  // History / audit feed (what we built Day 0; now secondary)
277
303
  if (req.method === 'GET' && (req.url === '/history' || req.url === '/history/')) {
278
304
  res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
279
- res.end(HISTORY_HTML);
305
+ res.end(withDevElu(HISTORY_HTML));
306
+ return;
307
+ }
308
+ // v0.6.0 M1 — project thread view; consumes /api/projects + /api/projects/:slug
309
+ if (req.method === 'GET' && (req.url === '/threads' || req.url === '/threads/' || req.url?.startsWith('/threads?'))) {
310
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
311
+ res.end(withDevElu(THREAD_HTML));
280
312
  return;
281
313
  }
282
314
  // Backwards-compat: /ui used to be the Mission Control audit view — now at /history
@@ -453,8 +485,28 @@ export function startMCPServer(options = {}) {
453
485
  const all = readinessForAllAgents();
454
486
  const auto = all.find((a) => a.id === 'conductor');
455
487
  const agents = all.filter((a) => a.id !== 'conductor');
488
+ // v0.6.0 M8 — surface cross-tool agent definitions (.opencode/agents/,
489
+ // .claude/agents/, .codehere/agents/) alongside the built-in adapters.
490
+ // These don't have a `state` (ready/needs_install) — they're definitions
491
+ // not runtimes. UI can render them as "available specialists" tagged
492
+ // with their source format. Best-effort: failures fall back to no
493
+ // cross-tool agents listed.
494
+ let crossTool = [];
495
+ try {
496
+ const result = readCrossToolAgents(process.cwd());
497
+ crossTool = result.agents.map((a) => ({
498
+ name: a.name,
499
+ description: a.description.length > 240 ? a.description.slice(0, 237) + '…' : a.description,
500
+ sourceFormat: a.sourceFormat,
501
+ mode: a.mode,
502
+ sourcePath: a.sourcePath,
503
+ }));
504
+ }
505
+ catch {
506
+ // best-effort; ignore parse failures
507
+ }
456
508
  res.writeHead(200, { 'Content-Type': 'application/json' });
457
- res.end(JSON.stringify({ agents, auto }));
509
+ res.end(JSON.stringify({ agents, auto, crossTool }));
458
510
  }
459
511
  catch (e) {
460
512
  res.writeHead(500, { 'Content-Type': 'application/json' });
@@ -475,6 +527,49 @@ export function startMCPServer(options = {}) {
475
527
  }
476
528
  return;
477
529
  }
530
+ // ── v0.6.0 M1 project thread routes ──────────────────────────────────────
531
+ // GET /api/projects — list all known projects (slug, cwd, title, taskCount, lastActivityAt)
532
+ // GET /api/projects/:slug — single project thread (meta + tasks merged by sessionId)
533
+ // Reader source of truth: ~/.codehere/projects/<slug>/thread.jsonl (per-project append-only log
534
+ // populated by migration + forward-mirror). Drives the new thread-view UI.
535
+ if (req.method === 'GET' && req.url === '/api/projects') {
536
+ try {
537
+ const projects = listProjects();
538
+ res.writeHead(200, { 'Content-Type': 'application/json' });
539
+ res.end(JSON.stringify({ projects }));
540
+ }
541
+ catch (e) {
542
+ res.writeHead(500, { 'Content-Type': 'application/json' });
543
+ res.end(JSON.stringify({ error: 'list_projects_failed', detail: String(e) }));
544
+ }
545
+ return;
546
+ }
547
+ if (req.method === 'GET' && req.url?.startsWith('/api/projects/')) {
548
+ try {
549
+ // Slug is the segment after /api/projects/. Strip optional query string.
550
+ const slug = req.url.replace('/api/projects/', '').split('?')[0];
551
+ // Slug shape is sha1[:12] (12 hex chars) OR the literal 'unknown' bucket.
552
+ // Reject anything else to keep the surface tight (and stop ../ traversal).
553
+ if (!/^([a-f0-9]{12}|unknown)$/.test(slug)) {
554
+ res.writeHead(400, { 'Content-Type': 'application/json' });
555
+ res.end(JSON.stringify({ error: 'invalid_slug' }));
556
+ return;
557
+ }
558
+ const thread = readProjectThread(slug);
559
+ if (!thread) {
560
+ res.writeHead(404, { 'Content-Type': 'application/json' });
561
+ res.end(JSON.stringify({ error: 'project_not_found', slug }));
562
+ return;
563
+ }
564
+ res.writeHead(200, { 'Content-Type': 'application/json' });
565
+ res.end(JSON.stringify({ thread }));
566
+ }
567
+ catch (e) {
568
+ res.writeHead(500, { 'Content-Type': 'application/json' });
569
+ res.end(JSON.stringify({ error: 'read_project_failed', detail: String(e) }));
570
+ }
571
+ return;
572
+ }
478
573
  // ── F1 task orchestration routes ─────────────────────────────────────────
479
574
  // POST /api/task — { prompt, cwd? } → { sessionId }
480
575
  // GET /api/tasks — list all sessions
@@ -560,6 +655,7 @@ export function startMCPServer(options = {}) {
560
655
  // in completedAtBySession map (populated when exit event fires).
561
656
  const completedAt = manager.getCompletedAt(s.sessionId);
562
657
  const finalReport = manager.getFinalReport(s.sessionId);
658
+ const cwd = manager.getCwd(s.sessionId);
563
659
  return {
564
660
  sessionId: s.sessionId,
565
661
  prompt: s.prompt,
@@ -573,6 +669,10 @@ export function startMCPServer(options = {}) {
573
669
  // Iter 12 — surface canAppendMessage so the UI knows whether to
574
670
  // render the reply input or a disabled "follow-up unsupported" state.
575
671
  canAppendMessage: s.canAppendMessage === true,
672
+ // v0.6.x Slack-shape spike — surface cwd so the dashboard's
673
+ // "Continue thread" intake button can find the most recent
674
+ // in-memory session for the current folder.
675
+ ...(cwd !== undefined ? { cwd } : {}),
576
676
  provenance: 'spawned',
577
677
  };
578
678
  });
@@ -650,7 +750,32 @@ export function startMCPServer(options = {}) {
650
750
  res.end(JSON.stringify({ error: 'text is required' }));
651
751
  return;
652
752
  }
753
+ const replyStartedAt = Date.now();
653
754
  const out = await session.appendMessage(text);
755
+ const replyCompletedAt = Date.now();
756
+ // v0.6.x — forward-mirror the reply turn into the project thread
757
+ // so /threads sees follow-ups as visible chronological turns
758
+ // (not just the original spawn). Best-effort: mirror failures
759
+ // never break the reply path.
760
+ try {
761
+ const replyCwd = manager.getCwd(sessionId);
762
+ if (replyCwd) {
763
+ mirrorReplyToThread({
764
+ parentSessionId: sessionId,
765
+ replySessionId: randomUUID(),
766
+ agent: session.agent,
767
+ prompt: text,
768
+ cwd: replyCwd,
769
+ startedAt: replyStartedAt,
770
+ completedAt: replyCompletedAt,
771
+ durationMs: out.durationMs,
772
+ result: out.result,
773
+ });
774
+ }
775
+ }
776
+ catch {
777
+ /* mirror is best-effort */
778
+ }
654
779
  res.writeHead(200, { 'Content-Type': 'application/json' });
655
780
  res.end(JSON.stringify({ sessionId, ...out }));
656
781
  }