heyiam 0.2.29 → 0.3.1

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 (186) hide show
  1. package/README.md +45 -0
  2. package/dist/auth.js +29 -3
  3. package/dist/config.js +10 -1
  4. package/dist/db.js +0 -1
  5. package/dist/export.js +124 -27
  6. package/dist/format-utils.js +5 -0
  7. package/dist/github.js +381 -0
  8. package/dist/index.js +168 -0
  9. package/dist/mount.js +300 -102
  10. package/dist/parsers/claude.js +2 -28
  11. package/dist/parsers/codex.js +2 -26
  12. package/dist/parsers/cursor.js +2 -26
  13. package/dist/parsers/duration.js +35 -0
  14. package/dist/parsers/gemini.js +2 -20
  15. package/dist/parsers/index.js +22 -3
  16. package/dist/parsers/types.js +0 -1
  17. package/dist/public/assets/index-Coilyhtr.css +1 -0
  18. package/dist/public/assets/index-D0noVMFu.js +44 -0
  19. package/dist/public/index.html +2 -2
  20. package/dist/redact.js +4 -104
  21. package/dist/render/build-render-data.js +9 -2
  22. package/dist/render/index.js +32 -5
  23. package/dist/render/liquid.js +147 -7
  24. package/dist/render/mock-data.js +303 -0
  25. package/dist/render/templates/aurora/portfolio.liquid +192 -0
  26. package/dist/render/templates/aurora/project.liquid +260 -0
  27. package/dist/render/templates/aurora/session.liquid +223 -0
  28. package/dist/render/templates/aurora/styles.css +1184 -0
  29. package/dist/render/templates/bauhaus/portfolio.liquid +169 -0
  30. package/dist/render/templates/bauhaus/project.liquid +300 -0
  31. package/dist/render/templates/bauhaus/session.liquid +333 -0
  32. package/dist/render/templates/bauhaus/styles.css +1645 -0
  33. package/dist/render/templates/blueprint/portfolio.liquid +153 -0
  34. package/dist/render/templates/blueprint/project.liquid +286 -0
  35. package/dist/render/templates/blueprint/session.liquid +248 -0
  36. package/dist/render/templates/blueprint/styles.css +1289 -0
  37. package/dist/render/templates/canvas/portfolio.liquid +203 -0
  38. package/dist/render/templates/canvas/project.liquid +235 -0
  39. package/dist/render/templates/canvas/session.liquid +223 -0
  40. package/dist/render/templates/canvas/styles.css +1440 -0
  41. package/dist/render/templates/carbon/portfolio.liquid +160 -0
  42. package/dist/render/templates/carbon/project.liquid +249 -0
  43. package/dist/render/templates/carbon/session.liquid +190 -0
  44. package/dist/render/templates/carbon/styles.css +1097 -0
  45. package/dist/render/templates/chalk/portfolio.liquid +189 -0
  46. package/dist/render/templates/chalk/project.liquid +245 -0
  47. package/dist/render/templates/chalk/session.liquid +215 -0
  48. package/dist/render/templates/chalk/styles.css +1161 -0
  49. package/dist/render/templates/circuit/portfolio.liquid +152 -0
  50. package/dist/render/templates/circuit/project.liquid +247 -0
  51. package/dist/render/templates/circuit/session.liquid +205 -0
  52. package/dist/render/templates/circuit/styles.css +1409 -0
  53. package/dist/render/templates/cosmos/portfolio.liquid +222 -0
  54. package/dist/render/templates/cosmos/project.liquid +327 -0
  55. package/dist/render/templates/cosmos/session.liquid +239 -0
  56. package/dist/render/templates/cosmos/styles.css +1157 -0
  57. package/dist/render/templates/daylight/portfolio.liquid +207 -0
  58. package/dist/render/templates/daylight/project.liquid +229 -0
  59. package/dist/render/templates/daylight/session.liquid +219 -0
  60. package/dist/render/templates/daylight/styles.css +1315 -0
  61. package/dist/render/templates/editorial/portfolio.liquid +110 -0
  62. package/dist/render/templates/editorial/project.liquid +202 -0
  63. package/dist/render/templates/editorial/session.liquid +171 -0
  64. package/dist/render/templates/editorial/styles.css +826 -0
  65. package/dist/render/templates/ember/portfolio.liquid +306 -0
  66. package/dist/render/templates/ember/project.liquid +232 -0
  67. package/dist/render/templates/ember/session.liquid +202 -0
  68. package/dist/render/templates/ember/styles.css +1289 -0
  69. package/dist/render/templates/glacier/portfolio.liquid +261 -0
  70. package/dist/render/templates/glacier/project.liquid +288 -0
  71. package/dist/render/templates/glacier/session.liquid +217 -0
  72. package/dist/render/templates/glacier/styles.css +1204 -0
  73. package/dist/render/templates/grid/portfolio.liquid +255 -0
  74. package/dist/render/templates/grid/project.liquid +306 -0
  75. package/dist/render/templates/grid/session.liquid +260 -0
  76. package/dist/render/templates/grid/styles.css +1445 -0
  77. package/dist/render/templates/kinetic/portfolio.liquid +158 -0
  78. package/dist/render/templates/kinetic/project.liquid +242 -0
  79. package/dist/render/templates/kinetic/session.liquid +228 -0
  80. package/dist/render/templates/kinetic/styles.css +948 -0
  81. package/dist/render/templates/meridian/portfolio.liquid +243 -0
  82. package/dist/render/templates/meridian/project.liquid +376 -0
  83. package/dist/render/templates/meridian/session.liquid +298 -0
  84. package/dist/render/templates/meridian/styles.css +1375 -0
  85. package/dist/render/templates/minimal/portfolio.liquid +71 -0
  86. package/dist/render/templates/minimal/project.liquid +154 -0
  87. package/dist/render/templates/minimal/session.liquid +140 -0
  88. package/dist/render/templates/minimal/styles.css +529 -0
  89. package/dist/render/templates/mono/portfolio.liquid +281 -0
  90. package/dist/render/templates/mono/project.liquid +275 -0
  91. package/dist/render/templates/mono/session.liquid +276 -0
  92. package/dist/render/templates/mono/styles.css +1022 -0
  93. package/dist/render/templates/neon/portfolio.liquid +207 -0
  94. package/dist/render/templates/neon/project.liquid +225 -0
  95. package/dist/render/templates/neon/session.liquid +195 -0
  96. package/dist/render/templates/neon/styles.css +1271 -0
  97. package/dist/render/templates/noir/portfolio.liquid +137 -0
  98. package/dist/render/templates/noir/project.liquid +220 -0
  99. package/dist/render/templates/noir/session.liquid +241 -0
  100. package/dist/render/templates/noir/styles.css +1229 -0
  101. package/dist/render/templates/obsidian/portfolio.liquid +247 -0
  102. package/dist/render/templates/obsidian/project.liquid +280 -0
  103. package/dist/render/templates/obsidian/session.liquid +241 -0
  104. package/dist/render/templates/obsidian/styles.css +1407 -0
  105. package/dist/render/templates/paper/portfolio.liquid +257 -0
  106. package/dist/render/templates/paper/project.liquid +235 -0
  107. package/dist/render/templates/paper/session.liquid +271 -0
  108. package/dist/render/templates/paper/styles.css +1513 -0
  109. package/dist/render/templates/parallax/portfolio.liquid +295 -0
  110. package/dist/render/templates/parallax/project.liquid +275 -0
  111. package/dist/render/templates/parallax/session.liquid +295 -0
  112. package/dist/render/templates/parallax/styles.css +1880 -0
  113. package/dist/render/templates/parchment/portfolio.liquid +280 -0
  114. package/dist/render/templates/parchment/project.liquid +289 -0
  115. package/dist/render/templates/parchment/session.liquid +346 -0
  116. package/dist/render/templates/parchment/styles.css +1401 -0
  117. package/dist/render/templates/partials/_beats.liquid +16 -0
  118. package/dist/render/templates/partials/_breadcrumb.liquid +9 -0
  119. package/dist/render/templates/partials/_footer.liquid +7 -0
  120. package/dist/render/templates/partials/_growth-chart.liquid +7 -0
  121. package/dist/render/templates/partials/_key-decisions.liquid +20 -0
  122. package/dist/render/templates/partials/_links.liquid +16 -0
  123. package/dist/render/templates/partials/_narrative.liquid +8 -0
  124. package/dist/render/templates/partials/_phases.liquid +20 -0
  125. package/dist/render/templates/partials/_portfolio-header.liquid +20 -0
  126. package/dist/render/templates/partials/_portfolio-projects.liquid +16 -0
  127. package/dist/render/templates/partials/_portfolio-stats.liquid +19 -0
  128. package/dist/render/templates/partials/_qa.liquid +13 -0
  129. package/dist/render/templates/partials/_screenshot.liquid +15 -0
  130. package/dist/render/templates/partials/_session-cards.liquid +30 -0
  131. package/dist/render/templates/partials/_session-header.liquid +39 -0
  132. package/dist/render/templates/partials/_session-sidebar.liquid +30 -0
  133. package/dist/render/templates/partials/_skills.liquid +12 -0
  134. package/dist/render/templates/partials/_source-breakdown.liquid +22 -0
  135. package/dist/render/templates/partials/_stats.liquid +38 -0
  136. package/dist/render/templates/partials/_work-timeline.liquid +7 -0
  137. package/dist/render/templates/project.liquid +7 -4
  138. package/dist/render/templates/radar/portfolio.liquid +223 -0
  139. package/dist/render/templates/radar/project.liquid +278 -0
  140. package/dist/render/templates/radar/session.liquid +300 -0
  141. package/dist/render/templates/radar/styles.css +1055 -0
  142. package/dist/render/templates/showcase/portfolio.liquid +221 -0
  143. package/dist/render/templates/showcase/project.liquid +237 -0
  144. package/dist/render/templates/showcase/session.liquid +210 -0
  145. package/dist/render/templates/showcase/styles.css +1284 -0
  146. package/dist/render/templates/signal/portfolio.liquid +217 -0
  147. package/dist/render/templates/signal/project.liquid +278 -0
  148. package/dist/render/templates/signal/session.liquid +282 -0
  149. package/dist/render/templates/signal/styles.css +1401 -0
  150. package/dist/render/templates/strata/portfolio.liquid +180 -0
  151. package/dist/render/templates/strata/project.liquid +282 -0
  152. package/dist/render/templates/strata/session.liquid +261 -0
  153. package/dist/render/templates/strata/styles.css +1354 -0
  154. package/dist/render/templates/styles.css +1190 -0
  155. package/dist/render/templates/terminal/portfolio.liquid +102 -0
  156. package/dist/render/templates/terminal/project.liquid +161 -0
  157. package/dist/render/templates/terminal/session.liquid +145 -0
  158. package/dist/render/templates/terminal/styles.css +497 -0
  159. package/dist/render/templates/verdant/portfolio.liquid +321 -0
  160. package/dist/render/templates/verdant/project.liquid +309 -0
  161. package/dist/render/templates/verdant/session.liquid +237 -0
  162. package/dist/render/templates/verdant/styles.css +1261 -0
  163. package/dist/render/templates/zen/portfolio.liquid +124 -0
  164. package/dist/render/templates/zen/project.liquid +187 -0
  165. package/dist/render/templates/zen/session.liquid +203 -0
  166. package/dist/render/templates/zen/styles.css +1211 -0
  167. package/dist/render/templates.js +90 -0
  168. package/dist/routes/auth.js +7 -3
  169. package/dist/routes/context.js +17 -10
  170. package/dist/routes/delete.js +195 -0
  171. package/dist/routes/enhance.js +57 -40
  172. package/dist/routes/export.js +14 -4
  173. package/dist/routes/github.js +254 -0
  174. package/dist/routes/index.js +2 -0
  175. package/dist/routes/portfolio-render-data.js +160 -0
  176. package/dist/routes/preview.js +555 -108
  177. package/dist/routes/projects.js +61 -24
  178. package/dist/routes/publish.js +320 -31
  179. package/dist/routes/settings.js +194 -1
  180. package/dist/routes/sse.js +9 -0
  181. package/dist/search.js +6 -0
  182. package/dist/server.js +11 -3
  183. package/dist/settings.js +112 -9
  184. package/package.json +3 -4
  185. package/dist/public/assets/index-CC9G8EF1.js +0 -21
  186. package/dist/public/assets/index-Dalqz2mC.css +0 -1
