heyiam 0.2.28 → 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-B_d6DlEI.js +0 -21
  177. package/dist/public/assets/index-Dalqz2mC.css +0 -1
package/README.md ADDED
@@ -0,0 +1,45 @@
1
+ # heyiam
2
+
3
+ [![npm version](https://img.shields.io/npm/v/heyiam)](https://www.npmjs.com/package/heyiam)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://github.com/interactivecats/heyi.am/blob/main/LICENSE)
5
+
6
+ Local-first CLI that indexes your AI coding sessions. Search locally, enhance with AI, and publish a portfolio of real work.
7
+
8
+ Discovers sessions from **Claude Code**, **Cursor**, **OpenAI Codex CLI**, and **Google Gemini CLI**.
9
+
10
+ ## Get Started
11
+
12
+ ```bash
13
+ npx heyiam
14
+ ```
15
+
16
+ Opens a local dashboard at `localhost:17845`. Browse projects, search sessions, and publish portfolio case studies to `heyi.am/:username`.
17
+
18
+ ## Commands
19
+
20
+ | Command | Description |
21
+ |---------|-------------|
22
+ | `heyiam` / `heyiam open` | Start local dashboard |
23
+ | `heyiam search [query]` | Full-text search across all sessions |
24
+ | `heyiam time` | Your time vs agent time per project |
25
+ | `heyiam context <id>` | Export session as compressed context for AI tools |
26
+ | `heyiam archive` | Discover and archive sessions from all sources |
27
+ | `heyiam sync` | Index sessions into SQLite |
28
+ | `heyiam status` | Archive health, session counts, daemon status |
29
+
30
+ ## Privacy
31
+
32
+ Everything stays local by default. Nothing leaves your machine unless you explicitly publish.
33
+
34
+ - Sessions are read from local tool storage (read-only)
35
+ - SQLite search index lives at `~/.local/share/heyiam/`
36
+ - Config at `~/.config/heyiam/`
37
+ - Common secret patterns are detected and redacted before upload, but this is not a guarantee
38
+
39
+ AI enhancement runs locally using your own `ANTHROPIC_API_KEY`.
40
+
41
+ ## Links
42
+
43
+ - [heyi.am](https://heyi.am) — published portfolios
44
+ - [heyiam.com](https://heyiam.com) — dashboard and auth
45
+ - [GitHub](https://github.com/interactivecats/heyi.am) — source code
package/dist/config.js CHANGED
@@ -1,4 +1,13 @@
1
1
  /** Base URL for the heyiam.com app API. Override with HEYIAM_API_URL for local dev. */
2
- export const API_URL = process.env.HEYIAM_API_URL ?? 'https://heyiam.com';
2
+ const DEFAULT_API_URL = 'https://heyiam.com';
3
+ export const API_URL = process.env.HEYIAM_API_URL ?? DEFAULT_API_URL;
3
4
  /** Base URL for the heyi.am public site. Override with HEYIAM_PUBLIC_URL for local dev. */
4
5
  export const PUBLIC_URL = process.env.HEYIAM_PUBLIC_URL ?? 'https://heyi.am';
6
+ /** Warn once to stderr if a non-default API URL is in use (env var override). */
7
+ let _apiUrlWarned = false;
8
+ export function warnIfNonDefaultApiUrl() {
9
+ if (!_apiUrlWarned && API_URL !== DEFAULT_API_URL) {
10
+ console.warn(`[security] API_URL overridden to ${API_URL} — auth tokens will be sent to this host`);
11
+ _apiUrlWarned = true;
12
+ }
13
+ }
package/dist/db.js CHANGED
@@ -9,10 +9,9 @@ import { homedir } from 'node:os';
9
9
  function getDataDir() {
10
10
  return process.env.HEYIAM_DATA_DIR || join(homedir(), '.local', 'share', 'heyiam');
11
11
  }
12
- export function getDbPath() {
12
+ function getDbPath() {
13
13
  return join(getDataDir(), 'sessions.db');
14
14
  }
15
- export const DB_PATH = join(homedir(), '.local', 'share', 'heyiam', 'sessions.db');
16
15
  const CURRENT_SCHEMA_VERSION = 5;
17
16
  // ── Singleton ────────────────────────────────────────────────
18
17
  let _db = null;
package/dist/export.js CHANGED
@@ -9,9 +9,10 @@ import { mkdirSync, writeFileSync, readFileSync, statSync, existsSync } from 'no
9
9
  import { deflateRawSync } from 'node:zlib';
10
10
  import { join, resolve, dirname } from 'node:path';
11
11
  import { fileURLToPath } from 'node:url';
12
- import { loadEnhancedData } from './settings.js';
12
+ import { loadEnhancedData, getDefaultTemplate } from './settings.js';
13
13
  import { renderProjectHtml, renderSessionHtml } from './render/index.js';
14
- import { escapeHtml, displayNameFromDir } from './format-utils.js';
14
+ import { resolveTemplate, getTemplateCss } from './render/templates.js';
15
+ import { escapeHtml, displayNameFromDir, toSlug } from './format-utils.js';
15
16
  import { buildProjectRenderData, buildSessionRenderData, buildSessionCard, } from './render/build-render-data.js';
16
17
  import { mergeActiveIntervals, sumIntervalMs } from './bridge.js';
17
18
  import { SCREENSHOTS_DIR } from './screenshot.js';
@@ -42,7 +43,7 @@ function resolveScreenshotDataUri(dirName, cache) {
42
43
  return `data:image/png;base64,${b64}`;
43
44
  }
44
45
  // Try local screenshot file
45
- const slug = dirName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
46
+ const slug = toSlug(dirName);
46
47
  const screenshotPath = join(SCREENSHOTS_DIR, `${slug}.png`);
47
48
  if (existsSync(screenshotPath)) {
48
49
  const buf = readFileSync(screenshotPath);
@@ -220,16 +221,19 @@ export async function exportHtml(dirName, cache, sessions, outputPath, username
220
221
  sourceTool: session.source ?? 'unknown',
221
222
  });
222
223
  });
223
- // Compute stats the same way the dashboard does
224
+ // Prefer DB-computed stats from opts (includes subagent data); fall back to session-based computation
224
225
  const totalLoc = sessions.reduce((sum, s) => sum + s.linesOfCode, 0);
225
226
  const totalDurationMinutes = computeMergedSessionDuration(sessions);
226
227
  const totalFilesChanged = opts?.totalFilesChanged ?? new Set(sessions.flatMap(s => s.filesChanged.map(f => f.path))).size;
227
- // Agent duration: sum child durations across all orchestrated sessions
228
- const totalAgentMinutes = sessions
229
- .filter((s) => s.isOrchestrated && s.children)
230
- .reduce((sum, s) => sum + s.children.reduce((cs, c) => cs + c.durationMinutes, 0), 0);
231
- const totalAgentDurationMinutes = totalAgentMinutes > 0 ? totalDurationMinutes + totalAgentMinutes : undefined;
232
- const totalTokens = sessions.reduce((sum, s) => sum + (s.tokenUsage?.input ?? 0) + (s.tokenUsage?.output ?? 0), 0) || undefined;
228
+ const totalAgentDurationMinutes = opts?.totalAgentDurationMinutes ?? (() => {
229
+ const agentMin = sessions
230
+ .filter((s) => s.isOrchestrated && s.children)
231
+ .reduce((sum, s) => sum + s.children.reduce((cs, c) => cs + c.durationMinutes, 0), 0);
232
+ return agentMin > 0 ? totalDurationMinutes + agentMin : undefined;
233
+ })();
234
+ const totalTokens = (opts?.totalInputTokens || opts?.totalOutputTokens)
235
+ ? (opts.totalInputTokens ?? 0) + (opts.totalOutputTokens ?? 0) || undefined
236
+ : sessions.reduce((sum, s) => sum + (s.tokenUsage?.input ?? 0) + (s.tokenUsage?.output ?? 0), 0) || undefined;
233
237
  // Resolve screenshot for embedding
234
238
  const screenshotUrl = resolveScreenshotDataUri(dirName, cache);
235
239
  // Render project index.html — pass same data shape as dashboard
@@ -256,12 +260,14 @@ export async function exportHtml(dirName, cache, sessions, outputPath, username
256
260
  sessionCards,
257
261
  sessionBaseUrl: './sessions',
258
262
  });
263
+ const templateName = resolveTemplate(undefined, getDefaultTemplate());
259
264
  const projectBody = renderProjectHtml(projectRenderData, {
260
265
  arc: result.arc,
261
266
  fullSessions: sessions,
262
- });
267
+ }, templateName);
263
268
  const projectHtml = buildStandalonePage(title, projectBody, {
264
269
  description: result.narrative?.slice(0, 200) || undefined,
270
+ templateName,
265
271
  });
266
272
  totalBytes += writeAndTrack(join(outputPath, 'index.html'), projectHtml, files);
267
273
  // Render session pages — only featured sessions (linked from project page)
@@ -279,10 +285,11 @@ export async function exportHtml(dirName, cache, sessions, outputPath, username
279
285
  projectSlug: slug,
280
286
  sessionSlug,
281
287
  sourceTool: session.source ?? 'unknown',
288
+ template: templateName,
282
289
  });
