heyiam 0.2.29 → 0.3.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 (177) hide show
  1. package/README.md +45 -0
  2. package/dist/config.js +10 -1
  3. package/dist/db.js +1 -2
  4. package/dist/export.js +40 -25
  5. package/dist/format-utils.js +5 -0
  6. package/dist/index.js +168 -0
  7. package/dist/mount.js +300 -102
  8. package/dist/parsers/claude.js +2 -28
  9. package/dist/parsers/codex.js +2 -26
  10. package/dist/parsers/cursor.js +2 -26
  11. package/dist/parsers/duration.js +35 -0
  12. package/dist/parsers/gemini.js +2 -20
  13. package/dist/parsers/types.js +0 -1
  14. package/dist/public/assets/index-BZ65TU_Y.js +40 -0
  15. package/dist/public/assets/index-CqCaW2cb.css +1 -0
  16. package/dist/public/index.html +2 -2
  17. package/dist/redact.js +4 -104
  18. package/dist/render/build-render-data.js +9 -2
  19. package/dist/render/index.js +32 -5
  20. package/dist/render/liquid.js +147 -7
  21. package/dist/render/mock-data.js +303 -0
  22. package/dist/render/templates/aurora/portfolio.liquid +204 -0
  23. package/dist/render/templates/aurora/project.liquid +260 -0
  24. package/dist/render/templates/aurora/session.liquid +223 -0
  25. package/dist/render/templates/aurora/styles.css +1178 -0
  26. package/dist/render/templates/bauhaus/portfolio.liquid +179 -0
  27. package/dist/render/templates/bauhaus/project.liquid +300 -0
  28. package/dist/render/templates/bauhaus/session.liquid +333 -0
  29. package/dist/render/templates/bauhaus/styles.css +1641 -0
  30. package/dist/render/templates/blueprint/portfolio.liquid +167 -0
  31. package/dist/render/templates/blueprint/project.liquid +286 -0
  32. package/dist/render/templates/blueprint/session.liquid +248 -0
  33. package/dist/render/templates/blueprint/styles.css +1285 -0
  34. package/dist/render/templates/canvas/portfolio.liquid +215 -0
  35. package/dist/render/templates/canvas/project.liquid +235 -0
  36. package/dist/render/templates/canvas/session.liquid +223 -0
  37. package/dist/render/templates/canvas/styles.css +1436 -0
  38. package/dist/render/templates/carbon/portfolio.liquid +170 -0
  39. package/dist/render/templates/carbon/project.liquid +249 -0
  40. package/dist/render/templates/carbon/session.liquid +190 -0
  41. package/dist/render/templates/carbon/styles.css +1091 -0
  42. package/dist/render/templates/chalk/portfolio.liquid +199 -0
  43. package/dist/render/templates/chalk/project.liquid +245 -0
  44. package/dist/render/templates/chalk/session.liquid +215 -0
  45. package/dist/render/templates/chalk/styles.css +1157 -0
  46. package/dist/render/templates/circuit/portfolio.liquid +162 -0
  47. package/dist/render/templates/circuit/project.liquid +247 -0
  48. package/dist/render/templates/circuit/session.liquid +205 -0
  49. package/dist/render/templates/circuit/styles.css +1403 -0
  50. package/dist/render/templates/cosmos/portfolio.liquid +232 -0
  51. package/dist/render/templates/cosmos/project.liquid +327 -0
  52. package/dist/render/templates/cosmos/session.liquid +239 -0
  53. package/dist/render/templates/cosmos/styles.css +1151 -0
  54. package/dist/render/templates/daylight/portfolio.liquid +217 -0
  55. package/dist/render/templates/daylight/project.liquid +229 -0
  56. package/dist/render/templates/daylight/session.liquid +219 -0
  57. package/dist/render/templates/daylight/styles.css +1311 -0
  58. package/dist/render/templates/editorial/portfolio.liquid +126 -0
  59. package/dist/render/templates/editorial/project.liquid +202 -0
  60. package/dist/render/templates/editorial/session.liquid +171 -0
  61. package/dist/render/templates/editorial/styles.css +822 -0
  62. package/dist/render/templates/ember/portfolio.liquid +318 -0
  63. package/dist/render/templates/ember/project.liquid +232 -0
  64. package/dist/render/templates/ember/session.liquid +202 -0
  65. package/dist/render/templates/ember/styles.css +1283 -0
  66. package/dist/render/templates/glacier/portfolio.liquid +271 -0
  67. package/dist/render/templates/glacier/project.liquid +288 -0
  68. package/dist/render/templates/glacier/session.liquid +217 -0
  69. package/dist/render/templates/glacier/styles.css +1200 -0
  70. package/dist/render/templates/grid/portfolio.liquid +265 -0
  71. package/dist/render/templates/grid/project.liquid +306 -0
  72. package/dist/render/templates/grid/session.liquid +260 -0
  73. package/dist/render/templates/grid/styles.css +1441 -0
  74. package/dist/render/templates/kinetic/portfolio.liquid +170 -0
  75. package/dist/render/templates/kinetic/project.liquid +242 -0
  76. package/dist/render/templates/kinetic/session.liquid +228 -0
  77. package/dist/render/templates/kinetic/styles.css +944 -0
  78. package/dist/render/templates/meridian/portfolio.liquid +255 -0
  79. package/dist/render/templates/meridian/project.liquid +376 -0
  80. package/dist/render/templates/meridian/session.liquid +298 -0
  81. package/dist/render/templates/meridian/styles.css +1369 -0
  82. package/dist/render/templates/minimal/portfolio.liquid +71 -0
  83. package/dist/render/templates/minimal/project.liquid +154 -0
  84. package/dist/render/templates/minimal/session.liquid +140 -0
  85. package/dist/render/templates/minimal/styles.css +525 -0
  86. package/dist/render/templates/mono/portfolio.liquid +291 -0
  87. package/dist/render/templates/mono/project.liquid +275 -0
  88. package/dist/render/templates/mono/session.liquid +276 -0
  89. package/dist/render/templates/mono/styles.css +1016 -0
  90. package/dist/render/templates/neon/portfolio.liquid +217 -0
  91. package/dist/render/templates/neon/project.liquid +225 -0
  92. package/dist/render/templates/neon/session.liquid +195 -0
  93. package/dist/render/templates/neon/styles.css +1265 -0
  94. package/dist/render/templates/noir/portfolio.liquid +137 -0
  95. package/dist/render/templates/noir/project.liquid +220 -0
  96. package/dist/render/templates/noir/session.liquid +241 -0
  97. package/dist/render/templates/noir/styles.css +1223 -0
  98. package/dist/render/templates/obsidian/portfolio.liquid +257 -0
  99. package/dist/render/templates/obsidian/project.liquid +280 -0
  100. package/dist/render/templates/obsidian/session.liquid +241 -0
  101. package/dist/render/templates/obsidian/styles.css +1401 -0
  102. package/dist/render/templates/paper/portfolio.liquid +267 -0
  103. package/dist/render/templates/paper/project.liquid +235 -0
  104. package/dist/render/templates/paper/session.liquid +271 -0
  105. package/dist/render/templates/paper/styles.css +1509 -0
  106. package/dist/render/templates/parallax/portfolio.liquid +305 -0
  107. package/dist/render/templates/parallax/project.liquid +275 -0
  108. package/dist/render/templates/parallax/session.liquid +295 -0
  109. package/dist/render/templates/parallax/styles.css +1874 -0
  110. package/dist/render/templates/parchment/portfolio.liquid +290 -0
  111. package/dist/render/templates/parchment/project.liquid +289 -0
  112. package/dist/render/templates/parchment/session.liquid +346 -0
  113. package/dist/render/templates/parchment/styles.css +1397 -0
  114. package/dist/render/templates/partials/_beats.liquid +16 -0
  115. package/dist/render/templates/partials/_breadcrumb.liquid +9 -0
  116. package/dist/render/templates/partials/_footer.liquid +7 -0
  117. package/dist/render/templates/partials/_growth-chart.liquid +7 -0
  118. package/dist/render/templates/partials/_key-decisions.liquid +20 -0
  119. package/dist/render/templates/partials/_links.liquid +16 -0
  120. package/dist/render/templates/partials/_narrative.liquid +8 -0
  121. package/dist/render/templates/partials/_phases.liquid +20 -0
  122. package/dist/render/templates/partials/_portfolio-header.liquid +20 -0
  123. package/dist/render/templates/partials/_portfolio-projects.liquid +16 -0
  124. package/dist/render/templates/partials/_portfolio-stats.liquid +19 -0
  125. package/dist/render/templates/partials/_qa.liquid +13 -0
  126. package/dist/render/templates/partials/_screenshot.liquid +15 -0
  127. package/dist/render/templates/partials/_session-cards.liquid +30 -0
  128. package/dist/render/templates/partials/_session-header.liquid +39 -0
  129. package/dist/render/templates/partials/_session-sidebar.liquid +30 -0
  130. package/dist/render/templates/partials/_skills.liquid +12 -0
  131. package/dist/render/templates/partials/_source-breakdown.liquid +22 -0
  132. package/dist/render/templates/partials/_stats.liquid +38 -0
  133. package/dist/render/templates/partials/_work-timeline.liquid +7 -0
  134. package/dist/render/templates/project.liquid +7 -4
  135. package/dist/render/templates/radar/portfolio.liquid +233 -0
  136. package/dist/render/templates/radar/project.liquid +278 -0
  137. package/dist/render/templates/radar/session.liquid +300 -0
  138. package/dist/render/templates/radar/styles.css +1049 -0
  139. package/dist/render/templates/showcase/portfolio.liquid +231 -0
  140. package/dist/render/templates/showcase/project.liquid +237 -0
  141. package/dist/render/templates/showcase/session.liquid +210 -0
  142. package/dist/render/templates/showcase/styles.css +1279 -0
  143. package/dist/render/templates/signal/portfolio.liquid +227 -0
  144. package/dist/render/templates/signal/project.liquid +278 -0
  145. package/dist/render/templates/signal/session.liquid +282 -0
  146. package/dist/render/templates/signal/styles.css +1395 -0
  147. package/dist/render/templates/strata/portfolio.liquid +192 -0
  148. package/dist/render/templates/strata/project.liquid +282 -0
  149. package/dist/render/templates/strata/session.liquid +261 -0
  150. package/dist/render/templates/strata/styles.css +1350 -0
  151. package/dist/render/templates/styles.css +1190 -0
  152. package/dist/render/templates/terminal/portfolio.liquid +118 -0
  153. package/dist/render/templates/terminal/project.liquid +161 -0
  154. package/dist/render/templates/terminal/session.liquid +145 -0
  155. package/dist/render/templates/terminal/styles.css +492 -0
  156. package/dist/render/templates/verdant/portfolio.liquid +333 -0
  157. package/dist/render/templates/verdant/project.liquid +309 -0
  158. package/dist/render/templates/verdant/session.liquid +237 -0
  159. package/dist/render/templates/verdant/styles.css +1257 -0
  160. package/dist/render/templates/zen/portfolio.liquid +136 -0
  161. package/dist/render/templates/zen/project.liquid +187 -0
  162. package/dist/render/templates/zen/session.liquid +203 -0
  163. package/dist/render/templates/zen/styles.css +1207 -0
  164. package/dist/render/templates.js +90 -0
  165. package/dist/routes/context.js +15 -10
  166. package/dist/routes/enhance.js +17 -40
  167. package/dist/routes/export.js +14 -4
  168. package/dist/routes/preview.js +480 -108
  169. package/dist/routes/projects.js +11 -19
  170. package/dist/routes/publish.js +15 -17
  171. package/dist/routes/settings.js +94 -1
  172. package/dist/routes/sse.js +9 -0
  173. package/dist/server.js +8 -2
  174. package/dist/settings.js +17 -9
  175. package/package.json +2 -4
  176. package/dist/public/assets/index-CC9G8EF1.js +0 -21
  177. package/dist/public/assets/index-Dalqz2mC.css +0 -1
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Template registry for heyi.am project/session rendering.
3
+ *
4
+ * Built-in templates ship with the CLI. Custom user templates
5
+ * can live in ~/.config/heyiam/templates/{name}/ (Phase 2).
6
+ */
7
+ import { readFileSync } from 'node:fs';
8
+ import { resolve, dirname } from 'node:path';
9
+ import { fileURLToPath } from 'node:url';
10
+ const __dirname = dirname(fileURLToPath(import.meta.url));
11
+ const TEMPLATES_DIR = resolve(__dirname, 'templates');
12
+ export const BUILT_IN_TEMPLATES = [
13
+ // Original 5
14
+ { name: 'editorial', label: 'Editorial', description: 'Classic light theme with card-based layout', accent: '#084471', mode: 'light', tags: [] },
15
+ { name: 'kinetic', label: 'Kinetic', description: 'Matches the heyi.am landing page — orange accent, section tags, narrative cards', accent: '#f97316', mode: 'dark', tags: ['animated'] },
16
+ { name: 'terminal', label: 'Terminal', description: 'Green-on-black terminal aesthetic with ASCII elements', accent: '#4ade80', mode: 'dark', tags: ['minimal'] },
17
+ { name: 'minimal', label: 'Typography', description: 'Ultra-clean light mode with serif typography', accent: '#1c1917', mode: 'light', tags: ['minimal'] },
18
+ { name: 'showcase', label: 'Showcase', description: 'Cinematic scroll animations with animated charts and stat counters', accent: '#818cf8', mode: 'dark', tags: ['animated'] },
19
+ // New templates
20
+ { name: 'parallax', label: 'Parallax', description: 'Fixed floating headshot with full-page parallax — content scrolls around the photo', accent: '#60a5fa', mode: 'dark', tags: ['animated'] },
21
+ { name: 'blueprint', label: 'Blueprint', description: 'Engineering schematic with SVG connector lines, grid background, and dimension annotations', accent: '#64748b', mode: 'light', tags: ['animated'] },
22
+ { name: 'radar', label: 'Radar', description: 'HUD cockpit with radar navigation widget and cyan-tinted luminous elements', accent: '#22d3ee', mode: 'dark', tags: ['animated', 'data-dense'] },
23
+ { name: 'strata', label: 'Strata', description: 'Depth-based parallax with overlapping card layers and warm amber palette', accent: '#d97706', mode: 'light', tags: ['animated'] },
24
+ { name: 'noir', label: 'Noir', description: 'Pure monochrome — black, white, gray only with bold typography and film noir drama', accent: '#e5e5e5', mode: 'dark', tags: ['minimal'] },
25
+ { name: 'verdant', label: 'Verdant', description: 'Nature-inspired with warm earthy palette, leaf motifs, and organic rounded shapes', accent: '#15803d', mode: 'light', tags: [] },
26
+ { name: 'neon', label: 'Neon', description: 'Synthwave aesthetic with pink and cyan dual accent and tasteful neon glow effects', accent: '#f472b6', mode: 'dark', tags: ['animated'] },
27
+ { name: 'paper', label: 'Paper', description: 'Newspaper print aesthetic with multi-column layout, drop caps, and serif typography', accent: '#1a1a1a', mode: 'light', tags: ['minimal'] },
28
+ { name: 'cosmos', label: 'Cosmos', description: 'Starfield background with gold accent and constellation SVG lines connecting elements', accent: '#fbbf24', mode: 'dark', tags: ['animated'] },
29
+ { name: 'bauhaus', label: 'Bauhaus', description: 'De Stijl geometric shapes in red, blue, yellow with thick borders and asymmetric grids', accent: '#dc2626', mode: 'light', tags: ['animated'] },
30
+ { name: 'mono', label: 'Mono', description: '100% monospace terminal — green on black, ASCII bar charts, git-log phases, typing animation', accent: '#4ade80', mode: 'dark', tags: ['minimal', 'data-dense'] },
31
+ { name: 'glacier', label: 'Glacier', description: 'Frosted glassmorphism with backdrop blur cards, cool blue palette, and soft shadows', accent: '#38bdf8', mode: 'light', tags: ['animated'] },
32
+ { name: 'ember', label: 'Ember', description: 'Warm dark theme with orange-to-red fire gradient accents and ember glow on stats', accent: '#f97316', mode: 'dark', tags: ['animated'] },
33
+ { name: 'zen', label: 'Zen', description: 'Japanese minimalism — maximum whitespace, no cards, thin rules, serif display, 640px column', accent: '#78716c', mode: 'light', tags: ['minimal'] },
34
+ { name: 'circuit', label: 'Circuit', description: 'PCB aesthetic with circuit trace patterns, component pads, and lime green accent', accent: '#a3e635', mode: 'dark', tags: ['animated'] },
35
+ { name: 'parchment', label: 'Parchment', description: 'Old book aesthetic with all-serif typography, sepia palette, drop caps, and colophon footer', accent: '#92400e', mode: 'light', tags: ['minimal'] },
36
+ { name: 'aurora', label: 'Aurora', description: 'Northern lights gradient header that slowly shifts — restrained dark with teal magic', accent: '#2dd4bf', mode: 'dark', tags: ['animated'] },
37
+ { name: 'grid', label: 'Grid', description: 'Bento dashboard layout with mixed-size CSS grid cells like iOS widgets', accent: '#6366f1', mode: 'light', tags: ['data-dense'] },
38
+ { name: 'obsidian', label: 'Obsidian', description: 'Deep black with purple gem accent and hover shimmer like light catching a gemstone', accent: '#a855f7', mode: 'dark', tags: ['animated'] },
39
+ { name: 'chalk', label: 'Chalk', description: 'Whiteboard aesthetic with handwritten display font, sketch-style borders, and annotation arrows', accent: '#334155', mode: 'light', tags: [] },
40
+ { name: 'signal', label: 'Signal', description: 'Mission control dashboard — dense data tables, status badges, and fast-updating metrics', accent: '#ef4444', mode: 'dark', tags: ['data-dense'] },
41
+ { name: 'canvas', label: 'Canvas', description: 'Art gallery with extreme whitespace, full-bleed images, and large airy typography', accent: '#fb7185', mode: 'light', tags: ['minimal'] },
42
+ { name: 'meridian', label: 'Meridian', description: 'Topographic map aesthetic with contour line patterns and elevation-style charts', accent: '#34d399', mode: 'dark', tags: ['animated'] },
43
+ { name: 'carbon', label: 'Carbon', description: 'Brushed metal industrial — diagonal stripe texture, silver chrome palette, no color', accent: '#94a3b8', mode: 'dark', tags: ['minimal'] },
44
+ { name: 'daylight', label: 'Daylight', description: 'Bright and airy with soft blue shadows, sky blue accent, and friendly rounded shapes', accent: '#0ea5e9', mode: 'light', tags: ['animated'] },
45
+ ];
46
+ const BUILT_IN_NAMES = new Set(BUILT_IN_TEMPLATES.map((t) => t.name));
47
+ export const DEFAULT_TEMPLATE = 'editorial';
48
+ export function isValidTemplate(name) {
49
+ return BUILT_IN_NAMES.has(name);
50
+ }
51
+ /**
52
+ * Resolve which template to use.
53
+ * Priority: project override → user default → 'editorial'
54
+ */
55
+ export function resolveTemplate(projectTemplate, userDefault) {
56
+ if (projectTemplate && isValidTemplate(projectTemplate))
57
+ return projectTemplate;
58
+ if (userDefault && isValidTemplate(userDefault))
59
+ return userDefault;
60
+ return DEFAULT_TEMPLATE;
61
+ }
62
+ /**
63
+ * Load concatenated CSS for a template (base + template-specific).
64
+ * Used by export.ts for standalone HTML and by preview.
65
+ */
66
+ const cssCache = new Map();
67
+ export function getTemplateCss(templateName) {
68
+ const name = isValidTemplate(templateName) ? templateName : DEFAULT_TEMPLATE;
69
+ const cached = cssCache.get(name);
70
+ if (cached !== undefined)
71
+ return cached;
72
+ let css = '';
73
+ try {
74
+ css = readFileSync(resolve(TEMPLATES_DIR, 'styles.css'), 'utf-8');
75
+ }
76
+ catch { /* empty */ }
77
+ try {
78
+ const templateCss = readFileSync(resolve(TEMPLATES_DIR, name, 'styles.css'), 'utf-8');
79
+ css += '\n\n/* === ' + name + ' template styles === */\n' + templateCss;
80
+ }
81
+ catch { /* no template-specific CSS — fine */ }
82
+ cssCache.set(name, css);
83
+ return css;
84
+ }
85
+ export function getTemplateNames() {
86
+ return BUILT_IN_TEMPLATES.map((t) => t.name);
87
+ }
88
+ export function getTemplateInfo(name) {
89
+ return BUILT_IN_TEMPLATES.find((t) => t.name === name);
90
+ }
@@ -3,12 +3,12 @@
3
3
  * Created during the server.ts refactor to avoid circular dependencies.
