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.
- package/README.md +45 -0
- package/dist/config.js +10 -1
- package/dist/db.js +1 -2
- package/dist/export.js +40 -25
- package/dist/format-utils.js +5 -0
- package/dist/index.js +168 -0
- package/dist/mount.js +300 -102
- package/dist/parsers/claude.js +2 -28
- package/dist/parsers/codex.js +2 -26
- package/dist/parsers/cursor.js +2 -26
- package/dist/parsers/duration.js +35 -0
- package/dist/parsers/gemini.js +2 -20
- package/dist/parsers/types.js +0 -1
- package/dist/public/assets/index-BZ65TU_Y.js +40 -0
- package/dist/public/assets/index-CqCaW2cb.css +1 -0
- package/dist/public/index.html +2 -2
- package/dist/redact.js +4 -104
- package/dist/render/build-render-data.js +9 -2
- package/dist/render/index.js +32 -5
- package/dist/render/liquid.js +147 -7
- package/dist/render/mock-data.js +303 -0
- package/dist/render/templates/aurora/portfolio.liquid +204 -0
- package/dist/render/templates/aurora/project.liquid +260 -0
- package/dist/render/templates/aurora/session.liquid +223 -0
- package/dist/render/templates/aurora/styles.css +1178 -0
- package/dist/render/templates/bauhaus/portfolio.liquid +179 -0
- package/dist/render/templates/bauhaus/project.liquid +300 -0
- package/dist/render/templates/bauhaus/session.liquid +333 -0
- package/dist/render/templates/bauhaus/styles.css +1641 -0
- package/dist/render/templates/blueprint/portfolio.liquid +167 -0
- package/dist/render/templates/blueprint/project.liquid +286 -0
- package/dist/render/templates/blueprint/session.liquid +248 -0
- package/dist/render/templates/blueprint/styles.css +1285 -0
- package/dist/render/templates/canvas/portfolio.liquid +215 -0
- package/dist/render/templates/canvas/project.liquid +235 -0
- package/dist/render/templates/canvas/session.liquid +223 -0
- package/dist/render/templates/canvas/styles.css +1436 -0
- package/dist/render/templates/carbon/portfolio.liquid +170 -0
- package/dist/render/templates/carbon/project.liquid +249 -0
- package/dist/render/templates/carbon/session.liquid +190 -0
- package/dist/render/templates/carbon/styles.css +1091 -0
- package/dist/render/templates/chalk/portfolio.liquid +199 -0
- package/dist/render/templates/chalk/project.liquid +245 -0
- package/dist/render/templates/chalk/session.liquid +215 -0
- package/dist/render/templates/chalk/styles.css +1157 -0
- package/dist/render/templates/circuit/portfolio.liquid +162 -0
- package/dist/render/templates/circuit/project.liquid +247 -0
- package/dist/render/templates/circuit/session.liquid +205 -0
- package/dist/render/templates/circuit/styles.css +1403 -0
- package/dist/render/templates/cosmos/portfolio.liquid +232 -0
- package/dist/render/templates/cosmos/project.liquid +327 -0
- package/dist/render/templates/cosmos/session.liquid +239 -0
- package/dist/render/templates/cosmos/styles.css +1151 -0
- package/dist/render/templates/daylight/portfolio.liquid +217 -0
- package/dist/render/templates/daylight/project.liquid +229 -0
- package/dist/render/templates/daylight/session.liquid +219 -0
- package/dist/render/templates/daylight/styles.css +1311 -0
- package/dist/render/templates/editorial/portfolio.liquid +126 -0
- package/dist/render/templates/editorial/project.liquid +202 -0
- package/dist/render/templates/editorial/session.liquid +171 -0
- package/dist/render/templates/editorial/styles.css +822 -0
- package/dist/render/templates/ember/portfolio.liquid +318 -0
- package/dist/render/templates/ember/project.liquid +232 -0
- package/dist/render/templates/ember/session.liquid +202 -0
- package/dist/render/templates/ember/styles.css +1283 -0
- package/dist/render/templates/glacier/portfolio.liquid +271 -0
- package/dist/render/templates/glacier/project.liquid +288 -0
- package/dist/render/templates/glacier/session.liquid +217 -0
- package/dist/render/templates/glacier/styles.css +1200 -0
- package/dist/render/templates/grid/portfolio.liquid +265 -0
- package/dist/render/templates/grid/project.liquid +306 -0
- package/dist/render/templates/grid/session.liquid +260 -0
- package/dist/render/templates/grid/styles.css +1441 -0
- package/dist/render/templates/kinetic/portfolio.liquid +170 -0
- package/dist/render/templates/kinetic/project.liquid +242 -0
- package/dist/render/templates/kinetic/session.liquid +228 -0
- package/dist/render/templates/kinetic/styles.css +944 -0
- package/dist/render/templates/meridian/portfolio.liquid +255 -0
- package/dist/render/templates/meridian/project.liquid +376 -0
- package/dist/render/templates/meridian/session.liquid +298 -0
- package/dist/render/templates/meridian/styles.css +1369 -0
- package/dist/render/templates/minimal/portfolio.liquid +71 -0
- package/dist/render/templates/minimal/project.liquid +154 -0
- package/dist/render/templates/minimal/session.liquid +140 -0
- package/dist/render/templates/minimal/styles.css +525 -0
- package/dist/render/templates/mono/portfolio.liquid +291 -0
- package/dist/render/templates/mono/project.liquid +275 -0
- package/dist/render/templates/mono/session.liquid +276 -0
- package/dist/render/templates/mono/styles.css +1016 -0
- package/dist/render/templates/neon/portfolio.liquid +217 -0
- package/dist/render/templates/neon/project.liquid +225 -0
- package/dist/render/templates/neon/session.liquid +195 -0
- package/dist/render/templates/neon/styles.css +1265 -0
- package/dist/render/templates/noir/portfolio.liquid +137 -0
- package/dist/render/templates/noir/project.liquid +220 -0
- package/dist/render/templates/noir/session.liquid +241 -0
- package/dist/render/templates/noir/styles.css +1223 -0
- package/dist/render/templates/obsidian/portfolio.liquid +257 -0
- package/dist/render/templates/obsidian/project.liquid +280 -0
- package/dist/render/templates/obsidian/session.liquid +241 -0
- package/dist/render/templates/obsidian/styles.css +1401 -0
- package/dist/render/templates/paper/portfolio.liquid +267 -0
- package/dist/render/templates/paper/project.liquid +235 -0
- package/dist/render/templates/paper/session.liquid +271 -0
- package/dist/render/templates/paper/styles.css +1509 -0
- package/dist/render/templates/parallax/portfolio.liquid +305 -0
- package/dist/render/templates/parallax/project.liquid +275 -0
- package/dist/render/templates/parallax/session.liquid +295 -0
- package/dist/render/templates/parallax/styles.css +1874 -0
- package/dist/render/templates/parchment/portfolio.liquid +290 -0
- package/dist/render/templates/parchment/project.liquid +289 -0
- package/dist/render/templates/parchment/session.liquid +346 -0
- package/dist/render/templates/parchment/styles.css +1397 -0
- package/dist/render/templates/partials/_beats.liquid +16 -0
- package/dist/render/templates/partials/_breadcrumb.liquid +9 -0
- package/dist/render/templates/partials/_footer.liquid +7 -0
- package/dist/render/templates/partials/_growth-chart.liquid +7 -0
- package/dist/render/templates/partials/_key-decisions.liquid +20 -0
- package/dist/render/templates/partials/_links.liquid +16 -0
- package/dist/render/templates/partials/_narrative.liquid +8 -0
- package/dist/render/templates/partials/_phases.liquid +20 -0
- package/dist/render/templates/partials/_portfolio-header.liquid +20 -0
- package/dist/render/templates/partials/_portfolio-projects.liquid +16 -0
- package/dist/render/templates/partials/_portfolio-stats.liquid +19 -0
- package/dist/render/templates/partials/_qa.liquid +13 -0
- package/dist/render/templates/partials/_screenshot.liquid +15 -0
- package/dist/render/templates/partials/_session-cards.liquid +30 -0
- package/dist/render/templates/partials/_session-header.liquid +39 -0
- package/dist/render/templates/partials/_session-sidebar.liquid +30 -0
- package/dist/render/templates/partials/_skills.liquid +12 -0
- package/dist/render/templates/partials/_source-breakdown.liquid +22 -0
- package/dist/render/templates/partials/_stats.liquid +38 -0
- package/dist/render/templates/partials/_work-timeline.liquid +7 -0
- package/dist/render/templates/project.liquid +7 -4
- package/dist/render/templates/radar/portfolio.liquid +233 -0
- package/dist/render/templates/radar/project.liquid +278 -0
- package/dist/render/templates/radar/session.liquid +300 -0
- package/dist/render/templates/radar/styles.css +1049 -0
- package/dist/render/templates/showcase/portfolio.liquid +231 -0
- package/dist/render/templates/showcase/project.liquid +237 -0
- package/dist/render/templates/showcase/session.liquid +210 -0
- package/dist/render/templates/showcase/styles.css +1279 -0
- package/dist/render/templates/signal/portfolio.liquid +227 -0
- package/dist/render/templates/signal/project.liquid +278 -0
- package/dist/render/templates/signal/session.liquid +282 -0
- package/dist/render/templates/signal/styles.css +1395 -0
- package/dist/render/templates/strata/portfolio.liquid +192 -0
- package/dist/render/templates/strata/project.liquid +282 -0
- package/dist/render/templates/strata/session.liquid +261 -0
- package/dist/render/templates/strata/styles.css +1350 -0
- package/dist/render/templates/styles.css +1190 -0
- package/dist/render/templates/terminal/portfolio.liquid +118 -0
- package/dist/render/templates/terminal/project.liquid +161 -0
- package/dist/render/templates/terminal/session.liquid +145 -0
- package/dist/render/templates/terminal/styles.css +492 -0
- package/dist/render/templates/verdant/portfolio.liquid +333 -0
- package/dist/render/templates/verdant/project.liquid +309 -0
- package/dist/render/templates/verdant/session.liquid +237 -0
- package/dist/render/templates/verdant/styles.css +1257 -0
- package/dist/render/templates/zen/portfolio.liquid +136 -0
- package/dist/render/templates/zen/project.liquid +187 -0
- package/dist/render/templates/zen/session.liquid +203 -0
- package/dist/render/templates/zen/styles.css +1207 -0
- package/dist/render/templates.js +90 -0
- package/dist/routes/context.js +15 -10
- package/dist/routes/enhance.js +17 -40
- package/dist/routes/export.js +14 -4
- package/dist/routes/preview.js +480 -108
- package/dist/routes/projects.js +11 -19
- package/dist/routes/publish.js +15 -17
- package/dist/routes/settings.js +94 -1
- package/dist/routes/sse.js +9 -0
- package/dist/server.js +8 -2
- package/dist/settings.js +17 -9
- package/package.json +2 -4
- package/dist/public/assets/index-CC9G8EF1.js +0 -21
- package/dist/public/assets/index-Dalqz2mC.css +0 -1
package/dist/routes/projects.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Router } from 'express';
|
|
2
2
|
import { statSync } from 'node:fs';
|
|
3
|
-
import { buildSessionList, buildProjectDetail } from './context.js';
|
|
3
|
+
import { requireProject, buildSessionList, buildProjectDetail } from './context.js';
|
|
4
4
|
export function createProjectsRouter(ctx) {
|
|
5
5
|
const router = Router();
|
|
6
6
|
router.get('/api/projects', async (_req, res) => {
|
|
@@ -18,13 +18,10 @@ export function createProjectsRouter(ctx) {
|
|
|
18
18
|
// Aggregated project detail for the hub screen
|
|
19
19
|
router.get('/api/projects/:project/detail', async (req, res) => {
|
|
20
20
|
try {
|
|
21
|
-
const
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
if (!proj) {
|
|
25
|
-
res.status(404).json({ error: 'Project not found' });
|
|
21
|
+
const project = String(req.params.project);
|
|
22
|
+
const proj = await requireProject(ctx, project, res);
|
|
23
|
+
if (!proj)
|
|
26
24
|
return;
|
|
27
|
-
}
|
|
28
25
|
res.json(buildProjectDetail(ctx.db, proj));
|
|
29
26
|
}
|
|
30
27
|
catch (err) {
|
|
@@ -95,13 +92,11 @@ export function createProjectsRouter(ctx) {
|
|
|
95
92
|
});
|
|
96
93
|
router.get('/api/projects/:project/sessions/:id', async (req, res) => {
|
|
97
94
|
try {
|
|
98
|
-
const
|
|
99
|
-
const
|
|
100
|
-
const proj =
|
|
101
|
-
if (!proj)
|
|
102
|
-
res.status(404).json({ error: { code: 'PROJECT_NOT_FOUND', message: 'Project not found' } });
|
|
95
|
+
const project = String(req.params.project);
|
|
96
|
+
const id = String(req.params.id);
|
|
97
|
+
const proj = await requireProject(ctx, project, res);
|
|
98
|
+
if (!proj)
|
|
103
99
|
return;
|
|
104
|
-
}
|
|
105
100
|
const meta = proj.sessions.find((s) => s.sessionId === id);
|
|
106
101
|
if (!meta) {
|
|
107
102
|
res.status(404).json({ error: { code: 'SESSION_NOT_FOUND', message: 'Session not found' } });
|
|
@@ -140,13 +135,10 @@ export function createProjectsRouter(ctx) {
|
|
|
140
135
|
router.get('/api/projects/:project/git-remote', async (req, res) => {
|
|
141
136
|
const { execFileSync } = await import('node:child_process');
|
|
142
137
|
try {
|
|
143
|
-
const
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
if (!proj) {
|
|
147
|
-
res.status(404).json({ error: { code: 'PROJECT_NOT_FOUND', message: 'Project not found' } });
|
|
138
|
+
const project = String(req.params.project);
|
|
139
|
+
const proj = await requireProject(ctx, project, res);
|
|
140
|
+
if (!proj)
|
|
148
141
|
return;
|
|
149
|
-
}
|
|
150
142
|
let projectPath = null;
|
|
151
143
|
for (const meta of proj.sessions) {
|
|
152
144
|
try {
|
package/dist/routes/publish.js
CHANGED
|
@@ -2,14 +2,16 @@ import { Router } from 'express';
|
|
|
2
2
|
import { readFileSync } from 'node:fs';
|
|
3
3
|
import { randomUUID } from 'node:crypto';
|
|
4
4
|
import { getAuthToken } from '../auth.js';
|
|
5
|
-
import { API_URL, PUBLIC_URL } from '../config.js';
|
|
6
|
-
import { loadEnhancedData, saveEnhancedData, saveUploadedState } from '../settings.js';
|
|
5
|
+
import { API_URL, PUBLIC_URL, warnIfNonDefaultApiUrl } from '../config.js';
|
|
6
|
+
import { loadEnhancedData, saveEnhancedData, saveUploadedState, getDefaultTemplate } from '../settings.js';
|
|
7
7
|
import { captureScreenshot } from '../screenshot.js';
|
|
8
8
|
import { redactSession, redactText, scanTextSync, formatFindings, stripHomePathsInText } from '../redact.js';
|
|
9
9
|
import { renderProjectHtml, renderSessionHtml } from '../render/index.js';
|
|
10
10
|
import { buildSessionRenderData, buildSessionCard, buildProjectRenderData } from '../render/build-render-data.js';
|
|
11
11
|
import { buildAgentSummary } from './context.js';
|
|
12
|
+
import { startSSE } from './sse.js';
|
|
12
13
|
import { displayNameFromDir } from '../sync.js';
|
|
14
|
+
import { toSlug } from '../format-utils.js';
|
|
13
15
|
import { getProjectUuid, getFileCountWithChildren } from '../db.js';
|
|
14
16
|
const IMAGE_KEY_PREFIX = 'images/';
|
|
15
17
|
export function createPublishRouter(ctx) {
|
|
@@ -32,7 +34,8 @@ export function createPublishRouter(ctx) {
|
|
|
32
34
|
totalTokens,
|
|
33
35
|
sessionCards: sessionCards || [],
|
|
34
36
|
});
|
|
35
|
-
const
|
|
37
|
+
const templateName = getDefaultTemplate() || 'editorial';
|
|
38
|
+
const html = renderProjectHtml(renderData, undefined, templateName);
|
|
36
39
|
res.json({ html });
|
|
37
40
|
}
|
|
38
41
|
catch (err) {
|
|
@@ -65,6 +68,7 @@ export function createPublishRouter(ctx) {
|
|
|
65
68
|
router.post('/api/projects/:project/upload', async (req, res) => {
|
|
66
69
|
const { project } = req.params;
|
|
67
70
|
const auth = getAuthToken();
|
|
71
|
+
warnIfNonDefaultApiUrl();
|
|
68
72
|
if (!auth) {
|
|
69
73
|
res.status(401).json({ error: { message: 'Authentication required' } });
|
|
70
74
|
return;
|
|
@@ -72,18 +76,11 @@ export function createPublishRouter(ctx) {
|
|
|
72
76
|
const { title: rawTitle, slug: rawSlug, narrative, repoUrl, projectUrl, timeline, skills, totalSessions, totalLoc, totalDurationMinutes, totalAgentDurationMinutes, totalFilesChanged, skippedSessions, selectedSessionIds, screenshotBase64, } = req.body;
|
|
73
77
|
// Ensure slug is the short project name, not the full encoded directory path
|
|
74
78
|
const shortName = displayNameFromDir(String(project));
|
|
75
|
-
const baseSlug = shortName
|
|
79
|
+
const baseSlug = toSlug(shortName);
|
|
76
80
|
const title = rawTitle === rawSlug ? shortName : rawTitle;
|
|
77
81
|
// Get stable project UUID from CLI database
|
|
78
82
|
const clientProjectId = getProjectUuid(ctx.db, String(project));
|
|
79
|
-
res
|
|
80
|
-
'Content-Type': 'text/event-stream',
|
|
81
|
-
'Cache-Control': 'no-cache',
|
|
82
|
-
Connection: 'keep-alive',
|
|
83
|
-
});
|
|
84
|
-
const send = (data) => {
|
|
85
|
-
res.write(`data: ${JSON.stringify(data)}\n\n`);
|
|
86
|
-
};
|
|
83
|
+
const send = startSSE(res);
|
|
87
84
|
try {
|
|
88
85
|
// Step 1: Upsert project on Phoenix (with slug conflict retry)
|
|
89
86
|
send({ type: 'project', status: 'creating' });
|
|
@@ -207,6 +204,7 @@ export function createPublishRouter(ctx) {
|
|
|
207
204
|
const failedSessions = [];
|
|
208
205
|
const uploadedSessionCards = [];
|
|
209
206
|
if (proj) {
|
|
207
|
+
const selectedTemplate = getDefaultTemplate() || 'editorial';
|
|
210
208
|
for (const sessionId of selectedSessionIds) {
|
|
211
209
|
const meta = proj.sessions.find((s) => s.sessionId === sessionId);
|
|
212
210
|
if (!meta)
|
|
@@ -215,8 +213,7 @@ export function createPublishRouter(ctx) {
|
|
|
215
213
|
try {
|
|
216
214
|
const session = await ctx.loadSession(meta.path, proj.name, sessionId);
|
|
217
215
|
const enhanced = loadEnhancedData(sessionId);
|
|
218
|
-
const sessionSlug = (enhanced?.title ?? session.title ?? sessionId)
|
|
219
|
-
.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '').slice(0, 80);
|
|
216
|
+
const sessionSlug = toSlug(enhanced?.title ?? session.title ?? sessionId, 80);
|
|
220
217
|
const agentSummary = await buildAgentSummary(meta.children ?? [], (c) => ctx.getSessionStats(c, proj.name), { deduplicate: true });
|
|
221
218
|
const devTake = (enhanced?.developerTake ?? session.developerTake ?? '').slice(0, 2000);
|
|
222
219
|
const sessionNarrative = enhanced?.narrative ?? '';
|
|
@@ -233,11 +230,12 @@ export function createPublishRouter(ctx) {
|
|
|
233
230
|
sessionSlug,
|
|
234
231
|
sourceTool: sessionSourceTool,
|
|
235
232
|
agentSummary,
|
|
233
|
+
template: selectedTemplate,
|
|
236
234
|
};
|
|
237
235
|
let sessionRenderedHtml = null;
|
|
238
236
|
try {
|
|
239
237
|
const sessionRenderData = buildSessionRenderData(renderOpts);
|
|
240
|
-
sessionRenderedHtml = renderSessionHtml(sessionRenderData);
|
|
238
|
+
sessionRenderedHtml = renderSessionHtml(sessionRenderData, selectedTemplate);
|
|
241
239
|
}
|
|
242
240
|
catch (renderErr) {
|
|
243
241
|
console.error(`[upload] Session render failed for ${sessionId}:`, renderErr.message);
|
|
@@ -259,7 +257,7 @@ export function createPublishRouter(ctx) {
|
|
|
259
257
|
end_time: session.endTime ? new Date(session.endTime).toISOString() : null,
|
|
260
258
|
cwd: session.cwd ?? null,
|
|
261
259
|
wall_clock_minutes: session.wallClockMinutes ?? null,
|
|
262
|
-
template:
|
|
260
|
+
template: selectedTemplate,
|
|
263
261
|
language: null,
|
|
264
262
|
tools: session.toolBreakdown?.map((t) => t.tool) ?? [],
|
|
265
263
|
skills: sessionSkills,
|
|
@@ -293,7 +291,7 @@ export function createPublishRouter(ctx) {
|
|
|
293
291
|
})(),
|
|
294
292
|
cwd: session.cwd ?? null,
|
|
295
293
|
wall_clock_minutes: session.wallClockMinutes ?? null,
|
|
296
|
-
template:
|
|
294
|
+
template: selectedTemplate,
|
|
297
295
|
skills: sessionSkills,
|
|
298
296
|
tools: session.toolBreakdown?.map((t) => t.tool) ?? [],
|
|
299
297
|
source: sessionSourceTool,
|
package/dist/routes/settings.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Router } from 'express';
|
|
2
|
-
import { saveAnthropicApiKey, clearAnthropicApiKey, getAnthropicApiKey } from '../settings.js';
|
|
2
|
+
import { saveAnthropicApiKey, clearAnthropicApiKey, getAnthropicApiKey, getSettings, setDefaultTemplate, getPortfolioProfile, savePortfolioProfile } from '../settings.js';
|
|
3
3
|
import { hasApiKey } from '../llm/index.js';
|
|
4
|
+
import { isValidTemplate, DEFAULT_TEMPLATE, BUILT_IN_TEMPLATES } from '../render/templates.js';
|
|
4
5
|
export function createSettingsRouter(_ctx) {
|
|
5
6
|
const router = Router();
|
|
6
7
|
// Save or clear the Anthropic API key
|
|
@@ -25,5 +26,97 @@ export function createSettingsRouter(_ctx) {
|
|
|
25
26
|
maskedKey: key ? `...${key.slice(-4)}` : null,
|
|
26
27
|
});
|
|
27
28
|
});
|
|
29
|
+
// List available templates
|
|
30
|
+
router.get('/api/templates', (_req, res) => {
|
|
31
|
+
const templates = BUILT_IN_TEMPLATES.map((t) => ({
|
|
32
|
+
name: t.name,
|
|
33
|
+
label: t.label,
|
|
34
|
+
description: t.description,
|
|
35
|
+
accent: t.accent,
|
|
36
|
+
mode: t.mode,
|
|
37
|
+
tags: t.tags,
|
|
38
|
+
builtIn: true,
|
|
39
|
+
}));
|
|
40
|
+
res.json({ templates });
|
|
41
|
+
});
|
|
42
|
+
// Get current portfolio theme
|
|
43
|
+
router.get('/api/settings/theme', (_req, res) => {
|
|
44
|
+
const settings = getSettings();
|
|
45
|
+
res.json({ template: settings.defaultTemplate ?? DEFAULT_TEMPLATE });
|
|
46
|
+
});
|
|
47
|
+
// Set portfolio theme
|
|
48
|
+
router.post('/api/settings/theme', (req, res) => {
|
|
49
|
+
const { template } = req.body;
|
|
50
|
+
if (!template || !isValidTemplate(template)) {
|
|
51
|
+
res.status(400).json({ error: 'Invalid template name' });
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
setDefaultTemplate(template);
|
|
55
|
+
console.log(`[settings] Portfolio theme set to: ${template}`);
|
|
56
|
+
res.json({ ok: true, template });
|
|
57
|
+
});
|
|
58
|
+
// Get portfolio profile data
|
|
59
|
+
router.get('/api/portfolio', (_req, res) => {
|
|
60
|
+
res.json(getPortfolioProfile());
|
|
61
|
+
});
|
|
62
|
+
// Save portfolio profile data
|
|
63
|
+
router.post('/api/portfolio', (req, res) => {
|
|
64
|
+
const body = req.body;
|
|
65
|
+
if (!body || typeof body !== 'object' || Array.isArray(body)) {
|
|
66
|
+
res.status(400).json({ error: { code: 'INVALID_BODY', message: 'Request body must be a JSON object' } });
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const ALLOWED_FIELDS = [
|
|
70
|
+
'displayName', 'bio', 'photoBase64', 'location', 'email', 'phone',
|
|
71
|
+
'linkedinUrl', 'githubUrl', 'twitterHandle', 'websiteUrl',
|
|
72
|
+
'resumeBase64', 'resumeFilename',
|
|
73
|
+
];
|
|
74
|
+
const errors = [];
|
|
75
|
+
// Structural validation: only allow known string fields
|
|
76
|
+
const cleaned = {};
|
|
77
|
+
for (const key of ALLOWED_FIELDS) {
|
|
78
|
+
const val = body[key];
|
|
79
|
+
if (val === undefined || val === null || val === '')
|
|
80
|
+
continue;
|
|
81
|
+
if (typeof val !== 'string') {
|
|
82
|
+
errors.push({ field: key, message: `${key} must be a string` });
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
cleaned[key] = val;
|
|
86
|
+
}
|
|
87
|
+
// Length limits
|
|
88
|
+
if (cleaned.displayName && cleaned.displayName.length > 200) {
|
|
89
|
+
errors.push({ field: 'displayName', message: 'Display name must be under 200 characters' });
|
|
90
|
+
}
|
|
91
|
+
if (cleaned.bio && cleaned.bio.length > 2000) {
|
|
92
|
+
errors.push({ field: 'bio', message: 'Bio must be under 2000 characters' });
|
|
93
|
+
}
|
|
94
|
+
if (cleaned.email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(cleaned.email)) {
|
|
95
|
+
errors.push({ field: 'email', message: 'Invalid email format' });
|
|
96
|
+
}
|
|
97
|
+
if (cleaned.linkedinUrl && !cleaned.linkedinUrl.startsWith('http')) {
|
|
98
|
+
errors.push({ field: 'linkedinUrl', message: 'LinkedIn URL must start with http' });
|
|
99
|
+
}
|
|
100
|
+
if (cleaned.githubUrl && !cleaned.githubUrl.startsWith('http')) {
|
|
101
|
+
errors.push({ field: 'githubUrl', message: 'GitHub URL must start with http' });
|
|
102
|
+
}
|
|
103
|
+
if (cleaned.websiteUrl && !cleaned.websiteUrl.startsWith('http')) {
|
|
104
|
+
errors.push({ field: 'websiteUrl', message: 'Website URL must start with http' });
|
|
105
|
+
}
|
|
106
|
+
// File size limits (base64 ~1.37x raw; cap photo at ~5MB, resume at ~10MB)
|
|
107
|
+
if (cleaned.photoBase64 && cleaned.photoBase64.length > 7_000_000) {
|
|
108
|
+
errors.push({ field: 'photoBase64', message: 'Photo must be under 5MB' });
|
|
109
|
+
}
|
|
110
|
+
if (cleaned.resumeBase64 && cleaned.resumeBase64.length > 14_000_000) {
|
|
111
|
+
errors.push({ field: 'resumeBase64', message: 'Resume must be under 10MB' });
|
|
112
|
+
}
|
|
113
|
+
if (errors.length > 0) {
|
|
114
|
+
res.status(400).json({ error: { code: 'VALIDATION_ERROR', message: 'Validation failed', fields: errors } });
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
savePortfolioProfile(cleaned);
|
|
118
|
+
console.log('[settings] Portfolio profile saved');
|
|
119
|
+
res.json({ ok: true });
|
|
120
|
+
});
|
|
28
121
|
return router;
|
|
29
122
|
}
|
|
@@ -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/server.js
CHANGED
|
@@ -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((
|
|
84
|
+
app.use((req, res, next) => {
|
|
85
85
|
res.setHeader('X-Content-Type-Options', 'nosniff');
|
|
86
|
-
|
|
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' }));
|
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,15 @@ export function resetOnboarding(configDir) {
|
|
|
47
55
|
delete settings.onboardingCompletedAt;
|
|
48
56
|
writeConfig(SETTINGS_FILE, settings, configDir);
|
|
49
57
|
}
|
|
58
|
+
// ── Portfolio profile ────────────────────────────────────────
|
|
59
|
+
export function getPortfolioProfile(configDir) {
|
|
60
|
+
return getSettings(configDir).portfolio ?? {};
|
|
61
|
+
}
|
|
62
|
+
export function savePortfolioProfile(data, configDir) {
|
|
63
|
+
const settings = getSettings(configDir);
|
|
64
|
+
settings.portfolio = data;
|
|
65
|
+
writeConfig(SETTINGS_FILE, settings, configDir);
|
|
66
|
+
}
|
|
50
67
|
/**
|
|
51
68
|
* Returns the Anthropic API key from settings file or env var.
|
|
52
69
|
* Env var takes precedence.
|
|
@@ -81,15 +98,6 @@ export function loadEnhancedData(sessionId, configDir) {
|
|
|
81
98
|
return null;
|
|
82
99
|
}
|
|
83
100
|
}
|
|
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
101
|
export function deleteEnhancedData(sessionId, configDir) {
|
|
94
102
|
const path = enhancedPath(sessionId, configDir);
|
|
95
103
|
if (existsSync(path))
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "heyiam",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Turn AI coding sessions into portfolio case studies",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -31,13 +31,11 @@
|
|
|
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",
|