283
- const sessionBody = renderSessionHtml(renderData);
290
+ const sessionBody = renderSessionHtml(renderData, templateName);
284
291
  const sessionDesc = (enhanced?.developerTake ?? session.developerTake ?? '').slice(0, 200) || undefined;
285
- const sessionHtml = buildStandalonePage(session.title, sessionBody, { description: sessionDesc });
292
+ const sessionHtml = buildStandalonePage(session.title, sessionBody, { description: sessionDesc, templateName });
286
293
  totalBytes += writeAndTrack(join(sessionsDir, `${sessionSlug}.html`), sessionHtml, files);
287
294
  }
288
295
  return { files, totalBytes, outputPath };
@@ -318,11 +325,15 @@ function buildProjectRenderInputs(dirName, cache, sessions, username, opts) {
318
325
  const totalLoc = sessions.reduce((sum, s) => sum + s.linesOfCode, 0);
319
326
  const totalDurationMinutes = computeMergedSessionDuration(sessions);
320
327
  const totalFilesChanged = opts?.totalFilesChanged ?? new Set(sessions.flatMap(s => s.filesChanged.map(f => f.path))).size;
321
- const totalAgentMinutes = sessions
322
- .filter((s) => s.isOrchestrated && s.children)
323
- .reduce((sum, s) => sum + s.children.reduce((cs, c) => cs + c.durationMinutes, 0), 0);
324
- const totalAgentDurationMinutes = totalAgentMinutes > 0 ? totalDurationMinutes + totalAgentMinutes : undefined;
325
- const totalTokens = sessions.reduce((sum, s) => sum + (s.tokenUsage?.input ?? 0) + (s.tokenUsage?.output ?? 0), 0) || undefined;
328
+ const totalAgentDurationMinutes = opts?.totalAgentDurationMinutes ?? (() => {
329
+ const agentMin = sessions
330
+ .filter((s) => s.isOrchestrated && s.children)
331
+ .reduce((sum, s) => sum + s.children.reduce((cs, c) => cs + c.durationMinutes, 0), 0);
332
+ return agentMin > 0 ? totalDurationMinutes + agentMin : undefined;
333
+ })();
334
+ const totalTokens = (opts?.totalInputTokens || opts?.totalOutputTokens)
335
+ ? (opts.totalInputTokens ?? 0) + (opts.totalOutputTokens ?? 0) || undefined
336
+ : sessions.reduce((sum, s) => sum + (s.tokenUsage?.input ?? 0) + (s.tokenUsage?.output ?? 0), 0) || undefined;
326
337
  return { result, slug, title, sessionCards, totalLoc, totalDurationMinutes, totalAgentDurationMinutes, totalFilesChanged, totalTokens };
327
338
  }
