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.
- package/README.md +45 -0
- package/dist/auth.js +29 -3
- package/dist/config.js +10 -1
- package/dist/db.js +0 -1
- package/dist/export.js +124 -27
- package/dist/format-utils.js +5 -0
- package/dist/github.js +381 -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/index.js +22 -3
- package/dist/parsers/types.js +0 -1
- package/dist/public/assets/index-Coilyhtr.css +1 -0
- package/dist/public/assets/index-D0noVMFu.js +44 -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 +192 -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 +1184 -0
- package/dist/render/templates/bauhaus/portfolio.liquid +169 -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 +1645 -0
- package/dist/render/templates/blueprint/portfolio.liquid +153 -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 +1289 -0
- package/dist/render/templates/canvas/portfolio.liquid +203 -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 +1440 -0
- package/dist/render/templates/carbon/portfolio.liquid +160 -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 +1097 -0
- package/dist/render/templates/chalk/portfolio.liquid +189 -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 +1161 -0
- package/dist/render/templates/circuit/portfolio.liquid +152 -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 +1409 -0
- package/dist/render/templates/cosmos/portfolio.liquid +222 -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 +1157 -0
- package/dist/render/templates/daylight/portfolio.liquid +207 -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 +1315 -0
- package/dist/render/templates/editorial/portfolio.liquid +110 -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 +826 -0
- package/dist/render/templates/ember/portfolio.liquid +306 -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 +1289 -0
- package/dist/render/templates/glacier/portfolio.liquid +261 -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 +1204 -0
- package/dist/render/templates/grid/portfolio.liquid +255 -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 +1445 -0
- package/dist/render/templates/kinetic/portfolio.liquid +158 -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 +948 -0
- package/dist/render/templates/meridian/portfolio.liquid +243 -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 +1375 -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 +529 -0
- package/dist/render/templates/mono/portfolio.liquid +281 -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 +1022 -0
- package/dist/render/templates/neon/portfolio.liquid +207 -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 +1271 -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 +1229 -0
- package/dist/render/templates/obsidian/portfolio.liquid +247 -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 +1407 -0
- package/dist/render/templates/paper/portfolio.liquid +257 -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 +1513 -0
- package/dist/render/templates/parallax/portfolio.liquid +295 -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 +1880 -0
- package/dist/render/templates/parchment/portfolio.liquid +280 -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 +1401 -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 +223 -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 +1055 -0
- package/dist/render/templates/showcase/portfolio.liquid +221 -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 +1284 -0
- package/dist/render/templates/signal/portfolio.liquid +217 -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 +1401 -0
- package/dist/render/templates/strata/portfolio.liquid +180 -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 +1354 -0
- package/dist/render/templates/styles.css +1190 -0
- package/dist/render/templates/terminal/portfolio.liquid +102 -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 +497 -0
- package/dist/render/templates/verdant/portfolio.liquid +321 -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 +1261 -0
- package/dist/render/templates/zen/portfolio.liquid +124 -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 +1211 -0
- package/dist/render/templates.js +90 -0
- package/dist/routes/auth.js +7 -3
- package/dist/routes/context.js +17 -10
- package/dist/routes/delete.js +195 -0
- package/dist/routes/enhance.js +57 -40
- package/dist/routes/export.js +14 -4
- package/dist/routes/github.js +254 -0
- package/dist/routes/index.js +2 -0
- package/dist/routes/portfolio-render-data.js +160 -0
- package/dist/routes/preview.js +555 -108
- package/dist/routes/projects.js +61 -24
- package/dist/routes/publish.js +320 -31
- package/dist/routes/settings.js +194 -1
- package/dist/routes/sse.js +9 -0
- package/dist/search.js +6 -0
- package/dist/server.js +11 -3
- package/dist/settings.js +112 -9
- package/package.json +3 -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,8 @@
|
|
|
1
1
|
import { Router } from 'express';
|
|
2
2
|
import { statSync } from 'node:fs';
|
|
3
|
-
import {
|
|
3
|
+
import { loadProjectEnhanceResult, saveProjectEnhanceResult } from '../settings.js';
|
|
4
|
+
import { requireProject, buildSessionList, buildProjectDetail } from './context.js';
|
|
5
|
+
import { invalidatePortfolioPreviewCache } from './preview.js';
|
|
4
6
|
export function createProjectsRouter(ctx) {
|
|
5
7
|
const router = Router();
|
|
6
8
|
router.get('/api/projects', async (_req, res) => {
|
|
@@ -18,13 +20,10 @@ export function createProjectsRouter(ctx) {
|
|
|
18
20
|
// Aggregated project detail for the hub screen
|
|
19
21
|
router.get('/api/projects/:project/detail', async (req, res) => {
|
|
20
22
|
try {
|
|
21
|
-
const
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
if (!proj) {
|
|
25
|
-
res.status(404).json({ error: 'Project not found' });
|
|
23
|
+
const project = String(req.params.project);
|
|
24
|
+
const proj = await requireProject(ctx, project, res);
|
|
25
|
+
if (!proj)
|
|
26
26
|
return;
|
|
27
|
-
}
|
|
28
27
|
res.json(buildProjectDetail(ctx.db, proj));
|
|
29
28
|
}
|
|
30
29
|
catch (err) {
|
|
@@ -95,13 +94,11 @@ export function createProjectsRouter(ctx) {
|
|
|
95
94
|
});
|
|
96
95
|
router.get('/api/projects/:project/sessions/:id', async (req, res) => {
|
|
97
96
|
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' } });
|
|
97
|
+
const project = String(req.params.project);
|
|
98
|
+
const id = String(req.params.id);
|
|
99
|
+
const proj = await requireProject(ctx, project, res);
|
|
100
|
+
if (!proj)
|
|
103
101
|
return;
|
|
104
|
-
}
|
|
105
102
|
const meta = proj.sessions.find((s) => s.sessionId === id);
|
|
106
103
|
if (!meta) {
|
|
107
104
|
res.status(404).json({ error: { code: 'SESSION_NOT_FOUND', message: 'Session not found' } });
|
|
@@ -140,13 +137,10 @@ export function createProjectsRouter(ctx) {
|
|
|
140
137
|
router.get('/api/projects/:project/git-remote', async (req, res) => {
|
|
141
138
|
const { execFileSync } = await import('node:child_process');
|
|
142
139
|
try {
|
|
143
|
-
const
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
if (!proj) {
|
|
147
|
-
res.status(404).json({ error: { code: 'PROJECT_NOT_FOUND', message: 'Project not found' } });
|
|
140
|
+
const project = String(req.params.project);
|
|
141
|
+
const proj = await requireProject(ctx, project, res);
|
|
142
|
+
if (!proj)
|
|
148
143
|
return;
|
|
149
|
-
}
|
|
150
144
|
let projectPath = null;
|
|
151
145
|
for (const meta of proj.sessions) {
|
|
152
146
|
try {
|
|
@@ -181,12 +175,55 @@ export function createProjectsRouter(ctx) {
|
|
|
181
175
|
res.status(500).json({ error: { code: 'GIT_REMOTE_FAILED', message: err.message } });
|
|
182
176
|
}
|
|
183
177
|
});
|
|
184
|
-
// ── Boundaries
|
|
185
|
-
router.get('/api/projects/:project/boundaries', (
|
|
186
|
-
|
|
178
|
+
// ── Boundaries — manage which sessions belong to a project ───
|
|
179
|
+
router.get('/api/projects/:project/boundaries', async (req, res) => {
|
|
180
|
+
try {
|
|
181
|
+
const project = String(req.params.project);
|
|
182
|
+
const proj = await requireProject(ctx, project, res);
|
|
183
|
+
if (!proj)
|
|
184
|
+
return;
|
|
185
|
+
const cache = loadProjectEnhanceResult(proj.dirName);
|
|
186
|
+
const allSessionIds = proj.sessions
|
|
187
|
+
.filter((s) => !s.isSubagent)
|
|
188
|
+
.map((s) => s.sessionId);
|
|
189
|
+
res.json({
|
|
190
|
+
selectedSessionIds: cache?.selectedSessionIds ?? [],
|
|
191
|
+
allSessionIds,
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
catch (err) {
|
|
195
|
+
res.status(500).json({ error: { code: 'BOUNDARIES_FAILED', message: err.message } });
|
|
196
|
+
}
|
|
187
197
|
});
|
|
188
|
-
router.put('/api/projects/:project/boundaries', (
|
|
189
|
-
|
|
198
|
+
router.put('/api/projects/:project/boundaries', async (req, res) => {
|
|
199
|
+
try {
|
|
200
|
+
const project = String(req.params.project);
|
|
201
|
+
const proj = await requireProject(ctx, project, res);
|
|
202
|
+
if (!proj)
|
|
203
|
+
return;
|
|
204
|
+
const { selectedSessionIds } = req.body;
|
|
205
|
+
if (!Array.isArray(selectedSessionIds) || selectedSessionIds.length === 0) {
|
|
206
|
+
res.status(400).json({ error: { code: 'INVALID_INPUT', message: 'selectedSessionIds must be a non-empty array' } });
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
const allIds = new Set(proj.sessions.map((s) => s.sessionId));
|
|
210
|
+
const invalid = selectedSessionIds.filter((id) => !allIds.has(id));
|
|
211
|
+
if (invalid.length > 0) {
|
|
212
|
+
res.status(400).json({ error: { code: 'INVALID_SESSION_IDS', message: `Unknown session IDs: ${invalid.join(', ')}` } });
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
const cache = loadProjectEnhanceResult(proj.dirName);
|
|
216
|
+
if (!cache) {
|
|
217
|
+
res.status(400).json({ error: { code: 'NO_CACHE', message: 'Project must be enhanced before managing sessions' } });
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
saveProjectEnhanceResult(proj.dirName, selectedSessionIds, cache.result, undefined, { title: cache.title, repoUrl: cache.repoUrl, projectUrl: cache.projectUrl, screenshotBase64: cache.screenshotBase64 });
|
|
221
|
+
invalidatePortfolioPreviewCache();
|
|
222
|
+
res.json({ ok: true, selectedSessionIds });
|
|
223
|
+
}
|
|
224
|
+
catch (err) {
|
|
225
|
+
res.status(500).json({ error: { code: 'BOUNDARIES_FAILED', message: err.message } });
|
|
226
|
+
}
|
|
190
227
|
});
|
|
191
228
|
return router;
|
|
192
229
|
}
|
package/dist/routes/publish.js
CHANGED
|
@@ -1,15 +1,23 @@
|
|
|
1
1
|
import { Router } from 'express';
|
|
2
|
-
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { readFileSync, mkdtempSync, rmSync, statSync } from 'node:fs';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
3
5
|
import { randomUUID } from 'node:crypto';
|
|
4
6
|
import { getAuthToken } from '../auth.js';
|
|
5
|
-
import { API_URL, PUBLIC_URL } from '../config.js';
|
|
6
|
-
import { loadEnhancedData, saveEnhancedData, saveUploadedState } from '../settings.js';
|
|
7
|
+
import { API_URL, PUBLIC_URL, warnIfNonDefaultApiUrl } from '../config.js';
|
|
8
|
+
import { loadEnhancedData, saveEnhancedData, saveUploadedState, getDefaultTemplate, getPortfolioProfile, hashPortfolioProfile, updatePortfolioPublishTarget, getPortfolioPublishState, isTranscriptIncluded, DEFAULT_PORTFOLIO_TARGET, } from '../settings.js';
|
|
9
|
+
import { generatePortfolioHtmlFragment, generateProjectHtmlFragment, generatePortfolioSite, createZipBuffer } from '../export.js';
|
|
10
|
+
import { buildPortfolioRenderData } from './portfolio-render-data.js';
|
|
11
|
+
import { buildProjectDetail } from './context.js';
|
|
7
12
|
import { captureScreenshot } from '../screenshot.js';
|
|
8
13
|
import { redactSession, redactText, scanTextSync, formatFindings, stripHomePathsInText } from '../redact.js';
|
|
9
14
|
import { renderProjectHtml, renderSessionHtml } from '../render/index.js';
|
|
10
15
|
import { buildSessionRenderData, buildSessionCard, buildProjectRenderData } from '../render/build-render-data.js';
|
|
11
16
|
import { buildAgentSummary } from './context.js';
|
|
17
|
+
import { invalidatePortfolioPreviewCache } from './preview.js';
|
|
18
|
+
import { startSSE } from './sse.js';
|
|
12
19
|
import { displayNameFromDir } from '../sync.js';
|
|
20
|
+
import { toSlug } from '../format-utils.js';
|
|
13
21
|
import { getProjectUuid, getFileCountWithChildren } from '../db.js';
|
|
14
22
|
const IMAGE_KEY_PREFIX = 'images/';
|
|
15
23
|
export function createPublishRouter(ctx) {
|
|
@@ -32,7 +40,8 @@ export function createPublishRouter(ctx) {
|
|
|
32
40
|
totalTokens,
|
|
33
41
|
sessionCards: sessionCards || [],
|
|
34
42
|
});
|
|
35
|
-
const
|
|
43
|
+
const templateName = getDefaultTemplate() || 'editorial';
|
|
44
|
+
const html = renderProjectHtml(renderData, undefined, templateName);
|
|
36
45
|
res.json({ html });
|
|
37
46
|
}
|
|
38
47
|
catch (err) {
|
|
@@ -55,6 +64,8 @@ export function createPublishRouter(ctx) {
|
|
|
55
64
|
}
|
|
56
65
|
const imageData = readFileSync(screenshotPath);
|
|
57
66
|
const base64 = imageData.toString('base64');
|
|
67
|
+
// Portfolio listing shows project screenshots — bust the cache.
|
|
68
|
+
invalidatePortfolioPreviewCache();
|
|
58
69
|
res.json({ ok: true, preview: `data:image/png;base64,${base64}` });
|
|
59
70
|
}
|
|
60
71
|
catch (err) {
|
|
@@ -65,6 +76,7 @@ export function createPublishRouter(ctx) {
|
|
|
65
76
|
router.post('/api/projects/:project/upload', async (req, res) => {
|
|
66
77
|
const { project } = req.params;
|
|
67
78
|
const auth = getAuthToken();
|
|
79
|
+
warnIfNonDefaultApiUrl();
|
|
68
80
|
if (!auth) {
|
|
69
81
|
res.status(401).json({ error: { message: 'Authentication required' } });
|
|
70
82
|
return;
|
|
@@ -72,18 +84,11 @@ export function createPublishRouter(ctx) {
|
|
|
72
84
|
const { title: rawTitle, slug: rawSlug, narrative, repoUrl, projectUrl, timeline, skills, totalSessions, totalLoc, totalDurationMinutes, totalAgentDurationMinutes, totalFilesChanged, skippedSessions, selectedSessionIds, screenshotBase64, } = req.body;
|
|
73
85
|
// Ensure slug is the short project name, not the full encoded directory path
|
|
74
86
|
const shortName = displayNameFromDir(String(project));
|
|
75
|
-
const baseSlug = shortName
|
|
87
|
+
const baseSlug = toSlug(shortName);
|
|
76
88
|
const title = rawTitle === rawSlug ? shortName : rawTitle;
|
|
77
89
|
// Get stable project UUID from CLI database
|
|
78
90
|
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
|
-
};
|
|
91
|
+
const send = startSSE(res);
|
|
87
92
|
try {
|
|
88
93
|
// Step 1: Upsert project on Phoenix (with slug conflict retry)
|
|
89
94
|
send({ type: 'project', status: 'creating' });
|
|
@@ -207,6 +212,7 @@ export function createPublishRouter(ctx) {
|
|
|
207
212
|
const failedSessions = [];
|
|
208
213
|
const uploadedSessionCards = [];
|
|
209
214
|
if (proj) {
|
|
215
|
+
const selectedTemplate = getDefaultTemplate() || 'editorial';
|
|
210
216
|
for (const sessionId of selectedSessionIds) {
|
|
211
217
|
const meta = proj.sessions.find((s) => s.sessionId === sessionId);
|
|
212
218
|
if (!meta)
|
|
@@ -215,8 +221,8 @@ export function createPublishRouter(ctx) {
|
|
|
215
221
|
try {
|
|
216
222
|
const session = await ctx.loadSession(meta.path, proj.name, sessionId);
|
|
217
223
|
const enhanced = loadEnhancedData(sessionId);
|
|
218
|
-
const sessionSlug = (enhanced?.title ?? session.title ?? sessionId)
|
|
219
|
-
|
|
224
|
+
const sessionSlug = toSlug(enhanced?.title ?? session.title ?? sessionId, 80);
|
|
225
|
+
const includeTranscript = isTranscriptIncluded(sessionId);
|
|
220
226
|
const agentSummary = await buildAgentSummary(meta.children ?? [], (c) => ctx.getSessionStats(c, proj.name), { deduplicate: true });
|
|
221
227
|
const devTake = (enhanced?.developerTake ?? session.developerTake ?? '').slice(0, 2000);
|
|
222
228
|
const sessionNarrative = enhanced?.narrative ?? '';
|
|
@@ -233,11 +239,12 @@ export function createPublishRouter(ctx) {
|
|
|
233
239
|
sessionSlug,
|
|
234
240
|
sourceTool: sessionSourceTool,
|
|
235
241
|
agentSummary,
|
|
242
|
+
template: selectedTemplate,
|
|
236
243
|
};
|
|
237
244
|
let sessionRenderedHtml = null;
|
|
238
245
|
try {
|
|
239
246
|
const sessionRenderData = buildSessionRenderData(renderOpts);
|
|
240
|
-
sessionRenderedHtml = renderSessionHtml(sessionRenderData);
|
|
247
|
+
sessionRenderedHtml = renderSessionHtml(sessionRenderData, selectedTemplate);
|
|
241
248
|
}
|
|
242
249
|
catch (renderErr) {
|
|
243
250
|
console.error(`[upload] Session render failed for ${sessionId}:`, renderErr.message);
|
|
@@ -259,7 +266,7 @@ export function createPublishRouter(ctx) {
|
|
|
259
266
|
end_time: session.endTime ? new Date(session.endTime).toISOString() : null,
|
|
260
267
|
cwd: session.cwd ?? null,
|
|
261
268
|
wall_clock_minutes: session.wallClockMinutes ?? null,
|
|
262
|
-
template:
|
|
269
|
+
template: selectedTemplate,
|
|
263
270
|
language: null,
|
|
264
271
|
tools: session.toolBreakdown?.map((t) => t.tool) ?? [],
|
|
265
272
|
skills: sessionSkills,
|
|
@@ -273,6 +280,22 @@ export function createPublishRouter(ctx) {
|
|
|
273
280
|
rendered_html: sessionRenderedHtml,
|
|
274
281
|
},
|
|
275
282
|
};
|
|
283
|
+
// Transcript-derived fields. When the user has toggled the
|
|
284
|
+
// transcript OFF for this session we omit them from the
|
|
285
|
+
// uploaded JSON entirely — see the spread below. The
|
|
286
|
+
// session's main payload (dev take, skills, Q&A, etc.) is
|
|
287
|
+
// unaffected.
|
|
288
|
+
const turnTimeline = (session.turnTimeline ?? []).map((t) => ({
|
|
289
|
+
timestamp: t.timestamp,
|
|
290
|
+
type: t.type,
|
|
291
|
+
content: (t.content ?? '').slice(0, 200),
|
|
292
|
+
tools: t.tools ?? [],
|
|
293
|
+
}));
|
|
294
|
+
const transcriptExcerpt = (session.rawLog ?? []).slice(0, 10).map((line, i) => {
|
|
295
|
+
const role = line.startsWith('> ') ? 'dev' : 'ai';
|
|
296
|
+
const text = role === 'dev' ? line.slice(2) : line;
|
|
297
|
+
return { role, id: `Turn ${i + 1}`, text, timestamp: null };
|
|
298
|
+
});
|
|
276
299
|
const sessionData = {
|
|
277
300
|
version: 1,
|
|
278
301
|
id: sessionId,
|
|
@@ -293,7 +316,7 @@ export function createPublishRouter(ctx) {
|
|
|
293
316
|
})(),
|
|
294
317
|
cwd: session.cwd ?? null,
|
|
295
318
|
wall_clock_minutes: session.wallClockMinutes ?? null,
|
|
296
|
-
template:
|
|
319
|
+
template: selectedTemplate,
|
|
297
320
|
skills: sessionSkills,
|
|
298
321
|
tools: session.toolBreakdown?.map((t) => t.tool) ?? [],
|
|
299
322
|
source: sessionSourceTool,
|
|
@@ -310,17 +333,8 @@ export function createPublishRouter(ctx) {
|
|
|
310
333
|
highlights: [],
|
|
311
334
|
tool_breakdown: (session.toolBreakdown ?? []).map((t) => ({ tool: t.tool, count: t.count })),
|
|
312
335
|
top_files: (session.filesChanged ?? []).slice(0, 20).map((f) => (typeof f === 'string' ? { path: f, additions: 0, deletions: 0 } : f)),
|
|
313
|
-
turn_timeline:
|
|
314
|
-
|
|
315
|
-
type: t.type,
|
|
316
|
-
content: (t.content ?? '').slice(0, 200),
|
|
317
|
-
tools: t.tools ?? [],
|
|
318
|
-
})),
|
|
319
|
-
transcript_excerpt: (session.rawLog ?? []).slice(0, 10).map((line, i) => {
|
|
320
|
-
const role = line.startsWith('> ') ? 'dev' : 'ai';
|
|
321
|
-
const text = role === 'dev' ? line.slice(2) : line;
|
|
322
|
-
return { role, id: `Turn ${i + 1}`, text, timestamp: null };
|
|
323
|
-
}),
|
|
336
|
+
...(includeTranscript ? { turn_timeline: turnTimeline } : {}),
|
|
337
|
+
...(includeTranscript ? { transcript_excerpt: transcriptExcerpt } : {}),
|
|
324
338
|
agent_summary: agentSummary,
|
|
325
339
|
children: agentSummary?.agents?.map((a) => ({
|
|
326
340
|
sessionId: a.role,
|
|
@@ -349,7 +363,12 @@ export function createPublishRouter(ctx) {
|
|
|
349
363
|
uploadedCount++;
|
|
350
364
|
try {
|
|
351
365
|
const sesData = await sessionRes.json();
|
|
352
|
-
if (sesData.upload_urls) {
|
|
366
|
+
if (sesData.upload_urls && includeTranscript) {
|
|
367
|
+
// Transcript-bearing S3 uploads. Skipped entirely when
|
|
368
|
+
// the user has toggled the transcript OFF for this
|
|
369
|
+
// session — the server never sees raw_log, turn
|
|
370
|
+
// timeline, or the bundled session-data JSON in that
|
|
371
|
+
// case.
|
|
353
372
|
const { raw: rawUrl, log: logUrl } = sesData.upload_urls;
|
|
354
373
|
if (rawUrl && meta.path && !meta.path.startsWith('cursor://')) {
|
|
355
374
|
try {
|
|
@@ -425,7 +444,14 @@ export function createPublishRouter(ctx) {
|
|
|
425
444
|
const screenshotUrl = uploadedImageKey
|
|
426
445
|
? `${PUBLIC_URL}/_img/${uploadedImageKey.replace(IMAGE_KEY_PREFIX, '')}`
|
|
427
446
|
: undefined;
|
|
428
|
-
const
|
|
447
|
+
const detailProj = detail.project;
|
|
448
|
+
const projectHtml = generateProjectHtmlFragment(String(project), enrichedCache, detail.sessions, auth.username, {
|
|
449
|
+
totalFilesChanged: totalFiles,
|
|
450
|
+
title,
|
|
451
|
+
screenshotUrl,
|
|
452
|
+
totalInputTokens: detailProj.totalInputTokens,
|
|
453
|
+
totalOutputTokens: detailProj.totalOutputTokens,
|
|
454
|
+
});
|
|
429
455
|
const renderRes = await fetch(`${API_URL}/api/projects`, {
|
|
430
456
|
method: 'POST',
|
|
431
457
|
headers: {
|
|
@@ -492,5 +518,268 @@ export function createPublishRouter(ctx) {
|
|
|
492
518
|
res.end();
|
|
493
519
|
}
|
|
494
520
|
});
|
|
521
|
+
// Read the current portfolio publish state (per-target snapshots, hashes,
|
|
522
|
+
// last errors, visibility). Authenticated via the same Bearer pattern as
|
|
523
|
+
// the upload route — the state file lives in the user's local config dir
|
|
524
|
+
// but it still references the signed-in identity, so don't leak it to
|
|
525
|
+
// unauthenticated callers.
|
|
526
|
+
router.get('/api/portfolio/state', async (_req, res) => {
|
|
527
|
+
const auth = getAuthToken();
|
|
528
|
+
if (!auth) {
|
|
529
|
+
res.status(401).json({
|
|
530
|
+
error: { code: 'UNAUTHENTICATED', message: 'Authentication required' },
|
|
531
|
+
});
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
try {
|
|
535
|
+
const state = getPortfolioPublishState();
|
|
536
|
+
res.json(state);
|
|
537
|
+
}
|
|
538
|
+
catch (err) {
|
|
539
|
+
const message = err.message;
|
|
540
|
+
console.error('[portfolio-state] Error:', message);
|
|
541
|
+
res.status(500).json({
|
|
542
|
+
error: { code: 'PORTFOLIO_STATE_READ_FAILED', message },
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
});
|
|
546
|
+
// Publish portfolio landing page to heyi.am
|
|
547
|
+
// Renders the portfolio HTML fragment locally, POSTs it to Phoenix,
|
|
548
|
+
// and persists the published snapshot + hash to settings on success.
|
|
549
|
+
router.post('/api/portfolio/upload', async (req, res) => {
|
|
550
|
+
const auth = getAuthToken();
|
|
551
|
+
warnIfNonDefaultApiUrl();
|
|
552
|
+
if (!auth) {
|
|
553
|
+
res.status(401).json({
|
|
554
|
+
error: { code: 'UNAUTHENTICATED', message: 'Authentication required' },
|
|
555
|
+
});
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
try {
|
|
559
|
+
const profile = getPortfolioProfile();
|
|
560
|
+
const templateName = getDefaultTemplate() || 'editorial';
|
|
561
|
+
const { renderData, filteredProjects } = await buildPortfolioRenderData(ctx, auth);
|
|
562
|
+
const renderedHtml = generatePortfolioHtmlFragment(renderData, templateName);
|
|
563
|
+
// Upload individual project pages for every project included in the
|
|
564
|
+
// portfolio. This ensures project detail pages exist on heyi.am even
|
|
565
|
+
// if the user never published them individually.
|
|
566
|
+
for (const rawProj of filteredProjects) {
|
|
567
|
+
try {
|
|
568
|
+
const detail = buildProjectDetail(ctx.db, rawProj);
|
|
569
|
+
const proj = detail.project;
|
|
570
|
+
const cache = detail.enhanceCache
|
|
571
|
+
?? { fingerprint: 'portfolio-upload', enhancedAt: new Date().toISOString(), selectedSessionIds: detail.sessions.map((s) => s.id), result: { narrative: '', arc: [], skills: [], timeline: [], questions: [] } };
|
|
572
|
+
const title = cache.title
|
|
573
|
+
|| proj.name || displayNameFromDir(rawProj.dirName);
|
|
574
|
+
const slug = toSlug(title);
|
|
575
|
+
const clientProjectId = getProjectUuid(ctx.db, rawProj.dirName);
|
|
576
|
+
const projectHtml = generateProjectHtmlFragment(rawProj.dirName, cache, detail.sessions, auth.username, {
|
|
577
|
+
totalFilesChanged: proj.totalFiles,
|
|
578
|
+
totalAgentDurationMinutes: proj.totalAgentDuration,
|
|
579
|
+
totalInputTokens: proj.totalInputTokens,
|
|
580
|
+
totalOutputTokens: proj.totalOutputTokens,
|
|
581
|
+
});
|
|
582
|
+
await fetch(`${API_URL}/api/projects`, {
|
|
583
|
+
method: 'POST',
|
|
584
|
+
headers: {
|
|
585
|
+
'Content-Type': 'application/json',
|
|
586
|
+
Authorization: `Bearer ${auth.token}`,
|
|
587
|
+
},
|
|
588
|
+
body: JSON.stringify({
|
|
589
|
+
project: {
|
|
590
|
+
client_project_id: clientProjectId,
|
|
591
|
+
title, slug,
|
|
592
|
+
narrative: cache.result?.narrative ?? '',
|
|
593
|
+
skills: cache.result?.skills ?? [],
|
|
594
|
+
total_sessions: proj.sessionCount,
|
|
595
|
+
total_loc: proj.totalLoc,
|
|
596
|
+
total_duration_minutes: proj.totalDuration,
|
|
597
|
+
total_agent_duration_minutes: proj.totalAgentDuration || null,
|
|
598
|
+
total_files_changed: proj.totalFiles,
|
|
599
|
+
total_input_tokens: proj.totalInputTokens,
|
|
600
|
+
total_output_tokens: proj.totalOutputTokens,
|
|
601
|
+
rendered_html: projectHtml,
|
|
602
|
+
},
|
|
603
|
+
}),
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
catch (projErr) {
|
|
607
|
+
console.warn(`[portfolio-upload] skipping project ${rawProj.dirName}:`, projErr.message);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
// POST portfolio landing page to Phoenix.
|
|
611
|
+
// Phoenix sanitizes the HTML, persists to users.rendered_portfolio_html,
|
|
612
|
+
// and applies the optional profile snapshot via Accounts.update_user_profile.
|
|
613
|
+
const phoenixRes = await fetch(`${API_URL}/api/portfolio/upload`, {
|
|
614
|
+
method: 'POST',
|
|
615
|
+
headers: {
|
|
616
|
+
'Content-Type': 'application/json',
|
|
617
|
+
Authorization: `Bearer ${auth.token}`,
|
|
618
|
+
},
|
|
619
|
+
body: JSON.stringify({
|
|
620
|
+
html: renderedHtml,
|
|
621
|
+
profile,
|
|
622
|
+
}),
|
|
623
|
+
});
|
|
624
|
+
if (!phoenixRes.ok) {
|
|
625
|
+
const errBody = await phoenixRes.json().catch(() => null);
|
|
626
|
+
const rawErr = errBody && typeof errBody === 'object'
|
|
627
|
+
? errBody.error
|
|
628
|
+
: null;
|
|
629
|
+
const errMsg = typeof rawErr === 'string'
|
|
630
|
+
? rawErr
|
|
631
|
+
: (rawErr && typeof rawErr === 'object' && 'message' in rawErr)
|
|
632
|
+
? rawErr.message
|
|
633
|
+
: `HTTP ${phoenixRes.status}`;
|
|
634
|
+
updatePortfolioPublishTarget(DEFAULT_PORTFOLIO_TARGET, {
|
|
635
|
+
lastError: errMsg,
|
|
636
|
+
lastErrorAt: new Date().toISOString(),
|
|
637
|
+
});
|
|
638
|
+
res.status(phoenixRes.status >= 500 ? 502 : phoenixRes.status).json({
|
|
639
|
+
error: { code: 'PORTFOLIO_UPLOAD_FAILED', message: errMsg },
|
|
640
|
+
});
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
const okBody = await phoenixRes.json().catch(() => ({}));
|
|
644
|
+
const publishedUrl = okBody.username ? `${API_URL}/${okBody.username}` : undefined;
|
|
645
|
+
const publishedAt = new Date().toISOString();
|
|
646
|
+
const hash = hashPortfolioProfile(profile);
|
|
647
|
+
updatePortfolioPublishTarget(DEFAULT_PORTFOLIO_TARGET, {
|
|
648
|
+
lastPublishedAt: publishedAt,
|
|
649
|
+
lastPublishedProfileHash: hash,
|
|
650
|
+
lastPublishedProfile: profile,
|
|
651
|
+
config: {},
|
|
652
|
+
url: publishedUrl,
|
|
653
|
+
lastError: undefined,
|
|
654
|
+
lastErrorAt: undefined,
|
|
655
|
+
});
|
|
656
|
+
// Published version of the portfolio just changed — drop any cached
|
|
657
|
+
// /preview/portfolio HTML so the next preview reflects reality.
|
|
658
|
+
invalidatePortfolioPreviewCache();
|
|
659
|
+
res.json({
|
|
660
|
+
ok: true,
|
|
661
|
+
url: publishedUrl ?? `${API_URL}/${auth.username}`,
|
|
662
|
+
publishedAt,
|
|
663
|
+
hash,
|
|
664
|
+
});
|
|
665
|
+
}
|
|
666
|
+
catch (err) {
|
|
667
|
+
const errMsg = err.message;
|
|
668
|
+
console.error('[portfolio-upload] Error:', errMsg);
|
|
669
|
+
try {
|
|
670
|
+
updatePortfolioPublishTarget(DEFAULT_PORTFOLIO_TARGET, {
|
|
671
|
+
lastError: errMsg,
|
|
672
|
+
lastErrorAt: new Date().toISOString(),
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
catch { /* don't mask the original error */ }
|
|
676
|
+
res.status(500).json({
|
|
677
|
+
error: { code: 'PORTFOLIO_UPLOAD_FAILED', message: errMsg },
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
});
|
|
681
|
+
// Export the portfolio as a downloadable .zip file.
|
|
682
|
+
//
|
|
683
|
+
// No path arg from the client. We render the portfolio site into a temp
|
|
684
|
+
// directory, zip it via createZipBuffer, stream the zip back as an
|
|
685
|
+
// attachment, then clean up the temp dir. Mirrors the existing
|
|
686
|
+
// single-project HTML download pattern (cli/src/routes/export.ts).
|
|
687
|
+
router.post('/api/portfolio/export', async (_req, res) => {
|
|
688
|
+
const auth = getAuthToken();
|
|
689
|
+
if (!auth) {
|
|
690
|
+
res.status(401).json({
|
|
691
|
+
error: { code: 'UNAUTHENTICATED', message: 'Authentication required' },
|
|
692
|
+
});
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
let tmpDir = null;
|
|
696
|
+
try {
|
|
697
|
+
const templateName = getDefaultTemplate() || 'editorial';
|
|
698
|
+
const { renderData } = await buildPortfolioRenderData(ctx, auth);
|
|
699
|
+
// Build per-project inputs for generatePortfolioSite. Each project
|
|
700
|
+
// needs its full session list + enhance cache — use the same
|
|
701
|
+
// buildProjectDetail path the single-project HTML export uses so
|
|
702
|
+
// there's exactly one "load everything about a project" helper.
|
|
703
|
+
const rawProjects = await ctx.getProjects();
|
|
704
|
+
const projectInputs = [];
|
|
705
|
+
for (const rawProj of rawProjects) {
|
|
706
|
+
try {
|
|
707
|
+
const detail = buildProjectDetail(ctx.db, rawProj);
|
|
708
|
+
const cache = detail.enhanceCache ?? {
|
|
709
|
+
fingerprint: 'export',
|
|
710
|
+
enhancedAt: new Date().toISOString(),
|
|
711
|
+
selectedSessionIds: detail.sessions.map((s) => s.id),
|
|
712
|
+
result: {
|
|
713
|
+
narrative: '',
|
|
714
|
+
arc: [],
|
|
715
|
+
skills: [],
|
|
716
|
+
timeline: [],
|
|
717
|
+
questions: [],
|
|
718
|
+
},
|
|
719
|
+
};
|
|
720
|
+
const proj = detail.project;
|
|
721
|
+
projectInputs.push({
|
|
722
|
+
dirName: rawProj.dirName,
|
|
723
|
+
cache,
|
|
724
|
+
sessions: detail.sessions,
|
|
725
|
+
opts: {
|
|
726
|
+
totalFilesChanged: proj.totalFiles,
|
|
727
|
+
totalAgentDurationMinutes: proj.totalAgentDuration,
|
|
728
|
+
totalInputTokens: proj.totalInputTokens,
|
|
729
|
+
totalOutputTokens: proj.totalOutputTokens,
|
|
730
|
+
},
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
catch (projErr) {
|
|
734
|
+
console.warn(`[portfolio-export] skipping project ${rawProj.dirName}:`, projErr.message);
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
tmpDir = mkdtempSync(path.join(tmpdir(), 'heyiam-portfolio-'));
|
|
738
|
+
const result = await generatePortfolioSite(renderData, projectInputs, tmpDir, templateName);
|
|
739
|
+
// Read every file generated into memory keyed by its path relative
|
|
740
|
+
// to the temp dir, then zip. Skip non-files defensively (the result
|
|
741
|
+
// list is files-only, but stat lets us catch directories that snuck
|
|
742
|
+
// in via a future code path).
|
|
743
|
+
const entries = [];
|
|
744
|
+
for (const filePath of result.files) {
|
|
745
|
+
try {
|
|
746
|
+
if (!statSync(filePath).isFile())
|
|
747
|
+
continue;
|
|
748
|
+
entries.push({
|
|
749
|
+
path: path.relative(tmpDir, filePath),
|
|
750
|
+
content: readFileSync(filePath, 'utf-8'),
|
|
751
|
+
});
|
|
752
|
+
}
|
|
753
|
+
catch (readErr) {
|
|
754
|
+
console.warn(`[portfolio-export] skipping unreadable file ${filePath}:`, readErr.message);
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
const zipBuffer = createZipBuffer(entries);
|
|
758
|
+
const datestamp = new Date().toISOString().slice(0, 10);
|
|
759
|
+
const safeUsername = auth.username.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
760
|
+
const filename = `portfolio-${safeUsername}-${datestamp}.zip`;
|
|
761
|
+
res.setHeader('Content-Type', 'application/zip');
|
|
762
|
+
res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
|
|
763
|
+
res.setHeader('Content-Length', zipBuffer.length);
|
|
764
|
+
res.send(zipBuffer);
|
|
765
|
+
}
|
|
766
|
+
catch (err) {
|
|
767
|
+
const errMsg = err.message;
|
|
768
|
+
console.error('[portfolio-export] Error:', errMsg);
|
|
769
|
+
if (!res.headersSent) {
|
|
770
|
+
res.status(500).json({
|
|
771
|
+
error: { code: 'PORTFOLIO_EXPORT_FAILED', message: errMsg },
|
|
772
|
+
});
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
finally {
|
|
776
|
+
if (tmpDir) {
|
|
777
|
+
try {
|
|
778
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
779
|
+
}
|
|
780
|
+
catch { /* best effort */ }
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
});
|
|
495
784
|
return router;
|
|
496
785
|
}
|