heyiam 0.2.19 → 0.2.20

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.
package/dist/db.js CHANGED
@@ -23,7 +23,7 @@ export function getDatabase(dbPath = getDbPath()) {
23
23
  _db = openDatabase(dbPath);
24
24
  return _db;
25
25
  }
26
- function closeDatabase() {
26
+ export function closeDatabase() {
27
27
  if (_db) {
28
28
  _db.close();
29
29
  _db = null;
@@ -127,10 +127,10 @@ function migrateToV2(db) {
127
127
  // Upsert schema version
128
128
  const existing = db.prepare('SELECT version FROM schema_version LIMIT 1').get();
129
129
  if (existing) {
130
- db.prepare('UPDATE schema_version SET version = ?').run(CURRENT_SCHEMA_VERSION);
130
+ db.prepare('UPDATE schema_version SET version = ?').run(2);
131
131
  }
132
132
  else {
133
- db.prepare('INSERT INTO schema_version (version) VALUES (?)').run(CURRENT_SCHEMA_VERSION);
133
+ db.prepare('INSERT INTO schema_version (version) VALUES (?)').run(2);
134
134
  }
135
135
  });
136
136
  tx();
@@ -143,7 +143,7 @@ function migrateToV3(db) {
143
143
  uuid TEXT NOT NULL
144
144
  )
145
145
  `);
146
- db.prepare('UPDATE schema_version SET version = ?').run(CURRENT_SCHEMA_VERSION);
146
+ db.prepare('UPDATE schema_version SET version = ?').run(3);
147
147
  });
148
148
  tx();
149
149
  }
@@ -156,7 +156,7 @@ function migrateToV5(db) {
156
156
  if (!cols.some(c => c.name === 'output_tokens')) {
157
157
  db.exec('ALTER TABLE sessions ADD COLUMN output_tokens INTEGER NOT NULL DEFAULT 0');
158
158
  }
159
- db.prepare('UPDATE schema_version SET version = ?').run(CURRENT_SCHEMA_VERSION);
159
+ db.prepare('UPDATE schema_version SET version = ?').run(5);
160
160
  });
161
161
  tx();
162
162
  }
@@ -167,7 +167,7 @@ function migrateToV4(db) {
167
167
  if (!cols.some(c => c.name === 'active_intervals')) {
168
168
  db.exec('ALTER TABLE sessions ADD COLUMN active_intervals TEXT');
169
169
  }
170
- db.prepare('UPDATE schema_version SET version = ?').run(CURRENT_SCHEMA_VERSION);
170
+ db.prepare('UPDATE schema_version SET version = ?').run(4);
171
171
  });
172
172
  tx();
173
173
  }
package/dist/export.js CHANGED
@@ -260,7 +260,9 @@ export async function exportHtml(dirName, cache, sessions, outputPath, username
260
260
  arc: result.arc,
261
261
  fullSessions: sessions,
262
262
  });
263
- const projectHtml = buildStandalonePage(title, projectBody);
263
+ const projectHtml = buildStandalonePage(title, projectBody, {
264
+ description: result.narrative?.slice(0, 200) || undefined,
265
+ });
264
266
  totalBytes += writeAndTrack(join(outputPath, 'index.html'), projectHtml, files);
265
267
  // Render session pages — only featured sessions (linked from project page)
266
268
  const featuredSessions = pickFeaturedSessions(sessions, cache);
@@ -268,17 +270,19 @@ export async function exportHtml(dirName, cache, sessions, outputPath, username
268
270
  mkdirSync(sessionsDir, { recursive: true });
269
271
  for (const session of featuredSessions) {
270
272
  const sessionSlug = slugify(session.title);
273
+ const enhanced = loadEnhancedData(session.id);
271
274
  const renderData = buildSessionRenderData({
272
275
  sessionId: session.id,
273
276
  session,
274
- enhanced: null,
277
+ enhanced,
275
278
  username,
276
279
  projectSlug: slug,
277
280
  sessionSlug,
278
281
  sourceTool: session.source ?? 'unknown',
279
282
  });
280
283
  const sessionBody = renderSessionHtml(renderData);
281
- const sessionHtml = buildStandalonePage(session.title, sessionBody);
284
+ const sessionDesc = (enhanced?.developerTake ?? session.developerTake ?? '').slice(0, 200) || undefined;
285
+ const sessionHtml = buildStandalonePage(session.title, sessionBody, { description: sessionDesc });
282
286
  totalBytes += writeAndTrack(join(sessionsDir, `${sessionSlug}.html`), sessionHtml, files);
283
287
  }
284
288
  return { files, totalBytes, outputPath };
@@ -364,20 +368,24 @@ export function generateHtmlFiles(dirName, cache, sessions, username = 'local',
364
368
  arc: result.arc,
365
369
  fullSessions: sessions,
366
370
  });
367
- files.push({ path: 'index.html', content: buildStandalonePage(title, projectBody) });
371
+ files.push({ path: 'index.html', content: buildStandalonePage(title, projectBody, {
372
+ description: result.narrative?.slice(0, 200) || undefined,
373
+ }) });
368
374
  const featuredSessions = pickFeaturedSessions(sessions, cache);
369
375
  for (const session of featuredSessions) {
370
376
  const sessionSlug = slugify(session.title);
377
+ const enhanced = loadEnhancedData(session.id);
371
378
  const renderData = buildSessionRenderData({
372
379
  sessionId: session.id,
373
- session, enhanced: null, username,
380
+ session, enhanced, username,
374
381
  projectSlug: slug, sessionSlug,
375
382
  sourceTool: session.source ?? 'unknown',
376
383
  });
377
384
  const sessionBody = renderSessionHtml(renderData);
385
+ const sessionDesc = (enhanced?.developerTake ?? session.developerTake ?? '').slice(0, 200) || undefined;
378
386
  files.push({
379
387
  path: `sessions/${sessionSlug}.html`,
380
- content: buildStandalonePage(session.title, sessionBody),
388
+ content: buildStandalonePage(session.title, sessionBody, { description: sessionDesc }),
381
389
  });
382
390
  }
383
391
  return files;
@@ -477,19 +485,31 @@ function getInlineMountJs() {
477
485
  return '';
478
486
  }
479
487
  }
480
- function buildStandalonePage(title, bodyHtml) {
488
+ function buildStandalonePage(title, bodyHtml, opts) {
481
489
  const css = getInlineCss();
482
490
  const cssTag = css
483
491
  ? `<style>${css}\nbody { overflow: auto !important; min-height: auto !important; background: var(--color-surface, #f8f9fb); }</style>`
484
492
  : '';
485
493
  const mountJs = getInlineMountJs();
486
494
  const scriptTag = mountJs ? `<script>${mountJs}</script>` : '';
495
+ const safeTitle = escapeHtml(title);
496
+ const safeDesc = opts?.description ? escapeHtml(opts.description) : '';
497
+ const ogTitle = `${safeTitle} — heyi.am`;
498
+ const ogTags = `<meta property="og:title" content="${ogTitle}" />
499
+ <meta property="og:site_name" content="heyi.am" />
500
+ <meta property="og:type" content="article" />
501
+ ${safeDesc ? `<meta property="og:description" content="${safeDesc}" />` : ''}
502
+ ${safeDesc ? `<meta name="description" content="${safeDesc}" />` : ''}
503
+ <meta name="twitter:card" content="summary" />
504
+ <meta name="twitter:title" content="${ogTitle}" />
505
+ ${safeDesc ? `<meta name="twitter:description" content="${safeDesc}" />` : ''}`;
487
506
  return `<!DOCTYPE html>
488
507
  <html lang="en">
489
508
  <head>
490
509
  <meta charset="utf-8" />
491
510
  <meta name="viewport" content="width=device-width, initial-scale=1" />
492
- <title>${escapeHtml(title)} — heyi.am</title>
511
+ <title>${ogTitle}</title>
512
+ ${ogTags}
493
513
  <link rel="preconnect" href="https://fonts.googleapis.com" />
494
514
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
495
515
  <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" />
package/dist/mount.js CHANGED
@@ -22991,9 +22991,9 @@
22991
22991
  ] }),
22992
22992
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "grid", gridTemplateColumns: "repeat(4, 1fr)", gap: "0.75rem", marginBottom: "1.25rem" }, children: [
22993
22993
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(StatBox, { label: "Active Time", value: formatDuration2(session.durationMinutes), primary: true, children: hasChildren && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(SplitLine, { you: formatDuration2(Math.max(0, session.durationMinutes - session.children.reduce((s, c) => s + c.durationMinutes, 0))), agent: formatDuration2(session.children.reduce((s, c) => s + c.durationMinutes, 0)) }) }),
22994
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(StatBox, { label: "Turns", value: session.turns, children: hasChildren && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(SplitLine, { you: "You", agent: `${session.children.length} agents` }) }),
22994
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(StatBox, { label: "Turns", value: session.turns, children: hasChildren && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(SplitLine, { you: "Human", agent: `${session.children.length} agents` }) }),
22995
22995
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(StatBox, { label: "Files", value: session.filesChanged?.length === 1 && session.filesChanged[0]?.path === "(aggregate)" ? "\u2014" : session.filesChanged?.length ?? "\u2014" }),
22996
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(StatBox, { label: "LOC", value: formatLoc2(session.linesOfCode), children: hasChildren && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(SplitLine, { you: formatLoc2(Math.max(0, session.linesOfCode - session.children.reduce((s, c) => s + c.linesOfCode, 0))), agent: formatLoc2(session.children.reduce((s, c) => s + c.linesOfCode, 0)) }) })
22996
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(StatBox, { label: "Lines changed", value: formatLoc2(session.linesOfCode), children: hasChildren && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(SplitLine, { you: formatLoc2(Math.max(0, session.linesOfCode - session.children.reduce((s, c) => s + c.linesOfCode, 0))), agent: formatLoc2(session.children.reduce((s, c) => s + c.linesOfCode, 0)) }) })
22997
22997
  ] }),
22998
22998
  session.developerTake && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { style: { fontSize: "0.9375rem", lineHeight: 1.6, color: "var(--on-surface, #191c1e)", borderLeft: "3px solid var(--primary, #084471)", paddingLeft: "0.75rem", marginBottom: "1.25rem" }, children: session.developerTake }),
22999
22999
  session.skills && session.skills.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { display: "flex", flexWrap: "wrap", gap: "0.375rem", marginBottom: "1.25rem" }, children: session.skills.map((skill) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: { fontFamily: "var(--font-mono, monospace)", fontSize: "11px", padding: "0.125rem 0.5rem", borderRadius: "0.25rem", background: "var(--violet-bg, #ede9fe)", color: "var(--violet, #6d28d9)" }, children: skill }, skill)) }),
@@ -23069,12 +23069,18 @@
23069
23069
  const [active, setActive] = (0, import_react3.useState)(null);
23070
23070
  showOverlay = (session) => setActive(session);
23071
23071
  if (!active) return null;
23072
- const slug = active.title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 80) || "untitled";
23072
+ const projectEl = document.querySelector("[data-session-base-url]");
23073
+ const baseUrl = projectEl?.getAttribute("data-session-base-url");
23074
+ let sessionPageUrl;
23075
+ if (baseUrl) {
23076
+ const slug = active.title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 80) || "untitled";
23077
+ sessionPageUrl = `${baseUrl}/${slug}.html`;
23078
+ }
23073
23079
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
23074
23080
  SessionOverlay,
23075
23081
  {
23076
23082
  session: active,
23077
- sessionPageUrl: `./sessions/${slug}.html`,
23083
+ sessionPageUrl,
23078
23084
  onClose: () => setActive(null)
23079
23085
  }
23080
23086
  );
@@ -1,4 +1,4 @@
1
- <div class="heyiam-project" data-render-version="2">
1
+ <div class="heyiam-project" data-render-version="2"{% if sessionBaseUrl %} data-session-base-url="{{ sessionBaseUrl }}"{% endif %}>
2
2
 
3
3
  {%- comment -%} Title {%- endcomment -%}
4
4
  <h1 class="project-title">{{ project.title }}</h1>
@@ -134,7 +134,6 @@ export function createPublishRouter(ctx) {
134
134
  }
135
135
  const projectData = await projectRes.json();
136
136
  send({ type: 'project', status: 'created', projectId: projectData.project_id, slug: projectData.slug });
137
- let screenshotUploaded = false;
138
137
  // Step 1b: Upload screenshot (non-fatal)
139
138
  if (screenshotBase64 || projectUrl) {
140
139
  try {
@@ -177,7 +176,6 @@ export function createPublishRouter(ctx) {
177
176
  },
178
177
  body: JSON.stringify({ key }),
179
178
  });
180
- screenshotUploaded = true;
181
179
  send({ type: 'screenshot', status: 'uploaded' });
182
180
  }
183
181
  else {
@@ -22,7 +22,7 @@ export function createSettingsRouter(_ctx) {
22
22
  const key = getAnthropicApiKey();
23
23
  res.json({
24
24
  hasKey: !!key,
25
- maskedKey: key ? `${key.slice(0, 4)}...` : null,
25
+ maskedKey: key ? `...${key.slice(-4)}` : null,
26
26
  });
27
27
  });
28
28
  return router;
@@ -52,14 +52,19 @@ function isUrlSafe(raw) {
52
52
  return false;
53
53
  // Reject localhost and private IPs
54
54
  const host = parsed.hostname.toLowerCase();
55
- if (host === 'localhost' || host === '127.0.0.1' || host === '::1') {
55
+ // Strip IPv6 brackets for comparison
56
+ const bare = host.startsWith('[') ? host.slice(1, -1) : host;
57
+ if (bare === 'localhost' || bare === '127.0.0.1' || bare === '::1' || bare === '0.0.0.0') {
56
58
  // Allow our own preview server
57
59
  const port = parsed.port || (parsed.protocol === 'https:' ? '443' : '80');
58
60
  if (port !== '17845')
59
61
  return false;
60
62
  }
61
- // Reject private IP ranges
62
- if (/^(10\.|172\.(1[6-9]|2\d|3[01])\.|192\.168\.|169\.254\.|0\.)/.test(host))
63
+ // Reject private IP ranges (IPv4)
64
+ if (/^(10\.|172\.(1[6-9]|2\d|3[01])\.|192\.168\.|169\.254\.|0\.)/.test(bare))
65
+ return false;
66
+ // Reject IPv6 private ranges (link-local, ULA, loopback, IPv4-mapped)
67
+ if (/^(fe80:|fc|fd|::ffff:(10\.|172\.(1[6-9]|2\d|3[01])\.|192\.168\.|127\.))/i.test(bare))
63
68
  return false;
64
69
  if (host.endsWith('.local') || host.endsWith('.internal'))
65
70
  return false;
package/dist/search.js CHANGED
@@ -1,5 +1,6 @@
1
1
  // Search logic combining FTS5 content search with metadata filters
2
2
  import { searchFts } from './db.js';
3
+ import { escapeLikeWildcards } from './format-utils.js';
3
4
  // ── Helpers ──────────────────────────────────────────────────
4
5
  /**
5
6
  * Decode projectDir to a human-readable project name.
@@ -69,7 +70,7 @@ function searchWithFilters(db, filters) {
69
70
  const params = [];
70
71
  if (filters?.project) {
71
72
  conditions.push('s.project_dir LIKE ?');
72
- params.push(`%${filters.project}%`);
73
+ params.push(`%${escapeLikeWildcards(filters.project)}%`);
73
74
  }
74
75
  if (filters?.source) {
75
76
  conditions.push('s.source = ?');
@@ -103,7 +104,7 @@ function searchWithFilters(db, filters) {
103
104
  ORDER BY s.start_time DESC
104
105
  LIMIT ?
105
106
  `;
106
- params.unshift(`%${filters.file}%`);
107
+ params.unshift(`%${escapeLikeWildcards(filters.file)}%`);
107
108
  params.push(MAX_RESULTS);
108
109
  }
109
110
  else {
package/dist/server.js CHANGED
@@ -5,7 +5,7 @@ import { existsSync, readFileSync, writeFileSync, mkdirSync, unlinkSync } from '
5
5
  import { execFileSync } from 'node:child_process';
6
6
  import { fileURLToPath } from 'node:url';
7
7
  import { homedir } from 'node:os';
8
- import { getDatabase } from './db.js';
8
+ import { getDatabase, closeDatabase } from './db.js';
9
9
  import { syncWithTracking, startFileWatcher, startCursorPolling, markSyncPending } from './sync.js';
10
10
  import { createRouteContext, createProjectsRouter, createEnhanceRouter, createPublishRouter, createSearchRouter, createSessionsRouter, createArchiveRouter, createAuthRouter, createSettingsRouter, createExportRouter, createPreviewRouter, createDashboardRouter, } from './routes/index.js';
11
11
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
@@ -77,13 +77,16 @@ export function createApp(sessionsBasePath, dbPath) {
77
77
  }
78
78
  next();
79
79
  });
80
- app.use(cors({ origin: ['http://localhost:17845', 'http://127.0.0.1:17845', 'http://localhost:5173'] }));
80
+ const corsOrigins = ['http://localhost:17845', 'http://127.0.0.1:17845'];
81
+ if (process.env.NODE_ENV !== 'production')
82
+ corsOrigins.push('http://localhost:5173');
83
+ app.use(cors({ origin: corsOrigins }));
81
84
  app.use((_req, res, next) => {
82
85
  res.setHeader('X-Content-Type-Options', 'nosniff');
83
86
  res.setHeader('X-Frame-Options', 'DENY');
84
87
  next();
85
88
  });
86
- app.use(express.json({ limit: '50mb' }));
89
+ app.use(express.json({ limit: '10mb' }));
87
90
  // ── Mount domain routers ───────────────────────────────────
88
91
  app.use(createProjectsRouter(ctx));
89
92
  app.use(createEnhanceRouter(ctx));
@@ -108,11 +111,11 @@ export function createApp(sessionsBasePath, dbPath) {
108
111
  const staticDir = existsSync(prodDir) ? prodDir : devDir;
109
112
  app.use(express.static(staticDir));
110
113
  // SPA fallback -- serve index.html for non-API routes
111
- const indexPath = path.join(staticDir, 'index.html');
114
+ // Express 5 requires { root } option for sendFile (absolute paths fail silently)
112
115
  app.get('/{*splat}', (_req, res) => {
113
- res.sendFile(indexPath, (err) => {
116
+ res.sendFile('index.html', { root: staticDir }, (err) => {
114
117
  if (err && !res.headersSent) {
115
- console.error(`[spa] sendFile failed for ${indexPath}:`, err.message);
118
+ console.error(`[spa] sendFile failed for ${staticDir}/index.html:`, err.message);
116
119
  res.status(404).send('Page not found');
117
120
  }
118
121
  });
@@ -146,11 +149,12 @@ export function startServer(port = 17845, options) {
146
149
  server.on('close', () => {
147
150
  stopFileWatcher();
148
151
  stopCursorPolling();
152
+ closeDatabase();
149
153
  removeServerPidFile();
150
154
  });
151
155
  }
152
156
  else {
153
- server.on('close', () => { removeServerPidFile(); });
157
+ server.on('close', () => { closeDatabase(); removeServerPidFile(); });
154
158
  }
155
159
  resolve(server);
156
160
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "heyiam",
3
- "version": "0.2.19",
3
+ "version": "0.2.20",
4
4
  "description": "Turn AI coding sessions into portfolio case studies",
5
5
  "type": "module",
6
6
  "license": "MIT",