328
339
  /**
@@ -346,15 +357,17 @@ export function generateProjectHtmlFragment(dirName, cache, sessions, username =
346
357
  totalLoc, totalDurationMinutes, totalAgentDurationMinutes, totalFilesChanged, totalTokens,
347
358
  sessionCards,
348
359
  });
360
+ const templateName = resolveTemplate(undefined, getDefaultTemplate());
349
361
  return renderProjectHtml(renderData, {
350
362
  arc: result.arc,
351
363
  fullSessions: sessions,
352
- });
364
+ }, templateName);
353
365
  }
354
366
  export function generateHtmlFiles(dirName, cache, sessions, username = 'local', opts) {
355
367
  const files = [];
356
368
  const { result, slug, title, sessionCards, totalLoc, totalDurationMinutes, totalAgentDurationMinutes, totalFilesChanged, totalTokens } = buildProjectRenderInputs(dirName, cache, sessions, username, opts);
357
369
  const screenshotUrl = resolveScreenshotDataUri(dirName, cache);
370
+ const templateName = resolveTemplate(undefined, getDefaultTemplate());
358
371
  const projectRenderData = buildProjectRenderData({
359
372
  username, slug, title,
360
373
  narrative: result.narrative,
@@ -371,9 +384,10 @@ export function generateHtmlFiles(dirName, cache, sessions, username = 'local',
371
384
  const projectBody = renderProjectHtml(projectRenderData, {
372
385
  arc: result.arc,
373
386
  fullSessions: sessions,
374
- });
387
+ }, templateName);
375
388
  files.push({ path: 'index.html', content: buildStandalonePage(title, projectBody, {
376
389
  description: result.narrative?.slice(0, 200) || undefined,
390
+ templateName,
377
391
  }) });
378
392
  const featuredSessions = pickFeaturedSessions(sessions, cache);
379
393
  for (const session of featuredSessions) {
@@ -384,12 +398,13 @@ export function generateHtmlFiles(dirName, cache, sessions, username = 'local',
384
398
  session, enhanced, username,
385
399
  projectSlug: slug, sessionSlug,
386
400
  sourceTool: session.source ?? 'unknown',
401
+ template: templateName,
387
402
  });
388
- const sessionBody = renderSessionHtml(renderData);
403
+ const sessionBody = renderSessionHtml(renderData, templateName);
389
404
  const sessionDesc = (enhanced?.developerTake ?? session.developerTake ?? '').slice(0, 200) || undefined;
390
405
  files.push({
391
406
  path: `sessions/${sessionSlug}.html`,
392
- content: buildStandalonePage(session.title, sessionBody, { description: sessionDesc }),
407
+ content: buildStandalonePage(session.title, sessionBody, { description: sessionDesc, templateName }),
393
408
  });
394
409
  }
395
410
  return files;
@@ -490,9 +505,9 @@ function getInlineMountJs() {
490
505
  }
491
506
  }
492
507
  function buildStandalonePage(title, bodyHtml, opts) {
493
- const css = getInlineCss();
508
+ const css = opts?.templateName ? getTemplateCss(opts.templateName) : getInlineCss();
494
509
  const cssTag = css
495
- ? `<style>${css}\nbody { overflow: auto !important; min-height: auto !important; background: var(--color-surface, #f8f9fb); }</style>`
510
+ ? `<style>${css}\nbody { overflow: auto !important; min-height: auto !important; }\n#root { min-height: auto !important; }</style>`
496
511
  : '';
497
512
  const mountJs = getInlineMountJs();
498
513
  const scriptTag = mountJs ? `<script>${mountJs}</script>` : '';
@@ -516,7 +531,7 @@ function buildStandalonePage(title, bodyHtml, opts) {
516
531
  ${ogTags}
517
532
  <link rel="preconnect" href="https://fonts.googleapis.com" />
518
533
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
519
- <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" />
534
+ <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" />
520
535
  ${cssTag}
521
536
  </head>
522
537
  <body>
@@ -16,6 +16,11 @@ export function displayNameFromDir(dirName) {
16
16
  const segments = dirName.split('-').filter(Boolean);
17
17
  return segments.length > 0 ? segments[segments.length - 1] : dirName;
18
18
  }
19
+ /** Generate a URL-safe slug from a string. */
20
+ export function toSlug(s, maxLen) {
21
+ const slug = s.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
22
+ return maxLen !== undefined ? slug.slice(0, maxLen) : slug;
23
+ }
19
24
  /** Escape LIKE wildcards in user input for safe use in SQL LIKE clauses. */