@@ -1,6 +1,11 @@
1
1
  import { Router } from 'express';
2
- import { saveAnthropicApiKey, clearAnthropicApiKey, getAnthropicApiKey } from '../settings.js';
2
+ import { existsSync } from 'node:fs';
3
+ import { saveAnthropicApiKey, clearAnthropicApiKey, getAnthropicApiKey, getSettings, setDefaultTemplate, getPortfolioProfile, savePortfolioProfile, isTranscriptIncluded, setTranscriptIncluded } from '../settings.js';
4
+ import { invalidatePortfolioPreviewCache } from './preview.js';
3
5
  import { hasApiKey } from '../llm/index.js';
6
+ import { isValidTemplate, DEFAULT_TEMPLATE, BUILT_IN_TEMPLATES } from '../render/templates.js';
7
+ import { getDbPath } from '../db.js';
8
+ import { getDaemonBinaryPath } from '../daemon-install.js';
4
9
  export function createSettingsRouter(_ctx) {
5
10
  const router = Router();
6
11
  // Save or clear the Anthropic API key
@@ -25,5 +30,193 @@ export function createSettingsRouter(_ctx) {
25
30
  maskedKey: key ? `...${key.slice(-4)}` : null,
26
31
  });
27
32
  });
33
+ // List available templates
34
+ router.get('/api/templates', (_req, res) => {
35
+ const templates = BUILT_IN_TEMPLATES.map((t) => ({
36
+ name: t.name,
37
+ label: t.label,
38
+ description: t.description,
39
+ accent: t.accent,
40
+ mode: t.mode,
41
+ tags: t.tags,
42
+ builtIn: true,
43
+ }));
44
+ res.json({ templates });
45
+ });
46
+ // Get current portfolio theme
47
+ router.get('/api/settings/theme', (_req, res) => {
48
+ const settings = getSettings();
49
+ res.json({ template: settings.defaultTemplate ?? DEFAULT_TEMPLATE });
50
+ });
51
+ // Set portfolio theme
52
+ router.post('/api/settings/theme', (req, res) => {
53
+ const { template } = req.body;
54
+ if (!template || !isValidTemplate(template)) {
55
+ res.status(400).json({ error: 'Invalid template name' });
56
+ return;
57
+ }
58
+ setDefaultTemplate(template);
59
+ invalidatePortfolioPreviewCache();
60
+ console.log(`[settings] Portfolio theme set to: ${template}`);
61
+ res.json({ ok: true, template });
62
+ });
63
+ // ── Per-session transcript toggle (publish-time, CLI-only) ───
64
+ //
65
+ // GET /api/sessions/:sessionId/transcript-setting — returns whether the
66
+ // session transcript will be included in the next publish. Defaults to
67
+ // true.
68
+ //
69
+ // PUT /api/sessions/:sessionId/transcript-setting { included: boolean }
70
+ // — flips the flag. When `false`, publish.ts skips all S3 transcript
71
+ // uploads and strips transcript-derived fields from the uploaded
72
+ // session JSON.
73
+ router.get('/api/sessions/:sessionId/transcript-setting', (req, res) => {
74
+ const sessionId = typeof req.params.sessionId === 'string' ? req.params.sessionId.trim() : '';
75
+ if (!sessionId || sessionId.length > 200) {
76
+ res.status(400).json({ error: { code: 'INVALID_PARAM', message: 'sessionId is required' } });
77
+ return;
78
+ }
79
+ res.json({ sessionId, included: isTranscriptIncluded(sessionId) });
80
+ });
81
+ router.put('/api/sessions/:sessionId/transcript-setting', (req, res) => {
82
+ const sessionId = typeof req.params.sessionId === 'string' ? req.params.sessionId.trim() : '';
83
+ if (!sessionId || sessionId.length > 200) {
84
+ res.status(400).json({ error: { code: 'INVALID_PARAM', message: 'sessionId is required' } });
85
+ return;
86
+ }
87
+ const body = req.body;
88
+ if (typeof body?.included !== 'boolean') {
89
+ res.status(400).json({ error: { code: 'INVALID_BODY', message: 'included must be a boolean' } });
90
+ return;
91
+ }
92
+ setTranscriptIncluded(sessionId, body.included);
93
+ res.json({ ok: true, sessionId, included: body.included });
94
+ });
95
+ // Get portfolio profile data
96
+ router.get('/api/portfolio', (_req, res) => {
97
+ res.json(getPortfolioProfile());
98
+ });
99
+ // Save portfolio profile data
100
+ router.post('/api/portfolio', (req, res) => {
101
+ const body = req.body;
102
+ if (!body || typeof body !== 'object' || Array.isArray(body)) {
103
+ res.status(400).json({ error: { code: 'INVALID_BODY', message: 'Request body must be a JSON object' } });
104
+ return;
105
+ }
106
+ const ALLOWED_FIELDS = [
107
+ 'displayName', 'bio', 'photoBase64', 'location', 'email', 'phone',
108
+ 'linkedinUrl', 'githubUrl', 'twitterHandle', 'websiteUrl',
109
+ 'resumeBase64', 'resumeFilename', 'accent',
110
+ ];
111
+ const errors = [];
112
+ // Structural validation: only allow known string fields
113
+ const cleaned = {};
114
+ for (const key of ALLOWED_FIELDS) {
115
+ const val = body[key];
116
+ if (val === undefined || val === null || val === '')
117
+ continue;
118
+ if (typeof val !== 'string') {
119
+ errors.push({ field: key, message: `${key} must be a string` });
120
+ continue;
121
+ }
122
+ cleaned[key] = val;
123
+ }
124
+ // Length limits
125
+ if (cleaned.displayName && cleaned.displayName.length > 200) {
126
+ errors.push({ field: 'displayName', message: 'Display name must be under 200 characters' });
127
+ }
128
+ if (cleaned.bio && cleaned.bio.length > 2000) {
129
+ errors.push({ field: 'bio', message: 'Bio must be under 2000 characters' });
130
+ }
131
+ if (cleaned.email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(cleaned.email)) {
132
+ errors.push({ field: 'email', message: 'Invalid email format' });
133
+ }
134
+ if (cleaned.linkedinUrl && !cleaned.linkedinUrl.startsWith('http')) {
135
+ errors.push({ field: 'linkedinUrl', message: 'LinkedIn URL must start with http' });
136
+ }
137
+ if (cleaned.githubUrl && !cleaned.githubUrl.startsWith('http')) {
138
+ errors.push({ field: 'githubUrl', message: 'GitHub URL must start with http' });
139
+ }
140
+ if (cleaned.websiteUrl && !cleaned.websiteUrl.startsWith('http')) {
141
+ errors.push({ field: 'websiteUrl', message: 'Website URL must start with http' });
142
+ }
143
+ if (cleaned.accent && !/^#[0-9a-fA-F]{6}$/.test(cleaned.accent)) {
144
+ errors.push({ field: 'accent', message: 'Accent must be a 6-digit hex color (e.g. #084471)' });
145
+ }
146
+ // File size limits (base64 ~1.37x raw; cap photo at ~5MB, resume at ~10MB)
147
+ if (cleaned.photoBase64 && cleaned.photoBase64.length > 7_000_000) {
148
+ errors.push({ field: 'photoBase64', message: 'Photo must be under 5MB' });
149
+ }
150
+ if (cleaned.resumeBase64 && cleaned.resumeBase64.length > 14_000_000) {
151
+ errors.push({ field: 'resumeBase64', message: 'Resume must be under 10MB' });
152
+ }
153
+ // projectsOnPortfolio: array of {projectId, included, order}. Validated
154
+ // structurally; unknown entries are dropped silently. Empty array allowed
155
+ // (means "default ordering, include everything" at render time).
156
+ if (body.projectsOnPortfolio !== undefined) {
157
+ if (!Array.isArray(body.projectsOnPortfolio)) {
158
+ errors.push({ field: 'projectsOnPortfolio', message: 'projectsOnPortfolio must be an array' });
159
+ }
160
+ else {
161
+ const cleanedProjects = [];
162
+ for (let i = 0; i < body.projectsOnPortfolio.length; i++) {
163
+ const raw = body.projectsOnPortfolio[i];
164
+ if (!raw || typeof raw !== 'object') {
165
+ errors.push({ field: `projectsOnPortfolio[${i}]`, message: 'must be an object' });
166
+ continue;
167
+ }
168
+ if (typeof raw.projectId !== 'string' || raw.projectId.length === 0) {
169
+ errors.push({ field: `projectsOnPortfolio[${i}].projectId`, message: 'must be a non-empty string' });
170
+ continue;
171
+ }
172
+ if (typeof raw.included !== 'boolean') {
173
+ errors.push({ field: `projectsOnPortfolio[${i}].included`, message: 'must be a boolean' });
174
+ continue;
175
+ }
176
+ if (typeof raw.order !== 'number' || !Number.isFinite(raw.order)) {
177
+ errors.push({ field: `projectsOnPortfolio[${i}].order`, message: 'must be a finite number' });
178
+ continue;
179
+ }
180
+ cleanedProjects.push({
181
+ projectId: raw.projectId,
182
+ included: raw.included,
183
+ order: raw.order,
184
+ });
185
+ }
186
+ cleaned.projectsOnPortfolio = cleanedProjects;
187
+ }
188
+ }
189
+ if (errors.length > 0) {
190
+ res.status(400).json({ error: { code: 'VALIDATION_ERROR', message: 'Validation failed', fields: errors } });
191
+ return;
192
+ }
193
+ savePortfolioProfile(cleaned);
194
+ invalidatePortfolioPreviewCache();
195
+ console.log('[settings] Portfolio profile saved');
196
+ res.json({ ok: true });
197
+ });
198
+ // Local data summary: read-only diagnostic info displayed in Settings
199
+ // (DB path, daemon install state). Archive count + last sync are served
200
+ // by /api/archive/stats so the frontend composes both responses.
201
+ router.get('/api/local-data', (_req, res) => {
202
+ try {
203
+ const dbPath = getDbPath();
204
+ const daemonBinaryPath = getDaemonBinaryPath();
205
+ const daemonInstalled = existsSync(daemonBinaryPath);
206
+ res.json({
207
+ dbPath,
208
+ daemon: {
209
+ installed: daemonInstalled,
210
+ binaryPath: daemonBinaryPath,
211
+ },
212
+ });
213
+ }
214
+ catch (err) {
215
+ console.error('[local-data]', err.message);
216
+ res.status(500).json({
217
+ error: { code: 'LOCAL_DATA_FAILED', message: 'Failed to read local data summary' },
218
+ });
219
+ }
220
+ });
28
221
  return router;