4
4
  */
5
5
  import path from 'node:path';
6
- import { readFileSync } from 'node:fs';
7
6
  import { fileURLToPath } from 'node:url';
8
7
  import { listSessions, parseSession } from '../parsers/index.js';
9
8
  import { bridgeToAnalyzer, mergeActiveIntervals, sumIntervalMs } from '../bridge.js';
10
9
  import { analyzeSession } from '../analyzer.js';
11
10
  import { loadEnhancedData, loadProjectEnhanceResult, getUploadedState, } from '../settings.js';
11
+ import { getTemplateCss } from '../render/templates.js';
12
12
  import { archiveSessionFiles } from '../archive.js';
13
13
  import { getDatabase, openDatabase, getSessionStats as dbGetSessionStats, getSessionCount, getAllSessionMetas, getAllProjectStats, getSessionsByProject, getProjectUuid, } from '../db.js';
14
14
  import { ensureSessionIndexed, displayNameFromDir } from '../sync.js';
@@ -39,7 +39,16 @@ function computeMergedDurationFromDb(db, projectDir, naiveSumMinutes) {
39
39
  return mergedMinutes > 0 ? mergedMinutes : naiveSumMinutes;
40
40
  }
41
41
  import { escapeHtml } from '../format-utils.js';
42
- export { escapeHtml };
42
+ /** Look up a project by name or dirName, sending 404 if not found. Returns null on miss. */
43
+ export async function requireProject(ctx, projectParam, res) {
44
+ const projects = await ctx.getProjects();
45
+ const proj = projects.find((p) => p.name === projectParam || p.dirName === projectParam);
46
+ if (!proj) {
47
+ res.status(404).json({ error: { code: 'PROJECT_NOT_FOUND', message: 'Project not found' } });
48
+ return null;
49
+ }
50
+ return proj;
51
+ }
43
52
  /**
44
53
  * Build an agent summary from child session metas. Returns null when
45
54
  * there are no children (or none produce valid stats).
@@ -466,13 +475,9 @@ export function createRouteContext(sessionsBasePath, dbPath) {
466
475
  };
467
476
  }
468
477
  // ── buildPreviewPage ─────────────────────────────────────
469
- function buildPreviewPage(title, bodyHtml, banner) {
470
- const renderCssPath = path.resolve(__dirname, '..', 'render', 'templates', 'styles.css');
471
- let inlineCss = '';
472
- try {
473
- inlineCss = readFileSync(renderCssPath, 'utf-8');
474
- }
475
- catch { /* */ }
478
+ function buildPreviewPage(title, bodyHtml, banner, templateName) {
479
+ // Load full template CSS (base + template-specific) via the same path as the React embed
480
+ const inlineCss = getTemplateCss(templateName || 'editorial');
476
481
  const cssTag = `<style>${inlineCss}\n/* Preview override */\nbody { overflow: auto !important; min-height: auto !important; }\n#root { min-height: auto !important; }</style>`;
477
482
  const bannerHtml = banner
478
483
  ? `<div style="background: var(--primary, #084471); color: white; text-align: center; padding: 0.5rem; font-family: 'Inter', sans-serif; font-size: 0.75rem; letter-spacing: 0.05em;">${escapeHtml(banner)}</div>`
@@ -486,7 +491,7 @@ export function createRouteContext(sessionsBasePath, dbPath) {
486
491
  <title>${escapeHtml(title)} — Preview</title>
487
492
  <link rel="preconnect" href="https://fonts.googleapis.com" />
488
493
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
489
- <link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600;700&family=Inter:wght@400;500;600;700&family=Space+Grotesk:wght@400;500;600;700&display=swap" rel="stylesheet" />
494
+ <link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600;700&family=Inter:wght@400;500;600;700&family=Space+Grotesk:wght@400;500;600;700&family=Cormorant+Garamond:ital,wght@0,400;0,600;1,400&family=Newsreader:ital,wght@0,400;0,600;1,400&display=swap" rel="stylesheet" />
490
495
  ${cssTag}
491
496
  </head>
492
497
  <body>
@@ -3,6 +3,8 @@ import { getProvider } from '../llm/index.js';
3
3
  import { triageSessions } from '../llm/triage.js';
4
4
  import { enhanceProject, refineNarrative } from '../llm/project-enhance.js';
5
5
  import { getAnthropicApiKey, saveEnhancedData, loadEnhancedData, deleteEnhancedData, loadFreshProjectEnhanceResult, saveProjectEnhanceResult, loadProjectEnhanceResult, buildProjectFingerprint, getUploadedState, } from '../settings.js';
6
+ import { requireProject } from './context.js';
7
+ import { startSSE } from './sse.js';
6
8
  export function createEnhanceRouter(ctx) {
7
9
  const router = Router();
8
10
  // Triage endpoint -- AI selects which sessions are worth showcasing (SSE stream)
@@ -11,21 +13,11 @@ export function createEnhanceRouter(ctx) {
11
13
  res.status(400).json({ error: { code: 'NO_API_KEY', message: 'No Anthropic API key configured. Add one in Settings or set ANTHROPIC_API_KEY.' } });
12
14
  return;
13
15
  }
14
- const { project } = req.params;
15
- const projects = await ctx.getProjects();
16
- const proj = projects.find((p) => p.name === project || p.dirName === project);
17
- if (!proj) {
18
- res.status(404).json({ error: { code: 'PROJECT_NOT_FOUND', message: 'Project not found' } });
16
+ const project = String(req.params.project);
17
+ const proj = await requireProject(ctx, project, res);
18
+ if (!proj)
19
19
  return;
20
- }
21
- res.writeHead(200, {
22
- 'Content-Type': 'text/event-stream',
23
- 'Cache-Control': 'no-cache',
24
- Connection: 'keep-alive',
25
- });
26
- const send = (event) => {
27
- res.write(`data: ${JSON.stringify(event)}\n\n`);
28
- };
20
+ const send = startSSE(res);
29
21
  try {
30
22
  const total = proj.sessions.length;
31
23
  const sessionsWithStats = [];
@@ -62,13 +54,11 @@ export function createEnhanceRouter(ctx) {
62
54
  // Enhance a single session
63
55
  router.post('/api/projects/:project/sessions/:id/enhance', async (req, res) => {
64
56
  try {
65
- const { project, id } = req.params;
66
- const projects = await ctx.getProjects();
67
- const proj = projects.find((p) => p.name === project || p.dirName === project);
68
- if (!proj) {
69
- res.status(404).json({ error: { code: 'PROJECT_NOT_FOUND', message: 'Project not found' } });
57
+ const project = String(req.params.project);
58
+ const id = String(req.params.id);
59
+ const proj = await requireProject(ctx, project, res);
60
+ if (!proj)
70
61
  return;
71
- }
72
62
  const meta = proj.sessions.find((s) => s.sessionId === id);
73
63
  if (!meta) {
74
64
  res.status(404).json({ error: { code: 'SESSION_NOT_FOUND', message: 'Session not found' } });
@@ -124,14 +114,7 @@ export function createEnhanceRouter(ctx) {
124
114
  res.status(400).json({ error: { code: 'NO_API_KEY', message: 'No Anthropic API key configured. Add one in Settings or set ANTHROPIC_API_KEY.' } });
125
115
  return;
126
116
  }
127
- res.writeHead(200, {
128
- 'Content-Type': 'text/event-stream',
129
- 'Cache-Control': 'no-cache',
130
- Connection: 'keep-alive',
131
- });
132
- const send = (data) => {
133
- res.write(`data: ${JSON.stringify(data)}\n\n`);
134
- };
117
+ const send = startSSE(res);
135
118
  try {
136
119
  const projects = await ctx.getProjects();
137
120
  const proj = projects.find((p) => p.name === project || p.dirName === project);
@@ -237,7 +220,7 @@ export function createEnhanceRouter(ctx) {
237
220
  });
238
221
  // Save project enhance result explicitly
239
222
  router.post('/api/projects/:project/enhance-save', async (req, res) => {
240
- const { project } = req.params;
223
+ const project = String(req.params.project);
241
224
  const { selectedSessionIds, result, title, repoUrl, projectUrl, screenshotBase64 } = req.body;
242
225
  if (!Array.isArray(selectedSessionIds) || !result?.narrative) {
243
226
  res.status(400).json({ error: { code: 'INVALID_INPUT', message: 'selectedSessionIds and result are required' } });
@@ -248,12 +231,9 @@ export function createEnhanceRouter(ctx) {
248
231
  return;
249
232
  }
250
233
  try {
251
- const projects = await ctx.getProjects();
252
- const proj = projects.find((p) => p.name === project || p.dirName === project);
253
- if (!proj) {
254
- res.status(404).json({ error: { code: 'PROJECT_NOT_FOUND', message: 'Project not found' } });
234
+ const proj = await requireProject(ctx, project, res);
235
+ if (!proj)
255
236
  return;
256
- }
257
237
  saveProjectEnhanceResult(proj.dirName, selectedSessionIds, result, undefined, { title, repoUrl, projectUrl, screenshotBase64 });
258
238
  res.json({ saved: true, enhancedAt: new Date().toISOString() });
259
239
  }
@@ -263,14 +243,11 @@ export function createEnhanceRouter(ctx) {
263
243
  });
264
244
  // Get cached project enhance result
265
245
  router.get('/api/projects/:project/enhance-cache', async (req, res) => {
266
- const { project } = req.params;
246
+ const project = String(req.params.project);
267
247
  try {
268
- const projects = await ctx.getProjects();
269
- const proj = projects.find((p) => p.name === project || p.dirName === project);
270
- if (!proj) {
271
- res.status(404).json({ error: { code: 'PROJECT_NOT_FOUND', message: 'Project not found' } });
248
+ const proj = await requireProject(ctx, project, res);
249
+ if (!proj)
272
250
  return;
273
- }
274
251
  const cached = loadProjectEnhanceResult(proj.dirName);
275
252
  if (!cached) {
276
253
  res.status(404).json({ error: { code: 'NO_CACHE', message: 'No cached enhance result' } });
@@ -111,9 +111,14 @@ export function createExportRouter(ctx) {
111
111
  return;
112
112
  }
113
113
  const cache = data.enhanceCache ?? buildFallbackCache(data.sessions);
114
- const totalFilesChanged = data.project.totalFiles;
114
+ const proj = data.project;
115
115
  const outDir = safeExportPath(outputPath, dirName, 'html');
116
- const result = await exportHtml(dirName, cache, data.sessions, outDir, 'local', { totalFilesChanged });
116
+ const result = await exportHtml(dirName, cache, data.sessions, outDir, 'local', {
117
+ totalFilesChanged: proj.totalFiles,
118
+ totalAgentDurationMinutes: proj.totalAgentDuration,
119
+ totalInputTokens: proj.totalInputTokens,
120
+ totalOutputTokens: proj.totalOutputTokens,
121
+ });
117
122
  res.json(result);
118
123
  }
119
124
  catch (err) {
@@ -131,8 +136,13 @@ export function createExportRouter(ctx) {
131
136
  return;
132
137
  }
133
138
  const cache = data.enhanceCache ?? buildFallbackCache(data.sessions);
134
- const totalFilesChanged = data.project.totalFiles;
135
- const htmlFiles = generateHtmlFiles(dirName, cache, data.sessions, 'local', { totalFilesChanged });
139
+ const proj = data.project;
140
+ const htmlFiles = generateHtmlFiles(dirName, cache, data.sessions, 'local', {
141
+ totalFilesChanged: proj.totalFiles,
142
+ totalAgentDurationMinutes: proj.totalAgentDuration,
143
+ totalInputTokens: proj.totalInputTokens,
144
+ totalOutputTokens: proj.totalOutputTokens,
145
+ });
136
146
  const zipBuffer = createZipBuffer(htmlFiles);
137
147
  const filename = `${dirName.replace(/[^a-zA-Z0-9_-]/g, '_')}.zip`;
138
148
  res.setHeader('Content-Type', 'application/zip');