heyiam 0.3.2 → 0.3.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/export.js +21 -2
- package/dist/public/assets/index-CMyamplX.css +1 -0
- package/dist/public/assets/index-Cq04whgG.js +37 -0
- package/dist/public/index.html +2 -2
- package/dist/routes/auth.js +3 -3
- package/dist/routes/project-session-upload.js +223 -0
- package/dist/routes/publish.js +158 -250
- package/dist/source-audit.js +30 -50
- package/package.json +1 -1
- package/dist/public/assets/index-Coilyhtr.css +0 -1
- package/dist/public/assets/index-D0noVMFu.js +0 -44
package/dist/public/index.html
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
6
6
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
7
|
<title>app</title>
|
|
8
|
-
<script type="module" crossorigin src="/assets/index-
|
|
9
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
8
|
+
<script type="module" crossorigin src="/assets/index-Cq04whgG.js"></script>
|
|
9
|
+
<link rel="stylesheet" crossorigin href="/assets/index-CMyamplX.css">
|
|
10
10
|
</head>
|
|
11
11
|
<body>
|
|
12
12
|
<div id="root"></div>
|
package/dist/routes/auth.js
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import { Router } from 'express';
|
|
2
2
|
import { checkAuthStatus, saveAuthToken, deleteAuthToken, normalizeUsername } from '../auth.js';
|
|
3
|
-
import { API_URL } from '../config.js';
|
|
3
|
+
import { API_URL, PUBLIC_URL } from '../config.js';
|
|
4
4
|
export function createAuthRouter(_ctx) {
|
|
5
5
|
const router = Router();
|
|
6
6
|
router.get('/api/auth/status', async (_req, res) => {
|
|
7
7
|
try {
|
|
8
8
|
const status = await checkAuthStatus(API_URL);
|
|
9
|
-
res.json(status);
|
|
9
|
+
res.json({ ...status, publicBaseUrl: PUBLIC_URL });
|
|
10
10
|
}
|
|
11
11
|
catch {
|
|
12
|
-
res.json({ authenticated: false });
|
|
12
|
+
res.json({ authenticated: false, publicBaseUrl: PUBLIC_URL });
|
|
13
13
|
}
|
|
14
14
|
});
|
|
15
15
|
// Start device auth flow -- proxy to Phoenix
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared session upload pipeline used by single-project publish (SSE) and
|
|
3
|
+
* portfolio publish (batch). Keeps POST /api/sessions payloads identical.
|
|
4
|
+
*/
|
|
5
|
+
import { readFileSync } from 'node:fs';
|
|
6
|
+
import { API_URL } from '../config.js';
|
|
7
|
+
import { loadEnhancedData, saveEnhancedData, getDefaultTemplate, isTranscriptIncluded, } from '../settings.js';
|
|
8
|
+
import { redactSession, redactText, scanTextSync, formatFindings, stripHomePathsInText } from '../redact.js';
|
|
9
|
+
import { renderSessionHtml } from '../render/index.js';
|
|
10
|
+
import { buildSessionRenderData, buildSessionCard } from '../render/build-render-data.js';
|
|
11
|
+
import { buildAgentSummary } from './context.js';
|
|
12
|
+
import { toSlug } from '../format-utils.js';
|
|
13
|
+
import { getFileCountWithChildren } from '../db.js';
|
|
14
|
+
export async function uploadSelectedSessions(ctx, auth, options) {
|
|
15
|
+
const { proj, projectData, selectedSessionIds, send } = options;
|
|
16
|
+
const notify = send ?? ((_evt) => { });
|
|
17
|
+
let uploadedCount = 0;
|
|
18
|
+
const failedSessions = [];
|
|
19
|
+
const uploadedSessionCards = [];
|
|
20
|
+
const selectedTemplate = getDefaultTemplate() || 'editorial';
|
|
21
|
+
for (const sessionId of selectedSessionIds) {
|
|
22
|
+
const meta = proj.sessions.find((s) => s.sessionId === sessionId);
|
|
23
|
+
if (!meta)
|
|
24
|
+
continue;
|
|
25
|
+
notify({ type: 'session', sessionId, status: 'uploading' });
|
|
26
|
+
try {
|
|
27
|
+
const session = await ctx.loadSession(meta.path, proj.name, sessionId);
|
|
28
|
+
const enhanced = loadEnhancedData(sessionId);
|
|
29
|
+
const sessionSlug = toSlug(enhanced?.title ?? session.title ?? sessionId, 80);
|
|
30
|
+
const includeTranscript = isTranscriptIncluded(sessionId);
|
|
31
|
+
const agentSummary = await buildAgentSummary(meta.children ?? [], (c) => ctx.getSessionStats(c, proj.name), { deduplicate: true });
|
|
32
|
+
const devTake = (enhanced?.developerTake ?? session.developerTake ?? '').slice(0, 2000);
|
|
33
|
+
const sessionNarrative = enhanced?.narrative ?? '';
|
|
34
|
+
const sessionTitle = enhanced?.title ?? session.title;
|
|
35
|
+
const sessionSkills = enhanced?.skills ?? session.skills ?? [];
|
|
36
|
+
const sessionSourceTool = session.source ?? meta.source ?? 'claude';
|
|
37
|
+
const sessionRecordedAt = session.date ? new Date(session.date).toISOString() : new Date().toISOString();
|
|
38
|
+
const renderOpts = {
|
|
39
|
+
sessionId,
|
|
40
|
+
session,
|
|
41
|
+
enhanced,
|
|
42
|
+
username: auth.username,
|
|
43
|
+
projectSlug: projectData.slug,
|
|
44
|
+
sessionSlug,
|
|
45
|
+
sourceTool: sessionSourceTool,
|
|
46
|
+
agentSummary,
|
|
47
|
+
template: selectedTemplate,
|
|
48
|
+
};
|
|
49
|
+
let sessionRenderedHtml = null;
|
|
50
|
+
try {
|
|
51
|
+
const sessionRenderData = buildSessionRenderData(renderOpts);
|
|
52
|
+
sessionRenderedHtml = renderSessionHtml(sessionRenderData, selectedTemplate);
|
|
53
|
+
}
|
|
54
|
+
catch (renderErr) {
|
|
55
|
+
console.error(`[upload] Session render failed for ${sessionId}:`, renderErr.message);
|
|
56
|
+
}
|
|
57
|
+
uploadedSessionCards.push(buildSessionCard(renderOpts));
|
|
58
|
+
const childLoc = agentSummary?.agents?.reduce((s, a) => s + (a.loc_changed ?? 0), 0) ?? 0;
|
|
59
|
+
const totalLocChanged = (session.linesOfCode ?? 0) + childLoc;
|
|
60
|
+
const totalFilesChanged = getFileCountWithChildren(ctx.db, sessionId) || session.filesChanged?.length || 0;
|
|
61
|
+
const sessionPayload = {
|
|
62
|
+
session: {
|
|
63
|
+
title: sessionTitle,
|
|
64
|
+
dev_take: devTake,
|
|
65
|
+
context: enhanced?.context ?? '',
|
|
66
|
+
duration_minutes: session.durationMinutes ?? 0,
|
|
67
|
+
turns: session.turns ?? 0,
|
|
68
|
+
files_changed: totalFilesChanged,
|
|
69
|
+
loc_changed: totalLocChanged,
|
|
70
|
+
recorded_at: sessionRecordedAt,
|
|
71
|
+
end_time: session.endTime ? new Date(session.endTime).toISOString() : null,
|
|
72
|
+
cwd: session.cwd ?? null,
|
|
73
|
+
wall_clock_minutes: session.wallClockMinutes ?? null,
|
|
74
|
+
template: selectedTemplate,
|
|
75
|
+
language: null,
|
|
76
|
+
tools: session.toolBreakdown?.map((t) => t.tool) ?? [],
|
|
77
|
+
skills: sessionSkills,
|
|
78
|
+
narrative: sessionNarrative,
|
|
79
|
+
project_name: proj.name,
|
|
80
|
+
project_id: projectData.project_id,
|
|
81
|
+
slug: sessionSlug,
|
|
82
|
+
status: 'unlisted',
|
|
83
|
+
source_tool: sessionSourceTool,
|
|
84
|
+
agent_summary: agentSummary,
|
|
85
|
+
rendered_html: sessionRenderedHtml,
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
const turnTimeline = (session.turnTimeline ?? []).map((t) => ({
|
|
89
|
+
timestamp: t.timestamp,
|
|
90
|
+
type: t.type,
|
|
91
|
+
content: (t.content ?? '').slice(0, 200),
|
|
92
|
+
tools: t.tools ?? [],
|
|
93
|
+
}));
|
|
94
|
+
const transcriptExcerpt = (session.rawLog ?? []).slice(0, 10).map((line, i) => {
|
|
95
|
+
const role = line.startsWith('> ') ? 'dev' : 'ai';
|
|
96
|
+
const text = role === 'dev' ? line.slice(2) : line;
|
|
97
|
+
return { role, id: `Turn ${i + 1}`, text, timestamp: null };
|
|
98
|
+
});
|
|
99
|
+
const sessionData = {
|
|
100
|
+
version: 1,
|
|
101
|
+
id: sessionId,
|
|
102
|
+
title: sessionTitle,
|
|
103
|
+
dev_take: devTake,
|
|
104
|
+
context: enhanced?.context ?? '',
|
|
105
|
+
duration_minutes: session.durationMinutes ?? 0,
|
|
106
|
+
turns: session.turns ?? 0,
|
|
107
|
+
files_changed: (session.filesChanged ?? []).slice(0, 20).map((f) => (typeof f === 'string' ? { path: f, additions: 0, deletions: 0 } : f)),
|
|
108
|
+
loc_changed: totalLocChanged,
|
|
109
|
+
date: sessionRecordedAt,
|
|
110
|
+
end_time: (() => {
|
|
111
|
+
if (!session.endTime || !session.date)
|
|
112
|
+
return null;
|
|
113
|
+
const wallMs = new Date(session.endTime).getTime() - new Date(session.date).getTime();
|
|
114
|
+
const activeMs = (session.durationMinutes ?? 0) * 60_000;
|
|
115
|
+
return wallMs <= activeMs * 3 ? new Date(session.endTime).toISOString() : null;
|
|
116
|
+
})(),
|
|
117
|
+
cwd: session.cwd ?? null,
|
|
118
|
+
wall_clock_minutes: session.wallClockMinutes ?? null,
|
|
119
|
+
template: selectedTemplate,
|
|
120
|
+
skills: sessionSkills,
|
|
121
|
+
tools: session.toolBreakdown?.map((t) => t.tool) ?? [],
|
|
122
|
+
source: sessionSourceTool,
|
|
123
|
+
slug: sessionSlug,
|
|
124
|
+
project_name: proj.name,
|
|
125
|
+
narrative: sessionNarrative,
|
|
126
|
+
status: 'unlisted',
|
|
127
|
+
raw_log: [],
|
|
128
|
+
execution_path: (enhanced?.executionSteps ?? session.executionPath ?? []).map((s, i) => ({
|
|
129
|
+
label: s.title ?? `Step ${i + 1}`,
|
|
130
|
+
description: s.description ?? s.body ?? '',
|
|
131
|
+
})),
|
|
132
|
+
qa_pairs: enhanced?.qaPairs ?? session.qaPairs ?? [],
|
|
133
|
+
highlights: [],
|
|
134
|
+
tool_breakdown: (session.toolBreakdown ?? []).map((t) => ({ tool: t.tool, count: t.count })),
|
|
135
|
+
top_files: (session.filesChanged ?? []).slice(0, 20).map((f) => (typeof f === 'string' ? { path: f, additions: 0, deletions: 0 } : f)),
|
|
136
|
+
...(includeTranscript ? { turn_timeline: turnTimeline } : {}),
|
|
137
|
+
...(includeTranscript ? { transcript_excerpt: transcriptExcerpt } : {}),
|
|
138
|
+
agent_summary: agentSummary,
|
|
139
|
+
children: agentSummary?.agents?.map((a) => ({
|
|
140
|
+
sessionId: a.role,
|
|
141
|
+
role: a.role,
|
|
142
|
+
durationMinutes: a.duration_minutes,
|
|
143
|
+
linesOfCode: a.loc_changed,
|
|
144
|
+
})) ?? [],
|
|
145
|
+
};
|
|
146
|
+
const sessionCwd = session.cwd ?? undefined;
|
|
147
|
+
const redactedPayload = redactSession(sessionPayload, 'high', sessionCwd);
|
|
148
|
+
const redactedData = redactSession(sessionData, 'high', sessionCwd);
|
|
149
|
+
const payloadFindings = scanTextSync(JSON.stringify(sessionPayload));
|
|
150
|
+
if (payloadFindings.length > 0) {
|
|
151
|
+
const summary = formatFindings(payloadFindings);
|
|
152
|
+
notify({ type: 'redaction', sessionId, message: summary });
|
|
153
|
+
}
|
|
154
|
+
const sessionRes = await fetch(`${API_URL}/api/sessions`, {
|
|
155
|
+
method: 'POST',
|
|
156
|
+
headers: {
|
|
157
|
+
'Content-Type': 'application/json',
|
|
158
|
+
Authorization: `Bearer ${auth.token}`,
|
|
159
|
+
},
|
|
160
|
+
body: JSON.stringify(redactedPayload),
|
|
161
|
+
});
|
|
162
|
+
if (sessionRes.ok) {
|
|
163
|
+
uploadedCount++;
|
|
164
|
+
try {
|
|
165
|
+
const sesData = await sessionRes.json();
|
|
166
|
+
if (sesData.upload_urls && includeTranscript) {
|
|
167
|
+
const { raw: rawUrl, log: logUrl } = sesData.upload_urls;
|
|
168
|
+
if (rawUrl && meta.path && !meta.path.startsWith('cursor://')) {
|
|
169
|
+
try {
|
|
170
|
+
const rawText = readFileSync(meta.path, 'utf-8');
|
|
171
|
+
let redactedRaw = redactText(rawText);
|
|
172
|
+
redactedRaw = stripHomePathsInText(redactedRaw, sessionCwd);
|
|
173
|
+
await fetch(rawUrl, { method: 'PUT', body: Buffer.from(redactedRaw, 'utf-8'), headers: { 'Content-Type': 'application/octet-stream' } });
|
|
174
|
+
}
|
|
175
|
+
catch { /* S3 upload is best-effort */ }
|
|
176
|
+
}
|
|
177
|
+
if (logUrl && session.rawLog && session.rawLog.length > 0) {
|
|
178
|
+
try {
|
|
179
|
+
const redactedLog = session.rawLog.map((line) => {
|
|
180
|
+
let cleaned = redactText(line);
|
|
181
|
+
cleaned = stripHomePathsInText(cleaned, sessionCwd);
|
|
182
|
+
return cleaned;
|
|
183
|
+
});
|
|
184
|
+
await fetch(logUrl, { method: 'PUT', body: JSON.stringify(redactedLog), headers: { 'Content-Type': 'application/json' } });
|
|
185
|
+
}
|
|
186
|
+
catch { /* S3 upload is best-effort */ }
|
|
187
|
+
}
|
|
188
|
+
if (sesData.upload_urls.session) {
|
|
189
|
+
try {
|
|
190
|
+
await fetch(sesData.upload_urls.session, {
|
|
191
|
+
method: 'PUT',
|
|
192
|
+
body: JSON.stringify(redactedData),
|
|
193
|
+
headers: { 'Content-Type': 'application/json' },
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
catch { /* S3 upload is best-effort */ }
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
catch { /* Response already consumed or no upload_urls -- not fatal */ }
|
|
201
|
+
if (enhanced) {
|
|
202
|
+
saveEnhancedData(sessionId, { ...enhanced, uploaded: true });
|
|
203
|
+
}
|
|
204
|
+
notify({ type: 'session', sessionId, status: 'uploaded' });
|
|
205
|
+
}
|
|
206
|
+
else {
|
|
207
|
+
const sesErrBody = await sessionRes.json().catch(() => null);
|
|
208
|
+
const rawSesErr = sesErrBody && typeof sesErrBody === 'object' ? sesErrBody.error : null;
|
|
209
|
+
const errMsg = typeof rawSesErr === 'string' ? rawSesErr
|
|
210
|
+
: (rawSesErr && typeof rawSesErr === 'object' && 'message' in rawSesErr) ? rawSesErr.message
|
|
211
|
+
: `HTTP ${sessionRes.status}`;
|
|
212
|
+
failedSessions.push({ sessionId, error: errMsg });
|
|
213
|
+
notify({ type: 'session', sessionId, status: 'failed', error: errMsg });
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
catch (err) {
|
|
217
|
+
const errMsg = err.message;
|
|
218
|
+
failedSessions.push({ sessionId, error: errMsg });
|
|
219
|
+
notify({ type: 'session', sessionId, status: 'failed', error: errMsg });
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return { uploadedCount, failedSessions, uploadedSessionCards };
|
|
223
|
+
}
|