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/settings.js
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { Router } from 'express';
|
|
2
|
-
import {
|
|
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((
|
|
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' }));
|
|
@@ -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.
|
|
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
|
},
|