29
222
  }
@@ -0,0 +1,9 @@
1
+ /** Set up an SSE response and return a typed send helper. */
2
+ export function startSSE(res) {
3
+ res.writeHead(200, {
4
+ 'Content-Type': 'text/event-stream',
5
+ 'Cache-Control': 'no-cache',
6
+ Connection: 'keep-alive',
7
+ });
8
+ return (data) => res.write(`data: ${JSON.stringify(data)}\n\n`);
9
+ }
package/dist/search.js CHANGED
@@ -7,6 +7,12 @@ import { escapeLikeWildcards } from './format-utils.js';
7
7
  * e.g. "-Users-test-Dev-myapp" → "Users/test/Dev/myapp"
8
8
  */
9
9
  export function decodeProjectName(projectDir) {
10
+ // Windows: "C-Users-ben-Dev-myapp" → "C:/Users/ben/Dev/myapp"
11
+ const winMatch = projectDir.match(/^([A-Z])-(.+)$/);
12
+ if (winMatch) {
13
+ return `${winMatch[1]}:/${winMatch[2].replace(/-/g, "/")}`;
14
+ }
15
+ // Unix: "-Users-ben-Dev-myapp" → "Users/ben/Dev/myapp"
10
16
  return projectDir.replace(/^-/, '').replace(/-/g, '/');
11
17
  }