20
25
  export function escapeLikeWildcards(str) {
21
26
  return str.replace(/[%_]/g, (c) => `\\${c}`);
package/dist/index.js CHANGED
@@ -650,6 +650,174 @@ daemon
650
650
  }
651
651
  console.log('\n Daemon uninstalled.\n');
652
652
  });
653
+ // ── Embed command ──────────────────────────────────────────
654
+ program
655
+ .command('embed')
656
+ .description('Generate embeddable widget snippets for your portfolio and projects')
657
+ .option('--project <name>', 'Generate embed for a specific project')
658
+ .option('--format <type>', 'Output format: widget, iframe, badge, html, or all', '')
659
+ .option('--sections <list>', 'Sections to include (comma-separated: stats,tools,skills,heatmap,recent)', 'stats')
660
+ .option('--theme <theme>', 'Color theme (dark or light)', 'dark')
661
+ .action(async (opts) => {
662
+ const { getAuthToken } = await import('./auth.js');
663
+ const { PUBLIC_URL } = await import('./config.js');
664
+ const { getUploadedState, getDataDir } = await import('./settings.js');
665
+ const { displayNameFromDir } = await import('./sync.js');
666
+ const { readdirSync, existsSync } = await import('node:fs');
667
+ const { join } = await import('node:path');
668
+ const auth = getAuthToken();
669
+ const username = auth?.username;
670
+ const sections = opts.sections || 'stats';
671
+ const theme = opts.theme || 'dark';
672
+ const queryParts = [];
673
+ if (sections !== 'stats')
674
+ queryParts.push(`sections=${sections}`);
675
+ if (theme !== 'dark')
676
+ queryParts.push(`theme=${theme}`);
677
+ const query = queryParts.length > 0 ? `?${queryParts.join('&')}` : '';
678
+ // Find published projects
679
+ const publishedDir = join(getDataDir(), 'published');
680
+ const publishedProjects = [];
681
+ if (existsSync(publishedDir)) {
682
+ for (const file of readdirSync(publishedDir)) {
683
+ if (!file.endsWith('.json'))
684
+ continue;
685
+ const dirName = file.replace(/\.json$/, '');
686
+ const state = getUploadedState(dirName);
687
+ if (state?.slug) {
688
+ publishedProjects.push({ dirName, slug: state.slug });
689
+ }
690
+ }
691
+ }
692
+ const isPublished = username && publishedProjects.length > 0;
693
+ const format = opts.format || (isPublished ? 'all' : 'html');
694
+ // Build local stats for static HTML
695
+ const db = openDatabase();
696
+ await syncSessionIndex(db);
697
+ const { getAllProjectStats } = await import('./db.js');
698
+ const allStats = getAllProjectStats(db);
699
+ if (opts.project) {
700
+ const match = publishedProjects.find((p) => p.slug === opts.project || p.dirName === opts.project || displayNameFromDir(p.dirName) === opts.project);
701
+ const localMatch = allStats.find((p) => p.projectName === opts.project || p.projectDir === opts.project || displayNameFromDir(p.projectDir) === opts.project);
702
+ if (!match && !localMatch) {
703
+ console.log(`\n Project "${opts.project}" not found.`);
704
+ if (allStats.length > 0) {
705
+ console.log(' Available projects:');
706
+ for (const p of allStats)
707
+ console.log(` ${p.projectName}`);
708
+ }
709
+ console.log('');
710
+ db.close();
711
+ return;
712
+ }
713
+ const base = match && username ? `${PUBLIC_URL}/${username}/${match.slug}` : null;
714
+ const stats = localMatch || (match ? allStats.find((p) => displayNameFromDir(p.projectDir) === displayNameFromDir(match.dirName)) : null);
715
+ const title = stats?.projectName || match?.slug || opts.project;
716
+ console.log('');
717
+ console.log(` ── ${title} ──────────────────────────────`);
718
+ printFormats(format, base, query, sections, theme, stats ? {
719
+ name: stats.projectName,
720
+ sessions: stats.sessionCount,
721
+ loc: stats.totalLoc,
722
+ duration: stats.totalDuration,
723
+ skills: stats.skills,
724
+ } : null);
725
+ }
726
+ else {
727
+ // Portfolio level
728
+ const base = username ? `${PUBLIC_URL}/${username}` : null;
729
+ // Aggregate stats across all projects
730
+ const totalSessions = allStats.reduce((s, p) => s + p.sessionCount, 0);
731
+ const totalLoc = allStats.reduce((s, p) => s + p.totalLoc, 0);
732
+ const totalDuration = allStats.reduce((s, p) => s + p.totalDuration, 0);
733
+ const allSkills = [...new Set(allStats.flatMap((p) => p.skills))].slice(0, 8);
734
+ console.log('');
735
+ console.log(' ── Portfolio ──────────────────────────────────');
736
+ printFormats(format, base, query, sections, theme, {
737
+ name: username || 'portfolio',
738
+ sessions: totalSessions,
739
+ loc: totalLoc,
740
+ duration: totalDuration,
741
+ skills: allSkills,
742
+ projectCount: allStats.length,
743
+ });
744
+ // Also show each published project
745
+ if (format !== 'html' && publishedProjects.length > 0) {
746
+ for (const p of publishedProjects) {
747
+ const projBase = `${PUBLIC_URL}/${username}/${p.slug}`;
748
+ const stats = allStats.find((s) => displayNameFromDir(s.projectDir) === displayNameFromDir(p.dirName));
749
+ console.log(` ── ${p.slug} ──────────────────────────────`);
750
+ printFormats(format, projBase, query, sections, theme, stats ? {
751
+ name: stats.projectName,
752
+ sessions: stats.sessionCount,
753
+ loc: stats.totalLoc,
754
+ duration: stats.totalDuration,
755
+ skills: stats.skills,
756
+ } : null);
757
+ }
758
+ }
759
+ }
760
+ db.close();
761
+ });
762
+ function printFormats(format, base, query, _sections, _theme, stats) {
763
+ const showAll = format === 'all';
764
+ if (base && (showAll || format === 'badge')) {
765
+ console.log('');
766
+ console.log(' Badge (GitHub README, markdown):');
767
+ console.log(` [![heyi.am](${base}/embed.svg)](${base})`);
768
+ console.log('');
769
+ }
770
+ if (base && (showAll || format === 'widget')) {
771
+ const dataAttrs = [`data-username="${base.split('/').slice(-2, -1)[0] || ''}"`,];
772
+ // If it's a project URL (3+ path segments after domain), add data-project
773
+ const pathParts = new URL(base).pathname.split('/').filter(Boolean);
774
+ if (pathParts.length >= 2)
775
+ dataAttrs.push(`data-project="${pathParts[1]}"`);
776
+ if (_sections !== 'stats')
777
+ dataAttrs.push(`data-sections="${_sections}"`);
778
+ if (_theme !== 'dark')
779
+ dataAttrs.push(`data-theme="${_theme}"`);
780
+ console.log(' Widget (personal site, blog):');
781
+ console.log(` <div class="heyiam-embed" ${dataAttrs.join(' ')}></div>`);
782
+ console.log(` <script src="${new URL(base).origin}/embed.js"></script>`);
783
+ console.log('');
784
+ }
785
+ if (base && (showAll || format === 'iframe')) {
786
+ console.log(' iframe:');
787
+ console.log(` <iframe src="${base}/embed${query}" width="480" height="200" frameborder="0"></iframe>`);
788
+ console.log('');
789
+ }
790
+ if (showAll || format === 'html') {
791
+ if (stats) {
792
+ const durationStr = stats.duration >= 60 ? `${Math.round(stats.duration / 60)}h` : `${stats.duration}m`;
793
+ const locStr = stats.loc >= 1000 ? `${(stats.loc / 1000).toFixed(1)}k` : String(stats.loc);
794
+ const skillChips = stats.skills.slice(0, 6).map((s) => `<span style="font-size:10px;padding:2px 8px;border-radius:3px;background:#1f2937;color:#9ca3af">${s}</span>`).join(' ');
795
+ const projectLine = stats.projectCount ? `<span style="font-size:10px;color:#6b7280">${stats.projectCount} projects</span>` : '';
796
+ console.log(' Static HTML (works anywhere, no JS needed):');
797
+ console.log(` <div style="font-family:ui-monospace,monospace;background:#0a0a0f;color:#e5e7eb;padding:16px 20px;border-radius:6px">`);
798
+ console.log(` <div style="font-size:15px;font-weight:600;color:#f9fafb;margin-bottom:12px">${stats.name} ${projectLine}</div>`);
799
+ console.log(` <div style="display:flex;gap:20px;flex-wrap:wrap;margin-bottom:10px">`);
800
+ console.log(` <div><div style="font-size:18px;font-weight:700;color:#f9fafb">${stats.sessions}</div><div style="font-size:10px;color:#6b7280;text-transform:uppercase;letter-spacing:0.08em">Sessions</div></div>`);
801
+ console.log(` <div><div style="font-size:18px;font-weight:700;color:#f9fafb">${locStr}</div><div style="font-size:10px;color:#6b7280;text-transform:uppercase;letter-spacing:0.08em">Lines Changed</div></div>`);
802
+ console.log(` <div><div style="font-size:18px;font-weight:700;color:#f9fafb">${durationStr}</div><div style="font-size:10px;color:#6b7280;text-transform:uppercase;letter-spacing:0.08em">Active Time</div></div>`);
803
+ console.log(` </div>`);
804
+ if (skillChips) {
805
+ console.log(` <div style="display:flex;gap:6px;flex-wrap:wrap">${skillChips}</div>`);
806
+ }
807
+ console.log(` </div>`);
808
+ console.log('');
809
+ }
810
+ else {
811
+ console.log(' Static HTML: No local stats available for this project.');
812
+ console.log('');
813
+ }
814
+ }
815
+ if (!base && format !== 'html') {
816
+ console.log(' Not published yet. Only static HTML is available.');
817
+ console.log(' Publish from the dashboard, then run again for badge/widget/iframe.');
818
+ console.log('');
819
+ }
820
+ }
653
821
  // ── Logout command ──────────────────────────────────────────
654
822
  program
655
823
  .command('logout')