12
18
  function rowToResult(row, snippet, score) {
package/dist/server.js CHANGED
@@ -7,7 +7,7 @@ import { fileURLToPath } from 'node:url';
7
7
  import { homedir } from 'node:os';
8
8
  import { getDatabase, closeDatabase } from './db.js';
9
9
  import { syncWithTracking, startFileWatcher, startCursorPolling, markSyncPending } from './sync.js';
10
- import { createRouteContext, createProjectsRouter, createEnhanceRouter, createPublishRouter, createSearchRouter, createSessionsRouter, createArchiveRouter, createAuthRouter, createSettingsRouter, createExportRouter, createPreviewRouter, createDashboardRouter, } from './routes/index.js';
10
+ import { createRouteContext, createProjectsRouter, createEnhanceRouter, createPublishRouter, createDeleteRouter, createSearchRouter, createSessionsRouter, createArchiveRouter, createAuthRouter, createSettingsRouter, createExportRouter, createPreviewRouter, createDashboardRouter, createGithubRouter, } from './routes/index.js';
11
11
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
12
12
  function getPackageVersion() {
13
13
  try {
@@ -81,9 +81,15 @@ export function createApp(sessionsBasePath, dbPath) {
81
81
  if (process.env.NODE_ENV !== 'production')
82
82
  corsOrigins.push('http://localhost:5173');
83
83
  app.use(cors({ origin: corsOrigins }));
84
- app.use((_req, res, next) => {
84
+ app.use((req, res, next) => {
85
85
  res.setHeader('X-Content-Type-Options', 'nosniff');
86
- res.setHeader('X-Frame-Options', 'DENY');
86
+ // Allow same-origin framing for preview pages (used by template browser iframes)
87
+ if (req.path.startsWith('/preview/')) {
88
+ res.setHeader('X-Frame-Options', 'SAMEORIGIN');
89
+ }
90
+ else {
91
+ res.setHeader('X-Frame-Options', 'DENY');
92
+ }
87
93
  next();
88
94
  });
89
95
  app.use(express.json({ limit: '10mb' }));
@@ -91,6 +97,7 @@ export function createApp(sessionsBasePath, dbPath) {
91
97
  app.use(createProjectsRouter(ctx));
92
98
  app.use(createEnhanceRouter(ctx));
93
99
  app.use(createPublishRouter(ctx));
100
+ app.use(createDeleteRouter(ctx));
94
101
  app.use(createSearchRouter(ctx));
95
102
  app.use(createSessionsRouter(ctx));
96
103
  app.use(createArchiveRouter(ctx));
@@ -99,6 +106,7 @@ export function createApp(sessionsBasePath, dbPath) {
99
106
  app.use(createExportRouter(ctx));
100
107
  app.use(createPreviewRouter(ctx));
101
108
  app.use(createDashboardRouter(ctx));
109
+ app.use(createGithubRouter(ctx));
102
110
  // ── Version endpoint (used by `heyiam open` to detect stale instances) ──
103
111
  app.get('/api/version', (_req, res) => {
104
112
  res.json({ server: 'heyiam', version: SERVER_VERSION });
package/dist/settings.js CHANGED
@@ -34,6 +34,14 @@ export function clearAnthropicApiKey(configDir) {
34
34
  delete settings.anthropicApiKey;
35
35
  writeConfig(SETTINGS_FILE, settings, configDir);
36
36
  }
37
+ export function getDefaultTemplate(configDir) {
38
+ return getSettings(configDir).defaultTemplate;
39
+ }
40
+ export function setDefaultTemplate(templateName, configDir) {
41
+ const settings = getSettings(configDir);
42
+ settings.defaultTemplate = templateName;
43
+ writeConfig(SETTINGS_FILE, settings, configDir);
44
+ }
37
45
  export function isOnboardingComplete(configDir) {
38
46
  return !!getSettings(configDir).onboardingCompletedAt;
39
47
  }
@@ -47,6 +55,43 @@ export function resetOnboarding(configDir) {
47
55
  delete settings.onboardingCompletedAt;
48
56
  writeConfig(SETTINGS_FILE, settings, configDir);
49
57
  }
58
+ // ── Per-session transcript visibility ────────────────────────
59
+ /**
60
+ * Return whether the session transcript should be included at publish time.
61
+ * Default is `true` — users must opt out explicitly.
62
+ */
63
+ export function isTranscriptIncluded(sessionId, configDir) {
64
+ const map = getSettings(configDir).transcriptIncluded ?? {};
65
+ const flag = map[sessionId];
66
+ return flag !== false;
67
+ }
68
+ /**
69
+ * Set the include-transcript flag for a single session. Persisted to the
70
+ * settings file alongside other user preferences.
71
+ */
72
+ export function setTranscriptIncluded(sessionId, included, configDir) {
73
+ const settings = getSettings(configDir);
74
+ const map = { ...(settings.transcriptIncluded ?? {}) };
75
+ if (included) {
76
+ // Default is `true`, so a `true` value is the same as absent — keep
77
+ // the map clean by deleting rather than writing redundant entries.
78
+ delete map[sessionId];
79
+ }
80
+ else {
81
+ map[sessionId] = false;
82
+ }
83
+ settings.transcriptIncluded = map;
84
+ writeConfig(SETTINGS_FILE, settings, configDir);
85
+ }
86
+ // ── Portfolio profile ────────────────────────────────────────
87
+ export function getPortfolioProfile(configDir) {
88
+ return getSettings(configDir).portfolio ?? {};
89
+ }
90
+ export function savePortfolioProfile(data, configDir) {
91
+ const settings = getSettings(configDir);
92
+ settings.portfolio = data;
93
+ writeConfig(SETTINGS_FILE, settings, configDir);
94
+ }
50
95
  /**
51
96
  * Returns the Anthropic API key from settings file or env var.
52
97
  * Env var takes precedence.
@@ -81,15 +126,6 @@ export function loadEnhancedData(sessionId, configDir) {
81
126
  return null;
82
127
  }
83
128
  }
84
- export function markAsUploaded(sessionId, configDir) {
85
- const data = loadEnhancedData(sessionId, configDir);
86
- if (!data)
87
- return;
88
- data.uploaded = true;
89
- const dir = enhancedDir(configDir);
90
- mkdirSync(dir, { recursive: true });
91
- writeFileSync(enhancedPath(sessionId, configDir), JSON.stringify(data, null, 2), { mode: 0o600 });
92
- }
93
129
  export function deleteEnhancedData(sessionId, configDir) {
94
130
  const path = enhancedPath(sessionId, configDir);
95
131
  if (existsSync(path))
@@ -188,3 +224,70 @@ export function getUploadedState(projectDirName, configDir) {
188
224
  return null;
189
225
  }
190
226
  }
227
+ /**
228
+ * Remove the local uploaded-state record for a project. Used when the
229
+ * remote copy is deleted from heyi.am — clears the "Uploaded" badge and
230
+ * per-session `uploaded: true` flags so the UI reflects reality without
231
+ * requiring a new publish round-trip.
232
+ */
233
+ export function clearUploadedState(projectDirName, configDir) {
234
+ const path = uploadedPath(projectDirName, configDir);
235
+ if (existsSync(path))
236
+ unlinkSync(path);
237
+ }
238
+ const PORTFOLIO_PUBLISH_FILE = 'portfolio-publish.json';
239
+ const DEFAULT_PORTFOLIO_TARGET = 'heyi.am';
240
+ function portfolioPublishPath(configDir = getDataDir()) {
241
+ return join(configDir, PORTFOLIO_PUBLISH_FILE);
242
+ }
243
+ /**
244
+ * Compute a stable hash of a portfolio profile snapshot. Keys are sorted
245
+ * recursively so logically-equal profiles always produce the same hash.
246
+ * Used for draft detection — no cryptographic guarantees required.
247
+ */
248
+ export function hashPortfolioProfile(profile) {
249
+ const canonical = canonicalStringify(profile);
250
+ return createHash('sha256').update(canonical).digest('hex').slice(0, 16);
251
+ }
252
+ function canonicalStringify(value) {
253
+ if (value === null || typeof value !== 'object')
254
+ return JSON.stringify(value);
255
+ if (Array.isArray(value))
256
+ return `[${value.map(canonicalStringify).join(',')}]`;
257
+ const obj = value;
258
+ const keys = Object.keys(obj).filter((k) => obj[k] !== undefined).sort();
259
+ return `{${keys.map((k) => `${JSON.stringify(k)}:${canonicalStringify(obj[k])}`).join(',')}}`;
260
+ }
261
+ export function getPortfolioPublishState(configDir) {
262
+ const path = portfolioPublishPath(configDir);
263
+ if (!existsSync(path))
264
+ return { targets: {} };
265
+ try {
266
+ const parsed = JSON.parse(readFileSync(path, 'utf-8'));
267
+ return parsed && typeof parsed === 'object' && parsed.targets ? parsed : { targets: {} };
268
+ }
269
+ catch {
270
+ return { targets: {} };
271
+ }
272
+ }
273
+ export function savePortfolioPublishState(state, configDir) {
274
+ const dir = configDir ?? getDataDir();
275
+ mkdirSync(dir, { recursive: true });
276
+ writeFileSync(portfolioPublishPath(configDir), JSON.stringify(state, null, 2), { mode: 0o600 });
277
+ }
278
+ /** Update (or create) a single target entry in the publish state. */
279
+ export function updatePortfolioPublishTarget(target, patch, configDir) {
280
+ const state = getPortfolioPublishState(configDir);
281
+ const existing = state.targets[target];
282
+ const base = existing ?? {
283
+ lastPublishedAt: '',
284
+ lastPublishedProfileHash: '',
285
+ lastPublishedProfile: {},
286
+ config: {},
287
+ visibility: target === DEFAULT_PORTFOLIO_TARGET ? 'public' : undefined,
288
+ };
289
+ state.targets[target] = { ...base, ...patch };
290
+ savePortfolioPublishState(state, configDir);
291
+ return state;
292
+ }
293
+ export { DEFAULT_PORTFOLIO_TARGET };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "heyiam",
3
- "version": "0.2.29",
3
+ "version": "0.3.1",
4
4
  "description": "Turn AI coding sessions into portfolio case studies",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -31,17 +31,16 @@
31
31
  "dev:api": "tsx watch src/index.ts open --no-open",
32
32
  "dev:app": "cd app && npm run dev",
33
33
  "test": "vitest run && cd app && npx vitest run",
34
- "test:backend": "vitest run",
34
+ "test:backend": "vitest run --exclude src/build-integrity.test.ts",
35
35
  "test:frontend": "cd app && npx vitest run"
36
36
  },
37
37
  "dependencies": {
38
38
  "@anthropic-ai/sdk": "^0.79.0",
39
- "@secretlint/node": "^11.4.0",
40
- "@secretlint/secretlint-rule-preset-recommend": "^11.4.0",
41
39
  "better-sqlite3": "^12.8.0",
42
40
  "commander": "^13.1.0",
43
41
  "cors": "^2.8.6",
44
42
  "express": "^5.2.1",
43
+ "keytar": "^7.9.0",
45
44
  "liquidjs": "^10.25.2",
46
45
  "open": "^10.1.0"
47
46
  },